@cabane/companion 0.6.213 → 0.6.215
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 +25 -9
- package/dist/cli.js +364 -184
- package/dist/pairing-config.js +96 -36
- package/dist/runtime.js +1 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,30 +4,46 @@ The Cabane Companion runs your Cabane agents on a device you choose. It receives
|
|
|
4
4
|
|
|
5
5
|
It uses **Claude Code**, **Codex** or **OpenCode**. The package includes Claude Code and Codex execution binaries, but setup does not run a vendor CLI installer or sign you in. OpenCode needs its own running server.
|
|
6
6
|
|
|
7
|
-
Start with [
|
|
7
|
+
Start with [Cabane Companion](https://cabane.ai/docs/cabane-companion), then [Mac setup](https://cabane.ai/docs/mac-setup), [Windows setup](https://cabane.ai/docs/windows-setup) or [Server and VPS setup](https://cabane.ai/docs/server-and-vps-setup). [FAQ and Help](https://cabane.ai/docs/faq-and-help) covers sign-in and troubleshooting. This README is the device-side reference: commands, configuration and local state.
|
|
8
8
|
|
|
9
|
-
Pairing supplies a device token, not your Cabane password or a full-account token. The device token is for this device's assignments and liveness; assigned agents receive workspace-bound credentials. The [trust guide](https://cabane.ai/docs/
|
|
9
|
+
Pairing supplies a device token, not your Cabane password or a full-account token. The device token is for this device's assignments and liveness; assigned agents receive workspace-bound credentials. The [trust guide](https://cabane.ai/docs/cabane-companion) explains the layers, records and access.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
On the device, in a terminal. One of:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
curl -fsSL https://cabane.ai/install.sh | bash
|
|
16
|
+
curl -fsSL https://cabane.ai/install.sh | bash
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The script brings its own Node (checksum-checked, into `~/.cabane`) and needs no sudo. Or, with Node 22 or newer already there (the only route on Windows):
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
npm i -g @cabane/companion && cabane setup
|
|
22
|
+
npm i -g @cabane/companion && cabane setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`cabane setup
|
|
25
|
+
`cabane setup` on an unpaired device prints a code and a link:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Pair this device
|
|
29
|
+
Enter this code in Cabane, or press Return to open the page:
|
|
30
|
+
|
|
31
|
+
WXYZ-2345
|
|
32
|
+
https://app.cabane.ai/pair/enter-code?code=WXYZ-2345
|
|
33
|
+
|
|
34
|
+
⠋ Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Enter the code in Cabane, signed in as yourself: from your own Settings → Devices → Add a device, from a workspace's Settings → Devices → Set up a new device, or on the page the link opens. Cabane shows which device asked; confirming pairs it to your account and nothing else. A workspace's dialog then adds the device to that workspace; from anywhere else, add it to a workspace from that workspace's Settings → Devices. Pairing from a workspace you're setting up places its default agent on the device once a harness there is usable.
|
|
38
|
+
|
|
39
|
+
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the server the device pairs with: pass `--server <url>` for your own Cabane.
|
|
40
|
+
|
|
41
|
+
From an interactive terminal, setup then starts the Companion in the background, waits for its first check-in and reports the device's state. Without an interactive terminal, setup stays attached to the running Companion.
|
|
26
42
|
|
|
27
43
|
If no harness is usable, interactive setup asks which one you want to set up. Follow the complete CLI installation and sign-in steps for [Mac setup](https://cabane.ai/docs/mac-setup#2-sign-in), [Windows setup](https://cabane.ai/docs/windows-setup#2-sign-in) or [Server and VPS setup](https://cabane.ai/docs/server-and-vps-setup#2-sign-in-without-a-browser), then check `cabane status`.
|
|
28
44
|
|
|
29
45
|
```
|
|
30
|
-
✓ Paired as "maya-mbp"
|
|
46
|
+
✓ Paired as "maya-mbp".
|
|
31
47
|
Cabane is running in the background. This device is ready — checked in 2s ago.
|
|
32
48
|
|
|
33
49
|
Harnesses on this device
|
|
@@ -38,7 +54,7 @@ If no harness is usable, interactive setup asks which one you want to set up. Fo
|
|
|
38
54
|
Status cabane status Stop cabane stop Log cabane logs
|
|
39
55
|
Start at login and keep Cabane updating itself: cabane service install
|
|
40
56
|
|
|
41
|
-
In Cabane: https://app.cabane.ai/
|
|
57
|
+
In Cabane: https://app.cabane.ai/settings/devices
|
|
42
58
|
```
|
|
43
59
|
|
|
44
60
|
Setup never installs the service. On macOS or Linux, run `cabane service install` when you want the Companion to start at login and keep itself updated; the closing block names it. Native Windows uses the background process and has no service installer.
|
|
@@ -51,7 +67,7 @@ The [server guide](https://cabane.ai/docs/server-and-vps-setup) provides a Docke
|
|
|
51
67
|
|
|
52
68
|
| Command | What it does |
|
|
53
69
|
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
54
|
-
| `setup
|
|
70
|
+
| `setup` | Unpaired: print a code to enter in Cabane, wait for the confirm, then detect harnesses and start. Paired: a re-runnable checklist that reports harnesses and starts what isn't running. |
|
|
55
71
|
| `start [--foreground]` | Start the Companion without prompts. From an interactive terminal, run in the background; noninteractive use may stay attached. Unpaired, it names `setup`. `--foreground` stays attached for your supervisor. |
|
|
56
72
|
| `stop` | Stop the Companion cleanly. Ends any turn in progress. |
|
|
57
73
|
| `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, where agents work, declared secret names, and local overrides. |
|
package/dist/cli.js
CHANGED
|
@@ -1418,7 +1418,7 @@ function pathForLog(path) {
|
|
|
1418
1418
|
const q = path.indexOf("?");
|
|
1419
1419
|
return q === -1 ? path : path.slice(0, q);
|
|
1420
1420
|
}
|
|
1421
|
-
function
|
|
1421
|
+
function sleep2(ms, signal) {
|
|
1422
1422
|
return new Promise((resolve2) => {
|
|
1423
1423
|
if (signal?.aborted) return resolve2();
|
|
1424
1424
|
const timer = setTimeout(() => {
|
|
@@ -1521,7 +1521,7 @@ var init_api = __esm({
|
|
|
1521
1521
|
},
|
|
1522
1522
|
"companion: API call failed transiently; retrying"
|
|
1523
1523
|
);
|
|
1524
|
-
await
|
|
1524
|
+
await sleep2(delayMs, signal);
|
|
1525
1525
|
if (signal?.aborted) throw err;
|
|
1526
1526
|
}
|
|
1527
1527
|
}
|
|
@@ -7510,7 +7510,7 @@ var init_opencode_lock = __esm({
|
|
|
7510
7510
|
});
|
|
7511
7511
|
|
|
7512
7512
|
// src/turn-containment.ts
|
|
7513
|
-
import { execFile, execFileSync, spawn as
|
|
7513
|
+
import { execFile, execFileSync, spawn as spawn4 } from "child_process";
|
|
7514
7514
|
import { readFileSync as readFileSync6 } from "fs";
|
|
7515
7515
|
import { promisify as promisify2 } from "util";
|
|
7516
7516
|
function turnScopeUnit(turnId, attempt) {
|
|
@@ -7639,7 +7639,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7639
7639
|
command: options.command
|
|
7640
7640
|
});
|
|
7641
7641
|
return asContained(
|
|
7642
|
-
|
|
7642
|
+
spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
|
|
7643
7643
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
7644
7644
|
env: options.env,
|
|
7645
7645
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7647,7 +7647,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7647
7647
|
);
|
|
7648
7648
|
}
|
|
7649
7649
|
function spawnInGroup(options) {
|
|
7650
|
-
const child =
|
|
7650
|
+
const child = spawn4(options.command, options.args, {
|
|
7651
7651
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
7652
7652
|
env: options.env,
|
|
7653
7653
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7729,7 +7729,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7729
7729
|
} catch {
|
|
7730
7730
|
}
|
|
7731
7731
|
}
|
|
7732
|
-
await
|
|
7732
|
+
await sleep3(fallbackGraceMs);
|
|
7733
7733
|
const afterTerm = processGroupMembers(pgid);
|
|
7734
7734
|
if (!afterTerm.ok) {
|
|
7735
7735
|
result.failures.push({
|
|
@@ -7792,7 +7792,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7792
7792
|
}
|
|
7793
7793
|
};
|
|
7794
7794
|
}
|
|
7795
|
-
var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS,
|
|
7795
|
+
var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS, sleep3, MANAGER_ENV_KEYS, managerEnvKey;
|
|
7796
7796
|
var init_turn_containment = __esm({
|
|
7797
7797
|
"src/turn-containment.ts"() {
|
|
7798
7798
|
"use strict";
|
|
@@ -7802,7 +7802,7 @@ var init_turn_containment = __esm({
|
|
|
7802
7802
|
FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
7803
7803
|
CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
7804
7804
|
OOM_READ_TIMEOUT_MS = 1500;
|
|
7805
|
-
|
|
7805
|
+
sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7806
7806
|
MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
|
|
7807
7807
|
managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
|
|
7808
7808
|
}
|
|
@@ -11729,7 +11729,6 @@ async function liveCompanionState() {
|
|
|
11729
11729
|
// src/commands/setup.ts
|
|
11730
11730
|
init_cabane_command();
|
|
11731
11731
|
init_config();
|
|
11732
|
-
import { hostname as hostname2 } from "os";
|
|
11733
11732
|
|
|
11734
11733
|
// src/logger.ts
|
|
11735
11734
|
init_config();
|
|
@@ -11904,61 +11903,346 @@ function getLogger() {
|
|
|
11904
11903
|
return cached;
|
|
11905
11904
|
}
|
|
11906
11905
|
|
|
11906
|
+
// src/pair-flow.ts
|
|
11907
|
+
init_cabane_command();
|
|
11908
|
+
import { spawn as spawn3 } from "child_process";
|
|
11909
|
+
import { hostname } from "os";
|
|
11910
|
+
|
|
11907
11911
|
// src/enrollment.ts
|
|
11908
11912
|
init_errors();
|
|
11909
|
-
|
|
11913
|
+
var EnrollmentExpiredError = class extends CompanionError {
|
|
11914
|
+
constructor() {
|
|
11915
|
+
super("this pairing code expired before it was confirmed.");
|
|
11916
|
+
this.name = "EnrollmentExpiredError";
|
|
11917
|
+
}
|
|
11918
|
+
};
|
|
11919
|
+
var EnrollmentCancelledError = class extends CompanionError {
|
|
11920
|
+
constructor() {
|
|
11921
|
+
super("pairing was cancelled.");
|
|
11922
|
+
this.name = "EnrollmentCancelledError";
|
|
11923
|
+
}
|
|
11924
|
+
};
|
|
11925
|
+
function trimBase(baseUrl) {
|
|
11926
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
11927
|
+
}
|
|
11928
|
+
async function postJson(baseUrl, path, body, signal) {
|
|
11910
11929
|
let res;
|
|
11911
11930
|
try {
|
|
11912
|
-
res = await fetch(`${baseUrl
|
|
11931
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
11913
11932
|
method: "POST",
|
|
11914
|
-
headers: {
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
},
|
|
11918
|
-
body: JSON.stringify({
|
|
11919
|
-
userCode: input.userCode,
|
|
11920
|
-
label: input.label,
|
|
11921
|
-
platform: process.platform,
|
|
11922
|
-
arch: process.arch,
|
|
11923
|
-
nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
|
|
11924
|
-
})
|
|
11933
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
11934
|
+
body: JSON.stringify(body),
|
|
11935
|
+
...signal ? { signal } : {}
|
|
11925
11936
|
});
|
|
11926
11937
|
} catch {
|
|
11938
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
11927
11939
|
throw new CompanionError(
|
|
11928
11940
|
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
11929
11941
|
);
|
|
11930
11942
|
}
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
throw new CompanionError(
|
|
11937
|
-
"The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
11938
|
-
);
|
|
11939
|
-
if (res.status === 404 || res.status === 410)
|
|
11943
|
+
let raw;
|
|
11944
|
+
try {
|
|
11945
|
+
raw = await res.text();
|
|
11946
|
+
} catch {
|
|
11947
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
11940
11948
|
throw new CompanionError(
|
|
11941
|
-
|
|
11949
|
+
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
11942
11950
|
);
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
if (
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11951
|
+
}
|
|
11952
|
+
let parsed = void 0;
|
|
11953
|
+
if (raw.length > 0) {
|
|
11954
|
+
try {
|
|
11955
|
+
parsed = JSON.parse(raw);
|
|
11956
|
+
} catch {
|
|
11957
|
+
parsed = raw;
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
if (res.status >= 400) {
|
|
11961
|
+
if (res.status === 429)
|
|
11962
|
+
throw new CompanionError(
|
|
11963
|
+
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
11964
|
+
);
|
|
11965
|
+
if (res.status === 404 && path.endsWith("/code"))
|
|
11966
|
+
throw new CompanionError(
|
|
11967
|
+
`No pairing endpoint at ${baseUrl}. Either that server is too old for this companion, or it isn't a Cabane API origin: the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
|
|
11968
|
+
);
|
|
11969
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
11970
|
+
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
11971
|
+
}
|
|
11972
|
+
return parsed;
|
|
11973
|
+
}
|
|
11974
|
+
function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
11975
|
+
return postJson(
|
|
11976
|
+
baseUrl,
|
|
11977
|
+
"/api/device-enrollment/code",
|
|
11978
|
+
{ ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
|
|
11979
|
+
opts.signal
|
|
11980
|
+
);
|
|
11981
|
+
}
|
|
11982
|
+
function hostFacts() {
|
|
11983
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
|
|
11949
11984
|
return {
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
|
|
11954
|
-
..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
|
|
11955
|
-
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
11985
|
+
platform: process.platform,
|
|
11986
|
+
arch: process.arch,
|
|
11987
|
+
...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
|
|
11956
11988
|
};
|
|
11957
11989
|
}
|
|
11958
11990
|
function deviceLabelFromHostname(hostname3) {
|
|
11959
11991
|
const trimmed = hostname3.trim().replace(/\.local$/i, "");
|
|
11960
11992
|
return trimmed.length ? trimmed.slice(0, 120) : void 0;
|
|
11961
11993
|
}
|
|
11994
|
+
function pollOnce(baseUrl, deviceCode, signal) {
|
|
11995
|
+
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
|
|
11996
|
+
}
|
|
11997
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
11998
|
+
async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
11999
|
+
const now = opts.now ?? (() => Date.now());
|
|
12000
|
+
const wait = opts.sleepMs ?? sleep;
|
|
12001
|
+
const deadline = now() + code.expiresIn * 1e3;
|
|
12002
|
+
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
12003
|
+
while (now() < deadline) {
|
|
12004
|
+
await wait(intervalMs);
|
|
12005
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
12006
|
+
const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
|
|
12007
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
12008
|
+
if (res.status === "complete") {
|
|
12009
|
+
if (!res.deviceToken || !res.baseUrl)
|
|
12010
|
+
throw new CompanionError("Cabane reported the pairing complete but returned no token.");
|
|
12011
|
+
return {
|
|
12012
|
+
baseUrl: res.baseUrl,
|
|
12013
|
+
deviceToken: res.deviceToken,
|
|
12014
|
+
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
12015
|
+
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
|
|
12016
|
+
};
|
|
12017
|
+
}
|
|
12018
|
+
if (res.status === "expired") throw new EnrollmentExpiredError();
|
|
12019
|
+
if (res.status === "slow_down") intervalMs += 1e3;
|
|
12020
|
+
}
|
|
12021
|
+
throw new EnrollmentExpiredError();
|
|
12022
|
+
}
|
|
12023
|
+
|
|
12024
|
+
// src/terminal-copy.ts
|
|
12025
|
+
init_cabane_command();
|
|
12026
|
+
var UNPAIRED = `This device isn't paired yet. Run ${cabane("setup")} on it.`;
|
|
12027
|
+
var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
|
|
12028
|
+
function age(at, now) {
|
|
12029
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
|
|
12030
|
+
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
|
|
12031
|
+
}
|
|
12032
|
+
function deviceLine(f, now = Date.now()) {
|
|
12033
|
+
if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
|
|
12034
|
+
if (f.lastHeartbeatRefused)
|
|
12035
|
+
return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
|
|
12036
|
+
if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
|
|
12037
|
+
const since = age(f.lastHeartbeatAckAt, now);
|
|
12038
|
+
if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
|
|
12039
|
+
return `can't reach the server \u2014 last checked in ${since} ago`;
|
|
12040
|
+
const view = f.lastAcknowledgedDevice;
|
|
12041
|
+
if (!view) return `checked in ${since} ago`;
|
|
12042
|
+
const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
|
|
12043
|
+
return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
|
|
12044
|
+
}
|
|
12045
|
+
function pairingLine(pair) {
|
|
12046
|
+
return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}".`;
|
|
12047
|
+
}
|
|
12048
|
+
function pathLines(reload) {
|
|
12049
|
+
return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
|
|
12050
|
+
}
|
|
12051
|
+
function agentLines(detection, opts) {
|
|
12052
|
+
const rows = agentRows(detection, opts);
|
|
12053
|
+
const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
|
|
12054
|
+
const width = Math.max(...names.map((name) => name.length)) + 3;
|
|
12055
|
+
return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
|
|
12056
|
+
}
|
|
12057
|
+
var modeLines = {
|
|
12058
|
+
background: "Cabane is running in the background",
|
|
12059
|
+
service: "Cabane is running as a service",
|
|
12060
|
+
attached: "Cabane is running in this process",
|
|
12061
|
+
existing: "Cabane was already running"
|
|
12062
|
+
};
|
|
12063
|
+
function closingBlock(input, now = Date.now()) {
|
|
12064
|
+
const { mode, facts } = input;
|
|
12065
|
+
const device = deviceLine(facts, now);
|
|
12066
|
+
const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
|
|
12067
|
+
const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
|
|
12068
|
+
const modeLine = modeLines[mode];
|
|
12069
|
+
const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
|
|
12070
|
+
const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
|
|
12071
|
+
if (noAck || stale || facts.lastHeartbeatRefused) {
|
|
12072
|
+
lines2.push(
|
|
12073
|
+
` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
|
|
12074
|
+
);
|
|
12075
|
+
} else {
|
|
12076
|
+
lines2.push(" Harnesses on this device");
|
|
12077
|
+
lines2.push(
|
|
12078
|
+
...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
|
|
12079
|
+
);
|
|
12080
|
+
lines2.push(
|
|
12081
|
+
"",
|
|
12082
|
+
` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
|
|
12083
|
+
);
|
|
12084
|
+
if (!input.service && input.servicePlatform) {
|
|
12085
|
+
lines2.push(
|
|
12086
|
+
` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
|
|
12087
|
+
);
|
|
12088
|
+
lines2.push(
|
|
12089
|
+
` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
|
|
12090
|
+
);
|
|
12091
|
+
} else if (input.service && input.autoUpdate)
|
|
12092
|
+
lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
|
|
12093
|
+
}
|
|
12094
|
+
if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
|
|
12095
|
+
if (mode === "attached") lines2.push("", " Listening for messages\u2026");
|
|
12096
|
+
return lines2;
|
|
12097
|
+
}
|
|
12098
|
+
function startFailure(message2, reload) {
|
|
12099
|
+
return [
|
|
12100
|
+
...pathLines(reload),
|
|
12101
|
+
`\u2717 Cabane couldn't start: ${message2}`,
|
|
12102
|
+
"",
|
|
12103
|
+
` Log: ${cabane("logs")} Try again: ${cabane("start")}`
|
|
12104
|
+
];
|
|
12105
|
+
}
|
|
12106
|
+
|
|
12107
|
+
// src/pair-flow.ts
|
|
12108
|
+
var MAX_CODES = 3;
|
|
12109
|
+
async function pairDevice(opts, deps = {}) {
|
|
12110
|
+
const requestCode = deps.requestCode ?? requestEnrollmentCode;
|
|
12111
|
+
const wait = deps.await ?? awaitEnrollment;
|
|
12112
|
+
const open2 = deps.open ?? openLink;
|
|
12113
|
+
const { ui, interactive } = opts;
|
|
12114
|
+
const label = deviceLabelFromHostname(hostname());
|
|
12115
|
+
const aborter = new AbortController();
|
|
12116
|
+
const onSigint = () => aborter.abort();
|
|
12117
|
+
process.on("SIGINT", onSigint);
|
|
12118
|
+
const spinner = ui.spinner();
|
|
12119
|
+
let current = null;
|
|
12120
|
+
const keys = interactive ? watchReturn({
|
|
12121
|
+
onReturn: () => {
|
|
12122
|
+
if (!current) return;
|
|
12123
|
+
const link = current.verificationUriComplete;
|
|
12124
|
+
void open2(link).then((opened) => {
|
|
12125
|
+
if (!opened)
|
|
12126
|
+
spinner.message(
|
|
12127
|
+
`Couldn't open a browser here. Open this page on any device: ${link}`
|
|
12128
|
+
);
|
|
12129
|
+
});
|
|
12130
|
+
},
|
|
12131
|
+
onInterrupt: () => aborter.abort(),
|
|
12132
|
+
...deps.input ? { input: deps.input } : {}
|
|
12133
|
+
}) : null;
|
|
12134
|
+
let minutesWaited = 0;
|
|
12135
|
+
let waiting = false;
|
|
12136
|
+
try {
|
|
12137
|
+
for (let attempt = 1; attempt <= MAX_CODES; attempt += 1) {
|
|
12138
|
+
let code;
|
|
12139
|
+
try {
|
|
12140
|
+
code = await requestCode(opts.baseUrl, {
|
|
12141
|
+
...label ? { label } : {},
|
|
12142
|
+
signal: aborter.signal
|
|
12143
|
+
});
|
|
12144
|
+
} catch (err) {
|
|
12145
|
+
if (!(err instanceof EnrollmentCancelledError)) throw err;
|
|
12146
|
+
ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
|
|
12147
|
+
process.exitCode = 130;
|
|
12148
|
+
return null;
|
|
12149
|
+
}
|
|
12150
|
+
current = code;
|
|
12151
|
+
const minutes = Math.max(1, Math.round(code.expiresIn / 60));
|
|
12152
|
+
const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUriComplete}`, ""];
|
|
12153
|
+
if (attempt === 1) {
|
|
12154
|
+
ui.step([
|
|
12155
|
+
"Pair this device",
|
|
12156
|
+
interactive ? "Enter this code in Cabane, or press Return to open the page:" : "Enter this code in Cabane:",
|
|
12157
|
+
...codeLines
|
|
12158
|
+
]);
|
|
12159
|
+
} else {
|
|
12160
|
+
ui.step(codeLines.slice(1));
|
|
12161
|
+
}
|
|
12162
|
+
spinner.start(
|
|
12163
|
+
`Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
|
|
12164
|
+
);
|
|
12165
|
+
waiting = true;
|
|
12166
|
+
try {
|
|
12167
|
+
const device = await wait(opts.baseUrl, code, { signal: aborter.signal });
|
|
12168
|
+
waiting = false;
|
|
12169
|
+
spinner.stop(pairingLine(device));
|
|
12170
|
+
return device;
|
|
12171
|
+
} catch (err) {
|
|
12172
|
+
waiting = false;
|
|
12173
|
+
if (err instanceof EnrollmentCancelledError) {
|
|
12174
|
+
spinner.stop("Pairing stopped.");
|
|
12175
|
+
ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
|
|
12176
|
+
process.exitCode = 130;
|
|
12177
|
+
return null;
|
|
12178
|
+
}
|
|
12179
|
+
if (!(err instanceof EnrollmentExpiredError)) {
|
|
12180
|
+
spinner.stop("Pairing stopped.");
|
|
12181
|
+
throw err;
|
|
12182
|
+
}
|
|
12183
|
+
minutesWaited += minutes;
|
|
12184
|
+
if (attempt < MAX_CODES) {
|
|
12185
|
+
spinner.stop("That code expired. Here's a fresh one:");
|
|
12186
|
+
continue;
|
|
12187
|
+
}
|
|
12188
|
+
spinner.stop(
|
|
12189
|
+
`Nothing confirmed in ${minutesWaited} minutes. Run ${cabane("setup")} when you're ready.`
|
|
12190
|
+
);
|
|
12191
|
+
process.exitCode = 1;
|
|
12192
|
+
return null;
|
|
12193
|
+
}
|
|
12194
|
+
}
|
|
12195
|
+
return null;
|
|
12196
|
+
} finally {
|
|
12197
|
+
if (waiting) spinner.stop("Pairing stopped.");
|
|
12198
|
+
keys?.release();
|
|
12199
|
+
process.removeListener("SIGINT", onSigint);
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
function watchReturn(handlers) {
|
|
12203
|
+
const input = handlers.input ?? process.stdin;
|
|
12204
|
+
if (!input.isTTY || typeof input.setRawMode !== "function") return { release: () => {
|
|
12205
|
+
} };
|
|
12206
|
+
const wasRaw = input.isRaw;
|
|
12207
|
+
const onData = (chunk) => {
|
|
12208
|
+
const text = chunk.toString();
|
|
12209
|
+
if (text.includes("")) handlers.onInterrupt();
|
|
12210
|
+
else if (text.includes("\r") || text.includes("\n")) handlers.onReturn();
|
|
12211
|
+
};
|
|
12212
|
+
input.setRawMode(true);
|
|
12213
|
+
input.on("data", onData);
|
|
12214
|
+
input.resume();
|
|
12215
|
+
return {
|
|
12216
|
+
release: () => {
|
|
12217
|
+
input.removeListener("data", onData);
|
|
12218
|
+
input.setRawMode(wasRaw);
|
|
12219
|
+
input.pause();
|
|
12220
|
+
}
|
|
12221
|
+
};
|
|
12222
|
+
}
|
|
12223
|
+
function pageOpener(platform, link) {
|
|
12224
|
+
if (platform === "darwin") return { command: "open", args: [link] };
|
|
12225
|
+
if (platform === "win32") return { command: "cmd", args: ["/c", "start", '""', `"${link}"`] };
|
|
12226
|
+
return { command: "xdg-open", args: [link] };
|
|
12227
|
+
}
|
|
12228
|
+
function openLink(link) {
|
|
12229
|
+
return new Promise((resolve2) => {
|
|
12230
|
+
const { command, args } = pageOpener(process.platform, link);
|
|
12231
|
+
try {
|
|
12232
|
+
const child = spawn3(command, args, {
|
|
12233
|
+
stdio: "ignore",
|
|
12234
|
+
detached: true,
|
|
12235
|
+
// `cmd` parses its own line: the quoting above is the one it needs, as written.
|
|
12236
|
+
...process.platform === "win32" ? { windowsVerbatimArguments: true } : {}
|
|
12237
|
+
});
|
|
12238
|
+
child.once("error", () => resolve2(false));
|
|
12239
|
+
child.once("exit", (code) => resolve2(code === 0));
|
|
12240
|
+
child.unref();
|
|
12241
|
+
} catch {
|
|
12242
|
+
resolve2(false);
|
|
12243
|
+
}
|
|
12244
|
+
});
|
|
12245
|
+
}
|
|
11962
12246
|
|
|
11963
12247
|
// src/commands/start.ts
|
|
11964
12248
|
init_cabane_command();
|
|
@@ -12501,7 +12785,7 @@ var emptyHeartbeatStatus = () => ({
|
|
|
12501
12785
|
|
|
12502
12786
|
// src/supervisor.ts
|
|
12503
12787
|
init_cabane_command();
|
|
12504
|
-
import { hostname } from "os";
|
|
12788
|
+
import { hostname as hostname2 } from "os";
|
|
12505
12789
|
|
|
12506
12790
|
// src/log-copy.ts
|
|
12507
12791
|
init_failure();
|
|
@@ -13541,7 +13825,7 @@ function hostReport() {
|
|
|
13541
13825
|
}
|
|
13542
13826
|
|
|
13543
13827
|
// src/providers/local-process.ts
|
|
13544
|
-
import { spawn as
|
|
13828
|
+
import { spawn as spawn5 } from "child_process";
|
|
13545
13829
|
import { constants as constants2 } from "os";
|
|
13546
13830
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13547
13831
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
@@ -13552,7 +13836,7 @@ function exitCode(code, signal) {
|
|
|
13552
13836
|
}
|
|
13553
13837
|
function createLocalProcessProvider(opts = {}) {
|
|
13554
13838
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13555
|
-
const spawnFn = opts.spawnFn ??
|
|
13839
|
+
const spawnFn = opts.spawnFn ?? spawn5;
|
|
13556
13840
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13557
13841
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13558
13842
|
const groupAlive = (pgid) => {
|
|
@@ -13928,7 +14212,7 @@ var SseSubscriber = class {
|
|
|
13928
14212
|
init_transcript_writer();
|
|
13929
14213
|
|
|
13930
14214
|
// src/updater.ts
|
|
13931
|
-
import { spawn as
|
|
14215
|
+
import { spawn as spawn6 } from "child_process";
|
|
13932
14216
|
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
|
|
13933
14217
|
import { dirname as dirname11, join as join26 } from "path";
|
|
13934
14218
|
|
|
@@ -14143,7 +14427,7 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
14143
14427
|
let stderr = "";
|
|
14144
14428
|
let child;
|
|
14145
14429
|
try {
|
|
14146
|
-
child =
|
|
14430
|
+
child = spawn6(command, [...args], {
|
|
14147
14431
|
stdio: ["ignore", "pipe", "pipe"],
|
|
14148
14432
|
env: opts.env ?? process.env
|
|
14149
14433
|
});
|
|
@@ -14647,7 +14931,7 @@ var CompanionSupervisor = class {
|
|
|
14647
14931
|
if (this.stopped) return;
|
|
14648
14932
|
this.log.info(
|
|
14649
14933
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
14650
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
14934
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
14651
14935
|
);
|
|
14652
14936
|
const retired = retiredConfigKeysOnDisk();
|
|
14653
14937
|
if (retired.length > 0) {
|
|
@@ -14682,9 +14966,7 @@ var CompanionSupervisor = class {
|
|
|
14682
14966
|
this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
|
|
14683
14967
|
this.detectTimer.unref?.();
|
|
14684
14968
|
if (!this.deviceApi || !this.config.deviceToken) {
|
|
14685
|
-
this.log.warn(
|
|
14686
|
-
"This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
|
|
14687
|
-
);
|
|
14969
|
+
this.log.warn(`This device isn't paired. Run ${cabane("setup")} on it.`);
|
|
14688
14970
|
return;
|
|
14689
14971
|
}
|
|
14690
14972
|
this.deviceSub = new SseSubscriber({
|
|
@@ -14695,7 +14977,7 @@ var CompanionSupervisor = class {
|
|
|
14695
14977
|
log: this.log,
|
|
14696
14978
|
lastEventId: null,
|
|
14697
14979
|
onOpen: () => {
|
|
14698
|
-
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
14980
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
|
|
14699
14981
|
this.deviceConnected = true;
|
|
14700
14982
|
for (const wr of this.workspaces.values()) {
|
|
14701
14983
|
this.hub.setConnected(wr.workspaceId, true);
|
|
@@ -15928,9 +16210,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
15928
16210
|
}
|
|
15929
16211
|
function defaultReexec() {
|
|
15930
16212
|
clearRuntimeState();
|
|
15931
|
-
void import("child_process").then(({ spawn:
|
|
16213
|
+
void import("child_process").then(({ spawn: spawn8 }) => {
|
|
15932
16214
|
try {
|
|
15933
|
-
const child =
|
|
16215
|
+
const child = spawn8(process.execPath, process.argv.slice(1), {
|
|
15934
16216
|
stdio: "inherit",
|
|
15935
16217
|
detached: false
|
|
15936
16218
|
});
|
|
@@ -16279,13 +16561,13 @@ async function observeStart(deps = {}) {
|
|
|
16279
16561
|
const read2 = deps.readState ?? readLiveRuntimeState;
|
|
16280
16562
|
const verify = deps.verify ?? verifyRuntime;
|
|
16281
16563
|
const now = deps.now ?? Date.now;
|
|
16282
|
-
const
|
|
16564
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
16283
16565
|
const deadline = now() + 8e3;
|
|
16284
16566
|
for (; ; ) {
|
|
16285
16567
|
const state = await read2();
|
|
16286
16568
|
if (state?.socket && await verify(state) === "ours") return state;
|
|
16287
16569
|
if (now() >= deadline) return null;
|
|
16288
|
-
await
|
|
16570
|
+
await sleep4(Math.min(150, Math.max(0, deadline - now())));
|
|
16289
16571
|
}
|
|
16290
16572
|
}
|
|
16291
16573
|
function markLaunchRequester() {
|
|
@@ -16703,7 +16985,7 @@ async function controlService(action, deps) {
|
|
|
16703
16985
|
init_cabane_command();
|
|
16704
16986
|
init_cli_entry();
|
|
16705
16987
|
init_config();
|
|
16706
|
-
import { spawn as
|
|
16988
|
+
import { spawn as spawn7 } from "child_process";
|
|
16707
16989
|
import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
|
|
16708
16990
|
init_config();
|
|
16709
16991
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -16785,7 +17067,7 @@ function defaultSpawnDetached(args) {
|
|
|
16785
17067
|
mkdirSync17(cabaneDir(), { recursive: true });
|
|
16786
17068
|
const logFd = openSync4(companionLogPath(), "a");
|
|
16787
17069
|
try {
|
|
16788
|
-
const child =
|
|
17070
|
+
const child = spawn7(process.execPath, [cliPath, ...args], {
|
|
16789
17071
|
detached: true,
|
|
16790
17072
|
stdio: ["ignore", logFd, logFd],
|
|
16791
17073
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -16803,89 +17085,6 @@ function defaultSpawnDetached(args) {
|
|
|
16803
17085
|
}
|
|
16804
17086
|
}
|
|
16805
17087
|
|
|
16806
|
-
// src/terminal-copy.ts
|
|
16807
|
-
init_cabane_command();
|
|
16808
|
-
var UNPAIRED = "This device isn't paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here.";
|
|
16809
|
-
var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
|
|
16810
|
-
function age(at, now) {
|
|
16811
|
-
const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
|
|
16812
|
-
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
|
|
16813
|
-
}
|
|
16814
|
-
function deviceLine(f, now = Date.now()) {
|
|
16815
|
-
if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
|
|
16816
|
-
if (f.lastHeartbeatRefused)
|
|
16817
|
-
return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
|
|
16818
|
-
if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
|
|
16819
|
-
const since = age(f.lastHeartbeatAckAt, now);
|
|
16820
|
-
if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
|
|
16821
|
-
return `can't reach the server \u2014 last checked in ${since} ago`;
|
|
16822
|
-
const view = f.lastAcknowledgedDevice;
|
|
16823
|
-
if (!view) return `checked in ${since} ago`;
|
|
16824
|
-
const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
|
|
16825
|
-
return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
|
|
16826
|
-
}
|
|
16827
|
-
function pairingLine(pair) {
|
|
16828
|
-
return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}" with ${pair.workspace?.name ?? "the workspace"}.`;
|
|
16829
|
-
}
|
|
16830
|
-
function pathLines(reload) {
|
|
16831
|
-
return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
|
|
16832
|
-
}
|
|
16833
|
-
function agentLines(detection, opts) {
|
|
16834
|
-
const rows = agentRows(detection, opts);
|
|
16835
|
-
const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
|
|
16836
|
-
const width = Math.max(...names.map((name) => name.length)) + 3;
|
|
16837
|
-
return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
|
|
16838
|
-
}
|
|
16839
|
-
var modeLines = {
|
|
16840
|
-
background: "Cabane is running in the background",
|
|
16841
|
-
service: "Cabane is running as a service",
|
|
16842
|
-
attached: "Cabane is running in this process",
|
|
16843
|
-
existing: "Cabane was already running"
|
|
16844
|
-
};
|
|
16845
|
-
function closingBlock(input, now = Date.now()) {
|
|
16846
|
-
const { mode, facts } = input;
|
|
16847
|
-
const device = deviceLine(facts, now);
|
|
16848
|
-
const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
|
|
16849
|
-
const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
|
|
16850
|
-
const modeLine = modeLines[mode];
|
|
16851
|
-
const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
|
|
16852
|
-
const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
|
|
16853
|
-
if (noAck || stale || facts.lastHeartbeatRefused) {
|
|
16854
|
-
lines2.push(
|
|
16855
|
-
` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
|
|
16856
|
-
);
|
|
16857
|
-
} else {
|
|
16858
|
-
lines2.push(" Harnesses on this device");
|
|
16859
|
-
lines2.push(
|
|
16860
|
-
...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
|
|
16861
|
-
);
|
|
16862
|
-
lines2.push(
|
|
16863
|
-
"",
|
|
16864
|
-
` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
|
|
16865
|
-
);
|
|
16866
|
-
if (!input.service && input.servicePlatform) {
|
|
16867
|
-
lines2.push(
|
|
16868
|
-
` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
|
|
16869
|
-
);
|
|
16870
|
-
lines2.push(
|
|
16871
|
-
` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
|
|
16872
|
-
);
|
|
16873
|
-
} else if (input.service && input.autoUpdate)
|
|
16874
|
-
lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
|
|
16875
|
-
}
|
|
16876
|
-
if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
|
|
16877
|
-
if (mode === "attached") lines2.push("", " Listening for messages\u2026");
|
|
16878
|
-
return lines2;
|
|
16879
|
-
}
|
|
16880
|
-
function startFailure(message2, reload) {
|
|
16881
|
-
return [
|
|
16882
|
-
...pathLines(reload),
|
|
16883
|
-
`\u2717 Cabane couldn't start: ${message2}`,
|
|
16884
|
-
"",
|
|
16885
|
-
` Log: ${cabane("logs")} Try again: ${cabane("start")}`
|
|
16886
|
-
];
|
|
16887
|
-
}
|
|
16888
|
-
|
|
16889
17088
|
// src/terminal-status.ts
|
|
16890
17089
|
async function readTerminalStatus(socket, timeoutMs = 1e3) {
|
|
16891
17090
|
if (!socket) return emptyHeartbeatStatus();
|
|
@@ -17024,18 +17223,17 @@ async function startScript(opts, deps) {
|
|
|
17024
17223
|
if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
|
|
17025
17224
|
}
|
|
17026
17225
|
}
|
|
17027
|
-
const
|
|
17028
|
-
const address = opts.pairing ? opts.pairing.devicesUrl : personalUrl;
|
|
17226
|
+
const address = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
|
|
17029
17227
|
if (!daemon)
|
|
17030
17228
|
for (const line of closingBlock({
|
|
17031
17229
|
mode,
|
|
17032
|
-
facts: { ...status2, running: true, devicesUrl: address
|
|
17230
|
+
facts: { ...status2, running: true, devicesUrl: address },
|
|
17033
17231
|
detection,
|
|
17034
17232
|
reload: reloadLine(),
|
|
17035
17233
|
service: installed,
|
|
17036
17234
|
servicePlatform: servicePlatform(),
|
|
17037
17235
|
autoUpdate: cfg.update?.auto !== false,
|
|
17038
|
-
|
|
17236
|
+
address
|
|
17039
17237
|
}))
|
|
17040
17238
|
write(line);
|
|
17041
17239
|
if (runtime) {
|
|
@@ -17171,22 +17369,22 @@ async function setup(opts = {}, deps = {}) {
|
|
|
17171
17369
|
} catch (err) {
|
|
17172
17370
|
for (const line of pathLines(reloadLine())) write(line);
|
|
17173
17371
|
write(err instanceof Error ? err.message : String(err));
|
|
17174
|
-
write(
|
|
17175
|
-
"Check the server connection, then take a fresh setup line from the workspace's Settings \u2192 Devices."
|
|
17176
|
-
);
|
|
17372
|
+
write(`Check the server connection, then run ${cabane("setup")} again.`);
|
|
17177
17373
|
process.exitCode = 1;
|
|
17178
17374
|
} finally {
|
|
17179
17375
|
setConsoleLogging(true);
|
|
17180
17376
|
}
|
|
17181
17377
|
}
|
|
17182
17378
|
async function setupScript(opts, deps) {
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17379
|
+
if (opts.code) {
|
|
17380
|
+
write(
|
|
17381
|
+
`Setup codes come from this device now: run ${cabane("setup")} and enter the code it shows in Cabane.`
|
|
17382
|
+
);
|
|
17187
17383
|
process.exitCode = 1;
|
|
17188
17384
|
return;
|
|
17189
17385
|
}
|
|
17386
|
+
const interactive = deps.interactive ?? isInteractive();
|
|
17387
|
+
const paired = isDevicePaired();
|
|
17190
17388
|
const ui = deps.ui ?? (interactive ? terminalUi() : plainUi());
|
|
17191
17389
|
const running = await (deps.live ?? liveCompanionState)();
|
|
17192
17390
|
const cfg = safeLoadConfig();
|
|
@@ -17197,32 +17395,16 @@ async function setupScript(opts, deps) {
|
|
|
17197
17395
|
process.exitCode = 1;
|
|
17198
17396
|
return;
|
|
17199
17397
|
}
|
|
17200
|
-
let
|
|
17201
|
-
if (
|
|
17398
|
+
let justPaired = false;
|
|
17399
|
+
if (!paired) {
|
|
17202
17400
|
const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
write(
|
|
17206
|
-
"This device is paired to another Cabane server. Log out before setting it up on a different server."
|
|
17207
|
-
);
|
|
17208
|
-
write(`Run ${cabane("logout")} before using the new setup line.`);
|
|
17209
|
-
process.exitCode = 1;
|
|
17210
|
-
return;
|
|
17211
|
-
}
|
|
17212
|
-
device = await (deps.claim ?? claimSetupCode)(baseUrl, {
|
|
17213
|
-
userCode: opts.code,
|
|
17214
|
-
label: deviceLabelFromHostname(hostname2()) ?? "Computer",
|
|
17215
|
-
...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
|
|
17216
|
-
});
|
|
17401
|
+
const device = await (deps.pair ?? pairDevice)({ baseUrl, interactive, ui });
|
|
17402
|
+
if (!device) return;
|
|
17217
17403
|
writePairedConfig(device);
|
|
17218
|
-
|
|
17404
|
+
justPaired = true;
|
|
17219
17405
|
}
|
|
17220
17406
|
const detection = await start(
|
|
17221
|
-
{
|
|
17222
|
-
...opts,
|
|
17223
|
-
...device ? { pairing: device } : {},
|
|
17224
|
-
...!interactive && device ? { foreground: true } : {}
|
|
17225
|
-
},
|
|
17407
|
+
{ ...opts, ...!interactive && justPaired ? { foreground: true } : {} },
|
|
17226
17408
|
{ ...deps, interactive }
|
|
17227
17409
|
);
|
|
17228
17410
|
if (interactive && detection && !anyUsable(detection)) {
|
|
@@ -17443,7 +17625,7 @@ async function stop(deps = {}) {
|
|
|
17443
17625
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
17444
17626
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
17445
17627
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
17446
|
-
const
|
|
17628
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
17447
17629
|
const now = deps.now ?? (() => Date.now());
|
|
17448
17630
|
const state = readState();
|
|
17449
17631
|
if (!state) {
|
|
@@ -17471,7 +17653,7 @@ async function stop(deps = {}) {
|
|
|
17471
17653
|
process.stdout.write("companion: stopped.\n");
|
|
17472
17654
|
return;
|
|
17473
17655
|
}
|
|
17474
|
-
await
|
|
17656
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
17475
17657
|
}
|
|
17476
17658
|
process.stdout.write(
|
|
17477
17659
|
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
@@ -17497,7 +17679,7 @@ function isAlive(kill, pid) {
|
|
|
17497
17679
|
var STARTUP_WAIT_MS = 2e4;
|
|
17498
17680
|
async function installAndStartService(deps = {}) {
|
|
17499
17681
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
17500
|
-
const
|
|
17682
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
17501
17683
|
const now = deps.now ?? Date.now;
|
|
17502
17684
|
const before = await serviceState(deps);
|
|
17503
17685
|
const running = readState();
|
|
@@ -17513,7 +17695,7 @@ async function installAndStartService(deps = {}) {
|
|
|
17513
17695
|
while (now() < deadline) {
|
|
17514
17696
|
const state = readState();
|
|
17515
17697
|
if (state?.socket) return true;
|
|
17516
|
-
await
|
|
17698
|
+
await sleep4(250);
|
|
17517
17699
|
}
|
|
17518
17700
|
write(
|
|
17519
17701
|
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
@@ -17643,9 +17825,7 @@ async function status(deps = {}) {
|
|
|
17643
17825
|
cfg = null;
|
|
17644
17826
|
}
|
|
17645
17827
|
if (!cfg || !cfg.deviceToken) {
|
|
17646
|
-
out(
|
|
17647
|
-
" Not paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here."
|
|
17648
|
-
);
|
|
17828
|
+
out(` Not paired yet. Run ${cabane("setup")} on this device.`);
|
|
17649
17829
|
for (const line of strays) out(line);
|
|
17650
17830
|
process.exitCode = 1;
|
|
17651
17831
|
return;
|
|
@@ -18308,7 +18488,7 @@ init_cabane_command();
|
|
|
18308
18488
|
rememberInvokedCommand();
|
|
18309
18489
|
var program = new Command();
|
|
18310
18490
|
program.name("cabane").description("Run your Cabane agents on this device, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
|
|
18311
|
-
program.command("setup
|
|
18491
|
+
program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the Cabane instance to pair with (default https://app.cabane.ai)").addOption(
|
|
18312
18492
|
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
18313
18493
|
"info",
|
|
18314
18494
|
"warn",
|
package/dist/pairing-config.js
CHANGED
|
@@ -413,55 +413,112 @@ function saveConfig(cfg) {
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
// src/enrollment.ts
|
|
416
|
-
|
|
416
|
+
var EnrollmentExpiredError = class extends CompanionError {
|
|
417
|
+
constructor() {
|
|
418
|
+
super("this pairing code expired before it was confirmed.");
|
|
419
|
+
this.name = "EnrollmentExpiredError";
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var EnrollmentCancelledError = class extends CompanionError {
|
|
423
|
+
constructor() {
|
|
424
|
+
super("pairing was cancelled.");
|
|
425
|
+
this.name = "EnrollmentCancelledError";
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
function trimBase(baseUrl) {
|
|
429
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
430
|
+
}
|
|
431
|
+
async function postJson(baseUrl, path, body, signal) {
|
|
417
432
|
let res;
|
|
418
433
|
try {
|
|
419
|
-
res = await fetch(`${baseUrl
|
|
434
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
420
435
|
method: "POST",
|
|
421
|
-
headers: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
},
|
|
425
|
-
body: JSON.stringify({
|
|
426
|
-
userCode: input.userCode,
|
|
427
|
-
label: input.label,
|
|
428
|
-
platform: process.platform,
|
|
429
|
-
arch: process.arch,
|
|
430
|
-
nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
|
|
431
|
-
})
|
|
436
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
437
|
+
body: JSON.stringify(body),
|
|
438
|
+
...signal ? { signal } : {}
|
|
432
439
|
});
|
|
433
440
|
} catch {
|
|
441
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
434
442
|
throw new CompanionError(
|
|
435
443
|
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
436
444
|
);
|
|
437
445
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
throw new CompanionError(
|
|
444
|
-
"The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
445
|
-
);
|
|
446
|
-
if (res.status === 404 || res.status === 410)
|
|
446
|
+
let raw;
|
|
447
|
+
try {
|
|
448
|
+
raw = await res.text();
|
|
449
|
+
} catch {
|
|
450
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
447
451
|
throw new CompanionError(
|
|
448
|
-
|
|
452
|
+
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
449
453
|
);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
}
|
|
455
|
+
let parsed = void 0;
|
|
456
|
+
if (raw.length > 0) {
|
|
457
|
+
try {
|
|
458
|
+
parsed = JSON.parse(raw);
|
|
459
|
+
} catch {
|
|
460
|
+
parsed = raw;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (res.status >= 400) {
|
|
464
|
+
if (res.status === 429)
|
|
465
|
+
throw new CompanionError(
|
|
466
|
+
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
467
|
+
);
|
|
468
|
+
if (res.status === 404 && path.endsWith("/code"))
|
|
469
|
+
throw new CompanionError(
|
|
470
|
+
`No pairing endpoint at ${baseUrl}. Either that server is too old for this companion, or it isn't a Cabane API origin: the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
|
|
471
|
+
);
|
|
472
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
473
|
+
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
474
|
+
}
|
|
475
|
+
return parsed;
|
|
476
|
+
}
|
|
477
|
+
function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
478
|
+
return postJson(
|
|
479
|
+
baseUrl,
|
|
480
|
+
"/api/device-enrollment/code",
|
|
481
|
+
{ ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
|
|
482
|
+
opts.signal
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function hostFacts() {
|
|
486
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
|
|
456
487
|
return {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
|
|
461
|
-
..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
|
|
462
|
-
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
488
|
+
platform: process.platform,
|
|
489
|
+
arch: process.arch,
|
|
490
|
+
...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
|
|
463
491
|
};
|
|
464
492
|
}
|
|
493
|
+
function pollOnce(baseUrl, deviceCode, signal) {
|
|
494
|
+
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
|
|
495
|
+
}
|
|
496
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
497
|
+
async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
498
|
+
const now = opts.now ?? (() => Date.now());
|
|
499
|
+
const wait = opts.sleepMs ?? sleep;
|
|
500
|
+
const deadline = now() + code.expiresIn * 1e3;
|
|
501
|
+
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
502
|
+
while (now() < deadline) {
|
|
503
|
+
await wait(intervalMs);
|
|
504
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
505
|
+
const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
|
|
506
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
507
|
+
if (res.status === "complete") {
|
|
508
|
+
if (!res.deviceToken || !res.baseUrl)
|
|
509
|
+
throw new CompanionError("Cabane reported the pairing complete but returned no token.");
|
|
510
|
+
return {
|
|
511
|
+
baseUrl: res.baseUrl,
|
|
512
|
+
deviceToken: res.deviceToken,
|
|
513
|
+
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
514
|
+
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (res.status === "expired") throw new EnrollmentExpiredError();
|
|
518
|
+
if (res.status === "slow_down") intervalMs += 1e3;
|
|
519
|
+
}
|
|
520
|
+
throw new EnrollmentExpiredError();
|
|
521
|
+
}
|
|
465
522
|
|
|
466
523
|
// src/pairing-config.ts
|
|
467
524
|
var DEFAULT_BASE_URL = "https://app.cabane.ai";
|
|
@@ -496,8 +553,11 @@ function isDevicePaired() {
|
|
|
496
553
|
export {
|
|
497
554
|
ApiError,
|
|
498
555
|
CompanionError,
|
|
499
|
-
|
|
556
|
+
EnrollmentCancelledError,
|
|
557
|
+
EnrollmentExpiredError,
|
|
558
|
+
awaitEnrollment,
|
|
500
559
|
isDevicePaired,
|
|
560
|
+
requestEnrollmentCode,
|
|
501
561
|
resolvePairBaseUrl,
|
|
502
562
|
writePairedConfig
|
|
503
563
|
};
|
package/dist/runtime.js
CHANGED
|
@@ -10201,9 +10201,7 @@ var CompanionSupervisor = class {
|
|
|
10201
10201
|
this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
|
|
10202
10202
|
this.detectTimer.unref?.();
|
|
10203
10203
|
if (!this.deviceApi || !this.config.deviceToken) {
|
|
10204
|
-
this.log.warn(
|
|
10205
|
-
"This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
|
|
10206
|
-
);
|
|
10204
|
+
this.log.warn(`This device isn't paired. Run ${cabane("setup")} on it.`);
|
|
10207
10205
|
return;
|
|
10208
10206
|
}
|
|
10209
10207
|
this.deviceSub = new SseSubscriber({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.215",
|
|
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",
|