@cabane/companion 0.6.154 → 0.6.156

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,24 @@ 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
+ // OpenCode's decline. Its block is an address, so a decline can't live there
311
+ // the way `claudeCode`/`codex` `enabled: false` does; without it a declined
312
+ // serve would be offered again on every `start`.
313
+ opencodeDeclined: z2.boolean().optional(),
200
314
  // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
201
315
  // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
202
316
  // never sees the key). The block's presence enables it; `enabled: false` turns
@@ -250,7 +364,7 @@ var companionConfigSchema = z2.object({
250
364
  });
251
365
  function loadConfig() {
252
366
  const path = configPath();
253
- if (!existsSync(path)) return null;
367
+ if (!existsSync2(path)) return null;
254
368
  let raw;
255
369
  try {
256
370
  raw = readFileSync(path, "utf8");
@@ -277,7 +391,7 @@ function loadConfig() {
277
391
  );
278
392
  }
279
393
  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.`
394
+ `${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
395
  );
282
396
  }
283
397
  return result.data;
@@ -286,7 +400,7 @@ function loadConfigTolerant() {
286
400
  const path = configPath();
287
401
  const empty = {};
288
402
  const fresh = { local: empty, note: null, hadPriorConfig: false };
289
- if (!existsSync(path)) return fresh;
403
+ if (!existsSync2(path)) return fresh;
290
404
  let raw;
291
405
  try {
292
406
  raw = readFileSync(path, "utf8");
@@ -306,7 +420,19 @@ function loadConfigTolerant() {
306
420
  }
307
421
  const strict = companionConfigSchema.safeParse(parsed);
308
422
  if (strict.success) {
309
- const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, logFormat, claudeCode: claudeCode2 } = strict.data;
423
+ const {
424
+ agents: agents2,
425
+ prepareHook: prepareHook2,
426
+ dashboardPort,
427
+ autoOpen,
428
+ logLevel,
429
+ logFormat,
430
+ claudeCode: claudeCode2,
431
+ codex: codex2,
432
+ opencode: opencode2,
433
+ opencodeDeclined,
434
+ harnessesChosen
435
+ } = strict.data;
310
436
  return {
311
437
  local: {
312
438
  ...agents2 !== void 0 ? { agents: agents2 } : {},
@@ -315,7 +441,11 @@ function loadConfigTolerant() {
315
441
  ...autoOpen !== void 0 ? { autoOpen } : {},
316
442
  ...logLevel !== void 0 ? { logLevel } : {},
317
443
  ...logFormat !== void 0 ? { logFormat } : {},
318
- ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
444
+ ...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {},
445
+ ...codex2 !== void 0 ? { codex: codex2 } : {},
446
+ ...opencode2 !== void 0 ? { opencode: opencode2 } : {},
447
+ ...opencodeDeclined !== void 0 ? { opencodeDeclined } : {},
448
+ ...harnessesChosen !== void 0 ? { harnessesChosen } : {}
319
449
  },
320
450
  note: null,
321
451
  hadPriorConfig: true
@@ -335,6 +465,12 @@ function loadConfigTolerant() {
335
465
  }
336
466
  const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
337
467
  if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
468
+ const codex = companionConfigSchema.shape.codex.safeParse(obj.codex);
469
+ if (codex.success && codex.data !== void 0) local.codex = codex.data;
470
+ const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
471
+ if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
472
+ if (obj.opencodeDeclined === true) local.opencodeDeclined = true;
473
+ if (obj.harnessesChosen === true) local.harnessesChosen = true;
338
474
  return {
339
475
  local,
340
476
  note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
@@ -343,7 +479,7 @@ function loadConfigTolerant() {
343
479
  }
344
480
  function saveConfig(cfg) {
345
481
  const path = configPath();
346
- mkdirSync(dirname(path), { recursive: true });
482
+ mkdirSync(dirname2(path), { recursive: true });
347
483
  try {
348
484
  chmodSync(cabaneDir(), 448);
349
485
  } catch {
@@ -406,7 +542,7 @@ async function postJson(baseUrl, path, body) {
406
542
  if (res.status >= 400) {
407
543
  if (res.status === 404 && path.endsWith("/code")) {
408
544
  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).`
545
+ `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
546
  );
411
547
  }
412
548
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
@@ -481,7 +617,7 @@ function resolvePairBaseUrl(server) {
481
617
  const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
482
618
  if (!isAllowedBaseUrl(raw)) {
483
619
  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\`.`
620
+ `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
621
  );
486
622
  }
487
623
  return raw;
@@ -489,12 +625,13 @@ function resolvePairBaseUrl(server) {
489
625
  function writePairedConfig(paired) {
490
626
  const { local, note, hadPriorConfig } = loadConfigTolerant();
491
627
  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 } },
628
+ // A device pairs connected to NOTHING, and its agents are chosen from here
629
+ // on: `harnessesChosen` tells the harness migration that an absent
630
+ // `claudeCode` block means "never offered", not a legacy Claude device.
631
+ // Only when there was no config before: stamped on a RE-pair, an upgrading
632
+ // user's working device would be disconnected. `local` spreads after,
633
+ // keeping a prior answer.
634
+ ...hadPriorConfig ? {} : { harnessesChosen: true },
498
635
  ...local,
499
636
  baseUrl: paired.baseUrl,
500
637
  deviceToken: paired.deviceToken,
@@ -1,7 +1,8 @@
1
1
  // src/bundled-binaries.ts
2
2
  import { realpathSync, statSync } from "fs";
3
3
  import { createRequire } from "module";
4
- import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+ import { dirname, join, sep } from "path";
5
6
  import { fileURLToPath } from "url";
6
7
  function nodeResolveSelf(specifier) {
7
8
  const here = fileURLToPath(import.meta.url);
@@ -178,11 +179,23 @@ var HARNESS_LABEL = {
178
179
  function missingNoun(status) {
179
180
  return status === "executable_missing" ? "executable" : "platform package";
180
181
  }
181
- function repairCommands() {
182
- return ["npm i -g @cabane/companion --include=optional"];
182
+ var INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
183
+ function companionInstall(execPath = process.execPath, home = homedir()) {
184
+ const real = (path) => {
185
+ try {
186
+ return realpathSync(path);
187
+ } catch {
188
+ return path;
189
+ }
190
+ };
191
+ const scriptNode = join(real(home), ".cabane", "node") + sep;
192
+ return real(execPath).startsWith(scriptNode) ? "script" : "npm";
193
+ }
194
+ function repairCommands(install = companionInstall()) {
195
+ return install === "script" ? [INSTALL_SCRIPT_COMMAND] : ["npm i -g @cabane/companion --include=optional"];
183
196
  }
184
- function repairAction() {
185
- return `Reinstall with \`${repairCommands()[0]}\`.`;
197
+ function repairAction(install = companionInstall()) {
198
+ return `Reinstall with \`${repairCommands(install)[0]}\`.`;
186
199
  }
187
200
  function installWarning(r) {
188
201
  return `warning: @cabane/companion is incomplete: its bundled ${HARNESS_LABEL[r.runtime]} ${missingNoun(r.status)} for ${r.platform}-${r.arch} is missing. ${repairAction()}`;