@commonlyai/cli 0.1.212 → 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 +8 -0
- package/src/commands/mcp.js +55 -0
- package/src/index.js +2 -0
- package/src/lib/mcp-token.js +36 -0
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) ────────────────────────────────
|
|
@@ -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
|
|
@@ -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
|
+
};
|