@cabane/companion 0.6.32 → 0.6.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1777 -1077
- package/dist/pairing-config.js +36 -21
- package/dist/runtime.js +653 -336
- package/package.json +2 -1
package/dist/runtime.js
CHANGED
|
@@ -287,10 +287,11 @@ var companionConfigSchema = z2.object({
|
|
|
287
287
|
// device with no device-level entry behaves exactly as it did before. A hook
|
|
288
288
|
// that fails still fails the dispatch loudly, as now.
|
|
289
289
|
prepareHook: prepareHookSchema.optional(),
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
290
|
+
// CT1085 §9: `dashboardPort` and `autoOpen` are READ AND IGNORED by the CLI —
|
|
291
|
+
// it binds no port and opens no browser. They stay in the schema (rather than
|
|
292
|
+
// becoming a strict-mode parse error on an existing config file) and still mean
|
|
293
|
+
// what they say to the Electron shell, the one front-end that renders a
|
|
294
|
+
// dashboard. `logLevel` is unaffected: pino level, live-editable.
|
|
294
295
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
295
296
|
autoOpen: z2.boolean().optional(),
|
|
296
297
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
@@ -460,21 +461,36 @@ function consoleMessageFormat(log, messageKey) {
|
|
|
460
461
|
return short ? `${short} ${msg}` : msg;
|
|
461
462
|
}
|
|
462
463
|
var cached = null;
|
|
463
|
-
|
|
464
|
-
|
|
464
|
+
var consoleLogging = true;
|
|
465
|
+
function createLogger(destinations = {}) {
|
|
465
466
|
const path = companionLogPath();
|
|
466
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
467
|
+
if (!destinations.file) mkdirSync2(dirname2(path), { recursive: true });
|
|
467
468
|
const streams = [];
|
|
468
469
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
469
470
|
const consoleStream = pretty({
|
|
470
471
|
colorize: true,
|
|
471
472
|
ignore: CONSOLE_IGNORE,
|
|
472
|
-
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey)
|
|
473
|
+
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
|
|
474
|
+
...destinations.console ? { destination: destinations.console } : {}
|
|
475
|
+
});
|
|
476
|
+
streams.push({
|
|
477
|
+
level: "info",
|
|
478
|
+
stream: {
|
|
479
|
+
write(chunk) {
|
|
480
|
+
if (consoleLogging) consoleStream.write(chunk);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
473
483
|
});
|
|
474
|
-
streams.push({ level: "info", stream: consoleStream });
|
|
475
484
|
}
|
|
476
|
-
streams.push({
|
|
477
|
-
|
|
485
|
+
streams.push({
|
|
486
|
+
level: "debug",
|
|
487
|
+
stream: destinations.file ?? createWriteStream(path, { flags: "a" })
|
|
488
|
+
});
|
|
489
|
+
return pino({ level: "debug" }, pino.multistream(streams));
|
|
490
|
+
}
|
|
491
|
+
function getLogger() {
|
|
492
|
+
if (cached) return cached;
|
|
493
|
+
cached = createLogger();
|
|
478
494
|
return cached;
|
|
479
495
|
}
|
|
480
496
|
|
|
@@ -950,6 +966,163 @@ function resolveStaticDir() {
|
|
|
950
966
|
return join4(dirname3(fileURLToPath(import.meta.url)), "static");
|
|
951
967
|
}
|
|
952
968
|
|
|
969
|
+
// src/control-socket.ts
|
|
970
|
+
import { createHash } from "crypto";
|
|
971
|
+
import { existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
972
|
+
import { createServer, connect } from "net";
|
|
973
|
+
import { join as join5 } from "path";
|
|
974
|
+
var CONTROL_TIMEOUT_MS = 1e3;
|
|
975
|
+
function controlSocketPath() {
|
|
976
|
+
const dir2 = cabaneDir();
|
|
977
|
+
if (process.platform === "win32") {
|
|
978
|
+
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
979
|
+
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
980
|
+
}
|
|
981
|
+
return join5(dir2, "companion.sock");
|
|
982
|
+
}
|
|
983
|
+
async function startControlServer(handlers) {
|
|
984
|
+
const path = controlSocketPath();
|
|
985
|
+
mkdirSync3(cabaneDir(), { recursive: true });
|
|
986
|
+
if (process.platform !== "win32" && existsSync3(path)) {
|
|
987
|
+
const alive = await ping(path);
|
|
988
|
+
if (alive) throw new Error(`another companion is already listening on ${path}`);
|
|
989
|
+
rmSync2(path, { force: true });
|
|
990
|
+
}
|
|
991
|
+
const server = createServer((socket) => {
|
|
992
|
+
void serveConnection(socket, handlers);
|
|
993
|
+
});
|
|
994
|
+
server.unref();
|
|
995
|
+
await new Promise((resolve, reject) => {
|
|
996
|
+
server.once("error", reject);
|
|
997
|
+
server.listen(path, () => {
|
|
998
|
+
server.removeListener("error", reject);
|
|
999
|
+
resolve();
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
server.on("error", () => {
|
|
1003
|
+
});
|
|
1004
|
+
return {
|
|
1005
|
+
path,
|
|
1006
|
+
close: () => new Promise((resolve) => {
|
|
1007
|
+
server.close(() => {
|
|
1008
|
+
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
1009
|
+
resolve();
|
|
1010
|
+
});
|
|
1011
|
+
})
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
async function serveConnection(socket, handlers) {
|
|
1015
|
+
socket.on("error", () => socket.destroy());
|
|
1016
|
+
const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
|
|
1017
|
+
if (line === null) {
|
|
1018
|
+
socket.destroy();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
let req;
|
|
1022
|
+
try {
|
|
1023
|
+
req = JSON.parse(line);
|
|
1024
|
+
} catch {
|
|
1025
|
+
reply(socket, { error: "malformed request" });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
if (req.cmd === "status") {
|
|
1030
|
+
reply(socket, handlers.status());
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (req.cmd === "connect") {
|
|
1034
|
+
const result = await handlers.connect(req.runtime, req.serverUrl);
|
|
1035
|
+
reply(socket, result);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (req.cmd === "stop") {
|
|
1039
|
+
reply(socket, { ok: true });
|
|
1040
|
+
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
reply(socket, { error: `unknown command "${String(req.cmd)}"` });
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
reply(socket, { error: err instanceof Error ? err.message : String(err) });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function reply(socket, body) {
|
|
1049
|
+
try {
|
|
1050
|
+
socket.end(`${JSON.stringify(body)}
|
|
1051
|
+
`);
|
|
1052
|
+
} catch {
|
|
1053
|
+
socket.destroy();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
1057
|
+
const socket = connect(path);
|
|
1058
|
+
try {
|
|
1059
|
+
await new Promise((resolve, reject) => {
|
|
1060
|
+
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
1061
|
+
timer.unref?.();
|
|
1062
|
+
socket.once("connect", () => {
|
|
1063
|
+
clearTimeout(timer);
|
|
1064
|
+
resolve();
|
|
1065
|
+
});
|
|
1066
|
+
socket.once("error", (err) => {
|
|
1067
|
+
clearTimeout(timer);
|
|
1068
|
+
reject(err);
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
socket.write(`${JSON.stringify(req)}
|
|
1072
|
+
`);
|
|
1073
|
+
const line = await readLine(socket, timeoutMs);
|
|
1074
|
+
if (line === null) throw new ControlTimeout();
|
|
1075
|
+
return JSON.parse(line);
|
|
1076
|
+
} finally {
|
|
1077
|
+
socket.destroy();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
var ControlTimeout = class extends Error {
|
|
1081
|
+
constructor() {
|
|
1082
|
+
super("the companion did not answer its control socket in time");
|
|
1083
|
+
this.name = "ControlTimeout";
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
function isNotListening(err) {
|
|
1087
|
+
const code = err?.code;
|
|
1088
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
1089
|
+
}
|
|
1090
|
+
function readLine(socket, timeoutMs) {
|
|
1091
|
+
return new Promise((resolve) => {
|
|
1092
|
+
let buf = "";
|
|
1093
|
+
let settled = false;
|
|
1094
|
+
const done = (v) => {
|
|
1095
|
+
if (settled) return;
|
|
1096
|
+
settled = true;
|
|
1097
|
+
clearTimeout(timer);
|
|
1098
|
+
socket.removeListener("data", onData);
|
|
1099
|
+
resolve(v);
|
|
1100
|
+
};
|
|
1101
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
1102
|
+
timer.unref?.();
|
|
1103
|
+
const onData = (chunk) => {
|
|
1104
|
+
buf += chunk.toString("utf8");
|
|
1105
|
+
const nl = buf.indexOf("\n");
|
|
1106
|
+
if (nl >= 0) done(buf.slice(0, nl));
|
|
1107
|
+
else if (buf.length > 1e6) done(null);
|
|
1108
|
+
};
|
|
1109
|
+
socket.on("data", onData);
|
|
1110
|
+
socket.once("close", () => done(null));
|
|
1111
|
+
socket.once("error", () => done(null));
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async function ping(path) {
|
|
1115
|
+
try {
|
|
1116
|
+
await controlRequest(path, { cmd: "status" });
|
|
1117
|
+
return true;
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
return !isNotListening(err);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/harness-check.ts
|
|
1124
|
+
import { spawn as spawn4 } from "child_process";
|
|
1125
|
+
|
|
953
1126
|
// src/harness-versions.ts
|
|
954
1127
|
import { spawn as spawn2 } from "child_process";
|
|
955
1128
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
@@ -1031,6 +1204,20 @@ async function safe(fn) {
|
|
|
1031
1204
|
}
|
|
1032
1205
|
}
|
|
1033
1206
|
|
|
1207
|
+
// src/manifest.ts
|
|
1208
|
+
var DEVICE_MANIFEST = {
|
|
1209
|
+
runtimes: [{ name: "claude-code", version: null }],
|
|
1210
|
+
capabilities: { hostFs: true, browser: true, userMcp: true }
|
|
1211
|
+
};
|
|
1212
|
+
function buildCompanionManifest(opts) {
|
|
1213
|
+
const v = opts.versions ?? {};
|
|
1214
|
+
const runtimes = [];
|
|
1215
|
+
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
1216
|
+
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
1217
|
+
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
1218
|
+
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1034
1221
|
// src/prereqs.ts
|
|
1035
1222
|
import { spawn as spawn3 } from "child_process";
|
|
1036
1223
|
async function claudeOnPath() {
|
|
@@ -1102,33 +1289,303 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1102
1289
|
);
|
|
1103
1290
|
}
|
|
1104
1291
|
|
|
1292
|
+
// src/harness-status.ts
|
|
1293
|
+
var HARNESS_LABELS = {
|
|
1294
|
+
"claude-code": "Claude Code",
|
|
1295
|
+
codex: "Codex",
|
|
1296
|
+
opencode: "opencode"
|
|
1297
|
+
};
|
|
1298
|
+
var LABELS = HARNESS_LABELS;
|
|
1299
|
+
function deriveHarnessSnapshot(signals) {
|
|
1300
|
+
const advertised = new Set(
|
|
1301
|
+
buildCompanionManifest({
|
|
1302
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
1303
|
+
// through the same function rather than re-decided.
|
|
1304
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
1305
|
+
opencode: signals.opencodeConfigured,
|
|
1306
|
+
codex: signals.codexEnabled
|
|
1307
|
+
}).runtimes.map((r) => r.name)
|
|
1308
|
+
);
|
|
1309
|
+
const harnesses = [
|
|
1310
|
+
deriveClaudeCode(signals, advertised.has("claude-code")),
|
|
1311
|
+
deriveCodex(signals, advertised.has("codex")),
|
|
1312
|
+
deriveOpencode(signals, advertised.has("opencode"))
|
|
1313
|
+
];
|
|
1314
|
+
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
1315
|
+
}
|
|
1316
|
+
function deriveClaudeCode(signals, manifestHas) {
|
|
1317
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
1318
|
+
if (manifestHas) {
|
|
1319
|
+
return {
|
|
1320
|
+
...base,
|
|
1321
|
+
state: "exposed",
|
|
1322
|
+
version: signals.claudeVersion,
|
|
1323
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
1324
|
+
enable: null
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (signals.claudeCodeConnected) {
|
|
1328
|
+
return {
|
|
1329
|
+
...base,
|
|
1330
|
+
state: "needs_attention",
|
|
1331
|
+
version: null,
|
|
1332
|
+
detail: "Connected, but the `claude` CLI isn\u2019t on your PATH. Install it (`npm i -g @anthropic-ai/claude-code`) and sign in, or disconnect it.",
|
|
1333
|
+
enable: null
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (signals.claudeOnPath) {
|
|
1337
|
+
return {
|
|
1338
|
+
...base,
|
|
1339
|
+
state: "detected_not_exposed",
|
|
1340
|
+
version: signals.claudeVersion,
|
|
1341
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
1342
|
+
enable: "claude-code"
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
...base,
|
|
1347
|
+
state: "not_detected",
|
|
1348
|
+
version: null,
|
|
1349
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
1350
|
+
enable: null
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function deriveCodex(signals, manifestHas) {
|
|
1354
|
+
const base = { runtime: "codex", label: LABELS.codex };
|
|
1355
|
+
if (manifestHas) {
|
|
1356
|
+
if (signals.codexOnPath) {
|
|
1357
|
+
return {
|
|
1358
|
+
...base,
|
|
1359
|
+
state: "exposed",
|
|
1360
|
+
version: signals.codexVersion,
|
|
1361
|
+
detail: "Codex is enabled and exposed to Cabane.",
|
|
1362
|
+
enable: null
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
...base,
|
|
1367
|
+
state: "needs_attention",
|
|
1368
|
+
version: null,
|
|
1369
|
+
detail: "Enabled, but the `codex` CLI isn\u2019t on your PATH. Install it and sign in (`codex login`), or turn Codex off.",
|
|
1370
|
+
enable: null
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (signals.codexOnPath) {
|
|
1374
|
+
return {
|
|
1375
|
+
...base,
|
|
1376
|
+
state: "detected_not_exposed",
|
|
1377
|
+
version: signals.codexVersion,
|
|
1378
|
+
detail: "Codex is installed but not exposed yet. Turn it on to let Cabane run Codex here.",
|
|
1379
|
+
enable: "codex"
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return {
|
|
1383
|
+
...base,
|
|
1384
|
+
state: "not_detected",
|
|
1385
|
+
version: null,
|
|
1386
|
+
detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
|
|
1387
|
+
enable: null
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function deriveOpencode(signals, manifestHas) {
|
|
1391
|
+
const base = { runtime: "opencode", label: LABELS.opencode };
|
|
1392
|
+
if (manifestHas) {
|
|
1393
|
+
if (signals.opencodeReachable) {
|
|
1394
|
+
return {
|
|
1395
|
+
...base,
|
|
1396
|
+
state: "exposed",
|
|
1397
|
+
version: signals.opencodeVersion,
|
|
1398
|
+
detail: "An opencode server is reachable and exposed to Cabane.",
|
|
1399
|
+
enable: null
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
...base,
|
|
1404
|
+
state: "needs_attention",
|
|
1405
|
+
version: null,
|
|
1406
|
+
detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
|
|
1407
|
+
enable: null
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
...base,
|
|
1412
|
+
state: "not_detected",
|
|
1413
|
+
version: null,
|
|
1414
|
+
detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
|
|
1415
|
+
enable: "opencode"
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function detectedRuntimesFor(snapshot) {
|
|
1419
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
1420
|
+
}
|
|
1421
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
1422
|
+
async function probeHarnessSignals(cfg, deps = {}) {
|
|
1423
|
+
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
1424
|
+
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
1425
|
+
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
1426
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1427
|
+
const serverUrl = cfg.opencode?.serverUrl;
|
|
1428
|
+
const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
1429
|
+
withTimeout(probeClaudePresence(), false),
|
|
1430
|
+
withTimeout(probeClaudeVersion(), null),
|
|
1431
|
+
withTimeout(probeCodexVersion(), null),
|
|
1432
|
+
serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
|
|
1433
|
+
]);
|
|
1434
|
+
return {
|
|
1435
|
+
claudeOnPath: claudeOnPathResult,
|
|
1436
|
+
claudeVersion,
|
|
1437
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
1438
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
1439
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
1440
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
1441
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
1442
|
+
codexOnPath: codexVersion !== null,
|
|
1443
|
+
codexVersion,
|
|
1444
|
+
codexEnabled: isCodexEnabled(cfg),
|
|
1445
|
+
opencodeConfigured: !!serverUrl,
|
|
1446
|
+
// A version came back ⟺ the serve answered its health endpoint (CT584).
|
|
1447
|
+
opencodeReachable: opencodeVersion !== null,
|
|
1448
|
+
opencodeVersion
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
function withTimeout(promise, fallback) {
|
|
1452
|
+
return new Promise((resolve) => {
|
|
1453
|
+
let settled = false;
|
|
1454
|
+
const done = (v) => {
|
|
1455
|
+
if (!settled) {
|
|
1456
|
+
settled = true;
|
|
1457
|
+
resolve(v);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
|
|
1461
|
+
timer.unref?.();
|
|
1462
|
+
promise.then(
|
|
1463
|
+
(v) => {
|
|
1464
|
+
clearTimeout(timer);
|
|
1465
|
+
done(v);
|
|
1466
|
+
},
|
|
1467
|
+
() => {
|
|
1468
|
+
clearTimeout(timer);
|
|
1469
|
+
done(fallback);
|
|
1470
|
+
}
|
|
1471
|
+
);
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/harness-check.ts
|
|
1476
|
+
var CHECK_TIMEOUT_MS = 4e3;
|
|
1477
|
+
async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
1478
|
+
const run = deps.run ?? runBounded;
|
|
1479
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1480
|
+
try {
|
|
1481
|
+
if (runtime === "opencode") {
|
|
1482
|
+
const url = cfg.opencode?.serverUrl;
|
|
1483
|
+
if (!url) return "absent";
|
|
1484
|
+
return await probeOpencode(url) !== null ? "ok" : "failed";
|
|
1485
|
+
}
|
|
1486
|
+
const { auth, presence } = runtime === "codex" ? {
|
|
1487
|
+
auth: ["codex", ["login", "status"]],
|
|
1488
|
+
presence: ["codex", ["--version"]]
|
|
1489
|
+
} : {
|
|
1490
|
+
auth: ["claude", ["auth", "status"]],
|
|
1491
|
+
presence: ["claude", ["--version"]]
|
|
1492
|
+
};
|
|
1493
|
+
const presenceRun = await run(presence[0], [...presence[1]]);
|
|
1494
|
+
if (presenceRun.error === "spawn") return "absent";
|
|
1495
|
+
if (presenceRun.code !== 0) return "unverified";
|
|
1496
|
+
const authRun = await run(auth[0], [...auth[1]]);
|
|
1497
|
+
if (authRun.code === 0) return "ok";
|
|
1498
|
+
if (looksUnsupported(authRun.output)) {
|
|
1499
|
+
return "unverified";
|
|
1500
|
+
}
|
|
1501
|
+
return "failed";
|
|
1502
|
+
} catch {
|
|
1503
|
+
return "unverified";
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function connectedLine(runtime, verdict) {
|
|
1507
|
+
if (verdict === "absent") throw new Error("an absent harness cannot be connected");
|
|
1508
|
+
const label = HARNESS_LABELS[runtime];
|
|
1509
|
+
if (verdict !== "failed") return `${label} connected.`;
|
|
1510
|
+
return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
|
|
1511
|
+
}
|
|
1512
|
+
var FAILED_SUFFIX = {
|
|
1513
|
+
"claude-code": "it doesn\u2019t look signed in yet. Run `claude` once and sign in, then it\u2019s ready.",
|
|
1514
|
+
codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
|
|
1515
|
+
opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
|
|
1516
|
+
};
|
|
1517
|
+
function absentLine(runtime) {
|
|
1518
|
+
if (runtime === "opencode") {
|
|
1519
|
+
return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
|
|
1520
|
+
}
|
|
1521
|
+
const label = HARNESS_LABELS[runtime];
|
|
1522
|
+
const login = runtime === "codex" ? "`codex login`" : "`claude`";
|
|
1523
|
+
return `${label} isn\u2019t installed on this machine \u2014 install it and sign in (${login}), then run this again.`;
|
|
1524
|
+
}
|
|
1525
|
+
function looksUnsupported(output) {
|
|
1526
|
+
return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
|
|
1527
|
+
output
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
function runBounded(command, args) {
|
|
1531
|
+
return new Promise((resolve) => {
|
|
1532
|
+
let settled = false;
|
|
1533
|
+
const done = (result) => {
|
|
1534
|
+
if (settled) return;
|
|
1535
|
+
settled = true;
|
|
1536
|
+
clearTimeout(timer);
|
|
1537
|
+
resolve(result);
|
|
1538
|
+
};
|
|
1539
|
+
let child;
|
|
1540
|
+
try {
|
|
1541
|
+
child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1542
|
+
} catch {
|
|
1543
|
+
resolve({ code: null, output: "", error: "spawn" });
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
let out = "";
|
|
1547
|
+
const capture = (chunk) => {
|
|
1548
|
+
if (out.length < 4096) out += chunk.toString();
|
|
1549
|
+
};
|
|
1550
|
+
child.stdout?.on("data", capture);
|
|
1551
|
+
child.stderr?.on("data", capture);
|
|
1552
|
+
const timer = setTimeout(() => {
|
|
1553
|
+
child.kill("SIGKILL");
|
|
1554
|
+
done({ code: null, output: out, error: "timeout" });
|
|
1555
|
+
}, CHECK_TIMEOUT_MS);
|
|
1556
|
+
timer.unref?.();
|
|
1557
|
+
child.once("error", () => done({ code: null, output: out, error: "spawn" }));
|
|
1558
|
+
child.once("exit", (code) => done({ code, output: out }));
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1105
1562
|
// src/runtime-file.ts
|
|
1106
1563
|
import {
|
|
1107
|
-
existsSync as
|
|
1564
|
+
existsSync as existsSync4,
|
|
1108
1565
|
readFileSync as readFileSync2,
|
|
1109
|
-
rmSync as
|
|
1566
|
+
rmSync as rmSync3,
|
|
1110
1567
|
writeFileSync as writeFileSync2,
|
|
1111
|
-
mkdirSync as
|
|
1568
|
+
mkdirSync as mkdirSync4,
|
|
1112
1569
|
openSync as openSync2,
|
|
1113
1570
|
closeSync as closeSync2
|
|
1114
1571
|
} from "fs";
|
|
1115
|
-
import { join as
|
|
1116
|
-
var
|
|
1572
|
+
import { join as join6 } from "path";
|
|
1573
|
+
var PROBE_TIMEOUT_MS2 = 1e3;
|
|
1117
1574
|
function runtimePath() {
|
|
1118
|
-
return
|
|
1575
|
+
return join6(cabaneDir(), "runtime.json");
|
|
1119
1576
|
}
|
|
1120
1577
|
function serialize(state) {
|
|
1121
1578
|
return JSON.stringify(state, null, 2) + "\n";
|
|
1122
1579
|
}
|
|
1123
1580
|
function writeRuntimeState(state) {
|
|
1124
1581
|
const path = runtimePath();
|
|
1125
|
-
|
|
1582
|
+
mkdirSync4(cabaneDir(), { recursive: true });
|
|
1126
1583
|
writeFileSync2(path, serialize(state), "utf8");
|
|
1127
1584
|
}
|
|
1128
1585
|
function acquireRuntimeState(state) {
|
|
1129
1586
|
const live = readLiveRuntimeState();
|
|
1130
1587
|
if (live) return { acquired: false, existing: live };
|
|
1131
|
-
|
|
1588
|
+
mkdirSync4(cabaneDir(), { recursive: true });
|
|
1132
1589
|
let fd;
|
|
1133
1590
|
try {
|
|
1134
1591
|
fd = openSync2(runtimePath(), "wx");
|
|
@@ -1144,11 +1601,21 @@ function acquireRuntimeState(state) {
|
|
|
1144
1601
|
}
|
|
1145
1602
|
function clearRuntimeState() {
|
|
1146
1603
|
const path = runtimePath();
|
|
1147
|
-
if (
|
|
1604
|
+
if (existsSync4(path)) rmSync3(path, { force: true });
|
|
1605
|
+
}
|
|
1606
|
+
function clearRuntimeStateIfOurs(instanceId) {
|
|
1607
|
+
const path = runtimePath();
|
|
1608
|
+
if (!existsSync4(path)) return;
|
|
1609
|
+
try {
|
|
1610
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1611
|
+
if (parsed.instanceId && parsed.instanceId !== instanceId) return;
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
rmSync3(path, { force: true });
|
|
1148
1615
|
}
|
|
1149
1616
|
function readLiveRuntimeState() {
|
|
1150
1617
|
const path = runtimePath();
|
|
1151
|
-
if (!
|
|
1618
|
+
if (!existsSync4(path)) return null;
|
|
1152
1619
|
let parsed;
|
|
1153
1620
|
try {
|
|
1154
1621
|
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -1164,33 +1631,21 @@ function readLiveRuntimeState() {
|
|
|
1164
1631
|
}
|
|
1165
1632
|
return parsed;
|
|
1166
1633
|
}
|
|
1167
|
-
async function verifyRuntime(state,
|
|
1168
|
-
if (!state.instanceId) return "unknown";
|
|
1169
|
-
const url = `${trimSlash(state.url)}/api/status`;
|
|
1170
|
-
let res;
|
|
1171
|
-
try {
|
|
1172
|
-
res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
1173
|
-
} catch (err) {
|
|
1174
|
-
return isConnRefused(err) ? "stale" : "unknown";
|
|
1175
|
-
}
|
|
1176
|
-
if (!res.ok) return "unknown";
|
|
1634
|
+
async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
1635
|
+
if (!state.instanceId || !state.socket) return "unknown";
|
|
1177
1636
|
let body;
|
|
1178
1637
|
try {
|
|
1179
|
-
body = await
|
|
1180
|
-
|
|
1181
|
-
|
|
1638
|
+
body = await requestImpl(
|
|
1639
|
+
state.socket,
|
|
1640
|
+
{ cmd: "status" },
|
|
1641
|
+
PROBE_TIMEOUT_MS2
|
|
1642
|
+
);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
return isNotListening(err) ? "stale" : "unknown";
|
|
1182
1645
|
}
|
|
1183
|
-
if (typeof body
|
|
1646
|
+
if (typeof body?.instance_id !== "string") return "unknown";
|
|
1184
1647
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1185
1648
|
}
|
|
1186
|
-
function isConnRefused(err) {
|
|
1187
|
-
if (!err || typeof err !== "object") return false;
|
|
1188
|
-
const cause = err.cause;
|
|
1189
|
-
return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
|
|
1190
|
-
}
|
|
1191
|
-
function trimSlash(s) {
|
|
1192
|
-
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
1193
|
-
}
|
|
1194
1649
|
|
|
1195
1650
|
// src/api.ts
|
|
1196
1651
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
@@ -1667,22 +2122,22 @@ function errorMessage2(status, body) {
|
|
|
1667
2122
|
// src/credentials.ts
|
|
1668
2123
|
import {
|
|
1669
2124
|
chmodSync as chmodSync2,
|
|
1670
|
-
existsSync as
|
|
1671
|
-
mkdirSync as
|
|
2125
|
+
existsSync as existsSync5,
|
|
2126
|
+
mkdirSync as mkdirSync5,
|
|
1672
2127
|
readFileSync as readFileSync3,
|
|
1673
2128
|
renameSync as renameSync2,
|
|
1674
|
-
rmSync as
|
|
2129
|
+
rmSync as rmSync4,
|
|
1675
2130
|
writeFileSync as writeFileSync3
|
|
1676
2131
|
} from "fs";
|
|
1677
|
-
import { dirname as dirname4, join as
|
|
2132
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1678
2133
|
import { z as z3 } from "zod";
|
|
1679
2134
|
function credentialsPath() {
|
|
1680
|
-
return
|
|
2135
|
+
return join7(cabaneDir(), "credentials.json");
|
|
1681
2136
|
}
|
|
1682
2137
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1683
2138
|
function load() {
|
|
1684
2139
|
const path = credentialsPath();
|
|
1685
|
-
if (!
|
|
2140
|
+
if (!existsSync5(path)) return {};
|
|
1686
2141
|
let raw;
|
|
1687
2142
|
try {
|
|
1688
2143
|
raw = readFileSync3(path, "utf8");
|
|
@@ -1699,7 +2154,7 @@ function load() {
|
|
|
1699
2154
|
}
|
|
1700
2155
|
function save(map) {
|
|
1701
2156
|
const path = credentialsPath();
|
|
1702
|
-
|
|
2157
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1703
2158
|
try {
|
|
1704
2159
|
chmodSync2(cabaneDir(), 448);
|
|
1705
2160
|
} catch {
|
|
@@ -1714,7 +2169,7 @@ function save(map) {
|
|
|
1714
2169
|
renameSync2(tmp, path);
|
|
1715
2170
|
} catch (err) {
|
|
1716
2171
|
try {
|
|
1717
|
-
|
|
2172
|
+
rmSync4(tmp, { force: true });
|
|
1718
2173
|
} catch {
|
|
1719
2174
|
}
|
|
1720
2175
|
throw err;
|
|
@@ -1744,20 +2199,20 @@ function pruneCredentials(keepAgentIds) {
|
|
|
1744
2199
|
}
|
|
1745
2200
|
|
|
1746
2201
|
// src/cursor.ts
|
|
1747
|
-
import { mkdirSync as
|
|
1748
|
-
import { join as
|
|
2202
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2203
|
+
import { join as join8 } from "path";
|
|
1749
2204
|
function pathFor(workspaceId) {
|
|
1750
|
-
return
|
|
2205
|
+
return join8(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
1751
2206
|
}
|
|
1752
2207
|
function readCursor(workspaceId) {
|
|
1753
2208
|
const path = pathFor(workspaceId);
|
|
1754
|
-
if (!
|
|
2209
|
+
if (!existsSync6(path)) return null;
|
|
1755
2210
|
const raw = readFileSync4(path, "utf8").trim();
|
|
1756
2211
|
return raw.length > 0 ? raw : null;
|
|
1757
2212
|
}
|
|
1758
2213
|
function writeCursor(workspaceId, eventId) {
|
|
1759
2214
|
const path = pathFor(workspaceId);
|
|
1760
|
-
|
|
2215
|
+
mkdirSync6(join8(cabaneDir(), "cursors"), { recursive: true });
|
|
1761
2216
|
writeFileSync4(path, eventId + "\n", "utf8");
|
|
1762
2217
|
}
|
|
1763
2218
|
|
|
@@ -1801,18 +2256,18 @@ var CursorTracker = class {
|
|
|
1801
2256
|
};
|
|
1802
2257
|
|
|
1803
2258
|
// src/dispatch-dedupe.ts
|
|
1804
|
-
import { mkdirSync as
|
|
1805
|
-
import { join as
|
|
2259
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
2260
|
+
import { join as join9 } from "path";
|
|
1806
2261
|
var MAX_IDS = 256;
|
|
1807
2262
|
function dir(log) {
|
|
1808
|
-
return
|
|
2263
|
+
return join9(cabaneDir(), log);
|
|
1809
2264
|
}
|
|
1810
2265
|
function pathFor2(log, workspaceId) {
|
|
1811
|
-
return
|
|
2266
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
1812
2267
|
}
|
|
1813
2268
|
function readIds(log, workspaceId) {
|
|
1814
2269
|
const path = pathFor2(log, workspaceId);
|
|
1815
|
-
if (!
|
|
2270
|
+
if (!existsSync7(path)) return [];
|
|
1816
2271
|
try {
|
|
1817
2272
|
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1818
2273
|
} catch {
|
|
@@ -1827,7 +2282,7 @@ function mark(log, workspaceId, eventId) {
|
|
|
1827
2282
|
if (ids.includes(eventId)) return;
|
|
1828
2283
|
ids.push(eventId);
|
|
1829
2284
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
1830
|
-
|
|
2285
|
+
mkdirSync7(dir(log), { recursive: true });
|
|
1831
2286
|
writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
1832
2287
|
}
|
|
1833
2288
|
function hasDispatched(workspaceId, eventId) {
|
|
@@ -1844,15 +2299,15 @@ function markCompleted(workspaceId, eventId) {
|
|
|
1844
2299
|
}
|
|
1845
2300
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
1846
2301
|
function resumeDir() {
|
|
1847
|
-
return
|
|
2302
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
1848
2303
|
}
|
|
1849
2304
|
function resumePathFor(workspaceId) {
|
|
1850
|
-
return
|
|
2305
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
1851
2306
|
}
|
|
1852
2307
|
function readResumeCounts(workspaceId) {
|
|
1853
2308
|
const out = /* @__PURE__ */ new Map();
|
|
1854
2309
|
const path = resumePathFor(workspaceId);
|
|
1855
|
-
if (!
|
|
2310
|
+
if (!existsSync7(path)) return out;
|
|
1856
2311
|
try {
|
|
1857
2312
|
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
1858
2313
|
const trimmed = line.trim();
|
|
@@ -1874,7 +2329,7 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
1874
2329
|
counts.set(eventId, next);
|
|
1875
2330
|
const entries = [...counts.entries()];
|
|
1876
2331
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
1877
|
-
|
|
2332
|
+
mkdirSync7(resumeDir(), { recursive: true });
|
|
1878
2333
|
writeFileSync5(
|
|
1879
2334
|
resumePathFor(workspaceId),
|
|
1880
2335
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
@@ -3766,7 +4221,7 @@ async function acquireServerTurnLock(url) {
|
|
|
3766
4221
|
};
|
|
3767
4222
|
}
|
|
3768
4223
|
function createHttpOpencodeTransport(opts) {
|
|
3769
|
-
const base =
|
|
4224
|
+
const base = trimSlash(opts.baseUrl);
|
|
3770
4225
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
3771
4226
|
return {
|
|
3772
4227
|
async run(spec, signal) {
|
|
@@ -3912,7 +4367,7 @@ function belongsToSession(ev, sessionId) {
|
|
|
3912
4367
|
function newOpencodeMessageId() {
|
|
3913
4368
|
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
3914
4369
|
}
|
|
3915
|
-
function
|
|
4370
|
+
function trimSlash(s) {
|
|
3916
4371
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
3917
4372
|
}
|
|
3918
4373
|
|
|
@@ -4662,7 +5117,13 @@ function parseCodexModel(model) {
|
|
|
4662
5117
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
4663
5118
|
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
4664
5119
|
var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
|
|
4665
|
-
var ENV_ENVELOPE_KEYS = [
|
|
5120
|
+
var ENV_ENVELOPE_KEYS = [
|
|
5121
|
+
"CABANE_PLAYGROUND",
|
|
5122
|
+
"CABANE_PLAYGROUND_BIN",
|
|
5123
|
+
"CABANE_CONVERSATION_ID",
|
|
5124
|
+
"CABANE_AGENT_ID",
|
|
5125
|
+
"CABANE_CONVERSATION_TITLE"
|
|
5126
|
+
];
|
|
4666
5127
|
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
|
|
4667
5128
|
const { policy, config } = req;
|
|
4668
5129
|
const directory = req.local.cwd ?? "";
|
|
@@ -5567,8 +6028,8 @@ var ConnectorHealthStore = class {
|
|
|
5567
6028
|
|
|
5568
6029
|
// src/dispatcher.ts
|
|
5569
6030
|
import { randomUUID } from "crypto";
|
|
5570
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
5571
|
-
import { join as
|
|
6031
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6032
|
+
import { join as join14 } from "path";
|
|
5572
6033
|
|
|
5573
6034
|
// src/summon.ts
|
|
5574
6035
|
import { z as z12 } from "zod";
|
|
@@ -5842,10 +6303,10 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5842
6303
|
|
|
5843
6304
|
// src/build-options.ts
|
|
5844
6305
|
function cabaneMcpUrl(baseUrl) {
|
|
5845
|
-
return `${
|
|
6306
|
+
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
5846
6307
|
}
|
|
5847
6308
|
function turnControlMcpUrl(baseUrl) {
|
|
5848
|
-
return `${
|
|
6309
|
+
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
5849
6310
|
}
|
|
5850
6311
|
function buildCompanionTurnRequest(params) {
|
|
5851
6312
|
const { turnContext: t } = params;
|
|
@@ -5894,18 +6355,18 @@ function buildCompanionTurnRequest(params) {
|
|
|
5894
6355
|
}
|
|
5895
6356
|
};
|
|
5896
6357
|
}
|
|
5897
|
-
function
|
|
6358
|
+
function trimSlash2(s) {
|
|
5898
6359
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
5899
6360
|
}
|
|
5900
6361
|
|
|
5901
6362
|
// src/codex-instructions.ts
|
|
5902
6363
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
5903
6364
|
import { tmpdir } from "os";
|
|
5904
|
-
import { join as
|
|
6365
|
+
import { join as join10 } from "path";
|
|
5905
6366
|
var PREFIX = "cabane-codex-instructions-";
|
|
5906
6367
|
async function writeCodexInstructionsFile(contents) {
|
|
5907
|
-
const dir2 = await mkdtemp(
|
|
5908
|
-
const path =
|
|
6368
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
6369
|
+
const path = join10(dir2, "instructions.md");
|
|
5909
6370
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
5910
6371
|
return {
|
|
5911
6372
|
path,
|
|
@@ -5916,20 +6377,20 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
5916
6377
|
}
|
|
5917
6378
|
|
|
5918
6379
|
// src/prepared.ts
|
|
5919
|
-
import { mkdirSync as
|
|
5920
|
-
import { join as
|
|
6380
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
6381
|
+
import { join as join11 } from "path";
|
|
5921
6382
|
function dirFor(workspaceId) {
|
|
5922
|
-
return
|
|
6383
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
5923
6384
|
}
|
|
5924
6385
|
function conversationDir(workspaceId, conversationId) {
|
|
5925
|
-
return
|
|
6386
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
5926
6387
|
}
|
|
5927
6388
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
5928
|
-
return
|
|
6389
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
5929
6390
|
}
|
|
5930
6391
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
5931
6392
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
5932
|
-
if (!
|
|
6393
|
+
if (!existsSync8(path)) return null;
|
|
5933
6394
|
try {
|
|
5934
6395
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
5935
6396
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
@@ -5944,7 +6405,7 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
5944
6405
|
}
|
|
5945
6406
|
}
|
|
5946
6407
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
5947
|
-
|
|
6408
|
+
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
5948
6409
|
writeFileSync6(
|
|
5949
6410
|
pathFor3(workspaceId, conversationId, agentId),
|
|
5950
6411
|
JSON.stringify(result) + "\n",
|
|
@@ -5952,21 +6413,21 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
|
5952
6413
|
);
|
|
5953
6414
|
}
|
|
5954
6415
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
5955
|
-
|
|
6416
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
5956
6417
|
}
|
|
5957
6418
|
|
|
5958
6419
|
// src/secrets.ts
|
|
5959
|
-
import { existsSync as
|
|
5960
|
-
import { join as
|
|
6420
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
6421
|
+
import { join as join12 } from "path";
|
|
5961
6422
|
import { z as z13 } from "zod";
|
|
5962
6423
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5963
6424
|
function secretsPath() {
|
|
5964
|
-
return
|
|
6425
|
+
return join12(cabaneDir(), "secrets.json");
|
|
5965
6426
|
}
|
|
5966
6427
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
5967
6428
|
function loadSecretStore() {
|
|
5968
6429
|
const path = secretsPath();
|
|
5969
|
-
if (!
|
|
6430
|
+
if (!existsSync9(path)) return makeStore({});
|
|
5970
6431
|
let raw;
|
|
5971
6432
|
try {
|
|
5972
6433
|
raw = readFileSync7(path, "utf8");
|
|
@@ -6047,10 +6508,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6047
6508
|
}
|
|
6048
6509
|
|
|
6049
6510
|
// src/transcript-writer.ts
|
|
6050
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
6051
|
-
import { join as
|
|
6511
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
6512
|
+
import { join as join13 } from "path";
|
|
6052
6513
|
function transcriptsDir() {
|
|
6053
|
-
return
|
|
6514
|
+
return join13(cabaneDir(), "transcripts");
|
|
6054
6515
|
}
|
|
6055
6516
|
var RETAIN = 200;
|
|
6056
6517
|
var TranscriptWriter = class {
|
|
@@ -6059,9 +6520,9 @@ var TranscriptWriter = class {
|
|
|
6059
6520
|
onWarn;
|
|
6060
6521
|
constructor(dir2, meta, onWarn) {
|
|
6061
6522
|
this.onWarn = onWarn;
|
|
6062
|
-
this.path =
|
|
6523
|
+
this.path = join13(dir2, fileName(meta));
|
|
6063
6524
|
try {
|
|
6064
|
-
|
|
6525
|
+
mkdirSync9(dir2, { recursive: true });
|
|
6065
6526
|
try {
|
|
6066
6527
|
chmodSync3(dir2, 448);
|
|
6067
6528
|
} catch {
|
|
@@ -6118,7 +6579,7 @@ function pruneOld(dir2, retain) {
|
|
|
6118
6579
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6119
6580
|
for (const f of drop) {
|
|
6120
6581
|
try {
|
|
6121
|
-
|
|
6582
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
6122
6583
|
} catch {
|
|
6123
6584
|
}
|
|
6124
6585
|
}
|
|
@@ -6464,7 +6925,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
6464
6925
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6465
6926
|
function checkoutState(cwd) {
|
|
6466
6927
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
6467
|
-
if (!
|
|
6928
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
6468
6929
|
let entries;
|
|
6469
6930
|
try {
|
|
6470
6931
|
entries = readdirSync2(cwd);
|
|
@@ -6474,15 +6935,15 @@ function checkoutState(cwd) {
|
|
|
6474
6935
|
if (entries.length === 0) {
|
|
6475
6936
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
6476
6937
|
}
|
|
6477
|
-
const gitPath =
|
|
6478
|
-
if (!
|
|
6938
|
+
const gitPath = join14(cwd, ".git");
|
|
6939
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
6479
6940
|
let stat;
|
|
6480
6941
|
try {
|
|
6481
6942
|
stat = statSync(gitPath);
|
|
6482
6943
|
} catch (error) {
|
|
6483
6944
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6484
6945
|
}
|
|
6485
|
-
if (stat.isDirectory() && !
|
|
6946
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
6486
6947
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
6487
6948
|
return { ok: true, reason: "usable" };
|
|
6488
6949
|
}
|
|
@@ -6673,7 +7134,7 @@ var Dispatcher = class {
|
|
|
6673
7134
|
let seqCounter = 0;
|
|
6674
7135
|
const nextSeq = () => ++seqCounter;
|
|
6675
7136
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
6676
|
-
if (effectiveCwd && !
|
|
7137
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
6677
7138
|
turnLog.warn(
|
|
6678
7139
|
{ cwd: effectiveCwd },
|
|
6679
7140
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -6981,7 +7442,7 @@ ${reason}`,
|
|
|
6981
7442
|
}
|
|
6982
7443
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
6983
7444
|
}
|
|
6984
|
-
const receiptPath =
|
|
7445
|
+
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
6985
7446
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
6986
7447
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6987
7448
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -7005,7 +7466,7 @@ ${reason}`,
|
|
|
7005
7466
|
})}
|
|
7006
7467
|
`;
|
|
7007
7468
|
try {
|
|
7008
|
-
|
|
7469
|
+
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
7009
7470
|
appendFileSync2(
|
|
7010
7471
|
receiptPath,
|
|
7011
7472
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -7433,202 +7894,6 @@ ${reason}`,
|
|
|
7433
7894
|
}
|
|
7434
7895
|
};
|
|
7435
7896
|
|
|
7436
|
-
// src/manifest.ts
|
|
7437
|
-
var DEVICE_MANIFEST = {
|
|
7438
|
-
runtimes: [{ name: "claude-code", version: null }],
|
|
7439
|
-
capabilities: { hostFs: true, browser: true, userMcp: true }
|
|
7440
|
-
};
|
|
7441
|
-
function buildCompanionManifest(opts) {
|
|
7442
|
-
const v = opts.versions ?? {};
|
|
7443
|
-
const runtimes = [];
|
|
7444
|
-
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
7445
|
-
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
7446
|
-
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
7447
|
-
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
7448
|
-
}
|
|
7449
|
-
|
|
7450
|
-
// src/harness-status.ts
|
|
7451
|
-
var LABELS = {
|
|
7452
|
-
"claude-code": "Claude Code",
|
|
7453
|
-
codex: "Codex",
|
|
7454
|
-
opencode: "opencode"
|
|
7455
|
-
};
|
|
7456
|
-
function deriveHarnessSnapshot(signals) {
|
|
7457
|
-
const advertised = new Set(
|
|
7458
|
-
buildCompanionManifest({
|
|
7459
|
-
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7460
|
-
// through the same function rather than re-decided.
|
|
7461
|
-
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
7462
|
-
opencode: signals.opencodeConfigured,
|
|
7463
|
-
codex: signals.codexEnabled
|
|
7464
|
-
}).runtimes.map((r) => r.name)
|
|
7465
|
-
);
|
|
7466
|
-
const harnesses = [
|
|
7467
|
-
deriveClaudeCode(signals, advertised.has("claude-code")),
|
|
7468
|
-
deriveCodex(signals, advertised.has("codex")),
|
|
7469
|
-
deriveOpencode(signals, advertised.has("opencode"))
|
|
7470
|
-
];
|
|
7471
|
-
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
7472
|
-
}
|
|
7473
|
-
function deriveClaudeCode(signals, manifestHas) {
|
|
7474
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
7475
|
-
if (manifestHas) {
|
|
7476
|
-
return {
|
|
7477
|
-
...base,
|
|
7478
|
-
state: "exposed",
|
|
7479
|
-
version: signals.claudeVersion,
|
|
7480
|
-
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7481
|
-
enable: null
|
|
7482
|
-
};
|
|
7483
|
-
}
|
|
7484
|
-
if (signals.claudeCodeConnected) {
|
|
7485
|
-
return {
|
|
7486
|
-
...base,
|
|
7487
|
-
state: "needs_attention",
|
|
7488
|
-
version: null,
|
|
7489
|
-
detail: "Connected, but the `claude` CLI isn\u2019t on your PATH. Install it (`npm i -g @anthropic-ai/claude-code`) and sign in, or disconnect it.",
|
|
7490
|
-
enable: null
|
|
7491
|
-
};
|
|
7492
|
-
}
|
|
7493
|
-
if (signals.claudeOnPath) {
|
|
7494
|
-
return {
|
|
7495
|
-
...base,
|
|
7496
|
-
state: "detected_not_exposed",
|
|
7497
|
-
version: signals.claudeVersion,
|
|
7498
|
-
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7499
|
-
enable: "claude-code"
|
|
7500
|
-
};
|
|
7501
|
-
}
|
|
7502
|
-
return {
|
|
7503
|
-
...base,
|
|
7504
|
-
state: "not_detected",
|
|
7505
|
-
version: null,
|
|
7506
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7507
|
-
enable: null
|
|
7508
|
-
};
|
|
7509
|
-
}
|
|
7510
|
-
function deriveCodex(signals, manifestHas) {
|
|
7511
|
-
const base = { runtime: "codex", label: LABELS.codex };
|
|
7512
|
-
if (manifestHas) {
|
|
7513
|
-
if (signals.codexOnPath) {
|
|
7514
|
-
return {
|
|
7515
|
-
...base,
|
|
7516
|
-
state: "exposed",
|
|
7517
|
-
version: signals.codexVersion,
|
|
7518
|
-
detail: "Codex is enabled and exposed to Cabane.",
|
|
7519
|
-
enable: null
|
|
7520
|
-
};
|
|
7521
|
-
}
|
|
7522
|
-
return {
|
|
7523
|
-
...base,
|
|
7524
|
-
state: "needs_attention",
|
|
7525
|
-
version: null,
|
|
7526
|
-
detail: "Enabled, but the `codex` CLI isn\u2019t on your PATH. Install it and sign in (`codex login`), or turn Codex off.",
|
|
7527
|
-
enable: null
|
|
7528
|
-
};
|
|
7529
|
-
}
|
|
7530
|
-
if (signals.codexOnPath) {
|
|
7531
|
-
return {
|
|
7532
|
-
...base,
|
|
7533
|
-
state: "detected_not_exposed",
|
|
7534
|
-
version: signals.codexVersion,
|
|
7535
|
-
detail: "Codex is installed but not exposed yet. Turn it on to let Cabane run Codex here.",
|
|
7536
|
-
enable: "codex"
|
|
7537
|
-
};
|
|
7538
|
-
}
|
|
7539
|
-
return {
|
|
7540
|
-
...base,
|
|
7541
|
-
state: "not_detected",
|
|
7542
|
-
version: null,
|
|
7543
|
-
detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
|
|
7544
|
-
enable: null
|
|
7545
|
-
};
|
|
7546
|
-
}
|
|
7547
|
-
function deriveOpencode(signals, manifestHas) {
|
|
7548
|
-
const base = { runtime: "opencode", label: LABELS.opencode };
|
|
7549
|
-
if (manifestHas) {
|
|
7550
|
-
if (signals.opencodeReachable) {
|
|
7551
|
-
return {
|
|
7552
|
-
...base,
|
|
7553
|
-
state: "exposed",
|
|
7554
|
-
version: signals.opencodeVersion,
|
|
7555
|
-
detail: "An opencode server is reachable and exposed to Cabane.",
|
|
7556
|
-
enable: null
|
|
7557
|
-
};
|
|
7558
|
-
}
|
|
7559
|
-
return {
|
|
7560
|
-
...base,
|
|
7561
|
-
state: "needs_attention",
|
|
7562
|
-
version: null,
|
|
7563
|
-
detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
|
|
7564
|
-
enable: null
|
|
7565
|
-
};
|
|
7566
|
-
}
|
|
7567
|
-
return {
|
|
7568
|
-
...base,
|
|
7569
|
-
state: "not_detected",
|
|
7570
|
-
version: null,
|
|
7571
|
-
detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
|
|
7572
|
-
enable: "opencode"
|
|
7573
|
-
};
|
|
7574
|
-
}
|
|
7575
|
-
function detectedRuntimesFor(snapshot) {
|
|
7576
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7577
|
-
}
|
|
7578
|
-
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
7579
|
-
async function probeHarnessSignals(cfg, deps = {}) {
|
|
7580
|
-
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
7581
|
-
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
7582
|
-
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
7583
|
-
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
7584
|
-
const serverUrl = cfg.opencode?.serverUrl;
|
|
7585
|
-
const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
7586
|
-
withTimeout(probeClaudePresence(), false),
|
|
7587
|
-
withTimeout(probeClaudeVersion(), null),
|
|
7588
|
-
withTimeout(probeCodexVersion(), null),
|
|
7589
|
-
serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
|
|
7590
|
-
]);
|
|
7591
|
-
return {
|
|
7592
|
-
claudeOnPath: claudeOnPathResult,
|
|
7593
|
-
claudeVersion,
|
|
7594
|
-
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7595
|
-
// the manifest gate and the probe above is only a suggestion.
|
|
7596
|
-
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
7597
|
-
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7598
|
-
// exposes codex; its config flag is the manifest gate either way).
|
|
7599
|
-
codexOnPath: codexVersion !== null,
|
|
7600
|
-
codexVersion,
|
|
7601
|
-
codexEnabled: isCodexEnabled(cfg),
|
|
7602
|
-
opencodeConfigured: !!serverUrl,
|
|
7603
|
-
// A version came back ⟺ the serve answered its health endpoint (CT584).
|
|
7604
|
-
opencodeReachable: opencodeVersion !== null,
|
|
7605
|
-
opencodeVersion
|
|
7606
|
-
};
|
|
7607
|
-
}
|
|
7608
|
-
function withTimeout(promise, fallback) {
|
|
7609
|
-
return new Promise((resolve) => {
|
|
7610
|
-
let settled = false;
|
|
7611
|
-
const done = (v) => {
|
|
7612
|
-
if (!settled) {
|
|
7613
|
-
settled = true;
|
|
7614
|
-
resolve(v);
|
|
7615
|
-
}
|
|
7616
|
-
};
|
|
7617
|
-
const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS2);
|
|
7618
|
-
timer.unref?.();
|
|
7619
|
-
promise.then(
|
|
7620
|
-
(v) => {
|
|
7621
|
-
clearTimeout(timer);
|
|
7622
|
-
done(v);
|
|
7623
|
-
},
|
|
7624
|
-
() => {
|
|
7625
|
-
clearTimeout(timer);
|
|
7626
|
-
done(fallback);
|
|
7627
|
-
}
|
|
7628
|
-
);
|
|
7629
|
-
});
|
|
7630
|
-
}
|
|
7631
|
-
|
|
7632
7897
|
// src/opencode-models.ts
|
|
7633
7898
|
var OPENCODE_RUNTIME = "opencode";
|
|
7634
7899
|
function mapOpencodeProviders(json) {
|
|
@@ -7672,15 +7937,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
7672
7937
|
|
|
7673
7938
|
// src/outbox.ts
|
|
7674
7939
|
import {
|
|
7675
|
-
existsSync as
|
|
7676
|
-
mkdirSync as
|
|
7940
|
+
existsSync as existsSync11,
|
|
7941
|
+
mkdirSync as mkdirSync11,
|
|
7677
7942
|
readdirSync as readdirSync3,
|
|
7678
7943
|
readFileSync as readFileSync8,
|
|
7679
7944
|
renameSync as renameSync3,
|
|
7680
|
-
rmSync as
|
|
7945
|
+
rmSync as rmSync7,
|
|
7681
7946
|
writeFileSync as writeFileSync7
|
|
7682
7947
|
} from "fs";
|
|
7683
|
-
import { join as
|
|
7948
|
+
import { join as join15 } from "path";
|
|
7684
7949
|
var MAX_ENTRIES = 2e3;
|
|
7685
7950
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7686
7951
|
var Outbox = class {
|
|
@@ -7693,17 +7958,17 @@ var Outbox = class {
|
|
|
7693
7958
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
7694
7959
|
// cases route writes at the right tmpdir.
|
|
7695
7960
|
dir() {
|
|
7696
|
-
return
|
|
7961
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7697
7962
|
}
|
|
7698
7963
|
fileFor(turnId, seq) {
|
|
7699
|
-
return
|
|
7964
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
7700
7965
|
}
|
|
7701
7966
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
7702
7967
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
7703
7968
|
// per-workspace bounds.
|
|
7704
7969
|
persist(entry) {
|
|
7705
7970
|
const dir2 = this.dir();
|
|
7706
|
-
|
|
7971
|
+
mkdirSync11(dir2, { recursive: true });
|
|
7707
7972
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
7708
7973
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
7709
7974
|
try {
|
|
@@ -7711,7 +7976,7 @@ var Outbox = class {
|
|
|
7711
7976
|
renameSync3(tmp, target);
|
|
7712
7977
|
} catch (err) {
|
|
7713
7978
|
try {
|
|
7714
|
-
|
|
7979
|
+
rmSync7(tmp, { force: true });
|
|
7715
7980
|
} catch {
|
|
7716
7981
|
}
|
|
7717
7982
|
this.log?.warn(
|
|
@@ -7728,7 +7993,7 @@ var Outbox = class {
|
|
|
7728
7993
|
// wedging the drain.
|
|
7729
7994
|
list() {
|
|
7730
7995
|
const dir2 = this.dir();
|
|
7731
|
-
if (!
|
|
7996
|
+
if (!existsSync11(dir2)) return [];
|
|
7732
7997
|
let names;
|
|
7733
7998
|
try {
|
|
7734
7999
|
names = readdirSync3(dir2);
|
|
@@ -7738,7 +8003,7 @@ var Outbox = class {
|
|
|
7738
8003
|
const entries = [];
|
|
7739
8004
|
for (const name of names) {
|
|
7740
8005
|
if (!name.endsWith(".json")) continue;
|
|
7741
|
-
const full =
|
|
8006
|
+
const full = join15(dir2, name);
|
|
7742
8007
|
try {
|
|
7743
8008
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
7744
8009
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -7758,13 +8023,13 @@ var Outbox = class {
|
|
|
7758
8023
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
7759
8024
|
remove(turnId, seq) {
|
|
7760
8025
|
try {
|
|
7761
|
-
|
|
8026
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
7762
8027
|
} catch {
|
|
7763
8028
|
}
|
|
7764
8029
|
}
|
|
7765
8030
|
size() {
|
|
7766
8031
|
const dir2 = this.dir();
|
|
7767
|
-
if (!
|
|
8032
|
+
if (!existsSync11(dir2)) return 0;
|
|
7768
8033
|
try {
|
|
7769
8034
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
7770
8035
|
} catch {
|
|
@@ -7777,7 +8042,7 @@ var Outbox = class {
|
|
|
7777
8042
|
"companion outbox: dropping unreadable entry"
|
|
7778
8043
|
);
|
|
7779
8044
|
try {
|
|
7780
|
-
|
|
8045
|
+
rmSync7(full, { force: true });
|
|
7781
8046
|
} catch {
|
|
7782
8047
|
}
|
|
7783
8048
|
}
|
|
@@ -8706,6 +8971,26 @@ var CompanionSupervisor = class {
|
|
|
8706
8971
|
async recheckHarnesses() {
|
|
8707
8972
|
await this.refreshHarnessStatuses();
|
|
8708
8973
|
}
|
|
8974
|
+
// CT1085 §1 step 3: beat NOW and wait for it to land. `start` calls this
|
|
8975
|
+
// between bringing the runtime up and asking the person anything, so the
|
|
8976
|
+
// browser's connect step is already showing what this machine has ("your
|
|
8977
|
+
// terminal is asking") rather than sitting blank while the terminal blocks on
|
|
8978
|
+
// an answer. Coalesces with an in-flight beat rather than stacking a second.
|
|
8979
|
+
async heartbeatNow() {
|
|
8980
|
+
if (this.inFlightHeartbeat) {
|
|
8981
|
+
await this.inFlightHeartbeat;
|
|
8982
|
+
return;
|
|
8983
|
+
}
|
|
8984
|
+
this.kickHeartbeat();
|
|
8985
|
+
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
8986
|
+
}
|
|
8987
|
+
// CT1085: the config as it stands right now — after any `enableHarness` write.
|
|
8988
|
+
// The control socket's connect handler needs it to run the shake-out check
|
|
8989
|
+
// against the URL/flag that was just persisted, not the one this process booted
|
|
8990
|
+
// with.
|
|
8991
|
+
currentConfig() {
|
|
8992
|
+
return this.config;
|
|
8993
|
+
}
|
|
8709
8994
|
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8710
8995
|
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8711
8996
|
// never installs a binary and never drives a login (BYO — Decided).
|
|
@@ -8847,9 +9132,9 @@ var CompanionSupervisor = class {
|
|
|
8847
9132
|
};
|
|
8848
9133
|
function defaultReexec() {
|
|
8849
9134
|
clearRuntimeState();
|
|
8850
|
-
void import("child_process").then(({ spawn:
|
|
9135
|
+
void import("child_process").then(({ spawn: spawn5 }) => {
|
|
8851
9136
|
try {
|
|
8852
|
-
const child =
|
|
9137
|
+
const child = spawn5(process.execPath, process.argv.slice(1), {
|
|
8853
9138
|
stdio: "inherit",
|
|
8854
9139
|
detached: false
|
|
8855
9140
|
});
|
|
@@ -8919,14 +9204,14 @@ function handleUncaught(log, err, origin) {
|
|
|
8919
9204
|
}
|
|
8920
9205
|
|
|
8921
9206
|
// src/crash-marker.ts
|
|
8922
|
-
import { existsSync as
|
|
8923
|
-
import { join as
|
|
9207
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9208
|
+
import { join as join16 } from "path";
|
|
8924
9209
|
function crashMarkerPath() {
|
|
8925
|
-
return
|
|
9210
|
+
return join16(cabaneDir(), "last-error.json");
|
|
8926
9211
|
}
|
|
8927
9212
|
function recordCrash(rec) {
|
|
8928
9213
|
try {
|
|
8929
|
-
|
|
9214
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
8930
9215
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
8931
9216
|
} catch {
|
|
8932
9217
|
}
|
|
@@ -8934,7 +9219,7 @@ function recordCrash(rec) {
|
|
|
8934
9219
|
function clearCrash() {
|
|
8935
9220
|
try {
|
|
8936
9221
|
const path = crashMarkerPath();
|
|
8937
|
-
if (
|
|
9222
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
8938
9223
|
} catch {
|
|
8939
9224
|
}
|
|
8940
9225
|
}
|
|
@@ -8953,7 +9238,13 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8953
9238
|
onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
|
|
8954
9239
|
)
|
|
8955
9240
|
}));
|
|
8956
|
-
await warnAboutHarnessReadiness(cfg, {
|
|
9241
|
+
await warnAboutHarnessReadiness(cfg, {
|
|
9242
|
+
probeClaude: async () => claudeCode,
|
|
9243
|
+
// CT1085: the CLI's onboarding script owns the terminal and says this in
|
|
9244
|
+
// its own words (the `!` block, or the per-harness offer), so it hands us a
|
|
9245
|
+
// sink that logs instead. Every other caller keeps the stderr line.
|
|
9246
|
+
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
9247
|
+
});
|
|
8957
9248
|
} catch (err) {
|
|
8958
9249
|
recordCrash({
|
|
8959
9250
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -8977,8 +9268,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8977
9268
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|
|
8978
9269
|
const claim = acquireRuntimeState({
|
|
8979
9270
|
pid: process.pid,
|
|
8980
|
-
url: "",
|
|
8981
|
-
port: 0,
|
|
8982
9271
|
startedAt,
|
|
8983
9272
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
8984
9273
|
instanceId
|
|
@@ -8986,7 +9275,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8986
9275
|
if (!claim.acquired) {
|
|
8987
9276
|
return { ok: false, reason: "already-running", existing: claim.existing ?? null };
|
|
8988
9277
|
}
|
|
8989
|
-
process.on("exit", () =>
|
|
9278
|
+
process.on("exit", () => clearRuntimeStateIfOurs(instanceId));
|
|
8990
9279
|
const hub = new CompanionStateHub({
|
|
8991
9280
|
// CT29: one device, one base URL — the cabane instance this device is paired
|
|
8992
9281
|
// with. The dashboard's connection line shows it.
|
|
@@ -9004,17 +9293,38 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9004
9293
|
harnessVersions
|
|
9005
9294
|
});
|
|
9006
9295
|
await supervisor.start();
|
|
9007
|
-
const
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9296
|
+
const connectHarness = async (runtime, serverUrl) => {
|
|
9297
|
+
const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
|
|
9298
|
+
const verdict = await shakeOutHarness(runtime, candidate);
|
|
9299
|
+
if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
|
|
9300
|
+
const result = await supervisor.enableHarness(
|
|
9301
|
+
runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
|
|
9302
|
+
);
|
|
9303
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
9304
|
+
return { ok: true, message: connectedLine(runtime, verdict) };
|
|
9305
|
+
};
|
|
9306
|
+
const control = await startControlServer({
|
|
9307
|
+
status: () => hub.statusJson(),
|
|
9308
|
+
connect: async (runtime, serverUrl) => {
|
|
9309
|
+
const result = await connectHarness(runtime, serverUrl);
|
|
9310
|
+
return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
|
|
9311
|
+
},
|
|
9312
|
+
stop: () => void supervisor.requestStop()
|
|
9012
9313
|
});
|
|
9013
|
-
|
|
9314
|
+
let dashboard = null;
|
|
9315
|
+
if (opts.dashboard) {
|
|
9316
|
+
const preferredPort = opts.port ?? cfg.dashboardPort;
|
|
9317
|
+
dashboard = await startDashboard({
|
|
9318
|
+
supervisor,
|
|
9319
|
+
hub,
|
|
9320
|
+
...preferredPort !== void 0 ? { port: preferredPort } : {}
|
|
9321
|
+
});
|
|
9322
|
+
hub.setDashboardUrl(dashboard.url);
|
|
9323
|
+
}
|
|
9014
9324
|
writeRuntimeState({
|
|
9015
9325
|
pid: process.pid,
|
|
9016
|
-
|
|
9017
|
-
port: dashboard.port,
|
|
9326
|
+
socket: control.path,
|
|
9327
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9018
9328
|
startedAt,
|
|
9019
9329
|
// SJ495: the daemon launcher sets this env on the detached child, so the
|
|
9020
9330
|
// marker records whether this companion is backgrounded (foreground start
|
|
@@ -9027,30 +9337,37 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9027
9337
|
const stop = async () => {
|
|
9028
9338
|
if (stopped) return;
|
|
9029
9339
|
stopped = true;
|
|
9030
|
-
|
|
9340
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9031
9341
|
try {
|
|
9032
9342
|
await supervisor.shutdown();
|
|
9033
|
-
await dashboard
|
|
9343
|
+
await closeSurfaces(control, dashboard);
|
|
9034
9344
|
} catch {
|
|
9035
9345
|
}
|
|
9036
9346
|
};
|
|
9037
9347
|
return {
|
|
9038
9348
|
ok: true,
|
|
9039
9349
|
runtime: {
|
|
9040
|
-
url: dashboard.url,
|
|
9041
|
-
|
|
9350
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9351
|
+
socketPath: control.path,
|
|
9042
9352
|
config: cfg,
|
|
9043
9353
|
stop,
|
|
9354
|
+
heartbeatNow: () => supervisor.heartbeatNow(),
|
|
9355
|
+
harnesses: () => hub.statusJson().harnesses ?? [],
|
|
9356
|
+
connectHarness,
|
|
9044
9357
|
drainForRestart: async (graceMs) => {
|
|
9045
|
-
|
|
9358
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9046
9359
|
const result = await supervisor.drainForRestart(graceMs);
|
|
9047
|
-
await dashboard
|
|
9360
|
+
await closeSurfaces(control, dashboard);
|
|
9048
9361
|
stopped = true;
|
|
9049
9362
|
return result;
|
|
9050
9363
|
}
|
|
9051
9364
|
}
|
|
9052
9365
|
};
|
|
9053
9366
|
}
|
|
9367
|
+
async function closeSurfaces(control, dashboard) {
|
|
9368
|
+
await control.close();
|
|
9369
|
+
if (dashboard) await dashboard.close();
|
|
9370
|
+
}
|
|
9054
9371
|
export {
|
|
9055
9372
|
createCompanionRuntime
|
|
9056
9373
|
};
|