@cabane/companion 0.6.31 → 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 +1871 -1267
- package/dist/pairing-config.js +36 -21
- package/dist/runtime.js +629 -375
- 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");
|
|
@@ -1142,13 +1573,23 @@ function acquireRuntimeState(state) {
|
|
|
1142
1573
|
}
|
|
1143
1574
|
return { acquired: true };
|
|
1144
1575
|
}
|
|
1145
|
-
function clearRuntimeState() {
|
|
1576
|
+
function clearRuntimeState() {
|
|
1577
|
+
const path = runtimePath();
|
|
1578
|
+
if (existsSync4(path)) rmSync3(path, { force: true });
|
|
1579
|
+
}
|
|
1580
|
+
function clearRuntimeStateIfOurs(instanceId) {
|
|
1146
1581
|
const path = runtimePath();
|
|
1147
|
-
if (
|
|
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
|
|
|
@@ -4626,9 +5055,13 @@ function sealHeld2(held, terminal) {
|
|
|
4626
5055
|
import { z as z11 } from "zod";
|
|
4627
5056
|
function codexToolPolicy(policy) {
|
|
4628
5057
|
return policy.hostFs ? {
|
|
4629
|
-
|
|
5058
|
+
sandboxMode: "danger-full-access",
|
|
5059
|
+
// Headless: nothing to approve, because nothing is denied.
|
|
4630
5060
|
approvalPolicy: "never",
|
|
4631
|
-
|
|
5061
|
+
// Full access means the command network is open regardless of the grant —
|
|
5062
|
+
// recorded as `true` so the value never claims a restriction that isn't
|
|
5063
|
+
// enforced.
|
|
5064
|
+
networkAccessEnabled: true
|
|
4632
5065
|
} : {
|
|
4633
5066
|
sandboxMode: "read-only",
|
|
4634
5067
|
// Headless: the sandbox is the boundary; never pause for a human.
|
|
@@ -4658,7 +5091,13 @@ function parseCodexModel(model) {
|
|
|
4658
5091
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
4659
5092
|
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
4660
5093
|
var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
|
|
4661
|
-
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
|
+
];
|
|
4662
5101
|
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
|
|
4663
5102
|
const { policy, config } = req;
|
|
4664
5103
|
const directory = req.local.cwd ?? "";
|
|
@@ -4748,7 +5187,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4748
5187
|
default_tools_approval_mode: "approve"
|
|
4749
5188
|
};
|
|
4750
5189
|
}
|
|
4751
|
-
const policy = codexToolPolicy(req.policy);
|
|
4752
5190
|
const tmpDir = req.local.env?.TMPDIR;
|
|
4753
5191
|
const shellEnv = {};
|
|
4754
5192
|
if (tmpDir) shellEnv.TMPDIR = tmpDir;
|
|
@@ -4756,7 +5194,6 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4756
5194
|
const value = req.local.env?.[key];
|
|
4757
5195
|
if (value !== void 0) shellEnv[key] = value;
|
|
4758
5196
|
}
|
|
4759
|
-
const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
|
|
4760
5197
|
return {
|
|
4761
5198
|
mcp_servers,
|
|
4762
5199
|
experimental_use_rmcp_client: true,
|
|
@@ -4764,28 +5201,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
4764
5201
|
// becomes Codex's base instructions, and Codex's own environment preamble goes
|
|
4765
5202
|
// with the harness prompt it belonged to.
|
|
4766
5203
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
4767
|
-
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
4768
|
-
...policy.permissionProfile ? {
|
|
4769
|
-
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
4770
|
-
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
4771
|
-
// workspace-root write grants the checkout, and the more-specific
|
|
4772
|
-
// `.git` write reopens the metadata Codex protects by default. Neither
|
|
4773
|
-
// rule grants an adjacent directory. Do not combine this with legacy `sandbox_mode` /
|
|
4774
|
-
// `sandbox_workspace_write`, which would restore the `.git` carve-out.
|
|
4775
|
-
approval_policy: policy.approvalPolicy,
|
|
4776
|
-
default_permissions: policy.permissionProfile,
|
|
4777
|
-
permissions: {
|
|
4778
|
-
[policy.permissionProfile]: {
|
|
4779
|
-
filesystem: {
|
|
4780
|
-
":root": "read",
|
|
4781
|
-
":workspace_roots": { ".": "write", ".git": "write" },
|
|
4782
|
-
// The one absolute-path grant — see the CT612-fallout note above.
|
|
4783
|
-
...gateLockDir ? { [gateLockDir]: "write" } : {}
|
|
4784
|
-
},
|
|
4785
|
-
network: { enabled: policy.networkAccessEnabled, mode: "full" }
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
} : {}
|
|
5204
|
+
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
4789
5205
|
};
|
|
4790
5206
|
}
|
|
4791
5207
|
function isStringRecord2(v) {
|
|
@@ -4799,11 +5215,10 @@ import {
|
|
|
4799
5215
|
function buildSdkThreadOptions(spec) {
|
|
4800
5216
|
return {
|
|
4801
5217
|
...spec.model ? { model: spec.model } : {},
|
|
4802
|
-
|
|
5218
|
+
sandboxMode: spec.policy.sandboxMode,
|
|
4803
5219
|
workingDirectory: spec.directory,
|
|
4804
5220
|
skipGitRepoCheck: spec.skipGitRepoCheck,
|
|
4805
|
-
|
|
4806
|
-
...spec.policy.sandboxMode === "workspace-write" ? { networkAccessEnabled: spec.policy.networkAccessEnabled } : {},
|
|
5221
|
+
approvalPolicy: spec.policy.approvalPolicy,
|
|
4807
5222
|
...spec.modelReasoningEffort ? { modelReasoningEffort: spec.modelReasoningEffort } : {}
|
|
4808
5223
|
};
|
|
4809
5224
|
}
|
|
@@ -5587,8 +6002,8 @@ var ConnectorHealthStore = class {
|
|
|
5587
6002
|
|
|
5588
6003
|
// src/dispatcher.ts
|
|
5589
6004
|
import { randomUUID } from "crypto";
|
|
5590
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
5591
|
-
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";
|
|
5592
6007
|
|
|
5593
6008
|
// src/summon.ts
|
|
5594
6009
|
import { z as z12 } from "zod";
|
|
@@ -5862,10 +6277,10 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5862
6277
|
|
|
5863
6278
|
// src/build-options.ts
|
|
5864
6279
|
function cabaneMcpUrl(baseUrl) {
|
|
5865
|
-
return `${
|
|
6280
|
+
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
5866
6281
|
}
|
|
5867
6282
|
function turnControlMcpUrl(baseUrl) {
|
|
5868
|
-
return `${
|
|
6283
|
+
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
5869
6284
|
}
|
|
5870
6285
|
function buildCompanionTurnRequest(params) {
|
|
5871
6286
|
const { turnContext: t } = params;
|
|
@@ -5914,18 +6329,18 @@ function buildCompanionTurnRequest(params) {
|
|
|
5914
6329
|
}
|
|
5915
6330
|
};
|
|
5916
6331
|
}
|
|
5917
|
-
function
|
|
6332
|
+
function trimSlash2(s) {
|
|
5918
6333
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
5919
6334
|
}
|
|
5920
6335
|
|
|
5921
6336
|
// src/codex-instructions.ts
|
|
5922
6337
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
5923
6338
|
import { tmpdir } from "os";
|
|
5924
|
-
import { join as
|
|
6339
|
+
import { join as join10 } from "path";
|
|
5925
6340
|
var PREFIX = "cabane-codex-instructions-";
|
|
5926
6341
|
async function writeCodexInstructionsFile(contents) {
|
|
5927
|
-
const dir2 = await mkdtemp(
|
|
5928
|
-
const path =
|
|
6342
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
6343
|
+
const path = join10(dir2, "instructions.md");
|
|
5929
6344
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
5930
6345
|
return {
|
|
5931
6346
|
path,
|
|
@@ -5936,20 +6351,20 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
5936
6351
|
}
|
|
5937
6352
|
|
|
5938
6353
|
// src/prepared.ts
|
|
5939
|
-
import { mkdirSync as
|
|
5940
|
-
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";
|
|
5941
6356
|
function dirFor(workspaceId) {
|
|
5942
|
-
return
|
|
6357
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
5943
6358
|
}
|
|
5944
6359
|
function conversationDir(workspaceId, conversationId) {
|
|
5945
|
-
return
|
|
6360
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
5946
6361
|
}
|
|
5947
6362
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
5948
|
-
return
|
|
6363
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
5949
6364
|
}
|
|
5950
6365
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
5951
6366
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
5952
|
-
if (!
|
|
6367
|
+
if (!existsSync8(path)) return null;
|
|
5953
6368
|
try {
|
|
5954
6369
|
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
5955
6370
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
@@ -5964,7 +6379,7 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
5964
6379
|
}
|
|
5965
6380
|
}
|
|
5966
6381
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
5967
|
-
|
|
6382
|
+
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
5968
6383
|
writeFileSync6(
|
|
5969
6384
|
pathFor3(workspaceId, conversationId, agentId),
|
|
5970
6385
|
JSON.stringify(result) + "\n",
|
|
@@ -5972,21 +6387,21 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
|
5972
6387
|
);
|
|
5973
6388
|
}
|
|
5974
6389
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
5975
|
-
|
|
6390
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
5976
6391
|
}
|
|
5977
6392
|
|
|
5978
6393
|
// src/secrets.ts
|
|
5979
|
-
import { existsSync as
|
|
5980
|
-
import { join as
|
|
6394
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
6395
|
+
import { join as join12 } from "path";
|
|
5981
6396
|
import { z as z13 } from "zod";
|
|
5982
6397
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5983
6398
|
function secretsPath() {
|
|
5984
|
-
return
|
|
6399
|
+
return join12(cabaneDir(), "secrets.json");
|
|
5985
6400
|
}
|
|
5986
6401
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
5987
6402
|
function loadSecretStore() {
|
|
5988
6403
|
const path = secretsPath();
|
|
5989
|
-
if (!
|
|
6404
|
+
if (!existsSync9(path)) return makeStore({});
|
|
5990
6405
|
let raw;
|
|
5991
6406
|
try {
|
|
5992
6407
|
raw = readFileSync7(path, "utf8");
|
|
@@ -6067,10 +6482,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6067
6482
|
}
|
|
6068
6483
|
|
|
6069
6484
|
// src/transcript-writer.ts
|
|
6070
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
6071
|
-
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";
|
|
6072
6487
|
function transcriptsDir() {
|
|
6073
|
-
return
|
|
6488
|
+
return join13(cabaneDir(), "transcripts");
|
|
6074
6489
|
}
|
|
6075
6490
|
var RETAIN = 200;
|
|
6076
6491
|
var TranscriptWriter = class {
|
|
@@ -6079,9 +6494,9 @@ var TranscriptWriter = class {
|
|
|
6079
6494
|
onWarn;
|
|
6080
6495
|
constructor(dir2, meta, onWarn) {
|
|
6081
6496
|
this.onWarn = onWarn;
|
|
6082
|
-
this.path =
|
|
6497
|
+
this.path = join13(dir2, fileName(meta));
|
|
6083
6498
|
try {
|
|
6084
|
-
|
|
6499
|
+
mkdirSync9(dir2, { recursive: true });
|
|
6085
6500
|
try {
|
|
6086
6501
|
chmodSync3(dir2, 448);
|
|
6087
6502
|
} catch {
|
|
@@ -6138,7 +6553,7 @@ function pruneOld(dir2, retain) {
|
|
|
6138
6553
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6139
6554
|
for (const f of drop) {
|
|
6140
6555
|
try {
|
|
6141
|
-
|
|
6556
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
6142
6557
|
} catch {
|
|
6143
6558
|
}
|
|
6144
6559
|
}
|
|
@@ -6484,7 +6899,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
6484
6899
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6485
6900
|
function checkoutState(cwd) {
|
|
6486
6901
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
6487
|
-
if (!
|
|
6902
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
6488
6903
|
let entries;
|
|
6489
6904
|
try {
|
|
6490
6905
|
entries = readdirSync2(cwd);
|
|
@@ -6494,15 +6909,15 @@ function checkoutState(cwd) {
|
|
|
6494
6909
|
if (entries.length === 0) {
|
|
6495
6910
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
6496
6911
|
}
|
|
6497
|
-
const gitPath =
|
|
6498
|
-
if (!
|
|
6912
|
+
const gitPath = join14(cwd, ".git");
|
|
6913
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
6499
6914
|
let stat;
|
|
6500
6915
|
try {
|
|
6501
6916
|
stat = statSync(gitPath);
|
|
6502
6917
|
} catch (error) {
|
|
6503
6918
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6504
6919
|
}
|
|
6505
|
-
if (stat.isDirectory() && !
|
|
6920
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
6506
6921
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
6507
6922
|
return { ok: true, reason: "usable" };
|
|
6508
6923
|
}
|
|
@@ -6693,7 +7108,7 @@ var Dispatcher = class {
|
|
|
6693
7108
|
let seqCounter = 0;
|
|
6694
7109
|
const nextSeq = () => ++seqCounter;
|
|
6695
7110
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
6696
|
-
if (effectiveCwd && !
|
|
7111
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
6697
7112
|
turnLog.warn(
|
|
6698
7113
|
{ cwd: effectiveCwd },
|
|
6699
7114
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -6829,22 +7244,7 @@ ${reason}`,
|
|
|
6829
7244
|
}
|
|
6830
7245
|
}
|
|
6831
7246
|
}
|
|
6832
|
-
|
|
6833
|
-
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
6834
|
-
const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
6835
|
-
try {
|
|
6836
|
-
mkdirSync9(tmpDir, { recursive: true });
|
|
6837
|
-
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
6838
|
-
} catch (err) {
|
|
6839
|
-
turnLog.warn(
|
|
6840
|
-
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
6841
|
-
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
6842
|
-
);
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
if (turnContext.runtime === "codex" && process.env.CABANE_GATE_LOCK_DIR) {
|
|
6846
|
-
turnEnv = { ...turnEnv, CABANE_GATE_LOCK_DIR: process.env.CABANE_GATE_LOCK_DIR };
|
|
6847
|
-
}
|
|
7247
|
+
const turnEnv = hookEnv;
|
|
6848
7248
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
6849
7249
|
const abortController = new AbortController();
|
|
6850
7250
|
this.aborts.set(key, abortController);
|
|
@@ -7016,7 +7416,7 @@ ${reason}`,
|
|
|
7016
7416
|
}
|
|
7017
7417
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
7018
7418
|
}
|
|
7019
|
-
const receiptPath =
|
|
7419
|
+
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
7020
7420
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
7021
7421
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7022
7422
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -7040,7 +7440,7 @@ ${reason}`,
|
|
|
7040
7440
|
})}
|
|
7041
7441
|
`;
|
|
7042
7442
|
try {
|
|
7043
|
-
|
|
7443
|
+
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
7044
7444
|
appendFileSync2(
|
|
7045
7445
|
receiptPath,
|
|
7046
7446
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -7468,202 +7868,6 @@ ${reason}`,
|
|
|
7468
7868
|
}
|
|
7469
7869
|
};
|
|
7470
7870
|
|
|
7471
|
-
// src/manifest.ts
|
|
7472
|
-
var DEVICE_MANIFEST = {
|
|
7473
|
-
runtimes: [{ name: "claude-code", version: null }],
|
|
7474
|
-
capabilities: { hostFs: true, browser: true, userMcp: true }
|
|
7475
|
-
};
|
|
7476
|
-
function buildCompanionManifest(opts) {
|
|
7477
|
-
const v = opts.versions ?? {};
|
|
7478
|
-
const runtimes = [];
|
|
7479
|
-
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
7480
|
-
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
7481
|
-
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
7482
|
-
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
7483
|
-
}
|
|
7484
|
-
|
|
7485
|
-
// src/harness-status.ts
|
|
7486
|
-
var LABELS = {
|
|
7487
|
-
"claude-code": "Claude Code",
|
|
7488
|
-
codex: "Codex",
|
|
7489
|
-
opencode: "opencode"
|
|
7490
|
-
};
|
|
7491
|
-
function deriveHarnessSnapshot(signals) {
|
|
7492
|
-
const advertised = new Set(
|
|
7493
|
-
buildCompanionManifest({
|
|
7494
|
-
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7495
|
-
// through the same function rather than re-decided.
|
|
7496
|
-
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
7497
|
-
opencode: signals.opencodeConfigured,
|
|
7498
|
-
codex: signals.codexEnabled
|
|
7499
|
-
}).runtimes.map((r) => r.name)
|
|
7500
|
-
);
|
|
7501
|
-
const harnesses = [
|
|
7502
|
-
deriveClaudeCode(signals, advertised.has("claude-code")),
|
|
7503
|
-
deriveCodex(signals, advertised.has("codex")),
|
|
7504
|
-
deriveOpencode(signals, advertised.has("opencode"))
|
|
7505
|
-
];
|
|
7506
|
-
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
7507
|
-
}
|
|
7508
|
-
function deriveClaudeCode(signals, manifestHas) {
|
|
7509
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
7510
|
-
if (manifestHas) {
|
|
7511
|
-
return {
|
|
7512
|
-
...base,
|
|
7513
|
-
state: "exposed",
|
|
7514
|
-
version: signals.claudeVersion,
|
|
7515
|
-
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7516
|
-
enable: null
|
|
7517
|
-
};
|
|
7518
|
-
}
|
|
7519
|
-
if (signals.claudeCodeConnected) {
|
|
7520
|
-
return {
|
|
7521
|
-
...base,
|
|
7522
|
-
state: "needs_attention",
|
|
7523
|
-
version: null,
|
|
7524
|
-
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.",
|
|
7525
|
-
enable: null
|
|
7526
|
-
};
|
|
7527
|
-
}
|
|
7528
|
-
if (signals.claudeOnPath) {
|
|
7529
|
-
return {
|
|
7530
|
-
...base,
|
|
7531
|
-
state: "detected_not_exposed",
|
|
7532
|
-
version: signals.claudeVersion,
|
|
7533
|
-
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7534
|
-
enable: "claude-code"
|
|
7535
|
-
};
|
|
7536
|
-
}
|
|
7537
|
-
return {
|
|
7538
|
-
...base,
|
|
7539
|
-
state: "not_detected",
|
|
7540
|
-
version: null,
|
|
7541
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7542
|
-
enable: null
|
|
7543
|
-
};
|
|
7544
|
-
}
|
|
7545
|
-
function deriveCodex(signals, manifestHas) {
|
|
7546
|
-
const base = { runtime: "codex", label: LABELS.codex };
|
|
7547
|
-
if (manifestHas) {
|
|
7548
|
-
if (signals.codexOnPath) {
|
|
7549
|
-
return {
|
|
7550
|
-
...base,
|
|
7551
|
-
state: "exposed",
|
|
7552
|
-
version: signals.codexVersion,
|
|
7553
|
-
detail: "Codex is enabled and exposed to Cabane.",
|
|
7554
|
-
enable: null
|
|
7555
|
-
};
|
|
7556
|
-
}
|
|
7557
|
-
return {
|
|
7558
|
-
...base,
|
|
7559
|
-
state: "needs_attention",
|
|
7560
|
-
version: null,
|
|
7561
|
-
detail: "Enabled, but the `codex` CLI isn\u2019t on your PATH. Install it and sign in (`codex login`), or turn Codex off.",
|
|
7562
|
-
enable: null
|
|
7563
|
-
};
|
|
7564
|
-
}
|
|
7565
|
-
if (signals.codexOnPath) {
|
|
7566
|
-
return {
|
|
7567
|
-
...base,
|
|
7568
|
-
state: "detected_not_exposed",
|
|
7569
|
-
version: signals.codexVersion,
|
|
7570
|
-
detail: "Codex is installed but not exposed yet. Turn it on to let Cabane run Codex here.",
|
|
7571
|
-
enable: "codex"
|
|
7572
|
-
};
|
|
7573
|
-
}
|
|
7574
|
-
return {
|
|
7575
|
-
...base,
|
|
7576
|
-
state: "not_detected",
|
|
7577
|
-
version: null,
|
|
7578
|
-
detail: "Not detected. Install the Codex CLI and sign in (`codex login`), then enable it here.",
|
|
7579
|
-
enable: null
|
|
7580
|
-
};
|
|
7581
|
-
}
|
|
7582
|
-
function deriveOpencode(signals, manifestHas) {
|
|
7583
|
-
const base = { runtime: "opencode", label: LABELS.opencode };
|
|
7584
|
-
if (manifestHas) {
|
|
7585
|
-
if (signals.opencodeReachable) {
|
|
7586
|
-
return {
|
|
7587
|
-
...base,
|
|
7588
|
-
state: "exposed",
|
|
7589
|
-
version: signals.opencodeVersion,
|
|
7590
|
-
detail: "An opencode server is reachable and exposed to Cabane.",
|
|
7591
|
-
enable: null
|
|
7592
|
-
};
|
|
7593
|
-
}
|
|
7594
|
-
return {
|
|
7595
|
-
...base,
|
|
7596
|
-
state: "needs_attention",
|
|
7597
|
-
version: null,
|
|
7598
|
-
detail: "Configured, but the opencode server isn\u2019t answering. Start `opencode serve` and check the URL.",
|
|
7599
|
-
enable: null
|
|
7600
|
-
};
|
|
7601
|
-
}
|
|
7602
|
-
return {
|
|
7603
|
-
...base,
|
|
7604
|
-
state: "not_detected",
|
|
7605
|
-
version: null,
|
|
7606
|
-
detail: "Not detected. Run `opencode serve` and add its URL here to expose opencode.",
|
|
7607
|
-
enable: "opencode"
|
|
7608
|
-
};
|
|
7609
|
-
}
|
|
7610
|
-
function detectedRuntimesFor(snapshot) {
|
|
7611
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7612
|
-
}
|
|
7613
|
-
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
7614
|
-
async function probeHarnessSignals(cfg, deps = {}) {
|
|
7615
|
-
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
7616
|
-
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
7617
|
-
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
7618
|
-
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
7619
|
-
const serverUrl = cfg.opencode?.serverUrl;
|
|
7620
|
-
const [claudeOnPathResult, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
7621
|
-
withTimeout(probeClaudePresence(), false),
|
|
7622
|
-
withTimeout(probeClaudeVersion(), null),
|
|
7623
|
-
withTimeout(probeCodexVersion(), null),
|
|
7624
|
-
serverUrl ? withTimeout(probeOpencode(serverUrl), null) : Promise.resolve(null)
|
|
7625
|
-
]);
|
|
7626
|
-
return {
|
|
7627
|
-
claudeOnPath: claudeOnPathResult,
|
|
7628
|
-
claudeVersion,
|
|
7629
|
-
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7630
|
-
// the manifest gate and the probe above is only a suggestion.
|
|
7631
|
-
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
7632
|
-
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7633
|
-
// exposes codex; its config flag is the manifest gate either way).
|
|
7634
|
-
codexOnPath: codexVersion !== null,
|
|
7635
|
-
codexVersion,
|
|
7636
|
-
codexEnabled: isCodexEnabled(cfg),
|
|
7637
|
-
opencodeConfigured: !!serverUrl,
|
|
7638
|
-
// A version came back ⟺ the serve answered its health endpoint (CT584).
|
|
7639
|
-
opencodeReachable: opencodeVersion !== null,
|
|
7640
|
-
opencodeVersion
|
|
7641
|
-
};
|
|
7642
|
-
}
|
|
7643
|
-
function withTimeout(promise, fallback) {
|
|
7644
|
-
return new Promise((resolve) => {
|
|
7645
|
-
let settled = false;
|
|
7646
|
-
const done = (v) => {
|
|
7647
|
-
if (!settled) {
|
|
7648
|
-
settled = true;
|
|
7649
|
-
resolve(v);
|
|
7650
|
-
}
|
|
7651
|
-
};
|
|
7652
|
-
const timer = setTimeout(() => done(fallback), PROBE_TIMEOUT_MS2);
|
|
7653
|
-
timer.unref?.();
|
|
7654
|
-
promise.then(
|
|
7655
|
-
(v) => {
|
|
7656
|
-
clearTimeout(timer);
|
|
7657
|
-
done(v);
|
|
7658
|
-
},
|
|
7659
|
-
() => {
|
|
7660
|
-
clearTimeout(timer);
|
|
7661
|
-
done(fallback);
|
|
7662
|
-
}
|
|
7663
|
-
);
|
|
7664
|
-
});
|
|
7665
|
-
}
|
|
7666
|
-
|
|
7667
7871
|
// src/opencode-models.ts
|
|
7668
7872
|
var OPENCODE_RUNTIME = "opencode";
|
|
7669
7873
|
function mapOpencodeProviders(json) {
|
|
@@ -7707,15 +7911,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
7707
7911
|
|
|
7708
7912
|
// src/outbox.ts
|
|
7709
7913
|
import {
|
|
7710
|
-
existsSync as
|
|
7711
|
-
mkdirSync as
|
|
7914
|
+
existsSync as existsSync11,
|
|
7915
|
+
mkdirSync as mkdirSync11,
|
|
7712
7916
|
readdirSync as readdirSync3,
|
|
7713
7917
|
readFileSync as readFileSync8,
|
|
7714
7918
|
renameSync as renameSync3,
|
|
7715
|
-
rmSync as
|
|
7919
|
+
rmSync as rmSync7,
|
|
7716
7920
|
writeFileSync as writeFileSync7
|
|
7717
7921
|
} from "fs";
|
|
7718
|
-
import { join as
|
|
7922
|
+
import { join as join15 } from "path";
|
|
7719
7923
|
var MAX_ENTRIES = 2e3;
|
|
7720
7924
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7721
7925
|
var Outbox = class {
|
|
@@ -7728,17 +7932,17 @@ var Outbox = class {
|
|
|
7728
7932
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
7729
7933
|
// cases route writes at the right tmpdir.
|
|
7730
7934
|
dir() {
|
|
7731
|
-
return
|
|
7935
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7732
7936
|
}
|
|
7733
7937
|
fileFor(turnId, seq) {
|
|
7734
|
-
return
|
|
7938
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
7735
7939
|
}
|
|
7736
7940
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
7737
7941
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
7738
7942
|
// per-workspace bounds.
|
|
7739
7943
|
persist(entry) {
|
|
7740
7944
|
const dir2 = this.dir();
|
|
7741
|
-
|
|
7945
|
+
mkdirSync11(dir2, { recursive: true });
|
|
7742
7946
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
7743
7947
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
7744
7948
|
try {
|
|
@@ -7746,7 +7950,7 @@ var Outbox = class {
|
|
|
7746
7950
|
renameSync3(tmp, target);
|
|
7747
7951
|
} catch (err) {
|
|
7748
7952
|
try {
|
|
7749
|
-
|
|
7953
|
+
rmSync7(tmp, { force: true });
|
|
7750
7954
|
} catch {
|
|
7751
7955
|
}
|
|
7752
7956
|
this.log?.warn(
|
|
@@ -7763,7 +7967,7 @@ var Outbox = class {
|
|
|
7763
7967
|
// wedging the drain.
|
|
7764
7968
|
list() {
|
|
7765
7969
|
const dir2 = this.dir();
|
|
7766
|
-
if (!
|
|
7970
|
+
if (!existsSync11(dir2)) return [];
|
|
7767
7971
|
let names;
|
|
7768
7972
|
try {
|
|
7769
7973
|
names = readdirSync3(dir2);
|
|
@@ -7773,7 +7977,7 @@ var Outbox = class {
|
|
|
7773
7977
|
const entries = [];
|
|
7774
7978
|
for (const name of names) {
|
|
7775
7979
|
if (!name.endsWith(".json")) continue;
|
|
7776
|
-
const full =
|
|
7980
|
+
const full = join15(dir2, name);
|
|
7777
7981
|
try {
|
|
7778
7982
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
7779
7983
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -7793,13 +7997,13 @@ var Outbox = class {
|
|
|
7793
7997
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
7794
7998
|
remove(turnId, seq) {
|
|
7795
7999
|
try {
|
|
7796
|
-
|
|
8000
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
7797
8001
|
} catch {
|
|
7798
8002
|
}
|
|
7799
8003
|
}
|
|
7800
8004
|
size() {
|
|
7801
8005
|
const dir2 = this.dir();
|
|
7802
|
-
if (!
|
|
8006
|
+
if (!existsSync11(dir2)) return 0;
|
|
7803
8007
|
try {
|
|
7804
8008
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
7805
8009
|
} catch {
|
|
@@ -7812,7 +8016,7 @@ var Outbox = class {
|
|
|
7812
8016
|
"companion outbox: dropping unreadable entry"
|
|
7813
8017
|
);
|
|
7814
8018
|
try {
|
|
7815
|
-
|
|
8019
|
+
rmSync7(full, { force: true });
|
|
7816
8020
|
} catch {
|
|
7817
8021
|
}
|
|
7818
8022
|
}
|
|
@@ -8741,6 +8945,26 @@ var CompanionSupervisor = class {
|
|
|
8741
8945
|
async recheckHarnesses() {
|
|
8742
8946
|
await this.refreshHarnessStatuses();
|
|
8743
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
|
+
}
|
|
8744
8968
|
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8745
8969
|
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8746
8970
|
// never installs a binary and never drives a login (BYO — Decided).
|
|
@@ -8882,9 +9106,9 @@ var CompanionSupervisor = class {
|
|
|
8882
9106
|
};
|
|
8883
9107
|
function defaultReexec() {
|
|
8884
9108
|
clearRuntimeState();
|
|
8885
|
-
void import("child_process").then(({ spawn:
|
|
9109
|
+
void import("child_process").then(({ spawn: spawn5 }) => {
|
|
8886
9110
|
try {
|
|
8887
|
-
const child =
|
|
9111
|
+
const child = spawn5(process.execPath, process.argv.slice(1), {
|
|
8888
9112
|
stdio: "inherit",
|
|
8889
9113
|
detached: false
|
|
8890
9114
|
});
|
|
@@ -8954,14 +9178,14 @@ function handleUncaught(log, err, origin) {
|
|
|
8954
9178
|
}
|
|
8955
9179
|
|
|
8956
9180
|
// src/crash-marker.ts
|
|
8957
|
-
import { existsSync as
|
|
8958
|
-
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";
|
|
8959
9183
|
function crashMarkerPath() {
|
|
8960
|
-
return
|
|
9184
|
+
return join16(cabaneDir(), "last-error.json");
|
|
8961
9185
|
}
|
|
8962
9186
|
function recordCrash(rec) {
|
|
8963
9187
|
try {
|
|
8964
|
-
|
|
9188
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
8965
9189
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
8966
9190
|
} catch {
|
|
8967
9191
|
}
|
|
@@ -8969,7 +9193,7 @@ function recordCrash(rec) {
|
|
|
8969
9193
|
function clearCrash() {
|
|
8970
9194
|
try {
|
|
8971
9195
|
const path = crashMarkerPath();
|
|
8972
|
-
if (
|
|
9196
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
8973
9197
|
} catch {
|
|
8974
9198
|
}
|
|
8975
9199
|
}
|
|
@@ -8988,7 +9212,13 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
8988
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)"
|
|
8989
9213
|
)
|
|
8990
9214
|
}));
|
|
8991
|
-
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
|
+
});
|
|
8992
9222
|
} catch (err) {
|
|
8993
9223
|
recordCrash({
|
|
8994
9224
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -9012,8 +9242,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9012
9242
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|
|
9013
9243
|
const claim = acquireRuntimeState({
|
|
9014
9244
|
pid: process.pid,
|
|
9015
|
-
url: "",
|
|
9016
|
-
port: 0,
|
|
9017
9245
|
startedAt,
|
|
9018
9246
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
9019
9247
|
instanceId
|
|
@@ -9021,7 +9249,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9021
9249
|
if (!claim.acquired) {
|
|
9022
9250
|
return { ok: false, reason: "already-running", existing: claim.existing ?? null };
|
|
9023
9251
|
}
|
|
9024
|
-
process.on("exit", () =>
|
|
9252
|
+
process.on("exit", () => clearRuntimeStateIfOurs(instanceId));
|
|
9025
9253
|
const hub = new CompanionStateHub({
|
|
9026
9254
|
// CT29: one device, one base URL — the cabane instance this device is paired
|
|
9027
9255
|
// with. The dashboard's connection line shows it.
|
|
@@ -9039,17 +9267,36 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9039
9267
|
harnessVersions
|
|
9040
9268
|
});
|
|
9041
9269
|
await supervisor.start();
|
|
9042
|
-
const
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
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()
|
|
9047
9285
|
});
|
|
9048
|
-
|
|
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
|
+
}
|
|
9049
9296
|
writeRuntimeState({
|
|
9050
9297
|
pid: process.pid,
|
|
9051
|
-
|
|
9052
|
-
port: dashboard.port,
|
|
9298
|
+
socket: control.path,
|
|
9299
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9053
9300
|
startedAt,
|
|
9054
9301
|
// SJ495: the daemon launcher sets this env on the detached child, so the
|
|
9055
9302
|
// marker records whether this companion is backgrounded (foreground start
|
|
@@ -9062,30 +9309,37 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9062
9309
|
const stop = async () => {
|
|
9063
9310
|
if (stopped) return;
|
|
9064
9311
|
stopped = true;
|
|
9065
|
-
|
|
9312
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9066
9313
|
try {
|
|
9067
9314
|
await supervisor.shutdown();
|
|
9068
|
-
await dashboard
|
|
9315
|
+
await closeSurfaces(control, dashboard);
|
|
9069
9316
|
} catch {
|
|
9070
9317
|
}
|
|
9071
9318
|
};
|
|
9072
9319
|
return {
|
|
9073
9320
|
ok: true,
|
|
9074
9321
|
runtime: {
|
|
9075
|
-
url: dashboard.url,
|
|
9076
|
-
|
|
9322
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
9323
|
+
socketPath: control.path,
|
|
9077
9324
|
config: cfg,
|
|
9078
9325
|
stop,
|
|
9326
|
+
heartbeatNow: () => supervisor.heartbeatNow(),
|
|
9327
|
+
harnesses: () => hub.statusJson().harnesses ?? [],
|
|
9328
|
+
connectHarness,
|
|
9079
9329
|
drainForRestart: async (graceMs) => {
|
|
9080
|
-
|
|
9330
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
9081
9331
|
const result = await supervisor.drainForRestart(graceMs);
|
|
9082
|
-
await dashboard
|
|
9332
|
+
await closeSurfaces(control, dashboard);
|
|
9083
9333
|
stopped = true;
|
|
9084
9334
|
return result;
|
|
9085
9335
|
}
|
|
9086
9336
|
}
|
|
9087
9337
|
};
|
|
9088
9338
|
}
|
|
9339
|
+
async function closeSurfaces(control, dashboard) {
|
|
9340
|
+
await control.close();
|
|
9341
|
+
if (dashboard) await dashboard.close();
|
|
9342
|
+
}
|
|
9089
9343
|
export {
|
|
9090
9344
|
createCompanionRuntime
|
|
9091
9345
|
};
|