@cabane/companion 0.6.156 → 0.6.157
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 +7 -109
- package/dist/bin.js +2 -1
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +1633 -193
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +9 -0
- package/dist/runtime.js +755 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ A freshly paired device runs nothing yet: pairing doesn't put any agent on it. I
|
|
|
106
106
|
| `pair [--server <url>]` | Pair this device with a short code entered in Cabane, without starting the Companion. Refuses, like `start`, until an agent is signed in. `start` pairs by itself, so `pair` is rarely needed. |
|
|
107
107
|
| `start [--foreground] [--daemon]` | Offer the signed-in agents, pair if needed, and run in the background; refuse, pairing nothing, when none is usable. `--daemon` is an accepted compatibility alias for the default. `--foreground` stays attached for a terminal or supervisor and never refuses. A non-interactive `start` on a paired device also stays attached. |
|
|
108
108
|
| `connect <claude-code\|codex\|opencode>` | Connect a signed-in agent; with the Companion running, it checks and connects over its control socket, no restart. Refuses a signed-out agent and changes nothing. |
|
|
109
|
+
| `update [--auto on\|off]` | Update to the version Cabane names now, between turns; with `--auto`, set whether the Companion keeps itself up to date. |
|
|
110
|
+
| `service install\|uninstall\|status` | Run the Companion as a systemd user service or a LaunchAgent that starts at boot and restarts if it stops; remove it; show its state and how updates are going. |
|
|
109
111
|
| `stop` | Stop a foreground or detached Companion cleanly. Idempotent — "nothing running" is a success. |
|
|
110
112
|
| `status` | Print the version and install, the paired device, whether it's running, each agent's sign-in, log and transcript paths, declared secret names, and local overrides. Works unpaired. |
|
|
111
113
|
| `logs [-n N] [-f]` | Show recent log lines or follow new ones. |
|
|
@@ -128,119 +130,15 @@ If `cabane start` says the Companion is running but the device stays **Offline**
|
|
|
128
130
|
|
|
129
131
|
## Background and supervised operation
|
|
130
132
|
|
|
131
|
-
In an interactive terminal, `cabane start` backgrounds itself, returns the prompt, and keeps running after the terminal closes
|
|
133
|
+
In an interactive terminal, `cabane start` backgrounds itself, returns the prompt, and keeps running after the terminal closes. The first time, it asks once whether to keep Cabane running and up to date on this computer.
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
cabane start
|
|
135
|
-
# ✓ Cabane is running in the background.
|
|
136
|
-
# Stop: cabane stop Logs: cabane logs
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
`start --daemon` takes the same path for compatibility. This detached process does not survive logout or reboot and Cabane does not install, enable, or update a login service. Use `cabane status` to check it and `cabane stop` to stop it.
|
|
140
|
-
|
|
141
|
-
If you want restart or login persistence, configure your own supervisor to run `cabane start --foreground`. The supervisor owns when the process starts and restarts; the Companion never writes these files for you.
|
|
142
|
-
|
|
143
|
-
### Find the executable paths
|
|
144
|
-
|
|
145
|
-
The examples below need absolute paths because service managers do not load your interactive shell setup:
|
|
146
|
-
|
|
147
|
-
```sh
|
|
148
|
-
command -v node
|
|
149
|
-
printf '%s\n' "$(npm root -g)/@cabane/companion/dist/cli.js"
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Confirm both paths exist, then substitute them for `/absolute/path/to/node` and `/absolute/path/to/cli.js`. NVM and global npm paths are often versioned and can move after a Node or package upgrade. After every upgrade, run these commands again, update both paths in your supervisor config, and reload/restart it.
|
|
153
|
-
|
|
154
|
-
### macOS LaunchAgent
|
|
155
|
-
|
|
156
|
-
Save this as `~/Library/LaunchAgents/ai.cabane.companion.plist`, replacing both paths:
|
|
157
|
-
|
|
158
|
-
```xml
|
|
159
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
160
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
161
|
-
<plist version="1.0">
|
|
162
|
-
<dict>
|
|
163
|
-
<key>Label</key><string>ai.cabane.companion</string>
|
|
164
|
-
<key>ProgramArguments</key>
|
|
165
|
-
<array>
|
|
166
|
-
<string>/absolute/path/to/node</string>
|
|
167
|
-
<string>/absolute/path/to/cli.js</string>
|
|
168
|
-
<string>start</string>
|
|
169
|
-
<string>--foreground</string>
|
|
170
|
-
</array>
|
|
171
|
-
<key>RunAtLoad</key><true/>
|
|
172
|
-
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
173
|
-
<key>StandardOutPath</key><string>/tmp/cabane-companion.out.log</string>
|
|
174
|
-
<key>StandardErrorPath</key><string>/tmp/cabane-companion.err.log</string>
|
|
175
|
-
</dict>
|
|
176
|
-
</plist>
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Load it with `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist`. Adding a login-persistent LaunchAgent is security-sensitive behavior and may be reported by macOS or endpoint-security tooling. Cabane cannot prevent or hide that report.
|
|
180
|
-
|
|
181
|
-
To update moved executable paths, unload it, edit the two paths, then load it again:
|
|
182
|
-
|
|
183
|
-
```sh
|
|
184
|
-
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
185
|
-
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
To remove it completely:
|
|
189
|
-
|
|
190
|
-
```sh
|
|
191
|
-
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
192
|
-
rm ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Linux systemd user unit
|
|
196
|
-
|
|
197
|
-
Save this as `~/.config/systemd/user/cabane-companion.service`, replacing both paths:
|
|
198
|
-
|
|
199
|
-
```ini
|
|
200
|
-
[Unit]
|
|
201
|
-
Description=Cabane Companion
|
|
202
|
-
After=network-online.target
|
|
203
|
-
|
|
204
|
-
[Service]
|
|
205
|
-
Type=simple
|
|
206
|
-
ExecStart=/absolute/path/to/node /absolute/path/to/cli.js start --foreground
|
|
207
|
-
Restart=on-failure
|
|
208
|
-
RestartSec=5
|
|
209
|
-
|
|
210
|
-
[Install]
|
|
211
|
-
WantedBy=default.target
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Load it with:
|
|
215
|
-
|
|
216
|
-
```sh
|
|
217
|
-
systemctl --user daemon-reload
|
|
218
|
-
systemctl --user enable --now cabane-companion.service
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
A user service normally stops when its login session ends. If this machine must keep running it after logout, an administrator can enable lingering with `sudo loginctl enable-linger "$USER"`; that is a separate operating-system persistence decision.
|
|
222
|
-
|
|
223
|
-
After an NVM/npm upgrade, stop the unit, rediscover and replace both executable paths, then reload and restart it:
|
|
224
|
-
|
|
225
|
-
```sh
|
|
226
|
-
systemctl --user stop cabane-companion.service
|
|
227
|
-
systemctl --user daemon-reload
|
|
228
|
-
systemctl --user start cabane-companion.service
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
To remove it completely:
|
|
232
|
-
|
|
233
|
-
```sh
|
|
234
|
-
systemctl --user disable --now cabane-companion.service
|
|
235
|
-
rm ~/.config/systemd/user/cabane-companion.service
|
|
236
|
-
systemctl --user daemon-reload
|
|
237
|
-
```
|
|
135
|
+
Yes, or `cabane service install` later, runs it as a service: a systemd user unit (`~/.config/systemd/user/cabane-companion.service`) on Linux, a LaunchAgent (`~/Library/LaunchAgents/ai.cabane.companion.plist`) on macOS, with auto-update on. It runs `<node> ~/.cabane/boot-guard.mjs start --foreground` on its own copy of this install under `~/.cabane/versions`, restarts it whatever it exits with, and reads `~/.cabane/service.env`, which holds your login shell's PATH and any headless sign-in (`CLAUDE_CODE_OAUTH_TOKEN=…`). `<node>` is the install script's `~/.cabane/node/bin/node` when it's there, otherwise the Node that ran `service install`; `cabane service status` says if it's gone. On Linux it tells you when lingering is off and a logout would stop it. With a service installed, `cabane start` and `cabane stop` start and stop the service; `cabane service uninstall` removes it.
|
|
238
136
|
|
|
239
|
-
|
|
137
|
+
Updates need the service: it's what restarts the Companion into a new version. The Companion follows the version Cabane names on each heartbeat: it installs it under `~/.cabane/versions/<version>`, checks it starts and still has the bundled agents this device uses, points `~/.cabane/current` at it between turns, and exits for the service to restart it. A version that stops three times before connecting, or can't connect within ten minutes of Cabane answering, is rolled back by the boot guard. `cabane update --auto off|on` stops or resumes updates under the service. Without the service, `cabane update` installs the latest version and the next `cabane start` runs it.
|
|
240
138
|
|
|
241
139
|
### Other supervisors
|
|
242
140
|
|
|
243
|
-
Run `cabane start --foreground
|
|
141
|
+
Run `cabane start --foreground` with `CABANE_COMPANION_SUPERVISED=1` in its environment. Configure the supervisor itself for restart and login/boot behavior, and use absolute executable paths when it does not load your interactive shell: `~/.cabane/node/bin/node` and `~/.cabane/current/dist/bin.js` don't move on an update. Without the boot guard a supervisor of your own doesn't roll back a release that fails before it starts.
|
|
244
142
|
|
|
245
143
|
## Configuring an agent: what's in Cabane vs. on this machine
|
|
246
144
|
|
|
@@ -489,4 +387,4 @@ It renders the conversation step by step — `🔧 tool(input) → ok/ERROR: res
|
|
|
489
387
|
|
|
490
388
|
## What v0 doesn't do yet
|
|
491
389
|
|
|
492
|
-
|
|
390
|
+
Windows has no service install and no self-update. There's no per-tool permissioning for an agent with host access yet.
|
package/dist/bin.js
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/boot-guard.ts
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join as join2 } from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
|
|
8
|
+
// src/boot-rule.ts
|
|
9
|
+
import {
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
symlinkSync,
|
|
16
|
+
writeFileSync
|
|
17
|
+
} from "fs";
|
|
18
|
+
import { dirname, join } from "path";
|
|
19
|
+
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
20
|
+
var MAX_UNCONFIRMED_BOOTS = 3;
|
|
21
|
+
var MAX_STAGE_ATTEMPTS = 3;
|
|
22
|
+
function bootVerdict(pending, version, lastCrash) {
|
|
23
|
+
if (!pending || pending.to !== version) return { kind: "run" };
|
|
24
|
+
if (pending.boots >= MAX_UNCONFIRMED_BOOTS) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "rollback",
|
|
27
|
+
pending,
|
|
28
|
+
reason: lastCrash ?? `it stopped ${pending.boots} times before connecting to Cabane`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { kind: "run", pending: { ...pending, boots: pending.boots + 1 } };
|
|
32
|
+
}
|
|
33
|
+
function superseded(pending, ...running) {
|
|
34
|
+
return !!pending && !running.includes(pending.to);
|
|
35
|
+
}
|
|
36
|
+
function rolledBackState(state, pending, reason, now) {
|
|
37
|
+
const { pending: _done, ...rest } = state;
|
|
38
|
+
return {
|
|
39
|
+
...rest,
|
|
40
|
+
lastResult: {
|
|
41
|
+
kind: "rolled_back",
|
|
42
|
+
from: pending.from,
|
|
43
|
+
to: pending.to,
|
|
44
|
+
at: now.toISOString(),
|
|
45
|
+
reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
|
|
46
|
+
},
|
|
47
|
+
failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function guardBoot(dir, now = Date.now()) {
|
|
51
|
+
const link = join(dir, "current");
|
|
52
|
+
let root2;
|
|
53
|
+
try {
|
|
54
|
+
root2 = realpathSync(link);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const statePath = join(dir, "update.json");
|
|
59
|
+
const state = readJson(statePath);
|
|
60
|
+
const version = readJson(join(root2, "package.json")).version ?? "";
|
|
61
|
+
const crash = readJson(join(dir, "last-error.json")).reason ?? null;
|
|
62
|
+
if (superseded(state.pending, version)) {
|
|
63
|
+
const { pending: _gone, ...rest } = state;
|
|
64
|
+
writeJson(statePath, rest);
|
|
65
|
+
return root2;
|
|
66
|
+
}
|
|
67
|
+
const verdict = bootVerdict(state.pending, version, crash);
|
|
68
|
+
if (verdict.kind === "rollback") {
|
|
69
|
+
const back = verdict.pending.fromRoot;
|
|
70
|
+
const fresh = `${link}.${process.pid}.${now}`;
|
|
71
|
+
symlinkSync(back, fresh);
|
|
72
|
+
renameSync(fresh, link);
|
|
73
|
+
writeJson(statePath, rolledBackState(state, verdict.pending, verdict.reason, new Date(now)));
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`Cabane ${verdict.pending.to} didn't come up (${verdict.reason}); going back to ${verdict.pending.from}.
|
|
76
|
+
`
|
|
77
|
+
);
|
|
78
|
+
return back;
|
|
79
|
+
}
|
|
80
|
+
if (verdict.pending) writeJson(statePath, { ...state, pending: verdict.pending });
|
|
81
|
+
return root2;
|
|
82
|
+
}
|
|
83
|
+
function readJson(path) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
86
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function writeJson(path, value) {
|
|
92
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
93
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
96
|
+
`, { mode: 384 });
|
|
97
|
+
renameSync(tmp, path);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
rmSync(tmp, { force: true });
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/boot-guard.ts
|
|
105
|
+
var root = guardBoot(join2(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane"));
|
|
106
|
+
if (!root) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
"Cabane isn't installed where the service expects it (~/.cabane/current). Run cabane service install again.\n"
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
await import(pathToFileURL(join2(root, "dist", "bin.js")).href);
|