Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const path = require('path');
const { scoutApp } = require('../../../config/native-agents/scout');

const BYO_PAGE = path.resolve(__dirname, '../../../../frontend/src/v2/components/V2AgentBYO.tsx');
const { daemonSetupCommandTemplate } = require('../../utils/daemonSetupCommandTemplate');

const constantFromPage = (name) => {
const source = fs.readFileSync(BYO_PAGE, 'utf8');
Expand All @@ -22,12 +23,17 @@ const constantFromPage = (name) => {
return match[1];
};

// Scout runs on every instance, so its copy names the URL placeholder, never a
// production host (CodeRabbit on #2727).
const SCOUT_API_PLACEHOLDER = '<API URL of this instance>';

describe('Scout onboarding knowledge', () => {
const prompt = scoutApp.systemPrompt;

test('gives the exact install and machine commands the Bring-your-own page shows', () => {
expect(prompt).toContain(constantFromPage('CLI_INSTALL_COMMAND'));
expect(prompt).toContain(constantFromPage('DAEMON_SETUP_COMMAND'));
expect(prompt).toContain(daemonSetupCommandTemplate(SCOUT_API_PLACEHOLDER));
expect(prompt).not.toMatch(/--instance https:\/\/api\.commonly\.me && commonly daemon register/);
expect(prompt).toContain('export COMMONLY_API_URL=');
expect(prompt).toContain('export COMMONLY_AGENT_TOKEN=');
expect(prompt).toContain('commonly agent run <name>');
Expand Down
4 changes: 3 additions & 1 deletion backend/__tests__/unit/scripts/docsSiteFacts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
const fs = require('fs');
const path = require('path');
const { daemonSetupCommandTemplate } = require('../../utils/daemonSetupCommandTemplate');

const ROOT = path.resolve(__dirname, '../../../..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
Expand Down Expand Up @@ -42,7 +43,8 @@ describe('docs.commonly.me says what the product does', () => {

test('the quickstart is the hosted path and the deeper connect page keeps the CLI commands', () => {
const install = constantFromPage('CLI_INSTALL_COMMAND');
const daemon = constantFromPage('DAEMON_SETUP_COMMAND');
// The docs site describes commonly.me, so its command names that instance.
const daemon = daemonSetupCommandTemplate('https://api.commonly.me');
expect(quickstart).toContain('Run it here');
expect(quickstart).not.toContain('npm i -g');
expect(connect).toContain(install);
Expand Down
17 changes: 17 additions & 0 deletions backend/__tests__/utils/daemonSetupCommandTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* The computer setup command, read from its one source: the frontend helper
* that builds it for the page's instance (TASK-547). Backend copy that repeats
* the command (Scout's prompt, the docs site) is checked against this template
* with the API URL left as a placeholder, so each copy keeps its own instance.
*/
const fs = require('fs');
const path = require('path');

const HELPER = path.resolve(__dirname, '../../../frontend/src/v2/utils/daemonSetupCommand.ts');

const daemonSetupCommandTemplate = (apiUrl) => {
const template = fs.readFileSync(HELPER, 'utf8').match(/`(npm i -g [^`]+)`/)[1];
return template.replace(/\$\{apiUrl[^}]*\}/, apiUrl);
};

module.exports = { daemonSetupCommandTemplate };
4 changes: 2 additions & 2 deletions backend/config/native-agents/scout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ export const scoutApp = {
+ 'Do not propose a connect card for it, and do not say OpenRouter, or any model '
+ 'provider, was the one it was walked on.\n'
+ ' * "Add a computer": `npm i -g @commonlyai/cli@latest && commonly login '
+ '&& commonly daemon register && commonly daemon install` registers their '
+ 'machine once; after that they add agents to it from this page with no '
+ '--instance <API URL of this instance> && commonly daemon register && commonly daemon install` registers their '
+ 'machine once (on commonly.me the API URL is https://api.commonly.me); after that they add agents to it from this page with no '
+ 'terminal.\n'
// Linux run 38982219348 (2026-10-16): the sandbox that keeps a connected
// agent to its own folder cannot start Claude Code on Linux or WSL, and
Expand Down
2 changes: 1 addition & 1 deletion docs-site/agents/connect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ These commands are verified on macOS. Windows is not verified yet.
When your agent should survive restarts, add the computer once:

```bash
npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install
npm i -g @commonlyai/cli@latest && commonly login --instance https://api.commonly.me && commonly daemon register && commonly daemon install
```

The daemon supervises the agents you add to that computer. Add them from
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import V2AgentRuntimeGrants, { describeRuntime } from '../components/V2AgentRuntimeGrants';
import { AuthContext } from '../../context/AuthContext';
import { daemonSetupCommand } from '../utils/daemonSetupCommand';

jest.mock('axios', () => {
const mock = {
Expand Down Expand Up @@ -75,7 +76,7 @@ test('revoked Runtime row offers an active computer and moves in one request', a
test('without an active computer, the setup hint includes daemon installation', async () => {
renderSection();
expect(await screen.findByText(/Register another computer to move this agent/)).toBeInTheDocument();
expect(screen.getByText('npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install')).toBeInTheDocument();
expect(screen.getByText(daemonSetupCommand())).toBeInTheDocument();
});

test('a changed installer refuses the move before release and explains the blocking installation', async () => {
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/v2/__tests__/byo-setup-command-invariant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @ts-nocheck
// TASK-547 (D39): a copy-paste setup command on the BYO, Connect or runtime
// pages must target the instance the page is on. A bare `commonly login` falls
// back to the CLI's built-in production default, so on dev or a self-hosted
// instance the computer would register against commonly.me.
const fs = require('fs');
const path = require('path');
const { daemonSetupCommand } = require('../utils/daemonSetupCommand');

const SOURCES = [
'components/V2AgentBYO.tsx',
'components/V2AgentRuntimeGrants.tsx',
'components/V2ConnectPage.tsx',
];
const read = (rel) => fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');

describe('copy-paste setup commands follow the page instance', () => {
beforeEach(() => { process.env.REACT_APP_API_URL = 'https://api-dev.example.test/'; });
afterEach(() => { delete process.env.REACT_APP_API_URL; });

test('the computer setup command logs in to the page instance, not the CLI default', () => {
const command = daemonSetupCommand();
expect(command).toContain('commonly login --instance https://api-dev.example.test ');
expect(command).not.toMatch(/commonly\.me/);
});

test('no BYO, Connect or runtime source hardcodes the production API host', () => {
SOURCES.forEach((rel) => {
expect([rel, read(rel).match(/api\.commonly\.me/g)]).toEqual([rel, null]);
});
});
});
13 changes: 7 additions & 6 deletions frontend/src/v2/components/V2AgentBYO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next';
import axios from '../../utils/axiosConfig';
import { recordByoStep, type ByoStep } from '../utils/byoTelemetry';
import getApiBaseUrl from '../../utils/apiBaseUrl';
import { daemonSetupCommand } from '../utils/daemonSetupCommand';
import V2FeaturePage from './V2FeaturePage';
import V2Avatar from './V2Avatar';
import V2Hint from './V2Hint';
Expand Down Expand Up @@ -65,10 +66,9 @@ type MachineRow = {
};
const MACHINE_STATUS_POLL_MS = 4000;
const MACHINE_STATUS_MAX_TICKS = 30;
// One-time computer setup (ADR-026 D1). Registration mints a machine-local
// credential, so it is inherently a terminal act — this page's job is to
// make it ONE paste, after which "On my computer" appears here.
const DAEMON_SETUP_COMMAND = 'npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install';
// Registration mints a machine-local credential, so it is inherently a terminal
// act — this page's job is to make it ONE paste (daemonSetupCommand), after
// which "On my computer" appears here.
const CLAUDE_FILE_NAME = 'CLAUDE.md';
type MachineAdapter = '' | 'claude' | 'codex';

Expand Down Expand Up @@ -952,6 +952,7 @@ const V2AgentBYO: React.FC = () => {
const chooseMode = (nextMode: 'hosted' | 'byo' | 'machine') => setMode(nextMode);

const apiUrl = getApiBaseUrl().replace(/\/+$/, '');
const daemonCommand = daemonSetupCommand(apiUrl);

const claudeSnippet = issued
? `claude mcp add commonly \\\n -e COMMONLY_API_URL=${apiUrl} \\\n -e COMMONLY_AGENT_TOKEN=${issued.token} \\\n -- npx -y @commonlyai/mcp`
Expand Down Expand Up @@ -1333,11 +1334,11 @@ const V2AgentBYO: React.FC = () => {
<div className="v2-byo__add-computer" data-testid="byo-add-computer">
<span className="v2-byo__label">{t('agentByo.addComputer.title')}</span>
<div className="v2-byo__add-computer-row">
<code>{DAEMON_SETUP_COMMAND}</code>
<code>{daemonCommand}</code>
<button
type="button"
className="v2-byo__copy"
onClick={() => copy('daemon-setup', DAEMON_SETUP_COMMAND)}
onClick={() => copy('daemon-setup', daemonCommand)}
>
{copied === 'daemon-setup' ? t('agentByo.addComputer.copied') : t('agentByo.addComputer.copy')}
</button>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/v2/components/V2AgentRuntimeGrants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useV2Api } from '../hooks/useV2Api';
import './V2AgentRuntimeGrants.css';
import V2AgentProviderKey from './V2AgentProviderKey';
import V2RuntimeControls, { RuntimeSettings, RuntimeUpdate } from './V2RuntimeControls';
import { daemonSetupCommand } from '../utils/daemonSetupCommand';

export interface Runtime extends RuntimeSettings {}

Expand Down Expand Up @@ -49,7 +50,6 @@ const WALKED_ADAPTERS: Record<string, string> = {
hermes: 'Hermes',
dsh: 'DeepSeek Harness',
};
const DAEMON_SETUP_COMMAND = 'npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install';

export const describeRuntime = (runtime: Runtime): { mode: 'native' | 'hosted' | 'daemon' | 'cli' | 'unknown'; adapter: string | null } => {
const type = String(runtime.runtimeType || '').toLowerCase();
Expand Down Expand Up @@ -198,7 +198,7 @@ const V2AgentRuntimeGrants: React.FC<Props> = ({
<button type="button" disabled={!machineChoice || moving} onClick={() => { void moveAgent(); }}>
{moving ? t('agentManage.runtime.moving') : t('agentManage.runtime.move')}
</button>
</> : <p>{t('agentManage.runtime.noActiveMachine')} <code>{DAEMON_SETUP_COMMAND}</code></p>
</> : <p>{t('agentManage.runtime.noActiveMachine')} <code>{daemonSetupCommand()}</code></p>
)}
</div>
)}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/v2/utils/daemonSetupCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import getApiBaseUrl from '../../utils/apiBaseUrl';

// One-time computer setup (ADR-026 D1). `--instance` is explicit: a bare
// `commonly login` falls back to the CLI's built-in production default, so a
// dev or self-hosted page would register its computer against commonly.me.
// Connect builds its login line the same way (V2ConnectPage).
export const daemonSetupCommand = (apiUrl: string = getApiBaseUrl()): string =>
`npm i -g @commonlyai/cli@latest && commonly login --instance ${apiUrl.replace(/\/+$/, '')} && commonly daemon register && commonly daemon install`;
Loading