pitroom 0.20.0 → 0.21.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 +9 -0
- package/PRIVACY.md +3 -1
- package/README.md +4 -2
- package/dist/lib.mjs +144 -28
- package/dist/pitroom.mjs +589 -312
- package/dist/ui/app.js +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +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 fs38 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -391,10 +391,10 @@ function parseEvents2(jsonl) {
|
|
|
391
391
|
}
|
|
392
392
|
case "turn.completed": {
|
|
393
393
|
const u = e.usage ?? {};
|
|
394
|
-
const
|
|
394
|
+
const cached4 = num2(u.cached_input_tokens);
|
|
395
395
|
const reasoning = num2(u.reasoning_output_tokens);
|
|
396
|
-
usage2.input += Math.max(0, num2(u.input_tokens) -
|
|
397
|
-
usage2.cacheRead +=
|
|
396
|
+
usage2.input += Math.max(0, num2(u.input_tokens) - cached4);
|
|
397
|
+
usage2.cacheRead += cached4;
|
|
398
398
|
usage2.cacheWrite += num2(u.cache_write_input_tokens);
|
|
399
399
|
usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
|
|
400
400
|
usage2.reasoning += reasoning;
|
|
@@ -768,9 +768,9 @@ function parseEvents3(jsonl) {
|
|
|
768
768
|
if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
|
|
769
769
|
} else if (e.type === "result") {
|
|
770
770
|
const s = e.stats ?? {};
|
|
771
|
-
const
|
|
772
|
-
usage2.cacheRead +=
|
|
773
|
-
usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) -
|
|
771
|
+
const cached4 = num3(s.cached);
|
|
772
|
+
usage2.cacheRead += cached4;
|
|
773
|
+
usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached4);
|
|
774
774
|
usage2.output += num3(s.output_tokens);
|
|
775
775
|
usage2.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
|
|
776
776
|
usage2.toolCalls = Math.max(usage2.toolCalls, num3(s.tool_calls));
|
|
@@ -1446,7 +1446,11 @@ var SCHEMA = {
|
|
|
1446
1446
|
mcpDash: "boolean",
|
|
1447
1447
|
notify: "boolean",
|
|
1448
1448
|
notifyCommand: "string",
|
|
1449
|
-
notifyAfter: "number"
|
|
1449
|
+
notifyAfter: "number",
|
|
1450
|
+
workerPrices: "record",
|
|
1451
|
+
priceFeed: "boolean",
|
|
1452
|
+
priceFeedUrl: "string",
|
|
1453
|
+
priceFeedHours: "number"
|
|
1450
1454
|
};
|
|
1451
1455
|
function configPath() {
|
|
1452
1456
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1544,7 +1548,11 @@ function effective(flags = {}) {
|
|
|
1544
1548
|
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
1545
1549
|
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
1546
1550
|
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
1547
|
-
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15)
|
|
1551
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
1552
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
1553
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
1554
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
1555
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
1548
1556
|
};
|
|
1549
1557
|
}
|
|
1550
1558
|
|
|
@@ -1608,6 +1616,8 @@ var BOOL_FLAGS = {
|
|
|
1608
1616
|
"--badge": "badge",
|
|
1609
1617
|
"--probe": "probe",
|
|
1610
1618
|
"--clear": "clear",
|
|
1619
|
+
"--refresh": "refresh",
|
|
1620
|
+
"--quiet": "quiet",
|
|
1611
1621
|
"--copy": "copy",
|
|
1612
1622
|
"--all": "all",
|
|
1613
1623
|
"--models": "models",
|
|
@@ -1728,9 +1738,9 @@ function effortFlag(p) {
|
|
|
1728
1738
|
import readline from "node:readline";
|
|
1729
1739
|
|
|
1730
1740
|
// src/core/run.ts
|
|
1731
|
-
import { spawn as
|
|
1732
|
-
import
|
|
1733
|
-
import
|
|
1741
|
+
import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
|
|
1742
|
+
import fs27 from "node:fs";
|
|
1743
|
+
import path23 from "node:path";
|
|
1734
1744
|
|
|
1735
1745
|
// src/vcs/git.ts
|
|
1736
1746
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -1944,7 +1954,9 @@ import fs12 from "node:fs";
|
|
|
1944
1954
|
// src/vcs/guard.ts
|
|
1945
1955
|
import fs11 from "node:fs";
|
|
1946
1956
|
import path9 from "node:path";
|
|
1947
|
-
var VERSION =
|
|
1957
|
+
var VERSION = 2;
|
|
1958
|
+
var WINDOWS = process.platform === "win32";
|
|
1959
|
+
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
1948
1960
|
var SHIM = `#!/bin/sh
|
|
1949
1961
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
1950
1962
|
# index or discard work. The primary agent reviews and commits, not the worker.
|
|
@@ -2021,9 +2033,10 @@ function shimDir() {
|
|
|
2021
2033
|
return path9.join(home(), "shim", `v${VERSION}`);
|
|
2022
2034
|
}
|
|
2023
2035
|
function findRealGit(skip) {
|
|
2036
|
+
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2024
2037
|
for (const dir of (process.env.PATH ?? "").split(path9.delimiter).filter(Boolean)) {
|
|
2025
|
-
if (path9.resolve(dir)
|
|
2026
|
-
const p = path9.join(dir, "git");
|
|
2038
|
+
if (same2(path9.resolve(dir), skip)) continue;
|
|
2039
|
+
const p = path9.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2027
2040
|
try {
|
|
2028
2041
|
fs11.accessSync(p, fs11.constants.X_OK);
|
|
2029
2042
|
if (fs11.statSync(p).isFile()) return p;
|
|
@@ -2032,6 +2045,24 @@ function findRealGit(skip) {
|
|
|
2032
2045
|
}
|
|
2033
2046
|
return void 0;
|
|
2034
2047
|
}
|
|
2048
|
+
function findGitSh(realGit) {
|
|
2049
|
+
let dir = path9.dirname(realGit);
|
|
2050
|
+
for (let i = 0; i < 4; i++) {
|
|
2051
|
+
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2052
|
+
const p = path9.join(dir, rel);
|
|
2053
|
+
if (fs11.existsSync(p)) return p;
|
|
2054
|
+
}
|
|
2055
|
+
const up = path9.dirname(dir);
|
|
2056
|
+
if (up === dir) break;
|
|
2057
|
+
dir = up;
|
|
2058
|
+
}
|
|
2059
|
+
return void 0;
|
|
2060
|
+
}
|
|
2061
|
+
var cmdShim = (sh) => `@echo off\r
|
|
2062
|
+
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
2063
|
+
"${sh}" "%~dp0git" %*\r
|
|
2064
|
+
exit /b %errorlevel%\r
|
|
2065
|
+
`;
|
|
2035
2066
|
var HOOK_VERSION = 1;
|
|
2036
2067
|
var REF_HOOK = `#!/bin/sh
|
|
2037
2068
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -2045,7 +2076,17 @@ function writeIfChanged(file2, content) {
|
|
|
2045
2076
|
fs11.mkdirSync(path9.dirname(file2), { recursive: true });
|
|
2046
2077
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2047
2078
|
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
2048
|
-
|
|
2079
|
+
renameOver(tmp, file2);
|
|
2080
|
+
}
|
|
2081
|
+
function pathKey(env) {
|
|
2082
|
+
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
2083
|
+
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
2084
|
+
}
|
|
2085
|
+
function onePath(env) {
|
|
2086
|
+
const key = pathKey(env);
|
|
2087
|
+
const out = { ...env };
|
|
2088
|
+
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
2089
|
+
return out;
|
|
2049
2090
|
}
|
|
2050
2091
|
function withGitConfig(env, entries) {
|
|
2051
2092
|
const out = { ...env };
|
|
@@ -2066,17 +2107,27 @@ function guardEnv(env) {
|
|
|
2066
2107
|
GIT_PAGER: "cat",
|
|
2067
2108
|
PAGER: "cat"
|
|
2068
2109
|
};
|
|
2069
|
-
if (process.platform === "win32") return { ...env, ...quiet };
|
|
2070
2110
|
writeIfChanged(path9.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
2071
|
-
env = withGitConfig(env, [
|
|
2072
|
-
["core.hooksPath", hooksDir()],
|
|
2111
|
+
env = withGitConfig(onePath(env), [
|
|
2112
|
+
["core.hooksPath", fwd(hooksDir())],
|
|
2073
2113
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
2074
2114
|
]);
|
|
2075
2115
|
const dir = shimDir();
|
|
2076
2116
|
const real = findRealGit(dir);
|
|
2077
2117
|
if (!real) return { ...env, ...quiet };
|
|
2118
|
+
let sh;
|
|
2119
|
+
if (WINDOWS) {
|
|
2120
|
+
sh = findGitSh(real);
|
|
2121
|
+
if (!sh) return { ...env, ...quiet };
|
|
2122
|
+
writeIfChanged(path9.join(dir, "git.cmd"), cmdShim(sh));
|
|
2123
|
+
}
|
|
2078
2124
|
writeIfChanged(path9.join(dir, "git"), SHIM);
|
|
2079
|
-
|
|
2125
|
+
const key = pathKey(env);
|
|
2126
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path9.delimiter}${env[key] ?? ""}` };
|
|
2127
|
+
}
|
|
2128
|
+
function shimReady() {
|
|
2129
|
+
const env = guardEnv({ ...process.env });
|
|
2130
|
+
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
2080
2131
|
}
|
|
2081
2132
|
|
|
2082
2133
|
// src/core/process.ts
|
|
@@ -2482,20 +2533,157 @@ function auditBadge(meta) {
|
|
|
2482
2533
|
}
|
|
2483
2534
|
|
|
2484
2535
|
// src/core/notify.ts
|
|
2485
|
-
import { spawn as
|
|
2536
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2486
2537
|
import crypto5 from "node:crypto";
|
|
2487
|
-
import
|
|
2488
|
-
import
|
|
2538
|
+
import fs22 from "node:fs";
|
|
2539
|
+
import path18 from "node:path";
|
|
2489
2540
|
|
|
2490
2541
|
// src/core/history.ts
|
|
2491
|
-
import
|
|
2542
|
+
import fs19 from "node:fs";
|
|
2492
2543
|
import { createRequire } from "node:module";
|
|
2493
|
-
import
|
|
2544
|
+
import path16 from "node:path";
|
|
2494
2545
|
import zlib from "node:zlib";
|
|
2495
2546
|
|
|
2496
2547
|
// src/core/receipt.ts
|
|
2548
|
+
import fs17 from "node:fs";
|
|
2549
|
+
import path15 from "node:path";
|
|
2550
|
+
|
|
2551
|
+
// src/core/prices.ts
|
|
2552
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2497
2553
|
import fs16 from "node:fs";
|
|
2498
2554
|
import path14 from "node:path";
|
|
2555
|
+
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2556
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2557
|
+
var MIN_MODELS = 100;
|
|
2558
|
+
function catalogLabel() {
|
|
2559
|
+
const url = effective().priceFeedUrl.value;
|
|
2560
|
+
if (url === DEFAULT_URL) return "models.dev";
|
|
2561
|
+
try {
|
|
2562
|
+
return new URL(url).host;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return url;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
var catalogFile = () => path14.join(home(), "prices.json");
|
|
2568
|
+
var attemptFile = () => path14.join(home(), "prices.attempt");
|
|
2569
|
+
function trim(raw) {
|
|
2570
|
+
const out = {};
|
|
2571
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2572
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2573
|
+
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2574
|
+
const c = m?.cost;
|
|
2575
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2576
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2577
|
+
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
return out;
|
|
2581
|
+
}
|
|
2582
|
+
var cached2;
|
|
2583
|
+
function readCatalog() {
|
|
2584
|
+
const file2 = catalogFile();
|
|
2585
|
+
try {
|
|
2586
|
+
const { mtimeMs } = fs16.statSync(file2);
|
|
2587
|
+
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2588
|
+
const parsed = JSON.parse(fs16.readFileSync(file2, "utf8"));
|
|
2589
|
+
const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2590
|
+
cached2 = { file: file2, mtimeMs, catalog: catalog5 };
|
|
2591
|
+
return catalog5;
|
|
2592
|
+
} catch {
|
|
2593
|
+
return void 0;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
var ageMs = (file2) => {
|
|
2597
|
+
try {
|
|
2598
|
+
return Date.now() - fs16.statSync(file2).mtimeMs;
|
|
2599
|
+
} catch {
|
|
2600
|
+
return Infinity;
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
function refreshDue() {
|
|
2604
|
+
const eff = effective();
|
|
2605
|
+
if (!eff.priceFeed.value) return false;
|
|
2606
|
+
if (ageMs(catalogFile()) < eff.priceFeedHours.value * 36e5) return false;
|
|
2607
|
+
return ageMs(attemptFile()) > RETRY_AFTER_FAILURE_MS;
|
|
2608
|
+
}
|
|
2609
|
+
async function refreshCatalog() {
|
|
2610
|
+
const url = effective().priceFeedUrl.value;
|
|
2611
|
+
try {
|
|
2612
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(3e4), headers: { accept: "application/json" } });
|
|
2613
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
2614
|
+
const models = trim(await r.json());
|
|
2615
|
+
const n = Object.keys(models).length;
|
|
2616
|
+
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
2617
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2618
|
+
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
2619
|
+
const catalog5 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
2620
|
+
fs16.writeFileSync(tmp, JSON.stringify(catalog5));
|
|
2621
|
+
renameOver(tmp, catalogFile());
|
|
2622
|
+
fs16.rmSync(attemptFile(), { force: true });
|
|
2623
|
+
return { state: "updated", models: n };
|
|
2624
|
+
} catch (e) {
|
|
2625
|
+
try {
|
|
2626
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2627
|
+
fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
2628
|
+
`);
|
|
2629
|
+
} catch {
|
|
2630
|
+
}
|
|
2631
|
+
return { state: "failed", error: e.message };
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function refreshInBackground() {
|
|
2635
|
+
try {
|
|
2636
|
+
if (!refreshDue()) return;
|
|
2637
|
+
const script = process.argv[1];
|
|
2638
|
+
if (!script) return;
|
|
2639
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2640
|
+
fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
2641
|
+
`);
|
|
2642
|
+
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
2643
|
+
child.on("error", () => void 0);
|
|
2644
|
+
child.unref();
|
|
2645
|
+
} catch {
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2649
|
+
function catalogPrice(backend, ...models) {
|
|
2650
|
+
const catalog5 = readCatalog();
|
|
2651
|
+
if (!catalog5) return void 0;
|
|
2652
|
+
for (const raw of models) {
|
|
2653
|
+
const id = raw?.split("#")[0];
|
|
2654
|
+
if (!id) continue;
|
|
2655
|
+
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2656
|
+
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2657
|
+
const triple = any ? catalog5.models[any] : void 0;
|
|
2658
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2659
|
+
const [input, output, cachedInput] = triple;
|
|
2660
|
+
return { input, output, cachedInput, key: any };
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
function catalogStatus() {
|
|
2666
|
+
const eff = effective();
|
|
2667
|
+
const c = readCatalog();
|
|
2668
|
+
let lastFailure;
|
|
2669
|
+
try {
|
|
2670
|
+
const t = fs16.readFileSync(attemptFile(), "utf8").trim();
|
|
2671
|
+
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
enabled: eff.priceFeed.value,
|
|
2676
|
+
url: eff.priceFeedUrl.value,
|
|
2677
|
+
hours: eff.priceFeedHours.value,
|
|
2678
|
+
file: catalogFile(),
|
|
2679
|
+
models: c ? Object.keys(c.models).length : void 0,
|
|
2680
|
+
fetchedAt: c?.fetchedAt,
|
|
2681
|
+
ageHours: c ? (Date.now() - Date.parse(c.fetchedAt)) / 36e5 : void 0,
|
|
2682
|
+
lastFailure
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/core/receipt.ts
|
|
2499
2687
|
var PRESETS = {
|
|
2500
2688
|
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2501
2689
|
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
@@ -2508,13 +2696,31 @@ function primaryPrice() {
|
|
|
2508
2696
|
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2509
2697
|
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2510
2698
|
}
|
|
2511
|
-
|
|
2699
|
+
const named = eff.primary.value;
|
|
2700
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2701
|
+
if (preset) return preset;
|
|
2702
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2703
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2512
2704
|
}
|
|
2513
2705
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2706
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2707
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2708
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2709
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2710
|
+
}
|
|
2711
|
+
function workerPrice(backend, ...models) {
|
|
2712
|
+
const prices = effective().workerPrices.value;
|
|
2713
|
+
for (const key of [...models.filter((m) => !!m).map((m) => `${backend}:${m.split("#")[0]}`), backend]) {
|
|
2714
|
+
if (prices[key] !== void 0) return parsePriceSpec(prices[key], key);
|
|
2715
|
+
}
|
|
2716
|
+
const c = effective().priceFeed.value ? catalogPrice(backend, ...models) : void 0;
|
|
2717
|
+
return c ? { name: c.key, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : void 0;
|
|
2718
|
+
}
|
|
2719
|
+
var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
2720
|
+
var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
|
|
2514
2721
|
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
2515
|
-
const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
2516
2722
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2517
|
-
return Math.max(0,
|
|
2723
|
+
return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
|
|
2518
2724
|
}
|
|
2519
2725
|
function record(meta) {
|
|
2520
2726
|
if (!meta.usage?.steps || meta.auditOf) return;
|
|
@@ -2527,17 +2733,18 @@ function record(meta) {
|
|
|
2527
2733
|
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
2528
2734
|
tokens: meta.usage.total,
|
|
2529
2735
|
returned: meta.returnedTokens ?? 0,
|
|
2530
|
-
workerCost: meta.usage
|
|
2736
|
+
workerCost: workerCostOf(meta.usage) ?? 0,
|
|
2737
|
+
...meta.usage.cost === void 0 && meta.usage.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
|
|
2531
2738
|
saved: meta.savedUsd ?? 0,
|
|
2532
2739
|
price: primaryPrice().name
|
|
2533
2740
|
};
|
|
2534
|
-
|
|
2535
|
-
|
|
2741
|
+
fs17.mkdirSync(path15.dirname(ledgerFile()), { recursive: true });
|
|
2742
|
+
fs17.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2536
2743
|
`);
|
|
2537
2744
|
}
|
|
2538
2745
|
function readLedger(sinceMs2) {
|
|
2539
|
-
if (!
|
|
2540
|
-
return
|
|
2746
|
+
if (!fs17.existsSync(ledgerFile())) return [];
|
|
2747
|
+
return fs17.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2541
2748
|
try {
|
|
2542
2749
|
return [JSON.parse(l)];
|
|
2543
2750
|
} catch {
|
|
@@ -2612,7 +2819,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2612
2819
|
}
|
|
2613
2820
|
|
|
2614
2821
|
// src/core/ui.ts
|
|
2615
|
-
import
|
|
2822
|
+
import fs18 from "node:fs";
|
|
2616
2823
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2617
2824
|
var RECENT = 40;
|
|
2618
2825
|
var RECENT_HOURS = 1;
|
|
@@ -2703,9 +2910,9 @@ function cardsFor(m) {
|
|
|
2703
2910
|
const out = [];
|
|
2704
2911
|
for (const [phase, text] of phases) {
|
|
2705
2912
|
const marker = runFile(m.id, `card-${phase}`);
|
|
2706
|
-
if (
|
|
2913
|
+
if (fs18.existsSync(marker)) continue;
|
|
2707
2914
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
2708
|
-
|
|
2915
|
+
fs18.writeFileSync(marker, "");
|
|
2709
2916
|
}
|
|
2710
2917
|
return out;
|
|
2711
2918
|
}
|
|
@@ -2716,10 +2923,10 @@ function hookCards(input) {
|
|
|
2716
2923
|
const response = event.tool_response;
|
|
2717
2924
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
2718
2925
|
const ids = [...new Set(`${command}
|
|
2719
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
2926
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs18.existsSync(`${runsDir()}/${id}`));
|
|
2720
2927
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
2721
2928
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
2722
|
-
if (ids.includes(id) ||
|
|
2929
|
+
if (ids.includes(id) || fs18.existsSync(runFile(id, "card-ended"))) continue;
|
|
2723
2930
|
try {
|
|
2724
2931
|
const m = readMeta(id);
|
|
2725
2932
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -2740,11 +2947,11 @@ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs17.existsSync(`${runsDir()}/${
|
|
|
2740
2947
|
var SCHEMA_VERSION = 1;
|
|
2741
2948
|
var PATCH_MAX = 1e6;
|
|
2742
2949
|
var ANSWER_MAX2 = 2e5;
|
|
2743
|
-
var
|
|
2744
|
-
var historyFile = () =>
|
|
2950
|
+
var cached3;
|
|
2951
|
+
var historyFile = () => path16.join(home(), "history.db");
|
|
2745
2952
|
function openDb() {
|
|
2746
2953
|
const file2 = historyFile();
|
|
2747
|
-
if (
|
|
2954
|
+
if (cached3?.file === file2) return cached3.db;
|
|
2748
2955
|
let db;
|
|
2749
2956
|
try {
|
|
2750
2957
|
const emit = process.emitWarning;
|
|
@@ -2755,14 +2962,14 @@ function openDb() {
|
|
|
2755
2962
|
} finally {
|
|
2756
2963
|
process.emitWarning = emit;
|
|
2757
2964
|
}
|
|
2758
|
-
|
|
2965
|
+
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
2759
2966
|
db = new DatabaseSync(file2);
|
|
2760
2967
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2761
2968
|
migrate(db);
|
|
2762
2969
|
} catch {
|
|
2763
2970
|
db = void 0;
|
|
2764
2971
|
}
|
|
2765
|
-
|
|
2972
|
+
cached3 = { file: file2, db };
|
|
2766
2973
|
return db;
|
|
2767
2974
|
}
|
|
2768
2975
|
function migrate(db) {
|
|
@@ -2790,31 +2997,31 @@ function migrate(db) {
|
|
|
2790
2997
|
function readRunFile(id, name) {
|
|
2791
2998
|
const f = runFile(id, name);
|
|
2792
2999
|
try {
|
|
2793
|
-
if (
|
|
2794
|
-
if (
|
|
3000
|
+
if (fs19.existsSync(f)) return fs19.readFileSync(f, "utf8");
|
|
3001
|
+
if (fs19.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs19.readFileSync(`${f}.gz`)).toString("utf8");
|
|
2795
3002
|
} catch {
|
|
2796
3003
|
}
|
|
2797
3004
|
return void 0;
|
|
2798
3005
|
}
|
|
2799
3006
|
function gzipFile(f) {
|
|
2800
|
-
if (!
|
|
2801
|
-
|
|
2802
|
-
|
|
3007
|
+
if (!fs19.existsSync(f)) return;
|
|
3008
|
+
fs19.writeFileSync(`${f}.gz`, zlib.gzipSync(fs19.readFileSync(f)));
|
|
3009
|
+
fs19.rmSync(f);
|
|
2803
3010
|
}
|
|
2804
3011
|
function compactRun(meta) {
|
|
2805
3012
|
if (!TERMINAL.includes(meta.state)) return;
|
|
2806
|
-
const dir =
|
|
3013
|
+
const dir = path16.dirname(runFile(meta.id, "meta.json"));
|
|
2807
3014
|
let names;
|
|
2808
3015
|
try {
|
|
2809
|
-
names =
|
|
3016
|
+
names = fs19.readdirSync(dir);
|
|
2810
3017
|
} catch {
|
|
2811
3018
|
return;
|
|
2812
3019
|
}
|
|
2813
3020
|
for (const n of names) {
|
|
2814
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
3021
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path16.join(dir, n));
|
|
2815
3022
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
2816
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
2817
|
-
else gzipFile(
|
|
3023
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs19.rmSync(path16.join(dir, n), { force: true });
|
|
3024
|
+
else gzipFile(path16.join(dir, n));
|
|
2818
3025
|
}
|
|
2819
3026
|
}
|
|
2820
3027
|
}
|
|
@@ -2858,7 +3065,7 @@ function recordRun(meta) {
|
|
|
2858
3065
|
tool_calls: u?.toolCalls ?? null,
|
|
2859
3066
|
tokens: u?.total ?? null,
|
|
2860
3067
|
returned_tokens: meta.returnedTokens ?? null,
|
|
2861
|
-
cost: u?.cost ?? null,
|
|
3068
|
+
cost: u?.cost ?? u?.costEstimate ?? null,
|
|
2862
3069
|
saved: meta.savedUsd ?? null,
|
|
2863
3070
|
files_changed: meta.changes?.length ?? 0,
|
|
2864
3071
|
applied: meta.applied ? 1 : 0,
|
|
@@ -3071,11 +3278,11 @@ function historyStats(sinceMs2) {
|
|
|
3071
3278
|
}
|
|
3072
3279
|
|
|
3073
3280
|
// src/core/report.ts
|
|
3074
|
-
import
|
|
3281
|
+
import fs21 from "node:fs";
|
|
3075
3282
|
|
|
3076
3283
|
// src/core/plan.ts
|
|
3077
|
-
import
|
|
3078
|
-
import
|
|
3284
|
+
import fs20 from "node:fs";
|
|
3285
|
+
import path17 from "node:path";
|
|
3079
3286
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
3080
3287
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
3081
3288
|
function structure(lines) {
|
|
@@ -3122,9 +3329,9 @@ function parsePlan(text, file2 = "") {
|
|
|
3122
3329
|
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
3123
3330
|
}
|
|
3124
3331
|
function loadPlan(file2) {
|
|
3125
|
-
const abs =
|
|
3126
|
-
if (!
|
|
3127
|
-
const plan = parsePlan(
|
|
3332
|
+
const abs = path17.resolve(file2);
|
|
3333
|
+
if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
3334
|
+
const plan = parsePlan(fs20.readFileSync(abs, "utf8"), fs20.realpathSync(abs));
|
|
3128
3335
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
3129
3336
|
return plan;
|
|
3130
3337
|
}
|
|
@@ -3143,7 +3350,7 @@ function brief(plan, task) {
|
|
|
3143
3350
|
].filter(Boolean).join("\n\n")}
|
|
3144
3351
|
`;
|
|
3145
3352
|
}
|
|
3146
|
-
var planName = (file2) =>
|
|
3353
|
+
var planName = (file2) => path17.basename(file2).replace(/\.md$/i, "");
|
|
3147
3354
|
|
|
3148
3355
|
// src/core/style.ts
|
|
3149
3356
|
var enabled = () => {
|
|
@@ -3203,7 +3410,7 @@ function duration(meta) {
|
|
|
3203
3410
|
}
|
|
3204
3411
|
function readSummary(meta) {
|
|
3205
3412
|
const f = runFile(meta.id, "summary.md");
|
|
3206
|
-
return
|
|
3413
|
+
return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3207
3414
|
}
|
|
3208
3415
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3209
3416
|
const out = [];
|
|
@@ -3246,7 +3453,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3246
3453
|
if (u && u.steps) {
|
|
3247
3454
|
const ratio = meta.returnedTokens ? `, ${Math.max(1, Math.round(u.total / meta.returnedTokens))}\xD7 compression` : "";
|
|
3248
3455
|
out.push(
|
|
3249
|
-
`\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost
|
|
3456
|
+
`\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost !== void 0 ? usd(u.cost) : u.costEstimate !== void 0 ? `~${usd(u.costEstimate)} (estimated from ${u.costSource === "catalog" ? `${catalogLabel()} prices` : "your workerPrices"})` : "n/a"} \xB7 returned ~${compact(meta.returnedTokens ?? 0)} tokens${ratio}` + (meta.savedUsd !== void 0 ? ` \xB7 est. saved ${usd(meta.savedUsd)} vs ${primaryPrice().name}` : "")
|
|
3250
3457
|
);
|
|
3251
3458
|
}
|
|
3252
3459
|
if (meta.changes) {
|
|
@@ -3289,8 +3496,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3289
3496
|
}
|
|
3290
3497
|
function live(meta) {
|
|
3291
3498
|
const f = runFile(meta.id, "events.jsonl");
|
|
3292
|
-
if (!
|
|
3293
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
3499
|
+
if (!fs21.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
3500
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs21.readFileSync(f, "utf8"));
|
|
3294
3501
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3295
3502
|
}
|
|
3296
3503
|
function progress(meta) {
|
|
@@ -3450,12 +3657,12 @@ function claimGroup(group) {
|
|
|
3450
3657
|
try {
|
|
3451
3658
|
const ids = groupIds(group).sort();
|
|
3452
3659
|
if (ids.length < 2) return true;
|
|
3453
|
-
const dir =
|
|
3454
|
-
|
|
3455
|
-
for (const f of
|
|
3456
|
-
if (Date.now() -
|
|
3660
|
+
const dir = path18.join(home(), "notified");
|
|
3661
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
3662
|
+
for (const f of fs22.readdirSync(dir)) {
|
|
3663
|
+
if (Date.now() - fs22.statSync(path18.join(dir, f)).mtimeMs > 30 * 864e5) fs22.rmSync(path18.join(dir, f), { force: true });
|
|
3457
3664
|
}
|
|
3458
|
-
|
|
3665
|
+
fs22.writeFileSync(path18.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
3459
3666
|
return true;
|
|
3460
3667
|
} catch {
|
|
3461
3668
|
return false;
|
|
@@ -3467,12 +3674,12 @@ function send(notice, meta) {
|
|
|
3467
3674
|
try {
|
|
3468
3675
|
let child;
|
|
3469
3676
|
if (command) {
|
|
3470
|
-
child =
|
|
3677
|
+
child = spawn3(command, { shell: true, env, detached: true, stdio: "ignore" });
|
|
3471
3678
|
} else if (process.platform === "darwin") {
|
|
3472
|
-
child =
|
|
3679
|
+
child = spawn3("osascript", ["-e", "on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run", notice.title, notice.body], { env, detached: true, stdio: "ignore" });
|
|
3473
3680
|
} else if (process.platform === "linux") {
|
|
3474
3681
|
const plain2 = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3475
|
-
child =
|
|
3682
|
+
child = spawn3("notify-send", ["--", plain2(notice.title), plain2(notice.body)], { env, detached: true, stdio: "ignore" });
|
|
3476
3683
|
} else {
|
|
3477
3684
|
return;
|
|
3478
3685
|
}
|
|
@@ -3489,30 +3696,30 @@ function notifyEnded(meta) {
|
|
|
3489
3696
|
}
|
|
3490
3697
|
|
|
3491
3698
|
// src/core/templates.ts
|
|
3492
|
-
import
|
|
3493
|
-
import
|
|
3699
|
+
import fs24 from "node:fs";
|
|
3700
|
+
import path20 from "node:path";
|
|
3494
3701
|
|
|
3495
3702
|
// src/core/install.ts
|
|
3496
|
-
import
|
|
3703
|
+
import fs23 from "node:fs";
|
|
3497
3704
|
import os9 from "node:os";
|
|
3498
|
-
import
|
|
3705
|
+
import path19 from "node:path";
|
|
3499
3706
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3500
3707
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
3501
3708
|
function packageRoot() {
|
|
3502
|
-
return
|
|
3709
|
+
return path19.resolve(path19.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
3503
3710
|
}
|
|
3504
3711
|
function skillNames(root = packageRoot()) {
|
|
3505
|
-
const dir =
|
|
3506
|
-
if (!
|
|
3507
|
-
return
|
|
3712
|
+
const dir = path19.join(root, "skills");
|
|
3713
|
+
if (!fs23.existsSync(dir)) return [];
|
|
3714
|
+
return fs23.readdirSync(dir).filter((d) => fs23.existsSync(path19.join(dir, d, "SKILL.md"))).sort();
|
|
3508
3715
|
}
|
|
3509
3716
|
function skillTargets() {
|
|
3510
3717
|
const home3 = os9.homedir();
|
|
3511
|
-
const targets = [
|
|
3512
|
-
if (
|
|
3718
|
+
const targets = [path19.join(home3, ".agents", "skills")];
|
|
3719
|
+
if (fs23.existsSync(path19.join(home3, ".claude"))) targets.push(path19.join(home3, ".claude", "skills"));
|
|
3513
3720
|
return targets;
|
|
3514
3721
|
}
|
|
3515
|
-
var launcherPath = () =>
|
|
3722
|
+
var launcherPath = () => path19.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
3516
3723
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
3517
3724
|
function launcherScript(bundle) {
|
|
3518
3725
|
if (process.platform === "win32") {
|
|
@@ -3542,7 +3749,7 @@ exit 127
|
|
|
3542
3749
|
function isOurLauncher(file2, root) {
|
|
3543
3750
|
if (linksInto(file2, root)) return true;
|
|
3544
3751
|
try {
|
|
3545
|
-
const s =
|
|
3752
|
+
const s = fs23.readFileSync(file2, "utf8");
|
|
3546
3753
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
3547
3754
|
} catch {
|
|
3548
3755
|
return false;
|
|
@@ -3550,64 +3757,64 @@ function isOurLauncher(file2, root) {
|
|
|
3550
3757
|
}
|
|
3551
3758
|
function isPitroomLauncher(file2) {
|
|
3552
3759
|
try {
|
|
3553
|
-
return
|
|
3760
|
+
return fs23.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
3554
3761
|
} catch {
|
|
3555
3762
|
return false;
|
|
3556
3763
|
}
|
|
3557
3764
|
}
|
|
3558
3765
|
function placeLauncher(bundle, root, force) {
|
|
3559
3766
|
const dest = launcherPath();
|
|
3560
|
-
|
|
3561
|
-
const exists =
|
|
3767
|
+
fs23.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
3768
|
+
const exists = fs23.lstatSync(dest, { throwIfNoEntry: false });
|
|
3562
3769
|
if (exists && !isOurLauncher(dest, root)) {
|
|
3563
3770
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
3564
|
-
|
|
3771
|
+
fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3565
3772
|
} else if (exists) {
|
|
3566
|
-
|
|
3773
|
+
fs23.rmSync(dest, { force: true });
|
|
3567
3774
|
}
|
|
3568
|
-
|
|
3775
|
+
fs23.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
3569
3776
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
3570
3777
|
}
|
|
3571
3778
|
function linksInto(link, dir) {
|
|
3572
|
-
const st =
|
|
3779
|
+
const st = fs23.lstatSync(link, { throwIfNoEntry: false });
|
|
3573
3780
|
if (!st?.isSymbolicLink()) return false;
|
|
3574
|
-
const target =
|
|
3575
|
-
const rel =
|
|
3576
|
-
return !rel.startsWith("..") && !
|
|
3781
|
+
const target = path19.resolve(path19.dirname(link), fs23.readlinkSync(link));
|
|
3782
|
+
const rel = path19.relative(dir, target);
|
|
3783
|
+
return !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
3577
3784
|
}
|
|
3578
3785
|
function place(src, dest, opts) {
|
|
3579
|
-
|
|
3580
|
-
const st =
|
|
3786
|
+
fs23.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
3787
|
+
const st = fs23.lstatSync(dest, { throwIfNoEntry: false });
|
|
3581
3788
|
if (st && !st.isSymbolicLink()) {
|
|
3582
3789
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
3583
|
-
|
|
3790
|
+
fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3584
3791
|
} else if (st) {
|
|
3585
|
-
|
|
3792
|
+
fs23.unlinkSync(dest);
|
|
3586
3793
|
}
|
|
3587
|
-
if (opts.copy)
|
|
3588
|
-
else
|
|
3794
|
+
if (opts.copy) fs23.cpSync(src, dest, { recursive: true });
|
|
3795
|
+
else fs23.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs23.statSync(src).isDirectory() ? "dir" : "file");
|
|
3589
3796
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
3590
3797
|
}
|
|
3591
3798
|
function install(opts) {
|
|
3592
3799
|
const root = packageRoot();
|
|
3593
3800
|
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
3594
|
-
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${
|
|
3801
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path19.join(root, "skills")}`);
|
|
3595
3802
|
const out = [];
|
|
3596
3803
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
3597
3804
|
for (const legacy of LEGACY) {
|
|
3598
|
-
const l =
|
|
3805
|
+
const l = path19.join(base2, legacy);
|
|
3599
3806
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
3600
|
-
|
|
3807
|
+
fs23.unlinkSync(l);
|
|
3601
3808
|
out.push(`\u2714 removed old link ${l}`);
|
|
3602
3809
|
}
|
|
3603
3810
|
}
|
|
3604
|
-
for (const name of skills2) out.push(place(
|
|
3811
|
+
for (const name of skills2) out.push(place(path19.join(root, "skills", name), path19.join(base2, name), opts));
|
|
3605
3812
|
}
|
|
3606
|
-
const bundle =
|
|
3607
|
-
if (
|
|
3813
|
+
const bundle = path19.join(root, "dist", "pitroom.mjs");
|
|
3814
|
+
if (fs23.existsSync(bundle)) {
|
|
3608
3815
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
3609
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
3610
|
-
if (!onPath) out.push(`! ${
|
|
3816
|
+
const onPath = (process.env.PATH ?? "").split(path19.delimiter).some((d) => path19.resolve(d) === path19.dirname(launcherPath()));
|
|
3817
|
+
if (!onPath) out.push(`! ${path19.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
3611
3818
|
}
|
|
3612
3819
|
return out;
|
|
3613
3820
|
}
|
|
@@ -3615,24 +3822,24 @@ function uninstall() {
|
|
|
3615
3822
|
const root = packageRoot();
|
|
3616
3823
|
const out = [];
|
|
3617
3824
|
for (const base2 of skillTargets()) {
|
|
3618
|
-
if (!
|
|
3619
|
-
for (const name of
|
|
3620
|
-
const l =
|
|
3825
|
+
if (!fs23.existsSync(base2)) continue;
|
|
3826
|
+
for (const name of fs23.readdirSync(base2)) {
|
|
3827
|
+
const l = path19.join(base2, name);
|
|
3621
3828
|
if (linksInto(l, root)) {
|
|
3622
|
-
|
|
3829
|
+
fs23.unlinkSync(l);
|
|
3623
3830
|
out.push(`\u2714 removed ${l}`);
|
|
3624
3831
|
}
|
|
3625
3832
|
}
|
|
3626
3833
|
}
|
|
3627
3834
|
if (isOurLauncher(launcherPath(), root)) {
|
|
3628
|
-
|
|
3835
|
+
fs23.rmSync(launcherPath(), { force: true });
|
|
3629
3836
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
3630
3837
|
}
|
|
3631
3838
|
return out.length ? out : ["nothing to remove"];
|
|
3632
3839
|
}
|
|
3633
3840
|
function installedSkills() {
|
|
3634
3841
|
const names = skillNames();
|
|
3635
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
3842
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs23.existsSync(path19.join(base2, n, "SKILL.md"))) }));
|
|
3636
3843
|
}
|
|
3637
3844
|
|
|
3638
3845
|
// src/core/templates.ts
|
|
@@ -3643,9 +3850,9 @@ var FILES = {
|
|
|
3643
3850
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3644
3851
|
};
|
|
3645
3852
|
function loadTemplate(name, root = packageRoot()) {
|
|
3646
|
-
const file2 =
|
|
3647
|
-
if (!
|
|
3648
|
-
return
|
|
3853
|
+
const file2 = path20.join(root, "skills", FILES[name]);
|
|
3854
|
+
if (!fs24.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3855
|
+
return fs24.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3649
3856
|
}
|
|
3650
3857
|
function fill(template, values) {
|
|
3651
3858
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3655,10 +3862,10 @@ function fill(template, values) {
|
|
|
3655
3862
|
|
|
3656
3863
|
// src/core/slots.ts
|
|
3657
3864
|
import crypto6 from "node:crypto";
|
|
3658
|
-
import
|
|
3659
|
-
import
|
|
3660
|
-
var slotsDir = () =>
|
|
3661
|
-
var locksDir = () =>
|
|
3865
|
+
import fs25 from "node:fs";
|
|
3866
|
+
import path21 from "node:path";
|
|
3867
|
+
var slotsDir = () => path21.join(home(), "slots");
|
|
3868
|
+
var locksDir = () => path21.join(home(), "locks");
|
|
3662
3869
|
var STARTUP_GRACE_MS = 3e4;
|
|
3663
3870
|
function holderActive(runId, self) {
|
|
3664
3871
|
if (!runId || runId === self) return false;
|
|
@@ -3672,23 +3879,23 @@ function holderActive(runId, self) {
|
|
|
3672
3879
|
}
|
|
3673
3880
|
}
|
|
3674
3881
|
function tryClaim(file2, runId) {
|
|
3675
|
-
|
|
3882
|
+
fs25.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
3676
3883
|
try {
|
|
3677
|
-
|
|
3884
|
+
fs25.writeFileSync(file2, runId, { flag: "wx" });
|
|
3678
3885
|
return true;
|
|
3679
3886
|
} catch (e) {
|
|
3680
3887
|
if (e.code !== "EEXIST") throw e;
|
|
3681
3888
|
}
|
|
3682
3889
|
let owner = "";
|
|
3683
3890
|
try {
|
|
3684
|
-
owner =
|
|
3891
|
+
owner = fs25.readFileSync(file2, "utf8").trim();
|
|
3685
3892
|
} catch {
|
|
3686
3893
|
}
|
|
3687
3894
|
if (owner === runId) return true;
|
|
3688
3895
|
if (holderActive(owner, runId)) return false;
|
|
3689
|
-
|
|
3896
|
+
fs25.rmSync(file2, { force: true });
|
|
3690
3897
|
try {
|
|
3691
|
-
|
|
3898
|
+
fs25.writeFileSync(file2, runId, { flag: "wx" });
|
|
3692
3899
|
return true;
|
|
3693
3900
|
} catch {
|
|
3694
3901
|
return false;
|
|
@@ -3697,25 +3904,25 @@ function tryClaim(file2, runId) {
|
|
|
3697
3904
|
function releaseIfOwner(file2, runId) {
|
|
3698
3905
|
if (!file2) return;
|
|
3699
3906
|
try {
|
|
3700
|
-
if (
|
|
3907
|
+
if (fs25.readFileSync(file2, "utf8").trim() === runId) fs25.rmSync(file2, { force: true });
|
|
3701
3908
|
} catch {
|
|
3702
3909
|
}
|
|
3703
3910
|
}
|
|
3704
3911
|
function tryAcquireSlot(runId, maxParallel) {
|
|
3705
3912
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3706
|
-
const file2 =
|
|
3913
|
+
const file2 = path21.join(slotsDir(), `slot-${n}`);
|
|
3707
3914
|
if (tryClaim(file2, runId)) return file2;
|
|
3708
3915
|
}
|
|
3709
3916
|
return void 0;
|
|
3710
3917
|
}
|
|
3711
3918
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
3712
|
-
var lockFile = (repoRoot2) =>
|
|
3919
|
+
var lockFile = (repoRoot2) => path21.join(locksDir(), `write-${crypto6.createHash("sha1").update(path21.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3713
3920
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3714
3921
|
const file2 = lockFile(repoRoot2);
|
|
3715
3922
|
if (tryClaim(file2, runId)) return;
|
|
3716
3923
|
let owner = "";
|
|
3717
3924
|
try {
|
|
3718
|
-
owner =
|
|
3925
|
+
owner = fs25.readFileSync(file2, "utf8").trim();
|
|
3719
3926
|
} catch {
|
|
3720
3927
|
}
|
|
3721
3928
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -3762,8 +3969,8 @@ ${task}`;
|
|
|
3762
3969
|
}
|
|
3763
3970
|
|
|
3764
3971
|
// src/core/refs.ts
|
|
3765
|
-
import
|
|
3766
|
-
import
|
|
3972
|
+
import fs26 from "node:fs";
|
|
3973
|
+
import path22 from "node:path";
|
|
3767
3974
|
var FILE_EXTENSIONS = new Set(
|
|
3768
3975
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
3769
3976
|
);
|
|
@@ -3814,13 +4021,13 @@ function extractRefs(answer) {
|
|
|
3814
4021
|
return [...seen.values()];
|
|
3815
4022
|
}
|
|
3816
4023
|
function verifyRefs(refs, dirs) {
|
|
3817
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4024
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path22.resolve(d)))];
|
|
3818
4025
|
const invalid = [];
|
|
3819
4026
|
const lineCache = /* @__PURE__ */ new Map();
|
|
3820
4027
|
const load = (file2) => {
|
|
3821
4028
|
if (!lineCache.has(file2)) {
|
|
3822
|
-
const size =
|
|
3823
|
-
const lines = size > MAX_BYTES ? null :
|
|
4029
|
+
const size = fs26.statSync(file2).size;
|
|
4030
|
+
const lines = size > MAX_BYTES ? null : fs26.readFileSync(file2, "utf8").split("\n");
|
|
3824
4031
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
3825
4032
|
lineCache.set(file2, lines);
|
|
3826
4033
|
}
|
|
@@ -3831,9 +4038,9 @@ function verifyRefs(refs, dirs) {
|
|
|
3831
4038
|
const candidates = (ref) => {
|
|
3832
4039
|
const direct = resolve(ref.file, roots);
|
|
3833
4040
|
if (direct) return [direct];
|
|
3834
|
-
if (
|
|
4041
|
+
if (path22.isAbsolute(ref.file)) return [];
|
|
3835
4042
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3836
|
-
return (byName().get(
|
|
4043
|
+
return (byName().get(path22.basename(wanted)) ?? []).filter((f) => f.split(path22.sep).join("/").endsWith(`/${wanted}`) || path22.basename(f) === wanted).slice(0, 20);
|
|
3837
4044
|
};
|
|
3838
4045
|
const check = (file2, ref) => {
|
|
3839
4046
|
const lines = load(file2);
|
|
@@ -3848,7 +4055,7 @@ function verifyRefs(refs, dirs) {
|
|
|
3848
4055
|
if (!files.length) {
|
|
3849
4056
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3850
4057
|
total++;
|
|
3851
|
-
invalid.push({ ref: ref.text, reason:
|
|
4058
|
+
invalid.push({ ref: ref.text, reason: path22.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3852
4059
|
continue;
|
|
3853
4060
|
}
|
|
3854
4061
|
total++;
|
|
@@ -3869,7 +4076,7 @@ function enclosedBy(lines, ref) {
|
|
|
3869
4076
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3870
4077
|
}
|
|
3871
4078
|
var isFileName = (name) => {
|
|
3872
|
-
const base2 =
|
|
4079
|
+
const base2 = path22.basename(name);
|
|
3873
4080
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3874
4081
|
};
|
|
3875
4082
|
function indexFiles(roots) {
|
|
@@ -3878,19 +4085,19 @@ function indexFiles(roots) {
|
|
|
3878
4085
|
const walk = (dir) => {
|
|
3879
4086
|
let entries;
|
|
3880
4087
|
try {
|
|
3881
|
-
entries =
|
|
4088
|
+
entries = fs26.readdirSync(dir, { withFileTypes: true });
|
|
3882
4089
|
} catch {
|
|
3883
4090
|
return;
|
|
3884
4091
|
}
|
|
3885
4092
|
for (const e of entries) {
|
|
3886
4093
|
if (count >= MAX_INDEXED) return;
|
|
3887
4094
|
if (e.isDirectory()) {
|
|
3888
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
4095
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path22.join(dir, e.name));
|
|
3889
4096
|
} else if (e.isFile()) {
|
|
3890
4097
|
count++;
|
|
3891
4098
|
const list2 = index.get(e.name);
|
|
3892
|
-
if (list2) list2.push(
|
|
3893
|
-
else index.set(e.name, [
|
|
4099
|
+
if (list2) list2.push(path22.join(dir, e.name));
|
|
4100
|
+
else index.set(e.name, [path22.join(dir, e.name)]);
|
|
3894
4101
|
}
|
|
3895
4102
|
}
|
|
3896
4103
|
};
|
|
@@ -3898,10 +4105,10 @@ function indexFiles(roots) {
|
|
|
3898
4105
|
return index;
|
|
3899
4106
|
}
|
|
3900
4107
|
function resolve(ref, roots) {
|
|
3901
|
-
const candidates =
|
|
4108
|
+
const candidates = path22.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path22.join(r, ref));
|
|
3902
4109
|
return candidates.find((c) => {
|
|
3903
4110
|
try {
|
|
3904
|
-
return
|
|
4111
|
+
return fs26.statSync(c).isFile() && inside(c, roots);
|
|
3905
4112
|
} catch {
|
|
3906
4113
|
return false;
|
|
3907
4114
|
}
|
|
@@ -3909,14 +4116,14 @@ function resolve(ref, roots) {
|
|
|
3909
4116
|
}
|
|
3910
4117
|
function inside(file2, roots) {
|
|
3911
4118
|
return roots.some((r) => {
|
|
3912
|
-
const rel =
|
|
3913
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4119
|
+
const rel = path22.relative(r, path22.resolve(file2));
|
|
4120
|
+
return rel !== "" && !rel.startsWith("..") && !path22.isAbsolute(rel);
|
|
3914
4121
|
});
|
|
3915
4122
|
}
|
|
3916
4123
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3917
4124
|
|
|
3918
4125
|
// src/core/run.ts
|
|
3919
|
-
var VERSION2 = true ? "0.
|
|
4126
|
+
var VERSION2 = true ? "0.21.0" : "0.0.0-dev";
|
|
3920
4127
|
function planWork(o) {
|
|
3921
4128
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3922
4129
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3948,7 +4155,7 @@ function prepareRun(o) {
|
|
|
3948
4155
|
parent = readMeta(o.continueFrom);
|
|
3949
4156
|
if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
|
|
3950
4157
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3951
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4158
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
|
|
3952
4159
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3953
4160
|
}
|
|
3954
4161
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3967,18 +4174,18 @@ function prepareRun(o) {
|
|
|
3967
4174
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3968
4175
|
}
|
|
3969
4176
|
const mode = parent?.mode ?? o.mode;
|
|
3970
|
-
const dir = canonical(
|
|
3971
|
-
if (!
|
|
4177
|
+
const dir = canonical(path23.resolve(parent?.dir ?? o.dir));
|
|
4178
|
+
if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3972
4179
|
const root = repoRoot(dir);
|
|
3973
4180
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3974
4181
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3975
4182
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3976
4183
|
}
|
|
3977
|
-
const files = o.files.map((f) =>
|
|
3978
|
-
for (const f of files) if (!
|
|
4184
|
+
const files = o.files.map((f) => path23.resolve(f));
|
|
4185
|
+
for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3979
4186
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
3980
4187
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
3981
|
-
if (parent?.snapshot?.dir && !
|
|
4188
|
+
if (parent?.snapshot?.dir && !fs27.existsSync(parent.snapshot.dir)) {
|
|
3982
4189
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
3983
4190
|
}
|
|
3984
4191
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -4025,13 +4232,13 @@ function prepareRun(o) {
|
|
|
4025
4232
|
try {
|
|
4026
4233
|
acquireWriteLock(root, meta.id);
|
|
4027
4234
|
} catch (e) {
|
|
4028
|
-
|
|
4235
|
+
fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4029
4236
|
throw e;
|
|
4030
4237
|
}
|
|
4031
4238
|
}
|
|
4032
|
-
|
|
4239
|
+
fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4033
4240
|
`);
|
|
4034
|
-
if (work.brief)
|
|
4241
|
+
if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4035
4242
|
return meta;
|
|
4036
4243
|
}
|
|
4037
4244
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4061,7 +4268,7 @@ function startInBackground(meta) {
|
|
|
4061
4268
|
if (!script) throw new Error("cannot locate the pitroom executable");
|
|
4062
4269
|
meta.background = true;
|
|
4063
4270
|
writeMeta(meta);
|
|
4064
|
-
const child =
|
|
4271
|
+
const child = spawn4(process.execPath, [script, "__exec", meta.id], {
|
|
4065
4272
|
detached: true,
|
|
4066
4273
|
stdio: "ignore",
|
|
4067
4274
|
cwd: meta.dir,
|
|
@@ -4113,7 +4320,7 @@ async function execute(meta) {
|
|
|
4113
4320
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4114
4321
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4115
4322
|
const from = runFile(meta.id, f);
|
|
4116
|
-
if (
|
|
4323
|
+
if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4117
4324
|
}
|
|
4118
4325
|
writeMeta(meta);
|
|
4119
4326
|
}
|
|
@@ -4177,8 +4384,8 @@ function prepareTree(meta) {
|
|
|
4177
4384
|
const s = readSnapshot(root);
|
|
4178
4385
|
meta.snapshot.dir = s.dir;
|
|
4179
4386
|
meta.snapshot.tree = s.tree;
|
|
4180
|
-
meta.cwd =
|
|
4181
|
-
if (!
|
|
4387
|
+
meta.cwd = path23.join(s.dir, path23.relative(root, meta.dir));
|
|
4388
|
+
if (!fs27.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
4182
4389
|
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
4183
4390
|
} catch (e) {
|
|
4184
4391
|
meta.snapshot = void 0;
|
|
@@ -4188,9 +4395,9 @@ function prepareTree(meta) {
|
|
|
4188
4395
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4189
4396
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4190
4397
|
meta.baseTree = snapshotTree(root);
|
|
4191
|
-
meta.worktree =
|
|
4398
|
+
meta.worktree = path23.join(worktreesDir(), meta.id);
|
|
4192
4399
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4193
|
-
meta.cwd =
|
|
4400
|
+
meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
|
|
4194
4401
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4195
4402
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4196
4403
|
}
|
|
@@ -4213,7 +4420,7 @@ function attempt(meta, backend, target) {
|
|
|
4213
4420
|
timeoutSec: meta.timeoutSec
|
|
4214
4421
|
});
|
|
4215
4422
|
}
|
|
4216
|
-
var read2 = (f) =>
|
|
4423
|
+
var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
|
|
4217
4424
|
function retryableFailure(meta, backend, res) {
|
|
4218
4425
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4219
4426
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -4233,12 +4440,12 @@ function finalize(meta, res) {
|
|
|
4233
4440
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4234
4441
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
4235
4442
|
meta.usage = run2.usage;
|
|
4236
|
-
|
|
4443
|
+
fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
4237
4444
|
`);
|
|
4238
4445
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
4239
4446
|
if (meta.reviewOf) {
|
|
4240
4447
|
meta.verdict = parseVerdict(run2.finalText);
|
|
4241
|
-
if (meta.packageFile)
|
|
4448
|
+
if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
|
|
4242
4449
|
}
|
|
4243
4450
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4244
4451
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4273,6 +4480,13 @@ function finalize(meta, res) {
|
|
|
4273
4480
|
if (meta.state === "done" && !run2.finalText) meta.warnings.push("worker finished without a written answer");
|
|
4274
4481
|
if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
|
|
4275
4482
|
meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4483
|
+
if (meta.usage && meta.usage.cost === void 0) {
|
|
4484
|
+
const price = workerPrice(ran.backend, meta.resolvedModel ?? run2.model, ran.model);
|
|
4485
|
+
if (price) {
|
|
4486
|
+
meta.usage.costEstimate = costAt(meta.usage, price);
|
|
4487
|
+
meta.usage.costSource = price.source ?? "config";
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4276
4490
|
meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
|
|
4277
4491
|
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
4278
4492
|
writeMeta(meta);
|
|
@@ -4280,6 +4494,7 @@ function finalize(meta, res) {
|
|
|
4280
4494
|
record(meta);
|
|
4281
4495
|
if (meta.auditOf) settleAudit(meta);
|
|
4282
4496
|
notifyEnded(meta);
|
|
4497
|
+
refreshInBackground();
|
|
4283
4498
|
return meta;
|
|
4284
4499
|
}
|
|
4285
4500
|
function captureChanges(meta) {
|
|
@@ -4290,13 +4505,13 @@ function captureChanges(meta) {
|
|
|
4290
4505
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4291
4506
|
meta.changes = d.changes;
|
|
4292
4507
|
meta.stats = d.stats;
|
|
4293
|
-
|
|
4508
|
+
fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4294
4509
|
}
|
|
4295
4510
|
function verifyPath() {
|
|
4296
|
-
const extra = [
|
|
4297
|
-
const parts = (process.env.PATH ?? "").split(
|
|
4298
|
-
for (const d of process.platform === "win32" ? [
|
|
4299
|
-
return parts.join(
|
|
4511
|
+
const extra = [path23.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4512
|
+
const parts = (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean);
|
|
4513
|
+
for (const d of process.platform === "win32" ? [path23.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
4514
|
+
return parts.join(path23.delimiter);
|
|
4300
4515
|
}
|
|
4301
4516
|
function runVerify(meta) {
|
|
4302
4517
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4309,7 +4524,7 @@ function runVerify(meta) {
|
|
|
4309
4524
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4310
4525
|
});
|
|
4311
4526
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4312
|
-
|
|
4527
|
+
fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4313
4528
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4314
4529
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4315
4530
|
}
|
|
@@ -4364,15 +4579,15 @@ function discardRun(meta) {
|
|
|
4364
4579
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4365
4580
|
}
|
|
4366
4581
|
function cleanupWorktree(meta) {
|
|
4367
|
-
if (meta.worktree &&
|
|
4582
|
+
if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4368
4583
|
}
|
|
4369
4584
|
|
|
4370
4585
|
// src/cli/mcp-extras.ts
|
|
4371
|
-
import
|
|
4372
|
-
import
|
|
4586
|
+
import fs28 from "node:fs";
|
|
4587
|
+
import path24 from "node:path";
|
|
4373
4588
|
|
|
4374
4589
|
// src/cli/mcp-support.ts
|
|
4375
|
-
import { spawn as
|
|
4590
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
4376
4591
|
var MAX_OUTPUT = 12e4;
|
|
4377
4592
|
var DEFAULT_WAIT = 50;
|
|
4378
4593
|
var MAX_WAIT = 540;
|
|
@@ -4440,7 +4655,7 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4440
4655
|
return new Promise((resolve2) => {
|
|
4441
4656
|
const script = process.argv[1];
|
|
4442
4657
|
if (!script) return resolve2({ code: 1, out: "", err: "cannot locate the pitroom executable" });
|
|
4443
|
-
const child =
|
|
4658
|
+
const child = spawn5(process.execPath, [script, ...args], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
4444
4659
|
let out = "";
|
|
4445
4660
|
let err = "";
|
|
4446
4661
|
child.stdout.on("data", (d) => out += d);
|
|
@@ -4536,17 +4751,17 @@ async function startAndWait(start, seconds, ctx, task) {
|
|
|
4536
4751
|
const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
|
|
4537
4752
|
if (started.code !== 0) return asResult(started);
|
|
4538
4753
|
let id;
|
|
4539
|
-
let
|
|
4754
|
+
let cached4 = false;
|
|
4540
4755
|
try {
|
|
4541
|
-
({ id, cached:
|
|
4756
|
+
({ id, cached: cached4 = false } = JSON.parse(started.out));
|
|
4542
4757
|
} catch {
|
|
4543
4758
|
return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4544
4759
|
}
|
|
4545
|
-
const dash =
|
|
4760
|
+
const dash = cached4 ? Promise.resolve(void 0) : dashIfSlow(() => [id]);
|
|
4546
4761
|
const waited = await waitFor([id], seconds, ctx);
|
|
4547
4762
|
const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
|
|
4548
|
-
if (cancelled(ctx) && !
|
|
4549
|
-
if (
|
|
4763
|
+
if (cancelled(ctx) && !cached4) await stopQuietly(["stop", id]);
|
|
4764
|
+
if (cached4) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
|
|
4550
4765
|
|
|
4551
4766
|
${result.text}`;
|
|
4552
4767
|
return result;
|
|
@@ -4654,7 +4869,7 @@ function skillPrompts() {
|
|
|
4654
4869
|
const root = packageRoot();
|
|
4655
4870
|
for (const name of skillNames(root)) {
|
|
4656
4871
|
try {
|
|
4657
|
-
const text =
|
|
4872
|
+
const text = fs28.readFileSync(path24.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4658
4873
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4659
4874
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4660
4875
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -5172,22 +5387,22 @@ async function serveMcp() {
|
|
|
5172
5387
|
|
|
5173
5388
|
// src/cli/mcp-http.ts
|
|
5174
5389
|
import crypto8 from "node:crypto";
|
|
5175
|
-
import
|
|
5390
|
+
import fs30 from "node:fs";
|
|
5176
5391
|
import http2 from "node:http";
|
|
5177
|
-
import
|
|
5392
|
+
import path26 from "node:path";
|
|
5178
5393
|
|
|
5179
5394
|
// src/core/dash.ts
|
|
5180
|
-
import { spawn as
|
|
5395
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
5181
5396
|
import crypto7 from "node:crypto";
|
|
5182
|
-
import
|
|
5397
|
+
import fs29 from "node:fs";
|
|
5183
5398
|
import http from "node:http";
|
|
5184
|
-
import
|
|
5399
|
+
import path25 from "node:path";
|
|
5185
5400
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5186
5401
|
|
|
5187
5402
|
// src/core/file-diff.ts
|
|
5188
5403
|
var PREVIEW_LINES = 300;
|
|
5189
5404
|
function gitPath(value, prefix = true) {
|
|
5190
|
-
let
|
|
5405
|
+
let path34 = value;
|
|
5191
5406
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
5192
5407
|
const bytes = [];
|
|
5193
5408
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -5195,9 +5410,9 @@ function gitPath(value, prefix = true) {
|
|
|
5195
5410
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
5196
5411
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
5197
5412
|
}
|
|
5198
|
-
|
|
5413
|
+
path34 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
5199
5414
|
}
|
|
5200
|
-
return prefix ?
|
|
5415
|
+
return prefix ? path34.replace(/^[ab]\//, "") : path34;
|
|
5201
5416
|
}
|
|
5202
5417
|
function headerPath(header) {
|
|
5203
5418
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5422,7 +5637,8 @@ function runDetail(id) {
|
|
|
5422
5637
|
toolCalls: u?.toolCalls,
|
|
5423
5638
|
tokens: u?.total,
|
|
5424
5639
|
returnedTokens: m.returnedTokens,
|
|
5425
|
-
cost: u?.cost,
|
|
5640
|
+
cost: u?.cost ?? u?.costEstimate,
|
|
5641
|
+
costEstimated: u?.cost === void 0 && u?.costEstimate !== void 0 ? 1 : void 0,
|
|
5426
5642
|
saved: m.savedUsd,
|
|
5427
5643
|
group: m.group,
|
|
5428
5644
|
directory: m.dir
|
|
@@ -5448,11 +5664,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
5448
5664
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5449
5665
|
function readAsset(route) {
|
|
5450
5666
|
try {
|
|
5451
|
-
const file2 =
|
|
5452
|
-
const { mtimeMs } =
|
|
5667
|
+
const file2 = path25.join(assetsDir(), path25.basename(route));
|
|
5668
|
+
const { mtimeMs } = fs29.statSync(file2);
|
|
5453
5669
|
const hit = assetCache.get(route);
|
|
5454
5670
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5455
|
-
const data =
|
|
5671
|
+
const data = fs29.readFileSync(file2);
|
|
5456
5672
|
assetCache.set(route, { mtimeMs, data });
|
|
5457
5673
|
return data;
|
|
5458
5674
|
} catch {
|
|
@@ -5558,7 +5774,7 @@ function startDash(opts) {
|
|
|
5558
5774
|
});
|
|
5559
5775
|
});
|
|
5560
5776
|
}
|
|
5561
|
-
var registryFile = () =>
|
|
5777
|
+
var registryFile = () => path25.join(home(), "dash.json");
|
|
5562
5778
|
async function ping(port) {
|
|
5563
5779
|
try {
|
|
5564
5780
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5570,12 +5786,12 @@ async function ping(port) {
|
|
|
5570
5786
|
async function runningDash() {
|
|
5571
5787
|
let r;
|
|
5572
5788
|
try {
|
|
5573
|
-
r = JSON.parse(
|
|
5789
|
+
r = JSON.parse(fs29.readFileSync(registryFile(), "utf8"));
|
|
5574
5790
|
} catch {
|
|
5575
5791
|
return void 0;
|
|
5576
5792
|
}
|
|
5577
5793
|
if (!isAlive(r.pid)) {
|
|
5578
|
-
|
|
5794
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5579
5795
|
return void 0;
|
|
5580
5796
|
}
|
|
5581
5797
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -5583,7 +5799,7 @@ async function runningDash() {
|
|
|
5583
5799
|
function openBrowser(url) {
|
|
5584
5800
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5585
5801
|
try {
|
|
5586
|
-
|
|
5802
|
+
spawn6(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
|
|
5587
5803
|
} catch {
|
|
5588
5804
|
}
|
|
5589
5805
|
}
|
|
@@ -5592,7 +5808,7 @@ async function dashCommand(o) {
|
|
|
5592
5808
|
if (o.stop) {
|
|
5593
5809
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5594
5810
|
process.kill(existing.pid, "SIGTERM");
|
|
5595
|
-
|
|
5811
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5596
5812
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5597
5813
|
return 0;
|
|
5598
5814
|
}
|
|
@@ -5602,7 +5818,7 @@ async function dashCommand(o) {
|
|
|
5602
5818
|
return 0;
|
|
5603
5819
|
}
|
|
5604
5820
|
if (o.detach) {
|
|
5605
|
-
const child =
|
|
5821
|
+
const child = spawn6(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
|
|
5606
5822
|
detached: true,
|
|
5607
5823
|
stdio: "ignore",
|
|
5608
5824
|
env: process.env
|
|
@@ -5629,9 +5845,9 @@ async function dashCommand(o) {
|
|
|
5629
5845
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5630
5846
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5631
5847
|
}
|
|
5632
|
-
|
|
5848
|
+
fs29.mkdirSync(home(), { recursive: true });
|
|
5633
5849
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5634
|
-
|
|
5850
|
+
fs29.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5635
5851
|
renameOver(tmp, registryFile());
|
|
5636
5852
|
const stop = () => void dash.close();
|
|
5637
5853
|
process.once("SIGINT", stop);
|
|
@@ -5641,7 +5857,7 @@ async function dashCommand(o) {
|
|
|
5641
5857
|
if (o.open) openBrowser(dash.url);
|
|
5642
5858
|
await dash.closed;
|
|
5643
5859
|
try {
|
|
5644
|
-
if (JSON.parse(
|
|
5860
|
+
if (JSON.parse(fs29.readFileSync(registryFile(), "utf8")).pid === process.pid) fs29.rmSync(registryFile(), { force: true });
|
|
5645
5861
|
} catch {
|
|
5646
5862
|
}
|
|
5647
5863
|
return 0;
|
|
@@ -5653,10 +5869,10 @@ var MAX_BODY = 4e6;
|
|
|
5653
5869
|
var MAX_SESSIONS = 64;
|
|
5654
5870
|
var IDLE_MS = 60 * 6e4;
|
|
5655
5871
|
var PATH = "/mcp";
|
|
5656
|
-
var tokenFile = () =>
|
|
5872
|
+
var tokenFile = () => path26.join(home(), "mcp-token");
|
|
5657
5873
|
var readToken = (file2) => {
|
|
5658
5874
|
try {
|
|
5659
|
-
const kept =
|
|
5875
|
+
const kept = fs30.readFileSync(file2, "utf8").trim();
|
|
5660
5876
|
return kept.length >= 16 ? kept : void 0;
|
|
5661
5877
|
} catch {
|
|
5662
5878
|
return void 0;
|
|
@@ -5671,10 +5887,10 @@ function mcpToken() {
|
|
|
5671
5887
|
const file2 = tokenFile();
|
|
5672
5888
|
let token = readToken(file2);
|
|
5673
5889
|
if (!token) {
|
|
5674
|
-
|
|
5890
|
+
fs30.mkdirSync(path26.dirname(file2), { recursive: true });
|
|
5675
5891
|
token = crypto8.randomBytes(32).toString("hex");
|
|
5676
5892
|
try {
|
|
5677
|
-
|
|
5893
|
+
fs30.writeFileSync(file2, `${token}
|
|
5678
5894
|
`, { mode: 384, flag: "wx" });
|
|
5679
5895
|
} catch (e) {
|
|
5680
5896
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5683,7 +5899,7 @@ function mcpToken() {
|
|
|
5683
5899
|
}
|
|
5684
5900
|
}
|
|
5685
5901
|
try {
|
|
5686
|
-
|
|
5902
|
+
fs30.chmodSync(file2, 384);
|
|
5687
5903
|
} catch {
|
|
5688
5904
|
}
|
|
5689
5905
|
return { token, from: file2 };
|
|
@@ -5865,16 +6081,16 @@ async function serveMcpHttp(opts) {
|
|
|
5865
6081
|
|
|
5866
6082
|
// src/cli/commands.ts
|
|
5867
6083
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5868
|
-
import
|
|
5869
|
-
import
|
|
6084
|
+
import fs36 from "node:fs";
|
|
6085
|
+
import path32 from "node:path";
|
|
5870
6086
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5871
6087
|
|
|
5872
6088
|
// src/core/init.ts
|
|
5873
|
-
import
|
|
5874
|
-
import
|
|
6089
|
+
import fs31 from "node:fs";
|
|
6090
|
+
import path27 from "node:path";
|
|
5875
6091
|
var FREE = /-free$/;
|
|
5876
6092
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5877
|
-
var isFound = (binary5) =>
|
|
6093
|
+
var isFound = (binary5) => path27.isAbsolute(binary5) && fs31.existsSync(binary5);
|
|
5878
6094
|
function planInit(opts = {}) {
|
|
5879
6095
|
const file2 = configPath();
|
|
5880
6096
|
const workers = backendIds().map((id) => {
|
|
@@ -5920,15 +6136,15 @@ function planInit(opts = {}) {
|
|
|
5920
6136
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5921
6137
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
5922
6138
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
5923
|
-
return { path: file2, exists:
|
|
6139
|
+
return { path: file2, exists: fs31.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5924
6140
|
}
|
|
5925
6141
|
function writeInit(plan, force) {
|
|
5926
6142
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5927
6143
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
5928
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
5929
|
-
|
|
5930
|
-
if (plan.exists)
|
|
5931
|
-
|
|
6144
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path27.basename(plan.path)}.bak)`);
|
|
6145
|
+
fs31.mkdirSync(path27.dirname(plan.path), { recursive: true });
|
|
6146
|
+
if (plan.exists) fs31.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
6147
|
+
fs31.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5932
6148
|
`);
|
|
5933
6149
|
return plan.path;
|
|
5934
6150
|
}
|
|
@@ -5949,17 +6165,17 @@ function formatInit(plan, written) {
|
|
|
5949
6165
|
|
|
5950
6166
|
// src/core/mcp-install.ts
|
|
5951
6167
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5952
|
-
import
|
|
6168
|
+
import fs32 from "node:fs";
|
|
5953
6169
|
import os10 from "node:os";
|
|
5954
|
-
import
|
|
6170
|
+
import path28 from "node:path";
|
|
5955
6171
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
5956
6172
|
var NAME = "pitroom";
|
|
5957
6173
|
function mcpCommand() {
|
|
5958
6174
|
const launcher = launcherPath();
|
|
5959
6175
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5960
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5961
|
-
const p =
|
|
5962
|
-
if (
|
|
6176
|
+
for (const dir of (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean)) {
|
|
6177
|
+
const p = path28.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
6178
|
+
if (fs32.existsSync(p)) return { command: path28.resolve(p), args: ["mcp"] };
|
|
5963
6179
|
}
|
|
5964
6180
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5965
6181
|
}
|
|
@@ -5975,7 +6191,7 @@ function run(backendId, args) {
|
|
|
5975
6191
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5976
6192
|
var readJson = (file2) => {
|
|
5977
6193
|
try {
|
|
5978
|
-
return JSON.parse(
|
|
6194
|
+
return JSON.parse(fs32.readFileSync(file2, "utf8"));
|
|
5979
6195
|
} catch {
|
|
5980
6196
|
return void 0;
|
|
5981
6197
|
}
|
|
@@ -5985,7 +6201,7 @@ var jsonState = (file2, c) => {
|
|
|
5985
6201
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
5986
6202
|
};
|
|
5987
6203
|
var home2 = () => os10.homedir();
|
|
5988
|
-
var geminiDir = () =>
|
|
6204
|
+
var geminiDir = () => path28.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
5989
6205
|
function cliClient(id, backend, name, where, o) {
|
|
5990
6206
|
return {
|
|
5991
6207
|
id,
|
|
@@ -6008,43 +6224,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
6008
6224
|
return {
|
|
6009
6225
|
id,
|
|
6010
6226
|
name,
|
|
6011
|
-
found: () =>
|
|
6227
|
+
found: () => fs32.existsSync(dir()),
|
|
6012
6228
|
state: (c) => jsonState(file2(), c),
|
|
6013
6229
|
add: (c) => {
|
|
6014
6230
|
let config = {};
|
|
6015
|
-
if (
|
|
6231
|
+
if (fs32.existsSync(file2())) {
|
|
6016
6232
|
config = readJson(file2());
|
|
6017
6233
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
6018
6234
|
const backup = `${file2()}.bak-pitroom`;
|
|
6019
|
-
if (!
|
|
6235
|
+
if (!fs32.existsSync(backup)) fs32.copyFileSync(file2(), backup);
|
|
6020
6236
|
}
|
|
6021
6237
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
6022
|
-
|
|
6238
|
+
fs32.mkdirSync(path28.dirname(file2()), { recursive: true });
|
|
6023
6239
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
6024
|
-
|
|
6240
|
+
fs32.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
6025
6241
|
`);
|
|
6026
|
-
|
|
6242
|
+
fs32.renameSync(tmp, file2());
|
|
6027
6243
|
return { ok: true, message: rel() };
|
|
6028
6244
|
},
|
|
6029
6245
|
remove: () => {
|
|
6030
6246
|
const config = readJson(file2());
|
|
6031
6247
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
6032
6248
|
delete config.mcpServers[NAME];
|
|
6033
|
-
|
|
6249
|
+
fs32.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
6034
6250
|
`);
|
|
6035
6251
|
return { ok: true, message: rel() };
|
|
6036
6252
|
}
|
|
6037
6253
|
};
|
|
6038
6254
|
}
|
|
6039
6255
|
function desktopFile() {
|
|
6040
|
-
if (process.platform === "darwin") return
|
|
6041
|
-
if (process.platform === "win32") return
|
|
6042
|
-
return
|
|
6256
|
+
if (process.platform === "darwin") return path28.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
6257
|
+
if (process.platform === "win32") return path28.join(process.env.APPDATA ?? path28.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
6258
|
+
return path28.join(process.env.XDG_CONFIG_HOME ?? path28.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
6043
6259
|
}
|
|
6044
6260
|
function codexState(c) {
|
|
6045
6261
|
let text;
|
|
6046
6262
|
try {
|
|
6047
|
-
text =
|
|
6263
|
+
text = fs32.readFileSync(path28.join(process.env.CODEX_HOME ?? path28.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
6048
6264
|
} catch {
|
|
6049
6265
|
return "absent";
|
|
6050
6266
|
}
|
|
@@ -6062,7 +6278,7 @@ function codexState(c) {
|
|
|
6062
6278
|
function clients() {
|
|
6063
6279
|
return [
|
|
6064
6280
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
6065
|
-
state: (c) => jsonState(
|
|
6281
|
+
state: (c) => jsonState(path28.join(home2(), ".claude.json"), c),
|
|
6066
6282
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
6067
6283
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
6068
6284
|
}),
|
|
@@ -6072,12 +6288,12 @@ function clients() {
|
|
|
6072
6288
|
remove: () => ["mcp", "remove", NAME]
|
|
6073
6289
|
}),
|
|
6074
6290
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
6075
|
-
state: (c) => jsonState(
|
|
6291
|
+
state: (c) => jsonState(path28.join(geminiDir(), "settings.json"), c),
|
|
6076
6292
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
6077
6293
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
6078
6294
|
}),
|
|
6079
|
-
jsonClient("cursor", "Cursor", () =>
|
|
6080
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
6295
|
+
jsonClient("cursor", "Cursor", () => path28.join(home2(), ".cursor", "mcp.json"), () => path28.join(home2(), ".cursor")),
|
|
6296
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path28.dirname(desktopFile()))
|
|
6081
6297
|
];
|
|
6082
6298
|
}
|
|
6083
6299
|
function attempt2(f) {
|
|
@@ -6148,16 +6364,16 @@ var parseClients = (value) => {
|
|
|
6148
6364
|
|
|
6149
6365
|
// src/core/plan-status.ts
|
|
6150
6366
|
import crypto9 from "node:crypto";
|
|
6151
|
-
import
|
|
6152
|
-
import
|
|
6367
|
+
import fs33 from "node:fs";
|
|
6368
|
+
import path29 from "node:path";
|
|
6153
6369
|
function notesFile(plan) {
|
|
6154
6370
|
const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
6155
|
-
return
|
|
6371
|
+
return path29.join(home(), "plans", id, "notes.md");
|
|
6156
6372
|
}
|
|
6157
6373
|
function readNotes(plan) {
|
|
6158
6374
|
const f = notesFile(plan);
|
|
6159
|
-
if (!
|
|
6160
|
-
return
|
|
6375
|
+
if (!fs33.existsSync(f)) return [];
|
|
6376
|
+
return fs33.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
6161
6377
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
6162
6378
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
6163
6379
|
});
|
|
@@ -6165,11 +6381,11 @@ function readNotes(plan) {
|
|
|
6165
6381
|
function addNote(planFile, text) {
|
|
6166
6382
|
const plan = loadPlan(planFile);
|
|
6167
6383
|
const f = notesFile(plan);
|
|
6168
|
-
|
|
6169
|
-
if (!
|
|
6384
|
+
fs33.mkdirSync(path29.dirname(f), { recursive: true });
|
|
6385
|
+
if (!fs33.existsSync(f)) fs33.writeFileSync(f, `# plan: ${plan.file}
|
|
6170
6386
|
`);
|
|
6171
6387
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
6172
|
-
|
|
6388
|
+
fs33.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
6173
6389
|
`);
|
|
6174
6390
|
return f;
|
|
6175
6391
|
}
|
|
@@ -6237,26 +6453,26 @@ ${rulings}`,
|
|
|
6237
6453
|
|
|
6238
6454
|
// src/core/cache.ts
|
|
6239
6455
|
import crypto10 from "node:crypto";
|
|
6240
|
-
import
|
|
6241
|
-
import
|
|
6456
|
+
import fs34 from "node:fs";
|
|
6457
|
+
import path30 from "node:path";
|
|
6242
6458
|
var SCAN2 = 500;
|
|
6243
6459
|
function cacheKey(o) {
|
|
6244
6460
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6245
6461
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6246
|
-
const dir = canonical(
|
|
6462
|
+
const dir = canonical(path30.resolve(o.dir));
|
|
6247
6463
|
const root = repoRoot(dir);
|
|
6248
6464
|
if (!root) return void 0;
|
|
6249
6465
|
try {
|
|
6250
6466
|
const tree = snapshotTree(root);
|
|
6251
6467
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6252
6468
|
const files = o.files.map((f) => {
|
|
6253
|
-
const abs =
|
|
6254
|
-
return [abs, crypto10.createHash("sha256").update(
|
|
6469
|
+
const abs = path30.resolve(f);
|
|
6470
|
+
return [abs, crypto10.createHash("sha256").update(fs34.readFileSync(abs)).digest("hex")];
|
|
6255
6471
|
});
|
|
6256
6472
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6257
6473
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6258
6474
|
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6259
|
-
const key = crypto10.createHash("sha256").update(JSON.stringify([question,
|
|
6475
|
+
const key = crypto10.createHash("sha256").update(JSON.stringify([question, path30.relative(root, dir), files, where])).digest("hex");
|
|
6260
6476
|
return { key, state: `${head}:${tree}` };
|
|
6261
6477
|
} catch {
|
|
6262
6478
|
return void 0;
|
|
@@ -6300,8 +6516,8 @@ var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on
|
|
|
6300
6516
|
|
|
6301
6517
|
// src/core/review.ts
|
|
6302
6518
|
import crypto11 from "node:crypto";
|
|
6303
|
-
import
|
|
6304
|
-
import
|
|
6519
|
+
import fs35 from "node:fs";
|
|
6520
|
+
import path31 from "node:path";
|
|
6305
6521
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6306
6522
|
var section = (title, body) => `## ${title}
|
|
6307
6523
|
|
|
@@ -6319,7 +6535,7 @@ function ancestors(m) {
|
|
|
6319
6535
|
function briefOf(m) {
|
|
6320
6536
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6321
6537
|
const f = runFile(root.id, "brief.md");
|
|
6322
|
-
return
|
|
6538
|
+
return fs35.existsSync(f) ? fs35.readFileSync(f, "utf8") : root.task;
|
|
6323
6539
|
}
|
|
6324
6540
|
function latestReview(ids) {
|
|
6325
6541
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6333,7 +6549,7 @@ function latestReview(ids) {
|
|
|
6333
6549
|
}
|
|
6334
6550
|
function diffOf(m, from = m.baseTree) {
|
|
6335
6551
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6336
|
-
if (where &&
|
|
6552
|
+
if (where && fs35.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6337
6553
|
if (from !== m.baseTree) {
|
|
6338
6554
|
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);
|
|
6339
6555
|
}
|
|
@@ -6347,7 +6563,7 @@ function runReview(id) {
|
|
|
6347
6563
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6348
6564
|
const chain = ancestors(m);
|
|
6349
6565
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6350
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6566
|
+
const dir = m.mode === "isolate" && m.worktree && fs35.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6351
6567
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6352
6568
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6353
6569
|
if (!previous) {
|
|
@@ -6423,19 +6639,19 @@ function writePackage(job) {
|
|
|
6423
6639
|
const root = repoRoot(job.dir);
|
|
6424
6640
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6425
6641
|
const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
|
|
6426
|
-
const inside2 = !
|
|
6427
|
-
const file2 = inside2 ?
|
|
6642
|
+
const inside2 = !path31.relative(fs35.realpathSync(root), fs35.realpathSync(g)).startsWith("..");
|
|
6643
|
+
const file2 = inside2 ? path31.join(g, "pitroom", name) : path31.join(root, ".pitroom", name);
|
|
6428
6644
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6429
|
-
|
|
6430
|
-
|
|
6645
|
+
fs35.mkdirSync(path31.dirname(file2), { recursive: true });
|
|
6646
|
+
fs35.writeFileSync(file2, job.package);
|
|
6431
6647
|
return file2;
|
|
6432
6648
|
}
|
|
6433
6649
|
function ignoreInGit(root, pattern) {
|
|
6434
|
-
const exclude =
|
|
6435
|
-
const text =
|
|
6650
|
+
const exclude = path31.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6651
|
+
const text = fs35.existsSync(exclude) ? fs35.readFileSync(exclude, "utf8") : "";
|
|
6436
6652
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6437
|
-
|
|
6438
|
-
|
|
6653
|
+
fs35.mkdirSync(path31.dirname(exclude), { recursive: true });
|
|
6654
|
+
fs35.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6439
6655
|
${pattern}
|
|
6440
6656
|
`);
|
|
6441
6657
|
}
|
|
@@ -6591,10 +6807,10 @@ async function cmdReview(p) {
|
|
|
6591
6807
|
writeMeta(meta);
|
|
6592
6808
|
}
|
|
6593
6809
|
} catch (e) {
|
|
6594
|
-
|
|
6810
|
+
fs36.rmSync(packageFile, { force: true });
|
|
6595
6811
|
throw e;
|
|
6596
6812
|
}
|
|
6597
|
-
|
|
6813
|
+
fs36.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6598
6814
|
return launch(p, meta);
|
|
6599
6815
|
}
|
|
6600
6816
|
async function cmdAudit(p) {
|
|
@@ -6624,6 +6840,47 @@ async function cmdAudit(p) {
|
|
|
6624
6840
|
writeMeta(m);
|
|
6625
6841
|
return launch(p, a);
|
|
6626
6842
|
}
|
|
6843
|
+
async function cmdPrices(p) {
|
|
6844
|
+
if (has(p, "refresh")) {
|
|
6845
|
+
const r = await refreshCatalog();
|
|
6846
|
+
if (r.state === "failed") {
|
|
6847
|
+
if (!has(p, "quiet")) console.error(`could not refresh the price catalog: ${r.error}`);
|
|
6848
|
+
return 1;
|
|
6849
|
+
}
|
|
6850
|
+
if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
|
|
6851
|
+
}
|
|
6852
|
+
const st = catalogStatus();
|
|
6853
|
+
const used = (() => {
|
|
6854
|
+
try {
|
|
6855
|
+
const chain = resolveChain();
|
|
6856
|
+
return [chain.worker, ...chain.fallback].map((t) => {
|
|
6857
|
+
const backend = getBackend(t.backend);
|
|
6858
|
+
const model = (t.model ?? "").split("#")[0];
|
|
6859
|
+
const price = workerPrice(t.backend, model);
|
|
6860
|
+
return {
|
|
6861
|
+
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
6862
|
+
reportsCost: backend.capabilities.reportsCost,
|
|
6863
|
+
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
6864
|
+
};
|
|
6865
|
+
});
|
|
6866
|
+
} catch {
|
|
6867
|
+
return [];
|
|
6868
|
+
}
|
|
6869
|
+
})();
|
|
6870
|
+
if (has(p, "json")) return console.log(JSON.stringify({ ...st, workers: used }, null, 2)), 0;
|
|
6871
|
+
console.log(`price feed: ${st.enabled ? "on" : 'off ("priceFeed": true in the config turns it on: Pitroom then fetches one public file, see PRIVACY.md)'}`);
|
|
6872
|
+
console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
|
|
6873
|
+
console.log(st.models ? `catalog: ${st.models} priced models, updated ${st.ageHours < 1 ? "within the hour" : `${st.ageHours.toFixed(1)} h ago`}` : "catalog: none yet (pitroom prices --refresh fetches it)");
|
|
6874
|
+
if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
|
|
6875
|
+
if (used.length) {
|
|
6876
|
+
console.log("\nthe workers in use:");
|
|
6877
|
+
for (const u of used) {
|
|
6878
|
+
const what2 = u.reportsCost ? "reports its own cost" : u.price ? `in ${u.price.input} \xB7 out ${u.price.output} \xB7 cached ${u.price.cachedInput} USD/1M (${u.price.source === "catalog" ? `price catalog ${catalogLabel()}` : "your workerPrices"})` : "no price: its runs count as free in the savings";
|
|
6879
|
+
console.log(` ${u.worker.padEnd(36)} ${what2}`);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
return 0;
|
|
6883
|
+
}
|
|
6627
6884
|
function cmdCooldown(p) {
|
|
6628
6885
|
if (has(p, "clear")) {
|
|
6629
6886
|
const n = clearCooldowns();
|
|
@@ -6662,7 +6919,7 @@ function cmdPlan(p) {
|
|
|
6662
6919
|
}
|
|
6663
6920
|
function cmdCrew(p) {
|
|
6664
6921
|
const file2 = flag(p, "task-file");
|
|
6665
|
-
const tasks = (file2 ?
|
|
6922
|
+
const tasks = (file2 ? fs36.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6666
6923
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6667
6924
|
if (has(p, "write") && tasks.length > 1) {
|
|
6668
6925
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6863,7 +7120,7 @@ function sinceMs(s) {
|
|
|
6863
7120
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6864
7121
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6865
7122
|
}
|
|
6866
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
7123
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs36.readFileSync(0, "utf8");
|
|
6867
7124
|
function cmdStatusline(p) {
|
|
6868
7125
|
const input = readStdin();
|
|
6869
7126
|
const lines = [];
|
|
@@ -6903,8 +7160,8 @@ function cmdModels(p) {
|
|
|
6903
7160
|
return 0;
|
|
6904
7161
|
}
|
|
6905
7162
|
function cmdHookStart() {
|
|
6906
|
-
const script =
|
|
6907
|
-
if (
|
|
7163
|
+
const script = path32.join(path32.dirname(path32.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
7164
|
+
if (fs36.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6908
7165
|
return 0;
|
|
6909
7166
|
}
|
|
6910
7167
|
function cmdInit(p) {
|
|
@@ -6944,8 +7201,8 @@ function cmdSavings(p) {
|
|
|
6944
7201
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6945
7202
|
const out = flag(p, "card");
|
|
6946
7203
|
if (out) {
|
|
6947
|
-
|
|
6948
|
-
console.log(`card written to ${
|
|
7204
|
+
fs36.writeFileSync(out, card(t, period));
|
|
7205
|
+
console.log(`card written to ${path32.resolve(out)}`);
|
|
6949
7206
|
}
|
|
6950
7207
|
if (has(p, "badge")) console.log(`})`);
|
|
6951
7208
|
return 0;
|
|
@@ -6987,7 +7244,7 @@ function cmdConfig(p) {
|
|
|
6987
7244
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6988
7245
|
return 0;
|
|
6989
7246
|
}
|
|
6990
|
-
console.log(`config file: ${configPath()}${
|
|
7247
|
+
console.log(`config file: ${configPath()}${fs36.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6991
7248
|
for (const [key, s] of Object.entries(eff)) {
|
|
6992
7249
|
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);
|
|
6993
7250
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -7011,7 +7268,7 @@ function cmdClean(p) {
|
|
|
7011
7268
|
const m = readMeta(id);
|
|
7012
7269
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
7013
7270
|
recordRun(m);
|
|
7014
|
-
|
|
7271
|
+
fs36.rmSync(runDir(id), { recursive: true, force: true });
|
|
7015
7272
|
}
|
|
7016
7273
|
const snapshots = prune(Date.now(), 0);
|
|
7017
7274
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -7022,9 +7279,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
7022
7279
|
|
|
7023
7280
|
// src/core/doctor.ts
|
|
7024
7281
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
7025
|
-
import
|
|
7282
|
+
import fs37 from "node:fs";
|
|
7026
7283
|
import os11 from "node:os";
|
|
7027
|
-
import
|
|
7284
|
+
import path33 from "node:path";
|
|
7028
7285
|
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
7029
7286
|
var NEXT = [
|
|
7030
7287
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -7047,10 +7304,10 @@ function usageLine() {
|
|
|
7047
7304
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
7048
7305
|
}
|
|
7049
7306
|
function firstNodeOnPath() {
|
|
7050
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7051
|
-
const file2 =
|
|
7307
|
+
for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
|
|
7308
|
+
const file2 = path33.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
7052
7309
|
try {
|
|
7053
|
-
if (!
|
|
7310
|
+
if (!fs37.statSync(file2).isFile()) continue;
|
|
7054
7311
|
} catch {
|
|
7055
7312
|
continue;
|
|
7056
7313
|
}
|
|
@@ -7077,19 +7334,16 @@ function doctor5(probe) {
|
|
|
7077
7334
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
7078
7335
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
7079
7336
|
const onPath = firstNodeOnPath();
|
|
7080
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
7337
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path33.resolve(onPath.path) !== path33.resolve(process.execPath)) {
|
|
7081
7338
|
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)`);
|
|
7082
7339
|
}
|
|
7083
7340
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
7084
7341
|
const cfg = loadConfig();
|
|
7085
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
7342
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs37.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
7086
7343
|
for (const w of cfg.warnings) add("warn", w);
|
|
7087
|
-
if (
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
7091
|
-
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
7092
|
-
}
|
|
7344
|
+
if (shimReady()) add("ok", "git guard shim ready");
|
|
7345
|
+
else if (process.platform === "win32") add("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
|
|
7346
|
+
else add("warn", "git guard unavailable (git not on PATH)");
|
|
7093
7347
|
section2("Worker chain");
|
|
7094
7348
|
const chain = [];
|
|
7095
7349
|
try {
|
|
@@ -7126,6 +7380,25 @@ function doctor5(probe) {
|
|
|
7126
7380
|
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
7127
7381
|
}
|
|
7128
7382
|
}
|
|
7383
|
+
const seenUnpriced = /* @__PURE__ */ new Set();
|
|
7384
|
+
for (const t of [...chain, ...tierTargets]) {
|
|
7385
|
+
let backend;
|
|
7386
|
+
try {
|
|
7387
|
+
backend = getBackend(t.backend);
|
|
7388
|
+
} catch {
|
|
7389
|
+
continue;
|
|
7390
|
+
}
|
|
7391
|
+
const model = (t.model ?? "").split("#")[0];
|
|
7392
|
+
const key = model ? `${t.backend}:${model}` : t.backend;
|
|
7393
|
+
if (backend.capabilities.reportsCost || seenUnpriced.has(key) || workerPrice(t.backend, model)) continue;
|
|
7394
|
+
seenUnpriced.add(key);
|
|
7395
|
+
add("warn", `${key} reports no cost, so its runs count as free in the savings: give its price in the config, "workerPrices": {"${key}": "<in>,<out>[,<cachedIn>]"} (USD per 1M tokens), or turn on "priceFeed": true to use the public price catalog`);
|
|
7396
|
+
}
|
|
7397
|
+
const feed = catalogStatus();
|
|
7398
|
+
if (feed.enabled) {
|
|
7399
|
+
if (feed.models) add(feed.lastFailure ? "warn" : "ok", `price catalog: ${feed.models} priced models from ${feed.url}, updated ${feed.ageHours < 1 ? "within the hour" : `${feed.ageHours.toFixed(1)} h ago`}${feed.lastFailure ? `; the last refresh failed (${feed.lastFailure.replace(/^\S+ /, "")})` : ""}`);
|
|
7400
|
+
else add("warn", `price feed is on but no catalog has been fetched yet${feed.lastFailure ? ` (the last try failed: ${feed.lastFailure.replace(/^\S+ /, "")})` : ""}: it is fetched after a run, or now with \`pitroom prices --refresh\``);
|
|
7401
|
+
}
|
|
7129
7402
|
for (const c of Object.values(activeCooldowns())) {
|
|
7130
7403
|
add("warn", `${c.target} is cooling down ${untilText(c)} (${c.reason.slice(0, 80)}): runs skip it while a fallback is left; \`pitroom cooldown --clear\` tries it again`);
|
|
7131
7404
|
}
|
|
@@ -7239,19 +7512,19 @@ function skillChecks() {
|
|
|
7239
7512
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
7240
7513
|
}
|
|
7241
7514
|
if (geminiExtensionInstalled()) {
|
|
7242
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7515
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.agents${path33.sep}`) && i.names.length);
|
|
7243
7516
|
checks.push(
|
|
7244
7517
|
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)" }
|
|
7245
7518
|
);
|
|
7246
7519
|
}
|
|
7247
7520
|
if (viaPlugin) {
|
|
7248
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7521
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.claude${path33.sep}`) && i.names.length);
|
|
7249
7522
|
checks.push(
|
|
7250
7523
|
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)" }
|
|
7251
7524
|
);
|
|
7252
7525
|
}
|
|
7253
7526
|
const launcher = launcherPath();
|
|
7254
|
-
if (
|
|
7527
|
+
if (fs37.existsSync(launcher)) {
|
|
7255
7528
|
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
7256
7529
|
checks.push(
|
|
7257
7530
|
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)}` }
|
|
@@ -7263,12 +7536,12 @@ function skillChecks() {
|
|
|
7263
7536
|
}
|
|
7264
7537
|
function geminiExtensionInstalled() {
|
|
7265
7538
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
7266
|
-
return
|
|
7539
|
+
return fs37.existsSync(path33.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7267
7540
|
}
|
|
7268
7541
|
function pluginInstalled() {
|
|
7269
7542
|
try {
|
|
7270
|
-
const f =
|
|
7271
|
-
const plugins = JSON.parse(
|
|
7543
|
+
const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7544
|
+
const plugins = JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {};
|
|
7272
7545
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7273
7546
|
} catch {
|
|
7274
7547
|
return false;
|
|
@@ -7285,24 +7558,24 @@ function superpowersActive() {
|
|
|
7285
7558
|
for (const p of openCodePlugins()) {
|
|
7286
7559
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
7287
7560
|
}
|
|
7288
|
-
for (const base2 of [
|
|
7289
|
-
const dir =
|
|
7290
|
-
if (
|
|
7561
|
+
for (const base2 of [path33.join(os11.homedir(), ".agents", "skills"), path33.join(os11.homedir(), ".claude", "skills")]) {
|
|
7562
|
+
const dir = path33.join(base2, "using-superpowers");
|
|
7563
|
+
if (fs37.existsSync(path33.join(dir, "SKILL.md"))) found.push(dir);
|
|
7291
7564
|
}
|
|
7292
7565
|
return found;
|
|
7293
7566
|
}
|
|
7294
7567
|
function claudePlugins() {
|
|
7295
7568
|
try {
|
|
7296
|
-
const f =
|
|
7297
|
-
return Object.keys(JSON.parse(
|
|
7569
|
+
const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7570
|
+
return Object.keys(JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {});
|
|
7298
7571
|
} catch {
|
|
7299
7572
|
return [];
|
|
7300
7573
|
}
|
|
7301
7574
|
}
|
|
7302
7575
|
function claudePluginEnabled(key) {
|
|
7303
7576
|
try {
|
|
7304
|
-
const f =
|
|
7305
|
-
return JSON.parse(
|
|
7577
|
+
const f = path33.join(os11.homedir(), ".claude", "settings.json");
|
|
7578
|
+
return JSON.parse(fs37.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7306
7579
|
} catch {
|
|
7307
7580
|
return true;
|
|
7308
7581
|
}
|
|
@@ -7311,7 +7584,7 @@ function codexPlugins() {
|
|
|
7311
7584
|
const plugins = /* @__PURE__ */ new Map();
|
|
7312
7585
|
let text;
|
|
7313
7586
|
try {
|
|
7314
|
-
text =
|
|
7587
|
+
text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7315
7588
|
} catch {
|
|
7316
7589
|
return plugins;
|
|
7317
7590
|
}
|
|
@@ -7329,11 +7602,11 @@ function codexPlugins() {
|
|
|
7329
7602
|
return plugins;
|
|
7330
7603
|
}
|
|
7331
7604
|
function openCodePlugins() {
|
|
7332
|
-
const dir =
|
|
7605
|
+
const dir = path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(os11.homedir(), ".config"), "opencode");
|
|
7333
7606
|
const plugins = [];
|
|
7334
7607
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7335
7608
|
try {
|
|
7336
|
-
const text =
|
|
7609
|
+
const text = fs37.readFileSync(path33.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7337
7610
|
const list2 = JSON.parse(text).plugin;
|
|
7338
7611
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7339
7612
|
} catch {
|
|
@@ -7341,7 +7614,7 @@ function openCodePlugins() {
|
|
|
7341
7614
|
}
|
|
7342
7615
|
for (const folder of ["plugin", "plugins"]) {
|
|
7343
7616
|
try {
|
|
7344
|
-
for (const f of
|
|
7617
|
+
for (const f of fs37.readdirSync(path33.join(dir, folder))) plugins.push(path33.join(dir, folder, f));
|
|
7345
7618
|
} catch {
|
|
7346
7619
|
}
|
|
7347
7620
|
}
|
|
@@ -7391,6 +7664,7 @@ Usage
|
|
|
7391
7664
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
7392
7665
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
7393
7666
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
7667
|
+
pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
|
|
7394
7668
|
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
7395
7669
|
--clear tries them again
|
|
7396
7670
|
pitroom mcp [-d DIR] [--http [--port N]]
|
|
@@ -7444,21 +7718,24 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
|
|
|
7444
7718
|
Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
|
|
7445
7719
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
7446
7720
|
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
7447
|
-
|
|
7448
|
-
|
|
7721
|
+
PITROOM_NOTIFY, PITROOM_NOTIFY_COMMAND, PITROOM_NOTIFY_AFTER, PITROOM_MCP_DASH, PITROOM_PRICE_FEED, PITROOM_PRICE_FEED_URL,
|
|
7722
|
+
PITROOM_PRICE_FEED_HOURS, PITROOM_<WORKER>_BIN
|
|
7723
|
+
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays,
|
|
7724
|
+
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours)`;
|
|
7449
7725
|
var COMMANDS = {
|
|
7450
7726
|
run: cmdRun,
|
|
7451
7727
|
crew: cmdCrew,
|
|
7452
7728
|
review: cmdReview,
|
|
7453
7729
|
audit: cmdAudit,
|
|
7454
7730
|
cooldown: cmdCooldown,
|
|
7731
|
+
prices: cmdPrices,
|
|
7455
7732
|
mcp: (p) => {
|
|
7456
7733
|
const port = flag(p, "port");
|
|
7457
7734
|
if (port !== void 0 && !has(p, "http")) throw new UserError("--port goes with --http (on stdio there is no port)");
|
|
7458
7735
|
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)");
|
|
7459
7736
|
const dir = flag(p, "dir");
|
|
7460
7737
|
if (dir !== void 0) {
|
|
7461
|
-
if (!
|
|
7738
|
+
if (!fs38.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7462
7739
|
process.chdir(dir);
|
|
7463
7740
|
process.env.PWD = process.cwd();
|
|
7464
7741
|
}
|