@cabane/companion 0.6.32 → 0.6.34
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 +1868 -1229
- package/dist/pairing-config.js +36 -21
- package/dist/runtime.js +618 -329
- 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(),
|
|
@@ -950,6 +951,163 @@ function resolveStaticDir() {
|
|
|
950
951
|
return join4(dirname3(fileURLToPath(import.meta.url)), "static");
|
|
951
952
|
}
|
|
952
953
|
|
|
954
|
+
// src/control-socket.ts
|
|
955
|
+
import { createHash } from "crypto";
|
|
956
|
+
import { existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
957
|
+
import { createServer, connect } from "net";
|
|
958
|
+
import { join as join5 } from "path";
|
|
959
|
+
var CONTROL_TIMEOUT_MS = 1e3;
|
|
960
|
+
function controlSocketPath() {
|
|
961
|
+
const dir2 = cabaneDir();
|
|
962
|
+
if (process.platform === "win32") {
|
|
963
|
+
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
964
|
+
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
965
|
+
}
|
|
966
|
+
return join5(dir2, "companion.sock");
|
|
967
|
+
}
|
|
968
|
+
async function startControlServer(handlers) {
|
|
969
|
+
const path = controlSocketPath();
|
|
970
|
+
mkdirSync3(cabaneDir(), { recursive: true });
|
|
971
|
+
if (process.platform !== "win32" && existsSync3(path)) {
|
|
972
|
+
const alive = await ping(path);
|
|
973
|
+
if (alive) throw new Error(`another companion is already listening on ${path}`);
|
|
974
|
+
rmSync2(path, { force: true });
|
|
975
|
+
}
|
|
976
|
+
const server = createServer((socket) => {
|
|
977
|
+
void serveConnection(socket, handlers);
|
|
978
|
+
});
|
|
979
|
+
server.unref();
|
|
980
|
+
await new Promise((resolve, reject) => {
|
|
981
|
+
server.once("error", reject);
|
|
982
|
+
server.listen(path, () => {
|
|
983
|
+
server.removeListener("error", reject);
|
|
984
|
+
resolve();
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
server.on("error", () => {
|
|
988
|
+
});
|
|
989
|
+
return {
|
|
990
|
+
path,
|
|
991
|
+
close: () => new Promise((resolve) => {
|
|
992
|
+
server.close(() => {
|
|
993
|
+
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
994
|
+
resolve();
|
|
995
|
+
});
|
|
996
|
+
})
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async function serveConnection(socket, handlers) {
|
|
1000
|
+
socket.on("error", () => socket.destroy());
|
|
1001
|
+
const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
|
|
1002
|
+
if (line === null) {
|
|
1003
|
+
socket.destroy();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
let req;
|
|
1007
|
+
try {
|
|
1008
|
+
req = JSON.parse(line);
|
|
1009
|
+
} catch {
|
|
1010
|
+
reply(socket, { error: "malformed request" });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
if (req.cmd === "status") {
|
|
1015
|
+
reply(socket, handlers.status());
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (req.cmd === "connect") {
|
|
1019
|
+
const result = await handlers.connect(req.runtime, req.serverUrl);
|
|
1020
|
+
reply(socket, result);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (req.cmd === "stop") {
|
|
1024
|
+
reply(socket, { ok: true });
|
|
1025
|
+
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
reply(socket, { error: `unknown command "${String(req.cmd)}"` });
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
reply(socket, { error: err instanceof Error ? err.message : String(err) });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function reply(socket, body) {
|
|
1034
|
+
try {
|
|
1035
|
+
socket.end(`${JSON.stringify(body)}
|
|
1036
|
+
`);
|
|
1037
|
+
} catch {
|
|
1038
|
+
socket.destroy();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
1042
|
+
const socket = connect(path);
|
|
1043
|
+
try {
|
|
1044
|
+
await new Promise((resolve, reject) => {
|
|
1045
|
+
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
1046
|
+
timer.unref?.();
|
|
1047
|
+
socket.once("connect", () => {
|
|
1048
|
+
clearTimeout(timer);
|
|
1049
|
+
resolve();
|
|
1050
|
+
});
|
|
1051
|
+
socket.once("error", (err) => {
|
|
1052
|
+
clearTimeout(timer);
|
|
1053
|
+
reject(err);
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
socket.write(`${JSON.stringify(req)}
|
|
1057
|
+
`);
|
|
1058
|
+
const line = await readLine(socket, timeoutMs);
|
|
1059
|
+
if (line === null) throw new ControlTimeout();
|
|
1060
|
+
return JSON.parse(line);
|
|
1061
|
+
} finally {
|
|
1062
|
+
socket.destroy();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
var ControlTimeout = class extends Error {
|
|
1066
|
+
constructor() {
|
|
1067
|
+
super("the companion did not answer its control socket in time");
|
|
1068
|
+
this.name = "ControlTimeout";
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
function isNotListening(err) {
|
|
1072
|
+
const code = err?.code;
|
|
1073
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
1074
|
+
}
|
|
1075
|
+
function readLine(socket, timeoutMs) {
|
|
1076
|
+
return new Promise((resolve) => {
|
|
1077
|
+
let buf = "";
|
|
1078
|
+
let settled = false;
|
|
1079
|
+
const done = (v) => {
|
|
1080
|
+
if (settled) return;
|
|
1081
|
+
settled = true;
|
|
1082
|
+
clearTimeout(timer);
|
|
1083
|
+
socket.removeListener("data", onData);
|
|
1084
|
+
resolve(v);
|
|
1085
|
+
};
|
|
1086
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
1087
|
+
timer.unref?.();
|
|
1088
|
+
const onData = (chunk) => {
|
|
1089
|
+
buf += chunk.toString("utf8");
|
|
1090
|
+
const nl = buf.indexOf("\n");
|
|
1091
|
+
if (nl >= 0) done(buf.slice(0, nl));
|
|
1092
|
+
else if (buf.length > 1e6) done(null);
|
|
1093
|
+
};
|
|
1094
|
+
socket.on("data", onData);
|
|
1095
|
+
socket.once("close", () => done(null));
|
|
1096
|
+
socket.once("error", () => done(null));
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
async function ping(path) {
|
|
1100
|
+
try {
|
|
1101
|
+
await controlRequest(path, { cmd: "status" });
|
|
1102
|
+
return true;
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return !isNotListening(err);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/harness-check.ts
|
|
1109
|
+
import { spawn as spawn4 } from "child_process";
|
|
1110
|
+
|
|
953
1111
|
// src/harness-versions.ts
|
|
954
1112
|
import { spawn as spawn2 } from "child_process";
|
|
955
1113
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
@@ -1031,6 +1189,20 @@ async function safe(fn) {
|
|
|
1031
1189
|
}
|
|
1032
1190
|
}
|
|
1033
1191
|
|
|
1192
|
+
// src/manifest.ts
|
|
1193
|
+
var DEVICE_MANIFEST = {
|
|
1194
|
+
runtimes: [{ name: "claude-code", version: null }],
|
|
1195
|
+
capabilities: { hostFs: true, browser: true, userMcp: true }
|
|
1196
|
+
};
|
|
1197
|
+
function buildCompanionManifest(opts) {
|
|
1198
|
+
const v = opts.versions ?? {};
|
|
1199
|
+
const runtimes = [];
|
|
1200
|
+
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
1201
|
+
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
1202
|
+
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
1203
|
+
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1034
1206
|
// src/prereqs.ts
|
|
1035
1207
|
import { spawn as spawn3 } from "child_process";
|
|
1036
1208
|
async function claudeOnPath() {
|
|
@@ -1102,33 +1274,292 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1102
1274
|
);
|
|
1103
1275
|
}
|
|
1104
1276
|
|
|
1277
|
+
// src/harness-status.ts
|
|
1278
|
+
var HARNESS_LABELS = {
|
|
1279
|
+
"claude-code": "Claude Code",
|
|
1280
|
+
codex: "Codex",
|
|
1281
|
+
opencode: "opencode"
|
|
1282
|
+
};
|
|
1283
|
+
var LABELS = HARNESS_LABELS;
|
|
1284
|
+
function deriveHarnessSnapshot(signals) {
|
|
1285
|
+
const advertised = new Set(
|
|
1286
|
+
buildCompanionManifest({
|
|
1287
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
1288
|
+
// through the same function rather than re-decided.
|
|
1289
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
1290
|
+
opencode: signals.opencodeConfigured,
|
|
1291
|
+
codex: signals.codexEnabled
|
|
1292
|
+
}).runtimes.map((r) => r.name)
|
|
1293
|
+
);
|
|
1294
|
+
const harnesses = [
|
|
1295
|
+
deriveClaudeCode(signals, advertised.has("claude-code")),
|
|
1296
|
+
deriveCodex(signals, advertised.has("codex")),
|
|
1297
|
+
deriveOpencode(signals, advertised.has("opencode"))
|
|
1298
|
+
];
|
|
1299
|
+
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
1300
|
+
}
|
|
1301
|
+
function deriveClaudeCode(signals, manifestHas) {
|
|
1302
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
1303
|
+
if (manifestHas) {
|
|
1304
|
+
return {
|
|
1305
|
+
...base,
|
|
1306
|
+
state: "exposed",
|
|
1307
|
+
version: signals.claudeVersion,
|
|
1308
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
1309
|
+
enable: null
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (signals.claudeCodeConnected) {
|
|
1313
|
+
return {
|
|
1314
|
+
...base,
|
|
1315
|
+
state: "needs_attention",
|
|
1316
|
+
version: null,
|
|
1317
|
+
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.",
|
|
1318
|
+
enable: null
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
if (signals.claudeOnPath) {
|
|
1322
|
+
return {
|
|
1323
|
+
...base,
|
|
1324
|
+
state: "detected_not_exposed",
|
|
1325
|
+
version: signals.claudeVersion,
|
|
1326
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
1327
|
+
enable: "claude-code"
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
...base,
|
|
1332
|
+
state: "not_detected",
|
|
1333
|
+
version: null,
|
|
1334
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
1335
|
+
enable: null
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function deriveCodex(signals, manifestHas) {
|
|
1339
|
+
const base = { runtime: "codex", label: LABELS.codex };
|
|
1340
|
+
if (manifestHas) {
|
|
1341
|
+
if (signals.codexOnPath) {
|
|
1342
|
+
return {
|
|
1343
|
+
...base,
|
|
1344
|
+
state: "exposed",
|
|
1345
|
+
version: signals.codexVersion,
|
|
1346
|
+
detail: "Codex is enabled and exposed to Cabane.",
|
|
1347
|
+
enable: null
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
...base,
|
|
1352
|
+
state: "needs_attention",
|
|
1353
|
+
version: null,
|
|
1354
|
+
detail: "Enabled, but the `codex` CLI isn\u2019t on your PATH. Install it and sign in (`codex login`), or turn Codex off.",
|
|
1355
|
+
enable: null
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (signals.codexOnPath) {
|
|
1359
|
+
return {
|
|
1360
|
+
...base,
|
|
1361
|
+
state: "detected_not_exposed",
|
|
1362
|
+
version: signals.codexVersion,
|
|
1363
|
+
detail: "Codex is installed but not exposed yet. Turn it on to let Cabane run Codex here.",
|
|
1364
|
+
enable: "codex"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
...base,
|
|
1369
|
+
state: "not_detected",
|
|
1370
|
+
version: null,
|
|
1371
|
+
detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
|
|
1372
|
+
enable: null
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function deriveOpencode(signals, manifestHas) {
|
|
1376
|
+
const base = { runtime: "opencode", label: LABELS.opencode };
|
|
1377
|
+
if (manifestHas) {
|
|
1378
|
+
if (signals.opencodeReachable) {
|
|
1379
|
+
return {
|
|
1380
|
+
...base,
|
|
1381
|
+
state: "exposed",
|
|
1382
|
+
version: signals.opencodeVersion,
|
|
1383
|
+
detail: "An opencode server is reachable and exposed to Cabane.",
|
|
1384
|
+
enable: null
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
...base,
|
|
1389
|
+
state: "needs_attention",
|
|
1390
|
+
version: null,
|
|
1391
|
+
detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
|
|
1392
|
+
enable: null
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
...base,
|
|
1397
|
+
state: "not_detected",
|
|
1398
|
+
version: null,
|
|
1399
|
+
detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
|
|
1400
|
+
enable: "opencode"
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function detectedRuntimesFor(snapshot) {
|
|
1404
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
1405
|
+
}
|
|
1406
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
1407
|
+
async function probeHarnessSignals(cfg, deps = {}) {
|
|
1408
|
+
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
1409
|
+
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
1410
|
+
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
1411
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1412
|
+
const serverUrl = cfg.opencode?.serverUrl;
|
|
1413
|
+
const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
1414
|
+
withTimeout(probeClaudePresence(), false),
|
|
1415
|
+
withTimeout(probeClaudeVersion(), null),
|
|
1416
|
+
withTimeout(probeCodexVersion(), null),
|
|
1417
|
+
serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
|
|
1418
|
+
]);
|
|
1419
|
+
return {
|
|
1420
|
+
claudeOnPath: claudeOnPathResult,
|
|
1421
|
+
claudeVersion,
|
|
1422
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
1423
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
1424
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
1425
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
1426
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
1427
|
+
codexOnPath: codexVersion !== null,
|
|
1428
|
+
codexVersion,
|
|
1429
|
+
codexEnabled: isCodexEnabled(cfg),
|
|
1430
|
+
opencodeConfigured: !!serverUrl,
|
|
1431
|
+
// A version came back ⟺ the serve answered its health endpoint (CT584).
|
|
1432
|
+
opencodeReachable: opencodeVersion !== null,
|
|
1433
|
+
opencodeVersion
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function withTimeout(promise, fallback) {
|
|
1437
|
+
return new Promise((resolve) => {
|
|
1438
|
+
let settled = false;
|
|
1439
|
+
const done = (v) => {
|
|
1440
|
+
if (!settled) {
|
|
1441
|
+
settled = true;
|
|
1442
|
+
resolve(v);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS);
|
|
1446
|
+
timer.unref?.();
|
|
1447
|
+
promise.then(
|
|
1448
|
+
(v) => {
|
|
1449
|
+
clearTimeout(timer);
|
|
1450
|
+
done(v);
|
|
1451
|
+
},
|
|
1452
|
+
() => {
|
|
1453
|
+
clearTimeout(timer);
|
|
1454
|
+
done(fallback);
|
|
1455
|
+
}
|
|
1456
|
+
);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/harness-check.ts
|
|
1461
|
+
var CHECK_TIMEOUT_MS = 4e3;
|
|
1462
|
+
async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
1463
|
+
const run = deps.run ?? runBounded;
|
|
1464
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1465
|
+
try {
|
|
1466
|
+
if (runtime === "opencode") {
|
|
1467
|
+
const url = cfg.opencode?.serverUrl;
|
|
1468
|
+
if (!url) return "failed";
|
|
1469
|
+
return await probeOpencode(url) !== null ? "ok" : "failed";
|
|
1470
|
+
}
|
|
1471
|
+
const { auth, presence } = runtime === "codex" ? {
|
|
1472
|
+
auth: ["codex", ["login", "status"]],
|
|
1473
|
+
presence: ["codex", ["--version"]]
|
|
1474
|
+
} : {
|
|
1475
|
+
auth: ["claude", ["auth", "status"]],
|
|
1476
|
+
presence: ["claude", ["--version"]]
|
|
1477
|
+
};
|
|
1478
|
+
const authRun = await run(auth[0], [...auth[1]]);
|
|
1479
|
+
if (authRun.code === 0) return "ok";
|
|
1480
|
+
if (looksUnsupported(authRun.output)) {
|
|
1481
|
+
const presenceRun = await run(presence[0], [...presence[1]]);
|
|
1482
|
+
return presenceRun.code === 0 ? "unverified" : "failed";
|
|
1483
|
+
}
|
|
1484
|
+
return "failed";
|
|
1485
|
+
} catch {
|
|
1486
|
+
return "unverified";
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function connectedLine(runtime, verdict) {
|
|
1490
|
+
const label = HARNESS_LABELS[runtime];
|
|
1491
|
+
if (verdict !== "failed") return `${label} connected.`;
|
|
1492
|
+
return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
|
|
1493
|
+
}
|
|
1494
|
+
var FAILED_SUFFIX = {
|
|
1495
|
+
"claude-code": "it doesn\u2019t look signed in yet. Run `claude` once and sign in, then it\u2019s ready.",
|
|
1496
|
+
codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
|
|
1497
|
+
opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
|
|
1498
|
+
};
|
|
1499
|
+
function looksUnsupported(output) {
|
|
1500
|
+
return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
|
|
1501
|
+
output
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
function runBounded(command, args) {
|
|
1505
|
+
return new Promise((resolve) => {
|
|
1506
|
+
let settled = false;
|
|
1507
|
+
const done = (code, output) => {
|
|
1508
|
+
if (settled) return;
|
|
1509
|
+
settled = true;
|
|
1510
|
+
clearTimeout(timer);
|
|
1511
|
+
resolve({ code, output });
|
|
1512
|
+
};
|
|
1513
|
+
let child;
|
|
1514
|
+
try {
|
|
1515
|
+
child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1516
|
+
} catch {
|
|
1517
|
+
resolve({ code: null, output: "" });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
let out = "";
|
|
1521
|
+
const capture = (chunk) => {
|
|
1522
|
+
if (out.length < 4096) out += chunk.toString();
|
|
1523
|
+
};
|
|
1524
|
+
child.stdout?.on("data", capture);
|
|
1525
|
+
child.stderr?.on("data", capture);
|
|
1526
|
+
const timer = setTimeout(() => {
|
|
1527
|
+
child.kill("SIGKILL");
|
|
1528
|
+
done(null, out);
|
|
1529
|
+
}, CHECK_TIMEOUT_MS);
|
|
1530
|
+
timer.unref?.();
|
|
1531
|
+
child.once("error", () => done(null, out));
|
|
1532
|
+
child.once("exit", (code) => done(code, out));
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1105
1536
|
// src/runtime-file.ts
|
|
1106
1537
|
import {
|
|
1107
|
-
existsSync as
|
|
1538
|
+
existsSync as existsSync4,
|
|
1108
1539
|
readFileSync as readFileSync2,
|
|
1109
|
-
rmSync as
|
|
1540
|
+
rmSync as rmSync3,
|
|
1110
1541
|
writeFileSync as writeFileSync2,
|
|
1111
|
-
mkdirSync as
|
|
1542
|
+
mkdirSync as mkdirSync4,
|
|
1112
1543
|
openSync as openSync2,
|
|
1113
1544
|
closeSync as closeSync2
|
|
1114
1545
|
} from "fs";
|
|
1115
|
-
import { join as
|
|
1116
|
-
var
|
|
1546
|
+
import { join as join6 } from "path";
|
|
1547
|
+
var PROBE_TIMEOUT_MS2 = 1e3;
|
|
1117
1548
|
function runtimePath() {
|
|
1118
|
-
return
|
|
1549
|
+
return join6(cabaneDir(), "runtime.json");
|
|
1119
1550
|
}
|
|
1120
1551
|
function serialize(state) {
|
|
1121
1552
|
return JSON.stringify(state, null, 2) + "\n";
|
|
1122
1553
|
}
|
|
1123
1554
|
function writeRuntimeState(state) {
|
|
1124
1555
|
const path = runtimePath();
|
|
1125
|
-
|
|
1556
|
+
mkdirSync4(cabaneDir(), { recursive: true });
|
|
1126
1557
|
writeFileSync2(path, serialize(state), "utf8");
|
|
1127
1558
|
}
|
|
1128
1559
|
function acquireRuntimeState(state) {
|
|
1129
1560
|
const live = readLiveRuntimeState();
|
|
1130
1561
|
if (live) return { acquired: false, existing: live };
|
|
1131
|
-
|
|
1562
|
+
mkdirSync4(cabaneDir(), { recursive: true });
|
|
1132
1563
|
let fd;
|
|
1133
1564
|
try {
|
|
1134
1565
|
fd = openSync2(runtimePath(), "wx");
|
|
@@ -1144,11 +1575,21 @@ function acquireRuntimeState(state) {
|
|
|
1144
1575
|
}
|
|
1145
1576
|
function clearRuntimeState() {
|
|
1146
1577
|
const path = runtimePath();
|
|
1147
|
-
if (
|
|
1578
|
+
if (existsSync4(path)) rmSync3(path, { force: true });
|
|
1579
|
+
}
|
|
1580
|
+
function clearRuntimeStateIfOurs(instanceId) {
|
|
1581
|
+
const path = runtimePath();
|
|
1582
|
+
if (!existsSync4(path)) return;
|
|
1583
|
+
try {
|
|
1584
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1585
|
+
if (parsed.instanceId && parsed.instanceId !== instanceId) return;
|
|
1586
|
+
} catch {
|
|
1587
|
+
}
|
|
1588
|
+
rmSync3(path, { force: true });
|
|
1148
1589
|
}
|
|
1149
1590
|
function readLiveRuntimeState() {
|
|
1150
1591
|
const path = runtimePath();
|
|
1151
|
-
if (!
|
|
1592
|
+
if (!existsSync4(path)) return null;
|
|
1152
1593
|
let parsed;
|
|
1153
1594
|
try {
|
|
1154
1595
|
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -1164,33 +1605,21 @@ function readLiveRuntimeState() {
|
|
|
1164
1605
|
}
|
|
1165
1606
|
return parsed;
|
|
1166
1607
|
}
|
|
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";
|
|
1608
|
+
async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
1609
|
+
if (!state.instanceId || !state.socket) return "unknown";
|
|
1177
1610
|
let body;
|
|
1178
1611
|
try {
|
|
1179
|
-
body = await
|
|
1180
|
-
|
|
1181
|
-
|
|
1612
|
+
body = await requestImpl(
|
|
1613
|
+
state.socket,
|
|
1614
|
+
{ cmd: "status" },
|
|
1615
|
+
PROBE_TIMEOUT_MS2
|
|
1616
|
+
);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
return isNotListening(err) ? "stale" : "unknown";
|
|
1182
1619
|
}
|
|
1183
|
-
if (typeof body
|
|
1620
|
+
if (typeof body?.instance_id !== "string") return "unknown";
|
|
1184
1621
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1185
1622
|
}
|
|
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
1623
|
|
|
1195
1624
|
// src/api.ts
|
|
1196
1625
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
@@ -1667,22 +2096,22 @@ function errorMessage2(status, body) {
|
|
|
1667
2096
|
// src/credentials.ts
|
|
1668
2097
|
import {
|
|
1669
2098
|
chmodSync as chmodSync2,
|
|
1670
|
-
existsSync as
|
|
1671
|
-
mkdirSync as
|
|
2099
|
+
existsSync as existsSync5,
|
|
2100
|
+
mkdirSync as mkdirSync5,
|
|
1672
2101
|
readFileSync as readFileSync3,
|
|
1673
2102
|
renameSync as renameSync2,
|
|
1674
|
-
rmSync as
|
|
2103
|
+
rmSync as rmSync4,
|
|
1675
2104
|
writeFileSync as writeFileSync3
|
|
1676
2105
|
} from "fs";
|
|
1677
|
-
import { dirname as dirname4, join as
|
|
2106
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1678
2107
|
import { z as z3 } from "zod";
|
|
1679
2108
|
function credentialsPath() {
|
|
1680
|
-
return
|
|
2109
|
+
return join7(cabaneDir(), "credentials.json");
|
|
1681
2110
|
}
|
|
1682
2111
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1683
2112
|
function load() {
|
|
1684
2113
|
const path = credentialsPath();
|
|
1685
|
-
if (!
|
|
2114
|
+
if (!existsSync5(path)) return {};
|
|
1686
2115
|
let raw;
|
|
1687
2116
|
try {
|
|
1688
2117
|
raw = readFileSync3(path, "utf8");
|
|
@@ -1699,7 +2128,7 @@ function load() {
|
|
|
1699
2128
|
}
|
|
1700
2129
|
function save(map) {
|
|
1701
2130
|
const path = credentialsPath();
|
|
1702
|
-
|
|
2131
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1703
2132
|
try {
|
|
1704
2133
|
chmodSync2(cabaneDir(), 448);
|
|
1705
2134
|
} catch {
|
|
@@ -1714,7 +2143,7 @@ function save(map) {
|
|
|
1714
2143
|
renameSync2(tmp, path);
|
|
1715
2144
|
} catch (err) {
|
|
1716
2145
|
try {
|
|
1717
|
-
|
|
2146
|
+
rmSync4(tmp, { force: true });
|
|
1718
2147
|
} catch {
|
|
1719
2148
|
}
|
|
1720
2149
|
throw err;
|
|
@@ -1744,20 +2173,20 @@ function pruneCredentials(keepAgentIds) {
|
|
|
1744
2173
|
}
|
|
1745
2174
|
|
|
1746
2175
|
// src/cursor.ts
|
|
1747
|
-
import { mkdirSync as
|
|
1748
|
-
import { join as
|
|
2176
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2177
|
+
import { join as join8 } from "path";
|
|
1749
2178
|
function pathFor(workspaceId) {
|
|
1750
|
-
return
|
|
2179
|
+
return join8(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
1751
2180
|
}
|
|
1752
2181
|
function readCursor(workspaceId) {
|
|
1753
2182
|
const path = pathFor(workspaceId);
|
|
1754
|
-
if (!
|
|
2183
|
+
if (!existsSync6(path)) return null;
|
|
1755
2184
|
const raw = readFileSync4(path, "utf8").trim();
|
|
1756
2185
|
return raw.length > 0 ? raw : null;
|
|
1757
2186
|
}
|
|
1758
2187
|
function writeCursor(workspaceId, eventId) {
|
|
1759
2188
|
const path = pathFor(workspaceId);
|
|
1760
|
-
|
|
2189
|
+
mkdirSync6(join8(cabaneDir(), "cursors"), { recursive: true });
|
|
1761
2190
|
writeFileSync4(path, eventId + "\n", "utf8");
|
|
1762
2191
|
}
|
|
1763
2192
|
|
|
@@ -1801,18 +2230,18 @@ var CursorTracker = class {
|
|
|
1801
2230
|
};
|
|
1802
2231
|
|
|
1803
2232
|
// src/dispatch-dedupe.ts
|
|
1804
|
-
import { mkdirSync as
|
|
1805
|
-
import { join as
|
|
2233
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
2234
|
+
import { join as join9 } from "path";
|
|
1806
2235
|
var MAX_IDS = 256;
|
|
1807
2236
|
function dir(log) {
|
|
1808
|
-
return
|
|
2237
|
+
return join9(cabaneDir(), log);
|
|
1809
2238
|
}
|
|
1810
2239
|
function pathFor2(log, workspaceId) {
|
|
1811
|
-
return
|
|
2240
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
1812
2241
|
}
|
|
1813
2242
|
function readIds(log, workspaceId) {
|
|
1814
2243
|
const path = pathFor2(log, workspaceId);
|
|
1815
|
-
if (!
|
|
2244
|
+
if (!existsSync7(path)) return [];
|
|
1816
2245
|
try {
|
|
1817
2246
|
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1818
2247
|
} catch {
|
|
@@ -1827,7 +2256,7 @@ function mark(log, workspaceId, eventId) {
|
|
|
1827
2256
|
if (ids.includes(eventId)) return;
|
|
1828
2257
|
ids.push(eventId);
|
|
1829
2258
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
1830
|
-
|
|
2259
|
+
mkdirSync7(dir(log), { recursive: true });
|
|
1831
2260
|
writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
1832
2261
|
}
|
|
1833
2262
|
function hasDispatched(workspaceId, eventId) {
|
|
@@ -1844,15 +2273,15 @@ function markCompleted(workspaceId, eventId) {
|
|
|
1844
2273
|
}
|
|
1845
2274
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
1846
2275
|
function resumeDir() {
|
|
1847
|
-
return
|
|
2276
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
1848
2277
|
}
|
|
1849
2278
|
function resumePathFor(workspaceId) {
|
|
1850
|
-
return
|
|
2279
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
1851
2280
|
}
|
|
1852
2281
|
function readResumeCounts(workspaceId) {
|
|
1853
2282
|
const out = /* @__PURE__ */ new Map();
|
|
1854
2283
|
const path = resumePathFor(workspaceId);
|
|
1855
|
-
if (!
|
|
2284
|
+
if (!existsSync7(path)) return out;
|
|
1856
2285
|
try {
|
|
1857
2286
|
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
1858
2287
|
const trimmed = line.trim();
|
|
@@ -1874,7 +2303,7 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
1874
2303
|
counts.set(eventId, next);
|
|
1875
2304
|
const entries = [...counts.entries()];
|
|
1876
2305
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
1877
|
-
|
|
2306
|
+
mkdirSync7(resumeDir(), { recursive: true });
|
|
1878
2307
|
writeFileSync5(
|
|
1879
2308
|
resumePathFor(workspaceId),
|
|
1880
2309
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
@@ -3766,7 +4195,7 @@ async function acquireServerTurnLock(url) {
|
|
|
3766
4195
|
};
|
|
3767
4196
|
}
|
|
3768
4197
|
function createHttpOpencodeTransport(opts) {
|
|
3769
|
-
const base =
|
|
4198
|
+
const base = trimSlash(opts.baseUrl);
|
|
3770
4199
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
3771
4200
|
return {
|
|
3772
4201
|
async run(spec, signal) {
|
|
@@ -3912,7 +4341,7 @@ function belongsToSession(ev, sessionId) {
|
|
|
3912
4341
|
function newOpencodeMessageId() {
|
|
3913
4342
|
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
3914
4343
|
}
|
|
3915
|
-
function
|
|
4344
|
+
function trimSlash(s) {
|
|
3916
4345
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
3917
4346
|
}
|
|
3918
4347
|
|
|
@@ -4662,7 +5091,13 @@ function parseCodexModel(model) {
|
|
|
4662
5091
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
4663
5092
|
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
4664
5093
|
var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
|
|
4665
|
-
var ENV_ENVELOPE_KEYS = [
|
|
5094
|
+
var ENV_ENVELOPE_KEYS = [
|
|
5095
|
+
"CABANE_PLAYGROUND",
|
|
5096
|
+
"CABANE_PLAYGROUND_BIN",
|
|
5097
|
+
"CABANE_CONVERSATION_ID",
|
|
5098
|
+
"CABANE_AGENT_ID",
|
|
5099
|
+
"CABANE_CONVERSATION_TITLE"
|
|
5100
|
+
];
|
|
4666
5101
|
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
|
|
4667
5102
|
const { policy, config } = req;
|
|
4668
5103
|
const directory = req.local.cwd ?? "";
|
|
@@ -5567,8 +6002,8 @@ var ConnectorHealthStore = class {
|
|
|
5567
6002
|
|
|
5568
6003
|
// src/dispatcher.ts
|
|
5569
6004
|
import { randomUUID } from "crypto";
|
|
5570
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
5571
|
-
import { join as
|
|
6005
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6006
|
+
import { join as join14 } from "path";
|
|
5572
6007
|
|
|
5573
6008
|
// src/summon.ts
|
|
5574
6009
|
import { z as z12 } from "zod";
|
|
@@ -5842,10 +6277,10 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5842
6277
|
|
|
5843
6278
|
// src/build-options.ts
|
|
5844
6279
|
function cabaneMcpUrl(baseUrl) {
|
|
5845
|
-
return `${
|
|
6280
|
+
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
5846
6281
|
}
|
|
5847
6282
|
function turnControlMcpUrl(baseUrl) {
|
|
5848
|
-
return `${
|
|
6283
|
+
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
5849
6284
|
}
|
|
5850
6285
|
function buildCompanionTurnRequest(params) {
|
|
5851
6286
|
const { turnContext: t } = params;
|
|
@@ -5894,18 +6329,18 @@ function buildCompanionTurnRequest(params) {
|
|
|
5894
6329
|
}
|
|
5895
6330
|
};
|
|
5896
6331
|
}
|
|
5897
|
-
function
|
|
6332
|
+
function trimSlash2(s) {
|
|
5898
6333
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
5899
6334
|
}
|
|
5900
6335
|
|
|
5901
6336
|
// src/codex-instructions.ts
|
|
5902
6337
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
5903
6338
|
import { tmpdir } from "os";
|
|
5904
|
-
import { join as
|
|
6339
|
+
import { join as join10 } from "path";
|
|
5905
6340
|
var PREFIX = "cabane-codex-instructions-";
|
|
5906
6341
|
async function writeCodexInstructionsFile(contents) {
|
|
5907
|
-
const dir2 = await mkdtemp(
|
|
5908
|
-
const path =
|
|
6342
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
6343
|
+
const path = join10(dir2, "instructions.md");
|
|
5909
6344
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
5910
6345
|
return {
|
|
5911
6346
|
path,
|
|
@@ -5916,20 +6351,20 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
5916
6351
|
}
|
|
5917
6352
|
|
|
5918
6353
|
// src/prepared.ts
|
|
5919
|
-
import { mkdirSync as
|
|
5920
|
-
import { join as
|
|
6354
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
6355
|
+
import { join as join11 } from "path";
|
|
5921
6356
|
function dirFor(workspaceId) {
|
|
5922
|
-
return
|
|
6357
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
5923
6358
|
}
|
|
5924
6359
|
function conversationDir(workspaceId, conversationId) {
|
|
5925
|
-
return
|
|
6360
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
5926
6361
|
}
|
|
5927
6362
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
5928
|
-
return
|
|
6363
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
5929
6364
|
}
|
|
5930
6365
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
5931
6366
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
5932
|
-
if (!
|
|
6367
|
+
if (!existsSync8(path)) return null;
|
|
5933
6368
|
try {
|
|
5934
6369
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
5935
6370
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
@@ -5944,7 +6379,7 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
5944
6379
|
}
|
|
5945
6380
|
}
|
|
5946
6381
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
5947
|
-
|
|
6382
|
+
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
5948
6383
|
writeFileSync6(
|
|
5949
6384
|
pathFor3(workspaceId, conversationId, agentId),
|
|
5950
6385
|
JSON.stringify(result) + "\n",
|
|
@@ -5952,21 +6387,21 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
|
5952
6387
|
);
|
|
5953
6388
|
}
|
|
5954
6389
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
5955
|
-
|
|
6390
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
5956
6391
|
}
|
|
5957
6392
|
|
|
5958
6393
|
// src/secrets.ts
|
|
5959
|
-
import { existsSync as
|
|
5960
|
-
import { join as
|
|
6394
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
6395
|
+
import { join as join12 } from "path";
|
|
5961
6396
|
import { z as z13 } from "zod";
|
|
5962
6397
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5963
6398
|
function secretsPath() {
|
|
5964
|
-
return
|
|
6399
|
+
return join12(cabaneDir(), "secrets.json");
|
|
5965
6400
|
}
|
|
5966
6401
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
5967
6402
|
function loadSecretStore() {
|
|
5968
6403
|
const path = secretsPath();
|
|
5969
|
-
if (!
|
|
6404
|
+
if (!existsSync9(path)) return makeStore({});
|
|
5970
6405
|
let raw;
|
|
5971
6406
|
try {
|
|
5972
6407
|
raw = readFileSync7(path, "utf8");
|
|
@@ -6047,10 +6482,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6047
6482
|
}
|
|
6048
6483
|
|
|
6049
6484
|
// src/transcript-writer.ts
|
|
6050
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
6051
|
-
import { join as
|
|
6485
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
6486
|
+
import { join as join13 } from "path";
|
|
6052
6487
|
function transcriptsDir() {
|
|
6053
|
-
return
|
|
6488
|
+
return join13(cabaneDir(), "transcripts");
|
|
6054
6489
|
}
|
|
6055
6490
|
var RETAIN = 200;
|
|
6056
6491
|
var TranscriptWriter = class {
|
|
@@ -6059,9 +6494,9 @@ var TranscriptWriter = class {
|
|
|
6059
6494
|
onWarn;
|
|
6060
6495
|
constructor(dir2, meta, onWarn) {
|
|
6061
6496
|
this.onWarn = onWarn;
|
|
6062
|
-
this.path =
|
|
6497
|
+
this.path = join13(dir2, fileName(meta));
|
|
6063
6498
|
try {
|
|
6064
|
-
|
|
6499
|
+
mkdirSync9(dir2, { recursive: true });
|
|
6065
6500
|
try {
|
|
6066
6501
|
chmodSync3(dir2, 448);
|
|
6067
6502
|
} catch {
|
|
@@ -6118,7 +6553,7 @@ function pruneOld(dir2, retain) {
|
|
|
6118
6553
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6119
6554
|
for (const f of drop) {
|
|
6120
6555
|
try {
|
|
6121
|
-
|
|
6556
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
6122
6557
|
} catch {
|
|
6123
6558
|
}
|
|
6124
6559
|
}
|
|
@@ -6464,7 +6899,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
6464
6899
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6465
6900
|
function checkoutState(cwd) {
|
|
6466
6901
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
6467
|
-
if (!
|
|
6902
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
6468
6903
|
let entries;
|
|
6469
6904
|
try {
|
|
6470
6905
|
entries = readdirSync2(cwd);
|
|
@@ -6474,15 +6909,15 @@ function checkoutState(cwd) {
|
|
|
6474
6909
|
if (entries.length === 0) {
|
|
6475
6910
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
6476
6911
|
}
|
|
6477
|
-
const gitPath =
|
|
6478
|
-
if (!
|
|
6912
|
+
const gitPath = join14(cwd, ".git");
|
|
6913
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
6479
6914
|
let stat;
|
|
6480
6915
|
try {
|
|
6481
6916
|
stat = statSync(gitPath);
|
|
6482
6917
|
} catch (error) {
|
|
6483
6918
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6484
6919
|
}
|
|
6485
|
-
if (stat.isDirectory() && !
|
|
6920
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
6486
6921
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
6487
6922
|
return { ok: true, reason: "usable" };
|
|
6488
6923
|
}
|
|
@@ -6673,7 +7108,7 @@ var Dispatcher = class {
|
|
|
6673
7108
|
let seqCounter = 0;
|
|
6674
7109
|
const nextSeq = () => ++seqCounter;
|
|
6675
7110
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
6676
|
-
if (effectiveCwd && !
|
|
7111
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
6677
7112
|
turnLog.warn(
|
|
6678
7113
|
{ cwd: effectiveCwd },
|
|
6679
7114
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -6981,7 +7416,7 @@ ${reason}`,
|
|
|
6981
7416
|
}
|
|
6982
7417
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
6983
7418
|
}
|
|
6984
|
-
const receiptPath =
|
|
7419
|
+
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
6985
7420
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
6986
7421
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6987
7422
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -7005,7 +7440,7 @@ ${reason}`,
|
|
|
7005
7440
|
})}
|
|
7006
7441
|
`;
|
|
7007
7442
|
try {
|
|
7008
|
-
|
|
7443
|
+
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
7009
7444
|
appendFileSync2(
|
|
7010
7445
|
receiptPath,
|
|
7011
7446
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -7433,202 +7868,6 @@ ${reason}`,
|
|
|
7433
7868
|
}
|
|
7434
7869
|
};
|
|
7435
7870
|
|
|
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
7871
|
// src/opencode-models.ts
|
|
7633
7872
|
var OPENCODE_RUNTIME = "opencode";
|
|
7634
7873
|
function mapOpencodeProviders(json) {
|
|
@@ -7672,15 +7911,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
7672
7911
|
|
|
7673
7912
|
// src/outbox.ts
|
|
7674
7913
|
import {
|
|
7675
|
-
existsSync as
|
|
7676
|
-
mkdirSync as
|
|
7914
|
+
existsSync as existsSync11,
|
|
7915
|
+
mkdirSync as mkdirSync11,
|
|
7677
7916
|
readdirSync as readdirSync3,
|
|
7678
7917
|
readFileSync as readFileSync8,
|
|
7679
7918
|
renameSync as renameSync3,
|
|
7680
|
-
rmSync as
|
|
7919
|
+
rmSync as rmSync7,
|
|
7681
7920
|
writeFileSync as writeFileSync7
|
|
7682
7921
|
} from "fs";
|
|
7683
|
-
import { join as
|
|
7922
|
+
import { join as join15 } from "path";
|
|
7684
7923
|
var MAX_ENTRIES = 2e3;
|
|
7685
7924
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7686
7925
|
var Outbox = class {
|
|
@@ -7693,17 +7932,17 @@ var Outbox = class {
|
|
|
7693
7932
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
7694
7933
|
// cases route writes at the right tmpdir.
|
|
7695
7934
|
dir() {
|
|
7696
|
-
return
|
|
7935
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7697
7936
|
}
|
|
7698
7937
|
fileFor(turnId, seq) {
|
|
7699
|
-
return
|
|
7938
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
7700
7939
|
}
|
|
7701
7940
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
7702
7941
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
7703
7942
|
// per-workspace bounds.
|
|
7704
7943
|
persist(entry) {
|
|
7705
7944
|
const dir2 = this.dir();
|
|
7706
|
-
|
|
7945
|
+
mkdirSync11(dir2, { recursive: true });
|
|
7707
7946
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
7708
7947
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
7709
7948
|
try {
|
|
@@ -7711,7 +7950,7 @@ var Outbox = class {
|
|
|
7711
7950
|
renameSync3(tmp, target);
|
|
7712
7951
|
} catch (err) {
|
|
7713
7952
|
try {
|
|
7714
|
-
|
|
7953
|
+
rmSync7(tmp, { force: true });
|
|
7715
7954
|
} catch {
|
|
7716
7955
|
}
|
|
7717
7956
|
this.log?.warn(
|
|
@@ -7728,7 +7967,7 @@ var Outbox = class {
|
|
|
7728
7967
|
// wedging the drain.
|
|
7729
7968
|
list() {
|
|
7730
7969
|
const dir2 = this.dir();
|
|
7731
|
-
if (!
|
|
7970
|
+
if (!existsSync11(dir2)) return [];
|
|
7732
7971
|
let names;
|
|
7733
7972
|
try {
|
|
7734
7973
|
names = readdirSync3(dir2);
|
|
@@ -7738,7 +7977,7 @@ var Outbox = class {
|
|
|
7738
7977
|
const entries = [];
|
|
7739
7978
|
for (const name of names) {
|
|
7740
7979
|
if (!name.endsWith(".json")) continue;
|
|
7741
|
-
const full =
|
|
7980
|
+
const full = join15(dir2, name);
|
|
7742
7981
|
try {
|
|
7743
7982
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
7744
7983
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -7758,13 +7997,13 @@ var Outbox = class {
|
|
|
7758
7997
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
7759
7998
|
remove(turnId, seq) {
|
|
7760
7999
|
try {
|
|
7761
|
-
|
|
8000
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
7762
8001
|
} catch {
|
|
7763
8002
|
}
|
|
7764
8003
|
}
|
|
7765
8004
|
size() {
|
|
7766
8005
|
const dir2 = this.dir();
|
|
7767
|
-
if (!
|
|
8006
|
+
if (!existsSync11(dir2)) return 0;
|
|
7768
8007
|
try {
|
|
7769
8008
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
7770
8009
|
} catch {
|
|
@@ -7777,7 +8016,7 @@ var Outbox = class {
|
|
|
7777
8016
|
"companion outbox: dropping unreadable entry"
|
|
7778
8017
|
);
|
|
7779
8018
|
try {
|
|
7780
|
-
|
|
8019
|
+
rmSync7(full, { force: true });
|
|
7781
8020
|
} catch {
|
|
7782
8021
|
}
|
|
7783
8022
|
}
|
|
@@ -8706,6 +8945,26 @@ var CompanionSupervisor = class {
|
|
|
8706
8945
|
async recheckHarnesses() {
|
|
8707
8946
|
await this.refreshHarnessStatuses();
|
|
8708
8947
|
}
|
|
8948
|
+
// CT1085 §1 step 3: beat NOW and wait for it to land. `start` calls this
|
|
8949
|
+
// between bringing the runtime up and asking the person anything, so the
|
|
8950
|
+
// browser's connect step is already showing what this machine has ("your
|
|
8951
|
+
// terminal is asking") rather than sitting blank while the terminal blocks on
|
|
8952
|
+
// an answer. Coalesces with an in-flight beat rather than stacking a second.
|
|
8953
|
+
async heartbeatNow() {
|
|
8954
|
+
if (this.inFlightHeartbeat) {
|
|
8955
|
+
await this.inFlightHeartbeat;
|
|
8956
|
+
return;
|
|
8957
|
+
}
|
|
8958
|
+
this.kickHeartbeat();
|
|
8959
|
+
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
8960
|
+
}
|
|
8961
|
+
// CT1085: the config as it stands right now — after any `enableHarness` write.
|
|
8962
|
+
// The control socket's connect handler needs it to run the shake-out check
|
|
8963
|
+
// against the URL/flag that was just persisted, not the one this process booted
|
|
8964
|
+
// with.
|
|
8965
|
+
currentConfig() {
|
|
8966
|
+
return this.config;
|
|
8967
|
+
}
|
|
8709
8968
|
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8710
8969
|
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8711
8970
|
// never installs a binary and never drives a login (BYO — Decided).
|
|
@@ -8847,9 +9106,9 @@ var CompanionSupervisor = class {
|
|
|
8847
9106
|
};
|
|
8848
9107
|
function defaultReexec() {
|
|
8849
9108
|
clearRuntimeState();
|
|
8850
|
-
void import("child_process").then(({ spawn:
|
|
9109
|
+
void import("child_process").then(({ spawn: spawn5 }) => {
|
|
8851
9110
|
try {
|
|
8852
|
-
const child =
|
|
9111
|
+
const child = spawn5(process.execPath, process.argv.slice(1), {
|
|
8853
9112
|
stdio: "inherit",
|
|
8854
9113
|
detached: false
|
|
8855
9114
|
});
|
|
@@ -8919,14 +9178,14 @@ function handleUncaught(log, err, origin) {
|
|
|
8919
9178
|
}
|
|
8920
9179
|
|
|
8921
9180
|
// src/crash-marker.ts
|
|
8922
|
-
import { existsSync as
|
|
8923
|
-
import { join as
|
|
9181
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9182
|
+
import { join as join16 } from "path";
|
|
8924
9183
|
function crashMarkerPath() {
|
|
8925
|
-
return
|
|
9184
|
+
return join16(cabaneDir(), "last-error.json");
|
|
8926
9185
|
}
|
|
8927
9186
|
function recordCrash(rec) {
|
|
8928
9187
|
try {
|
|
8929
|
-
|
|
9188
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
8930
9189
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
8931
9190
|
} catch {
|
|
8932
9191
|
}
|
|
@@ -8934,7 +9193,7 @@ function recordCrash(rec) {
|
|
|
8934
9193
|
function clearCrash() {
|
|
8935
9194
|
try {
|
|
8936
9195
|
const path = crashMarkerPath();
|
|
8937
|
-
if (
|
|
9196
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
8938
9197
|
} catch {
|
|
8939
9198
|
}
|
|
8940
9199
|
}
|
|
@@ -8953,7 +9212,13 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8953
9212
|
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
9213
|
)
|
|
8955
9214
|
}));
|
|
8956
|
-
await warnAboutHarnessReadiness(cfg, {
|
|
9215
|
+
await warnAboutHarnessReadiness(cfg, {
|
|
9216
|
+
probeClaude: async () => claudeCode,
|
|
9217
|
+
// CT1085: the CLI's onboarding script owns the terminal and says this in
|
|
9218
|
+
// its own words (the `!` block, or the per-harness offer), so it hands us a
|
|
9219
|
+
// sink that logs instead. Every other caller keeps the stderr line.
|
|
9220
|
+
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
9221
|
+
});
|
|
8957
9222
|
} catch (err) {
|
|
8958
9223
|
recordCrash({
|
|
8959
9224
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -8977,8 +9242,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8977
9242
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|
|
8978
9243
|
const claim = acquireRuntimeState({
|
|
8979
9244
|
pid: process.pid,
|
|
8980
|
-
url: "",
|
|
8981
|
-
port: 0,
|
|
8982
9245
|
startedAt,
|
|
8983
9246
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
8984
9247
|
instanceId
|
|
@@ -8986,7 +9249,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8986
9249
|
if (!claim.acquired) {
|
|
8987
9250
|
return { ok: false, reason: "already-running", existing: claim.existing ?? null };
|
|
8988
9251
|
}
|
|
8989
|
-
process.on("exit", () =>
|
|
9252
|
+
process.on("exit", () => clearRuntimeStateIfOurs(instanceId));
|
|
8990
9253
|
const hub = new CompanionStateHub({
|
|
8991
9254
|
// CT29: one device, one base URL — the cabane instance this device is paired
|
|
8992
9255
|
// with. The dashboard's connection line shows it.
|
|
@@ -9004,17 +9267,36 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9004
9267
|
harnessVersions
|
|
9005
9268
|
});
|
|
9006
9269
|
await supervisor.start();
|
|
9007
|
-
const
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9270
|
+
const connectHarness = async (runtime, serverUrl) => {
|
|
9271
|
+
const result = await supervisor.enableHarness(
|
|
9272
|
+
runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
|
|
9273
|
+
);
|
|
9274
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
9275
|
+
const verdict = await shakeOutHarness(runtime, supervisor.currentConfig());
|
|
9276
|
+
return { ok: true, message: connectedLine(runtime, verdict) };
|
|
9277
|
+
};
|
|
9278
|
+
const control = await startControlServer({
|
|
9279
|
+
status: () => hub.statusJson(),
|
|
9280
|
+
connect: async (runtime, serverUrl) => {
|
|
9281
|
+
const result = await connectHarness(runtime, serverUrl);
|
|
9282
|
+
return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
|
|
9283
|
+
},
|
|
9284
|
+
stop: () => void supervisor.requestStop()
|
|
9012
9285
|
});
|
|
9013
|
-
|
|
9286
|
+
let dashboard = null;
|
|
9287
|
+
if (opts.dashboard) {
|
|
9288
|
+
const preferredPort = opts.port ?? cfg.dashboardPort;
|
|
9289
|
+
dashboard = await startDashboard({
|
|
9290
|
+
supervisor,
|
|
9291
|
+
hub,
|
|
9292
|
+
...preferredPort !== void 0 ? { port: preferredPort } : {}
|
|
9293
|
+
});
|
|
9294
|
+
hub.setDashboardUrl(dashboard.url);
|
|
9295
|
+
}
|
|
9014
9296
|
writeRuntimeState({
|
|
9015
9297
|
pid: process.pid,
|
|
9016
|
-
|
|
9017
|
-
port: dashboard.port,
|
|
9298
|
+
socket: control.path,
|
|
9299
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9018
9300
|
startedAt,
|
|
9019
9301
|
// SJ495: the daemon launcher sets this env on the detached child, so the
|
|
9020
9302
|
// marker records whether this companion is backgrounded (foreground start
|
|
@@ -9027,30 +9309,37 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9027
9309
|
const stop = async () => {
|
|
9028
9310
|
if (stopped) return;
|
|
9029
9311
|
stopped = true;
|
|
9030
|
-
|
|
9312
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9031
9313
|
try {
|
|
9032
9314
|
await supervisor.shutdown();
|
|
9033
|
-
await dashboard
|
|
9315
|
+
await closeSurfaces(control, dashboard);
|
|
9034
9316
|
} catch {
|
|
9035
9317
|
}
|
|
9036
9318
|
};
|
|
9037
9319
|
return {
|
|
9038
9320
|
ok: true,
|
|
9039
9321
|
runtime: {
|
|
9040
|
-
url: dashboard.url,
|
|
9041
|
-
|
|
9322
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9323
|
+
socketPath: control.path,
|
|
9042
9324
|
config: cfg,
|
|
9043
9325
|
stop,
|
|
9326
|
+
heartbeatNow: () => supervisor.heartbeatNow(),
|
|
9327
|
+
harnesses: () => hub.statusJson().harnesses ?? [],
|
|
9328
|
+
connectHarness,
|
|
9044
9329
|
drainForRestart: async (graceMs) => {
|
|
9045
|
-
|
|
9330
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9046
9331
|
const result = await supervisor.drainForRestart(graceMs);
|
|
9047
|
-
await dashboard
|
|
9332
|
+
await closeSurfaces(control, dashboard);
|
|
9048
9333
|
stopped = true;
|
|
9049
9334
|
return result;
|
|
9050
9335
|
}
|
|
9051
9336
|
}
|
|
9052
9337
|
};
|
|
9053
9338
|
}
|
|
9339
|
+
async function closeSurfaces(control, dashboard) {
|
|
9340
|
+
await control.close();
|
|
9341
|
+
if (dashboard) await dashboard.close();
|
|
9342
|
+
}
|
|
9054
9343
|
export {
|
|
9055
9344
|
createCompanionRuntime
|
|
9056
9345
|
};
|