@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.
@@ -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 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";
@@ -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 bridge's one durable credential.
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
- // bridge also learns these from the first assignments pull, so they're not
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 bridge.
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.CABANE_BRIDGE_HOME || homedir(), ".cabane");
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. 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).`
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 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
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 bridgeConfigSchema = z3.object({
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 bridge's one durable credential,
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 bridge
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 / `BRIDGE_NO_OPEN=1` override per-run); logLevel: pino
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 bridge at it
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 = bridgeConfigSchema.safeParse(parsed);
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 bridge, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
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 = bridgeConfigSchema.safeParse(parsed);
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 bridge; re-pairing rewrote it.`
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 BridgeError(
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 BridgeError(
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/bridge-enrollment/code", {});
328
+ return postJson(baseUrl, "/api/device-enrollment/code", {});
324
329
  }
325
330
  function pollOnce(baseUrl, deviceCode) {
326
- return postJson(baseUrl, "/api/bridge-enrollment/poll", { deviceCode });
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 BridgeError("pairing was cancelled.");
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 BridgeError("pairing was cancelled.");
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 BridgeError("the server reported the pairing complete but returned no token.");
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 BridgeError(
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 BridgeError(
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 BridgeError(
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
- BridgeError,
409
+ CompanionError,
405
410
  isDevicePaired,
406
411
  requestEnrollmentCode,
407
412
  resolvePairBaseUrl,