@cabane/companion 0.5.0 → 0.6.0
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 +2 -2
- package/dist/cli.js +1667 -570
- package/dist/pairing-config.js +38 -33
- package/dist/runtime.js +1547 -477
- package/dist/static/app.js +22 -19
- package/dist/static/index.html +6 -6
- package/dist/static/styles.css +1 -1
- package/package.json +2 -3
package/dist/pairing-config.js
CHANGED
|
@@ -13,13 +13,13 @@ import { dirname, join } from "path";
|
|
|
13
13
|
import { z as z3 } from "zod";
|
|
14
14
|
|
|
15
15
|
// src/errors.ts
|
|
16
|
-
var
|
|
16
|
+
var CompanionError = class extends Error {
|
|
17
17
|
constructor(message) {
|
|
18
18
|
super(message);
|
|
19
|
-
this.name = "
|
|
19
|
+
this.name = "CompanionError";
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
var ApiError = class extends
|
|
22
|
+
var ApiError = class extends CompanionError {
|
|
23
23
|
constructor(status, message, body) {
|
|
24
24
|
super(message);
|
|
25
25
|
this.status = status;
|
|
@@ -29,7 +29,7 @@ var ApiError = class extends BridgeError {
|
|
|
29
29
|
status;
|
|
30
30
|
body;
|
|
31
31
|
};
|
|
32
|
-
var ConfigError = class extends
|
|
32
|
+
var ConfigError = class extends CompanionError {
|
|
33
33
|
constructor(message) {
|
|
34
34
|
super(message);
|
|
35
35
|
this.name = "ConfigError";
|
|
@@ -60,12 +60,12 @@ var pairingSchema = z.object({
|
|
|
60
60
|
baseUrl: z.string().url().refine(isAllowedBaseUrl, {
|
|
61
61
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
62
62
|
}),
|
|
63
|
-
// The `cabdev_` device token plaintext — the
|
|
63
|
+
// The `cabdev_` device token plaintext — the companion's one durable credential.
|
|
64
64
|
deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
|
|
65
65
|
message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
|
|
66
66
|
}),
|
|
67
67
|
// Optional identity hints the app may include for nicer local display. The
|
|
68
|
-
//
|
|
68
|
+
// companion also learns these from the first assignments pull, so they're not
|
|
69
69
|
// required.
|
|
70
70
|
deviceId: z.string().min(1).optional(),
|
|
71
71
|
deviceLabel: z.string().min(1).optional()
|
|
@@ -81,13 +81,18 @@ var prepareHookSchema = z2.object({
|
|
|
81
81
|
env: z2.record(z2.string(), z2.string()).optional(),
|
|
82
82
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
83
83
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
84
|
-
// fails with a clear timeout message rather than pinning the
|
|
84
|
+
// fails with a clear timeout message rather than pinning the companion.
|
|
85
85
|
timeoutMs: z2.number().int().positive().optional()
|
|
86
86
|
}).strict();
|
|
87
87
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
88
88
|
var prepareResultSchema = z2.object({
|
|
89
89
|
cwd: z2.string().min(1),
|
|
90
|
-
env: z2.record(z2.string(), z2.string()).optional()
|
|
90
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
91
|
+
nativeWorkAssignment: z2.object({
|
|
92
|
+
itemId: z2.string(),
|
|
93
|
+
executionId: z2.string(),
|
|
94
|
+
activationEpoch: z2.number().int().nonnegative()
|
|
95
|
+
}).strict().optional()
|
|
91
96
|
});
|
|
92
97
|
|
|
93
98
|
// src/config.ts
|
|
@@ -103,12 +108,12 @@ function realAccountHome() {
|
|
|
103
108
|
return realHomeCache;
|
|
104
109
|
}
|
|
105
110
|
function cabaneDir() {
|
|
106
|
-
const dir = join(process.env.
|
|
111
|
+
const dir = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
|
|
107
112
|
if (process.env.VITEST) {
|
|
108
113
|
const real = realAccountHome();
|
|
109
114
|
if (real && dir === join(real, ".cabane")) {
|
|
110
115
|
throw new Error(
|
|
111
|
-
`cabaneDir() resolved to the real ${dir} during a test run.
|
|
116
|
+
`cabaneDir() resolved to the real ${dir} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
|
|
112
117
|
);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
@@ -121,22 +126,22 @@ var localAgentConfigSchema = z3.object({
|
|
|
121
126
|
cwd: z3.string().optional(),
|
|
122
127
|
prepareHook: prepareHookSchema.optional(),
|
|
123
128
|
// CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
|
|
124
|
-
// by default on every
|
|
125
|
-
//
|
|
126
|
-
// On a
|
|
129
|
+
// by default on every companion (memory belongs in the Cabane workspace, and a
|
|
130
|
+
// shared companion would otherwise pool one cwd-keyed memory dir across users).
|
|
131
|
+
// On a companion you run yourself, set `claudeCode: { autoMemory: true }` to hand
|
|
127
132
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
128
133
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
129
134
|
// (in coding mode, where the checkout's project settings are read).
|
|
130
135
|
claudeCode: z3.object({ autoMemory: z3.boolean().optional() }).strict().optional()
|
|
131
136
|
}).strict();
|
|
132
|
-
var
|
|
137
|
+
var companionConfigSchema = z3.object({
|
|
133
138
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
134
139
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
135
140
|
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
136
141
|
baseUrl: z3.string().url().refine(isAllowedBaseUrl, {
|
|
137
142
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
138
143
|
}),
|
|
139
|
-
// The `cabdev_` device token plaintext — the
|
|
144
|
+
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
140
145
|
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
|
141
146
|
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
142
147
|
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
@@ -147,19 +152,19 @@ var bridgeConfigSchema = z3.object({
|
|
|
147
152
|
deviceId: z3.string().optional(),
|
|
148
153
|
deviceLabel: z3.string().optional(),
|
|
149
154
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
150
|
-
// agentId / username / `slug/username`. Hand-added by the operator; the
|
|
155
|
+
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
151
156
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
152
157
|
agents: z3.record(z3.string(), localAgentConfigSchema).optional(),
|
|
153
158
|
// Dashboard settings (all optional). dashboardPort: preferred bind port (next
|
|
154
159
|
// free one if taken); autoOpen: whether `start` opens the browser (the
|
|
155
|
-
// `--no-open` flag / `
|
|
160
|
+
// `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
|
|
156
161
|
// level, live-editable from the dashboard settings panel.
|
|
157
162
|
dashboardPort: z3.number().int().min(1).max(65535).optional(),
|
|
158
163
|
autoOpen: z3.boolean().optional(),
|
|
159
164
|
logLevel: z3.enum(["warn", "info", "debug"]).optional(),
|
|
160
165
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
161
166
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
162
|
-
// `/connect` — Cabane never sees provider keys), and points the
|
|
167
|
+
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
163
168
|
// here. Setting this makes the device advertise the `opencode` runtime on its
|
|
164
169
|
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
165
170
|
// routes those turns to this device) AND registers the opencode adapter in the
|
|
@@ -200,7 +205,7 @@ function loadConfig() {
|
|
|
200
205
|
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
201
206
|
);
|
|
202
207
|
}
|
|
203
|
-
const result =
|
|
208
|
+
const result = companionConfigSchema.safeParse(parsed);
|
|
204
209
|
if (!result.success) {
|
|
205
210
|
const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
|
|
206
211
|
if (agentIssue) {
|
|
@@ -209,7 +214,7 @@ function loadConfig() {
|
|
|
209
214
|
);
|
|
210
215
|
}
|
|
211
216
|
throw new ConfigError(
|
|
212
|
-
`${path} is from an incompatible or older version of the
|
|
217
|
+
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
|
|
213
218
|
);
|
|
214
219
|
}
|
|
215
220
|
return result.data;
|
|
@@ -231,7 +236,7 @@ function loadConfigTolerant() {
|
|
|
231
236
|
} catch {
|
|
232
237
|
return { local: empty, note: `${path} was unreadable (invalid JSON) and has been reset.` };
|
|
233
238
|
}
|
|
234
|
-
const strict =
|
|
239
|
+
const strict = companionConfigSchema.safeParse(parsed);
|
|
235
240
|
if (strict.success) {
|
|
236
241
|
const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
|
|
237
242
|
return {
|
|
@@ -255,7 +260,7 @@ function loadConfigTolerant() {
|
|
|
255
260
|
}
|
|
256
261
|
return {
|
|
257
262
|
local,
|
|
258
|
-
note: `the existing ${path} was from an older or incompatible
|
|
263
|
+
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`
|
|
259
264
|
};
|
|
260
265
|
}
|
|
261
266
|
function saveConfig(cfg) {
|
|
@@ -295,7 +300,7 @@ async function postJson(baseUrl, path, body) {
|
|
|
295
300
|
body: JSON.stringify(body)
|
|
296
301
|
});
|
|
297
302
|
} catch (err) {
|
|
298
|
-
throw new
|
|
303
|
+
throw new CompanionError(
|
|
299
304
|
`couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
|
|
300
305
|
);
|
|
301
306
|
}
|
|
@@ -310,7 +315,7 @@ async function postJson(baseUrl, path, body) {
|
|
|
310
315
|
}
|
|
311
316
|
if (res.status >= 400) {
|
|
312
317
|
if (res.status === 404 && path.endsWith("/code")) {
|
|
313
|
-
throw new
|
|
318
|
+
throw new CompanionError(
|
|
314
319
|
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server, or use \`cabane-companion pair --legacy\` with a pairing string from the app.`
|
|
315
320
|
);
|
|
316
321
|
}
|
|
@@ -320,16 +325,16 @@ async function postJson(baseUrl, path, body) {
|
|
|
320
325
|
return parsed;
|
|
321
326
|
}
|
|
322
327
|
function requestEnrollmentCode(baseUrl) {
|
|
323
|
-
return postJson(baseUrl, "/api/
|
|
328
|
+
return postJson(baseUrl, "/api/device-enrollment/code", {});
|
|
324
329
|
}
|
|
325
330
|
function pollOnce(baseUrl, deviceCode) {
|
|
326
|
-
return postJson(baseUrl, "/api/
|
|
331
|
+
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode });
|
|
327
332
|
}
|
|
328
333
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
334
|
async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
330
335
|
const now = opts.now ?? (() => Date.now());
|
|
331
336
|
const wait = opts.sleepMs ?? sleep;
|
|
332
|
-
if (opts.signal?.aborted) throw new
|
|
337
|
+
if (opts.signal?.aborted) throw new CompanionError("pairing was cancelled.");
|
|
333
338
|
const code = await requestEnrollmentCode(baseUrl);
|
|
334
339
|
if (opts.onCode) await opts.onCode(code);
|
|
335
340
|
print("");
|
|
@@ -344,11 +349,11 @@ async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
|
344
349
|
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
345
350
|
while (now() < deadline) {
|
|
346
351
|
await wait(intervalMs);
|
|
347
|
-
if (opts.signal?.aborted) throw new
|
|
352
|
+
if (opts.signal?.aborted) throw new CompanionError("pairing was cancelled.");
|
|
348
353
|
const res = await pollOnce(baseUrl, code.deviceCode);
|
|
349
354
|
if (res.status === "complete") {
|
|
350
355
|
if (!res.deviceToken || !res.baseUrl) {
|
|
351
|
-
throw new
|
|
356
|
+
throw new CompanionError("the server reported the pairing complete but returned no token.");
|
|
352
357
|
}
|
|
353
358
|
return {
|
|
354
359
|
baseUrl: res.baseUrl,
|
|
@@ -358,13 +363,13 @@ async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
|
358
363
|
};
|
|
359
364
|
}
|
|
360
365
|
if (res.status === "expired") {
|
|
361
|
-
throw new
|
|
366
|
+
throw new CompanionError(
|
|
362
367
|
"this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
|
|
363
368
|
);
|
|
364
369
|
}
|
|
365
370
|
if (res.status === "slow_down") intervalMs += 1e3;
|
|
366
371
|
}
|
|
367
|
-
throw new
|
|
372
|
+
throw new CompanionError(
|
|
368
373
|
"this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
|
|
369
374
|
);
|
|
370
375
|
}
|
|
@@ -374,7 +379,7 @@ var DEFAULT_BASE_URL = "https://cabane.ai";
|
|
|
374
379
|
function resolvePairBaseUrl(server) {
|
|
375
380
|
const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
|
|
376
381
|
if (!isAllowedBaseUrl(raw)) {
|
|
377
|
-
throw new
|
|
382
|
+
throw new CompanionError(
|
|
378
383
|
`invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`cabane-companion pair --server https://your-cabane\`.`
|
|
379
384
|
);
|
|
380
385
|
}
|
|
@@ -401,7 +406,7 @@ function isDevicePaired() {
|
|
|
401
406
|
}
|
|
402
407
|
export {
|
|
403
408
|
ApiError,
|
|
404
|
-
|
|
409
|
+
CompanionError,
|
|
405
410
|
isDevicePaired,
|
|
406
411
|
requestEnrollmentCode,
|
|
407
412
|
resolvePairBaseUrl,
|