@cabane/companion 0.6.49 → 0.6.50
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 +176 -152
- package/dist/runtime.js +1070 -1056
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -644,40 +644,58 @@ async function ping(path) {
|
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
// src/harness-check.ts
|
|
647
|
-
import { spawn as
|
|
647
|
+
import { spawn as spawn3 } from "child_process";
|
|
648
648
|
|
|
649
649
|
// src/harness-versions.ts
|
|
650
650
|
import { spawn as spawn2 } from "child_process";
|
|
651
651
|
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
652
|
+
var CLI_PROBE_TIMEOUT_MS = 4e3;
|
|
652
653
|
function parseVersionToken(raw) {
|
|
653
654
|
if (!raw) return null;
|
|
654
655
|
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
655
656
|
return m ? m[0] : null;
|
|
656
657
|
}
|
|
657
|
-
|
|
658
|
+
function probeCliPresence(command, spawnImpl = spawn2) {
|
|
658
659
|
return new Promise((resolve) => {
|
|
659
660
|
let settled = false;
|
|
660
|
-
|
|
661
|
+
let timer = null;
|
|
662
|
+
const done = (result) => {
|
|
661
663
|
if (!settled) {
|
|
662
664
|
settled = true;
|
|
663
|
-
|
|
665
|
+
if (timer) clearTimeout(timer);
|
|
666
|
+
resolve(result);
|
|
664
667
|
}
|
|
665
668
|
};
|
|
666
669
|
let child;
|
|
667
670
|
try {
|
|
668
671
|
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
669
672
|
} catch {
|
|
670
|
-
done(
|
|
673
|
+
done({ status: "absent" });
|
|
671
674
|
return;
|
|
672
675
|
}
|
|
673
676
|
let out = "";
|
|
674
677
|
child.stdout?.on("data", (chunk) => {
|
|
675
678
|
if (out.length < 4096) out += chunk.toString();
|
|
676
679
|
});
|
|
677
|
-
|
|
678
|
-
|
|
680
|
+
timer = setTimeout(() => {
|
|
681
|
+
child.kill?.("SIGKILL");
|
|
682
|
+
done({ status: "unusable" });
|
|
683
|
+
}, CLI_PROBE_TIMEOUT_MS);
|
|
684
|
+
timer.unref?.();
|
|
685
|
+
child.once("error", () => done({ status: "absent" }));
|
|
686
|
+
child.once("exit", (code) => {
|
|
687
|
+
const version = code === 0 ? parseVersionToken(out) : null;
|
|
688
|
+
done(version ? { status: "present", version } : { status: "unusable" });
|
|
689
|
+
});
|
|
679
690
|
});
|
|
680
691
|
}
|
|
692
|
+
function probeHarnessPresence(runtime, spawnImpl = spawn2) {
|
|
693
|
+
return probeCliPresence(runtime === "codex" ? "codex" : "claude", spawnImpl);
|
|
694
|
+
}
|
|
695
|
+
async function probeCliVersion(command, spawnImpl = spawn2) {
|
|
696
|
+
const result = await probeCliPresence(command, spawnImpl);
|
|
697
|
+
return result.status === "present" ? result.version : null;
|
|
698
|
+
}
|
|
681
699
|
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
682
700
|
try {
|
|
683
701
|
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
@@ -741,81 +759,6 @@ function buildCompanionManifest(opts) {
|
|
|
741
759
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
742
760
|
}
|
|
743
761
|
|
|
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
762
|
// src/harness-status.ts
|
|
820
763
|
var HARNESS_LABELS = {
|
|
821
764
|
"claude-code": "Claude Code",
|
|
@@ -976,20 +919,19 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
|
|
|
976
919
|
return await probeDefault() !== null ? DEFAULT_OPENCODE_SERVER_URL : null;
|
|
977
920
|
}
|
|
978
921
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
979
|
-
const
|
|
980
|
-
const probeClaudeVersion = deps.probeClaudeVersion ?? (() => probeCliVersion("claude"));
|
|
981
|
-
const probeCodexVersion = deps.probeCodexVersion ?? (() => probeCliVersion("codex"));
|
|
922
|
+
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
982
923
|
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
983
924
|
const configuredServerUrl = cfg.opencode?.serverUrl;
|
|
984
925
|
const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
|
|
985
|
-
const [
|
|
986
|
-
withTimeout(
|
|
987
|
-
withTimeout(
|
|
988
|
-
withTimeout(probeCodexVersion(), null),
|
|
926
|
+
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
927
|
+
deps.presence?.["claude-code"] ?? withTimeout(probePresence("claude-code"), { status: "absent" }),
|
|
928
|
+
deps.presence?.codex ?? withTimeout(probePresence("codex"), { status: "absent" }),
|
|
989
929
|
configuredServerUrl ? withTimeout(probeOpencode(opencodeProbeUrl), null) : probeDefaultOpencodeVersion(probeOpencode)
|
|
990
930
|
]);
|
|
931
|
+
const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
|
|
932
|
+
const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
|
|
991
933
|
return {
|
|
992
|
-
claudeOnPath:
|
|
934
|
+
claudeOnPath: claudePresence.status === "present",
|
|
993
935
|
claudeVersion,
|
|
994
936
|
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
995
937
|
// the manifest gate and the probe above is only a suggestion.
|
|
@@ -1041,16 +983,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1041
983
|
if (!url) return "absent";
|
|
1042
984
|
return await probeOpencode(url) !== null ? "ok" : "failed";
|
|
1043
985
|
}
|
|
1044
|
-
const { auth
|
|
1045
|
-
auth: ["codex", ["login", "status"]]
|
|
1046
|
-
presence: ["codex", ["--version"]]
|
|
986
|
+
const { auth } = runtime === "codex" ? {
|
|
987
|
+
auth: ["codex", ["login", "status"]]
|
|
1047
988
|
} : {
|
|
1048
|
-
auth: ["claude", ["auth", "status"]]
|
|
1049
|
-
presence: ["claude", ["--version"]]
|
|
989
|
+
auth: ["claude", ["auth", "status"]]
|
|
1050
990
|
};
|
|
1051
|
-
const
|
|
1052
|
-
if (
|
|
1053
|
-
if (presenceRun.code !== 0) return "unverified";
|
|
991
|
+
const presence = deps.presence ?? await (deps.probePresence ?? probeHarnessPresence)(runtime);
|
|
992
|
+
if (presence.status !== "present") return presence.status;
|
|
1054
993
|
const authRun = await run(auth[0], [...auth[1]]);
|
|
1055
994
|
if (authRun.code === 0) return "ok";
|
|
1056
995
|
if (looksUnsupported(authRun.output)) {
|
|
@@ -1062,7 +1001,9 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1062
1001
|
}
|
|
1063
1002
|
}
|
|
1064
1003
|
function connectedLine(runtime, verdict) {
|
|
1065
|
-
if (verdict === "absent"
|
|
1004
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
1005
|
+
throw new Error("an unavailable harness cannot be connected");
|
|
1006
|
+
}
|
|
1066
1007
|
const label = HARNESS_LABELS[runtime];
|
|
1067
1008
|
if (verdict !== "failed") return `${label} connected.`;
|
|
1068
1009
|
return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
|
|
@@ -1072,13 +1013,18 @@ var FAILED_SUFFIX = {
|
|
|
1072
1013
|
codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
|
|
1073
1014
|
opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
|
|
1074
1015
|
};
|
|
1075
|
-
function
|
|
1016
|
+
function presenceReason(runtime, status2) {
|
|
1076
1017
|
if (runtime === "opencode") {
|
|
1077
1018
|
return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
|
|
1078
1019
|
}
|
|
1079
1020
|
const label = HARNESS_LABELS[runtime];
|
|
1080
|
-
|
|
1081
|
-
|
|
1021
|
+
return status2 === "absent" ? `${label} isn't installed on this machine.` : `${label} is installed here but didn't report a version Cabane can read.`;
|
|
1022
|
+
}
|
|
1023
|
+
var CLI_PRESENCE_ACTION = "Install it and run this again.";
|
|
1024
|
+
var DASHBOARD_PRESENCE_ACTION = "Install it, then refresh.";
|
|
1025
|
+
function presenceRefusal(runtime, status2, action) {
|
|
1026
|
+
const reason = presenceReason(runtime, status2);
|
|
1027
|
+
return runtime === "opencode" ? reason : `${reason} ${action}`;
|
|
1082
1028
|
}
|
|
1083
1029
|
function looksUnsupported(output) {
|
|
1084
1030
|
return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
|
|
@@ -1096,7 +1042,7 @@ function runBounded(command, args) {
|
|
|
1096
1042
|
};
|
|
1097
1043
|
let child;
|
|
1098
1044
|
try {
|
|
1099
|
-
child =
|
|
1045
|
+
child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1100
1046
|
} catch {
|
|
1101
1047
|
resolve({ code: null, output: "", error: "spawn" });
|
|
1102
1048
|
return;
|
|
@@ -1299,18 +1245,31 @@ async function confirm(question) {
|
|
|
1299
1245
|
}
|
|
1300
1246
|
|
|
1301
1247
|
// src/commands/connect.ts
|
|
1302
|
-
async function connect2(raw, opts = {}) {
|
|
1248
|
+
async function connect2(raw, opts = {}, deps = {}) {
|
|
1303
1249
|
const runtime = parseHarnessRuntime(raw);
|
|
1304
1250
|
if (!runtime) {
|
|
1305
1251
|
throw new CompanionError(
|
|
1306
1252
|
`unknown harness "${raw}". Connectable harnesses are: claude-code, codex, opencode.`
|
|
1307
1253
|
);
|
|
1308
1254
|
}
|
|
1309
|
-
const
|
|
1255
|
+
const loadConfig2 = deps.loadConfig ?? requireConfig;
|
|
1256
|
+
const request = deps.request ?? controlRequest;
|
|
1257
|
+
const save2 = deps.save ?? saveConfig;
|
|
1258
|
+
const output = deps.output ?? write;
|
|
1259
|
+
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
1260
|
+
const shakeOut = deps.shakeOut ?? shakeOutHarness;
|
|
1261
|
+
const resolveServerUrl = deps.resolveServerUrl ?? resolveOpencodeServerUrl;
|
|
1262
|
+
const probeOpencode = deps.probeOpencode ?? probeOpencodeVersion;
|
|
1263
|
+
const cfg = loadConfig2();
|
|
1264
|
+
if (!opts.serverUrl?.trim() && alreadyConnected(runtime, cfg)) {
|
|
1265
|
+
output(`${INDENT}${HARNESS_LABELS[runtime]} is already connected on this device.`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const live = await (deps.findLiveSocket ?? liveSocket)();
|
|
1310
1269
|
if (live) {
|
|
1311
1270
|
let result;
|
|
1312
1271
|
try {
|
|
1313
|
-
result = await
|
|
1272
|
+
result = await request(
|
|
1314
1273
|
live,
|
|
1315
1274
|
{
|
|
1316
1275
|
cmd: "connect",
|
|
@@ -1326,42 +1285,38 @@ async function connect2(raw, opts = {}) {
|
|
|
1326
1285
|
);
|
|
1327
1286
|
}
|
|
1328
1287
|
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.`);
|
|
1288
|
+
output(INDENT + tick(result.message));
|
|
1335
1289
|
return;
|
|
1336
1290
|
}
|
|
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
1291
|
let next = cfg;
|
|
1292
|
+
const presence = runtime === "opencode" ? void 0 : await probePresence(runtime);
|
|
1293
|
+
if (presence && presence.status !== "present") {
|
|
1294
|
+
throw new CompanionError(presenceRefusal(runtime, presence.status, CLI_PRESENCE_ACTION));
|
|
1295
|
+
}
|
|
1343
1296
|
if (runtime === "claude-code") next = { ...cfg, claudeCode: { enabled: true } };
|
|
1344
1297
|
else if (runtime === "codex") next = { ...cfg, codex: { enabled: true } };
|
|
1345
1298
|
else {
|
|
1346
1299
|
const requestedServerUrl = opts.serverUrl?.trim();
|
|
1347
|
-
const serverUrl = await
|
|
1300
|
+
const serverUrl = await resolveServerUrl(cfg.opencode?.serverUrl, requestedServerUrl);
|
|
1348
1301
|
if (!serverUrl) {
|
|
1349
1302
|
throw new CompanionError(
|
|
1350
1303
|
"opencode is addressed by URL \u2014 pass it: `cabane-companion connect opencode --url http://127.0.0.1:4096`."
|
|
1351
1304
|
);
|
|
1352
1305
|
}
|
|
1353
|
-
if (requestedServerUrl && await
|
|
1306
|
+
if (requestedServerUrl && await probeOpencode(serverUrl) === null) {
|
|
1354
1307
|
throw new CompanionError(
|
|
1355
1308
|
`Couldn\u2019t reach an opencode server at ${serverUrl}. Start \`opencode serve\` and check the URL.`
|
|
1356
1309
|
);
|
|
1357
1310
|
}
|
|
1358
1311
|
next = { ...cfg, opencode: { serverUrl } };
|
|
1359
1312
|
}
|
|
1360
|
-
const verdict = await
|
|
1361
|
-
if (verdict === "absent"
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1313
|
+
const verdict = await shakeOut(runtime, next, { ...presence ? { presence } : {} });
|
|
1314
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
1315
|
+
throw new CompanionError(presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION));
|
|
1316
|
+
}
|
|
1317
|
+
save2(next);
|
|
1318
|
+
output(INDENT + tick(connectedLine(runtime, verdict)));
|
|
1319
|
+
output(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
|
|
1365
1320
|
}
|
|
1366
1321
|
async function liveSocket() {
|
|
1367
1322
|
const state = readLiveRuntimeState();
|
|
@@ -2082,12 +2037,18 @@ function registerRoutes(app, deps) {
|
|
|
2082
2037
|
const runtime = body.runtime;
|
|
2083
2038
|
if (runtime === "claude-code") {
|
|
2084
2039
|
const result = await supervisor.enableHarness({ runtime: "claude-code" });
|
|
2085
|
-
if (!result.ok)
|
|
2040
|
+
if (!result.ok) {
|
|
2041
|
+
const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
|
|
2042
|
+
return c.json({ error }, 400);
|
|
2043
|
+
}
|
|
2086
2044
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
2087
2045
|
}
|
|
2088
2046
|
if (runtime === "codex") {
|
|
2089
2047
|
const result = await supervisor.enableHarness({ runtime: "codex" });
|
|
2090
|
-
if (!result.ok)
|
|
2048
|
+
if (!result.ok) {
|
|
2049
|
+
const error = result.presence ? `${result.error} ${DASHBOARD_PRESENCE_ACTION}` : result.error;
|
|
2050
|
+
return c.json({ error }, 400);
|
|
2051
|
+
}
|
|
2091
2052
|
return c.json({ ok: true, status: hub.statusJson() });
|
|
2092
2053
|
}
|
|
2093
2054
|
if (runtime === "opencode") {
|
|
@@ -2246,6 +2207,57 @@ function resolveStaticDir() {
|
|
|
2246
2207
|
return join7(dirname4(fileURLToPath(import.meta.url)), "static");
|
|
2247
2208
|
}
|
|
2248
2209
|
|
|
2210
|
+
// src/prereqs.ts
|
|
2211
|
+
async function claudeOnPath() {
|
|
2212
|
+
return (await probeHarnessPresence("claude-code")).status === "present";
|
|
2213
|
+
}
|
|
2214
|
+
async function codexOnPath() {
|
|
2215
|
+
return (await probeHarnessPresence("codex")).status === "present";
|
|
2216
|
+
}
|
|
2217
|
+
async function requireStartConfig(deps = {}) {
|
|
2218
|
+
const requireCfg = deps.requireCfg ?? requireConfig;
|
|
2219
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
2220
|
+
const save2 = deps.save ?? saveConfig;
|
|
2221
|
+
const cfg = requireCfg();
|
|
2222
|
+
const claudeOnPathResult = await probeClaude();
|
|
2223
|
+
const migrated = migrateConnectedHarnesses(cfg, claudeOnPathResult);
|
|
2224
|
+
if (!migrated) return { cfg, claudeOnPath: claudeOnPathResult };
|
|
2225
|
+
save2(migrated);
|
|
2226
|
+
deps.onMigrated?.(migrated, claudeOnPathResult);
|
|
2227
|
+
return { cfg: migrated, claudeOnPath: claudeOnPathResult };
|
|
2228
|
+
}
|
|
2229
|
+
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
2230
|
+
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
2231
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
2232
|
+
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
2233
|
+
`));
|
|
2234
|
+
const connected = [
|
|
2235
|
+
...isClaudeCodeConnected(cfg) ? ["Claude Code"] : [],
|
|
2236
|
+
...isCodexEnabled(cfg) ? ["Codex"] : [],
|
|
2237
|
+
...cfg.opencode ? ["opencode"] : []
|
|
2238
|
+
];
|
|
2239
|
+
if (connected.length > 0) {
|
|
2240
|
+
if (isClaudeCodeConnected(cfg) && !await probeClaude()) {
|
|
2241
|
+
warn(
|
|
2242
|
+
"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."
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
2248
|
+
const installed = [
|
|
2249
|
+
...claudeInstalled ? ["Claude Code"] : [],
|
|
2250
|
+
...codexInstalled ? ["Codex"] : []
|
|
2251
|
+
];
|
|
2252
|
+
const connectCommands = [
|
|
2253
|
+
...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
|
|
2254
|
+
...codexInstalled ? ["`cabane-companion connect codex`"] : []
|
|
2255
|
+
];
|
|
2256
|
+
warn(
|
|
2257
|
+
"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).")
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2249
2261
|
// src/api.ts
|
|
2250
2262
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2251
2263
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
@@ -9065,11 +9077,9 @@ var CompanionSupervisor = class {
|
|
|
9065
9077
|
// can never disagree with what the manifest advertises. Null until the first
|
|
9066
9078
|
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
9067
9079
|
harnessSignals = null;
|
|
9068
|
-
//
|
|
9069
|
-
//
|
|
9070
|
-
|
|
9071
|
-
probeClaudePresence;
|
|
9072
|
-
probeCodexPresence;
|
|
9080
|
+
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
9081
|
+
// between attempts must be observed immediately.
|
|
9082
|
+
probePresence;
|
|
9073
9083
|
exitFn;
|
|
9074
9084
|
reexecFn;
|
|
9075
9085
|
dispatcherFactory;
|
|
@@ -9101,8 +9111,7 @@ var CompanionSupervisor = class {
|
|
|
9101
9111
|
this.log = opts.log;
|
|
9102
9112
|
this.hub = opts.hub;
|
|
9103
9113
|
this.claudeCode = opts.claudeCode ?? true;
|
|
9104
|
-
this.
|
|
9105
|
-
this.probeCodexPresence = opts.probeCodexPresence ?? codexOnPath;
|
|
9114
|
+
this.probePresence = opts.probePresence ?? probeHarnessPresence;
|
|
9106
9115
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
9107
9116
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
9108
9117
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
@@ -9800,10 +9809,11 @@ var CompanionSupervisor = class {
|
|
|
9800
9809
|
// over SSE). Fail-soft by construction — `probeHarnessSignals` never throws — but
|
|
9801
9810
|
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
9802
9811
|
// the heartbeat cadence (fresh presence for the manifest + UI) and on demand.
|
|
9803
|
-
async refreshHarnessStatuses() {
|
|
9812
|
+
async refreshHarnessStatuses(presence = {}) {
|
|
9804
9813
|
try {
|
|
9805
9814
|
const signals = await probeHarnessSignals(this.config, {
|
|
9806
|
-
|
|
9815
|
+
probePresence: this.probePresence,
|
|
9816
|
+
presence
|
|
9807
9817
|
});
|
|
9808
9818
|
this.harnessSignals = signals;
|
|
9809
9819
|
this.harnessVersions = {
|
|
@@ -9860,18 +9870,24 @@ var CompanionSupervisor = class {
|
|
|
9860
9870
|
// reachable/valid input).
|
|
9861
9871
|
async enableHarness(input) {
|
|
9862
9872
|
let next;
|
|
9873
|
+
let checkedPresence = null;
|
|
9863
9874
|
if (input.runtime === "claude-code") {
|
|
9864
|
-
|
|
9875
|
+
const presence = input.presence ?? await this.probePresence("claude-code");
|
|
9876
|
+
if (presence.status !== "present") {
|
|
9865
9877
|
return {
|
|
9866
9878
|
ok: false,
|
|
9867
|
-
error: "
|
|
9879
|
+
error: presenceReason("claude-code", presence.status),
|
|
9880
|
+
presence: true
|
|
9868
9881
|
};
|
|
9869
9882
|
}
|
|
9883
|
+
checkedPresence = { runtime: "claude-code", result: presence };
|
|
9870
9884
|
next = { ...this.config, claudeCode: { enabled: true } };
|
|
9871
9885
|
} else if (input.runtime === "codex") {
|
|
9872
|
-
|
|
9873
|
-
|
|
9886
|
+
const presence = input.presence ?? await this.probePresence("codex");
|
|
9887
|
+
if (presence.status !== "present") {
|
|
9888
|
+
return { ok: false, error: presenceReason("codex", presence.status), presence: true };
|
|
9874
9889
|
}
|
|
9890
|
+
checkedPresence = { runtime: "codex", result: presence };
|
|
9875
9891
|
next = { ...this.config, codex: { enabled: true } };
|
|
9876
9892
|
} else {
|
|
9877
9893
|
const serverUrl = input.serverUrl.trim();
|
|
@@ -9895,7 +9911,9 @@ var CompanionSupervisor = class {
|
|
|
9895
9911
|
this.config = next;
|
|
9896
9912
|
saveConfig(next);
|
|
9897
9913
|
this.rebuildDispatchers();
|
|
9898
|
-
await this.refreshHarnessStatuses(
|
|
9914
|
+
await this.refreshHarnessStatuses(
|
|
9915
|
+
checkedPresence ? { [checkedPresence.runtime]: checkedPresence.result } : {}
|
|
9916
|
+
);
|
|
9899
9917
|
this.kickHeartbeat();
|
|
9900
9918
|
return { ok: true };
|
|
9901
9919
|
}
|
|
@@ -9959,9 +9977,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
9959
9977
|
}
|
|
9960
9978
|
function defaultReexec() {
|
|
9961
9979
|
clearRuntimeState();
|
|
9962
|
-
void import("child_process").then(({ spawn:
|
|
9980
|
+
void import("child_process").then(({ spawn: spawn5 }) => {
|
|
9963
9981
|
try {
|
|
9964
|
-
const child =
|
|
9982
|
+
const child = spawn5(process.execPath, process.argv.slice(1), {
|
|
9965
9983
|
stdio: "inherit",
|
|
9966
9984
|
detached: false
|
|
9967
9985
|
});
|
|
@@ -10124,10 +10142,15 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10124
10142
|
const currentConfig = supervisor.currentConfig();
|
|
10125
10143
|
const resolvedServerUrl = runtime === "opencode" ? await resolveOpencodeServerUrl(currentConfig.opencode?.serverUrl, serverUrl) : null;
|
|
10126
10144
|
const candidate = runtime === "opencode" ? { ...currentConfig, opencode: { serverUrl: resolvedServerUrl ?? "" } } : currentConfig;
|
|
10127
|
-
const
|
|
10128
|
-
|
|
10145
|
+
const presence = runtime === "opencode" ? void 0 : await probeHarnessPresence(runtime);
|
|
10146
|
+
const verdict = await shakeOutHarness(runtime, candidate, {
|
|
10147
|
+
...presence ? { presence } : {}
|
|
10148
|
+
});
|
|
10149
|
+
if (verdict === "absent" || verdict === "unusable") {
|
|
10150
|
+
return { ok: false, error: presenceRefusal(runtime, verdict, CLI_PRESENCE_ACTION) };
|
|
10151
|
+
}
|
|
10129
10152
|
const result = await supervisor.enableHarness(
|
|
10130
|
-
runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime }
|
|
10153
|
+
runtime === "opencode" ? { runtime: "opencode", serverUrl: resolvedServerUrl ?? "" } : { runtime, presence }
|
|
10131
10154
|
);
|
|
10132
10155
|
if (!result.ok) return { ok: false, error: result.error };
|
|
10133
10156
|
return { ok: true, message: connectedLine(runtime, verdict) };
|
|
@@ -10192,7 +10215,7 @@ async function closeSurfaces(control, dashboard) {
|
|
|
10192
10215
|
}
|
|
10193
10216
|
|
|
10194
10217
|
// src/commands/daemon.ts
|
|
10195
|
-
import { spawn as
|
|
10218
|
+
import { spawn as spawn4 } from "child_process";
|
|
10196
10219
|
import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
|
|
10197
10220
|
|
|
10198
10221
|
// src/cli-entry.ts
|
|
@@ -10299,7 +10322,7 @@ function defaultSpawnDetached(args) {
|
|
|
10299
10322
|
mkdirSync13(cabaneDir(), { recursive: true });
|
|
10300
10323
|
const logFd = openSync3(companionLogPath(), "a");
|
|
10301
10324
|
try {
|
|
10302
|
-
return
|
|
10325
|
+
return spawn4(process.execPath, [cliPath, ...args], {
|
|
10303
10326
|
detached: true,
|
|
10304
10327
|
stdio: ["ignore", logFd, logFd],
|
|
10305
10328
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -10397,14 +10420,15 @@ function reportAlreadyRunning(pid) {
|
|
|
10397
10420
|
write("Connect a harness to the running companion: cabane-companion connect claude-code");
|
|
10398
10421
|
}
|
|
10399
10422
|
async function collectHarnessChoices(interactive) {
|
|
10400
|
-
const [
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
probeCliVersion("codex").catch(() => null),
|
|
10423
|
+
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
10424
|
+
probeHarnessPresence("claude-code").catch(() => ({ status: "absent" })),
|
|
10425
|
+
probeHarnessPresence("codex").catch(() => ({ status: "absent" })),
|
|
10404
10426
|
probeDefaultOpencodeVersion()
|
|
10405
10427
|
]);
|
|
10428
|
+
const claudeVersion = claudePresence.status === "present" ? claudePresence.version : null;
|
|
10429
|
+
const codexVersion = codexPresence.status === "present" ? codexPresence.version : null;
|
|
10406
10430
|
const found = [
|
|
10407
|
-
...
|
|
10431
|
+
...claudeVersion ? [{ runtime: "claude-code", version: claudeVersion }] : [],
|
|
10408
10432
|
...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
|
|
10409
10433
|
...opencodeVersion ? [
|
|
10410
10434
|
{
|