@cabane/companion 0.6.46 → 0.6.48

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
@@ -149,6 +149,7 @@ var runPrepareHook = (hook, input) => {
149
149
  CABANE_AGENT_ID: input.agentId,
150
150
  CABANE_AGENT_USERNAME: input.agentUsername,
151
151
  CABANE_RUNTIME: input.runtime ?? "",
152
+ CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
152
153
  // CT319: the conversation anchor is gone. These are kept as DEPRECATED
153
154
  // back-compat constants so an old user prepare script that still reads
154
155
  // them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
@@ -823,6 +824,7 @@ var HARNESS_LABELS = {
823
824
  };
824
825
  var LABELS = HARNESS_LABELS;
825
826
  var OFFER_ORDER = ["claude-code", "codex", "opencode"];
827
+ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
826
828
  function parseHarnessRuntime(raw) {
827
829
  const key = raw.trim().toLowerCase().replace(/[\s_]+/g, "-");
828
830
  if (key === "claude-code" || key === "claudecode" || key === "claude") return "claude-code";
@@ -941,6 +943,15 @@ function deriveOpencode(signals, manifestHas) {
941
943
  enable: null
942
944
  };
943
945
  }
946
+ if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
947
+ return {
948
+ ...base,
949
+ state: "detected_not_exposed",
950
+ version: signals.opencodeVersion,
951
+ detail: signals.opencodeDetectedUrl,
952
+ enable: "opencode"
953
+ };
954
+ }
944
955
  return {
945
956
  ...base,
946
957
  state: "not_detected",
@@ -950,17 +961,32 @@ function deriveOpencode(signals, manifestHas) {
950
961
  };
951
962
  }
952
963
  var PROBE_TIMEOUT_MS = 4e3;
964
+ var DEFAULT_OPENCODE_PROBE_TIMEOUT_MS = 1e3;
965
+ function probeDefaultOpencodeVersion(probeOpencode = (url) => probeOpencodeVersion(url)) {
966
+ return withTimeout(
967
+ probeOpencode(DEFAULT_OPENCODE_SERVER_URL),
968
+ null,
969
+ DEFAULT_OPENCODE_PROBE_TIMEOUT_MS
970
+ );
971
+ }
972
+ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl, probeDefault = probeDefaultOpencodeVersion) {
973
+ const requested = requestedServerUrl?.trim();
974
+ if (requested) return requested;
975
+ if (configuredServerUrl) return configuredServerUrl;
976
+ return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
977
+ }
953
978
  async function probeHarnessSignals(cfg, deps = {}) {
954
979
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
955
980
  const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
956
981
  const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
957
982
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
958
- const serverUrl = cfg.opencode?.serverUrl;
983
+ const configuredServerUrl = cfg.opencode?.serverUrl;
984
+ const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
959
985
  const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
960
986
  withTimeout(probeClaudePresence(), false),
961
987
  withTimeout(probeClaudeVersion(), null),
962
988
  withTimeout(probeCodexVersion(), null),
963
- serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
989
+ configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
964
990
  ]);
965
991
  return {
966
992
  claudeOnPath: claudeOnPathResult,
@@ -973,13 +999,14 @@ async function probeHarnessSignals(cfg, deps = {}) {
973
999
  codexOnPath: codexVersion !== null,
974
1000
  codexVersion,
975
1001
  codexEnabled: isCodexEnabled(cfg),
976
- opencodeConfigured: !!serverUrl,
1002
+ opencodeConfigured: !!configuredServerUrl,
977
1003
  // A version came back ⟺ the serve answered its health endpoint (CT584).
978
1004
  opencodeReachable: opencodeVersion !== null,
979
- opencodeVersion
1005
+ opencodeVersion,
1006
+ opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
980
1007
  };
981
1008
  }
982
- function withTimeout(promise, fallback) {
1009
+ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
983
1010
  return new Promise((resolve) => {
984
1011
  let settled = false;
985
1012
  const done = (v) => {
@@ -988,7 +1015,7 @@ function withTimeout(promise, fallback) {
988
1015
  resolve(v);
989
1016
  }
990
1017
  };
991
- const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
1018
+ const timer = setTimeout(() => done(fallback), timeoutMs);
992
1019
  timer.unref?.();
993
1020
  promise.then(
994
1021
  (v) => {
@@ -1316,13 +1343,14 @@ async function connect2(raw, opts = {}) {
1316
1343
  if (runtime === "claude-code") next = { ...cfg, claudeCode: { enabled: true } };
1317
1344
  else if (runtime === "codex") next = { ...cfg, codex: { enabled: true } };
1318
1345
  else {
1319
- const serverUrl = opts.serverUrl?.trim();
1346
+ const requestedServerUrl = opts.serverUrl?.trim();
1347
+ const serverUrl = await resolveOpencodeServerUrl(void 0, requestedServerUrl);
1320
1348
  if (!serverUrl) {
1321
1349
  throw new CompanionError(
1322
1350
  "opencode is addressed by URL \u2014 pass it: `cabane-companion connect opencode --url http://127.0.0.1:4096`."
1323
1351
  );
1324
1352
  }
1325
- if (await probeOpencodeVersion(serverUrl) === null) {
1353
+ if (requestedServerUrl && await probeOpencodeVersion(serverUrl) === null) {
1326
1354
  throw new CompanionError(
1327
1355
  `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
1328
1356
  );
@@ -5750,7 +5778,8 @@ var ENV_ENVELOPE_KEYS = [
5750
5778
  "CABANE_PLAYGROUND_BIN",
5751
5779
  "CABANE_CONVERSATION_ID",
5752
5780
  "CABANE_AGENT_ID",
5753
- "CABANE_CONVERSATION_TITLE"
5781
+ "CABANE_CONVERSATION_TITLE",
5782
+ "CABANE_HOST_ACCESS"
5754
5783
  ];
5755
5784
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
5756
5785
  const { policy, config } = req;
@@ -7834,6 +7863,7 @@ var Dispatcher = class {
7834
7863
  agentId: payload.agentId,
7835
7864
  agentUsername: this.opts.agentUsername,
7836
7865
  runtime: turnContext.runtime,
7866
+ hostAccess: turnContext.policy.hostFs,
7837
7867
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7838
7868
  title: turnContext.conversation.title,
7839
7869
  messageBody: message.body,
@@ -7895,6 +7925,7 @@ ${reason}`,
7895
7925
  agentId: payload.agentId,
7896
7926
  agentUsername: this.opts.agentUsername,
7897
7927
  runtime: turnContext.runtime,
7928
+ hostAccess: turnContext.policy.hostFs,
7898
7929
  // CT317/CT319: the trigger message's referenced-entry paths — what the
7899
7930
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
7900
7931
  // an older API. The conversation anchor is gone (CT319).
@@ -7938,7 +7969,10 @@ ${reason}`,
7938
7969
  }
7939
7970
  }
7940
7971
  }
7941
- const turnEnv = hookEnv;
7972
+ const turnEnv = {
7973
+ ...hookEnv,
7974
+ CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
7975
+ };
7942
7976
  const key = runKey(payload.conversationId, payload.agentId);
7943
7977
  const abortController = new AbortController();
7944
7978
  this.aborts.set(key, abortController);
@@ -8001,9 +8035,8 @@ ${reason}`,
8001
8035
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
8002
8036
  // SJ524: the hook-resolved cwd overrides the static local cwd.
8003
8037
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
8004
- // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
8005
- // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
8006
- ...turnEnv ? { env: turnEnv } : {},
8038
+ // CT1103: the prepare-hook env plus the authoritative host-access token.
8039
+ env: turnEnv,
8007
8040
  mcpServers: resolvedMcpServers,
8008
8041
  summonServer,
8009
8042
  // CT238: this turn's conversation, forwarded as the active-conversation
@@ -10056,11 +10089,13 @@ async function createCompanionRuntime(opts = {}) {
10056
10089
  });
10057
10090
  await supervisor.start();
10058
10091
  const connectHarness = async (runtime, serverUrl) => {
10059
- const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
10092
+ const currentConfig = supervisor.currentConfig();
10093
+ const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
10094
+ const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
10060
10095
  const verdict = await shakeOutHarness(runtime, candidate);
10061
10096
  if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
10062
10097
  const result = await supervisor.enableHarness(
10063
- runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
10098
+ runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
10064
10099
  );
10065
10100
  if (!result.ok) return { ok: false, error: result.error };
10066
10101
  return { ok: true, message: connectedLine(runtime, verdict) };
@@ -10330,17 +10365,22 @@ function reportAlreadyRunning(pid) {
10330
10365
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10331
10366
  }
10332
10367
  async function collectHarnessChoices(interactive) {
10333
- const opencodeUrl = "http://127.0.0.1:4096";
10334
10368
  const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10335
10369
  claudeOnPath().catch(() => false),
10336
10370
  probeCliVersion("claude").catch(() => null),
10337
10371
  probeCliVersion("codex").catch(() => null),
10338
- probeOpencodeVersion(opencodeUrl).catch(() => null)
10372
+ probeDefaultOpencodeVersion()
10339
10373
  ]);
10340
10374
  const found = [
10341
10375
  ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10342
10376
  ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10343
- ...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
10377
+ ...opencodeVersion ? [
10378
+ {
10379
+ runtime: "opencode",
10380
+ version: opencodeVersion,
10381
+ serverUrl: DEFAULT_OPENCODE_SERVER_URL
10382
+ }
10383
+ ] : []
10344
10384
  ].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
10345
10385
  if (found.length === 0) {
10346
10386
  write(
package/dist/runtime.js CHANGED
@@ -141,6 +141,7 @@ var runPrepareHook = (hook, input) => {
141
141
  CABANE_AGENT_ID: input.agentId,
142
142
  CABANE_AGENT_USERNAME: input.agentUsername,
143
143
  CABANE_RUNTIME: input.runtime ?? "",
144
+ CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
144
145
  // CT319: the conversation anchor is gone. These are kept as DEPRECATED
145
146
  // back-compat constants so an old user prepare script that still reads
146
147
  // them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
@@ -1300,6 +1301,7 @@ var HARNESS_LABELS = {
1300
1301
  opencode: "OpenCode"
1301
1302
  };
1302
1303
  var LABELS = HARNESS_LABELS;
1304
+ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
1303
1305
  function deriveHarnessSnapshot(signals) {
1304
1306
  const advertised = new Set(
1305
1307
  buildCompanionManifest({
@@ -1411,6 +1413,15 @@ function deriveOpencode(signals, manifestHas) {
1411
1413
  enable: null
1412
1414
  };
1413
1415
  }
1416
+ if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
1417
+ return {
1418
+ ...base,
1419
+ state: "detected_not_exposed",
1420
+ version: signals.opencodeVersion,
1421
+ detail: signals.opencodeDetectedUrl,
1422
+ enable: "opencode"
1423
+ };
1424
+ }
1414
1425
  return {
1415
1426
  ...base,
1416
1427
  state: "not_detected",
@@ -1420,17 +1431,32 @@ function deriveOpencode(signals, manifestHas) {
1420
1431
  };
1421
1432
  }
1422
1433
  var PROBE_TIMEOUT_MS = 4e3;
1434
+ var DEFAULT_OPENCODE_PROBE_TIMEOUT_MS = 1e3;
1435
+ function probeDefaultOpencodeVersion(probeOpencode = (url) => probeOpencodeVersion(url)) {
1436
+ return withTimeout(
1437
+ probeOpencode(DEFAULT_OPENCODE_SERVER_URL),
1438
+ null,
1439
+ DEFAULT_OPENCODE_PROBE_TIMEOUT_MS
1440
+ );
1441
+ }
1442
+ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl, probeDefault = probeDefaultOpencodeVersion) {
1443
+ const requested = requestedServerUrl?.trim();
1444
+ if (requested) return requested;
1445
+ if (configuredServerUrl) return configuredServerUrl;
1446
+ return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
1447
+ }
1423
1448
  async function probeHarnessSignals(cfg, deps = {}) {
1424
1449
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
1425
1450
  const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
1426
1451
  const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
1427
1452
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
1428
- const serverUrl = cfg.opencode?.serverUrl;
1453
+ const configuredServerUrl = cfg.opencode?.serverUrl;
1454
+ const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
1429
1455
  const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
1430
1456
  withTimeout(probeClaudePresence(), false),
1431
1457
  withTimeout(probeClaudeVersion(), null),
1432
1458
  withTimeout(probeCodexVersion(), null),
1433
- serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
1459
+ configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
1434
1460
  ]);
1435
1461
  return {
1436
1462
  claudeOnPath: claudeOnPathResult,
@@ -1443,13 +1469,14 @@ async function probeHarnessSignals(cfg, deps = {}) {
1443
1469
  codexOnPath: codexVersion !== null,
1444
1470
  codexVersion,
1445
1471
  codexEnabled: isCodexEnabled(cfg),
1446
- opencodeConfigured: !!serverUrl,
1472
+ opencodeConfigured: !!configuredServerUrl,
1447
1473
  // A version came back ⟺ the serve answered its health endpoint (CT584).
1448
1474
  opencodeReachable: opencodeVersion !== null,
1449
- opencodeVersion
1475
+ opencodeVersion,
1476
+ opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
1450
1477
  };
1451
1478
  }
1452
- function withTimeout(promise, fallback) {
1479
+ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
1453
1480
  return new Promise((resolve) => {
1454
1481
  let settled = false;
1455
1482
  const done = (v) => {
@@ -1458,7 +1485,7 @@ function withTimeout(promise, fallback) {
1458
1485
  resolve(v);
1459
1486
  }
1460
1487
  };
1461
- const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
1488
+ const timer = setTimeout(() => done(fallback), timeoutMs);
1462
1489
  timer.unref?.();
1463
1490
  promise.then(
1464
1491
  (v) => {
@@ -5259,7 +5286,8 @@ var ENV_ENVELOPE_KEYS = [
5259
5286
  "CABANE_PLAYGROUND_BIN",
5260
5287
  "CABANE_CONVERSATION_ID",
5261
5288
  "CABANE_AGENT_ID",
5262
- "CABANE_CONVERSATION_TITLE"
5289
+ "CABANE_CONVERSATION_TITLE",
5290
+ "CABANE_HOST_ACCESS"
5263
5291
  ];
5264
5292
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
5265
5293
  const { policy, config } = req;
@@ -7343,6 +7371,7 @@ var Dispatcher = class {
7343
7371
  agentId: payload.agentId,
7344
7372
  agentUsername: this.opts.agentUsername,
7345
7373
  runtime: turnContext.runtime,
7374
+ hostAccess: turnContext.policy.hostFs,
7346
7375
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7347
7376
  title: turnContext.conversation.title,
7348
7377
  messageBody: message.body,
@@ -7404,6 +7433,7 @@ ${reason}`,
7404
7433
  agentId: payload.agentId,
7405
7434
  agentUsername: this.opts.agentUsername,
7406
7435
  runtime: turnContext.runtime,
7436
+ hostAccess: turnContext.policy.hostFs,
7407
7437
  // CT317/CT319: the trigger message's referenced-entry paths — what the
7408
7438
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
7409
7439
  // an older API. The conversation anchor is gone (CT319).
@@ -7447,7 +7477,10 @@ ${reason}`,
7447
7477
  }
7448
7478
  }
7449
7479
  }
7450
- const turnEnv = hookEnv;
7480
+ const turnEnv = {
7481
+ ...hookEnv,
7482
+ CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
7483
+ };
7451
7484
  const key = runKey(payload.conversationId, payload.agentId);
7452
7485
  const abortController = new AbortController();
7453
7486
  this.aborts.set(key, abortController);
@@ -7510,9 +7543,8 @@ ${reason}`,
7510
7543
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
7511
7544
  // SJ524: the hook-resolved cwd overrides the static local cwd.
7512
7545
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
7513
- // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
7514
- // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
7515
- ...turnEnv ? { env: turnEnv } : {},
7546
+ // CT1103: the prepare-hook env plus the authoritative host-access token.
7547
+ env: turnEnv,
7516
7548
  mcpServers: resolvedMcpServers,
7517
7549
  summonServer,
7518
7550
  // CT238: this turn's conversation, forwarded as the active-conversation
@@ -9565,11 +9597,13 @@ async function createCompanionRuntime(opts = {}) {
9565
9597
  });
9566
9598
  await supervisor.start();
9567
9599
  const connectHarness = async (runtime, serverUrl) => {
9568
- const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
9600
+ const currentConfig = supervisor.currentConfig();
9601
+ const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
9602
+ const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
9569
9603
  const verdict = await shakeOutHarness(runtime, candidate);
9570
9604
  if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
9571
9605
  const result = await supervisor.enableHarness(
9572
- runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
9606
+ runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
9573
9607
  );
9574
9608
  if (!result.ok) return { ok: false, error: result.error };
9575
9609
  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.46",
3
+ "version": "0.6.48",
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",