@cabane/companion 0.6.19 → 0.6.21
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 +293 -676
- package/dist/pairing-config.js +47 -9
- package/dist/runtime.js +234 -648
- package/dist/static/app.js +11 -3
- package/package.json +2 -1
package/dist/runtime.js
CHANGED
|
@@ -288,6 +288,26 @@ var companionConfigSchema = z2.object({
|
|
|
288
288
|
opencode: z2.object({
|
|
289
289
|
serverUrl: z2.string().url()
|
|
290
290
|
}).strict().optional(),
|
|
291
|
+
// CT1082: the Claude Code runtime, when the user connected it on this machine.
|
|
292
|
+
// The change this task exists for: Claude Code used to be the one harness a
|
|
293
|
+
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
294
|
+
// consent — and it is now the third config-driven harness, opted into exactly
|
|
295
|
+
// like codex. `claude` on PATH is still required (you can't run what isn't
|
|
296
|
+
// installed), but presence alone no longer exposes anything: the manifest
|
|
297
|
+
// advertises claude-code only when this block says so AND the binary is there.
|
|
298
|
+
//
|
|
299
|
+
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
300
|
+
// Absent means the config predates this task — a device whose user was never
|
|
301
|
+
// asked — and the migration grandfathers it on first start. So every config
|
|
302
|
+
// written from here on carries the block explicitly, including a freshly-paired
|
|
303
|
+
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
304
|
+
// until its user says otherwise.
|
|
305
|
+
//
|
|
306
|
+
// Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
|
|
307
|
+
// above — that's one agent's memory switch, this is the device's connected set.
|
|
308
|
+
claudeCode: z2.object({
|
|
309
|
+
enabled: z2.boolean().optional()
|
|
310
|
+
}).strict().optional(),
|
|
291
311
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
292
312
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
293
313
|
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
@@ -304,19 +324,26 @@ var companionConfigSchema = z2.object({
|
|
|
304
324
|
function isCodexEnabled(cfg) {
|
|
305
325
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
306
326
|
}
|
|
327
|
+
function isClaudeCodeConnected(cfg) {
|
|
328
|
+
return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
329
|
+
}
|
|
330
|
+
function migrateConnectedHarnesses(cfg, claudeOnPath2) {
|
|
331
|
+
if (cfg.claudeCode !== void 0) return null;
|
|
332
|
+
return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
|
|
333
|
+
}
|
|
307
334
|
function localAgentConfig(cfg, agent) {
|
|
308
335
|
const map = cfg.agents ?? {};
|
|
309
336
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
310
337
|
}
|
|
311
338
|
function loadConfig() {
|
|
312
|
-
const
|
|
313
|
-
if (!existsSync(
|
|
339
|
+
const path = configPath();
|
|
340
|
+
if (!existsSync(path)) return null;
|
|
314
341
|
let raw;
|
|
315
342
|
try {
|
|
316
|
-
raw = readFileSync(
|
|
343
|
+
raw = readFileSync(path, "utf8");
|
|
317
344
|
} catch (err) {
|
|
318
345
|
throw new ConfigError(
|
|
319
|
-
`couldn't read ${
|
|
346
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
320
347
|
);
|
|
321
348
|
}
|
|
322
349
|
if (raw.trim().length === 0) return null;
|
|
@@ -325,7 +352,7 @@ function loadConfig() {
|
|
|
325
352
|
parsed = JSON.parse(raw);
|
|
326
353
|
} catch (err) {
|
|
327
354
|
throw new ConfigError(
|
|
328
|
-
`${
|
|
355
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
329
356
|
);
|
|
330
357
|
}
|
|
331
358
|
const result = companionConfigSchema.safeParse(parsed);
|
|
@@ -333,30 +360,30 @@ function loadConfig() {
|
|
|
333
360
|
const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
|
|
334
361
|
if (agentIssue) {
|
|
335
362
|
throw new ConfigError(
|
|
336
|
-
`${
|
|
363
|
+
`${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
|
|
337
364
|
);
|
|
338
365
|
}
|
|
339
366
|
throw new ConfigError(
|
|
340
|
-
`${
|
|
367
|
+
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
|
|
341
368
|
);
|
|
342
369
|
}
|
|
343
370
|
return result.data;
|
|
344
371
|
}
|
|
345
372
|
function saveConfig(cfg) {
|
|
346
|
-
const
|
|
347
|
-
mkdirSync(dirname(
|
|
373
|
+
const path = configPath();
|
|
374
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
348
375
|
try {
|
|
349
376
|
chmodSync(cabaneDir(), 448);
|
|
350
377
|
} catch {
|
|
351
378
|
}
|
|
352
|
-
const tmp = `${
|
|
379
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
353
380
|
try {
|
|
354
381
|
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
355
382
|
try {
|
|
356
383
|
chmodSync(tmp, 384);
|
|
357
384
|
} catch {
|
|
358
385
|
}
|
|
359
|
-
renameSync(tmp,
|
|
386
|
+
renameSync(tmp, path);
|
|
360
387
|
} catch (err) {
|
|
361
388
|
try {
|
|
362
389
|
rmSync(tmp, { force: true });
|
|
@@ -417,8 +444,8 @@ function consoleMessageFormat(log, messageKey) {
|
|
|
417
444
|
var cached = null;
|
|
418
445
|
function getLogger() {
|
|
419
446
|
if (cached) return cached;
|
|
420
|
-
const
|
|
421
|
-
mkdirSync2(dirname2(
|
|
447
|
+
const path = companionLogPath();
|
|
448
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
422
449
|
const streams = [];
|
|
423
450
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
424
451
|
const consoleStream = pretty({
|
|
@@ -428,7 +455,7 @@ function getLogger() {
|
|
|
428
455
|
});
|
|
429
456
|
streams.push({ level: "info", stream: consoleStream });
|
|
430
457
|
}
|
|
431
|
-
streams.push({ level: "debug", stream: createWriteStream(
|
|
458
|
+
streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
|
|
432
459
|
cached = pino({ level: "debug" }, pino.multistream(streams));
|
|
433
460
|
return cached;
|
|
434
461
|
}
|
|
@@ -739,6 +766,11 @@ function registerRoutes(app, deps) {
|
|
|
739
766
|
app.post("/api/harnesses/enable", async (c) => {
|
|
740
767
|
const body = await readJson(c);
|
|
741
768
|
const runtime = body.runtime;
|
|
769
|
+
if (runtime === "claude-code") {
|
|
770
|
+
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
771
|
+
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
772
|
+
return c.json({ ok: true, status: hub.statusJson() });
|
|
773
|
+
}
|
|
742
774
|
if (runtime === "codex") {
|
|
743
775
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
744
776
|
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
@@ -807,12 +839,12 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
807
839
|
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
808
840
|
return Math.min(Math.floor(n), max);
|
|
809
841
|
}
|
|
810
|
-
function tailFile(
|
|
811
|
-
if (!existsSync2(
|
|
842
|
+
function tailFile(path, lines) {
|
|
843
|
+
if (!existsSync2(path)) return [];
|
|
812
844
|
const MAX_BYTES = 256 * 1024;
|
|
813
845
|
let fd;
|
|
814
846
|
try {
|
|
815
|
-
fd = openSync(
|
|
847
|
+
fd = openSync(path, "r");
|
|
816
848
|
const size = fstatSync(fd).size;
|
|
817
849
|
const start = Math.max(0, size - MAX_BYTES);
|
|
818
850
|
const len = size - start;
|
|
@@ -1012,26 +1044,43 @@ async function codexOnPath() {
|
|
|
1012
1044
|
]);
|
|
1013
1045
|
return version !== null;
|
|
1014
1046
|
}
|
|
1015
|
-
async function
|
|
1047
|
+
async function requireStartConfig(deps = {}) {
|
|
1048
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
1049
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1050
|
+
const save2 = deps.save ?? saveConfig;
|
|
1051
|
+
const cfg = requireCfg();
|
|
1052
|
+
const claudeOnPathResult = await probeClaude();
|
|
1053
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
1054
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
1055
|
+
save2(migrated);
|
|
1056
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
1057
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
1058
|
+
}
|
|
1059
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
1016
1060
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1017
1061
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
1018
1062
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
1019
1063
|
`));
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
...cfg
|
|
1023
|
-
...
|
|
1064
|
+
const connected = [
|
|
1065
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
1066
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
1067
|
+
...cfg.opencode ? ["opencode"] : []
|
|
1024
1068
|
];
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1069
|
+
if (connected.length > 0) {
|
|
1070
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
1071
|
+
warn(
|
|
1072
|
+
"warning: Claude Code is connected on this device but `claude` isn\u2019t on your PATH, so it advertises nothing and a Claude-model agent won\u2019t be routed here. Install it (`npm i -g @anthropic-ai/claude-code`) and log in, or disconnect it."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1030
1075
|
return;
|
|
1031
1076
|
}
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1077
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
1078
|
+
const installed = [
|
|
1079
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
1080
|
+
...codexInstalled ? ["Codex"] : []
|
|
1081
|
+
];
|
|
1082
|
+
warn(
|
|
1083
|
+
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"} in the Companion dashboard (or in cabane, Settings \u2192 Devices) and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it in the Companion dashboard.")
|
|
1035
1084
|
);
|
|
1036
1085
|
}
|
|
1037
1086
|
|
|
@@ -1054,9 +1103,9 @@ function serialize(state) {
|
|
|
1054
1103
|
return JSON.stringify(state, null, 2) + "\n";
|
|
1055
1104
|
}
|
|
1056
1105
|
function writeRuntimeState(state) {
|
|
1057
|
-
const
|
|
1106
|
+
const path = runtimePath();
|
|
1058
1107
|
mkdirSync3(cabaneDir(), { recursive: true });
|
|
1059
|
-
writeFileSync2(
|
|
1108
|
+
writeFileSync2(path, serialize(state), "utf8");
|
|
1060
1109
|
}
|
|
1061
1110
|
function acquireRuntimeState(state) {
|
|
1062
1111
|
const live = readLiveRuntimeState();
|
|
@@ -1076,15 +1125,15 @@ function acquireRuntimeState(state) {
|
|
|
1076
1125
|
return { acquired: true };
|
|
1077
1126
|
}
|
|
1078
1127
|
function clearRuntimeState() {
|
|
1079
|
-
const
|
|
1080
|
-
if (existsSync3(
|
|
1128
|
+
const path = runtimePath();
|
|
1129
|
+
if (existsSync3(path)) rmSync2(path, { force: true });
|
|
1081
1130
|
}
|
|
1082
1131
|
function readLiveRuntimeState() {
|
|
1083
|
-
const
|
|
1084
|
-
if (!existsSync3(
|
|
1132
|
+
const path = runtimePath();
|
|
1133
|
+
if (!existsSync3(path)) return null;
|
|
1085
1134
|
let parsed;
|
|
1086
1135
|
try {
|
|
1087
|
-
parsed = JSON.parse(readFileSync2(
|
|
1136
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1088
1137
|
} catch {
|
|
1089
1138
|
return null;
|
|
1090
1139
|
}
|
|
@@ -1148,8 +1197,8 @@ var CabaneApi = class {
|
|
|
1148
1197
|
// One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
|
|
1149
1198
|
// rethrows transport errors (fetch reject) unchanged so the caller's retry
|
|
1150
1199
|
// logic can classify them.
|
|
1151
|
-
async attempt(method,
|
|
1152
|
-
const res = await fetch(`${this.base}${
|
|
1200
|
+
async attempt(method, path, body, signal) {
|
|
1201
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
1153
1202
|
method,
|
|
1154
1203
|
headers: {
|
|
1155
1204
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -1175,12 +1224,12 @@ var CabaneApi = class {
|
|
|
1175
1224
|
}
|
|
1176
1225
|
return parsed;
|
|
1177
1226
|
}
|
|
1178
|
-
async request(method,
|
|
1227
|
+
async request(method, path, body, opts = {}) {
|
|
1179
1228
|
const { signal, retry = false } = opts;
|
|
1180
1229
|
const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
|
|
1181
1230
|
for (let attempt = 1; ; attempt++) {
|
|
1182
1231
|
try {
|
|
1183
|
-
return await this.attempt(method,
|
|
1232
|
+
return await this.attempt(method, path, body, signal);
|
|
1184
1233
|
} catch (err) {
|
|
1185
1234
|
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1186
1235
|
await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
@@ -1206,9 +1255,9 @@ var CabaneApi = class {
|
|
|
1206
1255
|
// delivers it once the API returns. `(turnId, seq)` is the server's
|
|
1207
1256
|
// idempotency key, so a replay whose original POST's fate is unknown
|
|
1208
1257
|
// converges instead of duplicating.
|
|
1209
|
-
async durableCommit(kind,
|
|
1258
|
+
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1210
1259
|
try {
|
|
1211
|
-
await this.request("POST",
|
|
1260
|
+
await this.request("POST", path, body, {
|
|
1212
1261
|
retry: true,
|
|
1213
1262
|
...signal ? { signal } : {}
|
|
1214
1263
|
});
|
|
@@ -1217,7 +1266,7 @@ var CabaneApi = class {
|
|
|
1217
1266
|
if (!outbox) throw err;
|
|
1218
1267
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1219
1268
|
if (!isRetryable(err)) throw err;
|
|
1220
|
-
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path
|
|
1269
|
+
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
|
|
1221
1270
|
this.opts.log?.warn(
|
|
1222
1271
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1223
1272
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1369,12 +1418,12 @@ var CabaneApi = class {
|
|
|
1369
1418
|
// left best-effort: it's lower-stakes and self-heals on the next turn, so it
|
|
1370
1419
|
// stays a single-shot PATCH and is deliberately out of CT93's scope.
|
|
1371
1420
|
setActiveRun(workspaceId, conversationId, agentId, body) {
|
|
1372
|
-
const
|
|
1421
|
+
const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
|
|
1373
1422
|
const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
|
|
1374
1423
|
if (touchesFlag && this.opts.outbox) {
|
|
1375
|
-
return this.durableActiveRunWrite(
|
|
1424
|
+
return this.durableActiveRunWrite(path, conversationId, agentId, body);
|
|
1376
1425
|
}
|
|
1377
|
-
return this.request("PATCH",
|
|
1426
|
+
return this.request("PATCH", path, body);
|
|
1378
1427
|
}
|
|
1379
1428
|
// CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
|
|
1380
1429
|
// across the (conversation, agent) pair. Mirrors `durableCommit`, with two
|
|
@@ -1388,11 +1437,11 @@ var CabaneApi = class {
|
|
|
1388
1437
|
// later and clobber the state we just wrote (the cross-turn race: turn N's
|
|
1389
1438
|
// queued clear vs. turn N+1's live set). Combined with persist-overwrites-
|
|
1390
1439
|
// by-key, this is the full last-writer-wins guarantee.
|
|
1391
|
-
async durableActiveRunWrite(
|
|
1440
|
+
async durableActiveRunWrite(path, conversationId, agentId, body) {
|
|
1392
1441
|
const outbox = this.opts.outbox;
|
|
1393
1442
|
const key = activeRunOutboxKey(conversationId, agentId);
|
|
1394
1443
|
try {
|
|
1395
|
-
await this.request("PATCH",
|
|
1444
|
+
await this.request("PATCH", path, body, { retry: true });
|
|
1396
1445
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1397
1446
|
} catch (err) {
|
|
1398
1447
|
if (!outbox) throw err;
|
|
@@ -1405,7 +1454,7 @@ var CabaneApi = class {
|
|
|
1405
1454
|
turnId: key,
|
|
1406
1455
|
seq: ACTIVE_RUN_OUTBOX_SEQ,
|
|
1407
1456
|
method: "PATCH",
|
|
1408
|
-
path
|
|
1457
|
+
path,
|
|
1409
1458
|
body,
|
|
1410
1459
|
kind: "active-run"
|
|
1411
1460
|
});
|
|
@@ -1493,8 +1542,8 @@ var CabaneApi = class {
|
|
|
1493
1542
|
// shared resolver the in-app path uses. Omitting it (older call sites) returns
|
|
1494
1543
|
// the agent default — graceful degradation, no version coupling.
|
|
1495
1544
|
getAgentSelf(conversationId) {
|
|
1496
|
-
const
|
|
1497
|
-
return this.request("GET",
|
|
1545
|
+
const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
|
|
1546
|
+
return this.request("GET", path);
|
|
1498
1547
|
}
|
|
1499
1548
|
// The companion fetches the triggering message body by listing the
|
|
1500
1549
|
// conversation's messages and finding the one with `id === messageId`.
|
|
@@ -1549,8 +1598,8 @@ var DeviceApi = class {
|
|
|
1549
1598
|
get base() {
|
|
1550
1599
|
return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
1551
1600
|
}
|
|
1552
|
-
async request(method,
|
|
1553
|
-
const res = await fetch(`${this.base}${
|
|
1601
|
+
async request(method, path, body) {
|
|
1602
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
1554
1603
|
method,
|
|
1555
1604
|
headers: {
|
|
1556
1605
|
Authorization: `Bearer ${this.opts.deviceToken}`,
|
|
@@ -1614,11 +1663,11 @@ function credentialsPath() {
|
|
|
1614
1663
|
}
|
|
1615
1664
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1616
1665
|
function load() {
|
|
1617
|
-
const
|
|
1618
|
-
if (!existsSync4(
|
|
1666
|
+
const path = credentialsPath();
|
|
1667
|
+
if (!existsSync4(path)) return {};
|
|
1619
1668
|
let raw;
|
|
1620
1669
|
try {
|
|
1621
|
-
raw = readFileSync3(
|
|
1670
|
+
raw = readFileSync3(path, "utf8");
|
|
1622
1671
|
} catch {
|
|
1623
1672
|
return {};
|
|
1624
1673
|
}
|
|
@@ -1631,20 +1680,20 @@ function load() {
|
|
|
1631
1680
|
}
|
|
1632
1681
|
}
|
|
1633
1682
|
function save(map) {
|
|
1634
|
-
const
|
|
1635
|
-
mkdirSync4(dirname4(
|
|
1683
|
+
const path = credentialsPath();
|
|
1684
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1636
1685
|
try {
|
|
1637
1686
|
chmodSync2(cabaneDir(), 448);
|
|
1638
1687
|
} catch {
|
|
1639
1688
|
}
|
|
1640
|
-
const tmp = `${
|
|
1689
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
1641
1690
|
try {
|
|
1642
1691
|
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
1643
1692
|
try {
|
|
1644
1693
|
chmodSync2(tmp, 384);
|
|
1645
1694
|
} catch {
|
|
1646
1695
|
}
|
|
1647
|
-
renameSync2(tmp,
|
|
1696
|
+
renameSync2(tmp, path);
|
|
1648
1697
|
} catch (err) {
|
|
1649
1698
|
try {
|
|
1650
1699
|
rmSync3(tmp, { force: true });
|
|
@@ -1683,15 +1732,15 @@ function pathFor(workspaceId) {
|
|
|
1683
1732
|
return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
1684
1733
|
}
|
|
1685
1734
|
function readCursor(workspaceId) {
|
|
1686
|
-
const
|
|
1687
|
-
if (!existsSync5(
|
|
1688
|
-
const raw = readFileSync4(
|
|
1735
|
+
const path = pathFor(workspaceId);
|
|
1736
|
+
if (!existsSync5(path)) return null;
|
|
1737
|
+
const raw = readFileSync4(path, "utf8").trim();
|
|
1689
1738
|
return raw.length > 0 ? raw : null;
|
|
1690
1739
|
}
|
|
1691
1740
|
function writeCursor(workspaceId, eventId) {
|
|
1692
|
-
const
|
|
1741
|
+
const path = pathFor(workspaceId);
|
|
1693
1742
|
mkdirSync5(join7(cabaneDir(), "cursors"), { recursive: true });
|
|
1694
|
-
writeFileSync4(
|
|
1743
|
+
writeFileSync4(path, eventId + "\n", "utf8");
|
|
1695
1744
|
}
|
|
1696
1745
|
|
|
1697
1746
|
// src/cursor-tracker.ts
|
|
@@ -1744,10 +1793,10 @@ function pathFor2(log, workspaceId) {
|
|
|
1744
1793
|
return join8(dir(log), encodeURIComponent(workspaceId));
|
|
1745
1794
|
}
|
|
1746
1795
|
function readIds(log, workspaceId) {
|
|
1747
|
-
const
|
|
1748
|
-
if (!existsSync6(
|
|
1796
|
+
const path = pathFor2(log, workspaceId);
|
|
1797
|
+
if (!existsSync6(path)) return [];
|
|
1749
1798
|
try {
|
|
1750
|
-
return readFileSync5(
|
|
1799
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1751
1800
|
} catch {
|
|
1752
1801
|
return [];
|
|
1753
1802
|
}
|
|
@@ -1784,10 +1833,10 @@ function resumePathFor(workspaceId) {
|
|
|
1784
1833
|
}
|
|
1785
1834
|
function readResumeCounts(workspaceId) {
|
|
1786
1835
|
const out = /* @__PURE__ */ new Map();
|
|
1787
|
-
const
|
|
1788
|
-
if (!existsSync6(
|
|
1836
|
+
const path = resumePathFor(workspaceId);
|
|
1837
|
+
if (!existsSync6(path)) return out;
|
|
1789
1838
|
try {
|
|
1790
|
-
for (const line of readFileSync5(
|
|
1839
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
1791
1840
|
const trimmed = line.trim();
|
|
1792
1841
|
if (!trimmed) continue;
|
|
1793
1842
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -4725,536 +4774,10 @@ function isStringRecord2(v) {
|
|
|
4725
4774
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
4726
4775
|
}
|
|
4727
4776
|
|
|
4728
|
-
// node_modules/.pnpm/@openai+codex-sdk@0.147.0/node_modules/@openai/codex-sdk/dist/index.js
|
|
4729
|
-
import { promises as fs } from "fs";
|
|
4730
|
-
import os from "os";
|
|
4731
|
-
import path from "path";
|
|
4732
|
-
import { spawn as spawn4 } from "child_process";
|
|
4733
|
-
import { statSync } from "fs";
|
|
4734
|
-
import path2 from "path";
|
|
4735
|
-
import readline from "readline";
|
|
4736
|
-
import { createRequire } from "module";
|
|
4737
|
-
async function createOutputSchemaFile(schema) {
|
|
4738
|
-
if (schema === void 0) {
|
|
4739
|
-
return { cleanup: async () => {
|
|
4740
|
-
} };
|
|
4741
|
-
}
|
|
4742
|
-
if (!isJsonObject(schema)) {
|
|
4743
|
-
throw new Error("outputSchema must be a plain JSON object");
|
|
4744
|
-
}
|
|
4745
|
-
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
|
4746
|
-
const schemaPath = path.join(schemaDir, "schema.json");
|
|
4747
|
-
const cleanup = async () => {
|
|
4748
|
-
try {
|
|
4749
|
-
await fs.rm(schemaDir, { recursive: true, force: true });
|
|
4750
|
-
} catch {
|
|
4751
|
-
}
|
|
4752
|
-
};
|
|
4753
|
-
try {
|
|
4754
|
-
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
|
4755
|
-
return { schemaPath, cleanup };
|
|
4756
|
-
} catch (error) {
|
|
4757
|
-
await cleanup();
|
|
4758
|
-
throw error;
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
function isJsonObject(value) {
|
|
4762
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4763
|
-
}
|
|
4764
|
-
var Thread = class {
|
|
4765
|
-
_exec;
|
|
4766
|
-
_options;
|
|
4767
|
-
_id;
|
|
4768
|
-
_threadOptions;
|
|
4769
|
-
/** Returns the ID of the thread. Populated after the first turn starts. */
|
|
4770
|
-
get id() {
|
|
4771
|
-
return this._id;
|
|
4772
|
-
}
|
|
4773
|
-
/* @internal */
|
|
4774
|
-
constructor(exec, options, threadOptions, id = null) {
|
|
4775
|
-
this._exec = exec;
|
|
4776
|
-
this._options = options;
|
|
4777
|
-
this._id = id;
|
|
4778
|
-
this._threadOptions = threadOptions;
|
|
4779
|
-
}
|
|
4780
|
-
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
|
4781
|
-
async runStreamed(input, turnOptions = {}) {
|
|
4782
|
-
return { events: this.runStreamedInternal(input, turnOptions) };
|
|
4783
|
-
}
|
|
4784
|
-
async *runStreamedInternal(input, turnOptions = {}) {
|
|
4785
|
-
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
|
|
4786
|
-
const options = this._threadOptions;
|
|
4787
|
-
const { prompt, images } = normalizeInput(input);
|
|
4788
|
-
const generator = this._exec.run({
|
|
4789
|
-
input: prompt,
|
|
4790
|
-
baseUrl: this._options.baseUrl,
|
|
4791
|
-
apiKey: this._options.apiKey,
|
|
4792
|
-
threadId: this._id,
|
|
4793
|
-
images,
|
|
4794
|
-
model: options?.model,
|
|
4795
|
-
sandboxMode: options?.sandboxMode,
|
|
4796
|
-
workingDirectory: options?.workingDirectory,
|
|
4797
|
-
skipGitRepoCheck: options?.skipGitRepoCheck,
|
|
4798
|
-
outputSchemaFile: schemaPath,
|
|
4799
|
-
modelReasoningEffort: options?.modelReasoningEffort,
|
|
4800
|
-
signal: turnOptions.signal,
|
|
4801
|
-
networkAccessEnabled: options?.networkAccessEnabled,
|
|
4802
|
-
webSearchMode: options?.webSearchMode,
|
|
4803
|
-
webSearchEnabled: options?.webSearchEnabled,
|
|
4804
|
-
approvalPolicy: options?.approvalPolicy,
|
|
4805
|
-
additionalDirectories: options?.additionalDirectories
|
|
4806
|
-
});
|
|
4807
|
-
try {
|
|
4808
|
-
for await (const item of generator) {
|
|
4809
|
-
let parsed;
|
|
4810
|
-
try {
|
|
4811
|
-
parsed = JSON.parse(item);
|
|
4812
|
-
} catch (error) {
|
|
4813
|
-
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
|
4814
|
-
}
|
|
4815
|
-
if (parsed.type === "thread.started") {
|
|
4816
|
-
this._id = parsed.thread_id;
|
|
4817
|
-
} else if (parsed.type === "turn.completed") {
|
|
4818
|
-
parsed.usage.cache_write_input_tokens ??= 0;
|
|
4819
|
-
}
|
|
4820
|
-
yield parsed;
|
|
4821
|
-
}
|
|
4822
|
-
} finally {
|
|
4823
|
-
await cleanup();
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4826
|
-
/** Provides the input to the agent and returns the completed turn. */
|
|
4827
|
-
async run(input, turnOptions = {}) {
|
|
4828
|
-
const generator = this.runStreamedInternal(input, turnOptions);
|
|
4829
|
-
const items = [];
|
|
4830
|
-
let finalResponse = "";
|
|
4831
|
-
let usage = null;
|
|
4832
|
-
let turnFailure = null;
|
|
4833
|
-
for await (const event of generator) {
|
|
4834
|
-
if (event.type === "item.completed") {
|
|
4835
|
-
if (event.item.type === "agent_message") {
|
|
4836
|
-
finalResponse = event.item.text;
|
|
4837
|
-
}
|
|
4838
|
-
items.push(event.item);
|
|
4839
|
-
} else if (event.type === "turn.completed") {
|
|
4840
|
-
usage = event.usage;
|
|
4841
|
-
} else if (event.type === "turn.failed") {
|
|
4842
|
-
turnFailure = event.error;
|
|
4843
|
-
break;
|
|
4844
|
-
}
|
|
4845
|
-
}
|
|
4846
|
-
if (turnFailure) {
|
|
4847
|
-
throw new Error(turnFailure.message);
|
|
4848
|
-
}
|
|
4849
|
-
return { items, finalResponse, usage };
|
|
4850
|
-
}
|
|
4851
|
-
};
|
|
4852
|
-
function normalizeInput(input) {
|
|
4853
|
-
if (typeof input === "string") {
|
|
4854
|
-
return { prompt: input, images: [] };
|
|
4855
|
-
}
|
|
4856
|
-
const promptParts = [];
|
|
4857
|
-
const images = [];
|
|
4858
|
-
for (const item of input) {
|
|
4859
|
-
if (item.type === "text") {
|
|
4860
|
-
promptParts.push(item.text);
|
|
4861
|
-
} else if (item.type === "local_image") {
|
|
4862
|
-
images.push(item.path);
|
|
4863
|
-
}
|
|
4864
|
-
}
|
|
4865
|
-
return { prompt: promptParts.join("\n\n"), images };
|
|
4866
|
-
}
|
|
4867
|
-
var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
|
4868
|
-
var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
|
|
4869
|
-
var CODEX_NPM_NAME = "@openai/codex";
|
|
4870
|
-
var PLATFORM_PACKAGE_BY_TARGET = {
|
|
4871
|
-
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
|
4872
|
-
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
|
|
4873
|
-
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
|
|
4874
|
-
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
|
|
4875
|
-
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
|
|
4876
|
-
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
|
|
4877
|
-
};
|
|
4878
|
-
var moduleRequire = createRequire(import.meta.url);
|
|
4879
|
-
var CodexExec = class {
|
|
4880
|
-
executablePath;
|
|
4881
|
-
pathDirs;
|
|
4882
|
-
envOverride;
|
|
4883
|
-
configOverrides;
|
|
4884
|
-
constructor(executablePath = null, env, configOverrides) {
|
|
4885
|
-
if (executablePath) {
|
|
4886
|
-
this.executablePath = executablePath;
|
|
4887
|
-
this.pathDirs = [];
|
|
4888
|
-
} else {
|
|
4889
|
-
const resolved = findCodexPath();
|
|
4890
|
-
this.executablePath = resolved.executablePath;
|
|
4891
|
-
this.pathDirs = resolved.pathDirs;
|
|
4892
|
-
}
|
|
4893
|
-
this.envOverride = env;
|
|
4894
|
-
this.configOverrides = configOverrides;
|
|
4895
|
-
}
|
|
4896
|
-
async *run(args) {
|
|
4897
|
-
const commandArgs = ["exec", "--experimental-json"];
|
|
4898
|
-
if (this.configOverrides) {
|
|
4899
|
-
for (const override of serializeConfigOverrides(this.configOverrides)) {
|
|
4900
|
-
commandArgs.push("--config", override);
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
if (args.baseUrl) {
|
|
4904
|
-
commandArgs.push(
|
|
4905
|
-
"--config",
|
|
4906
|
-
`openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
|
|
4907
|
-
);
|
|
4908
|
-
}
|
|
4909
|
-
if (args.model) {
|
|
4910
|
-
commandArgs.push("--model", args.model);
|
|
4911
|
-
}
|
|
4912
|
-
if (args.sandboxMode) {
|
|
4913
|
-
commandArgs.push("--sandbox", args.sandboxMode);
|
|
4914
|
-
}
|
|
4915
|
-
if (args.workingDirectory) {
|
|
4916
|
-
commandArgs.push("--cd", args.workingDirectory);
|
|
4917
|
-
}
|
|
4918
|
-
if (args.additionalDirectories?.length) {
|
|
4919
|
-
for (const dir2 of args.additionalDirectories) {
|
|
4920
|
-
commandArgs.push("--add-dir", dir2);
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
if (args.skipGitRepoCheck) {
|
|
4924
|
-
commandArgs.push("--skip-git-repo-check");
|
|
4925
|
-
}
|
|
4926
|
-
if (args.outputSchemaFile) {
|
|
4927
|
-
commandArgs.push("--output-schema", args.outputSchemaFile);
|
|
4928
|
-
}
|
|
4929
|
-
if (args.modelReasoningEffort) {
|
|
4930
|
-
commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
|
|
4931
|
-
}
|
|
4932
|
-
if (args.networkAccessEnabled !== void 0) {
|
|
4933
|
-
commandArgs.push(
|
|
4934
|
-
"--config",
|
|
4935
|
-
`sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
|
|
4936
|
-
);
|
|
4937
|
-
}
|
|
4938
|
-
if (args.webSearchMode) {
|
|
4939
|
-
commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
|
|
4940
|
-
} else if (args.webSearchEnabled === true) {
|
|
4941
|
-
commandArgs.push("--config", `web_search="live"`);
|
|
4942
|
-
} else if (args.webSearchEnabled === false) {
|
|
4943
|
-
commandArgs.push("--config", `web_search="disabled"`);
|
|
4944
|
-
}
|
|
4945
|
-
if (args.approvalPolicy) {
|
|
4946
|
-
commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
|
|
4947
|
-
}
|
|
4948
|
-
if (args.threadId) {
|
|
4949
|
-
commandArgs.push("resume", args.threadId);
|
|
4950
|
-
}
|
|
4951
|
-
if (args.images?.length) {
|
|
4952
|
-
for (const image of args.images) {
|
|
4953
|
-
commandArgs.push("--image", image);
|
|
4954
|
-
}
|
|
4955
|
-
}
|
|
4956
|
-
const env = {};
|
|
4957
|
-
if (this.envOverride) {
|
|
4958
|
-
Object.assign(env, this.envOverride);
|
|
4959
|
-
} else {
|
|
4960
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
4961
|
-
if (value !== void 0) {
|
|
4962
|
-
env[key] = value;
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
if (!env[INTERNAL_ORIGINATOR_ENV]) {
|
|
4967
|
-
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
|
|
4968
|
-
}
|
|
4969
|
-
if (args.apiKey) {
|
|
4970
|
-
env.CODEX_API_KEY = args.apiKey;
|
|
4971
|
-
}
|
|
4972
|
-
if (this.pathDirs.length > 0) {
|
|
4973
|
-
prependPathDirs(env, this.pathDirs);
|
|
4974
|
-
}
|
|
4975
|
-
const child = spawn4(this.executablePath, commandArgs, {
|
|
4976
|
-
env,
|
|
4977
|
-
signal: args.signal
|
|
4978
|
-
});
|
|
4979
|
-
let spawnError = null;
|
|
4980
|
-
child.once("error", (err) => spawnError = err);
|
|
4981
|
-
if (!child.stdin) {
|
|
4982
|
-
child.kill();
|
|
4983
|
-
throw new Error("Child process has no stdin");
|
|
4984
|
-
}
|
|
4985
|
-
child.stdin.write(args.input);
|
|
4986
|
-
child.stdin.end();
|
|
4987
|
-
if (!child.stdout) {
|
|
4988
|
-
child.kill();
|
|
4989
|
-
throw new Error("Child process has no stdout");
|
|
4990
|
-
}
|
|
4991
|
-
const stderrChunks = [];
|
|
4992
|
-
if (child.stderr) {
|
|
4993
|
-
child.stderr.on("data", (data) => {
|
|
4994
|
-
stderrChunks.push(data);
|
|
4995
|
-
});
|
|
4996
|
-
}
|
|
4997
|
-
const exitPromise = new Promise(
|
|
4998
|
-
(resolve) => {
|
|
4999
|
-
child.once("exit", (code, signal) => {
|
|
5000
|
-
resolve({ code, signal });
|
|
5001
|
-
});
|
|
5002
|
-
}
|
|
5003
|
-
);
|
|
5004
|
-
const rl = readline.createInterface({
|
|
5005
|
-
input: child.stdout,
|
|
5006
|
-
crlfDelay: Infinity
|
|
5007
|
-
});
|
|
5008
|
-
try {
|
|
5009
|
-
for await (const line of rl) {
|
|
5010
|
-
yield line;
|
|
5011
|
-
}
|
|
5012
|
-
if (spawnError) throw spawnError;
|
|
5013
|
-
const { code, signal } = await exitPromise;
|
|
5014
|
-
if (code !== 0 || signal) {
|
|
5015
|
-
const stderrBuffer = Buffer.concat(stderrChunks);
|
|
5016
|
-
const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
|
|
5017
|
-
throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
|
|
5018
|
-
}
|
|
5019
|
-
} finally {
|
|
5020
|
-
rl.close();
|
|
5021
|
-
child.removeAllListeners();
|
|
5022
|
-
try {
|
|
5023
|
-
if (!child.killed) child.kill();
|
|
5024
|
-
} catch {
|
|
5025
|
-
}
|
|
5026
|
-
}
|
|
5027
|
-
}
|
|
5028
|
-
};
|
|
5029
|
-
function serializeConfigOverrides(configOverrides) {
|
|
5030
|
-
const overrides = [];
|
|
5031
|
-
flattenConfigOverrides(configOverrides, "", overrides);
|
|
5032
|
-
return overrides;
|
|
5033
|
-
}
|
|
5034
|
-
function flattenConfigOverrides(value, prefix, overrides) {
|
|
5035
|
-
if (!isPlainObject(value)) {
|
|
5036
|
-
if (prefix) {
|
|
5037
|
-
overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
|
|
5038
|
-
return;
|
|
5039
|
-
} else {
|
|
5040
|
-
throw new Error("Codex config overrides must be a plain object");
|
|
5041
|
-
}
|
|
5042
|
-
}
|
|
5043
|
-
const entries = Object.entries(value);
|
|
5044
|
-
if (!prefix && entries.length === 0) {
|
|
5045
|
-
return;
|
|
5046
|
-
}
|
|
5047
|
-
if (prefix && entries.length === 0) {
|
|
5048
|
-
overrides.push(`${prefix}={}`);
|
|
5049
|
-
return;
|
|
5050
|
-
}
|
|
5051
|
-
for (const [key, child] of entries) {
|
|
5052
|
-
if (!key) {
|
|
5053
|
-
throw new Error("Codex config override keys must be non-empty strings");
|
|
5054
|
-
}
|
|
5055
|
-
if (child === void 0) {
|
|
5056
|
-
continue;
|
|
5057
|
-
}
|
|
5058
|
-
const path3 = prefix ? `${prefix}.${key}` : key;
|
|
5059
|
-
if (isPlainObject(child)) {
|
|
5060
|
-
flattenConfigOverrides(child, path3, overrides);
|
|
5061
|
-
} else {
|
|
5062
|
-
overrides.push(`${path3}=${toTomlValue(child, path3)}`);
|
|
5063
|
-
}
|
|
5064
|
-
}
|
|
5065
|
-
}
|
|
5066
|
-
function toTomlValue(value, path3) {
|
|
5067
|
-
if (typeof value === "string") {
|
|
5068
|
-
return JSON.stringify(value);
|
|
5069
|
-
} else if (typeof value === "number") {
|
|
5070
|
-
if (!Number.isFinite(value)) {
|
|
5071
|
-
throw new Error(`Codex config override at ${path3} must be a finite number`);
|
|
5072
|
-
}
|
|
5073
|
-
return `${value}`;
|
|
5074
|
-
} else if (typeof value === "boolean") {
|
|
5075
|
-
return value ? "true" : "false";
|
|
5076
|
-
} else if (Array.isArray(value)) {
|
|
5077
|
-
const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
|
|
5078
|
-
return `[${rendered.join(", ")}]`;
|
|
5079
|
-
} else if (isPlainObject(value)) {
|
|
5080
|
-
const parts = [];
|
|
5081
|
-
for (const [key, child] of Object.entries(value)) {
|
|
5082
|
-
if (!key) {
|
|
5083
|
-
throw new Error("Codex config override keys must be non-empty strings");
|
|
5084
|
-
}
|
|
5085
|
-
if (child === void 0) {
|
|
5086
|
-
continue;
|
|
5087
|
-
}
|
|
5088
|
-
parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
|
|
5089
|
-
}
|
|
5090
|
-
return `{${parts.join(", ")}}`;
|
|
5091
|
-
} else if (value === null) {
|
|
5092
|
-
throw new Error(`Codex config override at ${path3} cannot be null`);
|
|
5093
|
-
} else {
|
|
5094
|
-
const typeName = typeof value;
|
|
5095
|
-
throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
|
|
5096
|
-
}
|
|
5097
|
-
}
|
|
5098
|
-
var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
5099
|
-
function formatTomlKey(key) {
|
|
5100
|
-
return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
|
|
5101
|
-
}
|
|
5102
|
-
function isPlainObject(value) {
|
|
5103
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5104
|
-
}
|
|
5105
|
-
function findCodexPath() {
|
|
5106
|
-
const { platform, arch } = process;
|
|
5107
|
-
let targetTriple = null;
|
|
5108
|
-
switch (platform) {
|
|
5109
|
-
case "linux":
|
|
5110
|
-
case "android":
|
|
5111
|
-
switch (arch) {
|
|
5112
|
-
case "x64":
|
|
5113
|
-
targetTriple = "x86_64-unknown-linux-musl";
|
|
5114
|
-
break;
|
|
5115
|
-
case "arm64":
|
|
5116
|
-
targetTriple = "aarch64-unknown-linux-musl";
|
|
5117
|
-
break;
|
|
5118
|
-
default:
|
|
5119
|
-
break;
|
|
5120
|
-
}
|
|
5121
|
-
break;
|
|
5122
|
-
case "darwin":
|
|
5123
|
-
switch (arch) {
|
|
5124
|
-
case "x64":
|
|
5125
|
-
targetTriple = "x86_64-apple-darwin";
|
|
5126
|
-
break;
|
|
5127
|
-
case "arm64":
|
|
5128
|
-
targetTriple = "aarch64-apple-darwin";
|
|
5129
|
-
break;
|
|
5130
|
-
default:
|
|
5131
|
-
break;
|
|
5132
|
-
}
|
|
5133
|
-
break;
|
|
5134
|
-
case "win32":
|
|
5135
|
-
switch (arch) {
|
|
5136
|
-
case "x64":
|
|
5137
|
-
targetTriple = "x86_64-pc-windows-msvc";
|
|
5138
|
-
break;
|
|
5139
|
-
case "arm64":
|
|
5140
|
-
targetTriple = "aarch64-pc-windows-msvc";
|
|
5141
|
-
break;
|
|
5142
|
-
default:
|
|
5143
|
-
break;
|
|
5144
|
-
}
|
|
5145
|
-
break;
|
|
5146
|
-
default:
|
|
5147
|
-
break;
|
|
5148
|
-
}
|
|
5149
|
-
if (!targetTriple) {
|
|
5150
|
-
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
|
5151
|
-
}
|
|
5152
|
-
const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
|
|
5153
|
-
if (!platformPackage) {
|
|
5154
|
-
throw new Error(`Unsupported target triple: ${targetTriple}`);
|
|
5155
|
-
}
|
|
5156
|
-
let vendorRoot;
|
|
5157
|
-
try {
|
|
5158
|
-
const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
|
|
5159
|
-
const codexRequire = createRequire(codexPackageJsonPath);
|
|
5160
|
-
const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
|
|
5161
|
-
vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
|
|
5162
|
-
} catch {
|
|
5163
|
-
throw new Error(
|
|
5164
|
-
`Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
|
|
5165
|
-
);
|
|
5166
|
-
}
|
|
5167
|
-
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
|
|
5168
|
-
const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
|
|
5169
|
-
if (!nativePackage) {
|
|
5170
|
-
throw new Error(
|
|
5171
|
-
`Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
|
|
5172
|
-
);
|
|
5173
|
-
}
|
|
5174
|
-
return nativePackage;
|
|
5175
|
-
}
|
|
5176
|
-
function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
|
|
5177
|
-
const packageRoot = path2.join(vendorRoot, targetTriple);
|
|
5178
|
-
const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
|
|
5179
|
-
if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
|
|
5180
|
-
return {
|
|
5181
|
-
executablePath: packageBinaryPath,
|
|
5182
|
-
pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
|
|
5183
|
-
};
|
|
5184
|
-
}
|
|
5185
|
-
const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
|
|
5186
|
-
if (isFile(legacyBinaryPath)) {
|
|
5187
|
-
return {
|
|
5188
|
-
executablePath: legacyBinaryPath,
|
|
5189
|
-
pathDirs: existingDirs(path2.join(packageRoot, "path"))
|
|
5190
|
-
};
|
|
5191
|
-
}
|
|
5192
|
-
return null;
|
|
5193
|
-
}
|
|
5194
|
-
function existingDirs(...dirs) {
|
|
5195
|
-
return dirs.filter(isDirectory);
|
|
5196
|
-
}
|
|
5197
|
-
function prependPathDirs(env, pathDirs, platform = process.platform) {
|
|
5198
|
-
const pathKey = pathEnvKey(env, platform);
|
|
5199
|
-
if (platform === "win32") {
|
|
5200
|
-
for (const key of Object.keys(env)) {
|
|
5201
|
-
if (key.toLowerCase() === "path" && key !== pathKey) {
|
|
5202
|
-
delete env[key];
|
|
5203
|
-
}
|
|
5204
|
-
}
|
|
5205
|
-
}
|
|
5206
|
-
const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
|
|
5207
|
-
env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
|
|
5208
|
-
}
|
|
5209
|
-
function pathEnvKey(env, platform) {
|
|
5210
|
-
if (platform !== "win32") {
|
|
5211
|
-
return "PATH";
|
|
5212
|
-
}
|
|
5213
|
-
const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
|
|
5214
|
-
return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
|
|
5215
|
-
}
|
|
5216
|
-
function isFile(filePath) {
|
|
5217
|
-
try {
|
|
5218
|
-
return statSync(filePath).isFile();
|
|
5219
|
-
} catch {
|
|
5220
|
-
return false;
|
|
5221
|
-
}
|
|
5222
|
-
}
|
|
5223
|
-
function isDirectory(filePath) {
|
|
5224
|
-
try {
|
|
5225
|
-
return statSync(filePath).isDirectory();
|
|
5226
|
-
} catch {
|
|
5227
|
-
return false;
|
|
5228
|
-
}
|
|
5229
|
-
}
|
|
5230
|
-
var Codex = class {
|
|
5231
|
-
exec;
|
|
5232
|
-
options;
|
|
5233
|
-
constructor(options = {}) {
|
|
5234
|
-
const { codexPathOverride, env, config } = options;
|
|
5235
|
-
this.exec = new CodexExec(codexPathOverride, env, config);
|
|
5236
|
-
this.options = options;
|
|
5237
|
-
}
|
|
5238
|
-
/**
|
|
5239
|
-
* Starts a new conversation with an agent.
|
|
5240
|
-
* @returns A new thread instance.
|
|
5241
|
-
*/
|
|
5242
|
-
startThread(options = {}) {
|
|
5243
|
-
return new Thread(this.exec, this.options, options);
|
|
5244
|
-
}
|
|
5245
|
-
/**
|
|
5246
|
-
* Resumes a conversation with an agent based on the thread id.
|
|
5247
|
-
* Threads are persisted in ~/.codex/sessions.
|
|
5248
|
-
*
|
|
5249
|
-
* @param id The id of the thread to resume.
|
|
5250
|
-
* @returns A new thread instance.
|
|
5251
|
-
*/
|
|
5252
|
-
resumeThread(id, options = {}) {
|
|
5253
|
-
return new Thread(this.exec, this.options, options, id);
|
|
5254
|
-
}
|
|
5255
|
-
};
|
|
5256
|
-
|
|
5257
4777
|
// packages/agent-runtime/src/codex/transport.ts
|
|
4778
|
+
import {
|
|
4779
|
+
Codex
|
|
4780
|
+
} from "@openai/codex-sdk";
|
|
5258
4781
|
function buildSdkThreadOptions(spec) {
|
|
5259
4782
|
return {
|
|
5260
4783
|
...spec.model ? { model: spec.model } : {},
|
|
@@ -6046,7 +5569,7 @@ var ConnectorHealthStore = class {
|
|
|
6046
5569
|
|
|
6047
5570
|
// src/dispatcher.ts
|
|
6048
5571
|
import { randomUUID } from "crypto";
|
|
6049
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync
|
|
5572
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync } from "fs";
|
|
6050
5573
|
import { join as join13 } from "path";
|
|
6051
5574
|
|
|
6052
5575
|
// src/summon.ts
|
|
@@ -6384,10 +5907,10 @@ import { join as join9 } from "path";
|
|
|
6384
5907
|
var PREFIX = "cabane-codex-instructions-";
|
|
6385
5908
|
async function writeCodexInstructionsFile(contents) {
|
|
6386
5909
|
const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
|
|
6387
|
-
const
|
|
6388
|
-
await writeFile(
|
|
5910
|
+
const path = join9(dir2, "instructions.md");
|
|
5911
|
+
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
6389
5912
|
return {
|
|
6390
|
-
path
|
|
5913
|
+
path,
|
|
6391
5914
|
cleanup: async () => {
|
|
6392
5915
|
await rm(dir2, { recursive: true, force: true });
|
|
6393
5916
|
}
|
|
@@ -6407,10 +5930,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
|
|
|
6407
5930
|
return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
6408
5931
|
}
|
|
6409
5932
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6410
|
-
const
|
|
6411
|
-
if (!existsSync7(
|
|
5933
|
+
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
5934
|
+
if (!existsSync7(path)) return null;
|
|
6412
5935
|
try {
|
|
6413
|
-
const parsed = JSON.parse(readFileSync6(
|
|
5936
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
6414
5937
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
6415
5938
|
return {
|
|
6416
5939
|
cwd: parsed.cwd,
|
|
@@ -6444,14 +5967,14 @@ function secretsPath() {
|
|
|
6444
5967
|
}
|
|
6445
5968
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
6446
5969
|
function loadSecretStore() {
|
|
6447
|
-
const
|
|
6448
|
-
if (!existsSync8(
|
|
5970
|
+
const path = secretsPath();
|
|
5971
|
+
if (!existsSync8(path)) return makeStore({});
|
|
6449
5972
|
let raw;
|
|
6450
5973
|
try {
|
|
6451
|
-
raw = readFileSync7(
|
|
5974
|
+
raw = readFileSync7(path, "utf8");
|
|
6452
5975
|
} catch (err) {
|
|
6453
5976
|
throw new ConfigError(
|
|
6454
|
-
`couldn't read ${
|
|
5977
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
6455
5978
|
);
|
|
6456
5979
|
}
|
|
6457
5980
|
if (raw.trim().length === 0) return makeStore({});
|
|
@@ -6460,13 +5983,13 @@ function loadSecretStore() {
|
|
|
6460
5983
|
parsed = JSON.parse(raw);
|
|
6461
5984
|
} catch (err) {
|
|
6462
5985
|
throw new ConfigError(
|
|
6463
|
-
`${
|
|
5986
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
6464
5987
|
);
|
|
6465
5988
|
}
|
|
6466
5989
|
const result = secretStoreSchema.safeParse(parsed);
|
|
6467
5990
|
if (!result.success) {
|
|
6468
5991
|
throw new ConfigError(
|
|
6469
|
-
`${
|
|
5992
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
6470
5993
|
);
|
|
6471
5994
|
}
|
|
6472
5995
|
return makeStore(result.data);
|
|
@@ -6902,7 +6425,7 @@ function checkoutState(cwd) {
|
|
|
6902
6425
|
if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
|
|
6903
6426
|
let stat;
|
|
6904
6427
|
try {
|
|
6905
|
-
stat =
|
|
6428
|
+
stat = statSync(gitPath);
|
|
6906
6429
|
} catch (error) {
|
|
6907
6430
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6908
6431
|
}
|
|
@@ -7850,7 +7373,9 @@ var LABELS = {
|
|
|
7850
7373
|
function deriveHarnessSnapshot(signals) {
|
|
7851
7374
|
const advertised = new Set(
|
|
7852
7375
|
buildCompanionManifest({
|
|
7853
|
-
|
|
7376
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7377
|
+
// through the same function rather than re-decided.
|
|
7378
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
7854
7379
|
opencode: signals.opencodeConfigured,
|
|
7855
7380
|
codex: signals.codexEnabled
|
|
7856
7381
|
}).runtimes.map((r) => r.name)
|
|
@@ -7863,20 +7388,40 @@ function deriveHarnessSnapshot(signals) {
|
|
|
7863
7388
|
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
7864
7389
|
}
|
|
7865
7390
|
function deriveClaudeCode(signals, manifestHas) {
|
|
7866
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"]
|
|
7391
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
7867
7392
|
if (manifestHas) {
|
|
7868
7393
|
return {
|
|
7869
7394
|
...base,
|
|
7870
7395
|
state: "exposed",
|
|
7871
7396
|
version: signals.claudeVersion,
|
|
7872
|
-
detail: "Claude Code is
|
|
7397
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7398
|
+
enable: null
|
|
7399
|
+
};
|
|
7400
|
+
}
|
|
7401
|
+
if (signals.claudeCodeConnected) {
|
|
7402
|
+
return {
|
|
7403
|
+
...base,
|
|
7404
|
+
state: "needs_attention",
|
|
7405
|
+
version: null,
|
|
7406
|
+
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.",
|
|
7407
|
+
enable: null
|
|
7408
|
+
};
|
|
7409
|
+
}
|
|
7410
|
+
if (signals.claudeOnPath) {
|
|
7411
|
+
return {
|
|
7412
|
+
...base,
|
|
7413
|
+
state: "detected_not_exposed",
|
|
7414
|
+
version: signals.claudeVersion,
|
|
7415
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7416
|
+
enable: "claude-code"
|
|
7873
7417
|
};
|
|
7874
7418
|
}
|
|
7875
7419
|
return {
|
|
7876
7420
|
...base,
|
|
7877
7421
|
state: "not_detected",
|
|
7878
7422
|
version: null,
|
|
7879
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it
|
|
7423
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7424
|
+
enable: null
|
|
7880
7425
|
};
|
|
7881
7426
|
}
|
|
7882
7427
|
function deriveCodex(signals, manifestHas) {
|
|
@@ -7944,6 +7489,9 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
7944
7489
|
enable: "opencode"
|
|
7945
7490
|
};
|
|
7946
7491
|
}
|
|
7492
|
+
function detectedRuntimesFor(snapshot) {
|
|
7493
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7494
|
+
}
|
|
7947
7495
|
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
7948
7496
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
7949
7497
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -7960,6 +7508,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
7960
7508
|
return {
|
|
7961
7509
|
claudeOnPath: claudeOnPathResult,
|
|
7962
7510
|
claudeVersion,
|
|
7511
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7512
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
7513
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
7963
7514
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7964
7515
|
// exposes codex; its config flag is the manifest gate either way).
|
|
7965
7516
|
codexOnPath: codexVersion !== null,
|
|
@@ -8333,8 +7884,8 @@ function sleep2(ms) {
|
|
|
8333
7884
|
}
|
|
8334
7885
|
|
|
8335
7886
|
// src/version.ts
|
|
8336
|
-
import { createRequire
|
|
8337
|
-
var pkg =
|
|
7887
|
+
import { createRequire } from "module";
|
|
7888
|
+
var pkg = createRequire(import.meta.url)("../package.json");
|
|
8338
7889
|
var COMPANION_VERSION = pkg.version;
|
|
8339
7890
|
|
|
8340
7891
|
// src/supervisor.ts
|
|
@@ -8368,6 +7919,10 @@ var CompanionSupervisor = class {
|
|
|
8368
7919
|
// can never disagree with what the manifest advertises. Null until the first
|
|
8369
7920
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
8370
7921
|
harnessSignals = null;
|
|
7922
|
+
// CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
|
|
7923
|
+
// NOT the cached beat signal — someone connecting right after installing Claude
|
|
7924
|
+
// Code shouldn't be refused by a snapshot up to a heartbeat old.
|
|
7925
|
+
probeClaudePresence;
|
|
8371
7926
|
exitFn;
|
|
8372
7927
|
reexecFn;
|
|
8373
7928
|
dispatcherFactory;
|
|
@@ -8398,6 +7953,7 @@ var CompanionSupervisor = class {
|
|
|
8398
7953
|
this.log = opts.log;
|
|
8399
7954
|
this.hub = opts.hub;
|
|
8400
7955
|
this.claudeCode = opts.claudeCode ?? true;
|
|
7956
|
+
this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
|
|
8401
7957
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
8402
7958
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
8403
7959
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -8460,11 +8016,10 @@ var CompanionSupervisor = class {
|
|
|
8460
8016
|
// claude-code when `claude` is on PATH, CT270 opencode when the operator
|
|
8461
8017
|
// configured an `opencode serve`.
|
|
8462
8018
|
manifest: buildCompanionManifest({
|
|
8463
|
-
//
|
|
8464
|
-
// until the first
|
|
8465
|
-
//
|
|
8466
|
-
|
|
8467
|
-
claudeCode: this.claudeCodePresent(),
|
|
8019
|
+
// CT1082: connected AND installed. Presence is the live re-probe (CT586),
|
|
8020
|
+
// falling back to the boot probe until the first one lands; consent is the
|
|
8021
|
+
// user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
|
|
8022
|
+
claudeCode: this.claudeCodeOffered(),
|
|
8468
8023
|
opencode: !!this.config.opencode?.serverUrl,
|
|
8469
8024
|
// CT481: advertise codex when the operator enabled it (config-gated,
|
|
8470
8025
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
@@ -8482,7 +8037,13 @@ var CompanionSupervisor = class {
|
|
|
8482
8037
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
8483
8038
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
8484
8039
|
// leaves this device's stored availability untouched.
|
|
8485
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8040
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
8041
|
+
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8042
|
+
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8043
|
+
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8044
|
+
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8045
|
+
// the same fail-soft contract `models` keeps.
|
|
8046
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8486
8047
|
});
|
|
8487
8048
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8488
8049
|
this.deviceId = res.deviceId;
|
|
@@ -8687,13 +8248,19 @@ var CompanionSupervisor = class {
|
|
|
8687
8248
|
"companion: stopped agent (unassigned)"
|
|
8688
8249
|
);
|
|
8689
8250
|
}
|
|
8690
|
-
// CT833: is Claude Code on this machine right now?
|
|
8691
|
-
//
|
|
8692
|
-
//
|
|
8693
|
-
// per-beat re-probe), falling back to the boot probe until the first one lands.
|
|
8251
|
+
// CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
|
|
8252
|
+
// falling back to the boot probe until the first one lands. Presence ONLY —
|
|
8253
|
+
// CT1082 split presence from exposure, so nothing routes on this directly.
|
|
8694
8254
|
claudeCodePresent() {
|
|
8695
8255
|
return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
|
|
8696
8256
|
}
|
|
8257
|
+
// CT1082: does this device OFFER claude-code — connected by its user and actually
|
|
8258
|
+
// installed? The ONE signal both the heartbeat manifest and the dispatcher's
|
|
8259
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
8260
|
+
// can't disagree (the CT833 invariant, now with consent in front of it).
|
|
8261
|
+
claudeCodeOffered() {
|
|
8262
|
+
return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
|
|
8263
|
+
}
|
|
8697
8264
|
buildDispatcher(ctx) {
|
|
8698
8265
|
if (this.dispatcherFactory) return this.dispatcherFactory(ctx);
|
|
8699
8266
|
return new Dispatcher({
|
|
@@ -8711,10 +8278,12 @@ var CompanionSupervisor = class {
|
|
|
8711
8278
|
}),
|
|
8712
8279
|
runConfig: ctx.runConfig,
|
|
8713
8280
|
log: this.log,
|
|
8714
|
-
// CT833: register the claude-code adapter only when
|
|
8715
|
-
//
|
|
8716
|
-
// after boot works on the next turn exactly as it
|
|
8717
|
-
|
|
8281
|
+
// CT833: register the claude-code adapter only when this device actually
|
|
8282
|
+
// offers claude-code — read per turn (not captured here), so a harness
|
|
8283
|
+
// installed or connected after boot works on the next turn exactly as it
|
|
8284
|
+
// appears on the next beat. CT1082: "offers" now means connected as well as
|
|
8285
|
+
// installed, so a disconnected harness can't be selected either.
|
|
8286
|
+
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
8718
8287
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
8719
8288
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
8720
8289
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
@@ -9020,9 +8589,12 @@ var CompanionSupervisor = class {
|
|
|
9020
8589
|
async recheckHarnesses() {
|
|
9021
8590
|
await this.refreshHarnessStatuses();
|
|
9022
8591
|
}
|
|
9023
|
-
// Friendly enable for the
|
|
9024
|
-
//
|
|
9025
|
-
//
|
|
8592
|
+
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8593
|
+
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8594
|
+
// never installs a binary and never drives a login (BYO — Decided).
|
|
8595
|
+
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
8596
|
+
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
8597
|
+
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
9026
8598
|
// - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
|
|
9027
8599
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
9028
8600
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
@@ -9032,7 +8604,15 @@ var CompanionSupervisor = class {
|
|
|
9032
8604
|
// reachable/valid input).
|
|
9033
8605
|
async enableHarness(input) {
|
|
9034
8606
|
let next;
|
|
9035
|
-
if (input.runtime === "
|
|
8607
|
+
if (input.runtime === "claude-code") {
|
|
8608
|
+
if (!await this.probeClaudePresence()) {
|
|
8609
|
+
return {
|
|
8610
|
+
ok: false,
|
|
8611
|
+
error: "Couldn\u2019t find `claude` on this machine\u2019s PATH. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in, then connect it."
|
|
8612
|
+
};
|
|
8613
|
+
}
|
|
8614
|
+
next = { ...this.config, claudeCode: { enabled: true } };
|
|
8615
|
+
} else if (input.runtime === "codex") {
|
|
9036
8616
|
next = { ...this.config, codex: { enabled: true } };
|
|
9037
8617
|
} else {
|
|
9038
8618
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -9138,9 +8718,9 @@ var CompanionSupervisor = class {
|
|
|
9138
8718
|
};
|
|
9139
8719
|
function defaultReexec() {
|
|
9140
8720
|
clearRuntimeState();
|
|
9141
|
-
void import("child_process").then(({ spawn:
|
|
8721
|
+
void import("child_process").then(({ spawn: spawn4 }) => {
|
|
9142
8722
|
try {
|
|
9143
|
-
const child =
|
|
8723
|
+
const child = spawn4(process.execPath, process.argv.slice(1), {
|
|
9144
8724
|
stdio: "inherit",
|
|
9145
8725
|
detached: false
|
|
9146
8726
|
});
|
|
@@ -9224,8 +8804,8 @@ function recordCrash(rec) {
|
|
|
9224
8804
|
}
|
|
9225
8805
|
function clearCrash() {
|
|
9226
8806
|
try {
|
|
9227
|
-
const
|
|
9228
|
-
if (existsSync11(
|
|
8807
|
+
const path = crashMarkerPath();
|
|
8808
|
+
if (existsSync11(path)) rmSync7(path, { force: true });
|
|
9229
8809
|
} catch {
|
|
9230
8810
|
}
|
|
9231
8811
|
}
|
|
@@ -9234,13 +8814,17 @@ function clearCrash() {
|
|
|
9234
8814
|
async function createCompanionRuntime(opts = {}) {
|
|
9235
8815
|
const log = getLogger();
|
|
9236
8816
|
installProcessSafetyNet(log);
|
|
9237
|
-
const probeClaude = opts.probeClaude ?? claudeOnPath;
|
|
9238
8817
|
let cfg;
|
|
9239
8818
|
let claudeCode;
|
|
9240
8819
|
try {
|
|
9241
|
-
cfg =
|
|
9242
|
-
|
|
9243
|
-
|
|
8820
|
+
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
8821
|
+
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
8822
|
+
onMigrated: (migrated, onPath) => log.info(
|
|
8823
|
+
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
8824
|
+
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)"
|
|
8825
|
+
)
|
|
8826
|
+
}));
|
|
8827
|
+
await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
|
|
9244
8828
|
} catch (err) {
|
|
9245
8829
|
recordCrash({
|
|
9246
8830
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -9252,7 +8836,9 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9252
8836
|
}
|
|
9253
8837
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
9254
8838
|
const harnessVersions = await probeHarnessVersions({
|
|
9255
|
-
|
|
8839
|
+
// CT1082: versions are probed for the runtimes this device OFFERS, and
|
|
8840
|
+
// claude-code is offered only when connected as well as installed.
|
|
8841
|
+
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
9256
8842
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
9257
8843
|
codex: isCodexEnabled(cfg)
|
|
9258
8844
|
});
|