@cabane/companion 0.5.0 → 0.6.1

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.
@@ -10,16 +10,16 @@ import {
10
10
  } from "fs";
11
11
  import { homedir, userInfo } from "os";
12
12
  import { dirname, join } from "path";
13
- import { z as z3 } from "zod";
13
+ import { z as z2 } from "zod";
14
14
 
15
15
  // src/errors.ts
16
- var BridgeError = class extends Error {
16
+ var CompanionError = class extends Error {
17
17
  constructor(message) {
18
18
  super(message);
19
- this.name = "BridgeError";
19
+ this.name = "CompanionError";
20
20
  }
21
21
  };
22
- var ApiError = class extends BridgeError {
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 BridgeError {
32
+ var ConfigError = class extends CompanionError {
33
33
  constructor(message) {
34
34
  super(message);
35
35
  this.name = "ConfigError";
@@ -37,9 +37,6 @@ var ConfigError = class extends BridgeError {
37
37
  };
38
38
 
39
39
  // src/pairing.ts
40
- import { z } from "zod";
41
- var PAIRING_VERSION = 1;
42
- var DEVICE_TOKEN_PREFIX = "cabdev_";
43
40
  function isAllowedBaseUrl(raw) {
44
41
  let url;
45
42
  try {
@@ -54,40 +51,24 @@ function isAllowedBaseUrl(raw) {
54
51
  }
55
52
  return false;
56
53
  }
57
- var pairingSchema = z.object({
58
- // Bumped if the wire shape changes incompatibly. We only accept v1.
59
- v: z.literal(PAIRING_VERSION),
60
- baseUrl: z.string().url().refine(isAllowedBaseUrl, {
61
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
62
- }),
63
- // The `cabdev_` device token plaintext — the bridge's one durable credential.
64
- deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
65
- message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
66
- }),
67
- // Optional identity hints the app may include for nicer local display. The
68
- // bridge also learns these from the first assignments pull, so they're not
69
- // required.
70
- deviceId: z.string().min(1).optional(),
71
- deviceLabel: z.string().min(1).optional()
72
- });
73
54
 
74
55
  // src/prepare-hook.ts
75
56
  import { spawn } from "child_process";
76
- import { z as z2 } from "zod";
77
- var prepareHookSchema = z2.object({
78
- command: z2.string().min(1),
79
- args: z2.array(z2.string()).optional(),
57
+ import { z } from "zod";
58
+ var prepareHookSchema = z.object({
59
+ command: z.string().min(1),
60
+ args: z.array(z.string()).optional(),
80
61
  // Extra env handed to the hook process itself (merged over process.env).
81
- env: z2.record(z2.string(), z2.string()).optional(),
62
+ env: z.record(z.string(), z.string()).optional(),
82
63
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
83
64
  // 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 bridge.
85
- timeoutMs: z2.number().int().positive().optional()
65
+ // fails with a clear timeout message rather than pinning the companion.
66
+ timeoutMs: z.number().int().positive().optional()
86
67
  }).strict();
87
68
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
88
- var prepareResultSchema = z2.object({
89
- cwd: z2.string().min(1),
90
- env: z2.record(z2.string(), z2.string()).optional()
69
+ var prepareResultSchema = z.object({
70
+ cwd: z.string().min(1),
71
+ env: z.record(z.string(), z.string()).optional()
91
72
  });
92
73
 
93
74
  // src/config.ts
@@ -103,12 +84,12 @@ function realAccountHome() {
103
84
  return realHomeCache;
104
85
  }
105
86
  function cabaneDir() {
106
- const dir = join(process.env.CABANE_BRIDGE_HOME || homedir(), ".cabane");
87
+ const dir = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
107
88
  if (process.env.VITEST) {
108
89
  const real = realAccountHome();
109
90
  if (real && dir === join(real, ".cabane")) {
110
91
  throw new Error(
111
- `cabaneDir() resolved to the real ${dir} during a test run. Bridge tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real bridge config (see apps/bridge/test/setup-home.ts).`
92
+ `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
93
  );
113
94
  }
114
95
  }
@@ -117,55 +98,55 @@ function cabaneDir() {
117
98
  function configPath() {
118
99
  return join(cabaneDir(), "config.json");
119
100
  }
120
- var localAgentConfigSchema = z3.object({
121
- cwd: z3.string().optional(),
101
+ var localAgentConfigSchema = z2.object({
102
+ cwd: z2.string().optional(),
122
103
  prepareHook: prepareHookSchema.optional(),
123
104
  // CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
124
- // by default on every bridge (memory belongs in the Cabane workspace, and the
125
- // house bridge would otherwise share one cwd-keyed memory dir across users).
126
- // On a bridge you run yourself, set `claudeCode: { autoMemory: true }` to hand
105
+ // by default on every companion (memory belongs in the Cabane workspace, and a
106
+ // shared companion would otherwise pool one cwd-keyed memory dir across users).
107
+ // On a companion you run yourself, set `claudeCode: { autoMemory: true }` to hand
127
108
  // auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
128
109
  // injecting the off switch and your normal Claude Code memory workflow applies
129
110
  // (in coding mode, where the checkout's project settings are read).
130
- claudeCode: z3.object({ autoMemory: z3.boolean().optional() }).strict().optional()
111
+ claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
131
112
  }).strict();
132
- var bridgeConfigSchema = z3.object({
113
+ var companionConfigSchema = z2.object({
133
114
  // The cabane instance this device is paired with. SJ515: https-enforced
134
115
  // (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
135
116
  // base URL onto the MITM-able channel the device token + prompt ride.
136
- baseUrl: z3.string().url().refine(isAllowedBaseUrl, {
117
+ baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
137
118
  message: "baseUrl must be https (loopback http is allowed for local dev only)"
138
119
  }),
139
- // The `cabdev_` device token plaintext — the bridge's one durable credential,
120
+ // The `cabdev_` device token plaintext — the companion's one durable credential,
140
121
  // presented as `Authorization: Bearer <token>` on the device-facing pull +
141
122
  // heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
142
123
  // "paired but logged out" state the supervisor refuses to run) while keeping
143
124
  // the rest of the config; `pair` always writes one.
144
- deviceToken: z3.string().optional(),
145
- // Identity hints, learned from the pairing string and refreshed on the first
125
+ deviceToken: z2.string().optional(),
126
+ // Identity hints, learned from the device flow and refreshed on the first
146
127
  // assignments pull. Cosmetic — used for `status`/dashboard display only.
147
- deviceId: z3.string().optional(),
148
- deviceLabel: z3.string().optional(),
128
+ deviceId: z2.string().optional(),
129
+ deviceLabel: z2.string().optional(),
149
130
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
150
- // agentId / username / `slug/username`. Hand-added by the operator; the bridge
131
+ // agentId / username / `slug/username`. Hand-added by the operator; the companion
151
132
  // never writes this (it only persists credentials + the device, elsewhere).
152
- agents: z3.record(z3.string(), localAgentConfigSchema).optional(),
133
+ agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
153
134
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
154
135
  // free one if taken); autoOpen: whether `start` opens the browser (the
155
- // `--no-open` flag / `BRIDGE_NO_OPEN=1` override per-run); logLevel: pino
136
+ // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
156
137
  // level, live-editable from the dashboard settings panel.
157
- dashboardPort: z3.number().int().min(1).max(65535).optional(),
158
- autoOpen: z3.boolean().optional(),
159
- logLevel: z3.enum(["warn", "info", "debug"]).optional(),
138
+ dashboardPort: z2.number().int().min(1).max(65535).optional(),
139
+ autoOpen: z2.boolean().optional(),
140
+ logLevel: z2.enum(["warn", "info", "debug"]).optional(),
160
141
  // CT270: the opencode runtime, when the operator runs one on this machine. The
161
142
  // operator installs opencode, starts `opencode serve` (auth via opencode's own
162
- // `/connect` — Cabane never sees provider keys), and points the bridge at it
143
+ // `/connect` — Cabane never sees provider keys), and points the companion at it
163
144
  // here. Setting this makes the device advertise the `opencode` runtime on its
164
145
  // heartbeat manifest (so the server offers DeepSeek/opencode models here and
165
146
  // routes those turns to this device) AND registers the opencode adapter in the
166
147
  // dispatcher. Absent → the device is claude-code-only, exactly as before.
167
- opencode: z3.object({
168
- serverUrl: z3.string().url()
148
+ opencode: z2.object({
149
+ serverUrl: z2.string().url()
169
150
  }).strict().optional(),
170
151
  // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
171
152
  // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
@@ -176,8 +157,8 @@ var bridgeConfigSchema = z3.object({
176
157
  // keeps the block but turns it off). Enabling makes the device advertise the
177
158
  // `codex` runtime on its heartbeat manifest AND registers the codex adapter in
178
159
  // the dispatcher. Absent → the device doesn't offer codex, exactly as before.
179
- codex: z3.object({
180
- enabled: z3.boolean().optional()
160
+ codex: z2.object({
161
+ enabled: z2.boolean().optional()
181
162
  }).strict().optional()
182
163
  });
183
164
  function loadConfig() {
@@ -200,7 +181,7 @@ function loadConfig() {
200
181
  `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
201
182
  );
202
183
  }
203
- const result = bridgeConfigSchema.safeParse(parsed);
184
+ const result = companionConfigSchema.safeParse(parsed);
204
185
  if (!result.success) {
205
186
  const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
206
187
  if (agentIssue) {
@@ -209,7 +190,7 @@ function loadConfig() {
209
190
  );
210
191
  }
211
192
  throw new ConfigError(
212
- `${path} is from an incompatible or older version of the bridge, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
193
+ `${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
194
  );
214
195
  }
215
196
  return result.data;
@@ -231,7 +212,7 @@ function loadConfigTolerant() {
231
212
  } catch {
232
213
  return { local: empty, note: `${path} was unreadable (invalid JSON) and has been reset.` };
233
214
  }
234
- const strict = bridgeConfigSchema.safeParse(parsed);
215
+ const strict = companionConfigSchema.safeParse(parsed);
235
216
  if (strict.success) {
236
217
  const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
237
218
  return {
@@ -246,7 +227,7 @@ function loadConfigTolerant() {
246
227
  }
247
228
  const obj = parsed && typeof parsed === "object" ? parsed : {};
248
229
  const local = {};
249
- const agents = z3.record(z3.string(), localAgentConfigSchema).safeParse(obj.agents);
230
+ const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
250
231
  if (agents.success) local.agents = agents.data;
251
232
  if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
252
233
  if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
@@ -255,7 +236,7 @@ function loadConfigTolerant() {
255
236
  }
256
237
  return {
257
238
  local,
258
- note: `the existing ${path} was from an older or incompatible bridge; re-pairing rewrote it.`
239
+ note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`
259
240
  };
260
241
  }
261
242
  function saveConfig(cfg) {
@@ -295,7 +276,7 @@ async function postJson(baseUrl, path, body) {
295
276
  body: JSON.stringify(body)
296
277
  });
297
278
  } catch (err) {
298
- throw new BridgeError(
279
+ throw new CompanionError(
299
280
  `couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
300
281
  );
301
282
  }
@@ -310,8 +291,8 @@ async function postJson(baseUrl, path, body) {
310
291
  }
311
292
  if (res.status >= 400) {
312
293
  if (res.status === 404 && path.endsWith("/code")) {
313
- throw new BridgeError(
314
- `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.`
294
+ throw new CompanionError(
295
+ `this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server to a version that supports \`cabane-companion pair\`.`
315
296
  );
316
297
  }
317
298
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
@@ -320,16 +301,16 @@ async function postJson(baseUrl, path, body) {
320
301
  return parsed;
321
302
  }
322
303
  function requestEnrollmentCode(baseUrl) {
323
- return postJson(baseUrl, "/api/bridge-enrollment/code", {});
304
+ return postJson(baseUrl, "/api/device-enrollment/code", {});
324
305
  }
325
306
  function pollOnce(baseUrl, deviceCode) {
326
- return postJson(baseUrl, "/api/bridge-enrollment/poll", { deviceCode });
307
+ return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode });
327
308
  }
328
309
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
329
310
  async function runDeviceFlow(baseUrl, print, opts = {}) {
330
311
  const now = opts.now ?? (() => Date.now());
331
312
  const wait = opts.sleepMs ?? sleep;
332
- if (opts.signal?.aborted) throw new BridgeError("pairing was cancelled.");
313
+ if (opts.signal?.aborted) throw new CompanionError("pairing was cancelled.");
333
314
  const code = await requestEnrollmentCode(baseUrl);
334
315
  if (opts.onCode) await opts.onCode(code);
335
316
  print("");
@@ -344,11 +325,11 @@ async function runDeviceFlow(baseUrl, print, opts = {}) {
344
325
  let intervalMs = Math.max(1, code.interval) * 1e3;
345
326
  while (now() < deadline) {
346
327
  await wait(intervalMs);
347
- if (opts.signal?.aborted) throw new BridgeError("pairing was cancelled.");
328
+ if (opts.signal?.aborted) throw new CompanionError("pairing was cancelled.");
348
329
  const res = await pollOnce(baseUrl, code.deviceCode);
349
330
  if (res.status === "complete") {
350
331
  if (!res.deviceToken || !res.baseUrl) {
351
- throw new BridgeError("the server reported the pairing complete but returned no token.");
332
+ throw new CompanionError("the server reported the pairing complete but returned no token.");
352
333
  }
353
334
  return {
354
335
  baseUrl: res.baseUrl,
@@ -358,13 +339,13 @@ async function runDeviceFlow(baseUrl, print, opts = {}) {
358
339
  };
359
340
  }
360
341
  if (res.status === "expired") {
361
- throw new BridgeError(
342
+ throw new CompanionError(
362
343
  "this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
363
344
  );
364
345
  }
365
346
  if (res.status === "slow_down") intervalMs += 1e3;
366
347
  }
367
- throw new BridgeError(
348
+ throw new CompanionError(
368
349
  "this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
369
350
  );
370
351
  }
@@ -374,7 +355,7 @@ var DEFAULT_BASE_URL = "https://cabane.ai";
374
355
  function resolvePairBaseUrl(server) {
375
356
  const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
376
357
  if (!isAllowedBaseUrl(raw)) {
377
- throw new BridgeError(
358
+ throw new CompanionError(
378
359
  `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
360
  );
380
361
  }
@@ -401,7 +382,7 @@ function isDevicePaired() {
401
382
  }
402
383
  export {
403
384
  ApiError,
404
- BridgeError,
385
+ CompanionError,
405
386
  isDevicePaired,
406
387
  requestEnrollmentCode,
407
388
  resolvePairBaseUrl,