@cabane/companion 0.6.155 → 0.6.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,108 @@
1
+ // src/cabane-command.ts
2
+ import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
3
+ import { homedir as homedir3 } from "os";
4
+ import { delimiter, join as join2, sep as sep2 } from "path";
5
+
6
+ // src/bundled-binaries.ts
7
+ import { realpathSync, statSync } from "fs";
8
+ import { createRequire } from "module";
9
+ import { homedir } from "os";
10
+ import { dirname, join, sep } from "path";
11
+ import { fileURLToPath } from "url";
12
+ var CODEX_CLI = "@openai/codex";
13
+ var CODEX_PACKAGE_BY_TRIPLE = {
14
+ "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
15
+ "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
16
+ "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
17
+ "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
18
+ "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
19
+ "aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
20
+ };
21
+ function companionInstall(execPath = process.execPath, home = homedir()) {
22
+ const real = (path) => {
23
+ try {
24
+ return realpathSync(path);
25
+ } catch {
26
+ return path;
27
+ }
28
+ };
29
+ const scriptNode = join(real(home), ".cabane", "node") + sep;
30
+ return real(execPath).startsWith(scriptNode) ? "script" : "npm";
31
+ }
32
+
33
+ // src/cli-entry.ts
34
+ import { existsSync } from "fs";
35
+ import { fileURLToPath as fileURLToPath2 } from "url";
36
+ var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
37
+ function companionCliEntry(deps = {}) {
38
+ const exists = deps.exists ?? existsSync;
39
+ const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
40
+ for (const candidate of candidates) {
41
+ if (exists(candidate)) return candidate;
42
+ }
43
+ const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
44
+ if (argv1 && exists(argv1)) return argv1;
45
+ return candidates[0] ?? "";
46
+ }
47
+
48
+ // src/term.ts
49
+ import { createInterface } from "readline";
50
+ import { homedir as homedir2 } from "os";
51
+ import pc from "picocolors";
52
+ function tildePath(path, home = homedir2()) {
53
+ if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
54
+ return path;
55
+ }
56
+
57
+ // src/cabane-command.ts
58
+ function installedCommandPath(deps = {}) {
59
+ const install = deps.install ?? companionInstall();
60
+ const home = deps.home ?? homedir3();
61
+ if (install === "script") return join2(home, ".cabane", "bin", "cabane");
62
+ const entry = deps.cliEntry ?? companionCliEntry();
63
+ const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
64
+ const at = entry.indexOf(marker);
65
+ if (at < 0) return null;
66
+ return join2(entry.slice(0, at), "bin", "cabane");
67
+ }
68
+ function cabaneCommand(deps = {}) {
69
+ const own = installedCommandPath(deps);
70
+ if (!own) return "cabane";
71
+ const realpath = deps.realpath ?? safeRealpath;
72
+ const isExecutable = deps.isExecutable ?? executable;
73
+ const ownReal = realpath(own);
74
+ if (!ownReal) return "cabane";
75
+ const onPath = firstOnPath("cabane", "path" in deps ? deps.path : process.env.PATH, isExecutable);
76
+ if (onPath && realpath(onPath) === ownReal) return "cabane";
77
+ return tildePath(own, deps.home);
78
+ }
79
+ function cabane(rest, deps) {
80
+ return `${cabaneCommand(deps)} ${rest}`;
81
+ }
82
+ function firstOnPath(name, path, isExecutable = executable) {
83
+ for (const dir of (path ?? "").split(delimiter)) {
84
+ if (!dir) continue;
85
+ const candidate = join2(dir, name);
86
+ if (isExecutable(candidate)) return candidate;
87
+ }
88
+ return null;
89
+ }
90
+ function executable(path) {
91
+ try {
92
+ accessSync(path, constants.X_OK);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ function safeRealpath(path) {
99
+ try {
100
+ return realpathSync2(path);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
1
106
  // src/config.ts
2
107
  import {
3
108
  chmodSync,
@@ -6,10 +111,10 @@ import {
6
111
  renameSync,
7
112
  rmSync,
8
113
  writeFileSync,
9
- existsSync
114
+ existsSync as existsSync2
10
115
  } from "fs";
11
- import { homedir, userInfo } from "os";
12
- import { dirname, join } from "path";
116
+ import { homedir as homedir4, userInfo } from "os";
117
+ import { dirname as dirname2, join as join3 } from "path";
13
118
  import { z as z2 } from "zod";
14
119
 
15
120
  // src/errors.ts
@@ -102,10 +207,10 @@ function realAccountHome() {
102
207
  return realHomeCache;
103
208
  }
104
209
  function cabaneDir() {
105
- const dir = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
210
+ const dir = join3(process.env.CABANE_COMPANION_HOME || homedir4(), ".cabane");
106
211
  if (process.env.VITEST) {
107
212
  const real = realAccountHome();
108
- if (real && dir === join(real, ".cabane")) {
213
+ if (real && dir === join3(real, ".cabane")) {
109
214
  throw new Error(
110
215
  `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).`
111
216
  );
@@ -114,7 +219,7 @@ function cabaneDir() {
114
219
  return dir;
115
220
  }
116
221
  function configPath() {
117
- return join(cabaneDir(), "config.json");
222
+ return join3(cabaneDir(), "config.json");
118
223
  }
119
224
  var localAgentConfigSchema = z2.object({
120
225
  cwd: z2.string().optional(),
@@ -188,15 +293,33 @@ var companionConfigSchema = z2.object({
188
293
  // the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
189
294
  // SDK never runs).
190
295
  //
191
- // The block's PRESENCE is also the migration marker
192
- // (`migrateConnectedHarnesses`): absent means a config whose user was never
193
- // asked. So every config written carries it, a fresh pair at `enabled: false`.
296
+ // Absent means one of two things, told apart by `harnessesChosen`: a legacy
297
+ // config whose user was never asked (`migrateConnectedHarnesses`), or a
298
+ // device that has never been offered Claude Code. `enabled: false` is a
299
+ // decline, and `start` doesn't offer it again.
194
300
  //
195
301
  // Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
196
302
  // switch.
197
303
  claudeCode: z2.object({
198
304
  enabled: z2.boolean().optional()
199
305
  }).strict().optional(),
306
+ // Written by every pairing that asks which agents to connect. With it, an
307
+ // absent `claudeCode` block means "never offered"; without it, the block's
308
+ // absence marks a config from before connectors were chosen.
309
+ harnessesChosen: z2.boolean().optional(),
310
+ // Whether this companion keeps itself current: installs the version Cabane names, between
311
+ // turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
312
+ // which `start` offers; off either way until someone says yes.
313
+ update: z2.object({
314
+ auto: z2.boolean()
315
+ }).strict().optional(),
316
+ // The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
317
+ // `cabane service install` still installs it.
318
+ serviceDeclined: z2.boolean().optional(),
319
+ // OpenCode's decline. Its block is an address, so a decline can't live there
320
+ // the way `claudeCode`/`codex` `enabled: false` does; without it a declined
321
+ // serve would be offered again on every `start`.
322
+ opencodeDeclined: z2.boolean().optional(),
200
323
  // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
201
324
  // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
202
325
  // never sees the key). The block's presence enables it; `enabled: false` turns
@@ -250,7 +373,7 @@ var companionConfigSchema = z2.object({
250
373
  });
251
374
  function loadConfig() {
252
375
  const path = configPath();
253
- if (!existsSync(path)) return null;
376
+ if (!existsSync2(path)) return null;
254
377
  let raw;
255
378
  try {
256
379
  raw = readFileSync(path, "utf8");
@@ -277,7 +400,7 @@ function loadConfig() {
277
400
  );
278
401
  }
279
402
  throw new ConfigError(
280
- `${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.`
403
+ `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("pair")}\` to re-pair, or \`${cabane("logout --purge")}\` to reset.`
281
404
  );
282
405
  }
283
406
  return result.data;
@@ -286,7 +409,7 @@ function loadConfigTolerant() {
286
409
  const path = configPath();
287
410
  const empty = {};
288
411
  const fresh = { local: empty, note: null, hadPriorConfig: false };
289
- if (!existsSync(path)) return fresh;
412
+ if (!existsSync2(path)) return fresh;
290
413
  let raw;
291
414
  try {
292
415
  raw = readFileSync(path, "utf8");
@@ -306,7 +429,19 @@ function loadConfigTolerant() {
306
429
  }
307
430
  const strict = companionConfigSchema.safeParse(parsed);
308
431
  if (strict.success) {
309
- const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, logFormat, claudeCode: claudeCode2 } = strict.data;
432
+ const {
433
+ agents: agents2,
434
+ prepareHook: prepareHook2,
435
+ dashboardPort,
436
+ autoOpen,
437
+ logLevel,
438
+ logFormat,
439
+ claudeCode: claudeCode2,
440
+ codex: codex2,
441
+ opencode: opencode2,
442
+ opencodeDeclined,
443
+ harnessesChosen
444
+ } = strict.data;
310
445
  return {
311
446
  local: {
312
447
  ...agents2 !== void 0 ? { agents: agents2 } : {},
@@ -315,7 +450,11 @@ function loadConfigTolerant() {
315
450
  ...autoOpen !== void 0 ? { autoOpen } : {},
316
451
  ...logLevel !== void 0 ? { logLevel } : {},
317
452
  ...logFormat !== void 0 ? { logFormat } : {},
318
- ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
453
+ ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {},
454
+ ...codex2 !== void 0 ? { codex: codex2 } : {},
455
+ ...opencode2 !== void 0 ? { opencode: opencode2 } : {},
456
+ ...opencodeDeclined !== void 0 ? { opencodeDeclined } : {},
457
+ ...harnessesChosen !== void 0 ? { harnessesChosen } : {}
319
458
  },
320
459
  note: null,
321
460
  hadPriorConfig: true
@@ -335,6 +474,12 @@ function loadConfigTolerant() {
335
474
  }
336
475
  const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
337
476
  if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
477
+ const codex = companionConfigSchema.shape.codex.safeParse(obj.codex);
478
+ if (codex.success && codex.data !== void 0) local.codex = codex.data;
479
+ const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
480
+ if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
481
+ if (obj.opencodeDeclined === true) local.opencodeDeclined = true;
482
+ if (obj.harnessesChosen === true) local.harnessesChosen = true;
338
483
  return {
339
484
  local,
340
485
  note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
@@ -343,7 +488,7 @@ function loadConfigTolerant() {
343
488
  }
344
489
  function saveConfig(cfg) {
345
490
  const path = configPath();
346
- mkdirSync(dirname(path), { recursive: true });
491
+ mkdirSync(dirname2(path), { recursive: true });
347
492
  try {
348
493
  chmodSync(cabaneDir(), 448);
349
494
  } catch {
@@ -406,7 +551,7 @@ async function postJson(baseUrl, path, body) {
406
551
  if (res.status >= 400) {
407
552
  if (res.status === 404 && path.endsWith("/code")) {
408
553
  throw new CompanionError(
409
- `no device-flow pairing endpoint at ${baseUrl}. Either that server predates \`cabane-companion pair\`, or it isn't a Cabane API origin \u2014 the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
554
+ `no device-flow pairing endpoint at ${baseUrl}. Either that server predates \`${cabane("pair")}\`, or it isn't a Cabane API origin \u2014 the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
410
555
  );
411
556
  }
412
557
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
@@ -481,7 +626,7 @@ function resolvePairBaseUrl(server) {
481
626
  const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
482
627
  if (!isAllowedBaseUrl(raw)) {
483
628
  throw new CompanionError(
484
- `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\`.`
629
+ `invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("start --server https://your-cabane")}\`.`
485
630
  );
486
631
  }
487
632
  return raw;
@@ -489,12 +634,13 @@ function resolvePairBaseUrl(server) {
489
634
  function writePairedConfig(paired) {
490
635
  const { local, note, hadPriorConfig } = loadConfigTolerant();
491
636
  const config = {
492
- // A device pairs connected to NOTHING: `claudeCode: { enabled: false }`
493
- // written explicitly, since absence is what the harness migration reads.
494
- // Omitted, a new device is auto-connected on first start; stamped on a
495
- // RE-pair, an upgrading user's working device is disconnected. So only when
496
- // there was no config before; `local` spreads after, keeping a prior answer.
497
- ...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
637
+ // A device pairs connected to NOTHING, and its agents are chosen from here
638
+ // on: `harnessesChosen` tells the harness migration that an absent
639
+ // `claudeCode` block means "never offered", not a legacy Claude device.
640
+ // Only when there was no config before: stamped on a RE-pair, an upgrading
641
+ // user's working device would be disconnected. `local` spreads after,
642
+ // keeping a prior answer.
643
+ ...hadPriorConfig ? {} : { harnessesChosen: true },
498
644
  ...local,
499
645
  baseUrl: paired.baseUrl,
500
646
  deviceToken: paired.deviceToken,