@cabane/companion 0.6.49 → 0.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +176 -152
  2. package/dist/runtime.js +1070 -1056
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -644,40 +644,58 @@ async function ping(path) {
644
644
  }
645
645
 
646
646
  // src/harness-check.ts
647
- import { spawn as spawn4 } from "child_process";
647
+ import { spawn as spawn3 } from "child_process";
648
648
 
649
649
  // src/harness-versions.ts
650
650
  import { spawn as spawn2 } from "child_process";
651
651
  var EMPTY = { claudeCode: null, opencode: null, codex: null };
652
+ var CLI_PROBE_TIMEOUT_MS = 4e3;
652
653
  function parseVersionToken(raw) {
653
654
  if (!raw) return null;
654
655
  const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
655
656
  return m ? m[0] : null;
656
657
  }
657
- async function probeCliVersion(command, spawnImpl = spawn2) {
658
+ function probeCliPresence(command, spawnImpl = spawn2) {
658
659
  return new Promise((resolve) => {
659
660
  let settled = false;
660
- const done = (v) => {
661
+ let timer = null;
662
+ const done = (result) => {
661
663
  if (!settled) {
662
664
  settled = true;
663
- resolve(v);
665
+ if (timer) clearTimeout(timer);
666
+ resolve(result);
664
667
  }
665
668
  };
666
669
  let child;
667
670
  try {
668
671
  child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
669
672
  } catch {
670
- done(null);
673
+ done({ status: "absent" });
671
674
  return;
672
675
  }
673
676
  let out = "";
674
677
  child.stdout?.on("data", (chunk) => {
675
678
  if (out.length < 4096) out += chunk.toString();
676
679
  });
677
- child.once("error", () => done(null));
678
- child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
680
+ timer = setTimeout(() => {
681
+ child.kill?.("SIGKILL");
682
+ done({ status: "unusable" });
683
+ }, CLI_PROBE_TIMEOUT_MS);
684
+ timer.unref?.();
685
+ child.once("error", () => done({ status: "absent" }));
686
+ child.once("exit", (code) => {
687
+ const version = code === 0 ? parseVersionToken(out) : null;
688
+ done(version ? { status: "present", version } : { status: "unusable" });
689
+ });
679
690
  });
680
691
  }
692
+ function probeHarnessPresence(runtime, spawnImpl = spawn2) {
693
+ return probeCliPresence(runtime === "codex" ? "codex" : "claude", spawnImpl);
694
+ }
695
+ async function probeCliVersion(command, spawnImpl = spawn2) {
696
+ const result = await probeCliPresence(command, spawnImpl);
697
+ return result.status === "present" ? result.version : null;
698
+ }
681
699
  async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
682
700
  try {
683
701
  const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
@@ -741,81 +759,6 @@ function buildCompanionManifest(opts) {
741
759
  return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
742
760
  }
743
761
 
744
- // src/prereqs.ts
745
- import { spawn as spawn3 } from "child_process";
746
- async function claudeOnPath() {
747
- return new Promise((resolve) => {
748
- let settled = false;
749
- const child = spawn3("claude", ["--version"], { stdio: "ignore" });
750
- child.once("error", () => {
751
- if (!settled) {
752
- settled = true;
753
- resolve(false);
754
- }
755
- });
756
- child.once("exit", (code) => {
757
- if (!settled) {
758
- settled = true;
759
- resolve(code === 0);
760
- }
761
- });
762
- });
763
- }
764
- var CODEX_PROBE_TIMEOUT_MS = 4e3;
765
- async function codexOnPath() {
766
- const version = await Promise.race([
767
- probeCliVersion("codex"),
768
- new Promise((resolve) => {
769
- const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
770
- timer.unref?.();
771
- })
772
- ]);
773
- return version !== null;
774
- }
775
- async function requireStartConfig(deps = {}) {
776
- const requireCfg = deps.requireCfg ?? requireConfig;
777
- const probeClaude = deps.probeClaude ?? claudeOnPath;
778
- const save2 = deps.save ?? saveConfig;
779
- const cfg = requireCfg();
780
- const claudeOnPathResult = await probeClaude();
781
- const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
782
- if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
783
- save2(migrated);
784
- deps.onMigrated?.(migrated, claudeOnPathResult);
785
- return { cfg: migrated, claudeOnPath: claudeOnPathResult };
786
- }
787
- async function warnAboutHarnessReadiness(cfg, deps = {}) {
788
- const probeClaude = deps.probeClaude ?? claudeOnPath;
789
- const probeCodex = deps.probeCodex ?? codexOnPath;
790
- const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
791
- `));
792
- const connected = [
793
- ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
794
- ...isCodexEnabled(cfg) ? ["Codex"] : [],
795
- ...cfg.opencode ? ["opencode"] : []
796
- ];
797
- if (connected.length > 0) {
798
- if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
799
- warn(
800
- "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."
801
- );
802
- }
803
- return;
804
- }
805
- const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
806
- const installed = [
807
- ...claudeInstalled ? ["Claude Code"] : [],
808
- ...codexInstalled ? ["Codex"] : []
809
- ];
810
- const connectCommands = [
811
- ...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
812
- ...codexInstalled ? ["`cabane-companion connect codex`"] : []
813
- ];
814
- warn(
815
- "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 with " : "it with "}${connectCommands.join(" or ")} 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 with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
816
- );
817
- }
818
-
819
762
  // src/harness-status.ts
820
763
  var HARNESS_LABELS = {
821
764
  "claude-code": "Claude Code",
@@ -976,20 +919,19 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
976
919
  return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
977
920
  }
978
921
  async function probeHarnessSignals(cfg, deps = {}) {
979
- const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
980
- const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
981
- const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
922
+ const probePresence = deps.probePresence ?? probeHarnessPresence;
982
923
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
983
924
  const configuredServerUrl = cfg.opencode?.serverUrl;
984
925
  const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
985
- const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
986
- withTimeout(probeClaudePresence(), false),
987
- withTimeout(probeClaudeVersion(), null),
988
- withTimeout(probeCodexVersion(), null),
926
+ const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
927
+ deps.presence?.["claude-code"] ?? withTimeout(probePresence("claude-code"), { status: "absent" }),
928
+ deps.presence?.codex ?? withTimeout(probePresence("codex"), { status: "absent" }),
989
929
  configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
990
930
  ]);
931
+ const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
932
+ const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
991
933
  return {
992
- claudeOnPath: claudeOnPathResult,
934
+ claudeOnPath: claudePresence.status === "present",
993
935
  claudeVersion,
994
936
  // CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
995
937
  // the manifest gate and the probe above is only a suggestion.
@@ -1041,16 +983,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1041
983
  if (!url) return "absent";
1042
984
  return await probeOpencode(url) !== null ? "ok" : "failed";
1043
985
  }
1044
- const { auth, presence } = runtime === "codex" ? {
1045
- auth: ["codex", ["login", "status"]],
1046
- presence: ["codex", ["--version"]]
986
+ const { auth } = runtime === "codex" ? {
987
+ auth: ["codex", ["login", "status"]]
1047
988
  } : {
1048
- auth: ["claude", ["auth", "status"]],
1049
- presence: ["claude", ["--version"]]
989
+ auth: ["claude", ["auth", "status"]]
1050
990
  };
1051
- const presenceRun = await run(presence[0], [...presence[1]]);
1052
- if (presenceRun.error === "spawn") return "absent";
1053
- if (presenceRun.code !== 0) return "unverified";
991
+ const presence = deps.presence ?? await (deps.probePresence ?? probeHarnessPresence)(runtime);
992
+ if (presence.status !== "present") return presence.status;
1054
993
  const authRun = await run(auth[0], [...auth[1]]);
1055
994
  if (authRun.code === 0) return "ok";
1056
995
  if (looksUnsupported(authRun.output)) {
@@ -1062,7 +1001,9 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
1062
1001
  }
1063
1002
  }
1064
1003
  function connectedLine(runtime, verdict) {
1065
- if (verdict === "absent") throw new Error("an absent harness cannot be connected");
1004
+ if (verdict === "absent" || verdict === "unusable") {
1005
+ throw new Error("an unavailable harness cannot be connected");
1006
+ }
1066
1007
  const label = HARNESS_LABELS[runtime];
1067
1008
  if (verdict !== "failed") return `${label} connected.`;
1068
1009
  return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
@@ -1072,13 +1013,18 @@ var FAILED_SUFFIX = {
1072
1013
  codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
1073
1014
  opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
1074
1015
  };
1075
- function absentLine(runtime) {
1016
+ function presenceReason(runtime, status2) {
1076
1017
  if (runtime === "opencode") {
1077
1018
  return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
1078
1019
  }
1079
1020
  const label = HARNESS_LABELS[runtime];
1080
- const login = runtime === "codex" ? "`codex login`" : "`claude`";
1081
- return `${label} isn\u2019t installed on this machine \u2014 install it and sign in (${login}), then run this again.`;
1021
+ return status2 === "absent" ? `${label} isn't installed on this machine.` : `${label} is installed here but didn't report a version Cabane can read.`;
1022
+ }
1023
+ var CLI_PRESENCE_ACTION = "Install it and run this again.";
1024
+ var DASHBOARD_PRESENCE_ACTION = "Install it, then refresh.";
1025
+ function presenceRefusal(runtime, status2, action) {
1026
+ const reason = presenceReason(runtime, status2);
1027
+ return runtime === "opencode" ? reason : `${reason} ${action}`;
1082
1028
  }
1083
1029
  function looksUnsupported(output) {
1084
1030
  return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
@@ -1096,7 +1042,7 @@ function runBounded(command, args) {
1096
1042
  };
1097
1043
  let child;
1098
1044
  try {
1099
- child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1045
+ child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1100
1046
  } catch {
1101
1047
  resolve({ code: null, output: "", error: "spawn" });
1102
1048
  return;
@@ -1299,18 +1245,31 @@ async function confirm(question) {
1299
1245
  }
1300
1246
 
1301
1247
  // src/commands/connect.ts
1302
- async function connect2(raw, opts = {}) {
1248
+ async function connect2(raw, opts = {}, deps = {}) {
1303
1249
  const runtime = parseHarnessRuntime(raw);
1304
1250
  if (!runtime) {
1305
1251
  throw new CompanionError(
1306
1252
  `unknown harness "${raw}". Connectable harnesses are: claude-code, codex, opencode.`
1307
1253
  );
1308
1254
  }
1309
- const live = await liveSocket();
1255
+ const loadConfig2 = deps.loadConfig ?? requireConfig;
1256
+ const request = deps.request ?? controlRequest;
1257
+ const save2 = deps.save ?? saveConfig;
1258
+ const output = deps.output ?? write;
1259
+ const probePresence = deps.probePresence ?? probeHarnessPresence;
1260
+ const shakeOut = deps.shakeOut ?? shakeOutHarness;
1261
+ const resolveServerUrl = deps.resolveServerUrl ?? resolveOpencodeServerUrl;
1262
+ const probeOpencode = deps.probeOpencode ?? probeOpencodeVersion;
1263
+ const cfg = loadConfig2();
1264
+ if (!opts.serverUrl?.trim() && alreadyConnected(runtime, cfg)) {
1265
+ output(`${INDENT}${HARNESS_LABELS[runtime]} is already connected on this device.`);
1266
+ return;
1267
+ }
1268
+ const live = await (deps.findLiveSocket ?? liveSocket)();
1310
1269
  if (live) {
1311
1270
  let result;
1312
1271
  try {
1313
- result = await controlRequest(
1272
+ result = await request(
1314
1273
  live,
1315
1274
  {
1316
1275
  cmd: "connect",
@@ -1326,42 +1285,38 @@ async function connect2(raw, opts = {}) {
1326
1285
  );
1327
1286
  }
1328
1287
  if (!result.ok) throw new CompanionError(result.message);
1329
- write(INDENT + tick(result.message));
1330
- return;
1331
- }
1332
- const cfg = requireConfig();
1333
- if (alreadyConnected(runtime, cfg)) {
1334
- write(`${INDENT}${HARNESS_LABELS[runtime]} is already connected on this device.`);
1288
+ output(INDENT + tick(result.message));
1335
1289
  return;
1336
1290
  }
1337
- if (runtime === "claude-code" && !await claudeOnPath()) {
1338
- throw new CompanionError(
1339
- "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."
1340
- );
1341
- }
1342
1291
  let next = cfg;
1292
+ const presence = runtime === "opencode" ? void 0 : await probePresence(runtime);
1293
+ if (presence && presence.status !== "present") {
1294
+ throw new CompanionError(presenceRefusal(runtime, presence.status, CLI_PRESENCE_ACTION));
1295
+ }
1343
1296
  if (runtime === "claude-code") next = { ...cfg, claudeCode: { enabled: true } };
1344
1297
  else if (runtime === "codex") next = { ...cfg, codex: { enabled: true } };
1345
1298
  else {
1346
1299
  const requestedServerUrl = opts.serverUrl?.trim();
1347
- const serverUrl = await resolveOpencodeServerUrl(void 0, requestedServerUrl);
1300
+ const serverUrl = await resolveServerUrl(cfg.opencode?.serverUrl, requestedServerUrl);
1348
1301
  if (!serverUrl) {
1349
1302
  throw new CompanionError(
1350
1303
  "opencode is addressed by URL \u2014 pass it: `cabane-companion connect opencode --url http://127.0.0.1:4096`."
1351
1304
  );
1352
1305
  }
1353
- if (requestedServerUrl && await probeOpencodeVersion(serverUrl) === null) {
1306
+ if (requestedServerUrl && await probeOpencode(serverUrl) === null) {
1354
1307
  throw new CompanionError(
1355
1308
  `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
1356
1309
  );
1357
1310
  }
1358
1311
  next = { ...cfg, opencode: { serverUrl } };
1359
1312
  }
1360
- const verdict = await shakeOutHarness(runtime, next);
1361
- if (verdict === "absent") throw new CompanionError(absentLine(runtime));
1362
- saveConfig(next);
1363
- write(INDENT + tick(connectedLine(runtime, verdict)));
1364
- write(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
1313
+ const verdict = await shakeOut(runtime, next, { ...presence ? { presence } : {} });
1314
+ if (verdict === "absent" || verdict === "unusable") {
1315
+ throw new CompanionError(presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION));
1316
+ }
1317
+ save2(next);
1318
+ output(INDENT + tick(connectedLine(runtime, verdict)));
1319
+ output(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
1365
1320
  }
1366
1321
  async function liveSocket() {
1367
1322
  const state = readLiveRuntimeState();
@@ -2082,12 +2037,18 @@ function registerRoutes(app, deps) {
2082
2037
  const runtime = body.runtime;
2083
2038
  if (runtime === "claude-code") {
2084
2039
  const result = await supervisor.enableHarness({ runtime: "claude-code" });
2085
- if (!result.ok) return c.json({ error: result.error }, 400);
2040
+ if (!result.ok) {
2041
+ const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
2042
+ return c.json({ error }, 400);
2043
+ }
2086
2044
  return c.json({ ok: true, status: hub.statusJson() });
2087
2045
  }
2088
2046
  if (runtime === "codex") {
2089
2047
  const result = await supervisor.enableHarness({ runtime: "codex" });
2090
- if (!result.ok) return c.json({ error: result.error }, 400);
2048
+ if (!result.ok) {
2049
+ const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
2050
+ return c.json({ error }, 400);
2051
+ }
2091
2052
  return c.json({ ok: true, status: hub.statusJson() });
2092
2053
  }
2093
2054
  if (runtime === "opencode") {
@@ -2246,6 +2207,57 @@ function resolveStaticDir() {
2246
2207
  return join7(dirname4(fileURLToPath(import.meta.url)), "static");
2247
2208
  }
2248
2209
 
2210
+ // src/prereqs.ts
2211
+ async function claudeOnPath() {
2212
+ return (await probeHarnessPresence("claude-code")).status === "present";
2213
+ }
2214
+ async function codexOnPath() {
2215
+ return (await probeHarnessPresence("codex")).status === "present";
2216
+ }
2217
+ async function requireStartConfig(deps = {}) {
2218
+ const requireCfg = deps.requireCfg ?? requireConfig;
2219
+ const probeClaude = deps.probeClaude ?? claudeOnPath;
2220
+ const save2 = deps.save ?? saveConfig;
2221
+ const cfg = requireCfg();
2222
+ const claudeOnPathResult = await probeClaude();
2223
+ const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
2224
+ if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
2225
+ save2(migrated);
2226
+ deps.onMigrated?.(migrated, claudeOnPathResult);
2227
+ return { cfg: migrated, claudeOnPath: claudeOnPathResult };
2228
+ }
2229
+ async function warnAboutHarnessReadiness(cfg, deps = {}) {
2230
+ const probeClaude = deps.probeClaude ?? claudeOnPath;
2231
+ const probeCodex = deps.probeCodex ?? codexOnPath;
2232
+ const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
2233
+ `));
2234
+ const connected = [
2235
+ ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
2236
+ ...isCodexEnabled(cfg) ? ["Codex"] : [],
2237
+ ...cfg.opencode ? ["opencode"] : []
2238
+ ];
2239
+ if (connected.length > 0) {
2240
+ if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
2241
+ warn(
2242
+ "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."
2243
+ );
2244
+ }
2245
+ return;
2246
+ }
2247
+ const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
2248
+ const installed = [
2249
+ ...claudeInstalled ? ["Claude Code"] : [],
2250
+ ...codexInstalled ? ["Codex"] : []
2251
+ ];
2252
+ const connectCommands = [
2253
+ ...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
2254
+ ...codexInstalled ? ["`cabane-companion connect codex`"] : []
2255
+ ];
2256
+ warn(
2257
+ "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 with " : "it with "}${connectCommands.join(" or ")} 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 with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
2258
+ );
2259
+ }
2260
+
2249
2261
  // src/api.ts
2250
2262
  var RETRY_BACKOFF_MS = [250, 750];
2251
2263
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
@@ -9065,11 +9077,9 @@ var CompanionSupervisor = class {
9065
9077
  // can never disagree with what the manifest advertises. Null until the first
9066
9078
  // probe (the heartbeat then falls back to the boot `claudeCode`).
9067
9079
  harnessSignals = null;
9068
- // CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
9069
- // NOT the cached beat signal — someone connecting right after installing Claude
9070
- // Code shouldn't be refused by a snapshot up to a heartbeat old.
9071
- probeClaudePresence;
9072
- probeCodexPresence;
9080
+ // Fresh per attempt, never the cached heartbeat signal: installing a harness
9081
+ // between attempts must be observed immediately.
9082
+ probePresence;
9073
9083
  exitFn;
9074
9084
  reexecFn;
9075
9085
  dispatcherFactory;
@@ -9101,8 +9111,7 @@ var CompanionSupervisor = class {
9101
9111
  this.log = opts.log;
9102
9112
  this.hub = opts.hub;
9103
9113
  this.claudeCode = opts.claudeCode ?? true;
9104
- this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
9105
- this.probeCodexPresence = opts.probeCodexPresence ?? codexOnPath;
9114
+ this.probePresence = opts.probePresence ?? probeHarnessPresence;
9106
9115
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
9107
9116
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
9108
9117
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -9800,10 +9809,11 @@ var CompanionSupervisor = class {
9800
9809
  // over SSE). Fail-soft by construction — `probeHarnessSignals` never throws — but
9801
9810
  // wrapped anyway so a harness refresh can never take a heartbeat down. Called on
9802
9811
  // the heartbeat cadence (fresh presence for the manifest + UI) and on demand.
9803
- async refreshHarnessStatuses() {
9812
+ async refreshHarnessStatuses(presence = {}) {
9804
9813
  try {
9805
9814
  const signals = await probeHarnessSignals(this.config, {
9806
- probeClaudePresence: this.probeClaudePresence
9815
+ probePresence: this.probePresence,
9816
+ presence
9807
9817
  });
9808
9818
  this.harnessSignals = signals;
9809
9819
  this.harnessVersions = {
@@ -9860,18 +9870,24 @@ var CompanionSupervisor = class {
9860
9870
  // reachable/valid input).
9861
9871
  async enableHarness(input) {
9862
9872
  let next;
9873
+ let checkedPresence = null;
9863
9874
  if (input.runtime === "claude-code") {
9864
- if (!await this.probeClaudePresence()) {
9875
+ const presence = input.presence ?? await this.probePresence("claude-code");
9876
+ if (presence.status !== "present") {
9865
9877
  return {
9866
9878
  ok: false,
9867
- 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."
9879
+ error: presenceReason("claude-code", presence.status),
9880
+ presence: true
9868
9881
  };
9869
9882
  }
9883
+ checkedPresence = { runtime: "claude-code", result: presence };
9870
9884
  next = { ...this.config, claudeCode: { enabled: true } };
9871
9885
  } else if (input.runtime === "codex") {
9872
- if (!await this.probeCodexPresence()) {
9873
- return { ok: false, error: absentLine("codex") };
9886
+ const presence = input.presence ?? await this.probePresence("codex");
9887
+ if (presence.status !== "present") {
9888
+ return { ok: false, error: presenceReason("codex", presence.status), presence: true };
9874
9889
  }
9890
+ checkedPresence = { runtime: "codex", result: presence };
9875
9891
  next = { ...this.config, codex: { enabled: true } };
9876
9892
  } else {
9877
9893
  const serverUrl = input.serverUrl.trim();
@@ -9895,7 +9911,9 @@ var CompanionSupervisor = class {
9895
9911
  this.config = next;
9896
9912
  saveConfig(next);
9897
9913
  this.rebuildDispatchers();
9898
- await this.refreshHarnessStatuses();
9914
+ await this.refreshHarnessStatuses(
9915
+ checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
9916
+ );
9899
9917
  this.kickHeartbeat();
9900
9918
  return { ok: true };
9901
9919
  }
@@ -9959,9 +9977,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
9959
9977
  }
9960
9978
  function defaultReexec() {
9961
9979
  clearRuntimeState();
9962
- void import("child_process").then(({ spawn: spawn6 }) => {
9980
+ void import("child_process").then(({ spawn: spawn5 }) => {
9963
9981
  try {
9964
- const child = spawn6(process.execPath, process.argv.slice(1), {
9982
+ const child = spawn5(process.execPath, process.argv.slice(1), {
9965
9983
  stdio: "inherit",
9966
9984
  detached: false
9967
9985
  });
@@ -10124,10 +10142,15 @@ async function createCompanionRuntime(opts = {}) {
10124
10142
  const currentConfig = supervisor.currentConfig();
10125
10143
  const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
10126
10144
  const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
10127
- const verdict = await shakeOutHarness(runtime, candidate);
10128
- if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
10145
+ const presence = runtime === "opencode" ? void 0 : await probeHarnessPresence(runtime);
10146
+ const verdict = await shakeOutHarness(runtime, candidate, {
10147
+ ...presence ? { presence } : {}
10148
+ });
10149
+ if (verdict === "absent" || verdict === "unusable") {
10150
+ return { ok: false, error: presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION) };
10151
+ }
10129
10152
  const result = await supervisor.enableHarness(
10130
- runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
10153
+ runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime, presence }
10131
10154
  );
10132
10155
  if (!result.ok) return { ok: false, error: result.error };
10133
10156
  return { ok: true, message: connectedLine(runtime, verdict) };
@@ -10192,7 +10215,7 @@ async function closeSurfaces(control, dashboard) {
10192
10215
  }
10193
10216
 
10194
10217
  // src/commands/daemon.ts
10195
- import { spawn as spawn5 } from "child_process";
10218
+ import { spawn as spawn4 } from "child_process";
10196
10219
  import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
10197
10220
 
10198
10221
  // src/cli-entry.ts
@@ -10299,7 +10322,7 @@ function defaultSpawnDetached(args) {
10299
10322
  mkdirSync13(cabaneDir(), { recursive: true });
10300
10323
  const logFd = openSync3(companionLogPath(), "a");
10301
10324
  try {
10302
- return spawn5(process.execPath, [cliPath, ...args], {
10325
+ return spawn4(process.execPath, [cliPath, ...args], {
10303
10326
  detached: true,
10304
10327
  stdio: ["ignore", logFd, logFd],
10305
10328
  env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
@@ -10397,14 +10420,15 @@ function reportAlreadyRunning(pid) {
10397
10420
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10398
10421
  }
10399
10422
  async function collectHarnessChoices(interactive) {
10400
- const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10401
- claudeOnPath().catch(() => false),
10402
- probeCliVersion("claude").catch(() => null),
10403
- probeCliVersion("codex").catch(() => null),
10423
+ const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
10424
+ probeHarnessPresence("claude-code").catch(() => ({ status: "absent" })),
10425
+ probeHarnessPresence("codex").catch(() => ({ status: "absent" })),
10404
10426
  probeDefaultOpencodeVersion()
10405
10427
  ]);
10428
+ const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
10429
+ const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
10406
10430
  const found = [
10407
- ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10431
+ ...claudeVersion ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10408
10432
  ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10409
10433
  ...opencodeVersion ? [
10410
10434
  {