@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 +1 -1
- package/src/commands/agent.js +28 -10
- package/src/commands/mcp.js +55 -0
- package/src/index.js +2 -0
- package/src/lib/adapters/codex.js +7 -0
- package/src/lib/mcp-token.js +36 -0
- package/src/lib/supervised-turn.js +2 -1
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -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
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
// line per turn goes to
|
|
1468
|
-
// delivery settles. Off, the spawn
|
|
1469
|
-
// always been, and nothing is written.
|
|
1470
|
-
superviseRuns
|
|
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:
|
|
1501
|
+
seat: agentName, turnTimeoutMs: effectiveTurnTimeoutMs, supervise: supervising, log,
|
|
1484
1502
|
});
|
|
1485
|
-
if (
|
|
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
|
|
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
|
|
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,
|