@cabane/companion 0.6.158 → 0.6.160

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/dist/cli.js CHANGED
@@ -9,166 +9,11 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/bundled-binaries.ts
13
- import { realpathSync, statSync } from "fs";
14
- import { createRequire } from "module";
12
+ // src/install-kind.ts
13
+ import { realpathSync } from "fs";
15
14
  import { homedir } from "os";
16
15
  import { dirname, join, sep } from "path";
17
16
  import { fileURLToPath } from "url";
18
- function nodeResolveSelf(specifier) {
19
- const here = fileURLToPath(import.meta.url);
20
- if (typeof import.meta.resolve === "function") {
21
- try {
22
- return fileURLToPath(import.meta.resolve(specifier));
23
- } catch {
24
- }
25
- }
26
- try {
27
- return createRequire(here).resolve(specifier);
28
- } catch {
29
- }
30
- return walkNodeModules(specifier, here);
31
- }
32
- function walkNodeModules(pkg2, fromPath) {
33
- let dir2 = dirname(fromPath);
34
- for (; ; ) {
35
- const manifest = join(dir2, "node_modules", pkg2, "package.json");
36
- if (nodeIsFile(manifest)) {
37
- try {
38
- return realpathSync(manifest);
39
- } catch {
40
- return manifest;
41
- }
42
- }
43
- const parent = dirname(dir2);
44
- if (parent === dir2) return null;
45
- dir2 = parent;
46
- }
47
- }
48
- function nodeResolve(specifier, fromPath) {
49
- try {
50
- return createRequire(fromPath).resolve(specifier);
51
- } catch {
52
- return null;
53
- }
54
- }
55
- function nodeIsFile(path) {
56
- try {
57
- return statSync(path).isFile();
58
- } catch {
59
- return false;
60
- }
61
- }
62
- function detectPreferMusl(platform = process.platform) {
63
- if (platform !== "linux") return false;
64
- const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
65
- const header = report?.header;
66
- return report != null && header?.glibcVersionRuntime === void 0;
67
- }
68
- function defaultResolveEnv() {
69
- return {
70
- platform: process.platform,
71
- arch: process.arch,
72
- preferMusl: detectPreferMusl(),
73
- // Both resolutions start from this module, which tsup bundles into
74
- // `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
75
- // root — so one environment serves the runtime checks and the install
76
- // verifier alike.
77
- resolveSelf: nodeResolveSelf,
78
- resolve: nodeResolve,
79
- isFile: nodeIsFile
80
- };
81
- }
82
- function claudePlatformPackages(platform, arch, preferMusl) {
83
- if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
84
- if (platform === "linux") {
85
- return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
86
- }
87
- return [`${CLAUDE_SDK}-${platform}-${arch}`];
88
- }
89
- function resolveClaude(env) {
90
- const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
91
- const base = {
92
- runtime: "claude-code",
93
- packageName: candidates[0] ?? null,
94
- platform: env.platform,
95
- arch: env.arch
96
- };
97
- const sdkEntry = env.resolveSelf(CLAUDE_SDK);
98
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
99
- let sawPackage = null;
100
- for (const pkg2 of candidates) {
101
- const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
102
- if (!manifest) continue;
103
- sawPackage ??= pkg2;
104
- const exe = join(dirname(manifest), env.platform === "win32" ? "claude.exe" : "claude");
105
- if (env.isFile(exe)) {
106
- return { ...base, packageName: pkg2, status: "present", path: exe };
107
- }
108
- }
109
- return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
110
- }
111
- function codexTargetTriple(platform, arch) {
112
- if (platform === "linux" || platform === "android") {
113
- if (arch === "x64") return "x86_64-unknown-linux-musl";
114
- if (arch === "arm64") return "aarch64-unknown-linux-musl";
115
- return null;
116
- }
117
- if (platform === "darwin") {
118
- if (arch === "x64") return "x86_64-apple-darwin";
119
- if (arch === "arm64") return "aarch64-apple-darwin";
120
- return null;
121
- }
122
- if (platform === "win32") {
123
- if (arch === "x64") return "x86_64-pc-windows-msvc";
124
- if (arch === "arm64") return "aarch64-pc-windows-msvc";
125
- return null;
126
- }
127
- return null;
128
- }
129
- function resolveCodex(env) {
130
- const triple = codexTargetTriple(env.platform, env.arch);
131
- const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
132
- const base = {
133
- runtime: "codex",
134
- packageName: pkg2 ?? null,
135
- platform: env.platform,
136
- arch: env.arch
137
- };
138
- if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
139
- const sdkEntry = env.resolveSelf(CODEX_SDK);
140
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
141
- const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
142
- if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
143
- const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
144
- if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
145
- const packageRoot = join(dirname(platformManifest), "vendor", triple);
146
- const exeName = env.platform === "win32" ? "codex.exe" : "codex";
147
- const current = join(packageRoot, "bin", exeName);
148
- if (env.isFile(current) && env.isFile(join(packageRoot, "codex-package.json"))) {
149
- return { ...base, status: "present", path: current };
150
- }
151
- const legacy = join(packageRoot, "codex", exeName);
152
- if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
153
- return { ...base, status: "executable_missing", path: null };
154
- }
155
- function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
156
- try {
157
- return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
158
- } catch {
159
- return {
160
- runtime,
161
- status: "platform_package_missing",
162
- path: null,
163
- packageName: null,
164
- platform: env.platform,
165
- arch: env.arch
166
- };
167
- }
168
- }
169
- function missingNoun(status2) {
170
- return status2 === "executable_missing" ? "executable" : "platform package";
171
- }
172
17
  function companionInstall(execPath = process.execPath, home = homedir()) {
173
18
  const real = (path) => {
174
19
  try {
@@ -180,47 +25,21 @@ function companionInstall(execPath = process.execPath, home = homedir()) {
180
25
  const scriptNode = join(real(home), ".cabane", "node") + sep;
181
26
  return real(execPath).startsWith(scriptNode) ? "script" : "npm";
182
27
  }
183
- function repairCommands(install = companionInstall()) {
184
- return install === "script" ? [INSTALL_SCRIPT_COMMAND] : ["npm i -g @cabane/companion --include=optional"];
185
- }
186
- function repairAction(install = companionInstall()) {
187
- return `Reinstall with \`${repairCommands(install)[0]}\`.`;
188
- }
189
- function startupWarning(r) {
190
- const label = HARNESS_LABEL[r.runtime];
191
- return `${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but its installation is incomplete (${missingNoun(r.status)} is missing). It cannot run turns until repaired. ${repairAction()} Then restart the companion.`;
192
- }
193
- function harnessIssueNote(runtime, status2, install = companionInstall()) {
194
- const intent = HARNESS_INTENT_WORD[runtime];
195
- const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
196
- return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status2)} is missing. ${repairAction(install)} Then restart the companion.`;
28
+ function repairRoute(packageRoot = ownRoot(), install = companionInstall()) {
29
+ let root = packageRoot;
30
+ try {
31
+ root = realpathSync(packageRoot);
32
+ } catch {
33
+ }
34
+ return root.includes(`${sep}.cabane${sep}versions${sep}`) ? "managed" : install;
197
35
  }
198
- function turnIncompleteCopy(runtime) {
199
- return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
36
+ function ownRoot() {
37
+ return dirname(dirname(fileURLToPath(import.meta.url)));
200
38
  }
201
- var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, INSTALL_SCRIPT_COMMAND;
202
- var init_bundled_binaries = __esm({
203
- "src/bundled-binaries.ts"() {
39
+ var INSTALL_SCRIPT_COMMAND;
40
+ var init_install_kind = __esm({
41
+ "src/install-kind.ts"() {
204
42
  "use strict";
205
- CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
206
- CODEX_SDK = "@openai/codex-sdk";
207
- CODEX_CLI = "@openai/codex";
208
- CODEX_PACKAGE_BY_TRIPLE = {
209
- "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
210
- "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
211
- "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
212
- "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
213
- "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
214
- "aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
215
- };
216
- HARNESS_LABEL = {
217
- "claude-code": "Claude Code",
218
- codex: "Codex"
219
- };
220
- HARNESS_INTENT_WORD = {
221
- "claude-code": "connected",
222
- codex: "enabled"
223
- };
224
43
  INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
225
44
  }
226
45
  });
@@ -358,16 +177,37 @@ var init_term = __esm({
358
177
  // src/cabane-command.ts
359
178
  import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
360
179
  import { homedir as homedir3 } from "os";
361
- import { delimiter, join as join2, sep as sep2 } from "path";
180
+ import { basename, delimiter, dirname as dirname2, join as join2, sep as sep2 } from "path";
181
+ function invokedCommand(argv1) {
182
+ if (!argv1 || argv1.includes(VERSIONS_SEGMENT)) return null;
183
+ const name = basename(argv1);
184
+ if (name === "cabane" || name === "cabane-companion") return join2(dirname2(argv1), "cabane");
185
+ return npmBinFor(argv1);
186
+ }
187
+ function rememberInvokedCommand(env = process.env, argv1 = process.argv[1]) {
188
+ if (env[INVOKED_COMMAND_ENV]) return;
189
+ const invoked = invokedCommand(argv1);
190
+ if (invoked) env[INVOKED_COMMAND_ENV] = invoked;
191
+ }
362
192
  function installedCommandPath(deps = {}) {
363
193
  const install = deps.install ?? companionInstall();
364
194
  const home = deps.home ?? homedir3();
365
195
  if (install === "script") return join2(home, ".cabane", "bin", "cabane");
366
196
  const entry = deps.cliEntry ?? companionCliEntry();
197
+ const own = npmBinFor(entry);
198
+ if (own) return own;
199
+ if (!entry.includes(VERSIONS_SEGMENT)) return null;
200
+ const isExecutable = deps.isExecutable ?? executable;
201
+ const invoked = "invoked" in deps ? deps.invoked : process.env[INVOKED_COMMAND_ENV];
202
+ if (invoked && isExecutable(invoked)) return invoked;
203
+ const besideNode = join2(dirname2(dirname2(deps.execPath ?? process.execPath)), "bin", "cabane");
204
+ return isExecutable(besideNode) ? besideNode : null;
205
+ }
206
+ function npmBinFor(path) {
367
207
  const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
368
- const at = entry.indexOf(marker);
208
+ const at = path.indexOf(marker);
369
209
  if (at < 0) return null;
370
- return join2(entry.slice(0, at), "bin", "cabane");
210
+ return join2(path.slice(0, at), "bin", "cabane");
371
211
  }
372
212
  function cabaneCommand(deps = {}) {
373
213
  const own = installedCommandPath(deps);
@@ -383,6 +223,9 @@ function cabaneCommand(deps = {}) {
383
223
  function cabane(rest, deps) {
384
224
  return `${cabaneCommand(deps)} ${rest}`;
385
225
  }
226
+ function mayNeedUpdate(deps) {
227
+ return `This companion may need an update: run ${cabane("update", deps)}`;
228
+ }
386
229
  function firstOnPath(name, path, isExecutable = executable) {
387
230
  for (const dir2 of (path ?? "").split(delimiter)) {
388
231
  if (!dir2) continue;
@@ -406,12 +249,15 @@ function safeRealpath(path) {
406
249
  return null;
407
250
  }
408
251
  }
252
+ var INVOKED_COMMAND_ENV, VERSIONS_SEGMENT;
409
253
  var init_cabane_command = __esm({
410
254
  "src/cabane-command.ts"() {
411
255
  "use strict";
412
- init_bundled_binaries();
256
+ init_install_kind();
413
257
  init_cli_entry();
414
258
  init_term();
259
+ INVOKED_COMMAND_ENV = "CABANE_INVOKED_COMMAND";
260
+ VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
415
261
  }
416
262
  });
417
263
 
@@ -641,7 +487,7 @@ import {
641
487
  existsSync as existsSync2
642
488
  } from "fs";
643
489
  import { homedir as homedir4, userInfo } from "os";
644
- import { dirname as dirname2, join as join3 } from "path";
490
+ import { dirname as dirname3, join as join3 } from "path";
645
491
  import { z as z2 } from "zod";
646
492
  function realAccountHome() {
647
493
  if (realHomeCache === void 0) {
@@ -835,7 +681,7 @@ function loadConfigTolerant() {
835
681
  }
836
682
  function saveConfig(cfg) {
837
683
  const path = configPath();
838
- mkdirSync(dirname2(path), { recursive: true });
684
+ mkdirSync(dirname3(path), { recursive: true });
839
685
  try {
840
686
  chmodSync(cabaneDir(), 448);
841
687
  } catch {
@@ -1032,6 +878,226 @@ var init_config = __esm({
1032
878
  }
1033
879
  });
1034
880
 
881
+ // src/bundled-binaries.ts
882
+ import { realpathSync as realpathSync3, statSync } from "fs";
883
+ import { createRequire } from "module";
884
+ import { dirname as dirname4, join as join4 } from "path";
885
+ import { fileURLToPath as fileURLToPath3 } from "url";
886
+ function nodeResolveSelf(specifier) {
887
+ const here = fileURLToPath3(import.meta.url);
888
+ if (typeof import.meta.resolve === "function") {
889
+ try {
890
+ return fileURLToPath3(import.meta.resolve(specifier));
891
+ } catch {
892
+ }
893
+ }
894
+ try {
895
+ return createRequire(here).resolve(specifier);
896
+ } catch {
897
+ }
898
+ return walkNodeModules(specifier, here);
899
+ }
900
+ function walkNodeModules(pkg2, fromPath) {
901
+ let dir2 = dirname4(fromPath);
902
+ for (; ; ) {
903
+ const manifest = join4(dir2, "node_modules", pkg2, "package.json");
904
+ if (nodeIsFile(manifest)) {
905
+ try {
906
+ return realpathSync3(manifest);
907
+ } catch {
908
+ return manifest;
909
+ }
910
+ }
911
+ const parent = dirname4(dir2);
912
+ if (parent === dir2) return null;
913
+ dir2 = parent;
914
+ }
915
+ }
916
+ function nodeResolve(specifier, fromPath) {
917
+ try {
918
+ return createRequire(fromPath).resolve(specifier);
919
+ } catch {
920
+ return null;
921
+ }
922
+ }
923
+ function nodeIsFile(path) {
924
+ try {
925
+ return statSync(path).isFile();
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+ function detectPreferMusl(platform = process.platform) {
931
+ if (platform !== "linux") return false;
932
+ const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
933
+ const header = report?.header;
934
+ return report != null && header?.glibcVersionRuntime === void 0;
935
+ }
936
+ function defaultResolveEnv() {
937
+ return {
938
+ platform: process.platform,
939
+ arch: process.arch,
940
+ preferMusl: detectPreferMusl(),
941
+ // Both resolutions start from this module, which tsup bundles into
942
+ // `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
943
+ // root — so one environment serves the runtime checks and the install
944
+ // verifier alike.
945
+ resolveSelf: nodeResolveSelf,
946
+ resolve: nodeResolve,
947
+ isFile: nodeIsFile
948
+ };
949
+ }
950
+ function claudePlatformPackages(platform, arch, preferMusl) {
951
+ if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
952
+ if (platform === "linux") {
953
+ return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
954
+ }
955
+ return [`${CLAUDE_SDK}-${platform}-${arch}`];
956
+ }
957
+ function resolveClaude(env) {
958
+ const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
959
+ const base = {
960
+ runtime: "claude-code",
961
+ packageName: candidates[0] ?? null,
962
+ platform: env.platform,
963
+ arch: env.arch
964
+ };
965
+ const sdkEntry = env.resolveSelf(CLAUDE_SDK);
966
+ if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
967
+ let sawPackage = null;
968
+ for (const pkg2 of candidates) {
969
+ const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
970
+ if (!manifest) continue;
971
+ sawPackage ??= pkg2;
972
+ const exe = join4(dirname4(manifest), env.platform === "win32" ? "claude.exe" : "claude");
973
+ if (env.isFile(exe)) {
974
+ return { ...base, packageName: pkg2, status: "present", path: exe };
975
+ }
976
+ }
977
+ return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
978
+ }
979
+ function codexTargetTriple(platform, arch) {
980
+ if (platform === "linux" || platform === "android") {
981
+ if (arch === "x64") return "x86_64-unknown-linux-musl";
982
+ if (arch === "arm64") return "aarch64-unknown-linux-musl";
983
+ return null;
984
+ }
985
+ if (platform === "darwin") {
986
+ if (arch === "x64") return "x86_64-apple-darwin";
987
+ if (arch === "arm64") return "aarch64-apple-darwin";
988
+ return null;
989
+ }
990
+ if (platform === "win32") {
991
+ if (arch === "x64") return "x86_64-pc-windows-msvc";
992
+ if (arch === "arm64") return "aarch64-pc-windows-msvc";
993
+ return null;
994
+ }
995
+ return null;
996
+ }
997
+ function codexPlatformPackage(platform, arch) {
998
+ const triple = codexTargetTriple(platform, arch);
999
+ return triple ? CODEX_PACKAGE_BY_TRIPLE[triple] ?? null : null;
1000
+ }
1001
+ function resolveCodex(env) {
1002
+ const triple = codexTargetTriple(env.platform, env.arch);
1003
+ const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
1004
+ const base = {
1005
+ runtime: "codex",
1006
+ packageName: pkg2 ?? null,
1007
+ platform: env.platform,
1008
+ arch: env.arch
1009
+ };
1010
+ if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
1011
+ const sdkEntry = env.resolveSelf(CODEX_SDK);
1012
+ if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
1013
+ const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
1014
+ if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
1015
+ const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
1016
+ if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
1017
+ const packageRoot = join4(dirname4(platformManifest), "vendor", triple);
1018
+ const exeName = env.platform === "win32" ? "codex.exe" : "codex";
1019
+ const current = join4(packageRoot, "bin", exeName);
1020
+ if (env.isFile(current) && env.isFile(join4(packageRoot, "codex-package.json"))) {
1021
+ return { ...base, status: "present", path: current };
1022
+ }
1023
+ const legacy = join4(packageRoot, "codex", exeName);
1024
+ if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
1025
+ return { ...base, status: "executable_missing", path: null };
1026
+ }
1027
+ function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
1028
+ try {
1029
+ return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
1030
+ } catch {
1031
+ return {
1032
+ runtime,
1033
+ status: "platform_package_missing",
1034
+ path: null,
1035
+ packageName: null,
1036
+ platform: env.platform,
1037
+ arch: env.arch
1038
+ };
1039
+ }
1040
+ }
1041
+ function missingNoun(status2) {
1042
+ return status2 === "executable_missing" ? "executable" : "platform package";
1043
+ }
1044
+ function repairCommands(route = repairRoute(), spell = cabane) {
1045
+ if (route === "managed") return [spell("stop"), spell("update")];
1046
+ if (route === "script") return [spell("stop"), INSTALL_SCRIPT_COMMAND];
1047
+ return ["npm i -g @cabane/companion --include=optional"];
1048
+ }
1049
+ function repairAction(route = repairRoute(), spell = cabane) {
1050
+ const [first, ...rest] = repairCommands(route, spell);
1051
+ if (rest.length === 0) return `Reinstall with \`${first}\`.`;
1052
+ return `Run \`${first}\`, then \`${rest.join("`, then `")}\`. ${STOP_WARNING}`;
1053
+ }
1054
+ function repairSteps(route = repairRoute()) {
1055
+ return repairCommands(route).join(", then ");
1056
+ }
1057
+ function repairCost(route = repairRoute()) {
1058
+ return repairCommands(route).length > 1 ? STOP_WARNING : "";
1059
+ }
1060
+ function startupWarning(r) {
1061
+ const label = HARNESS_LABEL[r.runtime];
1062
+ return `${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but its installation is incomplete (${missingNoun(r.status)} is missing). It cannot run turns until repaired. ${repairAction()} Then restart the companion.`;
1063
+ }
1064
+ function harnessIssueNote(runtime, status2, route = repairRoute(), spell = cabane) {
1065
+ const intent = HARNESS_INTENT_WORD[runtime];
1066
+ const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
1067
+ return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status2)} is missing. ${repairAction(route, spell)} Then restart the companion.`;
1068
+ }
1069
+ function turnIncompleteCopy(runtime) {
1070
+ return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
1071
+ }
1072
+ var CLAUDE_SDK, CODEX_SDK, CODEX_CLI, CODEX_PACKAGE_BY_TRIPLE, HARNESS_LABEL, HARNESS_INTENT_WORD, STOP_WARNING;
1073
+ var init_bundled_binaries = __esm({
1074
+ "src/bundled-binaries.ts"() {
1075
+ "use strict";
1076
+ init_cabane_command();
1077
+ init_install_kind();
1078
+ CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
1079
+ CODEX_SDK = "@openai/codex-sdk";
1080
+ CODEX_CLI = "@openai/codex";
1081
+ CODEX_PACKAGE_BY_TRIPLE = {
1082
+ "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
1083
+ "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
1084
+ "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
1085
+ "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
1086
+ "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
1087
+ "aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
1088
+ };
1089
+ HARNESS_LABEL = {
1090
+ "claude-code": "Claude Code",
1091
+ codex: "Codex"
1092
+ };
1093
+ HARNESS_INTENT_WORD = {
1094
+ "claude-code": "connected",
1095
+ codex: "enabled"
1096
+ };
1097
+ STOP_WARNING = "Stopping ends any turn in progress.";
1098
+ }
1099
+ });
1100
+
1035
1101
  // packages/agent-runtime/src/failure.ts
1036
1102
  import { z as z4 } from "zod";
1037
1103
  function encodeFailureReason(failure) {
@@ -1302,6 +1368,7 @@ var init_api = __esm({
1302
1368
  "src/api.ts"() {
1303
1369
  "use strict";
1304
1370
  init_api_error_shape();
1371
+ init_cabane_command();
1305
1372
  init_errors();
1306
1373
  DEVICE_HEADER = "x-cabane-device";
1307
1374
  RETRY_BACKOFF_MS = [250, 750];
@@ -1436,7 +1503,7 @@ var init_api = __esm({
1436
1503
  }
1437
1504
  this.opts.log?.warn(
1438
1505
  { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
1439
- "Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
1506
+ `Cabane rejected a saved update; it could not be delivered. ${mayNeedUpdate()}`
1440
1507
  );
1441
1508
  outbox.remove(entry.turnId, entry.seq);
1442
1509
  progressed = true;
@@ -7503,10 +7570,10 @@ var init_turn_containment = __esm({
7503
7570
 
7504
7571
  // src/secrets.ts
7505
7572
  import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
7506
- import { join as join12 } from "path";
7573
+ import { join as join13 } from "path";
7507
7574
  import { z as z14 } from "zod";
7508
7575
  function secretsPath() {
7509
- return join12(cabaneDir(), "secrets.json");
7576
+ return join13(cabaneDir(), "secrets.json");
7510
7577
  }
7511
7578
  function loadSecretStore() {
7512
7579
  const path = secretsPath();
@@ -8556,10 +8623,10 @@ var init_build_options = __esm({
8556
8623
  // src/codex-instructions.ts
8557
8624
  import { mkdtemp, rm, writeFile } from "fs/promises";
8558
8625
  import { tmpdir } from "os";
8559
- import { join as join13 } from "path";
8626
+ import { join as join14 } from "path";
8560
8627
  async function writeCodexInstructionsFile(contents) {
8561
- const dir2 = await mkdtemp(join13(tmpdir(), PREFIX));
8562
- const path = join13(dir2, "instructions.md");
8628
+ const dir2 = await mkdtemp(join14(tmpdir(), PREFIX));
8629
+ const path = join14(dir2, "instructions.md");
8563
8630
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
8564
8631
  return {
8565
8632
  path,
@@ -8579,13 +8646,13 @@ var init_codex_instructions = __esm({
8579
8646
  // src/codex-images.ts
8580
8647
  import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
8581
8648
  import { tmpdir as tmpdir2 } from "os";
8582
- import { join as join14 } from "path";
8649
+ import { join as join15 } from "path";
8583
8650
  function createCodexImagePreparer(opts) {
8584
8651
  const doFetch = opts.fetch ?? fetch;
8585
8652
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
8586
8653
  return async (urls, signal) => {
8587
8654
  signal.throwIfAborted();
8588
- const dir2 = await mkdtemp2(join14(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
8655
+ const dir2 = await mkdtemp2(join15(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
8589
8656
  const cleanup = async () => {
8590
8657
  try {
8591
8658
  await rm2(dir2, { recursive: true, force: true });
@@ -8606,7 +8673,7 @@ function createCodexImagePreparer(opts) {
8606
8673
  const ext = EXTENSIONS[type];
8607
8674
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
8608
8675
  const bytes = new Uint8Array(await res.arrayBuffer());
8609
- const path = join14(dir2, `image-${index + 1}.${ext}`);
8676
+ const path = join15(dir2, `image-${index + 1}.${ext}`);
8610
8677
  await writeFile2(path, bytes, { mode: 384 });
8611
8678
  return path;
8612
8679
  } catch (err) {
@@ -8752,15 +8819,15 @@ var init_prepare_failure = __esm({
8752
8819
 
8753
8820
  // src/prepared.ts
8754
8821
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
8755
- import { join as join15 } from "path";
8822
+ import { join as join16 } from "path";
8756
8823
  function dirFor(workspaceId) {
8757
- return join15(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8824
+ return join16(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8758
8825
  }
8759
8826
  function conversationDir(workspaceId, conversationId) {
8760
- return join15(dirFor(workspaceId), encodeURIComponent(conversationId));
8827
+ return join16(dirFor(workspaceId), encodeURIComponent(conversationId));
8761
8828
  }
8762
8829
  function pathFor2(workspaceId, conversationId, agentId) {
8763
- return join15(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8830
+ return join16(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8764
8831
  }
8765
8832
  function readPrepared(workspaceId, conversationId, agentId) {
8766
8833
  const path = pathFor2(workspaceId, conversationId, agentId);
@@ -8798,9 +8865,9 @@ var init_prepared = __esm({
8798
8865
 
8799
8866
  // src/transcript-writer.ts
8800
8867
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8801
- import { basename, dirname as dirname6, join as join16 } from "path";
8868
+ import { basename as basename2, dirname as dirname8, join as join17 } from "path";
8802
8869
  function transcriptsDir() {
8803
- return join16(runnerStateDir(), "transcripts");
8870
+ return join17(runnerStateDir(), "transcripts");
8804
8871
  }
8805
8872
  function fileName(meta) {
8806
8873
  const d = new Date(meta.ts);
@@ -8826,7 +8893,7 @@ function pruneOld(dir2, retain) {
8826
8893
  const drop = files.sort().slice(0, files.length - retain);
8827
8894
  for (const f of drop) {
8828
8895
  try {
8829
- rmSync6(join16(dir2, f), { force: true });
8896
+ rmSync6(join17(dir2, f), { force: true });
8830
8897
  } catch {
8831
8898
  }
8832
8899
  }
@@ -8844,7 +8911,7 @@ var init_transcript_writer = __esm({
8844
8911
  onWarn;
8845
8912
  constructor(dir2, meta, onWarn) {
8846
8913
  this.onWarn = onWarn;
8847
- this.path = join16(dir2, fileName(meta));
8914
+ this.path = join17(dir2, fileName(meta));
8848
8915
  try {
8849
8916
  mkdirSync8(dir2, { recursive: true });
8850
8917
  try {
@@ -8873,10 +8940,10 @@ var init_transcript_writer = __esm({
8873
8940
  preserveAnomaly() {
8874
8941
  if (this.broken) return;
8875
8942
  try {
8876
- const dir2 = join16(dirname6(this.path), "anomalies");
8943
+ const dir2 = join17(dirname8(this.path), "anomalies");
8877
8944
  mkdirSync8(dir2, { recursive: true, mode: 448 });
8878
8945
  chmodSync4(dir2, 448);
8879
- const target = join16(dir2, basename(this.path));
8946
+ const target = join17(dir2, basename2(this.path));
8880
8947
  copyFileSync(this.path, target);
8881
8948
  chmodSync4(target, 384);
8882
8949
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -8906,6 +8973,7 @@ var TurnCommitter;
8906
8973
  var init_turn_committer = __esm({
8907
8974
  "src/turn-committer.ts"() {
8908
8975
  "use strict";
8976
+ init_cabane_command();
8909
8977
  init_src();
8910
8978
  init_api_error_shape();
8911
8979
  TurnCommitter = class {
@@ -8915,7 +8983,7 @@ var init_turn_committer = __esm({
8915
8983
  this.onError = (err, hook) => {
8916
8984
  deps.log.warn(
8917
8985
  { ...apiErrorLogFields(err), hook },
8918
- `Couldn't save ${String(deps.log.bindings().agentName ?? "the agent")}'s ${hook === "text" ? "reply" : "reply or activity"} to Cabane. This companion may need an update: npm i -g @cabane/companion@latest`
8986
+ `Couldn't save ${String(deps.log.bindings().agentName ?? "the agent")}'s ${hook === "text" ? "reply" : "reply or activity"} to Cabane. ${mayNeedUpdate()}`
8919
8987
  );
8920
8988
  deps.onCommitFailed?.(err);
8921
8989
  };
@@ -9057,7 +9125,7 @@ async function postIncompleteNotice(ctx2, harness, resolution) {
9057
9125
  } catch (err) {
9058
9126
  ctx2.log.warn(
9059
9127
  { ...apiErrorLogFields(err) },
9060
- "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9128
+ `Couldn't save the explanation to Cabane. ${mayNeedUpdate()}`
9061
9129
  );
9062
9130
  return false;
9063
9131
  }
@@ -9119,6 +9187,7 @@ var init_turn_runtime_integrity = __esm({
9119
9187
  "src/turn-runtime-integrity.ts"() {
9120
9188
  "use strict";
9121
9189
  init_api_error_shape();
9190
+ init_cabane_command();
9122
9191
  init_bundled_binaries();
9123
9192
  }
9124
9193
  });
@@ -9167,6 +9236,7 @@ var init_turn_execution = __esm({
9167
9236
  "src/turn-execution.ts"() {
9168
9237
  "use strict";
9169
9238
  init_term();
9239
+ init_cabane_command();
9170
9240
  init_boot_id();
9171
9241
  init_src();
9172
9242
  init_build_options();
@@ -9405,7 +9475,7 @@ var init_turn_execution = __esm({
9405
9475
  } else {
9406
9476
  turnLog.warn(
9407
9477
  { ...apiErrorLogFields(err), turnId },
9408
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9478
+ `Couldn't save the turn's result to Cabane. ${mayNeedUpdate()}`
9409
9479
  );
9410
9480
  }
9411
9481
  }
@@ -9883,7 +9953,7 @@ var init_turn_execution = __esm({
9883
9953
  } catch (postErr) {
9884
9954
  turnLog.warn(
9885
9955
  { ...apiErrorLogFields(postErr) },
9886
- "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9956
+ `Couldn't save the explanation to Cabane. ${mayNeedUpdate()}`
9887
9957
  );
9888
9958
  const reason = `runtime_unavailable:${err.runtime}`;
9889
9959
  throw this.concluded(reason, reason);
@@ -10092,7 +10162,7 @@ var init_turn_execution = __esm({
10092
10162
  stateLength: event.state.length,
10093
10163
  runtime: turnRuntime
10094
10164
  },
10095
- "Couldn't save the agent's memory in Cabane. The next turn will start a new session. This companion may need an update: npm i -g @cabane/companion@latest"
10165
+ `Couldn't save the agent's memory in Cabane. The next turn will start a new session. ${mayNeedUpdate()}`
10096
10166
  );
10097
10167
  } else {
10098
10168
  turnLog.warn(
@@ -10256,7 +10326,7 @@ var init_turn_execution = __esm({
10256
10326
  } else {
10257
10327
  turnLog.warn(
10258
10328
  { ...apiErrorLogFields(err) },
10259
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
10329
+ `Couldn't save the turn's result to Cabane. ${mayNeedUpdate()}`
10260
10330
  );
10261
10331
  }
10262
10332
  }
@@ -10310,7 +10380,7 @@ var init_turn_execution = __esm({
10310
10380
  // src/dispatcher.ts
10311
10381
  import { randomUUID as randomUUID4 } from "crypto";
10312
10382
  import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
10313
- import { join as join17 } from "path";
10383
+ import { join as join18 } from "path";
10314
10384
  function checkoutState(cwd) {
10315
10385
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
10316
10386
  if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
@@ -10323,7 +10393,7 @@ function checkoutState(cwd) {
10323
10393
  if (entries.length === 0) {
10324
10394
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10325
10395
  }
10326
- const gitPath = join17(cwd, ".git");
10396
+ const gitPath = join18(cwd, ".git");
10327
10397
  if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
10328
10398
  let stat;
10329
10399
  try {
@@ -10331,7 +10401,7 @@ function checkoutState(cwd) {
10331
10401
  } catch (error) {
10332
10402
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10333
10403
  }
10334
- if (stat.isDirectory() && !existsSync11(join17(gitPath, "HEAD")))
10404
+ if (stat.isDirectory() && !existsSync11(join18(gitPath, "HEAD")))
10335
10405
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10336
10406
  return { ok: true, reason: "usable" };
10337
10407
  }
@@ -10703,15 +10773,15 @@ import {
10703
10773
  mkdirSync as mkdirSync11,
10704
10774
  mkdtempSync,
10705
10775
  readFileSync as readFileSync10,
10706
- realpathSync as realpathSync4,
10776
+ realpathSync as realpathSync5,
10707
10777
  rmSync as rmSync10,
10708
10778
  writeFileSync as writeFileSync7
10709
10779
  } from "fs";
10710
10780
  import { createRequire as createRequire2 } from "module";
10711
10781
  import { isIP } from "net";
10712
10782
  import { homedir as homedir6, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
10713
- import { dirname as dirname8, join as join21, sep as sep4 } from "path";
10714
- import { fileURLToPath as fileURLToPath5 } from "url";
10783
+ import { dirname as dirname10, join as join22, sep as sep4 } from "path";
10784
+ import { fileURLToPath as fileURLToPath6 } from "url";
10715
10785
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
10716
10786
  function accountHome() {
10717
10787
  try {
@@ -10742,23 +10812,23 @@ async function resolvedAddresses(baseUrl) {
10742
10812
  }
10743
10813
  }
10744
10814
  function companionPackageRoot(modulePath) {
10745
- for (let dir2 = dirname8(modulePath); ; dir2 = dirname8(dir2)) {
10815
+ for (let dir2 = dirname10(modulePath); ; dir2 = dirname10(dir2)) {
10746
10816
  try {
10747
- const manifest = JSON.parse(readFileSync10(join21(dir2, "package.json"), "utf8"));
10817
+ const manifest = JSON.parse(readFileSync10(join22(dir2, "package.json"), "utf8"));
10748
10818
  if (manifest.name === "@cabane/companion") return dir2;
10749
10819
  } catch {
10750
10820
  }
10751
- if (dirname8(dir2) === dir2) return null;
10821
+ if (dirname10(dir2) === dir2) return null;
10752
10822
  }
10753
10823
  }
10754
10824
  function harnessRuntimePaths() {
10755
- const paths = [dirname8(dirname8(process.execPath))];
10756
- const root = companionPackageRoot(fileURLToPath5(import.meta.url));
10825
+ const paths = [dirname10(dirname10(process.execPath))];
10826
+ const root = companionPackageRoot(fileURLToPath6(import.meta.url));
10757
10827
  if (root) paths.push(root);
10758
10828
  try {
10759
10829
  const require2 = createRequire2(import.meta.url);
10760
10830
  paths.push(
10761
- dirname8(realpathSync4(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
10831
+ dirname10(realpathSync5(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
10762
10832
  );
10763
10833
  } catch {
10764
10834
  }
@@ -10766,7 +10836,7 @@ function harnessRuntimePaths() {
10766
10836
  const bundled = resolveBundledBinary(runtime).path;
10767
10837
  if (bundled) {
10768
10838
  try {
10769
- paths.push(dirname8(realpathSync4(bundled)));
10839
+ paths.push(dirname10(realpathSync5(bundled)));
10770
10840
  } catch {
10771
10841
  }
10772
10842
  }
@@ -10777,9 +10847,9 @@ function ownAddresses() {
10777
10847
  return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
10778
10848
  }
10779
10849
  function storeWalkRoots(homes, cabaneDir2) {
10780
- const companionHome = dirname8(cabaneDir2);
10781
- const parent = dirname8(companionHome);
10782
- const beside = homes.includes(companionHome) || dirname8(parent) === parent ? [] : [parent];
10850
+ const companionHome = dirname10(cabaneDir2);
10851
+ const parent = dirname10(companionHome);
10852
+ const beside = homes.includes(companionHome) || dirname10(parent) === parent ? [] : [parent];
10783
10853
  const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
10784
10854
  return candidates.filter(
10785
10855
  (root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
@@ -10817,19 +10887,19 @@ async function walkCompanionStores(roots, opts = {}) {
10817
10887
  }
10818
10888
  function companionStoreDenies(input) {
10819
10889
  const { homes, cabaneDir: cabaneDir2, foundStores } = input;
10820
- const parent = dirname8(dirname8(cabaneDir2));
10890
+ const parent = dirname10(dirname10(cabaneDir2));
10821
10891
  const always = [
10822
10892
  cabaneDir2,
10823
10893
  ...homes.flatMap((home) => [
10824
- join21(home, ".cabane"),
10825
- join21(home, "*", ".cabane"),
10826
- join21(home, "*", "*", ".cabane")
10894
+ join22(home, ".cabane"),
10895
+ join22(home, "*", ".cabane"),
10896
+ join22(home, "*", "*", ".cabane")
10827
10897
  ]),
10828
10898
  // A glob anchored at the root has no directory to start from.
10829
- ...dirname8(parent) === parent ? [] : [join21(parent, "*", ".cabane")]
10899
+ ...dirname10(parent) === parent ? [] : [join22(parent, "*", ".cabane")]
10830
10900
  ];
10831
- const companionHome = dirname8(cabaneDir2);
10832
- const otherLogins = foundStores.map((store) => dirname8(store)).filter((home) => home !== companionHome).flatMap((home) => [join21(home, ".claude"), join21(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
10901
+ const companionHome = dirname10(cabaneDir2);
10902
+ const otherLogins = foundStores.map((store) => dirname10(store)).filter((home) => home !== companionHome).flatMap((home) => [join22(home, ".claude"), join22(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
10833
10903
  return [.../* @__PURE__ */ new Set([...always, ...foundStores, ...otherLogins])];
10834
10904
  }
10835
10905
  function companionStoreWatch(log) {
@@ -10846,7 +10916,7 @@ function isAtOrUnder(path, dir2) {
10846
10916
  }
10847
10917
  function expandHome(path, home) {
10848
10918
  if (path === "~") return home;
10849
- return path.startsWith("~/") ? join21(home, path.slice(2)) : path;
10919
+ return path.startsWith("~/") ? join22(home, path.slice(2)) : path;
10850
10920
  }
10851
10921
  function stripPort(pattern) {
10852
10922
  return pattern.startsWith("[") ? pattern.replace(/^\[([^\]]+)\](:\d+)?$/, "$1") : pattern.replace(/:\d+$/, "");
@@ -10864,8 +10934,8 @@ function patternsOverlap(a, b) {
10864
10934
  }
10865
10935
  function harnessSandboxConfig(input) {
10866
10936
  const { policy } = input;
10867
- const claudeDir = input.claudeConfigDir ?? join21(input.home, ".claude");
10868
- const codexDir = input.codexHome ?? join21(input.home, ".codex");
10937
+ const claudeDir = input.claudeConfigDir ?? join22(input.home, ".claude");
10938
+ const codexDir = input.codexHome ?? join22(input.home, ".codex");
10869
10939
  const homes = [.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])];
10870
10940
  const expand = (paths) => (paths ?? []).map((p) => expandHome(p, input.home));
10871
10941
  const cabane2 = cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses);
@@ -10882,7 +10952,7 @@ function harnessSandboxConfig(input) {
10882
10952
  foundStores: input.foundStores,
10883
10953
  ownState: [claudeDir, codexDir]
10884
10954
  }),
10885
- ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join21(home, p))),
10955
+ ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join22(home, p))),
10886
10956
  ...expand(policy.filesystem?.denyRead)
10887
10957
  ];
10888
10958
  const strict = policy.filesystem?.mode === "strict";
@@ -10890,7 +10960,7 @@ function harnessSandboxConfig(input) {
10890
10960
  const allowRead = strict ? [
10891
10961
  ...input.cwd ? [input.cwd] : [],
10892
10962
  claudeDir,
10893
- join21(input.home, ".claude.json"),
10963
+ join22(input.home, ".claude.json"),
10894
10964
  codexDir,
10895
10965
  ...input.runtimePaths ?? [],
10896
10966
  ...expand(policy.filesystem?.allowRead)
@@ -10930,7 +11000,7 @@ function harnessSandboxConfig(input) {
10930
11000
  ...input.cwd ? [input.cwd] : [],
10931
11001
  input.tmp,
10932
11002
  claudeDir,
10933
- join21(input.home, ".claude.json"),
11003
+ join22(input.home, ".claude.json"),
10934
11004
  codexDir,
10935
11005
  ...expand(policy.filesystem?.allowWrite)
10936
11006
  ],
@@ -11032,9 +11102,9 @@ async function prepareHarnessSandbox(input) {
11032
11102
  } finally {
11033
11103
  for (const name of held) delete process.env[name];
11034
11104
  }
11035
- mkdirSync11(join21(cabaneDir(), "sandbox"), { recursive: true });
11036
- const dir2 = mkdtempSync(join21(cabaneDir(), "sandbox", "turn-"));
11037
- const script = join21(dir2, "exec-sandboxed");
11105
+ mkdirSync11(join22(cabaneDir(), "sandbox"), { recursive: true });
11106
+ const dir2 = mkdtempSync(join22(cabaneDir(), "sandbox", "turn-"));
11107
+ const script = join22(dir2, "exec-sandboxed");
11038
11108
  writeFileSync7(
11039
11109
  script,
11040
11110
  [
@@ -11050,7 +11120,7 @@ async function prepareHarnessSandbox(input) {
11050
11120
  return {
11051
11121
  wrap: (command, args) => ({ command: script, args: [command, ...args] }),
11052
11122
  executableFor: (real) => {
11053
- const shim = join21(dir2, `exec-${++shims}`);
11123
+ const shim = join22(dir2, `exec-${++shims}`);
11054
11124
  writeFileSync7(shim, `#!/bin/bash
11055
11125
  exec ${shellQuote(script)} ${shellQuote(real)} "$@"
11056
11126
  `);
@@ -11143,7 +11213,7 @@ import {
11143
11213
  rmSync as rmSync11,
11144
11214
  writeFileSync as writeFileSync8
11145
11215
  } from "fs";
11146
- import { join as join22 } from "path";
11216
+ import { join as join23 } from "path";
11147
11217
  var MAX_ENTRIES, MAX_AGE_MS, Outbox;
11148
11218
  var init_outbox = __esm({
11149
11219
  "src/outbox.ts"() {
@@ -11161,10 +11231,10 @@ var init_outbox = __esm({
11161
11231
  // Resolved lazily (per call) so tests that swap HOME between
11162
11232
  // cases route writes at the right tmpdir.
11163
11233
  dir() {
11164
- return join22(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11234
+ return join23(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11165
11235
  }
11166
11236
  fileFor(turnId, seq) {
11167
- return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11237
+ return join23(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11168
11238
  }
11169
11239
  // Persist a commit for later draining. Atomic (temp file + rename) so a
11170
11240
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -11206,7 +11276,7 @@ var init_outbox = __esm({
11206
11276
  const entries = [];
11207
11277
  for (const name of names) {
11208
11278
  if (!name.endsWith(".json")) continue;
11209
- const full = join22(dir2, name);
11279
+ const full = join23(dir2, name);
11210
11280
  try {
11211
11281
  const parsed = JSON.parse(readFileSync11(full, "utf8"));
11212
11282
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -11346,7 +11416,7 @@ var init_image_manifest = __esm({
11346
11416
 
11347
11417
  // src/runner/exec-shim.ts
11348
11418
  import { chmodSync as chmodSync8, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync2, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "fs";
11349
- import { join as join31 } from "path";
11419
+ import { join as join32 } from "path";
11350
11420
  function shellQuote2(value) {
11351
11421
  return `'${value.replace(/'/g, `'\\''`)}'`;
11352
11422
  }
@@ -11356,10 +11426,10 @@ function createShimDir() {
11356
11426
  return {
11357
11427
  write(argv) {
11358
11428
  if (!dir2) {
11359
- mkdirSync20(join31(cabaneDir(), "runner"), { recursive: true });
11360
- dir2 = mkdtempSync2(join31(cabaneDir(), "runner", "turn-"));
11429
+ mkdirSync20(join32(cabaneDir(), "runner"), { recursive: true });
11430
+ dir2 = mkdtempSync2(join32(cabaneDir(), "runner", "turn-"));
11361
11431
  }
11362
- const path = join31(dir2, `exec-${++count}`);
11432
+ const path = join32(dir2, `exec-${++count}`);
11363
11433
  writeFileSync13(path, `#!/bin/sh
11364
11434
  exec ${argv.map(shellQuote2).join(" ")} "$@"
11365
11435
  `);
@@ -11716,13 +11786,13 @@ init_config();
11716
11786
  import { createHash } from "crypto";
11717
11787
  import { chmodSync as chmodSync2, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
11718
11788
  import { createServer, connect } from "net";
11719
- import { join as join5 } from "path";
11789
+ import { join as join6 } from "path";
11720
11790
 
11721
11791
  // src/harness-copy.ts
11722
11792
  init_bundled_binaries();
11723
11793
  init_cabane_command();
11724
11794
  import { homedir as homedir5 } from "os";
11725
- import { join as join4 } from "path";
11795
+ import { join as join5 } from "path";
11726
11796
 
11727
11797
  // src/harness-status.ts
11728
11798
  init_bundled_binaries();
@@ -11883,7 +11953,12 @@ function runtimeIssuesFor(gates) {
11883
11953
  for (const name of ["claude-code", "codex"]) {
11884
11954
  const gate = name === "codex" ? gates.codex : gates.claudeCode;
11885
11955
  if (!gate.intended || gate.bundled === "present") continue;
11886
- issues.push({ name, reason: gate.bundled, install: gates.install });
11956
+ issues.push({
11957
+ name,
11958
+ reason: gate.bundled,
11959
+ install: gates.install,
11960
+ ...gates.managed ? { managed: true } : {}
11961
+ });
11887
11962
  }
11888
11963
  return issues;
11889
11964
  }
@@ -12152,16 +12227,16 @@ function failureText(runtime, reason) {
12152
12227
  case "timeout":
12153
12228
  return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
12154
12229
  case "spawn":
12155
- return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairCommands()[0]} , then try again.`;
12230
+ return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
12156
12231
  case "config":
12157
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${join4(homedir5(), ".codex", "config.toml").replace(homedir5(), "~")})` : ""}. Fix it, then try again.`;
12232
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${join5(homedir5(), ".codex", "config.toml").replace(homedir5(), "~")})` : ""}. Fix it, then try again.`;
12158
12233
  case "unreadable":
12159
12234
  return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
12160
12235
  }
12161
12236
  }
12162
12237
  function bundleMissingText(runtime) {
12163
12238
  const label = HARNESS_LABELS[runtime];
12164
- return `This Cabane install is missing its bundled ${label}. Repair it with ${repairCommands()[0]} , then try again.`;
12239
+ return `This Cabane install is missing its bundled ${label}. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
12165
12240
  }
12166
12241
  function unusableReason(runtime, check) {
12167
12242
  if (check.status === "bundle_missing") return bundleMissingText(runtime);
@@ -12235,6 +12310,9 @@ function connectLines(outcome, opts = {}) {
12235
12310
  ]
12236
12311
  };
12237
12312
  }
12313
+ function spaced(sentence) {
12314
+ return sentence ? ` ${sentence}` : "";
12315
+ }
12238
12316
 
12239
12317
  // src/control-socket.ts
12240
12318
  var CONTROL_TIMEOUT_MS = 1e3;
@@ -12245,11 +12323,11 @@ function controlSocketPath() {
12245
12323
  if (process.platform === "win32") {
12246
12324
  return `\\\\.\\pipe\\cabane-companion-${key}`;
12247
12325
  }
12248
- const besideRuntime = join5(dir2, "companion.sock");
12326
+ const besideRuntime = join6(dir2, "companion.sock");
12249
12327
  if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
12250
12328
  const filename = `cabane-companion-${key}.sock`;
12251
- const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join5(process.env.XDG_RUNTIME_DIR, filename) : null;
12252
- return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join5("/tmp", filename);
12329
+ const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join6(process.env.XDG_RUNTIME_DIR, filename) : null;
12330
+ return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join6("/tmp", filename);
12253
12331
  }
12254
12332
  async function startControlServer(handlers) {
12255
12333
  const path = controlSocketPath();
@@ -12424,10 +12502,10 @@ import {
12424
12502
  openSync,
12425
12503
  closeSync
12426
12504
  } from "fs";
12427
- import { join as join6 } from "path";
12505
+ import { join as join7 } from "path";
12428
12506
  var PROBE_TIMEOUT_MS2 = 1e3;
12429
12507
  function runtimePath() {
12430
- return join6(cabaneDir(), "runtime.json");
12508
+ return join7(cabaneDir(), "runtime.json");
12431
12509
  }
12432
12510
  function serialize(state) {
12433
12511
  return JSON.stringify(state, null, 2) + "\n";
@@ -12642,10 +12720,10 @@ import {
12642
12720
  rmSync as rmSync4,
12643
12721
  writeFileSync as writeFileSync3
12644
12722
  } from "fs";
12645
- import { dirname as dirname3, join as join7 } from "path";
12723
+ import { dirname as dirname5, join as join8 } from "path";
12646
12724
  import { z as z3 } from "zod";
12647
12725
  function credentialsPath() {
12648
- return join7(cabaneDir(), "credentials.json");
12726
+ return join8(cabaneDir(), "credentials.json");
12649
12727
  }
12650
12728
  var credentialStoreSchema = z3.record(z3.string(), z3.string());
12651
12729
  function load() {
@@ -12667,7 +12745,7 @@ function load() {
12667
12745
  }
12668
12746
  function save(map) {
12669
12747
  const path = credentialsPath();
12670
- mkdirSync4(dirname3(path), { recursive: true });
12748
+ mkdirSync4(dirname5(path), { recursive: true });
12671
12749
  try {
12672
12750
  chmodSync3(cabaneDir(), 448);
12673
12751
  } catch {
@@ -12962,11 +13040,11 @@ import {
12962
13040
  renameSync as renameSync3,
12963
13041
  statSync as statSync2
12964
13042
  } from "fs";
12965
- import { dirname as dirname4, join as join8 } from "path";
13043
+ import { dirname as dirname6, join as join9 } from "path";
12966
13044
  import { stripVTControlCharacters } from "util";
12967
13045
  import pino from "pino";
12968
13046
  function companionLogPath() {
12969
- return join8(cabaneDir(), "companion.log");
13047
+ return join9(cabaneDir(), "companion.log");
12970
13048
  }
12971
13049
  function humanText(value) {
12972
13050
  return stripVTControlCharacters(String(value ?? "")).replace(
@@ -13080,7 +13158,7 @@ function configureLogger(log, config) {
13080
13158
  }
13081
13159
  function createLogger(destinations = {}, options = {}) {
13082
13160
  const path = companionLogPath();
13083
- if (!destinations.file) mkdirSync5(dirname4(path), { recursive: true });
13161
+ if (!destinations.file) mkdirSync5(dirname6(path), { recursive: true });
13084
13162
  const file = destinations.file ?? new RotatingLog(path);
13085
13163
  const state = { format: options.logFormat ?? "human" };
13086
13164
  const streams = [];
@@ -13133,15 +13211,15 @@ import { randomUUID as randomUUID7 } from "crypto";
13133
13211
 
13134
13212
  // src/dashboard/server.ts
13135
13213
  init_errors();
13136
- import { dirname as dirname5, join as join10 } from "path";
13137
- import { fileURLToPath as fileURLToPath3 } from "url";
13214
+ import { dirname as dirname7, join as join11 } from "path";
13215
+ import { fileURLToPath as fileURLToPath4 } from "url";
13138
13216
  import { serve } from "@hono/node-server";
13139
13217
  import { Hono } from "hono";
13140
13218
 
13141
13219
  // src/dashboard/routes.ts
13142
13220
  import { openSync as openSync3, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync6 } from "fs";
13143
13221
  import { readFile } from "fs/promises";
13144
- import { extname, join as join9, normalize } from "path";
13222
+ import { extname, join as join10, normalize } from "path";
13145
13223
  import { streamSSE } from "hono/streaming";
13146
13224
 
13147
13225
  // src/state.ts
@@ -13390,13 +13468,13 @@ var CONTENT_TYPES = {
13390
13468
  function registerRoutes(app, deps) {
13391
13469
  const { supervisor, hub, staticDir } = deps;
13392
13470
  app.get("/", async (c) => {
13393
- const html = await readFile(join9(staticDir, "index.html"), "utf8");
13471
+ const html = await readFile(join10(staticDir, "index.html"), "utf8");
13394
13472
  return c.html(html);
13395
13473
  });
13396
13474
  app.get("/static/:file", async (c) => {
13397
13475
  const file = c.req.param("file");
13398
13476
  const safe2 = normalize(file).replace(/^(\.\.[/\\])+/, "");
13399
- const full = join9(staticDir, safe2);
13477
+ const full = join10(staticDir, safe2);
13400
13478
  if (!full.startsWith(staticDir) || !existsSync6(full)) return c.notFound();
13401
13479
  const body = await readFile(full);
13402
13480
  const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
@@ -13608,7 +13686,7 @@ function isAddrInUse(err) {
13608
13686
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
13609
13687
  }
13610
13688
  function resolveStaticDir() {
13611
- return join10(dirname5(fileURLToPath3(import.meta.url)), "static");
13689
+ return join11(dirname7(fileURLToPath4(import.meta.url)), "static");
13612
13690
  }
13613
13691
 
13614
13692
  // src/supervisor.ts
@@ -13617,6 +13695,7 @@ import { hostname as hostname2 } from "os";
13617
13695
 
13618
13696
  // src/log-copy.ts
13619
13697
  init_failure();
13698
+ init_bundled_binaries();
13620
13699
  function turnFailureCopy(reason, runtime) {
13621
13700
  const failure = decodeFailureReason(reason);
13622
13701
  if (failure?.kind === "auth_expired") {
@@ -13635,7 +13714,7 @@ function turnFailureCopy(reason, runtime) {
13635
13714
  if (reason?.startsWith("missing_secret:"))
13636
13715
  return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
13637
13716
  if (reason?.startsWith("runtime_incomplete:"))
13638
- return "The agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
13717
+ return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
13639
13718
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
13640
13719
  return "The agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
13641
13720
  if (reason?.startsWith("model_unavailable:"))
@@ -13775,13 +13854,13 @@ init_config();
13775
13854
  // src/dispatch-dedupe.ts
13776
13855
  init_config();
13777
13856
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
13778
- import { join as join11 } from "path";
13857
+ import { join as join12 } from "path";
13779
13858
  var MAX_IDS = 256;
13780
13859
  function dir(log) {
13781
- return join11(cabaneDir(), log);
13860
+ return join12(cabaneDir(), log);
13782
13861
  }
13783
13862
  function pathFor(log, workspaceId) {
13784
- return join11(dir(log), encodeURIComponent(workspaceId));
13863
+ return join12(dir(log), encodeURIComponent(workspaceId));
13785
13864
  }
13786
13865
  function readIds(log, workspaceId) {
13787
13866
  const path = pathFor(log, workspaceId);
@@ -13818,10 +13897,10 @@ function markCompleted(workspaceId, eventId) {
13818
13897
  }
13819
13898
  var MAX_RESUME_ATTEMPTS = 3;
13820
13899
  function resumeDir() {
13821
- return join11(cabaneDir(), "resume-attempts");
13900
+ return join12(cabaneDir(), "resume-attempts");
13822
13901
  }
13823
13902
  function resumePathFor(workspaceId) {
13824
- return join11(resumeDir(), encodeURIComponent(workspaceId));
13903
+ return join12(resumeDir(), encodeURIComponent(workspaceId));
13825
13904
  }
13826
13905
  function readResumeCounts(workspaceId) {
13827
13906
  const out = /* @__PURE__ */ new Map();
@@ -13857,10 +13936,10 @@ function bumpResumeAttempt(workspaceId, eventId) {
13857
13936
  return next;
13858
13937
  }
13859
13938
  function turnDir() {
13860
- return join11(cabaneDir(), "turns");
13939
+ return join12(cabaneDir(), "turns");
13861
13940
  }
13862
13941
  function turnPathFor(workspaceId) {
13863
- return join11(turnDir(), encodeURIComponent(workspaceId));
13942
+ return join12(turnDir(), encodeURIComponent(workspaceId));
13864
13943
  }
13865
13944
  var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13866
13945
  function readTurnIds(workspaceId) {
@@ -13912,6 +13991,7 @@ function noResume() {
13912
13991
 
13913
13992
  // src/supervisor.ts
13914
13993
  init_bundled_binaries();
13994
+ init_install_kind();
13915
13995
 
13916
13996
  // src/connector-health.ts
13917
13997
  init_src();
@@ -14093,7 +14173,7 @@ import {
14093
14173
  statSync as statSync4,
14094
14174
  writeFileSync as writeFileSync6
14095
14175
  } from "fs";
14096
- import { join as join20 } from "path";
14176
+ import { join as join21 } from "path";
14097
14177
  import { Writable } from "stream";
14098
14178
 
14099
14179
  // src/providers/docker-cli.ts
@@ -14166,7 +14246,7 @@ import {
14166
14246
  renameSync as renameSync5,
14167
14247
  rmSync as rmSync8
14168
14248
  } from "fs";
14169
- import { join as join18 } from "path";
14249
+ import { join as join19 } from "path";
14170
14250
  function routes(t) {
14171
14251
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
14172
14252
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -14212,7 +14292,7 @@ function publishStaged(from, to, move = atomicMove) {
14212
14292
  const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
14213
14293
  let entry = {};
14214
14294
  try {
14215
- entry = JSON.parse(readFileSync8(join18(from, name), "utf8"));
14295
+ entry = JSON.parse(readFileSync8(join19(from, name), "utf8"));
14216
14296
  } catch {
14217
14297
  }
14218
14298
  return { name, entry };
@@ -14225,7 +14305,7 @@ function publishStaged(from, to, move = atomicMove) {
14225
14305
  let published = 0;
14226
14306
  for (const { name } of staged) {
14227
14307
  try {
14228
- move(join18(from, name), join18(to, name));
14308
+ move(join19(from, name), join19(to, name));
14229
14309
  } catch {
14230
14310
  return { published, complete: false };
14231
14311
  }
@@ -14237,9 +14317,9 @@ function publishStaged(from, to, move = atomicMove) {
14237
14317
 
14238
14318
  // src/providers/runner-base.ts
14239
14319
  import { createHash as createHash3 } from "crypto";
14240
- import { existsSync as existsSync13, realpathSync as realpathSync3 } from "fs";
14241
- import { dirname as dirname7, join as join19, sep as sep3 } from "path";
14242
- import { fileURLToPath as fileURLToPath4 } from "url";
14320
+ import { existsSync as existsSync13, realpathSync as realpathSync4 } from "fs";
14321
+ import { dirname as dirname9, join as join20, sep as sep3 } from "path";
14322
+ import { fileURLToPath as fileURLToPath5 } from "url";
14243
14323
  var COMPANION_MOUNT = "/opt/cabane/companion";
14244
14324
  var BASE_DOCKERFILE = `FROM node:22-slim
14245
14325
  RUN apt-get update \\
@@ -14283,17 +14363,17 @@ async function ensureBaseImage(docker, image, log) {
14283
14363
  }
14284
14364
  return true;
14285
14365
  }
14286
- function ownInstall(moduleFile = fileURLToPath4(import.meta.url)) {
14287
- let dir2 = dirname7(moduleFile);
14366
+ function ownInstall(moduleFile = fileURLToPath5(import.meta.url)) {
14367
+ let dir2 = dirname9(moduleFile);
14288
14368
  for (let i = 0; i < 4; i++) {
14289
- if (existsSync13(join19(dir2, "dist", "cli.js")) && existsSync13(join19(dir2, "package.json"))) break;
14290
- dir2 = dirname7(dir2);
14369
+ if (existsSync13(join20(dir2, "dist", "cli.js")) && existsSync13(join20(dir2, "package.json"))) break;
14370
+ dir2 = dirname9(dir2);
14291
14371
  }
14292
- if (!existsSync13(join19(dir2, "dist", "cli.js"))) return null;
14293
- const sdk = join19(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
14372
+ if (!existsSync13(join20(dir2, "dist", "cli.js"))) return null;
14373
+ const sdk = join20(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
14294
14374
  if (!existsSync13(sdk)) return null;
14295
- const root = realpathSync3(dir2);
14296
- return realpathSync3(sdk).startsWith(root + sep3) ? root : null;
14375
+ const root = realpathSync4(dir2);
14376
+ return realpathSync4(sdk).startsWith(root + sep3) ? root : null;
14297
14377
  }
14298
14378
  function companionSource(input) {
14299
14379
  if (input.configured) {
@@ -14429,39 +14509,39 @@ function networkCreateError(stderr) {
14429
14509
  }
14430
14510
  function sessionArchivePath(deviceDir, attemptId) {
14431
14511
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
14432
- return join20(deviceDir, "attempts", attemptId, "session.tar");
14512
+ return join21(deviceDir, "attempts", attemptId, "session.tar");
14433
14513
  }
14434
14514
  function pruneOtherAttempts(deviceDir, pair2, keep) {
14435
- const root = join20(deviceDir, "attempts");
14515
+ const root = join21(deviceDir, "attempts");
14436
14516
  if (!existsSync14(root)) return;
14437
14517
  for (const id of readdirSync4(root)) {
14438
14518
  if (id === keep || !UUID_RE.test(id)) continue;
14439
14519
  let pairOf = "";
14440
14520
  try {
14441
- pairOf = readFileSync9(join20(root, id, "pair"), "utf8");
14521
+ pairOf = readFileSync9(join21(root, id, "pair"), "utf8");
14442
14522
  } catch {
14443
14523
  continue;
14444
14524
  }
14445
14525
  if (pairOf === `${pair2.conversationId}/${pair2.agentId}`) {
14446
- rmSync9(join20(root, id), { recursive: true, force: true });
14526
+ rmSync9(join21(root, id), { recursive: true, force: true });
14447
14527
  }
14448
14528
  }
14449
14529
  }
14450
14530
  var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
14451
14531
  function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
14452
- const root = join20(deviceDir, "attempts");
14532
+ const root = join21(deviceDir, "attempts");
14453
14533
  if (!existsSync14(root)) return [];
14454
14534
  const removed = [];
14455
14535
  for (const id of readdirSync4(root)) {
14456
14536
  if (!UUID_RE.test(id)) continue;
14457
14537
  let kept;
14458
14538
  try {
14459
- kept = statSync4(join20(root, id)).mtimeMs;
14539
+ kept = statSync4(join21(root, id)).mtimeMs;
14460
14540
  } catch {
14461
14541
  continue;
14462
14542
  }
14463
14543
  if (now - kept < maxAgeMs) continue;
14464
- rmSync9(join20(root, id), { recursive: true, force: true });
14544
+ rmSync9(join21(root, id), { recursive: true, force: true });
14465
14545
  removed.push(id);
14466
14546
  }
14467
14547
  return removed;
@@ -14662,11 +14742,11 @@ function createDockerProvider(opts) {
14662
14742
  await must(["network", "disconnect", "-f", other, name], "moving the container");
14663
14743
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
14664
14744
  };
14665
- const socketRoot = opts.socketRoot ?? join20(
14745
+ const socketRoot = opts.socketRoot ?? join21(
14666
14746
  process.env.XDG_RUNTIME_DIR && existsSync14(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
14667
14747
  `cabane-runners-${opts.deviceKey}`
14668
14748
  );
14669
- const socketDir = (name) => join20(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
14749
+ const socketDir = (name) => join21(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
14670
14750
  const create = async (name, attempt) => {
14671
14751
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
14672
14752
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
@@ -14808,7 +14888,7 @@ function createDockerProvider(opts) {
14808
14888
  }
14809
14889
  };
14810
14890
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
14811
- const pendingPath = (name) => join20(opts.deviceDir, "runners-pending", `${name}.json`);
14891
+ const pendingPath = (name) => join21(opts.deviceDir, "runners-pending", `${name}.json`);
14812
14892
  const readPending = (name) => {
14813
14893
  try {
14814
14894
  const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
@@ -14822,12 +14902,12 @@ function createDockerProvider(opts) {
14822
14902
  rmSync9(pendingPath(name), { force: true });
14823
14903
  return;
14824
14904
  }
14825
- mkdirSync10(join20(opts.deviceDir, "runners-pending"), { recursive: true });
14905
+ mkdirSync10(join21(opts.deviceDir, "runners-pending"), { recursive: true });
14826
14906
  writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
14827
14907
  };
14828
14908
  const keepSession = async (name, attempt, attemptId) => {
14829
14909
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
14830
- mkdirSync10(join20(archive, ".."), { recursive: true });
14910
+ mkdirSync10(join21(archive, ".."), { recursive: true });
14831
14911
  const partial = `${archive}.${process.pid}.partial`;
14832
14912
  const file = new CappedFile(partial, sessionCap);
14833
14913
  const r = await execIn(
@@ -14844,7 +14924,7 @@ function createDockerProvider(opts) {
14844
14924
  );
14845
14925
  if (r.code === 0 && !file.over) {
14846
14926
  renameSync6(partial, archive);
14847
- writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
14927
+ writeFileSync6(join21(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
14848
14928
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
14849
14929
  await writeMarker(name, attemptId);
14850
14930
  log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -14858,7 +14938,7 @@ function createDockerProvider(opts) {
14858
14938
  file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
14859
14939
  );
14860
14940
  };
14861
- const stagingDir = (name) => join20(opts.deviceDir, "runners-pending", `${name}.outbox`);
14941
+ const stagingDir = (name) => join21(opts.deviceDir, "runners-pending", `${name}.outbox`);
14862
14942
  const ensureRunning = async (name) => {
14863
14943
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
14864
14944
  if (seen.code !== 0) return false;
@@ -14866,7 +14946,7 @@ function createDockerProvider(opts) {
14866
14946
  return (await docker.run(["start", name])).code === 0;
14867
14947
  };
14868
14948
  const dropArchive = (attemptId) => {
14869
- rmSync9(join20(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
14949
+ rmSync9(join21(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
14870
14950
  recursive: true,
14871
14951
  force: true
14872
14952
  });
@@ -14935,7 +15015,7 @@ function createDockerProvider(opts) {
14935
15015
  if (seen.stdout.trim().split(" ")[0] !== "true")
14936
15016
  await must(["start", name], "starting the container");
14937
15017
  const git = await serveGitCredentials({
14938
- socketPath: join20(socketDir(name), "git.sock"),
15018
+ socketPath: join21(socketDir(name), "git.sock"),
14939
15019
  hosts: opts.gitHosts,
14940
15020
  fill: opts.gitFill,
14941
15021
  log
@@ -15084,15 +15164,15 @@ function createDockerProvider(opts) {
15084
15164
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
15085
15165
  if (transcripts) {
15086
15166
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
15087
- if (kept.length) mkdirSync10(join20(opts.hostStateDir, "transcripts"), { recursive: true });
15088
- for (const f of kept) writeFileSync6(join20(opts.hostStateDir, "transcripts", f.name), f.data);
15167
+ if (kept.length) mkdirSync10(join21(opts.hostStateDir, "transcripts"), { recursive: true });
15168
+ for (const f of kept) writeFileSync6(join21(opts.hostStateDir, "transcripts", f.name), f.data);
15089
15169
  await removeIn(
15090
15170
  name,
15091
15171
  `${CONTAINER_STATE}/transcripts`,
15092
15172
  kept.map((f) => f.name)
15093
15173
  );
15094
15174
  }
15095
- const outboxRel = join20("outbox", encodeURIComponent(attempt.agentId));
15175
+ const outboxRel = join21("outbox", encodeURIComponent(attempt.agentId));
15096
15176
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
15097
15177
  let unpublished = false;
15098
15178
  if (outbox) {
@@ -15118,7 +15198,7 @@ function createDockerProvider(opts) {
15118
15198
  const staged = stagingDir(name);
15119
15199
  mkdirSync10(staged, { recursive: true });
15120
15200
  writeFileSync6(
15121
- join20(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
15201
+ join21(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
15122
15202
  `${JSON.stringify(entry)}
15123
15203
  `
15124
15204
  );
@@ -15131,7 +15211,7 @@ function createDockerProvider(opts) {
15131
15211
  if (outbox.complete) {
15132
15212
  const published = publishStaged(
15133
15213
  stagingDir(name),
15134
- join20(opts.hostStateDir, outboxRel),
15214
+ join21(opts.hostStateDir, outboxRel),
15135
15215
  opts.moveFile
15136
15216
  );
15137
15217
  queued = published.published;
@@ -15433,6 +15513,8 @@ var SseSubscriber = class {
15433
15513
  opts;
15434
15514
  aborted = false;
15435
15515
  controller = null;
15516
+ // Ends the reconnect backoff early, so a stop never waits out a sleep of up to 30 s.
15517
+ wake = null;
15436
15518
  lastEventId;
15437
15519
  // Set after `start()`; resolved when the subscriber loop exits (either
15438
15520
  // because `stop()` was called or because an auth failure tore it down).
@@ -15443,12 +15525,7 @@ var SseSubscriber = class {
15443
15525
  stop() {
15444
15526
  this.aborted = true;
15445
15527
  this.controller?.abort();
15446
- }
15447
- // Connect again after a `stop()`, once its loop has exited.
15448
- async resume() {
15449
- await this.finished;
15450
- this.aborted = false;
15451
- this.start();
15528
+ this.wake?.();
15452
15529
  }
15453
15530
  setCursor(eventId) {
15454
15531
  this.lastEventId = eventId;
@@ -15461,7 +15538,7 @@ var SseSubscriber = class {
15461
15538
  backoff = 500;
15462
15539
  if (!this.aborted) {
15463
15540
  this.opts.log.warn("Lost the connection to Cabane; reconnecting");
15464
- await sleep4(backoff);
15541
+ await this.backoff(backoff);
15465
15542
  }
15466
15543
  } catch (err) {
15467
15544
  if (this.aborted) return;
@@ -15481,11 +15558,21 @@ var SseSubscriber = class {
15481
15558
  },
15482
15559
  "Lost the connection to Cabane; reconnecting"
15483
15560
  );
15484
- await sleep4(backoff);
15561
+ await this.backoff(backoff);
15485
15562
  backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
15486
15563
  }
15487
15564
  }
15488
15565
  }
15566
+ backoff(ms) {
15567
+ return new Promise((resolve2) => {
15568
+ const timer = setTimeout(done, ms);
15569
+ this.wake = done;
15570
+ function done() {
15571
+ clearTimeout(timer);
15572
+ resolve2();
15573
+ }
15574
+ });
15575
+ }
15489
15576
  async connect() {
15490
15577
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
15491
15578
  const url = `${base}${this.opts.path}`;
@@ -15532,9 +15619,6 @@ var SseSubscriber = class {
15532
15619
  }
15533
15620
  }
15534
15621
  };
15535
- function sleep4(ms) {
15536
- return new Promise((resolve2) => setTimeout(resolve2, ms));
15537
- }
15538
15622
 
15539
15623
  // src/supervisor.ts
15540
15624
  init_transcript_writer();
@@ -15542,19 +15626,19 @@ init_transcript_writer();
15542
15626
  // src/updater.ts
15543
15627
  import { spawn as spawn8 } from "child_process";
15544
15628
  import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
15545
- import { dirname as dirname12, join as join26 } from "path";
15629
+ import { dirname as dirname14, join as join27 } from "path";
15546
15630
 
15547
15631
  // src/boot-rule.ts
15548
15632
  import {
15549
15633
  mkdirSync as mkdirSync13,
15550
15634
  readFileSync as readFileSync12,
15551
- realpathSync as realpathSync5,
15635
+ realpathSync as realpathSync6,
15552
15636
  renameSync as renameSync8,
15553
15637
  rmSync as rmSync12,
15554
15638
  symlinkSync,
15555
15639
  writeFileSync as writeFileSync9
15556
15640
  } from "fs";
15557
- import { dirname as dirname9, join as join23 } from "path";
15641
+ import { dirname as dirname11, join as join24 } from "path";
15558
15642
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
15559
15643
  var MAX_STAGE_ATTEMPTS = 3;
15560
15644
  function superseded(pending, ...running) {
@@ -15583,37 +15667,37 @@ import {
15583
15667
  mkdirSync as mkdirSync14,
15584
15668
  readFileSync as readFileSync13,
15585
15669
  readdirSync as readdirSync6,
15586
- realpathSync as realpathSync6,
15670
+ realpathSync as realpathSync7,
15587
15671
  renameSync as renameSync9,
15588
15672
  rmSync as rmSync13,
15589
15673
  symlinkSync as symlinkSync2
15590
15674
  } from "fs";
15591
- import { dirname as dirname10, join as join24 } from "path";
15592
- import { fileURLToPath as fileURLToPath6 } from "url";
15675
+ import { dirname as dirname12, join as join25 } from "path";
15676
+ import { fileURLToPath as fileURLToPath7 } from "url";
15593
15677
  function versionsDir() {
15594
- return join24(cabaneDir(), "versions");
15678
+ return join25(cabaneDir(), "versions");
15595
15679
  }
15596
15680
  function currentLink() {
15597
- return join24(cabaneDir(), "current");
15681
+ return join25(cabaneDir(), "current");
15598
15682
  }
15599
15683
  function bootGuardPath() {
15600
- return join24(cabaneDir(), "boot-guard.mjs");
15684
+ return join25(cabaneDir(), "boot-guard.mjs");
15601
15685
  }
15602
15686
  function pinnedNodeDir() {
15603
- return join24(cabaneDir(), "node");
15687
+ return join25(cabaneDir(), "node");
15604
15688
  }
15605
15689
  function pinnedNodePath() {
15606
- return join24(pinnedNodeDir(), "bin", "node");
15690
+ return join25(pinnedNodeDir(), "bin", "node");
15607
15691
  }
15608
15692
  function versionPackageRoot(version) {
15609
- return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
15693
+ return join25(versionsDir(), version, "node_modules", "@cabane", "companion");
15610
15694
  }
15611
15695
  function ownPackageRoot(moduleUrl = import.meta.url) {
15612
- return dirname10(dirname10(fileURLToPath6(moduleUrl)));
15696
+ return dirname12(dirname12(fileURLToPath7(moduleUrl)));
15613
15697
  }
15614
15698
  function packageVersion(root) {
15615
15699
  try {
15616
- const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
15700
+ const pkg2 = JSON.parse(readFileSync13(join25(root, "package.json"), "utf8"));
15617
15701
  return typeof pkg2.version === "string" ? pkg2.version : null;
15618
15702
  } catch {
15619
15703
  return null;
@@ -15621,15 +15705,15 @@ function packageVersion(root) {
15621
15705
  }
15622
15706
  function currentRoot() {
15623
15707
  try {
15624
- const root = realpathSync6(currentLink());
15625
- return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
15708
+ const root = realpathSync7(currentLink());
15709
+ return existsSync16(join25(root, "dist", "cli.js")) ? root : null;
15626
15710
  } catch {
15627
15711
  return null;
15628
15712
  }
15629
15713
  }
15630
15714
  function pointCurrentAt(root) {
15631
15715
  const link = currentLink();
15632
- mkdirSync14(dirname10(link), { recursive: true });
15716
+ mkdirSync14(dirname12(link), { recursive: true });
15633
15717
  const fresh = `${link}.${process.pid}.${Date.now()}`;
15634
15718
  symlinkSync2(root, fresh);
15635
15719
  try {
@@ -15645,7 +15729,7 @@ function pruneVersions(keep) {
15645
15729
  const removed = [];
15646
15730
  for (const name of readdirSync6(dir2)) {
15647
15731
  if (keep.includes(name)) continue;
15648
- rmSync13(join24(dir2, name), { recursive: true, force: true });
15732
+ rmSync13(join25(dir2, name), { recursive: true, force: true });
15649
15733
  removed.push(name);
15650
15734
  }
15651
15735
  return removed;
@@ -15665,7 +15749,7 @@ function compareVersions(a, b) {
15665
15749
  // src/update-state.ts
15666
15750
  init_config();
15667
15751
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
15668
- import { dirname as dirname11, join as join25 } from "path";
15752
+ import { dirname as dirname13, join as join26 } from "path";
15669
15753
  import { z as z18 } from "zod";
15670
15754
  var resultSchema = z18.object({
15671
15755
  kind: z18.enum(["updated", "rolled_back", "failed"]),
@@ -15689,7 +15773,7 @@ var stateSchema = z18.object({
15689
15773
  failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
15690
15774
  });
15691
15775
  function updateStatePath(dir2 = cabaneDir()) {
15692
- return join25(dir2, "update.json");
15776
+ return join26(dir2, "update.json");
15693
15777
  }
15694
15778
  function readUpdateState(path = updateStatePath()) {
15695
15779
  try {
@@ -15700,7 +15784,7 @@ function readUpdateState(path = updateStatePath()) {
15700
15784
  }
15701
15785
  }
15702
15786
  function writeUpdateState(state, path = updateStatePath()) {
15703
- mkdirSync15(dirname11(path), { recursive: true });
15787
+ mkdirSync15(dirname13(path), { recursive: true });
15704
15788
  const tmp = `${path}.${process.pid}.tmp`;
15705
15789
  writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
15706
15790
  `, { mode: 384 });
@@ -15717,7 +15801,7 @@ function updateMode(env = process.env) {
15717
15801
  return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
15718
15802
  }
15719
15803
  var STAGE_RETRY_MS = 60 * 6e4;
15720
- var SWAP_DRAIN_MS = 1e4;
15804
+ var SWAP_WAIT_MS = 10 * 6e4;
15721
15805
  var SOURCE_BUILD_VERSION = "0.0.0";
15722
15806
  function versionToStage(input) {
15723
15807
  const { own, state, now, force } = input;
@@ -15738,10 +15822,10 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
15738
15822
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
15739
15823
  }
15740
15824
  function writeBootGuard(root = ownPackageRoot()) {
15741
- const built = join26(root, "dist", "boot-guard.js");
15825
+ const built = join27(root, "dist", "boot-guard.js");
15742
15826
  if (!existsSync17(built)) return false;
15743
15827
  const target = bootGuardPath();
15744
- mkdirSync16(dirname12(target), { recursive: true });
15828
+ mkdirSync16(dirname14(target), { recursive: true });
15745
15829
  const tmp = `${target}.${process.pid}.tmp`;
15746
15830
  copyFileSync3(built, tmp);
15747
15831
  renameSync11(tmp, target);
@@ -15778,17 +15862,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
15778
15862
  async function stageVersion(version, harnesses, deps = {}) {
15779
15863
  const run = deps.run ?? runProcess;
15780
15864
  const node = deps.execPath ?? process.execPath;
15781
- const target = join26(versionsDir(), version);
15865
+ const target = join27(versionsDir(), version);
15782
15866
  const placed = versionPackageRoot(version);
15783
15867
  if (existsSync17(placed)) {
15784
15868
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
15785
15869
  return { ok: true, root: placed };
15786
15870
  }
15787
- const npm = join26(dirname12(node), "npm");
15871
+ const npm = join27(dirname14(node), "npm");
15788
15872
  if (!existsSync17(npm))
15789
- return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
15873
+ return { ok: false, reason: `there is no npm beside Node at ${dirname14(node)}` };
15790
15874
  mkdirSync16(versionsDir(), { recursive: true });
15791
- const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
15875
+ const staging = join27(versionsDir(), `.${version}.staging-${process.pid}`);
15792
15876
  rmSync14(staging, { recursive: true, force: true });
15793
15877
  const installed = await run(
15794
15878
  npm,
@@ -15805,7 +15889,7 @@ async function stageVersion(version, harnesses, deps = {}) {
15805
15889
  ],
15806
15890
  {
15807
15891
  // npm's install scripts run on this Node, not whichever `node` is first on PATH.
15808
- env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
15892
+ env: { ...process.env, PATH: `${dirname14(node)}:${process.env.PATH ?? ""}` },
15809
15893
  timeoutMs: 15 * 6e4
15810
15894
  }
15811
15895
  );
@@ -15817,7 +15901,7 @@ async function stageVersion(version, harnesses, deps = {}) {
15817
15901
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
15818
15902
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
15819
15903
  }
15820
- const root = join26(staging, "node_modules", "@cabane", "companion");
15904
+ const root = join27(staging, "node_modules", "@cabane", "companion");
15821
15905
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
15822
15906
  if (verdict) {
15823
15907
  rmSync14(staging, { recursive: true, force: true });
@@ -15829,7 +15913,7 @@ async function stageVersion(version, harnesses, deps = {}) {
15829
15913
  }
15830
15914
  async function verifyInstall(root, version, harnesses, deps) {
15831
15915
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
15832
- const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
15916
+ const answered = await deps.run(deps.execPath, [join27(root, "dist", "bin.js"), "--version"], {
15833
15917
  timeoutMs: 6e4,
15834
15918
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
15835
15919
  });
@@ -15837,7 +15921,7 @@ async function verifyInstall(root, version, harnesses, deps) {
15837
15921
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
15838
15922
  }
15839
15923
  if (harnesses.length === 0) return null;
15840
- const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
15924
+ const probed = await deps.run(deps.execPath, [join27(root, "dist", "postinstall.js"), "--json"], {
15841
15925
  timeoutMs: 6e4
15842
15926
  });
15843
15927
  let resolutions = {};
@@ -15867,6 +15951,8 @@ var CompanionUpdater = class {
15867
15951
  deps;
15868
15952
  staging = null;
15869
15953
  ready = null;
15954
+ // When `ready` verified; `SWAP_WAIT_MS` counts from here.
15955
+ readyAt = 0;
15870
15956
  swapping = false;
15871
15957
  watchdog = null;
15872
15958
  now;
@@ -15965,7 +16051,7 @@ var CompanionUpdater = class {
15965
16051
  return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
15966
16052
  if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
15967
16053
  if (this.ready === latest) {
15968
- void this.trySwap();
16054
+ void this.trySwap(true);
15969
16055
  return { ok: true, message: this.waitingMessage(latest) };
15970
16056
  }
15971
16057
  this.maybeStage(true);
@@ -15981,13 +16067,14 @@ var CompanionUpdater = class {
15981
16067
  message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
15982
16068
  };
15983
16069
  }
15984
- void this.trySwap();
16070
+ void this.trySwap(true);
15985
16071
  return { ok: true, message: this.waitingMessage(latest) };
15986
16072
  }
16073
+ // Only the person's `cabane update` asks, and that swaps at once.
15987
16074
  waitingMessage(version) {
15988
16075
  if (this.deps.mode === "attached")
15989
16076
  return `Cabane ${version} is installed. It runs from the next cabane start.`;
15990
- return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
16077
+ return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} now. The turns running now carry on after the restart.`;
15991
16078
  }
15992
16079
  maybeStage(force) {
15993
16080
  if (this.staging) return;
@@ -16005,6 +16092,7 @@ var CompanionUpdater = class {
16005
16092
  const outcome = await stage(version, this.deps.harnesses());
16006
16093
  if (outcome.ok) {
16007
16094
  this.ready = version;
16095
+ this.readyAt = this.now();
16008
16096
  updateUpdateState((s) => {
16009
16097
  const { failed: _failed, ...rest } = s;
16010
16098
  return rest;
@@ -16043,8 +16131,10 @@ var CompanionUpdater = class {
16043
16131
  });
16044
16132
  void this.staging.then(() => this.trySwap());
16045
16133
  }
16046
- // Swap into the ready version once nothing is running. Called on each heartbeat until it can.
16047
- async trySwap() {
16134
+ // Swap into the ready version: when nothing is running, when it has waited `SWAP_WAIT_MS` for
16135
+ // that, when this version is below the server's minimum, or `now` (the person asked). Called on
16136
+ // each heartbeat until it does.
16137
+ async trySwap(now = false) {
16048
16138
  const to = this.ready;
16049
16139
  if (!to || this.swapping) return;
16050
16140
  if (readUpdateState().pending) return;
@@ -16053,18 +16143,9 @@ var CompanionUpdater = class {
16053
16143
  this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
16054
16144
  return;
16055
16145
  }
16056
- if (!this.deps.isIdle()) return;
16146
+ if (!now && !this.deps.isIdle() && !this.waitedLongEnough() && !this.belowMinimum()) return;
16057
16147
  this.swapping = true;
16058
16148
  try {
16059
- await this.deps.pauseStream();
16060
- const sleep5 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
16061
- const deadline = this.now() + SWAP_DRAIN_MS;
16062
- await sleep5(250);
16063
- while (!this.deps.isIdle() && this.now() < deadline) await sleep5(250);
16064
- if (!this.deps.isIdle()) {
16065
- this.deps.resumeStream();
16066
- return;
16067
- }
16068
16149
  this.select(to);
16069
16150
  this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
16070
16151
  await this.deps.restart();
@@ -16072,6 +16153,14 @@ var CompanionUpdater = class {
16072
16153
  this.swapping = false;
16073
16154
  }
16074
16155
  }
16156
+ waitedLongEnough() {
16157
+ return this.now() - this.readyAt >= SWAP_WAIT_MS;
16158
+ }
16159
+ // Cabane refuses this version's turns: waiting would only turn them away.
16160
+ belowMinimum() {
16161
+ const minimum = readUpdateState().server?.minimum;
16162
+ return !!minimum && compareVersions(this.deps.own, minimum) < 0;
16163
+ }
16075
16164
  // Record the swap and point `current` at `to`, leaving the guard that counts its starts.
16076
16165
  select(to) {
16077
16166
  const fromRoot = currentRoot() ?? ownPackageRoot();
@@ -16096,6 +16185,7 @@ init_version2();
16096
16185
  init_src();
16097
16186
  var HEARTBEAT_INTERVAL_MS = 3e4;
16098
16187
  var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
16188
+ var FORCE_EXIT_MS = 4e3;
16099
16189
  var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
16100
16190
  function positiveIntEnv(name) {
16101
16191
  const raw = process.env[name];
@@ -16316,14 +16406,7 @@ var CompanionSupervisor = class {
16316
16406
  autoUpdate: () => this.config.update?.auto === true,
16317
16407
  harnesses: () => this.connectedBundledHarnesses(),
16318
16408
  isIdle: () => this.idle(),
16319
- pauseStream: async () => {
16320
- const sub = this.deviceSub;
16321
- if (!sub) return;
16322
- sub.stop();
16323
- await sub.finished;
16324
- },
16325
- resumeStream: () => void this.deviceSub?.resume(),
16326
- restart: () => this.restartForUpdate(),
16409
+ restart: () => this.restartBounded(),
16327
16410
  log: this.log
16328
16411
  });
16329
16412
  this.updater.start();
@@ -16416,7 +16499,7 @@ var CompanionSupervisor = class {
16416
16499
  this.versionSkewWarned = true;
16417
16500
  this.log.warn(
16418
16501
  { companionVersion: COMPANION_VERSION, serverVersion },
16419
- "This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
16502
+ `This companion and Cabane are on different versions. If turns fail, update with ${cabane("update")}`
16420
16503
  );
16421
16504
  }
16422
16505
  // Pull assignments and reconcile the live runner set against them. Re-entrancy
@@ -16645,7 +16728,8 @@ var CompanionSupervisor = class {
16645
16728
  bundled: this.bundledStatus("claude-code")
16646
16729
  },
16647
16730
  codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") },
16648
- install: companionInstall()
16731
+ install: companionInstall(),
16732
+ managed: repairRoute() === "managed"
16649
16733
  });
16650
16734
  }
16651
16735
  // A missing binary is reported ONCE, and again only when the answer CHANGES,
@@ -17445,8 +17529,22 @@ var CompanionSupervisor = class {
17445
17529
  );
17446
17530
  }
17447
17531
  }
17532
+ // The update's restart, held to the same `FORCE_EXIT_MS` a signal's stop gets. Exit 0: the
17533
+ // swap is recorded and `current` names the new version, so the service starts it either way.
17534
+ async restartBounded() {
17535
+ const force = setTimeout(() => {
17536
+ this.log.warn({ ms: FORCE_EXIT_MS }, "Restarting for the update: shutdown took too long");
17537
+ this.exitFn(0);
17538
+ }, FORCE_EXIT_MS);
17539
+ force.unref?.();
17540
+ try {
17541
+ await this.restartForUpdate();
17542
+ } finally {
17543
+ clearTimeout(force);
17544
+ }
17545
+ }
17448
17546
  // Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
17449
- // in progress. What a self-update swap waits for.
17547
+ // in progress. What a self-update swap waits for, up to `SWAP_WAIT_MS`.
17450
17548
  idle() {
17451
17549
  if (this.claims.size > 0 || this.recovering) return false;
17452
17550
  for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
@@ -17468,7 +17566,7 @@ var CompanionSupervisor = class {
17468
17566
  if (auto) this.kickHeartbeat();
17469
17567
  return {
17470
17568
  ok: true,
17471
- message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
17569
+ message: auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
17472
17570
  };
17473
17571
  }
17474
17572
  if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
@@ -17590,9 +17688,9 @@ function handleUncaught(log, err, origin) {
17590
17688
  // src/crash-marker.ts
17591
17689
  init_config();
17592
17690
  import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
17593
- import { join as join27 } from "path";
17691
+ import { join as join28 } from "path";
17594
17692
  function crashMarkerPath() {
17595
- return join27(cabaneDir(), "last-error.json");
17693
+ return join28(cabaneDir(), "last-error.json");
17596
17694
  }
17597
17695
  function recordCrash(rec2) {
17598
17696
  try {
@@ -17783,7 +17881,7 @@ import {
17783
17881
  writeFileSync as writeFileSync12
17784
17882
  } from "fs";
17785
17883
  import { homedir as homedir7, userInfo as userInfo3 } from "os";
17786
- import { dirname as dirname13, join as join28, sep as sep5 } from "path";
17884
+ import { dirname as dirname15, join as join29, sep as sep5 } from "path";
17787
17885
  function servicePlatform(platform = process.platform) {
17788
17886
  if (platform === "linux") return "systemd";
17789
17887
  if (platform === "darwin") return "launchd";
@@ -17801,15 +17899,15 @@ function serviceNode(execPath = process.execPath) {
17801
17899
  return existsSync19(pinnedNodePath()) ? pinnedNodePath() : execPath;
17802
17900
  }
17803
17901
  function servicePaths(platform, env = process.env, home = homedir7(), node = serviceNode()) {
17804
- const unitFile = platform === "systemd" ? join28(
17805
- env.XDG_CONFIG_HOME || join28(home, ".config"),
17902
+ const unitFile = platform === "systemd" ? join29(
17903
+ env.XDG_CONFIG_HOME || join29(home, ".config"),
17806
17904
  "systemd",
17807
17905
  "user",
17808
17906
  `${serviceName(env)}.service`
17809
- ) : join28(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
17907
+ ) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
17810
17908
  return {
17811
17909
  unitFile,
17812
- envFile: join28(cabaneDir(), "service.env"),
17910
+ envFile: join29(cabaneDir(), "service.env"),
17813
17911
  node,
17814
17912
  entry: bootGuardPath()
17815
17913
  };
@@ -17876,7 +17974,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
17876
17974
  " <key>ThrottleInterval</key><integer>5</integer>",
17877
17975
  // The companion writes its own log; the console copy would only grow a second one.
17878
17976
  " <key>StandardOutPath</key><string>/dev/null</string>",
17879
- ` <key>StandardErrorPath</key><string>${xml(join28(cabaneDir(), "service-error.log"))}</string>`,
17977
+ ` <key>StandardErrorPath</key><string>${xml(join29(cabaneDir(), "service-error.log"))}</string>`,
17880
17978
  "</dict>",
17881
17979
  "</plist>",
17882
17980
  ""
@@ -17931,11 +18029,11 @@ async function installService(deps = {}) {
17931
18029
  rewriteScriptWrappers();
17932
18030
  const paths = servicePaths(platform, env, deps.home, node);
17933
18031
  const servicePath = await loginShellPath(run, env);
17934
- mkdirSync18(dirname13(paths.envFile), { recursive: true });
18032
+ mkdirSync18(dirname15(paths.envFile), { recursive: true });
17935
18033
  const existing = existsSync19(paths.envFile) ? readFileSync16(paths.envFile, "utf8") : "";
17936
18034
  writeFileSync12(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
17937
18035
  chmodSync7(paths.envFile, 384);
17938
- mkdirSync18(dirname13(paths.unitFile), { recursive: true });
18036
+ mkdirSync18(dirname15(paths.unitFile), { recursive: true });
17939
18037
  const outcome = await writeAndStartUnit(platform, paths, env, servicePath, run);
17940
18038
  if (!outcome.ok) await removeUnit(platform, paths, env, run);
17941
18039
  return outcome;
@@ -18016,15 +18114,15 @@ async function seedManagedInstall(deps) {
18016
18114
  }
18017
18115
  const target = versionPackageRoot(version);
18018
18116
  if (!existsSync19(target)) {
18019
- const staging = join28(versionsDir(), `.${version}.seed-${process.pid}`);
18117
+ const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
18020
18118
  rmSync16(staging, { recursive: true, force: true });
18021
18119
  try {
18022
- cpSync(own, join28(staging, "node_modules", "@cabane", "companion"), {
18120
+ cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
18023
18121
  recursive: true,
18024
18122
  verbatimSymlinks: true
18025
18123
  });
18026
- rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
18027
- renameSync12(staging, join28(versionsDir(), version));
18124
+ rmSync16(join29(versionsDir(), version), { recursive: true, force: true });
18125
+ renameSync12(staging, join29(versionsDir(), version));
18028
18126
  } catch (err) {
18029
18127
  rmSync16(staging, { recursive: true, force: true });
18030
18128
  return {
@@ -18035,7 +18133,7 @@ async function seedManagedInstall(deps) {
18035
18133
  }
18036
18134
  const unrunnable = await verifyInstall(target, version, [], deps);
18037
18135
  if (unrunnable) {
18038
- rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
18136
+ rmSync16(join29(versionsDir(), version), { recursive: true, force: true });
18039
18137
  return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
18040
18138
  }
18041
18139
  pointCurrentAt(target);
@@ -18117,9 +18215,9 @@ function userName() {
18117
18215
  return process.env.USER ?? "";
18118
18216
  }
18119
18217
  }
18120
- function rewriteScriptWrappers(dir2 = join28(cabaneDir(), "bin")) {
18218
+ function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
18121
18219
  for (const name of ["cabane", "cabane-companion"]) {
18122
- const path = join28(dir2, name);
18220
+ const path = join29(dir2, name);
18123
18221
  let text;
18124
18222
  try {
18125
18223
  text = readFileSync16(path, "utf8");
@@ -18178,7 +18276,7 @@ async function startDaemon(opts = {}, deps = {}) {
18178
18276
  const readState = deps.readState ?? readLiveRuntimeState;
18179
18277
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
18180
18278
  const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
18181
- const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18279
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18182
18280
  const now = deps.now ?? (() => Date.now());
18183
18281
  const quiet = opts.report === "failures";
18184
18282
  const cfg = requireStartConfig({
@@ -18226,7 +18324,7 @@ async function startDaemon(opts = {}, deps = {}) {
18226
18324
  const deadline = now() + STARTUP_TIMEOUT_MS;
18227
18325
  let seen = readState();
18228
18326
  while (!ready(seen) && now() < deadline) {
18229
- await sleep5(POLL_INTERVAL_MS);
18327
+ await sleep4(POLL_INTERVAL_MS);
18230
18328
  seen = readState();
18231
18329
  }
18232
18330
  return ready(seen) ? seen : null;
@@ -18299,7 +18397,7 @@ async function stop(deps = {}) {
18299
18397
  const readState = deps.readState ?? readLiveRuntimeState;
18300
18398
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
18301
18399
  const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
18302
- const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18400
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18303
18401
  const now = deps.now ?? (() => Date.now());
18304
18402
  const state = readState();
18305
18403
  if (!state) {
@@ -18327,7 +18425,7 @@ async function stop(deps = {}) {
18327
18425
  process.stdout.write("companion: stopped.\n");
18328
18426
  return;
18329
18427
  }
18330
- await sleep5(POLL_INTERVAL_MS2);
18428
+ await sleep4(POLL_INTERVAL_MS2);
18331
18429
  }
18332
18430
  process.stdout.write(
18333
18431
  `companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
@@ -18353,7 +18451,7 @@ function isAlive(kill, pid) {
18353
18451
  var STARTUP_WAIT_MS = 2e4;
18354
18452
  async function installAndStartService(deps = {}) {
18355
18453
  const readState = deps.readState ?? readLiveRuntimeState;
18356
- const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18454
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18357
18455
  const now = deps.now ?? Date.now;
18358
18456
  const before = await serviceState(deps);
18359
18457
  const running = readState();
@@ -18369,7 +18467,7 @@ async function installAndStartService(deps = {}) {
18369
18467
  while (now() < deadline) {
18370
18468
  const state = readState();
18371
18469
  if (state?.socket) return true;
18372
- await sleep5(250);
18470
+ await sleep4(250);
18373
18471
  }
18374
18472
  write(
18375
18473
  `${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
@@ -18383,7 +18481,7 @@ async function serviceInstall(deps = {}) {
18383
18481
  }
18384
18482
  write(
18385
18483
  INDENT + tick(
18386
- "Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself between turns."
18484
+ "Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
18387
18485
  )
18388
18486
  );
18389
18487
  write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
@@ -18446,7 +18544,7 @@ function updateRows(state, auto, opts = {}) {
18446
18544
  rows.push(
18447
18545
  row(
18448
18546
  "Auto-update",
18449
- !opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
18547
+ !opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
18450
18548
  )
18451
18549
  );
18452
18550
  const last = state.lastResult;
@@ -18477,7 +18575,6 @@ function setRunsAsService(on) {
18477
18575
  }
18478
18576
 
18479
18577
  // src/commands/start.ts
18480
- var FORCE_EXIT_MS = 4e3;
18481
18578
  var MAX_CODES = 3;
18482
18579
  async function start(opts = {}) {
18483
18580
  setLogOverrides(opts);
@@ -18943,7 +19040,7 @@ async function askHowToRun(deps = {}) {
18943
19040
  return { asService: false };
18944
19041
  blank();
18945
19042
  const asService = await ask(
18946
- "Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself between turns."
19043
+ "Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself, usually between turns."
18947
19044
  );
18948
19045
  if (!asService) saveConfig({ ...cfg, serviceDeclined: true });
18949
19046
  blank();
@@ -19113,10 +19210,11 @@ async function logs(opts = {}) {
19113
19210
 
19114
19211
  // src/commands/status.ts
19115
19212
  init_bundled_binaries();
19213
+ init_install_kind();
19116
19214
  init_cabane_command();
19117
- import { readdirSync as readdirSync7, realpathSync as realpathSync7, existsSync as existsSync20 } from "fs";
19215
+ import { readdirSync as readdirSync7, realpathSync as realpathSync8, existsSync as existsSync20 } from "fs";
19118
19216
  import { homedir as homedir8 } from "os";
19119
- import { dirname as dirname14, join as join29 } from "path";
19217
+ import { dirname as dirname16, join as join30 } from "path";
19120
19218
  init_config();
19121
19219
  init_sandbox_policy();
19122
19220
  init_secrets();
@@ -19182,10 +19280,10 @@ function versionLine() {
19182
19280
  const install = companionInstall();
19183
19281
  const own = installedCommandPath({ install });
19184
19282
  if (install === "script") {
19185
- return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join29(homedir8(), ".cabane"))})`;
19283
+ return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join30(homedir8(), ".cabane"))})`;
19186
19284
  }
19187
19285
  if (own)
19188
- return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname14(dirname14(own)))})`;
19286
+ return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname16(dirname16(own)))})`;
19189
19287
  return `Cabane ${COMPANION_VERSION}`;
19190
19288
  }
19191
19289
  function otherInstallLines(running) {
@@ -19202,13 +19300,13 @@ function otherInstallLines(running) {
19202
19300
  function otherInstall(install, own) {
19203
19301
  const real = (p) => {
19204
19302
  try {
19205
- return realpathSync7(p);
19303
+ return realpathSync8(p);
19206
19304
  } catch {
19207
19305
  return null;
19208
19306
  }
19209
19307
  };
19210
19308
  if (install === "npm") {
19211
- const script = join29(
19309
+ const script = join30(
19212
19310
  homedir8(),
19213
19311
  ".cabane",
19214
19312
  "node",
@@ -19217,12 +19315,12 @@ function otherInstall(install, own) {
19217
19315
  "@cabane",
19218
19316
  "companion"
19219
19317
  );
19220
- return existsSync20(script) ? join29(homedir8(), ".cabane") : null;
19318
+ return existsSync20(script) ? join30(homedir8(), ".cabane") : null;
19221
19319
  }
19222
19320
  const found = firstOnPath("cabane-companion", process.env.PATH);
19223
19321
  if (!found) return null;
19224
- const ownAlias = own ? real(join29(dirname14(own), "cabane-companion")) : null;
19225
- return real(found) === ownAlias ? null : dirname14(found);
19322
+ const ownAlias = own ? real(join30(dirname16(own), "cabane-companion")) : null;
19323
+ return real(found) === ownAlias ? null : dirname16(found);
19226
19324
  }
19227
19325
  async function liveState() {
19228
19326
  const running = readLiveRuntimeState();
@@ -19282,7 +19380,7 @@ function signInText(runtime, status2, version) {
19282
19380
  case "signed_out":
19283
19381
  return `signed out \u2014 run ${loginCommand(runtime)}`;
19284
19382
  case "bundle_missing":
19285
- return `this install is missing it \u2014 repair with ${repairCommands()[0]}`;
19383
+ return `this install is missing it \u2014 repair with ${repairSteps()}${repairCost() ? `. ${repairCost()}` : ""}`;
19286
19384
  case "check_failed":
19287
19385
  return "connected, but its sign-in couldn't be checked";
19288
19386
  default:
@@ -19341,6 +19439,8 @@ init_config();
19341
19439
  init_term();
19342
19440
  init_version2();
19343
19441
  init_cabane_command();
19442
+ init_bundled_binaries();
19443
+ init_install_kind();
19344
19444
  var UPDATE_TIMEOUT_MS = 20 * 6e4;
19345
19445
  async function update(opts = {}) {
19346
19446
  const running = readLiveRuntimeState();
@@ -19358,20 +19458,43 @@ async function update(opts = {}) {
19358
19458
  write(`${INDENT}${r.ok ? tick(r.message) : r.message}`);
19359
19459
  return;
19360
19460
  }
19361
- const cfg2 = loadConfig();
19362
- if (!cfg2) {
19461
+ const cfg = loadConfig();
19462
+ if (!cfg) {
19363
19463
  write(`${INDENT}This computer isn't set up yet. Run ${cabane("start")} first.`);
19364
19464
  process.exitCode = 1;
19365
19465
  return;
19366
19466
  }
19367
- saveConfig({ ...cfg2, update: { auto } });
19467
+ saveConfig({ ...cfg, update: { auto } });
19368
19468
  write(
19369
19469
  INDENT + tick(
19370
- auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself."
19470
+ auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself."
19371
19471
  )
19372
19472
  );
19373
19473
  return;
19374
19474
  }
19475
+ const harnesses = connectedHarnesses();
19476
+ const latest = readUpdateState().server?.latest;
19477
+ if (!latest || compareVersions(latest, COMPANION_VERSION) <= 0) {
19478
+ const repair = await repairOwnCopy({ running: Boolean(running) });
19479
+ if (repair.kind === "running") {
19480
+ write(
19481
+ `${INDENT}This copy of Cabane ${COMPANION_VERSION} is incomplete: ${repair.reason}. Run ${cabane("stop")}, then ${cabane("update")} again to repair it. Stopping ends any turn in progress.`
19482
+ );
19483
+ process.exitCode = 1;
19484
+ return;
19485
+ }
19486
+ if (repair.kind === "failed") {
19487
+ write(`${INDENT}Couldn't repair Cabane ${COMPANION_VERSION}: ${repair.reason}.`);
19488
+ process.exitCode = 1;
19489
+ return;
19490
+ }
19491
+ if (repair.kind === "repaired") {
19492
+ write(
19493
+ INDENT + tick(`Repaired Cabane ${COMPANION_VERSION}. Start it with ${cabane("start")}`)
19494
+ );
19495
+ return;
19496
+ }
19497
+ }
19375
19498
  if (running?.socket) {
19376
19499
  const r = await controlRequest(
19377
19500
  running.socket,
@@ -19382,7 +19505,6 @@ async function update(opts = {}) {
19382
19505
  if (!r.ok) process.exitCode = 1;
19383
19506
  return;
19384
19507
  }
19385
- const latest = readUpdateState().server?.latest;
19386
19508
  if (!latest) {
19387
19509
  write(
19388
19510
  `${INDENT}Cabane hasn't said which version is latest yet. Run ${cabane("start")}, and it updates from there.`
@@ -19394,11 +19516,6 @@ async function update(opts = {}) {
19394
19516
  write(INDENT + tick(`Cabane ${COMPANION_VERSION} is up to date.`));
19395
19517
  return;
19396
19518
  }
19397
- const cfg = loadConfig();
19398
- const harnesses = cfg ? [
19399
- ...isClaudeCodeConnected(cfg) ? ["claude-code"] : [],
19400
- ...isCodexEnabled(cfg) ? ["codex"] : []
19401
- ] : [];
19402
19519
  write(
19403
19520
  `${INDENT}Installing Cabane ${latest}. It brings its own copies of the agents, so this can take a few minutes.`
19404
19521
  );
@@ -19438,6 +19555,31 @@ async function update(opts = {}) {
19438
19555
  pointCurrentAt(outcome.root);
19439
19556
  write(INDENT + tick(`Updated to ${latest}. Start it with ${cabane("start")}`));
19440
19557
  }
19558
+ function bundledHere(platform, arch) {
19559
+ return ["claude-code", ...codexPlatformPackage(platform, arch) ? ["codex"] : []];
19560
+ }
19561
+ function connectedHarnesses() {
19562
+ const cfg = loadConfig();
19563
+ if (!cfg) return [];
19564
+ return [
19565
+ ...isClaudeCodeConnected(cfg) ? ["claude-code"] : [],
19566
+ ...isCodexEnabled(cfg) ? ["codex"] : []
19567
+ ];
19568
+ }
19569
+ async function repairOwnCopy(deps) {
19570
+ if ((deps.route ?? repairRoute()) !== "managed") return { kind: "healthy" };
19571
+ const harnesses = deps.harnesses ?? bundledHere(deps.platform ?? process.platform, deps.arch ?? process.arch);
19572
+ const root = deps.root ?? ownPackageRoot();
19573
+ const version = deps.version ?? COMPANION_VERSION;
19574
+ const verify = deps.verify ?? ((r, v, h) => verifyInstall(r, v, h, { run: runProcess, execPath: process.execPath }));
19575
+ const broken = await verify(root, version, harnesses);
19576
+ if (!broken) return { kind: "healthy" };
19577
+ if (deps.running) return { kind: "running", reason: broken };
19578
+ const outcome = await (deps.stage ?? stageVersion)(version, harnesses);
19579
+ if (!outcome.ok) return { kind: "failed", reason: outcome.reason };
19580
+ (deps.point ?? pointCurrentAt)(outcome.root);
19581
+ return { kind: "repaired" };
19582
+ }
19441
19583
 
19442
19584
  // src/commands/transcript.ts
19443
19585
  init_cabane_command();
@@ -19445,7 +19587,7 @@ init_errors();
19445
19587
  init_src();
19446
19588
  init_transcript_writer();
19447
19589
  import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync8 } from "fs";
19448
- import { isAbsolute, join as join30 } from "path";
19590
+ import { isAbsolute, join as join31 } from "path";
19449
19591
  async function transcript(opts = {}) {
19450
19592
  const dir2 = transcriptsDir();
19451
19593
  if (opts.follow) {
@@ -19462,7 +19604,7 @@ async function transcript(opts = {}) {
19462
19604
  process.stdout.write(emptyMessage(dir2));
19463
19605
  return;
19464
19606
  }
19465
- process.stdout.write(renderFile(join30(dir2, newest)) + "\n");
19607
+ process.stdout.write(renderFile(join31(dir2, newest)) + "\n");
19466
19608
  return;
19467
19609
  }
19468
19610
  printList(dir2);
@@ -19545,7 +19687,7 @@ function isComplete(content) {
19545
19687
  async function followTranscripts(dir2) {
19546
19688
  const follower = new TranscriptFollower({
19547
19689
  listFiles: () => listFiles(dir2),
19548
- read: (f) => readFileSync17(join30(dir2, f), "utf8"),
19690
+ read: (f) => readFileSync17(join31(dir2, f), "utf8"),
19549
19691
  write: (s) => process.stdout.write(s),
19550
19692
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
19551
19693
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -19585,7 +19727,7 @@ function printList(dir2) {
19585
19727
 
19586
19728
  `);
19587
19729
  for (const f of files.slice(0, 20)) {
19588
- const { meta, outcome } = peek(join30(dir2, f));
19730
+ const { meta, outcome } = peek(join31(dir2, f));
19589
19731
  const when = fmtTime(rec(meta)?.ts);
19590
19732
  const ws = str2(rec(meta)?.workspaceSlug);
19591
19733
  const o = rec(outcome);
@@ -19623,10 +19765,10 @@ function resolveTarget(dir2, target) {
19623
19765
  if (existsSync21(target)) return target;
19624
19766
  throw new CompanionError(`no transcript at ${target}.`);
19625
19767
  }
19626
- const exact = join30(dir2, target);
19768
+ const exact = join31(dir2, target);
19627
19769
  if (existsSync21(exact)) return exact;
19628
19770
  const matches = listFiles(dir2).filter((f) => f.includes(target));
19629
- if (matches.length === 1) return join30(dir2, matches[0]);
19771
+ if (matches.length === 1) return join31(dir2, matches[0]);
19630
19772
  if (matches.length === 0) {
19631
19773
  throw new CompanionError(
19632
19774
  `no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
@@ -19790,6 +19932,8 @@ They are written per dispatch while \`${cabane("start")}\` is running.
19790
19932
  // src/cli.ts
19791
19933
  init_errors();
19792
19934
  init_version2();
19935
+ init_cabane_command();
19936
+ rememberInvokedCommand();
19793
19937
  var program = new Command();
19794
19938
  program.name("cabane").description("Run your Cabane agents on this computer, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
19795
19939
  program.command("pair").description(