@commonlyai/cli 0.1.212 → 0.1.214
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 +74 -18
- package/src/commands/mcp.js +55 -0
- package/src/index.js +2 -0
- package/src/lib/daemon-logs.js +19 -1
- package/src/lib/mcp-token.js +36 -0
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createOwnKeyResolver, createMachineReportClient } from '../lib/own-key-launch.js';
|
|
2
2
|
import { foregroundDaemonHint } from '../lib/daemon-hint.js';
|
|
3
3
|
import { loadDaemonRecord } from '../lib/daemon-store.js';
|
|
4
|
+
import { daemonSeatLogPath, logSize, readAppended, readLogTail } from '../lib/daemon-logs.js';
|
|
4
5
|
import { WALKED_HARNESSES } from '../lib/runtime-inventory.js';
|
|
5
6
|
import { createRuntimeReporter } from '../lib/runtime-observation.js';
|
|
6
7
|
/**
|
|
@@ -16,6 +17,7 @@ import { createRuntimeReporter } from '../lib/runtime-observation.js';
|
|
|
16
17
|
* heartbeat — manually trigger an agent's heartbeat
|
|
17
18
|
*/
|
|
18
19
|
|
|
20
|
+
import { refreshMcpTokenCopy, removeMcpTokenCopy } from '../lib/mcp-token.js';
|
|
19
21
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync } from 'fs';
|
|
20
22
|
import { join, dirname, basename, resolve as pathResolve } from 'path';
|
|
21
23
|
import { homedir, tmpdir } from 'os';
|
|
@@ -147,6 +149,12 @@ export const saveAgentToken = (name, record) => {
|
|
|
147
149
|
JSON.stringify({ ...record, savedAt: new Date().toISOString() }, null, 2),
|
|
148
150
|
{ encoding: 'utf8', mode: 0o600 },
|
|
149
151
|
);
|
|
152
|
+
// After the record is durable, so a failed refresh never loses a freshly minted token.
|
|
153
|
+
try {
|
|
154
|
+
refreshMcpTokenCopy(name, record?.runtimeToken);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(`Could not refresh the MCP token copy for '${name}': ${err.message}. Run: commonly mcp config --agent ${name}`);
|
|
157
|
+
}
|
|
150
158
|
};
|
|
151
159
|
|
|
152
160
|
export const loadAgentToken = (name) => {
|
|
@@ -162,6 +170,7 @@ export const loadAgentToken = (name) => {
|
|
|
162
170
|
export const deleteAgentToken = (name) => {
|
|
163
171
|
const file = tokenFile(name);
|
|
164
172
|
if (existsSync(file)) rmSync(file);
|
|
173
|
+
removeMcpTokenCopy(name);
|
|
165
174
|
};
|
|
166
175
|
|
|
167
176
|
// ── env-var bootstrap for `agent run` (#913) ────────────────────────────────
|
|
@@ -4366,38 +4375,85 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
|
|
|
4366
4375
|
});
|
|
4367
4376
|
|
|
4368
4377
|
// ── logs ──────────────────────────────────────────────────────────────────
|
|
4378
|
+
// Without --pod, the seat's local daemon log is read. With --pod, the owner
|
|
4379
|
+
// login reads recent events and runs from the member-gated seat-activity
|
|
4380
|
+
// route, whichever machine the seat runs on. The owner login is never sent to
|
|
4381
|
+
// the agent-runtime routes, which only accept a seat's own token.
|
|
4369
4382
|
agent
|
|
4370
4383
|
.command('logs <name>')
|
|
4371
|
-
.description('
|
|
4372
|
-
.option('--
|
|
4373
|
-
.option('--
|
|
4374
|
-
.option('--
|
|
4384
|
+
.description("Show a seat's recent log: local daemon log, or server activity with --pod")
|
|
4385
|
+
.option('--pod <podId>', 'Read server-side events and runs for a pod you belong to')
|
|
4386
|
+
.option('--instance-id <id>', 'Seat instance ID', 'default')
|
|
4387
|
+
.option('--follow', 'Continue printing new output', false)
|
|
4388
|
+
.option('--lines <n>', 'Number of trailing local lines to show', '50')
|
|
4389
|
+
.option('--instance <url>', 'Target Commonly instance (with --pod)')
|
|
4375
4390
|
.action(async (name, opts) => {
|
|
4391
|
+
if (opts.follow && opts.pod) {
|
|
4392
|
+
console.error('--follow needs a local seat log; server activity is a snapshot. Re-run without --follow.');
|
|
4393
|
+
process.exit(1);
|
|
4394
|
+
}
|
|
4395
|
+
const lines = Number(opts.lines);
|
|
4396
|
+
if (!Number.isInteger(lines) || lines < 1) {
|
|
4397
|
+
console.error(`--lines must be a positive integer, got '${opts.lines}'`);
|
|
4398
|
+
process.exit(1);
|
|
4399
|
+
}
|
|
4400
|
+
if (!opts.pod) {
|
|
4401
|
+
const path = daemonSeatLogPath(name);
|
|
4402
|
+
let output;
|
|
4403
|
+
try {
|
|
4404
|
+
output = readLogTail(path, lines);
|
|
4405
|
+
} catch (err) {
|
|
4406
|
+
console.error(`Cannot read ${path}: ${err.message}`);
|
|
4407
|
+
process.exit(1);
|
|
4408
|
+
}
|
|
4409
|
+
if (output === null) {
|
|
4410
|
+
console.error(`No local log for '${name}' at ${path}. Pass --pod <podId> to read its server-side activity.`);
|
|
4411
|
+
process.exit(1);
|
|
4412
|
+
}
|
|
4413
|
+
if (output) console.log(output);
|
|
4414
|
+
if (!opts.follow) return;
|
|
4415
|
+
// Poll rather than fs.watch: a rename-rotated log would leave a watch
|
|
4416
|
+
// on the old inode silent. Only appended bytes are printed.
|
|
4417
|
+
let offset = logSize(path);
|
|
4418
|
+
const timer = setInterval(() => {
|
|
4419
|
+
try {
|
|
4420
|
+
const next = readAppended(path, offset);
|
|
4421
|
+
offset = next.offset;
|
|
4422
|
+
if (next.text) process.stdout.write(next.text);
|
|
4423
|
+
} catch (err) {
|
|
4424
|
+
// Rotation can leave a gap with no file; wait for it to come back.
|
|
4425
|
+
if (err?.code === 'ENOENT') return;
|
|
4426
|
+
console.error(`Cannot follow ${path}: ${err.message}`);
|
|
4427
|
+
process.exit(1);
|
|
4428
|
+
}
|
|
4429
|
+
}, 1000);
|
|
4430
|
+
const close = () => { clearInterval(timer); process.exit(0); };
|
|
4431
|
+
process.once('SIGINT', close);
|
|
4432
|
+
process.once('SIGTERM', close);
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4376
4436
|
const token = getToken(opts.instance);
|
|
4377
4437
|
if (!token) { console.error('Not logged in. Run: commonly login'); process.exit(1); }
|
|
4378
|
-
|
|
4379
4438
|
const client = createClient({ instance: resolveInstanceUrl(opts.instance), token });
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4439
|
+
const printActivity = async () => {
|
|
4440
|
+
const { events = [], runs = [] } = await client.get('/api/agents/seat-activity', {
|
|
4441
|
+
podId: opts.pod,
|
|
4383
4442
|
agentName: name,
|
|
4384
4443
|
instanceId: opts.instanceId,
|
|
4385
|
-
limit: 20,
|
|
4386
4444
|
});
|
|
4387
4445
|
events.forEach((e) => {
|
|
4388
4446
|
const ts = new Date(e.createdAt).toTimeString().slice(0, 8);
|
|
4389
|
-
|
|
4390
|
-
console.log(`[${ts}] ${e.type.padEnd(18)} → ${outcome}`);
|
|
4447
|
+
console.log(`[${ts}] ${String(e.type).padEnd(18)} → ${e.outcome || e.status}`);
|
|
4391
4448
|
});
|
|
4392
|
-
|
|
4449
|
+
runs.forEach((r) => {
|
|
4450
|
+
const ts = new Date(r.startedAt).toTimeString().slice(0, 8);
|
|
4451
|
+
console.log(`[${ts}] run ${String(r.trigger).padEnd(14)} → ${r.status} (${r.totalTokens} tokens)`);
|
|
4452
|
+
});
|
|
4453
|
+
if (events.length === 0 && runs.length === 0) console.log(`No recent activity for '${name}' in this pod.`);
|
|
4393
4454
|
};
|
|
4394
|
-
|
|
4395
4455
|
try {
|
|
4396
|
-
await
|
|
4397
|
-
if (opts.follow) {
|
|
4398
|
-
console.log('Following... (Ctrl+C to stop)');
|
|
4399
|
-
setInterval(fetchAndPrint, 5000);
|
|
4400
|
-
}
|
|
4456
|
+
await printActivity();
|
|
4401
4457
|
} catch (err) {
|
|
4402
4458
|
console.error(`Failed: ${err.message}`);
|
|
4403
4459
|
process.exit(1);
|
|
@@ -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
|
package/src/lib/daemon-logs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
@@ -23,3 +23,21 @@ export const readLogTail = (path, count = 50) => {
|
|
|
23
23
|
throw error;
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
export const logSize = (path) => statSync(path).size;
|
|
28
|
+
|
|
29
|
+
// Bytes appended since `offset`, for `agent logs --follow`. A file that shrank
|
|
30
|
+
// (rotated or truncated) is read from the start again.
|
|
31
|
+
export const readAppended = (path, offset) => {
|
|
32
|
+
const size = logSize(path);
|
|
33
|
+
const start = size < offset ? 0 : offset;
|
|
34
|
+
if (size === start) return { text: '', offset: size };
|
|
35
|
+
const fd = openSync(path, 'r');
|
|
36
|
+
try {
|
|
37
|
+
const buffer = Buffer.alloc(size - start);
|
|
38
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
39
|
+
return { text: buffer.toString('utf8'), offset: size };
|
|
40
|
+
} finally {
|
|
41
|
+
closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -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
|
+
};
|