@commonlyai/cli 0.1.211 → 0.1.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.211",
3
+ "version": "0.1.213",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -16,6 +16,7 @@ import { createRuntimeReporter } from '../lib/runtime-observation.js';
16
16
  * heartbeat — manually trigger an agent's heartbeat
17
17
  */
18
18
 
19
+ import { refreshMcpTokenCopy, removeMcpTokenCopy } from '../lib/mcp-token.js';
19
20
  import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync } from 'fs';
20
21
  import { join, dirname, basename, resolve as pathResolve } from 'path';
21
22
  import { homedir, tmpdir } from 'os';
@@ -147,6 +148,12 @@ export const saveAgentToken = (name, record) => {
147
148
  JSON.stringify({ ...record, savedAt: new Date().toISOString() }, null, 2),
148
149
  { encoding: 'utf8', mode: 0o600 },
149
150
  );
151
+ // After the record is durable, so a failed refresh never loses a freshly minted token.
152
+ try {
153
+ refreshMcpTokenCopy(name, record?.runtimeToken);
154
+ } catch (err) {
155
+ console.warn(`Could not refresh the MCP token copy for '${name}': ${err.message}. Run: commonly mcp config --agent ${name}`);
156
+ }
150
157
  };
151
158
 
152
159
  export const loadAgentToken = (name) => {
@@ -162,6 +169,7 @@ export const loadAgentToken = (name) => {
162
169
  export const deleteAgentToken = (name) => {
163
170
  const file = tokenFile(name);
164
171
  if (existsSync(file)) rmSync(file);
172
+ removeMcpTokenCopy(name);
165
173
  };
166
174
 
167
175
  // ── env-var bootstrap for `agent run` (#913) ────────────────────────────────
@@ -1386,6 +1394,15 @@ const extractPrompt = (event) => {
1386
1394
  // cancel grace, and the wall-clock backstop past the adapter's own timeout.
1387
1395
  export { supervisedRunPolicy };
1388
1396
 
1397
+ // Codex turns are supervised by default (D211): a turn that stops making
1398
+ // progress is named and killed instead of holding the seat. COMMONLY_SUPERVISE_RUNS=0
1399
+ // opts out. Every other adapter stays off unless COMMONLY_SUPERVISE_RUNS=1.
1400
+ export const superviseRunsByDefault = (adapterName, env = process.env) => {
1401
+ if (env.COMMONLY_SUPERVISE_RUNS === '0') return false;
1402
+ if (adapterName === 'codex') return true;
1403
+ return env.COMMONLY_SUPERVISE_RUNS === '1';
1404
+ };
1405
+
1389
1406
  /**
1390
1407
  * Start the run loop. Polls /events, spawns the adapter per event, posts the
1391
1408
  * result into the pod, acks.
@@ -1461,14 +1478,15 @@ export const performRun = ({
1461
1478
  resolveOwnProviderKey = null,
1462
1479
  runtimeReportClient = null,
1463
1480
  refusalHandoff = null,
1464
- // Off unless COMMONLY_SUPERVISE_RUNS=1: each spawn runs under the seats
1465
- // supervisor (stall window, wall-clock backstop, a named failure on every
1466
- // settle). The CLI still talks to its provider directly, and one metadata
1467
- // line per turn goes to ~/.commonly/state/turns/<agent>.jsonl once
1468
- // delivery settles. Off, the spawn path is the plain adapter call it has
1469
- // always been, and nothing is written.
1470
- superviseRuns = process.env.COMMONLY_SUPERVISE_RUNS === '1',
1481
+ // Each spawn runs under the seats supervisor (stall window, wall-clock
1482
+ // backstop, a named failure on every settle) when superviseRuns is true.
1483
+ // Unset, the default comes from superviseRunsByDefault. The CLI still talks
1484
+ // to its provider directly, and one metadata line per turn goes to
1485
+ // ~/.commonly/state/turns/<agent>.jsonl once delivery settles. Off, the spawn
1486
+ // path is the plain adapter call it has always been, and nothing is written.
1487
+ superviseRuns,
1471
1488
  }) => {
1489
+ const supervising = superviseRuns ?? superviseRunsByDefault(adapter?.name);
1472
1490
  reportRuntime = reportRuntime && WALKED_HARNESSES.includes(adapter?.name);
1473
1491
  if (environment?.provider !== undefined && !adapter?.supportsProvider) {
1474
1492
  throw new Error('environment.provider requires a provider-capable adapter');
@@ -1480,9 +1498,9 @@ export const performRun = ({
1480
1498
  // The supervised run keeps its trace in memory, metadata only; no record of
1481
1499
  // a run is written to disk (lib/supervised-turn.js).
1482
1500
  const turnRunner = createTurnRunner({
1483
- seat: agentName, turnTimeoutMs: effectiveTurnTimeoutMs, supervise: superviseRuns, log,
1501
+ seat: agentName, turnTimeoutMs: effectiveTurnTimeoutMs, supervise: supervising, log,
1484
1502
  });
1485
- if (superviseRuns) log(turnRunner.describe(adapter?.name));
1503
+ if (supervising) log(turnRunner.describe(adapter?.name));
1486
1504
  const runtimeReporter = createRuntimeReporter({
1487
1505
  client: runtimeReportClient || { post: async () => {} },
1488
1506
  harness: adapter.name, detect: () => adapter.detect(),
@@ -1895,7 +1913,7 @@ export const performRun = ({
1895
1913
 
1896
1914
  // The spawn → post-decision half of a turn, split out so the claim release
1897
1915
  // above is a plain finally instead of threading through every return path.
1898
- // With superviseRuns, the turn's log line is written once delivery has
1916
+ // With supervising, the turn's log line is written once delivery has
1899
1917
  // settled; anything thrown after a spawn that resolved means the reply never
1900
1918
  // reached the pod. Off, `turn` writes nothing.
1901
1919
  const runTurn = async (args) => {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * commonly mcp config — print an MCP server config for a locally attached agent (TASK-666).
3
+ * The MCP server only accepts a cm_agent_* runtime token, so the config points
4
+ * COMMONLY_TOKEN_FILE at a 0600 copy of that token and never prints the token.
5
+ */
6
+ import { listLocalAgents, loadAgentToken } from './agent.js';
7
+ import { validMcpTokenName, writeMcpTokenCopy } from '../lib/mcp-token.js';
8
+
9
+ const NO_AGENT_HINT = 'Attach one first: commonly agent attach <adapter> --pod <podId> --name <name>';
10
+
11
+ export const buildMcpConfig = ({ agent } = {}) => {
12
+ let name = agent;
13
+ if (!name) {
14
+ const local = listLocalAgents();
15
+ if (local.length === 0) throw new Error(`No local agent has a runtime token on this computer. ${NO_AGENT_HINT}`);
16
+ if (local.length > 1) {
17
+ throw new Error(`More than one local agent (${local.map((a) => a.name).join(', ')}). Pick one with --agent <name>.`);
18
+ }
19
+ name = local[0].name;
20
+ }
21
+ if (!validMcpTokenName(name)) throw new Error(`Invalid agent name '${name}'`);
22
+ const record = loadAgentToken(name);
23
+ const token = String(record?.runtimeToken || '').trim();
24
+ if (!token) throw new Error(`No runtime token saved for agent '${name}'. ${NO_AGENT_HINT}`);
25
+ if (!token.startsWith('cm_agent_')) throw new Error(`The saved token for '${name}' is not a cm_agent_* runtime token. Re-attach it: commonly agent attach`);
26
+ if (!record.instanceUrl) throw new Error(`Agent '${name}' has no saved instance URL. Re-attach it.`);
27
+
28
+ const tokenPath = writeMcpTokenCopy(name, token);
29
+
30
+ return {
31
+ mcpServers: {
32
+ commonly: {
33
+ command: 'npx',
34
+ args: ['-y', '@commonlyai/mcp@latest'],
35
+ env: { COMMONLY_API_URL: record.instanceUrl, COMMONLY_TOKEN_FILE: tokenPath },
36
+ },
37
+ },
38
+ };
39
+ };
40
+
41
+ export const registerMcp = (program) => {
42
+ const mcp = program.command('mcp').description('Connect an MCP client (Claude Code, Codex, Cursor) to Commonly');
43
+ mcp
44
+ .command('config')
45
+ .description('Print an MCP server config for a locally attached agent; the token stays in a 0600 file')
46
+ .option('--agent <name>', 'Local agent to use (default: the only one)')
47
+ .action((opts) => {
48
+ try {
49
+ console.log(JSON.stringify(buildMcpConfig({ agent: opts.agent }), null, 2));
50
+ } catch (error) {
51
+ console.error(error.message);
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+ };
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ import { registerPod } from './commands/pod.js';
22
22
  import { registerDev } from './commands/dev.js';
23
23
  import { registerVerify } from './commands/verify.js';
24
24
  import { registerDoctor } from './commands/doctor.js';
25
+ import { registerMcp } from './commands/mcp.js';
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
28
  const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
@@ -57,6 +58,7 @@ registerVerify(program);
57
58
 
58
59
  // Install health: one line per check (TASK-660)
59
60
  registerDoctor(program);
61
+ registerMcp(program);
60
62
 
61
63
  // Each subcommand owns its own `--instance <url>` flag; a program-level
62
64
  // duplicate shadowed the subcommand value on commander v12, causing
@@ -897,6 +897,13 @@ export default {
897
897
  await writePublicCodexInstructions(childEnv.CODEX_HOME, ctx);
898
898
  }
899
899
 
900
+ // The awaits above can outlast a supervisor stall window. Once it has
901
+ // aborted, the attempt has already settled and a retry may be running,
902
+ // so no process is created for this one. Checked synchronously before
903
+ // runCodex, so the spawn itself cannot race the abort.
904
+ if (ctx.signal?.aborted) {
905
+ throw ctx.signal.reason || new Error('codex spawn cancelled before the process started');
906
+ }
900
907
  const { threadId } = await runCodex({
901
908
  args,
902
909
  cwd: ctx.cwd,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The 0600 copy of an agent's runtime token that `commonly mcp config` points
3
+ * COMMONLY_TOKEN_FILE at (TASK-666). It is a second live secret, so the agent
4
+ * token's lifecycle owns it: save refreshes it, delete removes it.
5
+ */
6
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { join } from 'path';
9
+
10
+ export const validMcpTokenName = (name) => typeof name === 'string' && /^[A-Za-z0-9._-]+$/.test(name) && !name.startsWith('.');
11
+
12
+ export const mcpTokenPath = (name) => {
13
+ if (!validMcpTokenName(name)) throw new Error(`Invalid agent name '${name}'`);
14
+ return join(homedir(), '.commonly', 'mcp', `${name}.token`);
15
+ };
16
+
17
+ // Remove first, then create exclusively at 0600: no write into a looser-mode file or through a symlink.
18
+ export const writeMcpTokenCopy = (name, token) => {
19
+ const dir = join(homedir(), '.commonly', 'mcp');
20
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
21
+ chmodSync(dir, 0o700);
22
+ const path = mcpTokenPath(name);
23
+ rmSync(path, { force: true });
24
+ writeFileSync(path, `${token}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
25
+ return path;
26
+ };
27
+
28
+ // A bad name never has a copy, so remove and refresh are no-ops for it and cannot block detach or save.
29
+ export const removeMcpTokenCopy = (name) => {
30
+ if (validMcpTokenName(name)) rmSync(mcpTokenPath(name), { force: true });
31
+ };
32
+
33
+ // Reattach mints a new token; a copy that already exists must follow it.
34
+ export const refreshMcpTokenCopy = (name, token) => {
35
+ if (token && validMcpTokenName(name) && existsSync(mcpTokenPath(name))) writeMcpTokenCopy(name, token);
36
+ };
@@ -3,7 +3,8 @@
3
3
  * path also writes one metadata line per turn (the turn log), once the
4
4
  * turn's outcome has reached the pod or failed to.
5
5
  *
6
- * COMMONLY_SUPERVISE_RUNS=1 (read by performRun as `superviseRuns`)
6
+ * codex by default, or COMMONLY_SUPERVISE_RUNS=1 (superviseRunsByDefault in
7
+ * commands/agent.js; COMMONLY_SUPERVISE_RUNS=0 opts codex out)
7
8
  * the turn runs under `superviseRun` from @commonlyai/seats: a stall
8
9
  * window, a wall-clock backstop, and a guarantee the turn settles with a
9
10
  * named failure. No provider relay: the CLI talks to its provider directly,