@cabane/companion 0.6.49 → 0.6.51
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 +251 -190
- package/dist/runtime.js +1142 -1091
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command, Option } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/control-socket.ts
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
|
-
import { existsSync as existsSync2, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
8
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
9
9
|
import { createServer, connect } from "net";
|
|
10
10
|
import { join as join2 } from "path";
|
|
11
11
|
|
|
@@ -495,13 +495,18 @@ function deleteConfig() {
|
|
|
495
495
|
|
|
496
496
|
// src/control-socket.ts
|
|
497
497
|
var CONTROL_TIMEOUT_MS = 1e3;
|
|
498
|
+
var MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
498
499
|
function controlSocketPath() {
|
|
499
500
|
const dir2 = cabaneDir();
|
|
501
|
+
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
500
502
|
if (process.platform === "win32") {
|
|
501
|
-
const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
|
|
502
503
|
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
503
504
|
}
|
|
504
|
-
|
|
505
|
+
const besideRuntime = join2(dir2, "companion.sock");
|
|
506
|
+
if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
|
|
507
|
+
const filename = `cabane-companion-${key}.sock`;
|
|
508
|
+
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync2(process.env.XDG_RUNTIME_DIR) ? join2(process.env.XDG_RUNTIME_DIR, filename) : null;
|
|
509
|
+
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join2("/tmp", filename);
|
|
505
510
|
}
|
|
506
511
|
async function startControlServer(handlers) {
|
|
507
512
|
const path = controlSocketPath();
|
|
@@ -522,6 +527,18 @@ async function startControlServer(handlers) {
|
|
|
522
527
|
resolve();
|
|
523
528
|
});
|
|
524
529
|
});
|
|
530
|
+
if (process.platform !== "win32") {
|
|
531
|
+
try {
|
|
532
|
+
chmodSync2(path, 384);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
535
|
+
try {
|
|
536
|
+
rmSync2(path, { force: true });
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
525
542
|
server.on("error", () => {
|
|
526
543
|
});
|
|
527
544
|
return {
|
|
@@ -644,40 +661,58 @@ async function ping(path) {
|
|
|
644
661
|
}
|
|
645
662
|
|
|
646
663
|
// src/harness-check.ts
|
|
647
|
-
import { spawn as
|
|
664
|
+
import { spawn as spawn3 } from "child_process";
|
|
648
665
|
|
|
649
666
|
// src/harness-versions.ts
|
|
650
667
|
import { spawn as spawn2 } from "child_process";
|
|
651
668
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
669
|
+
var CLI_PROBE_TIMEOUT_MS = 4e3;
|
|
652
670
|
function parseVersionToken(raw) {
|
|
653
671
|
if (!raw) return null;
|
|
654
672
|
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
655
673
|
return m ? m[0] : null;
|
|
656
674
|
}
|
|
657
|
-
|
|
675
|
+
function probeCliPresence(command, spawnImpl = spawn2) {
|
|
658
676
|
return new Promise((resolve) => {
|
|
659
677
|
let settled = false;
|
|
660
|
-
|
|
678
|
+
let timer = null;
|
|
679
|
+
const done = (result) => {
|
|
661
680
|
if (!settled) {
|
|
662
681
|
settled = true;
|
|
663
|
-
|
|
682
|
+
if (timer) clearTimeout(timer);
|
|
683
|
+
resolve(result);
|
|
664
684
|
}
|
|
665
685
|
};
|
|
666
686
|
let child;
|
|
667
687
|
try {
|
|
668
688
|
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
669
689
|
} catch {
|
|
670
|
-
done(
|
|
690
|
+
done({ status: "absent" });
|
|
671
691
|
return;
|
|
672
692
|
}
|
|
673
693
|
let out = "";
|
|
674
694
|
child.stdout?.on("data", (chunk) => {
|
|
675
695
|
if (out.length < 4096) out += chunk.toString();
|
|
676
696
|
});
|
|
677
|
-
|
|
678
|
-
|
|
697
|
+
timer = setTimeout(() => {
|
|
698
|
+
child.kill?.("SIGKILL");
|
|
699
|
+
done({ status: "unusable" });
|
|
700
|
+
}, CLI_PROBE_TIMEOUT_MS);
|
|
701
|
+
timer.unref?.();
|
|
702
|
+
child.once("error", () => done({ status: "absent" }));
|
|
703
|
+
child.once("exit", (code) => {
|
|
704
|
+
const version = code === 0 ? parseVersionToken(out) : null;
|
|
705
|
+
done(version ? { status: "present", version } : { status: "unusable" });
|
|
706
|
+
});
|
|
679
707
|
});
|
|
680
708
|
}
|
|
709
|
+
function probeHarnessPresence(runtime, spawnImpl = spawn2) {
|
|
710
|
+
return probeCliPresence(runtime === "codex" ? "codex" : "claude", spawnImpl);
|
|
711
|
+
}
|
|
712
|
+
async function probeCliVersion(command, spawnImpl = spawn2) {
|
|
713
|
+
const result = await probeCliPresence(command, spawnImpl);
|
|
714
|
+
return result.status === "present" ? result.version : null;
|
|
715
|
+
}
|
|
681
716
|
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
682
717
|
try {
|
|
683
718
|
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
@@ -741,81 +776,6 @@ function buildCompanionManifest(opts) {
|
|
|
741
776
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
742
777
|
}
|
|
743
778
|
|
|
744
|
-
// src/prereqs.ts
|
|
745
|
-
import { spawn as spawn3 } from "child_process";
|
|
746
|
-
async function claudeOnPath() {
|
|
747
|
-
return new Promise((resolve) => {
|
|
748
|
-
let settled = false;
|
|
749
|
-
const child = spawn3("claude", ["--version"], { stdio: "ignore" });
|
|
750
|
-
child.once("error", () => {
|
|
751
|
-
if (!settled) {
|
|
752
|
-
settled = true;
|
|
753
|
-
resolve(false);
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
child.once("exit", (code) => {
|
|
757
|
-
if (!settled) {
|
|
758
|
-
settled = true;
|
|
759
|
-
resolve(code === 0);
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
var CODEX_PROBE_TIMEOUT_MS = 4e3;
|
|
765
|
-
async function codexOnPath() {
|
|
766
|
-
const version = await Promise.race([
|
|
767
|
-
probeCliVersion("codex"),
|
|
768
|
-
new Promise((resolve) => {
|
|
769
|
-
const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
|
|
770
|
-
timer.unref?.();
|
|
771
|
-
})
|
|
772
|
-
]);
|
|
773
|
-
return version !== null;
|
|
774
|
-
}
|
|
775
|
-
async function requireStartConfig(deps = {}) {
|
|
776
|
-
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
777
|
-
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
778
|
-
const save2 = deps.save ?? saveConfig;
|
|
779
|
-
const cfg = requireCfg();
|
|
780
|
-
const claudeOnPathResult = await probeClaude();
|
|
781
|
-
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
782
|
-
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
783
|
-
save2(migrated);
|
|
784
|
-
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
785
|
-
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
786
|
-
}
|
|
787
|
-
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
788
|
-
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
789
|
-
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
790
|
-
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
791
|
-
`));
|
|
792
|
-
const connected = [
|
|
793
|
-
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
794
|
-
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
795
|
-
...cfg.opencode ? ["opencode"] : []
|
|
796
|
-
];
|
|
797
|
-
if (connected.length > 0) {
|
|
798
|
-
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
799
|
-
warn(
|
|
800
|
-
"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."
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
806
|
-
const installed = [
|
|
807
|
-
...claudeInstalled ? ["Claude Code"] : [],
|
|
808
|
-
...codexInstalled ? ["Codex"] : []
|
|
809
|
-
];
|
|
810
|
-
const connectCommands = [
|
|
811
|
-
...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
|
|
812
|
-
...codexInstalled ? ["`cabane-companion connect codex`"] : []
|
|
813
|
-
];
|
|
814
|
-
warn(
|
|
815
|
-
"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 with " : "it with "}${connectCommands.join(" or ")} 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 with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
779
|
// src/harness-status.ts
|
|
820
780
|
var HARNESS_LABELS = {
|
|
821
781
|
"claude-code": "Claude Code",
|
|
@@ -976,20 +936,19 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
|
|
|
976
936
|
return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
|
|
977
937
|
}
|
|
978
938
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
979
|
-
const
|
|
980
|
-
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
981
|
-
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
939
|
+
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
982
940
|
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
983
941
|
const configuredServerUrl = cfg.opencode?.serverUrl;
|
|
984
942
|
const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
|
|
985
|
-
const [
|
|
986
|
-
withTimeout(
|
|
987
|
-
withTimeout(
|
|
988
|
-
withTimeout(probeCodexVersion(), null),
|
|
943
|
+
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
944
|
+
deps.presence?.["claude-code"] ?? withTimeout(probePresence("claude-code"), { status: "absent" }),
|
|
945
|
+
deps.presence?.codex ?? withTimeout(probePresence("codex"), { status: "absent" }),
|
|
989
946
|
configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
|
|
990
947
|
]);
|
|
948
|
+
const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
|
|
949
|
+
const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
|
|
991
950
|
return {
|
|
992
|
-
claudeOnPath:
|
|
951
|
+
claudeOnPath: claudePresence.status === "present",
|
|
993
952
|
claudeVersion,
|
|
994
953
|
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
995
954
|
// the manifest gate and the probe above is only a suggestion.
|
|
@@ -1041,16 +1000,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1041
1000
|
if (!url) return "absent";
|
|
1042
1001
|
return await probeOpencode(url) !== null ? "ok" : "failed";
|
|
1043
1002
|
}
|
|
1044
|
-
const { auth
|
|
1045
|
-
auth: ["codex", ["login", "status"]]
|
|
1046
|
-
presence: ["codex", ["--version"]]
|
|
1003
|
+
const { auth } = runtime === "codex" ? {
|
|
1004
|
+
auth: ["codex", ["login", "status"]]
|
|
1047
1005
|
} : {
|
|
1048
|
-
auth: ["claude", ["auth", "status"]]
|
|
1049
|
-
presence: ["claude", ["--version"]]
|
|
1006
|
+
auth: ["claude", ["auth", "status"]]
|
|
1050
1007
|
};
|
|
1051
|
-
const
|
|
1052
|
-
if (
|
|
1053
|
-
if (presenceRun.code !== 0) return "unverified";
|
|
1008
|
+
const presence = deps.presence ?? await (deps.probePresence ?? probeHarnessPresence)(runtime);
|
|
1009
|
+
if (presence.status !== "present") return presence.status;
|
|
1054
1010
|
const authRun = await run(auth[0], [...auth[1]]);
|
|
1055
1011
|
if (authRun.code === 0) return "ok";
|
|
1056
1012
|
if (looksUnsupported(authRun.output)) {
|
|
@@ -1062,7 +1018,9 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1062
1018
|
}
|
|
1063
1019
|
}
|
|
1064
1020
|
function connectedLine(runtime, verdict) {
|
|
1065
|
-
if (verdict === "absent"
|
|
1021
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
1022
|
+
throw new Error("an unavailable harness cannot be connected");
|
|
1023
|
+
}
|
|
1066
1024
|
const label = HARNESS_LABELS[runtime];
|
|
1067
1025
|
if (verdict !== "failed") return `${label} connected.`;
|
|
1068
1026
|
return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
|
|
@@ -1072,13 +1030,18 @@ var FAILED_SUFFIX = {
|
|
|
1072
1030
|
codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
|
|
1073
1031
|
opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
|
|
1074
1032
|
};
|
|
1075
|
-
function
|
|
1033
|
+
function presenceReason(runtime, status2) {
|
|
1076
1034
|
if (runtime === "opencode") {
|
|
1077
1035
|
return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
|
|
1078
1036
|
}
|
|
1079
1037
|
const label = HARNESS_LABELS[runtime];
|
|
1080
|
-
|
|
1081
|
-
|
|
1038
|
+
return status2 === "absent" ? `${label} isn't installed on this machine.` : `${label} is installed here but didn't report a version Cabane can read.`;
|
|
1039
|
+
}
|
|
1040
|
+
var CLI_PRESENCE_ACTION = "Install it and run this again.";
|
|
1041
|
+
var DASHBOARD_PRESENCE_ACTION = "Install it, then refresh.";
|
|
1042
|
+
function presenceRefusal(runtime, status2, action) {
|
|
1043
|
+
const reason = presenceReason(runtime, status2);
|
|
1044
|
+
return runtime === "opencode" ? reason : `${reason} ${action}`;
|
|
1082
1045
|
}
|
|
1083
1046
|
function looksUnsupported(output) {
|
|
1084
1047
|
return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
|
|
@@ -1096,7 +1059,7 @@ function runBounded(command, args) {
|
|
|
1096
1059
|
};
|
|
1097
1060
|
let child;
|
|
1098
1061
|
try {
|
|
1099
|
-
child =
|
|
1062
|
+
child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1100
1063
|
} catch {
|
|
1101
1064
|
resolve({ code: null, output: "", error: "spawn" });
|
|
1102
1065
|
return;
|
|
@@ -1299,18 +1262,31 @@ async function confirm(question) {
|
|
|
1299
1262
|
}
|
|
1300
1263
|
|
|
1301
1264
|
// src/commands/connect.ts
|
|
1302
|
-
async function connect2(raw, opts = {}) {
|
|
1265
|
+
async function connect2(raw, opts = {}, deps = {}) {
|
|
1303
1266
|
const runtime = parseHarnessRuntime(raw);
|
|
1304
1267
|
if (!runtime) {
|
|
1305
1268
|
throw new CompanionError(
|
|
1306
1269
|
`unknown harness "${raw}". Connectable harnesses are: claude-code, codex, opencode.`
|
|
1307
1270
|
);
|
|
1308
1271
|
}
|
|
1309
|
-
const
|
|
1272
|
+
const loadConfig2 = deps.loadConfig ?? requireConfig;
|
|
1273
|
+
const request = deps.request ?? controlRequest;
|
|
1274
|
+
const save2 = deps.save ?? saveConfig;
|
|
1275
|
+
const output = deps.output ?? write;
|
|
1276
|
+
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
1277
|
+
const shakeOut = deps.shakeOut ?? shakeOutHarness;
|
|
1278
|
+
const resolveServerUrl = deps.resolveServerUrl ?? resolveOpencodeServerUrl;
|
|
1279
|
+
const probeOpencode = deps.probeOpencode ?? probeOpencodeVersion;
|
|
1280
|
+
const cfg = loadConfig2();
|
|
1281
|
+
if (!opts.serverUrl?.trim() && alreadyConnected(runtime, cfg)) {
|
|
1282
|
+
output(`${INDENT}${HARNESS_LABELS[runtime]} is already connected on this device.`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const live = await (deps.findLiveSocket ?? liveSocket)();
|
|
1310
1286
|
if (live) {
|
|
1311
1287
|
let result;
|
|
1312
1288
|
try {
|
|
1313
|
-
result = await
|
|
1289
|
+
result = await request(
|
|
1314
1290
|
live,
|
|
1315
1291
|
{
|
|
1316
1292
|
cmd: "connect",
|
|
@@ -1326,42 +1302,38 @@ async function connect2(raw, opts = {}) {
|
|
|
1326
1302
|
);
|
|
1327
1303
|
}
|
|
1328
1304
|
if (!result.ok) throw new CompanionError(result.message);
|
|
1329
|
-
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
const cfg = requireConfig();
|
|
1333
|
-
if (alreadyConnected(runtime, cfg)) {
|
|
1334
|
-
write(`${INDENT}${HARNESS_LABELS[runtime]} is already connected on this device.`);
|
|
1305
|
+
output(INDENT + tick(result.message));
|
|
1335
1306
|
return;
|
|
1336
1307
|
}
|
|
1337
|
-
if (runtime === "claude-code" && !await claudeOnPath()) {
|
|
1338
|
-
throw new CompanionError(
|
|
1339
|
-
"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."
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
1308
|
let next = cfg;
|
|
1309
|
+
const presence = runtime === "opencode" ? void 0 : await probePresence(runtime);
|
|
1310
|
+
if (presence && presence.status !== "present") {
|
|
1311
|
+
throw new CompanionError(presenceRefusal(runtime, presence.status, CLI_PRESENCE_ACTION));
|
|
1312
|
+
}
|
|
1343
1313
|
if (runtime === "claude-code") next = { ...cfg, claudeCode: { enabled: true } };
|
|
1344
1314
|
else if (runtime === "codex") next = { ...cfg, codex: { enabled: true } };
|
|
1345
1315
|
else {
|
|
1346
1316
|
const requestedServerUrl = opts.serverUrl?.trim();
|
|
1347
|
-
const serverUrl = await
|
|
1317
|
+
const serverUrl = await resolveServerUrl(cfg.opencode?.serverUrl, requestedServerUrl);
|
|
1348
1318
|
if (!serverUrl) {
|
|
1349
1319
|
throw new CompanionError(
|
|
1350
1320
|
"opencode is addressed by URL \u2014 pass it: `cabane-companion connect opencode --url http://127.0.0.1:4096`."
|
|
1351
1321
|
);
|
|
1352
1322
|
}
|
|
1353
|
-
if (requestedServerUrl && await
|
|
1323
|
+
if (requestedServerUrl && await probeOpencode(serverUrl) === null) {
|
|
1354
1324
|
throw new CompanionError(
|
|
1355
1325
|
`Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
|
|
1356
1326
|
);
|
|
1357
1327
|
}
|
|
1358
1328
|
next = { ...cfg, opencode: { serverUrl } };
|
|
1359
1329
|
}
|
|
1360
|
-
const verdict = await
|
|
1361
|
-
if (verdict === "absent"
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1330
|
+
const verdict = await shakeOut(runtime, next, { ...presence ? { presence } : {} });
|
|
1331
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
1332
|
+
throw new CompanionError(presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION));
|
|
1333
|
+
}
|
|
1334
|
+
save2(next);
|
|
1335
|
+
output(INDENT + tick(connectedLine(runtime, verdict)));
|
|
1336
|
+
output(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
|
|
1365
1337
|
}
|
|
1366
1338
|
async function liveSocket() {
|
|
1367
1339
|
const state = readLiveRuntimeState();
|
|
@@ -1393,7 +1365,7 @@ import { confirm as confirm2 } from "@inquirer/prompts";
|
|
|
1393
1365
|
|
|
1394
1366
|
// src/credentials.ts
|
|
1395
1367
|
import {
|
|
1396
|
-
chmodSync as
|
|
1368
|
+
chmodSync as chmodSync3,
|
|
1397
1369
|
existsSync as existsSync4,
|
|
1398
1370
|
mkdirSync as mkdirSync4,
|
|
1399
1371
|
readFileSync as readFileSync3,
|
|
@@ -1428,14 +1400,14 @@ function save(map) {
|
|
|
1428
1400
|
const path = credentialsPath();
|
|
1429
1401
|
mkdirSync4(dirname2(path), { recursive: true });
|
|
1430
1402
|
try {
|
|
1431
|
-
|
|
1403
|
+
chmodSync3(cabaneDir(), 448);
|
|
1432
1404
|
} catch {
|
|
1433
1405
|
}
|
|
1434
1406
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
1435
1407
|
try {
|
|
1436
1408
|
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
1437
1409
|
try {
|
|
1438
|
-
|
|
1410
|
+
chmodSync3(tmp, 384);
|
|
1439
1411
|
} catch {
|
|
1440
1412
|
}
|
|
1441
1413
|
renameSync2(tmp, path);
|
|
@@ -2082,12 +2054,18 @@ function registerRoutes(app, deps) {
|
|
|
2082
2054
|
const runtime = body.runtime;
|
|
2083
2055
|
if (runtime === "claude-code") {
|
|
2084
2056
|
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
2085
|
-
if (!result.ok)
|
|
2057
|
+
if (!result.ok) {
|
|
2058
|
+
const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
|
|
2059
|
+
return c.json({ error }, 400);
|
|
2060
|
+
}
|
|
2086
2061
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
2087
2062
|
}
|
|
2088
2063
|
if (runtime === "codex") {
|
|
2089
2064
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
2090
|
-
if (!result.ok)
|
|
2065
|
+
if (!result.ok) {
|
|
2066
|
+
const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
|
|
2067
|
+
return c.json({ error }, 400);
|
|
2068
|
+
}
|
|
2091
2069
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
2092
2070
|
}
|
|
2093
2071
|
if (runtime === "opencode") {
|
|
@@ -2246,6 +2224,57 @@ function resolveStaticDir() {
|
|
|
2246
2224
|
return join7(dirname4(fileURLToPath(import.meta.url)), "static");
|
|
2247
2225
|
}
|
|
2248
2226
|
|
|
2227
|
+
// src/prereqs.ts
|
|
2228
|
+
async function claudeOnPath() {
|
|
2229
|
+
return (await probeHarnessPresence("claude-code")).status === "present";
|
|
2230
|
+
}
|
|
2231
|
+
async function codexOnPath() {
|
|
2232
|
+
return (await probeHarnessPresence("codex")).status === "present";
|
|
2233
|
+
}
|
|
2234
|
+
async function requireStartConfig(deps = {}) {
|
|
2235
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
2236
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
2237
|
+
const save2 = deps.save ?? saveConfig;
|
|
2238
|
+
const cfg = requireCfg();
|
|
2239
|
+
const claudeOnPathResult = await probeClaude();
|
|
2240
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
2241
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
2242
|
+
save2(migrated);
|
|
2243
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
2244
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
2245
|
+
}
|
|
2246
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
2247
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
2248
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
2249
|
+
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
2250
|
+
`));
|
|
2251
|
+
const connected = [
|
|
2252
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
2253
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
2254
|
+
...cfg.opencode ? ["opencode"] : []
|
|
2255
|
+
];
|
|
2256
|
+
if (connected.length > 0) {
|
|
2257
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
2258
|
+
warn(
|
|
2259
|
+
"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."
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
2265
|
+
const installed = [
|
|
2266
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
2267
|
+
...codexInstalled ? ["Codex"] : []
|
|
2268
|
+
];
|
|
2269
|
+
const connectCommands = [
|
|
2270
|
+
...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
|
|
2271
|
+
...codexInstalled ? ["`cabane-companion connect codex`"] : []
|
|
2272
|
+
];
|
|
2273
|
+
warn(
|
|
2274
|
+
"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 with " : "it with "}${connectCommands.join(" or ")} 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 with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2249
2278
|
// src/api.ts
|
|
2250
2279
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2251
2280
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
@@ -7176,7 +7205,7 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
7176
7205
|
}
|
|
7177
7206
|
|
|
7178
7207
|
// src/transcript-writer.ts
|
|
7179
|
-
import { appendFileSync, chmodSync as
|
|
7208
|
+
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
7180
7209
|
import { basename, dirname as dirname5, join as join13 } from "path";
|
|
7181
7210
|
function transcriptsDir() {
|
|
7182
7211
|
return join13(cabaneDir(), "transcripts");
|
|
@@ -7193,7 +7222,7 @@ var TranscriptWriter = class {
|
|
|
7193
7222
|
try {
|
|
7194
7223
|
mkdirSync9(dir2, { recursive: true });
|
|
7195
7224
|
try {
|
|
7196
|
-
|
|
7225
|
+
chmodSync4(dir2, 448);
|
|
7197
7226
|
} catch {
|
|
7198
7227
|
}
|
|
7199
7228
|
pruneOld(dir2, RETAIN);
|
|
@@ -7220,10 +7249,10 @@ var TranscriptWriter = class {
|
|
|
7220
7249
|
try {
|
|
7221
7250
|
const dir2 = join13(dirname5(this.path), "anomalies");
|
|
7222
7251
|
mkdirSync9(dir2, { recursive: true, mode: 448 });
|
|
7223
|
-
|
|
7252
|
+
chmodSync4(dir2, 448);
|
|
7224
7253
|
const target = join13(dir2, basename(this.path));
|
|
7225
7254
|
copyFileSync(this.path, target);
|
|
7226
|
-
|
|
7255
|
+
chmodSync4(target, 384);
|
|
7227
7256
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
7228
7257
|
} catch (err) {
|
|
7229
7258
|
this.fail(err);
|
|
@@ -9065,11 +9094,9 @@ var CompanionSupervisor = class {
|
|
|
9065
9094
|
// can never disagree with what the manifest advertises. Null until the first
|
|
9066
9095
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
9067
9096
|
harnessSignals = null;
|
|
9068
|
-
//
|
|
9069
|
-
//
|
|
9070
|
-
|
|
9071
|
-
probeClaudePresence;
|
|
9072
|
-
probeCodexPresence;
|
|
9097
|
+
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
9098
|
+
// between attempts must be observed immediately.
|
|
9099
|
+
probePresence;
|
|
9073
9100
|
exitFn;
|
|
9074
9101
|
reexecFn;
|
|
9075
9102
|
dispatcherFactory;
|
|
@@ -9101,8 +9128,7 @@ var CompanionSupervisor = class {
|
|
|
9101
9128
|
this.log = opts.log;
|
|
9102
9129
|
this.hub = opts.hub;
|
|
9103
9130
|
this.claudeCode = opts.claudeCode ?? true;
|
|
9104
|
-
this.
|
|
9105
|
-
this.probeCodexPresence = opts.probeCodexPresence ?? codexOnPath;
|
|
9131
|
+
this.probePresence = opts.probePresence ?? probeHarnessPresence;
|
|
9106
9132
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
9107
9133
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
9108
9134
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -9800,10 +9826,11 @@ var CompanionSupervisor = class {
|
|
|
9800
9826
|
// over SSE). Fail-soft by construction — `probeHarnessSignals` never throws — but
|
|
9801
9827
|
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
9802
9828
|
// the heartbeat cadence (fresh presence for the manifest + UI) and on demand.
|
|
9803
|
-
async refreshHarnessStatuses() {
|
|
9829
|
+
async refreshHarnessStatuses(presence = {}) {
|
|
9804
9830
|
try {
|
|
9805
9831
|
const signals = await probeHarnessSignals(this.config, {
|
|
9806
|
-
|
|
9832
|
+
probePresence: this.probePresence,
|
|
9833
|
+
presence
|
|
9807
9834
|
});
|
|
9808
9835
|
this.harnessSignals = signals;
|
|
9809
9836
|
this.harnessVersions = {
|
|
@@ -9860,18 +9887,24 @@ var CompanionSupervisor = class {
|
|
|
9860
9887
|
// reachable/valid input).
|
|
9861
9888
|
async enableHarness(input) {
|
|
9862
9889
|
let next;
|
|
9890
|
+
let checkedPresence = null;
|
|
9863
9891
|
if (input.runtime === "claude-code") {
|
|
9864
|
-
|
|
9892
|
+
const presence = input.presence ?? await this.probePresence("claude-code");
|
|
9893
|
+
if (presence.status !== "present") {
|
|
9865
9894
|
return {
|
|
9866
9895
|
ok: false,
|
|
9867
|
-
error: "
|
|
9896
|
+
error: presenceReason("claude-code", presence.status),
|
|
9897
|
+
presence: true
|
|
9868
9898
|
};
|
|
9869
9899
|
}
|
|
9900
|
+
checkedPresence = { runtime: "claude-code", result: presence };
|
|
9870
9901
|
next = { ...this.config, claudeCode: { enabled: true } };
|
|
9871
9902
|
} else if (input.runtime === "codex") {
|
|
9872
|
-
|
|
9873
|
-
|
|
9903
|
+
const presence = input.presence ?? await this.probePresence("codex");
|
|
9904
|
+
if (presence.status !== "present") {
|
|
9905
|
+
return { ok: false, error: presenceReason("codex", presence.status), presence: true };
|
|
9874
9906
|
}
|
|
9907
|
+
checkedPresence = { runtime: "codex", result: presence };
|
|
9875
9908
|
next = { ...this.config, codex: { enabled: true } };
|
|
9876
9909
|
} else {
|
|
9877
9910
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -9895,7 +9928,9 @@ var CompanionSupervisor = class {
|
|
|
9895
9928
|
this.config = next;
|
|
9896
9929
|
saveConfig(next);
|
|
9897
9930
|
this.rebuildDispatchers();
|
|
9898
|
-
await this.refreshHarnessStatuses(
|
|
9931
|
+
await this.refreshHarnessStatuses(
|
|
9932
|
+
checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
|
|
9933
|
+
);
|
|
9899
9934
|
this.kickHeartbeat();
|
|
9900
9935
|
return { ok: true };
|
|
9901
9936
|
}
|
|
@@ -9959,9 +9994,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
9959
9994
|
}
|
|
9960
9995
|
function defaultReexec() {
|
|
9961
9996
|
clearRuntimeState();
|
|
9962
|
-
void import("child_process").then(({ spawn:
|
|
9997
|
+
void import("child_process").then(({ spawn: spawn5 }) => {
|
|
9963
9998
|
try {
|
|
9964
|
-
const child =
|
|
9999
|
+
const child = spawn5(process.execPath, process.argv.slice(1), {
|
|
9965
10000
|
stdio: "inherit",
|
|
9966
10001
|
detached: false
|
|
9967
10002
|
});
|
|
@@ -10102,7 +10137,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10102
10137
|
if (!claim.acquired) {
|
|
10103
10138
|
return { ok: false, reason: "already-running", existing: claim.existing ?? null };
|
|
10104
10139
|
}
|
|
10105
|
-
|
|
10140
|
+
const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
|
|
10141
|
+
process.on("exit", clearRuntimeOnExit);
|
|
10106
10142
|
const hub = new CompanionStateHub({
|
|
10107
10143
|
// CT29: one device, one base URL — the cabane instance this device is paired
|
|
10108
10144
|
// with. The dashboard's connection line shows it.
|
|
@@ -10124,49 +10160,73 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10124
10160
|
const currentConfig = supervisor.currentConfig();
|
|
10125
10161
|
const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
|
|
10126
10162
|
const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
|
|
10127
|
-
const
|
|
10128
|
-
|
|
10163
|
+
const presence = runtime === "opencode" ? void 0 : await probeHarnessPresence(runtime);
|
|
10164
|
+
const verdict = await shakeOutHarness(runtime, candidate, {
|
|
10165
|
+
...presence ? { presence } : {}
|
|
10166
|
+
});
|
|
10167
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
10168
|
+
return { ok: false, error: presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION) };
|
|
10169
|
+
}
|
|
10129
10170
|
const result = await supervisor.enableHarness(
|
|
10130
|
-
runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
|
|
10171
|
+
runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime, presence }
|
|
10131
10172
|
);
|
|
10132
10173
|
if (!result.ok) return { ok: false, error: result.error };
|
|
10133
10174
|
return { ok: true, message: connectedLine(runtime, verdict) };
|
|
10134
10175
|
};
|
|
10135
|
-
|
|
10136
|
-
status: () => hub.statusJson(),
|
|
10137
|
-
connect: async (runtime, serverUrl) => {
|
|
10138
|
-
const result = await connectHarness(runtime, serverUrl);
|
|
10139
|
-
return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
|
|
10140
|
-
},
|
|
10141
|
-
stop: () => void supervisor.requestStop()
|
|
10142
|
-
});
|
|
10176
|
+
let control = null;
|
|
10143
10177
|
let dashboard = null;
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10178
|
+
try {
|
|
10179
|
+
control = await startControlServer({
|
|
10180
|
+
status: () => hub.statusJson(),
|
|
10181
|
+
connect: async (runtime, serverUrl) => {
|
|
10182
|
+
const result = await connectHarness(runtime, serverUrl);
|
|
10183
|
+
return result.ok ? { ok: true, message: result.message } : { ok: false, message: result.error };
|
|
10184
|
+
},
|
|
10185
|
+
stop: () => void supervisor.requestStop()
|
|
10186
|
+
});
|
|
10187
|
+
if (opts.dashboard) {
|
|
10188
|
+
const preferredPort = opts.port ?? cfg.dashboardPort;
|
|
10189
|
+
dashboard = await startDashboard({
|
|
10190
|
+
supervisor,
|
|
10191
|
+
hub,
|
|
10192
|
+
...preferredPort !== void 0 ? { port: preferredPort } : {}
|
|
10193
|
+
});
|
|
10194
|
+
hub.setDashboardUrl(dashboard.url);
|
|
10195
|
+
}
|
|
10196
|
+
writeRuntimeState({
|
|
10197
|
+
pid: process.pid,
|
|
10198
|
+
socket: control.path,
|
|
10199
|
+
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
10200
|
+
startedAt,
|
|
10201
|
+
// SJ495: the daemon launcher sets this env on the detached child, so the
|
|
10202
|
+
// marker records whether this companion is backgrounded (foreground start
|
|
10203
|
+
// leaves it unset → false).
|
|
10204
|
+
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
10205
|
+
instanceId
|
|
10206
|
+
});
|
|
10207
|
+
clearCrash();
|
|
10208
|
+
} catch (err) {
|
|
10209
|
+
clearRuntimeStateIfOurs(instanceId);
|
|
10210
|
+
process.removeListener("exit", clearRuntimeOnExit);
|
|
10211
|
+
await Promise.allSettled([
|
|
10212
|
+
supervisor.shutdown(),
|
|
10213
|
+
...control ? [control.close()] : [],
|
|
10214
|
+
...dashboard ? [dashboard.close()] : []
|
|
10215
|
+
]);
|
|
10216
|
+
recordCrash({
|
|
10217
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
10218
|
+
...errorCode(err) ? { code: errorCode(err) } : {},
|
|
10219
|
+
origin: "startup",
|
|
10220
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10150
10221
|
});
|
|
10151
|
-
|
|
10222
|
+
throw err;
|
|
10152
10223
|
}
|
|
10153
|
-
writeRuntimeState({
|
|
10154
|
-
pid: process.pid,
|
|
10155
|
-
socket: control.path,
|
|
10156
|
-
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
10157
|
-
startedAt,
|
|
10158
|
-
// SJ495: the daemon launcher sets this env on the detached child, so the
|
|
10159
|
-
// marker records whether this companion is backgrounded (foreground start
|
|
10160
|
-
// leaves it unset → false).
|
|
10161
|
-
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
10162
|
-
instanceId
|
|
10163
|
-
});
|
|
10164
|
-
clearCrash();
|
|
10165
10224
|
let stopped = false;
|
|
10166
10225
|
const stop2 = async () => {
|
|
10167
10226
|
if (stopped) return;
|
|
10168
10227
|
stopped = true;
|
|
10169
10228
|
clearRuntimeStateIfOurs(instanceId);
|
|
10229
|
+
process.removeListener("exit", clearRuntimeOnExit);
|
|
10170
10230
|
try {
|
|
10171
10231
|
await supervisor.shutdown();
|
|
10172
10232
|
await closeSurfaces(control, dashboard);
|
|
@@ -10192,7 +10252,7 @@ async function closeSurfaces(control, dashboard) {
|
|
|
10192
10252
|
}
|
|
10193
10253
|
|
|
10194
10254
|
// src/commands/daemon.ts
|
|
10195
|
-
import { spawn as
|
|
10255
|
+
import { spawn as spawn4 } from "child_process";
|
|
10196
10256
|
import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
|
|
10197
10257
|
|
|
10198
10258
|
// src/cli-entry.ts
|
|
@@ -10299,7 +10359,7 @@ function defaultSpawnDetached(args) {
|
|
|
10299
10359
|
mkdirSync13(cabaneDir(), { recursive: true });
|
|
10300
10360
|
const logFd = openSync3(companionLogPath(), "a");
|
|
10301
10361
|
try {
|
|
10302
|
-
return
|
|
10362
|
+
return spawn4(process.execPath, [cliPath, ...args], {
|
|
10303
10363
|
detached: true,
|
|
10304
10364
|
stdio: ["ignore", logFd, logFd],
|
|
10305
10365
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -10397,14 +10457,15 @@ function reportAlreadyRunning(pid) {
|
|
|
10397
10457
|
write("Connect a harness to the running companion: cabane-companion connect claude-code");
|
|
10398
10458
|
}
|
|
10399
10459
|
async function collectHarnessChoices(interactive) {
|
|
10400
|
-
const [
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
probeCliVersion("codex").catch(() => null),
|
|
10460
|
+
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
10461
|
+
probeHarnessPresence("claude-code").catch(() => ({ status: "absent" })),
|
|
10462
|
+
probeHarnessPresence("codex").catch(() => ({ status: "absent" })),
|
|
10404
10463
|
probeDefaultOpencodeVersion()
|
|
10405
10464
|
]);
|
|
10465
|
+
const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
|
|
10466
|
+
const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
|
|
10406
10467
|
const found = [
|
|
10407
|
-
...
|
|
10468
|
+
...claudeVersion ? [{ runtime: "claude-code", version: claudeVersion }] : [],
|
|
10408
10469
|
...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
|
|
10409
10470
|
...opencodeVersion ? [
|
|
10410
10471
|
{
|