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