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/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 cached = num2(u.cached_input_tokens);
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) - cached);
386
- usage.cacheRead += cached;
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 fs4 from "node:fs";
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 cached = num3(s.cached);
649
- usage.cacheRead += cached;
650
- usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached);
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(fs4.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
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 (fs4.readFileSync(file, "utf8") === body) return root;
800
+ if (fs6.readFileSync(file, "utf8") === body) return root;
694
801
  } catch {
695
802
  }
696
- fs4.mkdirSync(dir, { recursive: true });
697
- fs4.writeFileSync(file, body);
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 (fs4.existsSync(from) && !fs4.existsSync(to)) fs4.symlinkSync(from, to);
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 (fs4.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
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(fs4.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
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 = fs4.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
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 fs5 from "node:fs";
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 fs5.realpathSync(p);
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 fs6 from "node:fs";
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 matches = [...line.matchAll(REF)];
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 = matches.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
1347
- const after = matches.find((m) => (m.index ?? 0) >= pos + i[0].length);
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
- matches.forEach((m, k) => {
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 = matches[k - 1];
1356
- const next = matches[k + 1];
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 = fs6.statSync(file).size;
1374
- const lines = size > MAX_BYTES ? null : fs6.readFileSync(file, "utf8").split("\n");
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 = fs6.readdirSync(dir, { withFileTypes: true });
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 list = index.get(e.name);
1443
- if (list) list.push(path7.join(dir, e.name));
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 fs6.statSync(c).isFile() && inside(c, roots);
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 fs7 from "node:fs";
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 (!fs7.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
1490
- return fs7.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
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 compact = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
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 (compact) {
1507
- for (const [, n, unit] of compact.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
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 fs8 from "node:fs";
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 (!fs8.existsSync(abs) || !fs8.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
1597
- const plan = parsePlan(fs8.readFileSync(abs, "utf8"), fs8.realpathSync(abs));
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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
- verifyRefs
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
  };