@cabane/companion 0.6.89 → 0.6.90
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/README.md +17 -11
- package/dist/cli.js +675 -215
- package/dist/pairing-config.js +9 -6
- package/dist/postinstall.js +209 -0
- package/dist/runtime.js +664 -204
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -128,7 +128,7 @@ function parsePrepareOutput(stdout) {
|
|
|
128
128
|
return { cwd: last };
|
|
129
129
|
}
|
|
130
130
|
var runPrepareHook = (hook, input) => {
|
|
131
|
-
return new Promise((
|
|
131
|
+
return new Promise((resolve2, reject) => {
|
|
132
132
|
const timeoutMs = hook.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
133
133
|
let settled = false;
|
|
134
134
|
const finish = (fn) => {
|
|
@@ -202,11 +202,11 @@ var runPrepareHook = (hook, input) => {
|
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
if (input.prepared) {
|
|
205
|
-
|
|
205
|
+
resolve2(input.prepared);
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
try {
|
|
209
|
-
|
|
209
|
+
resolve2(parsePrepareOutput(stdout));
|
|
210
210
|
} catch (err) {
|
|
211
211
|
reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
|
|
212
212
|
}
|
|
@@ -321,9 +321,10 @@ var companionConfigSchema = z2.object({
|
|
|
321
321
|
// The change this task exists for: Claude Code used to be the one harness a
|
|
322
322
|
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
323
323
|
// consent — and it is now the third config-driven harness, opted into exactly
|
|
324
|
-
// like codex.
|
|
325
|
-
//
|
|
326
|
-
//
|
|
324
|
+
// like codex. CT1379: the binary that must be there is the Agent SDK's own
|
|
325
|
+
// bundled executable — the file a turn actually launches — not the user's
|
|
326
|
+
// `claude` on PATH, which the SDK never runs. The manifest advertises
|
|
327
|
+
// claude-code only when this block says so AND that binary resolves.
|
|
327
328
|
//
|
|
328
329
|
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
329
330
|
// Absent means the config predates this task — a device whose user was never
|
|
@@ -343,9 +344,11 @@ var companionConfigSchema = z2.object({
|
|
|
343
344
|
// not a server address. The operator installs Codex + logs in (`codex login`, or
|
|
344
345
|
// `CODEX_API_KEY` — Cabane never sees the key) and sets `{ "codex": { "enabled":
|
|
345
346
|
// true } }`. Presence of the block enables it (an explicit `enabled: false`
|
|
346
|
-
// keeps the block but turns it off).
|
|
347
|
-
//
|
|
348
|
-
//
|
|
347
|
+
// keeps the block but turns it off). Absent → the device doesn't offer codex.
|
|
348
|
+
// CT1379: enabling is now half the answer, not all of it — the manifest and the
|
|
349
|
+
// adapter registry additionally require the SDK's vendored `codex` binary to
|
|
350
|
+
// resolve, because the enable flag alone advertised a runtime a device with a
|
|
351
|
+
// half-installed platform package could not start.
|
|
349
352
|
codex: z2.object({
|
|
350
353
|
enabled: z2.boolean().optional()
|
|
351
354
|
}).strict().optional()
|
|
@@ -523,18 +526,18 @@ async function startControlServer(handlers) {
|
|
|
523
526
|
void serveConnection(socket, handlers);
|
|
524
527
|
});
|
|
525
528
|
server.unref();
|
|
526
|
-
await new Promise((
|
|
529
|
+
await new Promise((resolve2, reject) => {
|
|
527
530
|
server.once("error", reject);
|
|
528
531
|
server.listen(path, () => {
|
|
529
532
|
server.removeListener("error", reject);
|
|
530
|
-
|
|
533
|
+
resolve2();
|
|
531
534
|
});
|
|
532
535
|
});
|
|
533
536
|
if (process.platform !== "win32") {
|
|
534
537
|
try {
|
|
535
538
|
chmodSync2(path, 384);
|
|
536
539
|
} catch (err) {
|
|
537
|
-
await new Promise((
|
|
540
|
+
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
538
541
|
try {
|
|
539
542
|
rmSync2(path, { force: true });
|
|
540
543
|
} catch {
|
|
@@ -546,10 +549,10 @@ async function startControlServer(handlers) {
|
|
|
546
549
|
});
|
|
547
550
|
return {
|
|
548
551
|
path,
|
|
549
|
-
close: () => new Promise((
|
|
552
|
+
close: () => new Promise((resolve2) => {
|
|
550
553
|
server.close(() => {
|
|
551
554
|
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
552
|
-
|
|
555
|
+
resolve2();
|
|
553
556
|
});
|
|
554
557
|
})
|
|
555
558
|
};
|
|
@@ -599,12 +602,12 @@ function reply(socket, body) {
|
|
|
599
602
|
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
600
603
|
const socket = connect(path);
|
|
601
604
|
try {
|
|
602
|
-
await new Promise((
|
|
605
|
+
await new Promise((resolve2, reject) => {
|
|
603
606
|
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
604
607
|
timer.unref?.();
|
|
605
608
|
socket.once("connect", () => {
|
|
606
609
|
clearTimeout(timer);
|
|
607
|
-
|
|
610
|
+
resolve2();
|
|
608
611
|
});
|
|
609
612
|
socket.once("error", (err) => {
|
|
610
613
|
clearTimeout(timer);
|
|
@@ -631,7 +634,7 @@ function isNotListening(err) {
|
|
|
631
634
|
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
632
635
|
}
|
|
633
636
|
function readLine(socket, timeoutMs) {
|
|
634
|
-
return new Promise((
|
|
637
|
+
return new Promise((resolve2) => {
|
|
635
638
|
let buf = "";
|
|
636
639
|
let settled = false;
|
|
637
640
|
const done = (v) => {
|
|
@@ -639,7 +642,7 @@ function readLine(socket, timeoutMs) {
|
|
|
639
642
|
settled = true;
|
|
640
643
|
clearTimeout(timer);
|
|
641
644
|
socket.removeListener("data", onData);
|
|
642
|
-
|
|
645
|
+
resolve2(v);
|
|
643
646
|
};
|
|
644
647
|
const timer = setTimeout(() => done(null), timeoutMs);
|
|
645
648
|
timer.unref?.();
|
|
@@ -676,14 +679,14 @@ function parseVersionToken(raw) {
|
|
|
676
679
|
return m ? m[0] : null;
|
|
677
680
|
}
|
|
678
681
|
function probeCliPresence(command, spawnImpl = spawn2) {
|
|
679
|
-
return new Promise((
|
|
682
|
+
return new Promise((resolve2) => {
|
|
680
683
|
let settled = false;
|
|
681
684
|
let timer = null;
|
|
682
685
|
const done = (result) => {
|
|
683
686
|
if (!settled) {
|
|
684
687
|
settled = true;
|
|
685
688
|
if (timer) clearTimeout(timer);
|
|
686
|
-
|
|
689
|
+
resolve2(result);
|
|
687
690
|
}
|
|
688
691
|
};
|
|
689
692
|
let child;
|
|
@@ -765,6 +768,203 @@ async function safe(fn) {
|
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
|
|
771
|
+
// src/bundled-binaries.ts
|
|
772
|
+
import { realpathSync, statSync } from "fs";
|
|
773
|
+
import { createRequire } from "module";
|
|
774
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
775
|
+
import { fileURLToPath } from "url";
|
|
776
|
+
function nodeResolveSelf(specifier) {
|
|
777
|
+
const here = fileURLToPath(import.meta.url);
|
|
778
|
+
if (typeof import.meta.resolve === "function") {
|
|
779
|
+
try {
|
|
780
|
+
return fileURLToPath(import.meta.resolve(specifier));
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
return createRequire(here).resolve(specifier);
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
return walkNodeModules(specifier, here);
|
|
789
|
+
}
|
|
790
|
+
function walkNodeModules(pkg2, fromPath) {
|
|
791
|
+
let dir2 = dirname2(fromPath);
|
|
792
|
+
for (; ; ) {
|
|
793
|
+
const manifest = join3(dir2, "node_modules", pkg2, "package.json");
|
|
794
|
+
if (nodeIsFile(manifest)) {
|
|
795
|
+
try {
|
|
796
|
+
return realpathSync(manifest);
|
|
797
|
+
} catch {
|
|
798
|
+
return manifest;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const parent = dirname2(dir2);
|
|
802
|
+
if (parent === dir2) return null;
|
|
803
|
+
dir2 = parent;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function nodeResolve(specifier, fromPath) {
|
|
807
|
+
try {
|
|
808
|
+
return createRequire(fromPath).resolve(specifier);
|
|
809
|
+
} catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function nodeIsFile(path) {
|
|
814
|
+
try {
|
|
815
|
+
return statSync(path).isFile();
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function detectPreferMusl(platform = process.platform) {
|
|
821
|
+
if (platform !== "linux") return false;
|
|
822
|
+
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
|
|
823
|
+
const header = report?.header;
|
|
824
|
+
return report != null && header?.glibcVersionRuntime === void 0;
|
|
825
|
+
}
|
|
826
|
+
function defaultResolveEnv() {
|
|
827
|
+
return {
|
|
828
|
+
platform: process.platform,
|
|
829
|
+
arch: process.arch,
|
|
830
|
+
preferMusl: detectPreferMusl(),
|
|
831
|
+
// Both resolutions start from this module, which tsup bundles into
|
|
832
|
+
// `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
|
|
833
|
+
// root — so one environment serves the runtime checks and the install
|
|
834
|
+
// verifier alike.
|
|
835
|
+
resolveSelf: nodeResolveSelf,
|
|
836
|
+
resolve: nodeResolve,
|
|
837
|
+
isFile: nodeIsFile
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
|
|
841
|
+
function claudePlatformPackages(platform, arch, preferMusl) {
|
|
842
|
+
if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
|
|
843
|
+
if (platform === "linux") {
|
|
844
|
+
return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
|
|
845
|
+
}
|
|
846
|
+
return [`${CLAUDE_SDK}-${platform}-${arch}`];
|
|
847
|
+
}
|
|
848
|
+
function resolveClaude(env) {
|
|
849
|
+
const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
|
|
850
|
+
const base = {
|
|
851
|
+
runtime: "claude-code",
|
|
852
|
+
packageName: candidates[0] ?? null,
|
|
853
|
+
platform: env.platform,
|
|
854
|
+
arch: env.arch
|
|
855
|
+
};
|
|
856
|
+
const sdkEntry = env.resolveSelf(CLAUDE_SDK);
|
|
857
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
858
|
+
let sawPackage = null;
|
|
859
|
+
for (const pkg2 of candidates) {
|
|
860
|
+
const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
|
|
861
|
+
if (!manifest) continue;
|
|
862
|
+
sawPackage ??= pkg2;
|
|
863
|
+
const exe = join3(dirname2(manifest), env.platform === "win32" ? "claude.exe" : "claude");
|
|
864
|
+
if (env.isFile(exe)) {
|
|
865
|
+
return { ...base, packageName: pkg2, status: "present", path: exe };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
|
|
869
|
+
}
|
|
870
|
+
var CODEX_SDK = "@openai/codex-sdk";
|
|
871
|
+
var CODEX_CLI = "@openai/codex";
|
|
872
|
+
function codexTargetTriple(platform, arch) {
|
|
873
|
+
if (platform === "linux" || platform === "android") {
|
|
874
|
+
if (arch === "x64") return "x86_64-unknown-linux-musl";
|
|
875
|
+
if (arch === "arm64") return "aarch64-unknown-linux-musl";
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
if (platform === "darwin") {
|
|
879
|
+
if (arch === "x64") return "x86_64-apple-darwin";
|
|
880
|
+
if (arch === "arm64") return "aarch64-apple-darwin";
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
if (platform === "win32") {
|
|
884
|
+
if (arch === "x64") return "x86_64-pc-windows-msvc";
|
|
885
|
+
if (arch === "arm64") return "aarch64-pc-windows-msvc";
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
891
|
+
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
892
|
+
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
893
|
+
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
894
|
+
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
895
|
+
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
896
|
+
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
897
|
+
};
|
|
898
|
+
function resolveCodex(env) {
|
|
899
|
+
const triple = codexTargetTriple(env.platform, env.arch);
|
|
900
|
+
const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
|
|
901
|
+
const base = {
|
|
902
|
+
runtime: "codex",
|
|
903
|
+
packageName: pkg2 ?? null,
|
|
904
|
+
platform: env.platform,
|
|
905
|
+
arch: env.arch
|
|
906
|
+
};
|
|
907
|
+
if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
|
|
908
|
+
const sdkEntry = env.resolveSelf(CODEX_SDK);
|
|
909
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
910
|
+
const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
|
|
911
|
+
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
912
|
+
const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
|
|
913
|
+
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
914
|
+
const packageRoot = join3(dirname2(platformManifest), "vendor", triple);
|
|
915
|
+
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
916
|
+
const current = join3(packageRoot, "bin", exeName);
|
|
917
|
+
if (env.isFile(current) && env.isFile(join3(packageRoot, "codex-package.json"))) {
|
|
918
|
+
return { ...base, status: "present", path: current };
|
|
919
|
+
}
|
|
920
|
+
const legacy = join3(packageRoot, "codex", exeName);
|
|
921
|
+
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
922
|
+
return { ...base, status: "executable_missing", path: null };
|
|
923
|
+
}
|
|
924
|
+
function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
|
|
925
|
+
try {
|
|
926
|
+
return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
|
|
927
|
+
} catch {
|
|
928
|
+
return {
|
|
929
|
+
runtime,
|
|
930
|
+
status: "platform_package_missing",
|
|
931
|
+
path: null,
|
|
932
|
+
packageName: null,
|
|
933
|
+
platform: env.platform,
|
|
934
|
+
arch: env.arch
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
var HARNESS_LABEL = {
|
|
939
|
+
"claude-code": "Claude Code",
|
|
940
|
+
codex: "Codex"
|
|
941
|
+
};
|
|
942
|
+
var HARNESS_INTENT_WORD = {
|
|
943
|
+
"claude-code": "connected",
|
|
944
|
+
codex: "enabled"
|
|
945
|
+
};
|
|
946
|
+
function missingNoun(status2) {
|
|
947
|
+
return status2 === "executable_missing" ? "executable" : "platform package";
|
|
948
|
+
}
|
|
949
|
+
function repairCommands() {
|
|
950
|
+
return ["npm i -g @cabane/companion --include=optional"];
|
|
951
|
+
}
|
|
952
|
+
function repairAction() {
|
|
953
|
+
return `Reinstall with \`${repairCommands()[0]}\`.`;
|
|
954
|
+
}
|
|
955
|
+
function startupWarning(r) {
|
|
956
|
+
const label = HARNESS_LABEL[r.runtime];
|
|
957
|
+
return `warning: ${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but @cabane/companion's bundled ${label} ${missingNoun(r.status)} is missing. It will not be offered to Cabane. ${repairAction()} Then restart the companion.`;
|
|
958
|
+
}
|
|
959
|
+
function harnessIssueNote(runtime, status2) {
|
|
960
|
+
const intent = HARNESS_INTENT_WORD[runtime];
|
|
961
|
+
const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
|
|
962
|
+
return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status2)} is missing. ${repairAction()} Then restart the companion.`;
|
|
963
|
+
}
|
|
964
|
+
function turnIncompleteCopy(runtime) {
|
|
965
|
+
return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
|
|
966
|
+
}
|
|
967
|
+
|
|
768
968
|
// src/manifest.ts
|
|
769
969
|
var DEVICE_MANIFEST = {
|
|
770
970
|
runtimes: [{ name: "claude-code", version: null }],
|
|
@@ -776,7 +976,20 @@ function buildCompanionManifest(opts) {
|
|
|
776
976
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
777
977
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
778
978
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
779
|
-
return {
|
|
979
|
+
return {
|
|
980
|
+
runtimes,
|
|
981
|
+
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
982
|
+
runtimeIssues: opts.runtimeIssues ?? []
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function runtimeIssuesFor(gates) {
|
|
986
|
+
const issues = [];
|
|
987
|
+
for (const name of ["claude-code", "codex"]) {
|
|
988
|
+
const gate = name === "codex" ? gates.codex : gates.claudeCode;
|
|
989
|
+
if (!gate.intended || gate.bundled === "present") continue;
|
|
990
|
+
issues.push({ name, reason: gate.bundled });
|
|
991
|
+
}
|
|
992
|
+
return issues;
|
|
780
993
|
}
|
|
781
994
|
|
|
782
995
|
// src/harness-status.ts
|
|
@@ -798,11 +1011,12 @@ function parseHarnessRuntime(raw) {
|
|
|
798
1011
|
function deriveHarnessSnapshot(signals) {
|
|
799
1012
|
const advertised = new Set(
|
|
800
1013
|
buildCompanionManifest({
|
|
801
|
-
// CT1082: connected AND
|
|
802
|
-
// through the same function
|
|
803
|
-
|
|
1014
|
+
// CT1082 + CT1379: connected AND the bundled binary the SDK launches is
|
|
1015
|
+
// there — the manifest's own rule, restated here through the same function
|
|
1016
|
+
// rather than re-decided. PATH is deliberately absent from both gates now.
|
|
1017
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeBundled === "present",
|
|
804
1018
|
opencode: signals.opencodeConfigured,
|
|
805
|
-
codex: signals.codexEnabled
|
|
1019
|
+
codex: signals.codexEnabled && signals.codexBundled === "present"
|
|
806
1020
|
}).runtimes.map((r) => r.name)
|
|
807
1021
|
);
|
|
808
1022
|
const harnesses = [
|
|
@@ -828,7 +1042,7 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
828
1042
|
...base,
|
|
829
1043
|
state: "needs_attention",
|
|
830
1044
|
version: null,
|
|
831
|
-
detail: "
|
|
1045
|
+
detail: harnessIssueNote("claude-code", signals.claudeBundled),
|
|
832
1046
|
enable: null
|
|
833
1047
|
};
|
|
834
1048
|
}
|
|
@@ -837,7 +1051,11 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
837
1051
|
...base,
|
|
838
1052
|
state: "detected_not_exposed",
|
|
839
1053
|
version: signals.claudeVersion,
|
|
840
|
-
|
|
1054
|
+
// CT1379: connecting records INTENT; it is not the same as becoming
|
|
1055
|
+
// runnable. With the bundled binary missing, a connect succeeds and the
|
|
1056
|
+
// device still advertises nothing — so this says so up front rather than
|
|
1057
|
+
// promising routing it can't deliver.
|
|
1058
|
+
detail: signals.claudeBundled === "present" ? "Claude Code is installed here but not connected yet. Connect it to offer Claude Code from this device." : `Claude Code is installed here but not connected yet \u2014 and @cabane/companion's bundled Claude Code ${missingNoun(signals.claudeBundled)} is missing, so connecting alone won't make it runnable. ${repairAction()}`,
|
|
841
1059
|
enable: "claude-code"
|
|
842
1060
|
};
|
|
843
1061
|
}
|
|
@@ -852,20 +1070,20 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
852
1070
|
function deriveCodex(signals, manifestHas) {
|
|
853
1071
|
const base = { runtime: "codex", label: LABELS.codex };
|
|
854
1072
|
if (manifestHas) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1073
|
+
return {
|
|
1074
|
+
...base,
|
|
1075
|
+
state: "exposed",
|
|
1076
|
+
version: signals.codexVersion,
|
|
1077
|
+
detail: "Codex is enabled and exposed to Cabane.",
|
|
1078
|
+
enable: null
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (signals.codexEnabled) {
|
|
864
1082
|
return {
|
|
865
1083
|
...base,
|
|
866
1084
|
state: "needs_attention",
|
|
867
1085
|
version: null,
|
|
868
|
-
detail: "
|
|
1086
|
+
detail: harnessIssueNote("codex", signals.codexBundled),
|
|
869
1087
|
enable: null
|
|
870
1088
|
};
|
|
871
1089
|
}
|
|
@@ -874,7 +1092,9 @@ function deriveCodex(signals, manifestHas) {
|
|
|
874
1092
|
...base,
|
|
875
1093
|
state: "detected_not_exposed",
|
|
876
1094
|
version: signals.codexVersion,
|
|
877
|
-
|
|
1095
|
+
// CT1379: same distinction as Claude Code — enabling is intent, not
|
|
1096
|
+
// launchability.
|
|
1097
|
+
detail: signals.codexBundled === "present" ? "Codex is installed but not enabled yet. Turn it on to offer Codex from this device." : `Codex is installed but not enabled yet \u2014 and @cabane/companion's bundled Codex ${missingNoun(signals.codexBundled)} is missing, so enabling alone won't make it runnable. ${repairAction()}`,
|
|
878
1098
|
enable: "codex"
|
|
879
1099
|
};
|
|
880
1100
|
}
|
|
@@ -941,6 +1161,7 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
|
|
|
941
1161
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
942
1162
|
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
943
1163
|
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1164
|
+
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
944
1165
|
const configuredServerUrl = cfg.opencode?.serverUrl;
|
|
945
1166
|
const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
|
|
946
1167
|
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
@@ -956,6 +1177,11 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
956
1177
|
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
957
1178
|
// the manifest gate and the probe above is only a suggestion.
|
|
958
1179
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
1180
|
+
// CT1379: the executable a Claude turn actually launches. Resolved every time
|
|
1181
|
+
// the signals are, so a binary that disappears between beats is caught on the
|
|
1182
|
+
// next one rather than at turn time.
|
|
1183
|
+
claudeBundled: resolveBundled("claude-code").status,
|
|
1184
|
+
codexBundled: resolveBundled("codex").status,
|
|
959
1185
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
960
1186
|
// exposes codex; its config flag is the manifest gate either way).
|
|
961
1187
|
codexOnPath: codexVersion !== null,
|
|
@@ -969,12 +1195,12 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
969
1195
|
};
|
|
970
1196
|
}
|
|
971
1197
|
function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
972
|
-
return new Promise((
|
|
1198
|
+
return new Promise((resolve2) => {
|
|
973
1199
|
let settled = false;
|
|
974
1200
|
const done = (v) => {
|
|
975
1201
|
if (!settled) {
|
|
976
1202
|
settled = true;
|
|
977
|
-
|
|
1203
|
+
resolve2(v);
|
|
978
1204
|
}
|
|
979
1205
|
};
|
|
980
1206
|
const timer = setTimeout(() => done(fallback), timeoutMs);
|
|
@@ -1052,19 +1278,19 @@ function looksUnsupported(output) {
|
|
|
1052
1278
|
);
|
|
1053
1279
|
}
|
|
1054
1280
|
function runBounded(command, args) {
|
|
1055
|
-
return new Promise((
|
|
1281
|
+
return new Promise((resolve2) => {
|
|
1056
1282
|
let settled = false;
|
|
1057
1283
|
const done = (result) => {
|
|
1058
1284
|
if (settled) return;
|
|
1059
1285
|
settled = true;
|
|
1060
1286
|
clearTimeout(timer);
|
|
1061
|
-
|
|
1287
|
+
resolve2(result);
|
|
1062
1288
|
};
|
|
1063
1289
|
let child;
|
|
1064
1290
|
try {
|
|
1065
1291
|
child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1066
1292
|
} catch {
|
|
1067
|
-
|
|
1293
|
+
resolve2({ code: null, output: "", error: "spawn" });
|
|
1068
1294
|
return;
|
|
1069
1295
|
}
|
|
1070
1296
|
let out = "";
|
|
@@ -1093,10 +1319,10 @@ import {
|
|
|
1093
1319
|
openSync,
|
|
1094
1320
|
closeSync
|
|
1095
1321
|
} from "fs";
|
|
1096
|
-
import { join as
|
|
1322
|
+
import { join as join4 } from "path";
|
|
1097
1323
|
var PROBE_TIMEOUT_MS2 = 1e3;
|
|
1098
1324
|
function runtimePath() {
|
|
1099
|
-
return
|
|
1325
|
+
return join4(cabaneDir(), "runtime.json");
|
|
1100
1326
|
}
|
|
1101
1327
|
function serialize(state) {
|
|
1102
1328
|
return JSON.stringify(state, null, 2) + "\n";
|
|
@@ -1255,8 +1481,8 @@ var Spinner = class {
|
|
|
1255
1481
|
async function confirm(question) {
|
|
1256
1482
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1257
1483
|
try {
|
|
1258
|
-
const answer = await new Promise((
|
|
1259
|
-
rl.question(`${INDENT}${question} [Y/n] `,
|
|
1484
|
+
const answer = await new Promise((resolve2) => {
|
|
1485
|
+
rl.question(`${INDENT}${question} [Y/n] `, resolve2);
|
|
1260
1486
|
});
|
|
1261
1487
|
return !/^n(o)?$/i.test(answer.trim());
|
|
1262
1488
|
} finally {
|
|
@@ -1378,10 +1604,10 @@ import {
|
|
|
1378
1604
|
rmSync as rmSync4,
|
|
1379
1605
|
writeFileSync as writeFileSync3
|
|
1380
1606
|
} from "fs";
|
|
1381
|
-
import { dirname as
|
|
1607
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1382
1608
|
import { z as z3 } from "zod";
|
|
1383
1609
|
function credentialsPath() {
|
|
1384
|
-
return
|
|
1610
|
+
return join5(cabaneDir(), "credentials.json");
|
|
1385
1611
|
}
|
|
1386
1612
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1387
1613
|
function load() {
|
|
@@ -1403,7 +1629,7 @@ function load() {
|
|
|
1403
1629
|
}
|
|
1404
1630
|
function save(map) {
|
|
1405
1631
|
const path = credentialsPath();
|
|
1406
|
-
mkdirSync4(
|
|
1632
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
1407
1633
|
try {
|
|
1408
1634
|
chmodSync3(cabaneDir(), 448);
|
|
1409
1635
|
} catch {
|
|
@@ -1548,7 +1774,7 @@ function deviceLabelFromHostname(hostname3) {
|
|
|
1548
1774
|
function pollOnce(baseUrl, deviceCode) {
|
|
1549
1775
|
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode });
|
|
1550
1776
|
}
|
|
1551
|
-
var sleep = (ms) => new Promise((
|
|
1777
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1552
1778
|
async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
1553
1779
|
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
1554
1780
|
const code = await requestEnrollmentCode(baseUrl, {
|
|
@@ -1674,11 +1900,11 @@ import { hostname as hostname2 } from "os";
|
|
|
1674
1900
|
|
|
1675
1901
|
// src/logger.ts
|
|
1676
1902
|
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
1677
|
-
import { dirname as
|
|
1903
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1678
1904
|
import pino from "pino";
|
|
1679
1905
|
import pretty from "pino-pretty";
|
|
1680
1906
|
function companionLogPath() {
|
|
1681
|
-
return
|
|
1907
|
+
return join6(cabaneDir(), "companion.log");
|
|
1682
1908
|
}
|
|
1683
1909
|
var CONSOLE_IGNORE = [
|
|
1684
1910
|
"pid",
|
|
@@ -1706,7 +1932,7 @@ function setConsoleLogging(enabled) {
|
|
|
1706
1932
|
}
|
|
1707
1933
|
function createLogger(destinations = {}) {
|
|
1708
1934
|
const path = companionLogPath();
|
|
1709
|
-
if (!destinations.file) mkdirSync5(
|
|
1935
|
+
if (!destinations.file) mkdirSync5(dirname4(path), { recursive: true });
|
|
1710
1936
|
const streams = [];
|
|
1711
1937
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
1712
1938
|
const consoleStream = pretty({
|
|
@@ -1740,15 +1966,15 @@ function getLogger() {
|
|
|
1740
1966
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1741
1967
|
|
|
1742
1968
|
// src/dashboard/server.ts
|
|
1743
|
-
import { dirname as
|
|
1744
|
-
import { fileURLToPath } from "url";
|
|
1969
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1970
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1745
1971
|
import { serve } from "@hono/node-server";
|
|
1746
1972
|
import { Hono } from "hono";
|
|
1747
1973
|
|
|
1748
1974
|
// src/dashboard/routes.ts
|
|
1749
1975
|
import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync5 } from "fs";
|
|
1750
1976
|
import { readFile } from "fs/promises";
|
|
1751
|
-
import { extname, join as
|
|
1977
|
+
import { extname, join as join7, normalize } from "path";
|
|
1752
1978
|
import { streamSSE } from "hono/streaming";
|
|
1753
1979
|
|
|
1754
1980
|
// src/state.ts
|
|
@@ -1997,13 +2223,13 @@ var CONTENT_TYPES = {
|
|
|
1997
2223
|
function registerRoutes(app, deps) {
|
|
1998
2224
|
const { supervisor, hub, staticDir } = deps;
|
|
1999
2225
|
app.get("/", async (c) => {
|
|
2000
|
-
const html = await readFile(
|
|
2226
|
+
const html = await readFile(join7(staticDir, "index.html"), "utf8");
|
|
2001
2227
|
return c.html(html);
|
|
2002
2228
|
});
|
|
2003
2229
|
app.get("/static/:file", async (c) => {
|
|
2004
2230
|
const file = c.req.param("file");
|
|
2005
2231
|
const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
2006
|
-
const full =
|
|
2232
|
+
const full = join7(staticDir, safe3);
|
|
2007
2233
|
if (!full.startsWith(staticDir) || !existsSync5(full)) return c.notFound();
|
|
2008
2234
|
const body = await readFile(full);
|
|
2009
2235
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
@@ -2103,8 +2329,8 @@ function registerRoutes(app, deps) {
|
|
|
2103
2329
|
while (!stream.aborted) {
|
|
2104
2330
|
if (queue.length === 0) {
|
|
2105
2331
|
await Promise.race([
|
|
2106
|
-
new Promise((
|
|
2107
|
-
wake =
|
|
2332
|
+
new Promise((resolve2) => {
|
|
2333
|
+
wake = resolve2;
|
|
2108
2334
|
}),
|
|
2109
2335
|
stream.sleep(25e3)
|
|
2110
2336
|
]);
|
|
@@ -2188,8 +2414,8 @@ async function startDashboard(opts) {
|
|
|
2188
2414
|
return {
|
|
2189
2415
|
url,
|
|
2190
2416
|
port,
|
|
2191
|
-
close: () => new Promise((
|
|
2192
|
-
server.close(() =>
|
|
2417
|
+
close: () => new Promise((resolve2) => {
|
|
2418
|
+
server.close(() => resolve2());
|
|
2193
2419
|
server.closeAllConnections?.();
|
|
2194
2420
|
})
|
|
2195
2421
|
};
|
|
@@ -2206,12 +2432,12 @@ async function startDashboard(opts) {
|
|
|
2206
2432
|
);
|
|
2207
2433
|
}
|
|
2208
2434
|
function listen(app, port) {
|
|
2209
|
-
return new Promise((
|
|
2435
|
+
return new Promise((resolve2, reject) => {
|
|
2210
2436
|
let settled = false;
|
|
2211
2437
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => {
|
|
2212
2438
|
if (!settled) {
|
|
2213
2439
|
settled = true;
|
|
2214
|
-
|
|
2440
|
+
resolve2(server);
|
|
2215
2441
|
}
|
|
2216
2442
|
});
|
|
2217
2443
|
server.on("error", (err) => {
|
|
@@ -2226,7 +2452,7 @@ function isAddrInUse(err) {
|
|
|
2226
2452
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
2227
2453
|
}
|
|
2228
2454
|
function resolveStaticDir() {
|
|
2229
|
-
return
|
|
2455
|
+
return join8(dirname5(fileURLToPath2(import.meta.url)), "static");
|
|
2230
2456
|
}
|
|
2231
2457
|
|
|
2232
2458
|
// src/prereqs.ts
|
|
@@ -2251,6 +2477,7 @@ async function requireStartConfig(deps = {}) {
|
|
|
2251
2477
|
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
2252
2478
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
2253
2479
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
2480
|
+
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
2254
2481
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
2255
2482
|
`));
|
|
2256
2483
|
const connected = [
|
|
@@ -2259,25 +2486,42 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
2259
2486
|
...cfg.opencode ? ["opencode"] : []
|
|
2260
2487
|
];
|
|
2261
2488
|
if (connected.length > 0) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
);
|
|
2489
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
2490
|
+
const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
|
|
2491
|
+
if (!intended) continue;
|
|
2492
|
+
const resolution = resolveBundled(runtime);
|
|
2493
|
+
if (resolution.status !== "present") warn(startupWarning(resolution));
|
|
2266
2494
|
}
|
|
2267
2495
|
return;
|
|
2268
2496
|
}
|
|
2269
2497
|
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
2270
|
-
const
|
|
2271
|
-
...claudeInstalled ? [
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2498
|
+
const found = [
|
|
2499
|
+
...claudeInstalled ? [
|
|
2500
|
+
{
|
|
2501
|
+
label: "Claude Code",
|
|
2502
|
+
command: "`cabane-companion connect claude-code`",
|
|
2503
|
+
bundled: resolveBundled("claude-code").status
|
|
2504
|
+
}
|
|
2505
|
+
] : [],
|
|
2506
|
+
...codexInstalled ? [
|
|
2507
|
+
{
|
|
2508
|
+
label: "Codex",
|
|
2509
|
+
command: "`cabane-companion connect codex`",
|
|
2510
|
+
bundled: resolveBundled("codex").status
|
|
2511
|
+
}
|
|
2512
|
+
] : []
|
|
2277
2513
|
];
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2514
|
+
const runnable = found.filter((h) => h.bundled === "present");
|
|
2515
|
+
const incomplete = found.filter((h) => h.bundled !== "present");
|
|
2516
|
+
const incompleteNote = incomplete.map(
|
|
2517
|
+
(h) => `${h.label} is installed here, but @cabane/companion's bundled ${h.label} ${missingNoun(h.bundled)} is missing, so connecting it alone won't make it runnable.`
|
|
2518
|
+
).join(" ");
|
|
2519
|
+
const guidance = runnable.length > 0 ? `We found ${runnable.map((h) => h.label).join(" and ")} on this machine \u2014 connect ${runnable.length > 1 ? "one with " : "it with "}${runnable.map((h) => h.command).join(" or ")} and turns start routing here.${incompleteNote ? ` ${incompleteNote} ${repairAction()}` : ""}` : incomplete.length > 0 ? (
|
|
2520
|
+
// Their machine is fine; this companion's install is not. Repair that,
|
|
2521
|
+
// then connect — no "install a harness" they already have.
|
|
2522
|
+
`${incompleteNote} ${repairAction()} Then connect it with ${incomplete.map((h) => h.command).join(" or ")}.`
|
|
2523
|
+
) : "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).";
|
|
2524
|
+
warn(`No harness is connected on this device yet, so no agent turn can run here. ${guidance}`);
|
|
2281
2525
|
}
|
|
2282
2526
|
|
|
2283
2527
|
// src/supervisor.ts
|
|
@@ -2755,15 +2999,15 @@ function isAbortError(err) {
|
|
|
2755
2999
|
return err instanceof Error && err.name === "AbortError";
|
|
2756
3000
|
}
|
|
2757
3001
|
function sleep2(ms, signal) {
|
|
2758
|
-
return new Promise((
|
|
2759
|
-
if (signal?.aborted) return
|
|
3002
|
+
return new Promise((resolve2) => {
|
|
3003
|
+
if (signal?.aborted) return resolve2();
|
|
2760
3004
|
const timer = setTimeout(() => {
|
|
2761
3005
|
signal?.removeEventListener("abort", onAbort);
|
|
2762
|
-
|
|
3006
|
+
resolve2();
|
|
2763
3007
|
}, ms);
|
|
2764
3008
|
const onAbort = () => {
|
|
2765
3009
|
clearTimeout(timer);
|
|
2766
|
-
|
|
3010
|
+
resolve2();
|
|
2767
3011
|
};
|
|
2768
3012
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2769
3013
|
});
|
|
@@ -2846,9 +3090,9 @@ function errorMessage2(status2, body) {
|
|
|
2846
3090
|
|
|
2847
3091
|
// src/cursor.ts
|
|
2848
3092
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2849
|
-
import { join as
|
|
3093
|
+
import { join as join9 } from "path";
|
|
2850
3094
|
function pathFor(workspaceId) {
|
|
2851
|
-
return
|
|
3095
|
+
return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
2852
3096
|
}
|
|
2853
3097
|
function readCursor(workspaceId) {
|
|
2854
3098
|
const path = pathFor(workspaceId);
|
|
@@ -2858,7 +3102,7 @@ function readCursor(workspaceId) {
|
|
|
2858
3102
|
}
|
|
2859
3103
|
function writeCursor(workspaceId, eventId) {
|
|
2860
3104
|
const path = pathFor(workspaceId);
|
|
2861
|
-
mkdirSync6(
|
|
3105
|
+
mkdirSync6(join9(cabaneDir(), "cursors"), { recursive: true });
|
|
2862
3106
|
writeFileSync4(path, eventId + "\n", "utf8");
|
|
2863
3107
|
}
|
|
2864
3108
|
|
|
@@ -2903,13 +3147,13 @@ var CursorTracker = class {
|
|
|
2903
3147
|
|
|
2904
3148
|
// src/dispatch-dedupe.ts
|
|
2905
3149
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
|
|
2906
|
-
import { join as
|
|
3150
|
+
import { join as join10 } from "path";
|
|
2907
3151
|
var MAX_IDS = 256;
|
|
2908
3152
|
function dir(log) {
|
|
2909
|
-
return
|
|
3153
|
+
return join10(cabaneDir(), log);
|
|
2910
3154
|
}
|
|
2911
3155
|
function pathFor2(log, workspaceId) {
|
|
2912
|
-
return
|
|
3156
|
+
return join10(dir(log), encodeURIComponent(workspaceId));
|
|
2913
3157
|
}
|
|
2914
3158
|
function readIds(log, workspaceId) {
|
|
2915
3159
|
const path = pathFor2(log, workspaceId);
|
|
@@ -2946,10 +3190,10 @@ function markCompleted(workspaceId, eventId) {
|
|
|
2946
3190
|
}
|
|
2947
3191
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
2948
3192
|
function resumeDir() {
|
|
2949
|
-
return
|
|
3193
|
+
return join10(cabaneDir(), "resume-attempts");
|
|
2950
3194
|
}
|
|
2951
3195
|
function resumePathFor(workspaceId) {
|
|
2952
|
-
return
|
|
3196
|
+
return join10(resumeDir(), encodeURIComponent(workspaceId));
|
|
2953
3197
|
}
|
|
2954
3198
|
function readResumeCounts(workspaceId) {
|
|
2955
3199
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -2985,10 +3229,10 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
2985
3229
|
return next;
|
|
2986
3230
|
}
|
|
2987
3231
|
function turnDir() {
|
|
2988
|
-
return
|
|
3232
|
+
return join10(cabaneDir(), "turns");
|
|
2989
3233
|
}
|
|
2990
3234
|
function turnPathFor(workspaceId) {
|
|
2991
|
-
return
|
|
3235
|
+
return join10(turnDir(), encodeURIComponent(workspaceId));
|
|
2992
3236
|
}
|
|
2993
3237
|
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2994
3238
|
function readTurnIds(workspaceId) {
|
|
@@ -5363,7 +5607,7 @@ var serverTurnTails = /* @__PURE__ */ new Map();
|
|
|
5363
5607
|
async function acquireServerTurnLock(url) {
|
|
5364
5608
|
const prev = serverTurnTails.get(url) ?? Promise.resolve();
|
|
5365
5609
|
let release;
|
|
5366
|
-
const done = new Promise((
|
|
5610
|
+
const done = new Promise((resolve2) => release = resolve2);
|
|
5367
5611
|
serverTurnTails.set(url, done);
|
|
5368
5612
|
await prev.catch(() => {
|
|
5369
5613
|
});
|
|
@@ -7602,8 +7846,8 @@ var ConnectorHealthStore = class {
|
|
|
7602
7846
|
};
|
|
7603
7847
|
|
|
7604
7848
|
// src/dispatcher.ts
|
|
7605
|
-
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
7606
|
-
import { join as
|
|
7849
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7850
|
+
import { join as join15 } from "path";
|
|
7607
7851
|
|
|
7608
7852
|
// src/turn-execution.ts
|
|
7609
7853
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
@@ -7965,11 +8209,11 @@ function trimSlash2(s) {
|
|
|
7965
8209
|
// src/codex-instructions.ts
|
|
7966
8210
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7967
8211
|
import { tmpdir } from "os";
|
|
7968
|
-
import { join as
|
|
8212
|
+
import { join as join11 } from "path";
|
|
7969
8213
|
var PREFIX = "cabane-codex-instructions-";
|
|
7970
8214
|
async function writeCodexInstructionsFile(contents) {
|
|
7971
|
-
const dir2 = await mkdtemp(
|
|
7972
|
-
const path =
|
|
8215
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
8216
|
+
const path = join11(dir2, "instructions.md");
|
|
7973
8217
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7974
8218
|
return {
|
|
7975
8219
|
path,
|
|
@@ -7979,6 +8223,42 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7979
8223
|
};
|
|
7980
8224
|
}
|
|
7981
8225
|
|
|
8226
|
+
// src/api-error-shape.ts
|
|
8227
|
+
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
8228
|
+
"dispatch_not_admitted",
|
|
8229
|
+
"turn_already_ended",
|
|
8230
|
+
"turn_belongs_elsewhere"
|
|
8231
|
+
]);
|
|
8232
|
+
function apiErrorCode(err) {
|
|
8233
|
+
if (!(err instanceof ApiError)) return null;
|
|
8234
|
+
const body = err.body;
|
|
8235
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
8236
|
+
}
|
|
8237
|
+
function leaseRefusal(err) {
|
|
8238
|
+
const code = apiErrorCode(err);
|
|
8239
|
+
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
8240
|
+
return null;
|
|
8241
|
+
}
|
|
8242
|
+
function isWriteFenceRefusal(err) {
|
|
8243
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
8244
|
+
}
|
|
8245
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
8246
|
+
function describeErrorBody(body) {
|
|
8247
|
+
if (body === void 0 || body === null) return void 0;
|
|
8248
|
+
let text;
|
|
8249
|
+
if (typeof body === "string") {
|
|
8250
|
+
text = body;
|
|
8251
|
+
} else {
|
|
8252
|
+
try {
|
|
8253
|
+
text = JSON.stringify(body);
|
|
8254
|
+
} catch {
|
|
8255
|
+
text = String(body);
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
if (text.length === 0) return void 0;
|
|
8259
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8260
|
+
}
|
|
8261
|
+
|
|
7982
8262
|
// src/turn-seq-floor.ts
|
|
7983
8263
|
var SeqFloorUnavailable = class extends Error {
|
|
7984
8264
|
constructor(detail) {
|
|
@@ -8014,15 +8294,15 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8014
8294
|
|
|
8015
8295
|
// src/prepared.ts
|
|
8016
8296
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
8017
|
-
import { join as
|
|
8297
|
+
import { join as join12 } from "path";
|
|
8018
8298
|
function dirFor(workspaceId) {
|
|
8019
|
-
return
|
|
8299
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
8020
8300
|
}
|
|
8021
8301
|
function conversationDir(workspaceId, conversationId) {
|
|
8022
|
-
return
|
|
8302
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8023
8303
|
}
|
|
8024
8304
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
8025
|
-
return
|
|
8305
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8026
8306
|
}
|
|
8027
8307
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8028
8308
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
@@ -8054,11 +8334,11 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
8054
8334
|
|
|
8055
8335
|
// src/secrets.ts
|
|
8056
8336
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
8057
|
-
import { join as
|
|
8337
|
+
import { join as join13 } from "path";
|
|
8058
8338
|
import { z as z14 } from "zod";
|
|
8059
8339
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8060
8340
|
function secretsPath() {
|
|
8061
|
-
return
|
|
8341
|
+
return join13(cabaneDir(), "secrets.json");
|
|
8062
8342
|
}
|
|
8063
8343
|
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8064
8344
|
function loadSecretStore() {
|
|
@@ -8145,9 +8425,9 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
8145
8425
|
|
|
8146
8426
|
// src/transcript-writer.ts
|
|
8147
8427
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8148
|
-
import { basename, dirname as
|
|
8428
|
+
import { basename, dirname as dirname6, join as join14 } from "path";
|
|
8149
8429
|
function transcriptsDir() {
|
|
8150
|
-
return
|
|
8430
|
+
return join14(cabaneDir(), "transcripts");
|
|
8151
8431
|
}
|
|
8152
8432
|
var RETAIN = 200;
|
|
8153
8433
|
var ANOMALY_RETAIN = 50;
|
|
@@ -8157,7 +8437,7 @@ var TranscriptWriter = class {
|
|
|
8157
8437
|
onWarn;
|
|
8158
8438
|
constructor(dir2, meta, onWarn) {
|
|
8159
8439
|
this.onWarn = onWarn;
|
|
8160
|
-
this.path =
|
|
8440
|
+
this.path = join14(dir2, fileName(meta));
|
|
8161
8441
|
try {
|
|
8162
8442
|
mkdirSync9(dir2, { recursive: true });
|
|
8163
8443
|
try {
|
|
@@ -8186,10 +8466,10 @@ var TranscriptWriter = class {
|
|
|
8186
8466
|
preserveAnomaly() {
|
|
8187
8467
|
if (this.broken) return;
|
|
8188
8468
|
try {
|
|
8189
|
-
const dir2 =
|
|
8469
|
+
const dir2 = join14(dirname6(this.path), "anomalies");
|
|
8190
8470
|
mkdirSync9(dir2, { recursive: true, mode: 448 });
|
|
8191
8471
|
chmodSync4(dir2, 448);
|
|
8192
|
-
const target =
|
|
8472
|
+
const target = join14(dir2, basename(this.path));
|
|
8193
8473
|
copyFileSync(this.path, target);
|
|
8194
8474
|
chmodSync4(target, 384);
|
|
8195
8475
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -8235,7 +8515,7 @@ function pruneOld(dir2, retain) {
|
|
|
8235
8515
|
const drop = files.sort().slice(0, files.length - retain);
|
|
8236
8516
|
for (const f of drop) {
|
|
8237
8517
|
try {
|
|
8238
|
-
rmSync6(
|
|
8518
|
+
rmSync6(join14(dir2, f), { force: true });
|
|
8239
8519
|
} catch {
|
|
8240
8520
|
}
|
|
8241
8521
|
}
|
|
@@ -8382,6 +8662,84 @@ var TurnCommitter = class {
|
|
|
8382
8662
|
}
|
|
8383
8663
|
};
|
|
8384
8664
|
|
|
8665
|
+
// src/turn-runtime-integrity.ts
|
|
8666
|
+
function bundledHarnessFor(runtime) {
|
|
8667
|
+
return runtime === "claude-code" || runtime === "codex" ? runtime : null;
|
|
8668
|
+
}
|
|
8669
|
+
function resolve(ctx, harness) {
|
|
8670
|
+
return (ctx.resolveBundled ?? resolveBundledBinary)(harness);
|
|
8671
|
+
}
|
|
8672
|
+
async function postIncompleteNotice(ctx, harness, resolution) {
|
|
8673
|
+
try {
|
|
8674
|
+
await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
|
|
8675
|
+
body: turnIncompleteCopy(harness),
|
|
8676
|
+
kind: "final",
|
|
8677
|
+
turnId: ctx.turnId,
|
|
8678
|
+
parentMessageId: ctx.parentMessageId
|
|
8679
|
+
});
|
|
8680
|
+
return true;
|
|
8681
|
+
} catch (err) {
|
|
8682
|
+
ctx.log.warn(
|
|
8683
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8684
|
+
"dispatcher: runtime-incomplete notice post failed"
|
|
8685
|
+
);
|
|
8686
|
+
return false;
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
async function checkBundledBinary(ctx) {
|
|
8690
|
+
const harness = bundledHarnessFor(ctx.runtime);
|
|
8691
|
+
if (!harness) return null;
|
|
8692
|
+
const resolution = resolve(ctx, harness);
|
|
8693
|
+
if (resolution.status === "present") return null;
|
|
8694
|
+
ctx.log.error(
|
|
8695
|
+
{
|
|
8696
|
+
runtime: harness,
|
|
8697
|
+
status: resolution.status,
|
|
8698
|
+
platform: resolution.platform,
|
|
8699
|
+
arch: resolution.arch
|
|
8700
|
+
},
|
|
8701
|
+
"dispatcher: bundled harness binary missing at dispatch"
|
|
8702
|
+
);
|
|
8703
|
+
return {
|
|
8704
|
+
harness,
|
|
8705
|
+
reason: `runtime_incomplete:${harness}`,
|
|
8706
|
+
posted: await postIncompleteNotice(ctx, harness, resolution)
|
|
8707
|
+
};
|
|
8708
|
+
}
|
|
8709
|
+
async function reclassifyThrow(ctx, opts) {
|
|
8710
|
+
const harness = bundledHarnessFor(ctx.runtime);
|
|
8711
|
+
if (!harness || opts.aborted) return null;
|
|
8712
|
+
const resolution = resolve(ctx, harness);
|
|
8713
|
+
if (resolution.status === "present") return null;
|
|
8714
|
+
ctx.log.error(
|
|
8715
|
+
{
|
|
8716
|
+
runtime: harness,
|
|
8717
|
+
status: resolution.status,
|
|
8718
|
+
platform: resolution.platform,
|
|
8719
|
+
arch: resolution.arch,
|
|
8720
|
+
originalReason: opts.originalReason
|
|
8721
|
+
},
|
|
8722
|
+
"dispatcher: adapter threw and the bundled harness binary is missing \u2014 reporting the incomplete runtime"
|
|
8723
|
+
);
|
|
8724
|
+
return {
|
|
8725
|
+
harness,
|
|
8726
|
+
reason: `runtime_incomplete:${harness}`,
|
|
8727
|
+
posted: await postIncompleteNotice(ctx, harness, resolution)
|
|
8728
|
+
};
|
|
8729
|
+
}
|
|
8730
|
+
async function guardBundledBinary(ctx, concluded, outcome) {
|
|
8731
|
+
const incomplete = await checkBundledBinary(ctx);
|
|
8732
|
+
if (!incomplete) return;
|
|
8733
|
+
outcome.runtimeIncomplete = incomplete.posted;
|
|
8734
|
+
throw incomplete.posted ? concluded(incomplete.reason) : concluded(incomplete.reason, incomplete.reason);
|
|
8735
|
+
}
|
|
8736
|
+
async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
8737
|
+
const lost = await reclassifyThrow(ctx, { aborted, originalReason: outcome.resultReason });
|
|
8738
|
+
if (!lost) return false;
|
|
8739
|
+
outcome.resultReason = lost.reason;
|
|
8740
|
+
return lost.posted;
|
|
8741
|
+
}
|
|
8742
|
+
|
|
8385
8743
|
// src/turn-execution.ts
|
|
8386
8744
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8387
8745
|
var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
|
|
@@ -8394,40 +8752,6 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
8394
8752
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8395
8753
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
8396
8754
|
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
8397
|
-
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
8398
|
-
"dispatch_not_admitted",
|
|
8399
|
-
"turn_already_ended",
|
|
8400
|
-
"turn_belongs_elsewhere"
|
|
8401
|
-
]);
|
|
8402
|
-
function apiErrorCode(err) {
|
|
8403
|
-
if (!(err instanceof ApiError)) return null;
|
|
8404
|
-
const body = err.body;
|
|
8405
|
-
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
8406
|
-
}
|
|
8407
|
-
function leaseRefusal(err) {
|
|
8408
|
-
const code = apiErrorCode(err);
|
|
8409
|
-
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
8410
|
-
return null;
|
|
8411
|
-
}
|
|
8412
|
-
function isWriteFenceRefusal(err) {
|
|
8413
|
-
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
8414
|
-
}
|
|
8415
|
-
var ERROR_BODY_LOG_CAP = 2e3;
|
|
8416
|
-
function describeErrorBody(body) {
|
|
8417
|
-
if (body === void 0 || body === null) return void 0;
|
|
8418
|
-
let text;
|
|
8419
|
-
if (typeof body === "string") {
|
|
8420
|
-
text = body;
|
|
8421
|
-
} else {
|
|
8422
|
-
try {
|
|
8423
|
-
text = JSON.stringify(body);
|
|
8424
|
-
} catch {
|
|
8425
|
-
text = String(body);
|
|
8426
|
-
}
|
|
8427
|
-
}
|
|
8428
|
-
if (text.length === 0) return void 0;
|
|
8429
|
-
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8430
|
-
}
|
|
8431
8755
|
function initialOutcome() {
|
|
8432
8756
|
return {
|
|
8433
8757
|
sessionWritten: false,
|
|
@@ -8446,7 +8770,8 @@ function initialOutcome() {
|
|
|
8446
8770
|
settledDiagnostics: null,
|
|
8447
8771
|
silentMarkerEmitted: false,
|
|
8448
8772
|
timeoutReason: null,
|
|
8449
|
-
leaseLost: false
|
|
8773
|
+
leaseLost: false,
|
|
8774
|
+
runtimeIncomplete: false
|
|
8450
8775
|
};
|
|
8451
8776
|
}
|
|
8452
8777
|
var TurnConcluded = class {
|
|
@@ -8597,7 +8922,9 @@ var TurnExecution = class {
|
|
|
8597
8922
|
activeRunStartedAt: null,
|
|
8598
8923
|
turnId,
|
|
8599
8924
|
settledMessageId: payload.messageId,
|
|
8600
|
-
|
|
8925
|
+
// CT1379: `runtimeIncomplete` is a real failure that deliberately carries
|
|
8926
|
+
// no `errorReason`, so the inference alone would read it as settled.
|
|
8927
|
+
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
8601
8928
|
};
|
|
8602
8929
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
8603
8930
|
try {
|
|
@@ -8937,8 +9264,21 @@ ${reason}`,
|
|
|
8937
9264
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
8938
9265
|
});
|
|
8939
9266
|
}
|
|
9267
|
+
integrityContext() {
|
|
9268
|
+
return {
|
|
9269
|
+
runtime: this.turnContext.runtime,
|
|
9270
|
+
workspaceId: this.workspaceId,
|
|
9271
|
+
conversationId: this.payload.conversationId,
|
|
9272
|
+
turnId: this.turnId,
|
|
9273
|
+
parentMessageId: this.payload.messageId,
|
|
9274
|
+
log: this.turnLog,
|
|
9275
|
+
postTurnMessage: (workspaceId, conversationId, body) => this.opts.api.postTurnMessage(workspaceId, conversationId, body),
|
|
9276
|
+
...this.opts.resolveBundled ? { resolveBundled: this.opts.resolveBundled } : {}
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
8940
9279
|
async selectAdapter() {
|
|
8941
9280
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9281
|
+
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
8942
9282
|
const onWarn = (msg, meta) => turnLog.warn(meta ?? {}, msg);
|
|
8943
9283
|
const adapters = [];
|
|
8944
9284
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
@@ -8947,7 +9287,7 @@ ${reason}`,
|
|
|
8947
9287
|
if (this.opts.opencodeServerUrl) {
|
|
8948
9288
|
adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
|
|
8949
9289
|
}
|
|
8950
|
-
if (this.opts.
|
|
9290
|
+
if (this.opts.codexAvailable?.() ?? false) {
|
|
8951
9291
|
adapters.push(
|
|
8952
9292
|
createCodexAdapter({
|
|
8953
9293
|
enabled: true,
|
|
@@ -9231,6 +9571,11 @@ ${reason}`,
|
|
|
9231
9571
|
o.okResult = false;
|
|
9232
9572
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
9233
9573
|
turnLog.error({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
9574
|
+
o.runtimeIncomplete = await absorbBundledLoss(
|
|
9575
|
+
this.integrityContext(),
|
|
9576
|
+
abortController.signal.aborted,
|
|
9577
|
+
o
|
|
9578
|
+
);
|
|
9234
9579
|
} finally {
|
|
9235
9580
|
this.disarmWatchdogs();
|
|
9236
9581
|
if (o.leaseLost) {
|
|
@@ -9279,13 +9624,13 @@ ${reason}`,
|
|
|
9279
9624
|
if (o.turnResolvedConfig && Object.keys(o.turnResolvedConfig).length > 0) {
|
|
9280
9625
|
body.resolvedConfig = o.turnResolvedConfig;
|
|
9281
9626
|
}
|
|
9282
|
-
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !this.skipState.skipped) {
|
|
9627
|
+
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !o.runtimeIncomplete && !this.skipState.skipped) {
|
|
9283
9628
|
body.errorReason = o.resultReason.slice(0, 200);
|
|
9284
9629
|
}
|
|
9285
9630
|
if (o.okResult) {
|
|
9286
9631
|
body.lastSeenMessageId = payload.messageId;
|
|
9287
9632
|
}
|
|
9288
|
-
body.outcome = o.okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
9633
|
+
body.outcome = o.okResult ? "settled" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
9289
9634
|
if (o.sessionDegraded) {
|
|
9290
9635
|
body.degraded = true;
|
|
9291
9636
|
}
|
|
@@ -9410,15 +9755,15 @@ function checkoutState(cwd) {
|
|
|
9410
9755
|
if (entries.length === 0) {
|
|
9411
9756
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
9412
9757
|
}
|
|
9413
|
-
const gitPath =
|
|
9758
|
+
const gitPath = join15(cwd, ".git");
|
|
9414
9759
|
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
9415
9760
|
let stat;
|
|
9416
9761
|
try {
|
|
9417
|
-
stat =
|
|
9762
|
+
stat = statSync2(gitPath);
|
|
9418
9763
|
} catch (error) {
|
|
9419
9764
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
9420
9765
|
}
|
|
9421
|
-
if (stat.isDirectory() && !existsSync11(
|
|
9766
|
+
if (stat.isDirectory() && !existsSync11(join15(gitPath, "HEAD")))
|
|
9422
9767
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
9423
9768
|
return { ok: true, reason: "usable" };
|
|
9424
9769
|
}
|
|
@@ -9553,7 +9898,7 @@ import {
|
|
|
9553
9898
|
rmSync as rmSync7,
|
|
9554
9899
|
writeFileSync as writeFileSync7
|
|
9555
9900
|
} from "fs";
|
|
9556
|
-
import { join as
|
|
9901
|
+
import { join as join16 } from "path";
|
|
9557
9902
|
var MAX_ENTRIES = 2e3;
|
|
9558
9903
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
9559
9904
|
var Outbox = class {
|
|
@@ -9566,10 +9911,10 @@ var Outbox = class {
|
|
|
9566
9911
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
9567
9912
|
// cases route writes at the right tmpdir.
|
|
9568
9913
|
dir() {
|
|
9569
|
-
return
|
|
9914
|
+
return join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
9570
9915
|
}
|
|
9571
9916
|
fileFor(turnId, seq) {
|
|
9572
|
-
return
|
|
9917
|
+
return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
9573
9918
|
}
|
|
9574
9919
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
9575
9920
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -9611,7 +9956,7 @@ var Outbox = class {
|
|
|
9611
9956
|
const entries = [];
|
|
9612
9957
|
for (const name of names) {
|
|
9613
9958
|
if (!name.endsWith(".json")) continue;
|
|
9614
|
-
const full =
|
|
9959
|
+
const full = join16(dir2, name);
|
|
9615
9960
|
try {
|
|
9616
9961
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
9617
9962
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -9863,12 +10208,12 @@ var SseSubscriber = class {
|
|
|
9863
10208
|
}
|
|
9864
10209
|
};
|
|
9865
10210
|
function sleep3(ms) {
|
|
9866
|
-
return new Promise((
|
|
10211
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
9867
10212
|
}
|
|
9868
10213
|
|
|
9869
10214
|
// src/version.ts
|
|
9870
|
-
import { createRequire } from "module";
|
|
9871
|
-
var pkg =
|
|
10215
|
+
import { createRequire as createRequire2 } from "module";
|
|
10216
|
+
var pkg = createRequire2(import.meta.url)("../package.json");
|
|
9872
10217
|
var COMPANION_VERSION = pkg.version;
|
|
9873
10218
|
|
|
9874
10219
|
// src/supervisor.ts
|
|
@@ -9890,10 +10235,15 @@ var CompanionSupervisor = class {
|
|
|
9890
10235
|
config;
|
|
9891
10236
|
log;
|
|
9892
10237
|
hub;
|
|
9893
|
-
//
|
|
9894
|
-
//
|
|
9895
|
-
//
|
|
9896
|
-
|
|
10238
|
+
// CT1379: the bundled-executable resolver — the execution-presence half of both
|
|
10239
|
+
// routing gates. Synchronous and cheap (a module resolve plus a stat), so unlike
|
|
10240
|
+
// the CLI probes it needs no boot-frozen fallback: a caller that asks before the
|
|
10241
|
+
// first harness refresh simply resolves now.
|
|
10242
|
+
resolveBundled;
|
|
10243
|
+
// CT1379: the last bundled status LOGGED per harness, so a missing binary is
|
|
10244
|
+
// reported at startup and on each healthy↔missing transition rather than on
|
|
10245
|
+
// every 30s beat (the CT484 latch pattern, per runtime).
|
|
10246
|
+
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
9897
10247
|
// CT571: the probed per-harness versions, reported on each heartbeat manifest.
|
|
9898
10248
|
// CT586: no longer boot-frozen — the per-heartbeat harness re-probe refreshes
|
|
9899
10249
|
// it, so a harness updated mid-run reports its new version without a restart.
|
|
@@ -9937,13 +10287,14 @@ var CompanionSupervisor = class {
|
|
|
9937
10287
|
this.config = opts.config;
|
|
9938
10288
|
this.log = opts.log;
|
|
9939
10289
|
this.hub = opts.hub;
|
|
9940
|
-
this.
|
|
10290
|
+
this.resolveBundled = opts.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
9941
10291
|
this.probePresence = opts.probePresence ?? probeHarnessPresence;
|
|
9942
10292
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
9943
10293
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
9944
10294
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
9945
10295
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
9946
10296
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
10297
|
+
this.latchBootBundledState();
|
|
9947
10298
|
}
|
|
9948
10299
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
9949
10300
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
@@ -10026,19 +10377,25 @@ var CompanionSupervisor = class {
|
|
|
10026
10377
|
const res = await this.deviceApi.heartbeat({
|
|
10027
10378
|
version: COMPANION_VERSION,
|
|
10028
10379
|
exposedSecretNames: store.names(),
|
|
10029
|
-
// Report each runtime only when this device can actually run it:
|
|
10030
|
-
// claude-code
|
|
10380
|
+
// Report each runtime only when this device can actually run it:
|
|
10381
|
+
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
10382
|
+
// and the user asked for the harness, CT270 opencode when the operator
|
|
10031
10383
|
// configured an `opencode serve`.
|
|
10032
10384
|
manifest: buildCompanionManifest({
|
|
10033
|
-
// CT1082: connected AND
|
|
10034
|
-
//
|
|
10035
|
-
// user's `claudeCode.enabled`. Claude Code
|
|
10385
|
+
// CT1082/CT1379: connected AND launchable. Execution presence is the
|
|
10386
|
+
// live bundled-binary resolution (refreshed on this beat), consent is
|
|
10387
|
+
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
10036
10388
|
claudeCode: this.claudeCodeOffered(),
|
|
10037
10389
|
opencode: !!this.config.opencode?.serverUrl,
|
|
10038
|
-
// CT481: advertise codex when the operator enabled it
|
|
10039
|
-
//
|
|
10040
|
-
// a
|
|
10041
|
-
|
|
10390
|
+
// CT481/CT1379: advertise codex when the operator enabled it AND its
|
|
10391
|
+
// vendored binary is there. The enable flag alone used to be enough,
|
|
10392
|
+
// which is how a device with a half-installed 331 MB platform package
|
|
10393
|
+
// advertised a runtime that could not start.
|
|
10394
|
+
codex: this.codexOffered(),
|
|
10395
|
+
// CT1379: and say WHY when an asked-for harness is absent from the list
|
|
10396
|
+
// above. Diagnostic only — the server never routes on it — and always
|
|
10397
|
+
// sent, so an empty array clears a previously reported issue.
|
|
10398
|
+
runtimeIssues: this.runtimeIssues(),
|
|
10042
10399
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
10043
10400
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
10044
10401
|
versions: this.harnessVersions
|
|
@@ -10259,18 +10616,103 @@ var CompanionSupervisor = class {
|
|
|
10259
10616
|
"companion: stopped agent (unassigned)"
|
|
10260
10617
|
);
|
|
10261
10618
|
}
|
|
10262
|
-
//
|
|
10263
|
-
//
|
|
10264
|
-
//
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
//
|
|
10271
|
-
//
|
|
10619
|
+
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
10620
|
+
// executable there? Live (the per-beat re-resolve), falling back to resolving
|
|
10621
|
+
// right now before the first refresh lands. Execution presence ONLY; consent is
|
|
10622
|
+
// the caller's half.
|
|
10623
|
+
bundledStatus(runtime) {
|
|
10624
|
+
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
10625
|
+
return cached2 ?? this.resolveBundled(runtime).status;
|
|
10626
|
+
}
|
|
10627
|
+
// The full resolution rather than just its status — the transition log needs the
|
|
10628
|
+
// platform/architecture and the repair the copy is built from, and re-resolving
|
|
10629
|
+
// is a module lookup plus a stat.
|
|
10630
|
+
bundledResolution(runtime) {
|
|
10631
|
+
return this.resolveBundled(runtime);
|
|
10632
|
+
}
|
|
10633
|
+
// CT1082/CT1379: does this device OFFER claude-code — connected by its user, and
|
|
10634
|
+
// able to start the binary a turn runs? The ONE signal both the heartbeat
|
|
10635
|
+
// manifest and the dispatcher's adapter registry read, so what the device
|
|
10636
|
+
// advertises and what it can select can't disagree (the CT833 invariant, with
|
|
10637
|
+
// consent in front of it and the real executable underneath it).
|
|
10272
10638
|
claudeCodeOffered() {
|
|
10273
|
-
return isClaudeCodeConnected(this.config) && this.
|
|
10639
|
+
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
10640
|
+
}
|
|
10641
|
+
// CT1379: and the same question for codex, which until now was the enable flag
|
|
10642
|
+
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
10643
|
+
// could not start. Same shape as Claude Code, same predicate on both sides.
|
|
10644
|
+
codexOffered() {
|
|
10645
|
+
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
10646
|
+
}
|
|
10647
|
+
// CT1379: the manifest's diagnostic half, derived from the same two facts as the
|
|
10648
|
+
// two predicates above so an issue and an advertisement can never both be made
|
|
10649
|
+
// for one harness.
|
|
10650
|
+
runtimeIssues() {
|
|
10651
|
+
return runtimeIssuesFor({
|
|
10652
|
+
claudeCode: {
|
|
10653
|
+
intended: isClaudeCodeConnected(this.config),
|
|
10654
|
+
bundled: this.bundledStatus("claude-code")
|
|
10655
|
+
},
|
|
10656
|
+
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
10657
|
+
});
|
|
10658
|
+
}
|
|
10659
|
+
// CT1379: a missing binary is reported ONCE, and again only when the answer
|
|
10660
|
+
// CHANGES — with the runtime, the platform/architecture and the resolution
|
|
10661
|
+
// result, which is enough to find the occurrence in `companion.log` without a
|
|
10662
|
+
// repro and carries nothing that could be a credential or a vendor's output.
|
|
10663
|
+
//
|
|
10664
|
+
// Two things this deliberately does NOT do (Codo's review, blocking finding #2):
|
|
10665
|
+
//
|
|
10666
|
+
// - It does not re-announce the state the device booted in. `warnAboutHarnessReadiness`
|
|
10667
|
+
// already said that on stderr at startup, in the specified words, at the one
|
|
10668
|
+
// moment a person is watching a terminal — and the `--daemon` launcher runs
|
|
10669
|
+
// that preflight in the FOREGROUND for exactly that reason, because the
|
|
10670
|
+
// child's output goes to a file. The boot state is latched in the
|
|
10671
|
+
// constructor, so the first refresh reports nothing new; it lands here as a
|
|
10672
|
+
// debug record instead, present for diagnosis without competing for
|
|
10673
|
+
// attention with the warning the operator already read.
|
|
10674
|
+
// - It does not report a transition in generic prose. A binary that goes
|
|
10675
|
+
// missing at 3am is the case the operator will actually meet, and a line
|
|
10676
|
+
// that says only "binary is missing" leaves them with no repair and no
|
|
10677
|
+
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
10678
|
+
// same sentence the startup path prints.
|
|
10679
|
+
logBundledTransitions() {
|
|
10680
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
10681
|
+
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
10682
|
+
const resolution = this.bundledResolution(runtime);
|
|
10683
|
+
const status2 = resolution.status;
|
|
10684
|
+
if (this.loggedBundledStatus.get(runtime) === status2) continue;
|
|
10685
|
+
this.loggedBundledStatus.set(runtime, status2);
|
|
10686
|
+
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status: status2 };
|
|
10687
|
+
if (status2 === "present") {
|
|
10688
|
+
const say = intended ? this.log.info : this.log.debug;
|
|
10689
|
+
say.call(this.log, meta, "companion: bundled harness binary is present again");
|
|
10690
|
+
continue;
|
|
10691
|
+
}
|
|
10692
|
+
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
10693
|
+
else
|
|
10694
|
+
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
10695
|
+
}
|
|
10696
|
+
}
|
|
10697
|
+
// CT1379: the boot state, latched so the first refresh reports no news, and
|
|
10698
|
+
// recorded once as evidence. Runs in the constructor — the resolve is a module
|
|
10699
|
+
// lookup plus a stat, so it costs nothing and needs no await.
|
|
10700
|
+
latchBootBundledState() {
|
|
10701
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
10702
|
+
const resolution = this.resolveBundled(runtime);
|
|
10703
|
+
this.loggedBundledStatus.set(runtime, resolution.status);
|
|
10704
|
+
if (resolution.status === "present") continue;
|
|
10705
|
+
this.log.debug(
|
|
10706
|
+
{
|
|
10707
|
+
runtime,
|
|
10708
|
+
platform: resolution.platform,
|
|
10709
|
+
arch: resolution.arch,
|
|
10710
|
+
status: resolution.status,
|
|
10711
|
+
intended: runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config)
|
|
10712
|
+
},
|
|
10713
|
+
"companion: bundled harness binary missing at startup (warned by the readiness check)"
|
|
10714
|
+
);
|
|
10715
|
+
}
|
|
10274
10716
|
}
|
|
10275
10717
|
buildDispatcher(ctx) {
|
|
10276
10718
|
const local = localAgentConfig(this.config, {
|
|
@@ -10294,16 +10736,23 @@ var CompanionSupervisor = class {
|
|
|
10294
10736
|
// CT833: register the claude-code adapter only when this device actually
|
|
10295
10737
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
10296
10738
|
// installed or connected after boot works on the next turn exactly as it
|
|
10297
|
-
// appears on the next beat. CT1082: "offers"
|
|
10298
|
-
//
|
|
10739
|
+
// appears on the next beat. CT1082: "offers" means connected as well as
|
|
10740
|
+
// runnable, so a disconnected harness can't be selected either.
|
|
10741
|
+
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
10742
|
+
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
10743
|
+
// other is not expressible.
|
|
10299
10744
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
10300
10745
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
10301
10746
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
10302
10747
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
10303
10748
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
10304
|
-
// CT481: register the codex adapter when this device offers codex;
|
|
10749
|
+
// CT481: register the codex adapter when this device offers codex; a miss
|
|
10305
10750
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
10306
|
-
|
|
10751
|
+
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
10752
|
+
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
10753
|
+
// from the enable flag alone, so a device could register an adapter for a
|
|
10754
|
+
// binary it did not have and keep it registered after the binary came back.
|
|
10755
|
+
codexAvailable: () => this.codexOffered(),
|
|
10307
10756
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
10308
10757
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
10309
10758
|
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
@@ -10673,9 +11122,13 @@ var CompanionSupervisor = class {
|
|
|
10673
11122
|
try {
|
|
10674
11123
|
const signals = await probeHarnessSignals(this.config, {
|
|
10675
11124
|
probePresence: this.probePresence,
|
|
10676
|
-
presence
|
|
11125
|
+
presence,
|
|
11126
|
+
// CT1379: the bundled executables are resolved on the same cadence as the
|
|
11127
|
+
// CLI probes, through the supervisor's seam so a test pins the host fact.
|
|
11128
|
+
resolveBundled: this.resolveBundled
|
|
10677
11129
|
});
|
|
10678
11130
|
this.harnessSignals = signals;
|
|
11131
|
+
this.logBundledTransitions();
|
|
10679
11132
|
this.harnessVersions = {
|
|
10680
11133
|
claudeCode: signals.claudeVersion,
|
|
10681
11134
|
opencode: signals.opencodeVersion,
|
|
@@ -10715,8 +11168,15 @@ var CompanionSupervisor = class {
|
|
|
10715
11168
|
return this.config;
|
|
10716
11169
|
}
|
|
10717
11170
|
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
10718
|
-
// `~/.cabane/config.json`, no hand-edited JSON.
|
|
10719
|
-
// never
|
|
11171
|
+
// `~/.cabane/config.json`, no hand-edited JSON. It never installs a binary and
|
|
11172
|
+
// never drives a login (BYO — Decided).
|
|
11173
|
+
//
|
|
11174
|
+
// CT1379: this records INTENT; it does not by itself expose anything. A harness
|
|
11175
|
+
// is advertised only when the user asked for it AND the SDK's own bundled binary
|
|
11176
|
+
// resolves, so a successful enable on a companion whose optional dependency
|
|
11177
|
+
// never installed leaves the device advertising nothing — correctly, and with
|
|
11178
|
+
// the Harnesses surface saying why. Read every "expose" below as "record the
|
|
11179
|
+
// half of the answer the user owns".
|
|
10720
11180
|
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
10721
11181
|
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
10722
11182
|
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
@@ -10725,9 +11185,10 @@ var CompanionSupervisor = class {
|
|
|
10725
11185
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
10726
11186
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
10727
11187
|
// typed failure the UI shows in place, not a silent bad write.
|
|
10728
|
-
// On success, persist, re-probe, and beat immediately so
|
|
10729
|
-
//
|
|
10730
|
-
//
|
|
11188
|
+
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
11189
|
+
// launchable starts taking turns at once rather than on the next tick — and one
|
|
11190
|
+
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
11191
|
+
// throws for a reachable/valid input).
|
|
10731
11192
|
async enableHarness(input) {
|
|
10732
11193
|
let next;
|
|
10733
11194
|
let checkedPresence = null;
|
|
@@ -10825,8 +11286,8 @@ var CompanionSupervisor = class {
|
|
|
10825
11286
|
};
|
|
10826
11287
|
async function waitForBoundedHeartbeat(heartbeat) {
|
|
10827
11288
|
let timer = null;
|
|
10828
|
-
const timeout = new Promise((
|
|
10829
|
-
timer = setTimeout(
|
|
11289
|
+
const timeout = new Promise((resolve2) => {
|
|
11290
|
+
timer = setTimeout(resolve2, SHUTDOWN_HEARTBEAT_WAIT_MS);
|
|
10830
11291
|
timer.unref?.();
|
|
10831
11292
|
});
|
|
10832
11293
|
try {
|
|
@@ -10910,9 +11371,9 @@ function handleUncaught(log, err, origin) {
|
|
|
10910
11371
|
|
|
10911
11372
|
// src/crash-marker.ts
|
|
10912
11373
|
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10913
|
-
import { join as
|
|
11374
|
+
import { join as join17 } from "path";
|
|
10914
11375
|
function crashMarkerPath() {
|
|
10915
|
-
return
|
|
11376
|
+
return join17(cabaneDir(), "last-error.json");
|
|
10916
11377
|
}
|
|
10917
11378
|
function recordCrash(rec2) {
|
|
10918
11379
|
try {
|
|
@@ -10961,8 +11422,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10961
11422
|
}
|
|
10962
11423
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
10963
11424
|
const harnessVersions = await probeHarnessVersions({
|
|
10964
|
-
//
|
|
10965
|
-
// claude
|
|
11425
|
+
// Connected AND its CLI answered — the two things that make a
|
|
11426
|
+
// `claude --version` probe worth spawning. Not the offer predicate.
|
|
10966
11427
|
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
10967
11428
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
10968
11429
|
codex: isCodexEnabled(cfg)
|
|
@@ -10995,7 +11456,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10995
11456
|
config: cfg,
|
|
10996
11457
|
log,
|
|
10997
11458
|
hub,
|
|
10998
|
-
claudeCode,
|
|
10999
11459
|
harnessVersions
|
|
11000
11460
|
});
|
|
11001
11461
|
await supervisor.start();
|
|
@@ -11100,11 +11560,11 @@ import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync
|
|
|
11100
11560
|
|
|
11101
11561
|
// src/cli-entry.ts
|
|
11102
11562
|
import { existsSync as existsSync14 } from "fs";
|
|
11103
|
-
import { fileURLToPath as
|
|
11563
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11104
11564
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
11105
11565
|
function companionCliEntry(deps = {}) {
|
|
11106
11566
|
const exists = deps.exists ?? existsSync14;
|
|
11107
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) =>
|
|
11567
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath3(new URL(rel, import.meta.url)));
|
|
11108
11568
|
for (const candidate of candidates) {
|
|
11109
11569
|
if (exists(candidate)) return candidate;
|
|
11110
11570
|
}
|
|
@@ -11451,7 +11911,7 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
11451
11911
|
}
|
|
11452
11912
|
}
|
|
11453
11913
|
async function runAttached(runtime) {
|
|
11454
|
-
await new Promise((
|
|
11914
|
+
await new Promise((resolve2) => {
|
|
11455
11915
|
let shuttingDown = false;
|
|
11456
11916
|
const shutdown = async (signal) => {
|
|
11457
11917
|
if (shuttingDown) {
|
|
@@ -11472,7 +11932,7 @@ companion: received ${signal}, shutting down\u2026
|
|
|
11472
11932
|
forceExit.unref?.();
|
|
11473
11933
|
await runtime.stop();
|
|
11474
11934
|
clearTimeout(forceExit);
|
|
11475
|
-
|
|
11935
|
+
resolve2();
|
|
11476
11936
|
process.exit(0);
|
|
11477
11937
|
};
|
|
11478
11938
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
@@ -11620,7 +12080,7 @@ function isAlive(kill, pid) {
|
|
|
11620
12080
|
|
|
11621
12081
|
// src/commands/transcript.ts
|
|
11622
12082
|
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
11623
|
-
import { isAbsolute, join as
|
|
12083
|
+
import { isAbsolute, join as join18 } from "path";
|
|
11624
12084
|
async function transcript(opts = {}) {
|
|
11625
12085
|
const dir2 = transcriptsDir();
|
|
11626
12086
|
if (opts.follow) {
|
|
@@ -11637,7 +12097,7 @@ async function transcript(opts = {}) {
|
|
|
11637
12097
|
process.stdout.write(emptyMessage(dir2));
|
|
11638
12098
|
return;
|
|
11639
12099
|
}
|
|
11640
|
-
process.stdout.write(renderFile(
|
|
12100
|
+
process.stdout.write(renderFile(join18(dir2, newest)) + "\n");
|
|
11641
12101
|
return;
|
|
11642
12102
|
}
|
|
11643
12103
|
printList(dir2);
|
|
@@ -11720,7 +12180,7 @@ function isComplete(content) {
|
|
|
11720
12180
|
async function followTranscripts(dir2) {
|
|
11721
12181
|
const follower = new TranscriptFollower({
|
|
11722
12182
|
listFiles: () => listFiles(dir2),
|
|
11723
|
-
read: (f) => readFileSync10(
|
|
12183
|
+
read: (f) => readFileSync10(join18(dir2, f), "utf8"),
|
|
11724
12184
|
write: (s) => process.stdout.write(s),
|
|
11725
12185
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
11726
12186
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -11729,14 +12189,14 @@ async function followTranscripts(dir2) {
|
|
|
11729
12189
|
process.stdout.write(`following ${dir2} \u2014 Ctrl-C to stop.
|
|
11730
12190
|
|
|
11731
12191
|
`);
|
|
11732
|
-
await new Promise((
|
|
12192
|
+
await new Promise((resolve2) => {
|
|
11733
12193
|
const timer = setInterval(() => follower.tick(), FOLLOW_POLL_MS);
|
|
11734
12194
|
const stop2 = () => {
|
|
11735
12195
|
clearInterval(timer);
|
|
11736
12196
|
process.removeListener("SIGINT", stop2);
|
|
11737
12197
|
process.removeListener("SIGTERM", stop2);
|
|
11738
12198
|
process.stdout.write("\n");
|
|
11739
|
-
|
|
12199
|
+
resolve2();
|
|
11740
12200
|
};
|
|
11741
12201
|
process.once("SIGINT", stop2);
|
|
11742
12202
|
process.once("SIGTERM", stop2);
|
|
@@ -11760,7 +12220,7 @@ function printList(dir2) {
|
|
|
11760
12220
|
|
|
11761
12221
|
`);
|
|
11762
12222
|
for (const f of files.slice(0, 20)) {
|
|
11763
|
-
const { meta, outcome } = peek(
|
|
12223
|
+
const { meta, outcome } = peek(join18(dir2, f));
|
|
11764
12224
|
const when = fmtTime(rec(meta)?.ts);
|
|
11765
12225
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
11766
12226
|
const o = rec(outcome);
|
|
@@ -11797,10 +12257,10 @@ function resolveTarget(dir2, target) {
|
|
|
11797
12257
|
if (existsSync15(target)) return target;
|
|
11798
12258
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
11799
12259
|
}
|
|
11800
|
-
const exact =
|
|
12260
|
+
const exact = join18(dir2, target);
|
|
11801
12261
|
if (existsSync15(exact)) return exact;
|
|
11802
12262
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
11803
|
-
if (matches.length === 1) return
|
|
12263
|
+
if (matches.length === 1) return join18(dir2, matches[0]);
|
|
11804
12264
|
if (matches.length === 0) {
|
|
11805
12265
|
throw new CompanionError(
|
|
11806
12266
|
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|