pitroom 0.26.0 → 0.27.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +9 -8
- package/dist/lib.mjs +122 -14
- package/dist/pitroom.mjs +537 -385
- package/docs/backends.md +5 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/pitroom-driven-development/re-review-prompt.md +2 -2
- package/skills/pitroom-driven-development/task-reviewer-prompt.md +2 -2
- package/skills/pitroom-review/code-reviewer.md +2 -2
package/dist/pitroom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs44 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -558,7 +558,7 @@ function denyCommandRules() {
|
|
|
558
558
|
"command(eval)"
|
|
559
559
|
];
|
|
560
560
|
}
|
|
561
|
-
function workerSettings(workspace, workerHome3, reads = []) {
|
|
561
|
+
function workerSettings(workspace, workerHome3, reads = [], writes = false) {
|
|
562
562
|
const real = (p) => {
|
|
563
563
|
try {
|
|
564
564
|
return fs6.realpathSync.native(p);
|
|
@@ -582,6 +582,8 @@ function workerSettings(workspace, workerHome3, reads = []) {
|
|
|
582
582
|
allow: [
|
|
583
583
|
"command(*)",
|
|
584
584
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
585
|
+
// agy 1.4 asks for edits in the workspace too, --mode accept-edits or not, and a headless run cannot ask
|
|
586
|
+
...writes ? both(workspace).map((p) => `write_file(${rulePath(p)})`) : [],
|
|
585
587
|
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
586
588
|
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
587
589
|
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
@@ -707,12 +709,12 @@ function baseSettings() {
|
|
|
707
709
|
}
|
|
708
710
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
709
711
|
var HOMES_KEPT_DAYS = 14;
|
|
710
|
-
function workerHome(workspace, reads = []) {
|
|
712
|
+
function workerHome(workspace, reads = [], writes = false) {
|
|
711
713
|
const root = path5.join(home(), "antigravity-home");
|
|
712
714
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
713
715
|
const dir = path5.join(root, id2);
|
|
714
716
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
715
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
717
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads, writes), null, 2)}
|
|
716
718
|
`);
|
|
717
719
|
if (process.platform === "darwin") {
|
|
718
720
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -769,7 +771,7 @@ function invocation(req) {
|
|
|
769
771
|
if (model) args.push("--model", model);
|
|
770
772
|
if (effort) args.push("--effort", effort);
|
|
771
773
|
const { command, prefix } = resolveCommand(binary());
|
|
772
|
-
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
774
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root), req.mode !== "read");
|
|
773
775
|
const env = { HOME: workerDir };
|
|
774
776
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
775
777
|
return { command, args: [...prefix, ...args], env };
|
|
@@ -826,9 +828,10 @@ function doctor({ models, hasFallback }) {
|
|
|
826
828
|
}
|
|
827
829
|
const checks = [
|
|
828
830
|
{ level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
|
|
829
|
-
|
|
831
|
+
// checked on macOS (agy 1.4.7, scripts/check-worker.mjs); Linux and Windows not run yet
|
|
832
|
+
process.platform === "darwin" ? { level: "ok", message: "agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes" } : {
|
|
830
833
|
level: "warn",
|
|
831
|
-
message:
|
|
834
|
+
message: `the Antigravity worker is in beta on ${process.platform === "win32" ? "Windows" : "Linux"} (checked on macOS only; node scripts/check-worker.mjs antigravity tries it here). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes`
|
|
832
835
|
}
|
|
833
836
|
];
|
|
834
837
|
if (process.platform === "win32") {
|
|
@@ -2343,7 +2346,8 @@ var SCHEMA = {
|
|
|
2343
2346
|
priceFeedUrl: "string",
|
|
2344
2347
|
priceFeedHours: "number",
|
|
2345
2348
|
budgetDaily: "number",
|
|
2346
|
-
rankWorkers: "boolean"
|
|
2349
|
+
rankWorkers: "boolean",
|
|
2350
|
+
autoFallback: "boolean"
|
|
2347
2351
|
};
|
|
2348
2352
|
function configPath() {
|
|
2349
2353
|
if (process.env.PITROOM_CONFIG) return path12.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -2453,6 +2457,7 @@ function effective(flags = {}) {
|
|
|
2453
2457
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2454
2458
|
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2455
2459
|
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2460
|
+
autoFallback: setting(void 0, flag01(e.PITROOM_AUTO_FALLBACK), c.autoFallback, false),
|
|
2456
2461
|
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2457
2462
|
};
|
|
2458
2463
|
}
|
|
@@ -2639,9 +2644,9 @@ function effortFlag(p) {
|
|
|
2639
2644
|
import readline from "node:readline";
|
|
2640
2645
|
|
|
2641
2646
|
// src/core/run.ts
|
|
2642
|
-
import { spawn as spawn4, spawnSync as
|
|
2643
|
-
import
|
|
2644
|
-
import
|
|
2647
|
+
import { spawn as spawn4, spawnSync as spawnSync10 } from "node:child_process";
|
|
2648
|
+
import fs32 from "node:fs";
|
|
2649
|
+
import path28 from "node:path";
|
|
2645
2650
|
|
|
2646
2651
|
// src/vcs/git.ts
|
|
2647
2652
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
@@ -2772,7 +2777,29 @@ function applyPatch(root, patchFile, reverse) {
|
|
|
2772
2777
|
function reviewDiff(root, a, b) {
|
|
2773
2778
|
return `${must(root, [...DIFF, "--stat", a, b]).trim()}
|
|
2774
2779
|
|
|
2775
|
-
${must(root, [...DIFF, "-U10", a, b])}`;
|
|
2780
|
+
${numberDiff(must(root, [...DIFF, "-U10", a, b]))}`;
|
|
2781
|
+
}
|
|
2782
|
+
var NUMBERED = "Each line of a hunk starts with its line number in the file after the change (a removed line has none): cite those.";
|
|
2783
|
+
function numberDiff(diff) {
|
|
2784
|
+
let next = 0;
|
|
2785
|
+
let width = 0;
|
|
2786
|
+
let inHunk = false;
|
|
2787
|
+
return diff.split("\n").map((l) => {
|
|
2788
|
+
if (l.startsWith("diff --git")) {
|
|
2789
|
+
inHunk = false;
|
|
2790
|
+
return l;
|
|
2791
|
+
}
|
|
2792
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
|
|
2793
|
+
if (hunk) {
|
|
2794
|
+
inHunk = true;
|
|
2795
|
+
next = Number(hunk[1]);
|
|
2796
|
+
width = String(next + Number(hunk[2] ?? 1)).length;
|
|
2797
|
+
return l;
|
|
2798
|
+
}
|
|
2799
|
+
if (!inHunk || !l || !"+- ".includes(l[0])) return l;
|
|
2800
|
+
if (l[0] === "-") return `-${" ".repeat(width)}| ${l.slice(1)}`;
|
|
2801
|
+
return `${l[0]}${String(next++).padStart(width)}| ${l.slice(1)}`;
|
|
2802
|
+
}).join("\n");
|
|
2776
2803
|
}
|
|
2777
2804
|
function rangeDiff(root, a, b) {
|
|
2778
2805
|
const range = `${a}..${b}`;
|
|
@@ -2787,7 +2814,9 @@ ${log2 || "(none)"}`,
|
|
|
2787
2814
|
${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
|
|
2788
2815
|
`## DIFF
|
|
2789
2816
|
|
|
2790
|
-
${
|
|
2817
|
+
${NUMBERED}
|
|
2818
|
+
|
|
2819
|
+
${numberDiff(must(root, [...DIFF, "-U10", since2]))}`
|
|
2791
2820
|
].join("\n\n");
|
|
2792
2821
|
}
|
|
2793
2822
|
function commitOf(root, ref) {
|
|
@@ -2799,6 +2828,10 @@ function gitDir(dir) {
|
|
|
2799
2828
|
return r.code === 0 ? r.stdout.trim() : void 0;
|
|
2800
2829
|
}
|
|
2801
2830
|
|
|
2831
|
+
// src/core/chain.ts
|
|
2832
|
+
import fs15 from "node:fs";
|
|
2833
|
+
import path14 from "node:path";
|
|
2834
|
+
|
|
2802
2835
|
// src/core/target.ts
|
|
2803
2836
|
function parseTarget(spec, defaultBackend) {
|
|
2804
2837
|
const s = spec.trim();
|
|
@@ -2857,17 +2890,52 @@ function resolveChain(flags = {}) {
|
|
|
2857
2890
|
warnings2.push(`fallback ${s} skipped: ${e.message}`);
|
|
2858
2891
|
}
|
|
2859
2892
|
}
|
|
2893
|
+
if (!fallback.length && eff.autoFallback.value) fallback.push(...autoFallback(worker, withDefault));
|
|
2860
2894
|
}
|
|
2861
2895
|
return { worker, fallback, warnings: warnings2 };
|
|
2862
2896
|
}
|
|
2897
|
+
var AUTO_MAX = 4;
|
|
2898
|
+
var bare = (t) => `${t.backend}:${(t.model ?? "").split("#")[0]}`;
|
|
2899
|
+
function installed(backend) {
|
|
2900
|
+
try {
|
|
2901
|
+
const bin = getBackend(backend).binary();
|
|
2902
|
+
return path14.isAbsolute(bin) && fs15.statSync(bin).isFile();
|
|
2903
|
+
} catch {
|
|
2904
|
+
return false;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function autoFallback(worker, withDefault = (t) => t) {
|
|
2908
|
+
const eff = effective();
|
|
2909
|
+
const named = [
|
|
2910
|
+
...Object.values(eff.tiers.value),
|
|
2911
|
+
...Object.entries(eff.models.value).map(([backend, model]) => `${backend}:${model}`),
|
|
2912
|
+
eff.worker.value
|
|
2913
|
+
];
|
|
2914
|
+
const costs = eff.costs.value;
|
|
2915
|
+
const out = [];
|
|
2916
|
+
for (const spec of named) {
|
|
2917
|
+
let t;
|
|
2918
|
+
try {
|
|
2919
|
+
t = withDefault(parseTarget(spec, worker.backend));
|
|
2920
|
+
getBackend(t.backend);
|
|
2921
|
+
} catch {
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
if (bare(t) === bare(worker) || out.some((o) => bare(o) === bare(t)) || !installed(t.backend)) continue;
|
|
2925
|
+
out.push(t);
|
|
2926
|
+
}
|
|
2927
|
+
const cost = (t) => costs[bare(t)] ?? Infinity;
|
|
2928
|
+
return out.map((t, i) => ({ t, i })).sort((a, b) => cost(a.t) - cost(b.t) || a.i - b.i).slice(0, AUTO_MAX).map((x) => x.t);
|
|
2929
|
+
}
|
|
2863
2930
|
|
|
2864
2931
|
// src/core/process.ts
|
|
2865
2932
|
import { spawn } from "node:child_process";
|
|
2866
|
-
import
|
|
2933
|
+
import fs17 from "node:fs";
|
|
2867
2934
|
|
|
2868
2935
|
// src/vcs/guard.ts
|
|
2869
|
-
import
|
|
2870
|
-
import
|
|
2936
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
2937
|
+
import fs16 from "node:fs";
|
|
2938
|
+
import path15 from "node:path";
|
|
2871
2939
|
var VERSION = 2;
|
|
2872
2940
|
var WINDOWS = process.platform === "win32";
|
|
2873
2941
|
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
@@ -2944,38 +3012,38 @@ esac
|
|
|
2944
3012
|
exec "$real" "$@"
|
|
2945
3013
|
`;
|
|
2946
3014
|
function shimDir() {
|
|
2947
|
-
return
|
|
3015
|
+
return path15.join(home(), "shim", `v${VERSION}`);
|
|
2948
3016
|
}
|
|
2949
3017
|
function scoopTarget(exe) {
|
|
2950
3018
|
try {
|
|
2951
|
-
const text =
|
|
3019
|
+
const text = fs16.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
|
|
2952
3020
|
const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
|
|
2953
|
-
return target &&
|
|
3021
|
+
return target && fs16.statSync(target).isFile() ? target : void 0;
|
|
2954
3022
|
} catch {
|
|
2955
3023
|
return void 0;
|
|
2956
3024
|
}
|
|
2957
3025
|
}
|
|
2958
3026
|
function findRealGit(skip) {
|
|
2959
3027
|
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2960
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2961
|
-
if (same2(
|
|
2962
|
-
const p =
|
|
3028
|
+
for (const dir of (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean)) {
|
|
3029
|
+
if (same2(path15.resolve(dir), skip)) continue;
|
|
3030
|
+
const p = path15.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2963
3031
|
try {
|
|
2964
|
-
|
|
2965
|
-
if (
|
|
3032
|
+
fs16.accessSync(p, fs16.constants.X_OK);
|
|
3033
|
+
if (fs16.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
|
|
2966
3034
|
} catch {
|
|
2967
3035
|
}
|
|
2968
3036
|
}
|
|
2969
3037
|
return void 0;
|
|
2970
3038
|
}
|
|
2971
3039
|
function findGitSh(realGit) {
|
|
2972
|
-
let dir =
|
|
3040
|
+
let dir = path15.dirname(realGit);
|
|
2973
3041
|
for (let i = 0; i < 4; i++) {
|
|
2974
3042
|
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2975
|
-
const p =
|
|
2976
|
-
if (
|
|
3043
|
+
const p = path15.join(dir, rel);
|
|
3044
|
+
if (fs16.existsSync(p)) return p;
|
|
2977
3045
|
}
|
|
2978
|
-
const up =
|
|
3046
|
+
const up = path15.dirname(dir);
|
|
2979
3047
|
if (up === dir) break;
|
|
2980
3048
|
dir = up;
|
|
2981
3049
|
}
|
|
@@ -3003,6 +3071,63 @@ var cmdShim = (sh) => [
|
|
|
3003
3071
|
"exit /b %errorlevel%",
|
|
3004
3072
|
""
|
|
3005
3073
|
].join("\r\n");
|
|
3074
|
+
var launcherSource = (sh) => `// pitroom git guard: git.exe launcher v${VERSION} (generated; edit src/vcs/guard.ts)
|
|
3075
|
+
using System;
|
|
3076
|
+
using System.Diagnostics;
|
|
3077
|
+
using System.IO;
|
|
3078
|
+
|
|
3079
|
+
static class PitroomGit {
|
|
3080
|
+
const string Sh = @"${sh.replace(/"/g, '""')}";
|
|
3081
|
+
|
|
3082
|
+
static int Main() {
|
|
3083
|
+
string line = Environment.CommandLine;
|
|
3084
|
+
// this program's own name, as Windows reads it: up to the closing quote when it starts with one, else to a space
|
|
3085
|
+
int i = 0;
|
|
3086
|
+
if (line.Length > 0 && line[0] == '"') { int q = line.IndexOf('"', 1); i = q < 0 ? line.Length : q + 1; }
|
|
3087
|
+
else { while (i < line.Length && line[i] != ' ' && line[i] != '\\t') i++; }
|
|
3088
|
+
string script = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "git");
|
|
3089
|
+
ProcessStartInfo start = new ProcessStartInfo(Sh, "\\"" + script + "\\"" + line.Substring(i));
|
|
3090
|
+
start.UseShellExecute = false;
|
|
3091
|
+
start.EnvironmentVariables["MSYS2_ARG_CONV_EXCL"] = "*";
|
|
3092
|
+
// Ctrl+C reaches git too: wait for it to end and pass its exit code on
|
|
3093
|
+
Console.CancelKeyPress += delegate (object o, ConsoleCancelEventArgs e) { e.Cancel = true; };
|
|
3094
|
+
using (Process git = Process.Start(start)) {
|
|
3095
|
+
git.WaitForExit();
|
|
3096
|
+
return git.ExitCode;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
`;
|
|
3101
|
+
function findCsc() {
|
|
3102
|
+
const windir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
|
|
3103
|
+
for (const fw of ["Framework64", "Framework"]) {
|
|
3104
|
+
const p = path15.join(windir, "Microsoft.NET", fw, "v4.0.30319", "csc.exe");
|
|
3105
|
+
if (fs16.existsSync(p)) return p;
|
|
3106
|
+
}
|
|
3107
|
+
return void 0;
|
|
3108
|
+
}
|
|
3109
|
+
function buildLauncher(dir, sh) {
|
|
3110
|
+
const source = launcherSource(sh);
|
|
3111
|
+
const src = path15.join(dir, "git-launcher.cs");
|
|
3112
|
+
const exe = path15.join(dir, "git.exe");
|
|
3113
|
+
if (fs16.existsSync(exe) && fs16.existsSync(src) && fs16.readFileSync(src, "utf8") === source) return;
|
|
3114
|
+
const csc = findCsc();
|
|
3115
|
+
if (!csc) return;
|
|
3116
|
+
const tmp = path15.join(dir, `git.${process.pid}.tmp.exe`);
|
|
3117
|
+
const tmpSrc = `${src}.${process.pid}.tmp`;
|
|
3118
|
+
try {
|
|
3119
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3120
|
+
fs16.writeFileSync(tmpSrc, source);
|
|
3121
|
+
const r = spawnSync9(csc, ["/nologo", "/optimize+", "/target:exe", `/out:${tmp}`, tmpSrc], { encoding: "utf8", timeout: 6e4, windowsHide: true });
|
|
3122
|
+
if (r.status !== 0 || !fs16.existsSync(tmp)) return;
|
|
3123
|
+
renameOver(tmp, exe);
|
|
3124
|
+
renameOver(tmpSrc, src);
|
|
3125
|
+
} catch {
|
|
3126
|
+
} finally {
|
|
3127
|
+
fs16.rmSync(tmp, { force: true });
|
|
3128
|
+
fs16.rmSync(tmpSrc, { force: true });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3006
3131
|
var HOOK_VERSION = 1;
|
|
3007
3132
|
var REF_HOOK = `#!/bin/sh
|
|
3008
3133
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -3010,12 +3135,12 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3010
3135
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3011
3136
|
exit 1
|
|
3012
3137
|
`;
|
|
3013
|
-
var hooksDir = () =>
|
|
3138
|
+
var hooksDir = () => path15.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
3014
3139
|
function writeIfChanged2(file2, content) {
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3140
|
+
if (fs16.existsSync(file2) && fs16.readFileSync(file2, "utf8") === content) return;
|
|
3141
|
+
fs16.mkdirSync(path15.dirname(file2), { recursive: true });
|
|
3017
3142
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
3018
|
-
|
|
3143
|
+
fs16.writeFileSync(tmp, content, { mode: 493 });
|
|
3019
3144
|
renameOver(tmp, file2);
|
|
3020
3145
|
}
|
|
3021
3146
|
function pathKey(env) {
|
|
@@ -3047,7 +3172,7 @@ function guardEnv(env) {
|
|
|
3047
3172
|
GIT_PAGER: "cat",
|
|
3048
3173
|
PAGER: "cat"
|
|
3049
3174
|
};
|
|
3050
|
-
writeIfChanged2(
|
|
3175
|
+
writeIfChanged2(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3051
3176
|
env = withGitConfig(onePath(env), [
|
|
3052
3177
|
["core.hooksPath", fwd(hooksDir())],
|
|
3053
3178
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3059,11 +3184,12 @@ function guardEnv(env) {
|
|
|
3059
3184
|
if (WINDOWS) {
|
|
3060
3185
|
sh = findGitSh(real);
|
|
3061
3186
|
if (!sh) return { ...env, ...quiet };
|
|
3062
|
-
writeIfChanged2(
|
|
3187
|
+
writeIfChanged2(path15.join(dir, "git.cmd"), cmdShim(sh));
|
|
3188
|
+
buildLauncher(dir, sh);
|
|
3063
3189
|
}
|
|
3064
|
-
writeIfChanged2(
|
|
3190
|
+
writeIfChanged2(path15.join(dir, "git"), SHIM);
|
|
3065
3191
|
const key = pathKey(env);
|
|
3066
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${
|
|
3192
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
|
|
3067
3193
|
}
|
|
3068
3194
|
function shimReady() {
|
|
3069
3195
|
const env = guardEnv({ ...process.env });
|
|
@@ -3072,8 +3198,8 @@ function shimReady() {
|
|
|
3072
3198
|
|
|
3073
3199
|
// src/core/process.ts
|
|
3074
3200
|
async function spawnWorker(inv, opts) {
|
|
3075
|
-
const out =
|
|
3076
|
-
const err =
|
|
3201
|
+
const out = fs17.openSync(opts.stdoutFile, "w");
|
|
3202
|
+
const err = fs17.openSync(opts.stderrFile, "w");
|
|
3077
3203
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3078
3204
|
const child = spawn(inv.command, inv.args, {
|
|
3079
3205
|
cwd: opts.cwd,
|
|
@@ -3096,7 +3222,7 @@ async function spawnWorker(inv, opts) {
|
|
|
3096
3222
|
}, opts.timeoutSec * 1e3);
|
|
3097
3223
|
res.code = await new Promise((resolve2) => {
|
|
3098
3224
|
child.on("error", (e) => {
|
|
3099
|
-
res.spawnError = e.code === "ENOENT" ?
|
|
3225
|
+
res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
3100
3226
|
resolve2(127);
|
|
3101
3227
|
});
|
|
3102
3228
|
child.on("close", (c) => resolve2(c));
|
|
@@ -3105,8 +3231,8 @@ async function spawnWorker(inv, opts) {
|
|
|
3105
3231
|
if (killer) clearTimeout(killer);
|
|
3106
3232
|
process.off("SIGINT", stop);
|
|
3107
3233
|
process.off("SIGTERM", stop);
|
|
3108
|
-
|
|
3109
|
-
|
|
3234
|
+
fs17.closeSync(out);
|
|
3235
|
+
fs17.closeSync(err);
|
|
3110
3236
|
return res;
|
|
3111
3237
|
}
|
|
3112
3238
|
|
|
@@ -3138,13 +3264,13 @@ function parseAudit(text) {
|
|
|
3138
3264
|
}
|
|
3139
3265
|
|
|
3140
3266
|
// src/core/cooldown.ts
|
|
3141
|
-
import
|
|
3142
|
-
import
|
|
3267
|
+
import fs18 from "node:fs";
|
|
3268
|
+
import path16 from "node:path";
|
|
3143
3269
|
var MAX_MS = 24 * 36e5;
|
|
3144
3270
|
var OVERLOAD_MS = 20 * 6e4;
|
|
3145
3271
|
var USAGE_MS = 36e5;
|
|
3146
3272
|
var DAILY_MS = 4 * 36e5;
|
|
3147
|
-
var file = () =>
|
|
3273
|
+
var file = () => path16.join(home(), "cooldowns.json");
|
|
3148
3274
|
function cooldownKey(target, backend) {
|
|
3149
3275
|
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
3150
3276
|
return `${target.backend}:${model}`;
|
|
@@ -3198,16 +3324,16 @@ function cooldownMs(message, now = Date.now()) {
|
|
|
3198
3324
|
}
|
|
3199
3325
|
function read() {
|
|
3200
3326
|
try {
|
|
3201
|
-
const raw = JSON.parse(
|
|
3327
|
+
const raw = JSON.parse(fs18.readFileSync(file(), "utf8"));
|
|
3202
3328
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3203
3329
|
} catch {
|
|
3204
3330
|
return {};
|
|
3205
3331
|
}
|
|
3206
3332
|
}
|
|
3207
3333
|
function write(all) {
|
|
3208
|
-
|
|
3334
|
+
fs18.mkdirSync(path16.dirname(file()), { recursive: true });
|
|
3209
3335
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3210
|
-
|
|
3336
|
+
fs18.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3211
3337
|
`);
|
|
3212
3338
|
renameOver(tmp, file());
|
|
3213
3339
|
}
|
|
@@ -3237,15 +3363,15 @@ function untilText(c, now = Date.now()) {
|
|
|
3237
3363
|
}
|
|
3238
3364
|
|
|
3239
3365
|
// src/core/snapshot.ts
|
|
3240
|
-
import
|
|
3241
|
-
import
|
|
3366
|
+
import fs19 from "node:fs";
|
|
3367
|
+
import path17 from "node:path";
|
|
3242
3368
|
import crypto5 from "node:crypto";
|
|
3243
3369
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
3244
3370
|
var KEEP_MS = 24 * 36e5;
|
|
3245
3371
|
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
3246
3372
|
var KEEP_NEWEST = 6;
|
|
3247
3373
|
var JUST_USED_MS = 10 * 6e4;
|
|
3248
|
-
var snapshotsDir = () =>
|
|
3374
|
+
var snapshotsDir = () => path17.join(home(), "snapshots");
|
|
3249
3375
|
function wantSnapshot(readIn2, root, dir) {
|
|
3250
3376
|
if (!root || readIn2 === "project") return void 0;
|
|
3251
3377
|
const secrets = findSecretFiles(dir);
|
|
@@ -3254,8 +3380,8 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
3254
3380
|
function readSnapshot(root, owner) {
|
|
3255
3381
|
const drop2 = findSecretFilesInTree(root, root);
|
|
3256
3382
|
const tree = snapshotTree(root, [], drop2);
|
|
3257
|
-
const dest =
|
|
3258
|
-
if (
|
|
3383
|
+
const dest = path17.join(snapshotsDir(), owner ? `${tree}-${owner}` : tree);
|
|
3384
|
+
if (fs19.existsSync(path17.join(dest, ".git"))) {
|
|
3259
3385
|
touch(dest);
|
|
3260
3386
|
return { dir: dest, tree, created: false };
|
|
3261
3387
|
}
|
|
@@ -3264,19 +3390,19 @@ function readSnapshot(root, owner) {
|
|
|
3264
3390
|
return { dir: dest, tree, created: true };
|
|
3265
3391
|
}
|
|
3266
3392
|
function restoreSnapshot(root, dir, tree) {
|
|
3267
|
-
const rel =
|
|
3268
|
-
if (!rel || rel.startsWith("..") ||
|
|
3269
|
-
if (!
|
|
3393
|
+
const rel = path17.relative(snapshotsDir(), dir);
|
|
3394
|
+
if (!rel || rel.startsWith("..") || path17.isAbsolute(rel)) throw new Error(`not a snapshot: ${dir}`);
|
|
3395
|
+
if (!fs19.existsSync(path17.join(dir, ".git"))) materialize(root, tree, dir);
|
|
3270
3396
|
}
|
|
3271
3397
|
function materialize(root, tree, dest) {
|
|
3272
|
-
const tmp =
|
|
3398
|
+
const tmp = path17.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
|
|
3273
3399
|
try {
|
|
3274
3400
|
createIsolatedCopy(root, tree, tmp);
|
|
3275
3401
|
readOnly(tmp);
|
|
3276
3402
|
try {
|
|
3277
|
-
|
|
3403
|
+
fs19.renameSync(tmp, dest);
|
|
3278
3404
|
} catch (e) {
|
|
3279
|
-
if (!
|
|
3405
|
+
if (!fs19.existsSync(path17.join(dest, ".git"))) throw e;
|
|
3280
3406
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3281
3407
|
}
|
|
3282
3408
|
} catch (e) {
|
|
@@ -3290,17 +3416,17 @@ function materialize(root, tree, dest) {
|
|
|
3290
3416
|
function touch(dir) {
|
|
3291
3417
|
try {
|
|
3292
3418
|
const now = /* @__PURE__ */ new Date();
|
|
3293
|
-
|
|
3419
|
+
fs19.utimesSync(dir, now, now);
|
|
3294
3420
|
} catch {
|
|
3295
3421
|
}
|
|
3296
3422
|
}
|
|
3297
3423
|
function readOnly(dir) {
|
|
3298
3424
|
const walk = (d) => {
|
|
3299
|
-
for (const e of
|
|
3425
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3300
3426
|
if (e.name === ".git") continue;
|
|
3301
|
-
const p =
|
|
3427
|
+
const p = path17.join(d, e.name);
|
|
3302
3428
|
if (e.isDirectory()) walk(p);
|
|
3303
|
-
else if (e.isFile())
|
|
3429
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
|
|
3304
3430
|
}
|
|
3305
3431
|
};
|
|
3306
3432
|
try {
|
|
@@ -3311,7 +3437,7 @@ function readOnly(dir) {
|
|
|
3311
3437
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
3312
3438
|
let names;
|
|
3313
3439
|
try {
|
|
3314
|
-
names =
|
|
3440
|
+
names = fs19.readdirSync(snapshotsDir());
|
|
3315
3441
|
} catch {
|
|
3316
3442
|
return 0;
|
|
3317
3443
|
}
|
|
@@ -3319,14 +3445,14 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3319
3445
|
for (const id2 of listRunIds()) {
|
|
3320
3446
|
try {
|
|
3321
3447
|
const m = readMeta(id2);
|
|
3322
|
-
if (m.snapshot?.dir && isActive(m.state)) inUse.add(
|
|
3448
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path17.resolve(m.snapshot.dir));
|
|
3323
3449
|
} catch {
|
|
3324
3450
|
}
|
|
3325
3451
|
}
|
|
3326
3452
|
const entries = names.map((n) => {
|
|
3327
|
-
const dir =
|
|
3453
|
+
const dir = path17.join(snapshotsDir(), n);
|
|
3328
3454
|
try {
|
|
3329
|
-
return { n, dir, age: now -
|
|
3455
|
+
return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
|
|
3330
3456
|
} catch {
|
|
3331
3457
|
return void 0;
|
|
3332
3458
|
}
|
|
@@ -3334,7 +3460,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3334
3460
|
let removed = 0;
|
|
3335
3461
|
const rank = { shared: 0, own: 0 };
|
|
3336
3462
|
for (const e of entries) {
|
|
3337
|
-
if (inUse.has(
|
|
3463
|
+
if (inUse.has(path17.resolve(e.dir))) continue;
|
|
3338
3464
|
const half = e.n.startsWith(".tmp-");
|
|
3339
3465
|
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
3340
3466
|
const surplus = !half && ++rank[e.n.includes("-") ? "own" : "shared"] > keepNewest && e.age >= JUST_USED_MS;
|
|
@@ -3350,10 +3476,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3350
3476
|
}
|
|
3351
3477
|
function makeWritable(dir) {
|
|
3352
3478
|
const walk = (d) => {
|
|
3353
|
-
for (const e of
|
|
3354
|
-
const p =
|
|
3479
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3480
|
+
const p = path17.join(d, e.name);
|
|
3355
3481
|
if (e.isDirectory()) walk(p);
|
|
3356
|
-
else if (e.isFile())
|
|
3482
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
|
|
3357
3483
|
}
|
|
3358
3484
|
};
|
|
3359
3485
|
try {
|
|
@@ -3364,17 +3490,17 @@ function makeWritable(dir) {
|
|
|
3364
3490
|
function backToProject(text, snapshotDir, root) {
|
|
3365
3491
|
let real = snapshotDir;
|
|
3366
3492
|
try {
|
|
3367
|
-
real =
|
|
3493
|
+
real = fs19.realpathSync(snapshotDir);
|
|
3368
3494
|
} catch {
|
|
3369
3495
|
}
|
|
3370
3496
|
let out = text;
|
|
3371
|
-
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${
|
|
3497
|
+
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path17.sep}`).join(`${root}${path17.sep}`).split(dir).join(root);
|
|
3372
3498
|
return out;
|
|
3373
3499
|
}
|
|
3374
3500
|
|
|
3375
3501
|
// src/core/audit.ts
|
|
3376
3502
|
import crypto6 from "node:crypto";
|
|
3377
|
-
import
|
|
3503
|
+
import path18 from "node:path";
|
|
3378
3504
|
var ANSWER_MAX = 8e3;
|
|
3379
3505
|
function auditRate(meta) {
|
|
3380
3506
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -3414,12 +3540,12 @@ function auditors(meta) {
|
|
|
3414
3540
|
const tiers = eff.tiers.value;
|
|
3415
3541
|
const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
|
|
3416
3542
|
const ran = meta.ran ?? meta.worker;
|
|
3417
|
-
const
|
|
3543
|
+
const bare2 = (t) => ({ backend: t.backend, model: t.model?.split("#")[0] || void 0 });
|
|
3418
3544
|
const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
|
|
3419
3545
|
const out = [];
|
|
3420
3546
|
for (const c of candidates) {
|
|
3421
|
-
const t =
|
|
3422
|
-
if (sameTarget(t,
|
|
3547
|
+
const t = bare2(parseTarget(c, def));
|
|
3548
|
+
if (sameTarget(t, bare2(ran)) || out.some((o) => sameTarget(bare2(parseTarget(o, def)), t))) continue;
|
|
3423
3549
|
out.push(c);
|
|
3424
3550
|
}
|
|
3425
3551
|
return out;
|
|
@@ -3428,9 +3554,9 @@ function relativeToAuditor(answer, meta) {
|
|
|
3428
3554
|
let out = answer;
|
|
3429
3555
|
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
3430
3556
|
for (const root of roots) {
|
|
3431
|
-
const rel =
|
|
3432
|
-
const prefix = rel ? `${rel.split(
|
|
3433
|
-
out = out.split(`${root}${
|
|
3557
|
+
const rel = path18.relative(meta.dir, root);
|
|
3558
|
+
const prefix = rel ? `${rel.split(path18.sep).join("/")}/` : "";
|
|
3559
|
+
out = out.split(`${root}${path18.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
|
|
3434
3560
|
}
|
|
3435
3561
|
return out;
|
|
3436
3562
|
}
|
|
@@ -3500,19 +3626,19 @@ function auditBadge(meta) {
|
|
|
3500
3626
|
}
|
|
3501
3627
|
|
|
3502
3628
|
// src/core/history.ts
|
|
3503
|
-
import
|
|
3629
|
+
import fs23 from "node:fs";
|
|
3504
3630
|
import { createRequire } from "node:module";
|
|
3505
|
-
import
|
|
3631
|
+
import path21 from "node:path";
|
|
3506
3632
|
import zlib from "node:zlib";
|
|
3507
3633
|
|
|
3508
3634
|
// src/core/receipt.ts
|
|
3509
|
-
import
|
|
3510
|
-
import
|
|
3635
|
+
import fs21 from "node:fs";
|
|
3636
|
+
import path20 from "node:path";
|
|
3511
3637
|
|
|
3512
3638
|
// src/core/prices.ts
|
|
3513
3639
|
import { spawn as spawn2 } from "node:child_process";
|
|
3514
|
-
import
|
|
3515
|
-
import
|
|
3640
|
+
import fs20 from "node:fs";
|
|
3641
|
+
import path19 from "node:path";
|
|
3516
3642
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
3517
3643
|
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
3518
3644
|
var MIN_MODELS = 100;
|
|
@@ -3525,8 +3651,8 @@ function catalogLabel() {
|
|
|
3525
3651
|
return url;
|
|
3526
3652
|
}
|
|
3527
3653
|
}
|
|
3528
|
-
var catalogFile = () =>
|
|
3529
|
-
var attemptFile = () =>
|
|
3654
|
+
var catalogFile = () => path19.join(home(), "prices.json");
|
|
3655
|
+
var attemptFile = () => path19.join(home(), "prices.attempt");
|
|
3530
3656
|
function trim(raw) {
|
|
3531
3657
|
const out = {};
|
|
3532
3658
|
if (!raw || typeof raw !== "object") return out;
|
|
@@ -3544,9 +3670,9 @@ var cached2;
|
|
|
3544
3670
|
function readCatalog() {
|
|
3545
3671
|
const file2 = catalogFile();
|
|
3546
3672
|
try {
|
|
3547
|
-
const { mtimeMs } =
|
|
3673
|
+
const { mtimeMs } = fs20.statSync(file2);
|
|
3548
3674
|
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
3549
|
-
const parsed = JSON.parse(
|
|
3675
|
+
const parsed = JSON.parse(fs20.readFileSync(file2, "utf8"));
|
|
3550
3676
|
const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
3551
3677
|
cached2 = { file: file2, mtimeMs, catalog: catalog6 };
|
|
3552
3678
|
return catalog6;
|
|
@@ -3556,7 +3682,7 @@ function readCatalog() {
|
|
|
3556
3682
|
}
|
|
3557
3683
|
var ageMs = (file2) => {
|
|
3558
3684
|
try {
|
|
3559
|
-
return Date.now() -
|
|
3685
|
+
return Date.now() - fs20.statSync(file2).mtimeMs;
|
|
3560
3686
|
} catch {
|
|
3561
3687
|
return Infinity;
|
|
3562
3688
|
}
|
|
@@ -3575,17 +3701,17 @@ async function refreshCatalog() {
|
|
|
3575
3701
|
const models = trim(await r.json());
|
|
3576
3702
|
const n = Object.keys(models).length;
|
|
3577
3703
|
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
3578
|
-
|
|
3704
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3579
3705
|
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
3580
3706
|
const catalog6 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
3581
|
-
|
|
3707
|
+
fs20.writeFileSync(tmp, JSON.stringify(catalog6));
|
|
3582
3708
|
renameOver(tmp, catalogFile());
|
|
3583
|
-
|
|
3709
|
+
fs20.rmSync(attemptFile(), { force: true });
|
|
3584
3710
|
return { state: "updated", models: n };
|
|
3585
3711
|
} catch (e) {
|
|
3586
3712
|
try {
|
|
3587
|
-
|
|
3588
|
-
|
|
3713
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3714
|
+
fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
3589
3715
|
`);
|
|
3590
3716
|
} catch {
|
|
3591
3717
|
}
|
|
@@ -3597,8 +3723,8 @@ function refreshInBackground() {
|
|
|
3597
3723
|
if (!refreshDue()) return;
|
|
3598
3724
|
const script = process.argv[1];
|
|
3599
3725
|
if (!script) return;
|
|
3600
|
-
|
|
3601
|
-
|
|
3726
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3727
|
+
fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
3602
3728
|
`);
|
|
3603
3729
|
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
3604
3730
|
child.on("error", () => void 0);
|
|
@@ -3628,7 +3754,7 @@ function catalogStatus() {
|
|
|
3628
3754
|
const c = readCatalog();
|
|
3629
3755
|
let lastFailure;
|
|
3630
3756
|
try {
|
|
3631
|
-
const t =
|
|
3757
|
+
const t = fs20.readFileSync(attemptFile(), "utf8").trim();
|
|
3632
3758
|
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
3633
3759
|
} catch {
|
|
3634
3760
|
}
|
|
@@ -3708,8 +3834,8 @@ function record(meta) {
|
|
|
3708
3834
|
saved: meta.savedUsd ?? 0,
|
|
3709
3835
|
price: primaryPrice().name
|
|
3710
3836
|
};
|
|
3711
|
-
|
|
3712
|
-
|
|
3837
|
+
fs21.mkdirSync(path20.dirname(ledgerFile()), { recursive: true });
|
|
3838
|
+
fs21.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
3713
3839
|
`);
|
|
3714
3840
|
return true;
|
|
3715
3841
|
}
|
|
@@ -3736,9 +3862,9 @@ function* finishedRuns() {
|
|
|
3736
3862
|
}
|
|
3737
3863
|
}
|
|
3738
3864
|
function readLedger(sinceMs2) {
|
|
3739
|
-
if (!
|
|
3865
|
+
if (!fs21.existsSync(ledgerFile())) return [];
|
|
3740
3866
|
const seen = /* @__PURE__ */ new Set();
|
|
3741
|
-
return
|
|
3867
|
+
return fs21.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
3742
3868
|
try {
|
|
3743
3869
|
const e = JSON.parse(l);
|
|
3744
3870
|
if (seen.has(e.id)) return [];
|
|
@@ -3816,7 +3942,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
3816
3942
|
}
|
|
3817
3943
|
|
|
3818
3944
|
// src/core/ui.ts
|
|
3819
|
-
import
|
|
3945
|
+
import fs22 from "node:fs";
|
|
3820
3946
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3821
3947
|
var RECENT = 40;
|
|
3822
3948
|
var RECENT_HOURS = 1;
|
|
@@ -3907,9 +4033,9 @@ function cardsFor(m) {
|
|
|
3907
4033
|
const out = [];
|
|
3908
4034
|
for (const [phase, text] of phases) {
|
|
3909
4035
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3910
|
-
if (
|
|
4036
|
+
if (fs22.existsSync(marker)) continue;
|
|
3911
4037
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3912
|
-
|
|
4038
|
+
fs22.writeFileSync(marker, "");
|
|
3913
4039
|
}
|
|
3914
4040
|
return out;
|
|
3915
4041
|
}
|
|
@@ -3920,10 +4046,10 @@ function hookCards(input) {
|
|
|
3920
4046
|
const response = event.tool_response;
|
|
3921
4047
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3922
4048
|
const ids = [...new Set(`${command}
|
|
3923
|
-
${output}`.match(RUN_ID) ?? [])].filter((id2) =>
|
|
4049
|
+
${output}`.match(RUN_ID) ?? [])].filter((id2) => fs22.existsSync(`${runsDir()}/${id2}`));
|
|
3924
4050
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3925
4051
|
for (const id2 of listRunIds().slice(-RECENT)) {
|
|
3926
|
-
if (ids.includes(id2) ||
|
|
4052
|
+
if (ids.includes(id2) || fs22.existsSync(runFile(id2, "card-ended"))) continue;
|
|
3927
4053
|
try {
|
|
3928
4054
|
const m = readMeta(id2);
|
|
3929
4055
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id2);
|
|
@@ -3945,7 +4071,7 @@ var SCHEMA_VERSION = 1;
|
|
|
3945
4071
|
var PATCH_MAX = 1e6;
|
|
3946
4072
|
var ANSWER_MAX2 = 2e5;
|
|
3947
4073
|
var cached3;
|
|
3948
|
-
var historyFile = () =>
|
|
4074
|
+
var historyFile = () => path21.join(home(), "history.db");
|
|
3949
4075
|
function openDb() {
|
|
3950
4076
|
const file2 = historyFile();
|
|
3951
4077
|
if (cached3?.file === file2) return cached3.db;
|
|
@@ -3959,7 +4085,7 @@ function openDb() {
|
|
|
3959
4085
|
} finally {
|
|
3960
4086
|
process.emitWarning = emit;
|
|
3961
4087
|
}
|
|
3962
|
-
|
|
4088
|
+
fs23.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
3963
4089
|
db = new DatabaseSync(file2);
|
|
3964
4090
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3965
4091
|
migrate(db);
|
|
@@ -3994,31 +4120,31 @@ function migrate(db) {
|
|
|
3994
4120
|
function readRunFile(id2, name) {
|
|
3995
4121
|
const f = runFile(id2, name);
|
|
3996
4122
|
try {
|
|
3997
|
-
if (
|
|
3998
|
-
if (
|
|
4123
|
+
if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
|
|
4124
|
+
if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3999
4125
|
} catch {
|
|
4000
4126
|
}
|
|
4001
4127
|
return void 0;
|
|
4002
4128
|
}
|
|
4003
4129
|
function gzipFile(f) {
|
|
4004
|
-
if (!
|
|
4005
|
-
|
|
4006
|
-
|
|
4130
|
+
if (!fs23.existsSync(f)) return;
|
|
4131
|
+
fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
|
|
4132
|
+
fs23.rmSync(f);
|
|
4007
4133
|
}
|
|
4008
4134
|
function compactRun(meta) {
|
|
4009
4135
|
if (!TERMINAL.includes(meta.state)) return;
|
|
4010
|
-
const dir =
|
|
4136
|
+
const dir = path21.dirname(runFile(meta.id, "meta.json"));
|
|
4011
4137
|
let names;
|
|
4012
4138
|
try {
|
|
4013
|
-
names =
|
|
4139
|
+
names = fs23.readdirSync(dir);
|
|
4014
4140
|
} catch {
|
|
4015
4141
|
return;
|
|
4016
4142
|
}
|
|
4017
4143
|
for (const n of names) {
|
|
4018
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
4144
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path21.join(dir, n));
|
|
4019
4145
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
4020
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
4021
|
-
else gzipFile(
|
|
4146
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path21.join(dir, n), { force: true });
|
|
4147
|
+
else gzipFile(path21.join(dir, n));
|
|
4022
4148
|
}
|
|
4023
4149
|
}
|
|
4024
4150
|
}
|
|
@@ -4338,7 +4464,7 @@ function historyStats(sinceMs2) {
|
|
|
4338
4464
|
}
|
|
4339
4465
|
|
|
4340
4466
|
// src/core/budget.ts
|
|
4341
|
-
import
|
|
4467
|
+
import fs24 from "node:fs";
|
|
4342
4468
|
var startOfDay = (now = /* @__PURE__ */ new Date()) => new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
4343
4469
|
function activeSpend(sinceMs2) {
|
|
4344
4470
|
let total = 0;
|
|
@@ -4348,8 +4474,8 @@ function activeSpend(sinceMs2) {
|
|
|
4348
4474
|
if (!isActive(m.state) || Date.parse(m.startedAt) < sinceMs2) continue;
|
|
4349
4475
|
const target = m.ran ?? m.worker;
|
|
4350
4476
|
const events = runFile(id2, "events.jsonl");
|
|
4351
|
-
if (!
|
|
4352
|
-
const run2 = getBackend(target.backend).parse(
|
|
4477
|
+
if (!fs24.existsSync(events)) continue;
|
|
4478
|
+
const run2 = getBackend(target.backend).parse(fs24.readFileSync(events, "utf8"));
|
|
4353
4479
|
if (run2.usage.cost !== void 0) total += run2.usage.cost;
|
|
4354
4480
|
else {
|
|
4355
4481
|
const price = workerPrice(target.backend, run2.model, target.model);
|
|
@@ -4418,15 +4544,15 @@ function rankChain(chain, recordOf, work) {
|
|
|
4418
4544
|
// src/core/notify.ts
|
|
4419
4545
|
import { spawn as spawn3 } from "node:child_process";
|
|
4420
4546
|
import crypto7 from "node:crypto";
|
|
4421
|
-
import
|
|
4422
|
-
import
|
|
4547
|
+
import fs27 from "node:fs";
|
|
4548
|
+
import path23 from "node:path";
|
|
4423
4549
|
|
|
4424
4550
|
// src/core/report.ts
|
|
4425
|
-
import
|
|
4551
|
+
import fs26 from "node:fs";
|
|
4426
4552
|
|
|
4427
4553
|
// src/core/plan.ts
|
|
4428
|
-
import
|
|
4429
|
-
import
|
|
4554
|
+
import fs25 from "node:fs";
|
|
4555
|
+
import path22 from "node:path";
|
|
4430
4556
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
4431
4557
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
4432
4558
|
function structure(lines) {
|
|
@@ -4473,9 +4599,9 @@ function parsePlan(text, file2 = "") {
|
|
|
4473
4599
|
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
4474
4600
|
}
|
|
4475
4601
|
function loadPlan(file2) {
|
|
4476
|
-
const abs =
|
|
4477
|
-
if (!
|
|
4478
|
-
const plan = parsePlan(
|
|
4602
|
+
const abs = path22.resolve(file2);
|
|
4603
|
+
if (!fs25.existsSync(abs) || !fs25.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
4604
|
+
const plan = parsePlan(fs25.readFileSync(abs, "utf8"), fs25.realpathSync(abs));
|
|
4479
4605
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
4480
4606
|
return plan;
|
|
4481
4607
|
}
|
|
@@ -4494,7 +4620,7 @@ function brief(plan, task) {
|
|
|
4494
4620
|
].filter(Boolean).join("\n\n")}
|
|
4495
4621
|
`;
|
|
4496
4622
|
}
|
|
4497
|
-
var planName = (file2) =>
|
|
4623
|
+
var planName = (file2) => path22.basename(file2).replace(/\.md$/i, "");
|
|
4498
4624
|
|
|
4499
4625
|
// src/core/style.ts
|
|
4500
4626
|
var enabled = () => {
|
|
@@ -4554,7 +4680,7 @@ function duration(meta) {
|
|
|
4554
4680
|
}
|
|
4555
4681
|
function readSummary(meta) {
|
|
4556
4682
|
const f = runFile(meta.id, "summary.md");
|
|
4557
|
-
return
|
|
4683
|
+
return fs26.existsSync(f) ? fs26.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
4558
4684
|
}
|
|
4559
4685
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
4560
4686
|
const out = [];
|
|
@@ -4640,8 +4766,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
4640
4766
|
}
|
|
4641
4767
|
function live(meta) {
|
|
4642
4768
|
const f = runFile(meta.id, "events.jsonl");
|
|
4643
|
-
if (!
|
|
4644
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
4769
|
+
if (!fs26.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
4770
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs26.readFileSync(f, "utf8"));
|
|
4645
4771
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
4646
4772
|
}
|
|
4647
4773
|
function progress(meta) {
|
|
@@ -4813,12 +4939,12 @@ function claimGroup(group) {
|
|
|
4813
4939
|
try {
|
|
4814
4940
|
const ids = groupIds(group).sort();
|
|
4815
4941
|
if (ids.length < 2) return true;
|
|
4816
|
-
const dir =
|
|
4817
|
-
|
|
4818
|
-
for (const f of
|
|
4819
|
-
if (Date.now() -
|
|
4942
|
+
const dir = path23.join(home(), "notified");
|
|
4943
|
+
fs27.mkdirSync(dir, { recursive: true });
|
|
4944
|
+
for (const f of fs27.readdirSync(dir)) {
|
|
4945
|
+
if (Date.now() - fs27.statSync(path23.join(dir, f)).mtimeMs > 30 * 864e5) fs27.rmSync(path23.join(dir, f), { force: true });
|
|
4820
4946
|
}
|
|
4821
|
-
|
|
4947
|
+
fs27.writeFileSync(path23.join(dir, crypto7.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
4822
4948
|
return true;
|
|
4823
4949
|
} catch {
|
|
4824
4950
|
return false;
|
|
@@ -4852,30 +4978,30 @@ function notifyEnded(meta) {
|
|
|
4852
4978
|
}
|
|
4853
4979
|
|
|
4854
4980
|
// src/core/templates.ts
|
|
4855
|
-
import
|
|
4856
|
-
import
|
|
4981
|
+
import fs29 from "node:fs";
|
|
4982
|
+
import path25 from "node:path";
|
|
4857
4983
|
|
|
4858
4984
|
// src/core/install.ts
|
|
4859
|
-
import
|
|
4985
|
+
import fs28 from "node:fs";
|
|
4860
4986
|
import os11 from "node:os";
|
|
4861
|
-
import
|
|
4987
|
+
import path24 from "node:path";
|
|
4862
4988
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4863
4989
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
4864
4990
|
function packageRoot() {
|
|
4865
|
-
return
|
|
4991
|
+
return path24.resolve(path24.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
4866
4992
|
}
|
|
4867
4993
|
function skillNames(root = packageRoot()) {
|
|
4868
|
-
const dir =
|
|
4869
|
-
if (!
|
|
4870
|
-
return
|
|
4994
|
+
const dir = path24.join(root, "skills");
|
|
4995
|
+
if (!fs28.existsSync(dir)) return [];
|
|
4996
|
+
return fs28.readdirSync(dir).filter((d) => fs28.existsSync(path24.join(dir, d, "SKILL.md"))).sort();
|
|
4871
4997
|
}
|
|
4872
4998
|
function skillTargets() {
|
|
4873
4999
|
const home3 = os11.homedir();
|
|
4874
|
-
const targets = [
|
|
4875
|
-
if (
|
|
5000
|
+
const targets = [path24.join(home3, ".agents", "skills")];
|
|
5001
|
+
if (fs28.existsSync(path24.join(home3, ".claude"))) targets.push(path24.join(home3, ".claude", "skills"));
|
|
4876
5002
|
return targets;
|
|
4877
5003
|
}
|
|
4878
|
-
var launcherPath = () =>
|
|
5004
|
+
var launcherPath = () => path24.join(os11.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
4879
5005
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
4880
5006
|
function launcherScript(bundle) {
|
|
4881
5007
|
if (process.platform === "win32") {
|
|
@@ -4905,7 +5031,7 @@ exit 127
|
|
|
4905
5031
|
function isOurLauncher(file2, root) {
|
|
4906
5032
|
if (linksInto(file2, root)) return true;
|
|
4907
5033
|
try {
|
|
4908
|
-
const s =
|
|
5034
|
+
const s = fs28.readFileSync(file2, "utf8");
|
|
4909
5035
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
4910
5036
|
} catch {
|
|
4911
5037
|
return false;
|
|
@@ -4913,64 +5039,64 @@ function isOurLauncher(file2, root) {
|
|
|
4913
5039
|
}
|
|
4914
5040
|
function isPitroomLauncher(file2) {
|
|
4915
5041
|
try {
|
|
4916
|
-
return
|
|
5042
|
+
return fs28.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
4917
5043
|
} catch {
|
|
4918
5044
|
return false;
|
|
4919
5045
|
}
|
|
4920
5046
|
}
|
|
4921
5047
|
function placeLauncher(bundle, root, force) {
|
|
4922
5048
|
const dest = launcherPath();
|
|
4923
|
-
|
|
4924
|
-
const exists =
|
|
5049
|
+
fs28.mkdirSync(path24.dirname(dest), { recursive: true });
|
|
5050
|
+
const exists = fs28.lstatSync(dest, { throwIfNoEntry: false });
|
|
4925
5051
|
if (exists && !isOurLauncher(dest, root)) {
|
|
4926
5052
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
4927
|
-
|
|
5053
|
+
fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
4928
5054
|
} else if (exists) {
|
|
4929
|
-
|
|
5055
|
+
fs28.rmSync(dest, { force: true });
|
|
4930
5056
|
}
|
|
4931
|
-
|
|
5057
|
+
fs28.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
4932
5058
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
4933
5059
|
}
|
|
4934
5060
|
function linksInto(link, dir) {
|
|
4935
|
-
const st =
|
|
5061
|
+
const st = fs28.lstatSync(link, { throwIfNoEntry: false });
|
|
4936
5062
|
if (!st?.isSymbolicLink()) return false;
|
|
4937
|
-
const target =
|
|
4938
|
-
const rel =
|
|
4939
|
-
return !rel.startsWith("..") && !
|
|
5063
|
+
const target = path24.resolve(path24.dirname(link), fs28.readlinkSync(link));
|
|
5064
|
+
const rel = path24.relative(dir, target);
|
|
5065
|
+
return !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
4940
5066
|
}
|
|
4941
5067
|
function place(src, dest, opts) {
|
|
4942
|
-
|
|
4943
|
-
const st =
|
|
5068
|
+
fs28.mkdirSync(path24.dirname(dest), { recursive: true });
|
|
5069
|
+
const st = fs28.lstatSync(dest, { throwIfNoEntry: false });
|
|
4944
5070
|
if (st && !st.isSymbolicLink()) {
|
|
4945
5071
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
4946
|
-
|
|
5072
|
+
fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
4947
5073
|
} else if (st) {
|
|
4948
|
-
|
|
5074
|
+
fs28.unlinkSync(dest);
|
|
4949
5075
|
}
|
|
4950
|
-
if (opts.copy)
|
|
4951
|
-
else
|
|
5076
|
+
if (opts.copy) fs28.cpSync(src, dest, { recursive: true });
|
|
5077
|
+
else fs28.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs28.statSync(src).isDirectory() ? "dir" : "file");
|
|
4952
5078
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
4953
5079
|
}
|
|
4954
5080
|
function install(opts) {
|
|
4955
5081
|
const root = packageRoot();
|
|
4956
5082
|
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
4957
|
-
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${
|
|
5083
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path24.join(root, "skills")}`);
|
|
4958
5084
|
const out = [];
|
|
4959
5085
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
4960
5086
|
for (const legacy of LEGACY) {
|
|
4961
|
-
const l =
|
|
5087
|
+
const l = path24.join(base2, legacy);
|
|
4962
5088
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
4963
|
-
|
|
5089
|
+
fs28.unlinkSync(l);
|
|
4964
5090
|
out.push(`\u2714 removed old link ${l}`);
|
|
4965
5091
|
}
|
|
4966
5092
|
}
|
|
4967
|
-
for (const name of skills2) out.push(place(
|
|
5093
|
+
for (const name of skills2) out.push(place(path24.join(root, "skills", name), path24.join(base2, name), opts));
|
|
4968
5094
|
}
|
|
4969
|
-
const bundle =
|
|
4970
|
-
if (
|
|
5095
|
+
const bundle = path24.join(root, "dist", "pitroom.mjs");
|
|
5096
|
+
if (fs28.existsSync(bundle)) {
|
|
4971
5097
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
4972
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
4973
|
-
if (!onPath) out.push(`! ${
|
|
5098
|
+
const onPath = (process.env.PATH ?? "").split(path24.delimiter).some((d) => path24.resolve(d) === path24.dirname(launcherPath()));
|
|
5099
|
+
if (!onPath) out.push(`! ${path24.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
4974
5100
|
}
|
|
4975
5101
|
return out;
|
|
4976
5102
|
}
|
|
@@ -4978,24 +5104,24 @@ function uninstall() {
|
|
|
4978
5104
|
const root = packageRoot();
|
|
4979
5105
|
const out = [];
|
|
4980
5106
|
for (const base2 of skillTargets()) {
|
|
4981
|
-
if (!
|
|
4982
|
-
for (const name of
|
|
4983
|
-
const l =
|
|
5107
|
+
if (!fs28.existsSync(base2)) continue;
|
|
5108
|
+
for (const name of fs28.readdirSync(base2)) {
|
|
5109
|
+
const l = path24.join(base2, name);
|
|
4984
5110
|
if (linksInto(l, root)) {
|
|
4985
|
-
|
|
5111
|
+
fs28.unlinkSync(l);
|
|
4986
5112
|
out.push(`\u2714 removed ${l}`);
|
|
4987
5113
|
}
|
|
4988
5114
|
}
|
|
4989
5115
|
}
|
|
4990
5116
|
if (isOurLauncher(launcherPath(), root)) {
|
|
4991
|
-
|
|
5117
|
+
fs28.rmSync(launcherPath(), { force: true });
|
|
4992
5118
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
4993
5119
|
}
|
|
4994
5120
|
return out.length ? out : ["nothing to remove"];
|
|
4995
5121
|
}
|
|
4996
5122
|
function installedSkills() {
|
|
4997
5123
|
const names = skillNames();
|
|
4998
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
5124
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs28.existsSync(path24.join(base2, n, "SKILL.md"))) }));
|
|
4999
5125
|
}
|
|
5000
5126
|
|
|
5001
5127
|
// src/core/templates.ts
|
|
@@ -5006,9 +5132,9 @@ var FILES = {
|
|
|
5006
5132
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
5007
5133
|
};
|
|
5008
5134
|
function loadTemplate(name, root = packageRoot()) {
|
|
5009
|
-
const file2 =
|
|
5010
|
-
if (!
|
|
5011
|
-
return
|
|
5135
|
+
const file2 = path25.join(root, "skills", FILES[name]);
|
|
5136
|
+
if (!fs29.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
5137
|
+
return fs29.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
5012
5138
|
}
|
|
5013
5139
|
function fill(template, values) {
|
|
5014
5140
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -5018,10 +5144,10 @@ function fill(template, values) {
|
|
|
5018
5144
|
|
|
5019
5145
|
// src/core/slots.ts
|
|
5020
5146
|
import crypto8 from "node:crypto";
|
|
5021
|
-
import
|
|
5022
|
-
import
|
|
5023
|
-
var slotsDir = () =>
|
|
5024
|
-
var locksDir = () =>
|
|
5147
|
+
import fs30 from "node:fs";
|
|
5148
|
+
import path26 from "node:path";
|
|
5149
|
+
var slotsDir = () => path26.join(home(), "slots");
|
|
5150
|
+
var locksDir = () => path26.join(home(), "locks");
|
|
5025
5151
|
var STARTUP_GRACE_MS = 3e4;
|
|
5026
5152
|
function holderActive(runId, self) {
|
|
5027
5153
|
if (!runId || runId === self) return false;
|
|
@@ -5035,23 +5161,23 @@ function holderActive(runId, self) {
|
|
|
5035
5161
|
}
|
|
5036
5162
|
}
|
|
5037
5163
|
function tryClaim(file2, runId) {
|
|
5038
|
-
|
|
5164
|
+
fs30.mkdirSync(path26.dirname(file2), { recursive: true });
|
|
5039
5165
|
try {
|
|
5040
|
-
|
|
5166
|
+
fs30.writeFileSync(file2, runId, { flag: "wx" });
|
|
5041
5167
|
return true;
|
|
5042
5168
|
} catch (e) {
|
|
5043
5169
|
if (e.code !== "EEXIST") throw e;
|
|
5044
5170
|
}
|
|
5045
5171
|
let owner = "";
|
|
5046
5172
|
try {
|
|
5047
|
-
owner =
|
|
5173
|
+
owner = fs30.readFileSync(file2, "utf8").trim();
|
|
5048
5174
|
} catch {
|
|
5049
5175
|
}
|
|
5050
5176
|
if (owner === runId) return true;
|
|
5051
5177
|
if (holderActive(owner, runId)) return false;
|
|
5052
|
-
|
|
5178
|
+
fs30.rmSync(file2, { force: true });
|
|
5053
5179
|
try {
|
|
5054
|
-
|
|
5180
|
+
fs30.writeFileSync(file2, runId, { flag: "wx" });
|
|
5055
5181
|
return true;
|
|
5056
5182
|
} catch {
|
|
5057
5183
|
return false;
|
|
@@ -5060,25 +5186,25 @@ function tryClaim(file2, runId) {
|
|
|
5060
5186
|
function releaseIfOwner(file2, runId) {
|
|
5061
5187
|
if (!file2) return;
|
|
5062
5188
|
try {
|
|
5063
|
-
if (
|
|
5189
|
+
if (fs30.readFileSync(file2, "utf8").trim() === runId) fs30.rmSync(file2, { force: true });
|
|
5064
5190
|
} catch {
|
|
5065
5191
|
}
|
|
5066
5192
|
}
|
|
5067
5193
|
function tryAcquireSlot(runId, maxParallel) {
|
|
5068
5194
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
5069
|
-
const file2 =
|
|
5195
|
+
const file2 = path26.join(slotsDir(), `slot-${n}`);
|
|
5070
5196
|
if (tryClaim(file2, runId)) return file2;
|
|
5071
5197
|
}
|
|
5072
5198
|
return void 0;
|
|
5073
5199
|
}
|
|
5074
5200
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
5075
|
-
var lockFile = (repoRoot2) =>
|
|
5201
|
+
var lockFile = (repoRoot2) => path26.join(locksDir(), `write-${crypto8.createHash("sha1").update(path26.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
5076
5202
|
function acquireWriteLock(repoRoot2, runId) {
|
|
5077
5203
|
const file2 = lockFile(repoRoot2);
|
|
5078
5204
|
if (tryClaim(file2, runId)) return;
|
|
5079
5205
|
let owner = "";
|
|
5080
5206
|
try {
|
|
5081
|
-
owner =
|
|
5207
|
+
owner = fs30.readFileSync(file2, "utf8").trim();
|
|
5082
5208
|
} catch {
|
|
5083
5209
|
}
|
|
5084
5210
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -5128,13 +5254,14 @@ ${task}`;
|
|
|
5128
5254
|
}
|
|
5129
5255
|
|
|
5130
5256
|
// src/core/refs.ts
|
|
5131
|
-
import
|
|
5132
|
-
import
|
|
5257
|
+
import fs31 from "node:fs";
|
|
5258
|
+
import path27 from "node:path";
|
|
5133
5259
|
var FILE_EXTENSIONS = new Set(
|
|
5134
5260
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
5135
5261
|
);
|
|
5136
5262
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
5137
5263
|
var MAX_INDEXED = 6e4;
|
|
5264
|
+
var MAX_SKIPPED = 3e5;
|
|
5138
5265
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
5139
5266
|
var REF = new RegExp(
|
|
5140
5267
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -5180,26 +5307,30 @@ function extractRefs(answer) {
|
|
|
5180
5307
|
return [...seen.values()];
|
|
5181
5308
|
}
|
|
5182
5309
|
function verifyRefs(refs, dirs) {
|
|
5183
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
5310
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path27.resolve(d)))];
|
|
5184
5311
|
const invalid = [];
|
|
5185
5312
|
const lineCache = /* @__PURE__ */ new Map();
|
|
5186
5313
|
const load = (file2) => {
|
|
5187
5314
|
if (!lineCache.has(file2)) {
|
|
5188
|
-
const size =
|
|
5189
|
-
const lines = size > MAX_BYTES ? null :
|
|
5315
|
+
const size = fs31.statSync(file2).size;
|
|
5316
|
+
const lines = size > MAX_BYTES ? null : fs31.readFileSync(file2, "utf8").split("\n");
|
|
5190
5317
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
5191
5318
|
lineCache.set(file2, lines);
|
|
5192
5319
|
}
|
|
5193
5320
|
return lineCache.get(file2);
|
|
5194
5321
|
};
|
|
5195
5322
|
let index;
|
|
5323
|
+
let skipped;
|
|
5196
5324
|
const byName = () => index ??= indexFiles(roots);
|
|
5325
|
+
const inSkipped = () => skipped ??= indexFiles(roots, true);
|
|
5197
5326
|
const candidates = (ref) => {
|
|
5198
5327
|
const direct = resolve(ref.file, roots);
|
|
5199
5328
|
if (direct) return [direct];
|
|
5200
|
-
if (
|
|
5329
|
+
if (path27.isAbsolute(ref.file)) return [];
|
|
5201
5330
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
5202
|
-
|
|
5331
|
+
const fits = (f) => path27.basename(f) === wanted || foldersInOrder(f.split(path27.sep).join("/"), wanted);
|
|
5332
|
+
const own = (byName().get(path27.basename(wanted)) ?? []).filter(fits);
|
|
5333
|
+
return (own.length || !isFileName(wanted) ? own : (inSkipped().get(path27.basename(wanted)) ?? []).filter(fits)).slice(0, 20);
|
|
5203
5334
|
};
|
|
5204
5335
|
const check = (file2, ref) => {
|
|
5205
5336
|
const lines = load(file2);
|
|
@@ -5214,7 +5345,7 @@ function verifyRefs(refs, dirs) {
|
|
|
5214
5345
|
if (!files.length) {
|
|
5215
5346
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
5216
5347
|
total++;
|
|
5217
|
-
invalid.push({ ref: ref.text, reason:
|
|
5348
|
+
invalid.push({ ref: ref.text, reason: path27.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
|
|
5218
5349
|
continue;
|
|
5219
5350
|
}
|
|
5220
5351
|
total++;
|
|
@@ -5235,39 +5366,54 @@ function enclosedBy(lines, ref) {
|
|
|
5235
5366
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
5236
5367
|
}
|
|
5237
5368
|
var isFileName = (name) => {
|
|
5238
|
-
const base2 =
|
|
5369
|
+
const base2 = path27.basename(name);
|
|
5239
5370
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
5240
5371
|
};
|
|
5241
|
-
function
|
|
5372
|
+
function foldersInOrder(file2, wanted) {
|
|
5373
|
+
const want = wanted.split("/");
|
|
5374
|
+
const have = file2.split("/");
|
|
5375
|
+
if (have.at(-1) !== want.at(-1)) return false;
|
|
5376
|
+
let i = have.length - 2;
|
|
5377
|
+
for (let j = want.length - 2; j >= 0; j--) {
|
|
5378
|
+
while (i >= 0 && have[i] !== want[j]) i--;
|
|
5379
|
+
if (i < 0) return false;
|
|
5380
|
+
i--;
|
|
5381
|
+
}
|
|
5382
|
+
return true;
|
|
5383
|
+
}
|
|
5384
|
+
function indexFiles(roots, skipped = false) {
|
|
5242
5385
|
const index = /* @__PURE__ */ new Map();
|
|
5386
|
+
const cap = skipped ? MAX_SKIPPED : MAX_INDEXED;
|
|
5243
5387
|
let count = 0;
|
|
5244
|
-
const walk = (dir) => {
|
|
5388
|
+
const walk = (dir, within) => {
|
|
5245
5389
|
let entries;
|
|
5246
5390
|
try {
|
|
5247
|
-
entries =
|
|
5391
|
+
entries = fs31.readdirSync(dir, { withFileTypes: true });
|
|
5248
5392
|
} catch {
|
|
5249
5393
|
return;
|
|
5250
5394
|
}
|
|
5251
5395
|
for (const e of entries) {
|
|
5252
|
-
if (count >=
|
|
5396
|
+
if (count >= cap) return;
|
|
5253
5397
|
if (e.isDirectory()) {
|
|
5254
|
-
if (
|
|
5255
|
-
|
|
5398
|
+
if (e.name === ".git") continue;
|
|
5399
|
+
const skip = SKIP_DIRS2.has(e.name);
|
|
5400
|
+
if (skipped || !skip) walk(path27.join(dir, e.name), within || skip);
|
|
5401
|
+
} else if (e.isFile() && within === skipped) {
|
|
5256
5402
|
count++;
|
|
5257
5403
|
const list2 = index.get(e.name);
|
|
5258
|
-
if (list2) list2.push(
|
|
5259
|
-
else index.set(e.name, [
|
|
5404
|
+
if (list2) list2.push(path27.join(dir, e.name));
|
|
5405
|
+
else index.set(e.name, [path27.join(dir, e.name)]);
|
|
5260
5406
|
}
|
|
5261
5407
|
}
|
|
5262
5408
|
};
|
|
5263
|
-
for (const r of roots) walk(r);
|
|
5409
|
+
for (const r of roots) walk(r, false);
|
|
5264
5410
|
return index;
|
|
5265
5411
|
}
|
|
5266
5412
|
function resolve(ref, roots) {
|
|
5267
|
-
const candidates =
|
|
5413
|
+
const candidates = path27.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path27.join(r, ref));
|
|
5268
5414
|
return candidates.find((c) => {
|
|
5269
5415
|
try {
|
|
5270
|
-
return
|
|
5416
|
+
return fs31.statSync(c).isFile() && inside2(c, roots);
|
|
5271
5417
|
} catch {
|
|
5272
5418
|
return false;
|
|
5273
5419
|
}
|
|
@@ -5275,14 +5421,14 @@ function resolve(ref, roots) {
|
|
|
5275
5421
|
}
|
|
5276
5422
|
function inside2(file2, roots) {
|
|
5277
5423
|
return roots.some((r) => {
|
|
5278
|
-
const rel =
|
|
5279
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
5424
|
+
const rel = path27.relative(r, path27.resolve(file2));
|
|
5425
|
+
return rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
|
|
5280
5426
|
});
|
|
5281
5427
|
}
|
|
5282
5428
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5283
5429
|
|
|
5284
5430
|
// src/core/run.ts
|
|
5285
|
-
var VERSION2 = true ? "0.
|
|
5431
|
+
var VERSION2 = true ? "0.27.0" : "0.0.0-dev";
|
|
5286
5432
|
function planWork(o) {
|
|
5287
5433
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
5288
5434
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -5314,7 +5460,7 @@ function prepareRun(o) {
|
|
|
5314
5460
|
parent = readMeta(o.continueFrom);
|
|
5315
5461
|
if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
|
|
5316
5462
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
5317
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
5463
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs32.existsSync(parent.worktree))) {
|
|
5318
5464
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
5319
5465
|
}
|
|
5320
5466
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -5344,22 +5490,22 @@ function prepareRun(o) {
|
|
|
5344
5490
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
5345
5491
|
}
|
|
5346
5492
|
const mode = parent?.mode ?? o.mode;
|
|
5347
|
-
const dir = canonical(
|
|
5348
|
-
if (!
|
|
5493
|
+
const dir = canonical(path28.resolve(parent?.dir ?? o.dir));
|
|
5494
|
+
if (!fs32.existsSync(dir) || !fs32.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
5349
5495
|
const root = repoRoot(dir);
|
|
5350
5496
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
5351
5497
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
5352
5498
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
5353
5499
|
}
|
|
5354
|
-
const files = o.files.map((f) =>
|
|
5355
|
-
for (const f of files) if (!
|
|
5500
|
+
const files = o.files.map((f) => path28.resolve(f));
|
|
5501
|
+
for (const f of files) if (!fs32.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
5356
5502
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
5357
5503
|
const untrusted = [worker, ...fallback].map((t) => getBackend(t.backend)).find(readsOwnSnapshot);
|
|
5358
5504
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
5359
5505
|
if (untrusted && mode === "read" && !parent && (o.review || o.inPlace || !root)) {
|
|
5360
5506
|
warnings2.push(`this read run is not in a snapshot${root ? "" : " (not a git repository)"}, and ${untrusted.name} does not reliably stay read-only: it could change files here`);
|
|
5361
5507
|
}
|
|
5362
|
-
if (parent?.snapshot?.dir && !
|
|
5508
|
+
if (parent?.snapshot?.dir && !fs32.existsSync(parent.snapshot.dir)) {
|
|
5363
5509
|
let restored = false;
|
|
5364
5510
|
if (parent.snapshot.private && root && parent.snapshot.tree) {
|
|
5365
5511
|
try {
|
|
@@ -5416,13 +5562,13 @@ function prepareRun(o) {
|
|
|
5416
5562
|
try {
|
|
5417
5563
|
acquireWriteLock(root, meta.id);
|
|
5418
5564
|
} catch (e) {
|
|
5419
|
-
|
|
5565
|
+
fs32.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
5420
5566
|
throw e;
|
|
5421
5567
|
}
|
|
5422
5568
|
}
|
|
5423
|
-
|
|
5569
|
+
fs32.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
5424
5570
|
`);
|
|
5425
|
-
if (work.brief)
|
|
5571
|
+
if (work.brief) fs32.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
5426
5572
|
return meta;
|
|
5427
5573
|
}
|
|
5428
5574
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -5523,7 +5669,7 @@ async function execute(meta) {
|
|
|
5523
5669
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message, ...why.partial ? { partial: true } : {} });
|
|
5524
5670
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
5525
5671
|
const from = runFile(meta.id, f);
|
|
5526
|
-
if (
|
|
5672
|
+
if (fs32.existsSync(from)) fs32.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
5527
5673
|
}
|
|
5528
5674
|
writeMeta(meta);
|
|
5529
5675
|
}
|
|
@@ -5620,9 +5766,9 @@ function prepareTree(meta) {
|
|
|
5620
5766
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
5621
5767
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
5622
5768
|
meta.baseTree = snapshotTree(root);
|
|
5623
|
-
meta.worktree =
|
|
5769
|
+
meta.worktree = path28.join(worktreesDir(), meta.id);
|
|
5624
5770
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
5625
|
-
meta.cwd =
|
|
5771
|
+
meta.cwd = path28.join(meta.worktree, path28.relative(root, meta.dir));
|
|
5626
5772
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
5627
5773
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
5628
5774
|
}
|
|
@@ -5653,8 +5799,8 @@ function ownSnapshot(meta, root, backend, planned, attemptNo) {
|
|
|
5653
5799
|
try {
|
|
5654
5800
|
const s = readSnapshot(root, planned.own ? `${meta.id}-${attemptNo}` : meta.id);
|
|
5655
5801
|
meta.snapshot = { dir: s.dir, tree: s.tree, left, private: true };
|
|
5656
|
-
meta.cwd =
|
|
5657
|
-
if (!
|
|
5802
|
+
meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
|
|
5803
|
+
if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
5658
5804
|
planned.own = { snapshot: { ...meta.snapshot }, cwd: meta.cwd };
|
|
5659
5805
|
const note = `${backend.name} reads a snapshot of your project of its own (it does not reliably stay read-only)`;
|
|
5660
5806
|
if (!meta.warnings.some((w) => w.startsWith(note))) {
|
|
@@ -5686,8 +5832,8 @@ function sharedSnapshot(meta, root) {
|
|
|
5686
5832
|
const s = readSnapshot(root);
|
|
5687
5833
|
snapshot.dir = s.dir;
|
|
5688
5834
|
snapshot.tree = s.tree;
|
|
5689
|
-
meta.cwd =
|
|
5690
|
-
if (!
|
|
5835
|
+
meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
|
|
5836
|
+
if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
5691
5837
|
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
5692
5838
|
} catch (e) {
|
|
5693
5839
|
meta.snapshot = void 0;
|
|
@@ -5715,7 +5861,7 @@ ${meta.handoff}` : meta.task, !!meta.parent, !!meta.auditOf),
|
|
|
5715
5861
|
timeoutSec: meta.timeoutSec
|
|
5716
5862
|
});
|
|
5717
5863
|
}
|
|
5718
|
-
var read2 = (f) =>
|
|
5864
|
+
var read2 = (f) => fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : "";
|
|
5719
5865
|
function retryableFailure(meta, backend, res) {
|
|
5720
5866
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
5721
5867
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -5754,12 +5900,12 @@ function finalize(meta, res) {
|
|
|
5754
5900
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
5755
5901
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
5756
5902
|
meta.usage = run2.usage;
|
|
5757
|
-
|
|
5903
|
+
fs32.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
5758
5904
|
`);
|
|
5759
5905
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
5760
5906
|
if (meta.reviewOf) {
|
|
5761
5907
|
meta.verdict = parseVerdict(run2.finalText);
|
|
5762
|
-
if (meta.packageFile)
|
|
5908
|
+
if (meta.packageFile) fs32.rmSync(meta.packageFile, { force: true });
|
|
5763
5909
|
}
|
|
5764
5910
|
for (const w of backend.warnings?.(run2, read2(runFile(meta.id, "stderr.log"))) ?? []) {
|
|
5765
5911
|
if (!meta.warnings.includes(w)) meta.warnings.push(w);
|
|
@@ -5824,16 +5970,16 @@ function captureChanges(meta) {
|
|
|
5824
5970
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
5825
5971
|
meta.changes = d.changes;
|
|
5826
5972
|
meta.stats = d.stats;
|
|
5827
|
-
|
|
5973
|
+
fs32.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
5828
5974
|
}
|
|
5829
5975
|
function verifyPath() {
|
|
5830
|
-
const extra = [
|
|
5831
|
-
const parts = (process.env.PATH ?? "").split(
|
|
5832
|
-
for (const d of process.platform === "win32" ? [
|
|
5833
|
-
return parts.join(
|
|
5976
|
+
const extra = [path28.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
5977
|
+
const parts = (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean);
|
|
5978
|
+
for (const d of process.platform === "win32" ? [path28.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
5979
|
+
return parts.join(path28.delimiter);
|
|
5834
5980
|
}
|
|
5835
5981
|
function runVerify(meta) {
|
|
5836
|
-
const r =
|
|
5982
|
+
const r = spawnSync10(meta.verify, {
|
|
5837
5983
|
cwd: meta.cwd,
|
|
5838
5984
|
shell: true,
|
|
5839
5985
|
encoding: "utf8",
|
|
@@ -5843,7 +5989,7 @@ function runVerify(meta) {
|
|
|
5843
5989
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
5844
5990
|
});
|
|
5845
5991
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
5846
|
-
|
|
5992
|
+
fs32.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
5847
5993
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
5848
5994
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
5849
5995
|
}
|
|
@@ -5898,12 +6044,12 @@ function discardRun(meta) {
|
|
|
5898
6044
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
5899
6045
|
}
|
|
5900
6046
|
function cleanupWorktree(meta) {
|
|
5901
|
-
if (meta.worktree &&
|
|
6047
|
+
if (meta.worktree && fs32.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
5902
6048
|
}
|
|
5903
6049
|
|
|
5904
6050
|
// src/cli/mcp-extras.ts
|
|
5905
|
-
import
|
|
5906
|
-
import
|
|
6051
|
+
import fs33 from "node:fs";
|
|
6052
|
+
import path29 from "node:path";
|
|
5907
6053
|
|
|
5908
6054
|
// src/cli/mcp-support.ts
|
|
5909
6055
|
import { spawn as spawn5 } from "node:child_process";
|
|
@@ -6188,7 +6334,7 @@ function skillPrompts() {
|
|
|
6188
6334
|
const root = packageRoot();
|
|
6189
6335
|
for (const name of skillNames(root)) {
|
|
6190
6336
|
try {
|
|
6191
|
-
const text =
|
|
6337
|
+
const text = fs33.readFileSync(path29.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
6192
6338
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
6193
6339
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
6194
6340
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -6709,22 +6855,22 @@ async function serveMcp() {
|
|
|
6709
6855
|
|
|
6710
6856
|
// src/cli/mcp-http.ts
|
|
6711
6857
|
import crypto10 from "node:crypto";
|
|
6712
|
-
import
|
|
6858
|
+
import fs35 from "node:fs";
|
|
6713
6859
|
import http2 from "node:http";
|
|
6714
|
-
import
|
|
6860
|
+
import path31 from "node:path";
|
|
6715
6861
|
|
|
6716
6862
|
// src/core/dash.ts
|
|
6717
6863
|
import { spawn as spawn6 } from "node:child_process";
|
|
6718
6864
|
import crypto9 from "node:crypto";
|
|
6719
|
-
import
|
|
6865
|
+
import fs34 from "node:fs";
|
|
6720
6866
|
import http from "node:http";
|
|
6721
|
-
import
|
|
6867
|
+
import path30 from "node:path";
|
|
6722
6868
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
6723
6869
|
|
|
6724
6870
|
// src/core/file-diff.ts
|
|
6725
6871
|
var PREVIEW_LINES = 300;
|
|
6726
6872
|
function gitPath(value, prefix = true) {
|
|
6727
|
-
let
|
|
6873
|
+
let path39 = value;
|
|
6728
6874
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
6729
6875
|
const bytes = [];
|
|
6730
6876
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -6732,9 +6878,9 @@ function gitPath(value, prefix = true) {
|
|
|
6732
6878
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
6733
6879
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
6734
6880
|
}
|
|
6735
|
-
|
|
6881
|
+
path39 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
6736
6882
|
}
|
|
6737
|
-
return prefix ?
|
|
6883
|
+
return prefix ? path39.replace(/^[ab]\//, "") : path39;
|
|
6738
6884
|
}
|
|
6739
6885
|
function headerPath(header) {
|
|
6740
6886
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -7015,11 +7161,11 @@ var assetsDir = () => fileURLToPath4(new URL("./ui/", import.meta.url));
|
|
|
7015
7161
|
var assetCache = /* @__PURE__ */ new Map();
|
|
7016
7162
|
function readAsset(route) {
|
|
7017
7163
|
try {
|
|
7018
|
-
const file2 =
|
|
7019
|
-
const { mtimeMs } =
|
|
7164
|
+
const file2 = path30.join(assetsDir(), path30.basename(route));
|
|
7165
|
+
const { mtimeMs } = fs34.statSync(file2);
|
|
7020
7166
|
const hit = assetCache.get(route);
|
|
7021
7167
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
7022
|
-
const data =
|
|
7168
|
+
const data = fs34.readFileSync(file2);
|
|
7023
7169
|
assetCache.set(route, { mtimeMs, data });
|
|
7024
7170
|
return data;
|
|
7025
7171
|
} catch {
|
|
@@ -7148,7 +7294,7 @@ function startDash(opts) {
|
|
|
7148
7294
|
});
|
|
7149
7295
|
});
|
|
7150
7296
|
}
|
|
7151
|
-
var registryFile = () =>
|
|
7297
|
+
var registryFile = () => path30.join(home(), "dash.json");
|
|
7152
7298
|
async function ping(port) {
|
|
7153
7299
|
try {
|
|
7154
7300
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -7160,12 +7306,12 @@ async function ping(port) {
|
|
|
7160
7306
|
async function runningDash() {
|
|
7161
7307
|
let r;
|
|
7162
7308
|
try {
|
|
7163
|
-
r = JSON.parse(
|
|
7309
|
+
r = JSON.parse(fs34.readFileSync(registryFile(), "utf8"));
|
|
7164
7310
|
} catch {
|
|
7165
7311
|
return void 0;
|
|
7166
7312
|
}
|
|
7167
7313
|
if (!isAlive(r.pid)) {
|
|
7168
|
-
|
|
7314
|
+
fs34.rmSync(registryFile(), { force: true });
|
|
7169
7315
|
return void 0;
|
|
7170
7316
|
}
|
|
7171
7317
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -7182,7 +7328,7 @@ async function dashCommand(o) {
|
|
|
7182
7328
|
if (o.stop) {
|
|
7183
7329
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
7184
7330
|
process.kill(existing.pid, "SIGTERM");
|
|
7185
|
-
|
|
7331
|
+
fs34.rmSync(registryFile(), { force: true });
|
|
7186
7332
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
7187
7333
|
return 0;
|
|
7188
7334
|
}
|
|
@@ -7220,9 +7366,9 @@ async function dashCommand(o) {
|
|
|
7220
7366
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
7221
7367
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
7222
7368
|
}
|
|
7223
|
-
|
|
7369
|
+
fs34.mkdirSync(home(), { recursive: true });
|
|
7224
7370
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
7225
|
-
|
|
7371
|
+
fs34.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
7226
7372
|
renameOver(tmp, registryFile());
|
|
7227
7373
|
const stop = () => void dash.close();
|
|
7228
7374
|
process.once("SIGINT", stop);
|
|
@@ -7232,7 +7378,7 @@ async function dashCommand(o) {
|
|
|
7232
7378
|
if (o.open) openBrowser(dash.url);
|
|
7233
7379
|
await dash.closed;
|
|
7234
7380
|
try {
|
|
7235
|
-
if (JSON.parse(
|
|
7381
|
+
if (JSON.parse(fs34.readFileSync(registryFile(), "utf8")).pid === process.pid) fs34.rmSync(registryFile(), { force: true });
|
|
7236
7382
|
} catch {
|
|
7237
7383
|
}
|
|
7238
7384
|
return 0;
|
|
@@ -7244,10 +7390,10 @@ var MAX_BODY = 4e6;
|
|
|
7244
7390
|
var MAX_SESSIONS = 64;
|
|
7245
7391
|
var IDLE_MS = 60 * 6e4;
|
|
7246
7392
|
var PATH = "/mcp";
|
|
7247
|
-
var tokenFile = () =>
|
|
7393
|
+
var tokenFile = () => path31.join(home(), "mcp-token");
|
|
7248
7394
|
var readToken = (file2) => {
|
|
7249
7395
|
try {
|
|
7250
|
-
const kept =
|
|
7396
|
+
const kept = fs35.readFileSync(file2, "utf8").trim();
|
|
7251
7397
|
return kept.length >= 16 ? kept : void 0;
|
|
7252
7398
|
} catch {
|
|
7253
7399
|
return void 0;
|
|
@@ -7262,10 +7408,10 @@ function mcpToken() {
|
|
|
7262
7408
|
const file2 = tokenFile();
|
|
7263
7409
|
let token = readToken(file2);
|
|
7264
7410
|
if (!token) {
|
|
7265
|
-
|
|
7411
|
+
fs35.mkdirSync(path31.dirname(file2), { recursive: true });
|
|
7266
7412
|
token = crypto10.randomBytes(32).toString("hex");
|
|
7267
7413
|
try {
|
|
7268
|
-
|
|
7414
|
+
fs35.writeFileSync(file2, `${token}
|
|
7269
7415
|
`, { mode: 384, flag: "wx" });
|
|
7270
7416
|
} catch (e) {
|
|
7271
7417
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -7274,7 +7420,7 @@ function mcpToken() {
|
|
|
7274
7420
|
}
|
|
7275
7421
|
}
|
|
7276
7422
|
try {
|
|
7277
|
-
|
|
7423
|
+
fs35.chmodSync(file2, 384);
|
|
7278
7424
|
} catch {
|
|
7279
7425
|
}
|
|
7280
7426
|
return { token, from: file2 };
|
|
@@ -7455,17 +7601,17 @@ async function serveMcpHttp(opts) {
|
|
|
7455
7601
|
}
|
|
7456
7602
|
|
|
7457
7603
|
// src/cli/commands.ts
|
|
7458
|
-
import { spawnSync as
|
|
7459
|
-
import
|
|
7460
|
-
import
|
|
7604
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
7605
|
+
import fs42 from "node:fs";
|
|
7606
|
+
import path37 from "node:path";
|
|
7461
7607
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7462
7608
|
|
|
7463
7609
|
// src/core/init.ts
|
|
7464
|
-
import
|
|
7465
|
-
import
|
|
7610
|
+
import fs36 from "node:fs";
|
|
7611
|
+
import path32 from "node:path";
|
|
7466
7612
|
var FREE = /-free$/;
|
|
7467
7613
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
7468
|
-
var isFound = (binary8) =>
|
|
7614
|
+
var isFound = (binary8) => path32.isAbsolute(binary8) && fs36.existsSync(binary8);
|
|
7469
7615
|
function planInit(opts = {}) {
|
|
7470
7616
|
const file2 = configPath();
|
|
7471
7617
|
const workers = backendIds().map((id2) => {
|
|
@@ -7511,15 +7657,15 @@ function planInit(opts = {}) {
|
|
|
7511
7657
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
7512
7658
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
7513
7659
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
7514
|
-
return { path: file2, exists:
|
|
7660
|
+
return { path: file2, exists: fs36.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
7515
7661
|
}
|
|
7516
7662
|
function writeInit(plan, force) {
|
|
7517
7663
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
7518
7664
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
7519
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
7520
|
-
|
|
7521
|
-
if (plan.exists)
|
|
7522
|
-
|
|
7665
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path32.basename(plan.path)}.bak)`);
|
|
7666
|
+
fs36.mkdirSync(path32.dirname(plan.path), { recursive: true });
|
|
7667
|
+
if (plan.exists) fs36.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
7668
|
+
fs36.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
7523
7669
|
`);
|
|
7524
7670
|
return plan.path;
|
|
7525
7671
|
}
|
|
@@ -7539,18 +7685,18 @@ function formatInit(plan, written) {
|
|
|
7539
7685
|
}
|
|
7540
7686
|
|
|
7541
7687
|
// src/core/mcp-install.ts
|
|
7542
|
-
import { spawnSync as
|
|
7543
|
-
import
|
|
7688
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
7689
|
+
import fs37 from "node:fs";
|
|
7544
7690
|
import os12 from "node:os";
|
|
7545
|
-
import
|
|
7691
|
+
import path33 from "node:path";
|
|
7546
7692
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
7547
7693
|
var NAME = "pitroom";
|
|
7548
7694
|
function mcpCommand() {
|
|
7549
7695
|
const launcher = launcherPath();
|
|
7550
7696
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
7551
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7552
|
-
const p =
|
|
7553
|
-
if (
|
|
7697
|
+
for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
|
|
7698
|
+
const p = path33.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
7699
|
+
if (fs37.existsSync(p)) return { command: path33.resolve(p), args: ["mcp"] };
|
|
7554
7700
|
}
|
|
7555
7701
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
7556
7702
|
}
|
|
@@ -7560,13 +7706,13 @@ var same = (a, b) => {
|
|
|
7560
7706
|
};
|
|
7561
7707
|
function run(backendId, args) {
|
|
7562
7708
|
const { command, prefix } = resolveCommand(getBackend(backendId).binary());
|
|
7563
|
-
const r =
|
|
7709
|
+
const r = spawnSync11(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
7564
7710
|
return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
7565
7711
|
}
|
|
7566
7712
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
7567
7713
|
var readJson = (file2) => {
|
|
7568
7714
|
try {
|
|
7569
|
-
return JSON.parse(
|
|
7715
|
+
return JSON.parse(fs37.readFileSync(file2, "utf8"));
|
|
7570
7716
|
} catch {
|
|
7571
7717
|
return void 0;
|
|
7572
7718
|
}
|
|
@@ -7576,7 +7722,7 @@ var jsonState = (file2, c) => {
|
|
|
7576
7722
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
7577
7723
|
};
|
|
7578
7724
|
var home2 = () => os12.homedir();
|
|
7579
|
-
var geminiDir = () =>
|
|
7725
|
+
var geminiDir = () => path33.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
7580
7726
|
function cliClient(id2, backend, name, where, o) {
|
|
7581
7727
|
return {
|
|
7582
7728
|
id: id2,
|
|
@@ -7599,43 +7745,43 @@ function jsonClient(id2, name, file2, dir) {
|
|
|
7599
7745
|
return {
|
|
7600
7746
|
id: id2,
|
|
7601
7747
|
name,
|
|
7602
|
-
found: () =>
|
|
7748
|
+
found: () => fs37.existsSync(dir()),
|
|
7603
7749
|
state: (c) => jsonState(file2(), c),
|
|
7604
7750
|
add: (c) => {
|
|
7605
7751
|
let config = {};
|
|
7606
|
-
if (
|
|
7752
|
+
if (fs37.existsSync(file2())) {
|
|
7607
7753
|
config = readJson(file2());
|
|
7608
7754
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
7609
7755
|
const backup = `${file2()}.bak-pitroom`;
|
|
7610
|
-
if (!
|
|
7756
|
+
if (!fs37.existsSync(backup)) fs37.copyFileSync(file2(), backup);
|
|
7611
7757
|
}
|
|
7612
7758
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
7613
|
-
|
|
7759
|
+
fs37.mkdirSync(path33.dirname(file2()), { recursive: true });
|
|
7614
7760
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
7615
|
-
|
|
7761
|
+
fs37.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
7616
7762
|
`);
|
|
7617
|
-
|
|
7763
|
+
fs37.renameSync(tmp, file2());
|
|
7618
7764
|
return { ok: true, message: rel() };
|
|
7619
7765
|
},
|
|
7620
7766
|
remove: () => {
|
|
7621
7767
|
const config = readJson(file2());
|
|
7622
7768
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
7623
7769
|
delete config.mcpServers[NAME];
|
|
7624
|
-
|
|
7770
|
+
fs37.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
7625
7771
|
`);
|
|
7626
7772
|
return { ok: true, message: rel() };
|
|
7627
7773
|
}
|
|
7628
7774
|
};
|
|
7629
7775
|
}
|
|
7630
7776
|
function desktopFile() {
|
|
7631
|
-
if (process.platform === "darwin") return
|
|
7632
|
-
if (process.platform === "win32") return
|
|
7633
|
-
return
|
|
7777
|
+
if (process.platform === "darwin") return path33.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
7778
|
+
if (process.platform === "win32") return path33.join(process.env.APPDATA ?? path33.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
7779
|
+
return path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
7634
7780
|
}
|
|
7635
7781
|
function codexState(c) {
|
|
7636
7782
|
let text;
|
|
7637
7783
|
try {
|
|
7638
|
-
text =
|
|
7784
|
+
text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
7639
7785
|
} catch {
|
|
7640
7786
|
return "absent";
|
|
7641
7787
|
}
|
|
@@ -7653,7 +7799,7 @@ function codexState(c) {
|
|
|
7653
7799
|
function clients() {
|
|
7654
7800
|
return [
|
|
7655
7801
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
7656
|
-
state: (c) => jsonState(
|
|
7802
|
+
state: (c) => jsonState(path33.join(home2(), ".claude.json"), c),
|
|
7657
7803
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
7658
7804
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
7659
7805
|
}),
|
|
@@ -7663,12 +7809,12 @@ function clients() {
|
|
|
7663
7809
|
remove: () => ["mcp", "remove", NAME]
|
|
7664
7810
|
}),
|
|
7665
7811
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
7666
|
-
state: (c) => jsonState(
|
|
7812
|
+
state: (c) => jsonState(path33.join(geminiDir(), "settings.json"), c),
|
|
7667
7813
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
7668
7814
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
7669
7815
|
}),
|
|
7670
|
-
jsonClient("cursor", "Cursor", () =>
|
|
7671
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
7816
|
+
jsonClient("cursor", "Cursor", () => path33.join(home2(), ".cursor", "mcp.json"), () => path33.join(home2(), ".cursor")),
|
|
7817
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path33.dirname(desktopFile()))
|
|
7672
7818
|
];
|
|
7673
7819
|
}
|
|
7674
7820
|
function attempt2(f) {
|
|
@@ -7739,16 +7885,16 @@ var parseClients = (value) => {
|
|
|
7739
7885
|
|
|
7740
7886
|
// src/core/plan-status.ts
|
|
7741
7887
|
import crypto11 from "node:crypto";
|
|
7742
|
-
import
|
|
7743
|
-
import
|
|
7888
|
+
import fs38 from "node:fs";
|
|
7889
|
+
import path34 from "node:path";
|
|
7744
7890
|
function notesFile(plan) {
|
|
7745
7891
|
const id2 = crypto11.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
7746
|
-
return
|
|
7892
|
+
return path34.join(home(), "plans", id2, "notes.md");
|
|
7747
7893
|
}
|
|
7748
7894
|
function readNotes(plan) {
|
|
7749
7895
|
const f = notesFile(plan);
|
|
7750
|
-
if (!
|
|
7751
|
-
return
|
|
7896
|
+
if (!fs38.existsSync(f)) return [];
|
|
7897
|
+
return fs38.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
7752
7898
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
7753
7899
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
7754
7900
|
});
|
|
@@ -7756,11 +7902,11 @@ function readNotes(plan) {
|
|
|
7756
7902
|
function addNote(planFile, text) {
|
|
7757
7903
|
const plan = loadPlan(planFile);
|
|
7758
7904
|
const f = notesFile(plan);
|
|
7759
|
-
|
|
7760
|
-
if (!
|
|
7905
|
+
fs38.mkdirSync(path34.dirname(f), { recursive: true });
|
|
7906
|
+
if (!fs38.existsSync(f)) fs38.writeFileSync(f, `# plan: ${plan.file}
|
|
7761
7907
|
`);
|
|
7762
7908
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
7763
|
-
|
|
7909
|
+
fs38.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
7764
7910
|
`);
|
|
7765
7911
|
return f;
|
|
7766
7912
|
}
|
|
@@ -7828,8 +7974,8 @@ ${rulings}`,
|
|
|
7828
7974
|
|
|
7829
7975
|
// src/core/cache.ts
|
|
7830
7976
|
import crypto12 from "node:crypto";
|
|
7831
|
-
import
|
|
7832
|
-
import
|
|
7977
|
+
import fs39 from "node:fs";
|
|
7978
|
+
import path35 from "node:path";
|
|
7833
7979
|
function preferredReadsOwnSnapshot(o) {
|
|
7834
7980
|
try {
|
|
7835
7981
|
const preferred = effective().worker.value;
|
|
@@ -7842,20 +7988,20 @@ var SCAN2 = 500;
|
|
|
7842
7988
|
function cacheKey(o) {
|
|
7843
7989
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
7844
7990
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
7845
|
-
const dir = canonical(
|
|
7991
|
+
const dir = canonical(path35.resolve(o.dir));
|
|
7846
7992
|
const root = repoRoot(dir);
|
|
7847
7993
|
if (!root) return void 0;
|
|
7848
7994
|
try {
|
|
7849
7995
|
const tree = snapshotTree(root);
|
|
7850
7996
|
const head = commitOf(root, "HEAD") ?? "";
|
|
7851
7997
|
const files = o.files.map((f) => {
|
|
7852
|
-
const abs =
|
|
7853
|
-
return [abs, crypto12.createHash("sha256").update(
|
|
7998
|
+
const abs = path35.resolve(f);
|
|
7999
|
+
return [abs, crypto12.createHash("sha256").update(fs39.readFileSync(abs)).digest("hex")];
|
|
7854
8000
|
});
|
|
7855
8001
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
7856
8002
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
7857
8003
|
const where = !o.inPlace && (preferredReadsOwnSnapshot(o) || wantSnapshot(readIn2, root, dir)) ? "snapshot" : "project";
|
|
7858
|
-
const key = crypto12.createHash("sha256").update(JSON.stringify([question,
|
|
8004
|
+
const key = crypto12.createHash("sha256").update(JSON.stringify([question, path35.relative(root, dir), files, where])).digest("hex");
|
|
7859
8005
|
return { key, state: `${head}:${tree}`, where };
|
|
7860
8006
|
} catch {
|
|
7861
8007
|
return void 0;
|
|
@@ -7898,13 +8044,13 @@ function ago(iso, now = Date.now()) {
|
|
|
7898
8044
|
var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
|
|
7899
8045
|
|
|
7900
8046
|
// src/core/eval.ts
|
|
7901
|
-
import { spawnSync as
|
|
7902
|
-
import
|
|
8047
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
8048
|
+
import fs40 from "node:fs";
|
|
7903
8049
|
var KINDS = ["definition", "count", "set"];
|
|
7904
8050
|
function loadEval(file2) {
|
|
7905
8051
|
let raw;
|
|
7906
8052
|
try {
|
|
7907
|
-
raw = JSON.parse(
|
|
8053
|
+
raw = JSON.parse(fs40.readFileSync(file2, "utf8"));
|
|
7908
8054
|
} catch (e) {
|
|
7909
8055
|
throw new UserError(`cannot read ${file2}: ${e.message}`);
|
|
7910
8056
|
}
|
|
@@ -7931,7 +8077,7 @@ function loadEval(file2) {
|
|
|
7931
8077
|
}
|
|
7932
8078
|
var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
7933
8079
|
function gitGrep(root, flags, t) {
|
|
7934
|
-
const r =
|
|
8080
|
+
const r = spawnSync12("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
|
|
7935
8081
|
encoding: "utf8",
|
|
7936
8082
|
maxBuffer: 64 * 1024 * 1024
|
|
7937
8083
|
});
|
|
@@ -8027,13 +8173,16 @@ function formatEval(rows, group) {
|
|
|
8027
8173
|
|
|
8028
8174
|
// src/core/review.ts
|
|
8029
8175
|
import crypto13 from "node:crypto";
|
|
8030
|
-
import
|
|
8031
|
-
import
|
|
8176
|
+
import fs41 from "node:fs";
|
|
8177
|
+
import path36 from "node:path";
|
|
8032
8178
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
8033
8179
|
var section = (title, body) => `## ${title}
|
|
8034
8180
|
|
|
8035
8181
|
${body.trim() || "(empty)"}
|
|
8036
8182
|
`;
|
|
8183
|
+
var numbered = (diff) => diff.trim() ? `${NUMBERED}
|
|
8184
|
+
|
|
8185
|
+
${/^[+ -]\s*\d+\| /m.test(diff) ? diff : numberDiff(diff)}` : diff;
|
|
8037
8186
|
function ancestors(m) {
|
|
8038
8187
|
const out = [];
|
|
8039
8188
|
for (let id2 = m.parent; id2; ) {
|
|
@@ -8046,7 +8195,7 @@ function ancestors(m) {
|
|
|
8046
8195
|
function briefOf(m) {
|
|
8047
8196
|
const root = [m, ...ancestors(m)].at(-1);
|
|
8048
8197
|
const f = runFile(root.id, "brief.md");
|
|
8049
|
-
return
|
|
8198
|
+
return fs41.existsSync(f) ? fs41.readFileSync(f, "utf8") : root.task;
|
|
8050
8199
|
}
|
|
8051
8200
|
function latestReview(ids) {
|
|
8052
8201
|
for (const id2 of listRunIds().reverse()) {
|
|
@@ -8060,7 +8209,7 @@ function latestReview(ids) {
|
|
|
8060
8209
|
}
|
|
8061
8210
|
function diffOf(m, from = m.baseTree) {
|
|
8062
8211
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
8063
|
-
if (where &&
|
|
8212
|
+
if (where && fs41.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
8064
8213
|
if (from !== m.baseTree) {
|
|
8065
8214
|
throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
|
|
8066
8215
|
}
|
|
@@ -8074,7 +8223,7 @@ function runReview(id2) {
|
|
|
8074
8223
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
8075
8224
|
const chain = ancestors(m);
|
|
8076
8225
|
const previous = latestReview(chain.map((a) => a.id));
|
|
8077
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
8226
|
+
const dir = m.mode === "isolate" && m.worktree && fs41.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
8078
8227
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
8079
8228
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
8080
8229
|
if (!previous) {
|
|
@@ -8082,7 +8231,7 @@ function runReview(id2) {
|
|
|
8082
8231
|
...job,
|
|
8083
8232
|
kind: "task",
|
|
8084
8233
|
package: [`# Review package \xB7 ${title}
|
|
8085
|
-
`, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", diffOf(m))].join("\n")
|
|
8234
|
+
`, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", numbered(diffOf(m)))].join("\n")
|
|
8086
8235
|
};
|
|
8087
8236
|
}
|
|
8088
8237
|
const reviewed = chain.find((a) => a.id === previous.reviewOf);
|
|
@@ -8096,7 +8245,7 @@ function runReview(id2) {
|
|
|
8096
8245
|
section("BRIEF", briefOf(m)),
|
|
8097
8246
|
section("PREVIOUS FINDINGS", readSummary(previous)),
|
|
8098
8247
|
section("FIX REPORT", readSummary(m)),
|
|
8099
|
-
section("FIX DIFF", diffOf(m, from))
|
|
8248
|
+
section("FIX DIFF", numbered(diffOf(m, from)))
|
|
8100
8249
|
].join("\n")
|
|
8101
8250
|
};
|
|
8102
8251
|
}
|
|
@@ -8150,19 +8299,19 @@ function writePackage(job) {
|
|
|
8150
8299
|
const root = repoRoot(job.dir);
|
|
8151
8300
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
8152
8301
|
const name = `review-${crypto13.randomBytes(4).toString("hex")}.md`;
|
|
8153
|
-
const inside3 = !
|
|
8154
|
-
const file2 = inside3 ?
|
|
8302
|
+
const inside3 = !path36.relative(fs41.realpathSync(root), fs41.realpathSync(g)).startsWith("..");
|
|
8303
|
+
const file2 = inside3 ? path36.join(g, "pitroom", name) : path36.join(root, ".pitroom", name);
|
|
8155
8304
|
if (!inside3) ignoreInGit(root, "/.pitroom/");
|
|
8156
|
-
|
|
8157
|
-
|
|
8305
|
+
fs41.mkdirSync(path36.dirname(file2), { recursive: true });
|
|
8306
|
+
fs41.writeFileSync(file2, job.package);
|
|
8158
8307
|
return file2;
|
|
8159
8308
|
}
|
|
8160
8309
|
function ignoreInGit(root, pattern) {
|
|
8161
|
-
const exclude =
|
|
8162
|
-
const text =
|
|
8310
|
+
const exclude = path36.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
8311
|
+
const text = fs41.existsSync(exclude) ? fs41.readFileSync(exclude, "utf8") : "";
|
|
8163
8312
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
8164
|
-
|
|
8165
|
-
|
|
8313
|
+
fs41.mkdirSync(path36.dirname(exclude), { recursive: true });
|
|
8314
|
+
fs41.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
8166
8315
|
${pattern}
|
|
8167
8316
|
`);
|
|
8168
8317
|
}
|
|
@@ -8318,10 +8467,10 @@ async function cmdReview(p) {
|
|
|
8318
8467
|
writeMeta(meta);
|
|
8319
8468
|
}
|
|
8320
8469
|
} catch (e) {
|
|
8321
|
-
|
|
8470
|
+
fs42.rmSync(packageFile, { force: true });
|
|
8322
8471
|
throw e;
|
|
8323
8472
|
}
|
|
8324
|
-
|
|
8473
|
+
fs42.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
8325
8474
|
return launch(p, meta);
|
|
8326
8475
|
}
|
|
8327
8476
|
async function cmdAudit(p) {
|
|
@@ -8436,7 +8585,7 @@ function cmdPlan(p) {
|
|
|
8436
8585
|
}
|
|
8437
8586
|
function cmdCrew(p) {
|
|
8438
8587
|
const file2 = flag(p, "task-file");
|
|
8439
|
-
const tasks = (file2 ?
|
|
8588
|
+
const tasks = (file2 ? fs42.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
8440
8589
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
8441
8590
|
if (has(p, "write") && tasks.length > 1) {
|
|
8442
8591
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -8463,7 +8612,7 @@ async function cmdEval(p) {
|
|
|
8463
8612
|
if (has(p, "write") || has(p, "isolate") || has(p, "continue") || has(p, "plan")) throw new UserError("eval asks read questions: drop -w/-i/--continue/--plan");
|
|
8464
8613
|
const questions = loadEval(file2);
|
|
8465
8614
|
const base2 = runOptions(p, "eval");
|
|
8466
|
-
const root = repoRoot(
|
|
8615
|
+
const root = repoRoot(path37.resolve(base2.dir));
|
|
8467
8616
|
const truths = new Map(questions.map((q) => [q.id, truthOf(q, root)]));
|
|
8468
8617
|
const workers = [...new Set(p.flags.get("worker") ?? [effective().worker.value])];
|
|
8469
8618
|
const group = flag(p, "group") ?? `eval-${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
@@ -8666,13 +8815,13 @@ function sinceMs(s) {
|
|
|
8666
8815
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
8667
8816
|
return Date.now() - Number(m[1]) * 864e5;
|
|
8668
8817
|
}
|
|
8669
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
8818
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs42.readFileSync(0, "utf8");
|
|
8670
8819
|
function cmdStatusline(p) {
|
|
8671
8820
|
const input = readStdin();
|
|
8672
8821
|
const lines = [];
|
|
8673
8822
|
const then = flag(p, "then");
|
|
8674
8823
|
if (then) {
|
|
8675
|
-
const r = process.platform === "win32" ?
|
|
8824
|
+
const r = process.platform === "win32" ? spawnSync13(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync13("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
8676
8825
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
8677
8826
|
}
|
|
8678
8827
|
try {
|
|
@@ -8706,8 +8855,8 @@ function cmdModels(p) {
|
|
|
8706
8855
|
return 0;
|
|
8707
8856
|
}
|
|
8708
8857
|
function cmdHookStart() {
|
|
8709
|
-
const script =
|
|
8710
|
-
if (
|
|
8858
|
+
const script = path37.join(path37.dirname(path37.dirname(fileURLToPath5(import.meta.url))), "hooks", "session-start.mjs");
|
|
8859
|
+
if (fs42.existsSync(script)) spawnSync13(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
8711
8860
|
return 0;
|
|
8712
8861
|
}
|
|
8713
8862
|
function cmdInit(p) {
|
|
@@ -8749,8 +8898,8 @@ function cmdSavings(p) {
|
|
|
8749
8898
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
8750
8899
|
const out = flag(p, "card");
|
|
8751
8900
|
if (out) {
|
|
8752
|
-
|
|
8753
|
-
console.log(`card written to ${
|
|
8901
|
+
fs42.writeFileSync(out, card(t, period));
|
|
8902
|
+
console.log(`card written to ${path37.resolve(out)}`);
|
|
8754
8903
|
}
|
|
8755
8904
|
if (has(p, "badge")) console.log(`})`);
|
|
8756
8905
|
return 0;
|
|
@@ -8792,7 +8941,7 @@ function cmdConfig(p) {
|
|
|
8792
8941
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings: warnings2 }, null, 2));
|
|
8793
8942
|
return 0;
|
|
8794
8943
|
}
|
|
8795
|
-
console.log(`config file: ${configPath()}${
|
|
8944
|
+
console.log(`config file: ${configPath()}${fs42.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
8796
8945
|
for (const [key, s] of Object.entries(eff)) {
|
|
8797
8946
|
const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
|
|
8798
8947
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -8816,7 +8965,7 @@ function cmdClean(p) {
|
|
|
8816
8965
|
const m = readMeta(id2);
|
|
8817
8966
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
8818
8967
|
recordRun(m);
|
|
8819
|
-
|
|
8968
|
+
fs42.rmSync(runDir(id2), { recursive: true, force: true });
|
|
8820
8969
|
}
|
|
8821
8970
|
const snapshots = prune(Date.now(), 0);
|
|
8822
8971
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -8826,10 +8975,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
|
|
|
8826
8975
|
var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
8827
8976
|
|
|
8828
8977
|
// src/core/doctor.ts
|
|
8829
|
-
import { spawnSync as
|
|
8830
|
-
import
|
|
8978
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
8979
|
+
import fs43 from "node:fs";
|
|
8831
8980
|
import os13 from "node:os";
|
|
8832
|
-
import
|
|
8981
|
+
import path38 from "node:path";
|
|
8833
8982
|
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
8834
8983
|
var NEXT = [
|
|
8835
8984
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -8852,14 +9001,14 @@ function usageLine() {
|
|
|
8852
9001
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
8853
9002
|
}
|
|
8854
9003
|
function firstNodeOnPath() {
|
|
8855
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
8856
|
-
const file2 =
|
|
9004
|
+
for (const dir of (process.env.PATH ?? "").split(path38.delimiter).filter(Boolean)) {
|
|
9005
|
+
const file2 = path38.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
8857
9006
|
try {
|
|
8858
|
-
if (!
|
|
9007
|
+
if (!fs43.statSync(file2).isFile()) continue;
|
|
8859
9008
|
} catch {
|
|
8860
9009
|
continue;
|
|
8861
9010
|
}
|
|
8862
|
-
const r =
|
|
9011
|
+
const r = spawnSync14(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
8863
9012
|
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
8864
9013
|
}
|
|
8865
9014
|
return void 0;
|
|
@@ -8883,14 +9032,16 @@ function doctor8(probe2) {
|
|
|
8883
9032
|
const addAll = (list2) => list2.forEach((c) => add2(c.level, c.message));
|
|
8884
9033
|
add2("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
8885
9034
|
const onPath = firstNodeOnPath();
|
|
8886
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
9035
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path38.resolve(onPath.path) !== path38.resolve(process.execPath)) {
|
|
8887
9036
|
add2("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
|
|
8888
9037
|
}
|
|
8889
9038
|
add2(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
8890
9039
|
const cfg = loadConfig();
|
|
8891
|
-
add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
9040
|
+
add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs43.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
8892
9041
|
for (const w of cfg.warnings) add2("warn", w);
|
|
8893
|
-
if (shimReady()
|
|
9042
|
+
if (shimReady() && process.platform === "win32" && !fs43.existsSync(path38.join(shimDir(), "git.exe"))) {
|
|
9043
|
+
add2("warn", "git guard shim ready as git.cmd only (no C# compiler of .NET Framework 4 to build git.exe): a program that starts git without a shell skips it, and PowerShell's unquoted & | < > reach git changed");
|
|
9044
|
+
} else if (shimReady()) add2("ok", "git guard shim ready");
|
|
8894
9045
|
else if (process.platform === "win32") add2("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
|
|
8895
9046
|
else add2("warn", "git guard unavailable (git not on PATH)");
|
|
8896
9047
|
section2("Worker chain");
|
|
@@ -8902,7 +9053,8 @@ function doctor8(probe2) {
|
|
|
8902
9053
|
} catch (e) {
|
|
8903
9054
|
add2("fail", e.message);
|
|
8904
9055
|
}
|
|
8905
|
-
|
|
9056
|
+
const auto = chain.length > 1 && !effective().fallback.value.length;
|
|
9057
|
+
if (chain.length) add2("ok", `worker chain: ${chain.map(describeTarget).join(" \u2192 ")}${auto ? ' (fallbacks from "autoFallback": the other workers your config names)' : ""}`);
|
|
8906
9058
|
const rate2 = effective().audit.value;
|
|
8907
9059
|
if (rate2 > 0 && chain.length) {
|
|
8908
9060
|
const auditor = pickAuditor({ worker: chain[0], fallback: chain.slice(1) });
|
|
@@ -9021,12 +9173,12 @@ function liveProbe(target) {
|
|
|
9021
9173
|
} catch (e) {
|
|
9022
9174
|
return { level: "fail", message: e.message };
|
|
9023
9175
|
}
|
|
9024
|
-
const scratch = readsOwnSnapshot(backend) ?
|
|
9176
|
+
const scratch = readsOwnSnapshot(backend) ? fs43.mkdtempSync(path38.join(os13.tmpdir(), "pitroom-probe-")) : void 0;
|
|
9025
9177
|
const cwd = scratch ?? process.cwd();
|
|
9026
9178
|
try {
|
|
9027
9179
|
return probe(backend, target, cwd);
|
|
9028
9180
|
} finally {
|
|
9029
|
-
if (scratch)
|
|
9181
|
+
if (scratch) fs43.rmSync(scratch, { recursive: true, force: true });
|
|
9030
9182
|
}
|
|
9031
9183
|
}
|
|
9032
9184
|
function probe(backend, target, cwd) {
|
|
@@ -9040,7 +9192,7 @@ function probe(backend, target, cwd) {
|
|
|
9040
9192
|
title: "pitroom doctor probe"
|
|
9041
9193
|
});
|
|
9042
9194
|
const t0 = Date.now();
|
|
9043
|
-
const r =
|
|
9195
|
+
const r = spawnSync14(inv.command, inv.args, {
|
|
9044
9196
|
encoding: "utf8",
|
|
9045
9197
|
timeout: 3e5,
|
|
9046
9198
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -9071,20 +9223,20 @@ function skillChecks() {
|
|
|
9071
9223
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
9072
9224
|
}
|
|
9073
9225
|
if (geminiExtensionInstalled()) {
|
|
9074
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
9226
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.agents${path38.sep}`) && i.names.length);
|
|
9075
9227
|
checks.push(
|
|
9076
9228
|
linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
|
|
9077
9229
|
);
|
|
9078
9230
|
}
|
|
9079
9231
|
if (viaPlugin) {
|
|
9080
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
9232
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.claude${path38.sep}`) && i.names.length);
|
|
9081
9233
|
checks.push(
|
|
9082
9234
|
linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
|
|
9083
9235
|
);
|
|
9084
9236
|
}
|
|
9085
9237
|
const launcher = launcherPath();
|
|
9086
|
-
if (
|
|
9087
|
-
const r =
|
|
9238
|
+
if (fs43.existsSync(launcher)) {
|
|
9239
|
+
const r = spawnSync14(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
9088
9240
|
checks.push(
|
|
9089
9241
|
r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
|
|
9090
9242
|
);
|
|
@@ -9095,12 +9247,12 @@ function skillChecks() {
|
|
|
9095
9247
|
}
|
|
9096
9248
|
function geminiExtensionInstalled() {
|
|
9097
9249
|
const home3 = process.env.GEMINI_CLI_HOME ?? os13.homedir();
|
|
9098
|
-
return
|
|
9250
|
+
return fs43.existsSync(path38.join(home3, ".gemini", "extensions", "pitroom"));
|
|
9099
9251
|
}
|
|
9100
9252
|
function pluginInstalled() {
|
|
9101
9253
|
try {
|
|
9102
|
-
const f =
|
|
9103
|
-
const plugins = JSON.parse(
|
|
9254
|
+
const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
9255
|
+
const plugins = JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {};
|
|
9104
9256
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
9105
9257
|
} catch {
|
|
9106
9258
|
return false;
|
|
@@ -9117,24 +9269,24 @@ function superpowersActive() {
|
|
|
9117
9269
|
for (const p of openCodePlugins()) {
|
|
9118
9270
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
9119
9271
|
}
|
|
9120
|
-
for (const base2 of [
|
|
9121
|
-
const dir =
|
|
9122
|
-
if (
|
|
9272
|
+
for (const base2 of [path38.join(os13.homedir(), ".agents", "skills"), path38.join(os13.homedir(), ".claude", "skills")]) {
|
|
9273
|
+
const dir = path38.join(base2, "using-superpowers");
|
|
9274
|
+
if (fs43.existsSync(path38.join(dir, "SKILL.md"))) found.push(dir);
|
|
9123
9275
|
}
|
|
9124
9276
|
return found;
|
|
9125
9277
|
}
|
|
9126
9278
|
function claudePlugins() {
|
|
9127
9279
|
try {
|
|
9128
|
-
const f =
|
|
9129
|
-
return Object.keys(JSON.parse(
|
|
9280
|
+
const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
9281
|
+
return Object.keys(JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {});
|
|
9130
9282
|
} catch {
|
|
9131
9283
|
return [];
|
|
9132
9284
|
}
|
|
9133
9285
|
}
|
|
9134
9286
|
function claudePluginEnabled(key) {
|
|
9135
9287
|
try {
|
|
9136
|
-
const f =
|
|
9137
|
-
return JSON.parse(
|
|
9288
|
+
const f = path38.join(os13.homedir(), ".claude", "settings.json");
|
|
9289
|
+
return JSON.parse(fs43.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
9138
9290
|
} catch {
|
|
9139
9291
|
return true;
|
|
9140
9292
|
}
|
|
@@ -9143,7 +9295,7 @@ function codexPlugins() {
|
|
|
9143
9295
|
const plugins = /* @__PURE__ */ new Map();
|
|
9144
9296
|
let text;
|
|
9145
9297
|
try {
|
|
9146
|
-
text =
|
|
9298
|
+
text = fs43.readFileSync(path38.join(process.env.CODEX_HOME ?? path38.join(os13.homedir(), ".codex"), "config.toml"), "utf8");
|
|
9147
9299
|
} catch {
|
|
9148
9300
|
return plugins;
|
|
9149
9301
|
}
|
|
@@ -9161,11 +9313,11 @@ function codexPlugins() {
|
|
|
9161
9313
|
return plugins;
|
|
9162
9314
|
}
|
|
9163
9315
|
function openCodePlugins() {
|
|
9164
|
-
const dir =
|
|
9316
|
+
const dir = path38.join(process.env.XDG_CONFIG_HOME ?? path38.join(os13.homedir(), ".config"), "opencode");
|
|
9165
9317
|
const plugins = [];
|
|
9166
9318
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
9167
9319
|
try {
|
|
9168
|
-
const text =
|
|
9320
|
+
const text = fs43.readFileSync(path38.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
9169
9321
|
const list2 = JSON.parse(text).plugin;
|
|
9170
9322
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
9171
9323
|
} catch {
|
|
@@ -9173,7 +9325,7 @@ function openCodePlugins() {
|
|
|
9173
9325
|
}
|
|
9174
9326
|
for (const folder of ["plugin", "plugins"]) {
|
|
9175
9327
|
try {
|
|
9176
|
-
for (const f of
|
|
9328
|
+
for (const f of fs43.readdirSync(path38.join(dir, folder))) plugins.push(path38.join(dir, folder, f));
|
|
9177
9329
|
} catch {
|
|
9178
9330
|
}
|
|
9179
9331
|
}
|
|
@@ -9300,7 +9452,7 @@ var COMMANDS = {
|
|
|
9300
9452
|
if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
|
|
9301
9453
|
const dir = flag(p, "dir");
|
|
9302
9454
|
if (dir !== void 0) {
|
|
9303
|
-
if (!
|
|
9455
|
+
if (!fs44.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
9304
9456
|
process.chdir(dir);
|
|
9305
9457
|
process.env.PWD = process.cwd();
|
|
9306
9458
|
}
|