@cabane/companion 0.6.235 → 0.6.237
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 +8 -8
- package/dist/bin.js +5 -1
- package/dist/cli.js +127 -62
- package/dist/pairing-config.js +53 -28
- package/dist/postinstall.js +339 -16
- package/dist/runtime.js +49 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,16 +10,16 @@ Pairing supplies a device token, not your Cabane password or a full-account toke
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
On the device, in a terminal. One of:
|
|
13
|
+
On the device, in a terminal. Copy the line from Your settings → Devices → Add a device in your Cabane: it carries your Cabane's address, shown here as `https://cabane.example.com`. One of:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
curl -fsSL https://cabane.
|
|
16
|
+
curl -fsSL https://cabane.example.com/install.sh | bash
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Your Cabane serves the script, with its own address written in. It brings its own Node (checksum-checked, into `~/.cabane`) and needs no sudo. Or, with Node 22 or newer already there (the only route on Windows):
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
npm i -g @cabane/companion && cabane setup
|
|
22
|
+
npm i -g @cabane/companion && cabane setup --server https://cabane.example.com
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
`cabane setup` on an unpaired device prints a code and a link:
|
|
@@ -29,14 +29,14 @@ Pair this device
|
|
|
29
29
|
Enter this code in Cabane, or press Return to open the page:
|
|
30
30
|
|
|
31
31
|
WXYZ-2345
|
|
32
|
-
https://
|
|
32
|
+
https://cabane.example.com/pair/enter-code
|
|
33
33
|
|
|
34
34
|
⠋ Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Enter the code in Cabane, signed in as yourself: from
|
|
37
|
+
Enter the code in Cabane, signed in as yourself: from Your settings → Devices → Add a device, from a workspace's Settings → Devices → Set up a new device, or on the page the link opens. Cabane shows which device asked; confirming pairs it to your account and nothing else. A workspace's dialog then adds the device to that workspace; from anywhere else, add it to a workspace from that workspace's Settings → Devices. Pairing from a workspace you're setting up places its default agent on the device once a harness there is usable.
|
|
38
38
|
|
|
39
|
-
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the
|
|
39
|
+
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the Cabane the device pairs with. There is no default: setup pairs with the Cabane named by `--server <url>`, by `CABANE_BASE_URL`, or by the device's config, and stops if none names one. Once a Cabane has issued a code, setup keeps its address, so after a cancelled or expired pairing a bare `cabane setup` pairs with the same one.
|
|
40
40
|
|
|
41
41
|
From an interactive terminal, setup then starts the Companion in the background, waits for its first check-in and reports the device's state. Without an interactive terminal, setup stays attached to the running Companion.
|
|
42
42
|
|
|
@@ -54,7 +54,7 @@ If no harness is usable, interactive setup asks which one you want to set up. Fo
|
|
|
54
54
|
Status cabane status Stop cabane stop Log cabane logs
|
|
55
55
|
Start at login and keep Cabane updating itself: cabane service install
|
|
56
56
|
|
|
57
|
-
In Cabane: https://
|
|
57
|
+
In Cabane: https://cabane.example.com/settings/devices
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Setup never installs the service. On macOS or Linux, run `cabane service install` when you want the Companion to start at login and keep itself updated; the closing block names it. Native Windows uses the background process and has no service installer.
|
package/dist/bin.js
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
var MIN_NODE_MAJOR = 22;
|
|
5
5
|
var major = Number(process.versions.node.split(".")[0]);
|
|
6
6
|
if (!(major >= MIN_NODE_MAJOR)) {
|
|
7
|
+
const found = `Cabane needs Node ${MIN_NODE_MAJOR} or newer, and this is Node ${process.versions.node}.`;
|
|
7
8
|
process.stderr.write(
|
|
8
|
-
|
|
9
|
+
process.platform === "win32" ? `${found} Install Node 22 or newer.
|
|
10
|
+
` : `${found}
|
|
11
|
+
Install Node 22 or newer. Or, to install Cabane with its own Node, run the
|
|
12
|
+
installer line from Your settings \u2192 Devices \u2192 Add a device in your Cabane.
|
|
9
13
|
`
|
|
10
14
|
);
|
|
11
15
|
process.exit(1);
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,13 @@ import { realpathSync } from "fs";
|
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { dirname, join, sep } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
|
+
function installScriptCommand(address) {
|
|
18
|
+
return `curl -fsSL ${address.replace(/\/+$/, "")}/install.sh | bash`;
|
|
19
|
+
}
|
|
20
|
+
function installLineWords(install = companionInstall()) {
|
|
21
|
+
const line = install === "script" ? "installer line" : "npm line";
|
|
22
|
+
return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
|
|
23
|
+
}
|
|
17
24
|
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
18
25
|
const real = (path) => {
|
|
19
26
|
try {
|
|
@@ -36,11 +43,9 @@ function repairRoute(packageRoot = ownRoot(), install = companionInstall()) {
|
|
|
36
43
|
function ownRoot() {
|
|
37
44
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
38
45
|
}
|
|
39
|
-
var INSTALL_SCRIPT_COMMAND;
|
|
40
46
|
var init_install_kind = __esm({
|
|
41
47
|
"src/install-kind.ts"() {
|
|
42
48
|
"use strict";
|
|
43
|
-
INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
|
|
@@ -222,7 +227,7 @@ var init_cabane_command = __esm({
|
|
|
222
227
|
});
|
|
223
228
|
|
|
224
229
|
// src/errors.ts
|
|
225
|
-
var CompanionError, ApiError, EdgeBlockError, ConfigError, PrepareHookError;
|
|
230
|
+
var CompanionError, StandaloneError, ApiError, EdgeBlockError, ConfigError, PrepareHookError;
|
|
226
231
|
var init_errors = __esm({
|
|
227
232
|
"src/errors.ts"() {
|
|
228
233
|
"use strict";
|
|
@@ -232,6 +237,12 @@ var init_errors = __esm({
|
|
|
232
237
|
this.name = "CompanionError";
|
|
233
238
|
}
|
|
234
239
|
};
|
|
240
|
+
StandaloneError = class extends CompanionError {
|
|
241
|
+
constructor(message2) {
|
|
242
|
+
super(message2);
|
|
243
|
+
this.name = "StandaloneError";
|
|
244
|
+
}
|
|
245
|
+
};
|
|
235
246
|
ApiError = class extends CompanionError {
|
|
236
247
|
constructor(status2, message2, body) {
|
|
237
248
|
super(message2);
|
|
@@ -603,31 +614,13 @@ function loadConfigTolerant() {
|
|
|
603
614
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
604
615
|
if (strict.success) {
|
|
605
616
|
const {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
logLevel,
|
|
612
|
-
logFormat,
|
|
613
|
-
harnesses: harnesses2,
|
|
614
|
-
opencode: opencode2
|
|
617
|
+
baseUrl: _baseUrl,
|
|
618
|
+
deviceToken: _deviceToken,
|
|
619
|
+
deviceId: _deviceId,
|
|
620
|
+
deviceLabel: _deviceLabel,
|
|
621
|
+
...local2
|
|
615
622
|
} = strict.data;
|
|
616
|
-
return {
|
|
617
|
-
local: {
|
|
618
|
-
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
619
|
-
...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
|
|
620
|
-
...root2 !== void 0 ? { root: root2 } : {},
|
|
621
|
-
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
622
|
-
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
623
|
-
...logLevel !== void 0 ? { logLevel } : {},
|
|
624
|
-
...logFormat !== void 0 ? { logFormat } : {},
|
|
625
|
-
...harnesses2 !== void 0 ? { harnesses: harnesses2 } : {},
|
|
626
|
-
...opencode2 !== void 0 ? { opencode: opencode2 } : {}
|
|
627
|
-
},
|
|
628
|
-
note: null,
|
|
629
|
-
hadPriorConfig: true
|
|
630
|
-
};
|
|
623
|
+
return { local: local2, note: null, hadPriorConfig: true };
|
|
631
624
|
}
|
|
632
625
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
633
626
|
const local = {};
|
|
@@ -647,6 +640,11 @@ function loadConfigTolerant() {
|
|
|
647
640
|
if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
|
|
648
641
|
const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
|
|
649
642
|
if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
|
|
643
|
+
const update2 = companionConfigSchema.shape.update.safeParse(obj.update);
|
|
644
|
+
if (update2.success && update2.data !== void 0) local.update = update2.data;
|
|
645
|
+
if (typeof obj.serviceDeclined === "boolean") local.serviceDeclined = obj.serviceDeclined;
|
|
646
|
+
const runner = companionConfigSchema.shape.runner.safeParse(obj.runner);
|
|
647
|
+
if (runner.success && runner.data !== void 0) local.runner = runner.data;
|
|
650
648
|
return {
|
|
651
649
|
local,
|
|
652
650
|
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
@@ -958,18 +956,32 @@ function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
|
|
|
958
956
|
function missingNoun(status2) {
|
|
959
957
|
return status2 === "executable_missing" ? "executable" : "platform package";
|
|
960
958
|
}
|
|
961
|
-
function repairCommands(route = repairRoute(), spell = cabane) {
|
|
959
|
+
function repairCommands(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
962
960
|
if (route === "managed") return [spell("stop"), spell("update")];
|
|
963
|
-
if (route === "script")
|
|
961
|
+
if (route === "script")
|
|
962
|
+
return [
|
|
963
|
+
spell("stop"),
|
|
964
|
+
address ? installScriptCommand(address) : { words: installLineWords("script") }
|
|
965
|
+
];
|
|
964
966
|
return ["npm i -g @cabane/companion --include=optional"];
|
|
965
967
|
}
|
|
966
|
-
function
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
968
|
+
function configuredAddress() {
|
|
969
|
+
try {
|
|
970
|
+
return loadConfig()?.baseUrl ?? null;
|
|
971
|
+
} catch {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function quoteStep(step, quote) {
|
|
976
|
+
return typeof step === "string" ? quote(step) : step.words;
|
|
977
|
+
}
|
|
978
|
+
function repairAction(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
979
|
+
const [first, ...rest] = repairCommands(route, spell, address);
|
|
980
|
+
if (rest.length === 0) return `Reinstall with ${quoteStep(first, backticks)}.`;
|
|
981
|
+
return `Run ${quoteStep(first, backticks)}, then ${rest.map((step) => quoteStep(step, backticks)).join(", then ")}. ${STOP_WARNING}`;
|
|
970
982
|
}
|
|
971
983
|
function repairSteps(route = repairRoute()) {
|
|
972
|
-
return repairCommands(route).join(", then ");
|
|
984
|
+
return repairCommands(route).map((step) => quoteStep(step, (command) => command)).join(", then ");
|
|
973
985
|
}
|
|
974
986
|
function repairCost(route = repairRoute()) {
|
|
975
987
|
return repairCommands(route).length > 1 ? STOP_WARNING : "";
|
|
@@ -981,11 +993,12 @@ function startupWarning(r) {
|
|
|
981
993
|
function turnIncompleteCopy(runtime) {
|
|
982
994
|
return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
|
|
983
995
|
}
|
|
984
|
-
var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, STOP_WARNING;
|
|
996
|
+
var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, backticks, STOP_WARNING;
|
|
985
997
|
var init_bundled_binaries = __esm({
|
|
986
998
|
"src/bundled-binaries.ts"() {
|
|
987
999
|
"use strict";
|
|
988
1000
|
init_cabane_command();
|
|
1001
|
+
init_config();
|
|
989
1002
|
init_install_kind();
|
|
990
1003
|
CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
|
|
991
1004
|
CODEX_SDK = "@openai/codex-sdk";
|
|
@@ -1006,6 +1019,7 @@ var init_bundled_binaries = __esm({
|
|
|
1006
1019
|
"claude-code": "connected",
|
|
1007
1020
|
codex: "enabled"
|
|
1008
1021
|
};
|
|
1022
|
+
backticks = (command) => `\`${command}\``;
|
|
1009
1023
|
STOP_WARNING = "Stopping ends any turn in progress.";
|
|
1010
1024
|
}
|
|
1011
1025
|
});
|
|
@@ -3205,9 +3219,11 @@ function envResolvedFrom(server, secretEnv) {
|
|
|
3205
3219
|
if (!server.env && Object.keys(resolved).length === 0) return void 0;
|
|
3206
3220
|
return { ...server.env, ...resolved };
|
|
3207
3221
|
}
|
|
3222
|
+
var TURN_TOKEN_ENV;
|
|
3208
3223
|
var init_mcp_secret_env = __esm({
|
|
3209
3224
|
"packages/agent-runtime/src/mcp-secret-env.ts"() {
|
|
3210
3225
|
"use strict";
|
|
3226
|
+
TURN_TOKEN_ENV = "CABANE_TURN_TOKEN";
|
|
3211
3227
|
}
|
|
3212
3228
|
});
|
|
3213
3229
|
|
|
@@ -3357,7 +3373,7 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3357
3373
|
type: "http",
|
|
3358
3374
|
url: req.cabane.mcpUrl,
|
|
3359
3375
|
headers: {
|
|
3360
|
-
Authorization:
|
|
3376
|
+
Authorization: `\${${TURN_TOKEN_ENV}}`,
|
|
3361
3377
|
[ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
|
|
3362
3378
|
},
|
|
3363
3379
|
// Claude Code 2.1.x defers every MCP tool behind tool-search by default;
|
|
@@ -3416,10 +3432,15 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3416
3432
|
//
|
|
3417
3433
|
// `secretEnv` — the values the MCP headers above reference — joins here and only here: it is
|
|
3418
3434
|
// this child's environment, not the host's.
|
|
3435
|
+
//
|
|
3436
|
+
// The turn token goes after everything a room secret, a hook or the inherited environment can
|
|
3437
|
+
// supply: this spread order is the only thing that stops a variable of the same name from
|
|
3438
|
+
// displacing the turn's own credential.
|
|
3419
3439
|
env: {
|
|
3420
3440
|
...process.env,
|
|
3421
3441
|
...req.local.env,
|
|
3422
3442
|
...req.local.secretEnv,
|
|
3443
|
+
[TURN_TOKEN_ENV]: `Bearer ${req.cabane.bearer}`,
|
|
3423
3444
|
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
|
|
3424
3445
|
},
|
|
3425
3446
|
...resume ? { resume } : {}
|
|
@@ -5249,12 +5270,16 @@ function codexSecretEnv(req) {
|
|
|
5249
5270
|
const env = { ...req.local.secretEnv };
|
|
5250
5271
|
for (const server of Object.values(req.local.mcpServers ?? {})) {
|
|
5251
5272
|
if ("url" in server) continue;
|
|
5252
|
-
for (const [variable, envName] of
|
|
5273
|
+
for (const [variable, envName] of forwardedEnvVars(server.envFromEnv)) {
|
|
5253
5274
|
const value = req.local.secretEnv?.[envName];
|
|
5254
5275
|
if (value !== void 0) env[variable] = value;
|
|
5255
5276
|
}
|
|
5256
5277
|
}
|
|
5257
|
-
|
|
5278
|
+
env[TURN_TOKEN_ENV] = `Bearer ${req.cabane.bearer}`;
|
|
5279
|
+
return env;
|
|
5280
|
+
}
|
|
5281
|
+
function forwardedEnvVars(envFromEnv) {
|
|
5282
|
+
return Object.entries(envFromEnv ?? {}).filter(([variable]) => variable !== TURN_TOKEN_ENV);
|
|
5258
5283
|
}
|
|
5259
5284
|
function buildInput(req, segments, preamble, resumed) {
|
|
5260
5285
|
const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
|
|
@@ -5296,7 +5321,7 @@ function buildConfig(req) {
|
|
|
5296
5321
|
default_tools_approval_mode: "approve"
|
|
5297
5322
|
};
|
|
5298
5323
|
} else if ("command" in server) {
|
|
5299
|
-
const forwarded =
|
|
5324
|
+
const forwarded = forwardedEnvVars(server.envFromEnv).map(([variable]) => variable).sort();
|
|
5300
5325
|
mcp_servers[name] = {
|
|
5301
5326
|
command: server.command,
|
|
5302
5327
|
...server.args ? { args: server.args } : {},
|
|
@@ -5324,19 +5349,15 @@ function buildConfig(req) {
|
|
|
5324
5349
|
}
|
|
5325
5350
|
mcp_servers[CABANE_MCP_SERVER3] = {
|
|
5326
5351
|
url: req.cabane.mcpUrl,
|
|
5327
|
-
http_headers: {
|
|
5328
|
-
|
|
5329
|
-
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5330
|
-
},
|
|
5352
|
+
http_headers: { [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId },
|
|
5353
|
+
env_http_headers: { Authorization: TURN_TOKEN_ENV },
|
|
5331
5354
|
default_tools_approval_mode: "approve"
|
|
5332
5355
|
};
|
|
5333
5356
|
if (req.cabane.turnControlUrl) {
|
|
5334
5357
|
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
5335
5358
|
url: req.cabane.turnControlUrl,
|
|
5336
|
-
http_headers: {
|
|
5337
|
-
|
|
5338
|
-
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5339
|
-
},
|
|
5359
|
+
http_headers: { [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId },
|
|
5360
|
+
env_http_headers: { Authorization: TURN_TOKEN_ENV },
|
|
5340
5361
|
default_tools_approval_mode: "approve"
|
|
5341
5362
|
};
|
|
5342
5363
|
}
|
|
@@ -5361,6 +5382,7 @@ var CABANE_MCP_SERVER3, ACTIVE_CONVERSATION_HEADER3, ENV_ENVELOPE_KEYS, CORE_REP
|
|
|
5361
5382
|
var init_run_spec2 = __esm({
|
|
5362
5383
|
"packages/agent-runtime/src/codex/run-spec.ts"() {
|
|
5363
5384
|
"use strict";
|
|
5385
|
+
init_mcp_secret_env();
|
|
5364
5386
|
init_descriptions();
|
|
5365
5387
|
init_standing();
|
|
5366
5388
|
init_policy3();
|
|
@@ -5476,7 +5498,7 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
5476
5498
|
// path (the CLI splits on `.` and ignores TOML quoting), so it must ride inside an inline-table
|
|
5477
5499
|
// value, which the SDK's flattener can't produce. See `config-overrides.ts`.
|
|
5478
5500
|
configOverrides: buildConfigOverrides(spec.config),
|
|
5479
|
-
|
|
5501
|
+
env: codexProcessEnv(spec.secretEnv)
|
|
5480
5502
|
};
|
|
5481
5503
|
const codex = new Codex(codexOptions);
|
|
5482
5504
|
const threadOptions = buildSdkThreadOptions(spec);
|
|
@@ -9951,6 +9973,7 @@ import { hostname } from "os";
|
|
|
9951
9973
|
|
|
9952
9974
|
// src/enrollment.ts
|
|
9953
9975
|
init_errors();
|
|
9976
|
+
init_install_kind();
|
|
9954
9977
|
var EnrollmentExpiredError = class extends CompanionError {
|
|
9955
9978
|
constructor() {
|
|
9956
9979
|
super("this pairing code expired before it was confirmed.");
|
|
@@ -10004,8 +10027,9 @@ async function postJson(baseUrl, path, body, signal) {
|
|
|
10004
10027
|
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
10005
10028
|
);
|
|
10006
10029
|
if (res.status === 404 && path.endsWith("/code"))
|
|
10007
|
-
throw new
|
|
10008
|
-
`No pairing endpoint at ${baseUrl}
|
|
10030
|
+
throw new StandaloneError(
|
|
10031
|
+
`No pairing endpoint at ${baseUrl}: it isn't a Cabane's address, or that Cabane is too old for this companion.
|
|
10032
|
+
Run ${installLineWords()}.`
|
|
10009
10033
|
);
|
|
10010
10034
|
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
10011
10035
|
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
@@ -10190,6 +10214,7 @@ async function pairDevice(opts, deps = {}) {
|
|
|
10190
10214
|
process.exitCode = 130;
|
|
10191
10215
|
return null;
|
|
10192
10216
|
}
|
|
10217
|
+
if (!current) opts.onCode?.();
|
|
10193
10218
|
current = code;
|
|
10194
10219
|
const minutes = Math.max(1, Math.round(code.expiresIn / 60));
|
|
10195
10220
|
const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUri}`, ""];
|
|
@@ -10296,10 +10321,11 @@ init_cabane_command();
|
|
|
10296
10321
|
init_config();
|
|
10297
10322
|
init_pairing();
|
|
10298
10323
|
init_errors();
|
|
10324
|
+
init_install_kind();
|
|
10299
10325
|
init_errors();
|
|
10300
|
-
var DEFAULT_BASE_URL = "https://app.cabane.ai";
|
|
10301
10326
|
function resolvePairBaseUrl(server) {
|
|
10302
|
-
const raw = (server ?? process.env.CABANE_BASE_URL ??
|
|
10327
|
+
const raw = (server ?? process.env.CABANE_BASE_URL ?? "").trim();
|
|
10328
|
+
if (!raw) throw new NoPairAddressError();
|
|
10303
10329
|
if (!isAllowedBaseUrl(raw)) {
|
|
10304
10330
|
throw new CompanionError(
|
|
10305
10331
|
`invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
|
|
@@ -10307,6 +10333,20 @@ function resolvePairBaseUrl(server) {
|
|
|
10307
10333
|
}
|
|
10308
10334
|
return raw;
|
|
10309
10335
|
}
|
|
10336
|
+
var NoPairAddressError = class extends StandaloneError {
|
|
10337
|
+
constructor(install = companionInstall()) {
|
|
10338
|
+
const words = installLineWords(install);
|
|
10339
|
+
super(
|
|
10340
|
+
`Setup doesn't know which Cabane to pair this device with. Run the
|
|
10341
|
+
${words.slice("the ".length)}.`
|
|
10342
|
+
);
|
|
10343
|
+
this.name = "NoPairAddressError";
|
|
10344
|
+
}
|
|
10345
|
+
};
|
|
10346
|
+
function rememberPairAddress(baseUrl) {
|
|
10347
|
+
const { local } = loadConfigTolerant();
|
|
10348
|
+
saveConfig({ ...local, baseUrl });
|
|
10349
|
+
}
|
|
10310
10350
|
function writePairedConfig(paired) {
|
|
10311
10351
|
const { local, note: note2 } = loadConfigTolerant();
|
|
10312
10352
|
const config = {
|
|
@@ -10644,7 +10684,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
10644
10684
|
const stopsFirst = commands.length > 1;
|
|
10645
10685
|
if (!stopsFirst) commands.push("cabane stop");
|
|
10646
10686
|
commands.push("cabane start");
|
|
10647
|
-
const list = commands.map((command) => `\`${command}\``);
|
|
10687
|
+
const list = commands.map((step) => quoteStep(step, (command) => `\`${command}\``));
|
|
10648
10688
|
return `${harnessLabel(runtime ?? reason.slice("runtime_incomplete:".length))}'s installation on this device is incomplete. Run ${list.slice(0, -1).join(", ")} and ${list.at(-1)}. ${stopsFirst ? `${STOP_WARNING} ` : ""}Reply in Cabane to try again.`;
|
|
10649
10689
|
}
|
|
10650
10690
|
const unavailable = /^(codex|opencode)_unavailable:/.exec(reason ?? "")?.[1] ?? (reason?.startsWith("runtime_unavailable:") ? reason.slice("runtime_unavailable:".length).trim() : void 0);
|
|
@@ -14814,6 +14854,7 @@ function userName() {
|
|
|
14814
14854
|
return process.env.USER ?? "";
|
|
14815
14855
|
}
|
|
14816
14856
|
}
|
|
14857
|
+
var SCRIPT_WRAPPER_COMMENT = /^# Written by \S+\/install\.sh\. /m;
|
|
14817
14858
|
function rewriteScriptWrappers(dir2 = join27(cabaneDir(), "bin")) {
|
|
14818
14859
|
for (const name of ["cabane", "cabane-companion"]) {
|
|
14819
14860
|
const path = join27(dir2, name);
|
|
@@ -14823,7 +14864,7 @@ function rewriteScriptWrappers(dir2 = join27(cabaneDir(), "bin")) {
|
|
|
14823
14864
|
} catch {
|
|
14824
14865
|
continue;
|
|
14825
14866
|
}
|
|
14826
|
-
if (!
|
|
14867
|
+
if (!SCRIPT_WRAPPER_COMMENT.test(text)) continue;
|
|
14827
14868
|
if (!text.includes("/dist/cli.js")) continue;
|
|
14828
14869
|
writeAtomic(path, text.replace("/dist/cli.js", "/dist/bin.js"));
|
|
14829
14870
|
chmodSync5(path, 493);
|
|
@@ -15160,6 +15201,9 @@ async function runAttached(runtime) {
|
|
|
15160
15201
|
});
|
|
15161
15202
|
}
|
|
15162
15203
|
|
|
15204
|
+
// src/commands/setup.ts
|
|
15205
|
+
init_errors();
|
|
15206
|
+
|
|
15163
15207
|
// src/prompts.ts
|
|
15164
15208
|
init_term();
|
|
15165
15209
|
import * as clack from "@clack/prompts";
|
|
@@ -15180,7 +15224,8 @@ function terminalUi() {
|
|
|
15180
15224
|
},
|
|
15181
15225
|
spinner: () => railSpinner(),
|
|
15182
15226
|
outro: (lines2) => clack.outro(lines2.join("\n ")),
|
|
15183
|
-
|
|
15227
|
+
// Clack draws only the first line on the rail's end; the rest align under its text.
|
|
15228
|
+
cancel: (message2) => clack.cancel(message2.split("\n").join("\n "))
|
|
15184
15229
|
};
|
|
15185
15230
|
}
|
|
15186
15231
|
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
@@ -15236,7 +15281,10 @@ function plainUi() {
|
|
|
15236
15281
|
outro: (lines2) => {
|
|
15237
15282
|
for (const line of lines2) write(`${INDENT}${line}`);
|
|
15238
15283
|
},
|
|
15239
|
-
|
|
15284
|
+
// A closing message can run to more than one line; each is indented like the first.
|
|
15285
|
+
cancel: (message2) => {
|
|
15286
|
+
for (const line of message2.split("\n")) write(`${INDENT}${line}`);
|
|
15287
|
+
}
|
|
15240
15288
|
};
|
|
15241
15289
|
}
|
|
15242
15290
|
|
|
@@ -15248,9 +15296,13 @@ async function setup(opts = {}, deps = {}) {
|
|
|
15248
15296
|
try {
|
|
15249
15297
|
await setupScript(opts, deps);
|
|
15250
15298
|
} catch (err) {
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15299
|
+
if (err instanceof StandaloneError) {
|
|
15300
|
+
write(err.message);
|
|
15301
|
+
} else {
|
|
15302
|
+
for (const line of pathLines(reloadLine())) write(line);
|
|
15303
|
+
write(err instanceof Error ? err.message : String(err));
|
|
15304
|
+
write(`Check the server connection, then run ${cabane("setup")} again.`);
|
|
15305
|
+
}
|
|
15254
15306
|
process.exitCode = 1;
|
|
15255
15307
|
} finally {
|
|
15256
15308
|
setConsoleLogging(true);
|
|
@@ -15278,8 +15330,21 @@ async function setupScript(opts, deps) {
|
|
|
15278
15330
|
}
|
|
15279
15331
|
let justPaired = false;
|
|
15280
15332
|
if (!paired) {
|
|
15281
|
-
|
|
15282
|
-
|
|
15333
|
+
let baseUrl;
|
|
15334
|
+
try {
|
|
15335
|
+
baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
|
|
15336
|
+
} catch (err) {
|
|
15337
|
+
if (!(err instanceof NoPairAddressError)) throw err;
|
|
15338
|
+
ui.cancel(err.message);
|
|
15339
|
+
process.exitCode = 1;
|
|
15340
|
+
return;
|
|
15341
|
+
}
|
|
15342
|
+
const device = await (deps.pair ?? pairDevice)({
|
|
15343
|
+
baseUrl,
|
|
15344
|
+
interactive,
|
|
15345
|
+
ui,
|
|
15346
|
+
onCode: () => rememberPairAddress(baseUrl)
|
|
15347
|
+
});
|
|
15283
15348
|
if (!device) return;
|
|
15284
15349
|
writePairedConfig(device);
|
|
15285
15350
|
justPaired = true;
|
|
@@ -16370,7 +16435,7 @@ init_cabane_command();
|
|
|
16370
16435
|
rememberInvokedCommand();
|
|
16371
16436
|
var program = new Command();
|
|
16372
16437
|
program.name("cabane").description("Run your Cabane agents on this device, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
|
|
16373
|
-
program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the Cabane
|
|
16438
|
+
program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the address of the Cabane to pair with").addOption(
|
|
16374
16439
|
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
16375
16440
|
"info",
|
|
16376
16441
|
"warn",
|
package/dist/pairing-config.js
CHANGED
|
@@ -8,6 +8,21 @@ import { realpathSync } from "fs";
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { dirname, join, sep } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
+
function installLineWords(install = companionInstall()) {
|
|
12
|
+
const line = install === "script" ? "installer line" : "npm line";
|
|
13
|
+
return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
|
|
14
|
+
}
|
|
15
|
+
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
16
|
+
const real = (path) => {
|
|
17
|
+
try {
|
|
18
|
+
return realpathSync(path);
|
|
19
|
+
} catch {
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const scriptNode = join(real(home), ".cabane", "node") + sep;
|
|
24
|
+
return real(execPath).startsWith(scriptNode) ? "script" : "npm";
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
// src/cli-entry.ts
|
|
13
28
|
import { existsSync } from "fs";
|
|
@@ -49,6 +64,12 @@ var CompanionError = class extends Error {
|
|
|
49
64
|
this.name = "CompanionError";
|
|
50
65
|
}
|
|
51
66
|
};
|
|
67
|
+
var StandaloneError = class extends CompanionError {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "StandaloneError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
52
73
|
var ApiError = class extends CompanionError {
|
|
53
74
|
constructor(status, message, body) {
|
|
54
75
|
super(message);
|
|
@@ -341,31 +362,13 @@ function loadConfigTolerant() {
|
|
|
341
362
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
342
363
|
if (strict.success) {
|
|
343
364
|
const {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
logLevel,
|
|
350
|
-
logFormat,
|
|
351
|
-
harnesses: harnesses2,
|
|
352
|
-
opencode: opencode2
|
|
365
|
+
baseUrl: _baseUrl,
|
|
366
|
+
deviceToken: _deviceToken,
|
|
367
|
+
deviceId: _deviceId,
|
|
368
|
+
deviceLabel: _deviceLabel,
|
|
369
|
+
...local2
|
|
353
370
|
} = strict.data;
|
|
354
|
-
return {
|
|
355
|
-
local: {
|
|
356
|
-
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
357
|
-
...prepareHook2 !== void 0 ? { prepareHook: prepareHook2 } : {},
|
|
358
|
-
...root2 !== void 0 ? { root: root2 } : {},
|
|
359
|
-
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
360
|
-
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
361
|
-
...logLevel !== void 0 ? { logLevel } : {},
|
|
362
|
-
...logFormat !== void 0 ? { logFormat } : {},
|
|
363
|
-
...harnesses2 !== void 0 ? { harnesses: harnesses2 } : {},
|
|
364
|
-
...opencode2 !== void 0 ? { opencode: opencode2 } : {}
|
|
365
|
-
},
|
|
366
|
-
note: null,
|
|
367
|
-
hadPriorConfig: true
|
|
368
|
-
};
|
|
371
|
+
return { local: local2, note: null, hadPriorConfig: true };
|
|
369
372
|
}
|
|
370
373
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
371
374
|
const local = {};
|
|
@@ -385,6 +388,11 @@ function loadConfigTolerant() {
|
|
|
385
388
|
if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
|
|
386
389
|
const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
|
|
387
390
|
if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
|
|
391
|
+
const update = companionConfigSchema.shape.update.safeParse(obj.update);
|
|
392
|
+
if (update.success && update.data !== void 0) local.update = update.data;
|
|
393
|
+
if (typeof obj.serviceDeclined === "boolean") local.serviceDeclined = obj.serviceDeclined;
|
|
394
|
+
const runner = companionConfigSchema.shape.runner.safeParse(obj.runner);
|
|
395
|
+
if (runner.success && runner.data !== void 0) local.runner = runner.data;
|
|
388
396
|
return {
|
|
389
397
|
local,
|
|
390
398
|
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
@@ -469,8 +477,9 @@ async function postJson(baseUrl, path, body, signal) {
|
|
|
469
477
|
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
470
478
|
);
|
|
471
479
|
if (res.status === 404 && path.endsWith("/code"))
|
|
472
|
-
throw new
|
|
473
|
-
`No pairing endpoint at ${baseUrl}
|
|
480
|
+
throw new StandaloneError(
|
|
481
|
+
`No pairing endpoint at ${baseUrl}: it isn't a Cabane's address, or that Cabane is too old for this companion.
|
|
482
|
+
Run ${installLineWords()}.`
|
|
474
483
|
);
|
|
475
484
|
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
476
485
|
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
@@ -524,9 +533,9 @@ async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
|
524
533
|
}
|
|
525
534
|
|
|
526
535
|
// src/pairing-config.ts
|
|
527
|
-
var DEFAULT_BASE_URL = "https://app.cabane.ai";
|
|
528
536
|
function resolvePairBaseUrl(server) {
|
|
529
|
-
const raw = (server ?? process.env.CABANE_BASE_URL ??
|
|
537
|
+
const raw = (server ?? process.env.CABANE_BASE_URL ?? "").trim();
|
|
538
|
+
if (!raw) throw new NoPairAddressError();
|
|
530
539
|
if (!isAllowedBaseUrl(raw)) {
|
|
531
540
|
throw new CompanionError(
|
|
532
541
|
`invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
|
|
@@ -534,6 +543,20 @@ function resolvePairBaseUrl(server) {
|
|
|
534
543
|
}
|
|
535
544
|
return raw;
|
|
536
545
|
}
|
|
546
|
+
var NoPairAddressError = class extends StandaloneError {
|
|
547
|
+
constructor(install = companionInstall()) {
|
|
548
|
+
const words = installLineWords(install);
|
|
549
|
+
super(
|
|
550
|
+
`Setup doesn't know which Cabane to pair this device with. Run the
|
|
551
|
+
${words.slice("the ".length)}.`
|
|
552
|
+
);
|
|
553
|
+
this.name = "NoPairAddressError";
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
function rememberPairAddress(baseUrl) {
|
|
557
|
+
const { local } = loadConfigTolerant();
|
|
558
|
+
saveConfig({ ...local, baseUrl });
|
|
559
|
+
}
|
|
537
560
|
function writePairedConfig(paired) {
|
|
538
561
|
const { local, note } = loadConfigTolerant();
|
|
539
562
|
const config = {
|
|
@@ -558,8 +581,10 @@ export {
|
|
|
558
581
|
CompanionError,
|
|
559
582
|
EnrollmentCancelledError,
|
|
560
583
|
EnrollmentExpiredError,
|
|
584
|
+
NoPairAddressError,
|
|
561
585
|
awaitEnrollment,
|
|
562
586
|
isDevicePaired,
|
|
587
|
+
rememberPairAddress,
|
|
563
588
|
requestEnrollmentCode,
|
|
564
589
|
resolvePairBaseUrl,
|
|
565
590
|
writePairedConfig
|
package/dist/postinstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/bundled-binaries.ts
|
|
2
2
|
import { realpathSync as realpathSync3, statSync } from "fs";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
|
-
import { dirname as
|
|
4
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6
6
|
|
|
7
7
|
// src/cabane-command.ts
|
|
@@ -14,7 +14,13 @@ import { realpathSync } from "fs";
|
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { dirname, join, sep } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
|
-
|
|
17
|
+
function installScriptCommand(address) {
|
|
18
|
+
return `curl -fsSL ${address.replace(/\/+$/, "")}/install.sh | bash`;
|
|
19
|
+
}
|
|
20
|
+
function installLineWords(install = companionInstall()) {
|
|
21
|
+
const line = install === "script" ? "installer line" : "npm line";
|
|
22
|
+
return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
|
|
23
|
+
}
|
|
18
24
|
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
19
25
|
const real = (path) => {
|
|
20
26
|
try {
|
|
@@ -57,6 +63,308 @@ function cabane(rest) {
|
|
|
57
63
|
return `cabane ${rest}`;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
// src/config.ts
|
|
67
|
+
import {
|
|
68
|
+
chmodSync,
|
|
69
|
+
mkdirSync,
|
|
70
|
+
readFileSync as readFileSync2,
|
|
71
|
+
renameSync,
|
|
72
|
+
rmSync,
|
|
73
|
+
writeFileSync,
|
|
74
|
+
existsSync as existsSync3
|
|
75
|
+
} from "fs";
|
|
76
|
+
import { homedir as homedir5, userInfo } from "os";
|
|
77
|
+
import { dirname as dirname3, isAbsolute, join as join4 } from "path";
|
|
78
|
+
import { z as z2 } from "zod";
|
|
79
|
+
|
|
80
|
+
// src/errors.ts
|
|
81
|
+
var CompanionError = class extends Error {
|
|
82
|
+
constructor(message) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "CompanionError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ConfigError = class extends CompanionError {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "ConfigError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/pairing.ts
|
|
95
|
+
function isAllowedBaseUrl(raw) {
|
|
96
|
+
let url;
|
|
97
|
+
try {
|
|
98
|
+
url = new URL(raw);
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (url.protocol === "https:") return true;
|
|
103
|
+
if (url.protocol === "http:") {
|
|
104
|
+
const host = url.hostname.toLowerCase();
|
|
105
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/prepare-hook.ts
|
|
111
|
+
import { spawn } from "child_process";
|
|
112
|
+
import { z } from "zod";
|
|
113
|
+
var prepareHookSchema = z.object({
|
|
114
|
+
command: z.string().min(1),
|
|
115
|
+
args: z.array(z.string()).optional(),
|
|
116
|
+
// Extra env handed to the hook process itself (merged over process.env).
|
|
117
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
118
|
+
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
119
|
+
// default is generous; a hook that hangs past this is killed and the turn
|
|
120
|
+
// fails with a clear timeout message rather than pinning the companion.
|
|
121
|
+
timeoutMs: z.number().int().positive().optional()
|
|
122
|
+
}).strict();
|
|
123
|
+
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
124
|
+
var prepareResultSchema = z.object({
|
|
125
|
+
cwd: z.string().min(1),
|
|
126
|
+
env: z.record(z.string(), z.string()).optional()
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// src/config.ts
|
|
130
|
+
var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
|
|
131
|
+
var realHomeCache;
|
|
132
|
+
function realAccountHome() {
|
|
133
|
+
if (realHomeCache === void 0) {
|
|
134
|
+
try {
|
|
135
|
+
realHomeCache = userInfo().homedir;
|
|
136
|
+
} catch {
|
|
137
|
+
realHomeCache = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return realHomeCache;
|
|
141
|
+
}
|
|
142
|
+
function cabaneDir() {
|
|
143
|
+
const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
|
|
144
|
+
if (process.env.VITEST) {
|
|
145
|
+
const real = realAccountHome();
|
|
146
|
+
if (real && dir === join4(real, ".cabane")) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`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).`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return dir;
|
|
153
|
+
}
|
|
154
|
+
function configPath() {
|
|
155
|
+
return join4(cabaneDir(), "config.json");
|
|
156
|
+
}
|
|
157
|
+
var localAgentConfigSchema = z2.object({
|
|
158
|
+
cwd: z2.string().optional(),
|
|
159
|
+
prepareHook: prepareHookSchema.optional(),
|
|
160
|
+
// The Claude Code auto-memory escape hatch. Auto-memory is forced OFF by
|
|
161
|
+
// default (memory belongs in the Cabane workspace, and a shared companion
|
|
162
|
+
// would pool one cwd-keyed memory dir across users). On a companion you run
|
|
163
|
+
// yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
|
|
164
|
+
// own `~/.claude/settings.json` — Cabane then stops
|
|
165
|
+
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
166
|
+
// (the user and project settings load on every Claude Code turn).
|
|
167
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
168
|
+
}).strict();
|
|
169
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
|
|
170
|
+
function isRootPath(value) {
|
|
171
|
+
return isAbsolute(value) || value === "~" || value.startsWith("~/");
|
|
172
|
+
}
|
|
173
|
+
var companionConfigSchema = z2.object({
|
|
174
|
+
// https-enforced (loopback exempt), so a hand-edited config can't put the
|
|
175
|
+
// device token and prompt on a plaintext channel.
|
|
176
|
+
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
177
|
+
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
178
|
+
}),
|
|
179
|
+
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
180
|
+
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
|
181
|
+
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
182
|
+
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
183
|
+
// the rest of the config; `pair` always writes one.
|
|
184
|
+
deviceToken: z2.string().optional(),
|
|
185
|
+
// Identity hints, learned from the device flow and refreshed on the first
|
|
186
|
+
// assignments pull. Cosmetic — used for `status`/dashboard display only.
|
|
187
|
+
deviceId: z2.string().optional(),
|
|
188
|
+
deviceLabel: z2.string().optional(),
|
|
189
|
+
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
190
|
+
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
191
|
+
// never writes this (it only persists credentials + the device, elsewhere).
|
|
192
|
+
agents: agentsConfigSchema.optional(),
|
|
193
|
+
// The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
|
|
194
|
+
// `agents` row names one. Per-agent hooks alone make the config a second
|
|
195
|
+
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
196
|
+
// in the operator's `$HOME`.
|
|
197
|
+
prepareHook: prepareHookSchema.optional(),
|
|
198
|
+
// The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
|
|
199
|
+
// `autoOpen` remains parseable for older config files but no longer controls
|
|
200
|
+
// startup. `logLevel` is pino's level, live-editable.
|
|
201
|
+
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
202
|
+
autoOpen: z2.boolean().optional(),
|
|
203
|
+
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
204
|
+
logFormat: z2.enum(["human", "json"]).optional(),
|
|
205
|
+
// Where this device's `opencode serve` answers, when it isn't the default
|
|
206
|
+
// address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
|
|
207
|
+
// that answers is connected. Auth is opencode's own; Cabane never sees
|
|
208
|
+
// provider keys.
|
|
209
|
+
opencode: z2.object({
|
|
210
|
+
serverUrl: z2.string().url()
|
|
211
|
+
}).strict().optional(),
|
|
212
|
+
// The one knob on agents: an allowlist. Agents are detected, never offered
|
|
213
|
+
// (`harness-detect.ts`): whatever is signed in here is connected. Someone with
|
|
214
|
+
// Codex on the device who doesn't want Cabane to see it lists the ones it may:
|
|
215
|
+
// `"harnesses": ["claude-code"]`. Absent means all three.
|
|
216
|
+
harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
|
|
217
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
218
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
219
|
+
// which `start` offers; off either way until someone says yes.
|
|
220
|
+
update: z2.object({
|
|
221
|
+
auto: z2.boolean()
|
|
222
|
+
}).strict().optional(),
|
|
223
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
224
|
+
// `cabane service install` still installs it.
|
|
225
|
+
serviceDeclined: z2.boolean().optional(),
|
|
226
|
+
// Where agents stand on this device. Every (agent, conversation) pair gets a directory under it,
|
|
227
|
+
// and an agent with no folder of its own and no prepare hook works there
|
|
228
|
+
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
229
|
+
// laptop creates no new visible folder; set it to make the work visible.
|
|
230
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
231
|
+
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
232
|
+
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
233
|
+
// is the operator's (a container or VM per turn, say); the companion knows nothing of it
|
|
234
|
+
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
235
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
236
|
+
// what a wrapper can rely on.
|
|
237
|
+
runner: z2.object({
|
|
238
|
+
command: z2.array(z2.string().min(1)).min(1),
|
|
239
|
+
// The external wrapper supplies only broker placeholders and removes
|
|
240
|
+
// credential mounts. Do not hand provider logins to that runner on stdin.
|
|
241
|
+
brokeredCredentials: z2.boolean().optional()
|
|
242
|
+
}).strict().optional()
|
|
243
|
+
});
|
|
244
|
+
var RETIRED_ROW_KEYS = ["sandbox"];
|
|
245
|
+
var RETIRED_HOOK_KEYS = ["validateCached", "failureReporter"];
|
|
246
|
+
function isObject(value) {
|
|
247
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
248
|
+
}
|
|
249
|
+
function splitRetired(raw) {
|
|
250
|
+
const keys = [];
|
|
251
|
+
const without = (obj, names, path) => {
|
|
252
|
+
const out = {};
|
|
253
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
254
|
+
if (names.includes(k)) keys.push(`${path}${k}`);
|
|
255
|
+
else out[k] = v;
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
};
|
|
259
|
+
const hook = (row, path) => isObject(row.prepareHook) ? { ...row, prepareHook: without(row.prepareHook, RETIRED_HOOK_KEYS, `${path}prepareHook.`) } : row;
|
|
260
|
+
const kept = hook(without(raw, ["runners"], ""), "");
|
|
261
|
+
if (isObject(kept.agents)) {
|
|
262
|
+
const agents = {};
|
|
263
|
+
for (const [key, row] of Object.entries(kept.agents)) {
|
|
264
|
+
const path = `agents.${key}.`;
|
|
265
|
+
agents[key] = isObject(row) ? hook(without(row, RETIRED_ROW_KEYS, path), path) : row;
|
|
266
|
+
}
|
|
267
|
+
kept.agents = agents;
|
|
268
|
+
}
|
|
269
|
+
return { keys, kept };
|
|
270
|
+
}
|
|
271
|
+
function withoutRetiredKeys(raw) {
|
|
272
|
+
return isObject(raw) ? splitRetired(raw).kept : raw;
|
|
273
|
+
}
|
|
274
|
+
var legacyOfferSchema = z2.object({
|
|
275
|
+
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
276
|
+
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
277
|
+
harnessesChosen: z2.boolean().optional(),
|
|
278
|
+
opencodeDeclined: z2.boolean().optional()
|
|
279
|
+
});
|
|
280
|
+
function migrateHarnessAllowlist(raw) {
|
|
281
|
+
const legacy = legacyOfferSchema.safeParse(raw);
|
|
282
|
+
if (!legacy.success || !legacy.data.harnessesChosen) return null;
|
|
283
|
+
const declined = /* @__PURE__ */ new Set();
|
|
284
|
+
if (legacy.data.claudeCode?.enabled === false) declined.add("claude-code");
|
|
285
|
+
if (legacy.data.codex?.enabled === false) declined.add("codex");
|
|
286
|
+
if (legacy.data.opencodeDeclined === true) declined.add("opencode");
|
|
287
|
+
if (declined.size === 0) return null;
|
|
288
|
+
return HARNESS_RUNTIMES.filter((runtime) => !declined.has(runtime));
|
|
289
|
+
}
|
|
290
|
+
function carriesLegacyOffer(raw) {
|
|
291
|
+
if (!raw || typeof raw !== "object") return false;
|
|
292
|
+
const obj = raw;
|
|
293
|
+
return "harnessesChosen" in obj || "opencodeDeclined" in obj || typeof obj.claudeCode === "object" && obj.claudeCode !== null && "enabled" in obj.claudeCode || obj.codex !== void 0;
|
|
294
|
+
}
|
|
295
|
+
function loadConfig() {
|
|
296
|
+
const path = configPath();
|
|
297
|
+
if (!existsSync3(path)) return null;
|
|
298
|
+
let raw;
|
|
299
|
+
try {
|
|
300
|
+
raw = readFileSync2(path, "utf8");
|
|
301
|
+
} catch (err) {
|
|
302
|
+
throw new ConfigError(
|
|
303
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (raw.trim().length === 0) return null;
|
|
307
|
+
let parsed;
|
|
308
|
+
try {
|
|
309
|
+
parsed = withoutRetiredKeys(JSON.parse(raw));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
throw new ConfigError(
|
|
312
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const result = companionConfigSchema.safeParse(parsed);
|
|
316
|
+
if (!result.success) {
|
|
317
|
+
const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
|
|
318
|
+
if (agentIssue) {
|
|
319
|
+
throw new ConfigError(
|
|
320
|
+
`${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const rootIssue = result.error.issues.find((i) => i.path[0] === "root");
|
|
324
|
+
if (rootIssue) {
|
|
325
|
+
throw new ConfigError(
|
|
326
|
+
`${path}: invalid "root" \u2014 ${rootIssue.message}. Fix it in the config and retry.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw new ConfigError(
|
|
330
|
+
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("setup")}\` to pair again, or \`${cabane("logout --purge")}\` to reset.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (carriesLegacyOffer(parsed)) {
|
|
334
|
+
const allowlist = migrateHarnessAllowlist(parsed);
|
|
335
|
+
const migrated = {
|
|
336
|
+
...result.data,
|
|
337
|
+
...allowlist && !result.data.harnesses ? { harnesses: allowlist } : {}
|
|
338
|
+
};
|
|
339
|
+
saveConfig(migrated);
|
|
340
|
+
return migrated;
|
|
341
|
+
}
|
|
342
|
+
return result.data;
|
|
343
|
+
}
|
|
344
|
+
function saveConfig(cfg) {
|
|
345
|
+
const path = configPath();
|
|
346
|
+
mkdirSync(dirname3(path), { recursive: true });
|
|
347
|
+
try {
|
|
348
|
+
chmodSync(cabaneDir(), 448);
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
352
|
+
try {
|
|
353
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
354
|
+
try {
|
|
355
|
+
chmodSync(tmp, 384);
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
renameSync(tmp, path);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
try {
|
|
361
|
+
rmSync(tmp, { force: true });
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
throw err;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
60
368
|
// src/bundled-binaries.ts
|
|
61
369
|
function nodeResolveSelf(specifier) {
|
|
62
370
|
const here = fileURLToPath3(import.meta.url);
|
|
@@ -73,9 +381,9 @@ function nodeResolveSelf(specifier) {
|
|
|
73
381
|
return walkNodeModules(specifier, here);
|
|
74
382
|
}
|
|
75
383
|
function walkNodeModules(pkg, fromPath) {
|
|
76
|
-
let dir =
|
|
384
|
+
let dir = dirname4(fromPath);
|
|
77
385
|
for (; ; ) {
|
|
78
|
-
const manifest =
|
|
386
|
+
const manifest = join5(dir, "node_modules", pkg, "package.json");
|
|
79
387
|
if (nodeIsFile(manifest)) {
|
|
80
388
|
try {
|
|
81
389
|
return realpathSync3(manifest);
|
|
@@ -83,7 +391,7 @@ function walkNodeModules(pkg, fromPath) {
|
|
|
83
391
|
return manifest;
|
|
84
392
|
}
|
|
85
393
|
}
|
|
86
|
-
const parent =
|
|
394
|
+
const parent = dirname4(dir);
|
|
87
395
|
if (parent === dir) return null;
|
|
88
396
|
dir = parent;
|
|
89
397
|
}
|
|
@@ -145,7 +453,7 @@ function resolveClaude(env) {
|
|
|
145
453
|
const manifest = env.resolve(`${pkg}/package.json`, sdkEntry);
|
|
146
454
|
if (!manifest) continue;
|
|
147
455
|
sawPackage ??= pkg;
|
|
148
|
-
const exe =
|
|
456
|
+
const exe = join5(dirname4(manifest), env.platform === "win32" ? "claude.exe" : "claude");
|
|
149
457
|
if (env.isFile(exe)) {
|
|
150
458
|
return { ...base, packageName: pkg, status: "present", path: exe };
|
|
151
459
|
}
|
|
@@ -196,13 +504,13 @@ function resolveCodex(env) {
|
|
|
196
504
|
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
197
505
|
const platformManifest = env.resolve(`${pkg}/package.json`, cliManifest);
|
|
198
506
|
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
199
|
-
const packageRoot =
|
|
507
|
+
const packageRoot = join5(dirname4(platformManifest), "vendor", triple);
|
|
200
508
|
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
201
|
-
const current =
|
|
202
|
-
if (env.isFile(current) && env.isFile(
|
|
509
|
+
const current = join5(packageRoot, "bin", exeName);
|
|
510
|
+
if (env.isFile(current) && env.isFile(join5(packageRoot, "codex-package.json"))) {
|
|
203
511
|
return { ...base, status: "present", path: current };
|
|
204
512
|
}
|
|
205
|
-
const legacy =
|
|
513
|
+
const legacy = join5(packageRoot, "codex", exeName);
|
|
206
514
|
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
207
515
|
return { ...base, status: "executable_missing", path: null };
|
|
208
516
|
}
|
|
@@ -233,15 +541,30 @@ var HARNESS_LABEL = {
|
|
|
233
541
|
function missingNoun(status) {
|
|
234
542
|
return status === "executable_missing" ? "executable" : "platform package";
|
|
235
543
|
}
|
|
236
|
-
function repairCommands(route = repairRoute(), spell = cabane) {
|
|
544
|
+
function repairCommands(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
237
545
|
if (route === "managed") return [spell("stop"), spell("update")];
|
|
238
|
-
if (route === "script")
|
|
546
|
+
if (route === "script")
|
|
547
|
+
return [
|
|
548
|
+
spell("stop"),
|
|
549
|
+
address ? installScriptCommand(address) : { words: installLineWords("script") }
|
|
550
|
+
];
|
|
239
551
|
return ["npm i -g @cabane/companion --include=optional"];
|
|
240
552
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
553
|
+
function configuredAddress() {
|
|
554
|
+
try {
|
|
555
|
+
return loadConfig()?.baseUrl ?? null;
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function quoteStep(step, quote) {
|
|
561
|
+
return typeof step === "string" ? quote(step) : step.words;
|
|
562
|
+
}
|
|
563
|
+
var backticks = (command) => `\`${command}\``;
|
|
564
|
+
function repairAction(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
565
|
+
const [first, ...rest] = repairCommands(route, spell, address);
|
|
566
|
+
if (rest.length === 0) return `Reinstall with ${quoteStep(first, backticks)}.`;
|
|
567
|
+
return `Run ${quoteStep(first, backticks)}, then ${rest.map((step) => quoteStep(step, backticks)).join(", then ")}. ${STOP_WARNING}`;
|
|
245
568
|
}
|
|
246
569
|
var STOP_WARNING = "Stopping ends any turn in progress.";
|
|
247
570
|
function installWarning(r) {
|
package/dist/runtime.js
CHANGED
|
@@ -12,7 +12,13 @@ import { realpathSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { dirname, join, sep } from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
-
|
|
15
|
+
function installScriptCommand(address) {
|
|
16
|
+
return `curl -fsSL ${address.replace(/\/+$/, "")}/install.sh | bash`;
|
|
17
|
+
}
|
|
18
|
+
function installLineWords(install = companionInstall()) {
|
|
19
|
+
const line = install === "script" ? "installer line" : "npm line";
|
|
20
|
+
return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
|
|
21
|
+
}
|
|
16
22
|
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
17
23
|
const real = (path) => {
|
|
18
24
|
try {
|
|
@@ -983,19 +989,34 @@ var HARNESS_INTENT_WORD = {
|
|
|
983
989
|
function missingNoun(status) {
|
|
984
990
|
return status === "executable_missing" ? "executable" : "platform package";
|
|
985
991
|
}
|
|
986
|
-
function repairCommands(route = repairRoute(), spell = cabane) {
|
|
992
|
+
function repairCommands(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
987
993
|
if (route === "managed") return [spell("stop"), spell("update")];
|
|
988
|
-
if (route === "script")
|
|
994
|
+
if (route === "script")
|
|
995
|
+
return [
|
|
996
|
+
spell("stop"),
|
|
997
|
+
address ? installScriptCommand(address) : { words: installLineWords("script") }
|
|
998
|
+
];
|
|
989
999
|
return ["npm i -g @cabane/companion --include=optional"];
|
|
990
1000
|
}
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1001
|
+
function configuredAddress() {
|
|
1002
|
+
try {
|
|
1003
|
+
return loadConfig()?.baseUrl ?? null;
|
|
1004
|
+
} catch {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function quoteStep(step, quote) {
|
|
1009
|
+
return typeof step === "string" ? quote(step) : step.words;
|
|
1010
|
+
}
|
|
1011
|
+
var backticks = (command) => `\`${command}\``;
|
|
1012
|
+
function repairAction(route = repairRoute(), spell = cabane, address = configuredAddress()) {
|
|
1013
|
+
const [first, ...rest] = repairCommands(route, spell, address);
|
|
1014
|
+
if (rest.length === 0) return `Reinstall with ${quoteStep(first, backticks)}.`;
|
|
1015
|
+
return `Run ${quoteStep(first, backticks)}, then ${rest.map((step) => quoteStep(step, backticks)).join(", then ")}. ${STOP_WARNING}`;
|
|
995
1016
|
}
|
|
996
1017
|
var STOP_WARNING = "Stopping ends any turn in progress.";
|
|
997
1018
|
function repairSteps(route = repairRoute()) {
|
|
998
|
-
return repairCommands(route).join(", then ");
|
|
1019
|
+
return repairCommands(route).map((step) => quoteStep(step, (command) => command)).join(", then ");
|
|
999
1020
|
}
|
|
1000
1021
|
function repairCost(route = repairRoute()) {
|
|
1001
1022
|
return repairCommands(route).length > 1 ? STOP_WARNING : "";
|
|
@@ -1610,7 +1631,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
1610
1631
|
const stopsFirst = commands.length > 1;
|
|
1611
1632
|
if (!stopsFirst) commands.push("cabane stop");
|
|
1612
1633
|
commands.push("cabane start");
|
|
1613
|
-
const list = commands.map((command) => `\`${command}\``);
|
|
1634
|
+
const list = commands.map((step) => quoteStep(step, (command) => `\`${command}\``));
|
|
1614
1635
|
return `${harnessLabel(runtime ?? reason.slice("runtime_incomplete:".length))}'s installation on this device is incomplete. Run ${list.slice(0, -1).join(", ")} and ${list.at(-1)}. ${stopsFirst ? `${STOP_WARNING} ` : ""}Reply in Cabane to try again.`;
|
|
1615
1636
|
}
|
|
1616
1637
|
const unavailable = /^(codex|opencode)_unavailable:/.exec(reason ?? "")?.[1] ?? (reason?.startsWith("runtime_unavailable:") ? reason.slice("runtime_unavailable:".length).trim() : void 0);
|
|
@@ -3134,6 +3155,7 @@ var CLAUDE_CODE_ADDENDUM = [
|
|
|
3134
3155
|
].join("\n");
|
|
3135
3156
|
|
|
3136
3157
|
// packages/agent-runtime/src/mcp-secret-env.ts
|
|
3158
|
+
var TURN_TOKEN_ENV = "CABANE_TURN_TOKEN";
|
|
3137
3159
|
function headersAsEnvReferences(server) {
|
|
3138
3160
|
const refs = Object.entries(server.headersFromEnv ?? {}).map(
|
|
3139
3161
|
([header, envName]) => [header, `\${${envName}}`]
|
|
@@ -3298,7 +3320,7 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3298
3320
|
type: "http",
|
|
3299
3321
|
url: req.cabane.mcpUrl,
|
|
3300
3322
|
headers: {
|
|
3301
|
-
Authorization:
|
|
3323
|
+
Authorization: `\${${TURN_TOKEN_ENV}}`,
|
|
3302
3324
|
[ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
|
|
3303
3325
|
},
|
|
3304
3326
|
// Claude Code 2.1.x defers every MCP tool behind tool-search by default;
|
|
@@ -3357,10 +3379,15 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3357
3379
|
//
|
|
3358
3380
|
// `secretEnv` — the values the MCP headers above reference — joins here and only here: it is
|
|
3359
3381
|
// this child's environment, not the host's.
|
|
3382
|
+
//
|
|
3383
|
+
// The turn token goes after everything a room secret, a hook or the inherited environment can
|
|
3384
|
+
// supply: this spread order is the only thing that stops a variable of the same name from
|
|
3385
|
+
// displacing the turn's own credential.
|
|
3360
3386
|
env: {
|
|
3361
3387
|
...process.env,
|
|
3362
3388
|
...req.local.env,
|
|
3363
3389
|
...req.local.secretEnv,
|
|
3390
|
+
[TURN_TOKEN_ENV]: `Bearer ${req.cabane.bearer}`,
|
|
3364
3391
|
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
|
|
3365
3392
|
},
|
|
3366
3393
|
...resume ? { resume } : {}
|
|
@@ -4991,12 +5018,16 @@ function codexSecretEnv(req) {
|
|
|
4991
5018
|
const env = { ...req.local.secretEnv };
|
|
4992
5019
|
for (const server of Object.values(req.local.mcpServers ?? {})) {
|
|
4993
5020
|
if ("url" in server) continue;
|
|
4994
|
-
for (const [variable, envName] of
|
|
5021
|
+
for (const [variable, envName] of forwardedEnvVars(server.envFromEnv)) {
|
|
4995
5022
|
const value = req.local.secretEnv?.[envName];
|
|
4996
5023
|
if (value !== void 0) env[variable] = value;
|
|
4997
5024
|
}
|
|
4998
5025
|
}
|
|
4999
|
-
|
|
5026
|
+
env[TURN_TOKEN_ENV] = `Bearer ${req.cabane.bearer}`;
|
|
5027
|
+
return env;
|
|
5028
|
+
}
|
|
5029
|
+
function forwardedEnvVars(envFromEnv) {
|
|
5030
|
+
return Object.entries(envFromEnv ?? {}).filter(([variable]) => variable !== TURN_TOKEN_ENV);
|
|
5000
5031
|
}
|
|
5001
5032
|
var CORE_REPLACES = "Your standing instructions changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
|
|
5002
5033
|
var CONVERSATION_REPLACES = "This conversation's standing context \u2014 its charters, skill indexes and roster \u2014 changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one, including charters and skills an earlier turn carried in its own text: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
|
|
@@ -5040,7 +5071,7 @@ function buildConfig(req) {
|
|
|
5040
5071
|
default_tools_approval_mode: "approve"
|
|
5041
5072
|
};
|
|
5042
5073
|
} else if ("command" in server) {
|
|
5043
|
-
const forwarded =
|
|
5074
|
+
const forwarded = forwardedEnvVars(server.envFromEnv).map(([variable]) => variable).sort();
|
|
5044
5075
|
mcp_servers[name] = {
|
|
5045
5076
|
command: server.command,
|
|
5046
5077
|
...server.args ? { args: server.args } : {},
|
|
@@ -5068,19 +5099,15 @@ function buildConfig(req) {
|
|
|
5068
5099
|
}
|
|
5069
5100
|
mcp_servers[CABANE_MCP_SERVER3] = {
|
|
5070
5101
|
url: req.cabane.mcpUrl,
|
|
5071
|
-
http_headers: {
|
|
5072
|
-
|
|
5073
|
-
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5074
|
-
},
|
|
5102
|
+
http_headers: { [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId },
|
|
5103
|
+
env_http_headers: { Authorization: TURN_TOKEN_ENV },
|
|
5075
5104
|
default_tools_approval_mode: "approve"
|
|
5076
5105
|
};
|
|
5077
5106
|
if (req.cabane.turnControlUrl) {
|
|
5078
5107
|
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
5079
5108
|
url: req.cabane.turnControlUrl,
|
|
5080
|
-
http_headers: {
|
|
5081
|
-
|
|
5082
|
-
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5083
|
-
},
|
|
5109
|
+
http_headers: { [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId },
|
|
5110
|
+
env_http_headers: { Authorization: TURN_TOKEN_ENV },
|
|
5084
5111
|
default_tools_approval_mode: "approve"
|
|
5085
5112
|
};
|
|
5086
5113
|
}
|
|
@@ -5196,7 +5223,7 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
5196
5223
|
// path (the CLI splits on `.` and ignores TOML quoting), so it must ride inside an inline-table
|
|
5197
5224
|
// value, which the SDK's flattener can't produce. See `config-overrides.ts`.
|
|
5198
5225
|
configOverrides: buildConfigOverrides(spec.config),
|
|
5199
|
-
|
|
5226
|
+
env: codexProcessEnv(spec.secretEnv)
|
|
5200
5227
|
};
|
|
5201
5228
|
const codex = new Codex(codexOptions);
|
|
5202
5229
|
const threadOptions = buildSdkThreadOptions(spec);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.237",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|