@commonlyai/cli 0.1.47 → 0.1.50

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.47",
3
+ "version": "0.1.50",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -12,10 +12,12 @@
12
12
  */
13
13
 
14
14
  import { Type } from 'typebox';
15
- import { connectMcp, readServers, toPiResult } from './pi-mcp-client.mjs';
15
+ import { connectMcp, takeServers, toPiResult } from './pi-mcp-client.mjs';
16
16
 
17
17
  export default async function commonlyMcpBridge(pi) {
18
- const servers = readServers(process.env.COMMONLY_PI_MCP);
18
+ // Read once and remove: the list carries the seat token, and pi's bash tool
19
+ // inherits this process's env (see takeServers).
20
+ const servers = takeServers(process.env);
19
21
  const clients = [];
20
22
  for (const server of servers) {
21
23
  const client = connectMcp(server);
@@ -78,6 +78,19 @@ export const toPiResult = (result) => {
78
78
  return { content: [{ type: 'text', text: result?.isError ? `error: ${text}` : text }], details: { isError: !!result?.isError } };
79
79
  };
80
80
 
81
+ /**
82
+ * Read the server list and REMOVE it from the environment. The list carries each
83
+ * server's substituted env — the seat's bearer token among it — and pi's `bash`
84
+ * tool spawns with `{ ...process.env }` (pi's getShellEnv), so leaving it in place
85
+ * lets one `env` from the model print the token. The MCP children already hold
86
+ * their own env from spawn time; nothing else reads this variable.
87
+ */
88
+ export const takeServers = (env = process.env) => {
89
+ const servers = readServers(env.COMMONLY_PI_MCP);
90
+ delete env.COMMONLY_PI_MCP;
91
+ return servers;
92
+ };
93
+
81
94
  export const readServers = (raw) => {
82
95
  if (!raw) return [];
83
96
  try { return JSON.parse(raw).filter((s) => s?.name && Array.isArray(s.command) && s.command.length); } catch { return []; }
@@ -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,35 @@ 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
+
148
+ /** Fail closed: pi cannot enforce `environment.sandbox`, so a seat that declares one does not start. */
149
+ export const assertNoSandboxDeclared = (environment = {}) => {
150
+ const sandbox = environment?.sandbox || {};
151
+ if (sandbox.trust === 'public') {
152
+ throw new Error('pi adapter: public-trust seats are not supported — pi has no enforced sandbox; do not attach a pi seat to a stranger-readable pod');
153
+ }
154
+ if (sandbox.mode && sandbox.mode !== 'none') {
155
+ throw new Error(`pi adapter: sandbox.mode=${sandbox.mode} cannot be enforced by pi; remove it or use the claude/codex adapter`);
156
+ }
157
+ };
158
+
130
159
  export const buildArgs = ({
131
160
  prompt, provider, model, thinking, sessionId, isResume, sessionDir, bridge,
132
161
  }) => [
@@ -196,9 +225,10 @@ export default {
196
225
  },
197
226
 
198
227
  async spawn(prompt, ctx = {}) {
199
- const isResume = !!ctx.sessionId;
200
- const sessionId = ctx.sessionId || randomUUID();
201
- const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: !isResume });
228
+ // pi has no enforced sandbox. claude.js and codex.js refuse a public-trust
229
+ // seat they cannot confine; so does this adapter, and it refuses any declared
230
+ // sandbox mode rather than run unconfined under a spec that promised one.
231
+ assertNoSandboxDeclared(ctx.environment);
202
232
  const provider = resolveProvider(ctx.environment);
203
233
  const model = ctx.environment?.model || DEFAULT_MODEL;
204
234
  const thinking = thinkingFor(ctx.environment?.effort);
@@ -213,6 +243,12 @@ export default {
213
243
  const sessionDir = join(home, 'sessions');
214
244
  await mkdir(agentDir, { recursive: true, mode: 0o700 });
215
245
  await mkdir(sessionDir, { recursive: true, mode: 0o700 });
246
+
247
+ // Resume only what pi wrote; anything else (a codex thread id from before
248
+ // the switch, a wiped home) starts fresh under a new id.
249
+ const isResume = await sessionExists(sessionDir, ctx.sessionId);
250
+ const sessionId = isResume ? ctx.sessionId : randomUUID();
251
+ const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: !isResume });
216
252
  await writeFile(join(agentDir, 'models.json'), `${JSON.stringify(buildModelsJson(provider, model), null, 2)}\n`, { mode: 0o600 });
217
253
 
218
254
  const servers = resolveMcpServers(ctx.environment?.mcp, ctx);