@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/cli.js
CHANGED
|
@@ -295,6 +295,26 @@ var companionConfigSchema = z2.object({
|
|
|
295
295
|
opencode: z2.object({
|
|
296
296
|
serverUrl: z2.string().url()
|
|
297
297
|
}).strict().optional(),
|
|
298
|
+
// CT1082: the Claude Code runtime, when the user connected it on this machine.
|
|
299
|
+
// The change this task exists for: Claude Code used to be the one harness a
|
|
300
|
+
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
301
|
+
// consent — and it is now the third config-driven harness, opted into exactly
|
|
302
|
+
// like codex. `claude` on PATH is still required (you can't run what isn't
|
|
303
|
+
// installed), but presence alone no longer exposes anything: the manifest
|
|
304
|
+
// advertises claude-code only when this block says so AND the binary is there.
|
|
305
|
+
//
|
|
306
|
+
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
307
|
+
// Absent means the config predates this task — a device whose user was never
|
|
308
|
+
// asked — and the migration grandfathers it on first start. So every config
|
|
309
|
+
// written from here on carries the block explicitly, including a freshly-paired
|
|
310
|
+
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
311
|
+
// until its user says otherwise.
|
|
312
|
+
//
|
|
313
|
+
// Not to be confused with the per-agent `agents.<key>.claudeCode.autoMemory`
|
|
314
|
+
// above — that's one agent's memory switch, this is the device's connected set.
|
|
315
|
+
claudeCode: z2.object({
|
|
316
|
+
enabled: z2.boolean().optional()
|
|
317
|
+
}).strict().optional(),
|
|
298
318
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
299
319
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
300
320
|
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
@@ -311,19 +331,26 @@ var companionConfigSchema = z2.object({
|
|
|
311
331
|
function isCodexEnabled(cfg) {
|
|
312
332
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
313
333
|
}
|
|
334
|
+
function isClaudeCodeConnected(cfg) {
|
|
335
|
+
return !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
336
|
+
}
|
|
337
|
+
function migrateConnectedHarnesses(cfg, claudeOnPath2) {
|
|
338
|
+
if (cfg.claudeCode !== void 0) return null;
|
|
339
|
+
return { ...cfg, claudeCode: { enabled: claudeOnPath2 } };
|
|
340
|
+
}
|
|
314
341
|
function localAgentConfig(cfg, agent) {
|
|
315
342
|
const map = cfg.agents ?? {};
|
|
316
343
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
317
344
|
}
|
|
318
345
|
function loadConfig() {
|
|
319
|
-
const
|
|
320
|
-
if (!existsSync(
|
|
346
|
+
const path = configPath();
|
|
347
|
+
if (!existsSync(path)) return null;
|
|
321
348
|
let raw;
|
|
322
349
|
try {
|
|
323
|
-
raw = readFileSync(
|
|
350
|
+
raw = readFileSync(path, "utf8");
|
|
324
351
|
} catch (err) {
|
|
325
352
|
throw new ConfigError(
|
|
326
|
-
`couldn't read ${
|
|
353
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
327
354
|
);
|
|
328
355
|
}
|
|
329
356
|
if (raw.trim().length === 0) return null;
|
|
@@ -332,7 +359,7 @@ function loadConfig() {
|
|
|
332
359
|
parsed = JSON.parse(raw);
|
|
333
360
|
} catch (err) {
|
|
334
361
|
throw new ConfigError(
|
|
335
|
-
`${
|
|
362
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
336
363
|
);
|
|
337
364
|
}
|
|
338
365
|
const result = companionConfigSchema.safeParse(parsed);
|
|
@@ -340,43 +367,50 @@ function loadConfig() {
|
|
|
340
367
|
const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
|
|
341
368
|
if (agentIssue) {
|
|
342
369
|
throw new ConfigError(
|
|
343
|
-
`${
|
|
370
|
+
`${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
|
|
344
371
|
);
|
|
345
372
|
}
|
|
346
373
|
throw new ConfigError(
|
|
347
|
-
`${
|
|
374
|
+
`${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.`
|
|
348
375
|
);
|
|
349
376
|
}
|
|
350
377
|
return result.data;
|
|
351
378
|
}
|
|
352
379
|
function loadConfigTolerant() {
|
|
353
|
-
const
|
|
380
|
+
const path = configPath();
|
|
354
381
|
const empty = {};
|
|
355
|
-
|
|
382
|
+
const fresh = { local: empty, note: null, hadPriorConfig: false };
|
|
383
|
+
if (!existsSync(path)) return fresh;
|
|
356
384
|
let raw;
|
|
357
385
|
try {
|
|
358
|
-
raw = readFileSync(
|
|
386
|
+
raw = readFileSync(path, "utf8");
|
|
359
387
|
} catch {
|
|
360
|
-
return
|
|
388
|
+
return fresh;
|
|
361
389
|
}
|
|
362
|
-
if (raw.trim().length === 0) return
|
|
390
|
+
if (raw.trim().length === 0) return fresh;
|
|
363
391
|
let parsed;
|
|
364
392
|
try {
|
|
365
393
|
parsed = JSON.parse(raw);
|
|
366
394
|
} catch {
|
|
367
|
-
return {
|
|
395
|
+
return {
|
|
396
|
+
local: empty,
|
|
397
|
+
note: `${path} was unreadable (invalid JSON) and has been reset.`,
|
|
398
|
+
hadPriorConfig: true
|
|
399
|
+
};
|
|
368
400
|
}
|
|
369
401
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
370
402
|
if (strict.success) {
|
|
371
|
-
const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
|
|
403
|
+
const { agents: agents2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
|
|
372
404
|
return {
|
|
373
405
|
local: {
|
|
374
406
|
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
375
407
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
376
408
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
377
|
-
...logLevel !== void 0 ? { logLevel } : {}
|
|
409
|
+
...logLevel !== void 0 ? { logLevel } : {},
|
|
410
|
+
...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
|
|
378
411
|
},
|
|
379
|
-
note: null
|
|
412
|
+
note: null,
|
|
413
|
+
hadPriorConfig: true
|
|
380
414
|
};
|
|
381
415
|
}
|
|
382
416
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
@@ -388,26 +422,29 @@ function loadConfigTolerant() {
|
|
|
388
422
|
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
389
423
|
local.logLevel = obj.logLevel;
|
|
390
424
|
}
|
|
425
|
+
const claudeCode = companionConfigSchema.shape.claudeCode.safeParse(obj.claudeCode);
|
|
426
|
+
if (claudeCode.success && claudeCode.data !== void 0) local.claudeCode = claudeCode.data;
|
|
391
427
|
return {
|
|
392
428
|
local,
|
|
393
|
-
note: `the existing ${
|
|
429
|
+
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
430
|
+
hadPriorConfig: true
|
|
394
431
|
};
|
|
395
432
|
}
|
|
396
433
|
function saveConfig(cfg) {
|
|
397
|
-
const
|
|
398
|
-
mkdirSync(dirname(
|
|
434
|
+
const path = configPath();
|
|
435
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
399
436
|
try {
|
|
400
437
|
chmodSync(cabaneDir(), 448);
|
|
401
438
|
} catch {
|
|
402
439
|
}
|
|
403
|
-
const tmp = `${
|
|
440
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
404
441
|
try {
|
|
405
442
|
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
406
443
|
try {
|
|
407
444
|
chmodSync(tmp, 384);
|
|
408
445
|
} catch {
|
|
409
446
|
}
|
|
410
|
-
renameSync(tmp,
|
|
447
|
+
renameSync(tmp, path);
|
|
411
448
|
} catch (err) {
|
|
412
449
|
try {
|
|
413
450
|
rmSync(tmp, { force: true });
|
|
@@ -426,9 +463,9 @@ function requireConfig() {
|
|
|
426
463
|
return cfg;
|
|
427
464
|
}
|
|
428
465
|
function deleteConfig() {
|
|
429
|
-
const
|
|
430
|
-
if (existsSync(
|
|
431
|
-
writeFileSync(
|
|
466
|
+
const path = configPath();
|
|
467
|
+
if (existsSync(path)) {
|
|
468
|
+
writeFileSync(path, "", { mode: 384 });
|
|
432
469
|
}
|
|
433
470
|
}
|
|
434
471
|
|
|
@@ -462,8 +499,8 @@ function consoleMessageFormat(log, messageKey) {
|
|
|
462
499
|
var cached = null;
|
|
463
500
|
function getLogger() {
|
|
464
501
|
if (cached) return cached;
|
|
465
|
-
const
|
|
466
|
-
mkdirSync2(dirname2(
|
|
502
|
+
const path = companionLogPath();
|
|
503
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
467
504
|
const streams = [];
|
|
468
505
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
469
506
|
const consoleStream = pretty({
|
|
@@ -473,7 +510,7 @@ function getLogger() {
|
|
|
473
510
|
});
|
|
474
511
|
streams.push({ level: "info", stream: consoleStream });
|
|
475
512
|
}
|
|
476
|
-
streams.push({ level: "debug", stream: createWriteStream(
|
|
513
|
+
streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
|
|
477
514
|
cached = pino({ level: "debug" }, pino.multistream(streams));
|
|
478
515
|
return cached;
|
|
479
516
|
}
|
|
@@ -592,26 +629,43 @@ async function codexOnPath() {
|
|
|
592
629
|
]);
|
|
593
630
|
return version !== null;
|
|
594
631
|
}
|
|
595
|
-
async function
|
|
632
|
+
async function requireStartConfig(deps = {}) {
|
|
633
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
634
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
635
|
+
const save2 = deps.save ?? saveConfig;
|
|
636
|
+
const cfg = requireCfg();
|
|
637
|
+
const claudeOnPathResult = await probeClaude();
|
|
638
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
639
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
640
|
+
save2(migrated);
|
|
641
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
642
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
643
|
+
}
|
|
644
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
596
645
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
597
646
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
598
647
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
599
648
|
`));
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
...cfg
|
|
603
|
-
...
|
|
649
|
+
const connected = [
|
|
650
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
651
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
652
|
+
...cfg.opencode ? ["opencode"] : []
|
|
604
653
|
];
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
654
|
+
if (connected.length > 0) {
|
|
655
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
656
|
+
warn(
|
|
657
|
+
"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."
|
|
658
|
+
);
|
|
659
|
+
}
|
|
610
660
|
return;
|
|
611
661
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
662
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
663
|
+
const installed = [
|
|
664
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
665
|
+
...codexInstalled ? ["Codex"] : []
|
|
666
|
+
];
|
|
667
|
+
warn(
|
|
668
|
+
"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.")
|
|
615
669
|
);
|
|
616
670
|
}
|
|
617
671
|
|
|
@@ -634,9 +688,9 @@ function serialize(state) {
|
|
|
634
688
|
return JSON.stringify(state, null, 2) + "\n";
|
|
635
689
|
}
|
|
636
690
|
function writeRuntimeState(state) {
|
|
637
|
-
const
|
|
691
|
+
const path = runtimePath();
|
|
638
692
|
mkdirSync3(cabaneDir(), { recursive: true });
|
|
639
|
-
writeFileSync2(
|
|
693
|
+
writeFileSync2(path, serialize(state), "utf8");
|
|
640
694
|
}
|
|
641
695
|
function acquireRuntimeState(state) {
|
|
642
696
|
const live = readLiveRuntimeState();
|
|
@@ -656,15 +710,15 @@ function acquireRuntimeState(state) {
|
|
|
656
710
|
return { acquired: true };
|
|
657
711
|
}
|
|
658
712
|
function clearRuntimeState() {
|
|
659
|
-
const
|
|
660
|
-
if (existsSync2(
|
|
713
|
+
const path = runtimePath();
|
|
714
|
+
if (existsSync2(path)) rmSync2(path, { force: true });
|
|
661
715
|
}
|
|
662
716
|
function readLiveRuntimeState() {
|
|
663
|
-
const
|
|
664
|
-
if (!existsSync2(
|
|
717
|
+
const path = runtimePath();
|
|
718
|
+
if (!existsSync2(path)) return null;
|
|
665
719
|
let parsed;
|
|
666
720
|
try {
|
|
667
|
-
parsed = JSON.parse(readFileSync2(
|
|
721
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
668
722
|
} catch {
|
|
669
723
|
return null;
|
|
670
724
|
}
|
|
@@ -709,15 +763,28 @@ function trimSlash(s) {
|
|
|
709
763
|
var STARTUP_TIMEOUT_MS = 8e3;
|
|
710
764
|
var POLL_INTERVAL_MS = 150;
|
|
711
765
|
async function startDaemon(opts = {}, deps = {}) {
|
|
712
|
-
const ensureRuntime = deps.ensureRuntime ??
|
|
713
|
-
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
766
|
+
const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
|
|
714
767
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
715
768
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
716
769
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
717
770
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
718
771
|
const now = deps.now ?? (() => Date.now());
|
|
719
|
-
const cfg =
|
|
720
|
-
|
|
772
|
+
const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
|
|
773
|
+
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
774
|
+
...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
|
|
775
|
+
...deps.probeClaude ? { probeClaude: deps.probeClaude } : {},
|
|
776
|
+
...deps.save ? { save: deps.save } : {},
|
|
777
|
+
// The child won't log it (the block is written by then), and this process owns
|
|
778
|
+
// the user's terminal — so the one-time record is one line, here.
|
|
779
|
+
onMigrated: (_migrated, onPath) => {
|
|
780
|
+
if (onPath) {
|
|
781
|
+
process.stdout.write(
|
|
782
|
+
"Carried Claude Code over as a connected harness on this device \u2014 connectors are chosen now, not detected.\n"
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
await ensureRuntime(cfg, { probeClaude: async () => claudeOnPath2 });
|
|
721
788
|
const existing = readState();
|
|
722
789
|
if (existing) {
|
|
723
790
|
if (await verify(existing) !== "stale") {
|
|
@@ -795,11 +862,11 @@ function credentialsPath() {
|
|
|
795
862
|
}
|
|
796
863
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
797
864
|
function load() {
|
|
798
|
-
const
|
|
799
|
-
if (!existsSync3(
|
|
865
|
+
const path = credentialsPath();
|
|
866
|
+
if (!existsSync3(path)) return {};
|
|
800
867
|
let raw;
|
|
801
868
|
try {
|
|
802
|
-
raw = readFileSync3(
|
|
869
|
+
raw = readFileSync3(path, "utf8");
|
|
803
870
|
} catch {
|
|
804
871
|
return {};
|
|
805
872
|
}
|
|
@@ -812,20 +879,20 @@ function load() {
|
|
|
812
879
|
}
|
|
813
880
|
}
|
|
814
881
|
function save(map) {
|
|
815
|
-
const
|
|
816
|
-
mkdirSync5(dirname3(
|
|
882
|
+
const path = credentialsPath();
|
|
883
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
817
884
|
try {
|
|
818
885
|
chmodSync2(cabaneDir(), 448);
|
|
819
886
|
} catch {
|
|
820
887
|
}
|
|
821
|
-
const tmp = `${
|
|
888
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
822
889
|
try {
|
|
823
890
|
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
824
891
|
try {
|
|
825
892
|
chmodSync2(tmp, 384);
|
|
826
893
|
} catch {
|
|
827
894
|
}
|
|
828
|
-
renameSync2(tmp,
|
|
895
|
+
renameSync2(tmp, path);
|
|
829
896
|
} catch (err) {
|
|
830
897
|
try {
|
|
831
898
|
rmSync3(tmp, { force: true });
|
|
@@ -857,8 +924,8 @@ function pruneCredentials(keepAgentIds) {
|
|
|
857
924
|
return removed;
|
|
858
925
|
}
|
|
859
926
|
function clearCredentials() {
|
|
860
|
-
const
|
|
861
|
-
if (existsSync3(
|
|
927
|
+
const path = credentialsPath();
|
|
928
|
+
if (existsSync3(path)) writeFileSync3(path, "", { mode: 384 });
|
|
862
929
|
}
|
|
863
930
|
|
|
864
931
|
// src/commands/logout.ts
|
|
@@ -897,10 +964,10 @@ async function logout(opts = {}) {
|
|
|
897
964
|
function trimBase(baseUrl) {
|
|
898
965
|
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
899
966
|
}
|
|
900
|
-
async function postJson(baseUrl,
|
|
967
|
+
async function postJson(baseUrl, path, body) {
|
|
901
968
|
let res;
|
|
902
969
|
try {
|
|
903
|
-
res = await fetch(`${trimBase(baseUrl)}${
|
|
970
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
904
971
|
method: "POST",
|
|
905
972
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
906
973
|
body: JSON.stringify(body)
|
|
@@ -920,7 +987,7 @@ async function postJson(baseUrl, path3, body) {
|
|
|
920
987
|
}
|
|
921
988
|
}
|
|
922
989
|
if (res.status >= 400) {
|
|
923
|
-
if (res.status === 404 &&
|
|
990
|
+
if (res.status === 404 && path.endsWith("/code")) {
|
|
924
991
|
throw new CompanionError(
|
|
925
992
|
`no device-flow pairing endpoint at ${baseUrl}. Either that server predates \`cabane-companion pair\`, or it isn't a Cabane API origin \u2014 the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
|
|
926
993
|
);
|
|
@@ -992,8 +1059,16 @@ function resolvePairBaseUrl(server) {
|
|
|
992
1059
|
return raw;
|
|
993
1060
|
}
|
|
994
1061
|
function writePairedConfig(paired) {
|
|
995
|
-
const { local, note } = loadConfigTolerant();
|
|
1062
|
+
const { local, note, hadPriorConfig } = loadConfigTolerant();
|
|
996
1063
|
const config = {
|
|
1064
|
+
// CT1082: a device pairs connected to NOTHING — `claudeCode: { enabled: false }`
|
|
1065
|
+
// written explicitly, not left absent, because absence is exactly what marks a
|
|
1066
|
+
// pre-CT1082 config for the grandfathering migration. Omit it and a brand-new
|
|
1067
|
+
// device gets auto-connected on its first start by the behaviour this task
|
|
1068
|
+
// removes; stamp it on a RE-pair and an upgrading user's working device gets
|
|
1069
|
+
// disconnected instead. So it's written only when there was no config here
|
|
1070
|
+
// before. `local` spreads after, so a re-pair keeps the answer already given.
|
|
1071
|
+
...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
|
|
997
1072
|
...local,
|
|
998
1073
|
baseUrl: paired.baseUrl,
|
|
999
1074
|
deviceToken: paired.deviceToken,
|
|
@@ -1390,6 +1465,11 @@ function registerRoutes(app, deps) {
|
|
|
1390
1465
|
app.post("/api/harnesses/enable", async (c) => {
|
|
1391
1466
|
const body = await readJson(c);
|
|
1392
1467
|
const runtime = body.runtime;
|
|
1468
|
+
if (runtime === "claude-code") {
|
|
1469
|
+
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
1470
|
+
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
1471
|
+
return c.json({ ok: true, status: hub.statusJson() });
|
|
1472
|
+
}
|
|
1393
1473
|
if (runtime === "codex") {
|
|
1394
1474
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
1395
1475
|
if (!result.ok) return c.json({ error: result.error }, 400);
|
|
@@ -1458,12 +1538,12 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
1458
1538
|
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1459
1539
|
return Math.min(Math.floor(n), max);
|
|
1460
1540
|
}
|
|
1461
|
-
function tailFile(
|
|
1462
|
-
if (!existsSync4(
|
|
1541
|
+
function tailFile(path, lines) {
|
|
1542
|
+
if (!existsSync4(path)) return [];
|
|
1463
1543
|
const MAX_BYTES = 256 * 1024;
|
|
1464
1544
|
let fd;
|
|
1465
1545
|
try {
|
|
1466
|
-
fd = openSync3(
|
|
1546
|
+
fd = openSync3(path, "r");
|
|
1467
1547
|
const size = fstatSync(fd).size;
|
|
1468
1548
|
const start2 = Math.max(0, size - MAX_BYTES);
|
|
1469
1549
|
const len = size - start2;
|
|
@@ -1574,8 +1654,8 @@ var CabaneApi = class {
|
|
|
1574
1654
|
// One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
|
|
1575
1655
|
// rethrows transport errors (fetch reject) unchanged so the caller's retry
|
|
1576
1656
|
// logic can classify them.
|
|
1577
|
-
async attempt(method,
|
|
1578
|
-
const res = await fetch(`${this.base}${
|
|
1657
|
+
async attempt(method, path, body, signal) {
|
|
1658
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
1579
1659
|
method,
|
|
1580
1660
|
headers: {
|
|
1581
1661
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -1601,12 +1681,12 @@ var CabaneApi = class {
|
|
|
1601
1681
|
}
|
|
1602
1682
|
return parsed;
|
|
1603
1683
|
}
|
|
1604
|
-
async request(method,
|
|
1684
|
+
async request(method, path, body, opts = {}) {
|
|
1605
1685
|
const { signal, retry = false } = opts;
|
|
1606
1686
|
const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
|
|
1607
1687
|
for (let attempt = 1; ; attempt++) {
|
|
1608
1688
|
try {
|
|
1609
|
-
return await this.attempt(method,
|
|
1689
|
+
return await this.attempt(method, path, body, signal);
|
|
1610
1690
|
} catch (err) {
|
|
1611
1691
|
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1612
1692
|
await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
@@ -1632,9 +1712,9 @@ var CabaneApi = class {
|
|
|
1632
1712
|
// delivers it once the API returns. `(turnId, seq)` is the server's
|
|
1633
1713
|
// idempotency key, so a replay whose original POST's fate is unknown
|
|
1634
1714
|
// converges instead of duplicating.
|
|
1635
|
-
async durableCommit(kind,
|
|
1715
|
+
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1636
1716
|
try {
|
|
1637
|
-
await this.request("POST",
|
|
1717
|
+
await this.request("POST", path, body, {
|
|
1638
1718
|
retry: true,
|
|
1639
1719
|
...signal ? { signal } : {}
|
|
1640
1720
|
});
|
|
@@ -1643,7 +1723,7 @@ var CabaneApi = class {
|
|
|
1643
1723
|
if (!outbox) throw err;
|
|
1644
1724
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1645
1725
|
if (!isRetryable(err)) throw err;
|
|
1646
|
-
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path
|
|
1726
|
+
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
|
|
1647
1727
|
this.opts.log?.warn(
|
|
1648
1728
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1649
1729
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1795,12 +1875,12 @@ var CabaneApi = class {
|
|
|
1795
1875
|
// left best-effort: it's lower-stakes and self-heals on the next turn, so it
|
|
1796
1876
|
// stays a single-shot PATCH and is deliberately out of CT93's scope.
|
|
1797
1877
|
setActiveRun(workspaceId, conversationId, agentId, body) {
|
|
1798
|
-
const
|
|
1878
|
+
const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
|
|
1799
1879
|
const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
|
|
1800
1880
|
if (touchesFlag && this.opts.outbox) {
|
|
1801
|
-
return this.durableActiveRunWrite(
|
|
1881
|
+
return this.durableActiveRunWrite(path, conversationId, agentId, body);
|
|
1802
1882
|
}
|
|
1803
|
-
return this.request("PATCH",
|
|
1883
|
+
return this.request("PATCH", path, body);
|
|
1804
1884
|
}
|
|
1805
1885
|
// CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
|
|
1806
1886
|
// across the (conversation, agent) pair. Mirrors `durableCommit`, with two
|
|
@@ -1814,11 +1894,11 @@ var CabaneApi = class {
|
|
|
1814
1894
|
// later and clobber the state we just wrote (the cross-turn race: turn N's
|
|
1815
1895
|
// queued clear vs. turn N+1's live set). Combined with persist-overwrites-
|
|
1816
1896
|
// by-key, this is the full last-writer-wins guarantee.
|
|
1817
|
-
async durableActiveRunWrite(
|
|
1897
|
+
async durableActiveRunWrite(path, conversationId, agentId, body) {
|
|
1818
1898
|
const outbox = this.opts.outbox;
|
|
1819
1899
|
const key = activeRunOutboxKey(conversationId, agentId);
|
|
1820
1900
|
try {
|
|
1821
|
-
await this.request("PATCH",
|
|
1901
|
+
await this.request("PATCH", path, body, { retry: true });
|
|
1822
1902
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1823
1903
|
} catch (err) {
|
|
1824
1904
|
if (!outbox) throw err;
|
|
@@ -1831,7 +1911,7 @@ var CabaneApi = class {
|
|
|
1831
1911
|
turnId: key,
|
|
1832
1912
|
seq: ACTIVE_RUN_OUTBOX_SEQ,
|
|
1833
1913
|
method: "PATCH",
|
|
1834
|
-
path
|
|
1914
|
+
path,
|
|
1835
1915
|
body,
|
|
1836
1916
|
kind: "active-run"
|
|
1837
1917
|
});
|
|
@@ -1919,8 +1999,8 @@ var CabaneApi = class {
|
|
|
1919
1999
|
// shared resolver the in-app path uses. Omitting it (older call sites) returns
|
|
1920
2000
|
// the agent default — graceful degradation, no version coupling.
|
|
1921
2001
|
getAgentSelf(conversationId) {
|
|
1922
|
-
const
|
|
1923
|
-
return this.request("GET",
|
|
2002
|
+
const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
|
|
2003
|
+
return this.request("GET", path);
|
|
1924
2004
|
}
|
|
1925
2005
|
// The companion fetches the triggering message body by listing the
|
|
1926
2006
|
// conversation's messages and finding the one with `id === messageId`.
|
|
@@ -1975,8 +2055,8 @@ var DeviceApi = class {
|
|
|
1975
2055
|
get base() {
|
|
1976
2056
|
return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
1977
2057
|
}
|
|
1978
|
-
async request(method,
|
|
1979
|
-
const res = await fetch(`${this.base}${
|
|
2058
|
+
async request(method, path, body) {
|
|
2059
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
1980
2060
|
method,
|
|
1981
2061
|
headers: {
|
|
1982
2062
|
Authorization: `Bearer ${this.opts.deviceToken}`,
|
|
@@ -2030,15 +2110,15 @@ function pathFor(workspaceId) {
|
|
|
2030
2110
|
return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
2031
2111
|
}
|
|
2032
2112
|
function readCursor(workspaceId) {
|
|
2033
|
-
const
|
|
2034
|
-
if (!existsSync5(
|
|
2035
|
-
const raw = readFileSync4(
|
|
2113
|
+
const path = pathFor(workspaceId);
|
|
2114
|
+
if (!existsSync5(path)) return null;
|
|
2115
|
+
const raw = readFileSync4(path, "utf8").trim();
|
|
2036
2116
|
return raw.length > 0 ? raw : null;
|
|
2037
2117
|
}
|
|
2038
2118
|
function writeCursor(workspaceId, eventId) {
|
|
2039
|
-
const
|
|
2119
|
+
const path = pathFor(workspaceId);
|
|
2040
2120
|
mkdirSync6(join7(cabaneDir(), "cursors"), { recursive: true });
|
|
2041
|
-
writeFileSync4(
|
|
2121
|
+
writeFileSync4(path, eventId + "\n", "utf8");
|
|
2042
2122
|
}
|
|
2043
2123
|
|
|
2044
2124
|
// src/cursor-tracker.ts
|
|
@@ -2091,10 +2171,10 @@ function pathFor2(log, workspaceId) {
|
|
|
2091
2171
|
return join8(dir(log), encodeURIComponent(workspaceId));
|
|
2092
2172
|
}
|
|
2093
2173
|
function readIds(log, workspaceId) {
|
|
2094
|
-
const
|
|
2095
|
-
if (!existsSync6(
|
|
2174
|
+
const path = pathFor2(log, workspaceId);
|
|
2175
|
+
if (!existsSync6(path)) return [];
|
|
2096
2176
|
try {
|
|
2097
|
-
return readFileSync5(
|
|
2177
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2098
2178
|
} catch {
|
|
2099
2179
|
return [];
|
|
2100
2180
|
}
|
|
@@ -2131,10 +2211,10 @@ function resumePathFor(workspaceId) {
|
|
|
2131
2211
|
}
|
|
2132
2212
|
function readResumeCounts(workspaceId) {
|
|
2133
2213
|
const out = /* @__PURE__ */ new Map();
|
|
2134
|
-
const
|
|
2135
|
-
if (!existsSync6(
|
|
2214
|
+
const path = resumePathFor(workspaceId);
|
|
2215
|
+
if (!existsSync6(path)) return out;
|
|
2136
2216
|
try {
|
|
2137
|
-
for (const line of readFileSync5(
|
|
2217
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
2138
2218
|
const trimmed = line.trim();
|
|
2139
2219
|
if (!trimmed) continue;
|
|
2140
2220
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -5072,536 +5152,10 @@ function isStringRecord2(v) {
|
|
|
5072
5152
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
5073
5153
|
}
|
|
5074
5154
|
|
|
5075
|
-
// node_modules/.pnpm/@openai+codex-sdk@0.147.0/node_modules/@openai/codex-sdk/dist/index.js
|
|
5076
|
-
import { promises as fs } from "fs";
|
|
5077
|
-
import os from "os";
|
|
5078
|
-
import path from "path";
|
|
5079
|
-
import { spawn as spawn6 } from "child_process";
|
|
5080
|
-
import { statSync } from "fs";
|
|
5081
|
-
import path2 from "path";
|
|
5082
|
-
import readline from "readline";
|
|
5083
|
-
import { createRequire } from "module";
|
|
5084
|
-
async function createOutputSchemaFile(schema) {
|
|
5085
|
-
if (schema === void 0) {
|
|
5086
|
-
return { cleanup: async () => {
|
|
5087
|
-
} };
|
|
5088
|
-
}
|
|
5089
|
-
if (!isJsonObject(schema)) {
|
|
5090
|
-
throw new Error("outputSchema must be a plain JSON object");
|
|
5091
|
-
}
|
|
5092
|
-
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
|
5093
|
-
const schemaPath = path.join(schemaDir, "schema.json");
|
|
5094
|
-
const cleanup = async () => {
|
|
5095
|
-
try {
|
|
5096
|
-
await fs.rm(schemaDir, { recursive: true, force: true });
|
|
5097
|
-
} catch {
|
|
5098
|
-
}
|
|
5099
|
-
};
|
|
5100
|
-
try {
|
|
5101
|
-
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
|
5102
|
-
return { schemaPath, cleanup };
|
|
5103
|
-
} catch (error) {
|
|
5104
|
-
await cleanup();
|
|
5105
|
-
throw error;
|
|
5106
|
-
}
|
|
5107
|
-
}
|
|
5108
|
-
function isJsonObject(value) {
|
|
5109
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5110
|
-
}
|
|
5111
|
-
var Thread = class {
|
|
5112
|
-
_exec;
|
|
5113
|
-
_options;
|
|
5114
|
-
_id;
|
|
5115
|
-
_threadOptions;
|
|
5116
|
-
/** Returns the ID of the thread. Populated after the first turn starts. */
|
|
5117
|
-
get id() {
|
|
5118
|
-
return this._id;
|
|
5119
|
-
}
|
|
5120
|
-
/* @internal */
|
|
5121
|
-
constructor(exec, options, threadOptions, id = null) {
|
|
5122
|
-
this._exec = exec;
|
|
5123
|
-
this._options = options;
|
|
5124
|
-
this._id = id;
|
|
5125
|
-
this._threadOptions = threadOptions;
|
|
5126
|
-
}
|
|
5127
|
-
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
|
5128
|
-
async runStreamed(input, turnOptions = {}) {
|
|
5129
|
-
return { events: this.runStreamedInternal(input, turnOptions) };
|
|
5130
|
-
}
|
|
5131
|
-
async *runStreamedInternal(input, turnOptions = {}) {
|
|
5132
|
-
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
|
|
5133
|
-
const options = this._threadOptions;
|
|
5134
|
-
const { prompt, images } = normalizeInput(input);
|
|
5135
|
-
const generator = this._exec.run({
|
|
5136
|
-
input: prompt,
|
|
5137
|
-
baseUrl: this._options.baseUrl,
|
|
5138
|
-
apiKey: this._options.apiKey,
|
|
5139
|
-
threadId: this._id,
|
|
5140
|
-
images,
|
|
5141
|
-
model: options?.model,
|
|
5142
|
-
sandboxMode: options?.sandboxMode,
|
|
5143
|
-
workingDirectory: options?.workingDirectory,
|
|
5144
|
-
skipGitRepoCheck: options?.skipGitRepoCheck,
|
|
5145
|
-
outputSchemaFile: schemaPath,
|
|
5146
|
-
modelReasoningEffort: options?.modelReasoningEffort,
|
|
5147
|
-
signal: turnOptions.signal,
|
|
5148
|
-
networkAccessEnabled: options?.networkAccessEnabled,
|
|
5149
|
-
webSearchMode: options?.webSearchMode,
|
|
5150
|
-
webSearchEnabled: options?.webSearchEnabled,
|
|
5151
|
-
approvalPolicy: options?.approvalPolicy,
|
|
5152
|
-
additionalDirectories: options?.additionalDirectories
|
|
5153
|
-
});
|
|
5154
|
-
try {
|
|
5155
|
-
for await (const item of generator) {
|
|
5156
|
-
let parsed;
|
|
5157
|
-
try {
|
|
5158
|
-
parsed = JSON.parse(item);
|
|
5159
|
-
} catch (error) {
|
|
5160
|
-
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
|
5161
|
-
}
|
|
5162
|
-
if (parsed.type === "thread.started") {
|
|
5163
|
-
this._id = parsed.thread_id;
|
|
5164
|
-
} else if (parsed.type === "turn.completed") {
|
|
5165
|
-
parsed.usage.cache_write_input_tokens ??= 0;
|
|
5166
|
-
}
|
|
5167
|
-
yield parsed;
|
|
5168
|
-
}
|
|
5169
|
-
} finally {
|
|
5170
|
-
await cleanup();
|
|
5171
|
-
}
|
|
5172
|
-
}
|
|
5173
|
-
/** Provides the input to the agent and returns the completed turn. */
|
|
5174
|
-
async run(input, turnOptions = {}) {
|
|
5175
|
-
const generator = this.runStreamedInternal(input, turnOptions);
|
|
5176
|
-
const items = [];
|
|
5177
|
-
let finalResponse = "";
|
|
5178
|
-
let usage = null;
|
|
5179
|
-
let turnFailure = null;
|
|
5180
|
-
for await (const event of generator) {
|
|
5181
|
-
if (event.type === "item.completed") {
|
|
5182
|
-
if (event.item.type === "agent_message") {
|
|
5183
|
-
finalResponse = event.item.text;
|
|
5184
|
-
}
|
|
5185
|
-
items.push(event.item);
|
|
5186
|
-
} else if (event.type === "turn.completed") {
|
|
5187
|
-
usage = event.usage;
|
|
5188
|
-
} else if (event.type === "turn.failed") {
|
|
5189
|
-
turnFailure = event.error;
|
|
5190
|
-
break;
|
|
5191
|
-
}
|
|
5192
|
-
}
|
|
5193
|
-
if (turnFailure) {
|
|
5194
|
-
throw new Error(turnFailure.message);
|
|
5195
|
-
}
|
|
5196
|
-
return { items, finalResponse, usage };
|
|
5197
|
-
}
|
|
5198
|
-
};
|
|
5199
|
-
function normalizeInput(input) {
|
|
5200
|
-
if (typeof input === "string") {
|
|
5201
|
-
return { prompt: input, images: [] };
|
|
5202
|
-
}
|
|
5203
|
-
const promptParts = [];
|
|
5204
|
-
const images = [];
|
|
5205
|
-
for (const item of input) {
|
|
5206
|
-
if (item.type === "text") {
|
|
5207
|
-
promptParts.push(item.text);
|
|
5208
|
-
} else if (item.type === "local_image") {
|
|
5209
|
-
images.push(item.path);
|
|
5210
|
-
}
|
|
5211
|
-
}
|
|
5212
|
-
return { prompt: promptParts.join("\n\n"), images };
|
|
5213
|
-
}
|
|
5214
|
-
var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
|
5215
|
-
var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
|
|
5216
|
-
var CODEX_NPM_NAME = "@openai/codex";
|
|
5217
|
-
var PLATFORM_PACKAGE_BY_TARGET = {
|
|
5218
|
-
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
|
5219
|
-
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
|
|
5220
|
-
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
|
|
5221
|
-
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
|
|
5222
|
-
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
|
|
5223
|
-
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
|
|
5224
|
-
};
|
|
5225
|
-
var moduleRequire = createRequire(import.meta.url);
|
|
5226
|
-
var CodexExec = class {
|
|
5227
|
-
executablePath;
|
|
5228
|
-
pathDirs;
|
|
5229
|
-
envOverride;
|
|
5230
|
-
configOverrides;
|
|
5231
|
-
constructor(executablePath = null, env, configOverrides) {
|
|
5232
|
-
if (executablePath) {
|
|
5233
|
-
this.executablePath = executablePath;
|
|
5234
|
-
this.pathDirs = [];
|
|
5235
|
-
} else {
|
|
5236
|
-
const resolved = findCodexPath();
|
|
5237
|
-
this.executablePath = resolved.executablePath;
|
|
5238
|
-
this.pathDirs = resolved.pathDirs;
|
|
5239
|
-
}
|
|
5240
|
-
this.envOverride = env;
|
|
5241
|
-
this.configOverrides = configOverrides;
|
|
5242
|
-
}
|
|
5243
|
-
async *run(args) {
|
|
5244
|
-
const commandArgs = ["exec", "--experimental-json"];
|
|
5245
|
-
if (this.configOverrides) {
|
|
5246
|
-
for (const override of serializeConfigOverrides(this.configOverrides)) {
|
|
5247
|
-
commandArgs.push("--config", override);
|
|
5248
|
-
}
|
|
5249
|
-
}
|
|
5250
|
-
if (args.baseUrl) {
|
|
5251
|
-
commandArgs.push(
|
|
5252
|
-
"--config",
|
|
5253
|
-
`openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
|
|
5254
|
-
);
|
|
5255
|
-
}
|
|
5256
|
-
if (args.model) {
|
|
5257
|
-
commandArgs.push("--model", args.model);
|
|
5258
|
-
}
|
|
5259
|
-
if (args.sandboxMode) {
|
|
5260
|
-
commandArgs.push("--sandbox", args.sandboxMode);
|
|
5261
|
-
}
|
|
5262
|
-
if (args.workingDirectory) {
|
|
5263
|
-
commandArgs.push("--cd", args.workingDirectory);
|
|
5264
|
-
}
|
|
5265
|
-
if (args.additionalDirectories?.length) {
|
|
5266
|
-
for (const dir2 of args.additionalDirectories) {
|
|
5267
|
-
commandArgs.push("--add-dir", dir2);
|
|
5268
|
-
}
|
|
5269
|
-
}
|
|
5270
|
-
if (args.skipGitRepoCheck) {
|
|
5271
|
-
commandArgs.push("--skip-git-repo-check");
|
|
5272
|
-
}
|
|
5273
|
-
if (args.outputSchemaFile) {
|
|
5274
|
-
commandArgs.push("--output-schema", args.outputSchemaFile);
|
|
5275
|
-
}
|
|
5276
|
-
if (args.modelReasoningEffort) {
|
|
5277
|
-
commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
|
|
5278
|
-
}
|
|
5279
|
-
if (args.networkAccessEnabled !== void 0) {
|
|
5280
|
-
commandArgs.push(
|
|
5281
|
-
"--config",
|
|
5282
|
-
`sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
|
|
5283
|
-
);
|
|
5284
|
-
}
|
|
5285
|
-
if (args.webSearchMode) {
|
|
5286
|
-
commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
|
|
5287
|
-
} else if (args.webSearchEnabled === true) {
|
|
5288
|
-
commandArgs.push("--config", `web_search="live"`);
|
|
5289
|
-
} else if (args.webSearchEnabled === false) {
|
|
5290
|
-
commandArgs.push("--config", `web_search="disabled"`);
|
|
5291
|
-
}
|
|
5292
|
-
if (args.approvalPolicy) {
|
|
5293
|
-
commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
|
|
5294
|
-
}
|
|
5295
|
-
if (args.threadId) {
|
|
5296
|
-
commandArgs.push("resume", args.threadId);
|
|
5297
|
-
}
|
|
5298
|
-
if (args.images?.length) {
|
|
5299
|
-
for (const image of args.images) {
|
|
5300
|
-
commandArgs.push("--image", image);
|
|
5301
|
-
}
|
|
5302
|
-
}
|
|
5303
|
-
const env = {};
|
|
5304
|
-
if (this.envOverride) {
|
|
5305
|
-
Object.assign(env, this.envOverride);
|
|
5306
|
-
} else {
|
|
5307
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
5308
|
-
if (value !== void 0) {
|
|
5309
|
-
env[key] = value;
|
|
5310
|
-
}
|
|
5311
|
-
}
|
|
5312
|
-
}
|
|
5313
|
-
if (!env[INTERNAL_ORIGINATOR_ENV]) {
|
|
5314
|
-
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
|
|
5315
|
-
}
|
|
5316
|
-
if (args.apiKey) {
|
|
5317
|
-
env.CODEX_API_KEY = args.apiKey;
|
|
5318
|
-
}
|
|
5319
|
-
if (this.pathDirs.length > 0) {
|
|
5320
|
-
prependPathDirs(env, this.pathDirs);
|
|
5321
|
-
}
|
|
5322
|
-
const child = spawn6(this.executablePath, commandArgs, {
|
|
5323
|
-
env,
|
|
5324
|
-
signal: args.signal
|
|
5325
|
-
});
|
|
5326
|
-
let spawnError = null;
|
|
5327
|
-
child.once("error", (err) => spawnError = err);
|
|
5328
|
-
if (!child.stdin) {
|
|
5329
|
-
child.kill();
|
|
5330
|
-
throw new Error("Child process has no stdin");
|
|
5331
|
-
}
|
|
5332
|
-
child.stdin.write(args.input);
|
|
5333
|
-
child.stdin.end();
|
|
5334
|
-
if (!child.stdout) {
|
|
5335
|
-
child.kill();
|
|
5336
|
-
throw new Error("Child process has no stdout");
|
|
5337
|
-
}
|
|
5338
|
-
const stderrChunks = [];
|
|
5339
|
-
if (child.stderr) {
|
|
5340
|
-
child.stderr.on("data", (data) => {
|
|
5341
|
-
stderrChunks.push(data);
|
|
5342
|
-
});
|
|
5343
|
-
}
|
|
5344
|
-
const exitPromise = new Promise(
|
|
5345
|
-
(resolve) => {
|
|
5346
|
-
child.once("exit", (code, signal) => {
|
|
5347
|
-
resolve({ code, signal });
|
|
5348
|
-
});
|
|
5349
|
-
}
|
|
5350
|
-
);
|
|
5351
|
-
const rl = readline.createInterface({
|
|
5352
|
-
input: child.stdout,
|
|
5353
|
-
crlfDelay: Infinity
|
|
5354
|
-
});
|
|
5355
|
-
try {
|
|
5356
|
-
for await (const line of rl) {
|
|
5357
|
-
yield line;
|
|
5358
|
-
}
|
|
5359
|
-
if (spawnError) throw spawnError;
|
|
5360
|
-
const { code, signal } = await exitPromise;
|
|
5361
|
-
if (code !== 0 || signal) {
|
|
5362
|
-
const stderrBuffer = Buffer.concat(stderrChunks);
|
|
5363
|
-
const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
|
|
5364
|
-
throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
|
|
5365
|
-
}
|
|
5366
|
-
} finally {
|
|
5367
|
-
rl.close();
|
|
5368
|
-
child.removeAllListeners();
|
|
5369
|
-
try {
|
|
5370
|
-
if (!child.killed) child.kill();
|
|
5371
|
-
} catch {
|
|
5372
|
-
}
|
|
5373
|
-
}
|
|
5374
|
-
}
|
|
5375
|
-
};
|
|
5376
|
-
function serializeConfigOverrides(configOverrides) {
|
|
5377
|
-
const overrides = [];
|
|
5378
|
-
flattenConfigOverrides(configOverrides, "", overrides);
|
|
5379
|
-
return overrides;
|
|
5380
|
-
}
|
|
5381
|
-
function flattenConfigOverrides(value, prefix, overrides) {
|
|
5382
|
-
if (!isPlainObject(value)) {
|
|
5383
|
-
if (prefix) {
|
|
5384
|
-
overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
|
|
5385
|
-
return;
|
|
5386
|
-
} else {
|
|
5387
|
-
throw new Error("Codex config overrides must be a plain object");
|
|
5388
|
-
}
|
|
5389
|
-
}
|
|
5390
|
-
const entries = Object.entries(value);
|
|
5391
|
-
if (!prefix && entries.length === 0) {
|
|
5392
|
-
return;
|
|
5393
|
-
}
|
|
5394
|
-
if (prefix && entries.length === 0) {
|
|
5395
|
-
overrides.push(`${prefix}={}`);
|
|
5396
|
-
return;
|
|
5397
|
-
}
|
|
5398
|
-
for (const [key, child] of entries) {
|
|
5399
|
-
if (!key) {
|
|
5400
|
-
throw new Error("Codex config override keys must be non-empty strings");
|
|
5401
|
-
}
|
|
5402
|
-
if (child === void 0) {
|
|
5403
|
-
continue;
|
|
5404
|
-
}
|
|
5405
|
-
const path3 = prefix ? `${prefix}.${key}` : key;
|
|
5406
|
-
if (isPlainObject(child)) {
|
|
5407
|
-
flattenConfigOverrides(child, path3, overrides);
|
|
5408
|
-
} else {
|
|
5409
|
-
overrides.push(`${path3}=${toTomlValue(child, path3)}`);
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
}
|
|
5413
|
-
function toTomlValue(value, path3) {
|
|
5414
|
-
if (typeof value === "string") {
|
|
5415
|
-
return JSON.stringify(value);
|
|
5416
|
-
} else if (typeof value === "number") {
|
|
5417
|
-
if (!Number.isFinite(value)) {
|
|
5418
|
-
throw new Error(`Codex config override at ${path3} must be a finite number`);
|
|
5419
|
-
}
|
|
5420
|
-
return `${value}`;
|
|
5421
|
-
} else if (typeof value === "boolean") {
|
|
5422
|
-
return value ? "true" : "false";
|
|
5423
|
-
} else if (Array.isArray(value)) {
|
|
5424
|
-
const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
|
|
5425
|
-
return `[${rendered.join(", ")}]`;
|
|
5426
|
-
} else if (isPlainObject(value)) {
|
|
5427
|
-
const parts = [];
|
|
5428
|
-
for (const [key, child] of Object.entries(value)) {
|
|
5429
|
-
if (!key) {
|
|
5430
|
-
throw new Error("Codex config override keys must be non-empty strings");
|
|
5431
|
-
}
|
|
5432
|
-
if (child === void 0) {
|
|
5433
|
-
continue;
|
|
5434
|
-
}
|
|
5435
|
-
parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
|
|
5436
|
-
}
|
|
5437
|
-
return `{${parts.join(", ")}}`;
|
|
5438
|
-
} else if (value === null) {
|
|
5439
|
-
throw new Error(`Codex config override at ${path3} cannot be null`);
|
|
5440
|
-
} else {
|
|
5441
|
-
const typeName = typeof value;
|
|
5442
|
-
throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
|
|
5443
|
-
}
|
|
5444
|
-
}
|
|
5445
|
-
var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
5446
|
-
function formatTomlKey(key) {
|
|
5447
|
-
return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
|
|
5448
|
-
}
|
|
5449
|
-
function isPlainObject(value) {
|
|
5450
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5451
|
-
}
|
|
5452
|
-
function findCodexPath() {
|
|
5453
|
-
const { platform: platform2, arch } = process;
|
|
5454
|
-
let targetTriple = null;
|
|
5455
|
-
switch (platform2) {
|
|
5456
|
-
case "linux":
|
|
5457
|
-
case "android":
|
|
5458
|
-
switch (arch) {
|
|
5459
|
-
case "x64":
|
|
5460
|
-
targetTriple = "x86_64-unknown-linux-musl";
|
|
5461
|
-
break;
|
|
5462
|
-
case "arm64":
|
|
5463
|
-
targetTriple = "aarch64-unknown-linux-musl";
|
|
5464
|
-
break;
|
|
5465
|
-
default:
|
|
5466
|
-
break;
|
|
5467
|
-
}
|
|
5468
|
-
break;
|
|
5469
|
-
case "darwin":
|
|
5470
|
-
switch (arch) {
|
|
5471
|
-
case "x64":
|
|
5472
|
-
targetTriple = "x86_64-apple-darwin";
|
|
5473
|
-
break;
|
|
5474
|
-
case "arm64":
|
|
5475
|
-
targetTriple = "aarch64-apple-darwin";
|
|
5476
|
-
break;
|
|
5477
|
-
default:
|
|
5478
|
-
break;
|
|
5479
|
-
}
|
|
5480
|
-
break;
|
|
5481
|
-
case "win32":
|
|
5482
|
-
switch (arch) {
|
|
5483
|
-
case "x64":
|
|
5484
|
-
targetTriple = "x86_64-pc-windows-msvc";
|
|
5485
|
-
break;
|
|
5486
|
-
case "arm64":
|
|
5487
|
-
targetTriple = "aarch64-pc-windows-msvc";
|
|
5488
|
-
break;
|
|
5489
|
-
default:
|
|
5490
|
-
break;
|
|
5491
|
-
}
|
|
5492
|
-
break;
|
|
5493
|
-
default:
|
|
5494
|
-
break;
|
|
5495
|
-
}
|
|
5496
|
-
if (!targetTriple) {
|
|
5497
|
-
throw new Error(`Unsupported platform: ${platform2} (${arch})`);
|
|
5498
|
-
}
|
|
5499
|
-
const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
|
|
5500
|
-
if (!platformPackage) {
|
|
5501
|
-
throw new Error(`Unsupported target triple: ${targetTriple}`);
|
|
5502
|
-
}
|
|
5503
|
-
let vendorRoot;
|
|
5504
|
-
try {
|
|
5505
|
-
const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
|
|
5506
|
-
const codexRequire = createRequire(codexPackageJsonPath);
|
|
5507
|
-
const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
|
|
5508
|
-
vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
|
|
5509
|
-
} catch {
|
|
5510
|
-
throw new Error(
|
|
5511
|
-
`Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
|
|
5512
|
-
);
|
|
5513
|
-
}
|
|
5514
|
-
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
|
|
5515
|
-
const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
|
|
5516
|
-
if (!nativePackage) {
|
|
5517
|
-
throw new Error(
|
|
5518
|
-
`Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
|
|
5519
|
-
);
|
|
5520
|
-
}
|
|
5521
|
-
return nativePackage;
|
|
5522
|
-
}
|
|
5523
|
-
function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
|
|
5524
|
-
const packageRoot = path2.join(vendorRoot, targetTriple);
|
|
5525
|
-
const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
|
|
5526
|
-
if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
|
|
5527
|
-
return {
|
|
5528
|
-
executablePath: packageBinaryPath,
|
|
5529
|
-
pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
|
|
5530
|
-
};
|
|
5531
|
-
}
|
|
5532
|
-
const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
|
|
5533
|
-
if (isFile(legacyBinaryPath)) {
|
|
5534
|
-
return {
|
|
5535
|
-
executablePath: legacyBinaryPath,
|
|
5536
|
-
pathDirs: existingDirs(path2.join(packageRoot, "path"))
|
|
5537
|
-
};
|
|
5538
|
-
}
|
|
5539
|
-
return null;
|
|
5540
|
-
}
|
|
5541
|
-
function existingDirs(...dirs) {
|
|
5542
|
-
return dirs.filter(isDirectory);
|
|
5543
|
-
}
|
|
5544
|
-
function prependPathDirs(env, pathDirs, platform2 = process.platform) {
|
|
5545
|
-
const pathKey = pathEnvKey(env, platform2);
|
|
5546
|
-
if (platform2 === "win32") {
|
|
5547
|
-
for (const key of Object.keys(env)) {
|
|
5548
|
-
if (key.toLowerCase() === "path" && key !== pathKey) {
|
|
5549
|
-
delete env[key];
|
|
5550
|
-
}
|
|
5551
|
-
}
|
|
5552
|
-
}
|
|
5553
|
-
const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
|
|
5554
|
-
env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
|
|
5555
|
-
}
|
|
5556
|
-
function pathEnvKey(env, platform2) {
|
|
5557
|
-
if (platform2 !== "win32") {
|
|
5558
|
-
return "PATH";
|
|
5559
|
-
}
|
|
5560
|
-
const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
|
|
5561
|
-
return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
|
|
5562
|
-
}
|
|
5563
|
-
function isFile(filePath) {
|
|
5564
|
-
try {
|
|
5565
|
-
return statSync(filePath).isFile();
|
|
5566
|
-
} catch {
|
|
5567
|
-
return false;
|
|
5568
|
-
}
|
|
5569
|
-
}
|
|
5570
|
-
function isDirectory(filePath) {
|
|
5571
|
-
try {
|
|
5572
|
-
return statSync(filePath).isDirectory();
|
|
5573
|
-
} catch {
|
|
5574
|
-
return false;
|
|
5575
|
-
}
|
|
5576
|
-
}
|
|
5577
|
-
var Codex = class {
|
|
5578
|
-
exec;
|
|
5579
|
-
options;
|
|
5580
|
-
constructor(options = {}) {
|
|
5581
|
-
const { codexPathOverride, env, config } = options;
|
|
5582
|
-
this.exec = new CodexExec(codexPathOverride, env, config);
|
|
5583
|
-
this.options = options;
|
|
5584
|
-
}
|
|
5585
|
-
/**
|
|
5586
|
-
* Starts a new conversation with an agent.
|
|
5587
|
-
* @returns A new thread instance.
|
|
5588
|
-
*/
|
|
5589
|
-
startThread(options = {}) {
|
|
5590
|
-
return new Thread(this.exec, this.options, options);
|
|
5591
|
-
}
|
|
5592
|
-
/**
|
|
5593
|
-
* Resumes a conversation with an agent based on the thread id.
|
|
5594
|
-
* Threads are persisted in ~/.codex/sessions.
|
|
5595
|
-
*
|
|
5596
|
-
* @param id The id of the thread to resume.
|
|
5597
|
-
* @returns A new thread instance.
|
|
5598
|
-
*/
|
|
5599
|
-
resumeThread(id, options = {}) {
|
|
5600
|
-
return new Thread(this.exec, this.options, options, id);
|
|
5601
|
-
}
|
|
5602
|
-
};
|
|
5603
|
-
|
|
5604
5155
|
// packages/agent-runtime/src/codex/transport.ts
|
|
5156
|
+
import {
|
|
5157
|
+
Codex
|
|
5158
|
+
} from "@openai/codex-sdk";
|
|
5605
5159
|
function buildSdkThreadOptions(spec) {
|
|
5606
5160
|
return {
|
|
5607
5161
|
...spec.model ? { model: spec.model } : {},
|
|
@@ -6393,7 +5947,7 @@ var ConnectorHealthStore = class {
|
|
|
6393
5947
|
|
|
6394
5948
|
// src/dispatcher.ts
|
|
6395
5949
|
import { randomUUID } from "crypto";
|
|
6396
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync
|
|
5950
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync } from "fs";
|
|
6397
5951
|
import { join as join13 } from "path";
|
|
6398
5952
|
|
|
6399
5953
|
// src/summon.ts
|
|
@@ -6731,10 +6285,10 @@ import { join as join9 } from "path";
|
|
|
6731
6285
|
var PREFIX = "cabane-codex-instructions-";
|
|
6732
6286
|
async function writeCodexInstructionsFile(contents) {
|
|
6733
6287
|
const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
|
|
6734
|
-
const
|
|
6735
|
-
await writeFile(
|
|
6288
|
+
const path = join9(dir2, "instructions.md");
|
|
6289
|
+
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
6736
6290
|
return {
|
|
6737
|
-
path
|
|
6291
|
+
path,
|
|
6738
6292
|
cleanup: async () => {
|
|
6739
6293
|
await rm(dir2, { recursive: true, force: true });
|
|
6740
6294
|
}
|
|
@@ -6754,10 +6308,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
|
|
|
6754
6308
|
return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
6755
6309
|
}
|
|
6756
6310
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6757
|
-
const
|
|
6758
|
-
if (!existsSync7(
|
|
6311
|
+
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
6312
|
+
if (!existsSync7(path)) return null;
|
|
6759
6313
|
try {
|
|
6760
|
-
const parsed = JSON.parse(readFileSync6(
|
|
6314
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
6761
6315
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
6762
6316
|
return {
|
|
6763
6317
|
cwd: parsed.cwd,
|
|
@@ -6791,14 +6345,14 @@ function secretsPath() {
|
|
|
6791
6345
|
}
|
|
6792
6346
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
6793
6347
|
function loadSecretStore() {
|
|
6794
|
-
const
|
|
6795
|
-
if (!existsSync8(
|
|
6348
|
+
const path = secretsPath();
|
|
6349
|
+
if (!existsSync8(path)) return makeStore({});
|
|
6796
6350
|
let raw;
|
|
6797
6351
|
try {
|
|
6798
|
-
raw = readFileSync7(
|
|
6352
|
+
raw = readFileSync7(path, "utf8");
|
|
6799
6353
|
} catch (err) {
|
|
6800
6354
|
throw new ConfigError(
|
|
6801
|
-
`couldn't read ${
|
|
6355
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
6802
6356
|
);
|
|
6803
6357
|
}
|
|
6804
6358
|
if (raw.trim().length === 0) return makeStore({});
|
|
@@ -6807,13 +6361,13 @@ function loadSecretStore() {
|
|
|
6807
6361
|
parsed = JSON.parse(raw);
|
|
6808
6362
|
} catch (err) {
|
|
6809
6363
|
throw new ConfigError(
|
|
6810
|
-
`${
|
|
6364
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
6811
6365
|
);
|
|
6812
6366
|
}
|
|
6813
6367
|
const result = secretStoreSchema.safeParse(parsed);
|
|
6814
6368
|
if (!result.success) {
|
|
6815
6369
|
throw new ConfigError(
|
|
6816
|
-
`${
|
|
6370
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
6817
6371
|
);
|
|
6818
6372
|
}
|
|
6819
6373
|
return makeStore(result.data);
|
|
@@ -7249,7 +6803,7 @@ function checkoutState(cwd) {
|
|
|
7249
6803
|
if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
|
|
7250
6804
|
let stat;
|
|
7251
6805
|
try {
|
|
7252
|
-
stat =
|
|
6806
|
+
stat = statSync(gitPath);
|
|
7253
6807
|
} catch (error) {
|
|
7254
6808
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
7255
6809
|
}
|
|
@@ -8197,7 +7751,9 @@ var LABELS = {
|
|
|
8197
7751
|
function deriveHarnessSnapshot(signals) {
|
|
8198
7752
|
const advertised = new Set(
|
|
8199
7753
|
buildCompanionManifest({
|
|
8200
|
-
|
|
7754
|
+
// CT1082: connected AND installed — the manifest's own rule, restated here
|
|
7755
|
+
// through the same function rather than re-decided.
|
|
7756
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeOnPath,
|
|
8201
7757
|
opencode: signals.opencodeConfigured,
|
|
8202
7758
|
codex: signals.codexEnabled
|
|
8203
7759
|
}).runtimes.map((r) => r.name)
|
|
@@ -8210,20 +7766,40 @@ function deriveHarnessSnapshot(signals) {
|
|
|
8210
7766
|
return { harnesses, anyExposed: harnesses.some((h) => h.state === "exposed") };
|
|
8211
7767
|
}
|
|
8212
7768
|
function deriveClaudeCode(signals, manifestHas) {
|
|
8213
|
-
const base = { runtime: "claude-code", label: LABELS["claude-code"]
|
|
7769
|
+
const base = { runtime: "claude-code", label: LABELS["claude-code"] };
|
|
8214
7770
|
if (manifestHas) {
|
|
8215
7771
|
return {
|
|
8216
7772
|
...base,
|
|
8217
7773
|
state: "exposed",
|
|
8218
7774
|
version: signals.claudeVersion,
|
|
8219
|
-
detail: "Claude Code is
|
|
7775
|
+
detail: "Claude Code is connected and exposed to Cabane.",
|
|
7776
|
+
enable: null
|
|
7777
|
+
};
|
|
7778
|
+
}
|
|
7779
|
+
if (signals.claudeCodeConnected) {
|
|
7780
|
+
return {
|
|
7781
|
+
...base,
|
|
7782
|
+
state: "needs_attention",
|
|
7783
|
+
version: null,
|
|
7784
|
+
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.",
|
|
7785
|
+
enable: null
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
if (signals.claudeOnPath) {
|
|
7789
|
+
return {
|
|
7790
|
+
...base,
|
|
7791
|
+
state: "detected_not_exposed",
|
|
7792
|
+
version: signals.claudeVersion,
|
|
7793
|
+
detail: "Claude Code is installed here but not connected yet. Connect it to let Cabane run Claude Code on this device.",
|
|
7794
|
+
enable: "claude-code"
|
|
8220
7795
|
};
|
|
8221
7796
|
}
|
|
8222
7797
|
return {
|
|
8223
7798
|
...base,
|
|
8224
7799
|
state: "not_detected",
|
|
8225
7800
|
version: null,
|
|
8226
|
-
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then it
|
|
7801
|
+
detail: "Not detected. Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and sign in with `claude`, then connect it here.",
|
|
7802
|
+
enable: null
|
|
8227
7803
|
};
|
|
8228
7804
|
}
|
|
8229
7805
|
function deriveCodex(signals, manifestHas) {
|
|
@@ -8291,6 +7867,9 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
8291
7867
|
enable: "opencode"
|
|
8292
7868
|
};
|
|
8293
7869
|
}
|
|
7870
|
+
function detectedRuntimesFor(snapshot) {
|
|
7871
|
+
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
7872
|
+
}
|
|
8294
7873
|
var PROBE_TIMEOUT_MS2 = 4e3;
|
|
8295
7874
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
8296
7875
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -8307,6 +7886,9 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
8307
7886
|
return {
|
|
8308
7887
|
claudeOnPath: claudeOnPathResult,
|
|
8309
7888
|
claudeVersion,
|
|
7889
|
+
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
7890
|
+
// the manifest gate and the probe above is only a suggestion.
|
|
7891
|
+
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
8310
7892
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
8311
7893
|
// exposes codex; its config flag is the manifest gate either way).
|
|
8312
7894
|
codexOnPath: codexVersion !== null,
|
|
@@ -8680,8 +8262,8 @@ function sleep3(ms) {
|
|
|
8680
8262
|
}
|
|
8681
8263
|
|
|
8682
8264
|
// src/version.ts
|
|
8683
|
-
import { createRequire
|
|
8684
|
-
var pkg =
|
|
8265
|
+
import { createRequire } from "module";
|
|
8266
|
+
var pkg = createRequire(import.meta.url)("../package.json");
|
|
8685
8267
|
var COMPANION_VERSION = pkg.version;
|
|
8686
8268
|
|
|
8687
8269
|
// src/supervisor.ts
|
|
@@ -8715,6 +8297,10 @@ var CompanionSupervisor = class {
|
|
|
8715
8297
|
// can never disagree with what the manifest advertises. Null until the first
|
|
8716
8298
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
8717
8299
|
harnessSignals = null;
|
|
8300
|
+
// CT1082: the fresh PATH probe the claude-code connect vets with. Deliberately
|
|
8301
|
+
// NOT the cached beat signal — someone connecting right after installing Claude
|
|
8302
|
+
// Code shouldn't be refused by a snapshot up to a heartbeat old.
|
|
8303
|
+
probeClaudePresence;
|
|
8718
8304
|
exitFn;
|
|
8719
8305
|
reexecFn;
|
|
8720
8306
|
dispatcherFactory;
|
|
@@ -8745,6 +8331,7 @@ var CompanionSupervisor = class {
|
|
|
8745
8331
|
this.log = opts.log;
|
|
8746
8332
|
this.hub = opts.hub;
|
|
8747
8333
|
this.claudeCode = opts.claudeCode ?? true;
|
|
8334
|
+
this.probeClaudePresence = opts.probeClaudePresence ?? claudeOnPath;
|
|
8748
8335
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
8749
8336
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
8750
8337
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -8807,11 +8394,10 @@ var CompanionSupervisor = class {
|
|
|
8807
8394
|
// claude-code when `claude` is on PATH, CT270 opencode when the operator
|
|
8808
8395
|
// configured an `opencode serve`.
|
|
8809
8396
|
manifest: buildCompanionManifest({
|
|
8810
|
-
//
|
|
8811
|
-
// until the first
|
|
8812
|
-
//
|
|
8813
|
-
|
|
8814
|
-
claudeCode: this.claudeCodePresent(),
|
|
8397
|
+
// CT1082: connected AND installed. Presence is the live re-probe (CT586),
|
|
8398
|
+
// falling back to the boot probe until the first one lands; consent is the
|
|
8399
|
+
// user's `claudeCode.enabled`. Claude Code no longer rides presence alone.
|
|
8400
|
+
claudeCode: this.claudeCodeOffered(),
|
|
8815
8401
|
opencode: !!this.config.opencode?.serverUrl,
|
|
8816
8402
|
// CT481: advertise codex when the operator enabled it (config-gated,
|
|
8817
8403
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
@@ -8829,7 +8415,13 @@ var CompanionSupervisor = class {
|
|
|
8829
8415
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
8830
8416
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
8831
8417
|
// leaves this device's stored availability untouched.
|
|
8832
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8418
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
8419
|
+
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8420
|
+
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8421
|
+
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8422
|
+
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8423
|
+
// the same fail-soft contract `models` keeps.
|
|
8424
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8833
8425
|
});
|
|
8834
8426
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8835
8427
|
this.deviceId = res.deviceId;
|
|
@@ -9034,13 +8626,19 @@ var CompanionSupervisor = class {
|
|
|
9034
8626
|
"companion: stopped agent (unassigned)"
|
|
9035
8627
|
);
|
|
9036
8628
|
}
|
|
9037
|
-
// CT833: is Claude Code on this machine right now?
|
|
9038
|
-
//
|
|
9039
|
-
//
|
|
9040
|
-
// per-beat re-probe), falling back to the boot probe until the first one lands.
|
|
8629
|
+
// CT833: is Claude Code on this machine right now? Live (the per-beat re-probe),
|
|
8630
|
+
// falling back to the boot probe until the first one lands. Presence ONLY —
|
|
8631
|
+
// CT1082 split presence from exposure, so nothing routes on this directly.
|
|
9041
8632
|
claudeCodePresent() {
|
|
9042
8633
|
return this.harnessSignals?.claudeOnPath ?? this.claudeCode;
|
|
9043
8634
|
}
|
|
8635
|
+
// CT1082: does this device OFFER claude-code — connected by its user and actually
|
|
8636
|
+
// installed? The ONE signal both the heartbeat manifest and the dispatcher's
|
|
8637
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
8638
|
+
// can't disagree (the CT833 invariant, now with consent in front of it).
|
|
8639
|
+
claudeCodeOffered() {
|
|
8640
|
+
return isClaudeCodeConnected(this.config) && this.claudeCodePresent();
|
|
8641
|
+
}
|
|
9044
8642
|
buildDispatcher(ctx) {
|
|
9045
8643
|
if (this.dispatcherFactory) return this.dispatcherFactory(ctx);
|
|
9046
8644
|
return new Dispatcher({
|
|
@@ -9058,10 +8656,12 @@ var CompanionSupervisor = class {
|
|
|
9058
8656
|
}),
|
|
9059
8657
|
runConfig: ctx.runConfig,
|
|
9060
8658
|
log: this.log,
|
|
9061
|
-
// CT833: register the claude-code adapter only when
|
|
9062
|
-
//
|
|
9063
|
-
// after boot works on the next turn exactly as it
|
|
9064
|
-
|
|
8659
|
+
// CT833: register the claude-code adapter only when this device actually
|
|
8660
|
+
// offers claude-code — read per turn (not captured here), so a harness
|
|
8661
|
+
// installed or connected after boot works on the next turn exactly as it
|
|
8662
|
+
// appears on the next beat. CT1082: "offers" now means connected as well as
|
|
8663
|
+
// installed, so a disconnected harness can't be selected either.
|
|
8664
|
+
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
9065
8665
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
9066
8666
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
9067
8667
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
@@ -9367,9 +8967,12 @@ var CompanionSupervisor = class {
|
|
|
9367
8967
|
async recheckHarnesses() {
|
|
9368
8968
|
await this.refreshHarnessStatuses();
|
|
9369
8969
|
}
|
|
9370
|
-
// Friendly enable for the
|
|
9371
|
-
//
|
|
9372
|
-
//
|
|
8970
|
+
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
8971
|
+
// `~/.cabane/config.json`, no hand-edited JSON. This is enable/expose ONLY: it
|
|
8972
|
+
// never installs a binary and never drives a login (BYO — Decided).
|
|
8973
|
+
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
8974
|
+
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
8975
|
+
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
9373
8976
|
// - codex: set `codex.enabled` (the CLI + `codex login` remain the user's).
|
|
9374
8977
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
9375
8978
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
@@ -9379,7 +8982,15 @@ var CompanionSupervisor = class {
|
|
|
9379
8982
|
// reachable/valid input).
|
|
9380
8983
|
async enableHarness(input) {
|
|
9381
8984
|
let next;
|
|
9382
|
-
if (input.runtime === "
|
|
8985
|
+
if (input.runtime === "claude-code") {
|
|
8986
|
+
if (!await this.probeClaudePresence()) {
|
|
8987
|
+
return {
|
|
8988
|
+
ok: false,
|
|
8989
|
+
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."
|
|
8990
|
+
};
|
|
8991
|
+
}
|
|
8992
|
+
next = { ...this.config, claudeCode: { enabled: true } };
|
|
8993
|
+
} else if (input.runtime === "codex") {
|
|
9383
8994
|
next = { ...this.config, codex: { enabled: true } };
|
|
9384
8995
|
} else {
|
|
9385
8996
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -9485,9 +9096,9 @@ var CompanionSupervisor = class {
|
|
|
9485
9096
|
};
|
|
9486
9097
|
function defaultReexec() {
|
|
9487
9098
|
clearRuntimeState();
|
|
9488
|
-
void import("child_process").then(({ spawn:
|
|
9099
|
+
void import("child_process").then(({ spawn: spawn6 }) => {
|
|
9489
9100
|
try {
|
|
9490
|
-
const child =
|
|
9101
|
+
const child = spawn6(process.execPath, process.argv.slice(1), {
|
|
9491
9102
|
stdio: "inherit",
|
|
9492
9103
|
detached: false
|
|
9493
9104
|
});
|
|
@@ -9571,8 +9182,8 @@ function recordCrash(rec2) {
|
|
|
9571
9182
|
}
|
|
9572
9183
|
function clearCrash() {
|
|
9573
9184
|
try {
|
|
9574
|
-
const
|
|
9575
|
-
if (existsSync11(
|
|
9185
|
+
const path = crashMarkerPath();
|
|
9186
|
+
if (existsSync11(path)) rmSync7(path, { force: true });
|
|
9576
9187
|
} catch {
|
|
9577
9188
|
}
|
|
9578
9189
|
}
|
|
@@ -9581,13 +9192,17 @@ function clearCrash() {
|
|
|
9581
9192
|
async function createCompanionRuntime(opts = {}) {
|
|
9582
9193
|
const log = getLogger();
|
|
9583
9194
|
installProcessSafetyNet(log);
|
|
9584
|
-
const probeClaude = opts.probeClaude ?? claudeOnPath;
|
|
9585
9195
|
let cfg;
|
|
9586
9196
|
let claudeCode;
|
|
9587
9197
|
try {
|
|
9588
|
-
cfg =
|
|
9589
|
-
|
|
9590
|
-
|
|
9198
|
+
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
9199
|
+
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
9200
|
+
onMigrated: (migrated, onPath) => log.info(
|
|
9201
|
+
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
9202
|
+
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)"
|
|
9203
|
+
)
|
|
9204
|
+
}));
|
|
9205
|
+
await warnAboutHarnessReadiness(cfg, { probeClaude: async () => claudeCode });
|
|
9591
9206
|
} catch (err) {
|
|
9592
9207
|
recordCrash({
|
|
9593
9208
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -9599,7 +9214,9 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9599
9214
|
}
|
|
9600
9215
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
9601
9216
|
const harnessVersions = await probeHarnessVersions({
|
|
9602
|
-
|
|
9217
|
+
// CT1082: versions are probed for the runtimes this device OFFERS, and
|
|
9218
|
+
// claude-code is offered only when connected as well as installed.
|
|
9219
|
+
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
9603
9220
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
9604
9221
|
codex: isCodexEnabled(cfg)
|
|
9605
9222
|
});
|
|
@@ -10071,11 +9688,11 @@ function printList(dir2) {
|
|
|
10071
9688
|
"render one: cabane-companion transcript <file> (or `--last` for the newest)\n"
|
|
10072
9689
|
);
|
|
10073
9690
|
}
|
|
10074
|
-
function peek(
|
|
9691
|
+
function peek(path) {
|
|
10075
9692
|
let meta;
|
|
10076
9693
|
let outcome;
|
|
10077
9694
|
try {
|
|
10078
|
-
for (const line of readFileSync10(
|
|
9695
|
+
for (const line of readFileSync10(path, "utf8").split("\n")) {
|
|
10079
9696
|
if (!line.trim()) continue;
|
|
10080
9697
|
const o = safeParse(line);
|
|
10081
9698
|
const t = str2(rec(o)?.type);
|
|
@@ -10105,13 +9722,13 @@ function resolveTarget(dir2, target) {
|
|
|
10105
9722
|
` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
|
|
10106
9723
|
);
|
|
10107
9724
|
}
|
|
10108
|
-
function renderFile(
|
|
9725
|
+
function renderFile(path) {
|
|
10109
9726
|
let content;
|
|
10110
9727
|
try {
|
|
10111
|
-
content = readFileSync10(
|
|
9728
|
+
content = readFileSync10(path, "utf8");
|
|
10112
9729
|
} catch (err) {
|
|
10113
9730
|
throw new CompanionError(
|
|
10114
|
-
`couldn't read ${
|
|
9731
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
10115
9732
|
);
|
|
10116
9733
|
}
|
|
10117
9734
|
return renderTranscript(content.split("\n"));
|