@cabane/companion 0.6.155 → 0.6.157
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 -169
- package/dist/bin.js +14 -0
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +4153 -2184
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +169 -23
- package/dist/runtime.js +1666 -833
- package/dist/static/app.js +2 -3
- package/package.json +4 -3
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,294 @@
|
|
|
1
1
|
// src/runtime.ts
|
|
2
2
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
3
3
|
|
|
4
|
+
// src/cabane-command.ts
|
|
5
|
+
import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
|
|
6
|
+
import { homedir as homedir3 } from "os";
|
|
7
|
+
import { delimiter, join as join2, sep as sep2 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/bundled-binaries.ts
|
|
10
|
+
import { realpathSync, statSync } from "fs";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join, sep } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
function nodeResolveSelf(specifier) {
|
|
16
|
+
const here = fileURLToPath(import.meta.url);
|
|
17
|
+
if (typeof import.meta.resolve === "function") {
|
|
18
|
+
try {
|
|
19
|
+
return fileURLToPath(import.meta.resolve(specifier));
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return createRequire(here).resolve(specifier);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
return walkNodeModules(specifier, here);
|
|
28
|
+
}
|
|
29
|
+
function walkNodeModules(pkg2, fromPath) {
|
|
30
|
+
let dir2 = dirname(fromPath);
|
|
31
|
+
for (; ; ) {
|
|
32
|
+
const manifest = join(dir2, "node_modules", pkg2, "package.json");
|
|
33
|
+
if (nodeIsFile(manifest)) {
|
|
34
|
+
try {
|
|
35
|
+
return realpathSync(manifest);
|
|
36
|
+
} catch {
|
|
37
|
+
return manifest;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parent = dirname(dir2);
|
|
41
|
+
if (parent === dir2) return null;
|
|
42
|
+
dir2 = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function nodeResolve(specifier, fromPath) {
|
|
46
|
+
try {
|
|
47
|
+
return createRequire(fromPath).resolve(specifier);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function nodeIsFile(path) {
|
|
53
|
+
try {
|
|
54
|
+
return statSync(path).isFile();
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function detectPreferMusl(platform = process.platform) {
|
|
60
|
+
if (platform !== "linux") return false;
|
|
61
|
+
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
|
|
62
|
+
const header = report?.header;
|
|
63
|
+
return report != null && header?.glibcVersionRuntime === void 0;
|
|
64
|
+
}
|
|
65
|
+
function defaultResolveEnv() {
|
|
66
|
+
return {
|
|
67
|
+
platform: process.platform,
|
|
68
|
+
arch: process.arch,
|
|
69
|
+
preferMusl: detectPreferMusl(),
|
|
70
|
+
// Both resolutions start from this module, which tsup bundles into
|
|
71
|
+
// `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
|
|
72
|
+
// root — so one environment serves the runtime checks and the install
|
|
73
|
+
// verifier alike.
|
|
74
|
+
resolveSelf: nodeResolveSelf,
|
|
75
|
+
resolve: nodeResolve,
|
|
76
|
+
isFile: nodeIsFile
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
|
|
80
|
+
function claudePlatformPackages(platform, arch, preferMusl) {
|
|
81
|
+
if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
|
|
82
|
+
if (platform === "linux") {
|
|
83
|
+
return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
|
|
84
|
+
}
|
|
85
|
+
return [`${CLAUDE_SDK}-${platform}-${arch}`];
|
|
86
|
+
}
|
|
87
|
+
function resolveClaude(env) {
|
|
88
|
+
const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
|
|
89
|
+
const base = {
|
|
90
|
+
runtime: "claude-code",
|
|
91
|
+
packageName: candidates[0] ?? null,
|
|
92
|
+
platform: env.platform,
|
|
93
|
+
arch: env.arch
|
|
94
|
+
};
|
|
95
|
+
const sdkEntry = env.resolveSelf(CLAUDE_SDK);
|
|
96
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
97
|
+
let sawPackage = null;
|
|
98
|
+
for (const pkg2 of candidates) {
|
|
99
|
+
const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
|
|
100
|
+
if (!manifest) continue;
|
|
101
|
+
sawPackage ??= pkg2;
|
|
102
|
+
const exe = join(dirname(manifest), env.platform === "win32" ? "claude.exe" : "claude");
|
|
103
|
+
if (env.isFile(exe)) {
|
|
104
|
+
return { ...base, packageName: pkg2, status: "present", path: exe };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
|
|
108
|
+
}
|
|
109
|
+
var CODEX_SDK = "@openai/codex-sdk";
|
|
110
|
+
var CODEX_CLI = "@openai/codex";
|
|
111
|
+
function codexTargetTriple(platform, arch) {
|
|
112
|
+
if (platform === "linux" || platform === "android") {
|
|
113
|
+
if (arch === "x64") return "x86_64-unknown-linux-musl";
|
|
114
|
+
if (arch === "arm64") return "aarch64-unknown-linux-musl";
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (platform === "darwin") {
|
|
118
|
+
if (arch === "x64") return "x86_64-apple-darwin";
|
|
119
|
+
if (arch === "arm64") return "aarch64-apple-darwin";
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (platform === "win32") {
|
|
123
|
+
if (arch === "x64") return "x86_64-pc-windows-msvc";
|
|
124
|
+
if (arch === "arm64") return "aarch64-pc-windows-msvc";
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
130
|
+
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
131
|
+
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
132
|
+
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
133
|
+
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
134
|
+
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
135
|
+
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
136
|
+
};
|
|
137
|
+
function resolveCodex(env) {
|
|
138
|
+
const triple = codexTargetTriple(env.platform, env.arch);
|
|
139
|
+
const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
|
|
140
|
+
const base = {
|
|
141
|
+
runtime: "codex",
|
|
142
|
+
packageName: pkg2 ?? null,
|
|
143
|
+
platform: env.platform,
|
|
144
|
+
arch: env.arch
|
|
145
|
+
};
|
|
146
|
+
if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
|
|
147
|
+
const sdkEntry = env.resolveSelf(CODEX_SDK);
|
|
148
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
149
|
+
const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
|
|
150
|
+
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
151
|
+
const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
|
|
152
|
+
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
153
|
+
const packageRoot = join(dirname(platformManifest), "vendor", triple);
|
|
154
|
+
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
155
|
+
const current = join(packageRoot, "bin", exeName);
|
|
156
|
+
if (env.isFile(current) && env.isFile(join(packageRoot, "codex-package.json"))) {
|
|
157
|
+
return { ...base, status: "present", path: current };
|
|
158
|
+
}
|
|
159
|
+
const legacy = join(packageRoot, "codex", exeName);
|
|
160
|
+
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
161
|
+
return { ...base, status: "executable_missing", path: null };
|
|
162
|
+
}
|
|
163
|
+
function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
|
|
164
|
+
try {
|
|
165
|
+
return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
|
|
166
|
+
} catch {
|
|
167
|
+
return {
|
|
168
|
+
runtime,
|
|
169
|
+
status: "platform_package_missing",
|
|
170
|
+
path: null,
|
|
171
|
+
packageName: null,
|
|
172
|
+
platform: env.platform,
|
|
173
|
+
arch: env.arch
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
var HARNESS_LABEL = {
|
|
178
|
+
"claude-code": "Claude Code",
|
|
179
|
+
codex: "Codex"
|
|
180
|
+
};
|
|
181
|
+
var HARNESS_INTENT_WORD = {
|
|
182
|
+
"claude-code": "connected",
|
|
183
|
+
codex: "enabled"
|
|
184
|
+
};
|
|
185
|
+
function missingNoun(status) {
|
|
186
|
+
return status === "executable_missing" ? "executable" : "platform package";
|
|
187
|
+
}
|
|
188
|
+
var INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
|
|
189
|
+
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
190
|
+
const real = (path) => {
|
|
191
|
+
try {
|
|
192
|
+
return realpathSync(path);
|
|
193
|
+
} catch {
|
|
194
|
+
return path;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const scriptNode = join(real(home), ".cabane", "node") + sep;
|
|
198
|
+
return real(execPath).startsWith(scriptNode) ? "script" : "npm";
|
|
199
|
+
}
|
|
200
|
+
function repairCommands(install = companionInstall()) {
|
|
201
|
+
return install === "script" ? [INSTALL_SCRIPT_COMMAND] : ["npm i -g @cabane/companion --include=optional"];
|
|
202
|
+
}
|
|
203
|
+
function repairAction(install = companionInstall()) {
|
|
204
|
+
return `Reinstall with \`${repairCommands(install)[0]}\`.`;
|
|
205
|
+
}
|
|
206
|
+
function startupWarning(r) {
|
|
207
|
+
const label = HARNESS_LABEL[r.runtime];
|
|
208
|
+
return `${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but its installation is incomplete (${missingNoun(r.status)} is missing). It cannot run turns until repaired. ${repairAction()} Then restart the companion.`;
|
|
209
|
+
}
|
|
210
|
+
function harnessIssueNote(runtime, status, install = companionInstall()) {
|
|
211
|
+
const intent = HARNESS_INTENT_WORD[runtime];
|
|
212
|
+
const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
|
|
213
|
+
return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status)} is missing. ${repairAction(install)} Then restart the companion.`;
|
|
214
|
+
}
|
|
215
|
+
function turnIncompleteCopy(runtime) {
|
|
216
|
+
return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/cli-entry.ts
|
|
220
|
+
import { existsSync } from "fs";
|
|
221
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
222
|
+
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
223
|
+
function companionCliEntry(deps = {}) {
|
|
224
|
+
const exists = deps.exists ?? existsSync;
|
|
225
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
|
|
226
|
+
for (const candidate of candidates) {
|
|
227
|
+
if (exists(candidate)) return candidate;
|
|
228
|
+
}
|
|
229
|
+
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
230
|
+
if (argv1 && exists(argv1)) return argv1;
|
|
231
|
+
return candidates[0] ?? "";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/term.ts
|
|
235
|
+
import { createInterface } from "readline";
|
|
236
|
+
import { homedir as homedir2 } from "os";
|
|
237
|
+
import pc from "picocolors";
|
|
238
|
+
function tildePath(path, home = homedir2()) {
|
|
239
|
+
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
240
|
+
return path;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/cabane-command.ts
|
|
244
|
+
function installedCommandPath(deps = {}) {
|
|
245
|
+
const install = deps.install ?? companionInstall();
|
|
246
|
+
const home = deps.home ?? homedir3();
|
|
247
|
+
if (install === "script") return join2(home, ".cabane", "bin", "cabane");
|
|
248
|
+
const entry = deps.cliEntry ?? companionCliEntry();
|
|
249
|
+
const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
|
|
250
|
+
const at = entry.indexOf(marker);
|
|
251
|
+
if (at < 0) return null;
|
|
252
|
+
return join2(entry.slice(0, at), "bin", "cabane");
|
|
253
|
+
}
|
|
254
|
+
function cabaneCommand(deps = {}) {
|
|
255
|
+
const own = installedCommandPath(deps);
|
|
256
|
+
if (!own) return "cabane";
|
|
257
|
+
const realpath = deps.realpath ?? safeRealpath;
|
|
258
|
+
const isExecutable = deps.isExecutable ?? executable;
|
|
259
|
+
const ownReal = realpath(own);
|
|
260
|
+
if (!ownReal) return "cabane";
|
|
261
|
+
const onPath = firstOnPath("cabane", "path" in deps ? deps.path : process.env.PATH, isExecutable);
|
|
262
|
+
if (onPath && realpath(onPath) === ownReal) return "cabane";
|
|
263
|
+
return tildePath(own, deps.home);
|
|
264
|
+
}
|
|
265
|
+
function cabane(rest, deps) {
|
|
266
|
+
return `${cabaneCommand(deps)} ${rest}`;
|
|
267
|
+
}
|
|
268
|
+
function firstOnPath(name, path, isExecutable = executable) {
|
|
269
|
+
for (const dir2 of (path ?? "").split(delimiter)) {
|
|
270
|
+
if (!dir2) continue;
|
|
271
|
+
const candidate = join2(dir2, name);
|
|
272
|
+
if (isExecutable(candidate)) return candidate;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function executable(path) {
|
|
277
|
+
try {
|
|
278
|
+
accessSync(path, constants.X_OK);
|
|
279
|
+
return true;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function safeRealpath(path) {
|
|
285
|
+
try {
|
|
286
|
+
return realpathSync2(path);
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
4
292
|
// src/config.ts
|
|
5
293
|
import {
|
|
6
294
|
chmodSync,
|
|
@@ -9,10 +297,10 @@ import {
|
|
|
9
297
|
renameSync,
|
|
10
298
|
rmSync,
|
|
11
299
|
writeFileSync,
|
|
12
|
-
existsSync
|
|
300
|
+
existsSync as existsSync2
|
|
13
301
|
} from "fs";
|
|
14
|
-
import { homedir, userInfo } from "os";
|
|
15
|
-
import { dirname, join } from "path";
|
|
302
|
+
import { homedir as homedir4, userInfo } from "os";
|
|
303
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
16
304
|
import { z as z2 } from "zod";
|
|
17
305
|
|
|
18
306
|
// src/errors.ts
|
|
@@ -225,13 +513,13 @@ function realAccountHome() {
|
|
|
225
513
|
return realHomeCache;
|
|
226
514
|
}
|
|
227
515
|
function agentCompanionHome() {
|
|
228
|
-
return
|
|
516
|
+
return join3(cabaneDir(), "agent-home");
|
|
229
517
|
}
|
|
230
518
|
function cabaneDir() {
|
|
231
|
-
const dir2 =
|
|
519
|
+
const dir2 = join3(process.env.CABANE_COMPANION_HOME || homedir4(), ".cabane");
|
|
232
520
|
if (process.env.VITEST) {
|
|
233
521
|
const real = realAccountHome();
|
|
234
|
-
if (real && dir2 ===
|
|
522
|
+
if (real && dir2 === join3(real, ".cabane")) {
|
|
235
523
|
throw new Error(
|
|
236
524
|
`cabaneDir() resolved to the real ${dir2} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
|
|
237
525
|
);
|
|
@@ -240,7 +528,7 @@ function cabaneDir() {
|
|
|
240
528
|
return dir2;
|
|
241
529
|
}
|
|
242
530
|
function configPath() {
|
|
243
|
-
return
|
|
531
|
+
return join3(cabaneDir(), "config.json");
|
|
244
532
|
}
|
|
245
533
|
var localAgentConfigSchema = z2.object({
|
|
246
534
|
cwd: z2.string().optional(),
|
|
@@ -259,11 +547,11 @@ var localAgentConfigSchema = z2.object({
|
|
|
259
547
|
sandbox: z2.enum(["os", "none"]).optional()
|
|
260
548
|
}).strict();
|
|
261
549
|
var AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
262
|
-
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents,
|
|
550
|
+
var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx2) => {
|
|
263
551
|
for (const [key, row] of Object.entries(agents)) {
|
|
264
552
|
if (row.sandbox === void 0) continue;
|
|
265
553
|
if (AGENT_ID_KEY.test(key)) continue;
|
|
266
|
-
|
|
554
|
+
ctx2.addIssue({
|
|
267
555
|
code: z2.ZodIssueCode.custom,
|
|
268
556
|
path: [key, "sandbox"],
|
|
269
557
|
message: "a sandbox exception must name one agent \u2014 key the row by the agent's id (shown in Cabane), not a username or workspace-slug/username"
|
|
@@ -314,15 +602,33 @@ 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
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
620
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
621
|
+
// which `start` offers; off either way until someone says yes.
|
|
622
|
+
update: z2.object({
|
|
623
|
+
auto: z2.boolean()
|
|
624
|
+
}).strict().optional(),
|
|
625
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
626
|
+
// `cabane service install` still installs it.
|
|
627
|
+
serviceDeclined: z2.boolean().optional(),
|
|
628
|
+
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
629
|
+
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
630
|
+
// serve would be offered again on every `start`.
|
|
631
|
+
opencodeDeclined: z2.boolean().optional(),
|
|
326
632
|
// Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
|
|
327
633
|
// an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
|
|
328
634
|
// never sees the key). The block's presence enables it; `enabled: false` turns
|
|
@@ -377,6 +683,9 @@ var companionConfigSchema = z2.object({
|
|
|
377
683
|
function runnersEnabled(cfg) {
|
|
378
684
|
return cfg.runners?.enabled === true;
|
|
379
685
|
}
|
|
686
|
+
function dockerRunnersConfigured(cfg) {
|
|
687
|
+
return runnersEnabled(cfg) && cfg.runners?.provider === "docker";
|
|
688
|
+
}
|
|
380
689
|
function runnerStateDir() {
|
|
381
690
|
return process.env.CABANE_RUNNER_STATE_DIR || cabaneDir();
|
|
382
691
|
}
|
|
@@ -386,9 +695,9 @@ function isCodexEnabled(cfg) {
|
|
|
386
695
|
function isClaudeCodeConnected(cfg) {
|
|
387
696
|
return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
388
697
|
}
|
|
389
|
-
function migrateConnectedHarnesses(cfg
|
|
390
|
-
if (cfg.claudeCode !== void 0) return null;
|
|
391
|
-
return { ...cfg, claudeCode: { enabled:
|
|
698
|
+
function migrateConnectedHarnesses(cfg) {
|
|
699
|
+
if (cfg.claudeCode !== void 0 || cfg.harnessesChosen) return null;
|
|
700
|
+
return { ...cfg, claudeCode: { enabled: true } };
|
|
392
701
|
}
|
|
393
702
|
function localAgentConfig(cfg, agent) {
|
|
394
703
|
const map = cfg.agents ?? {};
|
|
@@ -420,7 +729,7 @@ function agentSandboxRows(cfg) {
|
|
|
420
729
|
}
|
|
421
730
|
function loadConfig() {
|
|
422
731
|
const path = configPath();
|
|
423
|
-
if (!
|
|
732
|
+
if (!existsSync2(path)) return null;
|
|
424
733
|
let raw;
|
|
425
734
|
try {
|
|
426
735
|
raw = readFileSync(path, "utf8");
|
|
@@ -447,14 +756,14 @@ function loadConfig() {
|
|
|
447
756
|
);
|
|
448
757
|
}
|
|
449
758
|
throw new ConfigError(
|
|
450
|
-
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run
|
|
759
|
+
`${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
760
|
);
|
|
452
761
|
}
|
|
453
762
|
return result.data;
|
|
454
763
|
}
|
|
455
764
|
function saveConfig(cfg) {
|
|
456
765
|
const path = configPath();
|
|
457
|
-
mkdirSync(
|
|
766
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
458
767
|
try {
|
|
459
768
|
chmodSync(cabaneDir(), 448);
|
|
460
769
|
} catch {
|
|
@@ -479,77 +788,38 @@ function requireConfig() {
|
|
|
479
788
|
const cfg = loadConfig();
|
|
480
789
|
if (!cfg) {
|
|
481
790
|
throw new ConfigError(
|
|
482
|
-
|
|
791
|
+
`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
792
|
);
|
|
484
793
|
}
|
|
485
794
|
return cfg;
|
|
486
795
|
}
|
|
487
796
|
|
|
488
797
|
// src/dashboard/server.ts
|
|
489
|
-
import { dirname as dirname4, join as
|
|
490
|
-
import { fileURLToPath as
|
|
798
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
799
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
491
800
|
import { serve } from "@hono/node-server";
|
|
492
801
|
import { Hono } from "hono";
|
|
493
802
|
|
|
494
803
|
// src/dashboard/routes.ts
|
|
495
|
-
import { openSync, readSync, closeSync, fstatSync, existsSync as
|
|
804
|
+
import { openSync as openSync2, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
|
|
496
805
|
import { readFile } from "fs/promises";
|
|
497
|
-
import { extname, join as
|
|
806
|
+
import { extname, join as join6, normalize } from "path";
|
|
498
807
|
import { streamSSE } from "hono/streaming";
|
|
499
808
|
|
|
500
|
-
// src/harness-
|
|
501
|
-
import {
|
|
809
|
+
// src/harness-copy.ts
|
|
810
|
+
import { homedir as homedir5 } from "os";
|
|
811
|
+
import { join as join4 } from "path";
|
|
502
812
|
|
|
503
|
-
// src/harness-
|
|
813
|
+
// src/harness-usable.ts
|
|
504
814
|
import { spawn as spawn2 } from "child_process";
|
|
815
|
+
|
|
816
|
+
// src/harness-versions.ts
|
|
505
817
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
506
|
-
var CLI_PROBE_TIMEOUT_MS = 4e3;
|
|
507
818
|
function parseVersionToken(raw) {
|
|
508
819
|
if (!raw) return null;
|
|
509
820
|
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
510
821
|
return m ? m[0] : null;
|
|
511
822
|
}
|
|
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
823
|
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
554
824
|
try {
|
|
555
825
|
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
@@ -577,236 +847,98 @@ function extractOpencodeVersion(json) {
|
|
|
577
847
|
}
|
|
578
848
|
return null;
|
|
579
849
|
}
|
|
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
850
|
function emptyHarnessVersions() {
|
|
592
851
|
return { ...EMPTY };
|
|
593
852
|
}
|
|
594
|
-
async function safe(fn) {
|
|
595
|
-
try {
|
|
596
|
-
return await fn();
|
|
597
|
-
} catch {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
853
|
|
|
602
|
-
// src/
|
|
603
|
-
|
|
604
|
-
|
|
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);
|
|
854
|
+
// src/harness-usable.ts
|
|
855
|
+
function isUsable(check) {
|
|
856
|
+
return check.status === "signed_in" || check.status === "api_key";
|
|
621
857
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
}
|
|
858
|
+
var API_KEY_VARIABLES = {
|
|
859
|
+
"claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
|
860
|
+
codex: ["CODEX_API_KEY"]
|
|
861
|
+
};
|
|
862
|
+
function apiKeyVariable(runtime, env = process.env) {
|
|
863
|
+
return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
|
|
637
864
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
865
|
+
var CHECK_TIMEOUT_MS = 5e3;
|
|
866
|
+
async function checkHarnessUsable(runtime, deps = {}) {
|
|
867
|
+
const env = deps.env ?? process.env;
|
|
868
|
+
const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
|
|
869
|
+
const run = deps.run ?? runBounded;
|
|
870
|
+
const resolution = resolveBundled(runtime);
|
|
871
|
+
if (resolution.status !== "present" || !resolution.path) {
|
|
872
|
+
return {
|
|
873
|
+
status: "bundle_missing",
|
|
874
|
+
bundled: resolution.status === "present" ? "executable_missing" : resolution.status
|
|
875
|
+
};
|
|
643
876
|
}
|
|
644
|
-
|
|
645
|
-
|
|
877
|
+
const path = resolution.path;
|
|
878
|
+
const version = run(path, ["--version"], env).then(
|
|
879
|
+
(r) => r.code === 0 ? parseVersionToken(r.output) : null
|
|
880
|
+
);
|
|
881
|
+
const variable = apiKeyVariable(runtime, env);
|
|
882
|
+
if (variable) return { status: "api_key", variable, version: await version };
|
|
883
|
+
const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
|
|
884
|
+
const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
|
|
885
|
+
if (answer.error) return { status: "check_failed", reason: answer.error };
|
|
886
|
+
const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
|
|
887
|
+
if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
|
|
888
|
+
return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
|
|
889
|
+
}
|
|
890
|
+
function readClaudeStatus(answer) {
|
|
891
|
+
const start = answer.output.indexOf("{");
|
|
892
|
+
const end = answer.output.lastIndexOf("}");
|
|
893
|
+
if (start < 0 || end < start) return "unreadable";
|
|
646
894
|
try {
|
|
647
|
-
|
|
895
|
+
const parsed = JSON.parse(answer.output.slice(start, end + 1));
|
|
896
|
+
const loggedIn = parsed?.loggedIn;
|
|
897
|
+
return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
|
|
648
898
|
} catch {
|
|
649
|
-
return
|
|
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`];
|
|
899
|
+
return "unreadable";
|
|
677
900
|
}
|
|
678
|
-
return [`${CLAUDE_SDK}-${platform}-${arch}`];
|
|
679
901
|
}
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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";
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
if (platform === "win32") {
|
|
716
|
-
if (arch === "x64") return "x86_64-pc-windows-msvc";
|
|
717
|
-
if (arch === "arm64") return "aarch64-pc-windows-msvc";
|
|
718
|
-
return null;
|
|
719
|
-
}
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
723
|
-
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
724
|
-
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
725
|
-
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
726
|
-
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
727
|
-
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
728
|
-
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
729
|
-
};
|
|
730
|
-
function resolveCodex(env) {
|
|
731
|
-
const triple = codexTargetTriple(env.platform, env.arch);
|
|
732
|
-
const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
|
|
733
|
-
const base = {
|
|
734
|
-
runtime: "codex",
|
|
735
|
-
packageName: pkg2 ?? null,
|
|
736
|
-
platform: env.platform,
|
|
737
|
-
arch: env.arch
|
|
738
|
-
};
|
|
739
|
-
if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
|
|
740
|
-
const sdkEntry = env.resolveSelf(CODEX_SDK);
|
|
741
|
-
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
742
|
-
const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
|
|
743
|
-
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
744
|
-
const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
|
|
745
|
-
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
746
|
-
const packageRoot = join2(dirname2(platformManifest), "vendor", triple);
|
|
747
|
-
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
748
|
-
const current = join2(packageRoot, "bin", exeName);
|
|
749
|
-
if (env.isFile(current) && env.isFile(join2(packageRoot, "codex-package.json"))) {
|
|
750
|
-
return { ...base, status: "present", path: current };
|
|
751
|
-
}
|
|
752
|
-
const legacy = join2(packageRoot, "codex", exeName);
|
|
753
|
-
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
754
|
-
return { ...base, status: "executable_missing", path: null };
|
|
902
|
+
function readCodexStatus(answer) {
|
|
903
|
+
if (answer.code === 0) return true;
|
|
904
|
+
if (/not logged in/i.test(answer.output)) return false;
|
|
905
|
+
if (/config/i.test(answer.output)) return "config";
|
|
906
|
+
return "unreadable";
|
|
755
907
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
platform: env.platform,
|
|
766
|
-
arch: env.arch
|
|
908
|
+
function runBounded(path, args, env) {
|
|
909
|
+
return new Promise((resolve2) => {
|
|
910
|
+
let settled = false;
|
|
911
|
+
let out = "";
|
|
912
|
+
const done = (result) => {
|
|
913
|
+
if (settled) return;
|
|
914
|
+
settled = true;
|
|
915
|
+
clearTimeout(timer);
|
|
916
|
+
resolve2(result);
|
|
767
917
|
};
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
var HARNESS_LABEL = {
|
|
771
|
-
"claude-code": "Claude Code",
|
|
772
|
-
codex: "Codex"
|
|
773
|
-
};
|
|
774
|
-
var HARNESS_INTENT_WORD = {
|
|
775
|
-
"claude-code": "connected",
|
|
776
|
-
codex: "enabled"
|
|
777
|
-
};
|
|
778
|
-
function missingNoun(status) {
|
|
779
|
-
return status === "executable_missing" ? "executable" : "platform package";
|
|
780
|
-
}
|
|
781
|
-
var INSTALL_SCRIPT_COMMAND = "curl -fsSL https://cabane.ai/install.sh | bash";
|
|
782
|
-
function companionInstall(execPath = process.execPath, home = homedir2()) {
|
|
783
|
-
const real = (path) => {
|
|
918
|
+
let child;
|
|
784
919
|
try {
|
|
785
|
-
|
|
920
|
+
child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
786
921
|
} catch {
|
|
787
|
-
|
|
922
|
+
resolve2({ code: null, output: "", error: "spawn" });
|
|
923
|
+
return;
|
|
788
924
|
}
|
|
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.`;
|
|
925
|
+
const capture = (chunk) => {
|
|
926
|
+
if (out.length < 8192) out += chunk.toString();
|
|
927
|
+
};
|
|
928
|
+
child.stdout?.on("data", capture);
|
|
929
|
+
child.stderr?.on("data", capture);
|
|
930
|
+
const timer = setTimeout(() => {
|
|
931
|
+
child.kill("SIGKILL");
|
|
932
|
+
done({ code: null, output: out, error: "timeout" });
|
|
933
|
+
}, CHECK_TIMEOUT_MS);
|
|
934
|
+
timer.unref?.();
|
|
935
|
+
child.once("error", () => done({ code: null, output: out, error: "spawn" }));
|
|
936
|
+
child.once("close", (code) => done({ code, output: out }));
|
|
937
|
+
});
|
|
807
938
|
}
|
|
808
|
-
function
|
|
809
|
-
|
|
939
|
+
function checkVersion(check) {
|
|
940
|
+
if (!check) return null;
|
|
941
|
+
return check.status === "signed_in" || check.status === "api_key" || check.status === "signed_out" ? check.version : null;
|
|
810
942
|
}
|
|
811
943
|
|
|
812
944
|
// src/manifest.ts
|
|
@@ -864,13 +996,15 @@ function deriveHarnessSnapshot(signals) {
|
|
|
864
996
|
}
|
|
865
997
|
function deriveClaudeCode(signals, manifestHas) {
|
|
866
998
|
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
999
|
+
const version = checkVersion(signals.claudeCheck);
|
|
867
1000
|
if (manifestHas) {
|
|
868
1001
|
return {
|
|
869
1002
|
...base,
|
|
870
1003
|
state: "exposed",
|
|
871
|
-
version
|
|
872
|
-
detail: "
|
|
873
|
-
enable: null
|
|
1004
|
+
version,
|
|
1005
|
+
detail: exposedDetail("claude-code", signals.claudeCheck),
|
|
1006
|
+
enable: null,
|
|
1007
|
+
signIn: signals.claudeCheck?.status ?? null
|
|
874
1008
|
};
|
|
875
1009
|
}
|
|
876
1010
|
if (signals.claudeCodeConnected) {
|
|
@@ -879,37 +1013,56 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
879
1013
|
state: "needs_attention",
|
|
880
1014
|
version: null,
|
|
881
1015
|
detail: harnessIssueNote("claude-code", signals.claudeBundled),
|
|
882
|
-
enable: null
|
|
1016
|
+
enable: null,
|
|
1017
|
+
signIn: signals.claudeCheck?.status ?? null
|
|
883
1018
|
};
|
|
884
1019
|
}
|
|
885
|
-
if (signals.
|
|
1020
|
+
if (signals.claudeCheck && isUsable(signals.claudeCheck)) {
|
|
886
1021
|
return {
|
|
887
1022
|
...base,
|
|
888
1023
|
state: "detected_not_exposed",
|
|
889
|
-
version
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
enable: "claude-code"
|
|
1024
|
+
version,
|
|
1025
|
+
detail: `${usableLine("claude-code", signals.claudeCheck)} Connect it to offer Claude Code from this device.`,
|
|
1026
|
+
enable: "claude-code",
|
|
1027
|
+
signIn: signals.claudeCheck?.status ?? null
|
|
894
1028
|
};
|
|
895
1029
|
}
|
|
896
1030
|
return {
|
|
897
1031
|
...base,
|
|
898
1032
|
state: "not_detected",
|
|
899
1033
|
version: null,
|
|
900
|
-
detail: "
|
|
901
|
-
enable: null
|
|
1034
|
+
detail: notUsableDetail("claude-code", signals.claudeCheck),
|
|
1035
|
+
enable: null,
|
|
1036
|
+
signIn: signals.claudeCheck?.status ?? null
|
|
902
1037
|
};
|
|
903
1038
|
}
|
|
1039
|
+
function exposedDetail(runtime, check) {
|
|
1040
|
+
const label = LABELS[runtime];
|
|
1041
|
+
const problem = check ? unusableReason(runtime, check) : null;
|
|
1042
|
+
if (problem) return problem;
|
|
1043
|
+
if (check?.status === "signed_out") {
|
|
1044
|
+
const login = runtime === "codex" ? "codex login" : "claude auth login";
|
|
1045
|
+
return `${label} is connected, but signed out on this computer. Run \`${login}\` to use it again.`;
|
|
1046
|
+
}
|
|
1047
|
+
if (check?.status === "api_key") return `${label} is connected with an API key configured.`;
|
|
1048
|
+
return `${label} is connected and exposed to Cabane.`;
|
|
1049
|
+
}
|
|
1050
|
+
function notUsableDetail(runtime, check) {
|
|
1051
|
+
const problem = check ? unusableReason(runtime, check) : null;
|
|
1052
|
+
if (problem) return problem;
|
|
1053
|
+
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.";
|
|
1054
|
+
}
|
|
904
1055
|
function deriveCodex(signals, manifestHas) {
|
|
905
1056
|
const base = { runtime: "codex", label: LABELS.codex };
|
|
1057
|
+
const version = checkVersion(signals.codexCheck);
|
|
906
1058
|
if (manifestHas) {
|
|
907
1059
|
return {
|
|
908
1060
|
...base,
|
|
909
1061
|
state: "exposed",
|
|
910
|
-
version
|
|
911
|
-
detail: "
|
|
912
|
-
enable: null
|
|
1062
|
+
version,
|
|
1063
|
+
detail: exposedDetail("codex", signals.codexCheck),
|
|
1064
|
+
enable: null,
|
|
1065
|
+
signIn: signals.codexCheck?.status ?? null
|
|
913
1066
|
};
|
|
914
1067
|
}
|
|
915
1068
|
if (signals.codexEnabled) {
|
|
@@ -918,25 +1071,27 @@ function deriveCodex(signals, manifestHas) {
|
|
|
918
1071
|
state: "needs_attention",
|
|
919
1072
|
version: null,
|
|
920
1073
|
detail: harnessIssueNote("codex", signals.codexBundled),
|
|
921
|
-
enable: null
|
|
1074
|
+
enable: null,
|
|
1075
|
+
signIn: signals.codexCheck?.status ?? null
|
|
922
1076
|
};
|
|
923
1077
|
}
|
|
924
|
-
if (signals.
|
|
1078
|
+
if (signals.codexCheck && isUsable(signals.codexCheck)) {
|
|
925
1079
|
return {
|
|
926
1080
|
...base,
|
|
927
1081
|
state: "detected_not_exposed",
|
|
928
|
-
version
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1082
|
+
version,
|
|
1083
|
+
detail: `${usableLine("codex", signals.codexCheck)} Turn it on to offer Codex from this device.`,
|
|
1084
|
+
enable: "codex",
|
|
1085
|
+
signIn: signals.codexCheck?.status ?? null
|
|
932
1086
|
};
|
|
933
1087
|
}
|
|
934
1088
|
return {
|
|
935
1089
|
...base,
|
|
936
1090
|
state: "not_detected",
|
|
937
1091
|
version: null,
|
|
938
|
-
detail: "
|
|
939
|
-
enable: null
|
|
1092
|
+
detail: notUsableDetail("codex", signals.codexCheck),
|
|
1093
|
+
enable: null,
|
|
1094
|
+
signIn: signals.codexCheck?.status ?? null
|
|
940
1095
|
};
|
|
941
1096
|
}
|
|
942
1097
|
function deriveOpencode(signals, manifestHas) {
|
|
@@ -948,7 +1103,8 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
948
1103
|
state: "exposed",
|
|
949
1104
|
version: signals.opencodeVersion,
|
|
950
1105
|
detail: "An opencode server is reachable and exposed to Cabane.",
|
|
951
|
-
enable: null
|
|
1106
|
+
enable: null,
|
|
1107
|
+
signIn: null
|
|
952
1108
|
};
|
|
953
1109
|
}
|
|
954
1110
|
return {
|
|
@@ -956,7 +1112,8 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
956
1112
|
state: "needs_attention",
|
|
957
1113
|
version: null,
|
|
958
1114
|
detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
|
|
959
|
-
enable: null
|
|
1115
|
+
enable: null,
|
|
1116
|
+
signIn: null
|
|
960
1117
|
};
|
|
961
1118
|
}
|
|
962
1119
|
if (signals.opencodeReachable && signals.opencodeDetectedUrl) {
|
|
@@ -965,7 +1122,8 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
965
1122
|
state: "detected_not_exposed",
|
|
966
1123
|
version: signals.opencodeVersion,
|
|
967
1124
|
detail: signals.opencodeDetectedUrl,
|
|
968
|
-
enable: "opencode"
|
|
1125
|
+
enable: "opencode",
|
|
1126
|
+
signIn: null
|
|
969
1127
|
};
|
|
970
1128
|
}
|
|
971
1129
|
return {
|
|
@@ -973,7 +1131,8 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
973
1131
|
state: "not_detected",
|
|
974
1132
|
version: null,
|
|
975
1133
|
detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
|
|
976
|
-
enable: "opencode"
|
|
1134
|
+
enable: "opencode",
|
|
1135
|
+
signIn: null
|
|
977
1136
|
};
|
|
978
1137
|
}
|
|
979
1138
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
@@ -992,31 +1151,20 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
|
|
|
992
1151
|
return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
|
|
993
1152
|
}
|
|
994
1153
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
995
|
-
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
996
1154
|
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
997
1155
|
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
998
1156
|
const configuredServerUrl = cfg.opencode?.serverUrl;
|
|
999
1157
|
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;
|
|
1158
|
+
const opencodeVersion = configuredServerUrl ? await withTimeout(probeOpencode(opencodeProbeUrl), null) : await probeDefaultOpencodeVersion(probeOpencode);
|
|
1007
1159
|
return {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// The user's opt-in, the manifest gate; the probe above only suggests.
|
|
1160
|
+
claudeCheck: deps.checks?.["claude-code"] ?? null,
|
|
1161
|
+
// The user's opt-in, the manifest gate; the check above only suggests.
|
|
1011
1162
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
1012
|
-
// The executable a Claude turn launches, resolved with every
|
|
1163
|
+
// The executable a Claude turn launches, resolved with every refresh so a
|
|
1013
1164
|
// binary that disappears is caught on the next beat, not at turn time.
|
|
1014
1165
|
claudeBundled: resolveBundled("claude-code").status,
|
|
1015
1166
|
codexBundled: resolveBundled("codex").status,
|
|
1016
|
-
|
|
1017
|
-
// exposes codex; its config flag is the manifest gate either way).
|
|
1018
|
-
codexOnPath: codexVersion !== null,
|
|
1019
|
-
codexVersion,
|
|
1167
|
+
codexCheck: deps.checks?.codex ?? null,
|
|
1020
1168
|
codexEnabled: isCodexEnabled(cfg),
|
|
1021
1169
|
opencodeConfigured: !!configuredServerUrl,
|
|
1022
1170
|
// A version came back ⟺ the serve answered its health endpoint.
|
|
@@ -1049,104 +1197,94 @@ function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
|
1049
1197
|
});
|
|
1050
1198
|
}
|
|
1051
1199
|
|
|
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
|
-
}
|
|
1200
|
+
// src/harness-copy.ts
|
|
1201
|
+
function claudeLoginCommand(path = process.env.PATH) {
|
|
1202
|
+
if (firstOnPath("claude", path)) return "claude auth login";
|
|
1203
|
+
return "~/.local/bin/claude auth login";
|
|
1079
1204
|
}
|
|
1080
|
-
function
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1205
|
+
function ctx(c = {}) {
|
|
1206
|
+
return {
|
|
1207
|
+
cmd: c.cmd ?? ((rest) => cabane(rest)),
|
|
1208
|
+
claudeLogin: c.claudeLogin ?? claudeLoginCommand()
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function loginCommand(runtime, c = {}) {
|
|
1212
|
+
return runtime === "codex" ? "codex login" : ctx(c).claudeLogin;
|
|
1213
|
+
}
|
|
1214
|
+
function usableLine(runtime, check) {
|
|
1084
1215
|
const label = HARNESS_LABELS[runtime];
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1216
|
+
if (!check) return `${label} is running on this computer.`;
|
|
1217
|
+
const version = "version" in check && check.version ? ` (${check.version})` : "";
|
|
1218
|
+
if (check.status === "api_key")
|
|
1219
|
+
return `${label} has an API key configured on this computer${version}.`;
|
|
1220
|
+
return `${label} is signed in on this computer${version}.`;
|
|
1087
1221
|
}
|
|
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
|
-
}
|
|
1222
|
+
function failureText(runtime, reason) {
|
|
1097
1223
|
const label = HARNESS_LABELS[runtime];
|
|
1098
|
-
|
|
1224
|
+
switch (reason) {
|
|
1225
|
+
case "timeout":
|
|
1226
|
+
return `Couldn't check whether ${label} is signed in: the check didn't answer in time. Try again.`;
|
|
1227
|
+
case "spawn":
|
|
1228
|
+
return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairCommands()[0]} , then try again.`;
|
|
1229
|
+
case "config":
|
|
1230
|
+
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.`;
|
|
1231
|
+
case "unreadable":
|
|
1232
|
+
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.`;
|
|
1233
|
+
}
|
|
1099
1234
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const reason = presenceReason(runtime, status);
|
|
1104
|
-
return runtime === "opencode" ? reason : `${reason} ${action}`;
|
|
1235
|
+
function bundleMissingText(runtime) {
|
|
1236
|
+
const label = HARNESS_LABELS[runtime];
|
|
1237
|
+
return `This Cabane install is missing its bundled ${label}. Repair it with ${repairCommands()[0]} , then try again.`;
|
|
1105
1238
|
}
|
|
1106
|
-
function
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1239
|
+
function unusableReason(runtime, check) {
|
|
1240
|
+
if (check.status === "bundle_missing") return bundleMissingText(runtime);
|
|
1241
|
+
if (check.status === "check_failed") return failureText(runtime, check.reason);
|
|
1242
|
+
return null;
|
|
1110
1243
|
}
|
|
1111
|
-
function
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
};
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1244
|
+
function connectLines(outcome, opts = {}) {
|
|
1245
|
+
const c = ctx(opts);
|
|
1246
|
+
if (outcome.kind === "opencode") return { ok: false, lines: [outcome.message] };
|
|
1247
|
+
const label = HARNESS_LABELS[outcome.runtime];
|
|
1248
|
+
if (outcome.kind === "connected") return { ok: true, lines: [`${label} connected.`] };
|
|
1249
|
+
if (outcome.kind === "already") {
|
|
1250
|
+
const where = outcome.deviceLabel ?? "this computer";
|
|
1251
|
+
const how = outcome.check?.status === "api_key" ? ", with an API key configured" : outcome.check?.status === "signed_in" ? ", and signed in" : "";
|
|
1252
|
+
return { ok: true, lines: [`${label} is already connected on ${where}${how}.`] };
|
|
1253
|
+
}
|
|
1254
|
+
const { runtime, check, connected } = outcome;
|
|
1255
|
+
const problem = unusableReason(runtime, check);
|
|
1256
|
+
if (problem) return { ok: false, lines: [problem] };
|
|
1257
|
+
if (opts.windowKey) {
|
|
1258
|
+
return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
lines: [
|
|
1261
|
+
`${label} has an API key in this window, but the running Cabane can't see it.`,
|
|
1262
|
+
`Run ${c.cmd("stop")} then ${c.cmd("start")} here, and it will.`
|
|
1263
|
+
]
|
|
1130
1264
|
};
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
child.once("exit", (code) => done({ code, output: out }));
|
|
1140
|
-
});
|
|
1265
|
+
}
|
|
1266
|
+
const again = c.cmd(`connect ${runtime}`);
|
|
1267
|
+
return {
|
|
1268
|
+
ok: false,
|
|
1269
|
+
lines: [
|
|
1270
|
+
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.`
|
|
1271
|
+
]
|
|
1272
|
+
};
|
|
1141
1273
|
}
|
|
1142
1274
|
|
|
1143
1275
|
// src/logger.ts
|
|
1144
|
-
import {
|
|
1145
|
-
|
|
1276
|
+
import {
|
|
1277
|
+
createWriteStream,
|
|
1278
|
+
mkdirSync as mkdirSync2,
|
|
1279
|
+
openSync,
|
|
1280
|
+
renameSync as renameSync2,
|
|
1281
|
+
statSync as statSync2
|
|
1282
|
+
} from "fs";
|
|
1283
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1146
1284
|
import { stripVTControlCharacters } from "util";
|
|
1147
1285
|
import pino from "pino";
|
|
1148
1286
|
function companionLogPath() {
|
|
1149
|
-
return
|
|
1287
|
+
return join5(cabaneDir(), "companion.log");
|
|
1150
1288
|
}
|
|
1151
1289
|
function humanText(value) {
|
|
1152
1290
|
return stripVTControlCharacters(String(value ?? "")).replace(
|
|
@@ -1196,6 +1334,53 @@ function formatHumanLine(record) {
|
|
|
1196
1334
|
}
|
|
1197
1335
|
return line + "\n";
|
|
1198
1336
|
}
|
|
1337
|
+
var LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
1338
|
+
var LOG_ROTATIONS_KEPT = 3;
|
|
1339
|
+
var RotatingLog = class {
|
|
1340
|
+
constructor(path, maxBytes = LOG_ROTATE_BYTES, keep = LOG_ROTATIONS_KEPT) {
|
|
1341
|
+
this.path = path;
|
|
1342
|
+
this.maxBytes = maxBytes;
|
|
1343
|
+
this.keep = keep;
|
|
1344
|
+
this.size = sizeOf(path);
|
|
1345
|
+
this.stream = openAppend(path);
|
|
1346
|
+
}
|
|
1347
|
+
path;
|
|
1348
|
+
maxBytes;
|
|
1349
|
+
keep;
|
|
1350
|
+
stream;
|
|
1351
|
+
size;
|
|
1352
|
+
write(chunk) {
|
|
1353
|
+
const ok = this.stream.write(chunk);
|
|
1354
|
+
this.size += Buffer.byteLength(chunk);
|
|
1355
|
+
if (this.size >= this.maxBytes) this.rotate();
|
|
1356
|
+
return ok;
|
|
1357
|
+
}
|
|
1358
|
+
rotate() {
|
|
1359
|
+
this.stream.end();
|
|
1360
|
+
try {
|
|
1361
|
+
for (let i = this.keep - 1; i >= 1; i--) {
|
|
1362
|
+
try {
|
|
1363
|
+
renameSync2(`${this.path}.${i}`, `${this.path}.${i + 1}`);
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
renameSync2(this.path, `${this.path}.1`);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
this.stream = openAppend(this.path);
|
|
1371
|
+
this.size = sizeOf(this.path);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
function openAppend(path) {
|
|
1375
|
+
return createWriteStream("", { fd: openSync(path, "a") });
|
|
1376
|
+
}
|
|
1377
|
+
function sizeOf(path) {
|
|
1378
|
+
try {
|
|
1379
|
+
return statSync2(path).size;
|
|
1380
|
+
} catch {
|
|
1381
|
+
return 0;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1199
1384
|
var cached = null;
|
|
1200
1385
|
var consoleLogging = true;
|
|
1201
1386
|
var overrides = {};
|
|
@@ -1208,7 +1393,7 @@ function configureLogger(log, config) {
|
|
|
1208
1393
|
function createLogger(destinations = {}, options = {}) {
|
|
1209
1394
|
const path = companionLogPath();
|
|
1210
1395
|
if (!destinations.file) mkdirSync2(dirname3(path), { recursive: true });
|
|
1211
|
-
const file = destinations.file ??
|
|
1396
|
+
const file = destinations.file ?? new RotatingLog(path);
|
|
1212
1397
|
const state = { format: options.logFormat ?? "human" };
|
|
1213
1398
|
const streams = [];
|
|
1214
1399
|
const human = (chunk) => {
|
|
@@ -1499,14 +1684,14 @@ var CONTENT_TYPES = {
|
|
|
1499
1684
|
function registerRoutes(app, deps) {
|
|
1500
1685
|
const { supervisor, hub, staticDir } = deps;
|
|
1501
1686
|
app.get("/", async (c) => {
|
|
1502
|
-
const html = await readFile(
|
|
1687
|
+
const html = await readFile(join6(staticDir, "index.html"), "utf8");
|
|
1503
1688
|
return c.html(html);
|
|
1504
1689
|
});
|
|
1505
1690
|
app.get("/static/:file", async (c) => {
|
|
1506
1691
|
const file = c.req.param("file");
|
|
1507
|
-
const
|
|
1508
|
-
const full =
|
|
1509
|
-
if (!full.startsWith(staticDir) || !
|
|
1692
|
+
const safe2 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
1693
|
+
const full = join6(staticDir, safe2);
|
|
1694
|
+
if (!full.startsWith(staticDir) || !existsSync3(full)) return c.notFound();
|
|
1510
1695
|
const body = await readFile(full);
|
|
1511
1696
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
1512
1697
|
c.header("content-type", type);
|
|
@@ -1559,20 +1744,9 @@ function registerRoutes(app, deps) {
|
|
|
1559
1744
|
app.post("/api/harnesses/enable", async (c) => {
|
|
1560
1745
|
const body = await readJson(c);
|
|
1561
1746
|
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
|
-
}
|
|
1747
|
+
if (runtime === "claude-code" || runtime === "codex") {
|
|
1748
|
+
const result = await supervisor.enableHarness({ runtime });
|
|
1749
|
+
if (!result.ok) return c.json({ error: connectLines(result.outcome).lines.join(" ") }, 400);
|
|
1576
1750
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
1577
1751
|
}
|
|
1578
1752
|
if (runtime === "opencode") {
|
|
@@ -1583,7 +1757,7 @@ function registerRoutes(app, deps) {
|
|
|
1583
1757
|
runtime: "opencode",
|
|
1584
1758
|
serverUrl: body.serverUrl
|
|
1585
1759
|
});
|
|
1586
|
-
if (!result.ok) return c.json({ error: result.
|
|
1760
|
+
if (!result.ok) return c.json({ error: connectLines(result.outcome).lines.join(" ") }, 400);
|
|
1587
1761
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
1588
1762
|
}
|
|
1589
1763
|
return c.json({ error: `can't enable "${String(runtime)}" from the app` }, 400);
|
|
@@ -1639,11 +1813,11 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
1639
1813
|
return Math.min(Math.floor(n), max);
|
|
1640
1814
|
}
|
|
1641
1815
|
function tailFile(path, lines2) {
|
|
1642
|
-
if (!
|
|
1816
|
+
if (!existsSync3(path)) return [];
|
|
1643
1817
|
const MAX_BYTES = 256 * 1024;
|
|
1644
1818
|
let fd;
|
|
1645
1819
|
try {
|
|
1646
|
-
fd =
|
|
1820
|
+
fd = openSync2(path, "r");
|
|
1647
1821
|
const size = fstatSync(fd).size;
|
|
1648
1822
|
const start = Math.max(0, size - MAX_BYTES);
|
|
1649
1823
|
const len = size - start;
|
|
@@ -1728,14 +1902,14 @@ function isAddrInUse(err) {
|
|
|
1728
1902
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1729
1903
|
}
|
|
1730
1904
|
function resolveStaticDir() {
|
|
1731
|
-
return
|
|
1905
|
+
return join7(dirname4(fileURLToPath3(import.meta.url)), "static");
|
|
1732
1906
|
}
|
|
1733
1907
|
|
|
1734
1908
|
// src/control-socket.ts
|
|
1735
1909
|
import { createHash } from "crypto";
|
|
1736
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
1910
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
1737
1911
|
import { createServer, connect } from "net";
|
|
1738
|
-
import { join as
|
|
1912
|
+
import { join as join8 } from "path";
|
|
1739
1913
|
var CONTROL_TIMEOUT_MS = 1e3;
|
|
1740
1914
|
var MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
1741
1915
|
function controlSocketPath() {
|
|
@@ -1744,16 +1918,16 @@ function controlSocketPath() {
|
|
|
1744
1918
|
if (process.platform === "win32") {
|
|
1745
1919
|
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
1746
1920
|
}
|
|
1747
|
-
const besideRuntime =
|
|
1921
|
+
const besideRuntime = join8(dir2, "companion.sock");
|
|
1748
1922
|
if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
|
|
1749
1923
|
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 :
|
|
1924
|
+
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync4(process.env.XDG_RUNTIME_DIR) ? join8(process.env.XDG_RUNTIME_DIR, filename) : null;
|
|
1925
|
+
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join8("/tmp", filename);
|
|
1752
1926
|
}
|
|
1753
1927
|
async function startControlServer(handlers) {
|
|
1754
1928
|
const path = controlSocketPath();
|
|
1755
1929
|
mkdirSync3(cabaneDir(), { recursive: true });
|
|
1756
|
-
if (process.platform !== "win32" &&
|
|
1930
|
+
if (process.platform !== "win32" && existsSync4(path)) {
|
|
1757
1931
|
const alive = await ping(path);
|
|
1758
1932
|
if (alive) throw new Error(`another companion is already listening on ${path}`);
|
|
1759
1933
|
rmSync2(path, { force: true });
|
|
@@ -1813,10 +1987,16 @@ async function serveConnection(socket, handlers) {
|
|
|
1813
1987
|
return;
|
|
1814
1988
|
}
|
|
1815
1989
|
if (req.cmd === "connect") {
|
|
1816
|
-
const
|
|
1990
|
+
const outcome = await handlers.connect(req.runtime, req.serverUrl);
|
|
1991
|
+
const { ok, lines: lines2 } = connectLines(outcome, { cmd: (rest) => `cabane ${rest}` });
|
|
1992
|
+
const result = { ok, message: lines2.join(" "), outcome };
|
|
1817
1993
|
reply(socket, result);
|
|
1818
1994
|
return;
|
|
1819
1995
|
}
|
|
1996
|
+
if (req.cmd === "update") {
|
|
1997
|
+
reply(socket, await handlers.update(req.auto));
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1820
2000
|
if (req.cmd === "stop") {
|
|
1821
2001
|
reply(socket, { ok: true });
|
|
1822
2002
|
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
@@ -1903,88 +2083,49 @@ async function ping(path) {
|
|
|
1903
2083
|
}
|
|
1904
2084
|
|
|
1905
2085
|
// 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 = {}) {
|
|
2086
|
+
function requireStartConfig(deps = {}) {
|
|
1913
2087
|
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
1914
|
-
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1915
2088
|
const save2 = deps.save ?? saveConfig;
|
|
1916
2089
|
const cfg = requireCfg();
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
2090
|
+
const migrated = migrateConnectedHarnesses(cfg);
|
|
2091
|
+
if (!migrated) return cfg;
|
|
1920
2092
|
save2(migrated);
|
|
1921
|
-
deps.onMigrated?.(migrated
|
|
1922
|
-
return
|
|
2093
|
+
deps.onMigrated?.(migrated);
|
|
2094
|
+
return migrated;
|
|
1923
2095
|
}
|
|
1924
|
-
|
|
1925
|
-
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1926
|
-
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
2096
|
+
function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
1927
2097
|
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
1928
2098
|
const warn = deps.warn ?? ((message2) => process.stderr.write(`${message2}
|
|
1929
2099
|
`));
|
|
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
|
-
}
|
|
2100
|
+
const connected = isClaudeCodeConnected(cfg) || isCodexEnabled(cfg) || Boolean(cfg.opencode?.serverUrl);
|
|
2101
|
+
if (!connected) {
|
|
2102
|
+
warn(
|
|
2103
|
+
`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).`
|
|
2104
|
+
);
|
|
1942
2105
|
return;
|
|
1943
2106
|
}
|
|
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}`);
|
|
2107
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
2108
|
+
const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
|
|
2109
|
+
if (!intended) continue;
|
|
2110
|
+
const resolution = resolveBundled(runtime);
|
|
2111
|
+
if (resolution.status !== "present") warn(startupWarning(resolution));
|
|
2112
|
+
}
|
|
1972
2113
|
}
|
|
1973
2114
|
|
|
1974
2115
|
// src/runtime-file.ts
|
|
1975
2116
|
import {
|
|
1976
|
-
existsSync as
|
|
2117
|
+
existsSync as existsSync5,
|
|
1977
2118
|
readFileSync as readFileSync2,
|
|
1978
2119
|
rmSync as rmSync3,
|
|
1979
2120
|
writeFileSync as writeFileSync2,
|
|
1980
2121
|
mkdirSync as mkdirSync4,
|
|
1981
|
-
openSync as
|
|
2122
|
+
openSync as openSync3,
|
|
1982
2123
|
closeSync as closeSync2
|
|
1983
2124
|
} from "fs";
|
|
1984
|
-
import { join as
|
|
2125
|
+
import { join as join9 } from "path";
|
|
1985
2126
|
var PROBE_TIMEOUT_MS2 = 1e3;
|
|
1986
2127
|
function runtimePath() {
|
|
1987
|
-
return
|
|
2128
|
+
return join9(cabaneDir(), "runtime.json");
|
|
1988
2129
|
}
|
|
1989
2130
|
function serialize(state) {
|
|
1990
2131
|
return JSON.stringify(state, null, 2) + "\n";
|
|
@@ -2000,7 +2141,7 @@ function acquireRuntimeState(state) {
|
|
|
2000
2141
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
2001
2142
|
let fd;
|
|
2002
2143
|
try {
|
|
2003
|
-
fd =
|
|
2144
|
+
fd = openSync3(runtimePath(), "wx");
|
|
2004
2145
|
} catch {
|
|
2005
2146
|
return { acquired: false, existing: readLiveRuntimeState() };
|
|
2006
2147
|
}
|
|
@@ -2013,11 +2154,11 @@ function acquireRuntimeState(state) {
|
|
|
2013
2154
|
}
|
|
2014
2155
|
function clearRuntimeState() {
|
|
2015
2156
|
const path = runtimePath();
|
|
2016
|
-
if (
|
|
2157
|
+
if (existsSync5(path)) rmSync3(path, { force: true });
|
|
2017
2158
|
}
|
|
2018
2159
|
function clearRuntimeStateIfOurs(instanceId) {
|
|
2019
2160
|
const path = runtimePath();
|
|
2020
|
-
if (!
|
|
2161
|
+
if (!existsSync5(path)) return;
|
|
2021
2162
|
try {
|
|
2022
2163
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
2023
2164
|
if (parsed.instanceId && parsed.instanceId !== instanceId) return;
|
|
@@ -2027,7 +2168,7 @@ function clearRuntimeStateIfOurs(instanceId) {
|
|
|
2027
2168
|
}
|
|
2028
2169
|
function readLiveRuntimeState() {
|
|
2029
2170
|
const path = runtimePath();
|
|
2030
|
-
if (!
|
|
2171
|
+
if (!existsSync5(path)) return null;
|
|
2031
2172
|
let parsed;
|
|
2032
2173
|
try {
|
|
2033
2174
|
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -2192,7 +2333,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
2192
2333
|
return "Claude Code sign-in has expired on this machine. Open Claude Code, run /login, then reply in Cabane to try again.";
|
|
2193
2334
|
if (runtime === "codex")
|
|
2194
2335
|
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
|
|
2336
|
+
return "Sign-in has expired on this machine. Sign in to the agent, then reply in Cabane to try again.";
|
|
2196
2337
|
}
|
|
2197
2338
|
if (failure?.kind === "usage_capped")
|
|
2198
2339
|
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 +2344,9 @@ function turnFailureCopy(reason, runtime) {
|
|
|
2203
2344
|
if (reason?.startsWith("missing_secret:"))
|
|
2204
2345
|
return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
|
|
2205
2346
|
if (reason?.startsWith("runtime_incomplete:"))
|
|
2206
|
-
return "The
|
|
2347
|
+
return "The agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
|
|
2207
2348
|
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
2208
|
-
return "The
|
|
2349
|
+
return "The agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
|
|
2209
2350
|
if (reason?.startsWith("model_unavailable:"))
|
|
2210
2351
|
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
2211
2352
|
if (reason?.startsWith("prepare_failed:"))
|
|
@@ -2213,17 +2354,17 @@ function turnFailureCopy(reason, runtime) {
|
|
|
2213
2354
|
if (reason?.startsWith("fetch_failed:"))
|
|
2214
2355
|
return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
|
|
2215
2356
|
if (reason === "timeout_idle")
|
|
2216
|
-
return "The
|
|
2357
|
+
return "The agent stopped responding. Reply in Cabane to try again.";
|
|
2217
2358
|
if (reason === "timeout_total")
|
|
2218
2359
|
return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
|
|
2219
2360
|
if (reason === "result_error:error_max_turns")
|
|
2220
|
-
return "The
|
|
2361
|
+
return "The agent reached its step limit. Reply in Cabane to continue.";
|
|
2221
2362
|
if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
|
|
2222
|
-
return "The
|
|
2363
|
+
return "The agent finished without a response. Reply in Cabane to try again.";
|
|
2223
2364
|
if (reason === "session_start_failed")
|
|
2224
|
-
return "The
|
|
2365
|
+
return "The agent could not start a session. Restart the companion, then reply in Cabane to try again.";
|
|
2225
2366
|
if (reason === "no_terminal")
|
|
2226
|
-
return "The
|
|
2367
|
+
return "The agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
|
|
2227
2368
|
if (reason === "lease_unconfirmed")
|
|
2228
2369
|
return "Could not confirm with Cabane that the turn could start. Check this machine\u2019s connection, then reply in Cabane to try again.";
|
|
2229
2370
|
if (reason === "workspace_tools_missing")
|
|
@@ -2231,13 +2372,13 @@ function turnFailureCopy(reason, runtime) {
|
|
|
2231
2372
|
if (reason?.startsWith("seq_floor_unavailable:"))
|
|
2232
2373
|
return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
|
|
2233
2374
|
if (reason === "result_error:error_max_budget_usd")
|
|
2234
|
-
return "The
|
|
2375
|
+
return "The agent reached its spending limit. Check its budget settings before trying again in Cabane.";
|
|
2235
2376
|
if (reason === "result_error:error_max_structured_output_retries")
|
|
2236
|
-
return "The
|
|
2377
|
+
return "The agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
|
|
2237
2378
|
if (reason?.startsWith("result_error:"))
|
|
2238
|
-
return "The
|
|
2379
|
+
return "The agent stopped with an error while running. Check its transcript, then reply in Cabane to try again.";
|
|
2239
2380
|
if (reason === "setup_failed")
|
|
2240
|
-
return "The turn could not be set up. Check the
|
|
2381
|
+
return "The turn could not be set up. Check the agent setup on this machine, then reply in Cabane to try again.";
|
|
2241
2382
|
if (reason === "unexpected_role")
|
|
2242
2383
|
return "The message could not start an agent response. Send a new message in Cabane to try again.";
|
|
2243
2384
|
if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
|
|
@@ -2245,11 +2386,11 @@ function turnFailureCopy(reason, runtime) {
|
|
|
2245
2386
|
const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
|
|
2246
2387
|
if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
|
|
2247
2388
|
const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
|
|
2248
|
-
return `The
|
|
2389
|
+
return `The agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
|
|
2249
2390
|
}
|
|
2250
2391
|
if (detail)
|
|
2251
|
-
return `The
|
|
2252
|
-
return "The
|
|
2392
|
+
return `The agent reported: ${detail}. Check the agent setup on this machine, then reply in Cabane to try again.`;
|
|
2393
|
+
return "The agent could not finish this turn. Reply in Cabane to try again.";
|
|
2253
2394
|
}
|
|
2254
2395
|
|
|
2255
2396
|
// src/api-error-shape.ts
|
|
@@ -2883,22 +3024,22 @@ function errorMessage2(status, body) {
|
|
|
2883
3024
|
// src/credentials.ts
|
|
2884
3025
|
import {
|
|
2885
3026
|
chmodSync as chmodSync3,
|
|
2886
|
-
existsSync as
|
|
3027
|
+
existsSync as existsSync6,
|
|
2887
3028
|
mkdirSync as mkdirSync5,
|
|
2888
3029
|
readFileSync as readFileSync3,
|
|
2889
|
-
renameSync as
|
|
3030
|
+
renameSync as renameSync3,
|
|
2890
3031
|
rmSync as rmSync4,
|
|
2891
3032
|
writeFileSync as writeFileSync3
|
|
2892
3033
|
} from "fs";
|
|
2893
|
-
import { dirname as dirname5, join as
|
|
3034
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2894
3035
|
import { z as z4 } from "zod";
|
|
2895
3036
|
function credentialsPath() {
|
|
2896
|
-
return
|
|
3037
|
+
return join10(cabaneDir(), "credentials.json");
|
|
2897
3038
|
}
|
|
2898
3039
|
var credentialStoreSchema = z4.record(z4.string(), z4.string());
|
|
2899
3040
|
function load() {
|
|
2900
3041
|
const path = credentialsPath();
|
|
2901
|
-
if (!
|
|
3042
|
+
if (!existsSync6(path)) return {};
|
|
2902
3043
|
let raw;
|
|
2903
3044
|
try {
|
|
2904
3045
|
raw = readFileSync3(path, "utf8");
|
|
@@ -2927,7 +3068,7 @@ function save(map) {
|
|
|
2927
3068
|
chmodSync3(tmp, 384);
|
|
2928
3069
|
} catch {
|
|
2929
3070
|
}
|
|
2930
|
-
|
|
3071
|
+
renameSync3(tmp, path);
|
|
2931
3072
|
} catch (err) {
|
|
2932
3073
|
try {
|
|
2933
3074
|
rmSync4(tmp, { force: true });
|
|
@@ -2960,18 +3101,18 @@ function pruneCredentials(keepAgentIds) {
|
|
|
2960
3101
|
}
|
|
2961
3102
|
|
|
2962
3103
|
// src/dispatch-dedupe.ts
|
|
2963
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as
|
|
2964
|
-
import { join as
|
|
3104
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
3105
|
+
import { join as join11 } from "path";
|
|
2965
3106
|
var MAX_IDS = 256;
|
|
2966
3107
|
function dir(log) {
|
|
2967
|
-
return
|
|
3108
|
+
return join11(cabaneDir(), log);
|
|
2968
3109
|
}
|
|
2969
3110
|
function pathFor(log, workspaceId) {
|
|
2970
|
-
return
|
|
3111
|
+
return join11(dir(log), encodeURIComponent(workspaceId));
|
|
2971
3112
|
}
|
|
2972
3113
|
function readIds(log, workspaceId) {
|
|
2973
3114
|
const path = pathFor(log, workspaceId);
|
|
2974
|
-
if (!
|
|
3115
|
+
if (!existsSync7(path)) return [];
|
|
2975
3116
|
try {
|
|
2976
3117
|
return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2977
3118
|
} catch {
|
|
@@ -3004,15 +3145,15 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3004
3145
|
}
|
|
3005
3146
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3006
3147
|
function resumeDir() {
|
|
3007
|
-
return
|
|
3148
|
+
return join11(cabaneDir(), "resume-attempts");
|
|
3008
3149
|
}
|
|
3009
3150
|
function resumePathFor(workspaceId) {
|
|
3010
|
-
return
|
|
3151
|
+
return join11(resumeDir(), encodeURIComponent(workspaceId));
|
|
3011
3152
|
}
|
|
3012
3153
|
function readResumeCounts(workspaceId) {
|
|
3013
3154
|
const out = /* @__PURE__ */ new Map();
|
|
3014
3155
|
const path = resumePathFor(workspaceId);
|
|
3015
|
-
if (!
|
|
3156
|
+
if (!existsSync7(path)) return out;
|
|
3016
3157
|
try {
|
|
3017
3158
|
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3018
3159
|
const trimmed = line.trim();
|
|
@@ -3043,16 +3184,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3043
3184
|
return next;
|
|
3044
3185
|
}
|
|
3045
3186
|
function turnDir() {
|
|
3046
|
-
return
|
|
3187
|
+
return join11(cabaneDir(), "turns");
|
|
3047
3188
|
}
|
|
3048
3189
|
function turnPathFor(workspaceId) {
|
|
3049
|
-
return
|
|
3190
|
+
return join11(turnDir(), encodeURIComponent(workspaceId));
|
|
3050
3191
|
}
|
|
3051
3192
|
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
3193
|
function readTurnIds(workspaceId) {
|
|
3053
3194
|
const out = /* @__PURE__ */ new Map();
|
|
3054
3195
|
const path = turnPathFor(workspaceId);
|
|
3055
|
-
if (!
|
|
3196
|
+
if (!existsSync7(path)) return out;
|
|
3056
3197
|
try {
|
|
3057
3198
|
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3058
3199
|
const trimmed = line.trim();
|
|
@@ -3078,7 +3219,7 @@ function writeTurnIds(workspaceId, turns) {
|
|
|
3078
3219
|
const path = turnPathFor(workspaceId);
|
|
3079
3220
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3080
3221
|
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3081
|
-
|
|
3222
|
+
renameSync4(tmp, path);
|
|
3082
3223
|
return entries.length;
|
|
3083
3224
|
}
|
|
3084
3225
|
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
@@ -3964,8 +4105,8 @@ function readThinking(runtimeOptions) {
|
|
|
3964
4105
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
|
|
3965
4106
|
function parseClaudeCodeModel(model) {
|
|
3966
4107
|
if (!model) return null;
|
|
3967
|
-
const
|
|
3968
|
-
const bare =
|
|
4108
|
+
const sep5 = model.indexOf("/");
|
|
4109
|
+
const bare = sep5 === -1 || model.slice(0, sep5) !== "anthropic" ? model : model.slice(sep5 + 1);
|
|
3969
4110
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
3970
4111
|
}
|
|
3971
4112
|
|
|
@@ -4148,7 +4289,7 @@ function dedupe(xs) {
|
|
|
4148
4289
|
}
|
|
4149
4290
|
|
|
4150
4291
|
// packages/agent-runtime/src/claude-code/loop.ts
|
|
4151
|
-
async function* decodeSdkStream(iter,
|
|
4292
|
+
async function* decodeSdkStream(iter, ctx2) {
|
|
4152
4293
|
const pending = /* @__PURE__ */ new Map();
|
|
4153
4294
|
const buffer = createTerminalTextBuffer();
|
|
4154
4295
|
const out = [];
|
|
@@ -4169,7 +4310,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4169
4310
|
let preInitNotice = null;
|
|
4170
4311
|
try {
|
|
4171
4312
|
for await (const msg of iter) {
|
|
4172
|
-
if (
|
|
4313
|
+
if (ctx2.signal.aborted) return;
|
|
4173
4314
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
4174
4315
|
const initModel = msg.model;
|
|
4175
4316
|
if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
|
|
@@ -4180,19 +4321,19 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4180
4321
|
break;
|
|
4181
4322
|
}
|
|
4182
4323
|
const sdkSessionId = msg.session_id;
|
|
4183
|
-
if (!sessionEmitted && sdkSessionId && sdkSessionId !==
|
|
4324
|
+
if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx2.resumedSessionId) {
|
|
4184
4325
|
sessionEmitted = true;
|
|
4185
4326
|
yield {
|
|
4186
4327
|
type: "session",
|
|
4187
|
-
state: encodeSession({ sdkSessionId, cwd:
|
|
4188
|
-
...
|
|
4328
|
+
state: encodeSession({ sdkSessionId, cwd: ctx2.cwd ?? "" }),
|
|
4329
|
+
...ctx2.degraded ? { degraded: true } : {}
|
|
4189
4330
|
};
|
|
4190
4331
|
}
|
|
4191
4332
|
} else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
4192
4333
|
const compaction = readCompaction(msg);
|
|
4193
4334
|
if (compaction) compactions.push(compaction);
|
|
4194
4335
|
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
4195
|
-
if (preInitToolActivity(msg) &&
|
|
4336
|
+
if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
|
|
4196
4337
|
const failure = classifyPreInitFrame(msg, signals);
|
|
4197
4338
|
resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
4198
4339
|
if (msg.type === "assistant" && isHarnessAuthored(msg))
|
|
@@ -4236,7 +4377,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4236
4377
|
const window = readContextWindow(msg, resolvedModel);
|
|
4237
4378
|
if (window !== void 0) usage.contextWindow = window;
|
|
4238
4379
|
if (compactions.length > 0) usage.contextCompactions = compactions;
|
|
4239
|
-
const reading = await readHarnessContext(
|
|
4380
|
+
const reading = await readHarnessContext(ctx2.readContextUsage);
|
|
4240
4381
|
if (reading) Object.assign(usage, reading);
|
|
4241
4382
|
}
|
|
4242
4383
|
const isError = msg.is_error === true;
|
|
@@ -4252,20 +4393,20 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4252
4393
|
}
|
|
4253
4394
|
}
|
|
4254
4395
|
} catch (err) {
|
|
4255
|
-
if (
|
|
4396
|
+
if (ctx2.signal.aborted) throw err;
|
|
4256
4397
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
4257
4398
|
const failure = mcpInventory.initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
|
|
4258
|
-
if (!failure && (mcpInventory.initReceived ||
|
|
4399
|
+
if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
|
|
4259
4400
|
ok = false;
|
|
4260
4401
|
if (failure) {
|
|
4261
4402
|
resultReason = encodeFailureReason(failure);
|
|
4262
4403
|
} else {
|
|
4263
4404
|
resultReason = "session_start_failed";
|
|
4264
|
-
if (
|
|
4405
|
+
if (ctx2.startEvidence) ctx2.startEvidence.thrown = message2.slice(0, 200);
|
|
4265
4406
|
}
|
|
4266
4407
|
sawResult = true;
|
|
4267
4408
|
}
|
|
4268
|
-
if (
|
|
4409
|
+
if (ctx2.signal.aborted) return;
|
|
4269
4410
|
if (!workspaceProven) {
|
|
4270
4411
|
ok = false;
|
|
4271
4412
|
if (resultReason === void 0) {
|
|
@@ -5273,7 +5414,7 @@ function deepestError(node) {
|
|
|
5273
5414
|
}
|
|
5274
5415
|
|
|
5275
5416
|
// packages/agent-runtime/src/opencode/loop.ts
|
|
5276
|
-
async function* decodeOpencodeStream(events,
|
|
5417
|
+
async function* decodeOpencodeStream(events, ctx2) {
|
|
5277
5418
|
let held = null;
|
|
5278
5419
|
const pending = /* @__PURE__ */ new Map();
|
|
5279
5420
|
const startedTools = /* @__PURE__ */ new Set();
|
|
@@ -5282,9 +5423,9 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
5282
5423
|
let reason;
|
|
5283
5424
|
let settled = false;
|
|
5284
5425
|
const userMessageIds = /* @__PURE__ */ new Set();
|
|
5285
|
-
if (
|
|
5426
|
+
if (ctx2.skipMessageId) userMessageIds.add(ctx2.skipMessageId);
|
|
5286
5427
|
for await (const ev of events) {
|
|
5287
|
-
if (
|
|
5428
|
+
if (ctx2.signal.aborted) return;
|
|
5288
5429
|
if (ev.type === "message.updated") {
|
|
5289
5430
|
const info = readMessageInfo(ev.properties);
|
|
5290
5431
|
if (info && info.role === "user") userMessageIds.add(info.id);
|
|
@@ -5337,7 +5478,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
5337
5478
|
}
|
|
5338
5479
|
} else if (ev.type === "session.idle") {
|
|
5339
5480
|
const sid = readSessionId(ev.properties);
|
|
5340
|
-
if (sid && sid !==
|
|
5481
|
+
if (sid && sid !== ctx2.sessionId) continue;
|
|
5341
5482
|
const sealed = sealHeld(held, true);
|
|
5342
5483
|
held = null;
|
|
5343
5484
|
if (sealed) yield sealed;
|
|
@@ -5346,7 +5487,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
5346
5487
|
break;
|
|
5347
5488
|
} else if (ev.type === "session.error") {
|
|
5348
5489
|
const sid = readSessionId(ev.properties);
|
|
5349
|
-
if (sid && sid !==
|
|
5490
|
+
if (sid && sid !== ctx2.sessionId) continue;
|
|
5350
5491
|
const sealed = sealHeld(held, false);
|
|
5351
5492
|
held = null;
|
|
5352
5493
|
if (sealed) yield sealed;
|
|
@@ -5362,7 +5503,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
5362
5503
|
break;
|
|
5363
5504
|
}
|
|
5364
5505
|
}
|
|
5365
|
-
if (
|
|
5506
|
+
if (ctx2.signal.aborted) return;
|
|
5366
5507
|
if (!settled) {
|
|
5367
5508
|
const sealed = sealHeld(held, true);
|
|
5368
5509
|
held = null;
|
|
@@ -5420,9 +5561,9 @@ function readOpencodeDialect(runtimeOptions) {
|
|
|
5420
5561
|
|
|
5421
5562
|
// packages/agent-runtime/src/opencode/model.ts
|
|
5422
5563
|
function parseOpencodeModel(model) {
|
|
5423
|
-
const
|
|
5424
|
-
if (
|
|
5425
|
-
return { providerID: model.slice(0,
|
|
5564
|
+
const sep5 = model.indexOf("/");
|
|
5565
|
+
if (sep5 === -1) return { providerID: "", modelID: model };
|
|
5566
|
+
return { providerID: model.slice(0, sep5), modelID: model.slice(sep5 + 1) };
|
|
5426
5567
|
}
|
|
5427
5568
|
|
|
5428
5569
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
@@ -5710,10 +5851,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
5710
5851
|
const { value, done } = await reader.read();
|
|
5711
5852
|
if (done) return;
|
|
5712
5853
|
buf += decoder.decode(value, { stream: true });
|
|
5713
|
-
let
|
|
5714
|
-
while ((
|
|
5715
|
-
const frame = buf.slice(0,
|
|
5716
|
-
buf = buf.slice(
|
|
5854
|
+
let sep5;
|
|
5855
|
+
while ((sep5 = buf.indexOf("\n\n")) !== -1) {
|
|
5856
|
+
const frame = buf.slice(0, sep5);
|
|
5857
|
+
buf = buf.slice(sep5 + 2);
|
|
5717
5858
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
5718
5859
|
if (!data) continue;
|
|
5719
5860
|
let ev;
|
|
@@ -6461,7 +6602,7 @@ function readUsageBaseline(raw) {
|
|
|
6461
6602
|
}
|
|
6462
6603
|
|
|
6463
6604
|
// packages/agent-runtime/src/codex/loop.ts
|
|
6464
|
-
async function* decodeCodexStream(events,
|
|
6605
|
+
async function* decodeCodexStream(events, ctx2) {
|
|
6465
6606
|
let held = null;
|
|
6466
6607
|
const pending = /* @__PURE__ */ new Map();
|
|
6467
6608
|
const startedTools = /* @__PURE__ */ new Set();
|
|
@@ -6471,7 +6612,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6471
6612
|
let reason;
|
|
6472
6613
|
let usage;
|
|
6473
6614
|
let cumulativeUsage;
|
|
6474
|
-
let threadIdForSession =
|
|
6615
|
+
let threadIdForSession = ctx2.resumedThreadId;
|
|
6475
6616
|
let settled = false;
|
|
6476
6617
|
function* flushInterim() {
|
|
6477
6618
|
const sealed = sealHeld2(held, false);
|
|
@@ -6479,26 +6620,26 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6479
6620
|
if (sealed) yield sealed;
|
|
6480
6621
|
}
|
|
6481
6622
|
for await (const ev of events) {
|
|
6482
|
-
if (
|
|
6623
|
+
if (ctx2.signal.aborted) return;
|
|
6483
6624
|
if (ev.type === "thread.started") {
|
|
6484
6625
|
const threadId = readThreadId(ev);
|
|
6485
6626
|
if (threadId) threadIdForSession = threadId;
|
|
6486
6627
|
if (threadId && !sessionEmitted) {
|
|
6487
6628
|
const state = encodeSession3({
|
|
6488
6629
|
threadId,
|
|
6489
|
-
cwd:
|
|
6490
|
-
promptFingerprint:
|
|
6630
|
+
cwd: ctx2.cwd ?? "",
|
|
6631
|
+
promptFingerprint: ctx2.promptFingerprint ?? null,
|
|
6491
6632
|
// The baseline this turn inherited, carried through unchanged. This early write records the
|
|
6492
6633
|
// thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
|
|
6493
6634
|
// closing write records the new baseline.
|
|
6494
|
-
usageBaseline:
|
|
6635
|
+
usageBaseline: ctx2.usageBaseline ?? null
|
|
6495
6636
|
});
|
|
6496
|
-
if (threadId !==
|
|
6637
|
+
if (threadId !== ctx2.resumedThreadId || state !== ctx2.storedState) {
|
|
6497
6638
|
sessionEmitted = true;
|
|
6498
6639
|
yield {
|
|
6499
6640
|
type: "session",
|
|
6500
6641
|
state,
|
|
6501
|
-
...
|
|
6642
|
+
...ctx2.degraded ? { degraded: true } : {}
|
|
6502
6643
|
};
|
|
6503
6644
|
}
|
|
6504
6645
|
}
|
|
@@ -6567,7 +6708,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6567
6708
|
held = null;
|
|
6568
6709
|
if (sealed) yield sealed;
|
|
6569
6710
|
cumulativeUsage = readUsage(ev);
|
|
6570
|
-
usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage,
|
|
6711
|
+
usage = cumulativeUsage ? codexUsageDelta(cumulativeUsage, ctx2.usageBaseline ?? null) : void 0;
|
|
6571
6712
|
ok = true;
|
|
6572
6713
|
settled = true;
|
|
6573
6714
|
break;
|
|
@@ -6586,7 +6727,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6586
6727
|
break;
|
|
6587
6728
|
}
|
|
6588
6729
|
}
|
|
6589
|
-
if (
|
|
6730
|
+
if (ctx2.signal.aborted) return;
|
|
6590
6731
|
if (!settled) {
|
|
6591
6732
|
const sealed = sealHeld2(held, true);
|
|
6592
6733
|
held = null;
|
|
@@ -6596,19 +6737,19 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6596
6737
|
if (ok && cumulativeUsage && threadIdForSession) {
|
|
6597
6738
|
const closing = encodeSession3({
|
|
6598
6739
|
threadId: threadIdForSession,
|
|
6599
|
-
cwd:
|
|
6600
|
-
promptFingerprint:
|
|
6740
|
+
cwd: ctx2.cwd ?? "",
|
|
6741
|
+
promptFingerprint: ctx2.promptFingerprint ?? null,
|
|
6601
6742
|
usageBaseline: cumulativeUsage
|
|
6602
6743
|
});
|
|
6603
6744
|
yield { type: "session", state: closing };
|
|
6604
6745
|
}
|
|
6605
|
-
const resolvedConfig =
|
|
6746
|
+
const resolvedConfig = ctx2.resolvedReasoningEffort ? { reasoningEffort: ctx2.resolvedReasoningEffort } : void 0;
|
|
6606
6747
|
yield {
|
|
6607
6748
|
type: "result",
|
|
6608
6749
|
ok,
|
|
6609
6750
|
...reason ? { reason } : {},
|
|
6610
6751
|
...usage ? { usage } : {},
|
|
6611
|
-
...
|
|
6752
|
+
...ctx2.resolvedModel ? { resolvedModel: ctx2.resolvedModel } : {},
|
|
6612
6753
|
...resolvedConfig ? { resolvedConfig } : {}
|
|
6613
6754
|
};
|
|
6614
6755
|
}
|
|
@@ -6653,8 +6794,8 @@ function readCodexDialect(runtimeOptions) {
|
|
|
6653
6794
|
// packages/agent-runtime/src/codex/model.ts
|
|
6654
6795
|
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
6655
6796
|
function parseCodexModel(model) {
|
|
6656
|
-
const
|
|
6657
|
-
const bare =
|
|
6797
|
+
const sep5 = model.indexOf("/");
|
|
6798
|
+
const bare = sep5 === -1 || model.slice(0, sep5) !== "openai" ? model : model.slice(sep5 + 1);
|
|
6658
6799
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
6659
6800
|
}
|
|
6660
6801
|
|
|
@@ -7934,6 +8075,7 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7934
8075
|
// src/connector-health.ts
|
|
7935
8076
|
var ConnectorHealthStore = class {
|
|
7936
8077
|
byRuntime = /* @__PURE__ */ new Map();
|
|
8078
|
+
sources = /* @__PURE__ */ new Map();
|
|
7937
8079
|
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
7938
8080
|
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
7939
8081
|
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
@@ -7942,6 +8084,7 @@ var ConnectorHealthStore = class {
|
|
|
7942
8084
|
recordSettle(runtime, outcome) {
|
|
7943
8085
|
if (outcome.ok) {
|
|
7944
8086
|
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
8087
|
+
this.sources.set(runtime, "turn");
|
|
7945
8088
|
return;
|
|
7946
8089
|
}
|
|
7947
8090
|
const failure = decodeFailureReason(outcome.errorReason);
|
|
@@ -7953,6 +8096,26 @@ var ConnectorHealthStore = class {
|
|
|
7953
8096
|
limitedUntil: quota.limitedUntil,
|
|
7954
8097
|
lastError: outcome.errorReason ?? null
|
|
7955
8098
|
});
|
|
8099
|
+
this.sources.set(runtime, "turn");
|
|
8100
|
+
}
|
|
8101
|
+
// Record what the companion's own sign-in check found for a connected
|
|
8102
|
+
// runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
|
|
8103
|
+
// before any turn fails. Signed out seeds `auth_expired` unless a turn has
|
|
8104
|
+
// already spoken; usable clears only a seed of our own.
|
|
8105
|
+
recordLocalCheck(runtime, usable) {
|
|
8106
|
+
const source = this.sources.get(runtime);
|
|
8107
|
+
if (!usable) {
|
|
8108
|
+
if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
|
|
8109
|
+
this.byRuntime.set(runtime, {
|
|
8110
|
+
quotaState: "auth_expired",
|
|
8111
|
+
limitedUntil: null,
|
|
8112
|
+
lastError: encodeFailureReason({ kind: "auth_expired" })
|
|
8113
|
+
});
|
|
8114
|
+
this.sources.set(runtime, "local");
|
|
8115
|
+
return;
|
|
8116
|
+
}
|
|
8117
|
+
if (source !== "local") return;
|
|
8118
|
+
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
7956
8119
|
}
|
|
7957
8120
|
// The store's current opinion for one runtime, or undefined when it has
|
|
7958
8121
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
@@ -7981,11 +8144,11 @@ var ConnectorHealthStore = class {
|
|
|
7981
8144
|
|
|
7982
8145
|
// src/dispatcher.ts
|
|
7983
8146
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
7984
|
-
import { existsSync as
|
|
7985
|
-
import { join as
|
|
8147
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
8148
|
+
import { join as join17 } from "path";
|
|
7986
8149
|
|
|
7987
8150
|
// src/runner/launch.ts
|
|
7988
|
-
import { createInterface } from "readline";
|
|
8151
|
+
import { createInterface as createInterface2 } from "readline";
|
|
7989
8152
|
|
|
7990
8153
|
// src/runner/sandbox-policy.ts
|
|
7991
8154
|
import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
|
|
@@ -8162,7 +8325,7 @@ async function stopRemoteOpencodeSession(serverUrl, sessionId, opts = {}) {
|
|
|
8162
8325
|
}
|
|
8163
8326
|
|
|
8164
8327
|
// src/turn-containment.ts
|
|
8165
|
-
import { execFile, execFileSync, spawn as
|
|
8328
|
+
import { execFile, execFileSync, spawn as spawn3 } from "child_process";
|
|
8166
8329
|
import { readFileSync as readFileSync5 } from "fs";
|
|
8167
8330
|
import { promisify } from "util";
|
|
8168
8331
|
var execFileAsync = promisify(execFile);
|
|
@@ -8171,8 +8334,8 @@ var CONTAINMENT_STOP_GRACE_SEC = 5;
|
|
|
8171
8334
|
var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
8172
8335
|
var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
8173
8336
|
function turnScopeUnit(turnId, attempt) {
|
|
8174
|
-
const
|
|
8175
|
-
return `cabane-turn-${
|
|
8337
|
+
const safe2 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
8338
|
+
return `cabane-turn-${safe2}-${attempt}`;
|
|
8176
8339
|
}
|
|
8177
8340
|
async function reapTurnScopes(turnId) {
|
|
8178
8341
|
if (process.platform !== "linux") return { failures: [] };
|
|
@@ -8285,7 +8448,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8285
8448
|
command: options.command
|
|
8286
8449
|
});
|
|
8287
8450
|
return asContained(
|
|
8288
|
-
|
|
8451
|
+
spawn3("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
|
|
8289
8452
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8290
8453
|
env: options.env,
|
|
8291
8454
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8293,7 +8456,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8293
8456
|
);
|
|
8294
8457
|
}
|
|
8295
8458
|
function spawnInGroup(options) {
|
|
8296
|
-
const child =
|
|
8459
|
+
const child = spawn3(options.command, options.args, {
|
|
8297
8460
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8298
8461
|
env: options.env,
|
|
8299
8462
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -8440,17 +8603,17 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8440
8603
|
}
|
|
8441
8604
|
|
|
8442
8605
|
// src/secrets.ts
|
|
8443
|
-
import { existsSync as
|
|
8444
|
-
import { join as
|
|
8606
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
8607
|
+
import { join as join12 } from "path";
|
|
8445
8608
|
import { z as z14 } from "zod";
|
|
8446
8609
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8447
8610
|
function secretsPath() {
|
|
8448
|
-
return
|
|
8611
|
+
return join12(cabaneDir(), "secrets.json");
|
|
8449
8612
|
}
|
|
8450
8613
|
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8451
8614
|
function loadSecretStore() {
|
|
8452
8615
|
const path = secretsPath();
|
|
8453
|
-
if (!
|
|
8616
|
+
if (!existsSync8(path)) return makeStore({});
|
|
8454
8617
|
let raw;
|
|
8455
8618
|
try {
|
|
8456
8619
|
raw = readFileSync6(path, "utf8");
|
|
@@ -8829,7 +8992,7 @@ function relayRunnerLog(line, log) {
|
|
|
8829
8992
|
log[method](fields, typeof msg === "string" ? msg : "");
|
|
8830
8993
|
}
|
|
8831
8994
|
function lines(stream, onLine) {
|
|
8832
|
-
const rl =
|
|
8995
|
+
const rl = createInterface2({ input: stream, crlfDelay: Infinity });
|
|
8833
8996
|
rl.on("line", onLine);
|
|
8834
8997
|
return new Promise((resolve2) => rl.once("close", resolve2));
|
|
8835
8998
|
}
|
|
@@ -9169,19 +9332,9 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
9169
9332
|
return { ok: false, durationMs, reason };
|
|
9170
9333
|
}
|
|
9171
9334
|
|
|
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
9335
|
// src/turn-execution.ts
|
|
9183
9336
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
9184
|
-
import { existsSync as
|
|
9337
|
+
import { existsSync as existsSync10 } from "fs";
|
|
9185
9338
|
|
|
9186
9339
|
// src/turn-control-tools.ts
|
|
9187
9340
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -9425,11 +9578,11 @@ function trimSlash2(s) {
|
|
|
9425
9578
|
// src/codex-instructions.ts
|
|
9426
9579
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
9427
9580
|
import { tmpdir } from "os";
|
|
9428
|
-
import { join as
|
|
9581
|
+
import { join as join13 } from "path";
|
|
9429
9582
|
var PREFIX = "cabane-codex-instructions-";
|
|
9430
9583
|
async function writeCodexInstructionsFile(contents) {
|
|
9431
|
-
const dir2 = await mkdtemp(
|
|
9432
|
-
const path =
|
|
9584
|
+
const dir2 = await mkdtemp(join13(tmpdir(), PREFIX));
|
|
9585
|
+
const path = join13(dir2, "instructions.md");
|
|
9433
9586
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
9434
9587
|
return {
|
|
9435
9588
|
path,
|
|
@@ -9442,7 +9595,7 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
9442
9595
|
// src/codex-images.ts
|
|
9443
9596
|
import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
9444
9597
|
import { tmpdir as tmpdir2 } from "os";
|
|
9445
|
-
import { join as
|
|
9598
|
+
import { join as join14 } from "path";
|
|
9446
9599
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
9447
9600
|
var EXTENSIONS = {
|
|
9448
9601
|
"image/png": "png",
|
|
@@ -9455,7 +9608,7 @@ function createCodexImagePreparer(opts) {
|
|
|
9455
9608
|
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
9456
9609
|
return async (urls, signal) => {
|
|
9457
9610
|
signal.throwIfAborted();
|
|
9458
|
-
const dir2 = await mkdtemp2(
|
|
9611
|
+
const dir2 = await mkdtemp2(join14(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
|
|
9459
9612
|
const cleanup = async () => {
|
|
9460
9613
|
try {
|
|
9461
9614
|
await rm2(dir2, { recursive: true, force: true });
|
|
@@ -9476,7 +9629,7 @@ function createCodexImagePreparer(opts) {
|
|
|
9476
9629
|
const ext = EXTENSIONS[type];
|
|
9477
9630
|
if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
|
|
9478
9631
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
9479
|
-
const path =
|
|
9632
|
+
const path = join14(dir2, `image-${index + 1}.${ext}`);
|
|
9480
9633
|
await writeFile2(path, bytes, { mode: 384 });
|
|
9481
9634
|
return path;
|
|
9482
9635
|
} catch (err) {
|
|
@@ -9510,14 +9663,14 @@ var SeqFloorUnavailable = class extends Error {
|
|
|
9510
9663
|
}
|
|
9511
9664
|
detail;
|
|
9512
9665
|
};
|
|
9513
|
-
function resolveSeqFloor(sources,
|
|
9666
|
+
function resolveSeqFloor(sources, ctx2) {
|
|
9514
9667
|
const { serverFloor, outboxFloor } = sources;
|
|
9515
9668
|
if (serverFloor === void 0) {
|
|
9516
9669
|
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
9517
9670
|
}
|
|
9518
9671
|
const floor = Math.max(serverFloor, outboxFloor);
|
|
9519
|
-
|
|
9520
|
-
{ turnId:
|
|
9672
|
+
ctx2.log.debug(
|
|
9673
|
+
{ turnId: ctx2.turnId, floor, outboxFloor, serverFloor },
|
|
9521
9674
|
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
9522
9675
|
);
|
|
9523
9676
|
return floor;
|
|
@@ -9535,7 +9688,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
9535
9688
|
}
|
|
9536
9689
|
|
|
9537
9690
|
// src/prepare-failure.ts
|
|
9538
|
-
import { spawn as
|
|
9691
|
+
import { spawn as spawn4 } from "child_process";
|
|
9539
9692
|
import { hostname } from "os";
|
|
9540
9693
|
function prepareFailureCause(error) {
|
|
9541
9694
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
@@ -9573,7 +9726,7 @@ async function runPrepareWithReporting(runner, hook, input, context, reportError
|
|
|
9573
9726
|
}
|
|
9574
9727
|
function reportFailure(config, body) {
|
|
9575
9728
|
return new Promise((resolve2, reject) => {
|
|
9576
|
-
const child =
|
|
9729
|
+
const child = spawn4(config.command, config.args ?? [], {
|
|
9577
9730
|
stdio: ["pipe", "ignore", "ignore"],
|
|
9578
9731
|
env: { ...process.env, ...config.env }
|
|
9579
9732
|
});
|
|
@@ -9597,20 +9750,20 @@ function reportFailure(config, body) {
|
|
|
9597
9750
|
}
|
|
9598
9751
|
|
|
9599
9752
|
// src/prepared.ts
|
|
9600
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as
|
|
9601
|
-
import { join as
|
|
9753
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
|
|
9754
|
+
import { join as join15 } from "path";
|
|
9602
9755
|
function dirFor(workspaceId) {
|
|
9603
|
-
return
|
|
9756
|
+
return join15(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
9604
9757
|
}
|
|
9605
9758
|
function conversationDir(workspaceId, conversationId) {
|
|
9606
|
-
return
|
|
9759
|
+
return join15(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
9607
9760
|
}
|
|
9608
9761
|
function pathFor2(workspaceId, conversationId, agentId) {
|
|
9609
|
-
return
|
|
9762
|
+
return join15(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
9610
9763
|
}
|
|
9611
9764
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
9612
9765
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
9613
|
-
if (!
|
|
9766
|
+
if (!existsSync9(path)) return null;
|
|
9614
9767
|
try {
|
|
9615
9768
|
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
9616
9769
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
@@ -9638,9 +9791,9 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
9638
9791
|
|
|
9639
9792
|
// src/transcript-writer.ts
|
|
9640
9793
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
9641
|
-
import { basename, dirname as dirname6, join as
|
|
9794
|
+
import { basename, dirname as dirname6, join as join16 } from "path";
|
|
9642
9795
|
function transcriptsDir() {
|
|
9643
|
-
return
|
|
9796
|
+
return join16(runnerStateDir(), "transcripts");
|
|
9644
9797
|
}
|
|
9645
9798
|
var RETAIN = 200;
|
|
9646
9799
|
var ANOMALY_RETAIN = 50;
|
|
@@ -9650,7 +9803,7 @@ var TranscriptWriter = class {
|
|
|
9650
9803
|
onWarn;
|
|
9651
9804
|
constructor(dir2, meta, onWarn) {
|
|
9652
9805
|
this.onWarn = onWarn;
|
|
9653
|
-
this.path =
|
|
9806
|
+
this.path = join16(dir2, fileName(meta));
|
|
9654
9807
|
try {
|
|
9655
9808
|
mkdirSync8(dir2, { recursive: true });
|
|
9656
9809
|
try {
|
|
@@ -9679,10 +9832,10 @@ var TranscriptWriter = class {
|
|
|
9679
9832
|
preserveAnomaly() {
|
|
9680
9833
|
if (this.broken) return;
|
|
9681
9834
|
try {
|
|
9682
|
-
const dir2 =
|
|
9835
|
+
const dir2 = join16(dirname6(this.path), "anomalies");
|
|
9683
9836
|
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
9684
9837
|
chmodSync4(dir2, 448);
|
|
9685
|
-
const target =
|
|
9838
|
+
const target = join16(dir2, basename(this.path));
|
|
9686
9839
|
copyFileSync(this.path, target);
|
|
9687
9840
|
chmodSync4(target, 384);
|
|
9688
9841
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -9707,14 +9860,14 @@ var TranscriptWriter = class {
|
|
|
9707
9860
|
function fileName(meta) {
|
|
9708
9861
|
const d = new Date(meta.ts);
|
|
9709
9862
|
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
9710
|
-
const slug =
|
|
9863
|
+
const slug = safe(meta.workspaceSlug) || "ws";
|
|
9711
9864
|
const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
|
|
9712
9865
|
return `${stamp}-${slug}-${short}.jsonl`;
|
|
9713
9866
|
}
|
|
9714
9867
|
function pad(n) {
|
|
9715
9868
|
return String(n).padStart(2, "0");
|
|
9716
9869
|
}
|
|
9717
|
-
function
|
|
9870
|
+
function safe(s) {
|
|
9718
9871
|
return s.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
9719
9872
|
}
|
|
9720
9873
|
function pruneOld(dir2, retain) {
|
|
@@ -9728,7 +9881,7 @@ function pruneOld(dir2, retain) {
|
|
|
9728
9881
|
const drop = files.sort().slice(0, files.length - retain);
|
|
9729
9882
|
for (const f of drop) {
|
|
9730
9883
|
try {
|
|
9731
|
-
rmSync6(
|
|
9884
|
+
rmSync6(join16(dir2, f), { force: true });
|
|
9732
9885
|
} catch {
|
|
9733
9886
|
}
|
|
9734
9887
|
}
|
|
@@ -9867,32 +10020,32 @@ var TurnCommitter = class {
|
|
|
9867
10020
|
function bundledHarnessFor(runtime) {
|
|
9868
10021
|
return runtime === "claude-code" || runtime === "codex" ? runtime : null;
|
|
9869
10022
|
}
|
|
9870
|
-
function resolve(
|
|
9871
|
-
return (
|
|
10023
|
+
function resolve(ctx2, harness) {
|
|
10024
|
+
return (ctx2.resolveBundled ?? resolveBundledBinary)(harness);
|
|
9872
10025
|
}
|
|
9873
|
-
async function postIncompleteNotice(
|
|
10026
|
+
async function postIncompleteNotice(ctx2, harness, resolution) {
|
|
9874
10027
|
try {
|
|
9875
|
-
await
|
|
10028
|
+
await ctx2.postTurnMessage(ctx2.workspaceId, ctx2.conversationId, {
|
|
9876
10029
|
body: turnIncompleteCopy(harness),
|
|
9877
10030
|
kind: "runtime_notice",
|
|
9878
|
-
turnId:
|
|
9879
|
-
parentMessageId:
|
|
10031
|
+
turnId: ctx2.turnId,
|
|
10032
|
+
parentMessageId: ctx2.parentMessageId
|
|
9880
10033
|
});
|
|
9881
10034
|
return true;
|
|
9882
10035
|
} catch (err) {
|
|
9883
|
-
|
|
10036
|
+
ctx2.log.warn(
|
|
9884
10037
|
{ ...apiErrorLogFields(err) },
|
|
9885
10038
|
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9886
10039
|
);
|
|
9887
10040
|
return false;
|
|
9888
10041
|
}
|
|
9889
10042
|
}
|
|
9890
|
-
async function checkBundledBinary(
|
|
9891
|
-
const harness = bundledHarnessFor(
|
|
10043
|
+
async function checkBundledBinary(ctx2) {
|
|
10044
|
+
const harness = bundledHarnessFor(ctx2.runtime);
|
|
9892
10045
|
if (!harness) return null;
|
|
9893
|
-
const resolution = resolve(
|
|
10046
|
+
const resolution = resolve(ctx2, harness);
|
|
9894
10047
|
if (resolution.status === "present") return null;
|
|
9895
|
-
|
|
10048
|
+
ctx2.log.debug(
|
|
9896
10049
|
{
|
|
9897
10050
|
runtime: harness,
|
|
9898
10051
|
status: resolution.status,
|
|
@@ -9904,15 +10057,15 @@ async function checkBundledBinary(ctx) {
|
|
|
9904
10057
|
return {
|
|
9905
10058
|
harness,
|
|
9906
10059
|
reason: `runtime_incomplete:${harness}`,
|
|
9907
|
-
posted: await postIncompleteNotice(
|
|
10060
|
+
posted: await postIncompleteNotice(ctx2, harness, resolution)
|
|
9908
10061
|
};
|
|
9909
10062
|
}
|
|
9910
|
-
async function reclassifyThrow(
|
|
9911
|
-
const harness = bundledHarnessFor(
|
|
10063
|
+
async function reclassifyThrow(ctx2, opts) {
|
|
10064
|
+
const harness = bundledHarnessFor(ctx2.runtime);
|
|
9912
10065
|
if (!harness || opts.aborted) return null;
|
|
9913
|
-
const resolution = resolve(
|
|
10066
|
+
const resolution = resolve(ctx2, harness);
|
|
9914
10067
|
if (resolution.status === "present") return null;
|
|
9915
|
-
|
|
10068
|
+
ctx2.log.debug(
|
|
9916
10069
|
{
|
|
9917
10070
|
runtime: harness,
|
|
9918
10071
|
status: resolution.status,
|
|
@@ -9925,17 +10078,17 @@ async function reclassifyThrow(ctx, opts) {
|
|
|
9925
10078
|
return {
|
|
9926
10079
|
harness,
|
|
9927
10080
|
reason: `runtime_incomplete:${harness}`,
|
|
9928
|
-
posted: await postIncompleteNotice(
|
|
10081
|
+
posted: await postIncompleteNotice(ctx2, harness, resolution)
|
|
9929
10082
|
};
|
|
9930
10083
|
}
|
|
9931
|
-
async function guardBundledBinary(
|
|
9932
|
-
const incomplete = await checkBundledBinary(
|
|
10084
|
+
async function guardBundledBinary(ctx2, concluded, outcome) {
|
|
10085
|
+
const incomplete = await checkBundledBinary(ctx2);
|
|
9933
10086
|
if (!incomplete) return;
|
|
9934
10087
|
outcome.runtimeIncomplete = incomplete.posted;
|
|
9935
10088
|
throw incomplete.posted ? concluded(incomplete.reason) : concluded(incomplete.reason, incomplete.reason);
|
|
9936
10089
|
}
|
|
9937
|
-
async function absorbBundledLoss(
|
|
9938
|
-
const lost = await reclassifyThrow(
|
|
10090
|
+
async function absorbBundledLoss(ctx2, aborted, outcome) {
|
|
10091
|
+
const lost = await reclassifyThrow(ctx2, { aborted, originalReason: outcome.resultReason });
|
|
9939
10092
|
if (!lost) return false;
|
|
9940
10093
|
outcome.resultReason = lost.reason;
|
|
9941
10094
|
return lost.posted;
|
|
@@ -10330,7 +10483,7 @@ var TurnExecution = class {
|
|
|
10330
10483
|
const prepareHook = this.opts.local.prepareHook;
|
|
10331
10484
|
const cabaneCwd = this.turnContext.cwd;
|
|
10332
10485
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
10333
|
-
if (effectiveCwd && !
|
|
10486
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
10334
10487
|
turnLog.warn(
|
|
10335
10488
|
{ cwd: effectiveCwd },
|
|
10336
10489
|
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
@@ -11098,7 +11251,7 @@ function fingerprintSessionState(state) {
|
|
|
11098
11251
|
// src/dispatcher.ts
|
|
11099
11252
|
function checkoutState(cwd) {
|
|
11100
11253
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
11101
|
-
if (!
|
|
11254
|
+
if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
11102
11255
|
let entries;
|
|
11103
11256
|
try {
|
|
11104
11257
|
entries = readdirSync2(cwd);
|
|
@@ -11108,15 +11261,15 @@ function checkoutState(cwd) {
|
|
|
11108
11261
|
if (entries.length === 0) {
|
|
11109
11262
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
11110
11263
|
}
|
|
11111
|
-
const gitPath =
|
|
11112
|
-
if (!
|
|
11264
|
+
const gitPath = join17(cwd, ".git");
|
|
11265
|
+
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
11113
11266
|
let stat;
|
|
11114
11267
|
try {
|
|
11115
|
-
stat =
|
|
11268
|
+
stat = statSync3(gitPath);
|
|
11116
11269
|
} catch (error) {
|
|
11117
11270
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
11118
11271
|
}
|
|
11119
|
-
if (stat.isDirectory() && !
|
|
11272
|
+
if (stat.isDirectory() && !existsSync11(join17(gitPath, "HEAD")))
|
|
11120
11273
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
11121
11274
|
return { ok: true, reason: "usable" };
|
|
11122
11275
|
}
|
|
@@ -11229,18 +11382,18 @@ var Dispatcher = class {
|
|
|
11229
11382
|
};
|
|
11230
11383
|
|
|
11231
11384
|
// src/providers/local-process.ts
|
|
11232
|
-
import { spawn as
|
|
11233
|
-
import { constants } from "os";
|
|
11385
|
+
import { spawn as spawn5 } from "child_process";
|
|
11386
|
+
import { constants as constants2 } from "os";
|
|
11234
11387
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
11235
11388
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
11236
11389
|
function exitCode(code, signal) {
|
|
11237
11390
|
if (code !== null) return code;
|
|
11238
|
-
const signum = signal ?
|
|
11391
|
+
const signum = signal ? constants2.signals[signal] : void 0;
|
|
11239
11392
|
return signum !== void 0 ? 128 + signum : 1;
|
|
11240
11393
|
}
|
|
11241
11394
|
function createLocalProcessProvider(opts = {}) {
|
|
11242
11395
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
11243
|
-
const spawnFn = opts.spawnFn ??
|
|
11396
|
+
const spawnFn = opts.spawnFn ?? spawn5;
|
|
11244
11397
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
11245
11398
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
11246
11399
|
const groupAlive = (pgid) => {
|
|
@@ -11321,24 +11474,24 @@ function createLocalProcessProvider(opts = {}) {
|
|
|
11321
11474
|
|
|
11322
11475
|
// src/providers/docker.ts
|
|
11323
11476
|
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
11324
|
-
import { constants as
|
|
11477
|
+
import { constants as constants3, tmpdir as tmpdir3 } from "os";
|
|
11325
11478
|
import {
|
|
11326
11479
|
createReadStream,
|
|
11327
11480
|
createWriteStream as createWriteStream2,
|
|
11328
|
-
existsSync as
|
|
11481
|
+
existsSync as existsSync14,
|
|
11329
11482
|
mkdirSync as mkdirSync10,
|
|
11330
11483
|
readdirSync as readdirSync4,
|
|
11331
11484
|
readFileSync as readFileSync9,
|
|
11332
|
-
renameSync as
|
|
11485
|
+
renameSync as renameSync6,
|
|
11333
11486
|
rmSync as rmSync9,
|
|
11334
|
-
statSync as
|
|
11487
|
+
statSync as statSync4,
|
|
11335
11488
|
writeFileSync as writeFileSync6
|
|
11336
11489
|
} from "fs";
|
|
11337
|
-
import { join as
|
|
11490
|
+
import { join as join20 } from "path";
|
|
11338
11491
|
import { Writable } from "stream";
|
|
11339
11492
|
|
|
11340
11493
|
// src/providers/docker-cli.ts
|
|
11341
|
-
import { spawn as
|
|
11494
|
+
import { spawn as spawn6 } from "child_process";
|
|
11342
11495
|
function cliEnv(extra) {
|
|
11343
11496
|
const keep = [
|
|
11344
11497
|
"PATH",
|
|
@@ -11358,7 +11511,7 @@ function dockerCli(bin = "docker") {
|
|
|
11358
11511
|
return new Promise((resolve2) => {
|
|
11359
11512
|
let child;
|
|
11360
11513
|
try {
|
|
11361
|
-
child =
|
|
11514
|
+
child = spawn6(bin, [...args], {
|
|
11362
11515
|
env: cliEnv(opts.env),
|
|
11363
11516
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11364
11517
|
});
|
|
@@ -11389,13 +11542,13 @@ function dockerCli(bin = "docker") {
|
|
|
11389
11542
|
});
|
|
11390
11543
|
},
|
|
11391
11544
|
spawn(args, env) {
|
|
11392
|
-
return
|
|
11545
|
+
return spawn6(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
|
|
11393
11546
|
}
|
|
11394
11547
|
};
|
|
11395
11548
|
}
|
|
11396
11549
|
|
|
11397
11550
|
// src/runner/git-credential.ts
|
|
11398
|
-
import { spawn as
|
|
11551
|
+
import { spawn as spawn7 } from "child_process";
|
|
11399
11552
|
import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
|
|
11400
11553
|
import { connect as connect3, createServer as createServer3 } from "net";
|
|
11401
11554
|
|
|
@@ -11437,7 +11590,7 @@ function deviceGitFill(helper, timeoutMs = 15e3) {
|
|
|
11437
11590
|
"credential",
|
|
11438
11591
|
"fill"
|
|
11439
11592
|
];
|
|
11440
|
-
const child =
|
|
11593
|
+
const child = spawn7("git", args, {
|
|
11441
11594
|
stdio: ["pipe", "pipe", "ignore"],
|
|
11442
11595
|
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
|
|
11443
11596
|
});
|
|
@@ -11521,14 +11674,14 @@ async function serveGitCredentials(opts) {
|
|
|
11521
11674
|
// src/runner/collected-outbox.ts
|
|
11522
11675
|
import {
|
|
11523
11676
|
copyFileSync as copyFileSync2,
|
|
11524
|
-
existsSync as
|
|
11677
|
+
existsSync as existsSync12,
|
|
11525
11678
|
mkdirSync as mkdirSync9,
|
|
11526
11679
|
readdirSync as readdirSync3,
|
|
11527
11680
|
readFileSync as readFileSync8,
|
|
11528
|
-
renameSync as
|
|
11681
|
+
renameSync as renameSync5,
|
|
11529
11682
|
rmSync as rmSync8
|
|
11530
11683
|
} from "fs";
|
|
11531
|
-
import { join as
|
|
11684
|
+
import { join as join18 } from "path";
|
|
11532
11685
|
function routes(t) {
|
|
11533
11686
|
const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
|
|
11534
11687
|
const agent = `${conv}/participants/agents/${t.agentId}`;
|
|
@@ -11561,20 +11714,20 @@ function admitCollectedEntry(raw, turn) {
|
|
|
11561
11714
|
}
|
|
11562
11715
|
var atomicMove = (from, to) => {
|
|
11563
11716
|
try {
|
|
11564
|
-
|
|
11717
|
+
renameSync5(from, to);
|
|
11565
11718
|
} catch {
|
|
11566
11719
|
const partial = `${to}.partial`;
|
|
11567
11720
|
copyFileSync2(from, partial);
|
|
11568
|
-
|
|
11721
|
+
renameSync5(partial, to);
|
|
11569
11722
|
rmSync8(from, { force: true });
|
|
11570
11723
|
}
|
|
11571
11724
|
};
|
|
11572
11725
|
function publishStaged(from, to, move = atomicMove) {
|
|
11573
|
-
if (!
|
|
11726
|
+
if (!existsSync12(from)) return { published: 0, complete: true };
|
|
11574
11727
|
const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
|
|
11575
11728
|
let entry = {};
|
|
11576
11729
|
try {
|
|
11577
|
-
entry = JSON.parse(readFileSync8(
|
|
11730
|
+
entry = JSON.parse(readFileSync8(join18(from, name), "utf8"));
|
|
11578
11731
|
} catch {
|
|
11579
11732
|
}
|
|
11580
11733
|
return { name, entry };
|
|
@@ -11587,7 +11740,7 @@ function publishStaged(from, to, move = atomicMove) {
|
|
|
11587
11740
|
let published = 0;
|
|
11588
11741
|
for (const { name } of staged) {
|
|
11589
11742
|
try {
|
|
11590
|
-
move(
|
|
11743
|
+
move(join18(from, name), join18(to, name));
|
|
11591
11744
|
} catch {
|
|
11592
11745
|
return { published, complete: false };
|
|
11593
11746
|
}
|
|
@@ -11599,9 +11752,9 @@ function publishStaged(from, to, move = atomicMove) {
|
|
|
11599
11752
|
|
|
11600
11753
|
// src/providers/runner-base.ts
|
|
11601
11754
|
import { createHash as createHash3 } from "crypto";
|
|
11602
|
-
import { existsSync as
|
|
11603
|
-
import { dirname as dirname7, join as
|
|
11604
|
-
import { fileURLToPath as
|
|
11755
|
+
import { existsSync as existsSync13, realpathSync as realpathSync3 } from "fs";
|
|
11756
|
+
import { dirname as dirname7, join as join19, sep as sep3 } from "path";
|
|
11757
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
11605
11758
|
var COMPANION_MOUNT = "/opt/cabane/companion";
|
|
11606
11759
|
var BASE_DOCKERFILE = `FROM node:22-slim
|
|
11607
11760
|
RUN apt-get update \\
|
|
@@ -11645,17 +11798,17 @@ async function ensureBaseImage(docker, image, log) {
|
|
|
11645
11798
|
}
|
|
11646
11799
|
return true;
|
|
11647
11800
|
}
|
|
11648
|
-
function ownInstall(moduleFile =
|
|
11801
|
+
function ownInstall(moduleFile = fileURLToPath4(import.meta.url)) {
|
|
11649
11802
|
let dir2 = dirname7(moduleFile);
|
|
11650
11803
|
for (let i = 0; i < 4; i++) {
|
|
11651
|
-
if (
|
|
11804
|
+
if (existsSync13(join19(dir2, "dist", "cli.js")) && existsSync13(join19(dir2, "package.json"))) break;
|
|
11652
11805
|
dir2 = dirname7(dir2);
|
|
11653
11806
|
}
|
|
11654
|
-
if (!
|
|
11655
|
-
const sdk =
|
|
11656
|
-
if (!
|
|
11657
|
-
const root =
|
|
11658
|
-
return
|
|
11807
|
+
if (!existsSync13(join19(dir2, "dist", "cli.js"))) return null;
|
|
11808
|
+
const sdk = join19(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
|
|
11809
|
+
if (!existsSync13(sdk)) return null;
|
|
11810
|
+
const root = realpathSync3(dir2);
|
|
11811
|
+
return realpathSync3(sdk).startsWith(root + sep3) ? root : null;
|
|
11659
11812
|
}
|
|
11660
11813
|
function companionSource(input) {
|
|
11661
11814
|
if (input.configured) {
|
|
@@ -11791,23 +11944,42 @@ function networkCreateError(stderr) {
|
|
|
11791
11944
|
}
|
|
11792
11945
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
11793
11946
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
11794
|
-
return
|
|
11947
|
+
return join20(deviceDir, "attempts", attemptId, "session.tar");
|
|
11795
11948
|
}
|
|
11796
11949
|
function pruneOtherAttempts(deviceDir, pair, keep) {
|
|
11797
|
-
const root =
|
|
11798
|
-
if (!
|
|
11950
|
+
const root = join20(deviceDir, "attempts");
|
|
11951
|
+
if (!existsSync14(root)) return;
|
|
11799
11952
|
for (const id of readdirSync4(root)) {
|
|
11800
11953
|
if (id === keep || !UUID_RE.test(id)) continue;
|
|
11801
11954
|
let pairOf = "";
|
|
11802
11955
|
try {
|
|
11803
|
-
pairOf = readFileSync9(
|
|
11956
|
+
pairOf = readFileSync9(join20(root, id, "pair"), "utf8");
|
|
11804
11957
|
} catch {
|
|
11805
11958
|
continue;
|
|
11806
11959
|
}
|
|
11807
11960
|
if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
|
|
11808
|
-
rmSync9(
|
|
11961
|
+
rmSync9(join20(root, id), { recursive: true, force: true });
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
|
|
11966
|
+
function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
|
|
11967
|
+
const root = join20(deviceDir, "attempts");
|
|
11968
|
+
if (!existsSync14(root)) return [];
|
|
11969
|
+
const removed = [];
|
|
11970
|
+
for (const id of readdirSync4(root)) {
|
|
11971
|
+
if (!UUID_RE.test(id)) continue;
|
|
11972
|
+
let kept;
|
|
11973
|
+
try {
|
|
11974
|
+
kept = statSync4(join20(root, id)).mtimeMs;
|
|
11975
|
+
} catch {
|
|
11976
|
+
continue;
|
|
11809
11977
|
}
|
|
11978
|
+
if (now - kept < maxAgeMs) continue;
|
|
11979
|
+
rmSync9(join20(root, id), { recursive: true, force: true });
|
|
11980
|
+
removed.push(id);
|
|
11810
11981
|
}
|
|
11982
|
+
return removed;
|
|
11811
11983
|
}
|
|
11812
11984
|
var CappedFile = class extends Writable {
|
|
11813
11985
|
constructor(path, cap) {
|
|
@@ -12005,15 +12177,15 @@ function createDockerProvider(opts) {
|
|
|
12005
12177
|
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
12006
12178
|
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
12007
12179
|
};
|
|
12008
|
-
const socketRoot = opts.socketRoot ??
|
|
12009
|
-
process.env.XDG_RUNTIME_DIR &&
|
|
12180
|
+
const socketRoot = opts.socketRoot ?? join20(
|
|
12181
|
+
process.env.XDG_RUNTIME_DIR && existsSync14(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
|
|
12010
12182
|
`cabane-runners-${opts.deviceKey}`
|
|
12011
12183
|
);
|
|
12012
|
-
const socketDir = (name) =>
|
|
12184
|
+
const socketDir = (name) => join20(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
|
|
12013
12185
|
const create = async (name, attempt) => {
|
|
12014
12186
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
12015
12187
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
12016
|
-
const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") &&
|
|
12188
|
+
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
12189
|
await must(
|
|
12018
12190
|
[
|
|
12019
12191
|
"create",
|
|
@@ -12151,7 +12323,7 @@ function createDockerProvider(opts) {
|
|
|
12151
12323
|
}
|
|
12152
12324
|
};
|
|
12153
12325
|
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) =>
|
|
12326
|
+
const pendingPath = (name) => join20(opts.deviceDir, "runners-pending", `${name}.json`);
|
|
12155
12327
|
const readPending = (name) => {
|
|
12156
12328
|
try {
|
|
12157
12329
|
const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
|
|
@@ -12165,12 +12337,12 @@ function createDockerProvider(opts) {
|
|
|
12165
12337
|
rmSync9(pendingPath(name), { force: true });
|
|
12166
12338
|
return;
|
|
12167
12339
|
}
|
|
12168
|
-
mkdirSync10(
|
|
12340
|
+
mkdirSync10(join20(opts.deviceDir, "runners-pending"), { recursive: true });
|
|
12169
12341
|
writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
|
|
12170
12342
|
};
|
|
12171
12343
|
const keepSession = async (name, attempt, attemptId) => {
|
|
12172
12344
|
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
12173
|
-
mkdirSync10(
|
|
12345
|
+
mkdirSync10(join20(archive, ".."), { recursive: true });
|
|
12174
12346
|
const partial = `${archive}.${process.pid}.partial`;
|
|
12175
12347
|
const file = new CappedFile(partial, sessionCap);
|
|
12176
12348
|
const r = await execIn(
|
|
@@ -12186,8 +12358,8 @@ function createDockerProvider(opts) {
|
|
|
12186
12358
|
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
12187
12359
|
);
|
|
12188
12360
|
if (r.code === 0 && !file.over) {
|
|
12189
|
-
|
|
12190
|
-
writeFileSync6(
|
|
12361
|
+
renameSync6(partial, archive);
|
|
12362
|
+
writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
12191
12363
|
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
12192
12364
|
await writeMarker(name, attemptId);
|
|
12193
12365
|
log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
|
|
@@ -12201,7 +12373,7 @@ function createDockerProvider(opts) {
|
|
|
12201
12373
|
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
12374
|
);
|
|
12203
12375
|
};
|
|
12204
|
-
const stagingDir = (name) =>
|
|
12376
|
+
const stagingDir = (name) => join20(opts.deviceDir, "runners-pending", `${name}.outbox`);
|
|
12205
12377
|
const ensureRunning = async (name) => {
|
|
12206
12378
|
const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
|
|
12207
12379
|
if (seen.code !== 0) return false;
|
|
@@ -12209,7 +12381,7 @@ function createDockerProvider(opts) {
|
|
|
12209
12381
|
return (await docker.run(["start", name])).code === 0;
|
|
12210
12382
|
};
|
|
12211
12383
|
const dropArchive = (attemptId) => {
|
|
12212
|
-
rmSync9(
|
|
12384
|
+
rmSync9(join20(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
|
|
12213
12385
|
recursive: true,
|
|
12214
12386
|
force: true
|
|
12215
12387
|
});
|
|
@@ -12278,7 +12450,7 @@ function createDockerProvider(opts) {
|
|
|
12278
12450
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
12279
12451
|
await must(["start", name], "starting the container");
|
|
12280
12452
|
const git = await serveGitCredentials({
|
|
12281
|
-
socketPath:
|
|
12453
|
+
socketPath: join20(socketDir(name), "git.sock"),
|
|
12282
12454
|
hosts: opts.gitHosts,
|
|
12283
12455
|
fill: opts.gitFill,
|
|
12284
12456
|
log
|
|
@@ -12312,7 +12484,7 @@ function createDockerProvider(opts) {
|
|
|
12312
12484
|
}
|
|
12313
12485
|
if (attemptId && UUID_RE.test(attemptId)) {
|
|
12314
12486
|
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
12315
|
-
if (
|
|
12487
|
+
if (existsSync14(archive)) {
|
|
12316
12488
|
const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
|
|
12317
12489
|
input: createReadStream(archive)
|
|
12318
12490
|
});
|
|
@@ -12323,7 +12495,7 @@ function createDockerProvider(opts) {
|
|
|
12323
12495
|
);
|
|
12324
12496
|
} else {
|
|
12325
12497
|
log.info(
|
|
12326
|
-
{ container: name, attemptId, bytes:
|
|
12498
|
+
{ container: name, attemptId, bytes: statSync4(archive).size },
|
|
12327
12499
|
"runners: session files restored"
|
|
12328
12500
|
);
|
|
12329
12501
|
}
|
|
@@ -12378,7 +12550,7 @@ function createDockerProvider(opts) {
|
|
|
12378
12550
|
};
|
|
12379
12551
|
child.once("error", () => done(1));
|
|
12380
12552
|
child.once("close", (code, sig) => {
|
|
12381
|
-
const signum = sig ?
|
|
12553
|
+
const signum = sig ? constants3.signals[sig] : void 0;
|
|
12382
12554
|
done(code ?? (signum !== void 0 ? 128 + signum : 1));
|
|
12383
12555
|
});
|
|
12384
12556
|
if (signal.aborted) stop();
|
|
@@ -12427,15 +12599,15 @@ function createDockerProvider(opts) {
|
|
|
12427
12599
|
const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
|
|
12428
12600
|
if (transcripts) {
|
|
12429
12601
|
const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
|
|
12430
|
-
if (kept.length) mkdirSync10(
|
|
12431
|
-
for (const f of kept) writeFileSync6(
|
|
12602
|
+
if (kept.length) mkdirSync10(join20(opts.hostStateDir, "transcripts"), { recursive: true });
|
|
12603
|
+
for (const f of kept) writeFileSync6(join20(opts.hostStateDir, "transcripts", f.name), f.data);
|
|
12432
12604
|
await removeIn(
|
|
12433
12605
|
name,
|
|
12434
12606
|
`${CONTAINER_STATE}/transcripts`,
|
|
12435
12607
|
kept.map((f) => f.name)
|
|
12436
12608
|
);
|
|
12437
12609
|
}
|
|
12438
|
-
const outboxRel =
|
|
12610
|
+
const outboxRel = join20("outbox", encodeURIComponent(attempt.agentId));
|
|
12439
12611
|
const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
|
|
12440
12612
|
let unpublished = false;
|
|
12441
12613
|
if (outbox) {
|
|
@@ -12461,7 +12633,7 @@ function createDockerProvider(opts) {
|
|
|
12461
12633
|
const staged = stagingDir(name);
|
|
12462
12634
|
mkdirSync10(staged, { recursive: true });
|
|
12463
12635
|
writeFileSync6(
|
|
12464
|
-
|
|
12636
|
+
join20(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
|
|
12465
12637
|
`${JSON.stringify(entry)}
|
|
12466
12638
|
`
|
|
12467
12639
|
);
|
|
@@ -12474,7 +12646,7 @@ function createDockerProvider(opts) {
|
|
|
12474
12646
|
if (outbox.complete) {
|
|
12475
12647
|
const published = publishStaged(
|
|
12476
12648
|
stagingDir(name),
|
|
12477
|
-
|
|
12649
|
+
join20(opts.hostStateDir, outboxRel),
|
|
12478
12650
|
opts.moveFile
|
|
12479
12651
|
);
|
|
12480
12652
|
queued = published.published;
|
|
@@ -12683,15 +12855,15 @@ import {
|
|
|
12683
12855
|
mkdirSync as mkdirSync11,
|
|
12684
12856
|
mkdtempSync,
|
|
12685
12857
|
readFileSync as readFileSync10,
|
|
12686
|
-
realpathSync as
|
|
12858
|
+
realpathSync as realpathSync4,
|
|
12687
12859
|
rmSync as rmSync10,
|
|
12688
12860
|
writeFileSync as writeFileSync7
|
|
12689
12861
|
} from "fs";
|
|
12690
12862
|
import { createRequire as createRequire2 } from "module";
|
|
12691
12863
|
import { isIP } from "net";
|
|
12692
|
-
import { homedir as
|
|
12693
|
-
import { dirname as dirname8, join as
|
|
12694
|
-
import { fileURLToPath as
|
|
12864
|
+
import { homedir as homedir6, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
|
|
12865
|
+
import { dirname as dirname8, join as join21, sep as sep4 } from "path";
|
|
12866
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
12695
12867
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
12696
12868
|
function accountHome() {
|
|
12697
12869
|
try {
|
|
@@ -12787,13 +12959,13 @@ function companionStoreWatch(log) {
|
|
|
12787
12959
|
return new CompanionStoreWatch({
|
|
12788
12960
|
log,
|
|
12789
12961
|
roots: () => {
|
|
12790
|
-
const home =
|
|
12962
|
+
const home = homedir6();
|
|
12791
12963
|
return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
|
|
12792
12964
|
}
|
|
12793
12965
|
});
|
|
12794
12966
|
}
|
|
12795
12967
|
function isAtOrUnder(path, dir2) {
|
|
12796
|
-
return path === dir2 || path.startsWith(dir2 +
|
|
12968
|
+
return path === dir2 || path.startsWith(dir2 + sep4);
|
|
12797
12969
|
}
|
|
12798
12970
|
function osSandboxUsable(log) {
|
|
12799
12971
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
@@ -12892,15 +13064,15 @@ var DispatchClaims = class {
|
|
|
12892
13064
|
|
|
12893
13065
|
// src/outbox.ts
|
|
12894
13066
|
import {
|
|
12895
|
-
existsSync as
|
|
13067
|
+
existsSync as existsSync15,
|
|
12896
13068
|
mkdirSync as mkdirSync12,
|
|
12897
13069
|
readdirSync as readdirSync5,
|
|
12898
13070
|
readFileSync as readFileSync11,
|
|
12899
|
-
renameSync as
|
|
13071
|
+
renameSync as renameSync7,
|
|
12900
13072
|
rmSync as rmSync11,
|
|
12901
13073
|
writeFileSync as writeFileSync8
|
|
12902
13074
|
} from "fs";
|
|
12903
|
-
import { join as
|
|
13075
|
+
import { join as join22 } from "path";
|
|
12904
13076
|
var MAX_ENTRIES = 2e3;
|
|
12905
13077
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
12906
13078
|
var Outbox = class {
|
|
@@ -12913,10 +13085,10 @@ var Outbox = class {
|
|
|
12913
13085
|
// Resolved lazily (per call) so tests that swap HOME between
|
|
12914
13086
|
// cases route writes at the right tmpdir.
|
|
12915
13087
|
dir() {
|
|
12916
|
-
return
|
|
13088
|
+
return join22(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
12917
13089
|
}
|
|
12918
13090
|
fileFor(turnId, seq) {
|
|
12919
|
-
return
|
|
13091
|
+
return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
12920
13092
|
}
|
|
12921
13093
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
12922
13094
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -12928,7 +13100,7 @@ var Outbox = class {
|
|
|
12928
13100
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
12929
13101
|
try {
|
|
12930
13102
|
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
12931
|
-
|
|
13103
|
+
renameSync7(tmp, target);
|
|
12932
13104
|
} catch (err) {
|
|
12933
13105
|
try {
|
|
12934
13106
|
rmSync11(tmp, { force: true });
|
|
@@ -12948,7 +13120,7 @@ var Outbox = class {
|
|
|
12948
13120
|
// wedging the drain.
|
|
12949
13121
|
list() {
|
|
12950
13122
|
const dir2 = this.dir();
|
|
12951
|
-
if (!
|
|
13123
|
+
if (!existsSync15(dir2)) return [];
|
|
12952
13124
|
let names;
|
|
12953
13125
|
try {
|
|
12954
13126
|
names = readdirSync5(dir2);
|
|
@@ -12958,7 +13130,7 @@ var Outbox = class {
|
|
|
12958
13130
|
const entries = [];
|
|
12959
13131
|
for (const name of names) {
|
|
12960
13132
|
if (!name.endsWith(".json")) continue;
|
|
12961
|
-
const full =
|
|
13133
|
+
const full = join22(dir2, name);
|
|
12962
13134
|
try {
|
|
12963
13135
|
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
12964
13136
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -12994,7 +13166,7 @@ var Outbox = class {
|
|
|
12994
13166
|
// different: it is positive evidence that nothing was ever queued here.
|
|
12995
13167
|
maxSeqForTurn(turnId) {
|
|
12996
13168
|
const dir2 = this.dir();
|
|
12997
|
-
if (!
|
|
13169
|
+
if (!existsSync15(dir2)) return 0;
|
|
12998
13170
|
const names = readdirSync5(dir2);
|
|
12999
13171
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
13000
13172
|
let max = 0;
|
|
@@ -13014,7 +13186,7 @@ var Outbox = class {
|
|
|
13014
13186
|
}
|
|
13015
13187
|
size() {
|
|
13016
13188
|
const dir2 = this.dir();
|
|
13017
|
-
if (!
|
|
13189
|
+
if (!existsSync15(dir2)) return 0;
|
|
13018
13190
|
try {
|
|
13019
13191
|
return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
|
|
13020
13192
|
} catch {
|
|
@@ -13083,6 +13255,12 @@ var SseSubscriber = class {
|
|
|
13083
13255
|
this.aborted = true;
|
|
13084
13256
|
this.controller?.abort();
|
|
13085
13257
|
}
|
|
13258
|
+
// Connect again after a `stop()`, once its loop has exited.
|
|
13259
|
+
async resume() {
|
|
13260
|
+
await this.finished;
|
|
13261
|
+
this.aborted = false;
|
|
13262
|
+
this.start();
|
|
13263
|
+
}
|
|
13086
13264
|
setCursor(eventId) {
|
|
13087
13265
|
this.lastEventId = eventId;
|
|
13088
13266
|
}
|
|
@@ -13169,6 +13347,550 @@ function sleep3(ms) {
|
|
|
13169
13347
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
13170
13348
|
}
|
|
13171
13349
|
|
|
13350
|
+
// src/updater.ts
|
|
13351
|
+
import { spawn as spawn8 } from "child_process";
|
|
13352
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
|
|
13353
|
+
import { dirname as dirname12, join as join26 } from "path";
|
|
13354
|
+
|
|
13355
|
+
// src/boot-rule.ts
|
|
13356
|
+
import {
|
|
13357
|
+
mkdirSync as mkdirSync13,
|
|
13358
|
+
readFileSync as readFileSync12,
|
|
13359
|
+
realpathSync as realpathSync5,
|
|
13360
|
+
renameSync as renameSync8,
|
|
13361
|
+
rmSync as rmSync12,
|
|
13362
|
+
symlinkSync,
|
|
13363
|
+
writeFileSync as writeFileSync9
|
|
13364
|
+
} from "fs";
|
|
13365
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
13366
|
+
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
13367
|
+
var MAX_STAGE_ATTEMPTS = 3;
|
|
13368
|
+
function superseded(pending, ...running) {
|
|
13369
|
+
return !!pending && !running.includes(pending.to);
|
|
13370
|
+
}
|
|
13371
|
+
function rolledBackState(state, pending, reason, now) {
|
|
13372
|
+
const { pending: _done, ...rest } = state;
|
|
13373
|
+
return {
|
|
13374
|
+
...rest,
|
|
13375
|
+
lastResult: {
|
|
13376
|
+
kind: "rolled_back",
|
|
13377
|
+
from: pending.from,
|
|
13378
|
+
to: pending.to,
|
|
13379
|
+
at: now.toISOString(),
|
|
13380
|
+
reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
|
|
13381
|
+
},
|
|
13382
|
+
failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
|
|
13383
|
+
};
|
|
13384
|
+
}
|
|
13385
|
+
|
|
13386
|
+
// src/install-layout.ts
|
|
13387
|
+
import {
|
|
13388
|
+
existsSync as existsSync16,
|
|
13389
|
+
lstatSync,
|
|
13390
|
+
mkdirSync as mkdirSync14,
|
|
13391
|
+
readFileSync as readFileSync13,
|
|
13392
|
+
readdirSync as readdirSync6,
|
|
13393
|
+
realpathSync as realpathSync6,
|
|
13394
|
+
renameSync as renameSync9,
|
|
13395
|
+
rmSync as rmSync13,
|
|
13396
|
+
symlinkSync as symlinkSync2
|
|
13397
|
+
} from "fs";
|
|
13398
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
13399
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
13400
|
+
function versionsDir() {
|
|
13401
|
+
return join24(cabaneDir(), "versions");
|
|
13402
|
+
}
|
|
13403
|
+
function currentLink() {
|
|
13404
|
+
return join24(cabaneDir(), "current");
|
|
13405
|
+
}
|
|
13406
|
+
function bootGuardPath() {
|
|
13407
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
13408
|
+
}
|
|
13409
|
+
function versionPackageRoot(version) {
|
|
13410
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
13411
|
+
}
|
|
13412
|
+
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
13413
|
+
return dirname10(dirname10(fileURLToPath6(moduleUrl)));
|
|
13414
|
+
}
|
|
13415
|
+
function packageVersion(root) {
|
|
13416
|
+
try {
|
|
13417
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
13418
|
+
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
13419
|
+
} catch {
|
|
13420
|
+
return null;
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
13423
|
+
function currentRoot() {
|
|
13424
|
+
try {
|
|
13425
|
+
const root = realpathSync6(currentLink());
|
|
13426
|
+
return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
|
|
13427
|
+
} catch {
|
|
13428
|
+
return null;
|
|
13429
|
+
}
|
|
13430
|
+
}
|
|
13431
|
+
function pointCurrentAt(root) {
|
|
13432
|
+
const link = currentLink();
|
|
13433
|
+
mkdirSync14(dirname10(link), { recursive: true });
|
|
13434
|
+
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
13435
|
+
symlinkSync2(root, fresh);
|
|
13436
|
+
try {
|
|
13437
|
+
renameSync9(fresh, link);
|
|
13438
|
+
} catch (err) {
|
|
13439
|
+
rmSync13(fresh, { force: true });
|
|
13440
|
+
throw err;
|
|
13441
|
+
}
|
|
13442
|
+
}
|
|
13443
|
+
function pruneVersions(keep) {
|
|
13444
|
+
const dir2 = versionsDir();
|
|
13445
|
+
if (!existsSync16(dir2)) return [];
|
|
13446
|
+
const removed = [];
|
|
13447
|
+
for (const name of readdirSync6(dir2)) {
|
|
13448
|
+
if (keep.includes(name)) continue;
|
|
13449
|
+
rmSync13(join24(dir2, name), { recursive: true, force: true });
|
|
13450
|
+
removed.push(name);
|
|
13451
|
+
}
|
|
13452
|
+
return removed;
|
|
13453
|
+
}
|
|
13454
|
+
function compareVersions(a, b) {
|
|
13455
|
+
const parts = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
|
|
13456
|
+
const pa = parts(a);
|
|
13457
|
+
const pb = parts(b);
|
|
13458
|
+
if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
|
|
13459
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
13460
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
13461
|
+
if (d !== 0) return d;
|
|
13462
|
+
}
|
|
13463
|
+
return 0;
|
|
13464
|
+
}
|
|
13465
|
+
|
|
13466
|
+
// src/update-state.ts
|
|
13467
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
13468
|
+
import { dirname as dirname11, join as join25 } from "path";
|
|
13469
|
+
import { z as z18 } from "zod";
|
|
13470
|
+
var resultSchema = z18.object({
|
|
13471
|
+
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
13472
|
+
from: z18.string(),
|
|
13473
|
+
to: z18.string(),
|
|
13474
|
+
at: z18.string(),
|
|
13475
|
+
reason: z18.string().optional()
|
|
13476
|
+
});
|
|
13477
|
+
var pendingSchema = z18.object({
|
|
13478
|
+
from: z18.string(),
|
|
13479
|
+
// The package root `current` pointed at before the swap, so a rollback can point it back.
|
|
13480
|
+
fromRoot: z18.string(),
|
|
13481
|
+
to: z18.string(),
|
|
13482
|
+
at: z18.string(),
|
|
13483
|
+
boots: z18.number().int().nonnegative()
|
|
13484
|
+
});
|
|
13485
|
+
var stateSchema = z18.object({
|
|
13486
|
+
server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
|
|
13487
|
+
pending: pendingSchema.optional(),
|
|
13488
|
+
lastResult: resultSchema.optional(),
|
|
13489
|
+
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
13490
|
+
});
|
|
13491
|
+
function updateStatePath(dir2 = cabaneDir()) {
|
|
13492
|
+
return join25(dir2, "update.json");
|
|
13493
|
+
}
|
|
13494
|
+
function readUpdateState(path = updateStatePath()) {
|
|
13495
|
+
try {
|
|
13496
|
+
const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
|
|
13497
|
+
return parsed.success ? parsed.data : {};
|
|
13498
|
+
} catch {
|
|
13499
|
+
return {};
|
|
13500
|
+
}
|
|
13501
|
+
}
|
|
13502
|
+
function writeUpdateState(state, path = updateStatePath()) {
|
|
13503
|
+
mkdirSync15(dirname11(path), { recursive: true });
|
|
13504
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
13505
|
+
writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
|
|
13506
|
+
`, { mode: 384 });
|
|
13507
|
+
renameSync10(tmp, path);
|
|
13508
|
+
}
|
|
13509
|
+
function updateUpdateState(change, path = updateStatePath()) {
|
|
13510
|
+
const next = change(readUpdateState(path));
|
|
13511
|
+
writeUpdateState(next, path);
|
|
13512
|
+
return next;
|
|
13513
|
+
}
|
|
13514
|
+
|
|
13515
|
+
// src/updater.ts
|
|
13516
|
+
function updateMode(env = process.env) {
|
|
13517
|
+
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
13518
|
+
}
|
|
13519
|
+
var STAGE_RETRY_MS = 60 * 6e4;
|
|
13520
|
+
var SWAP_DRAIN_MS = 1e4;
|
|
13521
|
+
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
13522
|
+
function versionToStage(input) {
|
|
13523
|
+
const { own, state, now, force } = input;
|
|
13524
|
+
if (!input.auto && !force) return null;
|
|
13525
|
+
if (own === SOURCE_BUILD_VERSION) return null;
|
|
13526
|
+
const latest = state.server?.latest;
|
|
13527
|
+
if (!latest || compareVersions(latest, own) <= 0) return null;
|
|
13528
|
+
if (state.pending) return null;
|
|
13529
|
+
const failed = state.failed;
|
|
13530
|
+
if (!force && failed?.version === latest) {
|
|
13531
|
+
if (failed.attempts >= MAX_STAGE_ATTEMPTS) return null;
|
|
13532
|
+
if (now - Date.parse(failed.at) < STAGE_RETRY_MS) return null;
|
|
13533
|
+
}
|
|
13534
|
+
return latest;
|
|
13535
|
+
}
|
|
13536
|
+
function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
13537
|
+
if (existsSync17(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
|
|
13538
|
+
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
13539
|
+
}
|
|
13540
|
+
function writeBootGuard(root = ownPackageRoot()) {
|
|
13541
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
13542
|
+
if (!existsSync17(built)) return false;
|
|
13543
|
+
const target = bootGuardPath();
|
|
13544
|
+
mkdirSync16(dirname12(target), { recursive: true });
|
|
13545
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
13546
|
+
copyFileSync3(built, tmp);
|
|
13547
|
+
renameSync11(tmp, target);
|
|
13548
|
+
return true;
|
|
13549
|
+
}
|
|
13550
|
+
function oneLine(text) {
|
|
13551
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
13552
|
+
}
|
|
13553
|
+
var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
13554
|
+
let stdout = "";
|
|
13555
|
+
let stderr = "";
|
|
13556
|
+
let child;
|
|
13557
|
+
try {
|
|
13558
|
+
child = spawn8(command, [...args], {
|
|
13559
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13560
|
+
env: opts.env ?? process.env
|
|
13561
|
+
});
|
|
13562
|
+
} catch (err) {
|
|
13563
|
+
resolve2({ code: null, stdout, stderr: err instanceof Error ? err.message : String(err) });
|
|
13564
|
+
return;
|
|
13565
|
+
}
|
|
13566
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : null;
|
|
13567
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
13568
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
13569
|
+
child.on("error", (err) => {
|
|
13570
|
+
if (timer) clearTimeout(timer);
|
|
13571
|
+
resolve2({ code: null, stdout, stderr: stderr || err.message });
|
|
13572
|
+
});
|
|
13573
|
+
child.on("close", (code) => {
|
|
13574
|
+
if (timer) clearTimeout(timer);
|
|
13575
|
+
resolve2({ code, stdout, stderr });
|
|
13576
|
+
});
|
|
13577
|
+
});
|
|
13578
|
+
async function stageVersion(version, harnesses, deps = {}) {
|
|
13579
|
+
const run = deps.run ?? runProcess;
|
|
13580
|
+
const node = deps.execPath ?? process.execPath;
|
|
13581
|
+
const target = join26(versionsDir(), version);
|
|
13582
|
+
const placed = versionPackageRoot(version);
|
|
13583
|
+
if (existsSync17(placed)) {
|
|
13584
|
+
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
13585
|
+
return { ok: true, root: placed };
|
|
13586
|
+
}
|
|
13587
|
+
const npm = join26(dirname12(node), "npm");
|
|
13588
|
+
if (!existsSync17(npm))
|
|
13589
|
+
return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
|
|
13590
|
+
mkdirSync16(versionsDir(), { recursive: true });
|
|
13591
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
13592
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13593
|
+
const installed = await run(
|
|
13594
|
+
npm,
|
|
13595
|
+
[
|
|
13596
|
+
"install",
|
|
13597
|
+
"--prefix",
|
|
13598
|
+
staging,
|
|
13599
|
+
"--include=optional",
|
|
13600
|
+
"--no-audit",
|
|
13601
|
+
"--no-fund",
|
|
13602
|
+
"--no-save",
|
|
13603
|
+
"--loglevel=error",
|
|
13604
|
+
`@cabane/companion@${version}`
|
|
13605
|
+
],
|
|
13606
|
+
{
|
|
13607
|
+
// npm's install scripts run on this Node, not whichever `node` is first on PATH.
|
|
13608
|
+
env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
|
|
13609
|
+
timeoutMs: 15 * 6e4
|
|
13610
|
+
}
|
|
13611
|
+
);
|
|
13612
|
+
if (installed.code !== 0) {
|
|
13613
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13614
|
+
if (/\bE404\b|\bETARGET\b|No matching version found|is not in this registry/i.test(
|
|
13615
|
+
installed.stderr
|
|
13616
|
+
))
|
|
13617
|
+
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
13618
|
+
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
13619
|
+
}
|
|
13620
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
13621
|
+
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
13622
|
+
if (verdict) {
|
|
13623
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13624
|
+
return { ok: false, reason: verdict };
|
|
13625
|
+
}
|
|
13626
|
+
rmSync14(target, { recursive: true, force: true });
|
|
13627
|
+
renameSync11(staging, target);
|
|
13628
|
+
return { ok: true, root: versionPackageRoot(version) };
|
|
13629
|
+
}
|
|
13630
|
+
async function verifyInstall(root, version, harnesses, deps) {
|
|
13631
|
+
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
13632
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
13633
|
+
timeoutMs: 6e4,
|
|
13634
|
+
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
13635
|
+
});
|
|
13636
|
+
if (answered.code !== 0 || answered.stdout.trim() !== version) {
|
|
13637
|
+
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
13638
|
+
}
|
|
13639
|
+
if (harnesses.length === 0) return null;
|
|
13640
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
13641
|
+
timeoutMs: 6e4
|
|
13642
|
+
});
|
|
13643
|
+
let resolutions = {};
|
|
13644
|
+
try {
|
|
13645
|
+
resolutions = JSON.parse(probed.stdout);
|
|
13646
|
+
} catch {
|
|
13647
|
+
return "it couldn't say which bundled agents it has";
|
|
13648
|
+
}
|
|
13649
|
+
const missing = harnesses.filter((h) => resolutions[h]?.status !== "present");
|
|
13650
|
+
if (missing.length > 0) {
|
|
13651
|
+
return `its bundled ${missing.map(harnessName).join(" and ")} ${missing.length > 1 ? "are" : "is"} missing`;
|
|
13652
|
+
}
|
|
13653
|
+
return null;
|
|
13654
|
+
}
|
|
13655
|
+
function harnessName(h) {
|
|
13656
|
+
return h === "claude-code" ? "Claude Code" : "Codex";
|
|
13657
|
+
}
|
|
13658
|
+
function lastLine(text) {
|
|
13659
|
+
const lines2 = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
13660
|
+
return lines2.at(-1)?.slice(0, 300) ?? "";
|
|
13661
|
+
}
|
|
13662
|
+
var CompanionUpdater = class {
|
|
13663
|
+
constructor(deps) {
|
|
13664
|
+
this.deps = deps;
|
|
13665
|
+
this.now = deps.now ?? Date.now;
|
|
13666
|
+
}
|
|
13667
|
+
deps;
|
|
13668
|
+
staging = null;
|
|
13669
|
+
ready = null;
|
|
13670
|
+
swapping = false;
|
|
13671
|
+
watchdog = null;
|
|
13672
|
+
now;
|
|
13673
|
+
// At start: a recorded swap to a version that will never start again (someone installed or
|
|
13674
|
+
// chose another since) is dropped, or nothing would ever confirm it and no later update would
|
|
13675
|
+
// be installed.
|
|
13676
|
+
start() {
|
|
13677
|
+
const pending = readUpdateState().pending;
|
|
13678
|
+
const current = currentRoot();
|
|
13679
|
+
const currentVersion = current ? packageVersion(current) : null;
|
|
13680
|
+
if (superseded(pending, this.deps.own, ...currentVersion ? [currentVersion] : [])) {
|
|
13681
|
+
updateUpdateState(({ pending: _gone, ...rest }) => rest);
|
|
13682
|
+
this.deps.log.info(
|
|
13683
|
+
{ to: pending.to, own: this.deps.own },
|
|
13684
|
+
`Dropped the update to ${pending.to}: Cabane ${this.deps.own} was installed since`
|
|
13685
|
+
);
|
|
13686
|
+
}
|
|
13687
|
+
}
|
|
13688
|
+
// The deadline for an update this process is the result of: ten minutes from the first time
|
|
13689
|
+
// Cabane answers it without its stream connecting. Until Cabane answers, there's no verdict on
|
|
13690
|
+
// time: a server or network that is down says nothing about the version.
|
|
13691
|
+
armDeadline() {
|
|
13692
|
+
if (this.watchdog) return;
|
|
13693
|
+
this.watchdog = setTimeout(() => void this.giveUp(), UNCONFIRMED_WINDOW_MS);
|
|
13694
|
+
this.watchdog.unref?.();
|
|
13695
|
+
}
|
|
13696
|
+
stop() {
|
|
13697
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
13698
|
+
this.watchdog = null;
|
|
13699
|
+
}
|
|
13700
|
+
async giveUp() {
|
|
13701
|
+
const pending = readUpdateState().pending;
|
|
13702
|
+
if (!pending || pending.to !== this.deps.own) return;
|
|
13703
|
+
this.deps.log.warn(
|
|
13704
|
+
{ from: pending.from, to: pending.to },
|
|
13705
|
+
`Cabane ${pending.to} didn't connect within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes of Cabane answering; going back to ${pending.from}`
|
|
13706
|
+
);
|
|
13707
|
+
rollBack(
|
|
13708
|
+
pending,
|
|
13709
|
+
`it didn't connect to Cabane within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes`
|
|
13710
|
+
);
|
|
13711
|
+
await this.deps.restart();
|
|
13712
|
+
}
|
|
13713
|
+
// Auto-update is on and something will restart this companion into the new version.
|
|
13714
|
+
updatesItself() {
|
|
13715
|
+
return this.deps.mode === "supervised" && this.deps.autoUpdate();
|
|
13716
|
+
}
|
|
13717
|
+
// What the heartbeat reports.
|
|
13718
|
+
report() {
|
|
13719
|
+
const last = readUpdateState().lastResult;
|
|
13720
|
+
return { auto: this.updatesItself(), ...last ? { lastResult: last } : {} };
|
|
13721
|
+
}
|
|
13722
|
+
// A heartbeat landed. Record what the server named; confirm an update this process is the
|
|
13723
|
+
// result of; then, when there is something newer, install it and swap when idle.
|
|
13724
|
+
async onHeartbeat(server, streamConnected) {
|
|
13725
|
+
const state = updateUpdateState((s) => ({
|
|
13726
|
+
...s,
|
|
13727
|
+
...server ? { server: { ...server, at: new Date(this.now()).toISOString() } } : {}
|
|
13728
|
+
}));
|
|
13729
|
+
if (state.pending?.to === this.deps.own) {
|
|
13730
|
+
if (streamConnected) this.confirm(state.pending);
|
|
13731
|
+
else this.armDeadline();
|
|
13732
|
+
}
|
|
13733
|
+
if (this.ready) {
|
|
13734
|
+
await this.trySwap();
|
|
13735
|
+
return;
|
|
13736
|
+
}
|
|
13737
|
+
this.maybeStage(false);
|
|
13738
|
+
}
|
|
13739
|
+
confirm(pending) {
|
|
13740
|
+
this.stop();
|
|
13741
|
+
updateUpdateState((s) => {
|
|
13742
|
+
const { pending: _done, failed: _failed, ...rest } = s;
|
|
13743
|
+
return {
|
|
13744
|
+
...rest,
|
|
13745
|
+
lastResult: {
|
|
13746
|
+
kind: "updated",
|
|
13747
|
+
from: pending.from,
|
|
13748
|
+
to: pending.to,
|
|
13749
|
+
at: new Date(this.now()).toISOString()
|
|
13750
|
+
}
|
|
13751
|
+
};
|
|
13752
|
+
});
|
|
13753
|
+
const removed = pruneVersions([pending.to, pending.from]);
|
|
13754
|
+
this.deps.log.info(
|
|
13755
|
+
{ from: pending.from, to: pending.to, removed },
|
|
13756
|
+
`Updated Cabane from ${pending.from} to ${pending.to}`
|
|
13757
|
+
);
|
|
13758
|
+
}
|
|
13759
|
+
// `cabane update`, from the person: install the latest now, whatever the setting.
|
|
13760
|
+
async updateNow() {
|
|
13761
|
+
const state = readUpdateState();
|
|
13762
|
+
const latest = state.server?.latest;
|
|
13763
|
+
if (!latest) return { ok: false, message: "Cabane hasn't said which version is latest yet." };
|
|
13764
|
+
if (compareVersions(latest, this.deps.own) <= 0)
|
|
13765
|
+
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
13766
|
+
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13767
|
+
if (this.ready === latest) {
|
|
13768
|
+
void this.trySwap();
|
|
13769
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
13770
|
+
}
|
|
13771
|
+
this.maybeStage(true);
|
|
13772
|
+
if (this.staging) await this.staging;
|
|
13773
|
+
if (this.ready !== latest) {
|
|
13774
|
+
const after = readUpdateState();
|
|
13775
|
+
if (after.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13776
|
+
if (after.failed?.version === latest && after.failed.attempts === 0)
|
|
13777
|
+
return { ok: false, message: `Cabane ${latest} isn't published yet. Try again later.` };
|
|
13778
|
+
const failed = after.lastResult;
|
|
13779
|
+
return {
|
|
13780
|
+
ok: false,
|
|
13781
|
+
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
13782
|
+
};
|
|
13783
|
+
}
|
|
13784
|
+
void this.trySwap();
|
|
13785
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
13786
|
+
}
|
|
13787
|
+
waitingMessage(version) {
|
|
13788
|
+
if (this.deps.mode === "attached")
|
|
13789
|
+
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
13790
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
|
|
13791
|
+
}
|
|
13792
|
+
maybeStage(force) {
|
|
13793
|
+
if (this.staging) return;
|
|
13794
|
+
const version = versionToStage({
|
|
13795
|
+
auto: this.updatesItself(),
|
|
13796
|
+
own: this.deps.own,
|
|
13797
|
+
state: readUpdateState(),
|
|
13798
|
+
now: this.now(),
|
|
13799
|
+
force
|
|
13800
|
+
});
|
|
13801
|
+
if (!version) return;
|
|
13802
|
+
const stage = this.deps.stage ?? stageVersion;
|
|
13803
|
+
this.staging = (async () => {
|
|
13804
|
+
this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
|
|
13805
|
+
const outcome = await stage(version, this.deps.harnesses());
|
|
13806
|
+
if (outcome.ok) {
|
|
13807
|
+
this.ready = version;
|
|
13808
|
+
updateUpdateState((s) => {
|
|
13809
|
+
const { failed: _failed, ...rest } = s;
|
|
13810
|
+
return rest;
|
|
13811
|
+
});
|
|
13812
|
+
return;
|
|
13813
|
+
}
|
|
13814
|
+
if (outcome.unpublished) {
|
|
13815
|
+
this.deps.log.info({ version }, `Cabane ${version} isn't published yet; trying later`);
|
|
13816
|
+
updateUpdateState((s) => ({
|
|
13817
|
+
...s,
|
|
13818
|
+
failed: { version, attempts: 0, at: new Date(this.now()).toISOString() }
|
|
13819
|
+
}));
|
|
13820
|
+
return;
|
|
13821
|
+
}
|
|
13822
|
+
this.deps.log.warn({ version, reason: outcome.reason }, `Couldn't install Cabane ${version}`);
|
|
13823
|
+
updateUpdateState((s) => {
|
|
13824
|
+
const attempts = s.failed?.version === version ? s.failed.attempts + 1 : 1;
|
|
13825
|
+
const at = new Date(this.now()).toISOString();
|
|
13826
|
+
return {
|
|
13827
|
+
...s,
|
|
13828
|
+
failed: { version, attempts, at },
|
|
13829
|
+
// Reported once it has failed for good, so a registry blip isn't an email.
|
|
13830
|
+
...attempts >= MAX_STAGE_ATTEMPTS || force ? {
|
|
13831
|
+
lastResult: {
|
|
13832
|
+
kind: "failed",
|
|
13833
|
+
from: this.deps.own,
|
|
13834
|
+
to: version,
|
|
13835
|
+
at,
|
|
13836
|
+
reason: oneLine(outcome.reason)
|
|
13837
|
+
}
|
|
13838
|
+
} : {}
|
|
13839
|
+
};
|
|
13840
|
+
});
|
|
13841
|
+
})().finally(() => {
|
|
13842
|
+
this.staging = null;
|
|
13843
|
+
});
|
|
13844
|
+
void this.staging.then(() => this.trySwap());
|
|
13845
|
+
}
|
|
13846
|
+
// Swap into the ready version once nothing is running. Called on each heartbeat until it can.
|
|
13847
|
+
async trySwap() {
|
|
13848
|
+
const to = this.ready;
|
|
13849
|
+
if (!to || this.swapping) return;
|
|
13850
|
+
if (readUpdateState().pending) return;
|
|
13851
|
+
if (this.deps.mode === "attached") {
|
|
13852
|
+
this.select(to);
|
|
13853
|
+
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
13854
|
+
return;
|
|
13855
|
+
}
|
|
13856
|
+
if (!this.deps.isIdle()) return;
|
|
13857
|
+
this.swapping = true;
|
|
13858
|
+
try {
|
|
13859
|
+
await this.deps.pauseStream();
|
|
13860
|
+
const sleep4 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
13861
|
+
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
13862
|
+
await sleep4(250);
|
|
13863
|
+
while (!this.deps.isIdle() && this.now() < deadline) await sleep4(250);
|
|
13864
|
+
if (!this.deps.isIdle()) {
|
|
13865
|
+
this.deps.resumeStream();
|
|
13866
|
+
return;
|
|
13867
|
+
}
|
|
13868
|
+
this.select(to);
|
|
13869
|
+
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
13870
|
+
await this.deps.restart();
|
|
13871
|
+
} finally {
|
|
13872
|
+
this.swapping = false;
|
|
13873
|
+
}
|
|
13874
|
+
}
|
|
13875
|
+
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
13876
|
+
select(to) {
|
|
13877
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
13878
|
+
writeBootGuard();
|
|
13879
|
+
updateUpdateState((s) => ({
|
|
13880
|
+
...s,
|
|
13881
|
+
pending: {
|
|
13882
|
+
from: this.deps.own,
|
|
13883
|
+
fromRoot,
|
|
13884
|
+
to,
|
|
13885
|
+
at: new Date(this.now()).toISOString(),
|
|
13886
|
+
boots: 0
|
|
13887
|
+
}
|
|
13888
|
+
}));
|
|
13889
|
+
pointCurrentAt(versionPackageRoot(to));
|
|
13890
|
+
this.ready = null;
|
|
13891
|
+
}
|
|
13892
|
+
};
|
|
13893
|
+
|
|
13172
13894
|
// src/version.ts
|
|
13173
13895
|
import { createRequire as createRequire3 } from "module";
|
|
13174
13896
|
var pkg = createRequire3(import.meta.url)("../package.json");
|
|
@@ -13176,6 +13898,7 @@ var COMPANION_VERSION = pkg.version;
|
|
|
13176
13898
|
|
|
13177
13899
|
// src/supervisor.ts
|
|
13178
13900
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
13901
|
+
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
13179
13902
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
13180
13903
|
function positiveIntEnv(name) {
|
|
13181
13904
|
const raw = process.env[name];
|
|
@@ -13215,9 +13938,10 @@ var CompanionSupervisor = class {
|
|
|
13215
13938
|
// surface, so the UI never disagrees with what is advertised. Null until the
|
|
13216
13939
|
// first probe.
|
|
13217
13940
|
harnessSignals = null;
|
|
13218
|
-
// Fresh per
|
|
13219
|
-
//
|
|
13220
|
-
|
|
13941
|
+
// Fresh per connect, cached for the heartbeat: a sign-in between attempts
|
|
13942
|
+
// must be observed immediately, and a heartbeat must spawn nothing.
|
|
13943
|
+
runHarnessCheck;
|
|
13944
|
+
harnessChecks = {};
|
|
13221
13945
|
exitFn;
|
|
13222
13946
|
reexecFn;
|
|
13223
13947
|
dispatcherFactory;
|
|
@@ -13256,15 +13980,23 @@ var CompanionSupervisor = class {
|
|
|
13256
13980
|
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
13257
13981
|
// run the turn a second time.
|
|
13258
13982
|
claims = new DispatchClaims();
|
|
13983
|
+
archivePruneTimer = null;
|
|
13984
|
+
// Keeps this companion current when its person said so; null until paired.
|
|
13985
|
+
updater = null;
|
|
13986
|
+
restartForUpdate;
|
|
13259
13987
|
constructor(opts) {
|
|
13260
13988
|
this.config = opts.config;
|
|
13261
13989
|
this.log = opts.log;
|
|
13262
13990
|
this.hub = opts.hub;
|
|
13263
13991
|
this.resolveBundled = opts.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
13264
|
-
this.
|
|
13992
|
+
this.runHarnessCheck = opts.checkHarness ?? ((runtime) => checkHarnessUsable(runtime));
|
|
13265
13993
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
13266
13994
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
13267
13995
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
13996
|
+
this.restartForUpdate = opts.restartForUpdate ?? (async () => {
|
|
13997
|
+
await this.shutdown();
|
|
13998
|
+
this.exitFn(0);
|
|
13999
|
+
});
|
|
13268
14000
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
13269
14001
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
13270
14002
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
@@ -13288,7 +14020,7 @@ var CompanionSupervisor = class {
|
|
|
13288
14020
|
});
|
|
13289
14021
|
}
|
|
13290
14022
|
dockerRunners() {
|
|
13291
|
-
return
|
|
14023
|
+
return dockerRunnersConfigured(this.config);
|
|
13292
14024
|
}
|
|
13293
14025
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
13294
14026
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
@@ -13315,6 +14047,9 @@ var CompanionSupervisor = class {
|
|
|
13315
14047
|
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
13316
14048
|
}
|
|
13317
14049
|
this.latchBootBundledState();
|
|
14050
|
+
this.pruneSessionArchives();
|
|
14051
|
+
this.archivePruneTimer = setInterval(() => this.pruneSessionArchives(), ARCHIVE_PRUNE_MS);
|
|
14052
|
+
this.archivePruneTimer.unref?.();
|
|
13318
14053
|
}
|
|
13319
14054
|
const sandboxRows = agentSandboxRows(this.config);
|
|
13320
14055
|
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
@@ -13335,10 +14070,10 @@ var CompanionSupervisor = class {
|
|
|
13335
14070
|
else this.log.error({ posture }, `runners: ${summary}`);
|
|
13336
14071
|
void this.companionStores().refresh();
|
|
13337
14072
|
}
|
|
13338
|
-
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
14073
|
+
const startupHarnessRefresh = this.checkHarnesses().then(() => this.refreshHarnessStatuses());
|
|
13339
14074
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
13340
14075
|
if (!this.config.deviceToken) {
|
|
13341
|
-
this.log.warn("This device isn't paired. Run cabane
|
|
14076
|
+
this.log.warn("This device isn't paired. Run cabane start to pair it.");
|
|
13342
14077
|
return;
|
|
13343
14078
|
}
|
|
13344
14079
|
this.deviceApi = new DeviceApi({
|
|
@@ -13374,10 +14109,27 @@ var CompanionSupervisor = class {
|
|
|
13374
14109
|
},
|
|
13375
14110
|
onAuthFailure: () => {
|
|
13376
14111
|
this.log.error(
|
|
13377
|
-
|
|
14112
|
+
`Cabane no longer accepts this device. Run ${cabane("logout")}, then ${cabane("start")} to pair again.`
|
|
13378
14113
|
);
|
|
13379
14114
|
}
|
|
13380
14115
|
});
|
|
14116
|
+
this.updater = new CompanionUpdater({
|
|
14117
|
+
own: COMPANION_VERSION,
|
|
14118
|
+
mode: updateMode(),
|
|
14119
|
+
autoUpdate: () => this.config.update?.auto === true,
|
|
14120
|
+
harnesses: () => this.connectedBundledHarnesses(),
|
|
14121
|
+
isIdle: () => this.idle(),
|
|
14122
|
+
pauseStream: async () => {
|
|
14123
|
+
const sub = this.deviceSub;
|
|
14124
|
+
if (!sub) return;
|
|
14125
|
+
sub.stop();
|
|
14126
|
+
await sub.finished;
|
|
14127
|
+
},
|
|
14128
|
+
resumeStream: () => void this.deviceSub?.resume(),
|
|
14129
|
+
restart: () => this.restartForUpdate(),
|
|
14130
|
+
log: this.log
|
|
14131
|
+
});
|
|
14132
|
+
this.updater.start();
|
|
13381
14133
|
await this.refreshAssignments();
|
|
13382
14134
|
this.deviceSub.start();
|
|
13383
14135
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
@@ -13438,13 +14190,20 @@ var CompanionSupervisor = class {
|
|
|
13438
14190
|
// Omitted until the first observed failure or heal, so the server's
|
|
13439
14191
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
13440
14192
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
13441
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
14193
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
14194
|
+
...this.updater ? { update: this.updater.report() } : {}
|
|
13442
14195
|
});
|
|
13443
14196
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
13444
14197
|
this.deviceId = res.deviceId;
|
|
13445
14198
|
this.checkVersionSkew(res.serverVersion);
|
|
13446
14199
|
this.heartbeated = true;
|
|
13447
14200
|
void this.recoverRuns();
|
|
14201
|
+
void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
|
|
14202
|
+
(err) => this.log.warn(
|
|
14203
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
14204
|
+
"Couldn't check for a Cabane update; trying again on the next check-in."
|
|
14205
|
+
)
|
|
14206
|
+
);
|
|
13448
14207
|
} catch (err) {
|
|
13449
14208
|
this.log.warn(
|
|
13450
14209
|
{ ...apiErrorLogFields(err) },
|
|
@@ -13831,11 +14590,11 @@ var CompanionSupervisor = class {
|
|
|
13831
14590
|
if (this.runnerCommand) return this.runnerCommand;
|
|
13832
14591
|
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
13833
14592
|
}
|
|
13834
|
-
buildDispatcher(
|
|
14593
|
+
buildDispatcher(ctx2) {
|
|
13835
14594
|
const hostLocal = localAgentConfig(this.config, {
|
|
13836
|
-
agentId:
|
|
13837
|
-
agentUsername:
|
|
13838
|
-
workspaceSlug:
|
|
14595
|
+
agentId: ctx2.agentId,
|
|
14596
|
+
agentUsername: ctx2.agentUsername,
|
|
14597
|
+
workspaceSlug: ctx2.workspaceSlug
|
|
13839
14598
|
});
|
|
13840
14599
|
const local = this.dockerRunners() && hostLocal.prepareHook ? {
|
|
13841
14600
|
...hostLocal,
|
|
@@ -13844,20 +14603,20 @@ var CompanionSupervisor = class {
|
|
|
13844
14603
|
command: containerHookPath(hostLocal.prepareHook.command)
|
|
13845
14604
|
}
|
|
13846
14605
|
} : hostLocal;
|
|
13847
|
-
if (this.dispatcherFactory) return this.dispatcherFactory({ ...
|
|
14606
|
+
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx2, local });
|
|
13848
14607
|
return new Dispatcher({
|
|
13849
14608
|
deviceId: this.deviceId ?? void 0,
|
|
13850
|
-
api:
|
|
13851
|
-
baseUrl:
|
|
13852
|
-
workspaceId:
|
|
13853
|
-
workspaceSlug:
|
|
13854
|
-
agentId:
|
|
13855
|
-
agentUsername:
|
|
13856
|
-
credential:
|
|
14609
|
+
api: ctx2.api,
|
|
14610
|
+
baseUrl: ctx2.baseUrl,
|
|
14611
|
+
workspaceId: ctx2.workspaceId,
|
|
14612
|
+
workspaceSlug: ctx2.workspaceSlug,
|
|
14613
|
+
agentId: ctx2.agentId,
|
|
14614
|
+
agentUsername: ctx2.agentUsername,
|
|
14615
|
+
credential: ctx2.credential,
|
|
13857
14616
|
local,
|
|
13858
|
-
aborts:
|
|
13859
|
-
runConfig:
|
|
13860
|
-
log: this.log.child({ agentName:
|
|
14617
|
+
aborts: ctx2.aborts,
|
|
14618
|
+
runConfig: ctx2.runConfig,
|
|
14619
|
+
log: this.log.child({ agentName: ctx2.agentDisplayName ?? ctx2.agentUsername }),
|
|
13861
14620
|
// Read PER TURN off the heartbeat's own predicates, so a harness installed
|
|
13862
14621
|
// or connected after boot works on the next turn exactly as it appears on
|
|
13863
14622
|
// the next beat, and a runtime advertised by one and refused by the other
|
|
@@ -13870,7 +14629,7 @@ var CompanionSupervisor = class {
|
|
|
13870
14629
|
// DEFAULT_AGENT_IDLE_TIMEOUT_MS.
|
|
13871
14630
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
13872
14631
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
13873
|
-
observer: this.hub.observerFor(
|
|
14632
|
+
observer: this.hub.observerFor(ctx2.workspaceId, ctx2.agentId, ctx2.workspaceSlug),
|
|
13874
14633
|
transcriptDir: transcriptsDir(),
|
|
13875
14634
|
connectorHealth: this.connectorHealth,
|
|
13876
14635
|
...runnersEnabled(this.config) ? {
|
|
@@ -13881,7 +14640,7 @@ var CompanionSupervisor = class {
|
|
|
13881
14640
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13882
14641
|
connectorHealth: this.connectorHealth,
|
|
13883
14642
|
locks: this.runnerLocks,
|
|
13884
|
-
sandbox: this.runnerSandboxFor(agentSandbox(this.config,
|
|
14643
|
+
sandbox: this.runnerSandboxFor(agentSandbox(this.config, ctx2)),
|
|
13885
14644
|
policy: this.sandboxPolicy(),
|
|
13886
14645
|
companionStores: () => this.companionStores().stores()
|
|
13887
14646
|
}
|
|
@@ -14309,23 +15068,23 @@ var CompanionSupervisor = class {
|
|
|
14309
15068
|
configureLogger(this.log, next);
|
|
14310
15069
|
}
|
|
14311
15070
|
// ---- harnesses ----
|
|
14312
|
-
//
|
|
14313
|
-
// to the hub.
|
|
14314
|
-
//
|
|
14315
|
-
|
|
15071
|
+
// Assemble the signals from the cached sign-in checks, cache versions, and
|
|
15072
|
+
// push the snapshot to the hub. Runs no sign-in check: the heartbeat calls
|
|
15073
|
+
// this. `probeHarnessSignals` never throws, but this is wrapped anyway so a
|
|
15074
|
+
// refresh can never take a heartbeat down.
|
|
15075
|
+
async refreshHarnessStatuses() {
|
|
14316
15076
|
try {
|
|
14317
15077
|
const signals = await probeHarnessSignals(this.config, {
|
|
14318
|
-
|
|
14319
|
-
presence,
|
|
15078
|
+
checks: this.harnessChecks,
|
|
14320
15079
|
// Through the supervisor's seam, so a test pins the host fact.
|
|
14321
15080
|
resolveBundled: this.resolveBundled
|
|
14322
15081
|
});
|
|
14323
15082
|
this.harnessSignals = signals;
|
|
14324
15083
|
this.logBundledTransitions();
|
|
14325
15084
|
this.harnessVersions = {
|
|
14326
|
-
claudeCode: signals.
|
|
15085
|
+
claudeCode: checkVersion(signals.claudeCheck),
|
|
14327
15086
|
opencode: signals.opencodeVersion,
|
|
14328
|
-
codex: signals.
|
|
15087
|
+
codex: checkVersion(signals.codexCheck)
|
|
14329
15088
|
};
|
|
14330
15089
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
14331
15090
|
} catch (err) {
|
|
@@ -14335,9 +15094,32 @@ var CompanionSupervisor = class {
|
|
|
14335
15094
|
);
|
|
14336
15095
|
}
|
|
14337
15096
|
}
|
|
15097
|
+
// Check one bundled harness's sign-in in this process, cache it for the
|
|
15098
|
+
// heartbeat, and — for a harness the user connected — seed connector health,
|
|
15099
|
+
// so Cabane says "sign-in needed" without waiting for a turn to fail. A
|
|
15100
|
+
// check that couldn't answer seeds nothing either way.
|
|
15101
|
+
async checkHarness(runtime) {
|
|
15102
|
+
let check;
|
|
15103
|
+
try {
|
|
15104
|
+
check = await this.runHarnessCheck(runtime);
|
|
15105
|
+
} catch {
|
|
15106
|
+
check = { status: "check_failed", reason: "spawn" };
|
|
15107
|
+
}
|
|
15108
|
+
this.harnessChecks[runtime] = check;
|
|
15109
|
+
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
15110
|
+
if (intended && (isUsable(check) || check.status === "signed_out")) {
|
|
15111
|
+
this.connectorHealth.recordLocalCheck(runtime, isUsable(check));
|
|
15112
|
+
}
|
|
15113
|
+
return check;
|
|
15114
|
+
}
|
|
15115
|
+
async checkHarnesses() {
|
|
15116
|
+
if (this.dockerRunners()) return;
|
|
15117
|
+
await Promise.all([this.checkHarness("claude-code"), this.checkHarness("codex")]);
|
|
15118
|
+
}
|
|
14338
15119
|
// The dashboard "recheck harnesses" control — an immediate re-probe so a user who
|
|
14339
15120
|
// just installed/started a harness sees it flip without waiting for the next beat.
|
|
14340
15121
|
async recheckHarnesses() {
|
|
15122
|
+
await this.checkHarnesses();
|
|
14341
15123
|
await this.refreshHarnessStatuses();
|
|
14342
15124
|
}
|
|
14343
15125
|
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
@@ -14357,48 +15139,57 @@ var CompanionSupervisor = class {
|
|
|
14357
15139
|
currentConfig() {
|
|
14358
15140
|
return this.config;
|
|
14359
15141
|
}
|
|
14360
|
-
//
|
|
15142
|
+
// Connect a harness by flipping the flag the app owns in
|
|
14361
15143
|
// `~/.cabane/config.json`. Never installs a binary or drives a login (BYO).
|
|
14362
15144
|
//
|
|
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).
|
|
15145
|
+
// Claude Code and Codex are checked FIRST, in this process — the one that
|
|
15146
|
+
// will run their turns — and an agent that isn't usable here is refused before
|
|
15147
|
+
// anything is written: the terminal, the control socket and the dashboard all
|
|
15148
|
+
// come through here, so they all get the same answer. An agent already
|
|
15149
|
+
// connected is checked too and reported truthfully, with nothing written.
|
|
14370
15150
|
// - opencode: `opencode.serverUrl`, ONLY after health-probing the URL; an
|
|
14371
15151
|
// unreachable one is a typed failure shown in place.
|
|
14372
|
-
//
|
|
14373
|
-
//
|
|
15152
|
+
// A connect records INTENT: a harness is advertised only when its bundled
|
|
15153
|
+
// binary also resolves. On success: persist, refresh and beat at once, so the
|
|
15154
|
+
// answer is true in Cabane promptly either way. Never throws for a valid input.
|
|
14374
15155
|
async enableHarness(input) {
|
|
14375
15156
|
let next;
|
|
14376
|
-
let
|
|
14377
|
-
if (input.runtime === "claude-code") {
|
|
14378
|
-
const
|
|
14379
|
-
|
|
15157
|
+
let outcome;
|
|
15158
|
+
if (input.runtime === "claude-code" || input.runtime === "codex") {
|
|
15159
|
+
const runtime = input.runtime;
|
|
15160
|
+
const connected = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
15161
|
+
const check = this.dockerRunners() ? void 0 : await this.checkHarness(runtime);
|
|
15162
|
+
if (check && !isUsable(check)) {
|
|
15163
|
+
await this.refreshHarnessStatuses();
|
|
15164
|
+
this.kickHeartbeat();
|
|
15165
|
+
return { ok: false, outcome: { kind: "refused", runtime, check, connected } };
|
|
15166
|
+
}
|
|
15167
|
+
const label = this.config.deviceLabel;
|
|
15168
|
+
if (connected) {
|
|
15169
|
+
await this.refreshHarnessStatuses();
|
|
15170
|
+
this.kickHeartbeat();
|
|
14380
15171
|
return {
|
|
14381
|
-
ok:
|
|
14382
|
-
|
|
14383
|
-
|
|
15172
|
+
ok: true,
|
|
15173
|
+
outcome: {
|
|
15174
|
+
kind: "already",
|
|
15175
|
+
runtime,
|
|
15176
|
+
...check ? { check } : {},
|
|
15177
|
+
...label ? { deviceLabel: label } : {}
|
|
15178
|
+
}
|
|
14384
15179
|
};
|
|
14385
15180
|
}
|
|
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 } };
|
|
15181
|
+
next = runtime === "codex" ? { ...this.config, codex: { enabled: true } } : { ...this.config, claudeCode: { enabled: true } };
|
|
15182
|
+
outcome = { kind: "connected", runtime, ...check ? { check } : {} };
|
|
14395
15183
|
} else {
|
|
14396
15184
|
const serverUrl = input.serverUrl.trim();
|
|
14397
15185
|
const parsed = companionConfigSchema.shape.opencode.safeParse({ serverUrl });
|
|
14398
15186
|
if (!parsed.success) {
|
|
14399
15187
|
return {
|
|
14400
15188
|
ok: false,
|
|
14401
|
-
|
|
15189
|
+
outcome: {
|
|
15190
|
+
kind: "opencode",
|
|
15191
|
+
message: "That doesn\u2019t look like a valid URL (e.g. http://127.0.0.1:4096)."
|
|
15192
|
+
}
|
|
14402
15193
|
};
|
|
14403
15194
|
}
|
|
14404
15195
|
const boundedFetch = ((url, init2) => fetch(url, { ...init2, signal: AbortSignal.timeout(4e3) }));
|
|
@@ -14406,19 +15197,21 @@ var CompanionSupervisor = class {
|
|
|
14406
15197
|
if (version === null) {
|
|
14407
15198
|
return {
|
|
14408
15199
|
ok: false,
|
|
14409
|
-
|
|
15200
|
+
outcome: {
|
|
15201
|
+
kind: "opencode",
|
|
15202
|
+
message: `Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
|
|
15203
|
+
}
|
|
14410
15204
|
};
|
|
14411
15205
|
}
|
|
14412
15206
|
next = { ...this.config, opencode: { serverUrl } };
|
|
15207
|
+
outcome = { kind: "connected", runtime: "opencode" };
|
|
14413
15208
|
}
|
|
14414
15209
|
this.config = next;
|
|
14415
15210
|
saveConfig(next);
|
|
14416
15211
|
this.rebuildDispatchers();
|
|
14417
|
-
await this.refreshHarnessStatuses(
|
|
14418
|
-
checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
|
|
14419
|
-
);
|
|
15212
|
+
await this.refreshHarnessStatuses();
|
|
14420
15213
|
this.kickHeartbeat();
|
|
14421
|
-
return { ok: true };
|
|
15214
|
+
return { ok: true, outcome };
|
|
14422
15215
|
}
|
|
14423
15216
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
14424
15217
|
// the runner (its api, outbox, chains) intact, so constructor-captured options
|
|
@@ -14441,9 +15234,54 @@ var CompanionSupervisor = class {
|
|
|
14441
15234
|
}
|
|
14442
15235
|
}
|
|
14443
15236
|
}
|
|
15237
|
+
// A long-lived device keeps a session archive per conversation and agent it ever ran; those
|
|
15238
|
+
// nobody has used in a month go.
|
|
15239
|
+
pruneSessionArchives() {
|
|
15240
|
+
try {
|
|
15241
|
+
const removed = pruneIdleSessionArchives(cabaneDir());
|
|
15242
|
+
if (removed.length > 0)
|
|
15243
|
+
this.log.info({ removed: removed.length }, "runners: removed idle session archives");
|
|
15244
|
+
} catch (err) {
|
|
15245
|
+
this.log.warn(
|
|
15246
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
15247
|
+
"runners: couldn't remove idle session archives"
|
|
15248
|
+
);
|
|
15249
|
+
}
|
|
15250
|
+
}
|
|
15251
|
+
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
15252
|
+
// in progress. What a self-update swap waits for.
|
|
15253
|
+
idle() {
|
|
15254
|
+
if (this.claims.size > 0 || this.recovering) return false;
|
|
15255
|
+
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
15256
|
+
return true;
|
|
15257
|
+
}
|
|
15258
|
+
// The bundled harnesses this device has connected, which a new version must still carry.
|
|
15259
|
+
connectedBundledHarnesses() {
|
|
15260
|
+
return [
|
|
15261
|
+
...isClaudeCodeConnected(this.config) ? ["claude-code"] : [],
|
|
15262
|
+
...isCodexEnabled(this.config) ? ["codex"] : []
|
|
15263
|
+
];
|
|
15264
|
+
}
|
|
15265
|
+
// `cabane update`, over the control socket: set whether this companion keeps itself current
|
|
15266
|
+
// (`auto`), or install the latest now.
|
|
15267
|
+
async update(auto) {
|
|
15268
|
+
if (auto !== void 0) {
|
|
15269
|
+
this.config = { ...this.config, update: { auto } };
|
|
15270
|
+
saveConfig(this.config);
|
|
15271
|
+
if (auto) this.kickHeartbeat();
|
|
15272
|
+
return {
|
|
15273
|
+
ok: true,
|
|
15274
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
15275
|
+
};
|
|
15276
|
+
}
|
|
15277
|
+
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
|
15278
|
+
return this.updater.updateNow();
|
|
15279
|
+
}
|
|
14444
15280
|
// ---- control actions ----
|
|
14445
15281
|
async shutdown() {
|
|
14446
15282
|
this.stopped = true;
|
|
15283
|
+
this.updater?.stop();
|
|
15284
|
+
if (this.archivePruneTimer) clearInterval(this.archivePruneTimer);
|
|
14447
15285
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
14448
15286
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
14449
15287
|
const heartbeat = this.inFlightHeartbeat;
|
|
@@ -14550,22 +15388,22 @@ function handleUncaught(log, err, origin) {
|
|
|
14550
15388
|
}
|
|
14551
15389
|
|
|
14552
15390
|
// src/crash-marker.ts
|
|
14553
|
-
import { existsSync as
|
|
14554
|
-
import { join as
|
|
15391
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
15392
|
+
import { join as join27 } from "path";
|
|
14555
15393
|
function crashMarkerPath() {
|
|
14556
|
-
return
|
|
15394
|
+
return join27(cabaneDir(), "last-error.json");
|
|
14557
15395
|
}
|
|
14558
15396
|
function recordCrash(rec) {
|
|
14559
15397
|
try {
|
|
14560
|
-
|
|
14561
|
-
|
|
15398
|
+
mkdirSync17(cabaneDir(), { recursive: true });
|
|
15399
|
+
writeFileSync11(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
14562
15400
|
} catch {
|
|
14563
15401
|
}
|
|
14564
15402
|
}
|
|
14565
15403
|
function clearCrash() {
|
|
14566
15404
|
try {
|
|
14567
15405
|
const path = crashMarkerPath();
|
|
14568
|
-
if (
|
|
15406
|
+
if (existsSync18(path)) rmSync15(path, { force: true });
|
|
14569
15407
|
} catch {
|
|
14570
15408
|
}
|
|
14571
15409
|
}
|
|
@@ -14575,17 +15413,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14575
15413
|
const log = getLogger();
|
|
14576
15414
|
installProcessSafetyNet(log);
|
|
14577
15415
|
let cfg;
|
|
14578
|
-
let claudeCode;
|
|
14579
15416
|
try {
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
onMigrated: (migrated, onPath) => log.debug(
|
|
15417
|
+
cfg = requireStartConfig({
|
|
15418
|
+
onMigrated: (migrated) => log.debug(
|
|
14583
15419
|
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
14584
|
-
|
|
15420
|
+
"companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)"
|
|
14585
15421
|
)
|
|
14586
|
-
})
|
|
14587
|
-
|
|
14588
|
-
probeClaude: async () => claudeCode,
|
|
15422
|
+
});
|
|
15423
|
+
warnAboutHarnessReadiness(cfg, {
|
|
14589
15424
|
// The CLI's onboarding script says this in its own words, so it hands a
|
|
14590
15425
|
// sink that logs instead. Every other caller keeps the stderr line.
|
|
14591
15426
|
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
@@ -14600,13 +15435,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14600
15435
|
throw err;
|
|
14601
15436
|
}
|
|
14602
15437
|
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
15438
|
const instanceId = randomUUID7();
|
|
14611
15439
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14612
15440
|
const pre = readLiveRuntimeState();
|
|
@@ -14630,39 +15458,39 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14630
15458
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
14631
15459
|
instanceId
|
|
14632
15460
|
});
|
|
15461
|
+
let restartForUpdate = async () => {
|
|
15462
|
+
};
|
|
14633
15463
|
const supervisor = new CompanionSupervisor({
|
|
14634
15464
|
config: cfg,
|
|
14635
15465
|
log,
|
|
14636
15466
|
hub,
|
|
14637
|
-
|
|
15467
|
+
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
|
|
15468
|
+
restartForUpdate: () => restartForUpdate()
|
|
14638
15469
|
});
|
|
14639
15470
|
await supervisor.start();
|
|
14640
15471
|
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) };
|
|
15472
|
+
if (runtime !== "opencode") return (await supervisor.enableHarness({ runtime })).outcome;
|
|
15473
|
+
const configured = supervisor.currentConfig().opencode?.serverUrl;
|
|
15474
|
+
const resolved = await resolveOpencodeServerUrl(configured, serverUrl);
|
|
15475
|
+
if (!resolved) {
|
|
15476
|
+
return {
|
|
15477
|
+
kind: "opencode",
|
|
15478
|
+
message: "No opencode server is answering at http://127.0.0.1:4096. Start `opencode serve`, or pass its address with `--url`."
|
|
15479
|
+
};
|
|
14650
15480
|
}
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
return {
|
|
15481
|
+
if (!serverUrl?.trim() && configured === resolved) {
|
|
15482
|
+
const label = supervisor.currentConfig().deviceLabel;
|
|
15483
|
+
return { kind: "already", runtime, ...label ? { deviceLabel: label } : {} };
|
|
15484
|
+
}
|
|
15485
|
+
return (await supervisor.enableHarness({ runtime: "opencode", serverUrl: resolved })).outcome;
|
|
14656
15486
|
};
|
|
14657
15487
|
let control = null;
|
|
14658
15488
|
let dashboard = null;
|
|
14659
15489
|
try {
|
|
14660
15490
|
control = await startControlServer({
|
|
14661
15491
|
status: () => hub.statusJson(),
|
|
14662
|
-
connect:
|
|
14663
|
-
|
|
14664
|
-
return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
|
|
14665
|
-
},
|
|
15492
|
+
connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
|
|
15493
|
+
update: (auto) => supervisor.update(auto),
|
|
14666
15494
|
stop: () => void supervisor.requestStop()
|
|
14667
15495
|
});
|
|
14668
15496
|
if (opts.dashboard) {
|
|
@@ -14683,7 +15511,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14683
15511
|
// marker records whether this companion is backgrounded (foreground start
|
|
14684
15512
|
// leaves it unset → false).
|
|
14685
15513
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
14686
|
-
instanceId
|
|
15514
|
+
instanceId,
|
|
15515
|
+
...installedCommandPath() ? { command: installedCommandPath() } : {}
|
|
14687
15516
|
});
|
|
14688
15517
|
clearCrash();
|
|
14689
15518
|
} catch (err) {
|
|
@@ -14714,6 +15543,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14714
15543
|
} catch {
|
|
14715
15544
|
}
|
|
14716
15545
|
};
|
|
15546
|
+
restartForUpdate = async () => {
|
|
15547
|
+
await stop();
|
|
15548
|
+
process.exit(0);
|
|
15549
|
+
};
|
|
14717
15550
|
return {
|
|
14718
15551
|
ok: true,
|
|
14719
15552
|
runtime: {
|