@cabane/companion 0.6.154 → 0.6.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -1,6 +1,294 @@
1
1
  // src/runtime.ts
2
2
  import { randomUUID as randomUUID7 } from "crypto";
3
3
 
4
+ // src/cabane-command.ts
5
+ import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
6
+ import { homedir as homedir3 } from "os";
7
+ import { delimiter, join as join2, sep as sep2 } from "path";
8
+
9
+ // src/bundled-binaries.ts
10
+ import { realpathSync, statSync } from "fs";
11
+ import { createRequire } from "module";
12
+ import { homedir } from "os";
13
+ import { dirname, join, sep } from "path";
14
+ import { fileURLToPath } from "url";
15
+ function nodeResolveSelf(specifier) {
16
+ const here = fileURLToPath(import.meta.url);
17
+ if (typeof import.meta.resolve === "function") {
18
+ try {
19
+ return fileURLToPath(import.meta.resolve(specifier));
20
+ } catch {
21
+ }
22
+ }
23
+ try {
24
+ return createRequire(here).resolve(specifier);
25
+ } catch {
26
+ }
27
+ return walkNodeModules(specifier, here);
28
+ }
29
+ function walkNodeModules(pkg2, fromPath) {
30
+ let dir2 = dirname(fromPath);
31
+ for (; ; ) {
32
+ const manifest = join(dir2, "node_modules", pkg2, "package.json");
33
+ if (nodeIsFile(manifest)) {
34
+ try {
35
+ return realpathSync(manifest);
36
+ } catch {
37
+ return manifest;
38
+ }
39
+ }
40
+ const parent = dirname(dir2);
41
+ if (parent === dir2) return null;
42
+ dir2 = parent;
43
+ }
44
+ }
45
+ function nodeResolve(specifier, fromPath) {
46
+ try {
47
+ return createRequire(fromPath).resolve(specifier);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function nodeIsFile(path) {
53
+ try {
54
+ return statSync(path).isFile();
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ function detectPreferMusl(platform = process.platform) {
60
+ if (platform !== "linux") return false;
61
+ const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
62
+ const header = report?.header;
63
+ return report != null && header?.glibcVersionRuntime === void 0;
64
+ }
65
+ function defaultResolveEnv() {
66
+ return {
67
+ platform: process.platform,
68
+ arch: process.arch,
69
+ preferMusl: detectPreferMusl(),
70
+ // Both resolutions start from this module, which tsup bundles into
71
+ // `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
72
+ // root — so one environment serves the runtime checks and the install
73
+ // verifier alike.
74
+ resolveSelf: nodeResolveSelf,
75
+ resolve: nodeResolve,
76
+ isFile: nodeIsFile
77
+ };
78
+ }
79
+ var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
80
+ function claudePlatformPackages(platform, arch, preferMusl) {
81
+ if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
82
+ if (platform === "linux") {
83
+ return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
84
+ }
85
+ return [`${CLAUDE_SDK}-${platform}-${arch}`];
86
+ }
87
+ function resolveClaude(env) {
88
+ const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
89
+ const base = {
90
+ runtime: "claude-code",
91
+ packageName: candidates[0] ?? null,
92
+ platform: env.platform,
93
+ arch: env.arch
94
+ };
95
+ const sdkEntry = env.resolveSelf(CLAUDE_SDK);
96
+ if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
97
+ let sawPackage = null;
98
+ for (const pkg2 of candidates) {
99
+ const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
100
+ if (!manifest) continue;
101
+ sawPackage ??= pkg2;
102
+ const exe = join(dirname(manifest), env.platform === "win32" ? "claude.exe" : "claude");
103
+ if (env.isFile(exe)) {
104
+ return { ...base, packageName: pkg2, status: "present", path: exe };
105
+ }
106
+ }
107
+ return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
108
+ }
109
+ var CODEX_SDK = "@openai/codex-sdk";
110
+ var CODEX_CLI = "@openai/codex";
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
+ var CODEX_PACKAGE_BY_TRIPLE = {
130
+ "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
131
+ "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
132
+ "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
133
+ "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
134
+ "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
135
+ "aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
136
+ };
137
+ function resolveCodex(env) {
138
+ const triple = codexTargetTriple(env.platform, env.arch);
139
+ const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
140
+ const base = {
141
+ runtime: "codex",
142
+ packageName: pkg2 ?? null,
143
+ platform: env.platform,
144
+ arch: env.arch
145
+ };
146
+ if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
147
+ const sdkEntry = env.resolveSelf(CODEX_SDK);
148
+ if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
149
+ const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
150
+ if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
151
+ const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
152
+ if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
153
+ const packageRoot = join(dirname(platformManifest), "vendor", triple);
154
+ const exeName = env.platform === "win32" ? "codex.exe" : "codex";
155
+ const current = join(packageRoot, "bin", exeName);
156
+ if (env.isFile(current) && env.isFile(join(packageRoot, "codex-package.json"))) {
157
+ return { ...base, status: "present", path: current };
158
+ }
159
+ const legacy = join(packageRoot, "codex", exeName);
160
+ if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
161
+ return { ...base, status: "executable_missing", path: null };
162
+ }
163
+ function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
164
+ try {
165
+ return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
166
+ } catch {
167
+ return {
168
+ runtime,
169
+ status: "platform_package_missing",
170
+ path: null,
171
+ packageName: null,
172
+ platform: env.platform,
173
+ arch: env.arch
174
+ };
175
+ }
176
+ }
177
+ var HARNESS_LABEL = {
178
+ "claude-code": "Claude Code",
179
+ codex: "Codex"
180
+ };
181
+ var HARNESS_INTENT_WORD = {
182
+ "claude-code": "connected",
183
+ codex: "enabled"
184
+ };
185
+ function missingNoun(status) {
186
+ return status === "executable_missing" ? "executable" : "platform package";
187
+ }
188
+ var INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
189
+ function companionInstall(execPath = process.execPath, home = homedir()) {
190
+ const real = (path) => {
191
+ try {
192
+ return realpathSync(path);
193
+ } catch {
194
+ return path;
195
+ }
196
+ };
197
+ const scriptNode = join(real(home), ".cabane", "node") + sep;
198
+ return real(execPath).startsWith(scriptNode) ? "script" : "npm";
199
+ }
200
+ function repairCommands(install = companionInstall()) {
201
+ return install === "script" ? [INSTALL_SCRIPT_COMMAND] : ["npm i -g @cabane/companion --include=optional"];
202
+ }
203
+ function repairAction(install = companionInstall()) {
204
+ return `Reinstall with \`${repairCommands(install)[0]}\`.`;
205
+ }
206
+ function startupWarning(r) {
207
+ const label = HARNESS_LABEL[r.runtime];
208
+ 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.`;
209
+ }
210
+ function harnessIssueNote(runtime, status, install = companionInstall()) {
211
+ const intent = HARNESS_INTENT_WORD[runtime];
212
+ const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
213
+ return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status)} is missing. ${repairAction(install)} Then restart the companion.`;
214
+ }
215
+ function turnIncompleteCopy(runtime) {
216
+ return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
217
+ }
218
+
219
+ // src/cli-entry.ts
220
+ import { existsSync } from "fs";
221
+ import { fileURLToPath as fileURLToPath2 } from "url";
222
+ var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
223
+ function companionCliEntry(deps = {}) {
224
+ const exists = deps.exists ?? existsSync;
225
+ const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
226
+ for (const candidate of candidates) {
227
+ if (exists(candidate)) return candidate;
228
+ }
229
+ const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
230
+ if (argv1 && exists(argv1)) return argv1;
231
+ return candidates[0] ?? "";
232
+ }
233
+
234
+ // src/term.ts
235
+ import { createInterface } from "readline";
236
+ import { homedir as homedir2 } from "os";
237
+ import pc from "picocolors";
238
+ function tildePath(path, home = homedir2()) {
239
+ if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
240
+ return path;
241
+ }
242
+
243
+ // src/cabane-command.ts
244
+ function installedCommandPath(deps = {}) {
245
+ const install = deps.install ?? companionInstall();
246
+ const home = deps.home ?? homedir3();
247
+ if (install === "script") return join2(home, ".cabane", "bin", "cabane");
248
+ const entry = deps.cliEntry ?? companionCliEntry();
249
+ const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
250
+ const at = entry.indexOf(marker);
251
+ if (at < 0) return null;
252
+ return join2(entry.slice(0, at), "bin", "cabane");
253
+ }
254
+ function cabaneCommand(deps = {}) {
255
+ const own = installedCommandPath(deps);
256
+ if (!own) return "cabane";
257
+ const realpath = deps.realpath ?? safeRealpath;
258
+ const isExecutable = deps.isExecutable ?? executable;
259
+ const ownReal = realpath(own);
260
+ if (!ownReal) return "cabane";
261
+ const onPath = firstOnPath("cabane", "path" in deps ? deps.path : process.env.PATH, isExecutable);
262
+ if (onPath && realpath(onPath) === ownReal) return "cabane";
263
+ return tildePath(own, deps.home);
264
+ }
265
+ function cabane(rest, deps) {
266
+ return `${cabaneCommand(deps)} ${rest}`;
267
+ }
268
+ function firstOnPath(name, path, isExecutable = executable) {
269
+ for (const dir2 of (path ?? "").split(delimiter)) {
270
+ if (!dir2) continue;
271
+ const candidate = join2(dir2, name);
272
+ if (isExecutable(candidate)) return candidate;
273
+ }
274
+ return null;
275
+ }
276
+ function executable(path) {
277
+ try {
278
+ accessSync(path, constants.X_OK);
279
+ return true;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+ function safeRealpath(path) {
285
+ try {
286
+ return realpathSync2(path);
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+
4
292
  // src/config.ts
5
293
  import {
6
294
  chmodSync,
@@ -9,10 +297,10 @@ import {
9
297
  renameSync,
10
298
  rmSync,
11
299
  writeFileSync,
12
- existsSync
300
+ existsSync as existsSync2
13
301
  } from "fs";
14
- import { homedir, userInfo } from "os";
15
- import { dirname, join } from "path";
302
+ import { homedir as homedir4, userInfo } from "os";
303
+ import { dirname as dirname2, join as join3 } from "path";
16
304
  import { z as z2 } from "zod";
17
305
 
18
306
  // src/errors.ts
@@ -225,13 +513,13 @@ function realAccountHome() {
225
513
  return realHomeCache;
226
514
  }
227
515
  function agentCompanionHome() {
228
- return join(cabaneDir(), "agent-home");
516
+ return join3(cabaneDir(), "agent-home");
229
517
  }
230
518
  function cabaneDir() {
231
- const dir2 = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
519
+ const dir2 = join3(process.env.CABANE_COMPANION_HOME || homedir4(), ".cabane");
232
520
  if (process.env.VITEST) {
233
521
  const real = realAccountHome();
234
- if (real && dir2 === join(real, ".cabane")) {
522
+ if (real && dir2 === join3(real, ".cabane")) {
235
523
  throw new Error(
236
524
  `cabaneDir() resolved to the real ${dir2} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
237
525
  );
@@ -240,7 +528,7 @@ function cabaneDir() {
240
528
  return dir2;
241
529
  }
242
530
  function configPath() {
243
- return join(cabaneDir(), "config.json");
531
+ return join3(cabaneDir(), "config.json");
244
532
  }
245
533
  var localAgentConfigSchema = z2.object({
246
534
  cwd: z2.string().optional(),
@@ -259,11 +547,11 @@ var localAgentConfigSchema = z2.object({
259
547
  sandbox: z2.enum(["os", "none"]).optional()
260
548
  }).strict();
261
549
  var AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
262
- var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx) => {
550
+ var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx2) => {
263
551
  for (const [key, row] of Object.entries(agents)) {
264
552
  if (row.sandbox === void 0) continue;
265
553
  if (AGENT_ID_KEY.test(key)) continue;
266
- ctx.addIssue({
554
+ ctx2.addIssue({
267
555
  code: z2.ZodIssueCode.custom,
268
556
  path: [key, "sandbox"],
269
557
  message: "a sandbox exception must name one agent \u2014 key the row by the agent's id (shown in Cabane), not a username or workspace-slug/username"
@@ -314,15 +602,24 @@ var companionConfigSchema = z2.object({
314
602
  // the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
315
603
  // SDK never runs).
316
604
  //
317
- // The block's PRESENCE is also the migration marker
318
- // (`migrateConnectedHarnesses`): absent means a config whose user was never
319
- // asked. So every config written carries it, a fresh pair at `enabled: false`.
605
+ // Absent means one of two things, told apart by `harnessesChosen`: a legacy
606
+ // config whose user was never asked (`migrateConnectedHarnesses`), or a
607
+ // device that has never been offered Claude Code. `enabled: false` is a
608
+ // decline, and `start` doesn't offer it again.
320
609
  //
321
610
  // Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
322
611
  // switch.
323
612
  claudeCode: z2.object({
324
613
  enabled: z2.boolean().optional()
325
614
  }).strict().optional(),
615
+ // Written by every pairing that asks which agents to connect. With it, an
616
+ // absent `claudeCode` block means "never offered"; without it, the block's
617
+ // absence marks a config from before connectors were chosen.
618
+ harnessesChosen: z2.boolean().optional(),
619
+ // OpenCode's decline. Its block is an address, so a decline can't live there
620
+ // the way `claudeCode`/`codex` `enabled: false` does; without it a declined
621
+ // serve would be offered again on every `start`.
622
+ opencodeDeclined: z2.boolean().optional(),
326
623
  // Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
327
624
  // an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
328
625
  // never sees the key). The block's presence enables it; `enabled: false` turns
@@ -377,6 +674,9 @@ var companionConfigSchema = z2.object({
377
674
  function runnersEnabled(cfg) {
378
675
  return cfg.runners?.enabled === true;
379
676
  }
677
+ function dockerRunnersConfigured(cfg) {
678
+ return runnersEnabled(cfg) && cfg.runners?.provider === "docker";
679
+ }
380
680
  function runnerStateDir() {
381
681
  return process.env.CABANE_RUNNER_STATE_DIR || cabaneDir();
382
682
  }
@@ -386,9 +686,9 @@ function isCodexEnabled(cfg) {
386
686
  function isClaudeCodeConnected(cfg) {
387
687
  return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
388
688
  }
389
- function migrateConnectedHarnesses(cfg, claudeOnPath2) {
390
- if (cfg.claudeCode !== void 0) return null;
391
- return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
689
+ function migrateConnectedHarnesses(cfg) {
690
+ if (cfg.claudeCode !== void 0 || cfg.harnessesChosen) return null;
691
+ return { ...cfg, claudeCode: { enabled: true } };
392
692
  }
393
693
  function localAgentConfig(cfg, agent) {
394
694
  const map = cfg.agents ?? {};
@@ -420,7 +720,7 @@ function agentSandboxRows(cfg) {
420
720
  }
421
721
  function loadConfig() {
422
722
  const path = configPath();
423
- if (!existsSync(path)) return null;
723
+ if (!existsSync2(path)) return null;
424
724
  let raw;
425
725
  try {
426
726
  raw = readFileSync(path, "utf8");
@@ -447,14 +747,14 @@ function loadConfig() {
447
747
  );
448
748
  }
449
749
  throw new ConfigError(
450
- `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
750
+ `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("pair")}\` to re-pair, or \`${cabane("logout --purge")}\` to reset.`
451
751
  );
452
752
  }
453
753
  return result.data;
454
754
  }
455
755
  function saveConfig(cfg) {
456
756
  const path = configPath();
457
- mkdirSync(dirname(path), { recursive: true });
757
+ mkdirSync(dirname2(path), { recursive: true });
458
758
  try {
459
759
  chmodSync(cabaneDir(), 448);
460
760
  } catch {
@@ -479,321 +779,157 @@ function requireConfig() {
479
779
  const cfg = loadConfig();
480
780
  if (!cfg) {
481
781
  throw new ConfigError(
482
- "this companion is not paired. Run `cabane-companion pair` \u2014 it shows a short code \u2014 then enter that code in the cabane app (Settings \u2192 Companions) to connect this machine."
782
+ `this companion is not paired. Run \`${cabane("start")}\` \u2014 it shows a short code \u2014 then enter that code in the cabane app (Settings \u2192 Companions) to connect this machine.`
483
783
  );
484
784
  }
485
785
  return cfg;
486
786
  }
487
787
 
488
788
  // src/dashboard/server.ts
489
- import { dirname as dirname4, join as join5 } from "path";
490
- import { fileURLToPath as fileURLToPath2 } from "url";
789
+ import { dirname as dirname4, join as join7 } from "path";
790
+ import { fileURLToPath as fileURLToPath3 } from "url";
491
791
  import { serve } from "@hono/node-server";
492
792
  import { Hono } from "hono";
493
793
 
494
794
  // src/dashboard/routes.ts
495
- import { openSync, readSync, closeSync, fstatSync, existsSync as existsSync2 } from "fs";
795
+ import { openSync, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
496
796
  import { readFile } from "fs/promises";
497
- import { extname, join as join4, normalize } from "path";
797
+ import { extname, join as join6, normalize } from "path";
498
798
  import { streamSSE } from "hono/streaming";
499
799
 
500
- // src/harness-check.ts
501
- import { spawn as spawn3 } from "child_process";
800
+ // src/harness-copy.ts
801
+ import { homedir as homedir5 } from "os";
802
+ import { join as join4 } from "path";
502
803
 
503
- // src/harness-versions.ts
804
+ // src/harness-usable.ts
504
805
  import { spawn as spawn2 } from "child_process";
806
+
807
+ // src/harness-versions.ts
505
808
  var EMPTY = { claudeCode: null, opencode: null, codex: null };
506
- var CLI_PROBE_TIMEOUT_MS = 4e3;
507
809
  function parseVersionToken(raw) {
508
810
  if (!raw) return null;
509
811
  const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
510
812
  return m ? m[0] : null;
511
813
  }
512
- function probeCliPresence(command, spawnImpl = spawn2) {
513
- return new Promise((resolve2) => {
514
- let settled = false;
515
- let timer = null;
516
- const done = (result) => {
517
- if (!settled) {
518
- settled = true;
519
- if (timer) clearTimeout(timer);
520
- resolve2(result);
521
- }
522
- };
523
- let child;
524
- try {
525
- child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
526
- } catch {
527
- done({ status: "absent" });
528
- return;
529
- }
530
- let out = "";
531
- child.stdout?.on("data", (chunk) => {
532
- if (out.length < 4096) out += chunk.toString();
533
- });
534
- timer = setTimeout(() => {
535
- child.kill?.("SIGKILL");
536
- done({ status: "unusable" });
537
- }, CLI_PROBE_TIMEOUT_MS);
538
- timer.unref?.();
539
- child.once("error", () => done({ status: "absent" }));
540
- child.once("exit", (code) => {
541
- const version = code === 0 ? parseVersionToken(out) : null;
542
- done(version ? { status: "present", version } : { status: "unusable" });
543
- });
544
- });
545
- }
546
- function probeHarnessPresence(runtime, spawnImpl = spawn2) {
547
- return probeCliPresence(runtime === "codex" ? "codex" : "claude", spawnImpl);
548
- }
549
- async function probeCliVersion(command, spawnImpl = spawn2) {
550
- const result = await probeCliPresence(command, spawnImpl);
551
- return result.status === "present" ? result.version : null;
552
- }
553
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
554
- try {
555
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
556
- const res = await fetchImpl(`${base}/global/health`, {
557
- headers: { accept: "application/json" }
558
- });
559
- if (!res.ok) return null;
560
- return extractOpencodeVersion(await res.json());
561
- } catch {
562
- return null;
563
- }
564
- }
565
- function extractOpencodeVersion(json) {
566
- if (!json || typeof json !== "object") return null;
567
- const obj = json;
568
- const candidates = [obj.version];
569
- for (const v of Object.values(obj)) {
570
- if (v && typeof v === "object") candidates.push(v.version);
571
- }
572
- for (const c of candidates) {
573
- if (typeof c === "string") {
574
- const parsed = parseVersionToken(c);
575
- if (parsed) return parsed;
576
- }
577
- }
578
- return null;
579
- }
580
- async function probeHarnessVersions(opts, deps = {}) {
581
- const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
582
- const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
583
- const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
584
- const [claudeCode, codex, opencode] = await Promise.all([
585
- opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
586
- opts.codex ? safe(probeCodex) : Promise.resolve(null),
587
- opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
588
- ]);
589
- return { claudeCode, opencode, codex };
590
- }
591
- function emptyHarnessVersions() {
592
- return { ...EMPTY };
593
- }
594
- async function safe(fn) {
595
- try {
596
- return await fn();
597
- } catch {
598
- return null;
599
- }
600
- }
601
-
602
- // src/bundled-binaries.ts
603
- import { realpathSync, statSync } from "fs";
604
- import { createRequire } from "module";
605
- import { dirname as dirname2, join as join2 } from "path";
606
- import { fileURLToPath } from "url";
607
- function nodeResolveSelf(specifier) {
608
- const here = fileURLToPath(import.meta.url);
609
- if (typeof import.meta.resolve === "function") {
610
- try {
611
- return fileURLToPath(import.meta.resolve(specifier));
612
- } catch {
613
- }
614
- }
615
- try {
616
- return createRequire(here).resolve(specifier);
617
- } catch {
618
- }
619
- return walkNodeModules(specifier, here);
620
- }
621
- function walkNodeModules(pkg2, fromPath) {
622
- let dir2 = dirname2(fromPath);
623
- for (; ; ) {
624
- const manifest = join2(dir2, "node_modules", pkg2, "package.json");
625
- if (nodeIsFile(manifest)) {
626
- try {
627
- return realpathSync(manifest);
628
- } catch {
629
- return manifest;
630
- }
631
- }
632
- const parent = dirname2(dir2);
633
- if (parent === dir2) return null;
634
- dir2 = parent;
635
- }
636
- }
637
- function nodeResolve(specifier, fromPath) {
638
- try {
639
- return createRequire(fromPath).resolve(specifier);
640
- } catch {
641
- return null;
642
- }
643
- }
644
- function nodeIsFile(path) {
645
- try {
646
- return statSync(path).isFile();
647
- } catch {
648
- return false;
649
- }
650
- }
651
- function detectPreferMusl(platform = process.platform) {
652
- if (platform !== "linux") return false;
653
- const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
654
- const header = report?.header;
655
- return report != null && header?.glibcVersionRuntime === void 0;
656
- }
657
- function defaultResolveEnv() {
658
- return {
659
- platform: process.platform,
660
- arch: process.arch,
661
- preferMusl: detectPreferMusl(),
662
- // Both resolutions start from this module, which tsup bundles into
663
- // `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
664
- // root — so one environment serves the runtime checks and the install
665
- // verifier alike.
666
- resolveSelf: nodeResolveSelf,
667
- resolve: nodeResolve,
668
- isFile: nodeIsFile
669
- };
670
- }
671
- var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
672
- function claudePlatformPackages(platform, arch, preferMusl) {
673
- if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
674
- if (platform === "linux") {
675
- return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
676
- }
677
- return [`${CLAUDE_SDK}-${platform}-${arch}`];
678
- }
679
- function resolveClaude(env) {
680
- const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
681
- const base = {
682
- runtime: "claude-code",
683
- packageName: candidates[0] ?? null,
684
- platform: env.platform,
685
- arch: env.arch
686
- };
687
- const sdkEntry = env.resolveSelf(CLAUDE_SDK);
688
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
689
- let sawPackage = null;
690
- for (const pkg2 of candidates) {
691
- const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
692
- if (!manifest) continue;
693
- sawPackage ??= pkg2;
694
- const exe = join2(dirname2(manifest), env.platform === "win32" ? "claude.exe" : "claude");
695
- if (env.isFile(exe)) {
696
- return { ...base, packageName: pkg2, status: "present", path: exe };
697
- }
698
- }
699
- return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
700
- }
701
- var CODEX_SDK = "@openai/codex-sdk";
702
- var CODEX_CLI = "@openai/codex";
703
- function codexTargetTriple(platform, arch) {
704
- if (platform === "linux" || platform === "android") {
705
- if (arch === "x64") return "x86_64-unknown-linux-musl";
706
- if (arch === "arm64") return "aarch64-unknown-linux-musl";
814
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
815
+ try {
816
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
817
+ const res = await fetchImpl(`${base}/global/health`, {
818
+ headers: { accept: "application/json" }
819
+ });
820
+ if (!res.ok) return null;
821
+ return extractOpencodeVersion(await res.json());
822
+ } catch {
707
823
  return null;
708
824
  }
709
- if (platform === "darwin") {
710
- if (arch === "x64") return "x86_64-apple-darwin";
711
- if (arch === "arm64") return "aarch64-apple-darwin";
712
- return null;
825
+ }
826
+ function extractOpencodeVersion(json) {
827
+ if (!json || typeof json !== "object") return null;
828
+ const obj = json;
829
+ const candidates = [obj.version];
830
+ for (const v of Object.values(obj)) {
831
+ if (v && typeof v === "object") candidates.push(v.version);
713
832
  }
714
- if (platform === "win32") {
715
- if (arch === "x64") return "x86_64-pc-windows-msvc";
716
- if (arch === "arm64") return "aarch64-pc-windows-msvc";
717
- return null;
833
+ for (const c of candidates) {
834
+ if (typeof c === "string") {
835
+ const parsed = parseVersionToken(c);
836
+ if (parsed) return parsed;
837
+ }
718
838
  }
719
839
  return null;
720
840
  }
721
- var CODEX_PACKAGE_BY_TRIPLE = {
722
- "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
723
- "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
724
- "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
725
- "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
726
- "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
727
- "aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
841
+ function emptyHarnessVersions() {
842
+ return { ...EMPTY };
843
+ }
844
+
845
+ // src/harness-usable.ts
846
+ function isUsable(check) {
847
+ return check.status === "signed_in" || check.status === "api_key";
848
+ }
849
+ var API_KEY_VARIABLES = {
850
+ "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
851
+ codex: ["CODEX_API_KEY"]
728
852
  };
729
- function resolveCodex(env) {
730
- const triple = codexTargetTriple(env.platform, env.arch);
731
- const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
732
- const base = {
733
- runtime: "codex",
734
- packageName: pkg2 ?? null,
735
- platform: env.platform,
736
- arch: env.arch
737
- };
738
- if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
739
- const sdkEntry = env.resolveSelf(CODEX_SDK);
740
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
741
- const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
742
- if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
743
- const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
744
- if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
745
- const packageRoot = join2(dirname2(platformManifest), "vendor", triple);
746
- const exeName = env.platform === "win32" ? "codex.exe" : "codex";
747
- const current = join2(packageRoot, "bin", exeName);
748
- if (env.isFile(current) && env.isFile(join2(packageRoot, "codex-package.json"))) {
749
- return { ...base, status: "present", path: current };
750
- }
751
- const legacy = join2(packageRoot, "codex", exeName);
752
- if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
753
- return { ...base, status: "executable_missing", path: null };
853
+ function apiKeyVariable(runtime, env = process.env) {
854
+ return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
754
855
  }
755
- function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
756
- try {
757
- return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
758
- } catch {
856
+ var CHECK_TIMEOUT_MS = 5e3;
857
+ async function checkHarnessUsable(runtime, deps = {}) {
858
+ const env = deps.env ?? process.env;
859
+ const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
860
+ const run = deps.run ?? runBounded;
861
+ const resolution = resolveBundled(runtime);
862
+ if (resolution.status !== "present" || !resolution.path) {
759
863
  return {
760
- runtime,
761
- status: "platform_package_missing",
762
- path: null,
763
- packageName: null,
764
- platform: env.platform,
765
- arch: env.arch
864
+ status: "bundle_missing",
865
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
766
866
  };
767
867
  }
868
+ const path = resolution.path;
869
+ const version = run(path, ["--version"], env).then(
870
+ (r) => r.code === 0 ? parseVersionToken(r.output) : null
871
+ );
872
+ const variable = apiKeyVariable(runtime, env);
873
+ if (variable) return { status: "api_key", variable, version: await version };
874
+ const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
875
+ const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
876
+ if (answer.error) return { status: "check_failed", reason: answer.error };
877
+ const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
878
+ if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
879
+ return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
880
+ }
881
+ function readClaudeStatus(answer) {
882
+ const start = answer.output.indexOf("{");
883
+ const end = answer.output.lastIndexOf("}");
884
+ if (start < 0 || end < start) return "unreadable";
885
+ try {
886
+ const parsed = JSON.parse(answer.output.slice(start, end + 1));
887
+ const loggedIn = parsed?.loggedIn;
888
+ return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
889
+ } catch {
890
+ return "unreadable";
891
+ }
768
892
  }
769
- var HARNESS_LABEL = {
770
- "claude-code": "Claude Code",
771
- codex: "Codex"
772
- };
773
- var HARNESS_INTENT_WORD = {
774
- "claude-code": "connected",
775
- codex: "enabled"
776
- };
777
- function missingNoun(status) {
778
- return status === "executable_missing" ? "executable" : "platform package";
779
- }
780
- function repairCommands() {
781
- return ["npm i -g @cabane/companion --include=optional"];
782
- }
783
- function repairAction() {
784
- return `Reinstall with \`${repairCommands()[0]}\`.`;
785
- }
786
- function startupWarning(r) {
787
- const label = HARNESS_LABEL[r.runtime];
788
- 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.`;
893
+ function readCodexStatus(answer) {
894
+ if (answer.code === 0) return true;
895
+ if (/not logged in/i.test(answer.output)) return false;
896
+ if (/config/i.test(answer.output)) return "config";
897
+ return "unreadable";
789
898
  }
790
- function harnessIssueNote(runtime, status) {
791
- const intent = HARNESS_INTENT_WORD[runtime];
792
- const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
793
- return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status)} is missing. ${repairAction()} Then restart the companion.`;
899
+ function runBounded(path, args, env) {
900
+ return new Promise((resolve2) => {
901
+ let settled = false;
902
+ let out = "";
903
+ const done = (result) => {
904
+ if (settled) return;
905
+ settled = true;
906
+ clearTimeout(timer);
907
+ resolve2(result);
908
+ };
909
+ let child;
910
+ try {
911
+ child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
912
+ } catch {
913
+ resolve2({ code: null, output: "", error: "spawn" });
914
+ return;
915
+ }
916
+ const capture = (chunk) => {
917
+ if (out.length < 8192) out += chunk.toString();
918
+ };
919
+ child.stdout?.on("data", capture);
920
+ child.stderr?.on("data", capture);
921
+ const timer = setTimeout(() => {
922
+ child.kill("SIGKILL");
923
+ done({ code: null, output: out, error: "timeout" });
924
+ }, CHECK_TIMEOUT_MS);
925
+ timer.unref?.();
926
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
927
+ child.once("close", (code) => done({ code, output: out }));
928
+ });
794
929
  }
795
- function turnIncompleteCopy(runtime) {
796
- return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
930
+ function checkVersion(check) {
931
+ if (!check) return null;
932
+ return check.status === "signed_in" || check.status === "api_key" || check.status === "signed_out" ? check.version : null;
797
933
  }
798
934
 
799
935
  // src/manifest.ts
@@ -819,7 +955,7 @@ function runtimeIssuesFor(gates) {
819
955
  for (const name of ["claude-code", "codex"]) {
820
956
  const gate = name === "codex" ? gates.codex : gates.claudeCode;
821
957
  if (!gate.intended || gate.bundled === "present") continue;
822
- issues.push({ name, reason: gate.bundled });
958
+ issues.push({ name, reason: gate.bundled, install: gates.install });
823
959
  }
824
960
  return issues;
825
961
  }
@@ -851,13 +987,15 @@ function deriveHarnessSnapshot(signals) {
851
987
  }
852
988
  function deriveClaudeCode(signals, manifestHas) {
853
989
  const base = { runtime: "claude-code", label: LABELS["claude-code"] };
990
+ const version = checkVersion(signals.claudeCheck);
854
991
  if (manifestHas) {
855
992
  return {
856
993
  ...base,
857
994
  state: "exposed",
858
- version: signals.claudeVersion,
859
- detail: "Claude Code is connected and exposed to Cabane.",
860
- enable: null
995
+ version,
996
+ detail: exposedDetail("claude-code", signals.claudeCheck),
997
+ enable: null,
998
+ signIn: signals.claudeCheck?.status ?? null
861
999
  };
862
1000
  }
863
1001
  if (signals.claudeCodeConnected) {
@@ -866,37 +1004,56 @@ function deriveClaudeCode(signals, manifestHas) {
866
1004
  state: "needs_attention",
867
1005
  version: null,
868
1006
  detail: harnessIssueNote("claude-code", signals.claudeBundled),
869
- enable: null
1007
+ enable: null,
1008
+ signIn: signals.claudeCheck?.status ?? null
870
1009
  };
871
1010
  }
872
- if (signals.claudeOnPath) {
1011
+ if (signals.claudeCheck && isUsable(signals.claudeCheck)) {
873
1012
  return {
874
1013
  ...base,
875
1014
  state: "detected_not_exposed",
876
- version: signals.claudeVersion,
877
- // Connecting records INTENT, not runnability: with the bundled binary
878
- // missing a connect succeeds and nothing is advertised, so say so.
879
- detail: signals.claudeBundled === "present" ? "Claude Code is installed here but not connected yet. Connect it to offer Claude Code from this device." : `Claude Code is installed here but not connected yet \u2014 and @cabane/companion's bundled Claude Code ${missingNoun(signals.claudeBundled)} is missing, so connecting alone won't make it runnable. ${repairAction()}`,
880
- enable: "claude-code"
1015
+ version,
1016
+ detail: `${usableLine("claude-code", signals.claudeCheck)} Connect it to offer Claude Code from this device.`,
1017
+ enable: "claude-code",
1018
+ signIn: signals.claudeCheck?.status ?? null
881
1019
  };
882
1020
  }
883
1021
  return {
884
1022
  ...base,
885
1023
  state: "not_detected",
886
1024
  version: null,
887
- detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
888
- enable: null
1025
+ detail: notUsableDetail("claude-code", signals.claudeCheck),
1026
+ enable: null,
1027
+ signIn: signals.claudeCheck?.status ?? null
889
1028
  };
890
1029
  }
1030
+ function exposedDetail(runtime, check) {
1031
+ const label = LABELS[runtime];
1032
+ const problem = check ? unusableReason(runtime, check) : null;
1033
+ if (problem) return problem;
1034
+ if (check?.status === "signed_out") {
1035
+ const login = runtime === "codex" ? "codex login" : "claude auth login";
1036
+ return `${label} is connected, but signed out on this computer. Run \`${login}\` to use it again.`;
1037
+ }
1038
+ if (check?.status === "api_key") return `${label} is connected with an API key configured.`;
1039
+ return `${label} is connected and exposed to Cabane.`;
1040
+ }
1041
+ function notUsableDetail(runtime, check) {
1042
+ const problem = check ? unusableReason(runtime, check) : null;
1043
+ if (problem) return problem;
1044
+ return runtime === "codex" ? "Not signed in on this computer. Sign in with `codex login`, then enable it here." : "Not signed in on this computer. Sign in with `claude auth login`, then connect it here.";
1045
+ }
891
1046
  function deriveCodex(signals, manifestHas) {
892
1047
  const base = { runtime: "codex", label: LABELS.codex };
1048
+ const version = checkVersion(signals.codexCheck);
893
1049
  if (manifestHas) {
894
1050
  return {
895
1051
  ...base,
896
1052
  state: "exposed",
897
- version: signals.codexVersion,
898
- detail: "Codex is enabled and exposed to Cabane.",
899
- enable: null
1053
+ version,
1054
+ detail: exposedDetail("codex", signals.codexCheck),
1055
+ enable: null,
1056
+ signIn: signals.codexCheck?.status ?? null
900
1057
  };
901
1058
  }
902
1059
  if (signals.codexEnabled) {
@@ -905,25 +1062,27 @@ function deriveCodex(signals, manifestHas) {
905
1062
  state: "needs_attention",
906
1063
  version: null,
907
1064
  detail: harnessIssueNote("codex", signals.codexBundled),
908
- enable: null
1065
+ enable: null,
1066
+ signIn: signals.codexCheck?.status ?? null
909
1067
  };
910
1068
  }
911
- if (signals.codexOnPath) {
1069
+ if (signals.codexCheck && isUsable(signals.codexCheck)) {
912
1070
  return {
913
1071
  ...base,
914
1072
  state: "detected_not_exposed",
915
- version: signals.codexVersion,
916
- // As for Claude Code: enabling is intent, not launchability.
917
- detail: signals.codexBundled === "present" ? "Codex is installed but not enabled yet. Turn it on to offer Codex from this device." : `Codex is installed but not enabled yet \u2014 and @cabane/companion's bundled Codex ${missingNoun(signals.codexBundled)} is missing, so enabling alone won't make it runnable. ${repairAction()}`,
918
- enable: "codex"
1073
+ version,
1074
+ detail: `${usableLine("codex", signals.codexCheck)} Turn it on to offer Codex from this device.`,
1075
+ enable: "codex",
1076
+ signIn: signals.codexCheck?.status ?? null
919
1077
  };
920
1078
  }
921
1079
  return {
922
1080
  ...base,
923
1081
  state: "not_detected",
924
1082
  version: null,
925
- detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
926
- enable: null
1083
+ detail: notUsableDetail("codex", signals.codexCheck),
1084
+ enable: null,
1085
+ signIn: signals.codexCheck?.status ?? null
927
1086
  };
928
1087
  }
929
1088
  function deriveOpencode(signals, manifestHas) {
@@ -935,7 +1094,8 @@ function deriveOpencode(signals, manifestHas) {
935
1094
  state: "exposed",
936
1095
  version: signals.opencodeVersion,
937
1096
  detail: "An opencode server is reachable and exposed to Cabane.",
938
- enable: null
1097
+ enable: null,
1098
+ signIn: null
939
1099
  };
940
1100
  }
941
1101
  return {
@@ -943,7 +1103,8 @@ function deriveOpencode(signals, manifestHas) {
943
1103
  state: "needs_attention",
944
1104
  version: null,
945
1105
  detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
946
- enable: null
1106
+ enable: null,
1107
+ signIn: null
947
1108
  };
948
1109
  }
949
1110
  if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
@@ -952,7 +1113,8 @@ function deriveOpencode(signals, manifestHas) {
952
1113
  state: "detected_not_exposed",
953
1114
  version: signals.opencodeVersion,
954
1115
  detail: signals.opencodeDetectedUrl,
955
- enable: "opencode"
1116
+ enable: "opencode",
1117
+ signIn: null
956
1118
  };
957
1119
  }
958
1120
  return {
@@ -960,7 +1122,8 @@ function deriveOpencode(signals, manifestHas) {
960
1122
  state: "not_detected",
961
1123
  version: null,
962
1124
  detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
963
- enable: "opencode"
1125
+ enable: "opencode",
1126
+ signIn: null
964
1127
  };
965
1128
  }
966
1129
  var PROBE_TIMEOUT_MS = 4e3;
@@ -979,31 +1142,20 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
979
1142
  return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
980
1143
  }
981
1144
  async function probeHarnessSignals(cfg, deps = {}) {
982
- const probePresence = deps.probePresence ?? probeHarnessPresence;
983
1145
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
984
1146
  const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
985
1147
  const configuredServerUrl = cfg.opencode?.serverUrl;
986
1148
  const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
987
- const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
988
- deps.presence?.["claude-code"] ?? withTimeout(probePresence("claude-code"), { status: "absent" }),
989
- deps.presence?.codex ?? withTimeout(probePresence("codex"), { status: "absent" }),
990
- configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
991
- ]);
992
- const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
993
- const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
1149
+ const opencodeVersion = configuredServerUrl ? await withTimeout(probeOpencode(opencodeProbeUrl), null) : await probeDefaultOpencodeVersion(probeOpencode);
994
1150
  return {
995
- claudeOnPath: claudePresence.status === "present",
996
- claudeVersion,
997
- // The user's opt-in, the manifest gate; the probe above only suggests.
1151
+ claudeCheck: deps.checks?.["claude-code"] ?? null,
1152
+ // The user's opt-in, the manifest gate; the check above only suggests.
998
1153
  claudeCodeConnected: isClaudeCodeConnected(cfg),
999
- // The executable a Claude turn launches, resolved with every probe so a
1154
+ // The executable a Claude turn launches, resolved with every refresh so a
1000
1155
  // binary that disappears is caught on the next beat, not at turn time.
1001
1156
  claudeBundled: resolveBundled("claude-code").status,
1002
1157
  codexBundled: resolveBundled("codex").status,
1003
- // A parseable `codex --version` is our presence signal (presence alone never
1004
- // exposes codex; its config flag is the manifest gate either way).
1005
- codexOnPath: codexVersion !== null,
1006
- codexVersion,
1158
+ codexCheck: deps.checks?.codex ?? null,
1007
1159
  codexEnabled: isCodexEnabled(cfg),
1008
1160
  opencodeConfigured: !!configuredServerUrl,
1009
1161
  // A version came back ⟺ the serve answered its health endpoint.
@@ -1036,104 +1188,88 @@ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
1036
1188
  });
1037
1189
  }
1038
1190
 
1039
- // src/harness-check.ts
1040
- var CHECK_TIMEOUT_MS = 4e3;
1041
- async function shakeOutHarness(runtime, cfg, deps = {}) {
1042
- const run = deps.run ?? runBounded;
1043
- const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
1044
- try {
1045
- if (runtime === "opencode") {
1046
- const url = cfg.opencode?.serverUrl;
1047
- if (!url) return "absent";
1048
- return await probeOpencode(url) !== null ? "ok" : "failed";
1049
- }
1050
- const { auth } = runtime === "codex" ? {
1051
- auth: ["codex", ["login", "status"]]
1052
- } : {
1053
- auth: ["claude", ["auth", "status"]]
1054
- };
1055
- const presence = deps.presence ?? await (deps.probePresence ?? probeHarnessPresence)(runtime);
1056
- if (presence.status !== "present") return presence.status;
1057
- const authRun = await run(auth[0], [...auth[1]]);
1058
- if (authRun.code === 0) return "ok";
1059
- if (looksUnsupported(authRun.output)) {
1060
- return "unverified";
1061
- }
1062
- return "failed";
1063
- } catch {
1064
- return "unverified";
1065
- }
1191
+ // src/harness-copy.ts
1192
+ function claudeLoginCommand(path = process.env.PATH) {
1193
+ if (firstOnPath("claude", path)) return "claude auth login";
1194
+ return "~/.local/bin/claude auth login";
1066
1195
  }
1067
- function connectedLine(runtime, verdict) {
1068
- if (verdict === "absent" || verdict === "unusable") {
1069
- throw new Error("an unavailable harness cannot be connected");
1070
- }
1196
+ function ctx(c = {}) {
1197
+ return {
1198
+ cmd: c.cmd ?? ((rest) => cabane(rest)),
1199
+ claudeLogin: c.claudeLogin ?? claudeLoginCommand()
1200
+ };
1201
+ }
1202
+ function loginCommand(runtime, c = {}) {
1203
+ return runtime === "codex" ? "codex login" : ctx(c).claudeLogin;
1204
+ }
1205
+ function usableLine(runtime, check) {
1071
1206
  const label = HARNESS_LABELS[runtime];
1072
- if (verdict !== "failed") return `${label} connected.`;
1073
- return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
1207
+ if (!check) return `${label} is running on this computer.`;
1208
+ const version = "version" in check && check.version ? ` (${check.version})` : "";
1209
+ if (check.status === "api_key")
1210
+ return `${label} has an API key configured on this computer${version}.`;
1211
+ return `${label} is signed in on this computer${version}.`;
1074
1212
  }
1075
- var FAILED_SUFFIX = {
1076
- "claude-code": "it doesn\u2019t look signed in yet. Run `claude` once and sign in, then it\u2019s ready.",
1077
- codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
1078
- opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
1079
- };
1080
- function presenceReason(runtime, status) {
1081
- if (runtime === "opencode") {
1082
- return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
1083
- }
1213
+ function failureText(runtime, reason) {
1084
1214
  const label = HARNESS_LABELS[runtime];
1085
- return status === "absent" ? `${label} isn't installed on this machine.` : `${label} is installed here but didn't report a version Cabane can read.`;
1215
+ switch (reason) {
1216
+ case "timeout":
1217
+ return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
1218
+ case "spawn":
1219
+ return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairCommands()[0]} , then try again.`;
1220
+ case "config":
1221
+ 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.`;
1222
+ case "unreadable":
1223
+ 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.`;
1224
+ }
1086
1225
  }
1087
- var CLI_PRESENCE_ACTION = "Install it and run this again.";
1088
- var DASHBOARD_PRESENCE_ACTION = "Install it, then refresh.";
1089
- function presenceRefusal(runtime, status, action) {
1090
- const reason = presenceReason(runtime, status);
1091
- return runtime === "opencode" ? reason : `${reason} ${action}`;
1226
+ function bundleMissingText(runtime) {
1227
+ const label = HARNESS_LABELS[runtime];
1228
+ return `This Cabane install is missing its bundled ${label}. Repair it with ${repairCommands()[0]} , then try again.`;
1092
1229
  }
1093
- function looksUnsupported(output) {
1094
- return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
1095
- output
1096
- );
1230
+ function unusableReason(runtime, check) {
1231
+ if (check.status === "bundle_missing") return bundleMissingText(runtime);
1232
+ if (check.status === "check_failed") return failureText(runtime, check.reason);
1233
+ return null;
1097
1234
  }
1098
- function runBounded(command, args) {
1099
- return new Promise((resolve2) => {
1100
- let settled = false;
1101
- const done = (result) => {
1102
- if (settled) return;
1103
- settled = true;
1104
- clearTimeout(timer);
1105
- resolve2(result);
1106
- };
1107
- let child;
1108
- try {
1109
- child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1110
- } catch {
1111
- resolve2({ code: null, output: "", error: "spawn" });
1112
- return;
1113
- }
1114
- let out = "";
1115
- const capture = (chunk) => {
1116
- if (out.length < 4096) out += chunk.toString();
1235
+ function connectLines(outcome, opts = {}) {
1236
+ const c = ctx(opts);
1237
+ if (outcome.kind === "opencode") return { ok: false, lines: [outcome.message] };
1238
+ const label = HARNESS_LABELS[outcome.runtime];
1239
+ if (outcome.kind === "connected") return { ok: true, lines: [`${label} connected.`] };
1240
+ if (outcome.kind === "already") {
1241
+ const where = outcome.deviceLabel ?? "this computer";
1242
+ const how = outcome.check?.status === "api_key" ? ", with an API key configured" : outcome.check?.status === "signed_in" ? ", and signed in" : "";
1243
+ return { ok: true, lines: [`${label} is already connected on ${where}${how}.`] };
1244
+ }
1245
+ const { runtime, check, connected } = outcome;
1246
+ const problem = unusableReason(runtime, check);
1247
+ if (problem) return { ok: false, lines: [problem] };
1248
+ if (opts.windowKey) {
1249
+ return {
1250
+ ok: false,
1251
+ lines: [
1252
+ `${label} has an API key in this window, but the running Cabane can't see it.`,
1253
+ `Run ${c.cmd("stop")} then ${c.cmd("start")} here, and it will.`
1254
+ ]
1117
1255
  };
1118
- child.stdout?.on("data", capture);
1119
- child.stderr?.on("data", capture);
1120
- const timer = setTimeout(() => {
1121
- child.kill("SIGKILL");
1122
- done({ code: null, output: out, error: "timeout" });
1123
- }, CHECK_TIMEOUT_MS);
1124
- timer.unref?.();
1125
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1126
- child.once("exit", (code) => done({ code, output: out }));
1127
- });
1256
+ }
1257
+ const again = c.cmd(`connect ${runtime}`);
1258
+ return {
1259
+ ok: false,
1260
+ lines: [
1261
+ connected ? `${label} is connected here but signed out. Run ${loginCommand(runtime, c)} , then ${again} again.` : `${label} isn't signed in on this computer. Run ${loginCommand(runtime, c)} , then ${again} again.`
1262
+ ]
1263
+ };
1128
1264
  }
1129
1265
 
1130
1266
  // src/logger.ts
1131
1267
  import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
1132
- import { dirname as dirname3, join as join3 } from "path";
1268
+ import { dirname as dirname3, join as join5 } from "path";
1133
1269
  import { stripVTControlCharacters } from "util";
1134
1270
  import pino from "pino";
1135
1271
  function companionLogPath() {
1136
- return join3(cabaneDir(), "companion.log");
1272
+ return join5(cabaneDir(), "companion.log");
1137
1273
  }
1138
1274
  function humanText(value) {
1139
1275
  return stripVTControlCharacters(String(value ?? "")).replace(
@@ -1486,14 +1622,14 @@ var CONTENT_TYPES = {
1486
1622
  function registerRoutes(app, deps) {
1487
1623
  const { supervisor, hub, staticDir } = deps;
1488
1624
  app.get("/", async (c) => {
1489
- const html = await readFile(join4(staticDir, "index.html"), "utf8");
1625
+ const html = await readFile(join6(staticDir, "index.html"), "utf8");
1490
1626
  return c.html(html);
1491
1627
  });
1492
1628
  app.get("/static/:file", async (c) => {
1493
1629
  const file = c.req.param("file");
1494
- const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
1495
- const full = join4(staticDir, safe3);
1496
- if (!full.startsWith(staticDir) || !existsSync2(full)) return c.notFound();
1630
+ const safe2 = normalize(file).replace(/^(\.\.[/\\])+/, "");
1631
+ const full = join6(staticDir, safe2);
1632
+ if (!full.startsWith(staticDir) || !existsSync3(full)) return c.notFound();
1497
1633
  const body = await readFile(full);
1498
1634
  const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
1499
1635
  c.header("content-type", type);
@@ -1546,20 +1682,9 @@ function registerRoutes(app, deps) {
1546
1682
  app.post("/api/harnesses/enable", async (c) => {
1547
1683
  const body = await readJson(c);
1548
1684
  const runtime = body.runtime;
1549
- if (runtime === "claude-code") {
1550
- const result = await supervisor.enableHarness({ runtime: "claude-code" });
1551
- if (!result.ok) {
1552
- const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
1553
- return c.json({ error }, 400);
1554
- }
1555
- return c.json({ ok: true, status: hub.statusJson() });
1556
- }
1557
- if (runtime === "codex") {
1558
- const result = await supervisor.enableHarness({ runtime: "codex" });
1559
- if (!result.ok) {
1560
- const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
1561
- return c.json({ error }, 400);
1562
- }
1685
+ if (runtime === "claude-code" || runtime === "codex") {
1686
+ const result = await supervisor.enableHarness({ runtime });
1687
+ if (!result.ok) return c.json({ error: connectLines(result.outcome).lines.join(" ") }, 400);
1563
1688
  return c.json({ ok: true, status: hub.statusJson() });
1564
1689
  }
1565
1690
  if (runtime === "opencode") {
@@ -1570,7 +1695,7 @@ function registerRoutes(app, deps) {
1570
1695
  runtime: "opencode",
1571
1696
  serverUrl: body.serverUrl
1572
1697
  });
1573
- if (!result.ok) return c.json({ error: result.error }, 400);
1698
+ if (!result.ok) return c.json({ error: connectLines(result.outcome).lines.join(" ") }, 400);
1574
1699
  return c.json({ ok: true, status: hub.statusJson() });
1575
1700
  }
1576
1701
  return c.json({ error: `can't enable "${String(runtime)}" from the app` }, 400);
@@ -1626,7 +1751,7 @@ function clampLimit(raw, fallback, max = 200) {
1626
1751
  return Math.min(Math.floor(n), max);
1627
1752
  }
1628
1753
  function tailFile(path, lines2) {
1629
- if (!existsSync2(path)) return [];
1754
+ if (!existsSync3(path)) return [];
1630
1755
  const MAX_BYTES = 256 * 1024;
1631
1756
  let fd;
1632
1757
  try {
@@ -1715,14 +1840,14 @@ function isAddrInUse(err) {
1715
1840
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
1716
1841
  }
1717
1842
  function resolveStaticDir() {
1718
- return join5(dirname4(fileURLToPath2(import.meta.url)), "static");
1843
+ return join7(dirname4(fileURLToPath3(import.meta.url)), "static");
1719
1844
  }
1720
1845
 
1721
1846
  // src/control-socket.ts
1722
1847
  import { createHash } from "crypto";
1723
- import { chmodSync as chmodSync2, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
1848
+ import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
1724
1849
  import { createServer, connect } from "net";
1725
- import { join as join6 } from "path";
1850
+ import { join as join8 } from "path";
1726
1851
  var CONTROL_TIMEOUT_MS = 1e3;
1727
1852
  var MAX_UNIX_SOCKET_PATH_BYTES = 103;
1728
1853
  function controlSocketPath() {
@@ -1731,16 +1856,16 @@ function controlSocketPath() {
1731
1856
  if (process.platform === "win32") {
1732
1857
  return `\\\\.\\pipe\\cabane-companion-${key}`;
1733
1858
  }
1734
- const besideRuntime = join6(dir2, "companion.sock");
1859
+ const besideRuntime = join8(dir2, "companion.sock");
1735
1860
  if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
1736
1861
  const filename = `cabane-companion-${key}.sock`;
1737
- const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join6(process.env.XDG_RUNTIME_DIR, filename) : null;
1738
- return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join6("/tmp", filename);
1862
+ const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync4(process.env.XDG_RUNTIME_DIR) ? join8(process.env.XDG_RUNTIME_DIR, filename) : null;
1863
+ return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join8("/tmp", filename);
1739
1864
  }
1740
1865
  async function startControlServer(handlers) {
1741
1866
  const path = controlSocketPath();
1742
1867
  mkdirSync3(cabaneDir(), { recursive: true });
1743
- if (process.platform !== "win32" && existsSync3(path)) {
1868
+ if (process.platform !== "win32" && existsSync4(path)) {
1744
1869
  const alive = await ping(path);
1745
1870
  if (alive) throw new Error(`another companion is already listening on ${path}`);
1746
1871
  rmSync2(path, { force: true });
@@ -1800,7 +1925,9 @@ async function serveConnection(socket, handlers) {
1800
1925
  return;
1801
1926
  }
1802
1927
  if (req.cmd === "connect") {
1803
- const result = await handlers.connect(req.runtime, req.serverUrl);
1928
+ const outcome = await handlers.connect(req.runtime, req.serverUrl);
1929
+ const { ok, lines: lines2 } = connectLines(outcome, { cmd: (rest) => `cabane ${rest}` });
1930
+ const result = { ok, message: lines2.join(" "), outcome };
1804
1931
  reply(socket, result);
1805
1932
  return;
1806
1933
  }
@@ -1890,77 +2017,38 @@ async function ping(path) {
1890
2017
  }
1891
2018
 
1892
2019
  // src/prereqs.ts
1893
- async function claudeOnPath() {
1894
- return (await probeHarnessPresence("claude-code")).status === "present";
1895
- }
1896
- async function codexOnPath() {
1897
- return (await probeHarnessPresence("codex")).status === "present";
1898
- }
1899
- async function requireStartConfig(deps = {}) {
2020
+ function requireStartConfig(deps = {}) {
1900
2021
  const requireCfg = deps.requireCfg ?? requireConfig;
1901
- const probeClaude = deps.probeClaude ?? claudeOnPath;
1902
2022
  const save2 = deps.save ?? saveConfig;
1903
2023
  const cfg = requireCfg();
1904
- const claudeOnPathResult = await probeClaude();
1905
- const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
1906
- if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
2024
+ const migrated = migrateConnectedHarnesses(cfg);
2025
+ if (!migrated) return cfg;
1907
2026
  save2(migrated);
1908
- deps.onMigrated?.(migrated, claudeOnPathResult);
1909
- return { cfg: migrated, claudeOnPath: claudeOnPathResult };
2027
+ deps.onMigrated?.(migrated);
2028
+ return migrated;
1910
2029
  }
1911
- async function warnAboutHarnessReadiness(cfg, deps = {}) {
1912
- const probeClaude = deps.probeClaude ?? claudeOnPath;
1913
- const probeCodex = deps.probeCodex ?? codexOnPath;
2030
+ function warnAboutHarnessReadiness(cfg, deps = {}) {
1914
2031
  const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
1915
2032
  const warn = deps.warn ?? ((message2) => process.stderr.write(`${message2}
1916
2033
  `));
1917
- const connected = [
1918
- ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
1919
- ...isCodexEnabled(cfg) ? ["Codex"] : [],
1920
- ...cfg.opencode ? ["opencode"] : []
1921
- ];
1922
- if (connected.length > 0) {
1923
- for (const runtime of ["claude-code", "codex"]) {
1924
- const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
1925
- if (!intended) continue;
1926
- const resolution = resolveBundled(runtime);
1927
- if (resolution.status !== "present") warn(startupWarning(resolution));
1928
- }
2034
+ const connected = isClaudeCodeConnected(cfg) || isCodexEnabled(cfg) || Boolean(cfg.opencode?.serverUrl);
2035
+ if (!connected) {
2036
+ warn(
2037
+ `No agent is connected on this device yet, so no agent turn can run here. Sign in to one, then connect it with \`${cabane("connect claude-code")}\` (or codex, or opencode).`
2038
+ );
1929
2039
  return;
1930
2040
  }
1931
- const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
1932
- const found = [
1933
- ...claudeInstalled ? [
1934
- {
1935
- label: "Claude Code",
1936
- command: "`cabane-companion connect claude-code`",
1937
- bundled: resolveBundled("claude-code").status
1938
- }
1939
- ] : [],
1940
- ...codexInstalled ? [
1941
- {
1942
- label: "Codex",
1943
- command: "`cabane-companion connect codex`",
1944
- bundled: resolveBundled("codex").status
1945
- }
1946
- ] : []
1947
- ];
1948
- const runnable = found.filter((h) => h.bundled === "present");
1949
- const incomplete = found.filter((h) => h.bundled !== "present");
1950
- const incompleteNote = incomplete.map(
1951
- (h) => `${h.label} is installed here, but @cabane/companion's bundled ${h.label} ${missingNoun(h.bundled)} is missing, so connecting it alone won't make it runnable.`
1952
- ).join(" ");
1953
- const guidance = runnable.length > 0 ? `We found ${runnable.map((h) => h.label).join(" and ")} on this machine \u2014 connect ${runnable.length > 1 ? "one with " : "it with "}${runnable.map((h) => h.command).join(" or ")} and turns start routing here.${incompleteNote ? ` ${incompleteNote} ${repairAction()}` : ""}` : incomplete.length > 0 ? (
1954
- // Their machine is fine; this companion's install is not. Repair that,
1955
- // then connect — no "install a harness" they already have.
1956
- `${incompleteNote} ${repairAction()} Then connect it with ${incomplete.map((h) => h.command).join(" or ")}.`
1957
- ) : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).";
1958
- warn(`No harness is connected on this device yet, so no agent turn can run here. ${guidance}`);
2041
+ for (const runtime of ["claude-code", "codex"]) {
2042
+ const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
2043
+ if (!intended) continue;
2044
+ const resolution = resolveBundled(runtime);
2045
+ if (resolution.status !== "present") warn(startupWarning(resolution));
2046
+ }
1959
2047
  }
1960
2048
 
1961
2049
  // src/runtime-file.ts
1962
2050
  import {
1963
- existsSync as existsSync4,
2051
+ existsSync as existsSync5,
1964
2052
  readFileSync as readFileSync2,
1965
2053
  rmSync as rmSync3,
1966
2054
  writeFileSync as writeFileSync2,
@@ -1968,10 +2056,10 @@ import {
1968
2056
  openSync as openSync2,
1969
2057
  closeSync as closeSync2
1970
2058
  } from "fs";
1971
- import { join as join7 } from "path";
2059
+ import { join as join9 } from "path";
1972
2060
  var PROBE_TIMEOUT_MS2 = 1e3;
1973
2061
  function runtimePath() {
1974
- return join7(cabaneDir(), "runtime.json");
2062
+ return join9(cabaneDir(), "runtime.json");
1975
2063
  }
1976
2064
  function serialize(state) {
1977
2065
  return JSON.stringify(state, null, 2) + "\n";
@@ -2000,11 +2088,11 @@ function acquireRuntimeState(state) {
2000
2088
  }
2001
2089
  function clearRuntimeState() {
2002
2090
  const path = runtimePath();
2003
- if (existsSync4(path)) rmSync3(path, { force: true });
2091
+ if (existsSync5(path)) rmSync3(path, { force: true });
2004
2092
  }
2005
2093
  function clearRuntimeStateIfOurs(instanceId) {
2006
2094
  const path = runtimePath();
2007
- if (!existsSync4(path)) return;
2095
+ if (!existsSync5(path)) return;
2008
2096
  try {
2009
2097
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
2010
2098
  if (parsed.instanceId && parsed.instanceId !== instanceId) return;
@@ -2014,7 +2102,7 @@ function clearRuntimeStateIfOurs(instanceId) {
2014
2102
  }
2015
2103
  function readLiveRuntimeState() {
2016
2104
  const path = runtimePath();
2017
- if (!existsSync4(path)) return null;
2105
+ if (!existsSync5(path)) return null;
2018
2106
  let parsed;
2019
2107
  try {
2020
2108
  parsed = JSON.parse(readFileSync2(path, "utf8"));
@@ -2179,7 +2267,7 @@ function turnFailureCopy(reason, runtime) {
2179
2267
  return "Claude Code sign-in has expired on this machine. Open Claude Code, run /login, then reply in Cabane to try again.";
2180
2268
  if (runtime === "codex")
2181
2269
  return "Codex sign-in has expired on this machine. Run codex login, then reply in Cabane to try again.";
2182
- return "Sign-in has expired on this machine. Sign in to the coding agent, then reply in Cabane to try again.";
2270
+ return "Sign-in has expired on this machine. Sign in to the agent, then reply in Cabane to try again.";
2183
2271
  }
2184
2272
  if (failure?.kind === "usage_capped")
2185
2273
  return failure.resetsAt ? `The usage limit is reached; it resets around ${new Date(failure.resetsAt).toLocaleTimeString()}. Reply in Cabane after it resets.` : "The usage limit is reached. Try again later in Cabane.";
@@ -2190,9 +2278,9 @@ function turnFailureCopy(reason, runtime) {
2190
2278
  if (reason?.startsWith("missing_secret:"))
2191
2279
  return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
2192
2280
  if (reason?.startsWith("runtime_incomplete:"))
2193
- return "The coding agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
2281
+ return "The agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
2194
2282
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
2195
- return "The coding agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
2283
+ return "The agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
2196
2284
  if (reason?.startsWith("model_unavailable:"))
2197
2285
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
2198
2286
  if (reason?.startsWith("prepare_failed:"))
@@ -2200,17 +2288,17 @@ function turnFailureCopy(reason, runtime) {
2200
2288
  if (reason?.startsWith("fetch_failed:"))
2201
2289
  return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
2202
2290
  if (reason === "timeout_idle")
2203
- return "The coding agent stopped responding. Reply in Cabane to try again.";
2291
+ return "The agent stopped responding. Reply in Cabane to try again.";
2204
2292
  if (reason === "timeout_total")
2205
2293
  return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
2206
2294
  if (reason === "result_error:error_max_turns")
2207
- return "The coding agent reached its step limit. Reply in Cabane to continue.";
2295
+ return "The agent reached its step limit. Reply in Cabane to continue.";
2208
2296
  if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
2209
- return "The coding agent finished without a response. Reply in Cabane to try again.";
2297
+ return "The agent finished without a response. Reply in Cabane to try again.";
2210
2298
  if (reason === "session_start_failed")
2211
- return "The coding agent could not start a session. Restart the companion, then reply in Cabane to try again.";
2299
+ return "The agent could not start a session. Restart the companion, then reply in Cabane to try again.";
2212
2300
  if (reason === "no_terminal")
2213
- return "The coding agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
2301
+ return "The agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
2214
2302
  if (reason === "lease_unconfirmed")
2215
2303
  return "Could not confirm with Cabane that the turn could start. Check this machine\u2019s connection, then reply in Cabane to try again.";
2216
2304
  if (reason === "workspace_tools_missing")
@@ -2218,13 +2306,13 @@ function turnFailureCopy(reason, runtime) {
2218
2306
  if (reason?.startsWith("seq_floor_unavailable:"))
2219
2307
  return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
2220
2308
  if (reason === "result_error:error_max_budget_usd")
2221
- return "The coding agent reached its spending limit. Check its budget settings before trying again in Cabane.";
2309
+ return "The agent reached its spending limit. Check its budget settings before trying again in Cabane.";
2222
2310
  if (reason === "result_error:error_max_structured_output_retries")
2223
- return "The coding agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
2311
+ return "The agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
2224
2312
  if (reason?.startsWith("result_error:"))
2225
- return "The coding agent stopped with an error while running. Check its transcript, then reply in Cabane to try again.";
2313
+ return "The agent stopped with an error while running. Check its transcript, then reply in Cabane to try again.";
2226
2314
  if (reason === "setup_failed")
2227
- return "The turn could not be set up. Check the coding agent setup on this machine, then reply in Cabane to try again.";
2315
+ return "The turn could not be set up. Check the agent setup on this machine, then reply in Cabane to try again.";
2228
2316
  if (reason === "unexpected_role")
2229
2317
  return "The message could not start an agent response. Send a new message in Cabane to try again.";
2230
2318
  if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
@@ -2232,11 +2320,11 @@ function turnFailureCopy(reason, runtime) {
2232
2320
  const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
2233
2321
  if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
2234
2322
  const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
2235
- return `The coding agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
2323
+ return `The agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
2236
2324
  }
2237
2325
  if (detail)
2238
- return `The coding agent reported: ${detail}. Check the coding agent setup on this machine, then reply in Cabane to try again.`;
2239
- return "The coding agent could not finish this turn. Reply in Cabane to try again.";
2326
+ return `The agent reported: ${detail}. Check the agent setup on this machine, then reply in Cabane to try again.`;
2327
+ return "The agent could not finish this turn. Reply in Cabane to try again.";
2240
2328
  }
2241
2329
 
2242
2330
  // src/api-error-shape.ts
@@ -2870,22 +2958,22 @@ function errorMessage2(status, body) {
2870
2958
  // src/credentials.ts
2871
2959
  import {
2872
2960
  chmodSync as chmodSync3,
2873
- existsSync as existsSync5,
2961
+ existsSync as existsSync6,
2874
2962
  mkdirSync as mkdirSync5,
2875
2963
  readFileSync as readFileSync3,
2876
2964
  renameSync as renameSync2,
2877
2965
  rmSync as rmSync4,
2878
2966
  writeFileSync as writeFileSync3
2879
2967
  } from "fs";
2880
- import { dirname as dirname5, join as join8 } from "path";
2968
+ import { dirname as dirname5, join as join10 } from "path";
2881
2969
  import { z as z4 } from "zod";
2882
2970
  function credentialsPath() {
2883
- return join8(cabaneDir(), "credentials.json");
2971
+ return join10(cabaneDir(), "credentials.json");
2884
2972
  }
2885
2973
  var credentialStoreSchema = z4.record(z4.string(), z4.string());
2886
2974
  function load() {
2887
2975
  const path = credentialsPath();
2888
- if (!existsSync5(path)) return {};
2976
+ if (!existsSync6(path)) return {};
2889
2977
  let raw;
2890
2978
  try {
2891
2979
  raw = readFileSync3(path, "utf8");
@@ -2947,18 +3035,18 @@ function pruneCredentials(keepAgentIds) {
2947
3035
  }
2948
3036
 
2949
3037
  // src/dispatch-dedupe.ts
2950
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
2951
- import { join as join9 } from "path";
3038
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
3039
+ import { join as join11 } from "path";
2952
3040
  var MAX_IDS = 256;
2953
3041
  function dir(log) {
2954
- return join9(cabaneDir(), log);
3042
+ return join11(cabaneDir(), log);
2955
3043
  }
2956
3044
  function pathFor(log, workspaceId) {
2957
- return join9(dir(log), encodeURIComponent(workspaceId));
3045
+ return join11(dir(log), encodeURIComponent(workspaceId));
2958
3046
  }
2959
3047
  function readIds(log, workspaceId) {
2960
3048
  const path = pathFor(log, workspaceId);
2961
- if (!existsSync6(path)) return [];
3049
+ if (!existsSync7(path)) return [];
2962
3050
  try {
2963
3051
  return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2964
3052
  } catch {
@@ -2991,15 +3079,15 @@ function markCompleted(workspaceId, eventId) {
2991
3079
  }
2992
3080
  var MAX_RESUME_ATTEMPTS = 3;
2993
3081
  function resumeDir() {
2994
- return join9(cabaneDir(), "resume-attempts");
3082
+ return join11(cabaneDir(), "resume-attempts");
2995
3083
  }
2996
3084
  function resumePathFor(workspaceId) {
2997
- return join9(resumeDir(), encodeURIComponent(workspaceId));
3085
+ return join11(resumeDir(), encodeURIComponent(workspaceId));
2998
3086
  }
2999
3087
  function readResumeCounts(workspaceId) {
3000
3088
  const out = /* @__PURE__ */ new Map();
3001
3089
  const path = resumePathFor(workspaceId);
3002
- if (!existsSync6(path)) return out;
3090
+ if (!existsSync7(path)) return out;
3003
3091
  try {
3004
3092
  for (const line of readFileSync4(path, "utf8").split("\n")) {
3005
3093
  const trimmed = line.trim();
@@ -3030,16 +3118,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
3030
3118
  return next;
3031
3119
  }
3032
3120
  function turnDir() {
3033
- return join9(cabaneDir(), "turns");
3121
+ return join11(cabaneDir(), "turns");
3034
3122
  }
3035
3123
  function turnPathFor(workspaceId) {
3036
- return join9(turnDir(), encodeURIComponent(workspaceId));
3124
+ return join11(turnDir(), encodeURIComponent(workspaceId));
3037
3125
  }
3038
3126
  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;
3039
3127
  function readTurnIds(workspaceId) {
3040
3128
  const out = /* @__PURE__ */ new Map();
3041
3129
  const path = turnPathFor(workspaceId);
3042
- if (!existsSync6(path)) return out;
3130
+ if (!existsSync7(path)) return out;
3043
3131
  try {
3044
3132
  for (const line of readFileSync4(path, "utf8").split("\n")) {
3045
3133
  const trimmed = line.trim();
@@ -3951,8 +4039,8 @@ function readThinking(runtimeOptions) {
3951
4039
  var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
3952
4040
  function parseClaudeCodeModel(model) {
3953
4041
  if (!model) return null;
3954
- const sep3 = model.indexOf("/");
3955
- const bare = sep3 === -1 || model.slice(0, sep3) !== "anthropic" ? model : model.slice(sep3 + 1);
4042
+ const sep5 = model.indexOf("/");
4043
+ const bare = sep5 === -1 || model.slice(0, sep5) !== "anthropic" ? model : model.slice(sep5 + 1);
3956
4044
  return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
3957
4045
  }
3958
4046
 
@@ -4135,7 +4223,7 @@ function dedupe(xs) {
4135
4223
  }
4136
4224
 
4137
4225
  // packages/agent-runtime/src/claude-code/loop.ts
4138
- async function* decodeSdkStream(iter, ctx) {
4226
+ async function* decodeSdkStream(iter, ctx2) {
4139
4227
  const pending = /* @__PURE__ */ new Map();
4140
4228
  const buffer = createTerminalTextBuffer();
4141
4229
  const out = [];
@@ -4156,7 +4244,7 @@ async function* decodeSdkStream(iter, ctx) {
4156
4244
  let preInitNotice = null;
4157
4245
  try {
4158
4246
  for await (const msg of iter) {
4159
- if (ctx.signal.aborted) return;
4247
+ if (ctx2.signal.aborted) return;
4160
4248
  if (msg.type === "system" && msg.subtype === "init") {
4161
4249
  const initModel = msg.model;
4162
4250
  if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
@@ -4167,19 +4255,19 @@ async function* decodeSdkStream(iter, ctx) {
4167
4255
  break;
4168
4256
  }
4169
4257
  const sdkSessionId = msg.session_id;
4170
- if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx.resumedSessionId) {
4258
+ if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx2.resumedSessionId) {
4171
4259
  sessionEmitted = true;
4172
4260
  yield {
4173
4261
  type: "session",
4174
- state: encodeSession({ sdkSessionId, cwd: ctx.cwd ?? "" }),
4175
- ...ctx.degraded ? { degraded: true } : {}
4262
+ state: encodeSession({ sdkSessionId, cwd: ctx2.cwd ?? "" }),
4263
+ ...ctx2.degraded ? { degraded: true } : {}
4176
4264
  };
4177
4265
  }
4178
4266
  } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
4179
4267
  const compaction = readCompaction(msg);
4180
4268
  if (compaction) compactions.push(compaction);
4181
4269
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4182
- if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
4270
+ if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
4183
4271
  const failure = classifyPreInitFrame(msg, signals);
4184
4272
  resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4185
4273
  if (msg.type === "assistant" && isHarnessAuthored(msg))
@@ -4223,7 +4311,7 @@ async function* decodeSdkStream(iter, ctx) {
4223
4311
  const window = readContextWindow(msg, resolvedModel);
4224
4312
  if (window !== void 0) usage.contextWindow = window;
4225
4313
  if (compactions.length > 0) usage.contextCompactions = compactions;
4226
- const reading = await readHarnessContext(ctx.readContextUsage);
4314
+ const reading = await readHarnessContext(ctx2.readContextUsage);
4227
4315
  if (reading) Object.assign(usage, reading);
4228
4316
  }
4229
4317
  const isError = msg.is_error === true;
@@ -4239,20 +4327,20 @@ async function* decodeSdkStream(iter, ctx) {
4239
4327
  }
4240
4328
  }
4241
4329
  } catch (err) {
4242
- if (ctx.signal.aborted) throw err;
4330
+ if (ctx2.signal.aborted) throw err;
4243
4331
  const message2 = err instanceof Error ? err.message : String(err);
4244
4332
  const failure = mcpInventory.initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
4245
- if (!failure && (mcpInventory.initReceived || ctx.resumedSessionId === null)) throw err;
4333
+ if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
4246
4334
  ok = false;
4247
4335
  if (failure) {
4248
4336
  resultReason = encodeFailureReason(failure);
4249
4337
  } else {
4250
4338
  resultReason = "session_start_failed";
4251
- if (ctx.startEvidence) ctx.startEvidence.thrown = message2.slice(0, 200);
4339
+ if (ctx2.startEvidence) ctx2.startEvidence.thrown = message2.slice(0, 200);
4252
4340
  }
4253
4341
  sawResult = true;
4254
4342
  }
4255
- if (ctx.signal.aborted) return;
4343
+ if (ctx2.signal.aborted) return;
4256
4344
  if (!workspaceProven) {
4257
4345
  ok = false;
4258
4346
  if (resultReason === void 0) {
@@ -5260,7 +5348,7 @@ function deepestError(node) {
5260
5348
  }
5261
5349
 
5262
5350
  // packages/agent-runtime/src/opencode/loop.ts
5263
- async function* decodeOpencodeStream(events, ctx) {
5351
+ async function* decodeOpencodeStream(events, ctx2) {
5264
5352
  let held = null;
5265
5353
  const pending = /* @__PURE__ */ new Map();
5266
5354
  const startedTools = /* @__PURE__ */ new Set();
@@ -5269,9 +5357,9 @@ async function* decodeOpencodeStream(events, ctx) {
5269
5357
  let reason;
5270
5358
  let settled = false;
5271
5359
  const userMessageIds = /* @__PURE__ */ new Set();
5272
- if (ctx.skipMessageId) userMessageIds.add(ctx.skipMessageId);
5360
+ if (ctx2.skipMessageId) userMessageIds.add(ctx2.skipMessageId);
5273
5361
  for await (const ev of events) {
5274
- if (ctx.signal.aborted) return;
5362
+ if (ctx2.signal.aborted) return;
5275
5363
  if (ev.type === "message.updated") {
5276
5364
  const info = readMessageInfo(ev.properties);
5277
5365
  if (info && info.role === "user") userMessageIds.add(info.id);
@@ -5324,7 +5412,7 @@ async function* decodeOpencodeStream(events, ctx) {
5324
5412
  }
5325
5413
  } else if (ev.type === "session.idle") {
5326
5414
  const sid = readSessionId(ev.properties);
5327
- if (sid && sid !== ctx.sessionId) continue;
5415
+ if (sid && sid !== ctx2.sessionId) continue;
5328
5416
  const sealed = sealHeld(held, true);
5329
5417
  held = null;
5330
5418
  if (sealed) yield sealed;
@@ -5333,7 +5421,7 @@ async function* decodeOpencodeStream(events, ctx) {
5333
5421
  break;
5334
5422
  } else if (ev.type === "session.error") {
5335
5423
  const sid = readSessionId(ev.properties);
5336
- if (sid && sid !== ctx.sessionId) continue;
5424
+ if (sid && sid !== ctx2.sessionId) continue;
5337
5425
  const sealed = sealHeld(held, false);
5338
5426
  held = null;
5339
5427
  if (sealed) yield sealed;
@@ -5349,7 +5437,7 @@ async function* decodeOpencodeStream(events, ctx) {
5349
5437
  break;
5350
5438
  }
5351
5439
  }
5352
- if (ctx.signal.aborted) return;
5440
+ if (ctx2.signal.aborted) return;
5353
5441
  if (!settled) {
5354
5442
  const sealed = sealHeld(held, true);
5355
5443
  held = null;
@@ -5407,9 +5495,9 @@ function readOpencodeDialect(runtimeOptions) {
5407
5495
 
5408
5496
  // packages/agent-runtime/src/opencode/model.ts
5409
5497
  function parseOpencodeModel(model) {
5410
- const sep3 = model.indexOf("/");
5411
- if (sep3 === -1) return { providerID: "", modelID: model };
5412
- return { providerID: model.slice(0, sep3), modelID: model.slice(sep3 + 1) };
5498
+ const sep5 = model.indexOf("/");
5499
+ if (sep5 === -1) return { providerID: "", modelID: model };
5500
+ return { providerID: model.slice(0, sep5), modelID: model.slice(sep5 + 1) };
5413
5501
  }
5414
5502
 
5415
5503
  // packages/agent-runtime/src/opencode/run-spec.ts
@@ -5697,10 +5785,10 @@ async function* parseSseStream(body, sessionId, signal) {
5697
5785
  const { value, done } = await reader.read();
5698
5786
  if (done) return;
5699
5787
  buf += decoder.decode(value, { stream: true });
5700
- let sep3;
5701
- while ((sep3 = buf.indexOf("\n\n")) !== -1) {
5702
- const frame = buf.slice(0, sep3);
5703
- buf = buf.slice(sep3 + 2);
5788
+ let sep5;
5789
+ while ((sep5 = buf.indexOf("\n\n")) !== -1) {
5790
+ const frame = buf.slice(0, sep5);
5791
+ buf = buf.slice(sep5 + 2);
5704
5792
  const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
5705
5793
  if (!data) continue;
5706
5794
  let ev;
@@ -6448,7 +6536,7 @@ function readUsageBaseline(raw) {
6448
6536
  }
6449
6537
 
6450
6538
  // packages/agent-runtime/src/codex/loop.ts
6451
- async function* decodeCodexStream(events, ctx) {
6539
+ async function* decodeCodexStream(events, ctx2) {
6452
6540
  let held = null;
6453
6541
  const pending = /* @__PURE__ */ new Map();
6454
6542
  const startedTools = /* @__PURE__ */ new Set();
@@ -6458,7 +6546,7 @@ async function* decodeCodexStream(events, ctx) {
6458
6546
  let reason;
6459
6547
  let usage;
6460
6548
  let cumulativeUsage;
6461
- let threadIdForSession = ctx.resumedThreadId;
6549
+ let threadIdForSession = ctx2.resumedThreadId;
6462
6550
  let settled = false;
6463
6551
  function* flushInterim() {
6464
6552
  const sealed = sealHeld2(held, false);
@@ -6466,26 +6554,26 @@ async function* decodeCodexStream(events, ctx) {
6466
6554
  if (sealed) yield sealed;
6467
6555
  }
6468
6556
  for await (const ev of events) {
6469
- if (ctx.signal.aborted) return;
6557
+ if (ctx2.signal.aborted) return;
6470
6558
  if (ev.type === "thread.started") {
6471
6559
  const threadId = readThreadId(ev);
6472
6560
  if (threadId) threadIdForSession = threadId;
6473
6561
  if (threadId && !sessionEmitted) {
6474
6562
  const state = encodeSession3({
6475
6563
  threadId,
6476
- cwd: ctx.cwd ?? "",
6477
- promptFingerprint: ctx.promptFingerprint ?? null,
6564
+ cwd: ctx2.cwd ?? "",
6565
+ promptFingerprint: ctx2.promptFingerprint ?? null,
6478
6566
  // The baseline this turn inherited, carried through unchanged. This early write records the
6479
6567
  // thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
6480
6568
  // closing write records the new baseline.
6481
- usageBaseline: ctx.usageBaseline ?? null
6569
+ usageBaseline: ctx2.usageBaseline ?? null
6482
6570
  });
6483
- if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
6571
+ if (threadId !== ctx2.resumedThreadId || state !== ctx2.storedState) {
6484
6572
  sessionEmitted = true;
6485
6573
  yield {
6486
6574
  type: "session",
6487
6575
  state,
6488
- ...ctx.degraded ? { degraded: true } : {}
6576
+ ...ctx2.degraded ? { degraded: true } : {}
6489
6577
  };
6490
6578
  }
6491
6579
  }
@@ -6554,7 +6642,7 @@ async function* decodeCodexStream(events, ctx) {
6554
6642
  held = null;
6555
6643
  if (sealed) yield sealed;
6556
6644
  cumulativeUsage = readUsage(ev);
6557
- usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage, ctx.usageBaseline ?? null) : void 0;
6645
+ usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage, ctx2.usageBaseline ?? null) : void 0;
6558
6646
  ok = true;
6559
6647
  settled = true;
6560
6648
  break;
@@ -6573,7 +6661,7 @@ async function* decodeCodexStream(events, ctx) {
6573
6661
  break;
6574
6662
  }
6575
6663
  }
6576
- if (ctx.signal.aborted) return;
6664
+ if (ctx2.signal.aborted) return;
6577
6665
  if (!settled) {
6578
6666
  const sealed = sealHeld2(held, true);
6579
6667
  held = null;
@@ -6583,19 +6671,19 @@ async function* decodeCodexStream(events, ctx) {
6583
6671
  if (ok && cumulativeUsage && threadIdForSession) {
6584
6672
  const closing = encodeSession3({
6585
6673
  threadId: threadIdForSession,
6586
- cwd: ctx.cwd ?? "",
6587
- promptFingerprint: ctx.promptFingerprint ?? null,
6674
+ cwd: ctx2.cwd ?? "",
6675
+ promptFingerprint: ctx2.promptFingerprint ?? null,
6588
6676
  usageBaseline: cumulativeUsage
6589
6677
  });
6590
6678
  yield { type: "session", state: closing };
6591
6679
  }
6592
- const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
6680
+ const resolvedConfig = ctx2.resolvedReasoningEffort ? { reasoningEffort: ctx2.resolvedReasoningEffort } : void 0;
6593
6681
  yield {
6594
6682
  type: "result",
6595
6683
  ok,
6596
6684
  ...reason ? { reason } : {},
6597
6685
  ...usage ? { usage } : {},
6598
- ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
6686
+ ...ctx2.resolvedModel ? { resolvedModel: ctx2.resolvedModel } : {},
6599
6687
  ...resolvedConfig ? { resolvedConfig } : {}
6600
6688
  };
6601
6689
  }
@@ -6640,8 +6728,8 @@ function readCodexDialect(runtimeOptions) {
6640
6728
  // packages/agent-runtime/src/codex/model.ts
6641
6729
  var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
6642
6730
  function parseCodexModel(model) {
6643
- const sep3 = model.indexOf("/");
6644
- const bare = sep3 === -1 || model.slice(0, sep3) !== "openai" ? model : model.slice(sep3 + 1);
6731
+ const sep5 = model.indexOf("/");
6732
+ const bare = sep5 === -1 || model.slice(0, sep5) !== "openai" ? model : model.slice(sep5 + 1);
6645
6733
  return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
6646
6734
  }
6647
6735
 
@@ -7921,6 +8009,7 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7921
8009
  // src/connector-health.ts
7922
8010
  var ConnectorHealthStore = class {
7923
8011
  byRuntime = /* @__PURE__ */ new Map();
8012
+ sources = /* @__PURE__ */ new Map();
7924
8013
  // Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
7925
8014
  // classified failure sets `limited`(+reset) / `auth_expired` with the reason as
7926
8015
  // `last_error`; anything else (a user Stop, a timeout, an unclassified error)
@@ -7929,6 +8018,7 @@ var ConnectorHealthStore = class {
7929
8018
  recordSettle(runtime, outcome) {
7930
8019
  if (outcome.ok) {
7931
8020
  this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
8021
+ this.sources.set(runtime, "turn");
7932
8022
  return;
7933
8023
  }
7934
8024
  const failure = decodeFailureReason(outcome.errorReason);
@@ -7940,6 +8030,26 @@ var ConnectorHealthStore = class {
7940
8030
  limitedUntil: quota.limitedUntil,
7941
8031
  lastError: outcome.errorReason ?? null
7942
8032
  });
8033
+ this.sources.set(runtime, "turn");
8034
+ }
8035
+ // Record what the companion's own sign-in check found for a connected
8036
+ // runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
8037
+ // before any turn fails. Signed out seeds `auth_expired` unless a turn has
8038
+ // already spoken; usable clears only a seed of our own.
8039
+ recordLocalCheck(runtime, usable) {
8040
+ const source = this.sources.get(runtime);
8041
+ if (!usable) {
8042
+ if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
8043
+ this.byRuntime.set(runtime, {
8044
+ quotaState: "auth_expired",
8045
+ limitedUntil: null,
8046
+ lastError: encodeFailureReason({ kind: "auth_expired" })
8047
+ });
8048
+ this.sources.set(runtime, "local");
8049
+ return;
8050
+ }
8051
+ if (source !== "local") return;
8052
+ this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
7943
8053
  }
7944
8054
  // The store's current opinion for one runtime, or undefined when it has
7945
8055
  // no observation. Read by the dispatcher at idle-timeout fire: an idle reap
@@ -7968,11 +8078,11 @@ var ConnectorHealthStore = class {
7968
8078
 
7969
8079
  // src/dispatcher.ts
7970
8080
  import { randomUUID as randomUUID4 } from "crypto";
7971
- import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7972
- import { join as join15 } from "path";
8081
+ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
8082
+ import { join as join17 } from "path";
7973
8083
 
7974
8084
  // src/runner/launch.ts
7975
- import { createInterface } from "readline";
8085
+ import { createInterface as createInterface2 } from "readline";
7976
8086
 
7977
8087
  // src/runner/sandbox-policy.ts
7978
8088
  import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
@@ -8149,7 +8259,7 @@ async function stopRemoteOpencodeSession(serverUrl, sessionId, opts = {}) {
8149
8259
  }
8150
8260
 
8151
8261
  // src/turn-containment.ts
8152
- import { execFile, execFileSync, spawn as spawn4 } from "child_process";
8262
+ import { execFile, execFileSync, spawn as spawn3 } from "child_process";
8153
8263
  import { readFileSync as readFileSync5 } from "fs";
8154
8264
  import { promisify } from "util";
8155
8265
  var execFileAsync = promisify(execFile);
@@ -8158,8 +8268,8 @@ var CONTAINMENT_STOP_GRACE_SEC = 5;
8158
8268
  var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
8159
8269
  var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
8160
8270
  function turnScopeUnit(turnId, attempt) {
8161
- const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
8162
- return `cabane-turn-${safe3}-${attempt}`;
8271
+ const safe2 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
8272
+ return `cabane-turn-${safe2}-${attempt}`;
8163
8273
  }
8164
8274
  async function reapTurnScopes(turnId) {
8165
8275
  if (process.platform !== "linux") return { failures: [] };
@@ -8272,7 +8382,7 @@ function createTurnContainment(turnId, deps = {}) {
8272
8382
  command: options.command
8273
8383
  });
8274
8384
  return asContained(
8275
- spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
8385
+ spawn3("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
8276
8386
  ...options.cwd ? { cwd: options.cwd } : {},
8277
8387
  env: options.env,
8278
8388
  stdio: ["pipe", "pipe", "pipe"]
@@ -8280,7 +8390,7 @@ function createTurnContainment(turnId, deps = {}) {
8280
8390
  );
8281
8391
  }
8282
8392
  function spawnInGroup(options) {
8283
- const child = spawn4(options.command, options.args, {
8393
+ const child = spawn3(options.command, options.args, {
8284
8394
  ...options.cwd ? { cwd: options.cwd } : {},
8285
8395
  env: options.env,
8286
8396
  stdio: ["pipe", "pipe", "pipe"],
@@ -8427,17 +8537,17 @@ function createTurnContainment(turnId, deps = {}) {
8427
8537
  }
8428
8538
 
8429
8539
  // src/secrets.ts
8430
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
8431
- import { join as join10 } from "path";
8540
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
8541
+ import { join as join12 } from "path";
8432
8542
  import { z as z14 } from "zod";
8433
8543
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8434
8544
  function secretsPath() {
8435
- return join10(cabaneDir(), "secrets.json");
8545
+ return join12(cabaneDir(), "secrets.json");
8436
8546
  }
8437
8547
  var secretStoreSchema = z14.record(z14.string(), z14.string());
8438
8548
  function loadSecretStore() {
8439
8549
  const path = secretsPath();
8440
- if (!existsSync7(path)) return makeStore({});
8550
+ if (!existsSync8(path)) return makeStore({});
8441
8551
  let raw;
8442
8552
  try {
8443
8553
  raw = readFileSync6(path, "utf8");
@@ -8816,7 +8926,7 @@ function relayRunnerLog(line, log) {
8816
8926
  log[method](fields, typeof msg === "string" ? msg : "");
8817
8927
  }
8818
8928
  function lines(stream, onLine) {
8819
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
8929
+ const rl = createInterface2({ input: stream, crlfDelay: Infinity });
8820
8930
  rl.on("line", onLine);
8821
8931
  return new Promise((resolve2) => rl.once("close", resolve2));
8822
8932
  }
@@ -9156,19 +9266,9 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
9156
9266
  return { ok: false, durationMs, reason };
9157
9267
  }
9158
9268
 
9159
- // src/term.ts
9160
- import { createInterface as createInterface2 } from "readline";
9161
- import { homedir as homedir2 } from "os";
9162
- import pc from "picocolors";
9163
- function tildePath(path) {
9164
- const home = homedir2();
9165
- if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
9166
- return path;
9167
- }
9168
-
9169
9269
  // src/turn-execution.ts
9170
9270
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
9171
- import { existsSync as existsSync9 } from "fs";
9271
+ import { existsSync as existsSync10 } from "fs";
9172
9272
 
9173
9273
  // src/turn-control-tools.ts
9174
9274
  import { randomUUID as randomUUID2 } from "crypto";
@@ -9412,11 +9512,11 @@ function trimSlash2(s) {
9412
9512
  // src/codex-instructions.ts
9413
9513
  import { mkdtemp, rm, writeFile } from "fs/promises";
9414
9514
  import { tmpdir } from "os";
9415
- import { join as join11 } from "path";
9515
+ import { join as join13 } from "path";
9416
9516
  var PREFIX = "cabane-codex-instructions-";
9417
9517
  async function writeCodexInstructionsFile(contents) {
9418
- const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
9419
- const path = join11(dir2, "instructions.md");
9518
+ const dir2 = await mkdtemp(join13(tmpdir(), PREFIX));
9519
+ const path = join13(dir2, "instructions.md");
9420
9520
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
9421
9521
  return {
9422
9522
  path,
@@ -9429,7 +9529,7 @@ async function writeCodexInstructionsFile(contents) {
9429
9529
  // src/codex-images.ts
9430
9530
  import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
9431
9531
  import { tmpdir as tmpdir2 } from "os";
9432
- import { join as join12 } from "path";
9532
+ import { join as join14 } from "path";
9433
9533
  var FETCH_TIMEOUT_MS = 3e4;
9434
9534
  var EXTENSIONS = {
9435
9535
  "image/png": "png",
@@ -9442,7 +9542,7 @@ function createCodexImagePreparer(opts) {
9442
9542
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
9443
9543
  return async (urls, signal) => {
9444
9544
  signal.throwIfAborted();
9445
- const dir2 = await mkdtemp2(join12(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
9545
+ const dir2 = await mkdtemp2(join14(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
9446
9546
  const cleanup = async () => {
9447
9547
  try {
9448
9548
  await rm2(dir2, { recursive: true, force: true });
@@ -9463,7 +9563,7 @@ function createCodexImagePreparer(opts) {
9463
9563
  const ext = EXTENSIONS[type];
9464
9564
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
9465
9565
  const bytes = new Uint8Array(await res.arrayBuffer());
9466
- const path = join12(dir2, `image-${index + 1}.${ext}`);
9566
+ const path = join14(dir2, `image-${index + 1}.${ext}`);
9467
9567
  await writeFile2(path, bytes, { mode: 384 });
9468
9568
  return path;
9469
9569
  } catch (err) {
@@ -9497,14 +9597,14 @@ var SeqFloorUnavailable = class extends Error {
9497
9597
  }
9498
9598
  detail;
9499
9599
  };
9500
- function resolveSeqFloor(sources, ctx) {
9600
+ function resolveSeqFloor(sources, ctx2) {
9501
9601
  const { serverFloor, outboxFloor } = sources;
9502
9602
  if (serverFloor === void 0) {
9503
9603
  throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
9504
9604
  }
9505
9605
  const floor = Math.max(serverFloor, outboxFloor);
9506
- ctx.log.debug(
9507
- { turnId: ctx.turnId, floor, outboxFloor, serverFloor },
9606
+ ctx2.log.debug(
9607
+ { turnId: ctx2.turnId, floor, outboxFloor, serverFloor },
9508
9608
  "companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
9509
9609
  );
9510
9610
  return floor;
@@ -9522,7 +9622,7 @@ function readOutboxFloor(read, turnId, log) {
9522
9622
  }
9523
9623
 
9524
9624
  // src/prepare-failure.ts
9525
- import { spawn as spawn5 } from "child_process";
9625
+ import { spawn as spawn4 } from "child_process";
9526
9626
  import { hostname } from "os";
9527
9627
  function prepareFailureCause(error) {
9528
9628
  const message2 = error instanceof Error ? error.message : String(error);
@@ -9560,7 +9660,7 @@ async function runPrepareWithReporting(runner, hook, input, context, reportError
9560
9660
  }
9561
9661
  function reportFailure(config, body) {
9562
9662
  return new Promise((resolve2, reject) => {
9563
- const child = spawn5(config.command, config.args ?? [], {
9663
+ const child = spawn4(config.command, config.args ?? [], {
9564
9664
  stdio: ["pipe", "ignore", "ignore"],
9565
9665
  env: { ...process.env, ...config.env }
9566
9666
  });
@@ -9584,20 +9684,20 @@ function reportFailure(config, body) {
9584
9684
  }
9585
9685
 
9586
9686
  // src/prepared.ts
9587
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
9588
- import { join as join13 } from "path";
9687
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
9688
+ import { join as join15 } from "path";
9589
9689
  function dirFor(workspaceId) {
9590
- return join13(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9690
+ return join15(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9591
9691
  }
9592
9692
  function conversationDir(workspaceId, conversationId) {
9593
- return join13(dirFor(workspaceId), encodeURIComponent(conversationId));
9693
+ return join15(dirFor(workspaceId), encodeURIComponent(conversationId));
9594
9694
  }
9595
9695
  function pathFor2(workspaceId, conversationId, agentId) {
9596
- return join13(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9696
+ return join15(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9597
9697
  }
9598
9698
  function readPrepared(workspaceId, conversationId, agentId) {
9599
9699
  const path = pathFor2(workspaceId, conversationId, agentId);
9600
- if (!existsSync8(path)) return null;
9700
+ if (!existsSync9(path)) return null;
9601
9701
  try {
9602
9702
  const parsed = JSON.parse(readFileSync7(path, "utf8"));
9603
9703
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
@@ -9625,9 +9725,9 @@ function clearPrepared(workspaceId, conversationId, agentId) {
9625
9725
 
9626
9726
  // src/transcript-writer.ts
9627
9727
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
9628
- import { basename, dirname as dirname6, join as join14 } from "path";
9728
+ import { basename, dirname as dirname6, join as join16 } from "path";
9629
9729
  function transcriptsDir() {
9630
- return join14(runnerStateDir(), "transcripts");
9730
+ return join16(runnerStateDir(), "transcripts");
9631
9731
  }
9632
9732
  var RETAIN = 200;
9633
9733
  var ANOMALY_RETAIN = 50;
@@ -9637,7 +9737,7 @@ var TranscriptWriter = class {
9637
9737
  onWarn;
9638
9738
  constructor(dir2, meta, onWarn) {
9639
9739
  this.onWarn = onWarn;
9640
- this.path = join14(dir2, fileName(meta));
9740
+ this.path = join16(dir2, fileName(meta));
9641
9741
  try {
9642
9742
  mkdirSync8(dir2, { recursive: true });
9643
9743
  try {
@@ -9666,10 +9766,10 @@ var TranscriptWriter = class {
9666
9766
  preserveAnomaly() {
9667
9767
  if (this.broken) return;
9668
9768
  try {
9669
- const dir2 = join14(dirname6(this.path), "anomalies");
9769
+ const dir2 = join16(dirname6(this.path), "anomalies");
9670
9770
  mkdirSync8(dir2, { recursive: true, mode: 448 });
9671
9771
  chmodSync4(dir2, 448);
9672
- const target = join14(dir2, basename(this.path));
9772
+ const target = join16(dir2, basename(this.path));
9673
9773
  copyFileSync(this.path, target);
9674
9774
  chmodSync4(target, 384);
9675
9775
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9694,14 +9794,14 @@ var TranscriptWriter = class {
9694
9794
  function fileName(meta) {
9695
9795
  const d = new Date(meta.ts);
9696
9796
  const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
9697
- const slug = safe2(meta.workspaceSlug) || "ws";
9797
+ const slug = safe(meta.workspaceSlug) || "ws";
9698
9798
  const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
9699
9799
  return `${stamp}-${slug}-${short}.jsonl`;
9700
9800
  }
9701
9801
  function pad(n) {
9702
9802
  return String(n).padStart(2, "0");
9703
9803
  }
9704
- function safe2(s) {
9804
+ function safe(s) {
9705
9805
  return s.replace(/[^a-zA-Z0-9_-]/g, "-");
9706
9806
  }
9707
9807
  function pruneOld(dir2, retain) {
@@ -9715,7 +9815,7 @@ function pruneOld(dir2, retain) {
9715
9815
  const drop = files.sort().slice(0, files.length - retain);
9716
9816
  for (const f of drop) {
9717
9817
  try {
9718
- rmSync6(join14(dir2, f), { force: true });
9818
+ rmSync6(join16(dir2, f), { force: true });
9719
9819
  } catch {
9720
9820
  }
9721
9821
  }
@@ -9854,32 +9954,32 @@ var TurnCommitter = class {
9854
9954
  function bundledHarnessFor(runtime) {
9855
9955
  return runtime === "claude-code" || runtime === "codex" ? runtime : null;
9856
9956
  }
9857
- function resolve(ctx, harness) {
9858
- return (ctx.resolveBundled ?? resolveBundledBinary)(harness);
9957
+ function resolve(ctx2, harness) {
9958
+ return (ctx2.resolveBundled ?? resolveBundledBinary)(harness);
9859
9959
  }
9860
- async function postIncompleteNotice(ctx, harness, resolution) {
9960
+ async function postIncompleteNotice(ctx2, harness, resolution) {
9861
9961
  try {
9862
- await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
9962
+ await ctx2.postTurnMessage(ctx2.workspaceId, ctx2.conversationId, {
9863
9963
  body: turnIncompleteCopy(harness),
9864
9964
  kind: "runtime_notice",
9865
- turnId: ctx.turnId,
9866
- parentMessageId: ctx.parentMessageId
9965
+ turnId: ctx2.turnId,
9966
+ parentMessageId: ctx2.parentMessageId
9867
9967
  });
9868
9968
  return true;
9869
9969
  } catch (err) {
9870
- ctx.log.warn(
9970
+ ctx2.log.warn(
9871
9971
  { ...apiErrorLogFields(err) },
9872
9972
  "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9873
9973
  );
9874
9974
  return false;
9875
9975
  }
9876
9976
  }
9877
- async function checkBundledBinary(ctx) {
9878
- const harness = bundledHarnessFor(ctx.runtime);
9977
+ async function checkBundledBinary(ctx2) {
9978
+ const harness = bundledHarnessFor(ctx2.runtime);
9879
9979
  if (!harness) return null;
9880
- const resolution = resolve(ctx, harness);
9980
+ const resolution = resolve(ctx2, harness);
9881
9981
  if (resolution.status === "present") return null;
9882
- ctx.log.debug(
9982
+ ctx2.log.debug(
9883
9983
  {
9884
9984
  runtime: harness,
9885
9985
  status: resolution.status,
@@ -9891,15 +9991,15 @@ async function checkBundledBinary(ctx) {
9891
9991
  return {
9892
9992
  harness,
9893
9993
  reason: `runtime_incomplete:${harness}`,
9894
- posted: await postIncompleteNotice(ctx, harness, resolution)
9994
+ posted: await postIncompleteNotice(ctx2, harness, resolution)
9895
9995
  };
9896
9996
  }
9897
- async function reclassifyThrow(ctx, opts) {
9898
- const harness = bundledHarnessFor(ctx.runtime);
9997
+ async function reclassifyThrow(ctx2, opts) {
9998
+ const harness = bundledHarnessFor(ctx2.runtime);
9899
9999
  if (!harness || opts.aborted) return null;
9900
- const resolution = resolve(ctx, harness);
10000
+ const resolution = resolve(ctx2, harness);
9901
10001
  if (resolution.status === "present") return null;
9902
- ctx.log.debug(
10002
+ ctx2.log.debug(
9903
10003
  {
9904
10004
  runtime: harness,
9905
10005
  status: resolution.status,
@@ -9912,17 +10012,17 @@ async function reclassifyThrow(ctx, opts) {
9912
10012
  return {
9913
10013
  harness,
9914
10014
  reason: `runtime_incomplete:${harness}`,
9915
- posted: await postIncompleteNotice(ctx, harness, resolution)
10015
+ posted: await postIncompleteNotice(ctx2, harness, resolution)
9916
10016
  };
9917
10017
  }
9918
- async function guardBundledBinary(ctx, concluded, outcome) {
9919
- const incomplete = await checkBundledBinary(ctx);
10018
+ async function guardBundledBinary(ctx2, concluded, outcome) {
10019
+ const incomplete = await checkBundledBinary(ctx2);
9920
10020
  if (!incomplete) return;
9921
10021
  outcome.runtimeIncomplete = incomplete.posted;
9922
10022
  throw incomplete.posted ? concluded(incomplete.reason) : concluded(incomplete.reason, incomplete.reason);
9923
10023
  }
9924
- async function absorbBundledLoss(ctx, aborted, outcome) {
9925
- const lost = await reclassifyThrow(ctx, { aborted, originalReason: outcome.resultReason });
10024
+ async function absorbBundledLoss(ctx2, aborted, outcome) {
10025
+ const lost = await reclassifyThrow(ctx2, { aborted, originalReason: outcome.resultReason });
9926
10026
  if (!lost) return false;
9927
10027
  outcome.resultReason = lost.reason;
9928
10028
  return lost.posted;
@@ -10317,7 +10417,7 @@ var TurnExecution = class {
10317
10417
  const prepareHook = this.opts.local.prepareHook;
10318
10418
  const cabaneCwd = this.turnContext.cwd;
10319
10419
  let effectiveCwd = localCwd ?? cabaneCwd;
10320
- if (effectiveCwd && !existsSync9(effectiveCwd)) {
10420
+ if (effectiveCwd && !existsSync10(effectiveCwd)) {
10321
10421
  turnLog.warn(
10322
10422
  { cwd: effectiveCwd },
10323
10423
  "The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
@@ -11085,7 +11185,7 @@ function fingerprintSessionState(state) {
11085
11185
  // src/dispatcher.ts
11086
11186
  function checkoutState(cwd) {
11087
11187
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
11088
- if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
11188
+ if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
11089
11189
  let entries;
11090
11190
  try {
11091
11191
  entries = readdirSync2(cwd);
@@ -11095,15 +11195,15 @@ function checkoutState(cwd) {
11095
11195
  if (entries.length === 0) {
11096
11196
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
11097
11197
  }
11098
- const gitPath = join15(cwd, ".git");
11099
- if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
11198
+ const gitPath = join17(cwd, ".git");
11199
+ if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
11100
11200
  let stat;
11101
11201
  try {
11102
11202
  stat = statSync2(gitPath);
11103
11203
  } catch (error) {
11104
11204
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
11105
11205
  }
11106
- if (stat.isDirectory() && !existsSync10(join15(gitPath, "HEAD")))
11206
+ if (stat.isDirectory() && !existsSync11(join17(gitPath, "HEAD")))
11107
11207
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
11108
11208
  return { ok: true, reason: "usable" };
11109
11209
  }
@@ -11216,18 +11316,18 @@ var Dispatcher = class {
11216
11316
  };
11217
11317
 
11218
11318
  // src/providers/local-process.ts
11219
- import { spawn as spawn6 } from "child_process";
11220
- import { constants } from "os";
11319
+ import { spawn as spawn5 } from "child_process";
11320
+ import { constants as constants2 } from "os";
11221
11321
  var DEFAULT_KILL_GRACE_MS = 3e4;
11222
11322
  var DEFAULT_REAP_GRACE_MS = 3e3;
11223
11323
  function exitCode(code, signal) {
11224
11324
  if (code !== null) return code;
11225
- const signum = signal ? constants.signals[signal] : void 0;
11325
+ const signum = signal ? constants2.signals[signal] : void 0;
11226
11326
  return signum !== void 0 ? 128 + signum : 1;
11227
11327
  }
11228
11328
  function createLocalProcessProvider(opts = {}) {
11229
11329
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
11230
- const spawnFn = opts.spawnFn ?? spawn6;
11330
+ const spawnFn = opts.spawnFn ?? spawn5;
11231
11331
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
11232
11332
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
11233
11333
  const groupAlive = (pgid) => {
@@ -11308,11 +11408,11 @@ function createLocalProcessProvider(opts = {}) {
11308
11408
 
11309
11409
  // src/providers/docker.ts
11310
11410
  import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
11311
- import { constants as constants2, tmpdir as tmpdir3 } from "os";
11411
+ import { constants as constants3, tmpdir as tmpdir3 } from "os";
11312
11412
  import {
11313
11413
  createReadStream,
11314
11414
  createWriteStream as createWriteStream2,
11315
- existsSync as existsSync13,
11415
+ existsSync as existsSync14,
11316
11416
  mkdirSync as mkdirSync10,
11317
11417
  readdirSync as readdirSync4,
11318
11418
  readFileSync as readFileSync9,
@@ -11321,11 +11421,11 @@ import {
11321
11421
  statSync as statSync3,
11322
11422
  writeFileSync as writeFileSync6
11323
11423
  } from "fs";
11324
- import { join as join18 } from "path";
11424
+ import { join as join20 } from "path";
11325
11425
  import { Writable } from "stream";
11326
11426
 
11327
11427
  // src/providers/docker-cli.ts
11328
- import { spawn as spawn7 } from "child_process";
11428
+ import { spawn as spawn6 } from "child_process";
11329
11429
  function cliEnv(extra) {
11330
11430
  const keep = [
11331
11431
  "PATH",
@@ -11345,7 +11445,7 @@ function dockerCli(bin = "docker") {
11345
11445
  return new Promise((resolve2) => {
11346
11446
  let child;
11347
11447
  try {
11348
- child = spawn7(bin, [...args], {
11448
+ child = spawn6(bin, [...args], {
11349
11449
  env: cliEnv(opts.env),
11350
11450
  stdio: ["pipe", "pipe", "pipe"]
11351
11451
  });
@@ -11376,13 +11476,13 @@ function dockerCli(bin = "docker") {
11376
11476
  });
11377
11477
  },
11378
11478
  spawn(args, env) {
11379
- return spawn7(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
11479
+ return spawn6(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
11380
11480
  }
11381
11481
  };
11382
11482
  }
11383
11483
 
11384
11484
  // src/runner/git-credential.ts
11385
- import { spawn as spawn8 } from "child_process";
11485
+ import { spawn as spawn7 } from "child_process";
11386
11486
  import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
11387
11487
  import { connect as connect3, createServer as createServer3 } from "net";
11388
11488
 
@@ -11424,7 +11524,7 @@ function deviceGitFill(helper, timeoutMs = 15e3) {
11424
11524
  "credential",
11425
11525
  "fill"
11426
11526
  ];
11427
- const child = spawn8("git", args, {
11527
+ const child = spawn7("git", args, {
11428
11528
  stdio: ["pipe", "pipe", "ignore"],
11429
11529
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
11430
11530
  });
@@ -11508,14 +11608,14 @@ async function serveGitCredentials(opts) {
11508
11608
  // src/runner/collected-outbox.ts
11509
11609
  import {
11510
11610
  copyFileSync as copyFileSync2,
11511
- existsSync as existsSync11,
11611
+ existsSync as existsSync12,
11512
11612
  mkdirSync as mkdirSync9,
11513
11613
  readdirSync as readdirSync3,
11514
11614
  readFileSync as readFileSync8,
11515
11615
  renameSync as renameSync4,
11516
11616
  rmSync as rmSync8
11517
11617
  } from "fs";
11518
- import { join as join16 } from "path";
11618
+ import { join as join18 } from "path";
11519
11619
  function routes(t) {
11520
11620
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
11521
11621
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -11557,11 +11657,11 @@ var atomicMove = (from, to) => {
11557
11657
  }
11558
11658
  };
11559
11659
  function publishStaged(from, to, move = atomicMove) {
11560
- if (!existsSync11(from)) return { published: 0, complete: true };
11660
+ if (!existsSync12(from)) return { published: 0, complete: true };
11561
11661
  const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
11562
11662
  let entry = {};
11563
11663
  try {
11564
- entry = JSON.parse(readFileSync8(join16(from, name), "utf8"));
11664
+ entry = JSON.parse(readFileSync8(join18(from, name), "utf8"));
11565
11665
  } catch {
11566
11666
  }
11567
11667
  return { name, entry };
@@ -11574,7 +11674,7 @@ function publishStaged(from, to, move = atomicMove) {
11574
11674
  let published = 0;
11575
11675
  for (const { name } of staged) {
11576
11676
  try {
11577
- move(join16(from, name), join16(to, name));
11677
+ move(join18(from, name), join18(to, name));
11578
11678
  } catch {
11579
11679
  return { published, complete: false };
11580
11680
  }
@@ -11586,9 +11686,9 @@ function publishStaged(from, to, move = atomicMove) {
11586
11686
 
11587
11687
  // src/providers/runner-base.ts
11588
11688
  import { createHash as createHash3 } from "crypto";
11589
- import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
11590
- import { dirname as dirname7, join as join17, sep } from "path";
11591
- import { fileURLToPath as fileURLToPath3 } from "url";
11689
+ import { existsSync as existsSync13, realpathSync as realpathSync3 } from "fs";
11690
+ import { dirname as dirname7, join as join19, sep as sep3 } from "path";
11691
+ import { fileURLToPath as fileURLToPath4 } from "url";
11592
11692
  var COMPANION_MOUNT = "/opt/cabane/companion";
11593
11693
  var BASE_DOCKERFILE = `FROM node:22-slim
11594
11694
  RUN apt-get update \\
@@ -11632,17 +11732,17 @@ async function ensureBaseImage(docker, image, log) {
11632
11732
  }
11633
11733
  return true;
11634
11734
  }
11635
- function ownInstall(moduleFile = fileURLToPath3(import.meta.url)) {
11735
+ function ownInstall(moduleFile = fileURLToPath4(import.meta.url)) {
11636
11736
  let dir2 = dirname7(moduleFile);
11637
11737
  for (let i = 0; i < 4; i++) {
11638
- if (existsSync12(join17(dir2, "dist", "cli.js")) && existsSync12(join17(dir2, "package.json"))) break;
11738
+ if (existsSync13(join19(dir2, "dist", "cli.js")) && existsSync13(join19(dir2, "package.json"))) break;
11639
11739
  dir2 = dirname7(dir2);
11640
11740
  }
11641
- if (!existsSync12(join17(dir2, "dist", "cli.js"))) return null;
11642
- const sdk = join17(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
11643
- if (!existsSync12(sdk)) return null;
11644
- const root = realpathSync2(dir2);
11645
- return realpathSync2(sdk).startsWith(root + sep) ? root : null;
11741
+ if (!existsSync13(join19(dir2, "dist", "cli.js"))) return null;
11742
+ const sdk = join19(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
11743
+ if (!existsSync13(sdk)) return null;
11744
+ const root = realpathSync3(dir2);
11745
+ return realpathSync3(sdk).startsWith(root + sep3) ? root : null;
11646
11746
  }
11647
11747
  function companionSource(input) {
11648
11748
  if (input.configured) {
@@ -11778,21 +11878,21 @@ function networkCreateError(stderr) {
11778
11878
  }
11779
11879
  function sessionArchivePath(deviceDir, attemptId) {
11780
11880
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
11781
- return join18(deviceDir, "attempts", attemptId, "session.tar");
11881
+ return join20(deviceDir, "attempts", attemptId, "session.tar");
11782
11882
  }
11783
11883
  function pruneOtherAttempts(deviceDir, pair, keep) {
11784
- const root = join18(deviceDir, "attempts");
11785
- if (!existsSync13(root)) return;
11884
+ const root = join20(deviceDir, "attempts");
11885
+ if (!existsSync14(root)) return;
11786
11886
  for (const id of readdirSync4(root)) {
11787
11887
  if (id === keep || !UUID_RE.test(id)) continue;
11788
11888
  let pairOf = "";
11789
11889
  try {
11790
- pairOf = readFileSync9(join18(root, id, "pair"), "utf8");
11890
+ pairOf = readFileSync9(join20(root, id, "pair"), "utf8");
11791
11891
  } catch {
11792
11892
  continue;
11793
11893
  }
11794
11894
  if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
11795
- rmSync9(join18(root, id), { recursive: true, force: true });
11895
+ rmSync9(join20(root, id), { recursive: true, force: true });
11796
11896
  }
11797
11897
  }
11798
11898
  }
@@ -11992,15 +12092,15 @@ function createDockerProvider(opts) {
11992
12092
  await must(["network", "disconnect", "-f", other, name], "moving the container");
11993
12093
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
11994
12094
  };
11995
- const socketRoot = opts.socketRoot ?? join18(
11996
- process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
12095
+ const socketRoot = opts.socketRoot ?? join20(
12096
+ process.env.XDG_RUNTIME_DIR && existsSync14(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
11997
12097
  `cabane-runners-${opts.deviceKey}`
11998
12098
  );
11999
- const socketDir = (name) => join18(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
12099
+ const socketDir = (name) => join20(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
12000
12100
  const create = async (name, attempt) => {
12001
12101
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
12002
12102
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
12003
- const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") && existsSync13(m.source)).flatMap((m) => ["--mount", `type=bind,src=${m.source},dst=${m.target},readonly`]);
12103
+ const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") && existsSync14(m.source)).flatMap((m) => ["--mount", `type=bind,src=${m.source},dst=${m.target},readonly`]);
12004
12104
  await must(
12005
12105
  [
12006
12106
  "create",
@@ -12138,7 +12238,7 @@ function createDockerProvider(opts) {
12138
12238
  }
12139
12239
  };
12140
12240
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
12141
- const pendingPath = (name) => join18(opts.deviceDir, "runners-pending", `${name}.json`);
12241
+ const pendingPath = (name) => join20(opts.deviceDir, "runners-pending", `${name}.json`);
12142
12242
  const readPending = (name) => {
12143
12243
  try {
12144
12244
  const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
@@ -12152,12 +12252,12 @@ function createDockerProvider(opts) {
12152
12252
  rmSync9(pendingPath(name), { force: true });
12153
12253
  return;
12154
12254
  }
12155
- mkdirSync10(join18(opts.deviceDir, "runners-pending"), { recursive: true });
12255
+ mkdirSync10(join20(opts.deviceDir, "runners-pending"), { recursive: true });
12156
12256
  writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
12157
12257
  };
12158
12258
  const keepSession = async (name, attempt, attemptId) => {
12159
12259
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
12160
- mkdirSync10(join18(archive, ".."), { recursive: true });
12260
+ mkdirSync10(join20(archive, ".."), { recursive: true });
12161
12261
  const partial = `${archive}.${process.pid}.partial`;
12162
12262
  const file = new CappedFile(partial, sessionCap);
12163
12263
  const r = await execIn(
@@ -12174,7 +12274,7 @@ function createDockerProvider(opts) {
12174
12274
  );
12175
12275
  if (r.code === 0 && !file.over) {
12176
12276
  renameSync5(partial, archive);
12177
- writeFileSync6(join18(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12277
+ writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12178
12278
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
12179
12279
  await writeMarker(name, attemptId);
12180
12280
  log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -12188,7 +12288,7 @@ function createDockerProvider(opts) {
12188
12288
  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"
12189
12289
  );
12190
12290
  };
12191
- const stagingDir = (name) => join18(opts.deviceDir, "runners-pending", `${name}.outbox`);
12291
+ const stagingDir = (name) => join20(opts.deviceDir, "runners-pending", `${name}.outbox`);
12192
12292
  const ensureRunning = async (name) => {
12193
12293
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
12194
12294
  if (seen.code !== 0) return false;
@@ -12196,7 +12296,7 @@ function createDockerProvider(opts) {
12196
12296
  return (await docker.run(["start", name])).code === 0;
12197
12297
  };
12198
12298
  const dropArchive = (attemptId) => {
12199
- rmSync9(join18(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
12299
+ rmSync9(join20(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
12200
12300
  recursive: true,
12201
12301
  force: true
12202
12302
  });
@@ -12265,7 +12365,7 @@ function createDockerProvider(opts) {
12265
12365
  if (seen.stdout.trim().split(" ")[0] !== "true")
12266
12366
  await must(["start", name], "starting the container");
12267
12367
  const git = await serveGitCredentials({
12268
- socketPath: join18(socketDir(name), "git.sock"),
12368
+ socketPath: join20(socketDir(name), "git.sock"),
12269
12369
  hosts: opts.gitHosts,
12270
12370
  fill: opts.gitFill,
12271
12371
  log
@@ -12299,7 +12399,7 @@ function createDockerProvider(opts) {
12299
12399
  }
12300
12400
  if (attemptId && UUID_RE.test(attemptId)) {
12301
12401
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
12302
- if (existsSync13(archive)) {
12402
+ if (existsSync14(archive)) {
12303
12403
  const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
12304
12404
  input: createReadStream(archive)
12305
12405
  });
@@ -12365,7 +12465,7 @@ function createDockerProvider(opts) {
12365
12465
  };
12366
12466
  child.once("error", () => done(1));
12367
12467
  child.once("close", (code, sig) => {
12368
- const signum = sig ? constants2.signals[sig] : void 0;
12468
+ const signum = sig ? constants3.signals[sig] : void 0;
12369
12469
  done(code ?? (signum !== void 0 ? 128 + signum : 1));
12370
12470
  });
12371
12471
  if (signal.aborted) stop();
@@ -12414,15 +12514,15 @@ function createDockerProvider(opts) {
12414
12514
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
12415
12515
  if (transcripts) {
12416
12516
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
12417
- if (kept.length) mkdirSync10(join18(opts.hostStateDir, "transcripts"), { recursive: true });
12418
- for (const f of kept) writeFileSync6(join18(opts.hostStateDir, "transcripts", f.name), f.data);
12517
+ if (kept.length) mkdirSync10(join20(opts.hostStateDir, "transcripts"), { recursive: true });
12518
+ for (const f of kept) writeFileSync6(join20(opts.hostStateDir, "transcripts", f.name), f.data);
12419
12519
  await removeIn(
12420
12520
  name,
12421
12521
  `${CONTAINER_STATE}/transcripts`,
12422
12522
  kept.map((f) => f.name)
12423
12523
  );
12424
12524
  }
12425
- const outboxRel = join18("outbox", encodeURIComponent(attempt.agentId));
12525
+ const outboxRel = join20("outbox", encodeURIComponent(attempt.agentId));
12426
12526
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
12427
12527
  let unpublished = false;
12428
12528
  if (outbox) {
@@ -12448,7 +12548,7 @@ function createDockerProvider(opts) {
12448
12548
  const staged = stagingDir(name);
12449
12549
  mkdirSync10(staged, { recursive: true });
12450
12550
  writeFileSync6(
12451
- join18(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12551
+ join20(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12452
12552
  `${JSON.stringify(entry)}
12453
12553
  `
12454
12554
  );
@@ -12461,7 +12561,7 @@ function createDockerProvider(opts) {
12461
12561
  if (outbox.complete) {
12462
12562
  const published = publishStaged(
12463
12563
  stagingDir(name),
12464
- join18(opts.hostStateDir, outboxRel),
12564
+ join20(opts.hostStateDir, outboxRel),
12465
12565
  opts.moveFile
12466
12566
  );
12467
12567
  queued = published.published;
@@ -12670,15 +12770,15 @@ import {
12670
12770
  mkdirSync as mkdirSync11,
12671
12771
  mkdtempSync,
12672
12772
  readFileSync as readFileSync10,
12673
- realpathSync as realpathSync3,
12773
+ realpathSync as realpathSync4,
12674
12774
  rmSync as rmSync10,
12675
12775
  writeFileSync as writeFileSync7
12676
12776
  } from "fs";
12677
12777
  import { createRequire as createRequire2 } from "module";
12678
12778
  import { isIP } from "net";
12679
- import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12680
- import { dirname as dirname8, join as join19, sep as sep2 } from "path";
12681
- import { fileURLToPath as fileURLToPath4 } from "url";
12779
+ import { homedir as homedir6, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12780
+ import { dirname as dirname8, join as join21, sep as sep4 } from "path";
12781
+ import { fileURLToPath as fileURLToPath5 } from "url";
12682
12782
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
12683
12783
  function accountHome() {
12684
12784
  try {
@@ -12774,13 +12874,13 @@ function companionStoreWatch(log) {
12774
12874
  return new CompanionStoreWatch({
12775
12875
  log,
12776
12876
  roots: () => {
12777
- const home = homedir3();
12877
+ const home = homedir6();
12778
12878
  return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
12779
12879
  }
12780
12880
  });
12781
12881
  }
12782
12882
  function isAtOrUnder(path, dir2) {
12783
- return path === dir2 || path.startsWith(dir2 + sep2);
12883
+ return path === dir2 || path.startsWith(dir2 + sep4);
12784
12884
  }
12785
12885
  function osSandboxUsable(log) {
12786
12886
  if (!SandboxManager.isSupportedPlatform()) {
@@ -12879,7 +12979,7 @@ var DispatchClaims = class {
12879
12979
 
12880
12980
  // src/outbox.ts
12881
12981
  import {
12882
- existsSync as existsSync14,
12982
+ existsSync as existsSync15,
12883
12983
  mkdirSync as mkdirSync12,
12884
12984
  readdirSync as readdirSync5,
12885
12985
  readFileSync as readFileSync11,
@@ -12887,7 +12987,7 @@ import {
12887
12987
  rmSync as rmSync11,
12888
12988
  writeFileSync as writeFileSync8
12889
12989
  } from "fs";
12890
- import { join as join20 } from "path";
12990
+ import { join as join22 } from "path";
12891
12991
  var MAX_ENTRIES = 2e3;
12892
12992
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
12893
12993
  var Outbox = class {
@@ -12900,10 +13000,10 @@ var Outbox = class {
12900
13000
  // Resolved lazily (per call) so tests that swap HOME between
12901
13001
  // cases route writes at the right tmpdir.
12902
13002
  dir() {
12903
- return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
13003
+ return join22(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
12904
13004
  }
12905
13005
  fileFor(turnId, seq) {
12906
- return join20(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
13006
+ return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
12907
13007
  }
12908
13008
  // Persist a commit for later draining. Atomic (temp file + rename) so a
12909
13009
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -12935,7 +13035,7 @@ var Outbox = class {
12935
13035
  // wedging the drain.
12936
13036
  list() {
12937
13037
  const dir2 = this.dir();
12938
- if (!existsSync14(dir2)) return [];
13038
+ if (!existsSync15(dir2)) return [];
12939
13039
  let names;
12940
13040
  try {
12941
13041
  names = readdirSync5(dir2);
@@ -12945,7 +13045,7 @@ var Outbox = class {
12945
13045
  const entries = [];
12946
13046
  for (const name of names) {
12947
13047
  if (!name.endsWith(".json")) continue;
12948
- const full = join20(dir2, name);
13048
+ const full = join22(dir2, name);
12949
13049
  try {
12950
13050
  const parsed = JSON.parse(readFileSync11(full, "utf8"));
12951
13051
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -12981,7 +13081,7 @@ var Outbox = class {
12981
13081
  // different: it is positive evidence that nothing was ever queued here.
12982
13082
  maxSeqForTurn(turnId) {
12983
13083
  const dir2 = this.dir();
12984
- if (!existsSync14(dir2)) return 0;
13084
+ if (!existsSync15(dir2)) return 0;
12985
13085
  const names = readdirSync5(dir2);
12986
13086
  const prefix = `${encodeURIComponent(turnId)}__`;
12987
13087
  let max = 0;
@@ -13001,7 +13101,7 @@ var Outbox = class {
13001
13101
  }
13002
13102
  size() {
13003
13103
  const dir2 = this.dir();
13004
- if (!existsSync14(dir2)) return 0;
13104
+ if (!existsSync15(dir2)) return 0;
13005
13105
  try {
13006
13106
  return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
13007
13107
  } catch {
@@ -13202,9 +13302,10 @@ var CompanionSupervisor = class {
13202
13302
  // surface, so the UI never disagrees with what is advertised. Null until the
13203
13303
  // first probe.
13204
13304
  harnessSignals = null;
13205
- // Fresh per attempt, never the cached heartbeat signal: installing a harness
13206
- // between attempts must be observed immediately.
13207
- probePresence;
13305
+ // Fresh per connect, cached for the heartbeat: a sign-in between attempts
13306
+ // must be observed immediately, and a heartbeat must spawn nothing.
13307
+ runHarnessCheck;
13308
+ harnessChecks = {};
13208
13309
  exitFn;
13209
13310
  reexecFn;
13210
13311
  dispatcherFactory;
@@ -13248,7 +13349,7 @@ var CompanionSupervisor = class {
13248
13349
  this.log = opts.log;
13249
13350
  this.hub = opts.hub;
13250
13351
  this.resolveBundled = opts.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
13251
- this.probePresence = opts.probePresence ?? probeHarnessPresence;
13352
+ this.runHarnessCheck = opts.checkHarness ?? ((runtime) => checkHarnessUsable(runtime));
13252
13353
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
13253
13354
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
13254
13355
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -13275,7 +13376,7 @@ var CompanionSupervisor = class {
13275
13376
  });
13276
13377
  }
13277
13378
  dockerRunners() {
13278
- return runnersEnabled(this.config) && this.config.runners?.provider === "docker";
13379
+ return dockerRunnersConfigured(this.config);
13279
13380
  }
13280
13381
  // Stand up the data plane: pair check, initial assignments pull, then the
13281
13382
  // heartbeat + poll loops. A companion with no device token (logged out) does
@@ -13322,10 +13423,10 @@ var CompanionSupervisor = class {
13322
13423
  else this.log.error({ posture }, `runners: ${summary}`);
13323
13424
  void this.companionStores().refresh();
13324
13425
  }
13325
- const startupHarnessRefresh = this.refreshHarnessStatuses();
13426
+ const startupHarnessRefresh = this.checkHarnesses().then(() => this.refreshHarnessStatuses());
13326
13427
  this.trackHeartbeat(startupHarnessRefresh);
13327
13428
  if (!this.config.deviceToken) {
13328
- this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
13429
+ this.log.warn("This device isn't paired. Run cabane start to pair it.");
13329
13430
  return;
13330
13431
  }
13331
13432
  this.deviceApi = new DeviceApi({
@@ -13361,7 +13462,7 @@ var CompanionSupervisor = class {
13361
13462
  },
13362
13463
  onAuthFailure: () => {
13363
13464
  this.log.error(
13364
- "Cabane no longer accepts this device. Run cabane-companion logout, then cabane-companion start to pair again."
13465
+ `Cabane no longer accepts this device. Run ${cabane("logout")}, then ${cabane("start")} to pair again.`
13365
13466
  );
13366
13467
  }
13367
13468
  });
@@ -13675,7 +13776,8 @@ var CompanionSupervisor = class {
13675
13776
  intended: isClaudeCodeConnected(this.config),
13676
13777
  bundled: this.bundledStatus("claude-code")
13677
13778
  },
13678
- codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
13779
+ codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") },
13780
+ install: companionInstall()
13679
13781
  });
13680
13782
  }
13681
13783
  // A missing binary is reported ONCE, and again only when the answer CHANGES,
@@ -13817,11 +13919,11 @@ var CompanionSupervisor = class {
13817
13919
  if (this.runnerCommand) return this.runnerCommand;
13818
13920
  return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
13819
13921
  }
13820
- buildDispatcher(ctx) {
13922
+ buildDispatcher(ctx2) {
13821
13923
  const hostLocal = localAgentConfig(this.config, {
13822
- agentId: ctx.agentId,
13823
- agentUsername: ctx.agentUsername,
13824
- workspaceSlug: ctx.workspaceSlug
13924
+ agentId: ctx2.agentId,
13925
+ agentUsername: ctx2.agentUsername,
13926
+ workspaceSlug: ctx2.workspaceSlug
13825
13927
  });
13826
13928
  const local = this.dockerRunners() && hostLocal.prepareHook ? {
13827
13929
  ...hostLocal,
@@ -13830,20 +13932,20 @@ var CompanionSupervisor = class {
13830
13932
  command: containerHookPath(hostLocal.prepareHook.command)
13831
13933
  }
13832
13934
  } : hostLocal;
13833
- if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
13935
+ if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx2, local });
13834
13936
  return new Dispatcher({
13835
13937
  deviceId: this.deviceId ?? void 0,
13836
- api: ctx.api,
13837
- baseUrl: ctx.baseUrl,
13838
- workspaceId: ctx.workspaceId,
13839
- workspaceSlug: ctx.workspaceSlug,
13840
- agentId: ctx.agentId,
13841
- agentUsername: ctx.agentUsername,
13842
- credential: ctx.credential,
13938
+ api: ctx2.api,
13939
+ baseUrl: ctx2.baseUrl,
13940
+ workspaceId: ctx2.workspaceId,
13941
+ workspaceSlug: ctx2.workspaceSlug,
13942
+ agentId: ctx2.agentId,
13943
+ agentUsername: ctx2.agentUsername,
13944
+ credential: ctx2.credential,
13843
13945
  local,
13844
- aborts: ctx.aborts,
13845
- runConfig: ctx.runConfig,
13846
- log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
13946
+ aborts: ctx2.aborts,
13947
+ runConfig: ctx2.runConfig,
13948
+ log: this.log.child({ agentName: ctx2.agentDisplayName ?? ctx2.agentUsername }),
13847
13949
  // Read PER TURN off the heartbeat's own predicates, so a harness installed
13848
13950
  // or connected after boot works on the next turn exactly as it appears on
13849
13951
  // the next beat, and a runtime advertised by one and refused by the other
@@ -13856,7 +13958,7 @@ var CompanionSupervisor = class {
13856
13958
  // DEFAULT_AGENT_IDLE_TIMEOUT_MS.
13857
13959
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
13858
13960
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
13859
- observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
13961
+ observer: this.hub.observerFor(ctx2.workspaceId, ctx2.agentId, ctx2.workspaceSlug),
13860
13962
  transcriptDir: transcriptsDir(),
13861
13963
  connectorHealth: this.connectorHealth,
13862
13964
  ...runnersEnabled(this.config) ? {
@@ -13867,7 +13969,7 @@ var CompanionSupervisor = class {
13867
13969
  reportRunnerGone: (report) => this.reportRunnerGone(report),
13868
13970
  connectorHealth: this.connectorHealth,
13869
13971
  locks: this.runnerLocks,
13870
- sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx)),
13972
+ sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx2)),
13871
13973
  policy: this.sandboxPolicy(),
13872
13974
  companionStores: () => this.companionStores().stores()
13873
13975
  }
@@ -14295,23 +14397,23 @@ var CompanionSupervisor = class {
14295
14397
  configureLogger(this.log, next);
14296
14398
  }
14297
14399
  // ---- harnesses ----
14298
- // Probe all three harnesses, cache signals and versions, and push the snapshot
14299
- // to the hub. `probeHarnessSignals` never throws, but this is wrapped anyway so
14300
- // a refresh can never take a heartbeat down.
14301
- async refreshHarnessStatuses(presence = {}) {
14400
+ // Assemble the signals from the cached sign-in checks, cache versions, and
14401
+ // push the snapshot to the hub. Runs no sign-in check: the heartbeat calls
14402
+ // this. `probeHarnessSignals` never throws, but this is wrapped anyway so a
14403
+ // refresh can never take a heartbeat down.
14404
+ async refreshHarnessStatuses() {
14302
14405
  try {
14303
14406
  const signals = await probeHarnessSignals(this.config, {
14304
- probePresence: this.probePresence,
14305
- presence,
14407
+ checks: this.harnessChecks,
14306
14408
  // Through the supervisor's seam, so a test pins the host fact.
14307
14409
  resolveBundled: this.resolveBundled
14308
14410
  });
14309
14411
  this.harnessSignals = signals;
14310
14412
  this.logBundledTransitions();
14311
14413
  this.harnessVersions = {
14312
- claudeCode: signals.claudeVersion,
14414
+ claudeCode: checkVersion(signals.claudeCheck),
14313
14415
  opencode: signals.opencodeVersion,
14314
- codex: signals.codexVersion
14416
+ codex: checkVersion(signals.codexCheck)
14315
14417
  };
14316
14418
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
14317
14419
  } catch (err) {
@@ -14321,9 +14423,32 @@ var CompanionSupervisor = class {
14321
14423
  );
14322
14424
  }
14323
14425
  }
14426
+ // Check one bundled harness's sign-in in this process, cache it for the
14427
+ // heartbeat, and — for a harness the user connected — seed connector health,
14428
+ // so Cabane says "sign-in needed" without waiting for a turn to fail. A
14429
+ // check that couldn't answer seeds nothing either way.
14430
+ async checkHarness(runtime) {
14431
+ let check;
14432
+ try {
14433
+ check = await this.runHarnessCheck(runtime);
14434
+ } catch {
14435
+ check = { status: "check_failed", reason: "spawn" };
14436
+ }
14437
+ this.harnessChecks[runtime] = check;
14438
+ const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
14439
+ if (intended && (isUsable(check) || check.status === "signed_out")) {
14440
+ this.connectorHealth.recordLocalCheck(runtime, isUsable(check));
14441
+ }
14442
+ return check;
14443
+ }
14444
+ async checkHarnesses() {
14445
+ if (this.dockerRunners()) return;
14446
+ await Promise.all([this.checkHarness("claude-code"), this.checkHarness("codex")]);
14447
+ }
14324
14448
  // The dashboard "recheck harnesses" control — an immediate re-probe so a user who
14325
14449
  // just installed/started a harness sees it flip without waiting for the next beat.
14326
14450
  async recheckHarnesses() {
14451
+ await this.checkHarnesses();
14327
14452
  await this.refreshHarnessStatuses();
14328
14453
  }
14329
14454
  // Beat NOW and wait for it to land. `start` calls this before asking the
@@ -14343,48 +14468,57 @@ var CompanionSupervisor = class {
14343
14468
  currentConfig() {
14344
14469
  return this.config;
14345
14470
  }
14346
- // Enable a config-driven harness by flipping the flag the app owns in
14471
+ // Connect a harness by flipping the flag the app owns in
14347
14472
  // `~/.cabane/config.json`. Never installs a binary or drives a login (BYO).
14348
14473
  //
14349
- // This records INTENT, the user's half: a harness is advertised only when also
14350
- // its bundled binary resolves, so an enable on a companion whose optional
14351
- // dependency never installed advertises nothing, and the Harnesses surface
14352
- // says why.
14353
- // - claude-code: `claudeCode.enabled`, after a presence check. The terminal
14354
- // offer and the web pairing flow both call this.
14355
- // - codex: `codex.enabled`, after a presence check (login stays the user's).
14474
+ // Claude Code and Codex are checked FIRST, in this process — the one that
14475
+ // will run their turns — and an agent that isn't usable here is refused before
14476
+ // anything is written: the terminal, the control socket and the dashboard all
14477
+ // come through here, so they all get the same answer. An agent already
14478
+ // connected is checked too and reported truthfully, with nothing written.
14356
14479
  // - opencode: `opencode.serverUrl`, ONLY after health-probing the URL; an
14357
14480
  // unreachable one is a typed failure shown in place.
14358
- // On success: persist, re-probe and beat at once, so the answer is true in
14359
- // Cabane promptly either way. Never throws for a valid input.
14481
+ // A connect records INTENT: a harness is advertised only when its bundled
14482
+ // binary also resolves. On success: persist, refresh and beat at once, so the
14483
+ // answer is true in Cabane promptly either way. Never throws for a valid input.
14360
14484
  async enableHarness(input) {
14361
14485
  let next;
14362
- let checkedPresence = null;
14363
- if (input.runtime === "claude-code") {
14364
- const presence = input.presence ?? await this.probePresence("claude-code");
14365
- if (presence.status !== "present") {
14486
+ let outcome;
14487
+ if (input.runtime === "claude-code" || input.runtime === "codex") {
14488
+ const runtime = input.runtime;
14489
+ const connected = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
14490
+ const check = this.dockerRunners() ? void 0 : await this.checkHarness(runtime);
14491
+ if (check && !isUsable(check)) {
14492
+ await this.refreshHarnessStatuses();
14493
+ this.kickHeartbeat();
14494
+ return { ok: false, outcome: { kind: "refused", runtime, check, connected } };
14495
+ }
14496
+ const label = this.config.deviceLabel;
14497
+ if (connected) {
14498
+ await this.refreshHarnessStatuses();
14499
+ this.kickHeartbeat();
14366
14500
  return {
14367
- ok: false,
14368
- error: presenceReason("claude-code", presence.status),
14369
- presence: true
14501
+ ok: true,
14502
+ outcome: {
14503
+ kind: "already",
14504
+ runtime,
14505
+ ...check ? { check } : {},
14506
+ ...label ? { deviceLabel: label } : {}
14507
+ }
14370
14508
  };
14371
14509
  }
14372
- checkedPresence = { runtime: "claude-code", result: presence };
14373
- next = { ...this.config, claudeCode: { enabled: true } };
14374
- } else if (input.runtime === "codex") {
14375
- const presence = input.presence ?? await this.probePresence("codex");
14376
- if (presence.status !== "present") {
14377
- return { ok: false, error: presenceReason("codex", presence.status), presence: true };
14378
- }
14379
- checkedPresence = { runtime: "codex", result: presence };
14380
- next = { ...this.config, codex: { enabled: true } };
14510
+ next = runtime === "codex" ? { ...this.config, codex: { enabled: true } } : { ...this.config, claudeCode: { enabled: true } };
14511
+ outcome = { kind: "connected", runtime, ...check ? { check } : {} };
14381
14512
  } else {
14382
14513
  const serverUrl = input.serverUrl.trim();
14383
14514
  const parsed = companionConfigSchema.shape.opencode.safeParse({ serverUrl });
14384
14515
  if (!parsed.success) {
14385
14516
  return {
14386
14517
  ok: false,
14387
- error: "That doesn\u2019t look like a valid URL (e.g. http://127.0.0.1:4096)."
14518
+ outcome: {
14519
+ kind: "opencode",
14520
+ message: "That doesn\u2019t look like a valid URL (e.g. http://127.0.0.1:4096)."
14521
+ }
14388
14522
  };
14389
14523
  }
14390
14524
  const boundedFetch = ((url, init2) => fetch(url, { ...init2, signal: AbortSignal.timeout(4e3) }));
@@ -14392,19 +14526,21 @@ var CompanionSupervisor = class {
14392
14526
  if (version === null) {
14393
14527
  return {
14394
14528
  ok: false,
14395
- error: `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
14529
+ outcome: {
14530
+ kind: "opencode",
14531
+ message: `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
14532
+ }
14396
14533
  };
14397
14534
  }
14398
14535
  next = { ...this.config, opencode: { serverUrl } };
14536
+ outcome = { kind: "connected", runtime: "opencode" };
14399
14537
  }
14400
14538
  this.config = next;
14401
14539
  saveConfig(next);
14402
14540
  this.rebuildDispatchers();
14403
- await this.refreshHarnessStatuses(
14404
- checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
14405
- );
14541
+ await this.refreshHarnessStatuses();
14406
14542
  this.kickHeartbeat();
14407
- return { ok: true };
14543
+ return { ok: true, outcome };
14408
14544
  }
14409
14545
  // Re-create every running agent's Dispatcher from the CURRENT config, keeping
14410
14546
  // the runner (its api, outbox, chains) intact, so constructor-captured options
@@ -14464,9 +14600,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
14464
14600
  }
14465
14601
  function defaultReexec() {
14466
14602
  clearRuntimeState();
14467
- void import("child_process").then(({ spawn: spawn9 }) => {
14603
+ void import("child_process").then(({ spawn: spawn8 }) => {
14468
14604
  try {
14469
- const child = spawn9(process.execPath, process.argv.slice(1), {
14605
+ const child = spawn8(process.execPath, process.argv.slice(1), {
14470
14606
  stdio: "inherit",
14471
14607
  detached: false
14472
14608
  });
@@ -14536,10 +14672,10 @@ function handleUncaught(log, err, origin) {
14536
14672
  }
14537
14673
 
14538
14674
  // src/crash-marker.ts
14539
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
14540
- import { join as join21 } from "path";
14675
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
14676
+ import { join as join23 } from "path";
14541
14677
  function crashMarkerPath() {
14542
- return join21(cabaneDir(), "last-error.json");
14678
+ return join23(cabaneDir(), "last-error.json");
14543
14679
  }
14544
14680
  function recordCrash(rec) {
14545
14681
  try {
@@ -14551,7 +14687,7 @@ function recordCrash(rec) {
14551
14687
  function clearCrash() {
14552
14688
  try {
14553
14689
  const path = crashMarkerPath();
14554
- if (existsSync15(path)) rmSync12(path, { force: true });
14690
+ if (existsSync16(path)) rmSync12(path, { force: true });
14555
14691
  } catch {
14556
14692
  }
14557
14693
  }
@@ -14561,17 +14697,14 @@ async function createCompanionRuntime(opts = {}) {
14561
14697
  const log = getLogger();
14562
14698
  installProcessSafetyNet(log);
14563
14699
  let cfg;
14564
- let claudeCode;
14565
14700
  try {
14566
- ({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
14567
- ...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
14568
- onMigrated: (migrated, onPath) => log.debug(
14701
+ cfg = requireStartConfig({
14702
+ onMigrated: (migrated) => log.debug(
14569
14703
  { claudeCode: isClaudeCodeConnected(migrated) },
14570
- onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
14704
+ "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)"
14571
14705
  )
14572
- }));
14573
- await warnAboutHarnessReadiness(cfg, {
14574
- probeClaude: async () => claudeCode,
14706
+ });
14707
+ warnAboutHarnessReadiness(cfg, {
14575
14708
  // The CLI's onboarding script says this in its own words, so it hands a
14576
14709
  // sink that logs instead. Every other caller keeps the stderr line.
14577
14710
  ...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
@@ -14586,13 +14719,6 @@ async function createCompanionRuntime(opts = {}) {
14586
14719
  throw err;
14587
14720
  }
14588
14721
  configureLogger(log, cfg);
14589
- const harnessVersions = await probeHarnessVersions({
14590
- // Connected AND its CLI answered — the two things that make a
14591
- // `claude --version` probe worth spawning. Not the offer predicate.
14592
- claudeCode: claudeCode && isClaudeCodeConnected(cfg),
14593
- opencodeServerUrl: cfg.opencode?.serverUrl,
14594
- codex: isCodexEnabled(cfg)
14595
- });
14596
14722
  const instanceId = randomUUID7();
14597
14723
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14598
14724
  const pre = readLiveRuntimeState();
@@ -14620,35 +14746,31 @@ async function createCompanionRuntime(opts = {}) {
14620
14746
  config: cfg,
14621
14747
  log,
14622
14748
  hub,
14623
- harnessVersions
14749
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
14624
14750
  });
14625
14751
  await supervisor.start();
14626
14752
  const connectHarness = async (runtime, serverUrl) => {
14627
- const currentConfig = supervisor.currentConfig();
14628
- const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
14629
- const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
14630
- const presence = runtime === "opencode" ? void 0 : await probeHarnessPresence(runtime);
14631
- const verdict = await shakeOutHarness(runtime, candidate, {
14632
- ...presence ? { presence } : {}
14633
- });
14634
- if (verdict === "absent" || verdict === "unusable") {
14635
- return { ok: false, error: presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION) };
14753
+ if (runtime !== "opencode") return (await supervisor.enableHarness({ runtime })).outcome;
14754
+ const configured = supervisor.currentConfig().opencode?.serverUrl;
14755
+ const resolved = await resolveOpencodeServerUrl(configured, serverUrl);
14756
+ if (!resolved) {
14757
+ return {
14758
+ kind: "opencode",
14759
+ message: "No opencode server is answering at http://127.0.0.1:4096. Start `opencode serve`, or pass its address with `--url`."
14760
+ };
14636
14761
  }
14637
- const result = await supervisor.enableHarness(
14638
- runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime, presence }
14639
- );
14640
- if (!result.ok) return { ok: false, error: result.error };
14641
- return { ok: true, message: connectedLine(runtime, verdict) };
14762
+ if (!serverUrl?.trim() && configured === resolved) {
14763
+ const label = supervisor.currentConfig().deviceLabel;
14764
+ return { kind: "already", runtime, ...label ? { deviceLabel: label } : {} };
14765
+ }
14766
+ return (await supervisor.enableHarness({ runtime: "opencode", serverUrl: resolved })).outcome;
14642
14767
  };
14643
14768
  let control = null;
14644
14769
  let dashboard = null;
14645
14770
  try {
14646
14771
  control = await startControlServer({
14647
14772
  status: () => hub.statusJson(),
14648
- connect: async (runtime, serverUrl) => {
14649
- const result = await connectHarness(runtime, serverUrl);
14650
- return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
14651
- },
14773
+ connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
14652
14774
  stop: () => void supervisor.requestStop()
14653
14775
  });
14654
14776
  if (opts.dashboard) {
@@ -14669,7 +14791,8 @@ async function createCompanionRuntime(opts = {}) {
14669
14791
  // marker records whether this companion is backgrounded (foreground start
14670
14792
  // leaves it unset → false).
14671
14793
  daemon: process.env.CABANE_COMPANION_DAEMON === "1",
14672
- instanceId
14794
+ instanceId,
14795
+ ...installedCommandPath() ? { command: installedCommandPath() } : {}
14673
14796
  });
14674
14797
  clearCrash();
14675
14798
  } catch (err) {