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