@cabane/companion 0.6.19 → 0.6.21

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/dist/runtime.js CHANGED
@@ -288,6 +288,26 @@ var companionConfigSchema = z2.object({
288
288
  opencode: z2.object({
289
289
  serverUrl: z2.string().url()
290
290
  }).strict().optional(),
291
+ // CT1082: the Claude Code runtime, when the user connected it on this machine.
292
+ // The change this task exists for: Claude Code used to be the one harness a
293
+ // device exposed with no configuration — a `claude --version` exit-0 was taken as
294
+ // consent — and it is now the third config-driven harness, opted into exactly
295
+ // like codex. `claude` on PATH is still required (you can't run what isn't
296
+ // installed), but presence alone no longer exposes anything: the manifest
297
+ // advertises claude-code only when this block says so AND the binary is there.
298
+ //
299
+ // The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
300
+ // Absent means the config predates this task — a device whose user was never
301
+ // asked — and the migration grandfathers it on first start. So every config
302
+ // written from here on carries the block explicitly, including a freshly-paired
303
+ // one, which starts at `enabled: false`: a new device is connected to nothing
304
+ // until its user says otherwise.
305
+ //
306
+ // Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
307
+ // above — that's one agent's memory switch, this is the device's connected set.
308
+ claudeCode: z2.object({
309
+ enabled: z2.boolean().optional()
310
+ }).strict().optional(),
291
311
  // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
292
312
  // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
293
313
  // the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
@@ -304,19 +324,26 @@ var companionConfigSchema = z2.object({
304
324
  function isCodexEnabled(cfg) {
305
325
  return !!cfg.codex && cfg.codex.enabled !== false;
306
326
  }
327
+ function isClaudeCodeConnected(cfg) {
328
+ return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
329
+ }
330
+ function migrateConnectedHarnesses(cfg, claudeOnPath2) {
331
+ if (cfg.claudeCode !== void 0) return null;
332
+ return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
333
+ }
307
334
  function localAgentConfig(cfg, agent) {
308
335
  const map = cfg.agents ?? {};
309
336
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
310
337
  }
311
338
  function loadConfig() {
312
- const path3 = configPath();
313
- if (!existsSync(path3)) return null;
339
+ const path = configPath();
340
+ if (!existsSync(path)) return null;
314
341
  let raw;
315
342
  try {
316
- raw = readFileSync(path3, "utf8");
343
+ raw = readFileSync(path, "utf8");
317
344
  } catch (err) {
318
345
  throw new ConfigError(
319
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
346
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
320
347
  );
321
348
  }
322
349
  if (raw.trim().length === 0) return null;
@@ -325,7 +352,7 @@ function loadConfig() {
325
352
  parsed = JSON.parse(raw);
326
353
  } catch (err) {
327
354
  throw new ConfigError(
328
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
355
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
329
356
  );
330
357
  }
331
358
  const result = companionConfigSchema.safeParse(parsed);
@@ -333,30 +360,30 @@ function loadConfig() {
333
360
  const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
334
361
  if (agentIssue) {
335
362
  throw new ConfigError(
336
- `${path3}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
363
+ `${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
337
364
  );
338
365
  }
339
366
  throw new ConfigError(
340
- `${path3} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
367
+ `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
341
368
  );
342
369
  }
343
370
  return result.data;
344
371
  }
345
372
  function saveConfig(cfg) {
346
- const path3 = configPath();
347
- mkdirSync(dirname(path3), { recursive: true });
373
+ const path = configPath();
374
+ mkdirSync(dirname(path), { recursive: true });
348
375
  try {
349
376
  chmodSync(cabaneDir(), 448);
350
377
  } catch {
351
378
  }
352
- const tmp = `${path3}.${process.pid}.tmp`;
379
+ const tmp = `${path}.${process.pid}.tmp`;
353
380
  try {
354
381
  writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
355
382
  try {
356
383
  chmodSync(tmp, 384);
357
384
  } catch {
358
385
  }
359
- renameSync(tmp, path3);
386
+ renameSync(tmp, path);
360
387
  } catch (err) {
361
388
  try {
362
389
  rmSync(tmp, { force: true });
@@ -417,8 +444,8 @@ function consoleMessageFormat(log, messageKey) {
417
444
  var cached = null;
418
445
  function getLogger() {
419
446
  if (cached) return cached;
420
- const path3 = companionLogPath();
421
- mkdirSync2(dirname2(path3), { recursive: true });
447
+ const path = companionLogPath();
448
+ mkdirSync2(dirname2(path), { recursive: true });
422
449
  const streams = [];
423
450
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
424
451
  const consoleStream = pretty({
@@ -428,7 +455,7 @@ function getLogger() {
428
455
  });
429
456
  streams.push({ level: "info", stream: consoleStream });
430
457
  }
431
- streams.push({ level: "debug", stream: createWriteStream(path3, { flags: "a" }) });
458
+ streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
432
459
  cached = pino({ level: "debug" }, pino.multistream(streams));
433
460
  return cached;
434
461
  }
@@ -739,6 +766,11 @@ function registerRoutes(app, deps) {
739
766
  app.post("/api/harnesses/enable", async (c) => {
740
767
  const body = await readJson(c);
741
768
  const runtime = body.runtime;
769
+ if (runtime === "claude-code") {
770
+ const result = await supervisor.enableHarness({ runtime: "claude-code" });
771
+ if (!result.ok) return c.json({ error: result.error }, 400);
772
+ return c.json({ ok: true, status: hub.statusJson() });
773
+ }
742
774
  if (runtime === "codex") {
743
775
  const result = await supervisor.enableHarness({ runtime: "codex" });
744
776
  if (!result.ok) return c.json({ error: result.error }, 400);
@@ -807,12 +839,12 @@ function clampLimit(raw, fallback, max = 200) {
807
839
  if (!Number.isFinite(n) || n <= 0) return fallback;
808
840
  return Math.min(Math.floor(n), max);
809
841
  }
810
- function tailFile(path3, lines) {
811
- if (!existsSync2(path3)) return [];
842
+ function tailFile(path, lines) {
843
+ if (!existsSync2(path)) return [];
812
844
  const MAX_BYTES = 256 * 1024;
813
845
  let fd;
814
846
  try {
815
- fd = openSync(path3, "r");
847
+ fd = openSync(path, "r");
816
848
  const size = fstatSync(fd).size;
817
849
  const start = Math.max(0, size - MAX_BYTES);
818
850
  const len = size - start;
@@ -1012,26 +1044,43 @@ async function codexOnPath() {
1012
1044
  ]);
1013
1045
  return version !== null;
1014
1046
  }
1015
- async function ensureRuntimeAvailable(cfg, deps = {}) {
1047
+ async function requireStartConfig(deps = {}) {
1048
+ const requireCfg = deps.requireCfg ?? requireConfig;
1049
+ const probeClaude = deps.probeClaude ?? claudeOnPath;
1050
+ const save2 = deps.save ?? saveConfig;
1051
+ const cfg = requireCfg();
1052
+ const claudeOnPathResult = await probeClaude();
1053
+ const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
1054
+ if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
1055
+ save2(migrated);
1056
+ deps.onMigrated?.(migrated, claudeOnPathResult);
1057
+ return { cfg: migrated, claudeOnPath: claudeOnPathResult };
1058
+ }
1059
+ async function warnAboutHarnessReadiness(cfg, deps = {}) {
1016
1060
  const probeClaude = deps.probeClaude ?? claudeOnPath;
1017
1061
  const probeCodex = deps.probeCodex ?? codexOnPath;
1018
1062
  const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
1019
1063
  `));
1020
- if (await probeClaude()) return;
1021
- const alternates = [
1022
- ...cfg.opencode ? ["opencode"] : [],
1023
- ...isCodexEnabled(cfg) ? ["codex"] : []
1064
+ const connected = [
1065
+ ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
1066
+ ...isCodexEnabled(cfg) ? ["Codex"] : [],
1067
+ ...cfg.opencode ? ["opencode"] : []
1024
1068
  ];
1025
- if (alternates.length > 0) {
1026
- const exposed = alternates.join(" + ");
1027
- warn(
1028
- `warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
1029
- );
1069
+ if (connected.length > 0) {
1070
+ if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
1071
+ warn(
1072
+ "warning: Claude Code is connected on this device but `claude` isn\u2019t on your PATH, so it advertises nothing and a Claude-model agent won\u2019t be routed here. Install it (`npm i -g @anthropic-ai/claude-code`) and log in, or disconnect it."
1073
+ );
1074
+ }
1030
1075
  return;
1031
1076
  }
1032
- const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
1033
- throw new CompanionError(
1034
- "This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
1077
+ const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
1078
+ const installed = [
1079
+ ...claudeInstalled ? ["Claude Code"] : [],
1080
+ ...codexInstalled ? ["Codex"] : []
1081
+ ];
1082
+ warn(
1083
+ "No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"} in the Companion dashboard (or in cabane, Settings \u2192 Devices) and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it in the Companion dashboard.")
1035
1084
  );
1036
1085
  }
1037
1086
 
@@ -1054,9 +1103,9 @@ function serialize(state) {
1054
1103
  return JSON.stringify(state, null, 2) + "\n";
1055
1104
  }
1056
1105
  function writeRuntimeState(state) {
1057
- const path3 = runtimePath();
1106
+ const path = runtimePath();
1058
1107
  mkdirSync3(cabaneDir(), { recursive: true });
1059
- writeFileSync2(path3, serialize(state), "utf8");
1108
+ writeFileSync2(path, serialize(state), "utf8");
1060
1109
  }
1061
1110
  function acquireRuntimeState(state) {
1062
1111
  const live = readLiveRuntimeState();
@@ -1076,15 +1125,15 @@ function acquireRuntimeState(state) {
1076
1125
  return { acquired: true };
1077
1126
  }
1078
1127
  function clearRuntimeState() {
1079
- const path3 = runtimePath();
1080
- if (existsSync3(path3)) rmSync2(path3, { force: true });
1128
+ const path = runtimePath();
1129
+ if (existsSync3(path)) rmSync2(path, { force: true });
1081
1130
  }
1082
1131
  function readLiveRuntimeState() {
1083
- const path3 = runtimePath();
1084
- if (!existsSync3(path3)) return null;
1132
+ const path = runtimePath();
1133
+ if (!existsSync3(path)) return null;
1085
1134
  let parsed;
1086
1135
  try {
1087
- parsed = JSON.parse(readFileSync2(path3, "utf8"));
1136
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
1088
1137
  } catch {
1089
1138
  return null;
1090
1139
  }
@@ -1148,8 +1197,8 @@ var CabaneApi = class {
1148
1197
  // One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
1149
1198
  // rethrows transport errors (fetch reject) unchanged so the caller's retry
1150
1199
  // logic can classify them.
1151
- async attempt(method, path3, body, signal) {
1152
- const res = await fetch(`${this.base}${path3}`, {
1200
+ async attempt(method, path, body, signal) {
1201
+ const res = await fetch(`${this.base}${path}`, {
1153
1202
  method,
1154
1203
  headers: {
1155
1204
  Authorization: `Bearer ${this.opts.token}`,
@@ -1175,12 +1224,12 @@ var CabaneApi = class {
1175
1224
  }
1176
1225
  return parsed;
1177
1226
  }
1178
- async request(method, path3, body, opts = {}) {
1227
+ async request(method, path, body, opts = {}) {
1179
1228
  const { signal, retry = false } = opts;
1180
1229
  const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
1181
1230
  for (let attempt = 1; ; attempt++) {
1182
1231
  try {
1183
- return await this.attempt(method, path3, body, signal);
1232
+ return await this.attempt(method, path, body, signal);
1184
1233
  } catch (err) {
1185
1234
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1186
1235
  await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
@@ -1206,9 +1255,9 @@ var CabaneApi = class {
1206
1255
  // delivers it once the API returns. `(turnId, seq)` is the server's
1207
1256
  // idempotency key, so a replay whose original POST's fate is unknown
1208
1257
  // converges instead of duplicating.
1209
- async durableCommit(kind, path3, body, turnId, seq, signal) {
1258
+ async durableCommit(kind, path, body, turnId, seq, signal) {
1210
1259
  try {
1211
- await this.request("POST", path3, body, {
1260
+ await this.request("POST", path, body, {
1212
1261
  retry: true,
1213
1262
  ...signal ? { signal } : {}
1214
1263
  });
@@ -1217,7 +1266,7 @@ var CabaneApi = class {
1217
1266
  if (!outbox) throw err;
1218
1267
  if (signal?.aborted || isAbortError(err)) throw err;
1219
1268
  if (!isRetryable(err)) throw err;
1220
- outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1269
+ outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
1221
1270
  this.opts.log?.warn(
1222
1271
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1223
1272
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1369,12 +1418,12 @@ var CabaneApi = class {
1369
1418
  // left best-effort: it's lower-stakes and self-heals on the next turn, so it
1370
1419
  // stays a single-shot PATCH and is deliberately out of CT93's scope.
1371
1420
  setActiveRun(workspaceId, conversationId, agentId, body) {
1372
- const path3 = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1421
+ const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1373
1422
  const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
1374
1423
  if (touchesFlag && this.opts.outbox) {
1375
- return this.durableActiveRunWrite(path3, conversationId, agentId, body);
1424
+ return this.durableActiveRunWrite(path, conversationId, agentId, body);
1376
1425
  }
1377
- return this.request("PATCH", path3, body);
1426
+ return this.request("PATCH", path, body);
1378
1427
  }
1379
1428
  // CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
1380
1429
  // across the (conversation, agent) pair. Mirrors `durableCommit`, with two
@@ -1388,11 +1437,11 @@ var CabaneApi = class {
1388
1437
  // later and clobber the state we just wrote (the cross-turn race: turn N's
1389
1438
  // queued clear vs. turn N+1's live set). Combined with persist-overwrites-
1390
1439
  // by-key, this is the full last-writer-wins guarantee.
1391
- async durableActiveRunWrite(path3, conversationId, agentId, body) {
1440
+ async durableActiveRunWrite(path, conversationId, agentId, body) {
1392
1441
  const outbox = this.opts.outbox;
1393
1442
  const key = activeRunOutboxKey(conversationId, agentId);
1394
1443
  try {
1395
- await this.request("PATCH", path3, body, { retry: true });
1444
+ await this.request("PATCH", path, body, { retry: true });
1396
1445
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1397
1446
  } catch (err) {
1398
1447
  if (!outbox) throw err;
@@ -1405,7 +1454,7 @@ var CabaneApi = class {
1405
1454
  turnId: key,
1406
1455
  seq: ACTIVE_RUN_OUTBOX_SEQ,
1407
1456
  method: "PATCH",
1408
- path: path3,
1457
+ path,
1409
1458
  body,
1410
1459
  kind: "active-run"
1411
1460
  });
@@ -1493,8 +1542,8 @@ var CabaneApi = class {
1493
1542
  // shared resolver the in-app path uses. Omitting it (older call sites) returns
1494
1543
  // the agent default — graceful degradation, no version coupling.
1495
1544
  getAgentSelf(conversationId) {
1496
- const path3 = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1497
- return this.request("GET", path3);
1545
+ const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1546
+ return this.request("GET", path);
1498
1547
  }
1499
1548
  // The companion fetches the triggering message body by listing the
1500
1549
  // conversation's messages and finding the one with `id === messageId`.
@@ -1549,8 +1598,8 @@ var DeviceApi = class {
1549
1598
  get base() {
1550
1599
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
1551
1600
  }
1552
- async request(method, path3, body) {
1553
- const res = await fetch(`${this.base}${path3}`, {
1601
+ async request(method, path, body) {
1602
+ const res = await fetch(`${this.base}${path}`, {
1554
1603
  method,
1555
1604
  headers: {
1556
1605
  Authorization: `Bearer ${this.opts.deviceToken}`,
@@ -1614,11 +1663,11 @@ function credentialsPath() {
1614
1663
  }
1615
1664
  var credentialStoreSchema = z3.record(z3.string(), z3.string());
1616
1665
  function load() {
1617
- const path3 = credentialsPath();
1618
- if (!existsSync4(path3)) return {};
1666
+ const path = credentialsPath();
1667
+ if (!existsSync4(path)) return {};
1619
1668
  let raw;
1620
1669
  try {
1621
- raw = readFileSync3(path3, "utf8");
1670
+ raw = readFileSync3(path, "utf8");
1622
1671
  } catch {
1623
1672
  return {};
1624
1673
  }
@@ -1631,20 +1680,20 @@ function load() {
1631
1680
  }
1632
1681
  }
1633
1682
  function save(map) {
1634
- const path3 = credentialsPath();
1635
- mkdirSync4(dirname4(path3), { recursive: true });
1683
+ const path = credentialsPath();
1684
+ mkdirSync4(dirname4(path), { recursive: true });
1636
1685
  try {
1637
1686
  chmodSync2(cabaneDir(), 448);
1638
1687
  } catch {
1639
1688
  }
1640
- const tmp = `${path3}.${process.pid}.tmp`;
1689
+ const tmp = `${path}.${process.pid}.tmp`;
1641
1690
  try {
1642
1691
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
1643
1692
  try {
1644
1693
  chmodSync2(tmp, 384);
1645
1694
  } catch {
1646
1695
  }
1647
- renameSync2(tmp, path3);
1696
+ renameSync2(tmp, path);
1648
1697
  } catch (err) {
1649
1698
  try {
1650
1699
  rmSync3(tmp, { force: true });
@@ -1683,15 +1732,15 @@ function pathFor(workspaceId) {
1683
1732
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
1684
1733
  }
1685
1734
  function readCursor(workspaceId) {
1686
- const path3 = pathFor(workspaceId);
1687
- if (!existsSync5(path3)) return null;
1688
- const raw = readFileSync4(path3, "utf8").trim();
1735
+ const path = pathFor(workspaceId);
1736
+ if (!existsSync5(path)) return null;
1737
+ const raw = readFileSync4(path, "utf8").trim();
1689
1738
  return raw.length > 0 ? raw : null;
1690
1739
  }
1691
1740
  function writeCursor(workspaceId, eventId) {
1692
- const path3 = pathFor(workspaceId);
1741
+ const path = pathFor(workspaceId);
1693
1742
  mkdirSync5(join7(cabaneDir(), "cursors"), { recursive: true });
1694
- writeFileSync4(path3, eventId + "\n", "utf8");
1743
+ writeFileSync4(path, eventId + "\n", "utf8");
1695
1744
  }
1696
1745
 
1697
1746
  // src/cursor-tracker.ts
@@ -1744,10 +1793,10 @@ function pathFor2(log, workspaceId) {
1744
1793
  return join8(dir(log), encodeURIComponent(workspaceId));
1745
1794
  }
1746
1795
  function readIds(log, workspaceId) {
1747
- const path3 = pathFor2(log, workspaceId);
1748
- if (!existsSync6(path3)) return [];
1796
+ const path = pathFor2(log, workspaceId);
1797
+ if (!existsSync6(path)) return [];
1749
1798
  try {
1750
- return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
1799
+ return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
1751
1800
  } catch {
1752
1801
  return [];
1753
1802
  }
@@ -1784,10 +1833,10 @@ function resumePathFor(workspaceId) {
1784
1833
  }
1785
1834
  function readResumeCounts(workspaceId) {
1786
1835
  const out = /* @__PURE__ */ new Map();
1787
- const path3 = resumePathFor(workspaceId);
1788
- if (!existsSync6(path3)) return out;
1836
+ const path = resumePathFor(workspaceId);
1837
+ if (!existsSync6(path)) return out;
1789
1838
  try {
1790
- for (const line of readFileSync5(path3, "utf8").split("\n")) {
1839
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
1791
1840
  const trimmed = line.trim();
1792
1841
  if (!trimmed) continue;
1793
1842
  const tab = trimmed.lastIndexOf(" ");
@@ -4725,536 +4774,10 @@ function isStringRecord2(v) {
4725
4774
  return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
4726
4775
  }
4727
4776
 
4728
- // node_modules/.pnpm/@openai+codex-sdk@0.147.0/node_modules/@openai/codex-sdk/dist/index.js
4729
- import { promises as fs } from "fs";
4730
- import os from "os";
4731
- import path from "path";
4732
- import { spawn as spawn4 } from "child_process";
4733
- import { statSync } from "fs";
4734
- import path2 from "path";
4735
- import readline from "readline";
4736
- import { createRequire } from "module";
4737
- async function createOutputSchemaFile(schema) {
4738
- if (schema === void 0) {
4739
- return { cleanup: async () => {
4740
- } };
4741
- }
4742
- if (!isJsonObject(schema)) {
4743
- throw new Error("outputSchema must be a plain JSON object");
4744
- }
4745
- const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
4746
- const schemaPath = path.join(schemaDir, "schema.json");
4747
- const cleanup = async () => {
4748
- try {
4749
- await fs.rm(schemaDir, { recursive: true, force: true });
4750
- } catch {
4751
- }
4752
- };
4753
- try {
4754
- await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
4755
- return { schemaPath, cleanup };
4756
- } catch (error) {
4757
- await cleanup();
4758
- throw error;
4759
- }
4760
- }
4761
- function isJsonObject(value) {
4762
- return typeof value === "object" && value !== null && !Array.isArray(value);
4763
- }
4764
- var Thread = class {
4765
- _exec;
4766
- _options;
4767
- _id;
4768
- _threadOptions;
4769
- /** Returns the ID of the thread. Populated after the first turn starts. */
4770
- get id() {
4771
- return this._id;
4772
- }
4773
- /* @internal */
4774
- constructor(exec, options, threadOptions, id = null) {
4775
- this._exec = exec;
4776
- this._options = options;
4777
- this._id = id;
4778
- this._threadOptions = threadOptions;
4779
- }
4780
- /** Provides the input to the agent and streams events as they are produced during the turn. */
4781
- async runStreamed(input, turnOptions = {}) {
4782
- return { events: this.runStreamedInternal(input, turnOptions) };
4783
- }
4784
- async *runStreamedInternal(input, turnOptions = {}) {
4785
- const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
4786
- const options = this._threadOptions;
4787
- const { prompt, images } = normalizeInput(input);
4788
- const generator = this._exec.run({
4789
- input: prompt,
4790
- baseUrl: this._options.baseUrl,
4791
- apiKey: this._options.apiKey,
4792
- threadId: this._id,
4793
- images,
4794
- model: options?.model,
4795
- sandboxMode: options?.sandboxMode,
4796
- workingDirectory: options?.workingDirectory,
4797
- skipGitRepoCheck: options?.skipGitRepoCheck,
4798
- outputSchemaFile: schemaPath,
4799
- modelReasoningEffort: options?.modelReasoningEffort,
4800
- signal: turnOptions.signal,
4801
- networkAccessEnabled: options?.networkAccessEnabled,
4802
- webSearchMode: options?.webSearchMode,
4803
- webSearchEnabled: options?.webSearchEnabled,
4804
- approvalPolicy: options?.approvalPolicy,
4805
- additionalDirectories: options?.additionalDirectories
4806
- });
4807
- try {
4808
- for await (const item of generator) {
4809
- let parsed;
4810
- try {
4811
- parsed = JSON.parse(item);
4812
- } catch (error) {
4813
- throw new Error(`Failed to parse item: ${item}`, { cause: error });
4814
- }
4815
- if (parsed.type === "thread.started") {
4816
- this._id = parsed.thread_id;
4817
- } else if (parsed.type === "turn.completed") {
4818
- parsed.usage.cache_write_input_tokens ??= 0;
4819
- }
4820
- yield parsed;
4821
- }
4822
- } finally {
4823
- await cleanup();
4824
- }
4825
- }
4826
- /** Provides the input to the agent and returns the completed turn. */
4827
- async run(input, turnOptions = {}) {
4828
- const generator = this.runStreamedInternal(input, turnOptions);
4829
- const items = [];
4830
- let finalResponse = "";
4831
- let usage = null;
4832
- let turnFailure = null;
4833
- for await (const event of generator) {
4834
- if (event.type === "item.completed") {
4835
- if (event.item.type === "agent_message") {
4836
- finalResponse = event.item.text;
4837
- }
4838
- items.push(event.item);
4839
- } else if (event.type === "turn.completed") {
4840
- usage = event.usage;
4841
- } else if (event.type === "turn.failed") {
4842
- turnFailure = event.error;
4843
- break;
4844
- }
4845
- }
4846
- if (turnFailure) {
4847
- throw new Error(turnFailure.message);
4848
- }
4849
- return { items, finalResponse, usage };
4850
- }
4851
- };
4852
- function normalizeInput(input) {
4853
- if (typeof input === "string") {
4854
- return { prompt: input, images: [] };
4855
- }
4856
- const promptParts = [];
4857
- const images = [];
4858
- for (const item of input) {
4859
- if (item.type === "text") {
4860
- promptParts.push(item.text);
4861
- } else if (item.type === "local_image") {
4862
- images.push(item.path);
4863
- }
4864
- }
4865
- return { prompt: promptParts.join("\n\n"), images };
4866
- }
4867
- var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
4868
- var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
4869
- var CODEX_NPM_NAME = "@openai/codex";
4870
- var PLATFORM_PACKAGE_BY_TARGET = {
4871
- "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
4872
- "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
4873
- "x86_64-apple-darwin": "@openai/codex-darwin-x64",
4874
- "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
4875
- "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
4876
- "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
4877
- };
4878
- var moduleRequire = createRequire(import.meta.url);
4879
- var CodexExec = class {
4880
- executablePath;
4881
- pathDirs;
4882
- envOverride;
4883
- configOverrides;
4884
- constructor(executablePath = null, env, configOverrides) {
4885
- if (executablePath) {
4886
- this.executablePath = executablePath;
4887
- this.pathDirs = [];
4888
- } else {
4889
- const resolved = findCodexPath();
4890
- this.executablePath = resolved.executablePath;
4891
- this.pathDirs = resolved.pathDirs;
4892
- }
4893
- this.envOverride = env;
4894
- this.configOverrides = configOverrides;
4895
- }
4896
- async *run(args) {
4897
- const commandArgs = ["exec", "--experimental-json"];
4898
- if (this.configOverrides) {
4899
- for (const override of serializeConfigOverrides(this.configOverrides)) {
4900
- commandArgs.push("--config", override);
4901
- }
4902
- }
4903
- if (args.baseUrl) {
4904
- commandArgs.push(
4905
- "--config",
4906
- `openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
4907
- );
4908
- }
4909
- if (args.model) {
4910
- commandArgs.push("--model", args.model);
4911
- }
4912
- if (args.sandboxMode) {
4913
- commandArgs.push("--sandbox", args.sandboxMode);
4914
- }
4915
- if (args.workingDirectory) {
4916
- commandArgs.push("--cd", args.workingDirectory);
4917
- }
4918
- if (args.additionalDirectories?.length) {
4919
- for (const dir2 of args.additionalDirectories) {
4920
- commandArgs.push("--add-dir", dir2);
4921
- }
4922
- }
4923
- if (args.skipGitRepoCheck) {
4924
- commandArgs.push("--skip-git-repo-check");
4925
- }
4926
- if (args.outputSchemaFile) {
4927
- commandArgs.push("--output-schema", args.outputSchemaFile);
4928
- }
4929
- if (args.modelReasoningEffort) {
4930
- commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
4931
- }
4932
- if (args.networkAccessEnabled !== void 0) {
4933
- commandArgs.push(
4934
- "--config",
4935
- `sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
4936
- );
4937
- }
4938
- if (args.webSearchMode) {
4939
- commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
4940
- } else if (args.webSearchEnabled === true) {
4941
- commandArgs.push("--config", `web_search="live"`);
4942
- } else if (args.webSearchEnabled === false) {
4943
- commandArgs.push("--config", `web_search="disabled"`);
4944
- }
4945
- if (args.approvalPolicy) {
4946
- commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
4947
- }
4948
- if (args.threadId) {
4949
- commandArgs.push("resume", args.threadId);
4950
- }
4951
- if (args.images?.length) {
4952
- for (const image of args.images) {
4953
- commandArgs.push("--image", image);
4954
- }
4955
- }
4956
- const env = {};
4957
- if (this.envOverride) {
4958
- Object.assign(env, this.envOverride);
4959
- } else {
4960
- for (const [key, value] of Object.entries(process.env)) {
4961
- if (value !== void 0) {
4962
- env[key] = value;
4963
- }
4964
- }
4965
- }
4966
- if (!env[INTERNAL_ORIGINATOR_ENV]) {
4967
- env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
4968
- }
4969
- if (args.apiKey) {
4970
- env.CODEX_API_KEY = args.apiKey;
4971
- }
4972
- if (this.pathDirs.length > 0) {
4973
- prependPathDirs(env, this.pathDirs);
4974
- }
4975
- const child = spawn4(this.executablePath, commandArgs, {
4976
- env,
4977
- signal: args.signal
4978
- });
4979
- let spawnError = null;
4980
- child.once("error", (err) => spawnError = err);
4981
- if (!child.stdin) {
4982
- child.kill();
4983
- throw new Error("Child process has no stdin");
4984
- }
4985
- child.stdin.write(args.input);
4986
- child.stdin.end();
4987
- if (!child.stdout) {
4988
- child.kill();
4989
- throw new Error("Child process has no stdout");
4990
- }
4991
- const stderrChunks = [];
4992
- if (child.stderr) {
4993
- child.stderr.on("data", (data) => {
4994
- stderrChunks.push(data);
4995
- });
4996
- }
4997
- const exitPromise = new Promise(
4998
- (resolve) => {
4999
- child.once("exit", (code, signal) => {
5000
- resolve({ code, signal });
5001
- });
5002
- }
5003
- );
5004
- const rl = readline.createInterface({
5005
- input: child.stdout,
5006
- crlfDelay: Infinity
5007
- });
5008
- try {
5009
- for await (const line of rl) {
5010
- yield line;
5011
- }
5012
- if (spawnError) throw spawnError;
5013
- const { code, signal } = await exitPromise;
5014
- if (code !== 0 || signal) {
5015
- const stderrBuffer = Buffer.concat(stderrChunks);
5016
- const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
5017
- throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
5018
- }
5019
- } finally {
5020
- rl.close();
5021
- child.removeAllListeners();
5022
- try {
5023
- if (!child.killed) child.kill();
5024
- } catch {
5025
- }
5026
- }
5027
- }
5028
- };
5029
- function serializeConfigOverrides(configOverrides) {
5030
- const overrides = [];
5031
- flattenConfigOverrides(configOverrides, "", overrides);
5032
- return overrides;
5033
- }
5034
- function flattenConfigOverrides(value, prefix, overrides) {
5035
- if (!isPlainObject(value)) {
5036
- if (prefix) {
5037
- overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
5038
- return;
5039
- } else {
5040
- throw new Error("Codex config overrides must be a plain object");
5041
- }
5042
- }
5043
- const entries = Object.entries(value);
5044
- if (!prefix && entries.length === 0) {
5045
- return;
5046
- }
5047
- if (prefix && entries.length === 0) {
5048
- overrides.push(`${prefix}={}`);
5049
- return;
5050
- }
5051
- for (const [key, child] of entries) {
5052
- if (!key) {
5053
- throw new Error("Codex config override keys must be non-empty strings");
5054
- }
5055
- if (child === void 0) {
5056
- continue;
5057
- }
5058
- const path3 = prefix ? `${prefix}.${key}` : key;
5059
- if (isPlainObject(child)) {
5060
- flattenConfigOverrides(child, path3, overrides);
5061
- } else {
5062
- overrides.push(`${path3}=${toTomlValue(child, path3)}`);
5063
- }
5064
- }
5065
- }
5066
- function toTomlValue(value, path3) {
5067
- if (typeof value === "string") {
5068
- return JSON.stringify(value);
5069
- } else if (typeof value === "number") {
5070
- if (!Number.isFinite(value)) {
5071
- throw new Error(`Codex config override at ${path3} must be a finite number`);
5072
- }
5073
- return `${value}`;
5074
- } else if (typeof value === "boolean") {
5075
- return value ? "true" : "false";
5076
- } else if (Array.isArray(value)) {
5077
- const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
5078
- return `[${rendered.join(", ")}]`;
5079
- } else if (isPlainObject(value)) {
5080
- const parts = [];
5081
- for (const [key, child] of Object.entries(value)) {
5082
- if (!key) {
5083
- throw new Error("Codex config override keys must be non-empty strings");
5084
- }
5085
- if (child === void 0) {
5086
- continue;
5087
- }
5088
- parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
5089
- }
5090
- return `{${parts.join(", ")}}`;
5091
- } else if (value === null) {
5092
- throw new Error(`Codex config override at ${path3} cannot be null`);
5093
- } else {
5094
- const typeName = typeof value;
5095
- throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
5096
- }
5097
- }
5098
- var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
5099
- function formatTomlKey(key) {
5100
- return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
5101
- }
5102
- function isPlainObject(value) {
5103
- return typeof value === "object" && value !== null && !Array.isArray(value);
5104
- }
5105
- function findCodexPath() {
5106
- const { platform, arch } = process;
5107
- let targetTriple = null;
5108
- switch (platform) {
5109
- case "linux":
5110
- case "android":
5111
- switch (arch) {
5112
- case "x64":
5113
- targetTriple = "x86_64-unknown-linux-musl";
5114
- break;
5115
- case "arm64":
5116
- targetTriple = "aarch64-unknown-linux-musl";
5117
- break;
5118
- default:
5119
- break;
5120
- }
5121
- break;
5122
- case "darwin":
5123
- switch (arch) {
5124
- case "x64":
5125
- targetTriple = "x86_64-apple-darwin";
5126
- break;
5127
- case "arm64":
5128
- targetTriple = "aarch64-apple-darwin";
5129
- break;
5130
- default:
5131
- break;
5132
- }
5133
- break;
5134
- case "win32":
5135
- switch (arch) {
5136
- case "x64":
5137
- targetTriple = "x86_64-pc-windows-msvc";
5138
- break;
5139
- case "arm64":
5140
- targetTriple = "aarch64-pc-windows-msvc";
5141
- break;
5142
- default:
5143
- break;
5144
- }
5145
- break;
5146
- default:
5147
- break;
5148
- }
5149
- if (!targetTriple) {
5150
- throw new Error(`Unsupported platform: ${platform} (${arch})`);
5151
- }
5152
- const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
5153
- if (!platformPackage) {
5154
- throw new Error(`Unsupported target triple: ${targetTriple}`);
5155
- }
5156
- let vendorRoot;
5157
- try {
5158
- const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
5159
- const codexRequire = createRequire(codexPackageJsonPath);
5160
- const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
5161
- vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
5162
- } catch {
5163
- throw new Error(
5164
- `Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5165
- );
5166
- }
5167
- const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
5168
- const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
5169
- if (!nativePackage) {
5170
- throw new Error(
5171
- `Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5172
- );
5173
- }
5174
- return nativePackage;
5175
- }
5176
- function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
5177
- const packageRoot = path2.join(vendorRoot, targetTriple);
5178
- const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
5179
- if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
5180
- return {
5181
- executablePath: packageBinaryPath,
5182
- pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
5183
- };
5184
- }
5185
- const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
5186
- if (isFile(legacyBinaryPath)) {
5187
- return {
5188
- executablePath: legacyBinaryPath,
5189
- pathDirs: existingDirs(path2.join(packageRoot, "path"))
5190
- };
5191
- }
5192
- return null;
5193
- }
5194
- function existingDirs(...dirs) {
5195
- return dirs.filter(isDirectory);
5196
- }
5197
- function prependPathDirs(env, pathDirs, platform = process.platform) {
5198
- const pathKey = pathEnvKey(env, platform);
5199
- if (platform === "win32") {
5200
- for (const key of Object.keys(env)) {
5201
- if (key.toLowerCase() === "path" && key !== pathKey) {
5202
- delete env[key];
5203
- }
5204
- }
5205
- }
5206
- const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
5207
- env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
5208
- }
5209
- function pathEnvKey(env, platform) {
5210
- if (platform !== "win32") {
5211
- return "PATH";
5212
- }
5213
- const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
5214
- return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
5215
- }
5216
- function isFile(filePath) {
5217
- try {
5218
- return statSync(filePath).isFile();
5219
- } catch {
5220
- return false;
5221
- }
5222
- }
5223
- function isDirectory(filePath) {
5224
- try {
5225
- return statSync(filePath).isDirectory();
5226
- } catch {
5227
- return false;
5228
- }
5229
- }
5230
- var Codex = class {
5231
- exec;
5232
- options;
5233
- constructor(options = {}) {
5234
- const { codexPathOverride, env, config } = options;
5235
- this.exec = new CodexExec(codexPathOverride, env, config);
5236
- this.options = options;
5237
- }
5238
- /**
5239
- * Starts a new conversation with an agent.
5240
- * @returns A new thread instance.
5241
- */
5242
- startThread(options = {}) {
5243
- return new Thread(this.exec, this.options, options);
5244
- }
5245
- /**
5246
- * Resumes a conversation with an agent based on the thread id.
5247
- * Threads are persisted in ~/.codex/sessions.
5248
- *
5249
- * @param id The id of the thread to resume.
5250
- * @returns A new thread instance.
5251
- */
5252
- resumeThread(id, options = {}) {
5253
- return new Thread(this.exec, this.options, options, id);
5254
- }
5255
- };
5256
-
5257
4777
  // packages/agent-runtime/src/codex/transport.ts
4778
+ import {
4779
+ Codex
4780
+ } from "@openai/codex-sdk";
5258
4781
  function buildSdkThreadOptions(spec) {
5259
4782
  return {
5260
4783
  ...spec.model ? { model: spec.model } : {},
@@ -6046,7 +5569,7 @@ var ConnectorHealthStore = class {
6046
5569
 
6047
5570
  // src/dispatcher.ts
6048
5571
  import { randomUUID } from "crypto";
6049
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync as statSync2 } from "fs";
5572
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync } from "fs";
6050
5573
  import { join as join13 } from "path";
6051
5574
 
6052
5575
  // src/summon.ts
@@ -6384,10 +5907,10 @@ import { join as join9 } from "path";
6384
5907
  var PREFIX = "cabane-codex-instructions-";
6385
5908
  async function writeCodexInstructionsFile(contents) {
6386
5909
  const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6387
- const path3 = join9(dir2, "instructions.md");
6388
- await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
5910
+ const path = join9(dir2, "instructions.md");
5911
+ await writeFile(path, contents, { encoding: "utf8", mode: 384 });
6389
5912
  return {
6390
- path: path3,
5913
+ path,
6391
5914
  cleanup: async () => {
6392
5915
  await rm(dir2, { recursive: true, force: true });
6393
5916
  }
@@ -6407,10 +5930,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
6407
5930
  return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6408
5931
  }
6409
5932
  function readPrepared(workspaceId, conversationId, agentId) {
6410
- const path3 = pathFor3(workspaceId, conversationId, agentId);
6411
- if (!existsSync7(path3)) return null;
5933
+ const path = pathFor3(workspaceId, conversationId, agentId);
5934
+ if (!existsSync7(path)) return null;
6412
5935
  try {
6413
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
5936
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
6414
5937
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
6415
5938
  return {
6416
5939
  cwd: parsed.cwd,
@@ -6444,14 +5967,14 @@ function secretsPath() {
6444
5967
  }
6445
5968
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6446
5969
  function loadSecretStore() {
6447
- const path3 = secretsPath();
6448
- if (!existsSync8(path3)) return makeStore({});
5970
+ const path = secretsPath();
5971
+ if (!existsSync8(path)) return makeStore({});
6449
5972
  let raw;
6450
5973
  try {
6451
- raw = readFileSync7(path3, "utf8");
5974
+ raw = readFileSync7(path, "utf8");
6452
5975
  } catch (err) {
6453
5976
  throw new ConfigError(
6454
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
5977
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
6455
5978
  );
6456
5979
  }
6457
5980
  if (raw.trim().length === 0) return makeStore({});
@@ -6460,13 +5983,13 @@ function loadSecretStore() {
6460
5983
  parsed = JSON.parse(raw);
6461
5984
  } catch (err) {
6462
5985
  throw new ConfigError(
6463
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
5986
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6464
5987
  );
6465
5988
  }
6466
5989
  const result = secretStoreSchema.safeParse(parsed);
6467
5990
  if (!result.success) {
6468
5991
  throw new ConfigError(
6469
- `${path3} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
5992
+ `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6470
5993
  );
6471
5994
  }
6472
5995
  return makeStore(result.data);
@@ -6902,7 +6425,7 @@ function checkoutState(cwd) {
6902
6425
  if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
6903
6426
  let stat;
6904
6427
  try {
6905
- stat = statSync2(gitPath);
6428
+ stat = statSync(gitPath);
6906
6429
  } catch (error) {
6907
6430
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
6908
6431
  }
@@ -7850,7 +7373,9 @@ var LABELS = {
7850
7373
  function deriveHarnessSnapshot(signals) {
7851
7374
  const advertised = new Set(
7852
7375
  buildCompanionManifest({
7853
- claudeCode: signals.claudeOnPath,
7376
+ // CT1082: connected AND installed — the manifest's own rule, restated here
7377
+ // through the same function rather than re-decided.
7378
+ claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
7854
7379
  opencode: signals.opencodeConfigured,
7855
7380
  codex: signals.codexEnabled
7856
7381
  }).runtimes.map((r) => r.name)
@@ -7863,20 +7388,40 @@ function deriveHarnessSnapshot(signals) {
7863
7388
  return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
7864
7389
  }
7865
7390
  function deriveClaudeCode(signals, manifestHas) {
7866
- const base = { runtime: "claude-code", label: LABELS["claude-code"], enable: null };
7391
+ const base = { runtime: "claude-code", label: LABELS["claude-code"] };
7867
7392
  if (manifestHas) {
7868
7393
  return {
7869
7394
  ...base,
7870
7395
  state: "exposed",
7871
7396
  version: signals.claudeVersion,
7872
- detail: "Claude Code is installed and exposed to Cabane."
7397
+ detail: "Claude Code is connected and exposed to Cabane.",
7398
+ enable: null
7399
+ };
7400
+ }
7401
+ if (signals.claudeCodeConnected) {
7402
+ return {
7403
+ ...base,
7404
+ state: "needs_attention",
7405
+ version: null,
7406
+ detail: "Connected, but the `claude` CLI isn\u2019t on your PATH. Install it (`npm i -g @anthropic-ai/claude-code`) and sign in, or disconnect it.",
7407
+ enable: null
7408
+ };
7409
+ }
7410
+ if (signals.claudeOnPath) {
7411
+ return {
7412
+ ...base,
7413
+ state: "detected_not_exposed",
7414
+ version: signals.claudeVersion,
7415
+ detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
7416
+ enable: "claude-code"
7873
7417
  };
7874
7418
  }
7875
7419
  return {
7876
7420
  ...base,
7877
7421
  state: "not_detected",
7878
7422
  version: null,
7879
- detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it appears here."
7423
+ detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
7424
+ enable: null
7880
7425
  };
7881
7426
  }
7882
7427
  function deriveCodex(signals, manifestHas) {
@@ -7944,6 +7489,9 @@ function deriveOpencode(signals, manifestHas) {
7944
7489
  enable: "opencode"
7945
7490
  };
7946
7491
  }
7492
+ function detectedRuntimesFor(snapshot) {
7493
+ return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
7494
+ }
7947
7495
  var PROBE_TIMEOUT_MS2 = 4e3;
7948
7496
  async function probeHarnessSignals(cfg, deps = {}) {
7949
7497
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -7960,6 +7508,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
7960
7508
  return {
7961
7509
  claudeOnPath: claudeOnPathResult,
7962
7510
  claudeVersion,
7511
+ // CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
7512
+ // the manifest gate and the probe above is only a suggestion.
7513
+ claudeCodeConnected: isClaudeCodeConnected(cfg),
7963
7514
  // A parseable `codex --version` is our presence signal (presence alone never
7964
7515
  // exposes codex; its config flag is the manifest gate either way).
7965
7516
  codexOnPath: codexVersion !== null,
@@ -8333,8 +7884,8 @@ function sleep2(ms) {
8333
7884
  }
8334
7885
 
8335
7886
  // src/version.ts
8336
- import { createRequire as createRequire2 } from "module";
8337
- var pkg = createRequire2(import.meta.url)("../package.json");
7887
+ import { createRequire } from "module";
7888
+ var pkg = createRequire(import.meta.url)("../package.json");
8338
7889
  var COMPANION_VERSION = pkg.version;
8339
7890
 
8340
7891
  // src/supervisor.ts
@@ -8368,6 +7919,10 @@ var CompanionSupervisor = class {
8368
7919
  // can never disagree with what the manifest advertises. Null until the first
8369
7920
  // probe (the heartbeat then falls back to the boot `claudeCode`).
8370
7921
  harnessSignals = null;
7922
+ // CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
7923
+ // NOT the cached beat signal — someone connecting right after installing Claude
7924
+ // Code shouldn't be refused by a snapshot up to a heartbeat old.
7925
+ probeClaudePresence;
8371
7926
  exitFn;
8372
7927
  reexecFn;
8373
7928
  dispatcherFactory;
@@ -8398,6 +7953,7 @@ var CompanionSupervisor = class {
8398
7953
  this.log = opts.log;
8399
7954
  this.hub = opts.hub;
8400
7955
  this.claudeCode = opts.claudeCode ?? true;
7956
+ this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
8401
7957
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
8402
7958
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
8403
7959
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -8460,11 +8016,10 @@ var CompanionSupervisor = class {
8460
8016
  // claude-code when `claude` is on PATH, CT270 opencode when the operator
8461
8017
  // configured an `opencode serve`.
8462
8018
  manifest: buildCompanionManifest({
8463
- // CT586: prefer the live re-probe's presence; fall back to the boot probe
8464
- // until the first re-probe lands. Same exit-0 `claude --version` signal
8465
- // either way, so the manifest's claude-code advertising is unchanged in
8466
- // meaning — just refreshed on the beat instead of frozen at boot.
8467
- claudeCode: this.claudeCodePresent(),
8019
+ // CT1082: connected AND installed. Presence is the live re-probe (CT586),
8020
+ // falling back to the boot probe until the first one lands; consent is the
8021
+ // user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
8022
+ claudeCode: this.claudeCodeOffered(),
8468
8023
  opencode: !!this.config.opencode?.serverUrl,
8469
8024
  // CT481: advertise codex when the operator enabled it (config-gated,
8470
8025
  // like opencode — the CLI's presence is the operator's responsibility;
@@ -8482,7 +8037,13 @@ var CompanionSupervisor = class {
8482
8037
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
8483
8038
  // A null (probe failed / no opencode) omits the field, and the server then
8484
8039
  // leaves this device's stored availability untouched.
8485
- ...opencodeModels !== null ? { models: opencodeModels } : {}
8040
+ ...opencodeModels !== null ? { models: opencodeModels } : {},
8041
+ // CT1082: what this machine has that the user hasn't connected, so the web
8042
+ // UI can offer it without a companion round-trip. Sent only once a probe has
8043
+ // actually landed (`harnessSignals` non-null) — an absent field means "we
8044
+ // didn't look this beat" and leaves the server's stored suggestion alone,
8045
+ // the same fail-soft contract `models` keeps.
8046
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8486
8047
  });
8487
8048
  this.hub.setDevice({ deviceId: res.deviceId });
8488
8049
  this.deviceId = res.deviceId;
@@ -8687,13 +8248,19 @@ var CompanionSupervisor = class {
8687
8248
  "companion: stopped agent (unassigned)"
8688
8249
  );
8689
8250
  }
8690
- // CT833: is Claude Code on this machine right now? The ONE signal both the
8691
- // heartbeat manifest and the dispatcher's adapter registry read, so what the
8692
- // device advertises and what it can actually select can't disagree. Live (the
8693
- // per-beat re-probe), falling back to the boot probe until the first one lands.
8251
+ // CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
8252
+ // falling back to the boot probe until the first one lands. Presence ONLY —
8253
+ // CT1082 split presence from exposure, so nothing routes on this directly.
8694
8254
  claudeCodePresent() {
8695
8255
  return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
8696
8256
  }
8257
+ // CT1082: does this device OFFER claude-code — connected by its user and actually
8258
+ // installed? The ONE signal both the heartbeat manifest and the dispatcher's
8259
+ // adapter registry read, so what the device advertises and what it can select
8260
+ // can't disagree (the CT833 invariant, now with consent in front of it).
8261
+ claudeCodeOffered() {
8262
+ return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
8263
+ }
8697
8264
  buildDispatcher(ctx) {
8698
8265
  if (this.dispatcherFactory) return this.dispatcherFactory(ctx);
8699
8266
  return new Dispatcher({
@@ -8711,10 +8278,12 @@ var CompanionSupervisor = class {
8711
8278
  }),
8712
8279
  runConfig: ctx.runConfig,
8713
8280
  log: this.log,
8714
- // CT833: register the claude-code adapter only when `claude` is actually on
8715
- // this machine — read per turn (not captured here), so a harness installed
8716
- // after boot works on the next turn exactly as it appears on the next beat.
8717
- claudeCodeAvailable: () => this.claudeCodePresent(),
8281
+ // CT833: register the claude-code adapter only when this device actually
8282
+ // offers claude-code — read per turn (not captured here), so a harness
8283
+ // installed or connected after boot works on the next turn exactly as it
8284
+ // appears on the next beat. CT1082: "offers" now means connected as well as
8285
+ // installed, so a disconnected harness can't be selected either.
8286
+ claudeCodeAvailable: () => this.claudeCodeOffered(),
8718
8287
  // CT270: the opencode server URL (operator-configured), when this device
8719
8288
  // offers the opencode runtime. Threaded so an opencode turn selects the
8720
8289
  // opencode adapter; unset leaves the device claude-code-only.
@@ -9020,9 +8589,12 @@ var CompanionSupervisor = class {
9020
8589
  async recheckHarnesses() {
9021
8590
  await this.refreshHarnessStatuses();
9022
8591
  }
9023
- // Friendly enable for the two config-driven harnesses — flip the flag the app
9024
- // owns in `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose
9025
- // ONLY: it never installs a binary and never drives a login (BYO — Decided).
8592
+ // Friendly enable for the config-driven harnesses — flip the flag the app owns in
8593
+ // `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
8594
+ // never installs a binary and never drives a login (BYO — Decided).
8595
+ // - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
8596
+ // on PATH. This is the callable the terminal offer ("We found Claude Code.
8597
+ // Connect it? [Y/n]") and the web pairing flow both wire to.
9026
8598
  // - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
9027
8599
  // - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
9028
8600
  // so we never advertise a serve that isn't there. An unreachable URL is a
@@ -9032,7 +8604,15 @@ var CompanionSupervisor = class {
9032
8604
  // reachable/valid input).
9033
8605
  async enableHarness(input) {
9034
8606
  let next;
9035
- if (input.runtime === "codex") {
8607
+ if (input.runtime === "claude-code") {
8608
+ if (!await this.probeClaudePresence()) {
8609
+ return {
8610
+ ok: false,
8611
+ error: "Couldn\u2019t find `claude` on this machine\u2019s PATH. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in, then connect it."
8612
+ };
8613
+ }
8614
+ next = { ...this.config, claudeCode: { enabled: true } };
8615
+ } else if (input.runtime === "codex") {
9036
8616
  next = { ...this.config, codex: { enabled: true } };
9037
8617
  } else {
9038
8618
  const serverUrl = input.serverUrl.trim();
@@ -9138,9 +8718,9 @@ var CompanionSupervisor = class {
9138
8718
  };
9139
8719
  function defaultReexec() {
9140
8720
  clearRuntimeState();
9141
- void import("child_process").then(({ spawn: spawn5 }) => {
8721
+ void import("child_process").then(({ spawn: spawn4 }) => {
9142
8722
  try {
9143
- const child = spawn5(process.execPath, process.argv.slice(1), {
8723
+ const child = spawn4(process.execPath, process.argv.slice(1), {
9144
8724
  stdio: "inherit",
9145
8725
  detached: false
9146
8726
  });
@@ -9224,8 +8804,8 @@ function recordCrash(rec) {
9224
8804
  }
9225
8805
  function clearCrash() {
9226
8806
  try {
9227
- const path3 = crashMarkerPath();
9228
- if (existsSync11(path3)) rmSync7(path3, { force: true });
8807
+ const path = crashMarkerPath();
8808
+ if (existsSync11(path)) rmSync7(path, { force: true });
9229
8809
  } catch {
9230
8810
  }
9231
8811
  }
@@ -9234,13 +8814,17 @@ function clearCrash() {
9234
8814
  async function createCompanionRuntime(opts = {}) {
9235
8815
  const log = getLogger();
9236
8816
  installProcessSafetyNet(log);
9237
- const probeClaude = opts.probeClaude ?? claudeOnPath;
9238
8817
  let cfg;
9239
8818
  let claudeCode;
9240
8819
  try {
9241
- cfg = requireConfig();
9242
- claudeCode = await probeClaude();
9243
- await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
8820
+ ({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
8821
+ ...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
8822
+ onMigrated: (migrated, onPath) => log.info(
8823
+ { claudeCode: isClaudeCodeConnected(migrated) },
8824
+ onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
8825
+ )
8826
+ }));
8827
+ await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
9244
8828
  } catch (err) {
9245
8829
  recordCrash({
9246
8830
  reason: err instanceof Error ? err.message : String(err),
@@ -9252,7 +8836,9 @@ async function createCompanionRuntime(opts = {}) {
9252
8836
  }
9253
8837
  if (cfg.logLevel) log.level = cfg.logLevel;
9254
8838
  const harnessVersions = await probeHarnessVersions({
9255
- claudeCode,
8839
+ // CT1082: versions are probed for the runtimes this device OFFERS, and
8840
+ // claude-code is offered only when connected as well as installed.
8841
+ claudeCode: claudeCode && isClaudeCodeConnected(cfg),
9256
8842
  opencodeServerUrl: cfg.opencode?.serverUrl,
9257
8843
  codex: isCodexEnabled(cfg)
9258
8844
  });