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