@cabane/companion 0.6.45 → 0.6.47

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
@@ -823,6 +823,7 @@ var HARNESS_LABELS = {
823
823
  };
824
824
  var LABELS = HARNESS_LABELS;
825
825
  var OFFER_ORDER = ["claude-code", "codex", "opencode"];
826
+ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
826
827
  function parseHarnessRuntime(raw) {
827
828
  const key = raw.trim().toLowerCase().replace(/[\s_]+/g, "-");
828
829
  if (key === "claude-code" || key === "claudecode" || key === "claude") return "claude-code";
@@ -941,6 +942,15 @@ function deriveOpencode(signals, manifestHas) {
941
942
  enable: null
942
943
  };
943
944
  }
945
+ if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
946
+ return {
947
+ ...base,
948
+ state: "detected_not_exposed",
949
+ version: signals.opencodeVersion,
950
+ detail: signals.opencodeDetectedUrl,
951
+ enable: "opencode"
952
+ };
953
+ }
944
954
  return {
945
955
  ...base,
946
956
  state: "not_detected",
@@ -950,17 +960,32 @@ function deriveOpencode(signals, manifestHas) {
950
960
  };
951
961
  }
952
962
  var PROBE_TIMEOUT_MS = 4e3;
963
+ var DEFAULT_OPENCODE_PROBE_TIMEOUT_MS = 1e3;
964
+ function probeDefaultOpencodeVersion(probeOpencode = (url) => probeOpencodeVersion(url)) {
965
+ return withTimeout(
966
+ probeOpencode(DEFAULT_OPENCODE_SERVER_URL),
967
+ null,
968
+ DEFAULT_OPENCODE_PROBE_TIMEOUT_MS
969
+ );
970
+ }
971
+ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl, probeDefault = probeDefaultOpencodeVersion) {
972
+ const requested = requestedServerUrl?.trim();
973
+ if (requested) return requested;
974
+ if (configuredServerUrl) return configuredServerUrl;
975
+ return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
976
+ }
953
977
  async function probeHarnessSignals(cfg, deps = {}) {
954
978
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
955
979
  const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
956
980
  const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
957
981
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
958
- const serverUrl = cfg.opencode?.serverUrl;
982
+ const configuredServerUrl = cfg.opencode?.serverUrl;
983
+ const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
959
984
  const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
960
985
  withTimeout(probeClaudePresence(), false),
961
986
  withTimeout(probeClaudeVersion(), null),
962
987
  withTimeout(probeCodexVersion(), null),
963
- serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
988
+ configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
964
989
  ]);
965
990
  return {
966
991
  claudeOnPath: claudeOnPathResult,
@@ -973,13 +998,14 @@ async function probeHarnessSignals(cfg, deps = {}) {
973
998
  codexOnPath: codexVersion !== null,
974
999
  codexVersion,
975
1000
  codexEnabled: isCodexEnabled(cfg),
976
- opencodeConfigured: !!serverUrl,
1001
+ opencodeConfigured: !!configuredServerUrl,
977
1002
  // A version came back ⟺ the serve answered its health endpoint (CT584).
978
1003
  opencodeReachable: opencodeVersion !== null,
979
- opencodeVersion
1004
+ opencodeVersion,
1005
+ opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
980
1006
  };
981
1007
  }
982
- function withTimeout(promise, fallback) {
1008
+ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
983
1009
  return new Promise((resolve) => {
984
1010
  let settled = false;
985
1011
  const done = (v) => {
@@ -988,7 +1014,7 @@ function withTimeout(promise, fallback) {
988
1014
  resolve(v);
989
1015
  }
990
1016
  };
991
- const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
1017
+ const timer = setTimeout(() => done(fallback), timeoutMs);
992
1018
  timer.unref?.();
993
1019
  promise.then(
994
1020
  (v) => {
@@ -1316,13 +1342,14 @@ async function connect2(raw, opts = {}) {
1316
1342
  if (runtime === "claude-code") next = { ...cfg, claudeCode: { enabled: true } };
1317
1343
  else if (runtime === "codex") next = { ...cfg, codex: { enabled: true } };
1318
1344
  else {
1319
- const serverUrl = opts.serverUrl?.trim();
1345
+ const requestedServerUrl = opts.serverUrl?.trim();
1346
+ const serverUrl = await resolveOpencodeServerUrl(void 0, requestedServerUrl);
1320
1347
  if (!serverUrl) {
1321
1348
  throw new CompanionError(
1322
1349
  "opencode is addressed by URL \u2014 pass it: `cabane-companion connect opencode --url http://127.0.0.1:4096`."
1323
1350
  );
1324
1351
  }
1325
- if (await probeOpencodeVersion(serverUrl) === null) {
1352
+ if (requestedServerUrl && await probeOpencodeVersion(serverUrl) === null) {
1326
1353
  throw new CompanionError(
1327
1354
  `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
1328
1355
  );
@@ -9004,6 +9031,7 @@ var CompanionSupervisor = class {
9004
9031
  // NOT the cached beat signal — someone connecting right after installing Claude
9005
9032
  // Code shouldn't be refused by a snapshot up to a heartbeat old.
9006
9033
  probeClaudePresence;
9034
+ probeCodexPresence;
9007
9035
  exitFn;
9008
9036
  reexecFn;
9009
9037
  dispatcherFactory;
@@ -9036,6 +9064,7 @@ var CompanionSupervisor = class {
9036
9064
  this.hub = opts.hub;
9037
9065
  this.claudeCode = opts.claudeCode ?? true;
9038
9066
  this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
9067
+ this.probeCodexPresence = opts.probeCodexPresence ?? codexOnPath;
9039
9068
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
9040
9069
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
9041
9070
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -9783,7 +9812,8 @@ var CompanionSupervisor = class {
9783
9812
  // - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
9784
9813
  // on PATH. This is the callable the terminal offer ("We found Claude Code.
9785
9814
  // Connect it? [Y/n]") and the web pairing flow both wire to.
9786
- // - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
9815
+ // - codex: set `codex.enabled` after checking `codex` is on PATH (the CLI +
9816
+ // `codex login` remain the user's).
9787
9817
  // - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
9788
9818
  // so we never advertise a serve that isn't there. An unreachable URL is a
9789
9819
  // typed failure the UI shows in place, not a silent bad write.
@@ -9801,6 +9831,9 @@ var CompanionSupervisor = class {
9801
9831
  }
9802
9832
  next = { ...this.config, claudeCode: { enabled: true } };
9803
9833
  } else if (input.runtime === "codex") {
9834
+ if (!await this.probeCodexPresence()) {
9835
+ return { ok: false, error: absentLine("codex") };
9836
+ }
9804
9837
  next = { ...this.config, codex: { enabled: true } };
9805
9838
  } else {
9806
9839
  const serverUrl = input.serverUrl.trim();
@@ -10050,11 +10083,13 @@ async function createCompanionRuntime(opts = {}) {
10050
10083
  });
10051
10084
  await supervisor.start();
10052
10085
  const connectHarness = async (runtime, serverUrl) => {
10053
- const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
10086
+ const currentConfig = supervisor.currentConfig();
10087
+ const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
10088
+ const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
10054
10089
  const verdict = await shakeOutHarness(runtime, candidate);
10055
10090
  if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
10056
10091
  const result = await supervisor.enableHarness(
10057
- runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
10092
+ runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
10058
10093
  );
10059
10094
  if (!result.ok) return { ok: false, error: result.error };
10060
10095
  return { ok: true, message: connectedLine(runtime, verdict) };
@@ -10324,17 +10359,22 @@ function reportAlreadyRunning(pid) {
10324
10359
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10325
10360
  }
10326
10361
  async function collectHarnessChoices(interactive) {
10327
- const opencodeUrl = "http://127.0.0.1:4096";
10328
10362
  const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10329
10363
  claudeOnPath().catch(() => false),
10330
10364
  probeCliVersion("claude").catch(() => null),
10331
10365
  probeCliVersion("codex").catch(() => null),
10332
- probeOpencodeVersion(opencodeUrl).catch(() => null)
10366
+ probeDefaultOpencodeVersion()
10333
10367
  ]);
10334
10368
  const found = [
10335
10369
  ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10336
10370
  ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10337
- ...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
10371
+ ...opencodeVersion ? [
10372
+ {
10373
+ runtime: "opencode",
10374
+ version: opencodeVersion,
10375
+ serverUrl: DEFAULT_OPENCODE_SERVER_URL
10376
+ }
10377
+ ] : []
10338
10378
  ].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
10339
10379
  if (found.length === 0) {
10340
10380
  write(
package/dist/runtime.js CHANGED
@@ -1300,6 +1300,7 @@ var HARNESS_LABELS = {
1300
1300
  opencode: "OpenCode"
1301
1301
  };
1302
1302
  var LABELS = HARNESS_LABELS;
1303
+ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
1303
1304
  function deriveHarnessSnapshot(signals) {
1304
1305
  const advertised = new Set(
1305
1306
  buildCompanionManifest({
@@ -1411,6 +1412,15 @@ function deriveOpencode(signals, manifestHas) {
1411
1412
  enable: null
1412
1413
  };
1413
1414
  }
1415
+ if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
1416
+ return {
1417
+ ...base,
1418
+ state: "detected_not_exposed",
1419
+ version: signals.opencodeVersion,
1420
+ detail: signals.opencodeDetectedUrl,
1421
+ enable: "opencode"
1422
+ };
1423
+ }
1414
1424
  return {
1415
1425
  ...base,
1416
1426
  state: "not_detected",
@@ -1420,17 +1430,32 @@ function deriveOpencode(signals, manifestHas) {
1420
1430
  };
1421
1431
  }
1422
1432
  var PROBE_TIMEOUT_MS = 4e3;
1433
+ var DEFAULT_OPENCODE_PROBE_TIMEOUT_MS = 1e3;
1434
+ function probeDefaultOpencodeVersion(probeOpencode = (url) => probeOpencodeVersion(url)) {
1435
+ return withTimeout(
1436
+ probeOpencode(DEFAULT_OPENCODE_SERVER_URL),
1437
+ null,
1438
+ DEFAULT_OPENCODE_PROBE_TIMEOUT_MS
1439
+ );
1440
+ }
1441
+ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl, probeDefault = probeDefaultOpencodeVersion) {
1442
+ const requested = requestedServerUrl?.trim();
1443
+ if (requested) return requested;
1444
+ if (configuredServerUrl) return configuredServerUrl;
1445
+ return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
1446
+ }
1423
1447
  async function probeHarnessSignals(cfg, deps = {}) {
1424
1448
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
1425
1449
  const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
1426
1450
  const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
1427
1451
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
1428
- const serverUrl = cfg.opencode?.serverUrl;
1452
+ const configuredServerUrl = cfg.opencode?.serverUrl;
1453
+ const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
1429
1454
  const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
1430
1455
  withTimeout(probeClaudePresence(), false),
1431
1456
  withTimeout(probeClaudeVersion(), null),
1432
1457
  withTimeout(probeCodexVersion(), null),
1433
- serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
1458
+ configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
1434
1459
  ]);
1435
1460
  return {
1436
1461
  claudeOnPath: claudeOnPathResult,
@@ -1443,13 +1468,14 @@ async function probeHarnessSignals(cfg, deps = {}) {
1443
1468
  codexOnPath: codexVersion !== null,
1444
1469
  codexVersion,
1445
1470
  codexEnabled: isCodexEnabled(cfg),
1446
- opencodeConfigured: !!serverUrl,
1471
+ opencodeConfigured: !!configuredServerUrl,
1447
1472
  // A version came back ⟺ the serve answered its health endpoint (CT584).
1448
1473
  opencodeReachable: opencodeVersion !== null,
1449
- opencodeVersion
1474
+ opencodeVersion,
1475
+ opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
1450
1476
  };
1451
1477
  }
1452
- function withTimeout(promise, fallback) {
1478
+ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
1453
1479
  return new Promise((resolve) => {
1454
1480
  let settled = false;
1455
1481
  const done = (v) => {
@@ -1458,7 +1484,7 @@ function withTimeout(promise, fallback) {
1458
1484
  resolve(v);
1459
1485
  }
1460
1486
  };
1461
- const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
1487
+ const timer = setTimeout(() => done(fallback), timeoutMs);
1462
1488
  timer.unref?.();
1463
1489
  promise.then(
1464
1490
  (v) => {
@@ -8513,6 +8539,7 @@ var CompanionSupervisor = class {
8513
8539
  // NOT the cached beat signal — someone connecting right after installing Claude
8514
8540
  // Code shouldn't be refused by a snapshot up to a heartbeat old.
8515
8541
  probeClaudePresence;
8542
+ probeCodexPresence;
8516
8543
  exitFn;
8517
8544
  reexecFn;
8518
8545
  dispatcherFactory;
@@ -8545,6 +8572,7 @@ var CompanionSupervisor = class {
8545
8572
  this.hub = opts.hub;
8546
8573
  this.claudeCode = opts.claudeCode ?? true;
8547
8574
  this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
8575
+ this.probeCodexPresence = opts.probeCodexPresence ?? codexOnPath;
8548
8576
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
8549
8577
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
8550
8578
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -9292,7 +9320,8 @@ var CompanionSupervisor = class {
9292
9320
  // - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
9293
9321
  // on PATH. This is the callable the terminal offer ("We found Claude Code.
9294
9322
  // Connect it? [Y/n]") and the web pairing flow both wire to.
9295
- // - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
9323
+ // - codex: set `codex.enabled` after checking `codex` is on PATH (the CLI +
9324
+ // `codex login` remain the user's).
9296
9325
  // - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
9297
9326
  // so we never advertise a serve that isn't there. An unreachable URL is a
9298
9327
  // typed failure the UI shows in place, not a silent bad write.
@@ -9310,6 +9339,9 @@ var CompanionSupervisor = class {
9310
9339
  }
9311
9340
  next = { ...this.config, claudeCode: { enabled: true } };
9312
9341
  } else if (input.runtime === "codex") {
9342
+ if (!await this.probeCodexPresence()) {
9343
+ return { ok: false, error: absentLine("codex") };
9344
+ }
9313
9345
  next = { ...this.config, codex: { enabled: true } };
9314
9346
  } else {
9315
9347
  const serverUrl = input.serverUrl.trim();
@@ -9559,11 +9591,13 @@ async function createCompanionRuntime(opts = {}) {
9559
9591
  });
9560
9592
  await supervisor.start();
9561
9593
  const connectHarness = async (runtime, serverUrl) => {
9562
- const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
9594
+ const currentConfig = supervisor.currentConfig();
9595
+ const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
9596
+ const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
9563
9597
  const verdict = await shakeOutHarness(runtime, candidate);
9564
9598
  if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
9565
9599
  const result = await supervisor.enableHarness(
9566
- runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
9600
+ runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
9567
9601
  );
9568
9602
  if (!result.ok) return { ok: false, error: result.error };
9569
9603
  return { ok: true, message: connectedLine(runtime, verdict) };
@@ -158,6 +158,7 @@ function renderHarnesses() {
158
158
  const input = el('input');
159
159
  input.type = 'text';
160
160
  input.placeholder = 'opencode serve URL (e.g. http://127.0.0.1:4096)';
161
+ if (h.state === 'detected_not_exposed') input.value = h.detail;
161
162
  const btn = el('button', 'primary small', 'Add opencode');
162
163
  const submit = () => {
163
164
  const url = input.value.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.45",
3
+ "version": "0.6.47",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",