@cabane/companion 0.6.155 → 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 +73 -169
- package/dist/bin.js +14 -0
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +4153 -2184
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +169 -23
- package/dist/runtime.js +1666 -833
- package/dist/static/app.js +2 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# cabane
|
|
1
|
+
# cabane
|
|
2
2
|
|
|
3
|
-
Connect a **
|
|
3
|
+
Connect a **agent on your machine** to your [Cabane](https://cabane.ai) workspaces as a **responder** — it replies to messages inside Cabane while running as a full local AI client, so you can drive work against your own codebase, files, and MCP servers without putting any of it in Cabane. The model and tools run on your box; only the reply crosses back.
|
|
4
4
|
|
|
5
5
|
The Companion runs your agents through a **harness** on your machine — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See [Harnesses](#harnesses).
|
|
6
6
|
|
|
@@ -10,102 +10,110 @@ This is **v0**. It's a small CLI — install it from npm with one command, pair
|
|
|
10
10
|
|
|
11
11
|
## The model in one paragraph
|
|
12
12
|
|
|
13
|
-
The Companion runs on a **device you pair**, running **agents you assign to it**. `cabane
|
|
13
|
+
The Companion runs on a **device you pair**, running **agents you assign to it**. `cabane start` pairs the device with a short code the first time, then runs. From then on it **pulls its assignments from Cabane at runtime** — every agent you've pointed at this device, across all your workspaces — and runs each one locally. Which agents to run, and how they're configured (host access, MCP servers, model), is owned by Cabane and delivered per turn. The machine only holds the bits that _must_ be local: the device token, your secrets, and any overrides you set for this machine.
|
|
14
14
|
|
|
15
15
|
> **No account password or full-account token ever touches your machine.** The short-code pairing flow delivers a **device token** (`cabdev_…`) directly to the waiting CLI — good only for pulling this device's assignments and reporting liveness. Each agent the Companion runs gets its own **workspace-bound agent token**, delivered once when the agent is assigned and scoped to that one agent in that one workspace. Renaming, deactivating and removing a device, rotating its token, and assigning agents to it all live in Cabane; the CLI only pairs the machine and runs the agents locally.
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Sign in to an agent first.** The Companion runs turns with its own bundled copies of Claude Code and Codex, so it never needs them on your PATH, only signed in. `cabane start` checks with those bundled copies and refuses to pair until one is signed in, or has an API key, printing the lines to run. The Companion never runs a sign-in itself. One agent is enough; a machine can offer several. [Harnesses](#harnesses) has the full wiring.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
**Claude Code** — install it globally _and_ log in:
|
|
21
|
+
**Claude Code** (needs a Claude Pro or Max plan):
|
|
24
22
|
|
|
25
23
|
```sh
|
|
26
|
-
|
|
27
|
-
claude
|
|
24
|
+
curl -fsSL https://claude.ai/install.sh | bash
|
|
25
|
+
~/.local/bin/claude auth login
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **The install** puts `claude` on your PATH (`which claude` should resolve). That is what makes the Companion _offer_ to connect Claude Code, and what `cabane-companion status` reports a version from. It is not what runs your turns, and on its own it exposes nothing — that takes connecting it _and_ the bundled binary (below).
|
|
33
|
-
- **The login** writes the credential the turn actually uses. The Companion does _not_ run your global `claude` to answer a turn — inference goes through the bundled Claude Agent SDK, which spawns its own native binary — but the SDK reads the credential your Claude Code login left on disk. So a machine with `claude` installed and never logged in looks fine and then fails every turn with **"Sign-in needed"**.
|
|
28
|
+
The full path works in the same window even when Claude's installer leaves `~/.local/bin` off your PATH. With an Anthropic API key instead, `export ANTHROPIC_API_KEY=…` in the window that runs `cabane start`: the Companion reports it as **API key configured**. A key lives in that window's environment, so one exported later in another window isn't seen by a running Companion; `cabane connect` says so. A plan sign-in is stored on disk and never needs a restart.
|
|
34
29
|
|
|
35
|
-
**
|
|
30
|
+
**Codex** — `codex login`, or `export CODEX_API_KEY=…` in the window that runs `cabane start` (Cabane never sees the key).
|
|
36
31
|
|
|
37
|
-
**
|
|
38
|
-
|
|
39
|
-
**opencode** — install it, authenticate a provider through its own flow, and start its server and leave it running (`opencode serve --port 4096`). The Companion finds a server on that port by itself; for one anywhere else, connect it with `cabane-companion connect opencode --url <url>`.
|
|
32
|
+
**opencode** — authenticate a provider through its own flow and leave its server running (`opencode serve --port 4096`). A reachable server counts as usable; for one anywhere else, connect it with `cabane connect opencode --url <url>`.
|
|
40
33
|
|
|
41
34
|
You also need **a Cabane account.** A device belongs to your account, not to one workspace: once paired, it is available in every workspace the account is in.
|
|
42
35
|
|
|
43
36
|
## Install
|
|
44
37
|
|
|
38
|
+
Two ways, identical from `cabane start` on.
|
|
39
|
+
|
|
40
|
+
**The install script** brings its own Node (22, checksum-checked, into `~/.cabane`), needs no sudo, and runs `cabane start` in the same window:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
curl -fsSL https://cabane.ai/install.sh | bash
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**npm**, with Node 22 or newer:
|
|
47
|
+
|
|
45
48
|
```sh
|
|
46
49
|
npm i -g @cabane/companion
|
|
50
|
+
cabane start
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Either puts `cabane` on your PATH, with `cabane-companion` kept as an alias. If `cabane` isn't found right after an npm install, npm's global bin isn't on your PATH: run `"$(npm prefix -g)/bin/cabane" start`, and the Companion prints full paths from then on. If npm reports `EEXIST … cabane`, an older self-host CLI owns the name: `npm i -g @cabane/cli@latest` (its command is `cabane-server` now), then install again.
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
To **update**, stop first, because a stop ends any turn in progress: `cabane stop`, then re-run the install script (it starts the new version), or `npm i -g @cabane/companion@latest` and `cabane start`. The install script changes nothing while a Companion is running.
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
## First run
|
|
54
58
|
|
|
55
59
|
```sh
|
|
56
|
-
cabane
|
|
60
|
+
cabane start
|
|
57
61
|
```
|
|
58
62
|
|
|
59
|
-
It names each
|
|
63
|
+
It names each agent that's signed in and asks whether to use it in Cabane. Press Enter for yes, or type `n` to leave one out. Then it prints a pairing code and a link, and waits. The code is good for 15 minutes; press Return to open the link with the code filled in.
|
|
60
64
|
|
|
61
65
|
```
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
Claude Code is signed in on this computer (2.1.289).
|
|
67
|
+
Use it in Cabane? [Y/n]
|
|
68
|
+
✓ Claude Code — available once this computer is paired.
|
|
64
69
|
|
|
65
|
-
Now
|
|
70
|
+
Now pair this computer. Enter this code on the Cabane page:
|
|
66
71
|
|
|
67
72
|
4HSP-FPWB
|
|
68
73
|
|
|
69
|
-
or
|
|
74
|
+
or press Return to open this link in your browser:
|
|
70
75
|
|
|
71
76
|
https://app.cabane.ai/pair/enter-code?code=4HSP-FPWB
|
|
72
77
|
|
|
73
78
|
Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
|
|
74
79
|
```
|
|
75
80
|
|
|
81
|
+
With nothing signed in, it pairs nothing, starts nothing, prints the sign-in lines above and exits non-zero.
|
|
82
|
+
|
|
76
83
|
In Cabane, open the **Account menu** at the bottom left and choose **Settings**, then **Devices**, then **Pair a device** and **Enter pairing code**. Type the eight characters and choose **Pair device**. Or open the link from the terminal, which fills the code in. Any browser signed in to the account will do, so a headless machine needs no browser of its own. The device token is delivered straight to the waiting CLI. Nothing is copied or pasted.
|
|
77
84
|
|
|
78
85
|
The terminal finishes by itself:
|
|
79
86
|
|
|
80
87
|
```
|
|
81
|
-
✓ Paired
|
|
88
|
+
✓ Paired with Maya Finch's Cabane account as maya-mbp.
|
|
82
89
|
✓ Claude Code connected.
|
|
83
|
-
✓ Cabane
|
|
84
|
-
Stop: cabane
|
|
90
|
+
✓ Cabane is running in the background. You can close this window.
|
|
91
|
+
Stop: cabane stop Logs: cabane logs
|
|
85
92
|
|
|
86
|
-
! It won't restart on its own
|
|
87
|
-
just run cabane-companion start again.
|
|
93
|
+
! It won't restart on its own after a reboot — run cabane start again.
|
|
88
94
|
|
|
89
95
|
All set on this side. Finish up in Cabane.
|
|
90
96
|
```
|
|
91
97
|
|
|
92
|
-
Back in Cabane, **Name this device** asks for a **Device name**, which starts as the machine's hostname and can change later. Choose **Save and continue**. **What Runs on This Device** lists each
|
|
98
|
+
Back in Cabane, **Name this device** asks for a **Device name**, which starts as the machine's hostname and can change later. Choose **Save and continue**. **What Runs on This Device** lists each agent on the device as a connector; choose **Continue**. The device is then listed under **Settings → Devices** as **Online**.
|
|
93
99
|
|
|
94
100
|
A freshly paired device runs nothing yet: pairing doesn't put any agent on it. In a workspace, open **Settings**, then **Agents**, choose an agent, and under **Assignment** choose **Assign a device**, pick this device, and choose **Assign device**. Then, under **Execution**, choose its **Connector** and model and choose **Save execution settings**. Until those are saved, the agent's row in the list still says "Not configured". Only the device's own owner can assign an agent to it. The Create an agent guide in Cabane's docs walks these screens. Assign agents in as many workspaces as the account is in: one running Companion serves them all.
|
|
95
101
|
|
|
96
102
|
## Commands
|
|
97
103
|
|
|
98
|
-
| Command | What it does
|
|
99
|
-
| ---------------------------------------- |
|
|
100
|
-
| `pair [--server <url>]` | Pair this device with a short code entered in Cabane, without starting the Companion. `start
|
|
101
|
-
| `start [--foreground] [--daemon]` |
|
|
102
|
-
| `connect <claude-code\|codex\|opencode>` | Connect a
|
|
103
|
-
| `
|
|
104
|
-
| `status`
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
|
|
|
104
|
+
| Command | What it does |
|
|
105
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
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
|
+
| `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
|
+
| `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. |
|
|
111
|
+
| `stop` | Stop a foreground or detached Companion cleanly. Idempotent — "nothing running" is a success. |
|
|
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. |
|
|
113
|
+
| `logs [-n N] [-f]` | Show recent log lines or follow new ones. |
|
|
114
|
+
| `transcript [file] [--last] [-f]` | Show a recent full agent transcript. No args lists recent turns; `--last` renders the newest; `--follow` live-watches new turns. |
|
|
115
|
+
| `logout [-y] [--purge]` | Remove the device token and cached agent credentials. `--purge` removes the whole local config. Neither removes the device from Cabane. |
|
|
116
|
+
| `--version` / `-V` | Print the Companion version. |
|
|
109
117
|
|
|
110
118
|
Everything else about a device is done in Cabane, not the CLI: **rename**, **deactivate**, **rotate token** and **remove** are on the device's page under the account's **Settings → Devices**, and agents are assigned on each workspace's **Settings → Agents**.
|
|
111
119
|
|
|
@@ -113,128 +121,24 @@ Everything else about a device is done in Cabane, not the CLI: **rename**, **dea
|
|
|
113
121
|
|
|
114
122
|
Each of these starts in Cabane, on the device's page, and three of them need a command on the device afterwards.
|
|
115
123
|
|
|
116
|
-
- **After a reboot, or whenever the Companion has stopped**, run `cabane
|
|
117
|
-
- **Deactivate** turns the device off without removing it. The Companion on the device exits. To bring it back, choose **Reactivate** in Cabane, then run `cabane
|
|
118
|
-
- **Rotate token** shows a new device token (`cabdev_…`) once, and the old one stops working at once. On the device, run `cabane
|
|
119
|
-
- **Remove device** can't be undone. The Companion on the device exits. To pair the same machine again, run `cabane
|
|
124
|
+
- **After a reboot, or whenever the Companion has stopped**, run `cabane start`. Running it while the Companion is already running changes nothing. In Cabane, a device whose Companion has stopped turns to **Offline** about two minutes later, and back to **Online** once it is up.
|
|
125
|
+
- **Deactivate** turns the device off without removing it. The Companion on the device exits. To bring it back, choose **Reactivate** in Cabane, then run `cabane start` on the device. The Companion's log says to log out and pair again; that isn't needed.
|
|
126
|
+
- **Rotate token** shows a new device token (`cabdev_…`) once, and the old one stops working at once. On the device, run `cabane stop`, replace the value of `deviceToken` in `~/.cabane/config.json` with the new token, then run `cabane start`. Stop first: a Companion that is still running keeps the old token, and `start` does nothing while one is running.
|
|
127
|
+
- **Remove device** can't be undone. The Companion on the device exits. To pair the same machine again, run `cabane logout`, then `cabane start`.
|
|
120
128
|
|
|
121
|
-
If `cabane
|
|
129
|
+
If `cabane start` says the Companion is running but the device stays **Offline**, the Companion may have exited straight after starting, which happens when Cabane no longer accepts the device. `cabane status` and `cabane logs` show which.
|
|
122
130
|
|
|
123
131
|
## Background and supervised operation
|
|
124
132
|
|
|
125
|
-
In an interactive terminal, `cabane
|
|
126
|
-
|
|
127
|
-
```sh
|
|
128
|
-
cabane-companion start
|
|
129
|
-
# ✓ Cabane companion is running in the background.
|
|
130
|
-
# Stop: cabane-companion stop Logs: cabane-companion logs (~/.cabane/companion.log)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
`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-companion status` to check it and `cabane-companion stop` to stop it.
|
|
134
|
-
|
|
135
|
-
If you want restart or login persistence, configure your own supervisor to run `cabane-companion start --foreground`. The supervisor owns when the process starts and restarts; the Companion never writes these files for you.
|
|
136
|
-
|
|
137
|
-
### Find the executable paths
|
|
138
|
-
|
|
139
|
-
The examples below need absolute paths because service managers do not load your interactive shell setup:
|
|
140
|
-
|
|
141
|
-
```sh
|
|
142
|
-
command -v node
|
|
143
|
-
printf '%s\n' "$(npm root -g)/@cabane/companion/dist/cli.js"
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
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.
|
|
147
|
-
|
|
148
|
-
### macOS LaunchAgent
|
|
149
|
-
|
|
150
|
-
Save this as `~/Library/LaunchAgents/ai.cabane.companion.plist`, replacing both paths:
|
|
151
|
-
|
|
152
|
-
```xml
|
|
153
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
154
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
155
|
-
<plist version="1.0">
|
|
156
|
-
<dict>
|
|
157
|
-
<key>Label</key><string>ai.cabane.companion</string>
|
|
158
|
-
<key>ProgramArguments</key>
|
|
159
|
-
<array>
|
|
160
|
-
<string>/absolute/path/to/node</string>
|
|
161
|
-
<string>/absolute/path/to/cli.js</string>
|
|
162
|
-
<string>start</string>
|
|
163
|
-
<string>--foreground</string>
|
|
164
|
-
</array>
|
|
165
|
-
<key>RunAtLoad</key><true/>
|
|
166
|
-
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
167
|
-
<key>StandardOutPath</key><string>/tmp/cabane-companion.out.log</string>
|
|
168
|
-
<key>StandardErrorPath</key><string>/tmp/cabane-companion.err.log</string>
|
|
169
|
-
</dict>
|
|
170
|
-
</plist>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
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.
|
|
174
|
-
|
|
175
|
-
To update moved executable paths, unload it, edit the two paths, then load it again:
|
|
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.
|
|
176
134
|
|
|
177
|
-
|
|
178
|
-
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
179
|
-
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
To remove it completely:
|
|
183
|
-
|
|
184
|
-
```sh
|
|
185
|
-
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
186
|
-
rm ~/Library/LaunchAgents/ai.cabane.companion.plist
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Linux systemd user unit
|
|
190
|
-
|
|
191
|
-
Save this as `~/.config/systemd/user/cabane-companion.service`, replacing both paths:
|
|
192
|
-
|
|
193
|
-
```ini
|
|
194
|
-
[Unit]
|
|
195
|
-
Description=Cabane Companion
|
|
196
|
-
After=network-online.target
|
|
197
|
-
|
|
198
|
-
[Service]
|
|
199
|
-
Type=simple
|
|
200
|
-
ExecStart=/absolute/path/to/node /absolute/path/to/cli.js start --foreground
|
|
201
|
-
Restart=on-failure
|
|
202
|
-
RestartSec=5
|
|
203
|
-
|
|
204
|
-
[Install]
|
|
205
|
-
WantedBy=default.target
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Load it with:
|
|
209
|
-
|
|
210
|
-
```sh
|
|
211
|
-
systemctl --user daemon-reload
|
|
212
|
-
systemctl --user enable --now cabane-companion.service
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
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.
|
|
216
|
-
|
|
217
|
-
After an NVM/npm upgrade, stop the unit, rediscover and replace both executable paths, then reload and restart it:
|
|
218
|
-
|
|
219
|
-
```sh
|
|
220
|
-
systemctl --user stop cabane-companion.service
|
|
221
|
-
systemctl --user daemon-reload
|
|
222
|
-
systemctl --user start cabane-companion.service
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
To remove it completely:
|
|
226
|
-
|
|
227
|
-
```sh
|
|
228
|
-
systemctl --user disable --now cabane-companion.service
|
|
229
|
-
rm ~/.config/systemd/user/cabane-companion.service
|
|
230
|
-
systemctl --user daemon-reload
|
|
231
|
-
```
|
|
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.
|
|
232
136
|
|
|
233
|
-
|
|
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.
|
|
234
138
|
|
|
235
139
|
### Other supervisors
|
|
236
140
|
|
|
237
|
-
Run `cabane
|
|
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.
|
|
238
142
|
|
|
239
143
|
## Configuring an agent: what's in Cabane vs. on this machine
|
|
240
144
|
|
|
@@ -259,7 +163,7 @@ MCP servers in the agent's Environment don't depend on host access: a stdio serv
|
|
|
259
163
|
|
|
260
164
|
An agent with host access starts in its **Working directory**, set in Cabane in the same **Environment** dialog as host access. Left empty, or naming a folder that isn't on the device, the agent starts in the folder the Companion was started from; a missing folder is reported only in the Companion's log.
|
|
261
165
|
|
|
262
|
-
The machine can override it. A `cwd` in `~/.cabane/config.json` wins over the directory set in Cabane, and a prepare-hook wins over both. Add an `agents` map keyed by the agent's id, its `@username`, or `workspace-slug/username` (all three show up in `cabane
|
|
166
|
+
The machine can override it. A `cwd` in `~/.cabane/config.json` wins over the directory set in Cabane, and a prepare-hook wins over both. Add an `agents` map keyed by the agent's id, its `@username`, or `workspace-slug/username` (all three show up in `cabane status`, copy-pasteable):
|
|
263
167
|
|
|
264
168
|
```jsonc
|
|
265
169
|
{
|
|
@@ -278,7 +182,7 @@ The machine can override it. A `cwd` in `~/.cabane/config.json` wins over the di
|
|
|
278
182
|
}
|
|
279
183
|
```
|
|
280
184
|
|
|
281
|
-
A malformed `agents` block fails `cabane
|
|
185
|
+
A malformed `agents` block fails `cabane start` immediately with a message pointing at the exact field — it won't silently fall back.
|
|
282
186
|
|
|
283
187
|
**A hook that is the same for every agent goes at the top level instead.** If your hook works out the directory itself — from the conversation and agent ids the Companion hands it — then listing every agent by name is a roster you have to remember to update, and an agent you forget gets no hook at all and starts its turn wherever the Companion happens to be. So `prepareHook` can sit beside `agents` rather than inside it:
|
|
284
188
|
|
|
@@ -291,7 +195,7 @@ A malformed `agents` block fails `cabane-companion start` immediately with a mes
|
|
|
291
195
|
}
|
|
292
196
|
```
|
|
293
197
|
|
|
294
|
-
The agent's own entry wins when it names a hook; otherwise the device-level one applies. It survives a re-pair along with the rest of your local settings, and `cabane
|
|
198
|
+
The agent's own entry wins when it names a hook; otherwise the device-level one applies. It survives a re-pair along with the rest of your local settings, and `cabane status` prints it. A hook that fails still fails the turn loudly either way.
|
|
295
199
|
|
|
296
200
|
### Auto-memory (off by default)
|
|
297
201
|
|
|
@@ -390,7 +294,7 @@ Paths may start with `~/` and may be globs. An invalid `policy` doesn't stop the
|
|
|
390
294
|
|
|
391
295
|
A `gh auth login` session on the machine isn't used: `~/.config/gh` is unreadable in the sandbox. Only the token in `secrets.json` is.
|
|
392
296
|
|
|
393
|
-
`cabane
|
|
297
|
+
`cabane status` prints the effective posture: the filesystem mode, extra hosts, denied hosts and reads, the **names** of masked credentials, never their values, and each agent excepted on its row. The Companion also logs it once at start.
|
|
394
298
|
|
|
395
299
|
## Harnesses
|
|
396
300
|
|
|
@@ -408,7 +312,7 @@ What "exposed" means differs per harness, because each is discoverable in a diff
|
|
|
408
312
|
|
|
409
313
|
Claude Code and Codex each run a native binary their SDK ships, installed alongside the Companion as an optional npm dependency. Those are what a turn launches — not the `claude` / `codex` CLIs on your PATH, which are for setup and login. If one didn't install, the Companion says so on startup and on the device's page in Cabane, with the command that repairs it: `npm i -g @cabane/companion --include=optional`.
|
|
410
314
|
|
|
411
|
-
**You usually don't hand-edit the config.** Interactive `cabane
|
|
315
|
+
**You usually don't hand-edit the config.** Interactive `cabane start` offers each signed-in agent, and `cabane connect codex` (or `claude-code` / `opencode`) can connect one later. Editing `~/.cabane/config.json` yourself is the fallback and what a headless device wants:
|
|
412
316
|
|
|
413
317
|
```jsonc
|
|
414
318
|
{
|
|
@@ -419,7 +323,7 @@ Claude Code and Codex each run a native binary their SDK ships, installed alongs
|
|
|
419
323
|
}
|
|
420
324
|
```
|
|
421
325
|
|
|
422
|
-
Every block is optional and independent — set only the harnesses you have. Claude Code and Codex each take a flag and no URL, because the SDK bundled with the Companion spawns its own native binary per turn; opencode is a long-lived server addressed by URL, so run one `opencode serve` per Companion process. A hand-edit needs a restart (`cabane
|
|
326
|
+
Every block is optional and independent — set only the harnesses you have. Claude Code and Codex each take a flag and no URL, because the SDK bundled with the Companion spawns its own native binary per turn; opencode is a long-lived server addressed by URL, so run one `opencode serve` per Companion process. A hand-edit needs a restart (`cabane stop && cabane start`); the `connect` command updates a running Companion over its control socket.
|
|
423
327
|
|
|
424
328
|
Everything else about a Companion-run agent is the same whichever harness runs it — same assignment, same working directory, same secrets. A harness is a way to execute a turn, not a different way to run the Companion.
|
|
425
329
|
|
|
@@ -447,7 +351,7 @@ No SQLite, no embedded DB. Cabane is the source of truth for which agents to run
|
|
|
447
351
|
|
|
448
352
|
## Logs
|
|
449
353
|
|
|
450
|
-
`cabane
|
|
354
|
+
`cabane logs` shows the last 50 lines. Use `-n 20` for twenty lines or
|
|
451
355
|
`-f` to follow new lines until Ctrl-C. The initial read is capped at 1 MiB.
|
|
452
356
|
The file is `~/.cabane/companion.log` and is human-readable by default: local
|
|
453
357
|
`HH:MM:SS`, a level word for warnings and errors, then a sentence. Turns include
|
|
@@ -458,7 +362,7 @@ a reason, the next step and the transcript path.
|
|
|
458
362
|
- `warn`: degraded operation, such as reconnecting or updates waiting to arrive.
|
|
459
363
|
- `debug`: internal diagnostics for investigating a problem. Errors are always shown.
|
|
460
364
|
|
|
461
|
-
Choose with `cabane
|
|
365
|
+
Choose with `cabane start --log-level debug` or `"logLevel": "debug"`
|
|
462
366
|
in `~/.cabane/config.json`. Config changes apply on reload; a start flag wins.
|
|
463
367
|
Use `--log-format json` or `"logFormat": "json"` for machine-readable JSON with
|
|
464
368
|
full ids. The default is `human`; the terminal always stays human-readable.
|
|
@@ -471,10 +375,10 @@ Cabane shows the agent's final reply, but not _how_ it got there. When a turn mi
|
|
|
471
375
|
Read one back in a readable form:
|
|
472
376
|
|
|
473
377
|
```sh
|
|
474
|
-
cabane
|
|
475
|
-
cabane
|
|
476
|
-
cabane
|
|
477
|
-
cabane
|
|
378
|
+
cabane transcript --last # render the most recent turn
|
|
379
|
+
cabane transcript # list recent turns (newest first)
|
|
380
|
+
cabane transcript <file> # render a specific one (filename or a substring)
|
|
381
|
+
cabane transcript --follow # live-watch turns as they land (-f; Ctrl-C to stop)
|
|
478
382
|
```
|
|
479
383
|
|
|
480
384
|
`--follow` (`-f`) is a built-in live watch: it renders the current turn as its lines append and rolls to the next turn when a new dispatch starts — handy for demos or watching the agent work in real time.
|
|
@@ -483,4 +387,4 @@ It renders the conversation step by step — `🔧 tool(input) → ok/ERROR: res
|
|
|
483
387
|
|
|
484
388
|
## What v0 doesn't do yet
|
|
485
389
|
|
|
486
|
-
|
|
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
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
var MIN_NODE_MAJOR = 22;
|
|
5
|
+
var major = Number(process.versions.node.split(".")[0]);
|
|
6
|
+
if (!(major >= MIN_NODE_MAJOR)) {
|
|
7
|
+
process.stderr.write(
|
|
8
|
+
`Cabane needs Node ${MIN_NODE_MAJOR} or newer, and this is Node ${process.versions.node}. Install Node 22 or newer, or use the install script, which brings its own: curl -fsSL https://cabane.ai/install.sh | bash
|
|
9
|
+
`
|
|
10
|
+
);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
var { launch } = await import("./launch.js");
|
|
14
|
+
await launch();
|
|
@@ -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);
|