@cabane/companion 0.6.181 → 0.6.183

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/README.md CHANGED
@@ -44,16 +44,26 @@ The script brings its own Node (checksum-checked, into `~/.cabane`) and needs no
44
44
  npm i -g @cabane/companion && cabane setup CODE
45
45
  ```
46
46
 
47
- Either way `cabane setup CODE` pairs the device to your account, joins it to that workspace, detects your harnesses, and starts the Companion: as a service on macOS and Linux, in the background elsewhere. No confirm, no name screen, no browser on the device. The same line on a device that is already paired joins it to the workspace and mints nothing.
47
+ Either way `cabane setup CODE` pairs the device to your account, joins it to that workspace, starts the Companion in the background, waits for its first check-in, and ends with the device's state in Cabane's words. No confirm, no name screen, no browser on the device. The same line on a device that is already paired joins it to the workspace and mints nothing.
48
48
 
49
49
  ```
50
- ✓ Claude Code signed in (2.1.289) — connected
51
- Codex not signed in — run codex login
52
- OpenCode not running
53
- All set here. Back to the browser.
50
+ ✓ Paired as "maya-mbp" with Acme.
51
+ Cabane is running in the background. This device is ready — checked in 2s ago.
52
+
53
+ Harnesses on this device
54
+ ✓ Claude Code connected — signed in (2.1.289)
55
+ Codex needs sign-in — run codex login
56
+ OpenCode off — not running
57
+
58
+ Status cabane status Stop cabane stop Log cabane logs
59
+ Start at login and keep Cabane updating itself: cabane service install
60
+
61
+ In Cabane: https://app.cabane.ai/acme/settings/devices
54
62
  ```
55
63
 
56
- With nothing signed in, the device still pairs and the Companion still runs; the rows say which harness needs sign-in, and the device shows **Needs attention** in Cabane until one is usable. Sign in, and the next check finds it: no restart.
64
+ The device is online the moment the Companion checks in, before any harness is checked; Cabane shows it as **Checking harnesses…** until the first detection lands, then **Ready** or **Needs attention**. With nothing signed in, the device still pairs and the Companion still runs; the rows say which harness needs sign-in. Sign in, and the next check finds it: no restart. A harness that was signed in earlier in this run stays offered through a lapsed login, so a turn reaches the device, fails with the sign-in notice, and the first reply after you sign in again runs with no wait.
65
+
66
+ Setup never installs the service. Run `cabane service install` when you want the Companion to start at login and keep itself updated; the closing block names it.
57
67
 
58
68
  A fresh device runs nothing yet. In a workspace, open an agent's page and assign it to the device; the Companion picks the assignment up on its next poll.
59
69
 
@@ -61,23 +71,23 @@ If `cabane` isn't found afterwards, open a new terminal, or run the `source` lin
61
71
 
62
72
  ## Commands
63
73
 
64
- | Command | What it does |
65
- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
- | `setup [CODE]` | With a code: pair or join, detect harnesses, start. Without: a re-runnable checklist that reports harnesses and starts what isn't running. |
67
- | `start [--foreground]` | Run in the background. Never asks. Unpaired, it names `setup`. `--foreground` stays attached, for a supervisor of your own. |
68
- | `stop` | Stop a running Companion cleanly. Ends any turn in progress. |
69
- | `status` | Version and install, the paired device by its Cabane name, whether it's running, each harness's state, the sandbox posture, declared secret names, and local overrides. |
70
- | `update [--auto on\|off]` | Install the version Cabane names now; with `--auto`, set whether the Companion keeps itself up to date. |
71
- | `service install\|uninstall\|status` | Run as a systemd user service or a LaunchAgent that starts at boot and restarts if it stops; remove it; show its state. |
72
- | `logs [-n N] [-f]` | Recent log lines, or follow. |
73
- | `transcript [file] [--last] [-f]` | Read back a full agent turn, tool calls and results included. |
74
- | `logout [--purge]` | Remove the device token and cached agent credentials. Doesn't remove the device from Cabane. |
74
+ | Command | What it does |
75
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `setup [CODE]` | With a code: pair or join, detect harnesses, start. Without: a re-runnable checklist that reports harnesses and starts what isn't running. |
77
+ | `start [--foreground]` | Run in the background. Never asks. Unpaired, it names `setup`. `--foreground` stays attached, for a supervisor of your own. |
78
+ | `stop` | Stop a running Companion cleanly. Ends any turn in progress. |
79
+ | `status` | Version and install, the paired device by its Cabane name, its state in Cabane's words and when it last checked in, each harness's state, the sandbox posture, declared secret names, and local overrides. |
80
+ | `update [--auto on\|off]` | Install the version Cabane names now; with `--auto`, set whether the Companion keeps itself up to date. |
81
+ | `service install\|uninstall\|status` | Run as a systemd user service or a LaunchAgent that starts at boot and restarts if it stops; remove it; show its state. |
82
+ | `logs [-n N] [-f]` | Recent log lines, or follow. |
83
+ | `transcript [file] [--last] [-f]` | Read back a full agent turn, tool calls and results included. |
84
+ | `logout [--purge]` | Remove the device token and cached agent credentials. Doesn't remove the device from Cabane. |
75
85
 
76
86
  Anything else about a device is done in Cabane, on the device's page.
77
87
 
78
88
  ## Keeping it running
79
89
 
80
- Under the service the Companion restarts if it stops, comes back after a reboot, and follows the version Cabane names: it installs the new version under `~/.cabane/versions`, checks it starts, and swaps between turns. A version that can't start is rolled back by the boot guard. `cabane update --auto off` stops updates; without the service, `cabane update` installs and the next `cabane start` runs it.
90
+ Without the service, a reboot or a crash leaves the Companion stopped until the next `cabane start`, and the device shows **Offline** in Cabane. Under the service it restarts if it stops, comes back after a reboot, and follows the version Cabane names: it installs the new version under `~/.cabane/versions`, checks it starts, and swaps between turns. A version that can't start is rolled back by the boot guard. `cabane update --auto off` stops updates; without the service, `cabane update` installs and the next `cabane start` runs it.
81
91
 
82
92
  Other supervisors: run `cabane start --foreground` with `CABANE_COMPANION_SUPERVISED=1` set, using the absolute paths `~/.cabane/node/bin/node` and `~/.cabane/current/dist/bin.js`, which don't move on an update.
83
93
 
package/dist/cli.js CHANGED
@@ -12554,11 +12554,24 @@ function parseHarnessRuntime(raw) {
12554
12554
  if (key === "opencode") return "opencode";
12555
12555
  return null;
12556
12556
  }
12557
- function advertisedRuntimes(detection) {
12558
- return usableRuntimes(detection);
12557
+ function stillRunnable(agent) {
12558
+ return agent.state === "usable" || agent.state === "signed_out" || agent.state === "problem" && agent.check.status === "check_failed";
12559
+ }
12560
+ function rememberUsed(used, detection) {
12561
+ const next = /* @__PURE__ */ new Set();
12562
+ for (const runtime of ["claude-code", "codex"]) {
12563
+ const agent = detection[runtime];
12564
+ if (agent.state === "usable" || used.has(runtime) && stillRunnable(agent)) next.add(runtime);
12565
+ }
12566
+ return next;
12567
+ }
12568
+ function advertisedRuntimes(detection, used) {
12569
+ return HARNESS_RUNTIMES.filter(
12570
+ (runtime) => detection[runtime].state === "usable" || used.has(runtime) && stillRunnable(detection[runtime])
12571
+ );
12559
12572
  }
12560
12573
  function deriveHarnessSnapshot(detection) {
12561
- const advertised = new Set(advertisedRuntimes(detection));
12574
+ const advertised = new Set(usableRuntimes(detection));
12562
12575
  const harnesses = HARNESS_RUNTIMES.map(
12563
12576
  (runtime) => derive(runtime, detection[runtime], advertised.has(runtime))
12564
12577
  );
@@ -16163,6 +16176,9 @@ var CompanionSupervisor = class {
16163
16176
  // control socket, so none can disagree with what is advertised. Null until
16164
16177
  // the first detection; a heartbeat spawns nothing and reads this.
16165
16178
  detection = null;
16179
+ // The bundled harnesses a detection in this run found usable, which stay offered
16180
+ // through a lapsed sign-in (`rememberUsed`). Empty until the first detection.
16181
+ used = /* @__PURE__ */ new Set();
16166
16182
  detecting = null;
16167
16183
  // Liveness may be acknowledged before discovery. Execution waits on this
16168
16184
  // first look only, never on a periodic refresh of an established inventory.
@@ -16407,8 +16423,7 @@ var CompanionSupervisor = class {
16407
16423
  async sendHeartbeat(first = false) {
16408
16424
  if (!this.deviceApi) return;
16409
16425
  try {
16410
- if (!first && this.detection && advertisedRuntimes(this.detection).length === 0)
16411
- await this.detect();
16426
+ if (!first && this.detection && this.advertised().length === 0) await this.detect();
16412
16427
  const store = loadSecretStoreTolerant(
16413
16428
  (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
16414
16429
  );
@@ -16742,11 +16757,15 @@ var CompanionSupervisor = class {
16742
16757
  bundledResolution(runtime) {
16743
16758
  return this.resolveBundled(runtime);
16744
16759
  }
16745
- // Does this device OFFER a runtime — found usable by the last detection? The
16746
- // ONE signal the heartbeat manifest, the adapter registry and the dispatcher
16747
- // read, so what the device advertises and what it can select can't disagree.
16760
+ // Does this device OFFER a runtime — usable now, or used in this run and still
16761
+ // runnable? The ONE signal the heartbeat manifest, the adapter registry and the
16762
+ // dispatcher read, so what the device advertises and what it can select can't
16763
+ // disagree.
16748
16764
  offered(runtime) {
16749
- return this.detection !== null && advertisedRuntimes(this.detection).includes(runtime);
16765
+ return this.advertised().includes(runtime);
16766
+ }
16767
+ advertised() {
16768
+ return this.detection ? advertisedRuntimes(this.detection, this.used) : [];
16750
16769
  }
16751
16770
  // The address an OpenCode turn runs against: the serve detection found, else
16752
16771
  // the configured one, for the model list a beat still asks for.
@@ -17405,6 +17424,7 @@ var CompanionSupervisor = class {
17405
17424
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
17406
17425
  });
17407
17426
  this.detection = detection;
17427
+ this.used = rememberUsed(this.used, detection);
17408
17428
  this.logBundledTransitions();
17409
17429
  this.harnessVersions = {
17410
17430
  claudeCode: detectedVersion(detection["claude-code"]),
@@ -17459,7 +17479,7 @@ var CompanionSupervisor = class {
17459
17479
  const health = this.connectorHealth.reports();
17460
17480
  if (health.length === 0 || !this.detection) return health;
17461
17481
  const named = new Set(health.map((r) => r.runtime));
17462
- const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
17482
+ const bare = this.advertised().filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
17463
17483
  return [...health, ...bare];
17464
17484
  }
17465
17485
  // Beat NOW and wait for it to land. `start` calls this before asking the
package/dist/runtime.js CHANGED
@@ -13042,11 +13042,24 @@ function withTimeout(promise, fallback, timeoutMs) {
13042
13042
  }
13043
13043
 
13044
13044
  // src/harness-status.ts
13045
- function advertisedRuntimes(detection) {
13046
- return usableRuntimes(detection);
13045
+ function stillRunnable(agent) {
13046
+ return agent.state === "usable" || agent.state === "signed_out" || agent.state === "problem" && agent.check.status === "check_failed";
13047
+ }
13048
+ function rememberUsed(used, detection) {
13049
+ const next = /* @__PURE__ */ new Set();
13050
+ for (const runtime of ["claude-code", "codex"]) {
13051
+ const agent = detection[runtime];
13052
+ if (agent.state === "usable" || used.has(runtime) && stillRunnable(agent)) next.add(runtime);
13053
+ }
13054
+ return next;
13055
+ }
13056
+ function advertisedRuntimes(detection, used) {
13057
+ return HARNESS_RUNTIMES.filter(
13058
+ (runtime) => detection[runtime].state === "usable" || used.has(runtime) && stillRunnable(detection[runtime])
13059
+ );
13047
13060
  }
13048
13061
  function deriveHarnessSnapshot(detection) {
13049
- const advertised = new Set(advertisedRuntimes(detection));
13062
+ const advertised = new Set(usableRuntimes(detection));
13050
13063
  const harnesses = HARNESS_RUNTIMES.map(
13051
13064
  (runtime) => derive(runtime, detection[runtime], advertised.has(runtime))
13052
13065
  );
@@ -14095,6 +14108,9 @@ var CompanionSupervisor = class {
14095
14108
  // control socket, so none can disagree with what is advertised. Null until
14096
14109
  // the first detection; a heartbeat spawns nothing and reads this.
14097
14110
  detection = null;
14111
+ // The bundled harnesses a detection in this run found usable, which stay offered
14112
+ // through a lapsed sign-in (`rememberUsed`). Empty until the first detection.
14113
+ used = /* @__PURE__ */ new Set();
14098
14114
  detecting = null;
14099
14115
  // Liveness may be acknowledged before discovery. Execution waits on this
14100
14116
  // first look only, never on a periodic refresh of an established inventory.
@@ -14339,8 +14355,7 @@ var CompanionSupervisor = class {
14339
14355
  async sendHeartbeat(first = false) {
14340
14356
  if (!this.deviceApi) return;
14341
14357
  try {
14342
- if (!first && this.detection && advertisedRuntimes(this.detection).length === 0)
14343
- await this.detect();
14358
+ if (!first && this.detection && this.advertised().length === 0) await this.detect();
14344
14359
  const store = loadSecretStoreTolerant(
14345
14360
  (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
14346
14361
  );
@@ -14674,11 +14689,15 @@ var CompanionSupervisor = class {
14674
14689
  bundledResolution(runtime) {
14675
14690
  return this.resolveBundled(runtime);
14676
14691
  }
14677
- // Does this device OFFER a runtime — found usable by the last detection? The
14678
- // ONE signal the heartbeat manifest, the adapter registry and the dispatcher
14679
- // read, so what the device advertises and what it can select can't disagree.
14692
+ // Does this device OFFER a runtime — usable now, or used in this run and still
14693
+ // runnable? The ONE signal the heartbeat manifest, the adapter registry and the
14694
+ // dispatcher read, so what the device advertises and what it can select can't
14695
+ // disagree.
14680
14696
  offered(runtime) {
14681
- return this.detection !== null && advertisedRuntimes(this.detection).includes(runtime);
14697
+ return this.advertised().includes(runtime);
14698
+ }
14699
+ advertised() {
14700
+ return this.detection ? advertisedRuntimes(this.detection, this.used) : [];
14682
14701
  }
14683
14702
  // The address an OpenCode turn runs against: the serve detection found, else
14684
14703
  // the configured one, for the model list a beat still asks for.
@@ -15337,6 +15356,7 @@ var CompanionSupervisor = class {
15337
15356
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
15338
15357
  });
15339
15358
  this.detection = detection;
15359
+ this.used = rememberUsed(this.used, detection);
15340
15360
  this.logBundledTransitions();
15341
15361
  this.harnessVersions = {
15342
15362
  claudeCode: detectedVersion(detection["claude-code"]),
@@ -15391,7 +15411,7 @@ var CompanionSupervisor = class {
15391
15411
  const health = this.connectorHealth.reports();
15392
15412
  if (health.length === 0 || !this.detection) return health;
15393
15413
  const named = new Set(health.map((r) => r.runtime));
15394
- const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
15414
+ const bare = this.advertised().filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
15395
15415
  return [...health, ...bare];
15396
15416
  }
15397
15417
  // Beat NOW and wait for it to land. `start` calls this before asking the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.181",
3
+ "version": "0.6.183",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an 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",