@commonlyai/cli 0.1.45 → 0.1.48
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
package/src/commands/agent.js
CHANGED
|
@@ -33,6 +33,7 @@ import { pollRetryPolicy } from '../lib/poll-retry.js';
|
|
|
33
33
|
import { detectMemorySources, composeImport, importMemory } from '../lib/memory-import.js';
|
|
34
34
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
35
35
|
import { parseEnvironmentFile, resolveWorkspace, validateEnvironmentSpec } from '../lib/environment.js';
|
|
36
|
+
import { ADAPTERS_WITH_DEFAULT_MCP, defaultMcpServers } from '../lib/default-environment.js';
|
|
36
37
|
import {
|
|
37
38
|
FOCUS_FRAME_MAX_CODE_POINTS,
|
|
38
39
|
formatPodFocusFrame,
|
|
@@ -247,14 +248,9 @@ const PRIVATE_RESPONSE_EVENT_TYPES = new Set(['agent.ask', 'agent.ask.response']
|
|
|
247
248
|
|
|
248
249
|
// ── default environment for adapters that benefit from auto-MCP wiring ─────
|
|
249
250
|
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// the 2026-07-22 as-operator attribution incident, where an MCP-less codex
|
|
254
|
-
// agent posted through the operator's CLI profile because it had no
|
|
255
|
-
// commonly_* tools of its own). `stub` does not. Returning null means "no
|
|
256
|
-
// default" — the wrapper proceeds with environment=null exactly like before.
|
|
257
|
-
const ADAPTERS_WITH_DEFAULT_MCP = new Set(['claude', 'codex', 'pi']);
|
|
251
|
+
// Which adapters can consume `mcp[]`, and the server declaration they get by
|
|
252
|
+
// default, live in lib/default-environment.js — the daemon's per-seat token
|
|
253
|
+
// provisioning reads the same module, so the two can never drift apart.
|
|
258
254
|
const CODEX_PERMISSION_PROFILE_MIN_VERSION = [0, 138, 0];
|
|
259
255
|
|
|
260
256
|
const versionAtLeast = (version, minimum) => {
|
|
@@ -285,23 +281,11 @@ const BUNDLED_COMMONLY_SKILL_DIR = pathResolve(
|
|
|
285
281
|
// reply conversationally, use the roster, don't double-post). Without the
|
|
286
282
|
// skill, wrapper-spawned CLIs fly blind and behave inconsistently — some
|
|
287
283
|
// narrate after tool-posting (double-post), some default to NO_REPLY on a
|
|
288
|
-
// normal question.
|
|
289
|
-
//
|
|
284
|
+
// normal question. Returning null means "no default" — a non-consuming adapter
|
|
285
|
+
// proceeds with environment=null exactly like before.
|
|
290
286
|
export const buildDefaultEnvironment = (adapterName) => {
|
|
291
287
|
if (!ADAPTERS_WITH_DEFAULT_MCP.has(adapterName)) return null;
|
|
292
|
-
const environment = {
|
|
293
|
-
mcp: [
|
|
294
|
-
{
|
|
295
|
-
name: 'commonly',
|
|
296
|
-
transport: 'stdio',
|
|
297
|
-
command: ['npx', '-y', '@commonlyai/mcp@latest'],
|
|
298
|
-
env: {
|
|
299
|
-
COMMONLY_API_URL: '${COMMONLY_API_URL}',
|
|
300
|
-
COMMONLY_AGENT_TOKEN: '${COMMONLY_AGENT_TOKEN}',
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
};
|
|
288
|
+
const environment = { mcp: defaultMcpServers(adapterName) };
|
|
305
289
|
// Only advertise the skill if it actually shipped (defensive: a broken
|
|
306
290
|
// package that dropped the skills/ dir shouldn't hand mountSkills a
|
|
307
291
|
// missing-source path every spawn).
|
package/src/lib/adapters/pi.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { spawn as childSpawn, spawnSync } from 'child_process';
|
|
36
36
|
import { createHash, randomUUID } from 'crypto';
|
|
37
|
-
import { mkdir, writeFile } from 'fs/promises';
|
|
37
|
+
import { mkdir, readdir, writeFile } from 'fs/promises';
|
|
38
38
|
import { homedir } from 'os';
|
|
39
39
|
import { dirname, join } from 'path';
|
|
40
40
|
import { fileURLToPath } from 'url';
|
|
@@ -127,6 +127,24 @@ export const seatHome = (ctx) => {
|
|
|
127
127
|
return ctx._piHome || join(homedir(), '.commonly', 'pi-homes', hash);
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Only a session pi wrote can be resumed. The wrapper persists one id per
|
|
132
|
+
* (agent, pod) across adapters, so a seat switched from codex hands pi the
|
|
133
|
+
* codex thread id on its first turn — pi answers `No session found` and the
|
|
134
|
+
* turn fails (sprint-impl's first spawn, 2026-09-18 05:06Z). pi's session
|
|
135
|
+
* files are `<timestamp>_<id>.jsonl` under the seat's session dir; an id with
|
|
136
|
+
* no file starts a fresh session under a new id, which the wrapper persists.
|
|
137
|
+
*/
|
|
138
|
+
export const sessionExists = async (sessionDir, sessionId) => {
|
|
139
|
+
if (!sessionId) return false;
|
|
140
|
+
try {
|
|
141
|
+
const files = await readdir(sessionDir);
|
|
142
|
+
return files.some((name) => name.endsWith(`_${sessionId}.jsonl`) || name === `${sessionId}.jsonl`);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
130
148
|
export const buildArgs = ({
|
|
131
149
|
prompt, provider, model, thinking, sessionId, isResume, sessionDir, bridge,
|
|
132
150
|
}) => [
|
|
@@ -196,9 +214,6 @@ export default {
|
|
|
196
214
|
},
|
|
197
215
|
|
|
198
216
|
async spawn(prompt, ctx = {}) {
|
|
199
|
-
const isResume = !!ctx.sessionId;
|
|
200
|
-
const sessionId = ctx.sessionId || randomUUID();
|
|
201
|
-
const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: !isResume });
|
|
202
217
|
const provider = resolveProvider(ctx.environment);
|
|
203
218
|
const model = ctx.environment?.model || DEFAULT_MODEL;
|
|
204
219
|
const thinking = thinkingFor(ctx.environment?.effort);
|
|
@@ -213,6 +228,12 @@ export default {
|
|
|
213
228
|
const sessionDir = join(home, 'sessions');
|
|
214
229
|
await mkdir(agentDir, { recursive: true, mode: 0o700 });
|
|
215
230
|
await mkdir(sessionDir, { recursive: true, mode: 0o700 });
|
|
231
|
+
|
|
232
|
+
// Resume only what pi wrote; anything else (a codex thread id from before
|
|
233
|
+
// the switch, a wiped home) starts fresh under a new id.
|
|
234
|
+
const isResume = await sessionExists(sessionDir, ctx.sessionId);
|
|
235
|
+
const sessionId = isResume ? ctx.sessionId : randomUUID();
|
|
236
|
+
const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: !isResume });
|
|
216
237
|
await writeFile(join(agentDir, 'models.json'), `${JSON.stringify(buildModelsJson(provider, model), null, 2)}\n`, { mode: 0o600 });
|
|
217
238
|
|
|
218
239
|
const servers = resolveMcpServers(ctx.environment?.mcp, ctx);
|
|
@@ -2,6 +2,8 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { isAbsolute, resolve as pathResolve } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { withDefaultMcpServer } from './default-environment.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* ADR-026 Phase 2, slice 2: the resident supervision loop behind
|
|
7
9
|
* `commonly daemon run`.
|
|
@@ -181,9 +183,14 @@ export const createDaemonSupervisor = ({
|
|
|
181
183
|
adapterChanged = existing.adapter !== nextAdapter;
|
|
182
184
|
}
|
|
183
185
|
if (wanted) {
|
|
184
|
-
const
|
|
186
|
+
const merged = wanted.declared
|
|
185
187
|
? wanted.value
|
|
186
188
|
: { ...(existing.environment || {}), ...wanted.value };
|
|
189
|
+
// A server-declared environment replaces the local one, so a seat whose
|
|
190
|
+
// declaration carries no mcp[] would come back tool-less. Re-apply the
|
|
191
|
+
// shipped default here and below, or the seat silently loses every
|
|
192
|
+
// commonly_* tool the next time the UI edits its model (TASK-048).
|
|
193
|
+
const nextEnvironment = withDefaultMcpServer(merged, nextAdapter);
|
|
187
194
|
const workspacePath = workspacePathFor(nextEnvironment);
|
|
188
195
|
const nextRecord = {
|
|
189
196
|
...existing,
|
|
@@ -200,10 +207,32 @@ export const createDaemonSupervisor = ({
|
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
if (adapterChanged) {
|
|
203
|
-
|
|
210
|
+
// An adapter that consumes mcp[] must not be started on a record that
|
|
211
|
+
// declares none, even when only the adapter itself changed.
|
|
212
|
+
const nextEnvironment = withDefaultMcpServer(existing.environment, nextAdapter);
|
|
213
|
+
saveToken(row.agentName, {
|
|
214
|
+
...existing,
|
|
215
|
+
adapter: nextAdapter,
|
|
216
|
+
...(nextEnvironment ? { environment: nextEnvironment } : {}),
|
|
217
|
+
});
|
|
204
218
|
log('runtime adapter changed — restarting the seat to load it');
|
|
205
219
|
return 'changed';
|
|
206
220
|
}
|
|
221
|
+
if (!wanted) {
|
|
222
|
+
// A record written by a CLI older than the shipped default carries no
|
|
223
|
+
// mcp[] — the c4-smoke record was exactly this, and the operator had to
|
|
224
|
+
// hand-add the entry. Behind a row that declares nothing there is no
|
|
225
|
+
// other write path: the record is never touched again, so the seat
|
|
226
|
+
// stays tool-less for as long as it runs. Heal it here, and only when
|
|
227
|
+
// the environment actually changed — otherwise every tick rewrites the
|
|
228
|
+
// file and restarts the seat forever.
|
|
229
|
+
const nextEnvironment = withDefaultMcpServer(existing.environment, existing.adapter);
|
|
230
|
+
if (!isDeepStrictEqual(existing.environment || null, nextEnvironment || null)) {
|
|
231
|
+
saveToken(row.agentName, { ...existing, environment: nextEnvironment });
|
|
232
|
+
log('record predates the commonly MCP baseline — restarting the seat to load it');
|
|
233
|
+
return 'changed';
|
|
234
|
+
}
|
|
235
|
+
}
|
|
207
236
|
return 'ready';
|
|
208
237
|
}
|
|
209
238
|
const requestedAdapter = row.runtime && typeof row.runtime === 'object'
|
|
@@ -246,6 +275,13 @@ export const createDaemonSupervisor = ({
|
|
|
246
275
|
return false;
|
|
247
276
|
}
|
|
248
277
|
const environment = environmentFor(row);
|
|
278
|
+
// A seat installed server-side (no local `agent attach`) arrives with no
|
|
279
|
+
// mcp[] at all: without the default it spawns a CLI that has no commonly_*
|
|
280
|
+
// tools and cannot post. See lib/default-environment.js.
|
|
281
|
+
const recordEnvironment = withDefaultMcpServer(
|
|
282
|
+
environment ? environment.value : null,
|
|
283
|
+
adapter,
|
|
284
|
+
);
|
|
249
285
|
saveToken(row.agentName, {
|
|
250
286
|
agentName: row.agentName,
|
|
251
287
|
instanceId: row.instanceId,
|
|
@@ -253,9 +289,9 @@ export const createDaemonSupervisor = ({
|
|
|
253
289
|
instanceUrl: record.instanceUrl,
|
|
254
290
|
podId: row.podIds?.[0] || null,
|
|
255
291
|
adapter,
|
|
256
|
-
...(
|
|
257
|
-
...(
|
|
258
|
-
const workspacePath = workspacePathFor(
|
|
292
|
+
...(recordEnvironment ? { environment: recordEnvironment } : {}),
|
|
293
|
+
...(recordEnvironment ? (() => {
|
|
294
|
+
const workspacePath = workspacePathFor(recordEnvironment);
|
|
259
295
|
return workspacePath ? { workspacePath } : {};
|
|
260
296
|
})() : {}),
|
|
261
297
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commonly MCP server every wrapper seat gets by default (ADR-008 `mcp[]`).
|
|
3
|
+
*
|
|
4
|
+
* One declaration, three consumers: `agent attach`, the COMMONLY_AGENT_TOKEN
|
|
5
|
+
* bootstrap in `agent run`, and the daemon's per-seat token provisioning. The
|
|
6
|
+
* third consumer was missing until 2026-09-18 — a seat installed server-side
|
|
7
|
+
* (nobody ever ran `agent attach` on that host) got a token record carrying
|
|
8
|
+
* runtime model/effort and no `mcp[]` at all, so the CLI it spawned had no
|
|
9
|
+
* `commonly_*` tools and could not post; the operator hand-added the entry to
|
|
10
|
+
* get the seat working (C4 run, TASK-048).
|
|
11
|
+
*
|
|
12
|
+
* Only adapters with a real `mcp[]` consumption path get a default:
|
|
13
|
+
* claude — `--mcp-config` (adapters/claude.js)
|
|
14
|
+
* codex — `-c mcp_servers.*` overrides (adapters/codex.js; added after the
|
|
15
|
+
* 2026-07-22 as-operator attribution incident, where an MCP-less
|
|
16
|
+
* codex agent posted through the operator's own CLI profile because
|
|
17
|
+
* it had no commonly_* tool of its own)
|
|
18
|
+
* pi — stdio servers from the environment spec (adapters/pi.js)
|
|
19
|
+
* `stub` has no consumption path and must keep being handed `environment: null`.
|
|
20
|
+
*
|
|
21
|
+
* The placeholders are substituted at spawn time by the adapter, so the
|
|
22
|
+
* declaration itself carries no secret and is safe to persist to a token file.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const ADAPTERS_WITH_DEFAULT_MCP = new Set(['claude', 'codex', 'pi']);
|
|
26
|
+
|
|
27
|
+
export const COMMONLY_MCP_SERVER_NAME = 'commonly';
|
|
28
|
+
|
|
29
|
+
export const commonlyMcpServer = () => ({
|
|
30
|
+
name: COMMONLY_MCP_SERVER_NAME,
|
|
31
|
+
transport: 'stdio',
|
|
32
|
+
command: ['npx', '-y', '@commonlyai/mcp@latest'],
|
|
33
|
+
env: {
|
|
34
|
+
COMMONLY_API_URL: '${COMMONLY_API_URL}',
|
|
35
|
+
COMMONLY_AGENT_TOKEN: '${COMMONLY_AGENT_TOKEN}',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** The default `mcp[]` for an adapter, or [] when it has no consumption path. */
|
|
40
|
+
export const defaultMcpServers = (adapterName) => (
|
|
41
|
+
ADAPTERS_WITH_DEFAULT_MCP.has(adapterName) ? [commonlyMcpServer()] : []
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ensure `environment` declares the commonly MCP server, touching nothing that
|
|
46
|
+
* is already declared.
|
|
47
|
+
*
|
|
48
|
+
* The predicate is "is there an mcp entry named commonly?", not "is mcp[]
|
|
49
|
+
* absent?" — the daemon projects live room grants into this same array, so a
|
|
50
|
+
* seat can arrive with `mcp: [<grant broker>]` and no kernel server, and it is
|
|
51
|
+
* exactly as tool-less as one with no `mcp` key at all.
|
|
52
|
+
*
|
|
53
|
+
* Returns the SAME reference when nothing needs adding, so callers can use the
|
|
54
|
+
* identity as a dirty check. A declared commonly entry is never replaced or
|
|
55
|
+
* duplicated — an operator's hand-set command (a pinned version, a staging
|
|
56
|
+
* checkout) wins over the shipped default — and a non-consuming adapter is
|
|
57
|
+
* returned untouched. A malformed (non-object) spec is not ours to repair.
|
|
58
|
+
*/
|
|
59
|
+
export const withDefaultMcpServer = (environment, adapterName) => {
|
|
60
|
+
if (!ADAPTERS_WITH_DEFAULT_MCP.has(adapterName)) return environment;
|
|
61
|
+
if (environment !== null && environment !== undefined
|
|
62
|
+
&& (typeof environment !== 'object' || Array.isArray(environment))) {
|
|
63
|
+
return environment;
|
|
64
|
+
}
|
|
65
|
+
const declared = Array.isArray(environment?.mcp) ? environment.mcp : null;
|
|
66
|
+
if (declared && declared.some((server) => server?.name === COMMONLY_MCP_SERVER_NAME)) {
|
|
67
|
+
return environment;
|
|
68
|
+
}
|
|
69
|
+
return { ...(environment || {}), mcp: [...(declared || []), commonlyMcpServer()] };
|
|
70
|
+
};
|