pitroom 0.16.0 → 0.17.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 +23 -0
- package/README.md +7 -5
- package/dist/pitroom.mjs +571 -335
- package/dist/ui/app.js +30 -30
- package/gemini-extension.json +1 -1
- package/package.json +2 -1
- package/skills/pitroom-driven-development/SKILL.md +1 -1
- package/skills/pitroom-review/SKILL.md +1 -1
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 fs35 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -1420,7 +1420,8 @@ var SCHEMA = {
|
|
|
1420
1420
|
tiers: "record",
|
|
1421
1421
|
costs: "numbers",
|
|
1422
1422
|
audit: "number",
|
|
1423
|
-
readIn: "string"
|
|
1423
|
+
readIn: "string",
|
|
1424
|
+
cacheDays: "number"
|
|
1424
1425
|
};
|
|
1425
1426
|
function configPath() {
|
|
1426
1427
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1478,6 +1479,10 @@ var rate = (v) => {
|
|
|
1478
1479
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1479
1480
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1480
1481
|
};
|
|
1482
|
+
var days = (v) => {
|
|
1483
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1484
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
1485
|
+
};
|
|
1481
1486
|
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1482
1487
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1483
1488
|
function setting(flag2, env, conf, fallback) {
|
|
@@ -1504,7 +1509,8 @@ function effective(flags = {}) {
|
|
|
1504
1509
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1505
1510
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1506
1511
|
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1507
|
-
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
|
|
1512
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
1513
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7)
|
|
1508
1514
|
};
|
|
1509
1515
|
}
|
|
1510
1516
|
|
|
@@ -1581,6 +1587,7 @@ var BOOL_FLAGS = {
|
|
|
1581
1587
|
"--stop": "stop",
|
|
1582
1588
|
"--open": "open",
|
|
1583
1589
|
"--serve": "serve",
|
|
1590
|
+
"--fresh": "fresh",
|
|
1584
1591
|
"--http": "http",
|
|
1585
1592
|
"-h": "help",
|
|
1586
1593
|
"--help": "help",
|
|
@@ -1730,16 +1737,16 @@ function repoRoot(dir) {
|
|
|
1730
1737
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1731
1738
|
return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
|
|
1732
1739
|
}
|
|
1733
|
-
function snapshotTree(root, exclude = [],
|
|
1740
|
+
function snapshotTree(root, exclude = [], drop2 = []) {
|
|
1734
1741
|
const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
|
|
1735
1742
|
const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
1736
1743
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
1737
1744
|
try {
|
|
1738
1745
|
if (fs9.existsSync(real)) fs9.copyFileSync(real, tmp);
|
|
1739
1746
|
else must(root, ["read-tree", "--empty"], env);
|
|
1740
|
-
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...
|
|
1741
|
-
if (
|
|
1742
|
-
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${
|
|
1747
|
+
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
1748
|
+
if (drop2.length) {
|
|
1749
|
+
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
|
|
1743
1750
|
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
1744
1751
|
}
|
|
1745
1752
|
return must(root, ["write-tree"], env).trim();
|
|
@@ -1804,6 +1811,7 @@ ${must(root, [...DIFF, "-U10", a, b])}`;
|
|
|
1804
1811
|
}
|
|
1805
1812
|
function rangeDiff(root, a, b) {
|
|
1806
1813
|
const range = `${a}..${b}`;
|
|
1814
|
+
const since2 = git(root, ["merge-base", a, b]).code === 0 ? `${a}...${b}` : range;
|
|
1807
1815
|
const log2 = must(root, ["log", "--oneline", "--no-decorate", range]).trim();
|
|
1808
1816
|
return [
|
|
1809
1817
|
`## COMMITS
|
|
@@ -1811,10 +1819,10 @@ function rangeDiff(root, a, b) {
|
|
|
1811
1819
|
${log2 || "(none)"}`,
|
|
1812
1820
|
`## FILES CHANGED
|
|
1813
1821
|
|
|
1814
|
-
${must(root, [...DIFF, "--stat",
|
|
1822
|
+
${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
|
|
1815
1823
|
`## DIFF
|
|
1816
1824
|
|
|
1817
|
-
${must(root, [...DIFF, "-U10",
|
|
1825
|
+
${must(root, [...DIFF, "-U10", since2])}`
|
|
1818
1826
|
].join("\n\n");
|
|
1819
1827
|
}
|
|
1820
1828
|
function commitOf(root, ref) {
|
|
@@ -2049,7 +2057,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2049
2057
|
process.once("SIGINT", stop);
|
|
2050
2058
|
process.once("SIGTERM", stop);
|
|
2051
2059
|
let killer;
|
|
2052
|
-
const
|
|
2060
|
+
const timer2 = setTimeout(() => {
|
|
2053
2061
|
res.timedOut = true;
|
|
2054
2062
|
child.kill("SIGTERM");
|
|
2055
2063
|
killer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
@@ -2061,7 +2069,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2061
2069
|
});
|
|
2062
2070
|
child.on("close", (c) => resolve2(c));
|
|
2063
2071
|
});
|
|
2064
|
-
clearTimeout(
|
|
2072
|
+
clearTimeout(timer2);
|
|
2065
2073
|
if (killer) clearTimeout(killer);
|
|
2066
2074
|
process.off("SIGINT", stop);
|
|
2067
2075
|
process.off("SIGTERM", stop);
|
|
@@ -2234,8 +2242,8 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
2234
2242
|
return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
|
|
2235
2243
|
}
|
|
2236
2244
|
function readSnapshot(root) {
|
|
2237
|
-
const
|
|
2238
|
-
const tree = snapshotTree(root, [],
|
|
2245
|
+
const drop2 = findSecretFilesInTree(root, root);
|
|
2246
|
+
const tree = snapshotTree(root, [], drop2);
|
|
2239
2247
|
const dest = path12.join(snapshotsDir(), tree);
|
|
2240
2248
|
if (fs14.existsSync(path12.join(dest, ".git"))) {
|
|
2241
2249
|
touch(dest);
|
|
@@ -2377,6 +2385,9 @@ function auditTask(meta, answer) {
|
|
|
2377
2385
|
return [
|
|
2378
2386
|
"You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
|
|
2379
2387
|
"check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
|
|
2388
|
+
"Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
|
|
2389
|
+
'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
|
|
2390
|
+
"for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
|
|
2380
2391
|
"Do not edit anything and do not run anything that changes files.",
|
|
2381
2392
|
"",
|
|
2382
2393
|
"QUESTION:",
|
|
@@ -2391,7 +2402,7 @@ function auditTask(meta, answer) {
|
|
|
2391
2402
|
"DISPUTED:",
|
|
2392
2403
|
"- <the claim> \u2014 <what is actually true, with path:line>",
|
|
2393
2404
|
"",
|
|
2394
|
-
"AGREE: every key claim you checked holds. PARTIAL: some are wrong or
|
|
2405
|
+
"AGREE: every key claim you checked holds and the answer meets every condition of the question (no extra or missing items, the right count). PARTIAL: some are wrong, unverified, or the answer goes beyond or falls short of the question's scope. DISAGREE: the main conclusion is wrong.",
|
|
2395
2406
|
'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
|
|
2396
2407
|
].join("\n");
|
|
2397
2408
|
}
|
|
@@ -2577,18 +2588,18 @@ function place(src, dest, opts) {
|
|
|
2577
2588
|
}
|
|
2578
2589
|
function install(opts) {
|
|
2579
2590
|
const root = packageRoot();
|
|
2580
|
-
const
|
|
2581
|
-
if (opts.skills !== false && !
|
|
2591
|
+
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
2592
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
|
|
2582
2593
|
const out = [];
|
|
2583
2594
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
2584
2595
|
for (const legacy of LEGACY) {
|
|
2585
2596
|
const l = path14.join(base2, legacy);
|
|
2586
|
-
if (!
|
|
2597
|
+
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
2587
2598
|
fs16.unlinkSync(l);
|
|
2588
2599
|
out.push(`\u2714 removed old link ${l}`);
|
|
2589
2600
|
}
|
|
2590
2601
|
}
|
|
2591
|
-
for (const name of
|
|
2602
|
+
for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
|
|
2592
2603
|
}
|
|
2593
2604
|
const bundle = path14.join(root, "dist", "pitroom.mjs");
|
|
2594
2605
|
if (fs16.existsSync(bundle)) {
|
|
@@ -3350,6 +3361,7 @@ function archivedId(ref) {
|
|
|
3350
3361
|
return void 0;
|
|
3351
3362
|
}
|
|
3352
3363
|
}
|
|
3364
|
+
var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
|
|
3353
3365
|
var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
|
|
3354
3366
|
function listHistory(q = {}) {
|
|
3355
3367
|
const db = openDb();
|
|
@@ -3369,7 +3381,7 @@ function listHistory(q = {}) {
|
|
|
3369
3381
|
args.push(q.backend);
|
|
3370
3382
|
}
|
|
3371
3383
|
if (q.state) {
|
|
3372
|
-
where.push(q.state === "problem" ?
|
|
3384
|
+
where.push(q.state === "problem" ? `(r.state IN ('failed','timeout','stopped') OR ${VERIFY_FAILED})` : "r.state = ?");
|
|
3373
3385
|
if (q.state !== "problem") args.push(q.state);
|
|
3374
3386
|
}
|
|
3375
3387
|
if (q.group) {
|
|
@@ -3384,7 +3396,7 @@ function listHistory(q = {}) {
|
|
|
3384
3396
|
try {
|
|
3385
3397
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
3386
3398
|
const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
3387
|
-
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
3399
|
+
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
3388
3400
|
return {
|
|
3389
3401
|
total,
|
|
3390
3402
|
rows: rows.map((r) => ({
|
|
@@ -3398,6 +3410,7 @@ function listHistory(q = {}) {
|
|
|
3398
3410
|
group: r.grp ?? void 0,
|
|
3399
3411
|
verdict: r.verdict ?? void 0,
|
|
3400
3412
|
audit: auditBadge({ audit: r.audit_state ? { id: "", state: r.audit_state, verdict: r.audit_verdict ?? void 0 } : void 0 }),
|
|
3413
|
+
verifyFailed: r.verify_failed ? true : void 0,
|
|
3401
3414
|
seconds: r.seconds ?? void 0,
|
|
3402
3415
|
steps: r.steps ?? void 0,
|
|
3403
3416
|
tokens: r.tokens ?? void 0,
|
|
@@ -3544,7 +3557,9 @@ function readSummary(meta) {
|
|
|
3544
3557
|
}
|
|
3545
3558
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3546
3559
|
const out = [];
|
|
3547
|
-
|
|
3560
|
+
const verifyFailed = meta.state === "done" && meta.verifyResult && !meta.verifyResult.ok;
|
|
3561
|
+
const head = verifyFailed ? stateColour("failed", "\u26A0 done \xB7 verify failed") : stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`);
|
|
3562
|
+
out.push(`${bold("pitroom")} ${head} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
|
|
3548
3563
|
const ran = meta.ran ?? meta.worker;
|
|
3549
3564
|
const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
|
|
3550
3565
|
out.push(ids.filter(Boolean).join(" \xB7 "));
|
|
@@ -3610,7 +3625,9 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3610
3625
|
}
|
|
3611
3626
|
if (meta.verifyResult) {
|
|
3612
3627
|
const v = meta.verifyResult;
|
|
3613
|
-
|
|
3628
|
+
const notFound = !v.ok && v.code === 127 && /not found|no such file/i.test(v.tail);
|
|
3629
|
+
out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
|
|
3630
|
+
if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
|
|
3614
3631
|
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
3615
3632
|
}
|
|
3616
3633
|
if (isActive(meta.state)) {
|
|
@@ -3634,7 +3651,7 @@ function progress(meta) {
|
|
|
3634
3651
|
}
|
|
3635
3652
|
|
|
3636
3653
|
// src/core/run.ts
|
|
3637
|
-
var VERSION2 = true ? "0.
|
|
3654
|
+
var VERSION2 = true ? "0.17.0" : "0.0.0-dev";
|
|
3638
3655
|
function planWork(o) {
|
|
3639
3656
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3640
3657
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3732,6 +3749,7 @@ function prepareRun(o) {
|
|
|
3732
3749
|
auditOf: o.audit?.of,
|
|
3733
3750
|
auditRate: o.auditRate,
|
|
3734
3751
|
inPlace: o.inPlace || void 0,
|
|
3752
|
+
cache: o.cache,
|
|
3735
3753
|
snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
|
|
3736
3754
|
plan: work.plan ?? o.review?.plan ?? parent?.plan,
|
|
3737
3755
|
reviewKind: o.review?.kind,
|
|
@@ -4003,6 +4021,12 @@ function captureChanges(meta) {
|
|
|
4003
4021
|
meta.stats = d.stats;
|
|
4004
4022
|
fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4005
4023
|
}
|
|
4024
|
+
function verifyPath() {
|
|
4025
|
+
const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4026
|
+
const parts = (process.env.PATH ?? "").split(path20.delimiter).filter(Boolean);
|
|
4027
|
+
for (const d of process.platform === "win32" ? [path20.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
4028
|
+
return parts.join(path20.delimiter);
|
|
4029
|
+
}
|
|
4006
4030
|
function runVerify(meta) {
|
|
4007
4031
|
const r = spawnSync6(meta.verify, {
|
|
4008
4032
|
cwd: meta.cwd,
|
|
@@ -4011,7 +4035,7 @@ function runVerify(meta) {
|
|
|
4011
4035
|
timeout: 15 * 6e4,
|
|
4012
4036
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4013
4037
|
maxBuffer: 64 * 1024 * 1024,
|
|
4014
|
-
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4038
|
+
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4015
4039
|
});
|
|
4016
4040
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4017
4041
|
fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
@@ -4062,6 +4086,10 @@ function cleanupWorktree(meta) {
|
|
|
4062
4086
|
if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4063
4087
|
}
|
|
4064
4088
|
|
|
4089
|
+
// src/cli/mcp-extras.ts
|
|
4090
|
+
import fs25 from "node:fs";
|
|
4091
|
+
import path21 from "node:path";
|
|
4092
|
+
|
|
4065
4093
|
// src/cli/mcp-support.ts
|
|
4066
4094
|
import { spawn as spawn3 } from "node:child_process";
|
|
4067
4095
|
|
|
@@ -4248,12 +4276,12 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4248
4276
|
child.stdout.on("data", (d) => out += d);
|
|
4249
4277
|
child.stderr.on("data", (d) => err += d);
|
|
4250
4278
|
const stop = () => child.kill("SIGTERM");
|
|
4251
|
-
const
|
|
4279
|
+
const timer2 = setTimeout(stop, timeoutMs);
|
|
4252
4280
|
signal?.addEventListener("abort", stop, { once: true });
|
|
4253
4281
|
if (signal?.aborted) stop();
|
|
4254
4282
|
child.on("error", (e) => resolve2({ code: 1, out, err: err || String(e.message) }));
|
|
4255
4283
|
child.on("close", (code) => {
|
|
4256
|
-
clearTimeout(
|
|
4284
|
+
clearTimeout(timer2);
|
|
4257
4285
|
signal?.removeEventListener("abort", stop);
|
|
4258
4286
|
resolve2({ code, out: strip(out).trim(), err: strip(err).trim() });
|
|
4259
4287
|
});
|
|
@@ -4264,11 +4292,11 @@ function asResult(r, still) {
|
|
|
4264
4292
|
if (r.code === 75 && still) return { text: clip6(`${text}
|
|
4265
4293
|
|
|
4266
4294
|
${still}`) };
|
|
4267
|
-
return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 1 && !r.out };
|
|
4295
|
+
return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 5 || r.code === 6 || r.code === 1 && !r.out };
|
|
4268
4296
|
}
|
|
4269
4297
|
var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
|
|
4270
4298
|
function trackProgress(ctx, ids) {
|
|
4271
|
-
const
|
|
4299
|
+
const tick2 = () => {
|
|
4272
4300
|
try {
|
|
4273
4301
|
const lines = ids().map((id) => freshMeta(id)).filter((m) => isActive(m.state)).map((m) => progress(m).replace(/^pitroom\s+/, ""));
|
|
4274
4302
|
if (lines.length) ctx.progress(lines.slice(0, 3).join("\n") + (lines.length > 3 ? `
|
|
@@ -4276,11 +4304,11 @@ function trackProgress(ctx, ids) {
|
|
|
4276
4304
|
} catch {
|
|
4277
4305
|
}
|
|
4278
4306
|
};
|
|
4279
|
-
const first = setTimeout(
|
|
4280
|
-
const
|
|
4307
|
+
const first = setTimeout(tick2, 800);
|
|
4308
|
+
const timer2 = setInterval(tick2, PROGRESS_EVERY_MS);
|
|
4281
4309
|
return () => {
|
|
4282
4310
|
clearTimeout(first);
|
|
4283
|
-
clearInterval(
|
|
4311
|
+
clearInterval(timer2);
|
|
4284
4312
|
};
|
|
4285
4313
|
}
|
|
4286
4314
|
async function waitFor(ids, seconds, ctx, group) {
|
|
@@ -4305,13 +4333,17 @@ async function startAndWait(start, seconds, ctx, task) {
|
|
|
4305
4333
|
const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
|
|
4306
4334
|
if (started.code !== 0) return asResult(started);
|
|
4307
4335
|
let id;
|
|
4336
|
+
let cached3 = false;
|
|
4308
4337
|
try {
|
|
4309
|
-
id = JSON.parse(started.out)
|
|
4338
|
+
({ id, cached: cached3 = false } = JSON.parse(started.out));
|
|
4310
4339
|
} catch {
|
|
4311
4340
|
return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4312
4341
|
}
|
|
4313
4342
|
const result = await waitFor([id], seconds, ctx);
|
|
4314
|
-
if (cancelled(ctx)) await stopQuietly(["stop", id]);
|
|
4343
|
+
if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
|
|
4344
|
+
if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
|
|
4345
|
+
|
|
4346
|
+
${result.text}`;
|
|
4315
4347
|
return result;
|
|
4316
4348
|
}
|
|
4317
4349
|
async function startCrew(flags, tasks, seconds, ctx) {
|
|
@@ -4337,6 +4369,7 @@ var RpcError = class extends Error {
|
|
|
4337
4369
|
};
|
|
4338
4370
|
var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
|
|
4339
4371
|
var LISTED = 30;
|
|
4372
|
+
var runOfUri = (uri) => RUN.exec(uri)?.[1];
|
|
4340
4373
|
var oneLine6 = (text, n) => {
|
|
4341
4374
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
4342
4375
|
return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
|
|
@@ -4399,16 +4432,50 @@ Call pitroom_run with mode "isolate" (add "verify" with the project's test comma
|
|
|
4399
4432
|
title: "Split work across workers",
|
|
4400
4433
|
description: "Independent tasks run in parallel as one group of workers.",
|
|
4401
4434
|
arguments: [{ name: "tasks", description: "The tasks, one per line.", required: true }],
|
|
4402
|
-
text: (a) => `Run these independent tasks in parallel with
|
|
4435
|
+
text: (a) => `Run these independent tasks in parallel: call pitroom_run with "tasks" (one worker each):
|
|
4403
4436
|
|
|
4404
4437
|
${a.tasks}
|
|
4405
4438
|
|
|
4406
4439
|
Make each task self-contained. Use mode "isolate" if they change files, then read every patch (pitroom_show, patch: true) and apply them with pitroom_apply (group). Only split work that does not depend on each other.`
|
|
4407
4440
|
}
|
|
4408
4441
|
];
|
|
4409
|
-
var
|
|
4442
|
+
var TOOL_MAP = 'Through this MCP server the `pitroom` commands below are tools: `pitroom run` is pitroom_run (`-i` is mode "isolate", `-w` mode "write", `--continue` is continue, `--verify` is verify), `pitroom crew` is pitroom_run with tasks, `pitroom wait` pitroom_wait, `pitroom show`/`status` pitroom_show, `pitroom review` pitroom_review, `pitroom audit` pitroom_audit, `pitroom apply`/`discard`/`revert`/`stop` the tools of those names, `pitroom history`/`ls`/`models`/`doctor` pitroom_info. With a shell the commands work as well.';
|
|
4443
|
+
var skills;
|
|
4444
|
+
function skillPrompts() {
|
|
4445
|
+
if (skills) return skills;
|
|
4446
|
+
const found = [];
|
|
4447
|
+
const root = packageRoot();
|
|
4448
|
+
for (const name of skillNames(root)) {
|
|
4449
|
+
try {
|
|
4450
|
+
const text = fs25.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4451
|
+
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4452
|
+
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4453
|
+
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
4454
|
+
const title = /^#\s+(.+)$/m.exec(body)?.[1]?.trim() ?? name;
|
|
4455
|
+
found.push({
|
|
4456
|
+
name,
|
|
4457
|
+
title,
|
|
4458
|
+
// the first sentence: prompts/list is read whole by some clients, so it stays short
|
|
4459
|
+
description: `Skill: ${description.split(/(?<=\.)\s/)[0]}`,
|
|
4460
|
+
arguments: [{ name: "task", description: "What you are about to do (optional)." }],
|
|
4461
|
+
text: (a) => `${TOOL_MAP}
|
|
4462
|
+
|
|
4463
|
+
${body}${a.task ? `
|
|
4464
|
+
|
|
4465
|
+
---
|
|
4466
|
+
|
|
4467
|
+
The task: ${a.task}` : ""}`
|
|
4468
|
+
});
|
|
4469
|
+
} catch {
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
skills = found.filter((p) => !PROMPTS.some((b) => b.name === p.name));
|
|
4473
|
+
return skills;
|
|
4474
|
+
}
|
|
4475
|
+
var allPrompts = () => [...PROMPTS, ...skillPrompts()];
|
|
4476
|
+
var listPrompts = () => ({ prompts: allPrompts().map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
|
|
4410
4477
|
function getPrompt(name, given) {
|
|
4411
|
-
const p =
|
|
4478
|
+
const p = allPrompts().find((x) => x.name === name);
|
|
4412
4479
|
if (!p) throw new RpcError(-32602, `unknown prompt: ${String(name)}`);
|
|
4413
4480
|
const args = {};
|
|
4414
4481
|
const raw = given && typeof given === "object" && !Array.isArray(given) ? given : {};
|
|
@@ -4422,84 +4489,110 @@ function getPrompt(name, given) {
|
|
|
4422
4489
|
|
|
4423
4490
|
// src/cli/mcp-tools.ts
|
|
4424
4491
|
var WORKER_PROPS = {
|
|
4425
|
-
worker: { type: "string", description: 'Worker
|
|
4426
|
-
model: { type: "string", description: "Model for
|
|
4427
|
-
tier: { type: "string", description: "
|
|
4428
|
-
effort: { type: "string", description: "
|
|
4429
|
-
dir: { type: "string", description: "Project directory (default: the
|
|
4430
|
-
files: { type: "array", items: { type: "string" }, description: "Files to attach
|
|
4431
|
-
verify: { type: "string", description: '
|
|
4432
|
-
link: { type: "array", items: { type: "string" }, description: 'Ignored
|
|
4433
|
-
web: { type: "boolean", description: "
|
|
4434
|
-
group: { type: "string", description: "
|
|
4492
|
+
worker: { type: "string", description: 'Worker "backend[:model]", e.g. "opencode", "claude:haiku". Default: configured.' },
|
|
4493
|
+
model: { type: "string", description: "Model for that worker." },
|
|
4494
|
+
tier: { type: "string", description: "cheap, standard or capable (from the config)." },
|
|
4495
|
+
effort: { type: "string", description: "low, medium, high, xhigh." },
|
|
4496
|
+
dir: { type: "string", description: "Project directory (default: the server's)." },
|
|
4497
|
+
files: { type: "array", items: { type: "string" }, description: "Files to attach." },
|
|
4498
|
+
verify: { type: "string", description: 'Command run afterwards, e.g. "npm test".' },
|
|
4499
|
+
link: { type: "array", items: { type: "string" }, description: 'Ignored dirs to link into an isolated copy, e.g. ["node_modules"].' },
|
|
4500
|
+
web: { type: "boolean", description: "Allow web tools." },
|
|
4501
|
+
group: { type: "string", description: "Group name for related runs." }
|
|
4435
4502
|
};
|
|
4436
|
-
var WAIT_PROP = { waitSeconds: { type: "number", description: `
|
|
4437
|
-
var RUN_ID2 = { type: "string", description: '
|
|
4438
|
-
var SINCE = { type: "string", description: "A window: 7d, 30d, \u2026 or all." };
|
|
4503
|
+
var WAIT_PROP = { waitSeconds: { type: "number", description: `Wait this long before returning "still running" (default ${DEFAULT_WAIT}, max ${MAX_WAIT}).` } };
|
|
4504
|
+
var RUN_ID2 = { type: "string", description: 'Run id, or "last".' };
|
|
4439
4505
|
var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
4506
|
+
var TOPICS = {
|
|
4507
|
+
runs: ["running", "group"],
|
|
4508
|
+
history: ["text", "state", "model", "since", "limit"],
|
|
4509
|
+
stats: ["since"],
|
|
4510
|
+
savings: ["since", "perModel"],
|
|
4511
|
+
models: ["worker", "all"],
|
|
4512
|
+
cooldown: [],
|
|
4513
|
+
config: [],
|
|
4514
|
+
doctor: []
|
|
4515
|
+
};
|
|
4516
|
+
function infoArgs(topic, a) {
|
|
4517
|
+
const allowed = TOPICS[topic];
|
|
4518
|
+
const stray = Object.keys(a).filter((k) => k !== "topic" && a[k] !== void 0 && a[k] !== null && !allowed.includes(k));
|
|
4519
|
+
if (stray.length) throw new ToolError(`${stray.map((k) => `"${k}"`).join(", ")} ${stray.length > 1 ? "do" : "does"} not go with topic "${topic}" (${allowed.length ? `it takes ${allowed.join(", ")}` : "it takes no options"})`);
|
|
4520
|
+
const group = str(a, "group");
|
|
4521
|
+
const worker = str(a, "worker");
|
|
4522
|
+
switch (topic) {
|
|
4523
|
+
case "runs":
|
|
4524
|
+
return { args: group ? ["status", "-g", group] : ["ls", ...bool(a, "running") ? ["--running"] : []] };
|
|
4525
|
+
case "history": {
|
|
4526
|
+
const limit = a.limit;
|
|
4527
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ToolError('"limit" must be a whole number from 1 to 200');
|
|
4528
|
+
const text = str(a, "text");
|
|
4529
|
+
const state = str(a, "state");
|
|
4530
|
+
const model = str(a, "model");
|
|
4531
|
+
return { args: ["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []] };
|
|
4532
|
+
}
|
|
4533
|
+
case "stats":
|
|
4534
|
+
return { args: ["history", "stats", ...since(a)] };
|
|
4535
|
+
case "savings":
|
|
4536
|
+
return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
|
|
4537
|
+
case "models":
|
|
4538
|
+
return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
|
|
4539
|
+
case "doctor":
|
|
4540
|
+
return { args: ["doctor"], timeoutMs: 12e4 };
|
|
4541
|
+
case "cooldown":
|
|
4542
|
+
case "config":
|
|
4543
|
+
return { args: [topic] };
|
|
4544
|
+
default:
|
|
4545
|
+
throw new Error(`topic "${topic}" has no command`);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4440
4548
|
var TOOLS2 = [
|
|
4441
4549
|
{
|
|
4442
4550
|
name: "pitroom_run",
|
|
4443
4551
|
title: "Run a worker",
|
|
4444
|
-
description:
|
|
4552
|
+
description: 'Hand a bounded task to a cheaper worker agent. mode "read" (default): read-only research, an answer with verified file:line references. "isolate": the worker edits a private copy and you get the exact diff (then pitroom_review, pitroom_apply or pitroom_discard). "write": edits the working tree (undo: pitroom_revert). Give "tasks" instead of "task" to run independent tasks in parallel (read or isolate). Returns the report(s) and a receipt (a read question asked before on the same code comes back cached), or "still running" for pitroom_wait. The using-pitroom skill (or prompt) says when and how to delegate. Cancelling the call stops the run.',
|
|
4445
4553
|
inputSchema: {
|
|
4446
4554
|
type: "object",
|
|
4447
4555
|
properties: {
|
|
4448
|
-
task: { type: "string", description: "What
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4556
|
+
task: { type: "string", description: "What to do, with the context the worker needs." },
|
|
4557
|
+
tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "Or several independent tasks, one worker each." },
|
|
4558
|
+
mode: { type: "string", enum: ["read", "isolate", "write"] },
|
|
4559
|
+
continue: { type: "string", description: "A finished run to follow up in the same worker session." },
|
|
4560
|
+
inPlace: { type: "boolean", description: "Read: read the directory itself, not a snapshot without secret-looking files." },
|
|
4561
|
+
audit: { type: "boolean", description: "Read: have another worker re-check the answer (each one, with tasks)." },
|
|
4562
|
+
fresh: { type: "boolean", description: "Read: ask a worker even if this question was answered on the same code." },
|
|
4453
4563
|
...WORKER_PROPS,
|
|
4454
4564
|
...WAIT_PROP
|
|
4455
|
-
}
|
|
4456
|
-
required: ["task"]
|
|
4565
|
+
}
|
|
4457
4566
|
},
|
|
4458
4567
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4459
4568
|
async call(a, ctx) {
|
|
4460
|
-
const task = str(a, "task"
|
|
4569
|
+
const task = str(a, "task");
|
|
4570
|
+
const tasks = strs(a, "tasks").filter((t) => t.trim());
|
|
4571
|
+
if (!task && !tasks.length) throw new ToolError('give "task" (or "tasks" for parallel work)');
|
|
4572
|
+
if (task && tasks.length) throw new ToolError('give "task" or "tasks", not both');
|
|
4461
4573
|
const mode = oneOf(a, "mode", ["read", "isolate", "write"], "read");
|
|
4462
4574
|
const flags = mode === "isolate" ? ["-i"] : mode === "write" ? ["-w"] : [];
|
|
4463
|
-
const follow = str(a, "continue");
|
|
4464
|
-
if (follow) flags.push("--continue", follow);
|
|
4465
4575
|
if (mode === "read") {
|
|
4466
4576
|
if (bool(a, "inPlace")) flags.push("--in-place");
|
|
4467
4577
|
if (bool(a, "audit")) flags.push("--audit");
|
|
4578
|
+
if (bool(a, "fresh")) flags.push("--fresh");
|
|
4579
|
+
}
|
|
4580
|
+
const follow = str(a, "continue");
|
|
4581
|
+
if (tasks.length) {
|
|
4582
|
+
if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
|
|
4583
|
+
if (mode === "write") throw new ToolError('parallel workers never write in place: use mode "isolate" (each gets its own copy)');
|
|
4584
|
+
if (follow) throw new ToolError('"continue" follows up one run: give "task"');
|
|
4585
|
+
return startCrew([...flags, ...workerFlags(a)], tasks, waitSeconds(a), ctx);
|
|
4468
4586
|
}
|
|
4587
|
+
if (follow) flags.push("--continue", follow);
|
|
4469
4588
|
return startAndWait(["run", ...flags, ...workerFlags(a)], waitSeconds(a), ctx, task);
|
|
4470
4589
|
}
|
|
4471
4590
|
},
|
|
4472
|
-
{
|
|
4473
|
-
name: "pitroom_crew",
|
|
4474
|
-
title: "Run workers in parallel",
|
|
4475
|
-
description: 'Start several independent tasks at once as one group of workers (read, or isolate: each edits its own copy) and wait for the group. Returns every report, or "still running" with the group name for pitroom_wait. Only for tasks that do not depend on each other. Cancelling the call stops the group.',
|
|
4476
|
-
inputSchema: {
|
|
4477
|
-
type: "object",
|
|
4478
|
-
properties: {
|
|
4479
|
-
tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "One self-contained task per worker." },
|
|
4480
|
-
mode: { type: "string", enum: ["read", "isolate"], description: "read (default) or isolate." },
|
|
4481
|
-
...WORKER_PROPS,
|
|
4482
|
-
...WAIT_PROP
|
|
4483
|
-
},
|
|
4484
|
-
required: ["tasks"]
|
|
4485
|
-
},
|
|
4486
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4487
|
-
async call(a, ctx) {
|
|
4488
|
-
const tasks = strs(a, "tasks").filter((t) => t.trim());
|
|
4489
|
-
if (!tasks.length) throw new ToolError('"tasks" needs at least one task');
|
|
4490
|
-
if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
|
|
4491
|
-
const mode = oneOf(a, "mode", ["read", "isolate"], "read");
|
|
4492
|
-
return startCrew([...mode === "isolate" ? ["-i"] : [], ...workerFlags(a)], tasks, waitSeconds(a), ctx);
|
|
4493
|
-
}
|
|
4494
|
-
},
|
|
4495
4591
|
{
|
|
4496
4592
|
name: "pitroom_wait",
|
|
4497
4593
|
title: "Wait for runs",
|
|
4498
|
-
description: 'Wait for runs
|
|
4499
|
-
inputSchema: {
|
|
4500
|
-
type: "object",
|
|
4501
|
-
properties: { runs: { type: "array", items: { type: "string" }, description: "Run ids." }, group: { type: "string", description: "Or a group name." }, ...WAIT_PROP }
|
|
4502
|
-
},
|
|
4594
|
+
description: 'Wait for runs that came back "still running" and return their reports.',
|
|
4595
|
+
inputSchema: { type: "object", properties: { runs: { type: "array", items: { type: "string" } }, group: { type: "string" }, ...WAIT_PROP } },
|
|
4503
4596
|
annotations: READ_ONLY,
|
|
4504
4597
|
async call(a, ctx) {
|
|
4505
4598
|
const runs = strs(a, "runs");
|
|
@@ -4508,126 +4601,56 @@ var TOOLS2 = [
|
|
|
4508
4601
|
return waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
|
|
4509
4602
|
}
|
|
4510
4603
|
},
|
|
4511
|
-
{
|
|
4512
|
-
name: "pitroom_status",
|
|
4513
|
-
title: "Run status",
|
|
4514
|
-
description: "The state and live progress of a run (default: the latest), or of a group.",
|
|
4515
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string", description: "A group name." } } },
|
|
4516
|
-
annotations: READ_ONLY,
|
|
4517
|
-
async call(a, ctx) {
|
|
4518
|
-
const group = str(a, "group");
|
|
4519
|
-
const run2 = str(a, "run");
|
|
4520
|
-
if (group && run2) throw new ToolError('give "run" or "group", not both');
|
|
4521
|
-
return plain(["status", ...group ? ["-g", group] : run2 ? [run2] : []], ctx);
|
|
4522
|
-
}
|
|
4523
|
-
},
|
|
4524
|
-
{
|
|
4525
|
-
name: "pitroom_list",
|
|
4526
|
-
title: "List runs",
|
|
4527
|
-
description: "The latest runs with their state, worker and task; with running only the active ones, or one group's.",
|
|
4528
|
-
inputSchema: { type: "object", properties: { running: { type: "boolean", description: "Only runs that are queued or running." }, group: { type: "string", description: "A group name." } } },
|
|
4529
|
-
annotations: READ_ONLY,
|
|
4530
|
-
async call(a, ctx) {
|
|
4531
|
-
const group = str(a, "group");
|
|
4532
|
-
return plain(["ls", ...bool(a, "running") ? ["--running"] : [], ...group ? ["-g", group] : []], ctx);
|
|
4533
|
-
}
|
|
4534
|
-
},
|
|
4535
4604
|
{
|
|
4536
4605
|
name: "pitroom_show",
|
|
4537
4606
|
title: "Show a run",
|
|
4538
|
-
description: "A
|
|
4539
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } }
|
|
4607
|
+
description: "A run's report (live progress while it runs); patch: the exact diff; full: the untruncated answer.",
|
|
4608
|
+
inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } } },
|
|
4540
4609
|
annotations: READ_ONLY,
|
|
4541
4610
|
async call(a, ctx) {
|
|
4542
|
-
const run2 = str(a, "run"
|
|
4611
|
+
const run2 = str(a, "run") ?? "last";
|
|
4543
4612
|
return plain(["show", run2, ...bool(a, "patch") ? ["--patch"] : bool(a, "full") ? ["--full"] : []], ctx);
|
|
4544
4613
|
}
|
|
4545
4614
|
},
|
|
4546
4615
|
{
|
|
4547
|
-
name: "
|
|
4548
|
-
title: "
|
|
4549
|
-
description: "
|
|
4616
|
+
name: "pitroom_info",
|
|
4617
|
+
title: "Pitroom information",
|
|
4618
|
+
description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), cooldown (rate-limited models being skipped), config, doctor (setup check).",
|
|
4550
4619
|
inputSchema: {
|
|
4551
4620
|
type: "object",
|
|
4552
4621
|
properties: {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4622
|
+
topic: { type: "string", enum: Object.keys(TOPICS) },
|
|
4623
|
+
text: { type: "string" },
|
|
4624
|
+
state: { type: "string" },
|
|
4625
|
+
model: { type: "string" },
|
|
4626
|
+
since: { type: "string", description: "7d, 30d, \u2026 or all." },
|
|
4627
|
+
limit: { type: "number" },
|
|
4628
|
+
running: { type: "boolean" },
|
|
4629
|
+
group: { type: "string" },
|
|
4630
|
+
worker: { type: "string" },
|
|
4631
|
+
all: { type: "boolean" },
|
|
4632
|
+
perModel: { type: "boolean" }
|
|
4633
|
+
},
|
|
4634
|
+
required: ["topic"]
|
|
4559
4635
|
},
|
|
4560
4636
|
annotations: READ_ONLY,
|
|
4561
4637
|
async call(a, ctx) {
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
const
|
|
4565
|
-
|
|
4566
|
-
const model = str(a, "model");
|
|
4567
|
-
return plain(["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []], ctx);
|
|
4568
|
-
}
|
|
4569
|
-
},
|
|
4570
|
-
{
|
|
4571
|
-
name: "pitroom_stats",
|
|
4572
|
-
title: "Worker statistics",
|
|
4573
|
-
description: "Runs, success rate, average time and tokens per worker and model, and how often their answers were confirmed by audits. Use it to pick a worker.",
|
|
4574
|
-
inputSchema: { type: "object", properties: { since: SINCE } },
|
|
4575
|
-
annotations: READ_ONLY,
|
|
4576
|
-
async call(a, ctx) {
|
|
4577
|
-
return plain(["history", "stats", ...since(a)], ctx);
|
|
4578
|
-
}
|
|
4579
|
-
},
|
|
4580
|
-
{
|
|
4581
|
-
name: "pitroom_savings",
|
|
4582
|
-
title: "What the workers saved",
|
|
4583
|
-
description: "The tokens and money workers took over from you, per model, from the receipts of the runs.",
|
|
4584
|
-
inputSchema: { type: "object", properties: { since: SINCE, models: { type: "boolean", description: "Break it down per model." } } },
|
|
4585
|
-
annotations: READ_ONLY,
|
|
4586
|
-
async call(a, ctx) {
|
|
4587
|
-
return plain(["savings", ...since(a), ...bool(a, "models") ? ["--models"] : []], ctx);
|
|
4588
|
-
}
|
|
4589
|
-
},
|
|
4590
|
-
{
|
|
4591
|
-
name: "pitroom_models",
|
|
4592
|
-
title: "Worker models",
|
|
4593
|
-
description: "The models a worker backend offers, with effort levels, the costs set in the config and recent usage. Use it to choose a model for pitroom_run.",
|
|
4594
|
-
inputSchema: { type: "object", properties: { worker: { type: "string", description: "A backend: opencode, codex, claude, gemini. Default: the configured one." }, all: { type: "boolean", description: "Include models that are hidden by default." } } },
|
|
4595
|
-
annotations: READ_ONLY,
|
|
4596
|
-
async call(a, ctx) {
|
|
4597
|
-
const worker = str(a, "worker");
|
|
4598
|
-
return plain(["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []], ctx);
|
|
4599
|
-
}
|
|
4600
|
-
},
|
|
4601
|
-
{
|
|
4602
|
-
name: "pitroom_cooldown",
|
|
4603
|
-
title: "Rate-limited models",
|
|
4604
|
-
description: 'The models that said "rate limited" and are being skipped for now (runs go to the next worker). With clear they are tried again immediately.',
|
|
4605
|
-
inputSchema: { type: "object", properties: { clear: { type: "boolean", description: "Forget the cooldowns." } } },
|
|
4606
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4607
|
-
async call(a, ctx) {
|
|
4608
|
-
return plain(["cooldown", ...bool(a, "clear") ? ["--clear"] : []], ctx);
|
|
4609
|
-
}
|
|
4610
|
-
},
|
|
4611
|
-
{
|
|
4612
|
-
name: "pitroom_doctor",
|
|
4613
|
-
title: "Check the setup",
|
|
4614
|
-
description: "Checks the setup without spending tokens: the workers found and their logins, the config, the skills, where the MCP server is registered. Use it when a run fails to start.",
|
|
4615
|
-
inputSchema: { type: "object", properties: {} },
|
|
4616
|
-
annotations: READ_ONLY,
|
|
4617
|
-
async call(_a, ctx) {
|
|
4618
|
-
return plain(["doctor"], ctx, 12e4);
|
|
4638
|
+
if (str(a, "topic") === void 0) throw new ToolError(`"topic" is required: one of ${Object.keys(TOPICS).join(", ")}`);
|
|
4639
|
+
const topic = oneOf(a, "topic", Object.keys(TOPICS), "runs");
|
|
4640
|
+
const { args, timeoutMs } = infoArgs(topic, a);
|
|
4641
|
+
return plain(args, ctx, timeoutMs);
|
|
4619
4642
|
}
|
|
4620
4643
|
},
|
|
4621
4644
|
{
|
|
4622
4645
|
name: "pitroom_review",
|
|
4623
4646
|
title: "Review a change",
|
|
4624
|
-
description: `A read-only reviewer (
|
|
4647
|
+
description: `A read-only reviewer (another worker by default) judges a run's change or a commit range ("main..HEAD"): findings with severities and a SPEC / QUALITY verdict.`,
|
|
4625
4648
|
inputSchema: {
|
|
4626
4649
|
type: "object",
|
|
4627
4650
|
properties: {
|
|
4628
|
-
run:
|
|
4651
|
+
run: RUN_ID2,
|
|
4629
4652
|
range: { type: "string", description: 'Or a commit range "A..B".' },
|
|
4630
|
-
plan: { type: "string", description: "With range: the plan file
|
|
4653
|
+
plan: { type: "string", description: "With range: the plan file it implements." },
|
|
4631
4654
|
worker: WORKER_PROPS.worker,
|
|
4632
4655
|
tier: WORKER_PROPS.tier,
|
|
4633
4656
|
dir: WORKER_PROPS.dir,
|
|
@@ -4648,7 +4671,7 @@ var TOOLS2 = [
|
|
|
4648
4671
|
{
|
|
4649
4672
|
name: "pitroom_audit",
|
|
4650
4673
|
title: "Audit an answer",
|
|
4651
|
-
description: "Another worker re-checks a
|
|
4674
|
+
description: "Another worker re-checks a read run's answer: AGREE, PARTIAL or DISAGREE, with the disputed claims.",
|
|
4652
4675
|
inputSchema: { type: "object", properties: { run: RUN_ID2, worker: WORKER_PROPS.worker, ...WAIT_PROP }, required: ["run"] },
|
|
4653
4676
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4654
4677
|
async call(a, ctx) {
|
|
@@ -4660,7 +4683,7 @@ var TOOLS2 = [
|
|
|
4660
4683
|
{
|
|
4661
4684
|
name: "pitroom_apply",
|
|
4662
4685
|
title: "Apply an isolated change",
|
|
4663
|
-
description: "Land an isolated run's patch
|
|
4686
|
+
description: "Land an isolated run's patch (or a group's, in order) on the working tree; checked first. A patch deleting files needs allowDelete: pass it only after checking the deletions were asked for.",
|
|
4664
4687
|
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, allowDelete: { type: "boolean" } } },
|
|
4665
4688
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
4666
4689
|
async call(a, ctx) {
|
|
@@ -4668,13 +4691,13 @@ var TOOLS2 = [
|
|
|
4668
4691
|
const run2 = str(a, "run");
|
|
4669
4692
|
if (!run2 && !group) throw new ToolError('give "run" or "group"');
|
|
4670
4693
|
if (run2 && group) throw new ToolError('give "run" or "group", not both');
|
|
4671
|
-
return plain(["apply", ...group
|
|
4694
|
+
return plain(["apply", ...group ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], ctx, 12e4);
|
|
4672
4695
|
}
|
|
4673
4696
|
},
|
|
4674
4697
|
{
|
|
4675
4698
|
name: "pitroom_discard",
|
|
4676
4699
|
title: "Discard an isolated change",
|
|
4677
|
-
description: "Drop an isolated run's private copy (
|
|
4700
|
+
description: "Drop an isolated run's private copy (the patch is kept).",
|
|
4678
4701
|
inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
|
|
4679
4702
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4680
4703
|
async call(a, ctx) {
|
|
@@ -4684,7 +4707,7 @@ var TOOLS2 = [
|
|
|
4684
4707
|
{
|
|
4685
4708
|
name: "pitroom_revert",
|
|
4686
4709
|
title: "Undo an in-place change",
|
|
4687
|
-
description:
|
|
4710
|
+
description: `Undo a mode "write" run's changes (refused if the files changed since).`,
|
|
4688
4711
|
inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
|
|
4689
4712
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
4690
4713
|
async call(a, ctx) {
|
|
@@ -4694,32 +4717,115 @@ var TOOLS2 = [
|
|
|
4694
4717
|
{
|
|
4695
4718
|
name: "pitroom_stop",
|
|
4696
4719
|
title: "Stop runs",
|
|
4697
|
-
description: "Stop a running or queued run, or
|
|
4698
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" } } },
|
|
4720
|
+
description: "Stop a running or queued run, or a group; cooldowns: try rate-limited models again now.",
|
|
4721
|
+
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, cooldowns: { type: "boolean" } } },
|
|
4699
4722
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4700
4723
|
async call(a, ctx) {
|
|
4701
4724
|
const run2 = str(a, "run");
|
|
4702
4725
|
const group = str(a, "group");
|
|
4703
|
-
if (
|
|
4726
|
+
if (bool(a, "cooldowns")) {
|
|
4727
|
+
if (run2 || group) throw new ToolError('"cooldowns" goes alone');
|
|
4728
|
+
return plain(["cooldown", "--clear"], ctx);
|
|
4729
|
+
}
|
|
4730
|
+
if (!run2 && !group) throw new ToolError('give "run" or "group" (or "cooldowns": true)');
|
|
4704
4731
|
if (run2 && group) throw new ToolError('give "run" or "group", not both');
|
|
4705
4732
|
return plain(["stop", ...run2 ? [run2] : ["-g", group]], ctx);
|
|
4706
4733
|
}
|
|
4707
4734
|
}
|
|
4708
4735
|
];
|
|
4709
4736
|
|
|
4737
|
+
// src/cli/mcp-watch.ts
|
|
4738
|
+
var EVERY_MS = 2e3;
|
|
4739
|
+
var MAX_SUBSCRIPTIONS = 100;
|
|
4740
|
+
var watched = /* @__PURE__ */ new Set();
|
|
4741
|
+
var timer;
|
|
4742
|
+
var signature = (uri) => {
|
|
4743
|
+
const id = runOfUri(uri);
|
|
4744
|
+
if (!id) return void 0;
|
|
4745
|
+
try {
|
|
4746
|
+
const m = freshMeta(id);
|
|
4747
|
+
return uri.endsWith("/patch") ? `${m.state}:${m.changes?.length ?? 0}` : m.state;
|
|
4748
|
+
} catch {
|
|
4749
|
+
return void 0;
|
|
4750
|
+
}
|
|
4751
|
+
};
|
|
4752
|
+
var newestRun = () => {
|
|
4753
|
+
try {
|
|
4754
|
+
return listRunIds().at(-1);
|
|
4755
|
+
} catch {
|
|
4756
|
+
return void 0;
|
|
4757
|
+
}
|
|
4758
|
+
};
|
|
4759
|
+
function subscribe(session, uri) {
|
|
4760
|
+
if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4761
|
+
if (!runOfUri(uri)) throw new RpcError(-32602, `only runs can be subscribed to (pitroom://run/<id>): ${uri}`);
|
|
4762
|
+
if (!session.subscriptions.has(uri) && session.subscriptions.size >= MAX_SUBSCRIPTIONS) throw new RpcError(-32602, `at most ${MAX_SUBSCRIPTIONS} subscriptions: unsubscribe from finished runs first`);
|
|
4763
|
+
const sig = signature(uri);
|
|
4764
|
+
if (sig === void 0) throw new RpcError(-32002, `unknown resource: ${uri}`);
|
|
4765
|
+
session.subscriptions.set(uri, sig);
|
|
4766
|
+
}
|
|
4767
|
+
function unsubscribe(session, uri) {
|
|
4768
|
+
if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4769
|
+
session.subscriptions.delete(uri);
|
|
4770
|
+
}
|
|
4771
|
+
function tick() {
|
|
4772
|
+
const listening = [...watched].filter((s) => s.push);
|
|
4773
|
+
if (!listening.length) return;
|
|
4774
|
+
const newest = newestRun();
|
|
4775
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4776
|
+
for (const s of listening) {
|
|
4777
|
+
const push = s.push;
|
|
4778
|
+
if (newest !== s.newestRun) {
|
|
4779
|
+
s.newestRun = newest;
|
|
4780
|
+
push({ jsonrpc: "2.0", method: "notifications/resources/list_changed" });
|
|
4781
|
+
}
|
|
4782
|
+
for (const [uri, last] of s.subscriptions) {
|
|
4783
|
+
if (!seen.has(uri)) seen.set(uri, signature(uri));
|
|
4784
|
+
const now = seen.get(uri);
|
|
4785
|
+
if (now === last) continue;
|
|
4786
|
+
if (now === void 0) s.subscriptions.delete(uri);
|
|
4787
|
+
else s.subscriptions.set(uri, now);
|
|
4788
|
+
push({ jsonrpc: "2.0", method: "notifications/resources/updated", params: { uri } });
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
function watch2(session) {
|
|
4793
|
+
session.newestRun = newestRun();
|
|
4794
|
+
watched.add(session);
|
|
4795
|
+
if (!timer) {
|
|
4796
|
+
timer = setInterval(tick, EVERY_MS);
|
|
4797
|
+
timer.unref();
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function unwatch(session) {
|
|
4801
|
+
watched.delete(session);
|
|
4802
|
+
session.subscriptions.clear();
|
|
4803
|
+
if (!watched.size && timer) {
|
|
4804
|
+
clearInterval(timer);
|
|
4805
|
+
timer = void 0;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4710
4809
|
// src/cli/mcp.ts
|
|
4711
4810
|
var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
4712
4811
|
var INSTRUCTIONS = [
|
|
4713
4812
|
"Pitroom hands bounded work to cheaper worker agents and returns a verified answer, the exact diff and a cost receipt. You decide, verify and answer.",
|
|
4714
|
-
'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard);
|
|
4813
|
+
'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard); pitroom_run with "tasks" for independent tasks in parallel; pitroom_info (topic history) finds an earlier answer before you ask again.',
|
|
4715
4814
|
'A run can take minutes: pitroom_run waits up to waitSeconds, then returns the run id as "still running"; call pitroom_wait with it.',
|
|
4815
|
+
"Pitroom's skills (using-pitroom, pitroom-research, pitroom-implement, pitroom-review, pitroom-crew, pitroom-debugging, pitroom-tdd and more) say when to delegate and how to check what comes back: if you have them, use the matching one alongside these tools; if not, the prompts of the same names hold them.",
|
|
4716
4816
|
"It is optional: for a small task you can do yourself, skip it."
|
|
4717
4817
|
].join(" ");
|
|
4718
4818
|
var log = (msg) => void process.stderr.write(`pitroom mcp: ${msg}
|
|
4719
4819
|
`);
|
|
4720
|
-
|
|
4820
|
+
function newSession() {
|
|
4821
|
+
const session = { inflight: /* @__PURE__ */ new Map(), subscriptions: /* @__PURE__ */ new Map() };
|
|
4822
|
+
watch2(session);
|
|
4823
|
+
return session;
|
|
4824
|
+
}
|
|
4721
4825
|
function endSession(session) {
|
|
4722
4826
|
for (const { abort } of session.inflight.values()) abort.abort("closed");
|
|
4827
|
+
unwatch(session);
|
|
4828
|
+
session.push = void 0;
|
|
4723
4829
|
}
|
|
4724
4830
|
function progressToken(params) {
|
|
4725
4831
|
const meta = params && typeof params === "object" ? params._meta : void 0;
|
|
@@ -4737,13 +4843,13 @@ function contextFor(params, abort, notify) {
|
|
|
4737
4843
|
}
|
|
4738
4844
|
};
|
|
4739
4845
|
}
|
|
4740
|
-
async function dispatch(method, params, ctx) {
|
|
4846
|
+
async function dispatch(method, params, ctx, session) {
|
|
4741
4847
|
switch (method) {
|
|
4742
4848
|
case "initialize": {
|
|
4743
4849
|
const asked = typeof params.protocolVersion === "string" ? params.protocolVersion : "";
|
|
4744
4850
|
return {
|
|
4745
4851
|
protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
|
|
4746
|
-
capabilities: { tools: { listChanged: false }, resources: { listChanged:
|
|
4852
|
+
capabilities: { tools: { listChanged: false }, resources: { listChanged: true, subscribe: true }, prompts: { listChanged: false } },
|
|
4747
4853
|
serverInfo: { name: "pitroom", title: "Pitroom", version: VERSION2 },
|
|
4748
4854
|
instructions: INSTRUCTIONS
|
|
4749
4855
|
};
|
|
@@ -4772,6 +4878,12 @@ async function dispatch(method, params, ctx) {
|
|
|
4772
4878
|
case "resources/read":
|
|
4773
4879
|
if (typeof params.uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4774
4880
|
return readResource(params.uri, ctx);
|
|
4881
|
+
case "resources/subscribe":
|
|
4882
|
+
subscribe(session, params.uri);
|
|
4883
|
+
return {};
|
|
4884
|
+
case "resources/unsubscribe":
|
|
4885
|
+
unsubscribe(session, params.uri);
|
|
4886
|
+
return {};
|
|
4775
4887
|
case "prompts/list":
|
|
4776
4888
|
return listPrompts();
|
|
4777
4889
|
case "prompts/get":
|
|
@@ -4799,7 +4911,7 @@ async function handle(session, msg, notify) {
|
|
|
4799
4911
|
const entry = { abort, ctx: contextFor(params, abort, notify) };
|
|
4800
4912
|
if (isRequest) session.inflight.set(m.id, entry);
|
|
4801
4913
|
try {
|
|
4802
|
-
const result = await dispatch(m.method, params, entry.ctx);
|
|
4914
|
+
const result = await dispatch(m.method, params, entry.ctx, session);
|
|
4803
4915
|
if (abort.signal.aborted && abort.signal.reason === "cancelled") return void 0;
|
|
4804
4916
|
return isRequest ? { jsonrpc: "2.0", id: m.id, result } : void 0;
|
|
4805
4917
|
} catch (e) {
|
|
@@ -4824,6 +4936,7 @@ async function serveMcp() {
|
|
|
4824
4936
|
const send = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
|
|
4825
4937
|
`);
|
|
4826
4938
|
const session = newSession();
|
|
4939
|
+
session.push = send;
|
|
4827
4940
|
const rl = readline.createInterface({ input: process.stdin });
|
|
4828
4941
|
const pending = /* @__PURE__ */ new Set();
|
|
4829
4942
|
const handleLine = async (line) => {
|
|
@@ -4844,28 +4957,29 @@ async function serveMcp() {
|
|
|
4844
4957
|
void p.finally(() => pending.delete(p));
|
|
4845
4958
|
});
|
|
4846
4959
|
await new Promise((resolve2) => rl.once("close", resolve2));
|
|
4960
|
+
unwatch(session);
|
|
4847
4961
|
await Promise.allSettled([...pending]);
|
|
4848
4962
|
return 0;
|
|
4849
4963
|
}
|
|
4850
4964
|
|
|
4851
4965
|
// src/cli/mcp-http.ts
|
|
4852
4966
|
import crypto7 from "node:crypto";
|
|
4853
|
-
import
|
|
4967
|
+
import fs27 from "node:fs";
|
|
4854
4968
|
import http2 from "node:http";
|
|
4855
|
-
import
|
|
4969
|
+
import path23 from "node:path";
|
|
4856
4970
|
|
|
4857
4971
|
// src/core/dash.ts
|
|
4858
4972
|
import { spawn as spawn4 } from "node:child_process";
|
|
4859
4973
|
import crypto6 from "node:crypto";
|
|
4860
|
-
import
|
|
4974
|
+
import fs26 from "node:fs";
|
|
4861
4975
|
import http from "node:http";
|
|
4862
|
-
import
|
|
4976
|
+
import path22 from "node:path";
|
|
4863
4977
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4864
4978
|
|
|
4865
4979
|
// src/core/file-diff.ts
|
|
4866
4980
|
var PREVIEW_LINES = 300;
|
|
4867
4981
|
function gitPath(value, prefix = true) {
|
|
4868
|
-
let
|
|
4982
|
+
let path31 = value;
|
|
4869
4983
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
4870
4984
|
const bytes = [];
|
|
4871
4985
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -4873,9 +4987,9 @@ function gitPath(value, prefix = true) {
|
|
|
4873
4987
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
4874
4988
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
4875
4989
|
}
|
|
4876
|
-
|
|
4990
|
+
path31 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
4877
4991
|
}
|
|
4878
|
-
return prefix ?
|
|
4992
|
+
return prefix ? path31.replace(/^[ab]\//, "") : path31;
|
|
4879
4993
|
}
|
|
4880
4994
|
function headerPath(header) {
|
|
4881
4995
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5005,6 +5119,7 @@ function toRun(m) {
|
|
|
5005
5119
|
return {
|
|
5006
5120
|
id: m.id,
|
|
5007
5121
|
state: m.state,
|
|
5122
|
+
verifyFailed: m.verifyResult && !m.verifyResult.ok ? true : void 0,
|
|
5008
5123
|
kind: kind(m),
|
|
5009
5124
|
// the model that really ran, once the CLI has said which; until then just the worker, never a guess
|
|
5010
5125
|
worker: workerName(ranTarget(m)),
|
|
@@ -5019,7 +5134,7 @@ function toRun(m) {
|
|
|
5019
5134
|
audit: auditBadge(m),
|
|
5020
5135
|
changes: m.changes?.length || void 0,
|
|
5021
5136
|
applied: m.applied || void 0,
|
|
5022
|
-
note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
|
|
5137
|
+
note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verifyResult && !m.verifyResult.ok ? `verify failed: ${m.verify ?? ""}` : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
|
|
5023
5138
|
};
|
|
5024
5139
|
}
|
|
5025
5140
|
function dashState(opts = {}) {
|
|
@@ -5123,11 +5238,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
5123
5238
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5124
5239
|
function readAsset(route) {
|
|
5125
5240
|
try {
|
|
5126
|
-
const file2 =
|
|
5127
|
-
const { mtimeMs } =
|
|
5241
|
+
const file2 = path22.join(assetsDir(), path22.basename(route));
|
|
5242
|
+
const { mtimeMs } = fs26.statSync(file2);
|
|
5128
5243
|
const hit = assetCache.get(route);
|
|
5129
5244
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5130
|
-
const data =
|
|
5245
|
+
const data = fs26.readFileSync(file2);
|
|
5131
5246
|
assetCache.set(route, { mtimeMs, data });
|
|
5132
5247
|
return data;
|
|
5133
5248
|
} catch {
|
|
@@ -5160,21 +5275,21 @@ function handler(touch2) {
|
|
|
5160
5275
|
}
|
|
5161
5276
|
if (url.pathname === "/api/history") {
|
|
5162
5277
|
const q = url.searchParams;
|
|
5163
|
-
const
|
|
5278
|
+
const days2 = Number(q.get("days") ?? 0);
|
|
5164
5279
|
return json(res, 200, listHistory({
|
|
5165
5280
|
text: (q.get("q") ?? "").slice(0, 200),
|
|
5166
5281
|
model: q.get("model") || void 0,
|
|
5167
5282
|
backend: q.get("backend") || void 0,
|
|
5168
5283
|
state: q.get("state") || void 0,
|
|
5169
5284
|
group: q.get("group") || void 0,
|
|
5170
|
-
sinceMs:
|
|
5285
|
+
sinceMs: days2 > 0 ? Date.now() - days2 * 864e5 : void 0,
|
|
5171
5286
|
beforeId: RUN_ID3.test(q.get("before") ?? "") ? q.get("before") : void 0,
|
|
5172
5287
|
limit: Number(q.get("limit") ?? 30) || 30
|
|
5173
5288
|
}));
|
|
5174
5289
|
}
|
|
5175
5290
|
if (url.pathname === "/api/stats") {
|
|
5176
|
-
const
|
|
5177
|
-
return json(res, 200, { ...historyStats(
|
|
5291
|
+
const days2 = Number(url.searchParams.get("days") ?? 30);
|
|
5292
|
+
return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
|
|
5178
5293
|
}
|
|
5179
5294
|
const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
|
|
5180
5295
|
if (m) {
|
|
@@ -5193,21 +5308,21 @@ function startDash(opts) {
|
|
|
5193
5308
|
const port = server.address().port;
|
|
5194
5309
|
let done;
|
|
5195
5310
|
const closed = new Promise((r) => done = r);
|
|
5196
|
-
let
|
|
5311
|
+
let timer2;
|
|
5197
5312
|
const close = () => {
|
|
5198
|
-
if (
|
|
5313
|
+
if (timer2) clearInterval(timer2);
|
|
5199
5314
|
server.closeAllConnections?.();
|
|
5200
5315
|
return new Promise((r) => server.close(() => (done(), r())));
|
|
5201
5316
|
};
|
|
5202
5317
|
if (opts.idleMs && opts.idleMs > 0) {
|
|
5203
|
-
|
|
5204
|
-
|
|
5318
|
+
timer2 = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
|
|
5319
|
+
timer2.unref();
|
|
5205
5320
|
}
|
|
5206
5321
|
resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
|
|
5207
5322
|
});
|
|
5208
5323
|
});
|
|
5209
5324
|
}
|
|
5210
|
-
var registryFile = () =>
|
|
5325
|
+
var registryFile = () => path22.join(home(), "dash.json");
|
|
5211
5326
|
async function ping(port) {
|
|
5212
5327
|
try {
|
|
5213
5328
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5218,11 +5333,11 @@ async function ping(port) {
|
|
|
5218
5333
|
}
|
|
5219
5334
|
async function runningDash() {
|
|
5220
5335
|
try {
|
|
5221
|
-
const r = JSON.parse(
|
|
5336
|
+
const r = JSON.parse(fs26.readFileSync(registryFile(), "utf8"));
|
|
5222
5337
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
5223
5338
|
} catch {
|
|
5224
5339
|
}
|
|
5225
|
-
|
|
5340
|
+
fs26.rmSync(registryFile(), { force: true });
|
|
5226
5341
|
return void 0;
|
|
5227
5342
|
}
|
|
5228
5343
|
function openBrowser(url) {
|
|
@@ -5237,7 +5352,7 @@ async function dashCommand(o) {
|
|
|
5237
5352
|
if (o.stop) {
|
|
5238
5353
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5239
5354
|
process.kill(existing.pid, "SIGTERM");
|
|
5240
|
-
|
|
5355
|
+
fs26.rmSync(registryFile(), { force: true });
|
|
5241
5356
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5242
5357
|
return 0;
|
|
5243
5358
|
}
|
|
@@ -5274,8 +5389,8 @@ async function dashCommand(o) {
|
|
|
5274
5389
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5275
5390
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5276
5391
|
}
|
|
5277
|
-
|
|
5278
|
-
|
|
5392
|
+
fs26.mkdirSync(home(), { recursive: true });
|
|
5393
|
+
fs26.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5279
5394
|
const stop = () => void dash.close();
|
|
5280
5395
|
process.once("SIGINT", stop);
|
|
5281
5396
|
process.once("SIGTERM", stop);
|
|
@@ -5284,7 +5399,7 @@ async function dashCommand(o) {
|
|
|
5284
5399
|
if (o.open) openBrowser(dash.url);
|
|
5285
5400
|
await dash.closed;
|
|
5286
5401
|
try {
|
|
5287
|
-
if (JSON.parse(
|
|
5402
|
+
if (JSON.parse(fs26.readFileSync(registryFile(), "utf8")).pid === process.pid) fs26.rmSync(registryFile(), { force: true });
|
|
5288
5403
|
} catch {
|
|
5289
5404
|
}
|
|
5290
5405
|
return 0;
|
|
@@ -5296,10 +5411,10 @@ var MAX_BODY = 4e6;
|
|
|
5296
5411
|
var MAX_SESSIONS = 64;
|
|
5297
5412
|
var IDLE_MS = 60 * 6e4;
|
|
5298
5413
|
var PATH = "/mcp";
|
|
5299
|
-
var tokenFile = () =>
|
|
5414
|
+
var tokenFile = () => path23.join(home(), "mcp-token");
|
|
5300
5415
|
var readToken = (file2) => {
|
|
5301
5416
|
try {
|
|
5302
|
-
const kept =
|
|
5417
|
+
const kept = fs27.readFileSync(file2, "utf8").trim();
|
|
5303
5418
|
return kept.length >= 16 ? kept : void 0;
|
|
5304
5419
|
} catch {
|
|
5305
5420
|
return void 0;
|
|
@@ -5314,10 +5429,10 @@ function mcpToken() {
|
|
|
5314
5429
|
const file2 = tokenFile();
|
|
5315
5430
|
let token = readToken(file2);
|
|
5316
5431
|
if (!token) {
|
|
5317
|
-
|
|
5432
|
+
fs27.mkdirSync(path23.dirname(file2), { recursive: true });
|
|
5318
5433
|
token = crypto7.randomBytes(32).toString("hex");
|
|
5319
5434
|
try {
|
|
5320
|
-
|
|
5435
|
+
fs27.writeFileSync(file2, `${token}
|
|
5321
5436
|
`, { mode: 384, flag: "wx" });
|
|
5322
5437
|
} catch (e) {
|
|
5323
5438
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5326,7 +5441,7 @@ function mcpToken() {
|
|
|
5326
5441
|
}
|
|
5327
5442
|
}
|
|
5328
5443
|
try {
|
|
5329
|
-
|
|
5444
|
+
fs27.chmodSync(file2, 384);
|
|
5330
5445
|
} catch {
|
|
5331
5446
|
}
|
|
5332
5447
|
return { token, from: file2 };
|
|
@@ -5337,6 +5452,15 @@ var sameSecret = (given, token) => {
|
|
|
5337
5452
|
const b = crypto7.createHash("sha256").update(token).digest();
|
|
5338
5453
|
return crypto7.timingSafeEqual(a, b);
|
|
5339
5454
|
};
|
|
5455
|
+
var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
|
|
5456
|
+
function drop(sessions, id) {
|
|
5457
|
+
const k = sessions.get(id);
|
|
5458
|
+
if (!k) return;
|
|
5459
|
+
k.closeStream?.();
|
|
5460
|
+
endSession(k.session);
|
|
5461
|
+
sessions.delete(id);
|
|
5462
|
+
}
|
|
5463
|
+
var PING_MS = 25e3;
|
|
5340
5464
|
function reply(res, status, body, headers = {}) {
|
|
5341
5465
|
if (res.headersSent) return void res.end();
|
|
5342
5466
|
const text = body === void 0 ? "" : JSON.stringify(body);
|
|
@@ -5362,9 +5486,9 @@ var wantsStream = (parsed) => {
|
|
|
5362
5486
|
};
|
|
5363
5487
|
function evictIdle(sessions) {
|
|
5364
5488
|
let oldest;
|
|
5365
|
-
for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || entry[1]
|
|
5489
|
+
for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || idleSince(entry[1]) < idleSince(oldest[1]))) oldest = entry;
|
|
5366
5490
|
if (!oldest) return false;
|
|
5367
|
-
sessions
|
|
5491
|
+
drop(sessions, oldest[0]);
|
|
5368
5492
|
return true;
|
|
5369
5493
|
}
|
|
5370
5494
|
function makeHandler(token, sessions) {
|
|
@@ -5378,13 +5502,44 @@ function makeHandler(token, sessions) {
|
|
|
5378
5502
|
if (!auth || !sameSecret(auth, token)) return reply(res, 401, failure5(-32001, "a bearer token is needed"), { "WWW-Authenticate": "Bearer" });
|
|
5379
5503
|
const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
5380
5504
|
if (req.method === "DELETE") {
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
endSession(kept2.session);
|
|
5384
|
-
sessions.delete(sid);
|
|
5505
|
+
if (!sid || !sessions.has(sid)) return reply(res, 404, failure5(-32600, "no such session"));
|
|
5506
|
+
drop(sessions, sid);
|
|
5385
5507
|
return reply(res, 204);
|
|
5386
5508
|
}
|
|
5387
|
-
if (req.method
|
|
5509
|
+
if (req.method === "GET") {
|
|
5510
|
+
if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406, failure5(-32600, "a GET opens the event stream: send Accept: text/event-stream"));
|
|
5511
|
+
if (!sid) return reply(res, 400, failure5(-32600, "Mcp-Session-Id is missing: initialize first"));
|
|
5512
|
+
const kept2 = sessions.get(sid);
|
|
5513
|
+
if (!kept2) return reply(res, 404, failure5(-32600, "no such session: initialize again"));
|
|
5514
|
+
kept2.closeStream?.();
|
|
5515
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
5516
|
+
res.write(": open\n\n");
|
|
5517
|
+
const open = () => !res.writableEnded && !res.destroyed;
|
|
5518
|
+
const push = (msg) => {
|
|
5519
|
+
if (!open()) return;
|
|
5520
|
+
kept2.streamSeen = Date.now();
|
|
5521
|
+
res.write(`event: message
|
|
5522
|
+
data: ${JSON.stringify(msg)}
|
|
5523
|
+
|
|
5524
|
+
`);
|
|
5525
|
+
};
|
|
5526
|
+
const ping2 = setInterval(() => open() && res.write(": ping\n\n"), PING_MS);
|
|
5527
|
+
ping2.unref();
|
|
5528
|
+
res.on("error", () => {
|
|
5529
|
+
});
|
|
5530
|
+
kept2.streamSeen = Date.now();
|
|
5531
|
+
const close = () => {
|
|
5532
|
+
clearInterval(ping2);
|
|
5533
|
+
if (kept2.session.push === push) kept2.session.push = void 0;
|
|
5534
|
+
if (kept2.closeStream === close) kept2.closeStream = void 0;
|
|
5535
|
+
if (!res.writableEnded) res.end();
|
|
5536
|
+
};
|
|
5537
|
+
kept2.session.push = push;
|
|
5538
|
+
kept2.closeStream = close;
|
|
5539
|
+
res.on("close", close);
|
|
5540
|
+
return;
|
|
5541
|
+
}
|
|
5542
|
+
if (req.method !== "POST") return reply(res, 405, failure5(-32600, "GET, POST or DELETE"), { Allow: "GET, POST, DELETE" });
|
|
5388
5543
|
const body = await readBody(req);
|
|
5389
5544
|
if (body === "too-big") return reply(res, 413, failure5(-32600, `a request body is at most ${MAX_BODY} bytes`));
|
|
5390
5545
|
let parsed;
|
|
@@ -5397,12 +5552,8 @@ function makeHandler(token, sessions) {
|
|
|
5397
5552
|
let kept;
|
|
5398
5553
|
const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5399
5554
|
if (single?.method === "initialize") {
|
|
5400
|
-
|
|
5401
|
-
if (
|
|
5402
|
-
endSession(old.session);
|
|
5403
|
-
sessions.delete(sid);
|
|
5404
|
-
}
|
|
5405
|
-
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, "too many sessions with requests in flight"));
|
|
5555
|
+
if (sid) drop(sessions, sid);
|
|
5556
|
+
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, `${MAX_SESSIONS} sessions are busy with requests: try again when one ends`));
|
|
5406
5557
|
const id = crypto7.randomUUID();
|
|
5407
5558
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
5408
5559
|
sessions.set(id, kept);
|
|
@@ -5444,7 +5595,7 @@ async function serveMcpHttp(opts) {
|
|
|
5444
5595
|
});
|
|
5445
5596
|
});
|
|
5446
5597
|
const sweep = setInterval(() => {
|
|
5447
|
-
for (const [id, k] of sessions) if (Date.now() - k
|
|
5598
|
+
for (const [id, k] of [...sessions]) if (Date.now() - idleSince(k) > IDLE_MS && !k.session.inflight.size) drop(sessions, id);
|
|
5448
5599
|
}, 6e4);
|
|
5449
5600
|
sweep.unref();
|
|
5450
5601
|
const port = await new Promise((resolve2, reject) => {
|
|
@@ -5464,7 +5615,7 @@ async function serveMcpHttp(opts) {
|
|
|
5464
5615
|
process.once("SIGTERM", stop);
|
|
5465
5616
|
});
|
|
5466
5617
|
clearInterval(sweep);
|
|
5467
|
-
for (const
|
|
5618
|
+
for (const id of [...sessions.keys()]) drop(sessions, id);
|
|
5468
5619
|
server.closeAllConnections();
|
|
5469
5620
|
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
5470
5621
|
return 0;
|
|
@@ -5472,16 +5623,16 @@ async function serveMcpHttp(opts) {
|
|
|
5472
5623
|
|
|
5473
5624
|
// src/cli/commands.ts
|
|
5474
5625
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5475
|
-
import
|
|
5476
|
-
import
|
|
5626
|
+
import fs33 from "node:fs";
|
|
5627
|
+
import path29 from "node:path";
|
|
5477
5628
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5478
5629
|
|
|
5479
5630
|
// src/core/init.ts
|
|
5480
|
-
import
|
|
5481
|
-
import
|
|
5631
|
+
import fs28 from "node:fs";
|
|
5632
|
+
import path24 from "node:path";
|
|
5482
5633
|
var FREE = /-free$/;
|
|
5483
5634
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5484
|
-
var isFound = (binary5) =>
|
|
5635
|
+
var isFound = (binary5) => path24.isAbsolute(binary5) && fs28.existsSync(binary5);
|
|
5485
5636
|
function planInit(opts = {}) {
|
|
5486
5637
|
const file2 = configPath();
|
|
5487
5638
|
const workers = backendIds().map((id) => {
|
|
@@ -5527,15 +5678,15 @@ function planInit(opts = {}) {
|
|
|
5527
5678
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5528
5679
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
5529
5680
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
5530
|
-
return { path: file2, exists:
|
|
5681
|
+
return { path: file2, exists: fs28.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5531
5682
|
}
|
|
5532
5683
|
function writeInit(plan, force) {
|
|
5533
5684
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5534
5685
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
5535
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
5536
|
-
|
|
5537
|
-
if (plan.exists)
|
|
5538
|
-
|
|
5686
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path24.basename(plan.path)}.bak)`);
|
|
5687
|
+
fs28.mkdirSync(path24.dirname(plan.path), { recursive: true });
|
|
5688
|
+
if (plan.exists) fs28.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
5689
|
+
fs28.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5539
5690
|
`);
|
|
5540
5691
|
return plan.path;
|
|
5541
5692
|
}
|
|
@@ -5556,17 +5707,17 @@ function formatInit(plan, written) {
|
|
|
5556
5707
|
|
|
5557
5708
|
// src/core/mcp-install.ts
|
|
5558
5709
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5559
|
-
import
|
|
5710
|
+
import fs29 from "node:fs";
|
|
5560
5711
|
import os10 from "node:os";
|
|
5561
|
-
import
|
|
5712
|
+
import path25 from "node:path";
|
|
5562
5713
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
5563
5714
|
var NAME = "pitroom";
|
|
5564
5715
|
function mcpCommand() {
|
|
5565
5716
|
const launcher = launcherPath();
|
|
5566
5717
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5567
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5568
|
-
const p =
|
|
5569
|
-
if (
|
|
5718
|
+
for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
|
|
5719
|
+
const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
5720
|
+
if (fs29.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
|
|
5570
5721
|
}
|
|
5571
5722
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5572
5723
|
}
|
|
@@ -5582,7 +5733,7 @@ function run(backendId, args) {
|
|
|
5582
5733
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5583
5734
|
var readJson = (file2) => {
|
|
5584
5735
|
try {
|
|
5585
|
-
return JSON.parse(
|
|
5736
|
+
return JSON.parse(fs29.readFileSync(file2, "utf8"));
|
|
5586
5737
|
} catch {
|
|
5587
5738
|
return void 0;
|
|
5588
5739
|
}
|
|
@@ -5592,7 +5743,7 @@ var jsonState = (file2, c) => {
|
|
|
5592
5743
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
5593
5744
|
};
|
|
5594
5745
|
var home2 = () => os10.homedir();
|
|
5595
|
-
var geminiDir = () =>
|
|
5746
|
+
var geminiDir = () => path25.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
5596
5747
|
function cliClient(id, backend, name, where, o) {
|
|
5597
5748
|
return {
|
|
5598
5749
|
id,
|
|
@@ -5615,43 +5766,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
5615
5766
|
return {
|
|
5616
5767
|
id,
|
|
5617
5768
|
name,
|
|
5618
|
-
found: () =>
|
|
5769
|
+
found: () => fs29.existsSync(dir()),
|
|
5619
5770
|
state: (c) => jsonState(file2(), c),
|
|
5620
5771
|
add: (c) => {
|
|
5621
5772
|
let config = {};
|
|
5622
|
-
if (
|
|
5773
|
+
if (fs29.existsSync(file2())) {
|
|
5623
5774
|
config = readJson(file2());
|
|
5624
5775
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
5625
5776
|
const backup = `${file2()}.bak-pitroom`;
|
|
5626
|
-
if (!
|
|
5777
|
+
if (!fs29.existsSync(backup)) fs29.copyFileSync(file2(), backup);
|
|
5627
5778
|
}
|
|
5628
5779
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
5629
|
-
|
|
5780
|
+
fs29.mkdirSync(path25.dirname(file2()), { recursive: true });
|
|
5630
5781
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
5631
|
-
|
|
5782
|
+
fs29.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
5632
5783
|
`);
|
|
5633
|
-
|
|
5784
|
+
fs29.renameSync(tmp, file2());
|
|
5634
5785
|
return { ok: true, message: rel() };
|
|
5635
5786
|
},
|
|
5636
5787
|
remove: () => {
|
|
5637
5788
|
const config = readJson(file2());
|
|
5638
5789
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
5639
5790
|
delete config.mcpServers[NAME];
|
|
5640
|
-
|
|
5791
|
+
fs29.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
5641
5792
|
`);
|
|
5642
5793
|
return { ok: true, message: rel() };
|
|
5643
5794
|
}
|
|
5644
5795
|
};
|
|
5645
5796
|
}
|
|
5646
5797
|
function desktopFile() {
|
|
5647
|
-
if (process.platform === "darwin") return
|
|
5648
|
-
if (process.platform === "win32") return
|
|
5649
|
-
return
|
|
5798
|
+
if (process.platform === "darwin") return path25.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
5799
|
+
if (process.platform === "win32") return path25.join(process.env.APPDATA ?? path25.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
5800
|
+
return path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
5650
5801
|
}
|
|
5651
5802
|
function codexState(c) {
|
|
5652
5803
|
let text;
|
|
5653
5804
|
try {
|
|
5654
|
-
text =
|
|
5805
|
+
text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
5655
5806
|
} catch {
|
|
5656
5807
|
return "absent";
|
|
5657
5808
|
}
|
|
@@ -5669,7 +5820,7 @@ function codexState(c) {
|
|
|
5669
5820
|
function clients() {
|
|
5670
5821
|
return [
|
|
5671
5822
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
5672
|
-
state: (c) => jsonState(
|
|
5823
|
+
state: (c) => jsonState(path25.join(home2(), ".claude.json"), c),
|
|
5673
5824
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
5674
5825
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
5675
5826
|
}),
|
|
@@ -5679,12 +5830,12 @@ function clients() {
|
|
|
5679
5830
|
remove: () => ["mcp", "remove", NAME]
|
|
5680
5831
|
}),
|
|
5681
5832
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
5682
|
-
state: (c) => jsonState(
|
|
5833
|
+
state: (c) => jsonState(path25.join(geminiDir(), "settings.json"), c),
|
|
5683
5834
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
5684
5835
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
5685
5836
|
}),
|
|
5686
|
-
jsonClient("cursor", "Cursor", () =>
|
|
5687
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
5837
|
+
jsonClient("cursor", "Cursor", () => path25.join(home2(), ".cursor", "mcp.json"), () => path25.join(home2(), ".cursor")),
|
|
5838
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path25.dirname(desktopFile()))
|
|
5688
5839
|
];
|
|
5689
5840
|
}
|
|
5690
5841
|
function attempt2(f) {
|
|
@@ -5755,16 +5906,16 @@ var parseClients = (value) => {
|
|
|
5755
5906
|
|
|
5756
5907
|
// src/core/plan-status.ts
|
|
5757
5908
|
import crypto8 from "node:crypto";
|
|
5758
|
-
import
|
|
5759
|
-
import
|
|
5909
|
+
import fs30 from "node:fs";
|
|
5910
|
+
import path26 from "node:path";
|
|
5760
5911
|
function notesFile(plan) {
|
|
5761
5912
|
const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
5762
|
-
return
|
|
5913
|
+
return path26.join(home(), "plans", id, "notes.md");
|
|
5763
5914
|
}
|
|
5764
5915
|
function readNotes(plan) {
|
|
5765
5916
|
const f = notesFile(plan);
|
|
5766
|
-
if (!
|
|
5767
|
-
return
|
|
5917
|
+
if (!fs30.existsSync(f)) return [];
|
|
5918
|
+
return fs30.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
5768
5919
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
5769
5920
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
5770
5921
|
});
|
|
@@ -5772,11 +5923,11 @@ function readNotes(plan) {
|
|
|
5772
5923
|
function addNote(planFile, text) {
|
|
5773
5924
|
const plan = loadPlan(planFile);
|
|
5774
5925
|
const f = notesFile(plan);
|
|
5775
|
-
|
|
5776
|
-
if (!
|
|
5926
|
+
fs30.mkdirSync(path26.dirname(f), { recursive: true });
|
|
5927
|
+
if (!fs30.existsSync(f)) fs30.writeFileSync(f, `# plan: ${plan.file}
|
|
5777
5928
|
`);
|
|
5778
5929
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
5779
|
-
|
|
5930
|
+
fs30.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
5780
5931
|
`);
|
|
5781
5932
|
return f;
|
|
5782
5933
|
}
|
|
@@ -5842,10 +5993,73 @@ ${rulings}`,
|
|
|
5842
5993
|
].join("\n");
|
|
5843
5994
|
}
|
|
5844
5995
|
|
|
5845
|
-
// src/core/
|
|
5996
|
+
// src/core/cache.ts
|
|
5846
5997
|
import crypto9 from "node:crypto";
|
|
5847
|
-
import
|
|
5848
|
-
import
|
|
5998
|
+
import fs31 from "node:fs";
|
|
5999
|
+
import path27 from "node:path";
|
|
6000
|
+
var SCAN2 = 500;
|
|
6001
|
+
function cacheKey(o) {
|
|
6002
|
+
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6003
|
+
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6004
|
+
const dir = path27.resolve(o.dir);
|
|
6005
|
+
const root = repoRoot(dir);
|
|
6006
|
+
if (!root) return void 0;
|
|
6007
|
+
try {
|
|
6008
|
+
const tree = snapshotTree(root);
|
|
6009
|
+
const head = commitOf(root, "HEAD") ?? "";
|
|
6010
|
+
const files = o.files.map((f) => {
|
|
6011
|
+
const abs = path27.resolve(f);
|
|
6012
|
+
return [abs, crypto9.createHash("sha256").update(fs31.readFileSync(abs)).digest("hex")];
|
|
6013
|
+
});
|
|
6014
|
+
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6015
|
+
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6016
|
+
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6017
|
+
const key = crypto9.createHash("sha256").update(JSON.stringify([question, path27.relative(root, dir), files, where])).digest("hex");
|
|
6018
|
+
return { key, state: `${head}:${tree}` };
|
|
6019
|
+
} catch {
|
|
6020
|
+
return void 0;
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
function heldUp(m) {
|
|
6024
|
+
if (m.state !== "done" || m.mode !== "read" || m.reviewOf || m.auditOf) return false;
|
|
6025
|
+
if (m.refs && m.refs.invalid.length) return false;
|
|
6026
|
+
if (m.warnings.some((w) => w.startsWith("READ-ONLY VIOLATION"))) return false;
|
|
6027
|
+
if (m.audit && (isActive(m.audit.state) || m.audit.verdict === "disagree" || m.audit.verdict === "partial")) return false;
|
|
6028
|
+
try {
|
|
6029
|
+
return readSummary(m).trim() !== "";
|
|
6030
|
+
} catch {
|
|
6031
|
+
return false;
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
function findCached(k, now = Date.now()) {
|
|
6035
|
+
const maxAge = effective().cacheDays.value * 864e5;
|
|
6036
|
+
let best;
|
|
6037
|
+
for (const id of listRunIds().slice(-SCAN2)) {
|
|
6038
|
+
let m;
|
|
6039
|
+
try {
|
|
6040
|
+
m = readMeta(id);
|
|
6041
|
+
} catch {
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
6044
|
+
if (m.cache?.key !== k.key || m.cache.state !== k.state) continue;
|
|
6045
|
+
const at = Date.parse(m.endedAt ?? m.startedAt);
|
|
6046
|
+
if (now - at > maxAge || best && at <= best.at || !heldUp(m)) continue;
|
|
6047
|
+
best = { m, at };
|
|
6048
|
+
}
|
|
6049
|
+
return best?.m;
|
|
6050
|
+
}
|
|
6051
|
+
function ago(iso, now = Date.now()) {
|
|
6052
|
+
const s = Math.max(0, (now - Date.parse(iso)) / 1e3);
|
|
6053
|
+
if (s < 3600) return `${Math.max(1, Math.round(s / 60))} min ago`;
|
|
6054
|
+
if (s < 86400) return `${Math.round(s / 3600)} h ago`;
|
|
6055
|
+
return `${Math.round(s / 86400)} d ago`;
|
|
6056
|
+
}
|
|
6057
|
+
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`;
|
|
6058
|
+
|
|
6059
|
+
// src/core/review.ts
|
|
6060
|
+
import crypto10 from "node:crypto";
|
|
6061
|
+
import fs32 from "node:fs";
|
|
6062
|
+
import path28 from "node:path";
|
|
5849
6063
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
5850
6064
|
var section = (title, body) => `## ${title}
|
|
5851
6065
|
|
|
@@ -5863,7 +6077,7 @@ function ancestors(m) {
|
|
|
5863
6077
|
function briefOf(m) {
|
|
5864
6078
|
const root = [m, ...ancestors(m)].at(-1);
|
|
5865
6079
|
const f = runFile(root.id, "brief.md");
|
|
5866
|
-
return
|
|
6080
|
+
return fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : root.task;
|
|
5867
6081
|
}
|
|
5868
6082
|
function latestReview(ids) {
|
|
5869
6083
|
for (const id of listRunIds().reverse()) {
|
|
@@ -5877,7 +6091,7 @@ function latestReview(ids) {
|
|
|
5877
6091
|
}
|
|
5878
6092
|
function diffOf(m, from = m.baseTree) {
|
|
5879
6093
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
5880
|
-
if (where &&
|
|
6094
|
+
if (where && fs32.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
5881
6095
|
if (from !== m.baseTree) {
|
|
5882
6096
|
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);
|
|
5883
6097
|
}
|
|
@@ -5891,7 +6105,7 @@ function runReview(id) {
|
|
|
5891
6105
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
5892
6106
|
const chain = ancestors(m);
|
|
5893
6107
|
const previous = latestReview(chain.map((a) => a.id));
|
|
5894
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6108
|
+
const dir = m.mode === "isolate" && m.worktree && fs32.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
5895
6109
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
5896
6110
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
5897
6111
|
if (!previous) {
|
|
@@ -5964,12 +6178,25 @@ function pickReviewer(job) {
|
|
|
5964
6178
|
}
|
|
5965
6179
|
function writePackage(job) {
|
|
5966
6180
|
const g = gitDir(job.dir);
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
6181
|
+
const root = repoRoot(job.dir);
|
|
6182
|
+
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6183
|
+
const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
|
|
6184
|
+
const inside2 = !path28.relative(fs32.realpathSync(root), fs32.realpathSync(g)).startsWith("..");
|
|
6185
|
+
const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
|
|
6186
|
+
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6187
|
+
fs32.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
6188
|
+
fs32.writeFileSync(file2, job.package);
|
|
5971
6189
|
return file2;
|
|
5972
6190
|
}
|
|
6191
|
+
function ignoreInGit(root, pattern) {
|
|
6192
|
+
const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6193
|
+
const text = fs32.existsSync(exclude) ? fs32.readFileSync(exclude, "utf8") : "";
|
|
6194
|
+
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6195
|
+
fs32.mkdirSync(path28.dirname(exclude), { recursive: true });
|
|
6196
|
+
fs32.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6197
|
+
${pattern}
|
|
6198
|
+
`);
|
|
6199
|
+
}
|
|
5973
6200
|
|
|
5974
6201
|
// src/core/models.ts
|
|
5975
6202
|
var base = (model) => (model ?? "").split("#")[0];
|
|
@@ -6086,7 +6313,15 @@ warning: ${w}`).join("")
|
|
|
6086
6313
|
async function cmdRun(p) {
|
|
6087
6314
|
const opts = { ...runOptions(p, readTask(p)), plan: planStep(p) };
|
|
6088
6315
|
if (!opts.task.trim() && !opts.plan) throw new UserError('no task given (pitroom "find where X is handled")');
|
|
6089
|
-
|
|
6316
|
+
const cache = cacheKey(opts);
|
|
6317
|
+
const hit = cache && !has(p, "fresh") ? findCached(cache) : void 0;
|
|
6318
|
+
if (hit) {
|
|
6319
|
+
console.log(has(p, "json") ? JSON.stringify({ ...hit, cached: true }, null, 2) : `${cachedNote(hit)}
|
|
6320
|
+
|
|
6321
|
+
${formatReport(hit)}`);
|
|
6322
|
+
return exitCodeFor(hit);
|
|
6323
|
+
}
|
|
6324
|
+
return launch(p, prepareRun({ ...opts, cache }));
|
|
6090
6325
|
}
|
|
6091
6326
|
async function cmdReview(p) {
|
|
6092
6327
|
const range = flag(p, "range");
|
|
@@ -6114,10 +6349,10 @@ async function cmdReview(p) {
|
|
|
6114
6349
|
writeMeta(meta);
|
|
6115
6350
|
}
|
|
6116
6351
|
} catch (e) {
|
|
6117
|
-
|
|
6352
|
+
fs33.rmSync(packageFile, { force: true });
|
|
6118
6353
|
throw e;
|
|
6119
6354
|
}
|
|
6120
|
-
|
|
6355
|
+
fs33.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6121
6356
|
return launch(p, meta);
|
|
6122
6357
|
}
|
|
6123
6358
|
async function cmdAudit(p) {
|
|
@@ -6185,7 +6420,7 @@ function cmdPlan(p) {
|
|
|
6185
6420
|
}
|
|
6186
6421
|
function cmdCrew(p) {
|
|
6187
6422
|
const file2 = flag(p, "task-file");
|
|
6188
|
-
const tasks = (file2 ?
|
|
6423
|
+
const tasks = (file2 ? fs33.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6189
6424
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6190
6425
|
if (has(p, "write") && tasks.length > 1) {
|
|
6191
6426
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6306,7 +6541,7 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
6306
6541
|
const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
|
|
6307
6542
|
if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
|
|
6308
6543
|
if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
|
|
6309
|
-
const body = rows.map((r) => [r.id, when(r.startedAt), r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
6544
|
+
const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
6310
6545
|
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
|
|
6311
6546
|
const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
|
|
6312
6547
|
const fmt = (r, header = false) => r.map((c, i) => {
|
|
@@ -6388,7 +6623,7 @@ function sinceMs(s) {
|
|
|
6388
6623
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6389
6624
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6390
6625
|
}
|
|
6391
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
6626
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs33.readFileSync(0, "utf8");
|
|
6392
6627
|
function cmdStatusline(p) {
|
|
6393
6628
|
const input = readStdin();
|
|
6394
6629
|
const lines = [];
|
|
@@ -6428,8 +6663,8 @@ function cmdModels(p) {
|
|
|
6428
6663
|
return 0;
|
|
6429
6664
|
}
|
|
6430
6665
|
function cmdHookStart() {
|
|
6431
|
-
const script =
|
|
6432
|
-
if (
|
|
6666
|
+
const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
6667
|
+
if (fs33.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6433
6668
|
return 0;
|
|
6434
6669
|
}
|
|
6435
6670
|
function cmdInit(p) {
|
|
@@ -6469,8 +6704,8 @@ function cmdSavings(p) {
|
|
|
6469
6704
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6470
6705
|
const out = flag(p, "card");
|
|
6471
6706
|
if (out) {
|
|
6472
|
-
|
|
6473
|
-
console.log(`card written to ${
|
|
6707
|
+
fs33.writeFileSync(out, card(t, period));
|
|
6708
|
+
console.log(`card written to ${path29.resolve(out)}`);
|
|
6474
6709
|
}
|
|
6475
6710
|
if (has(p, "badge")) console.log(`})`);
|
|
6476
6711
|
return 0;
|
|
@@ -6512,7 +6747,7 @@ function cmdConfig(p) {
|
|
|
6512
6747
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6513
6748
|
return 0;
|
|
6514
6749
|
}
|
|
6515
|
-
console.log(`config file: ${configPath()}${
|
|
6750
|
+
console.log(`config file: ${configPath()}${fs33.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6516
6751
|
for (const [key, s] of Object.entries(eff)) {
|
|
6517
6752
|
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);
|
|
6518
6753
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -6521,22 +6756,22 @@ function cmdConfig(p) {
|
|
|
6521
6756
|
return 0;
|
|
6522
6757
|
}
|
|
6523
6758
|
function cmdClean(p) {
|
|
6524
|
-
const
|
|
6525
|
-
const cutoff = Date.now() -
|
|
6759
|
+
const days2 = Number(flag(p, "days") ?? 14);
|
|
6760
|
+
const cutoff = Date.now() - days2 * 864e5;
|
|
6526
6761
|
const old = listRunIds().filter((id) => {
|
|
6527
6762
|
const m = freshMeta(id);
|
|
6528
6763
|
const pendingPatch = m.mode === "isolate" && !m.applied && !m.discarded && m.changes?.length;
|
|
6529
6764
|
return TERMINAL.includes(m.state) && Date.parse(m.startedAt) < cutoff && !pendingPatch;
|
|
6530
6765
|
});
|
|
6531
6766
|
if (!has(p, "yes")) {
|
|
6532
|
-
console.log(`${old.length} run(s) older than ${
|
|
6767
|
+
console.log(`${old.length} run(s) older than ${days2} days would be removed (unapplied isolate patches are kept). Re-run with --yes.`);
|
|
6533
6768
|
return 0;
|
|
6534
6769
|
}
|
|
6535
6770
|
for (const id of old) {
|
|
6536
6771
|
const m = readMeta(id);
|
|
6537
6772
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
6538
6773
|
recordRun(m);
|
|
6539
|
-
|
|
6774
|
+
fs33.rmSync(runDir(id), { recursive: true, force: true });
|
|
6540
6775
|
}
|
|
6541
6776
|
const snapshots = prune(Date.now(), 0);
|
|
6542
6777
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -6547,9 +6782,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
6547
6782
|
|
|
6548
6783
|
// src/core/doctor.ts
|
|
6549
6784
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6550
|
-
import
|
|
6785
|
+
import fs34 from "node:fs";
|
|
6551
6786
|
import os11 from "node:os";
|
|
6552
|
-
import
|
|
6787
|
+
import path30 from "node:path";
|
|
6553
6788
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
6554
6789
|
var NEXT = [
|
|
6555
6790
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -6559,10 +6794,10 @@ var NEXT = [
|
|
|
6559
6794
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
6560
6795
|
];
|
|
6561
6796
|
function firstNodeOnPath() {
|
|
6562
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6563
|
-
const file2 =
|
|
6797
|
+
for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
|
|
6798
|
+
const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
6564
6799
|
try {
|
|
6565
|
-
if (!
|
|
6800
|
+
if (!fs34.statSync(file2).isFile()) continue;
|
|
6566
6801
|
} catch {
|
|
6567
6802
|
continue;
|
|
6568
6803
|
}
|
|
@@ -6589,12 +6824,12 @@ function doctor5(probe) {
|
|
|
6589
6824
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
6590
6825
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
6591
6826
|
const onPath = firstNodeOnPath();
|
|
6592
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
6827
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path30.resolve(onPath.path) !== path30.resolve(process.execPath)) {
|
|
6593
6828
|
add("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)`);
|
|
6594
6829
|
}
|
|
6595
6830
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
6596
6831
|
const cfg = loadConfig();
|
|
6597
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
6832
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
6598
6833
|
for (const w of cfg.warnings) add("warn", w);
|
|
6599
6834
|
if (process.platform !== "win32") {
|
|
6600
6835
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -6747,19 +6982,19 @@ function skillChecks() {
|
|
|
6747
6982
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
6748
6983
|
}
|
|
6749
6984
|
if (geminiExtensionInstalled()) {
|
|
6750
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
6985
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.agents${path30.sep}`) && i.names.length);
|
|
6751
6986
|
checks.push(
|
|
6752
6987
|
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)" }
|
|
6753
6988
|
);
|
|
6754
6989
|
}
|
|
6755
6990
|
if (viaPlugin) {
|
|
6756
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
6991
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.claude${path30.sep}`) && i.names.length);
|
|
6757
6992
|
checks.push(
|
|
6758
6993
|
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)" }
|
|
6759
6994
|
);
|
|
6760
6995
|
}
|
|
6761
6996
|
const launcher = launcherPath();
|
|
6762
|
-
if (
|
|
6997
|
+
if (fs34.existsSync(launcher)) {
|
|
6763
6998
|
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
6764
6999
|
checks.push(
|
|
6765
7000
|
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)}` }
|
|
@@ -6771,12 +7006,12 @@ function skillChecks() {
|
|
|
6771
7006
|
}
|
|
6772
7007
|
function geminiExtensionInstalled() {
|
|
6773
7008
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
6774
|
-
return
|
|
7009
|
+
return fs34.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
|
|
6775
7010
|
}
|
|
6776
7011
|
function pluginInstalled() {
|
|
6777
7012
|
try {
|
|
6778
|
-
const f =
|
|
6779
|
-
const plugins = JSON.parse(
|
|
7013
|
+
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7014
|
+
const plugins = JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {};
|
|
6780
7015
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
6781
7016
|
} catch {
|
|
6782
7017
|
return false;
|
|
@@ -6793,24 +7028,24 @@ function superpowersActive() {
|
|
|
6793
7028
|
for (const p of openCodePlugins()) {
|
|
6794
7029
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
6795
7030
|
}
|
|
6796
|
-
for (const base2 of [
|
|
6797
|
-
const dir =
|
|
6798
|
-
if (
|
|
7031
|
+
for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
|
|
7032
|
+
const dir = path30.join(base2, "using-superpowers");
|
|
7033
|
+
if (fs34.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
|
|
6799
7034
|
}
|
|
6800
7035
|
return found;
|
|
6801
7036
|
}
|
|
6802
7037
|
function claudePlugins() {
|
|
6803
7038
|
try {
|
|
6804
|
-
const f =
|
|
6805
|
-
return Object.keys(JSON.parse(
|
|
7039
|
+
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7040
|
+
return Object.keys(JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {});
|
|
6806
7041
|
} catch {
|
|
6807
7042
|
return [];
|
|
6808
7043
|
}
|
|
6809
7044
|
}
|
|
6810
7045
|
function claudePluginEnabled(key) {
|
|
6811
7046
|
try {
|
|
6812
|
-
const f =
|
|
6813
|
-
return JSON.parse(
|
|
7047
|
+
const f = path30.join(os11.homedir(), ".claude", "settings.json");
|
|
7048
|
+
return JSON.parse(fs34.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
6814
7049
|
} catch {
|
|
6815
7050
|
return true;
|
|
6816
7051
|
}
|
|
@@ -6819,7 +7054,7 @@ function codexPlugins() {
|
|
|
6819
7054
|
const plugins = /* @__PURE__ */ new Map();
|
|
6820
7055
|
let text;
|
|
6821
7056
|
try {
|
|
6822
|
-
text =
|
|
7057
|
+
text = fs34.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
6823
7058
|
} catch {
|
|
6824
7059
|
return plugins;
|
|
6825
7060
|
}
|
|
@@ -6837,11 +7072,11 @@ function codexPlugins() {
|
|
|
6837
7072
|
return plugins;
|
|
6838
7073
|
}
|
|
6839
7074
|
function openCodePlugins() {
|
|
6840
|
-
const dir =
|
|
7075
|
+
const dir = path30.join(process.env.XDG_CONFIG_HOME ?? path30.join(os11.homedir(), ".config"), "opencode");
|
|
6841
7076
|
const plugins = [];
|
|
6842
7077
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
6843
7078
|
try {
|
|
6844
|
-
const text =
|
|
7079
|
+
const text = fs34.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
6845
7080
|
const list2 = JSON.parse(text).plugin;
|
|
6846
7081
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
6847
7082
|
} catch {
|
|
@@ -6849,7 +7084,7 @@ function openCodePlugins() {
|
|
|
6849
7084
|
}
|
|
6850
7085
|
for (const folder of ["plugin", "plugins"]) {
|
|
6851
7086
|
try {
|
|
6852
|
-
for (const f of
|
|
7087
|
+
for (const f of fs34.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
|
|
6853
7088
|
} catch {
|
|
6854
7089
|
}
|
|
6855
7090
|
}
|
|
@@ -6929,6 +7164,7 @@ Run options
|
|
|
6929
7164
|
--in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
|
|
6930
7165
|
files when the directory has any; config "readIn": auto | snapshot | project)
|
|
6931
7166
|
--audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
|
|
7167
|
+
--fresh ask a worker even when the same read question was answered on the same code (config "cacheDays", default 7)
|
|
6932
7168
|
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
6933
7169
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|
|
6934
7170
|
--step N the plan task for --plan
|
|
@@ -6950,9 +7186,9 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
|
|
|
6950
7186
|
5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
|
|
6951
7187
|
Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
|
|
6952
7188
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
6953
|
-
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG,
|
|
7189
|
+
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
6954
7190
|
PITROOM_<WORKER>_BIN
|
|
6955
|
-
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel)`;
|
|
7191
|
+
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays)`;
|
|
6956
7192
|
var COMMANDS = {
|
|
6957
7193
|
run: cmdRun,
|
|
6958
7194
|
crew: cmdCrew,
|
|
@@ -6965,7 +7201,7 @@ var COMMANDS = {
|
|
|
6965
7201
|
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)");
|
|
6966
7202
|
const dir = flag(p, "dir");
|
|
6967
7203
|
if (dir !== void 0) {
|
|
6968
|
-
if (!
|
|
7204
|
+
if (!fs35.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
6969
7205
|
process.chdir(dir);
|
|
6970
7206
|
process.env.PWD = process.cwd();
|
|
6971
7207
|
}
|