@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/cli.js CHANGED
@@ -295,6 +295,26 @@ var companionConfigSchema = z2.object({
295
295
  opencode: z2.object({
296
296
  serverUrl: z2.string().url()
297
297
  }).strict().optional(),
298
+ // CT1082: the Claude Code runtime, when the user connected it on this machine.
299
+ // The change this task exists for: Claude Code used to be the one harness a
300
+ // device exposed with no configuration — a `claude --version` exit-0 was taken as
301
+ // consent — and it is now the third config-driven harness, opted into exactly
302
+ // like codex. `claude` on PATH is still required (you can't run what isn't
303
+ // installed), but presence alone no longer exposes anything: the manifest
304
+ // advertises claude-code only when this block says so AND the binary is there.
305
+ //
306
+ // The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
307
+ // Absent means the config predates this task — a device whose user was never
308
+ // asked — and the migration grandfathers it on first start. So every config
309
+ // written from here on carries the block explicitly, including a freshly-paired
310
+ // one, which starts at `enabled: false`: a new device is connected to nothing
311
+ // until its user says otherwise.
312
+ //
313
+ // Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
314
+ // above — that's one agent's memory switch, this is the device's connected set.
315
+ claudeCode: z2.object({
316
+ enabled: z2.boolean().optional()
317
+ }).strict().optional(),
298
318
  // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
299
319
  // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
300
320
  // the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
@@ -311,19 +331,26 @@ var companionConfigSchema = z2.object({
311
331
  function isCodexEnabled(cfg) {
312
332
  return !!cfg.codex && cfg.codex.enabled !== false;
313
333
  }
334
+ function isClaudeCodeConnected(cfg) {
335
+ return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
336
+ }
337
+ function migrateConnectedHarnesses(cfg, claudeOnPath2) {
338
+ if (cfg.claudeCode !== void 0) return null;
339
+ return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
340
+ }
314
341
  function localAgentConfig(cfg, agent) {
315
342
  const map = cfg.agents ?? {};
316
343
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
317
344
  }
318
345
  function loadConfig() {
319
- const path3 = configPath();
320
- if (!existsSync(path3)) return null;
346
+ const path = configPath();
347
+ if (!existsSync(path)) return null;
321
348
  let raw;
322
349
  try {
323
- raw = readFileSync(path3, "utf8");
350
+ raw = readFileSync(path, "utf8");
324
351
  } catch (err) {
325
352
  throw new ConfigError(
326
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
353
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
327
354
  );
328
355
  }
329
356
  if (raw.trim().length === 0) return null;
@@ -332,7 +359,7 @@ function loadConfig() {
332
359
  parsed = JSON.parse(raw);
333
360
  } catch (err) {
334
361
  throw new ConfigError(
335
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
362
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
336
363
  );
337
364
  }
338
365
  const result = companionConfigSchema.safeParse(parsed);
@@ -340,43 +367,50 @@ function loadConfig() {
340
367
  const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
341
368
  if (agentIssue) {
342
369
  throw new ConfigError(
343
- `${path3}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
370
+ `${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
344
371
  );
345
372
  }
346
373
  throw new ConfigError(
347
- `${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.`
374
+ `${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.`
348
375
  );
349
376
  }
350
377
  return result.data;
351
378
  }
352
379
  function loadConfigTolerant() {
353
- const path3 = configPath();
380
+ const path = configPath();
354
381
  const empty = {};
355
- if (!existsSync(path3)) return { local: empty, note: null };
382
+ const fresh = { local: empty, note: null, hadPriorConfig: false };
383
+ if (!existsSync(path)) return fresh;
356
384
  let raw;
357
385
  try {
358
- raw = readFileSync(path3, "utf8");
386
+ raw = readFileSync(path, "utf8");
359
387
  } catch {
360
- return { local: empty, note: null };
388
+ return fresh;
361
389
  }
362
- if (raw.trim().length === 0) return { local: empty, note: null };
390
+ if (raw.trim().length === 0) return fresh;
363
391
  let parsed;
364
392
  try {
365
393
  parsed = JSON.parse(raw);
366
394
  } catch {
367
- return { local: empty, note: `${path3} was unreadable (invalid JSON) and has been reset.` };
395
+ return {
396
+ local: empty,
397
+ note: `${path} was unreadable (invalid JSON) and has been reset.`,
398
+ hadPriorConfig: true
399
+ };
368
400
  }
369
401
  const strict = companionConfigSchema.safeParse(parsed);
370
402
  if (strict.success) {
371
- const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
403
+ const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
372
404
  return {
373
405
  local: {
374
406
  ...agents2 !== void 0 ? { agents: agents2 } : {},
375
407
  ...dashboardPort !== void 0 ? { dashboardPort } : {},
376
408
  ...autoOpen !== void 0 ? { autoOpen } : {},
377
- ...logLevel !== void 0 ? { logLevel } : {}
409
+ ...logLevel !== void 0 ? { logLevel } : {},
410
+ ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
378
411
  },
379
- note: null
412
+ note: null,
413
+ hadPriorConfig: true
380
414
  };
381
415
  }
382
416
  const obj = parsed && typeof parsed === "object" ? parsed : {};
@@ -388,26 +422,29 @@ function loadConfigTolerant() {
388
422
  if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
389
423
  local.logLevel = obj.logLevel;
390
424
  }
425
+ const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
426
+ if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
391
427
  return {
392
428
  local,
393
- note: `the existing ${path3} was from an older or incompatible companion; re-pairing rewrote it.`
429
+ note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
430
+ hadPriorConfig: true
394
431
  };
395
432
  }
396
433
  function saveConfig(cfg) {
397
- const path3 = configPath();
398
- mkdirSync(dirname(path3), { recursive: true });
434
+ const path = configPath();
435
+ mkdirSync(dirname(path), { recursive: true });
399
436
  try {
400
437
  chmodSync(cabaneDir(), 448);
401
438
  } catch {
402
439
  }
403
- const tmp = `${path3}.${process.pid}.tmp`;
440
+ const tmp = `${path}.${process.pid}.tmp`;
404
441
  try {
405
442
  writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
406
443
  try {
407
444
  chmodSync(tmp, 384);
408
445
  } catch {
409
446
  }
410
- renameSync(tmp, path3);
447
+ renameSync(tmp, path);
411
448
  } catch (err) {
412
449
  try {
413
450
  rmSync(tmp, { force: true });
@@ -426,9 +463,9 @@ function requireConfig() {
426
463
  return cfg;
427
464
  }
428
465
  function deleteConfig() {
429
- const path3 = configPath();
430
- if (existsSync(path3)) {
431
- writeFileSync(path3, "", { mode: 384 });
466
+ const path = configPath();
467
+ if (existsSync(path)) {
468
+ writeFileSync(path, "", { mode: 384 });
432
469
  }
433
470
  }
434
471
 
@@ -462,8 +499,8 @@ function consoleMessageFormat(log, messageKey) {
462
499
  var cached = null;
463
500
  function getLogger() {
464
501
  if (cached) return cached;
465
- const path3 = companionLogPath();
466
- mkdirSync2(dirname2(path3), { recursive: true });
502
+ const path = companionLogPath();
503
+ mkdirSync2(dirname2(path), { recursive: true });
467
504
  const streams = [];
468
505
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
469
506
  const consoleStream = pretty({
@@ -473,7 +510,7 @@ function getLogger() {
473
510
  });
474
511
  streams.push({ level: "info", stream: consoleStream });
475
512
  }
476
- streams.push({ level: "debug", stream: createWriteStream(path3, { flags: "a" }) });
513
+ streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
477
514
  cached = pino({ level: "debug" }, pino.multistream(streams));
478
515
  return cached;
479
516
  }
@@ -592,26 +629,43 @@ async function codexOnPath() {
592
629
  ]);
593
630
  return version !== null;
594
631
  }
595
- async function ensureRuntimeAvailable(cfg, deps = {}) {
632
+ async function requireStartConfig(deps = {}) {
633
+ const requireCfg = deps.requireCfg ?? requireConfig;
634
+ const probeClaude = deps.probeClaude ?? claudeOnPath;
635
+ const save2 = deps.save ?? saveConfig;
636
+ const cfg = requireCfg();
637
+ const claudeOnPathResult = await probeClaude();
638
+ const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
639
+ if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
640
+ save2(migrated);
641
+ deps.onMigrated?.(migrated, claudeOnPathResult);
642
+ return { cfg: migrated, claudeOnPath: claudeOnPathResult };
643
+ }
644
+ async function warnAboutHarnessReadiness(cfg, deps = {}) {
596
645
  const probeClaude = deps.probeClaude ?? claudeOnPath;
597
646
  const probeCodex = deps.probeCodex ?? codexOnPath;
598
647
  const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
599
648
  `));
600
- if (await probeClaude()) return;
601
- const alternates = [
602
- ...cfg.opencode ? ["opencode"] : [],
603
- ...isCodexEnabled(cfg) ? ["codex"] : []
649
+ const connected = [
650
+ ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
651
+ ...isCodexEnabled(cfg) ? ["Codex"] : [],
652
+ ...cfg.opencode ? ["opencode"] : []
604
653
  ];
605
- if (alternates.length > 0) {
606
- const exposed = alternates.join(" + ");
607
- warn(
608
- `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.`
609
- );
654
+ if (connected.length > 0) {
655
+ if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
656
+ warn(
657
+ "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."
658
+ );
659
+ }
610
660
  return;
611
661
  }
612
- 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";
613
- throw new CompanionError(
614
- "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).'
662
+ const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
663
+ const installed = [
664
+ ...claudeInstalled ? ["Claude Code"] : [],
665
+ ...codexInstalled ? ["Codex"] : []
666
+ ];
667
+ warn(
668
+ "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.")
615
669
  );
616
670
  }
617
671
 
@@ -634,9 +688,9 @@ function serialize(state) {
634
688
  return JSON.stringify(state, null, 2) + "\n";
635
689
  }
636
690
  function writeRuntimeState(state) {
637
- const path3 = runtimePath();
691
+ const path = runtimePath();
638
692
  mkdirSync3(cabaneDir(), { recursive: true });
639
- writeFileSync2(path3, serialize(state), "utf8");
693
+ writeFileSync2(path, serialize(state), "utf8");
640
694
  }
641
695
  function acquireRuntimeState(state) {
642
696
  const live = readLiveRuntimeState();
@@ -656,15 +710,15 @@ function acquireRuntimeState(state) {
656
710
  return { acquired: true };
657
711
  }
658
712
  function clearRuntimeState() {
659
- const path3 = runtimePath();
660
- if (existsSync2(path3)) rmSync2(path3, { force: true });
713
+ const path = runtimePath();
714
+ if (existsSync2(path)) rmSync2(path, { force: true });
661
715
  }
662
716
  function readLiveRuntimeState() {
663
- const path3 = runtimePath();
664
- if (!existsSync2(path3)) return null;
717
+ const path = runtimePath();
718
+ if (!existsSync2(path)) return null;
665
719
  let parsed;
666
720
  try {
667
- parsed = JSON.parse(readFileSync2(path3, "utf8"));
721
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
668
722
  } catch {
669
723
  return null;
670
724
  }
@@ -709,15 +763,28 @@ function trimSlash(s) {
709
763
  var STARTUP_TIMEOUT_MS = 8e3;
710
764
  var POLL_INTERVAL_MS = 150;
711
765
  async function startDaemon(opts = {}, deps = {}) {
712
- const ensureRuntime = deps.ensureRuntime ?? ensureRuntimeAvailable;
713
- const requireCfg = deps.requireCfg ?? requireConfig;
766
+ const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
714
767
  const readState = deps.readState ?? readLiveRuntimeState;
715
768
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
716
769
  const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
717
770
  const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
718
771
  const now = deps.now ?? (() => Date.now());
719
- const cfg = requireCfg();
720
- await ensureRuntime(cfg);
772
+ const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
773
+ // Unset → `requireStartConfig`'s own `requireConfig`, the real one.
774
+ ...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
775
+ ...deps.probeClaude ? { probeClaude: deps.probeClaude } : {},
776
+ ...deps.save ? { save: deps.save } : {},
777
+ // The child won't log it (the block is written by then), and this process owns
778
+ // the user's terminal — so the one-time record is one line, here.
779
+ onMigrated: (_migrated, onPath) => {
780
+ if (onPath) {
781
+ process.stdout.write(
782
+ "Carried Claude Code over as a connected harness on this device \u2014 connectors are chosen now, not detected.\n"
783
+ );
784
+ }
785
+ }
786
+ });
787
+ await ensureRuntime(cfg, { probeClaude: async () => claudeOnPath2 });
721
788
  const existing = readState();
722
789
  if (existing) {
723
790
  if (await verify(existing) !== "stale") {
@@ -795,11 +862,11 @@ function credentialsPath() {
795
862
  }
796
863
  var credentialStoreSchema = z3.record(z3.string(), z3.string());
797
864
  function load() {
798
- const path3 = credentialsPath();
799
- if (!existsSync3(path3)) return {};
865
+ const path = credentialsPath();
866
+ if (!existsSync3(path)) return {};
800
867
  let raw;
801
868
  try {
802
- raw = readFileSync3(path3, "utf8");
869
+ raw = readFileSync3(path, "utf8");
803
870
  } catch {
804
871
  return {};
805
872
  }
@@ -812,20 +879,20 @@ function load() {
812
879
  }
813
880
  }
814
881
  function save(map) {
815
- const path3 = credentialsPath();
816
- mkdirSync5(dirname3(path3), { recursive: true });
882
+ const path = credentialsPath();
883
+ mkdirSync5(dirname3(path), { recursive: true });
817
884
  try {
818
885
  chmodSync2(cabaneDir(), 448);
819
886
  } catch {
820
887
  }
821
- const tmp = `${path3}.${process.pid}.tmp`;
888
+ const tmp = `${path}.${process.pid}.tmp`;
822
889
  try {
823
890
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
824
891
  try {
825
892
  chmodSync2(tmp, 384);
826
893
  } catch {
827
894
  }
828
- renameSync2(tmp, path3);
895
+ renameSync2(tmp, path);
829
896
  } catch (err) {
830
897
  try {
831
898
  rmSync3(tmp, { force: true });
@@ -857,8 +924,8 @@ function pruneCredentials(keepAgentIds) {
857
924
  return removed;
858
925
  }
859
926
  function clearCredentials() {
860
- const path3 = credentialsPath();
861
- if (existsSync3(path3)) writeFileSync3(path3, "", { mode: 384 });
927
+ const path = credentialsPath();
928
+ if (existsSync3(path)) writeFileSync3(path, "", { mode: 384 });
862
929
  }
863
930
 
864
931
  // src/commands/logout.ts
@@ -897,10 +964,10 @@ async function logout(opts = {}) {
897
964
  function trimBase(baseUrl) {
898
965
  return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
899
966
  }
900
- async function postJson(baseUrl, path3, body) {
967
+ async function postJson(baseUrl, path, body) {
901
968
  let res;
902
969
  try {
903
- res = await fetch(`${trimBase(baseUrl)}${path3}`, {
970
+ res = await fetch(`${trimBase(baseUrl)}${path}`, {
904
971
  method: "POST",
905
972
  headers: { "content-type": "application/json", accept: "application/json" },
906
973
  body: JSON.stringify(body)
@@ -920,7 +987,7 @@ async function postJson(baseUrl, path3, body) {
920
987
  }
921
988
  }
922
989
  if (res.status >= 400) {
923
- if (res.status === 404 && path3.endsWith("/code")) {
990
+ if (res.status === 404 && path.endsWith("/code")) {
924
991
  throw new CompanionError(
925
992
  `no device-flow pairing endpoint at ${baseUrl}. Either that server predates \`cabane-companion pair\`, or it isn't a Cabane API origin \u2014 the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
926
993
  );
@@ -992,8 +1059,16 @@ function resolvePairBaseUrl(server) {
992
1059
  return raw;
993
1060
  }
994
1061
  function writePairedConfig(paired) {
995
- const { local, note } = loadConfigTolerant();
1062
+ const { local, note, hadPriorConfig } = loadConfigTolerant();
996
1063
  const config = {
1064
+ // CT1082: a device pairs connected to NOTHING — `claudeCode: { enabled: false }`
1065
+ // written explicitly, not left absent, because absence is exactly what marks a
1066
+ // pre-CT1082 config for the grandfathering migration. Omit it and a brand-new
1067
+ // device gets auto-connected on its first start by the behaviour this task
1068
+ // removes; stamp it on a RE-pair and an upgrading user's working device gets
1069
+ // disconnected instead. So it's written only when there was no config here
1070
+ // before. `local` spreads after, so a re-pair keeps the answer already given.
1071
+ ...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
997
1072
  ...local,
998
1073
  baseUrl: paired.baseUrl,
999
1074
  deviceToken: paired.deviceToken,
@@ -1390,6 +1465,11 @@ function registerRoutes(app, deps) {
1390
1465
  app.post("/api/harnesses/enable", async (c) => {
1391
1466
  const body = await readJson(c);
1392
1467
  const runtime = body.runtime;
1468
+ if (runtime === "claude-code") {
1469
+ const result = await supervisor.enableHarness({ runtime: "claude-code" });
1470
+ if (!result.ok) return c.json({ error: result.error }, 400);
1471
+ return c.json({ ok: true, status: hub.statusJson() });
1472
+ }
1393
1473
  if (runtime === "codex") {
1394
1474
  const result = await supervisor.enableHarness({ runtime: "codex" });
1395
1475
  if (!result.ok) return c.json({ error: result.error }, 400);
@@ -1458,12 +1538,12 @@ function clampLimit(raw, fallback, max = 200) {
1458
1538
  if (!Number.isFinite(n) || n <= 0) return fallback;
1459
1539
  return Math.min(Math.floor(n), max);
1460
1540
  }
1461
- function tailFile(path3, lines) {
1462
- if (!existsSync4(path3)) return [];
1541
+ function tailFile(path, lines) {
1542
+ if (!existsSync4(path)) return [];
1463
1543
  const MAX_BYTES = 256 * 1024;
1464
1544
  let fd;
1465
1545
  try {
1466
- fd = openSync3(path3, "r");
1546
+ fd = openSync3(path, "r");
1467
1547
  const size = fstatSync(fd).size;
1468
1548
  const start2 = Math.max(0, size - MAX_BYTES);
1469
1549
  const len = size - start2;
@@ -1574,8 +1654,8 @@ var CabaneApi = class {
1574
1654
  // One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
1575
1655
  // rethrows transport errors (fetch reject) unchanged so the caller's retry
1576
1656
  // logic can classify them.
1577
- async attempt(method, path3, body, signal) {
1578
- const res = await fetch(`${this.base}${path3}`, {
1657
+ async attempt(method, path, body, signal) {
1658
+ const res = await fetch(`${this.base}${path}`, {
1579
1659
  method,
1580
1660
  headers: {
1581
1661
  Authorization: `Bearer ${this.opts.token}`,
@@ -1601,12 +1681,12 @@ var CabaneApi = class {
1601
1681
  }
1602
1682
  return parsed;
1603
1683
  }
1604
- async request(method, path3, body, opts = {}) {
1684
+ async request(method, path, body, opts = {}) {
1605
1685
  const { signal, retry = false } = opts;
1606
1686
  const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
1607
1687
  for (let attempt = 1; ; attempt++) {
1608
1688
  try {
1609
- return await this.attempt(method, path3, body, signal);
1689
+ return await this.attempt(method, path, body, signal);
1610
1690
  } catch (err) {
1611
1691
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1612
1692
  await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
@@ -1632,9 +1712,9 @@ var CabaneApi = class {
1632
1712
  // delivers it once the API returns. `(turnId, seq)` is the server's
1633
1713
  // idempotency key, so a replay whose original POST's fate is unknown
1634
1714
  // converges instead of duplicating.
1635
- async durableCommit(kind, path3, body, turnId, seq, signal) {
1715
+ async durableCommit(kind, path, body, turnId, seq, signal) {
1636
1716
  try {
1637
- await this.request("POST", path3, body, {
1717
+ await this.request("POST", path, body, {
1638
1718
  retry: true,
1639
1719
  ...signal ? { signal } : {}
1640
1720
  });
@@ -1643,7 +1723,7 @@ var CabaneApi = class {
1643
1723
  if (!outbox) throw err;
1644
1724
  if (signal?.aborted || isAbortError(err)) throw err;
1645
1725
  if (!isRetryable(err)) throw err;
1646
- outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1726
+ outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
1647
1727
  this.opts.log?.warn(
1648
1728
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1649
1729
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1795,12 +1875,12 @@ var CabaneApi = class {
1795
1875
  // left best-effort: it's lower-stakes and self-heals on the next turn, so it
1796
1876
  // stays a single-shot PATCH and is deliberately out of CT93's scope.
1797
1877
  setActiveRun(workspaceId, conversationId, agentId, body) {
1798
- const path3 = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1878
+ const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1799
1879
  const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
1800
1880
  if (touchesFlag && this.opts.outbox) {
1801
- return this.durableActiveRunWrite(path3, conversationId, agentId, body);
1881
+ return this.durableActiveRunWrite(path, conversationId, agentId, body);
1802
1882
  }
1803
- return this.request("PATCH", path3, body);
1883
+ return this.request("PATCH", path, body);
1804
1884
  }
1805
1885
  // CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
1806
1886
  // across the (conversation, agent) pair. Mirrors `durableCommit`, with two
@@ -1814,11 +1894,11 @@ var CabaneApi = class {
1814
1894
  // later and clobber the state we just wrote (the cross-turn race: turn N's
1815
1895
  // queued clear vs. turn N+1's live set). Combined with persist-overwrites-
1816
1896
  // by-key, this is the full last-writer-wins guarantee.
1817
- async durableActiveRunWrite(path3, conversationId, agentId, body) {
1897
+ async durableActiveRunWrite(path, conversationId, agentId, body) {
1818
1898
  const outbox = this.opts.outbox;
1819
1899
  const key = activeRunOutboxKey(conversationId, agentId);
1820
1900
  try {
1821
- await this.request("PATCH", path3, body, { retry: true });
1901
+ await this.request("PATCH", path, body, { retry: true });
1822
1902
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1823
1903
  } catch (err) {
1824
1904
  if (!outbox) throw err;
@@ -1831,7 +1911,7 @@ var CabaneApi = class {
1831
1911
  turnId: key,
1832
1912
  seq: ACTIVE_RUN_OUTBOX_SEQ,
1833
1913
  method: "PATCH",
1834
- path: path3,
1914
+ path,
1835
1915
  body,
1836
1916
  kind: "active-run"
1837
1917
  });
@@ -1919,8 +1999,8 @@ var CabaneApi = class {
1919
1999
  // shared resolver the in-app path uses. Omitting it (older call sites) returns
1920
2000
  // the agent default — graceful degradation, no version coupling.
1921
2001
  getAgentSelf(conversationId) {
1922
- const path3 = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1923
- return this.request("GET", path3);
2002
+ const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
2003
+ return this.request("GET", path);
1924
2004
  }
1925
2005
  // The companion fetches the triggering message body by listing the
1926
2006
  // conversation's messages and finding the one with `id === messageId`.
@@ -1975,8 +2055,8 @@ var DeviceApi = class {
1975
2055
  get base() {
1976
2056
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
1977
2057
  }
1978
- async request(method, path3, body) {
1979
- const res = await fetch(`${this.base}${path3}`, {
2058
+ async request(method, path, body) {
2059
+ const res = await fetch(`${this.base}${path}`, {
1980
2060
  method,
1981
2061
  headers: {
1982
2062
  Authorization: `Bearer ${this.opts.deviceToken}`,
@@ -2030,15 +2110,15 @@ function pathFor(workspaceId) {
2030
2110
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
2031
2111
  }
2032
2112
  function readCursor(workspaceId) {
2033
- const path3 = pathFor(workspaceId);
2034
- if (!existsSync5(path3)) return null;
2035
- const raw = readFileSync4(path3, "utf8").trim();
2113
+ const path = pathFor(workspaceId);
2114
+ if (!existsSync5(path)) return null;
2115
+ const raw = readFileSync4(path, "utf8").trim();
2036
2116
  return raw.length > 0 ? raw : null;
2037
2117
  }
2038
2118
  function writeCursor(workspaceId, eventId) {
2039
- const path3 = pathFor(workspaceId);
2119
+ const path = pathFor(workspaceId);
2040
2120
  mkdirSync6(join7(cabaneDir(), "cursors"), { recursive: true });
2041
- writeFileSync4(path3, eventId + "\n", "utf8");
2121
+ writeFileSync4(path, eventId + "\n", "utf8");
2042
2122
  }
2043
2123
 
2044
2124
  // src/cursor-tracker.ts
@@ -2091,10 +2171,10 @@ function pathFor2(log, workspaceId) {
2091
2171
  return join8(dir(log), encodeURIComponent(workspaceId));
2092
2172
  }
2093
2173
  function readIds(log, workspaceId) {
2094
- const path3 = pathFor2(log, workspaceId);
2095
- if (!existsSync6(path3)) return [];
2174
+ const path = pathFor2(log, workspaceId);
2175
+ if (!existsSync6(path)) return [];
2096
2176
  try {
2097
- return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2177
+ return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2098
2178
  } catch {
2099
2179
  return [];
2100
2180
  }
@@ -2131,10 +2211,10 @@ function resumePathFor(workspaceId) {
2131
2211
  }
2132
2212
  function readResumeCounts(workspaceId) {
2133
2213
  const out = /* @__PURE__ */ new Map();
2134
- const path3 = resumePathFor(workspaceId);
2135
- if (!existsSync6(path3)) return out;
2214
+ const path = resumePathFor(workspaceId);
2215
+ if (!existsSync6(path)) return out;
2136
2216
  try {
2137
- for (const line of readFileSync5(path3, "utf8").split("\n")) {
2217
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
2138
2218
  const trimmed = line.trim();
2139
2219
  if (!trimmed) continue;
2140
2220
  const tab = trimmed.lastIndexOf(" ");
@@ -5072,536 +5152,10 @@ function isStringRecord2(v) {
5072
5152
  return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
5073
5153
  }
5074
5154
 
5075
- // node_modules/.pnpm/@openai+codex-sdk@0.147.0/node_modules/@openai/codex-sdk/dist/index.js
5076
- import { promises as fs } from "fs";
5077
- import os from "os";
5078
- import path from "path";
5079
- import { spawn as spawn6 } from "child_process";
5080
- import { statSync } from "fs";
5081
- import path2 from "path";
5082
- import readline from "readline";
5083
- import { createRequire } from "module";
5084
- async function createOutputSchemaFile(schema) {
5085
- if (schema === void 0) {
5086
- return { cleanup: async () => {
5087
- } };
5088
- }
5089
- if (!isJsonObject(schema)) {
5090
- throw new Error("outputSchema must be a plain JSON object");
5091
- }
5092
- const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
5093
- const schemaPath = path.join(schemaDir, "schema.json");
5094
- const cleanup = async () => {
5095
- try {
5096
- await fs.rm(schemaDir, { recursive: true, force: true });
5097
- } catch {
5098
- }
5099
- };
5100
- try {
5101
- await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
5102
- return { schemaPath, cleanup };
5103
- } catch (error) {
5104
- await cleanup();
5105
- throw error;
5106
- }
5107
- }
5108
- function isJsonObject(value) {
5109
- return typeof value === "object" && value !== null && !Array.isArray(value);
5110
- }
5111
- var Thread = class {
5112
- _exec;
5113
- _options;
5114
- _id;
5115
- _threadOptions;
5116
- /** Returns the ID of the thread. Populated after the first turn starts. */
5117
- get id() {
5118
- return this._id;
5119
- }
5120
- /* @internal */
5121
- constructor(exec, options, threadOptions, id = null) {
5122
- this._exec = exec;
5123
- this._options = options;
5124
- this._id = id;
5125
- this._threadOptions = threadOptions;
5126
- }
5127
- /** Provides the input to the agent and streams events as they are produced during the turn. */
5128
- async runStreamed(input, turnOptions = {}) {
5129
- return { events: this.runStreamedInternal(input, turnOptions) };
5130
- }
5131
- async *runStreamedInternal(input, turnOptions = {}) {
5132
- const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
5133
- const options = this._threadOptions;
5134
- const { prompt, images } = normalizeInput(input);
5135
- const generator = this._exec.run({
5136
- input: prompt,
5137
- baseUrl: this._options.baseUrl,
5138
- apiKey: this._options.apiKey,
5139
- threadId: this._id,
5140
- images,
5141
- model: options?.model,
5142
- sandboxMode: options?.sandboxMode,
5143
- workingDirectory: options?.workingDirectory,
5144
- skipGitRepoCheck: options?.skipGitRepoCheck,
5145
- outputSchemaFile: schemaPath,
5146
- modelReasoningEffort: options?.modelReasoningEffort,
5147
- signal: turnOptions.signal,
5148
- networkAccessEnabled: options?.networkAccessEnabled,
5149
- webSearchMode: options?.webSearchMode,
5150
- webSearchEnabled: options?.webSearchEnabled,
5151
- approvalPolicy: options?.approvalPolicy,
5152
- additionalDirectories: options?.additionalDirectories
5153
- });
5154
- try {
5155
- for await (const item of generator) {
5156
- let parsed;
5157
- try {
5158
- parsed = JSON.parse(item);
5159
- } catch (error) {
5160
- throw new Error(`Failed to parse item: ${item}`, { cause: error });
5161
- }
5162
- if (parsed.type === "thread.started") {
5163
- this._id = parsed.thread_id;
5164
- } else if (parsed.type === "turn.completed") {
5165
- parsed.usage.cache_write_input_tokens ??= 0;
5166
- }
5167
- yield parsed;
5168
- }
5169
- } finally {
5170
- await cleanup();
5171
- }
5172
- }
5173
- /** Provides the input to the agent and returns the completed turn. */
5174
- async run(input, turnOptions = {}) {
5175
- const generator = this.runStreamedInternal(input, turnOptions);
5176
- const items = [];
5177
- let finalResponse = "";
5178
- let usage = null;
5179
- let turnFailure = null;
5180
- for await (const event of generator) {
5181
- if (event.type === "item.completed") {
5182
- if (event.item.type === "agent_message") {
5183
- finalResponse = event.item.text;
5184
- }
5185
- items.push(event.item);
5186
- } else if (event.type === "turn.completed") {
5187
- usage = event.usage;
5188
- } else if (event.type === "turn.failed") {
5189
- turnFailure = event.error;
5190
- break;
5191
- }
5192
- }
5193
- if (turnFailure) {
5194
- throw new Error(turnFailure.message);
5195
- }
5196
- return { items, finalResponse, usage };
5197
- }
5198
- };
5199
- function normalizeInput(input) {
5200
- if (typeof input === "string") {
5201
- return { prompt: input, images: [] };
5202
- }
5203
- const promptParts = [];
5204
- const images = [];
5205
- for (const item of input) {
5206
- if (item.type === "text") {
5207
- promptParts.push(item.text);
5208
- } else if (item.type === "local_image") {
5209
- images.push(item.path);
5210
- }
5211
- }
5212
- return { prompt: promptParts.join("\n\n"), images };
5213
- }
5214
- var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
5215
- var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
5216
- var CODEX_NPM_NAME = "@openai/codex";
5217
- var PLATFORM_PACKAGE_BY_TARGET = {
5218
- "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
5219
- "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
5220
- "x86_64-apple-darwin": "@openai/codex-darwin-x64",
5221
- "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
5222
- "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
5223
- "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
5224
- };
5225
- var moduleRequire = createRequire(import.meta.url);
5226
- var CodexExec = class {
5227
- executablePath;
5228
- pathDirs;
5229
- envOverride;
5230
- configOverrides;
5231
- constructor(executablePath = null, env, configOverrides) {
5232
- if (executablePath) {
5233
- this.executablePath = executablePath;
5234
- this.pathDirs = [];
5235
- } else {
5236
- const resolved = findCodexPath();
5237
- this.executablePath = resolved.executablePath;
5238
- this.pathDirs = resolved.pathDirs;
5239
- }
5240
- this.envOverride = env;
5241
- this.configOverrides = configOverrides;
5242
- }
5243
- async *run(args) {
5244
- const commandArgs = ["exec", "--experimental-json"];
5245
- if (this.configOverrides) {
5246
- for (const override of serializeConfigOverrides(this.configOverrides)) {
5247
- commandArgs.push("--config", override);
5248
- }
5249
- }
5250
- if (args.baseUrl) {
5251
- commandArgs.push(
5252
- "--config",
5253
- `openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
5254
- );
5255
- }
5256
- if (args.model) {
5257
- commandArgs.push("--model", args.model);
5258
- }
5259
- if (args.sandboxMode) {
5260
- commandArgs.push("--sandbox", args.sandboxMode);
5261
- }
5262
- if (args.workingDirectory) {
5263
- commandArgs.push("--cd", args.workingDirectory);
5264
- }
5265
- if (args.additionalDirectories?.length) {
5266
- for (const dir2 of args.additionalDirectories) {
5267
- commandArgs.push("--add-dir", dir2);
5268
- }
5269
- }
5270
- if (args.skipGitRepoCheck) {
5271
- commandArgs.push("--skip-git-repo-check");
5272
- }
5273
- if (args.outputSchemaFile) {
5274
- commandArgs.push("--output-schema", args.outputSchemaFile);
5275
- }
5276
- if (args.modelReasoningEffort) {
5277
- commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
5278
- }
5279
- if (args.networkAccessEnabled !== void 0) {
5280
- commandArgs.push(
5281
- "--config",
5282
- `sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
5283
- );
5284
- }
5285
- if (args.webSearchMode) {
5286
- commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
5287
- } else if (args.webSearchEnabled === true) {
5288
- commandArgs.push("--config", `web_search="live"`);
5289
- } else if (args.webSearchEnabled === false) {
5290
- commandArgs.push("--config", `web_search="disabled"`);
5291
- }
5292
- if (args.approvalPolicy) {
5293
- commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
5294
- }
5295
- if (args.threadId) {
5296
- commandArgs.push("resume", args.threadId);
5297
- }
5298
- if (args.images?.length) {
5299
- for (const image of args.images) {
5300
- commandArgs.push("--image", image);
5301
- }
5302
- }
5303
- const env = {};
5304
- if (this.envOverride) {
5305
- Object.assign(env, this.envOverride);
5306
- } else {
5307
- for (const [key, value] of Object.entries(process.env)) {
5308
- if (value !== void 0) {
5309
- env[key] = value;
5310
- }
5311
- }
5312
- }
5313
- if (!env[INTERNAL_ORIGINATOR_ENV]) {
5314
- env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
5315
- }
5316
- if (args.apiKey) {
5317
- env.CODEX_API_KEY = args.apiKey;
5318
- }
5319
- if (this.pathDirs.length > 0) {
5320
- prependPathDirs(env, this.pathDirs);
5321
- }
5322
- const child = spawn6(this.executablePath, commandArgs, {
5323
- env,
5324
- signal: args.signal
5325
- });
5326
- let spawnError = null;
5327
- child.once("error", (err) => spawnError = err);
5328
- if (!child.stdin) {
5329
- child.kill();
5330
- throw new Error("Child process has no stdin");
5331
- }
5332
- child.stdin.write(args.input);
5333
- child.stdin.end();
5334
- if (!child.stdout) {
5335
- child.kill();
5336
- throw new Error("Child process has no stdout");
5337
- }
5338
- const stderrChunks = [];
5339
- if (child.stderr) {
5340
- child.stderr.on("data", (data) => {
5341
- stderrChunks.push(data);
5342
- });
5343
- }
5344
- const exitPromise = new Promise(
5345
- (resolve) => {
5346
- child.once("exit", (code, signal) => {
5347
- resolve({ code, signal });
5348
- });
5349
- }
5350
- );
5351
- const rl = readline.createInterface({
5352
- input: child.stdout,
5353
- crlfDelay: Infinity
5354
- });
5355
- try {
5356
- for await (const line of rl) {
5357
- yield line;
5358
- }
5359
- if (spawnError) throw spawnError;
5360
- const { code, signal } = await exitPromise;
5361
- if (code !== 0 || signal) {
5362
- const stderrBuffer = Buffer.concat(stderrChunks);
5363
- const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
5364
- throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
5365
- }
5366
- } finally {
5367
- rl.close();
5368
- child.removeAllListeners();
5369
- try {
5370
- if (!child.killed) child.kill();
5371
- } catch {
5372
- }
5373
- }
5374
- }
5375
- };
5376
- function serializeConfigOverrides(configOverrides) {
5377
- const overrides = [];
5378
- flattenConfigOverrides(configOverrides, "", overrides);
5379
- return overrides;
5380
- }
5381
- function flattenConfigOverrides(value, prefix, overrides) {
5382
- if (!isPlainObject(value)) {
5383
- if (prefix) {
5384
- overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
5385
- return;
5386
- } else {
5387
- throw new Error("Codex config overrides must be a plain object");
5388
- }
5389
- }
5390
- const entries = Object.entries(value);
5391
- if (!prefix && entries.length === 0) {
5392
- return;
5393
- }
5394
- if (prefix && entries.length === 0) {
5395
- overrides.push(`${prefix}={}`);
5396
- return;
5397
- }
5398
- for (const [key, child] of entries) {
5399
- if (!key) {
5400
- throw new Error("Codex config override keys must be non-empty strings");
5401
- }
5402
- if (child === void 0) {
5403
- continue;
5404
- }
5405
- const path3 = prefix ? `${prefix}.${key}` : key;
5406
- if (isPlainObject(child)) {
5407
- flattenConfigOverrides(child, path3, overrides);
5408
- } else {
5409
- overrides.push(`${path3}=${toTomlValue(child, path3)}`);
5410
- }
5411
- }
5412
- }
5413
- function toTomlValue(value, path3) {
5414
- if (typeof value === "string") {
5415
- return JSON.stringify(value);
5416
- } else if (typeof value === "number") {
5417
- if (!Number.isFinite(value)) {
5418
- throw new Error(`Codex config override at ${path3} must be a finite number`);
5419
- }
5420
- return `${value}`;
5421
- } else if (typeof value === "boolean") {
5422
- return value ? "true" : "false";
5423
- } else if (Array.isArray(value)) {
5424
- const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
5425
- return `[${rendered.join(", ")}]`;
5426
- } else if (isPlainObject(value)) {
5427
- const parts = [];
5428
- for (const [key, child] of Object.entries(value)) {
5429
- if (!key) {
5430
- throw new Error("Codex config override keys must be non-empty strings");
5431
- }
5432
- if (child === void 0) {
5433
- continue;
5434
- }
5435
- parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
5436
- }
5437
- return `{${parts.join(", ")}}`;
5438
- } else if (value === null) {
5439
- throw new Error(`Codex config override at ${path3} cannot be null`);
5440
- } else {
5441
- const typeName = typeof value;
5442
- throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
5443
- }
5444
- }
5445
- var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
5446
- function formatTomlKey(key) {
5447
- return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
5448
- }
5449
- function isPlainObject(value) {
5450
- return typeof value === "object" && value !== null && !Array.isArray(value);
5451
- }
5452
- function findCodexPath() {
5453
- const { platform: platform2, arch } = process;
5454
- let targetTriple = null;
5455
- switch (platform2) {
5456
- case "linux":
5457
- case "android":
5458
- switch (arch) {
5459
- case "x64":
5460
- targetTriple = "x86_64-unknown-linux-musl";
5461
- break;
5462
- case "arm64":
5463
- targetTriple = "aarch64-unknown-linux-musl";
5464
- break;
5465
- default:
5466
- break;
5467
- }
5468
- break;
5469
- case "darwin":
5470
- switch (arch) {
5471
- case "x64":
5472
- targetTriple = "x86_64-apple-darwin";
5473
- break;
5474
- case "arm64":
5475
- targetTriple = "aarch64-apple-darwin";
5476
- break;
5477
- default:
5478
- break;
5479
- }
5480
- break;
5481
- case "win32":
5482
- switch (arch) {
5483
- case "x64":
5484
- targetTriple = "x86_64-pc-windows-msvc";
5485
- break;
5486
- case "arm64":
5487
- targetTriple = "aarch64-pc-windows-msvc";
5488
- break;
5489
- default:
5490
- break;
5491
- }
5492
- break;
5493
- default:
5494
- break;
5495
- }
5496
- if (!targetTriple) {
5497
- throw new Error(`Unsupported platform: ${platform2} (${arch})`);
5498
- }
5499
- const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
5500
- if (!platformPackage) {
5501
- throw new Error(`Unsupported target triple: ${targetTriple}`);
5502
- }
5503
- let vendorRoot;
5504
- try {
5505
- const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
5506
- const codexRequire = createRequire(codexPackageJsonPath);
5507
- const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
5508
- vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
5509
- } catch {
5510
- throw new Error(
5511
- `Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5512
- );
5513
- }
5514
- const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
5515
- const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
5516
- if (!nativePackage) {
5517
- throw new Error(
5518
- `Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5519
- );
5520
- }
5521
- return nativePackage;
5522
- }
5523
- function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
5524
- const packageRoot = path2.join(vendorRoot, targetTriple);
5525
- const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
5526
- if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
5527
- return {
5528
- executablePath: packageBinaryPath,
5529
- pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
5530
- };
5531
- }
5532
- const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
5533
- if (isFile(legacyBinaryPath)) {
5534
- return {
5535
- executablePath: legacyBinaryPath,
5536
- pathDirs: existingDirs(path2.join(packageRoot, "path"))
5537
- };
5538
- }
5539
- return null;
5540
- }
5541
- function existingDirs(...dirs) {
5542
- return dirs.filter(isDirectory);
5543
- }
5544
- function prependPathDirs(env, pathDirs, platform2 = process.platform) {
5545
- const pathKey = pathEnvKey(env, platform2);
5546
- if (platform2 === "win32") {
5547
- for (const key of Object.keys(env)) {
5548
- if (key.toLowerCase() === "path" && key !== pathKey) {
5549
- delete env[key];
5550
- }
5551
- }
5552
- }
5553
- const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
5554
- env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
5555
- }
5556
- function pathEnvKey(env, platform2) {
5557
- if (platform2 !== "win32") {
5558
- return "PATH";
5559
- }
5560
- const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
5561
- return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
5562
- }
5563
- function isFile(filePath) {
5564
- try {
5565
- return statSync(filePath).isFile();
5566
- } catch {
5567
- return false;
5568
- }
5569
- }
5570
- function isDirectory(filePath) {
5571
- try {
5572
- return statSync(filePath).isDirectory();
5573
- } catch {
5574
- return false;
5575
- }
5576
- }
5577
- var Codex = class {
5578
- exec;
5579
- options;
5580
- constructor(options = {}) {
5581
- const { codexPathOverride, env, config } = options;
5582
- this.exec = new CodexExec(codexPathOverride, env, config);
5583
- this.options = options;
5584
- }
5585
- /**
5586
- * Starts a new conversation with an agent.
5587
- * @returns A new thread instance.
5588
- */
5589
- startThread(options = {}) {
5590
- return new Thread(this.exec, this.options, options);
5591
- }
5592
- /**
5593
- * Resumes a conversation with an agent based on the thread id.
5594
- * Threads are persisted in ~/.codex/sessions.
5595
- *
5596
- * @param id The id of the thread to resume.
5597
- * @returns A new thread instance.
5598
- */
5599
- resumeThread(id, options = {}) {
5600
- return new Thread(this.exec, this.options, options, id);
5601
- }
5602
- };
5603
-
5604
5155
  // packages/agent-runtime/src/codex/transport.ts
5156
+ import {
5157
+ Codex
5158
+ } from "@openai/codex-sdk";
5605
5159
  function buildSdkThreadOptions(spec) {
5606
5160
  return {
5607
5161
  ...spec.model ? { model: spec.model } : {},
@@ -6393,7 +5947,7 @@ var ConnectorHealthStore = class {
6393
5947
 
6394
5948
  // src/dispatcher.ts
6395
5949
  import { randomUUID } from "crypto";
6396
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync as statSync2 } from "fs";
5950
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync } from "fs";
6397
5951
  import { join as join13 } from "path";
6398
5952
 
6399
5953
  // src/summon.ts
@@ -6731,10 +6285,10 @@ import { join as join9 } from "path";
6731
6285
  var PREFIX = "cabane-codex-instructions-";
6732
6286
  async function writeCodexInstructionsFile(contents) {
6733
6287
  const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6734
- const path3 = join9(dir2, "instructions.md");
6735
- await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6288
+ const path = join9(dir2, "instructions.md");
6289
+ await writeFile(path, contents, { encoding: "utf8", mode: 384 });
6736
6290
  return {
6737
- path: path3,
6291
+ path,
6738
6292
  cleanup: async () => {
6739
6293
  await rm(dir2, { recursive: true, force: true });
6740
6294
  }
@@ -6754,10 +6308,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
6754
6308
  return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6755
6309
  }
6756
6310
  function readPrepared(workspaceId, conversationId, agentId) {
6757
- const path3 = pathFor3(workspaceId, conversationId, agentId);
6758
- if (!existsSync7(path3)) return null;
6311
+ const path = pathFor3(workspaceId, conversationId, agentId);
6312
+ if (!existsSync7(path)) return null;
6759
6313
  try {
6760
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
6314
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
6761
6315
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
6762
6316
  return {
6763
6317
  cwd: parsed.cwd,
@@ -6791,14 +6345,14 @@ function secretsPath() {
6791
6345
  }
6792
6346
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6793
6347
  function loadSecretStore() {
6794
- const path3 = secretsPath();
6795
- if (!existsSync8(path3)) return makeStore({});
6348
+ const path = secretsPath();
6349
+ if (!existsSync8(path)) return makeStore({});
6796
6350
  let raw;
6797
6351
  try {
6798
- raw = readFileSync7(path3, "utf8");
6352
+ raw = readFileSync7(path, "utf8");
6799
6353
  } catch (err) {
6800
6354
  throw new ConfigError(
6801
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
6355
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
6802
6356
  );
6803
6357
  }
6804
6358
  if (raw.trim().length === 0) return makeStore({});
@@ -6807,13 +6361,13 @@ function loadSecretStore() {
6807
6361
  parsed = JSON.parse(raw);
6808
6362
  } catch (err) {
6809
6363
  throw new ConfigError(
6810
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6364
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6811
6365
  );
6812
6366
  }
6813
6367
  const result = secretStoreSchema.safeParse(parsed);
6814
6368
  if (!result.success) {
6815
6369
  throw new ConfigError(
6816
- `${path3} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6370
+ `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6817
6371
  );
6818
6372
  }
6819
6373
  return makeStore(result.data);
@@ -7249,7 +6803,7 @@ function checkoutState(cwd) {
7249
6803
  if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
7250
6804
  let stat;
7251
6805
  try {
7252
- stat = statSync2(gitPath);
6806
+ stat = statSync(gitPath);
7253
6807
  } catch (error) {
7254
6808
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
7255
6809
  }
@@ -8197,7 +7751,9 @@ var LABELS = {
8197
7751
  function deriveHarnessSnapshot(signals) {
8198
7752
  const advertised = new Set(
8199
7753
  buildCompanionManifest({
8200
- claudeCode: signals.claudeOnPath,
7754
+ // CT1082: connected AND installed — the manifest's own rule, restated here
7755
+ // through the same function rather than re-decided.
7756
+ claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
8201
7757
  opencode: signals.opencodeConfigured,
8202
7758
  codex: signals.codexEnabled
8203
7759
  }).runtimes.map((r) => r.name)
@@ -8210,20 +7766,40 @@ function deriveHarnessSnapshot(signals) {
8210
7766
  return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
8211
7767
  }
8212
7768
  function deriveClaudeCode(signals, manifestHas) {
8213
- const base = { runtime: "claude-code", label: LABELS["claude-code"], enable: null };
7769
+ const base = { runtime: "claude-code", label: LABELS["claude-code"] };
8214
7770
  if (manifestHas) {
8215
7771
  return {
8216
7772
  ...base,
8217
7773
  state: "exposed",
8218
7774
  version: signals.claudeVersion,
8219
- detail: "Claude Code is installed and exposed to Cabane."
7775
+ detail: "Claude Code is connected and exposed to Cabane.",
7776
+ enable: null
7777
+ };
7778
+ }
7779
+ if (signals.claudeCodeConnected) {
7780
+ return {
7781
+ ...base,
7782
+ state: "needs_attention",
7783
+ version: null,
7784
+ 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.",
7785
+ enable: null
7786
+ };
7787
+ }
7788
+ if (signals.claudeOnPath) {
7789
+ return {
7790
+ ...base,
7791
+ state: "detected_not_exposed",
7792
+ version: signals.claudeVersion,
7793
+ detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
7794
+ enable: "claude-code"
8220
7795
  };
8221
7796
  }
8222
7797
  return {
8223
7798
  ...base,
8224
7799
  state: "not_detected",
8225
7800
  version: null,
8226
- detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it appears here."
7801
+ detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
7802
+ enable: null
8227
7803
  };
8228
7804
  }
8229
7805
  function deriveCodex(signals, manifestHas) {
@@ -8291,6 +7867,9 @@ function deriveOpencode(signals, manifestHas) {
8291
7867
  enable: "opencode"
8292
7868
  };
8293
7869
  }
7870
+ function detectedRuntimesFor(snapshot) {
7871
+ return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
7872
+ }
8294
7873
  var PROBE_TIMEOUT_MS2 = 4e3;
8295
7874
  async function probeHarnessSignals(cfg, deps = {}) {
8296
7875
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -8307,6 +7886,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
8307
7886
  return {
8308
7887
  claudeOnPath: claudeOnPathResult,
8309
7888
  claudeVersion,
7889
+ // CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
7890
+ // the manifest gate and the probe above is only a suggestion.
7891
+ claudeCodeConnected: isClaudeCodeConnected(cfg),
8310
7892
  // A parseable `codex --version` is our presence signal (presence alone never
8311
7893
  // exposes codex; its config flag is the manifest gate either way).
8312
7894
  codexOnPath: codexVersion !== null,
@@ -8680,8 +8262,8 @@ function sleep3(ms) {
8680
8262
  }
8681
8263
 
8682
8264
  // src/version.ts
8683
- import { createRequire as createRequire2 } from "module";
8684
- var pkg = createRequire2(import.meta.url)("../package.json");
8265
+ import { createRequire } from "module";
8266
+ var pkg = createRequire(import.meta.url)("../package.json");
8685
8267
  var COMPANION_VERSION = pkg.version;
8686
8268
 
8687
8269
  // src/supervisor.ts
@@ -8715,6 +8297,10 @@ var CompanionSupervisor = class {
8715
8297
  // can never disagree with what the manifest advertises. Null until the first
8716
8298
  // probe (the heartbeat then falls back to the boot `claudeCode`).
8717
8299
  harnessSignals = null;
8300
+ // CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
8301
+ // NOT the cached beat signal — someone connecting right after installing Claude
8302
+ // Code shouldn't be refused by a snapshot up to a heartbeat old.
8303
+ probeClaudePresence;
8718
8304
  exitFn;
8719
8305
  reexecFn;
8720
8306
  dispatcherFactory;
@@ -8745,6 +8331,7 @@ var CompanionSupervisor = class {
8745
8331
  this.log = opts.log;
8746
8332
  this.hub = opts.hub;
8747
8333
  this.claudeCode = opts.claudeCode ?? true;
8334
+ this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
8748
8335
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
8749
8336
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
8750
8337
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -8807,11 +8394,10 @@ var CompanionSupervisor = class {
8807
8394
  // claude-code when `claude` is on PATH, CT270 opencode when the operator
8808
8395
  // configured an `opencode serve`.
8809
8396
  manifest: buildCompanionManifest({
8810
- // CT586: prefer the live re-probe's presence; fall back to the boot probe
8811
- // until the first re-probe lands. Same exit-0 `claude --version` signal
8812
- // either way, so the manifest's claude-code advertising is unchanged in
8813
- // meaning — just refreshed on the beat instead of frozen at boot.
8814
- claudeCode: this.claudeCodePresent(),
8397
+ // CT1082: connected AND installed. Presence is the live re-probe (CT586),
8398
+ // falling back to the boot probe until the first one lands; consent is the
8399
+ // user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
8400
+ claudeCode: this.claudeCodeOffered(),
8815
8401
  opencode: !!this.config.opencode?.serverUrl,
8816
8402
  // CT481: advertise codex when the operator enabled it (config-gated,
8817
8403
  // like opencode — the CLI's presence is the operator's responsibility;
@@ -8829,7 +8415,13 @@ var CompanionSupervisor = class {
8829
8415
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
8830
8416
  // A null (probe failed / no opencode) omits the field, and the server then
8831
8417
  // leaves this device's stored availability untouched.
8832
- ...opencodeModels !== null ? { models: opencodeModels } : {}
8418
+ ...opencodeModels !== null ? { models: opencodeModels } : {},
8419
+ // CT1082: what this machine has that the user hasn't connected, so the web
8420
+ // UI can offer it without a companion round-trip. Sent only once a probe has
8421
+ // actually landed (`harnessSignals` non-null) — an absent field means "we
8422
+ // didn't look this beat" and leaves the server's stored suggestion alone,
8423
+ // the same fail-soft contract `models` keeps.
8424
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8833
8425
  });
8834
8426
  this.hub.setDevice({ deviceId: res.deviceId });
8835
8427
  this.deviceId = res.deviceId;
@@ -9034,13 +8626,19 @@ var CompanionSupervisor = class {
9034
8626
  "companion: stopped agent (unassigned)"
9035
8627
  );
9036
8628
  }
9037
- // CT833: is Claude Code on this machine right now? The ONE signal both the
9038
- // heartbeat manifest and the dispatcher's adapter registry read, so what the
9039
- // device advertises and what it can actually select can't disagree. Live (the
9040
- // per-beat re-probe), falling back to the boot probe until the first one lands.
8629
+ // CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
8630
+ // falling back to the boot probe until the first one lands. Presence ONLY —
8631
+ // CT1082 split presence from exposure, so nothing routes on this directly.
9041
8632
  claudeCodePresent() {
9042
8633
  return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
9043
8634
  }
8635
+ // CT1082: does this device OFFER claude-code — connected by its user and actually
8636
+ // installed? The ONE signal both the heartbeat manifest and the dispatcher's
8637
+ // adapter registry read, so what the device advertises and what it can select
8638
+ // can't disagree (the CT833 invariant, now with consent in front of it).
8639
+ claudeCodeOffered() {
8640
+ return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
8641
+ }
9044
8642
  buildDispatcher(ctx) {
9045
8643
  if (this.dispatcherFactory) return this.dispatcherFactory(ctx);
9046
8644
  return new Dispatcher({
@@ -9058,10 +8656,12 @@ var CompanionSupervisor = class {
9058
8656
  }),
9059
8657
  runConfig: ctx.runConfig,
9060
8658
  log: this.log,
9061
- // CT833: register the claude-code adapter only when `claude` is actually on
9062
- // this machine — read per turn (not captured here), so a harness installed
9063
- // after boot works on the next turn exactly as it appears on the next beat.
9064
- claudeCodeAvailable: () => this.claudeCodePresent(),
8659
+ // CT833: register the claude-code adapter only when this device actually
8660
+ // offers claude-code — read per turn (not captured here), so a harness
8661
+ // installed or connected after boot works on the next turn exactly as it
8662
+ // appears on the next beat. CT1082: "offers" now means connected as well as
8663
+ // installed, so a disconnected harness can't be selected either.
8664
+ claudeCodeAvailable: () => this.claudeCodeOffered(),
9065
8665
  // CT270: the opencode server URL (operator-configured), when this device
9066
8666
  // offers the opencode runtime. Threaded so an opencode turn selects the
9067
8667
  // opencode adapter; unset leaves the device claude-code-only.
@@ -9367,9 +8967,12 @@ var CompanionSupervisor = class {
9367
8967
  async recheckHarnesses() {
9368
8968
  await this.refreshHarnessStatuses();
9369
8969
  }
9370
- // Friendly enable for the two config-driven harnesses — flip the flag the app
9371
- // owns in `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose
9372
- // ONLY: it never installs a binary and never drives a login (BYO — Decided).
8970
+ // Friendly enable for the config-driven harnesses — flip the flag the app owns in
8971
+ // `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
8972
+ // never installs a binary and never drives a login (BYO — Decided).
8973
+ // - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
8974
+ // on PATH. This is the callable the terminal offer ("We found Claude Code.
8975
+ // Connect it? [Y/n]") and the web pairing flow both wire to.
9373
8976
  // - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
9374
8977
  // - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
9375
8978
  // so we never advertise a serve that isn't there. An unreachable URL is a
@@ -9379,7 +8982,15 @@ var CompanionSupervisor = class {
9379
8982
  // reachable/valid input).
9380
8983
  async enableHarness(input) {
9381
8984
  let next;
9382
- if (input.runtime === "codex") {
8985
+ if (input.runtime === "claude-code") {
8986
+ if (!await this.probeClaudePresence()) {
8987
+ return {
8988
+ ok: false,
8989
+ 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."
8990
+ };
8991
+ }
8992
+ next = { ...this.config, claudeCode: { enabled: true } };
8993
+ } else if (input.runtime === "codex") {
9383
8994
  next = { ...this.config, codex: { enabled: true } };
9384
8995
  } else {
9385
8996
  const serverUrl = input.serverUrl.trim();
@@ -9485,9 +9096,9 @@ var CompanionSupervisor = class {
9485
9096
  };
9486
9097
  function defaultReexec() {
9487
9098
  clearRuntimeState();
9488
- void import("child_process").then(({ spawn: spawn7 }) => {
9099
+ void import("child_process").then(({ spawn: spawn6 }) => {
9489
9100
  try {
9490
- const child = spawn7(process.execPath, process.argv.slice(1), {
9101
+ const child = spawn6(process.execPath, process.argv.slice(1), {
9491
9102
  stdio: "inherit",
9492
9103
  detached: false
9493
9104
  });
@@ -9571,8 +9182,8 @@ function recordCrash(rec2) {
9571
9182
  }
9572
9183
  function clearCrash() {
9573
9184
  try {
9574
- const path3 = crashMarkerPath();
9575
- if (existsSync11(path3)) rmSync7(path3, { force: true });
9185
+ const path = crashMarkerPath();
9186
+ if (existsSync11(path)) rmSync7(path, { force: true });
9576
9187
  } catch {
9577
9188
  }
9578
9189
  }
@@ -9581,13 +9192,17 @@ function clearCrash() {
9581
9192
  async function createCompanionRuntime(opts = {}) {
9582
9193
  const log = getLogger();
9583
9194
  installProcessSafetyNet(log);
9584
- const probeClaude = opts.probeClaude ?? claudeOnPath;
9585
9195
  let cfg;
9586
9196
  let claudeCode;
9587
9197
  try {
9588
- cfg = requireConfig();
9589
- claudeCode = await probeClaude();
9590
- await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
9198
+ ({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
9199
+ ...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
9200
+ onMigrated: (migrated, onPath) => log.info(
9201
+ { claudeCode: isClaudeCodeConnected(migrated) },
9202
+ 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)"
9203
+ )
9204
+ }));
9205
+ await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
9591
9206
  } catch (err) {
9592
9207
  recordCrash({
9593
9208
  reason: err instanceof Error ? err.message : String(err),
@@ -9599,7 +9214,9 @@ async function createCompanionRuntime(opts = {}) {
9599
9214
  }
9600
9215
  if (cfg.logLevel) log.level = cfg.logLevel;
9601
9216
  const harnessVersions = await probeHarnessVersions({
9602
- claudeCode,
9217
+ // CT1082: versions are probed for the runtimes this device OFFERS, and
9218
+ // claude-code is offered only when connected as well as installed.
9219
+ claudeCode: claudeCode && isClaudeCodeConnected(cfg),
9603
9220
  opencodeServerUrl: cfg.opencode?.serverUrl,
9604
9221
  codex: isCodexEnabled(cfg)
9605
9222
  });
@@ -10071,11 +9688,11 @@ function printList(dir2) {
10071
9688
  "render one: cabane-companion transcript <file> (or `--last` for the newest)\n"
10072
9689
  );
10073
9690
  }
10074
- function peek(path3) {
9691
+ function peek(path) {
10075
9692
  let meta;
10076
9693
  let outcome;
10077
9694
  try {
10078
- for (const line of readFileSync10(path3, "utf8").split("\n")) {
9695
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
10079
9696
  if (!line.trim()) continue;
10080
9697
  const o = safeParse(line);
10081
9698
  const t = str2(rec(o)?.type);
@@ -10105,13 +9722,13 @@ function resolveTarget(dir2, target) {
10105
9722
  ` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
10106
9723
  );
10107
9724
  }
10108
- function renderFile(path3) {
9725
+ function renderFile(path) {
10109
9726
  let content;
10110
9727
  try {
10111
- content = readFileSync10(path3, "utf8");
9728
+ content = readFileSync10(path, "utf8");
10112
9729
  } catch (err) {
10113
9730
  throw new CompanionError(
10114
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
9731
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
10115
9732
  );
10116
9733
  }
10117
9734
  return renderTranscript(content.split("\n"));