@cabane/companion 0.6.40 → 0.6.41

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
@@ -819,7 +819,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
819
819
  var HARNESS_LABELS = {
820
820
  "claude-code": "Claude Code",
821
821
  codex: "Codex",
822
- opencode: "opencode"
822
+ opencode: "OpenCode"
823
823
  };
824
824
  var LABELS = HARNESS_LABELS;
825
825
  var OFFER_ORDER = ["claude-code", "codex", "opencode"];
@@ -949,9 +949,6 @@ function deriveOpencode(signals, manifestHas) {
949
949
  enable: "opencode"
950
950
  };
951
951
  }
952
- function detectedRuntimesFor(snapshot) {
953
- return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
954
- }
955
952
  var PROBE_TIMEOUT_MS = 4e3;
956
953
  async function probeHarnessSignals(cfg, deps = {}) {
957
954
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -1198,8 +1195,6 @@ function blank() {
1198
1195
  process.stdout.write("\n");
1199
1196
  }
1200
1197
  var tick = (rest) => `${pc.green("\u2713")} ${rest}`;
1201
- var arrow = (rest) => `${pc.yellow("\u2192")} ${rest}`;
1202
- var bang = (rest) => `${pc.yellow("!")} ${rest}`;
1203
1198
  function tildePath(path) {
1204
1199
  const home = homedir2();
1205
1200
  if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
@@ -9140,13 +9135,7 @@ var CompanionSupervisor = class {
9140
9135
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
9141
9136
  // A null (probe failed / no opencode) omits the field, and the server then
9142
9137
  // leaves this device's stored availability untouched.
9143
- ...opencodeModels !== null ? { models: opencodeModels } : {},
9144
- // CT1082: what this machine has that the user hasn't connected, so the web
9145
- // UI can offer it without a companion round-trip. Sent only once a probe has
9146
- // actually landed (`harnessSignals` non-null) — an absent field means "we
9147
- // didn't look this beat" and leaves the server's stored suggestion alone,
9148
- // the same fail-soft contract `models` keeps.
9149
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
9138
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
9150
9139
  });
9151
9140
  this.hub.setDevice({ deviceId: res.deviceId });
9152
9141
  this.deviceId = res.deviceId;
@@ -10241,6 +10230,7 @@ async function startScript(opts, interactive) {
10241
10230
  return;
10242
10231
  }
10243
10232
  blank();
10233
+ const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
10244
10234
  let paired = null;
10245
10235
  if (!isDevicePaired()) {
10246
10236
  paired = await pairHere(opts, interactive);
@@ -10262,12 +10252,18 @@ async function startScript(opts, interactive) {
10262
10252
  return;
10263
10253
  }
10264
10254
  const runtime = result.runtime;
10255
+ const connected = [];
10256
+ for (const choice of held) {
10257
+ if (!choice.accepted) continue;
10258
+ const outcome = await runtime.connectHarness(choice.runtime, choice.serverUrl);
10259
+ if (outcome.ok) connected.push(HARNESS_LABELS[choice.runtime]);
10260
+ else write(`${INDENT}${outcome.error}`);
10261
+ }
10265
10262
  await runtime.heartbeatNow();
10266
- const offered = await runConnectorOffer(runtime, { interactive, justPaired });
10267
10263
  if (interactive && !opts.foreground) {
10268
10264
  await handOffToBackground(runtime, {
10269
10265
  justPaired,
10270
- spaceAbove: justPaired || offered.printedSomething
10266
+ connected
10271
10267
  });
10272
10268
  return;
10273
10269
  }
@@ -10279,7 +10275,7 @@ async function startScript(opts, interactive) {
10279
10275
  write(`${INDENT}Listening for messages\u2026`);
10280
10276
  setConsoleLogging(true);
10281
10277
  } else {
10282
- if (!offered.printedSomething) blank();
10278
+ blank();
10283
10279
  write(
10284
10280
  `${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
10285
10281
  );
@@ -10301,11 +10297,60 @@ function reportAlreadyRunning(pid) {
10301
10297
  write("Stop it first with `cabane-companion stop` if you want to relaunch.");
10302
10298
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10303
10299
  }
10300
+ async function collectHarnessChoices(interactive) {
10301
+ const opencodeUrl = "http://127.0.0.1:4096";
10302
+ const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10303
+ claudeOnPath().catch(() => false),
10304
+ probeCliVersion("claude").catch(() => null),
10305
+ probeCliVersion("codex").catch(() => null),
10306
+ probeOpencodeVersion(opencodeUrl).catch(() => null)
10307
+ ]);
10308
+ const found = [
10309
+ ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10310
+ ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10311
+ ...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
10312
+ ].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
10313
+ if (found.length === 0) {
10314
+ write(
10315
+ `${INDENT}No coding agent found on this machine yet \u2014 you can pair now and add one later.`
10316
+ );
10317
+ blank();
10318
+ return [];
10319
+ }
10320
+ write(`${INDENT}Welcome to Cabane. Let's set up this machine \u2014 two steps, both right here:`);
10321
+ blank();
10322
+ write(`${DATA_INDENT}1. Choose which coding agents Cabane can use on this machine`);
10323
+ write(`${DATA_INDENT}2. Pair this machine with your Cabane account`);
10324
+ blank();
10325
+ const choices = [];
10326
+ for (const harness of found) {
10327
+ const phrase = prePairFoundPhrase(harness);
10328
+ if (!interactive) {
10329
+ write(`${INDENT}${phrase}. Add it with: ${connectCommand(harness.runtime)}`);
10330
+ choices.push({ ...harness, accepted: false });
10331
+ continue;
10332
+ }
10333
+ const accepted = await confirm(`${phrase}. Make it available in Cabane?`);
10334
+ choices.push({ ...harness, accepted });
10335
+ if (accepted) {
10336
+ write(
10337
+ INDENT + tick(`${HARNESS_LABELS[harness.runtime]} \u2014 available once this device is paired.`)
10338
+ );
10339
+ } else {
10340
+ write(`${INDENT}Skipped. Add it any time with: ${connectCommand(harness.runtime)}`);
10341
+ }
10342
+ blank();
10343
+ }
10344
+ return choices;
10345
+ }
10346
+ function prePairFoundPhrase(harness) {
10347
+ if (harness.runtime === "opencode") return `We found OpenCode at ${harness.serverUrl}`;
10348
+ const label = HARNESS_LABELS[harness.runtime];
10349
+ return harness.version ? `We found ${label} on this machine (${harness.version})` : `We found ${label} on this machine`;
10350
+ }
10304
10351
  async function pairHere(opts, interactive) {
10305
10352
  const baseUrl = resolvePairBaseUrl(opts.server);
10306
10353
  const label = deviceLabelFromHostname(hostname2());
10307
- write(`${INDENT}Not paired yet.`);
10308
- blank();
10309
10354
  const aborter = new AbortController();
10310
10355
  const onSigint = () => aborter.abort();
10311
10356
  process.on("SIGINT", onSigint);
@@ -10318,7 +10363,7 @@ async function pairHere(opts, interactive) {
10318
10363
  if (attempt === 1) printFirstCode(code);
10319
10364
  else printFreshCode(code);
10320
10365
  spinner.start(
10321
- `Waiting for you to confirm it in Cabane\u2026 (the code is good for ${minutes} minutes)`
10366
+ `Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
10322
10367
  );
10323
10368
  try {
10324
10369
  const device = await awaitEnrollment(baseUrl, code, { signal: aborter.signal });
@@ -10328,7 +10373,7 @@ async function pairHere(opts, interactive) {
10328
10373
  if (err instanceof EnrollmentCancelledError) {
10329
10374
  spinner.clear();
10330
10375
  blank();
10331
- write(`${INDENT}Pairing cancelled. Run \`cabane-companion start\` when you're ready.`);
10376
+ write(`${INDENT}Pairing cancelled. Run cabane-companion start when you're ready.`);
10332
10377
  return null;
10333
10378
  }
10334
10379
  if (!(err instanceof EnrollmentExpiredError)) throw err;
@@ -10339,8 +10384,9 @@ async function pairHere(opts, interactive) {
10339
10384
  }
10340
10385
  spinner.freeze();
10341
10386
  blank();
10342
- write(`${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here.`);
10343
- write(`${INDENT}Run \`cabane-companion start\` again when you're ready.`);
10387
+ write(
10388
+ `${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here. Run cabane-companion start again when you're ready.`
10389
+ );
10344
10390
  return null;
10345
10391
  }
10346
10392
  }
@@ -10350,11 +10396,11 @@ async function pairHere(opts, interactive) {
10350
10396
  }
10351
10397
  }
10352
10398
  function printFirstCode(code) {
10353
- write(`${INDENT}Enter this code in Cabane:`);
10399
+ write(`${INDENT}Now let's pair this machine. Enter this code in Cabane:`);
10354
10400
  blank();
10355
10401
  write(`${DATA_INDENT}${code.userCode}`);
10356
10402
  blank();
10357
- write(`${INDENT}or open this link:`);
10403
+ write(`${INDENT}or, if Cabane isn\u2019t open:`);
10358
10404
  blank();
10359
10405
  write(`${DATA_INDENT}${code.verificationUriComplete}`);
10360
10406
  blank();
@@ -10369,54 +10415,6 @@ function printFreshCode(code) {
10369
10415
  function possessive(ownerName) {
10370
10416
  return ownerName ? `${ownerName}'s` : "your Cabane";
10371
10417
  }
10372
- async function runConnectorOffer(runtime, ctx) {
10373
- const snapshot = runtime.harnesses();
10374
- const detected = OFFER_ORDER.map((r) => snapshot.find((h) => h.runtime === r)).filter(
10375
- (h) => !!h && h.state === "detected_not_exposed"
10376
- );
10377
- if (detected.length === 0) {
10378
- if (!ctx.justPaired) return { printedSomething: false };
10379
- blank();
10380
- write(
10381
- INDENT + bang(
10382
- "No coding agent found on this machine yet \u2014 install Claude Code, Codex or opencode and sign in,"
10383
- )
10384
- );
10385
- write(`${INDENT} then: cabane-companion connect claude-code (or codex, or opencode)`);
10386
- return { printedSomething: true };
10387
- }
10388
- blank();
10389
- if (!ctx.interactive) {
10390
- for (const h of detected) {
10391
- write(
10392
- `${INDENT}${foundPhrase(h, runtime.config)} but it isn't connected. Connect it: ${connectCommand(h.runtime)}`
10393
- );
10394
- }
10395
- return { printedSomething: true };
10396
- }
10397
- for (const h of detected) {
10398
- const yes = await confirm(`${foundPhrase(h, runtime.config)}. Connect it to Cabane?`);
10399
- if (!yes) {
10400
- write(`${INDENT}Skipped. Connect it later with: ${connectCommand(h.runtime)}`);
10401
- continue;
10402
- }
10403
- const outcome = await runtime.connectHarness(h.runtime);
10404
- if (!outcome.ok) {
10405
- write(`${INDENT}${outcome.error}`);
10406
- continue;
10407
- }
10408
- write(INDENT + tick(outcome.message));
10409
- }
10410
- return { printedSomething: true };
10411
- }
10412
- function foundPhrase(h, cfg) {
10413
- const label = HARNESS_LABELS[h.runtime];
10414
- if (h.runtime === "opencode") {
10415
- const url = cfg.opencode?.serverUrl;
10416
- return url ? `We found ${label} at ${url}` : `We found ${label} on this machine`;
10417
- }
10418
- return h.version ? `We found ${label} on this machine (${h.version})` : `We found ${label} on this machine`;
10419
- }
10420
10418
  function connectCommand(runtime) {
10421
10419
  return `cabane-companion connect ${runtime}`;
10422
10420
  }
@@ -10426,12 +10424,22 @@ async function handOffToBackground(runtime, ctx) {
10426
10424
  if (!outcome.started) {
10427
10425
  return;
10428
10426
  }
10429
- if (ctx.spaceAbove) blank();
10427
+ if (ctx.justPaired) {
10428
+ for (const label of ctx.connected) write(INDENT + tick(`${label} connected.`));
10429
+ if (ctx.connected.length === 0) {
10430
+ write(
10431
+ `${INDENT}Add a coding agent any time: cabane-companion connect claude-code (or codex, or opencode)`
10432
+ );
10433
+ }
10434
+ }
10430
10435
  write(INDENT + tick("Cabane companion is running in the background."));
10431
10436
  write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
10432
10437
  if (ctx.justPaired) {
10433
10438
  blank();
10434
- write(INDENT + arrow("Head back to Cabane to finish up."));
10439
+ write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
10440
+ write(`${INDENT} just run cabane-companion start again.`);
10441
+ blank();
10442
+ write(`${INDENT}All set on this side. Finish up in Cabane.`);
10435
10443
  }
10436
10444
  }
10437
10445
  async function runAttached(runtime) {
package/dist/runtime.js CHANGED
@@ -1297,7 +1297,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
1297
1297
  var HARNESS_LABELS = {
1298
1298
  "claude-code": "Claude Code",
1299
1299
  codex: "Codex",
1300
- opencode: "opencode"
1300
+ opencode: "OpenCode"
1301
1301
  };
1302
1302
  var LABELS = HARNESS_LABELS;
1303
1303
  function deriveHarnessSnapshot(signals) {
@@ -1419,9 +1419,6 @@ function deriveOpencode(signals, manifestHas) {
1419
1419
  enable: "opencode"
1420
1420
  };
1421
1421
  }
1422
- function detectedRuntimesFor(snapshot) {
1423
- return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
1424
- }
1425
1422
  var PROBE_TIMEOUT_MS = 4e3;
1426
1423
  async function probeHarnessSignals(cfg, deps = {}) {
1427
1424
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -8647,13 +8644,7 @@ var CompanionSupervisor = class {
8647
8644
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
8648
8645
  // A null (probe failed / no opencode) omits the field, and the server then
8649
8646
  // leaves this device's stored availability untouched.
8650
- ...opencodeModels !== null ? { models: opencodeModels } : {},
8651
- // CT1082: what this machine has that the user hasn't connected, so the web
8652
- // UI can offer it without a companion round-trip. Sent only once a probe has
8653
- // actually landed (`harnessSignals` non-null) — an absent field means "we
8654
- // didn't look this beat" and leaves the server's stored suggestion alone,
8655
- // the same fail-soft contract `models` keeps.
8656
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8647
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
8657
8648
  });
8658
8649
  this.hub.setDevice({ deviceId: res.deviceId });
8659
8650
  this.deviceId = res.deviceId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.40",
3
+ "version": "0.6.41",
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",