pitroom 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/PRIVACY.md +3 -1
- package/README.md +77 -2
- package/dist/lib.mjs +837 -40
- package/dist/pitroom.mjs +1342 -906
- package/dist/ui/app.js +10 -10
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/lib.mjs
CHANGED
|
@@ -380,10 +380,10 @@ function parseEvents2(jsonl) {
|
|
|
380
380
|
}
|
|
381
381
|
case "turn.completed": {
|
|
382
382
|
const u = e.usage ?? {};
|
|
383
|
-
const
|
|
383
|
+
const cached3 = num2(u.cached_input_tokens);
|
|
384
384
|
const reasoning = num2(u.reasoning_output_tokens);
|
|
385
|
-
usage.input += Math.max(0, num2(u.input_tokens) -
|
|
386
|
-
usage.cacheRead +=
|
|
385
|
+
usage.input += Math.max(0, num2(u.input_tokens) - cached3);
|
|
386
|
+
usage.cacheRead += cached3;
|
|
387
387
|
usage.cacheWrite += num2(u.cache_write_input_tokens);
|
|
388
388
|
usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
|
|
389
389
|
usage.reasoning += reasoning;
|
|
@@ -544,14 +544,35 @@ var codex = {
|
|
|
544
544
|
|
|
545
545
|
// src/backends/gemini/index.ts
|
|
546
546
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
547
|
-
import
|
|
547
|
+
import fs6 from "node:fs";
|
|
548
548
|
import os5 from "node:os";
|
|
549
549
|
import path5 from "node:path";
|
|
550
550
|
import { fileURLToPath } from "node:url";
|
|
551
551
|
|
|
552
552
|
// src/core/store.ts
|
|
553
|
+
import fs5 from "node:fs";
|
|
553
554
|
import os4 from "node:os";
|
|
554
555
|
import path4 from "node:path";
|
|
556
|
+
import crypto from "node:crypto";
|
|
557
|
+
|
|
558
|
+
// src/core/fs-atomic.ts
|
|
559
|
+
import fs4 from "node:fs";
|
|
560
|
+
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
561
|
+
function renameOver(tmp, file) {
|
|
562
|
+
for (let attempt = 0; ; attempt++) {
|
|
563
|
+
try {
|
|
564
|
+
fs4.renameSync(tmp, file);
|
|
565
|
+
return;
|
|
566
|
+
} catch (e) {
|
|
567
|
+
if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
|
|
568
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/core/store.ts
|
|
574
|
+
var TERMINAL = ["done", "failed", "timeout", "stopped"];
|
|
575
|
+
var isActive = (s) => !TERMINAL.includes(s);
|
|
555
576
|
function home() {
|
|
556
577
|
if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
|
|
557
578
|
if (process.platform === "win32") {
|
|
@@ -559,6 +580,92 @@ function home() {
|
|
|
559
580
|
}
|
|
560
581
|
return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
|
|
561
582
|
}
|
|
583
|
+
var runsDir = () => path4.join(home(), "runs");
|
|
584
|
+
var ledgerFile = () => path4.join(home(), "ledger.jsonl");
|
|
585
|
+
var runDir = (id) => path4.join(runsDir(), id);
|
|
586
|
+
var runFile = (id, name) => path4.join(runDir(id), name);
|
|
587
|
+
function newRunId(now = /* @__PURE__ */ new Date()) {
|
|
588
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
589
|
+
const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
590
|
+
return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
|
|
591
|
+
}
|
|
592
|
+
var onFinished;
|
|
593
|
+
var archive;
|
|
594
|
+
function writeMeta(meta) {
|
|
595
|
+
fs5.mkdirSync(runDir(meta.id), { recursive: true });
|
|
596
|
+
const file = runFile(meta.id, "meta.json");
|
|
597
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
598
|
+
fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
599
|
+
renameOver(tmp, file);
|
|
600
|
+
if (TERMINAL.includes(meta.state)) {
|
|
601
|
+
try {
|
|
602
|
+
onFinished?.(meta);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function readMeta(id) {
|
|
608
|
+
const file = runFile(id, "meta.json");
|
|
609
|
+
if (!fs5.existsSync(file)) {
|
|
610
|
+
const kept = archive?.meta(id);
|
|
611
|
+
if (kept) return upgrade(kept);
|
|
612
|
+
}
|
|
613
|
+
return upgrade(JSON.parse(fs5.readFileSync(file, "utf8")));
|
|
614
|
+
}
|
|
615
|
+
function upgrade(raw) {
|
|
616
|
+
if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
|
|
617
|
+
if (!Array.isArray(raw.fallback)) {
|
|
618
|
+
raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
|
|
619
|
+
}
|
|
620
|
+
raw.warnings ??= [];
|
|
621
|
+
raw.files ??= [];
|
|
622
|
+
raw.link ??= [];
|
|
623
|
+
return raw;
|
|
624
|
+
}
|
|
625
|
+
function listRunIds() {
|
|
626
|
+
if (!fs5.existsSync(runsDir())) return [];
|
|
627
|
+
return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
|
|
628
|
+
}
|
|
629
|
+
function resolveRun(ref) {
|
|
630
|
+
const ids = listRunIds();
|
|
631
|
+
if (!ref || ref === "latest" || ref === "last") {
|
|
632
|
+
if (!ids.length) throw new UserError("no runs yet");
|
|
633
|
+
return ids[ids.length - 1];
|
|
634
|
+
}
|
|
635
|
+
if (ids.includes(ref)) return ref;
|
|
636
|
+
const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
|
|
637
|
+
if (hits.length === 1) return hits[0];
|
|
638
|
+
if (!hits.length) {
|
|
639
|
+
const kept = archive?.id(ref);
|
|
640
|
+
if (kept) return kept;
|
|
641
|
+
}
|
|
642
|
+
throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
|
|
643
|
+
}
|
|
644
|
+
function isAlive(pid) {
|
|
645
|
+
if (!pid) return false;
|
|
646
|
+
try {
|
|
647
|
+
process.kill(pid, 0);
|
|
648
|
+
return true;
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return e.code === "EPERM";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
var STOP_FILE = "stop-requested";
|
|
654
|
+
var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
|
|
655
|
+
function freshMeta(id) {
|
|
656
|
+
const meta = readMeta(id);
|
|
657
|
+
if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
|
|
658
|
+
const latest = readMeta(id);
|
|
659
|
+
if (TERMINAL.includes(latest.state)) return latest;
|
|
660
|
+
const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
|
|
661
|
+
latest.state = stopped ? "stopped" : "failed";
|
|
662
|
+
if (!stopped) latest.error ??= "worker process exited unexpectedly";
|
|
663
|
+
latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
664
|
+
writeMeta(latest);
|
|
665
|
+
return latest;
|
|
666
|
+
}
|
|
667
|
+
return meta;
|
|
668
|
+
}
|
|
562
669
|
|
|
563
670
|
// src/backends/gemini/events.ts
|
|
564
671
|
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
|
|
@@ -645,9 +752,9 @@ function parseEvents3(jsonl) {
|
|
|
645
752
|
if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
|
|
646
753
|
} else if (e.type === "result") {
|
|
647
754
|
const s = e.stats ?? {};
|
|
648
|
-
const
|
|
649
|
-
usage.cacheRead +=
|
|
650
|
-
usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) -
|
|
755
|
+
const cached3 = num3(s.cached);
|
|
756
|
+
usage.cacheRead += cached3;
|
|
757
|
+
usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached3);
|
|
651
758
|
usage.output += num3(s.output_tokens);
|
|
652
759
|
usage.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
|
|
653
760
|
usage.toolCalls = Math.max(usage.toolCalls, num3(s.tool_calls));
|
|
@@ -684,22 +791,22 @@ var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)
|
|
|
684
791
|
function workerHome() {
|
|
685
792
|
const root = path5.join(home(), "gemini-home");
|
|
686
793
|
const dir = path5.join(root, ".gemini");
|
|
687
|
-
const base = JSON.parse(
|
|
794
|
+
const base = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
|
|
688
795
|
const auth = settings().security?.auth;
|
|
689
796
|
const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
|
|
690
797
|
`;
|
|
691
798
|
const file = path5.join(dir, "settings.json");
|
|
692
799
|
try {
|
|
693
|
-
if (
|
|
800
|
+
if (fs6.readFileSync(file, "utf8") === body) return root;
|
|
694
801
|
} catch {
|
|
695
802
|
}
|
|
696
|
-
|
|
697
|
-
|
|
803
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
804
|
+
fs6.writeFileSync(file, body);
|
|
698
805
|
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
699
806
|
const from = path5.join(geminiHome(), name);
|
|
700
807
|
const to = path5.join(dir, name);
|
|
701
808
|
try {
|
|
702
|
-
if (
|
|
809
|
+
if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
|
|
703
810
|
} catch {
|
|
704
811
|
}
|
|
705
812
|
}
|
|
@@ -726,7 +833,7 @@ function invocation3(req) {
|
|
|
726
833
|
const { command, prefix } = resolveCommand(binary3());
|
|
727
834
|
const env = { GEMINI_CLI_HOME: workerHome() };
|
|
728
835
|
const trusted = path5.join(geminiHome(), "trustedFolders.json");
|
|
729
|
-
if (
|
|
836
|
+
if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
|
|
730
837
|
return { command, args: [...prefix, ...args], env };
|
|
731
838
|
}
|
|
732
839
|
function classify3(message) {
|
|
@@ -751,7 +858,7 @@ function geminiHome() {
|
|
|
751
858
|
}
|
|
752
859
|
function settings() {
|
|
753
860
|
try {
|
|
754
|
-
return JSON.parse(
|
|
861
|
+
return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
|
|
755
862
|
} catch {
|
|
756
863
|
return {};
|
|
757
864
|
}
|
|
@@ -770,7 +877,7 @@ function doctor3({ models, hasFallback }) {
|
|
|
770
877
|
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
|
|
771
878
|
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
772
879
|
const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
|
|
773
|
-
const signedIn =
|
|
880
|
+
const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
|
|
774
881
|
const type = settings().security?.auth?.selectedType;
|
|
775
882
|
if (key || vertex) {
|
|
776
883
|
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
@@ -929,7 +1036,7 @@ function describe2(input) {
|
|
|
929
1036
|
}
|
|
930
1037
|
|
|
931
1038
|
// src/backends/opencode/profiles.ts
|
|
932
|
-
import
|
|
1039
|
+
import fs7 from "node:fs";
|
|
933
1040
|
import os6 from "node:os";
|
|
934
1041
|
import path6 from "node:path";
|
|
935
1042
|
var AGENT = { read: "pitroom-read", write: "pitroom-write" };
|
|
@@ -1040,7 +1147,7 @@ function scratchDirs() {
|
|
|
1040
1147
|
}
|
|
1041
1148
|
function realpath(p) {
|
|
1042
1149
|
try {
|
|
1043
|
-
return
|
|
1150
|
+
return fs7.realpathSync(p);
|
|
1044
1151
|
} catch {
|
|
1045
1152
|
return p;
|
|
1046
1153
|
}
|
|
@@ -1313,7 +1420,7 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
|
1313
1420
|
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
1314
1421
|
|
|
1315
1422
|
// src/core/refs.ts
|
|
1316
|
-
import
|
|
1423
|
+
import fs8 from "node:fs";
|
|
1317
1424
|
import path7 from "node:path";
|
|
1318
1425
|
var FILE_EXTENSIONS = new Set(
|
|
1319
1426
|
"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(" ")
|
|
@@ -1337,23 +1444,23 @@ function looksLikeSymbol(m) {
|
|
|
1337
1444
|
function extractRefs(answer) {
|
|
1338
1445
|
const seen = /* @__PURE__ */ new Map();
|
|
1339
1446
|
for (const line of answer.split("\n")) {
|
|
1340
|
-
const
|
|
1447
|
+
const matches2 = [...line.matchAll(REF)];
|
|
1341
1448
|
const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
|
|
1342
1449
|
const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
|
|
1343
1450
|
const idents = [...line.matchAll(IDENT)].filter((i) => {
|
|
1344
1451
|
if (!looksLikeSymbol(i)) return false;
|
|
1345
1452
|
const pos = i.index ?? 0;
|
|
1346
|
-
const before =
|
|
1347
|
-
const after =
|
|
1453
|
+
const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
|
|
1454
|
+
const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
|
|
1348
1455
|
return !(before && after && !sepBetween((before.index ?? 0) + before[0].length, after.index ?? 0));
|
|
1349
1456
|
});
|
|
1350
|
-
|
|
1457
|
+
matches2.forEach((m, k) => {
|
|
1351
1458
|
const start = Number(m[2]);
|
|
1352
1459
|
const end = m[3] ? Number(m[3]) : start;
|
|
1353
1460
|
if (start < 1 || end < start) return;
|
|
1354
1461
|
const at = m.index ?? 0;
|
|
1355
|
-
const prev =
|
|
1356
|
-
const next =
|
|
1462
|
+
const prev = matches2[k - 1];
|
|
1463
|
+
const next = matches2[k + 1];
|
|
1357
1464
|
const from = Math.max(at - 80, prev ? (prev.index ?? 0) + prev[0].length : 0, ...seps.filter((s) => s < at).map((s) => s + 1));
|
|
1358
1465
|
const to = Math.min(at + m[0].length + 40, next?.index ?? Infinity, ...seps.filter((s) => s >= at + m[0].length));
|
|
1359
1466
|
const near = new Set(idents.filter((i) => (i.index ?? 0) >= from && (i.index ?? 0) < to).map((i) => i[1]));
|
|
@@ -1370,8 +1477,8 @@ function verifyRefs(refs, dirs) {
|
|
|
1370
1477
|
const lineCache = /* @__PURE__ */ new Map();
|
|
1371
1478
|
const load = (file) => {
|
|
1372
1479
|
if (!lineCache.has(file)) {
|
|
1373
|
-
const size =
|
|
1374
|
-
const lines = size > MAX_BYTES ? null :
|
|
1480
|
+
const size = fs8.statSync(file).size;
|
|
1481
|
+
const lines = size > MAX_BYTES ? null : fs8.readFileSync(file, "utf8").split("\n");
|
|
1375
1482
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
1376
1483
|
lineCache.set(file, lines);
|
|
1377
1484
|
}
|
|
@@ -1429,7 +1536,7 @@ function indexFiles(roots) {
|
|
|
1429
1536
|
const walk = (dir) => {
|
|
1430
1537
|
let entries;
|
|
1431
1538
|
try {
|
|
1432
|
-
entries =
|
|
1539
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1433
1540
|
} catch {
|
|
1434
1541
|
return;
|
|
1435
1542
|
}
|
|
@@ -1439,8 +1546,8 @@ function indexFiles(roots) {
|
|
|
1439
1546
|
if (!SKIP_DIRS.has(e.name)) walk(path7.join(dir, e.name));
|
|
1440
1547
|
} else if (e.isFile()) {
|
|
1441
1548
|
count++;
|
|
1442
|
-
const
|
|
1443
|
-
if (
|
|
1549
|
+
const list2 = index.get(e.name);
|
|
1550
|
+
if (list2) list2.push(path7.join(dir, e.name));
|
|
1444
1551
|
else index.set(e.name, [path7.join(dir, e.name)]);
|
|
1445
1552
|
}
|
|
1446
1553
|
}
|
|
@@ -1452,7 +1559,7 @@ function resolve(ref, roots) {
|
|
|
1452
1559
|
const candidates = path7.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path7.join(r, ref));
|
|
1453
1560
|
return candidates.find((c) => {
|
|
1454
1561
|
try {
|
|
1455
|
-
return
|
|
1562
|
+
return fs8.statSync(c).isFile() && inside(c, roots);
|
|
1456
1563
|
} catch {
|
|
1457
1564
|
return false;
|
|
1458
1565
|
}
|
|
@@ -1467,7 +1574,7 @@ function inside(file, roots) {
|
|
|
1467
1574
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1468
1575
|
|
|
1469
1576
|
// src/core/templates.ts
|
|
1470
|
-
import
|
|
1577
|
+
import fs9 from "node:fs";
|
|
1471
1578
|
import path9 from "node:path";
|
|
1472
1579
|
|
|
1473
1580
|
// src/core/install.ts
|
|
@@ -1486,8 +1593,8 @@ var FILES = {
|
|
|
1486
1593
|
};
|
|
1487
1594
|
function loadTemplate(name, root = packageRoot()) {
|
|
1488
1595
|
const file = path9.join(root, "skills", FILES[name]);
|
|
1489
|
-
if (!
|
|
1490
|
-
return
|
|
1596
|
+
if (!fs9.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
|
|
1597
|
+
return fs9.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
1491
1598
|
}
|
|
1492
1599
|
function fill(template, values) {
|
|
1493
1600
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -1500,11 +1607,11 @@ var MAX_MS = 24 * 36e5;
|
|
|
1500
1607
|
var OVERLOAD_MS = 20 * 6e4;
|
|
1501
1608
|
var DAILY_MS = 4 * 36e5;
|
|
1502
1609
|
function retryAfterMs(message) {
|
|
1503
|
-
const
|
|
1610
|
+
const compact2 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
1504
1611
|
const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
|
|
1505
1612
|
let ms = 0;
|
|
1506
|
-
if (
|
|
1507
|
-
for (const [, n, unit] of
|
|
1613
|
+
if (compact2) {
|
|
1614
|
+
for (const [, n, unit] of compact2.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
|
|
1508
1615
|
} else if (words) {
|
|
1509
1616
|
ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
|
|
1510
1617
|
}
|
|
@@ -1544,7 +1651,7 @@ function parseAudit(text) {
|
|
|
1544
1651
|
}
|
|
1545
1652
|
|
|
1546
1653
|
// src/core/plan.ts
|
|
1547
|
-
import
|
|
1654
|
+
import fs10 from "node:fs";
|
|
1548
1655
|
import path10 from "node:path";
|
|
1549
1656
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
1550
1657
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
@@ -1593,8 +1700,8 @@ function parsePlan(text, file = "") {
|
|
|
1593
1700
|
}
|
|
1594
1701
|
function loadPlan(file) {
|
|
1595
1702
|
const abs = path10.resolve(file);
|
|
1596
|
-
if (!
|
|
1597
|
-
const plan = parsePlan(
|
|
1703
|
+
if (!fs10.existsSync(abs) || !fs10.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
|
|
1704
|
+
const plan = parsePlan(fs10.readFileSync(abs, "utf8"), fs10.realpathSync(abs));
|
|
1598
1705
|
if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
1599
1706
|
return plan;
|
|
1600
1707
|
}
|
|
@@ -1614,26 +1721,716 @@ function brief(plan, task) {
|
|
|
1614
1721
|
`;
|
|
1615
1722
|
}
|
|
1616
1723
|
var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
1724
|
+
|
|
1725
|
+
// src/vcs/guard.ts
|
|
1726
|
+
import fs11 from "node:fs";
|
|
1727
|
+
import path11 from "node:path";
|
|
1728
|
+
var VERSION = 2;
|
|
1729
|
+
var WINDOWS = process.platform === "win32";
|
|
1730
|
+
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
1731
|
+
var SHIM = `#!/bin/sh
|
|
1732
|
+
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
1733
|
+
# index or discard work. The primary agent reviews and commits, not the worker.
|
|
1734
|
+
set -f
|
|
1735
|
+
real="\${PITROOM_REAL_GIT:-}"
|
|
1736
|
+
if [ -z "$real" ] || [ ! -x "$real" ]; then
|
|
1737
|
+
echo "pitroom: git guard cannot find the real git" >&2
|
|
1738
|
+
exit 127
|
|
1739
|
+
fi
|
|
1740
|
+
block() {
|
|
1741
|
+
echo "pitroom: 'git $1' is blocked for workers (the primary agent reviews and commits)" >&2
|
|
1742
|
+
exit 1
|
|
1743
|
+
}
|
|
1744
|
+
sub=""; skip=""; after=""
|
|
1745
|
+
for a do
|
|
1746
|
+
if [ -n "$sub" ]; then after="$after $a"; continue; fi
|
|
1747
|
+
case "$skip" in
|
|
1748
|
+
c) skip=""; case "$a" in [Aa][Ll][Ii][Aa][Ss].*) block "-c $a" ;; esac; continue ;;
|
|
1749
|
+
v) skip=""; continue ;;
|
|
1750
|
+
esac
|
|
1751
|
+
case "$a" in
|
|
1752
|
+
-c|--config-env) skip=c ;;
|
|
1753
|
+
-C|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path) skip=v ;;
|
|
1754
|
+
-*) ;;
|
|
1755
|
+
*) sub="$a" ;;
|
|
1756
|
+
esac
|
|
1757
|
+
done
|
|
1758
|
+
[ -z "$sub" ] && exec "$real" "$@"
|
|
1759
|
+
|
|
1760
|
+
# Resolve aliases (also those injected via GIT_CONFIG_* env); shell aliases are refused.
|
|
1761
|
+
depth=0
|
|
1762
|
+
while def=$("$real" config --get "alias.$sub" 2>/dev/null) && [ -n "$def" ]; do
|
|
1763
|
+
depth=$((depth+1)); [ "$depth" -gt 5 ] && block "$sub (alias loop)"
|
|
1764
|
+
case "$def" in '!'*) block "$sub (shell alias)" ;; -*) block "$sub (alias with options)" ;; esac
|
|
1765
|
+
next=\${def%% *}
|
|
1766
|
+
[ "$next" != "$def" ] && after=" \${def#* }$after"
|
|
1767
|
+
sub="$next"
|
|
1768
|
+
done
|
|
1769
|
+
|
|
1770
|
+
has() { for f in "$@"; do case " $after " in *" $f "*|*" $f="*) return 0 ;; esac; done; return 1; }
|
|
1771
|
+
count() { n=0; for w in $after; do case "$w" in -*) ;; *) n=$((n+1)) ;; esac; done; echo "$n"; }
|
|
1772
|
+
|
|
1773
|
+
case "$sub" in
|
|
1774
|
+
commit|push|pull|reset|checkout|restore|clean|stash|rebase|merge|switch|cherry-pick|revert|am|\\
|
|
1775
|
+
worktree|update-ref|update-index|filter-branch|filter-repo|gc|prune|replace|notes|add|rm|mv|\\
|
|
1776
|
+
sparse-checkout|read-tree|commit-tree|write-tree|fast-import|bisect)
|
|
1777
|
+
block "$sub" ;;
|
|
1778
|
+
branch)
|
|
1779
|
+
has -d -D --delete -m -M --move -c -C --copy -f --force -u --set-upstream-to --unset-upstream --edit-description && block "branch$after"
|
|
1780
|
+
[ "$(count)" -gt 0 ] && ! has -l --list --contains --no-contains --merged --no-merged --points-at && block "branch$after" ;;
|
|
1781
|
+
tag)
|
|
1782
|
+
has -d --delete -a --annotate -s --sign -u --local-user -f --force -m --message -F --file && block "tag$after"
|
|
1783
|
+
[ "$(count)" -gt 0 ] && ! has -l --list --contains --no-contains --merged --no-merged --points-at && block "tag$after" ;;
|
|
1784
|
+
config)
|
|
1785
|
+
has --unset --unset-all --add --replace-all --rename-section --remove-section -e --edit set unset rename-section remove-section edit && block "config$after"
|
|
1786
|
+
[ "$(count)" -ge 2 ] && ! has get --get --get-all --get-regexp --get-urlmatch && block "config$after" ;;
|
|
1787
|
+
remote)
|
|
1788
|
+
for w in $after; do case "$w" in add|remove|rm|rename|set-url|set-head|set-branches|prune|update) block "remote $w" ;; esac; done ;;
|
|
1789
|
+
fetch)
|
|
1790
|
+
for w in $after; do case "$w" in -*) ;; *:*) block "fetch $w (writes a local ref)" ;; esac; done ;;
|
|
1791
|
+
apply)
|
|
1792
|
+
has --index --cached -3 --3way && block "apply$after" ;;
|
|
1793
|
+
reflog)
|
|
1794
|
+
has expire delete && block "reflog$after" ;;
|
|
1795
|
+
symbolic-ref)
|
|
1796
|
+
has -d --delete && block "symbolic-ref$after"
|
|
1797
|
+
[ "$(count)" -ge 2 ] && block "symbolic-ref$after" ;;
|
|
1798
|
+
submodule)
|
|
1799
|
+
for w in $after; do case "$w" in -*) ;; status|summary) break ;; *) block "submodule $w" ;; esac; done ;;
|
|
1800
|
+
esac
|
|
1801
|
+
exec "$real" "$@"
|
|
1802
|
+
`;
|
|
1803
|
+
function shimDir() {
|
|
1804
|
+
return path11.join(home(), "shim", `v${VERSION}`);
|
|
1805
|
+
}
|
|
1806
|
+
function findRealGit(skip) {
|
|
1807
|
+
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1808
|
+
for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
|
|
1809
|
+
if (same(path11.resolve(dir), skip)) continue;
|
|
1810
|
+
const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
|
|
1811
|
+
try {
|
|
1812
|
+
fs11.accessSync(p, fs11.constants.X_OK);
|
|
1813
|
+
if (fs11.statSync(p).isFile()) return p;
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return void 0;
|
|
1818
|
+
}
|
|
1819
|
+
function findGitSh(realGit) {
|
|
1820
|
+
let dir = path11.dirname(realGit);
|
|
1821
|
+
for (let i = 0; i < 4; i++) {
|
|
1822
|
+
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
1823
|
+
const p = path11.join(dir, rel);
|
|
1824
|
+
if (fs11.existsSync(p)) return p;
|
|
1825
|
+
}
|
|
1826
|
+
const up = path11.dirname(dir);
|
|
1827
|
+
if (up === dir) break;
|
|
1828
|
+
dir = up;
|
|
1829
|
+
}
|
|
1830
|
+
return void 0;
|
|
1831
|
+
}
|
|
1832
|
+
var cmdShim = (sh) => `@echo off\r
|
|
1833
|
+
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
1834
|
+
"${sh}" "%~dp0git" %*\r
|
|
1835
|
+
exit /b %errorlevel%\r
|
|
1836
|
+
`;
|
|
1837
|
+
var HOOK_VERSION = 1;
|
|
1838
|
+
var REF_HOOK = `#!/bin/sh
|
|
1839
|
+
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
1840
|
+
[ "$1" = prepared ] || exit 0
|
|
1841
|
+
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
1842
|
+
exit 1
|
|
1843
|
+
`;
|
|
1844
|
+
var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
1845
|
+
function writeIfChanged(file, content) {
|
|
1846
|
+
if (fs11.existsSync(file) && fs11.readFileSync(file, "utf8") === content) return;
|
|
1847
|
+
fs11.mkdirSync(path11.dirname(file), { recursive: true });
|
|
1848
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1849
|
+
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
1850
|
+
renameOver(tmp, file);
|
|
1851
|
+
}
|
|
1852
|
+
function pathKey(env) {
|
|
1853
|
+
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
1854
|
+
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
1855
|
+
}
|
|
1856
|
+
function onePath(env) {
|
|
1857
|
+
const key = pathKey(env);
|
|
1858
|
+
const out = { ...env };
|
|
1859
|
+
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
1860
|
+
return out;
|
|
1861
|
+
}
|
|
1862
|
+
function withGitConfig(env, entries) {
|
|
1863
|
+
const out = { ...env };
|
|
1864
|
+
let n = Number(env.GIT_CONFIG_COUNT) || 0;
|
|
1865
|
+
for (const [key, value] of entries) {
|
|
1866
|
+
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
1867
|
+
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
1868
|
+
n++;
|
|
1869
|
+
}
|
|
1870
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
function guardEnv(env) {
|
|
1874
|
+
const quiet = {
|
|
1875
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1876
|
+
GIT_EDITOR: "true",
|
|
1877
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
1878
|
+
GIT_PAGER: "cat",
|
|
1879
|
+
PAGER: "cat"
|
|
1880
|
+
};
|
|
1881
|
+
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
1882
|
+
env = withGitConfig(onePath(env), [
|
|
1883
|
+
["core.hooksPath", fwd(hooksDir())],
|
|
1884
|
+
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
1885
|
+
]);
|
|
1886
|
+
const dir = shimDir();
|
|
1887
|
+
const real = findRealGit(dir);
|
|
1888
|
+
if (!real) return { ...env, ...quiet };
|
|
1889
|
+
let sh;
|
|
1890
|
+
if (WINDOWS) {
|
|
1891
|
+
sh = findGitSh(real);
|
|
1892
|
+
if (!sh) return { ...env, ...quiet };
|
|
1893
|
+
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
1894
|
+
}
|
|
1895
|
+
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
1896
|
+
const key = pathKey(env);
|
|
1897
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
|
|
1898
|
+
}
|
|
1899
|
+
function shimReady() {
|
|
1900
|
+
const env = guardEnv({ ...process.env });
|
|
1901
|
+
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/core/slots.ts
|
|
1905
|
+
import crypto2 from "node:crypto";
|
|
1906
|
+
import fs12 from "node:fs";
|
|
1907
|
+
import path12 from "node:path";
|
|
1908
|
+
var slotsDir = () => path12.join(home(), "slots");
|
|
1909
|
+
var locksDir = () => path12.join(home(), "locks");
|
|
1910
|
+
var STARTUP_GRACE_MS = 3e4;
|
|
1911
|
+
function holderActive(runId, self) {
|
|
1912
|
+
if (!runId || runId === self) return false;
|
|
1913
|
+
try {
|
|
1914
|
+
const m = freshMeta(runId);
|
|
1915
|
+
if (!isActive(m.state)) return false;
|
|
1916
|
+
if (m.pid) return isAlive(m.pid);
|
|
1917
|
+
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
1918
|
+
} catch {
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function tryClaim(file, runId) {
|
|
1923
|
+
fs12.mkdirSync(path12.dirname(file), { recursive: true });
|
|
1924
|
+
try {
|
|
1925
|
+
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1926
|
+
return true;
|
|
1927
|
+
} catch (e) {
|
|
1928
|
+
if (e.code !== "EEXIST") throw e;
|
|
1929
|
+
}
|
|
1930
|
+
let owner = "";
|
|
1931
|
+
try {
|
|
1932
|
+
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
if (owner === runId) return true;
|
|
1936
|
+
if (holderActive(owner, runId)) return false;
|
|
1937
|
+
fs12.rmSync(file, { force: true });
|
|
1938
|
+
try {
|
|
1939
|
+
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1940
|
+
return true;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function releaseIfOwner(file, runId) {
|
|
1946
|
+
if (!file) return;
|
|
1947
|
+
try {
|
|
1948
|
+
if (fs12.readFileSync(file, "utf8").trim() === runId) fs12.rmSync(file, { force: true });
|
|
1949
|
+
} catch {
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function tryAcquireSlot(runId, maxParallel) {
|
|
1953
|
+
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
1954
|
+
const file = path12.join(slotsDir(), `slot-${n}`);
|
|
1955
|
+
if (tryClaim(file, runId)) return file;
|
|
1956
|
+
}
|
|
1957
|
+
return void 0;
|
|
1958
|
+
}
|
|
1959
|
+
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
1960
|
+
function slotHolders() {
|
|
1961
|
+
if (!fs12.existsSync(slotsDir())) return [];
|
|
1962
|
+
return fs12.readdirSync(slotsDir()).map((f) => {
|
|
1963
|
+
try {
|
|
1964
|
+
return fs12.readFileSync(path12.join(slotsDir(), f), "utf8").trim();
|
|
1965
|
+
} catch {
|
|
1966
|
+
return "";
|
|
1967
|
+
}
|
|
1968
|
+
}).filter((id) => holderActive(id, ""));
|
|
1969
|
+
}
|
|
1970
|
+
var lockFile = (repoRoot) => path12.join(locksDir(), `write-${crypto2.createHash("sha1").update(path12.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
|
|
1971
|
+
function acquireWriteLock(repoRoot, runId) {
|
|
1972
|
+
const file = lockFile(repoRoot);
|
|
1973
|
+
if (tryClaim(file, runId)) return;
|
|
1974
|
+
let owner = "";
|
|
1975
|
+
try {
|
|
1976
|
+
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
1980
|
+
}
|
|
1981
|
+
var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
|
|
1982
|
+
|
|
1983
|
+
// src/core/secrets.ts
|
|
1984
|
+
import { execFileSync } from "node:child_process";
|
|
1985
|
+
import fs13 from "node:fs";
|
|
1986
|
+
import path13 from "node:path";
|
|
1987
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
1988
|
+
var MAX_VISITED = 2e4;
|
|
1989
|
+
var MAX_DEPTH = 4;
|
|
1990
|
+
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
1991
|
+
function looksSecret(file) {
|
|
1992
|
+
const name = file.toLowerCase();
|
|
1993
|
+
if (TEMPLATE.test(name)) return false;
|
|
1994
|
+
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
1995
|
+
return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
|
|
1996
|
+
}
|
|
1997
|
+
function findSecretFiles(dir) {
|
|
1998
|
+
const found = [];
|
|
1999
|
+
let visited = 0;
|
|
2000
|
+
const walk = (d, depth) => {
|
|
2001
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2002
|
+
let entries;
|
|
2003
|
+
try {
|
|
2004
|
+
entries = fs13.readdirSync(d, { withFileTypes: true });
|
|
2005
|
+
} catch {
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
for (const e of entries) {
|
|
2009
|
+
visited++;
|
|
2010
|
+
if (e.isDirectory()) {
|
|
2011
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2012
|
+
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2013
|
+
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
walk(dir, 0);
|
|
2018
|
+
return found.sort();
|
|
2019
|
+
}
|
|
2020
|
+
function findSecretFilesInTree(root, dir) {
|
|
2021
|
+
try {
|
|
2022
|
+
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2023
|
+
return out.split("\0").filter((f) => f && looksSecret(path13.basename(f)) && !path13.relative(dir, path13.join(root, f)).startsWith("..")).map((f) => slash(path13.relative(dir, path13.join(root, f)))).sort();
|
|
2024
|
+
} catch {
|
|
2025
|
+
return [];
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function secretWarning(files, mode) {
|
|
2029
|
+
if (!files.length) return void 0;
|
|
2030
|
+
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
2031
|
+
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
2032
|
+
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
2033
|
+
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
2034
|
+
}
|
|
2035
|
+
var slash = (p) => p.split(path13.sep).join("/");
|
|
2036
|
+
|
|
2037
|
+
// src/core/receipt.ts
|
|
2038
|
+
import fs16 from "node:fs";
|
|
2039
|
+
|
|
2040
|
+
// src/core/config.ts
|
|
2041
|
+
import fs14 from "node:fs";
|
|
2042
|
+
import os7 from "node:os";
|
|
2043
|
+
import path14 from "node:path";
|
|
2044
|
+
var SCHEMA = {
|
|
2045
|
+
worker: "string",
|
|
2046
|
+
fallback: "string[]",
|
|
2047
|
+
timeout: "string",
|
|
2048
|
+
primary: "string",
|
|
2049
|
+
price: "string",
|
|
2050
|
+
link: "string[]",
|
|
2051
|
+
web: "boolean",
|
|
2052
|
+
maxParallel: "number",
|
|
2053
|
+
models: "record",
|
|
2054
|
+
tiers: "record",
|
|
2055
|
+
costs: "numbers",
|
|
2056
|
+
audit: "number",
|
|
2057
|
+
readIn: "string",
|
|
2058
|
+
cacheDays: "number",
|
|
2059
|
+
countRateLimits: "boolean",
|
|
2060
|
+
mcpDash: "boolean",
|
|
2061
|
+
notify: "boolean",
|
|
2062
|
+
notifyCommand: "string",
|
|
2063
|
+
notifyAfter: "number",
|
|
2064
|
+
workerPrices: "record",
|
|
2065
|
+
priceFeed: "boolean",
|
|
2066
|
+
priceFeedUrl: "string",
|
|
2067
|
+
priceFeedHours: "number"
|
|
2068
|
+
};
|
|
2069
|
+
function configPath() {
|
|
2070
|
+
if (process.env.PITROOM_CONFIG) return path14.resolve(process.env.PITROOM_CONFIG);
|
|
2071
|
+
const base = process.platform === "win32" ? process.env.APPDATA ?? path14.join(os7.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path14.join(os7.homedir(), ".config");
|
|
2072
|
+
return path14.join(base, "pitroom", "config.json");
|
|
2073
|
+
}
|
|
2074
|
+
var cached;
|
|
2075
|
+
function loadConfig() {
|
|
2076
|
+
if (cached) return cached;
|
|
2077
|
+
const file = configPath();
|
|
2078
|
+
const config = {};
|
|
2079
|
+
const warnings = [];
|
|
2080
|
+
if (fs14.existsSync(file)) {
|
|
2081
|
+
let raw;
|
|
2082
|
+
try {
|
|
2083
|
+
raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
2084
|
+
} catch (e) {
|
|
2085
|
+
warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
|
|
2086
|
+
}
|
|
2087
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
2088
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2089
|
+
const type = SCHEMA[key];
|
|
2090
|
+
if (!type) {
|
|
2091
|
+
warnings.push(`${file}: unknown key "${key}" ignored`);
|
|
2092
|
+
} else if (matches(value, type)) {
|
|
2093
|
+
config[key] = value;
|
|
2094
|
+
} else {
|
|
2095
|
+
warnings.push(`${file}: "${key}" must be ${type}; ignored`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
cached = { config, warnings };
|
|
2101
|
+
return cached;
|
|
2102
|
+
}
|
|
2103
|
+
function matches(v, type) {
|
|
2104
|
+
if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
2105
|
+
if (type === "record") {
|
|
2106
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
|
|
2107
|
+
}
|
|
2108
|
+
if (type === "numbers") {
|
|
2109
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
2110
|
+
}
|
|
2111
|
+
if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2112
|
+
return typeof v === type;
|
|
2113
|
+
}
|
|
2114
|
+
var DEFAULT_PARALLEL = 20;
|
|
2115
|
+
var MAX_PARALLEL_LIMIT = 30;
|
|
2116
|
+
var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
|
|
2117
|
+
var positiveInt = (v) => {
|
|
2118
|
+
const n = Number(v);
|
|
2119
|
+
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2120
|
+
};
|
|
2121
|
+
var rate = (v) => {
|
|
2122
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2123
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
2124
|
+
};
|
|
2125
|
+
var days = (v) => {
|
|
2126
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2127
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
2128
|
+
};
|
|
2129
|
+
var flag01 = (v) => {
|
|
2130
|
+
const t = v?.trim().toLowerCase();
|
|
2131
|
+
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
2132
|
+
};
|
|
2133
|
+
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2134
|
+
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2135
|
+
function setting(flag, env, conf, fallback) {
|
|
2136
|
+
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2137
|
+
if (env !== void 0) return { value: env, source: "env" };
|
|
2138
|
+
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2139
|
+
return { value: fallback, source: "default" };
|
|
2140
|
+
}
|
|
2141
|
+
function effective(flags = {}) {
|
|
2142
|
+
const c = loadConfig().config;
|
|
2143
|
+
const e = process.env;
|
|
2144
|
+
return {
|
|
2145
|
+
worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
|
|
2146
|
+
/** Overrides the model of the preferred worker only. */
|
|
2147
|
+
model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
|
|
2148
|
+
fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
|
|
2149
|
+
timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
|
|
2150
|
+
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2151
|
+
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2152
|
+
link: setting(void 0, void 0, c.link, []),
|
|
2153
|
+
web: setting(void 0, void 0, c.web, false),
|
|
2154
|
+
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2155
|
+
models: setting(void 0, void 0, c.models, {}),
|
|
2156
|
+
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2157
|
+
costs: setting(void 0, void 0, c.costs, {}),
|
|
2158
|
+
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2159
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2160
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2161
|
+
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2162
|
+
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2163
|
+
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2164
|
+
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2165
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2166
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2167
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2168
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2169
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/core/prices.ts
|
|
2174
|
+
import fs15 from "node:fs";
|
|
2175
|
+
import path15 from "node:path";
|
|
2176
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2177
|
+
var catalogFile = () => path15.join(home(), "prices.json");
|
|
2178
|
+
function trim(raw) {
|
|
2179
|
+
const out = {};
|
|
2180
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2181
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2182
|
+
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2183
|
+
const c = m?.cost;
|
|
2184
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2185
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2186
|
+
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return out;
|
|
2190
|
+
}
|
|
2191
|
+
var cached2;
|
|
2192
|
+
function readCatalog() {
|
|
2193
|
+
const file = catalogFile();
|
|
2194
|
+
try {
|
|
2195
|
+
const { mtimeMs } = fs15.statSync(file);
|
|
2196
|
+
if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2197
|
+
const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2198
|
+
const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2199
|
+
cached2 = { file, mtimeMs, catalog: catalog5 };
|
|
2200
|
+
return catalog5;
|
|
2201
|
+
} catch {
|
|
2202
|
+
return void 0;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2206
|
+
function catalogPrice(backend, ...models) {
|
|
2207
|
+
const catalog5 = readCatalog();
|
|
2208
|
+
if (!catalog5) return void 0;
|
|
2209
|
+
for (const raw of models) {
|
|
2210
|
+
const id = raw?.split("#")[0];
|
|
2211
|
+
if (!id) continue;
|
|
2212
|
+
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2213
|
+
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2214
|
+
const triple = any ? catalog5.models[any] : void 0;
|
|
2215
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2216
|
+
const [input, output, cachedInput] = triple;
|
|
2217
|
+
return { input, output, cachedInput, key: any };
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return void 0;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// src/core/receipt.ts
|
|
2224
|
+
var PRESETS = {
|
|
2225
|
+
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2226
|
+
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2227
|
+
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2228
|
+
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2229
|
+
};
|
|
2230
|
+
function primaryPrice() {
|
|
2231
|
+
const eff = effective();
|
|
2232
|
+
const custom = eff.price.value?.split(",").map(Number);
|
|
2233
|
+
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2234
|
+
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2235
|
+
}
|
|
2236
|
+
const named = eff.primary.value;
|
|
2237
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2238
|
+
if (preset) return preset;
|
|
2239
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2240
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2241
|
+
}
|
|
2242
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2243
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2244
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2245
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2246
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2247
|
+
}
|
|
2248
|
+
var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
|
|
2249
|
+
var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
|
|
2250
|
+
function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
2251
|
+
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2252
|
+
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2253
|
+
}
|
|
2254
|
+
function readLedger(sinceMs) {
|
|
2255
|
+
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2256
|
+
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2257
|
+
try {
|
|
2258
|
+
return [JSON.parse(l)];
|
|
2259
|
+
} catch {
|
|
2260
|
+
return [];
|
|
2261
|
+
}
|
|
2262
|
+
}).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
|
|
2263
|
+
}
|
|
2264
|
+
function totals(entries) {
|
|
2265
|
+
const t = entries.reduce(
|
|
2266
|
+
(a, e) => ({
|
|
2267
|
+
runs: a.runs + 1,
|
|
2268
|
+
tokens: a.tokens + e.tokens,
|
|
2269
|
+
returned: a.returned + e.returned,
|
|
2270
|
+
workerCost: a.workerCost + e.workerCost,
|
|
2271
|
+
saved: a.saved + e.saved
|
|
2272
|
+
}),
|
|
2273
|
+
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2274
|
+
);
|
|
2275
|
+
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2276
|
+
}
|
|
2277
|
+
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2278
|
+
function compact(n) {
|
|
2279
|
+
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2280
|
+
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2281
|
+
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2282
|
+
return String(Math.round(n));
|
|
2283
|
+
}
|
|
2284
|
+
function badgeUrl(t) {
|
|
2285
|
+
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2286
|
+
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2287
|
+
}
|
|
2288
|
+
var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
2289
|
+
function card(t, period) {
|
|
2290
|
+
const W = 720, H = 316, M = 32, GAP = 12;
|
|
2291
|
+
const tile = (i, value, label) => {
|
|
2292
|
+
const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
|
|
2293
|
+
return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
|
|
2294
|
+
<text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
|
|
2295
|
+
};
|
|
2296
|
+
const when = period.toUpperCase();
|
|
2297
|
+
const pill = 22 + when.length * 7.4;
|
|
2298
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
|
|
2299
|
+
<style>
|
|
2300
|
+
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
|
|
2301
|
+
.name{font-size:20px;font-weight:650;letter-spacing:-.01em}
|
|
2302
|
+
.sub{font-size:12.5px;fill:#a1a1aa}
|
|
2303
|
+
.pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
|
|
2304
|
+
.lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
|
|
2305
|
+
.big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
|
|
2306
|
+
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2307
|
+
.foot{font-size:11.5px;fill:#71717a}
|
|
2308
|
+
</style>
|
|
2309
|
+
<defs>
|
|
2310
|
+
<radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
|
|
2311
|
+
<radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
|
|
2312
|
+
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2313
|
+
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2314
|
+
</defs>
|
|
2315
|
+
<g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
|
|
2316
|
+
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2317
|
+
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2318
|
+
<path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
|
|
2319
|
+
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2320
|
+
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2321
|
+
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2322
|
+
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2323
|
+
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2324
|
+
${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
|
|
2325
|
+
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2326
|
+
</svg>
|
|
2327
|
+
`;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/core/process.ts
|
|
2331
|
+
import { spawn } from "node:child_process";
|
|
2332
|
+
import fs17 from "node:fs";
|
|
2333
|
+
async function spawnWorker(inv, opts) {
|
|
2334
|
+
const out = fs17.openSync(opts.stdoutFile, "w");
|
|
2335
|
+
const err = fs17.openSync(opts.stderrFile, "w");
|
|
2336
|
+
const res = { code: null, timedOut: false, stopped: false };
|
|
2337
|
+
const child = spawn(inv.command, inv.args, {
|
|
2338
|
+
cwd: opts.cwd,
|
|
2339
|
+
stdio: ["ignore", out, err],
|
|
2340
|
+
// OpenCode v2 takes its workspace from $PWD, not the process cwd: without this a worker
|
|
2341
|
+
// started in an isolated copy would read and edit the directory pitroom was launched from.
|
|
2342
|
+
env: guardEnv({ ...process.env, ...inv.env, PWD: opts.cwd, PITROOM_ACTIVE: "1" })
|
|
2343
|
+
});
|
|
2344
|
+
const stop = () => {
|
|
2345
|
+
res.stopped = true;
|
|
2346
|
+
child.kill("SIGTERM");
|
|
2347
|
+
};
|
|
2348
|
+
process.once("SIGINT", stop);
|
|
2349
|
+
process.once("SIGTERM", stop);
|
|
2350
|
+
let killer;
|
|
2351
|
+
const timer = setTimeout(() => {
|
|
2352
|
+
res.timedOut = true;
|
|
2353
|
+
child.kill("SIGTERM");
|
|
2354
|
+
killer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
2355
|
+
}, opts.timeoutSec * 1e3);
|
|
2356
|
+
res.code = await new Promise((resolve2) => {
|
|
2357
|
+
child.on("error", (e) => {
|
|
2358
|
+
res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2359
|
+
resolve2(127);
|
|
2360
|
+
});
|
|
2361
|
+
child.on("close", (c) => resolve2(c));
|
|
2362
|
+
});
|
|
2363
|
+
clearTimeout(timer);
|
|
2364
|
+
if (killer) clearTimeout(killer);
|
|
2365
|
+
process.off("SIGINT", stop);
|
|
2366
|
+
process.off("SIGTERM", stop);
|
|
2367
|
+
fs17.closeSync(out);
|
|
2368
|
+
fs17.closeSync(err);
|
|
2369
|
+
return res;
|
|
2370
|
+
}
|
|
1617
2371
|
export {
|
|
1618
2372
|
DEFAULT_BACKEND,
|
|
2373
|
+
STOP_FILE,
|
|
2374
|
+
TERMINAL,
|
|
2375
|
+
acquireWriteLock,
|
|
1619
2376
|
allBackends,
|
|
1620
2377
|
backendIds,
|
|
2378
|
+
badgeUrl,
|
|
1621
2379
|
brief,
|
|
2380
|
+
card,
|
|
2381
|
+
catalogPrice,
|
|
2382
|
+
compact,
|
|
1622
2383
|
cooldownMs,
|
|
2384
|
+
costAt,
|
|
1623
2385
|
describeTarget,
|
|
2386
|
+
estimateTokens,
|
|
1624
2387
|
extractRefs,
|
|
1625
2388
|
fill,
|
|
2389
|
+
findSecretFiles,
|
|
2390
|
+
findSecretFilesInTree,
|
|
1626
2391
|
formatTarget,
|
|
2392
|
+
freshMeta,
|
|
1627
2393
|
getBackend,
|
|
2394
|
+
guardEnv,
|
|
2395
|
+
home,
|
|
2396
|
+
isActive,
|
|
2397
|
+
isAlive,
|
|
2398
|
+
listRunIds,
|
|
1628
2399
|
loadPlan,
|
|
1629
2400
|
loadTemplate,
|
|
2401
|
+
looksSecret,
|
|
2402
|
+
newRunId,
|
|
1630
2403
|
parseAudit,
|
|
1631
2404
|
parsePlan,
|
|
2405
|
+
parsePriceSpec,
|
|
1632
2406
|
parseStatus,
|
|
1633
2407
|
parseTarget,
|
|
1634
2408
|
parseVerdict,
|
|
1635
2409
|
planName,
|
|
1636
2410
|
planTask,
|
|
2411
|
+
primaryPrice,
|
|
2412
|
+
readCatalog,
|
|
2413
|
+
readLedger,
|
|
2414
|
+
readMeta,
|
|
2415
|
+
releaseSlot,
|
|
2416
|
+
releaseWriteLock,
|
|
2417
|
+
requestStop,
|
|
2418
|
+
resolveRun,
|
|
1637
2419
|
retryAfterMs,
|
|
1638
|
-
|
|
2420
|
+
runDir,
|
|
2421
|
+
runFile,
|
|
2422
|
+
runsDir,
|
|
2423
|
+
savedUsd,
|
|
2424
|
+
secretWarning,
|
|
2425
|
+
shimDir,
|
|
2426
|
+
shimReady,
|
|
2427
|
+
slotHolders,
|
|
2428
|
+
spawnWorker,
|
|
2429
|
+
totals,
|
|
2430
|
+
trim,
|
|
2431
|
+
tryAcquireSlot,
|
|
2432
|
+
usd,
|
|
2433
|
+
verifyRefs,
|
|
2434
|
+
workerCostOf,
|
|
2435
|
+
writeMeta
|
|
1639
2436
|
};
|