@cabane/companion 0.6.155 → 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,77 +779,38 @@ 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
814
  async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
554
815
  try {
555
816
  const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
@@ -557,256 +818,118 @@ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
557
818
  headers: { accept: "application/json" }
558
819
  });
559
820
  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 { homedir as homedir2 } from "os";
606
- import { dirname as dirname2, join as join2, sep } from "path";
607
- import { fileURLToPath } from "url";
608
- function nodeResolveSelf(specifier) {
609
- const here = fileURLToPath(import.meta.url);
610
- if (typeof import.meta.resolve === "function") {
611
- try {
612
- return fileURLToPath(import.meta.resolve(specifier));
613
- } catch {
614
- }
615
- }
616
- try {
617
- return createRequire(here).resolve(specifier);
618
- } catch {
619
- }
620
- return walkNodeModules(specifier, here);
621
- }
622
- function walkNodeModules(pkg2, fromPath) {
623
- let dir2 = dirname2(fromPath);
624
- for (; ; ) {
625
- const manifest = join2(dir2, "node_modules", pkg2, "package.json");
626
- if (nodeIsFile(manifest)) {
627
- try {
628
- return realpathSync(manifest);
629
- } catch {
630
- return manifest;
631
- }
632
- }
633
- const parent = dirname2(dir2);
634
- if (parent === dir2) return null;
635
- dir2 = parent;
636
- }
637
- }
638
- function nodeResolve(specifier, fromPath) {
639
- try {
640
- return createRequire(fromPath).resolve(specifier);
641
- } catch {
642
- return null;
643
- }
644
- }
645
- function nodeIsFile(path) {
646
- try {
647
- return statSync(path).isFile();
648
- } catch {
649
- return false;
650
- }
651
- }
652
- function detectPreferMusl(platform = process.platform) {
653
- if (platform !== "linux") return false;
654
- const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
655
- const header = report?.header;
656
- return report != null && header?.glibcVersionRuntime === void 0;
657
- }
658
- function defaultResolveEnv() {
659
- return {
660
- platform: process.platform,
661
- arch: process.arch,
662
- preferMusl: detectPreferMusl(),
663
- // Both resolutions start from this module, which tsup bundles into
664
- // `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
665
- // root — so one environment serves the runtime checks and the install
666
- // verifier alike.
667
- resolveSelf: nodeResolveSelf,
668
- resolve: nodeResolve,
669
- isFile: nodeIsFile
670
- };
671
- }
672
- var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
673
- function claudePlatformPackages(platform, arch, preferMusl) {
674
- if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
675
- if (platform === "linux") {
676
- return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
677
- }
678
- return [`${CLAUDE_SDK}-${platform}-${arch}`];
679
- }
680
- function resolveClaude(env) {
681
- const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
682
- const base = {
683
- runtime: "claude-code",
684
- packageName: candidates[0] ?? null,
685
- platform: env.platform,
686
- arch: env.arch
687
- };
688
- const sdkEntry = env.resolveSelf(CLAUDE_SDK);
689
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
690
- let sawPackage = null;
691
- for (const pkg2 of candidates) {
692
- const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
693
- if (!manifest) continue;
694
- sawPackage ??= pkg2;
695
- const exe = join2(dirname2(manifest), env.platform === "win32" ? "claude.exe" : "claude");
696
- if (env.isFile(exe)) {
697
- return { ...base, packageName: pkg2, status: "present", path: exe };
698
- }
699
- }
700
- return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
701
- }
702
- var CODEX_SDK = "@openai/codex-sdk";
703
- var CODEX_CLI = "@openai/codex";
704
- function codexTargetTriple(platform, arch) {
705
- if (platform === "linux" || platform === "android") {
706
- if (arch === "x64") return "x86_64-unknown-linux-musl";
707
- if (arch === "arm64") return "aarch64-unknown-linux-musl";
708
- return null;
709
- }
710
- if (platform === "darwin") {
711
- if (arch === "x64") return "x86_64-apple-darwin";
712
- if (arch === "arm64") return "aarch64-apple-darwin";
821
+ return extractOpencodeVersion(await res.json());
822
+ } catch {
713
823
  return null;
714
824
  }
715
- if (platform === "win32") {
716
- if (arch === "x64") return "x86_64-pc-windows-msvc";
717
- if (arch === "arm64") return "aarch64-pc-windows-msvc";
718
- 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);
832
+ }
833
+ for (const c of candidates) {
834
+ if (typeof c === "string") {
835
+ const parsed = parseVersionToken(c);
836
+ if (parsed) return parsed;
837
+ }
719
838
  }
720
839
  return null;
721
840
  }
722
- var CODEX_PACKAGE_BY_TRIPLE = {
723
- "x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
724
- "aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
725
- "x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
726
- "aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
727
- "x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
728
- "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"]
729
852
  };
730
- function resolveCodex(env) {
731
- const triple = codexTargetTriple(env.platform, env.arch);
732
- const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
733
- const base = {
734
- runtime: "codex",
735
- packageName: pkg2 ?? null,
736
- platform: env.platform,
737
- arch: env.arch
738
- };
739
- if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
740
- const sdkEntry = env.resolveSelf(CODEX_SDK);
741
- if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
742
- const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
743
- if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
744
- const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
745
- if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
746
- const packageRoot = join2(dirname2(platformManifest), "vendor", triple);
747
- const exeName = env.platform === "win32" ? "codex.exe" : "codex";
748
- const current = join2(packageRoot, "bin", exeName);
749
- if (env.isFile(current) && env.isFile(join2(packageRoot, "codex-package.json"))) {
750
- return { ...base, status: "present", path: current };
751
- }
752
- const legacy = join2(packageRoot, "codex", exeName);
753
- if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
754
- 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;
755
855
  }
756
- function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
757
- try {
758
- return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
759
- } 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) {
760
863
  return {
761
- runtime,
762
- status: "platform_package_missing",
763
- path: null,
764
- packageName: null,
765
- platform: env.platform,
766
- arch: env.arch
864
+ status: "bundle_missing",
865
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
767
866
  };
768
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
+ }
769
892
  }
770
- var HARNESS_LABEL = {
771
- "claude-code": "Claude Code",
772
- codex: "Codex"
773
- };
774
- var HARNESS_INTENT_WORD = {
775
- "claude-code": "connected",
776
- codex: "enabled"
777
- };
778
- function missingNoun(status) {
779
- return status === "executable_missing" ? "executable" : "platform package";
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";
780
898
  }
781
- var INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
782
- function companionInstall(execPath = process.execPath, home = homedir2()) {
783
- const real = (path) => {
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;
784
910
  try {
785
- return realpathSync(path);
911
+ child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
786
912
  } catch {
787
- return path;
913
+ resolve2({ code: null, output: "", error: "spawn" });
914
+ return;
788
915
  }
789
- };
790
- const scriptNode = join2(real(home), ".cabane", "node") + sep;
791
- return real(execPath).startsWith(scriptNode) ? "script" : "npm";
792
- }
793
- function repairCommands(install = companionInstall()) {
794
- return install === "script" ? [INSTALL_SCRIPT_COMMAND] : ["npm i -g @cabane/companion --include=optional"];
795
- }
796
- function repairAction(install = companionInstall()) {
797
- return `Reinstall with \`${repairCommands(install)[0]}\`.`;
798
- }
799
- function startupWarning(r) {
800
- const label = HARNESS_LABEL[r.runtime];
801
- 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.`;
802
- }
803
- function harnessIssueNote(runtime, status, install = companionInstall()) {
804
- const intent = HARNESS_INTENT_WORD[runtime];
805
- const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
806
- return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status)} is missing. ${repairAction(install)} Then restart the companion.`;
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
+ });
807
929
  }
808
- function turnIncompleteCopy(runtime) {
809
- 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;
810
933
  }
811
934
 
812
935
  // src/manifest.ts
@@ -864,13 +987,15 @@ function deriveHarnessSnapshot(signals) {
864
987
  }
865
988
  function deriveClaudeCode(signals, manifestHas) {
866
989
  const base = { runtime: "claude-code", label: LABELS["claude-code"] };
990
+ const version = checkVersion(signals.claudeCheck);
867
991
  if (manifestHas) {
868
992
  return {
869
993
  ...base,
870
994
  state: "exposed",
871
- version: signals.claudeVersion,
872
- detail: "Claude Code is connected and exposed to Cabane.",
873
- enable: null
995
+ version,
996
+ detail: exposedDetail("claude-code", signals.claudeCheck),
997
+ enable: null,
998
+ signIn: signals.claudeCheck?.status ?? null
874
999
  };
875
1000
  }
876
1001
  if (signals.claudeCodeConnected) {
@@ -879,37 +1004,56 @@ function deriveClaudeCode(signals, manifestHas) {
879
1004
  state: "needs_attention",
880
1005
  version: null,
881
1006
  detail: harnessIssueNote("claude-code", signals.claudeBundled),
882
- enable: null
1007
+ enable: null,
1008
+ signIn: signals.claudeCheck?.status ?? null
883
1009
  };
884
1010
  }
885
- if (signals.claudeOnPath) {
1011
+ if (signals.claudeCheck && isUsable(signals.claudeCheck)) {
886
1012
  return {
887
1013
  ...base,
888
1014
  state: "detected_not_exposed",
889
- version: signals.claudeVersion,
890
- // Connecting records INTENT, not runnability: with the bundled binary
891
- // missing a connect succeeds and nothing is advertised, so say so.
892
- 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()}`,
893
- 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
894
1019
  };
895
1020
  }
896
1021
  return {
897
1022
  ...base,
898
1023
  state: "not_detected",
899
1024
  version: null,
900
- detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
901
- enable: null
1025
+ detail: notUsableDetail("claude-code", signals.claudeCheck),
1026
+ enable: null,
1027
+ signIn: signals.claudeCheck?.status ?? null
902
1028
  };
903
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
+ }
904
1046
  function deriveCodex(signals, manifestHas) {
905
1047
  const base = { runtime: "codex", label: LABELS.codex };
1048
+ const version = checkVersion(signals.codexCheck);
906
1049
  if (manifestHas) {
907
1050
  return {
908
1051
  ...base,
909
1052
  state: "exposed",
910
- version: signals.codexVersion,
911
- detail: "Codex is enabled and exposed to Cabane.",
912
- enable: null
1053
+ version,
1054
+ detail: exposedDetail("codex", signals.codexCheck),
1055
+ enable: null,
1056
+ signIn: signals.codexCheck?.status ?? null
913
1057
  };
914
1058
  }
915
1059
  if (signals.codexEnabled) {
@@ -918,25 +1062,27 @@ function deriveCodex(signals, manifestHas) {
918
1062
  state: "needs_attention",
919
1063
  version: null,
920
1064
  detail: harnessIssueNote("codex", signals.codexBundled),
921
- enable: null
1065
+ enable: null,
1066
+ signIn: signals.codexCheck?.status ?? null
922
1067
  };
923
1068
  }
924
- if (signals.codexOnPath) {
1069
+ if (signals.codexCheck && isUsable(signals.codexCheck)) {
925
1070
  return {
926
1071
  ...base,
927
1072
  state: "detected_not_exposed",
928
- version: signals.codexVersion,
929
- // As for Claude Code: enabling is intent, not launchability.
930
- 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()}`,
931
- 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
932
1077
  };
933
1078
  }
934
1079
  return {
935
1080
  ...base,
936
1081
  state: "not_detected",
937
1082
  version: null,
938
- detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
939
- enable: null
1083
+ detail: notUsableDetail("codex", signals.codexCheck),
1084
+ enable: null,
1085
+ signIn: signals.codexCheck?.status ?? null
940
1086
  };
941
1087
  }
942
1088
  function deriveOpencode(signals, manifestHas) {
@@ -948,7 +1094,8 @@ function deriveOpencode(signals, manifestHas) {
948
1094
  state: "exposed",
949
1095
  version: signals.opencodeVersion,
950
1096
  detail: "An opencode server is reachable and exposed to Cabane.",
951
- enable: null
1097
+ enable: null,
1098
+ signIn: null
952
1099
  };
953
1100
  }
954
1101
  return {
@@ -956,7 +1103,8 @@ function deriveOpencode(signals, manifestHas) {
956
1103
  state: "needs_attention",
957
1104
  version: null,
958
1105
  detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
959
- enable: null
1106
+ enable: null,
1107
+ signIn: null
960
1108
  };
961
1109
  }
962
1110
  if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
@@ -965,7 +1113,8 @@ function deriveOpencode(signals, manifestHas) {
965
1113
  state: "detected_not_exposed",
966
1114
  version: signals.opencodeVersion,
967
1115
  detail: signals.opencodeDetectedUrl,
968
- enable: "opencode"
1116
+ enable: "opencode",
1117
+ signIn: null
969
1118
  };
970
1119
  }
971
1120
  return {
@@ -973,7 +1122,8 @@ function deriveOpencode(signals, manifestHas) {
973
1122
  state: "not_detected",
974
1123
  version: null,
975
1124
  detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
976
- enable: "opencode"
1125
+ enable: "opencode",
1126
+ signIn: null
977
1127
  };
978
1128
  }
979
1129
  var PROBE_TIMEOUT_MS = 4e3;
@@ -992,31 +1142,20 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
992
1142
  return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
993
1143
  }
994
1144
  async function probeHarnessSignals(cfg, deps = {}) {
995
- const probePresence = deps.probePresence ?? probeHarnessPresence;
996
1145
  const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
997
1146
  const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
998
1147
  const configuredServerUrl = cfg.opencode?.serverUrl;
999
1148
  const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
1000
- const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
1001
- deps.presence?.["claude-code"] ?? withTimeout(probePresence("claude-code"), { status: "absent" }),
1002
- deps.presence?.codex ?? withTimeout(probePresence("codex"), { status: "absent" }),
1003
- configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
1004
- ]);
1005
- const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
1006
- const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
1149
+ const opencodeVersion = configuredServerUrl ? await withTimeout(probeOpencode(opencodeProbeUrl), null) : await probeDefaultOpencodeVersion(probeOpencode);
1007
1150
  return {
1008
- claudeOnPath: claudePresence.status === "present",
1009
- claudeVersion,
1010
- // 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.
1011
1153
  claudeCodeConnected: isClaudeCodeConnected(cfg),
1012
- // 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
1013
1155
  // binary that disappears is caught on the next beat, not at turn time.
1014
1156
  claudeBundled: resolveBundled("claude-code").status,
1015
1157
  codexBundled: resolveBundled("codex").status,
1016
- // A parseable `codex --version` is our presence signal (presence alone never
1017
- // exposes codex; its config flag is the manifest gate either way).
1018
- codexOnPath: codexVersion !== null,
1019
- codexVersion,
1158
+ codexCheck: deps.checks?.codex ?? null,
1020
1159
  codexEnabled: isCodexEnabled(cfg),
1021
1160
  opencodeConfigured: !!configuredServerUrl,
1022
1161
  // A version came back ⟺ the serve answered its health endpoint.
@@ -1049,104 +1188,88 @@ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
1049
1188
  });
1050
1189
  }
1051
1190
 
1052
- // src/harness-check.ts
1053
- var CHECK_TIMEOUT_MS = 4e3;
1054
- async function shakeOutHarness(runtime, cfg, deps = {}) {
1055
- const run = deps.run ?? runBounded;
1056
- const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
1057
- try {
1058
- if (runtime === "opencode") {
1059
- const url = cfg.opencode?.serverUrl;
1060
- if (!url) return "absent";
1061
- return await probeOpencode(url) !== null ? "ok" : "failed";
1062
- }
1063
- const { auth } = runtime === "codex" ? {
1064
- auth: ["codex", ["login", "status"]]
1065
- } : {
1066
- auth: ["claude", ["auth", "status"]]
1067
- };
1068
- const presence = deps.presence ?? await (deps.probePresence ?? probeHarnessPresence)(runtime);
1069
- if (presence.status !== "present") return presence.status;
1070
- const authRun = await run(auth[0], [...auth[1]]);
1071
- if (authRun.code === 0) return "ok";
1072
- if (looksUnsupported(authRun.output)) {
1073
- return "unverified";
1074
- }
1075
- return "failed";
1076
- } catch {
1077
- return "unverified";
1078
- }
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";
1079
1195
  }
1080
- function connectedLine(runtime, verdict) {
1081
- if (verdict === "absent" || verdict === "unusable") {
1082
- throw new Error("an unavailable harness cannot be connected");
1083
- }
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) {
1084
1206
  const label = HARNESS_LABELS[runtime];
1085
- if (verdict !== "failed") return `${label} connected.`;
1086
- 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}.`;
1087
1212
  }
1088
- var FAILED_SUFFIX = {
1089
- "claude-code": "it doesn\u2019t look signed in yet. Run `claude` once and sign in, then it\u2019s ready.",
1090
- codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
1091
- opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
1092
- };
1093
- function presenceReason(runtime, status) {
1094
- if (runtime === "opencode") {
1095
- return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
1096
- }
1213
+ function failureText(runtime, reason) {
1097
1214
  const label = HARNESS_LABELS[runtime];
1098
- 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
+ }
1099
1225
  }
1100
- var CLI_PRESENCE_ACTION = "Install it and run this again.";
1101
- var DASHBOARD_PRESENCE_ACTION = "Install it, then refresh.";
1102
- function presenceRefusal(runtime, status, action) {
1103
- const reason = presenceReason(runtime, status);
1104
- 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.`;
1105
1229
  }
1106
- function looksUnsupported(output) {
1107
- return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
1108
- output
1109
- );
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;
1110
1234
  }
1111
- function runBounded(command, args) {
1112
- return new Promise((resolve2) => {
1113
- let settled = false;
1114
- const done = (result) => {
1115
- if (settled) return;
1116
- settled = true;
1117
- clearTimeout(timer);
1118
- resolve2(result);
1119
- };
1120
- let child;
1121
- try {
1122
- child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1123
- } catch {
1124
- resolve2({ code: null, output: "", error: "spawn" });
1125
- return;
1126
- }
1127
- let out = "";
1128
- const capture = (chunk) => {
1129
- 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
+ ]
1130
1255
  };
1131
- child.stdout?.on("data", capture);
1132
- child.stderr?.on("data", capture);
1133
- const timer = setTimeout(() => {
1134
- child.kill("SIGKILL");
1135
- done({ code: null, output: out, error: "timeout" });
1136
- }, CHECK_TIMEOUT_MS);
1137
- timer.unref?.();
1138
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1139
- child.once("exit", (code) => done({ code, output: out }));
1140
- });
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
+ };
1141
1264
  }
1142
1265
 
1143
1266
  // src/logger.ts
1144
1267
  import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
1145
- import { dirname as dirname3, join as join3 } from "path";
1268
+ import { dirname as dirname3, join as join5 } from "path";
1146
1269
  import { stripVTControlCharacters } from "util";
1147
1270
  import pino from "pino";
1148
1271
  function companionLogPath() {
1149
- return join3(cabaneDir(), "companion.log");
1272
+ return join5(cabaneDir(), "companion.log");
1150
1273
  }
1151
1274
  function humanText(value) {
1152
1275
  return stripVTControlCharacters(String(value ?? "")).replace(
@@ -1499,14 +1622,14 @@ var CONTENT_TYPES = {
1499
1622
  function registerRoutes(app, deps) {
1500
1623
  const { supervisor, hub, staticDir } = deps;
1501
1624
  app.get("/", async (c) => {
1502
- const html = await readFile(join4(staticDir, "index.html"), "utf8");
1625
+ const html = await readFile(join6(staticDir, "index.html"), "utf8");
1503
1626
  return c.html(html);
1504
1627
  });
1505
1628
  app.get("/static/:file", async (c) => {
1506
1629
  const file = c.req.param("file");
1507
- const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
1508
- const full = join4(staticDir, safe3);
1509
- 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();
1510
1633
  const body = await readFile(full);
1511
1634
  const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
1512
1635
  c.header("content-type", type);
@@ -1559,20 +1682,9 @@ function registerRoutes(app, deps) {
1559
1682
  app.post("/api/harnesses/enable", async (c) => {
1560
1683
  const body = await readJson(c);
1561
1684
  const runtime = body.runtime;
1562
- if (runtime === "claude-code") {
1563
- const result = await supervisor.enableHarness({ runtime: "claude-code" });
1564
- if (!result.ok) {
1565
- const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
1566
- return c.json({ error }, 400);
1567
- }
1568
- return c.json({ ok: true, status: hub.statusJson() });
1569
- }
1570
- if (runtime === "codex") {
1571
- const result = await supervisor.enableHarness({ runtime: "codex" });
1572
- if (!result.ok) {
1573
- const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
1574
- return c.json({ error }, 400);
1575
- }
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);
1576
1688
  return c.json({ ok: true, status: hub.statusJson() });
1577
1689
  }
1578
1690
  if (runtime === "opencode") {
@@ -1583,7 +1695,7 @@ function registerRoutes(app, deps) {
1583
1695
  runtime: "opencode",
1584
1696
  serverUrl: body.serverUrl
1585
1697
  });
1586
- if (!result.ok) return c.json({ error: result.error }, 400);
1698
+ if (!result.ok) return c.json({ error: connectLines(result.outcome).lines.join(" ") }, 400);
1587
1699
  return c.json({ ok: true, status: hub.statusJson() });
1588
1700
  }
1589
1701
  return c.json({ error: `can't enable "${String(runtime)}" from the app` }, 400);
@@ -1639,7 +1751,7 @@ function clampLimit(raw, fallback, max = 200) {
1639
1751
  return Math.min(Math.floor(n), max);
1640
1752
  }
1641
1753
  function tailFile(path, lines2) {
1642
- if (!existsSync2(path)) return [];
1754
+ if (!existsSync3(path)) return [];
1643
1755
  const MAX_BYTES = 256 * 1024;
1644
1756
  let fd;
1645
1757
  try {
@@ -1728,14 +1840,14 @@ function isAddrInUse(err) {
1728
1840
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
1729
1841
  }
1730
1842
  function resolveStaticDir() {
1731
- return join5(dirname4(fileURLToPath2(import.meta.url)), "static");
1843
+ return join7(dirname4(fileURLToPath3(import.meta.url)), "static");
1732
1844
  }
1733
1845
 
1734
1846
  // src/control-socket.ts
1735
1847
  import { createHash } from "crypto";
1736
- 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";
1737
1849
  import { createServer, connect } from "net";
1738
- import { join as join6 } from "path";
1850
+ import { join as join8 } from "path";
1739
1851
  var CONTROL_TIMEOUT_MS = 1e3;
1740
1852
  var MAX_UNIX_SOCKET_PATH_BYTES = 103;
1741
1853
  function controlSocketPath() {
@@ -1744,16 +1856,16 @@ function controlSocketPath() {
1744
1856
  if (process.platform === "win32") {
1745
1857
  return `\\\\.\\pipe\\cabane-companion-${key}`;
1746
1858
  }
1747
- const besideRuntime = join6(dir2, "companion.sock");
1859
+ const besideRuntime = join8(dir2, "companion.sock");
1748
1860
  if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
1749
1861
  const filename = `cabane-companion-${key}.sock`;
1750
- const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join6(process.env.XDG_RUNTIME_DIR, filename) : null;
1751
- 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);
1752
1864
  }
1753
1865
  async function startControlServer(handlers) {
1754
1866
  const path = controlSocketPath();
1755
1867
  mkdirSync3(cabaneDir(), { recursive: true });
1756
- if (process.platform !== "win32" && existsSync3(path)) {
1868
+ if (process.platform !== "win32" && existsSync4(path)) {
1757
1869
  const alive = await ping(path);
1758
1870
  if (alive) throw new Error(`another companion is already listening on ${path}`);
1759
1871
  rmSync2(path, { force: true });
@@ -1813,7 +1925,9 @@ async function serveConnection(socket, handlers) {
1813
1925
  return;
1814
1926
  }
1815
1927
  if (req.cmd === "connect") {
1816
- 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 };
1817
1931
  reply(socket, result);
1818
1932
  return;
1819
1933
  }
@@ -1903,77 +2017,38 @@ async function ping(path) {
1903
2017
  }
1904
2018
 
1905
2019
  // src/prereqs.ts
1906
- async function claudeOnPath() {
1907
- return (await probeHarnessPresence("claude-code")).status === "present";
1908
- }
1909
- async function codexOnPath() {
1910
- return (await probeHarnessPresence("codex")).status === "present";
1911
- }
1912
- async function requireStartConfig(deps = {}) {
2020
+ function requireStartConfig(deps = {}) {
1913
2021
  const requireCfg = deps.requireCfg ?? requireConfig;
1914
- const probeClaude = deps.probeClaude ?? claudeOnPath;
1915
2022
  const save2 = deps.save ?? saveConfig;
1916
2023
  const cfg = requireCfg();
1917
- const claudeOnPathResult = await probeClaude();
1918
- const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
1919
- if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
2024
+ const migrated = migrateConnectedHarnesses(cfg);
2025
+ if (!migrated) return cfg;
1920
2026
  save2(migrated);
1921
- deps.onMigrated?.(migrated, claudeOnPathResult);
1922
- return { cfg: migrated, claudeOnPath: claudeOnPathResult };
2027
+ deps.onMigrated?.(migrated);
2028
+ return migrated;
1923
2029
  }
1924
- async function warnAboutHarnessReadiness(cfg, deps = {}) {
1925
- const probeClaude = deps.probeClaude ?? claudeOnPath;
1926
- const probeCodex = deps.probeCodex ?? codexOnPath;
2030
+ function warnAboutHarnessReadiness(cfg, deps = {}) {
1927
2031
  const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
1928
2032
  const warn = deps.warn ?? ((message2) => process.stderr.write(`${message2}
1929
2033
  `));
1930
- const connected = [
1931
- ...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
1932
- ...isCodexEnabled(cfg) ? ["Codex"] : [],
1933
- ...cfg.opencode ? ["opencode"] : []
1934
- ];
1935
- if (connected.length > 0) {
1936
- for (const runtime of ["claude-code", "codex"]) {
1937
- const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
1938
- if (!intended) continue;
1939
- const resolution = resolveBundled(runtime);
1940
- if (resolution.status !== "present") warn(startupWarning(resolution));
1941
- }
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
+ );
1942
2039
  return;
1943
2040
  }
1944
- const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
1945
- const found = [
1946
- ...claudeInstalled ? [
1947
- {
1948
- label: "Claude Code",
1949
- command: "`cabane-companion connect claude-code`",
1950
- bundled: resolveBundled("claude-code").status
1951
- }
1952
- ] : [],
1953
- ...codexInstalled ? [
1954
- {
1955
- label: "Codex",
1956
- command: "`cabane-companion connect codex`",
1957
- bundled: resolveBundled("codex").status
1958
- }
1959
- ] : []
1960
- ];
1961
- const runnable = found.filter((h) => h.bundled === "present");
1962
- const incomplete = found.filter((h) => h.bundled !== "present");
1963
- const incompleteNote = incomplete.map(
1964
- (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.`
1965
- ).join(" ");
1966
- 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 ? (
1967
- // Their machine is fine; this companion's install is not. Repair that,
1968
- // then connect — no "install a harness" they already have.
1969
- `${incompleteNote} ${repairAction()} Then connect it with ${incomplete.map((h) => h.command).join(" or ")}.`
1970
- ) : "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).";
1971
- 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
+ }
1972
2047
  }
1973
2048
 
1974
2049
  // src/runtime-file.ts
1975
2050
  import {
1976
- existsSync as existsSync4,
2051
+ existsSync as existsSync5,
1977
2052
  readFileSync as readFileSync2,
1978
2053
  rmSync as rmSync3,
1979
2054
  writeFileSync as writeFileSync2,
@@ -1981,10 +2056,10 @@ import {
1981
2056
  openSync as openSync2,
1982
2057
  closeSync as closeSync2
1983
2058
  } from "fs";
1984
- import { join as join7 } from "path";
2059
+ import { join as join9 } from "path";
1985
2060
  var PROBE_TIMEOUT_MS2 = 1e3;
1986
2061
  function runtimePath() {
1987
- return join7(cabaneDir(), "runtime.json");
2062
+ return join9(cabaneDir(), "runtime.json");
1988
2063
  }
1989
2064
  function serialize(state) {
1990
2065
  return JSON.stringify(state, null, 2) + "\n";
@@ -2013,11 +2088,11 @@ function acquireRuntimeState(state) {
2013
2088
  }
2014
2089
  function clearRuntimeState() {
2015
2090
  const path = runtimePath();
2016
- if (existsSync4(path)) rmSync3(path, { force: true });
2091
+ if (existsSync5(path)) rmSync3(path, { force: true });
2017
2092
  }
2018
2093
  function clearRuntimeStateIfOurs(instanceId) {
2019
2094
  const path = runtimePath();
2020
- if (!existsSync4(path)) return;
2095
+ if (!existsSync5(path)) return;
2021
2096
  try {
2022
2097
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
2023
2098
  if (parsed.instanceId && parsed.instanceId !== instanceId) return;
@@ -2027,7 +2102,7 @@ function clearRuntimeStateIfOurs(instanceId) {
2027
2102
  }
2028
2103
  function readLiveRuntimeState() {
2029
2104
  const path = runtimePath();
2030
- if (!existsSync4(path)) return null;
2105
+ if (!existsSync5(path)) return null;
2031
2106
  let parsed;
2032
2107
  try {
2033
2108
  parsed = JSON.parse(readFileSync2(path, "utf8"));
@@ -2192,7 +2267,7 @@ function turnFailureCopy(reason, runtime) {
2192
2267
  return "Claude Code sign-in has expired on this machine. Open Claude Code, run /login, then reply in Cabane to try again.";
2193
2268
  if (runtime === "codex")
2194
2269
  return "Codex sign-in has expired on this machine. Run codex login, then reply in Cabane to try again.";
2195
- 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.";
2196
2271
  }
2197
2272
  if (failure?.kind === "usage_capped")
2198
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.";
@@ -2203,9 +2278,9 @@ function turnFailureCopy(reason, runtime) {
2203
2278
  if (reason?.startsWith("missing_secret:"))
2204
2279
  return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
2205
2280
  if (reason?.startsWith("runtime_incomplete:"))
2206
- 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.";
2207
2282
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
2208
- 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.";
2209
2284
  if (reason?.startsWith("model_unavailable:"))
2210
2285
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
2211
2286
  if (reason?.startsWith("prepare_failed:"))
@@ -2213,17 +2288,17 @@ function turnFailureCopy(reason, runtime) {
2213
2288
  if (reason?.startsWith("fetch_failed:"))
2214
2289
  return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
2215
2290
  if (reason === "timeout_idle")
2216
- return "The coding agent stopped responding. Reply in Cabane to try again.";
2291
+ return "The agent stopped responding. Reply in Cabane to try again.";
2217
2292
  if (reason === "timeout_total")
2218
2293
  return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
2219
2294
  if (reason === "result_error:error_max_turns")
2220
- 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.";
2221
2296
  if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
2222
- 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.";
2223
2298
  if (reason === "session_start_failed")
2224
- 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.";
2225
2300
  if (reason === "no_terminal")
2226
- 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.";
2227
2302
  if (reason === "lease_unconfirmed")
2228
2303
  return "Could not confirm with Cabane that the turn could start. Check this machine\u2019s connection, then reply in Cabane to try again.";
2229
2304
  if (reason === "workspace_tools_missing")
@@ -2231,13 +2306,13 @@ function turnFailureCopy(reason, runtime) {
2231
2306
  if (reason?.startsWith("seq_floor_unavailable:"))
2232
2307
  return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
2233
2308
  if (reason === "result_error:error_max_budget_usd")
2234
- 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.";
2235
2310
  if (reason === "result_error:error_max_structured_output_retries")
2236
- 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.";
2237
2312
  if (reason?.startsWith("result_error:"))
2238
- 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.";
2239
2314
  if (reason === "setup_failed")
2240
- 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.";
2241
2316
  if (reason === "unexpected_role")
2242
2317
  return "The message could not start an agent response. Send a new message in Cabane to try again.";
2243
2318
  if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
@@ -2245,11 +2320,11 @@ function turnFailureCopy(reason, runtime) {
2245
2320
  const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
2246
2321
  if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
2247
2322
  const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
2248
- 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.`;
2249
2324
  }
2250
2325
  if (detail)
2251
- return `The coding agent reported: ${detail}. Check the coding agent setup on this machine, then reply in Cabane to try again.`;
2252
- 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.";
2253
2328
  }
2254
2329
 
2255
2330
  // src/api-error-shape.ts
@@ -2883,22 +2958,22 @@ function errorMessage2(status, body) {
2883
2958
  // src/credentials.ts
2884
2959
  import {
2885
2960
  chmodSync as chmodSync3,
2886
- existsSync as existsSync5,
2961
+ existsSync as existsSync6,
2887
2962
  mkdirSync as mkdirSync5,
2888
2963
  readFileSync as readFileSync3,
2889
2964
  renameSync as renameSync2,
2890
2965
  rmSync as rmSync4,
2891
2966
  writeFileSync as writeFileSync3
2892
2967
  } from "fs";
2893
- import { dirname as dirname5, join as join8 } from "path";
2968
+ import { dirname as dirname5, join as join10 } from "path";
2894
2969
  import { z as z4 } from "zod";
2895
2970
  function credentialsPath() {
2896
- return join8(cabaneDir(), "credentials.json");
2971
+ return join10(cabaneDir(), "credentials.json");
2897
2972
  }
2898
2973
  var credentialStoreSchema = z4.record(z4.string(), z4.string());
2899
2974
  function load() {
2900
2975
  const path = credentialsPath();
2901
- if (!existsSync5(path)) return {};
2976
+ if (!existsSync6(path)) return {};
2902
2977
  let raw;
2903
2978
  try {
2904
2979
  raw = readFileSync3(path, "utf8");
@@ -2960,18 +3035,18 @@ function pruneCredentials(keepAgentIds) {
2960
3035
  }
2961
3036
 
2962
3037
  // src/dispatch-dedupe.ts
2963
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
2964
- 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";
2965
3040
  var MAX_IDS = 256;
2966
3041
  function dir(log) {
2967
- return join9(cabaneDir(), log);
3042
+ return join11(cabaneDir(), log);
2968
3043
  }
2969
3044
  function pathFor(log, workspaceId) {
2970
- return join9(dir(log), encodeURIComponent(workspaceId));
3045
+ return join11(dir(log), encodeURIComponent(workspaceId));
2971
3046
  }
2972
3047
  function readIds(log, workspaceId) {
2973
3048
  const path = pathFor(log, workspaceId);
2974
- if (!existsSync6(path)) return [];
3049
+ if (!existsSync7(path)) return [];
2975
3050
  try {
2976
3051
  return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2977
3052
  } catch {
@@ -3004,15 +3079,15 @@ function markCompleted(workspaceId, eventId) {
3004
3079
  }
3005
3080
  var MAX_RESUME_ATTEMPTS = 3;
3006
3081
  function resumeDir() {
3007
- return join9(cabaneDir(), "resume-attempts");
3082
+ return join11(cabaneDir(), "resume-attempts");
3008
3083
  }
3009
3084
  function resumePathFor(workspaceId) {
3010
- return join9(resumeDir(), encodeURIComponent(workspaceId));
3085
+ return join11(resumeDir(), encodeURIComponent(workspaceId));
3011
3086
  }
3012
3087
  function readResumeCounts(workspaceId) {
3013
3088
  const out = /* @__PURE__ */ new Map();
3014
3089
  const path = resumePathFor(workspaceId);
3015
- if (!existsSync6(path)) return out;
3090
+ if (!existsSync7(path)) return out;
3016
3091
  try {
3017
3092
  for (const line of readFileSync4(path, "utf8").split("\n")) {
3018
3093
  const trimmed = line.trim();
@@ -3043,16 +3118,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
3043
3118
  return next;
3044
3119
  }
3045
3120
  function turnDir() {
3046
- return join9(cabaneDir(), "turns");
3121
+ return join11(cabaneDir(), "turns");
3047
3122
  }
3048
3123
  function turnPathFor(workspaceId) {
3049
- return join9(turnDir(), encodeURIComponent(workspaceId));
3124
+ return join11(turnDir(), encodeURIComponent(workspaceId));
3050
3125
  }
3051
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;
3052
3127
  function readTurnIds(workspaceId) {
3053
3128
  const out = /* @__PURE__ */ new Map();
3054
3129
  const path = turnPathFor(workspaceId);
3055
- if (!existsSync6(path)) return out;
3130
+ if (!existsSync7(path)) return out;
3056
3131
  try {
3057
3132
  for (const line of readFileSync4(path, "utf8").split("\n")) {
3058
3133
  const trimmed = line.trim();
@@ -3964,8 +4039,8 @@ function readThinking(runtimeOptions) {
3964
4039
  var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
3965
4040
  function parseClaudeCodeModel(model) {
3966
4041
  if (!model) return null;
3967
- const sep4 = model.indexOf("/");
3968
- const bare = sep4 === -1 || model.slice(0, sep4) !== "anthropic" ? model : model.slice(sep4 + 1);
4042
+ const sep5 = model.indexOf("/");
4043
+ const bare = sep5 === -1 || model.slice(0, sep5) !== "anthropic" ? model : model.slice(sep5 + 1);
3969
4044
  return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
3970
4045
  }
3971
4046
 
@@ -4148,7 +4223,7 @@ function dedupe(xs) {
4148
4223
  }
4149
4224
 
4150
4225
  // packages/agent-runtime/src/claude-code/loop.ts
4151
- async function* decodeSdkStream(iter, ctx) {
4226
+ async function* decodeSdkStream(iter, ctx2) {
4152
4227
  const pending = /* @__PURE__ */ new Map();
4153
4228
  const buffer = createTerminalTextBuffer();
4154
4229
  const out = [];
@@ -4169,7 +4244,7 @@ async function* decodeSdkStream(iter, ctx) {
4169
4244
  let preInitNotice = null;
4170
4245
  try {
4171
4246
  for await (const msg of iter) {
4172
- if (ctx.signal.aborted) return;
4247
+ if (ctx2.signal.aborted) return;
4173
4248
  if (msg.type === "system" && msg.subtype === "init") {
4174
4249
  const initModel = msg.model;
4175
4250
  if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
@@ -4180,19 +4255,19 @@ async function* decodeSdkStream(iter, ctx) {
4180
4255
  break;
4181
4256
  }
4182
4257
  const sdkSessionId = msg.session_id;
4183
- if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx.resumedSessionId) {
4258
+ if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx2.resumedSessionId) {
4184
4259
  sessionEmitted = true;
4185
4260
  yield {
4186
4261
  type: "session",
4187
- state: encodeSession({ sdkSessionId, cwd: ctx.cwd ?? "" }),
4188
- ...ctx.degraded ? { degraded: true } : {}
4262
+ state: encodeSession({ sdkSessionId, cwd: ctx2.cwd ?? "" }),
4263
+ ...ctx2.degraded ? { degraded: true } : {}
4189
4264
  };
4190
4265
  }
4191
4266
  } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
4192
4267
  const compaction = readCompaction(msg);
4193
4268
  if (compaction) compactions.push(compaction);
4194
4269
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4195
- if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
4270
+ if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
4196
4271
  const failure = classifyPreInitFrame(msg, signals);
4197
4272
  resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4198
4273
  if (msg.type === "assistant" && isHarnessAuthored(msg))
@@ -4236,7 +4311,7 @@ async function* decodeSdkStream(iter, ctx) {
4236
4311
  const window = readContextWindow(msg, resolvedModel);
4237
4312
  if (window !== void 0) usage.contextWindow = window;
4238
4313
  if (compactions.length > 0) usage.contextCompactions = compactions;
4239
- const reading = await readHarnessContext(ctx.readContextUsage);
4314
+ const reading = await readHarnessContext(ctx2.readContextUsage);
4240
4315
  if (reading) Object.assign(usage, reading);
4241
4316
  }
4242
4317
  const isError = msg.is_error === true;
@@ -4252,20 +4327,20 @@ async function* decodeSdkStream(iter, ctx) {
4252
4327
  }
4253
4328
  }
4254
4329
  } catch (err) {
4255
- if (ctx.signal.aborted) throw err;
4330
+ if (ctx2.signal.aborted) throw err;
4256
4331
  const message2 = err instanceof Error ? err.message : String(err);
4257
4332
  const failure = mcpInventory.initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
4258
- if (!failure && (mcpInventory.initReceived || ctx.resumedSessionId === null)) throw err;
4333
+ if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
4259
4334
  ok = false;
4260
4335
  if (failure) {
4261
4336
  resultReason = encodeFailureReason(failure);
4262
4337
  } else {
4263
4338
  resultReason = "session_start_failed";
4264
- if (ctx.startEvidence) ctx.startEvidence.thrown = message2.slice(0, 200);
4339
+ if (ctx2.startEvidence) ctx2.startEvidence.thrown = message2.slice(0, 200);
4265
4340
  }
4266
4341
  sawResult = true;
4267
4342
  }
4268
- if (ctx.signal.aborted) return;
4343
+ if (ctx2.signal.aborted) return;
4269
4344
  if (!workspaceProven) {
4270
4345
  ok = false;
4271
4346
  if (resultReason === void 0) {
@@ -5273,7 +5348,7 @@ function deepestError(node) {
5273
5348
  }
5274
5349
 
5275
5350
  // packages/agent-runtime/src/opencode/loop.ts
5276
- async function* decodeOpencodeStream(events, ctx) {
5351
+ async function* decodeOpencodeStream(events, ctx2) {
5277
5352
  let held = null;
5278
5353
  const pending = /* @__PURE__ */ new Map();
5279
5354
  const startedTools = /* @__PURE__ */ new Set();
@@ -5282,9 +5357,9 @@ async function* decodeOpencodeStream(events, ctx) {
5282
5357
  let reason;
5283
5358
  let settled = false;
5284
5359
  const userMessageIds = /* @__PURE__ */ new Set();
5285
- if (ctx.skipMessageId) userMessageIds.add(ctx.skipMessageId);
5360
+ if (ctx2.skipMessageId) userMessageIds.add(ctx2.skipMessageId);
5286
5361
  for await (const ev of events) {
5287
- if (ctx.signal.aborted) return;
5362
+ if (ctx2.signal.aborted) return;
5288
5363
  if (ev.type === "message.updated") {
5289
5364
  const info = readMessageInfo(ev.properties);
5290
5365
  if (info && info.role === "user") userMessageIds.add(info.id);
@@ -5337,7 +5412,7 @@ async function* decodeOpencodeStream(events, ctx) {
5337
5412
  }
5338
5413
  } else if (ev.type === "session.idle") {
5339
5414
  const sid = readSessionId(ev.properties);
5340
- if (sid && sid !== ctx.sessionId) continue;
5415
+ if (sid && sid !== ctx2.sessionId) continue;
5341
5416
  const sealed = sealHeld(held, true);
5342
5417
  held = null;
5343
5418
  if (sealed) yield sealed;
@@ -5346,7 +5421,7 @@ async function* decodeOpencodeStream(events, ctx) {
5346
5421
  break;
5347
5422
  } else if (ev.type === "session.error") {
5348
5423
  const sid = readSessionId(ev.properties);
5349
- if (sid && sid !== ctx.sessionId) continue;
5424
+ if (sid && sid !== ctx2.sessionId) continue;
5350
5425
  const sealed = sealHeld(held, false);
5351
5426
  held = null;
5352
5427
  if (sealed) yield sealed;
@@ -5362,7 +5437,7 @@ async function* decodeOpencodeStream(events, ctx) {
5362
5437
  break;
5363
5438
  }
5364
5439
  }
5365
- if (ctx.signal.aborted) return;
5440
+ if (ctx2.signal.aborted) return;
5366
5441
  if (!settled) {
5367
5442
  const sealed = sealHeld(held, true);
5368
5443
  held = null;
@@ -5420,9 +5495,9 @@ function readOpencodeDialect(runtimeOptions) {
5420
5495
 
5421
5496
  // packages/agent-runtime/src/opencode/model.ts
5422
5497
  function parseOpencodeModel(model) {
5423
- const sep4 = model.indexOf("/");
5424
- if (sep4 === -1) return { providerID: "", modelID: model };
5425
- return { providerID: model.slice(0, sep4), modelID: model.slice(sep4 + 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) };
5426
5501
  }
5427
5502
 
5428
5503
  // packages/agent-runtime/src/opencode/run-spec.ts
@@ -5710,10 +5785,10 @@ async function* parseSseStream(body, sessionId, signal) {
5710
5785
  const { value, done } = await reader.read();
5711
5786
  if (done) return;
5712
5787
  buf += decoder.decode(value, { stream: true });
5713
- let sep4;
5714
- while ((sep4 = buf.indexOf("\n\n")) !== -1) {
5715
- const frame = buf.slice(0, sep4);
5716
- buf = buf.slice(sep4 + 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);
5717
5792
  const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
5718
5793
  if (!data) continue;
5719
5794
  let ev;
@@ -6461,7 +6536,7 @@ function readUsageBaseline(raw) {
6461
6536
  }
6462
6537
 
6463
6538
  // packages/agent-runtime/src/codex/loop.ts
6464
- async function* decodeCodexStream(events, ctx) {
6539
+ async function* decodeCodexStream(events, ctx2) {
6465
6540
  let held = null;
6466
6541
  const pending = /* @__PURE__ */ new Map();
6467
6542
  const startedTools = /* @__PURE__ */ new Set();
@@ -6471,7 +6546,7 @@ async function* decodeCodexStream(events, ctx) {
6471
6546
  let reason;
6472
6547
  let usage;
6473
6548
  let cumulativeUsage;
6474
- let threadIdForSession = ctx.resumedThreadId;
6549
+ let threadIdForSession = ctx2.resumedThreadId;
6475
6550
  let settled = false;
6476
6551
  function* flushInterim() {
6477
6552
  const sealed = sealHeld2(held, false);
@@ -6479,26 +6554,26 @@ async function* decodeCodexStream(events, ctx) {
6479
6554
  if (sealed) yield sealed;
6480
6555
  }
6481
6556
  for await (const ev of events) {
6482
- if (ctx.signal.aborted) return;
6557
+ if (ctx2.signal.aborted) return;
6483
6558
  if (ev.type === "thread.started") {
6484
6559
  const threadId = readThreadId(ev);
6485
6560
  if (threadId) threadIdForSession = threadId;
6486
6561
  if (threadId && !sessionEmitted) {
6487
6562
  const state = encodeSession3({
6488
6563
  threadId,
6489
- cwd: ctx.cwd ?? "",
6490
- promptFingerprint: ctx.promptFingerprint ?? null,
6564
+ cwd: ctx2.cwd ?? "",
6565
+ promptFingerprint: ctx2.promptFingerprint ?? null,
6491
6566
  // The baseline this turn inherited, carried through unchanged. This early write records the
6492
6567
  // thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
6493
6568
  // closing write records the new baseline.
6494
- usageBaseline: ctx.usageBaseline ?? null
6569
+ usageBaseline: ctx2.usageBaseline ?? null
6495
6570
  });
6496
- if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
6571
+ if (threadId !== ctx2.resumedThreadId || state !== ctx2.storedState) {
6497
6572
  sessionEmitted = true;
6498
6573
  yield {
6499
6574
  type: "session",
6500
6575
  state,
6501
- ...ctx.degraded ? { degraded: true } : {}
6576
+ ...ctx2.degraded ? { degraded: true } : {}
6502
6577
  };
6503
6578
  }
6504
6579
  }
@@ -6567,7 +6642,7 @@ async function* decodeCodexStream(events, ctx) {
6567
6642
  held = null;
6568
6643
  if (sealed) yield sealed;
6569
6644
  cumulativeUsage = readUsage(ev);
6570
- usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage, ctx.usageBaseline ?? null) : void 0;
6645
+ usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage, ctx2.usageBaseline ?? null) : void 0;
6571
6646
  ok = true;
6572
6647
  settled = true;
6573
6648
  break;
@@ -6586,7 +6661,7 @@ async function* decodeCodexStream(events, ctx) {
6586
6661
  break;
6587
6662
  }
6588
6663
  }
6589
- if (ctx.signal.aborted) return;
6664
+ if (ctx2.signal.aborted) return;
6590
6665
  if (!settled) {
6591
6666
  const sealed = sealHeld2(held, true);
6592
6667
  held = null;
@@ -6596,19 +6671,19 @@ async function* decodeCodexStream(events, ctx) {
6596
6671
  if (ok && cumulativeUsage && threadIdForSession) {
6597
6672
  const closing = encodeSession3({
6598
6673
  threadId: threadIdForSession,
6599
- cwd: ctx.cwd ?? "",
6600
- promptFingerprint: ctx.promptFingerprint ?? null,
6674
+ cwd: ctx2.cwd ?? "",
6675
+ promptFingerprint: ctx2.promptFingerprint ?? null,
6601
6676
  usageBaseline: cumulativeUsage
6602
6677
  });
6603
6678
  yield { type: "session", state: closing };
6604
6679
  }
6605
- const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
6680
+ const resolvedConfig = ctx2.resolvedReasoningEffort ? { reasoningEffort: ctx2.resolvedReasoningEffort } : void 0;
6606
6681
  yield {
6607
6682
  type: "result",
6608
6683
  ok,
6609
6684
  ...reason ? { reason } : {},
6610
6685
  ...usage ? { usage } : {},
6611
- ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
6686
+ ...ctx2.resolvedModel ? { resolvedModel: ctx2.resolvedModel } : {},
6612
6687
  ...resolvedConfig ? { resolvedConfig } : {}
6613
6688
  };
6614
6689
  }
@@ -6653,8 +6728,8 @@ function readCodexDialect(runtimeOptions) {
6653
6728
  // packages/agent-runtime/src/codex/model.ts
6654
6729
  var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
6655
6730
  function parseCodexModel(model) {
6656
- const sep4 = model.indexOf("/");
6657
- const bare = sep4 === -1 || model.slice(0, sep4) !== "openai" ? model : model.slice(sep4 + 1);
6731
+ const sep5 = model.indexOf("/");
6732
+ const bare = sep5 === -1 || model.slice(0, sep5) !== "openai" ? model : model.slice(sep5 + 1);
6658
6733
  return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
6659
6734
  }
6660
6735
 
@@ -7934,6 +8009,7 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7934
8009
  // src/connector-health.ts
7935
8010
  var ConnectorHealthStore = class {
7936
8011
  byRuntime = /* @__PURE__ */ new Map();
8012
+ sources = /* @__PURE__ */ new Map();
7937
8013
  // Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
7938
8014
  // classified failure sets `limited`(+reset) / `auth_expired` with the reason as
7939
8015
  // `last_error`; anything else (a user Stop, a timeout, an unclassified error)
@@ -7942,6 +8018,7 @@ var ConnectorHealthStore = class {
7942
8018
  recordSettle(runtime, outcome) {
7943
8019
  if (outcome.ok) {
7944
8020
  this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
8021
+ this.sources.set(runtime, "turn");
7945
8022
  return;
7946
8023
  }
7947
8024
  const failure = decodeFailureReason(outcome.errorReason);
@@ -7953,6 +8030,26 @@ var ConnectorHealthStore = class {
7953
8030
  limitedUntil: quota.limitedUntil,
7954
8031
  lastError: outcome.errorReason ?? null
7955
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 });
7956
8053
  }
7957
8054
  // The store's current opinion for one runtime, or undefined when it has
7958
8055
  // no observation. Read by the dispatcher at idle-timeout fire: an idle reap
@@ -7981,11 +8078,11 @@ var ConnectorHealthStore = class {
7981
8078
 
7982
8079
  // src/dispatcher.ts
7983
8080
  import { randomUUID as randomUUID4 } from "crypto";
7984
- import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7985
- 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";
7986
8083
 
7987
8084
  // src/runner/launch.ts
7988
- import { createInterface } from "readline";
8085
+ import { createInterface as createInterface2 } from "readline";
7989
8086
 
7990
8087
  // src/runner/sandbox-policy.ts
7991
8088
  import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
@@ -8162,7 +8259,7 @@ async function stopRemoteOpencodeSession(serverUrl, sessionId, opts = {}) {
8162
8259
  }
8163
8260
 
8164
8261
  // src/turn-containment.ts
8165
- import { execFile, execFileSync, spawn as spawn4 } from "child_process";
8262
+ import { execFile, execFileSync, spawn as spawn3 } from "child_process";
8166
8263
  import { readFileSync as readFileSync5 } from "fs";
8167
8264
  import { promisify } from "util";
8168
8265
  var execFileAsync = promisify(execFile);
@@ -8171,8 +8268,8 @@ var CONTAINMENT_STOP_GRACE_SEC = 5;
8171
8268
  var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
8172
8269
  var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
8173
8270
  function turnScopeUnit(turnId, attempt) {
8174
- const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
8175
- return `cabane-turn-${safe3}-${attempt}`;
8271
+ const safe2 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
8272
+ return `cabane-turn-${safe2}-${attempt}`;
8176
8273
  }
8177
8274
  async function reapTurnScopes(turnId) {
8178
8275
  if (process.platform !== "linux") return { failures: [] };
@@ -8285,7 +8382,7 @@ function createTurnContainment(turnId, deps = {}) {
8285
8382
  command: options.command
8286
8383
  });
8287
8384
  return asContained(
8288
- spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
8385
+ spawn3("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
8289
8386
  ...options.cwd ? { cwd: options.cwd } : {},
8290
8387
  env: options.env,
8291
8388
  stdio: ["pipe", "pipe", "pipe"]
@@ -8293,7 +8390,7 @@ function createTurnContainment(turnId, deps = {}) {
8293
8390
  );
8294
8391
  }
8295
8392
  function spawnInGroup(options) {
8296
- const child = spawn4(options.command, options.args, {
8393
+ const child = spawn3(options.command, options.args, {
8297
8394
  ...options.cwd ? { cwd: options.cwd } : {},
8298
8395
  env: options.env,
8299
8396
  stdio: ["pipe", "pipe", "pipe"],
@@ -8440,17 +8537,17 @@ function createTurnContainment(turnId, deps = {}) {
8440
8537
  }
8441
8538
 
8442
8539
  // src/secrets.ts
8443
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
8444
- import { join as join10 } from "path";
8540
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
8541
+ import { join as join12 } from "path";
8445
8542
  import { z as z14 } from "zod";
8446
8543
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8447
8544
  function secretsPath() {
8448
- return join10(cabaneDir(), "secrets.json");
8545
+ return join12(cabaneDir(), "secrets.json");
8449
8546
  }
8450
8547
  var secretStoreSchema = z14.record(z14.string(), z14.string());
8451
8548
  function loadSecretStore() {
8452
8549
  const path = secretsPath();
8453
- if (!existsSync7(path)) return makeStore({});
8550
+ if (!existsSync8(path)) return makeStore({});
8454
8551
  let raw;
8455
8552
  try {
8456
8553
  raw = readFileSync6(path, "utf8");
@@ -8829,7 +8926,7 @@ function relayRunnerLog(line, log) {
8829
8926
  log[method](fields, typeof msg === "string" ? msg : "");
8830
8927
  }
8831
8928
  function lines(stream, onLine) {
8832
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
8929
+ const rl = createInterface2({ input: stream, crlfDelay: Infinity });
8833
8930
  rl.on("line", onLine);
8834
8931
  return new Promise((resolve2) => rl.once("close", resolve2));
8835
8932
  }
@@ -9169,19 +9266,9 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
9169
9266
  return { ok: false, durationMs, reason };
9170
9267
  }
9171
9268
 
9172
- // src/term.ts
9173
- import { createInterface as createInterface2 } from "readline";
9174
- import { homedir as homedir3 } from "os";
9175
- import pc from "picocolors";
9176
- function tildePath(path) {
9177
- const home = homedir3();
9178
- if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
9179
- return path;
9180
- }
9181
-
9182
9269
  // src/turn-execution.ts
9183
9270
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
9184
- import { existsSync as existsSync9 } from "fs";
9271
+ import { existsSync as existsSync10 } from "fs";
9185
9272
 
9186
9273
  // src/turn-control-tools.ts
9187
9274
  import { randomUUID as randomUUID2 } from "crypto";
@@ -9425,11 +9512,11 @@ function trimSlash2(s) {
9425
9512
  // src/codex-instructions.ts
9426
9513
  import { mkdtemp, rm, writeFile } from "fs/promises";
9427
9514
  import { tmpdir } from "os";
9428
- import { join as join11 } from "path";
9515
+ import { join as join13 } from "path";
9429
9516
  var PREFIX = "cabane-codex-instructions-";
9430
9517
  async function writeCodexInstructionsFile(contents) {
9431
- const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
9432
- const path = join11(dir2, "instructions.md");
9518
+ const dir2 = await mkdtemp(join13(tmpdir(), PREFIX));
9519
+ const path = join13(dir2, "instructions.md");
9433
9520
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
9434
9521
  return {
9435
9522
  path,
@@ -9442,7 +9529,7 @@ async function writeCodexInstructionsFile(contents) {
9442
9529
  // src/codex-images.ts
9443
9530
  import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
9444
9531
  import { tmpdir as tmpdir2 } from "os";
9445
- import { join as join12 } from "path";
9532
+ import { join as join14 } from "path";
9446
9533
  var FETCH_TIMEOUT_MS = 3e4;
9447
9534
  var EXTENSIONS = {
9448
9535
  "image/png": "png",
@@ -9455,7 +9542,7 @@ function createCodexImagePreparer(opts) {
9455
9542
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
9456
9543
  return async (urls, signal) => {
9457
9544
  signal.throwIfAborted();
9458
- const dir2 = await mkdtemp2(join12(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
9545
+ const dir2 = await mkdtemp2(join14(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
9459
9546
  const cleanup = async () => {
9460
9547
  try {
9461
9548
  await rm2(dir2, { recursive: true, force: true });
@@ -9476,7 +9563,7 @@ function createCodexImagePreparer(opts) {
9476
9563
  const ext = EXTENSIONS[type];
9477
9564
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
9478
9565
  const bytes = new Uint8Array(await res.arrayBuffer());
9479
- const path = join12(dir2, `image-${index + 1}.${ext}`);
9566
+ const path = join14(dir2, `image-${index + 1}.${ext}`);
9480
9567
  await writeFile2(path, bytes, { mode: 384 });
9481
9568
  return path;
9482
9569
  } catch (err) {
@@ -9510,14 +9597,14 @@ var SeqFloorUnavailable = class extends Error {
9510
9597
  }
9511
9598
  detail;
9512
9599
  };
9513
- function resolveSeqFloor(sources, ctx) {
9600
+ function resolveSeqFloor(sources, ctx2) {
9514
9601
  const { serverFloor, outboxFloor } = sources;
9515
9602
  if (serverFloor === void 0) {
9516
9603
  throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
9517
9604
  }
9518
9605
  const floor = Math.max(serverFloor, outboxFloor);
9519
- ctx.log.debug(
9520
- { turnId: ctx.turnId, floor, outboxFloor, serverFloor },
9606
+ ctx2.log.debug(
9607
+ { turnId: ctx2.turnId, floor, outboxFloor, serverFloor },
9521
9608
  "companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
9522
9609
  );
9523
9610
  return floor;
@@ -9535,7 +9622,7 @@ function readOutboxFloor(read, turnId, log) {
9535
9622
  }
9536
9623
 
9537
9624
  // src/prepare-failure.ts
9538
- import { spawn as spawn5 } from "child_process";
9625
+ import { spawn as spawn4 } from "child_process";
9539
9626
  import { hostname } from "os";
9540
9627
  function prepareFailureCause(error) {
9541
9628
  const message2 = error instanceof Error ? error.message : String(error);
@@ -9573,7 +9660,7 @@ async function runPrepareWithReporting(runner, hook, input, context, reportError
9573
9660
  }
9574
9661
  function reportFailure(config, body) {
9575
9662
  return new Promise((resolve2, reject) => {
9576
- const child = spawn5(config.command, config.args ?? [], {
9663
+ const child = spawn4(config.command, config.args ?? [], {
9577
9664
  stdio: ["pipe", "ignore", "ignore"],
9578
9665
  env: { ...process.env, ...config.env }
9579
9666
  });
@@ -9597,20 +9684,20 @@ function reportFailure(config, body) {
9597
9684
  }
9598
9685
 
9599
9686
  // src/prepared.ts
9600
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
9601
- 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";
9602
9689
  function dirFor(workspaceId) {
9603
- return join13(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9690
+ return join15(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9604
9691
  }
9605
9692
  function conversationDir(workspaceId, conversationId) {
9606
- return join13(dirFor(workspaceId), encodeURIComponent(conversationId));
9693
+ return join15(dirFor(workspaceId), encodeURIComponent(conversationId));
9607
9694
  }
9608
9695
  function pathFor2(workspaceId, conversationId, agentId) {
9609
- return join13(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9696
+ return join15(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9610
9697
  }
9611
9698
  function readPrepared(workspaceId, conversationId, agentId) {
9612
9699
  const path = pathFor2(workspaceId, conversationId, agentId);
9613
- if (!existsSync8(path)) return null;
9700
+ if (!existsSync9(path)) return null;
9614
9701
  try {
9615
9702
  const parsed = JSON.parse(readFileSync7(path, "utf8"));
9616
9703
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
@@ -9638,9 +9725,9 @@ function clearPrepared(workspaceId, conversationId, agentId) {
9638
9725
 
9639
9726
  // src/transcript-writer.ts
9640
9727
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
9641
- import { basename, dirname as dirname6, join as join14 } from "path";
9728
+ import { basename, dirname as dirname6, join as join16 } from "path";
9642
9729
  function transcriptsDir() {
9643
- return join14(runnerStateDir(), "transcripts");
9730
+ return join16(runnerStateDir(), "transcripts");
9644
9731
  }
9645
9732
  var RETAIN = 200;
9646
9733
  var ANOMALY_RETAIN = 50;
@@ -9650,7 +9737,7 @@ var TranscriptWriter = class {
9650
9737
  onWarn;
9651
9738
  constructor(dir2, meta, onWarn) {
9652
9739
  this.onWarn = onWarn;
9653
- this.path = join14(dir2, fileName(meta));
9740
+ this.path = join16(dir2, fileName(meta));
9654
9741
  try {
9655
9742
  mkdirSync8(dir2, { recursive: true });
9656
9743
  try {
@@ -9679,10 +9766,10 @@ var TranscriptWriter = class {
9679
9766
  preserveAnomaly() {
9680
9767
  if (this.broken) return;
9681
9768
  try {
9682
- const dir2 = join14(dirname6(this.path), "anomalies");
9769
+ const dir2 = join16(dirname6(this.path), "anomalies");
9683
9770
  mkdirSync8(dir2, { recursive: true, mode: 448 });
9684
9771
  chmodSync4(dir2, 448);
9685
- const target = join14(dir2, basename(this.path));
9772
+ const target = join16(dir2, basename(this.path));
9686
9773
  copyFileSync(this.path, target);
9687
9774
  chmodSync4(target, 384);
9688
9775
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9707,14 +9794,14 @@ var TranscriptWriter = class {
9707
9794
  function fileName(meta) {
9708
9795
  const d = new Date(meta.ts);
9709
9796
  const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
9710
- const slug = safe2(meta.workspaceSlug) || "ws";
9797
+ const slug = safe(meta.workspaceSlug) || "ws";
9711
9798
  const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
9712
9799
  return `${stamp}-${slug}-${short}.jsonl`;
9713
9800
  }
9714
9801
  function pad(n) {
9715
9802
  return String(n).padStart(2, "0");
9716
9803
  }
9717
- function safe2(s) {
9804
+ function safe(s) {
9718
9805
  return s.replace(/[^a-zA-Z0-9_-]/g, "-");
9719
9806
  }
9720
9807
  function pruneOld(dir2, retain) {
@@ -9728,7 +9815,7 @@ function pruneOld(dir2, retain) {
9728
9815
  const drop = files.sort().slice(0, files.length - retain);
9729
9816
  for (const f of drop) {
9730
9817
  try {
9731
- rmSync6(join14(dir2, f), { force: true });
9818
+ rmSync6(join16(dir2, f), { force: true });
9732
9819
  } catch {
9733
9820
  }
9734
9821
  }
@@ -9867,32 +9954,32 @@ var TurnCommitter = class {
9867
9954
  function bundledHarnessFor(runtime) {
9868
9955
  return runtime === "claude-code" || runtime === "codex" ? runtime : null;
9869
9956
  }
9870
- function resolve(ctx, harness) {
9871
- return (ctx.resolveBundled ?? resolveBundledBinary)(harness);
9957
+ function resolve(ctx2, harness) {
9958
+ return (ctx2.resolveBundled ?? resolveBundledBinary)(harness);
9872
9959
  }
9873
- async function postIncompleteNotice(ctx, harness, resolution) {
9960
+ async function postIncompleteNotice(ctx2, harness, resolution) {
9874
9961
  try {
9875
- await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
9962
+ await ctx2.postTurnMessage(ctx2.workspaceId, ctx2.conversationId, {
9876
9963
  body: turnIncompleteCopy(harness),
9877
9964
  kind: "runtime_notice",
9878
- turnId: ctx.turnId,
9879
- parentMessageId: ctx.parentMessageId
9965
+ turnId: ctx2.turnId,
9966
+ parentMessageId: ctx2.parentMessageId
9880
9967
  });
9881
9968
  return true;
9882
9969
  } catch (err) {
9883
- ctx.log.warn(
9970
+ ctx2.log.warn(
9884
9971
  { ...apiErrorLogFields(err) },
9885
9972
  "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9886
9973
  );
9887
9974
  return false;
9888
9975
  }
9889
9976
  }
9890
- async function checkBundledBinary(ctx) {
9891
- const harness = bundledHarnessFor(ctx.runtime);
9977
+ async function checkBundledBinary(ctx2) {
9978
+ const harness = bundledHarnessFor(ctx2.runtime);
9892
9979
  if (!harness) return null;
9893
- const resolution = resolve(ctx, harness);
9980
+ const resolution = resolve(ctx2, harness);
9894
9981
  if (resolution.status === "present") return null;
9895
- ctx.log.debug(
9982
+ ctx2.log.debug(
9896
9983
  {
9897
9984
  runtime: harness,
9898
9985
  status: resolution.status,
@@ -9904,15 +9991,15 @@ async function checkBundledBinary(ctx) {
9904
9991
  return {
9905
9992
  harness,
9906
9993
  reason: `runtime_incomplete:${harness}`,
9907
- posted: await postIncompleteNotice(ctx, harness, resolution)
9994
+ posted: await postIncompleteNotice(ctx2, harness, resolution)
9908
9995
  };
9909
9996
  }
9910
- async function reclassifyThrow(ctx, opts) {
9911
- const harness = bundledHarnessFor(ctx.runtime);
9997
+ async function reclassifyThrow(ctx2, opts) {
9998
+ const harness = bundledHarnessFor(ctx2.runtime);
9912
9999
  if (!harness || opts.aborted) return null;
9913
- const resolution = resolve(ctx, harness);
10000
+ const resolution = resolve(ctx2, harness);
9914
10001
  if (resolution.status === "present") return null;
9915
- ctx.log.debug(
10002
+ ctx2.log.debug(
9916
10003
  {
9917
10004
  runtime: harness,
9918
10005
  status: resolution.status,
@@ -9925,17 +10012,17 @@ async function reclassifyThrow(ctx, opts) {
9925
10012
  return {
9926
10013
  harness,
9927
10014
  reason: `runtime_incomplete:${harness}`,
9928
- posted: await postIncompleteNotice(ctx, harness, resolution)
10015
+ posted: await postIncompleteNotice(ctx2, harness, resolution)
9929
10016
  };
9930
10017
  }
9931
- async function guardBundledBinary(ctx, concluded, outcome) {
9932
- const incomplete = await checkBundledBinary(ctx);
10018
+ async function guardBundledBinary(ctx2, concluded, outcome) {
10019
+ const incomplete = await checkBundledBinary(ctx2);
9933
10020
  if (!incomplete) return;
9934
10021
  outcome.runtimeIncomplete = incomplete.posted;
9935
10022
  throw incomplete.posted ? concluded(incomplete.reason) : concluded(incomplete.reason, incomplete.reason);
9936
10023
  }
9937
- async function absorbBundledLoss(ctx, aborted, outcome) {
9938
- 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 });
9939
10026
  if (!lost) return false;
9940
10027
  outcome.resultReason = lost.reason;
9941
10028
  return lost.posted;
@@ -10330,7 +10417,7 @@ var TurnExecution = class {
10330
10417
  const prepareHook = this.opts.local.prepareHook;
10331
10418
  const cabaneCwd = this.turnContext.cwd;
10332
10419
  let effectiveCwd = localCwd ?? cabaneCwd;
10333
- if (effectiveCwd && !existsSync9(effectiveCwd)) {
10420
+ if (effectiveCwd && !existsSync10(effectiveCwd)) {
10334
10421
  turnLog.warn(
10335
10422
  { cwd: effectiveCwd },
10336
10423
  "The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
@@ -11098,7 +11185,7 @@ function fingerprintSessionState(state) {
11098
11185
  // src/dispatcher.ts
11099
11186
  function checkoutState(cwd) {
11100
11187
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
11101
- 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})` };
11102
11189
  let entries;
11103
11190
  try {
11104
11191
  entries = readdirSync2(cwd);
@@ -11108,15 +11195,15 @@ function checkoutState(cwd) {
11108
11195
  if (entries.length === 0) {
11109
11196
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
11110
11197
  }
11111
- const gitPath = join15(cwd, ".git");
11112
- if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
11198
+ const gitPath = join17(cwd, ".git");
11199
+ if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
11113
11200
  let stat;
11114
11201
  try {
11115
11202
  stat = statSync2(gitPath);
11116
11203
  } catch (error) {
11117
11204
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
11118
11205
  }
11119
- if (stat.isDirectory() && !existsSync10(join15(gitPath, "HEAD")))
11206
+ if (stat.isDirectory() && !existsSync11(join17(gitPath, "HEAD")))
11120
11207
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
11121
11208
  return { ok: true, reason: "usable" };
11122
11209
  }
@@ -11229,18 +11316,18 @@ var Dispatcher = class {
11229
11316
  };
11230
11317
 
11231
11318
  // src/providers/local-process.ts
11232
- import { spawn as spawn6 } from "child_process";
11233
- import { constants } from "os";
11319
+ import { spawn as spawn5 } from "child_process";
11320
+ import { constants as constants2 } from "os";
11234
11321
  var DEFAULT_KILL_GRACE_MS = 3e4;
11235
11322
  var DEFAULT_REAP_GRACE_MS = 3e3;
11236
11323
  function exitCode(code, signal) {
11237
11324
  if (code !== null) return code;
11238
- const signum = signal ? constants.signals[signal] : void 0;
11325
+ const signum = signal ? constants2.signals[signal] : void 0;
11239
11326
  return signum !== void 0 ? 128 + signum : 1;
11240
11327
  }
11241
11328
  function createLocalProcessProvider(opts = {}) {
11242
11329
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
11243
- const spawnFn = opts.spawnFn ?? spawn6;
11330
+ const spawnFn = opts.spawnFn ?? spawn5;
11244
11331
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
11245
11332
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
11246
11333
  const groupAlive = (pgid) => {
@@ -11321,11 +11408,11 @@ function createLocalProcessProvider(opts = {}) {
11321
11408
 
11322
11409
  // src/providers/docker.ts
11323
11410
  import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
11324
- import { constants as constants2, tmpdir as tmpdir3 } from "os";
11411
+ import { constants as constants3, tmpdir as tmpdir3 } from "os";
11325
11412
  import {
11326
11413
  createReadStream,
11327
11414
  createWriteStream as createWriteStream2,
11328
- existsSync as existsSync13,
11415
+ existsSync as existsSync14,
11329
11416
  mkdirSync as mkdirSync10,
11330
11417
  readdirSync as readdirSync4,
11331
11418
  readFileSync as readFileSync9,
@@ -11334,11 +11421,11 @@ import {
11334
11421
  statSync as statSync3,
11335
11422
  writeFileSync as writeFileSync6
11336
11423
  } from "fs";
11337
- import { join as join18 } from "path";
11424
+ import { join as join20 } from "path";
11338
11425
  import { Writable } from "stream";
11339
11426
 
11340
11427
  // src/providers/docker-cli.ts
11341
- import { spawn as spawn7 } from "child_process";
11428
+ import { spawn as spawn6 } from "child_process";
11342
11429
  function cliEnv(extra) {
11343
11430
  const keep = [
11344
11431
  "PATH",
@@ -11358,7 +11445,7 @@ function dockerCli(bin = "docker") {
11358
11445
  return new Promise((resolve2) => {
11359
11446
  let child;
11360
11447
  try {
11361
- child = spawn7(bin, [...args], {
11448
+ child = spawn6(bin, [...args], {
11362
11449
  env: cliEnv(opts.env),
11363
11450
  stdio: ["pipe", "pipe", "pipe"]
11364
11451
  });
@@ -11389,13 +11476,13 @@ function dockerCli(bin = "docker") {
11389
11476
  });
11390
11477
  },
11391
11478
  spawn(args, env) {
11392
- return spawn7(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
11479
+ return spawn6(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
11393
11480
  }
11394
11481
  };
11395
11482
  }
11396
11483
 
11397
11484
  // src/runner/git-credential.ts
11398
- import { spawn as spawn8 } from "child_process";
11485
+ import { spawn as spawn7 } from "child_process";
11399
11486
  import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
11400
11487
  import { connect as connect3, createServer as createServer3 } from "net";
11401
11488
 
@@ -11437,7 +11524,7 @@ function deviceGitFill(helper, timeoutMs = 15e3) {
11437
11524
  "credential",
11438
11525
  "fill"
11439
11526
  ];
11440
- const child = spawn8("git", args, {
11527
+ const child = spawn7("git", args, {
11441
11528
  stdio: ["pipe", "pipe", "ignore"],
11442
11529
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
11443
11530
  });
@@ -11521,14 +11608,14 @@ async function serveGitCredentials(opts) {
11521
11608
  // src/runner/collected-outbox.ts
11522
11609
  import {
11523
11610
  copyFileSync as copyFileSync2,
11524
- existsSync as existsSync11,
11611
+ existsSync as existsSync12,
11525
11612
  mkdirSync as mkdirSync9,
11526
11613
  readdirSync as readdirSync3,
11527
11614
  readFileSync as readFileSync8,
11528
11615
  renameSync as renameSync4,
11529
11616
  rmSync as rmSync8
11530
11617
  } from "fs";
11531
- import { join as join16 } from "path";
11618
+ import { join as join18 } from "path";
11532
11619
  function routes(t) {
11533
11620
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
11534
11621
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -11570,11 +11657,11 @@ var atomicMove = (from, to) => {
11570
11657
  }
11571
11658
  };
11572
11659
  function publishStaged(from, to, move = atomicMove) {
11573
- if (!existsSync11(from)) return { published: 0, complete: true };
11660
+ if (!existsSync12(from)) return { published: 0, complete: true };
11574
11661
  const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
11575
11662
  let entry = {};
11576
11663
  try {
11577
- entry = JSON.parse(readFileSync8(join16(from, name), "utf8"));
11664
+ entry = JSON.parse(readFileSync8(join18(from, name), "utf8"));
11578
11665
  } catch {
11579
11666
  }
11580
11667
  return { name, entry };
@@ -11587,7 +11674,7 @@ function publishStaged(from, to, move = atomicMove) {
11587
11674
  let published = 0;
11588
11675
  for (const { name } of staged) {
11589
11676
  try {
11590
- move(join16(from, name), join16(to, name));
11677
+ move(join18(from, name), join18(to, name));
11591
11678
  } catch {
11592
11679
  return { published, complete: false };
11593
11680
  }
@@ -11599,9 +11686,9 @@ function publishStaged(from, to, move = atomicMove) {
11599
11686
 
11600
11687
  // src/providers/runner-base.ts
11601
11688
  import { createHash as createHash3 } from "crypto";
11602
- import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
11603
- import { dirname as dirname7, join as join17, sep as sep2 } from "path";
11604
- 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";
11605
11692
  var COMPANION_MOUNT = "/opt/cabane/companion";
11606
11693
  var BASE_DOCKERFILE = `FROM node:22-slim
11607
11694
  RUN apt-get update \\
@@ -11645,17 +11732,17 @@ async function ensureBaseImage(docker, image, log) {
11645
11732
  }
11646
11733
  return true;
11647
11734
  }
11648
- function ownInstall(moduleFile = fileURLToPath3(import.meta.url)) {
11735
+ function ownInstall(moduleFile = fileURLToPath4(import.meta.url)) {
11649
11736
  let dir2 = dirname7(moduleFile);
11650
11737
  for (let i = 0; i < 4; i++) {
11651
- 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;
11652
11739
  dir2 = dirname7(dir2);
11653
11740
  }
11654
- if (!existsSync12(join17(dir2, "dist", "cli.js"))) return null;
11655
- const sdk = join17(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
11656
- if (!existsSync12(sdk)) return null;
11657
- const root = realpathSync2(dir2);
11658
- return realpathSync2(sdk).startsWith(root + sep2) ? 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;
11659
11746
  }
11660
11747
  function companionSource(input) {
11661
11748
  if (input.configured) {
@@ -11791,21 +11878,21 @@ function networkCreateError(stderr) {
11791
11878
  }
11792
11879
  function sessionArchivePath(deviceDir, attemptId) {
11793
11880
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
11794
- return join18(deviceDir, "attempts", attemptId, "session.tar");
11881
+ return join20(deviceDir, "attempts", attemptId, "session.tar");
11795
11882
  }
11796
11883
  function pruneOtherAttempts(deviceDir, pair, keep) {
11797
- const root = join18(deviceDir, "attempts");
11798
- if (!existsSync13(root)) return;
11884
+ const root = join20(deviceDir, "attempts");
11885
+ if (!existsSync14(root)) return;
11799
11886
  for (const id of readdirSync4(root)) {
11800
11887
  if (id === keep || !UUID_RE.test(id)) continue;
11801
11888
  let pairOf = "";
11802
11889
  try {
11803
- pairOf = readFileSync9(join18(root, id, "pair"), "utf8");
11890
+ pairOf = readFileSync9(join20(root, id, "pair"), "utf8");
11804
11891
  } catch {
11805
11892
  continue;
11806
11893
  }
11807
11894
  if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
11808
- rmSync9(join18(root, id), { recursive: true, force: true });
11895
+ rmSync9(join20(root, id), { recursive: true, force: true });
11809
11896
  }
11810
11897
  }
11811
11898
  }
@@ -12005,15 +12092,15 @@ function createDockerProvider(opts) {
12005
12092
  await must(["network", "disconnect", "-f", other, name], "moving the container");
12006
12093
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
12007
12094
  };
12008
- const socketRoot = opts.socketRoot ?? join18(
12009
- 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(),
12010
12097
  `cabane-runners-${opts.deviceKey}`
12011
12098
  );
12012
- 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));
12013
12100
  const create = async (name, attempt) => {
12014
12101
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
12015
12102
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
12016
- 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`]);
12017
12104
  await must(
12018
12105
  [
12019
12106
  "create",
@@ -12151,7 +12238,7 @@ function createDockerProvider(opts) {
12151
12238
  }
12152
12239
  };
12153
12240
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
12154
- const pendingPath = (name) => join18(opts.deviceDir, "runners-pending", `${name}.json`);
12241
+ const pendingPath = (name) => join20(opts.deviceDir, "runners-pending", `${name}.json`);
12155
12242
  const readPending = (name) => {
12156
12243
  try {
12157
12244
  const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
@@ -12165,12 +12252,12 @@ function createDockerProvider(opts) {
12165
12252
  rmSync9(pendingPath(name), { force: true });
12166
12253
  return;
12167
12254
  }
12168
- mkdirSync10(join18(opts.deviceDir, "runners-pending"), { recursive: true });
12255
+ mkdirSync10(join20(opts.deviceDir, "runners-pending"), { recursive: true });
12169
12256
  writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
12170
12257
  };
12171
12258
  const keepSession = async (name, attempt, attemptId) => {
12172
12259
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
12173
- mkdirSync10(join18(archive, ".."), { recursive: true });
12260
+ mkdirSync10(join20(archive, ".."), { recursive: true });
12174
12261
  const partial = `${archive}.${process.pid}.partial`;
12175
12262
  const file = new CappedFile(partial, sessionCap);
12176
12263
  const r = await execIn(
@@ -12187,7 +12274,7 @@ function createDockerProvider(opts) {
12187
12274
  );
12188
12275
  if (r.code === 0 && !file.over) {
12189
12276
  renameSync5(partial, archive);
12190
- writeFileSync6(join18(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12277
+ writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12191
12278
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
12192
12279
  await writeMarker(name, attemptId);
12193
12280
  log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -12201,7 +12288,7 @@ function createDockerProvider(opts) {
12201
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"
12202
12289
  );
12203
12290
  };
12204
- const stagingDir = (name) => join18(opts.deviceDir, "runners-pending", `${name}.outbox`);
12291
+ const stagingDir = (name) => join20(opts.deviceDir, "runners-pending", `${name}.outbox`);
12205
12292
  const ensureRunning = async (name) => {
12206
12293
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
12207
12294
  if (seen.code !== 0) return false;
@@ -12209,7 +12296,7 @@ function createDockerProvider(opts) {
12209
12296
  return (await docker.run(["start", name])).code === 0;
12210
12297
  };
12211
12298
  const dropArchive = (attemptId) => {
12212
- rmSync9(join18(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
12299
+ rmSync9(join20(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
12213
12300
  recursive: true,
12214
12301
  force: true
12215
12302
  });
@@ -12278,7 +12365,7 @@ function createDockerProvider(opts) {
12278
12365
  if (seen.stdout.trim().split(" ")[0] !== "true")
12279
12366
  await must(["start", name], "starting the container");
12280
12367
  const git = await serveGitCredentials({
12281
- socketPath: join18(socketDir(name), "git.sock"),
12368
+ socketPath: join20(socketDir(name), "git.sock"),
12282
12369
  hosts: opts.gitHosts,
12283
12370
  fill: opts.gitFill,
12284
12371
  log
@@ -12312,7 +12399,7 @@ function createDockerProvider(opts) {
12312
12399
  }
12313
12400
  if (attemptId && UUID_RE.test(attemptId)) {
12314
12401
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
12315
- if (existsSync13(archive)) {
12402
+ if (existsSync14(archive)) {
12316
12403
  const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
12317
12404
  input: createReadStream(archive)
12318
12405
  });
@@ -12378,7 +12465,7 @@ function createDockerProvider(opts) {
12378
12465
  };
12379
12466
  child.once("error", () => done(1));
12380
12467
  child.once("close", (code, sig) => {
12381
- const signum = sig ? constants2.signals[sig] : void 0;
12468
+ const signum = sig ? constants3.signals[sig] : void 0;
12382
12469
  done(code ?? (signum !== void 0 ? 128 + signum : 1));
12383
12470
  });
12384
12471
  if (signal.aborted) stop();
@@ -12427,15 +12514,15 @@ function createDockerProvider(opts) {
12427
12514
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
12428
12515
  if (transcripts) {
12429
12516
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
12430
- if (kept.length) mkdirSync10(join18(opts.hostStateDir, "transcripts"), { recursive: true });
12431
- 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);
12432
12519
  await removeIn(
12433
12520
  name,
12434
12521
  `${CONTAINER_STATE}/transcripts`,
12435
12522
  kept.map((f) => f.name)
12436
12523
  );
12437
12524
  }
12438
- const outboxRel = join18("outbox", encodeURIComponent(attempt.agentId));
12525
+ const outboxRel = join20("outbox", encodeURIComponent(attempt.agentId));
12439
12526
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
12440
12527
  let unpublished = false;
12441
12528
  if (outbox) {
@@ -12461,7 +12548,7 @@ function createDockerProvider(opts) {
12461
12548
  const staged = stagingDir(name);
12462
12549
  mkdirSync10(staged, { recursive: true });
12463
12550
  writeFileSync6(
12464
- join18(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12551
+ join20(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12465
12552
  `${JSON.stringify(entry)}
12466
12553
  `
12467
12554
  );
@@ -12474,7 +12561,7 @@ function createDockerProvider(opts) {
12474
12561
  if (outbox.complete) {
12475
12562
  const published = publishStaged(
12476
12563
  stagingDir(name),
12477
- join18(opts.hostStateDir, outboxRel),
12564
+ join20(opts.hostStateDir, outboxRel),
12478
12565
  opts.moveFile
12479
12566
  );
12480
12567
  queued = published.published;
@@ -12683,15 +12770,15 @@ import {
12683
12770
  mkdirSync as mkdirSync11,
12684
12771
  mkdtempSync,
12685
12772
  readFileSync as readFileSync10,
12686
- realpathSync as realpathSync3,
12773
+ realpathSync as realpathSync4,
12687
12774
  rmSync as rmSync10,
12688
12775
  writeFileSync as writeFileSync7
12689
12776
  } from "fs";
12690
12777
  import { createRequire as createRequire2 } from "module";
12691
12778
  import { isIP } from "net";
12692
- import { homedir as homedir4, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12693
- import { dirname as dirname8, join as join19, sep as sep3 } from "path";
12694
- 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";
12695
12782
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
12696
12783
  function accountHome() {
12697
12784
  try {
@@ -12787,13 +12874,13 @@ function companionStoreWatch(log) {
12787
12874
  return new CompanionStoreWatch({
12788
12875
  log,
12789
12876
  roots: () => {
12790
- const home = homedir4();
12877
+ const home = homedir6();
12791
12878
  return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
12792
12879
  }
12793
12880
  });
12794
12881
  }
12795
12882
  function isAtOrUnder(path, dir2) {
12796
- return path === dir2 || path.startsWith(dir2 + sep3);
12883
+ return path === dir2 || path.startsWith(dir2 + sep4);
12797
12884
  }
12798
12885
  function osSandboxUsable(log) {
12799
12886
  if (!SandboxManager.isSupportedPlatform()) {
@@ -12892,7 +12979,7 @@ var DispatchClaims = class {
12892
12979
 
12893
12980
  // src/outbox.ts
12894
12981
  import {
12895
- existsSync as existsSync14,
12982
+ existsSync as existsSync15,
12896
12983
  mkdirSync as mkdirSync12,
12897
12984
  readdirSync as readdirSync5,
12898
12985
  readFileSync as readFileSync11,
@@ -12900,7 +12987,7 @@ import {
12900
12987
  rmSync as rmSync11,
12901
12988
  writeFileSync as writeFileSync8
12902
12989
  } from "fs";
12903
- import { join as join20 } from "path";
12990
+ import { join as join22 } from "path";
12904
12991
  var MAX_ENTRIES = 2e3;
12905
12992
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
12906
12993
  var Outbox = class {
@@ -12913,10 +13000,10 @@ var Outbox = class {
12913
13000
  // Resolved lazily (per call) so tests that swap HOME between
12914
13001
  // cases route writes at the right tmpdir.
12915
13002
  dir() {
12916
- return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
13003
+ return join22(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
12917
13004
  }
12918
13005
  fileFor(turnId, seq) {
12919
- return join20(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
13006
+ return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
12920
13007
  }
12921
13008
  // Persist a commit for later draining. Atomic (temp file + rename) so a
12922
13009
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -12948,7 +13035,7 @@ var Outbox = class {
12948
13035
  // wedging the drain.
12949
13036
  list() {
12950
13037
  const dir2 = this.dir();
12951
- if (!existsSync14(dir2)) return [];
13038
+ if (!existsSync15(dir2)) return [];
12952
13039
  let names;
12953
13040
  try {
12954
13041
  names = readdirSync5(dir2);
@@ -12958,7 +13045,7 @@ var Outbox = class {
12958
13045
  const entries = [];
12959
13046
  for (const name of names) {
12960
13047
  if (!name.endsWith(".json")) continue;
12961
- const full = join20(dir2, name);
13048
+ const full = join22(dir2, name);
12962
13049
  try {
12963
13050
  const parsed = JSON.parse(readFileSync11(full, "utf8"));
12964
13051
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -12994,7 +13081,7 @@ var Outbox = class {
12994
13081
  // different: it is positive evidence that nothing was ever queued here.
12995
13082
  maxSeqForTurn(turnId) {
12996
13083
  const dir2 = this.dir();
12997
- if (!existsSync14(dir2)) return 0;
13084
+ if (!existsSync15(dir2)) return 0;
12998
13085
  const names = readdirSync5(dir2);
12999
13086
  const prefix = `${encodeURIComponent(turnId)}__`;
13000
13087
  let max = 0;
@@ -13014,7 +13101,7 @@ var Outbox = class {
13014
13101
  }
13015
13102
  size() {
13016
13103
  const dir2 = this.dir();
13017
- if (!existsSync14(dir2)) return 0;
13104
+ if (!existsSync15(dir2)) return 0;
13018
13105
  try {
13019
13106
  return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
13020
13107
  } catch {
@@ -13215,9 +13302,10 @@ var CompanionSupervisor = class {
13215
13302
  // surface, so the UI never disagrees with what is advertised. Null until the
13216
13303
  // first probe.
13217
13304
  harnessSignals = null;
13218
- // Fresh per attempt, never the cached heartbeat signal: installing a harness
13219
- // between attempts must be observed immediately.
13220
- 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 = {};
13221
13309
  exitFn;
13222
13310
  reexecFn;
13223
13311
  dispatcherFactory;
@@ -13261,7 +13349,7 @@ var CompanionSupervisor = class {
13261
13349
  this.log = opts.log;
13262
13350
  this.hub = opts.hub;
13263
13351
  this.resolveBundled = opts.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
13264
- this.probePresence = opts.probePresence ?? probeHarnessPresence;
13352
+ this.runHarnessCheck = opts.checkHarness ?? ((runtime) => checkHarnessUsable(runtime));
13265
13353
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
13266
13354
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
13267
13355
  this.reexecFn = opts.reexec ?? defaultReexec;
@@ -13288,7 +13376,7 @@ var CompanionSupervisor = class {
13288
13376
  });
13289
13377
  }
13290
13378
  dockerRunners() {
13291
- return runnersEnabled(this.config) && this.config.runners?.provider === "docker";
13379
+ return dockerRunnersConfigured(this.config);
13292
13380
  }
13293
13381
  // Stand up the data plane: pair check, initial assignments pull, then the
13294
13382
  // heartbeat + poll loops. A companion with no device token (logged out) does
@@ -13335,10 +13423,10 @@ var CompanionSupervisor = class {
13335
13423
  else this.log.error({ posture }, `runners: ${summary}`);
13336
13424
  void this.companionStores().refresh();
13337
13425
  }
13338
- const startupHarnessRefresh = this.refreshHarnessStatuses();
13426
+ const startupHarnessRefresh = this.checkHarnesses().then(() => this.refreshHarnessStatuses());
13339
13427
  this.trackHeartbeat(startupHarnessRefresh);
13340
13428
  if (!this.config.deviceToken) {
13341
- 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.");
13342
13430
  return;
13343
13431
  }
13344
13432
  this.deviceApi = new DeviceApi({
@@ -13374,7 +13462,7 @@ var CompanionSupervisor = class {
13374
13462
  },
13375
13463
  onAuthFailure: () => {
13376
13464
  this.log.error(
13377
- "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.`
13378
13466
  );
13379
13467
  }
13380
13468
  });
@@ -13831,11 +13919,11 @@ var CompanionSupervisor = class {
13831
13919
  if (this.runnerCommand) return this.runnerCommand;
13832
13920
  return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
13833
13921
  }
13834
- buildDispatcher(ctx) {
13922
+ buildDispatcher(ctx2) {
13835
13923
  const hostLocal = localAgentConfig(this.config, {
13836
- agentId: ctx.agentId,
13837
- agentUsername: ctx.agentUsername,
13838
- workspaceSlug: ctx.workspaceSlug
13924
+ agentId: ctx2.agentId,
13925
+ agentUsername: ctx2.agentUsername,
13926
+ workspaceSlug: ctx2.workspaceSlug
13839
13927
  });
13840
13928
  const local = this.dockerRunners() && hostLocal.prepareHook ? {
13841
13929
  ...hostLocal,
@@ -13844,20 +13932,20 @@ var CompanionSupervisor = class {
13844
13932
  command: containerHookPath(hostLocal.prepareHook.command)
13845
13933
  }
13846
13934
  } : hostLocal;
13847
- if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
13935
+ if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx2, local });
13848
13936
  return new Dispatcher({
13849
13937
  deviceId: this.deviceId ?? void 0,
13850
- api: ctx.api,
13851
- baseUrl: ctx.baseUrl,
13852
- workspaceId: ctx.workspaceId,
13853
- workspaceSlug: ctx.workspaceSlug,
13854
- agentId: ctx.agentId,
13855
- agentUsername: ctx.agentUsername,
13856
- 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,
13857
13945
  local,
13858
- aborts: ctx.aborts,
13859
- runConfig: ctx.runConfig,
13860
- 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 }),
13861
13949
  // Read PER TURN off the heartbeat's own predicates, so a harness installed
13862
13950
  // or connected after boot works on the next turn exactly as it appears on
13863
13951
  // the next beat, and a runtime advertised by one and refused by the other
@@ -13870,7 +13958,7 @@ var CompanionSupervisor = class {
13870
13958
  // DEFAULT_AGENT_IDLE_TIMEOUT_MS.
13871
13959
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
13872
13960
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
13873
- observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
13961
+ observer: this.hub.observerFor(ctx2.workspaceId, ctx2.agentId, ctx2.workspaceSlug),
13874
13962
  transcriptDir: transcriptsDir(),
13875
13963
  connectorHealth: this.connectorHealth,
13876
13964
  ...runnersEnabled(this.config) ? {
@@ -13881,7 +13969,7 @@ var CompanionSupervisor = class {
13881
13969
  reportRunnerGone: (report) => this.reportRunnerGone(report),
13882
13970
  connectorHealth: this.connectorHealth,
13883
13971
  locks: this.runnerLocks,
13884
- sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx)),
13972
+ sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx2)),
13885
13973
  policy: this.sandboxPolicy(),
13886
13974
  companionStores: () => this.companionStores().stores()
13887
13975
  }
@@ -14309,23 +14397,23 @@ var CompanionSupervisor = class {
14309
14397
  configureLogger(this.log, next);
14310
14398
  }
14311
14399
  // ---- harnesses ----
14312
- // Probe all three harnesses, cache signals and versions, and push the snapshot
14313
- // to the hub. `probeHarnessSignals` never throws, but this is wrapped anyway so
14314
- // a refresh can never take a heartbeat down.
14315
- 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() {
14316
14405
  try {
14317
14406
  const signals = await probeHarnessSignals(this.config, {
14318
- probePresence: this.probePresence,
14319
- presence,
14407
+ checks: this.harnessChecks,
14320
14408
  // Through the supervisor's seam, so a test pins the host fact.
14321
14409
  resolveBundled: this.resolveBundled
14322
14410
  });
14323
14411
  this.harnessSignals = signals;
14324
14412
  this.logBundledTransitions();
14325
14413
  this.harnessVersions = {
14326
- claudeCode: signals.claudeVersion,
14414
+ claudeCode: checkVersion(signals.claudeCheck),
14327
14415
  opencode: signals.opencodeVersion,
14328
- codex: signals.codexVersion
14416
+ codex: checkVersion(signals.codexCheck)
14329
14417
  };
14330
14418
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
14331
14419
  } catch (err) {
@@ -14335,9 +14423,32 @@ var CompanionSupervisor = class {
14335
14423
  );
14336
14424
  }
14337
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
+ }
14338
14448
  // The dashboard "recheck harnesses" control — an immediate re-probe so a user who
14339
14449
  // just installed/started a harness sees it flip without waiting for the next beat.
14340
14450
  async recheckHarnesses() {
14451
+ await this.checkHarnesses();
14341
14452
  await this.refreshHarnessStatuses();
14342
14453
  }
14343
14454
  // Beat NOW and wait for it to land. `start` calls this before asking the
@@ -14357,48 +14468,57 @@ var CompanionSupervisor = class {
14357
14468
  currentConfig() {
14358
14469
  return this.config;
14359
14470
  }
14360
- // 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
14361
14472
  // `~/.cabane/config.json`. Never installs a binary or drives a login (BYO).
14362
14473
  //
14363
- // This records INTENT, the user's half: a harness is advertised only when also
14364
- // its bundled binary resolves, so an enable on a companion whose optional
14365
- // dependency never installed advertises nothing, and the Harnesses surface
14366
- // says why.
14367
- // - claude-code: `claudeCode.enabled`, after a presence check. The terminal
14368
- // offer and the web pairing flow both call this.
14369
- // - 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.
14370
14479
  // - opencode: `opencode.serverUrl`, ONLY after health-probing the URL; an
14371
14480
  // unreachable one is a typed failure shown in place.
14372
- // On success: persist, re-probe and beat at once, so the answer is true in
14373
- // 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.
14374
14484
  async enableHarness(input) {
14375
14485
  let next;
14376
- let checkedPresence = null;
14377
- if (input.runtime === "claude-code") {
14378
- const presence = input.presence ?? await this.probePresence("claude-code");
14379
- 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();
14380
14500
  return {
14381
- ok: false,
14382
- error: presenceReason("claude-code", presence.status),
14383
- presence: true
14501
+ ok: true,
14502
+ outcome: {
14503
+ kind: "already",
14504
+ runtime,
14505
+ ...check ? { check } : {},
14506
+ ...label ? { deviceLabel: label } : {}
14507
+ }
14384
14508
  };
14385
14509
  }
14386
- checkedPresence = { runtime: "claude-code", result: presence };
14387
- next = { ...this.config, claudeCode: { enabled: true } };
14388
- } else if (input.runtime === "codex") {
14389
- const presence = input.presence ?? await this.probePresence("codex");
14390
- if (presence.status !== "present") {
14391
- return { ok: false, error: presenceReason("codex", presence.status), presence: true };
14392
- }
14393
- checkedPresence = { runtime: "codex", result: presence };
14394
- 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 } : {} };
14395
14512
  } else {
14396
14513
  const serverUrl = input.serverUrl.trim();
14397
14514
  const parsed = companionConfigSchema.shape.opencode.safeParse({ serverUrl });
14398
14515
  if (!parsed.success) {
14399
14516
  return {
14400
14517
  ok: false,
14401
- 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
+ }
14402
14522
  };
14403
14523
  }
14404
14524
  const boundedFetch = ((url, init2) => fetch(url, { ...init2, signal: AbortSignal.timeout(4e3) }));
@@ -14406,19 +14526,21 @@ var CompanionSupervisor = class {
14406
14526
  if (version === null) {
14407
14527
  return {
14408
14528
  ok: false,
14409
- 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
+ }
14410
14533
  };
14411
14534
  }
14412
14535
  next = { ...this.config, opencode: { serverUrl } };
14536
+ outcome = { kind: "connected", runtime: "opencode" };
14413
14537
  }
14414
14538
  this.config = next;
14415
14539
  saveConfig(next);
14416
14540
  this.rebuildDispatchers();
14417
- await this.refreshHarnessStatuses(
14418
- checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
14419
- );
14541
+ await this.refreshHarnessStatuses();
14420
14542
  this.kickHeartbeat();
14421
- return { ok: true };
14543
+ return { ok: true, outcome };
14422
14544
  }
14423
14545
  // Re-create every running agent's Dispatcher from the CURRENT config, keeping
14424
14546
  // the runner (its api, outbox, chains) intact, so constructor-captured options
@@ -14478,9 +14600,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
14478
14600
  }
14479
14601
  function defaultReexec() {
14480
14602
  clearRuntimeState();
14481
- void import("child_process").then(({ spawn: spawn9 }) => {
14603
+ void import("child_process").then(({ spawn: spawn8 }) => {
14482
14604
  try {
14483
- const child = spawn9(process.execPath, process.argv.slice(1), {
14605
+ const child = spawn8(process.execPath, process.argv.slice(1), {
14484
14606
  stdio: "inherit",
14485
14607
  detached: false
14486
14608
  });
@@ -14550,10 +14672,10 @@ function handleUncaught(log, err, origin) {
14550
14672
  }
14551
14673
 
14552
14674
  // src/crash-marker.ts
14553
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
14554
- 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";
14555
14677
  function crashMarkerPath() {
14556
- return join21(cabaneDir(), "last-error.json");
14678
+ return join23(cabaneDir(), "last-error.json");
14557
14679
  }
14558
14680
  function recordCrash(rec) {
14559
14681
  try {
@@ -14565,7 +14687,7 @@ function recordCrash(rec) {
14565
14687
  function clearCrash() {
14566
14688
  try {
14567
14689
  const path = crashMarkerPath();
14568
- if (existsSync15(path)) rmSync12(path, { force: true });
14690
+ if (existsSync16(path)) rmSync12(path, { force: true });
14569
14691
  } catch {
14570
14692
  }
14571
14693
  }
@@ -14575,17 +14697,14 @@ async function createCompanionRuntime(opts = {}) {
14575
14697
  const log = getLogger();
14576
14698
  installProcessSafetyNet(log);
14577
14699
  let cfg;
14578
- let claudeCode;
14579
14700
  try {
14580
- ({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
14581
- ...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
14582
- onMigrated: (migrated, onPath) => log.debug(
14701
+ cfg = requireStartConfig({
14702
+ onMigrated: (migrated) => log.debug(
14583
14703
  { claudeCode: isClaudeCodeConnected(migrated) },
14584
- 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)"
14585
14705
  )
14586
- }));
14587
- await warnAboutHarnessReadiness(cfg, {
14588
- probeClaude: async () => claudeCode,
14706
+ });
14707
+ warnAboutHarnessReadiness(cfg, {
14589
14708
  // The CLI's onboarding script says this in its own words, so it hands a
14590
14709
  // sink that logs instead. Every other caller keeps the stderr line.
14591
14710
  ...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
@@ -14600,13 +14719,6 @@ async function createCompanionRuntime(opts = {}) {
14600
14719
  throw err;
14601
14720
  }
14602
14721
  configureLogger(log, cfg);
14603
- const harnessVersions = await probeHarnessVersions({
14604
- // Connected AND its CLI answered — the two things that make a
14605
- // `claude --version` probe worth spawning. Not the offer predicate.
14606
- claudeCode: claudeCode && isClaudeCodeConnected(cfg),
14607
- opencodeServerUrl: cfg.opencode?.serverUrl,
14608
- codex: isCodexEnabled(cfg)
14609
- });
14610
14722
  const instanceId = randomUUID7();
14611
14723
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14612
14724
  const pre = readLiveRuntimeState();
@@ -14634,35 +14746,31 @@ async function createCompanionRuntime(opts = {}) {
14634
14746
  config: cfg,
14635
14747
  log,
14636
14748
  hub,
14637
- harnessVersions
14749
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
14638
14750
  });
14639
14751
  await supervisor.start();
14640
14752
  const connectHarness = async (runtime, serverUrl) => {
14641
- const currentConfig = supervisor.currentConfig();
14642
- const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
14643
- const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
14644
- const presence = runtime === "opencode" ? void 0 : await probeHarnessPresence(runtime);
14645
- const verdict = await shakeOutHarness(runtime, candidate, {
14646
- ...presence ? { presence } : {}
14647
- });
14648
- if (verdict === "absent" || verdict === "unusable") {
14649
- 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
+ };
14650
14761
  }
14651
- const result = await supervisor.enableHarness(
14652
- runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime, presence }
14653
- );
14654
- if (!result.ok) return { ok: false, error: result.error };
14655
- 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;
14656
14767
  };
14657
14768
  let control = null;
14658
14769
  let dashboard = null;
14659
14770
  try {
14660
14771
  control = await startControlServer({
14661
14772
  status: () => hub.statusJson(),
14662
- connect: async (runtime, serverUrl) => {
14663
- const result = await connectHarness(runtime, serverUrl);
14664
- return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
14665
- },
14773
+ connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
14666
14774
  stop: () => void supervisor.requestStop()
14667
14775
  });
14668
14776
  if (opts.dashboard) {
@@ -14683,7 +14791,8 @@ async function createCompanionRuntime(opts = {}) {
14683
14791
  // marker records whether this companion is backgrounded (foreground start
14684
14792
  // leaves it unset → false).
14685
14793
  daemon: process.env.CABANE_COMPANION_DAEMON === "1",
14686
- instanceId
14794
+ instanceId,
14795
+ ...installedCommandPath() ? { command: installedCommandPath() } : {}
14687
14796
  });
14688
14797
  clearCrash();
14689
14798
  } catch (err) {