@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.
- package/README.md +73 -169
- package/dist/bin.js +14 -0
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +4153 -2184
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +169 -23
- package/dist/runtime.js +1666 -833
- package/dist/static/app.js +2 -3
- package/package.json +4 -3
package/dist/pairing-config.js
CHANGED
|
@@ -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 =
|
|
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 ===
|
|
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
|
|
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
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
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,
|