pitroom 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pitroom",
3
3
  "description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
4
- "version": "0.20.0",
4
+ "version": "0.21.0",
5
5
  "author": {
6
6
  "name": "TahaAtas"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pitroom",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "A free pit crew for your expensive coding agent: delegate research, fixes, tests, reviews and parallel investigations to cheap worker agents running your own models, with verified answers, exact diffs and cost receipts.",
5
5
  "author": {
6
6
  "name": "TahaAtas"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Windows
6
+ - **A real git guard on Windows.** Until now the guard (a worker may not commit, reset, push, `git add`, `clean` and so on) was a `sh` script and Windows had none. Now the ref and push guard (git's own env config with a `reference-transaction` hook) is on there too, and the `git` shim runs on the `sh.exe` that Git for Windows brings (`git.cmd` for cmd.exe and PowerShell, `git` for Git Bash). A worker that starts git without a shell skips the shim and is left with the config layer. The guard tests, which were skipped on Windows, run there now; `pitroom doctor` reports the shim as ready or says what is missing.
7
+
8
+ ### Costs
9
+ - **A price catalog that keeps itself up to date (opt-in).** `"priceFeed": true` (or `PITROOM_PRICE_FEED=1`): at most once a day, in a background process after a run (a run never waits for it), Pitroom fetches the public models.dev price list (the one OpenCode reads; USD per 1M tokens of about 8,000 models) and keeps a trimmed copy (about 400 KB) in its state directory; a failed or implausible answer keeps the old copy and is not retried for an hour. It is a fallback behind your own `workerPrices`, and the receipt says which was used (`estimated from models.dev prices`). `"primary"` can then be any model id of the catalog for the savings comparison, so it no longer goes stale (before: four fixed presets). New command `pitroom prices [--refresh] [--json]`, a doctor line, and settings `priceFeedUrl` and `priceFeedHours`. **Off by default**, because until now Pitroom's own code made no network request at all: PRIVACY.md says what the request carries (nothing about you; a plain GET) and what it shows (your IP address to models.dev).
10
+ - **Prices for the CLIs that report no cost.** Codex and Gemini give tokens but no cost, so a run's `worker cost` was `n/a` and counted as free in the savings, which overstated them. The new config `"workerPrices": {"codex:gpt-6.1-sol": "1.25,10,0.125"}` (USD per 1M tokens, "in,out[,cachedIn]", keyed `backend:model` or `backend`) lets you say what they cost: the cost is then estimated from the tokens, shown as `~$0.016 (estimated from your workerPrices)` in the receipt and marked estimated in the dashboard, kept in the ledger and history, and taken off the savings. A cost the CLI reported always wins. `pitroom doctor` warns about a worker in your chain that reports no cost and has no price. Pitroom still knows no vendor prices and fetches none: the numbers are yours.
11
+
3
12
  ## 0.20.0
4
13
 
5
14
  ### Notifications
package/PRIVACY.md CHANGED
@@ -6,7 +6,9 @@ Pitroom is a command-line tool and a set of agent skills that run on your own co
6
6
 
7
7
  ## What the Pitroom authors collect
8
8
 
9
- Nothing. Pitroom has no accounts, no analytics and no telemetry, and its own code makes no network requests. No data is sent to the authors.
9
+ Nothing. Pitroom has no accounts, no analytics and no telemetry, and by default its own code makes no network requests. No data is sent to the authors.
10
+
11
+ **One request you can turn on.** With `"priceFeed": true` in the config (or `PITROOM_PRICE_FEED=1`; off by default) Pitroom keeps a price list of AI models up to date: at most once a day, in a background process after a run, it sends a plain `GET` for one public file (`https://models.dev/api.json` unless you set `priceFeedUrl`) and keeps a trimmed copy in its state directory. The request carries no data about you, your code, your tasks or your runs, and no cookie or account; like any web request it shows your IP address to the server that answers it. Nothing is fetched while the feed is off, and `pitroom prices` shows what it holds.
10
12
 
11
13
  ## Data categories and purposes
12
14
 
package/README.md CHANGED
@@ -276,7 +276,7 @@ To repeat it on your own repository: `pitroom crew -d <repo> -g bench "<question
276
276
 
277
277
  macOS and Linux are the main platforms. **Windows** runs the same test suite in CI (Windows Server, Node 22 and 24, with the fake workers; the worker CLIs themselves are not run there), with these differences:
278
278
 
279
- - **No git guard.** Pitroom's shim that blocks a worker's history changes and pushes is a `sh` script and is not available on Windows; `pitroom doctor` says so. Read-only runs are still limited by each worker CLI's own rules and `--isolate` keeps edits in a copy, so prefer `--isolate` and check the patch before `apply`.
279
+ - **The git guard** is the same two layers as elsewhere, built on the `sh.exe` that Git for Windows ships: git's own config (a `reference-transaction` hook that refuses every ref update, and push URLs that go nowhere) is always on; the `git` shim (`git.cmd` for cmd.exe and PowerShell, `git` for Git Bash) adds friendly refusals and also blocks `add`, `restore`, `clean` and the rest. A worker that starts git without a shell (a plain `CreateProcess`) skips the shim, and is left with the config layer. `pitroom doctor` says which is in effect. It is verified in CI with Git for Windows, not yet with the worker CLIs on a real Windows machine.
280
280
  - **The launcher is `pitroom.cmd`** (`%USERPROFILE%\.local\bin`), started with the Node that ran `pitroom install` or the one in `PITROOM_NODE`; it does not search for another Node.
281
281
  - **Line endings.** Pitroom compares and applies patches byte for byte. Keep `core.autocrlf` off (or an `eol=lf` in `.gitattributes`, as this repository has); a checkout converted to CRLF has not been tested with `apply`.
282
282
  - A read snapshot's files are not made read-only (Windows has no POSIX modes), and a `--verify` command runs in `cmd.exe`, which reports a missing command as `is not recognized`.
@@ -678,7 +678,7 @@ codex gpt-6-sol low/medium*/high/xhigh/max/ultra 1 1 24k
678
678
  claude haiku low/medium/high/xhigh/max ? - - - -
679
679
  ```
680
680
 
681
- Pitroom cannot know vendor prices and does not fetch them, so a cost is what you enter: `"costs": {"codex:gpt-6-sol": 1, "codex:gpt-6.1-sol": 2}` in the config, in any unit (they are only compared). `pitroom doctor` then prints the cost of the models in use and says when you priced a cheaper one of the same worker. A target that names only an effort (`codex:#low`) uses the model from `models`; a Codex or Claude Code worker with no pinned model gets a warning, because it would run the vendor's own default, which can change and cost more. `--effort LEVEL` sets the level for one run; your agent picks model and level from `pitroom models` (cheapest that fits: `low` for lookups, `medium` for ordinary changes, `high` for reviews).
681
+ Pitroom knows no vendor prices of its own and, unless you turn on `priceFeed` (see the config notes), fetches none, so a cost is what you enter: `"costs": {"codex:gpt-6-sol": 1, "codex:gpt-6.1-sol": 2}` in the config, in any unit (they are only compared; dollar prices for the receipts of Codex and Gemini go in `workerPrices`, see the config notes). `pitroom doctor` then prints the cost of the models in use and says when you priced a cheaper one of the same worker. A target that names only an effort (`codex:#low`) uses the model from `models`; a Codex or Claude Code worker with no pinned model gets a warning, because it would run the vendor's own default, which can change and cost more. `--effort LEVEL` sets the level for one run; your agent picks model and level from `pitroom models` (cheapest that fits: `low` for lookups, `medium` for ordinary changes, `high` for reviews).
682
682
 
683
683
  Fallbacks cross backends (e.g. `"fallback": ["codex:#low", "opencode"]`): a worker that is rate-limited, logged out or missing its model hands the task to the next. Follow-ups (`--continue`) always stay on the worker that owns the session. Writing an adapter: [docs/backends.md](docs/backends.md). A model that says "rate limited" (a daily quota used up, an overloaded provider) is remembered for the time its message gives, or a guess: the next runs skip it while a fallback is left, so they do not each wait for it to fail. `pitroom cooldown` lists what is skipped and until when, `--clear` tries it again, and `doctor` shows it too.
684
684
 
@@ -730,6 +730,8 @@ Or put defaults in `~/.config/pitroom/config.json` (flags and env still win); `p
730
730
  - `tiers`: `cheap`, `standard` and `capable` name the workers for plan tasks and `--tier`; an optional `review` tier names who reviews a run (by default another worker than the implementer's, `standard` first).
731
731
  - `audit`: the chance (0 to 1, default 0 = off) that a finished read run is re-checked in the background, see [Audits](#audits). The `audit` tier (else `cheap`) names who does it.
732
732
  - `costs`: your relative cost per `worker:model`, only compared with each other; `maxParallel`: workers at once (default 20, at most 30).
733
+ - `workerPrices`: USD per 1M tokens of a worker model, for the CLIs that report no cost (Codex, Gemini): `"workerPrices": {"codex:gpt-6.1-sol": "1.25,10,0.125"}` is "in,out[,cachedIn]", keyed `backend:model` (the model that ran, else the one you asked for) or `backend` alone. Without one the receipt says `worker cost n/a` and the run counts as free in the savings, which overstates them (`pitroom doctor` warns); with one the cost is estimated from the tokens, shown as `~$0.016 (estimated from your workerPrices)`, kept in the ledger, history and dashboard, and taken off the savings. Pitroom knows no vendor prices and does not fetch them, so this is your number.
734
+ - `priceFeed`: keep a price catalog up to date (default `false`; `PITROOM_PRICE_FEED=1`). The catalog is the public [models.dev](https://models.dev) list OpenCode also reads (USD per 1M tokens of about 8,000 models): at most once a day (`priceFeedHours`, default 24), in a background process after a run, never while a run waits, Pitroom sends a plain `GET` for that one file (`priceFeedUrl` / `PITROOM_PRICE_FEED_URL` to use a mirror, `priceFeedHours` / `PITROOM_PRICE_FEED_HOURS` for the interval) and keeps a trimmed copy (about 400 KB) in its state directory; a failed refresh keeps the old copy and is not retried for an hour. It is only a fallback: your `workerPrices` come first, and the receipt says which was used (`estimated from models.dev prices`). With it, `"primary": "<model id>"` can name any model of the catalog for the savings comparison (`"primary": "claude-opus-5-5"`), where without it only the four presets exist. `pitroom prices` shows the feed, the catalog and the price each worker in use gets; `pitroom prices --refresh` fetches now. What it sends: [PRIVACY.md](PRIVACY.md). The listed prices are API list prices: a worker on a subscription (Codex with a ChatGPT account) does not pay them, so for such a worker the estimate is what the same tokens would cost on the API.
733
735
  - `notify`: a desktop notification when a run that went to the background (`--bg`, a crew, an MCP call) has ended, so a long job does not have to be watched (default `false`; `PITROOM_NOTIFY=1`). Runs that went well but took less than `notifyAfter` seconds (default 15; `PITROOM_NOTIFY_AFTER`) and audits are not announced (a failure always is), and a crew says so once, when its last run ends, judged as a whole. A worker process that is killed outright never gets to say so. The built-in notifier uses `osascript` on macOS and `notify-send` on Linux (none for Windows yet); `notifyCommand` runs a command of your own instead, in a shell, with the text in `PITROOM_NOTIFY_TITLE` and `PITROOM_NOTIFY_BODY` (and `PITROOM_NOTIFY_RUN`, `PITROOM_NOTIFY_STATE`): for example `"notifyCommand": "curl -s -d \"$PITROOM_NOTIFY_BODY\" ntfy.sh/my-topic"`.
734
736
  - `mcpDash`: whether a run that an MCP client starts, and that is still going after a few seconds, also starts the dashboard and returns its address (default `true`; `PITROOM_MCP_DASH=0` or `"mcpDash": false` turns it off), see [the MCP server](#1-install-pick-your-agent).
735
737
  - `countRateLimits`: whether runs that failed on a rate limit or quota count in `pitroom history stats` and the dashboard's Stats (default `false`: they are left out of the run counts, success rates, times and tokens and shown apart as rate-limited, since they say nothing about the worker's work; `true` counts them as not ok).
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 cached2 = 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) - cached2);
386
- usage.cacheRead += cached2;
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;
@@ -752,9 +752,9 @@ function parseEvents3(jsonl) {
752
752
  if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
753
753
  } else if (e.type === "result") {
754
754
  const s = e.stats ?? {};
755
- const cached2 = num3(s.cached);
756
- usage.cacheRead += cached2;
757
- usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached2);
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);
758
758
  usage.output += num3(s.output_tokens);
759
759
  usage.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
760
760
  usage.toolCalls = Math.max(usage.toolCalls, num3(s.tool_calls));
@@ -1725,7 +1725,9 @@ var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
1725
1725
  // src/vcs/guard.ts
1726
1726
  import fs11 from "node:fs";
1727
1727
  import path11 from "node:path";
1728
- var VERSION = 1;
1728
+ var VERSION = 2;
1729
+ var WINDOWS = process.platform === "win32";
1730
+ var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
1729
1731
  var SHIM = `#!/bin/sh
1730
1732
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
1731
1733
  # index or discard work. The primary agent reviews and commits, not the worker.
@@ -1802,9 +1804,10 @@ function shimDir() {
1802
1804
  return path11.join(home(), "shim", `v${VERSION}`);
1803
1805
  }
1804
1806
  function findRealGit(skip) {
1807
+ const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
1805
1808
  for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
1806
- if (path11.resolve(dir) === skip) continue;
1807
- const p = path11.join(dir, "git");
1809
+ if (same(path11.resolve(dir), skip)) continue;
1810
+ const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
1808
1811
  try {
1809
1812
  fs11.accessSync(p, fs11.constants.X_OK);
1810
1813
  if (fs11.statSync(p).isFile()) return p;
@@ -1813,6 +1816,24 @@ function findRealGit(skip) {
1813
1816
  }
1814
1817
  return void 0;
1815
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
+ `;
1816
1837
  var HOOK_VERSION = 1;
1817
1838
  var REF_HOOK = `#!/bin/sh
1818
1839
  # pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
@@ -1826,7 +1847,17 @@ function writeIfChanged(file, content) {
1826
1847
  fs11.mkdirSync(path11.dirname(file), { recursive: true });
1827
1848
  const tmp = `${file}.${process.pid}.tmp`;
1828
1849
  fs11.writeFileSync(tmp, content, { mode: 493 });
1829
- fs11.renameSync(tmp, file);
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;
1830
1861
  }
1831
1862
  function withGitConfig(env, entries) {
1832
1863
  const out = { ...env };
@@ -1847,17 +1878,27 @@ function guardEnv(env) {
1847
1878
  GIT_PAGER: "cat",
1848
1879
  PAGER: "cat"
1849
1880
  };
1850
- if (process.platform === "win32") return { ...env, ...quiet };
1851
1881
  writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
1852
- env = withGitConfig(env, [
1853
- ["core.hooksPath", hooksDir()],
1882
+ env = withGitConfig(onePath(env), [
1883
+ ["core.hooksPath", fwd(hooksDir())],
1854
1884
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
1855
1885
  ]);
1856
1886
  const dir = shimDir();
1857
1887
  const real = findRealGit(dir);
1858
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
+ }
1859
1895
  writeIfChanged(path11.join(dir, "git"), SHIM);
1860
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path11.delimiter}${env.PATH ?? ""}` };
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()));
1861
1902
  }
1862
1903
 
1863
1904
  // src/core/slots.ts
@@ -1994,7 +2035,7 @@ function secretWarning(files, mode) {
1994
2035
  var slash = (p) => p.split(path13.sep).join("/");
1995
2036
 
1996
2037
  // src/core/receipt.ts
1997
- import fs15 from "node:fs";
2038
+ import fs16 from "node:fs";
1998
2039
 
1999
2040
  // src/core/config.ts
2000
2041
  import fs14 from "node:fs";
@@ -2019,7 +2060,11 @@ var SCHEMA = {
2019
2060
  mcpDash: "boolean",
2020
2061
  notify: "boolean",
2021
2062
  notifyCommand: "string",
2022
- notifyAfter: "number"
2063
+ notifyAfter: "number",
2064
+ workerPrices: "record",
2065
+ priceFeed: "boolean",
2066
+ priceFeedUrl: "string",
2067
+ priceFeedHours: "number"
2023
2068
  };
2024
2069
  function configPath() {
2025
2070
  if (process.env.PITROOM_CONFIG) return path14.resolve(process.env.PITROOM_CONFIG);
@@ -2117,10 +2162,64 @@ function effective(flags = {}) {
2117
2162
  mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
2118
2163
  notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
2119
2164
  notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
2120
- notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15)
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)
2121
2170
  };
2122
2171
  }
2123
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
+
2124
2223
  // src/core/receipt.ts
2125
2224
  var PRESETS = {
2126
2225
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
@@ -2134,17 +2233,27 @@ function primaryPrice() {
2134
2233
  if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
2135
2234
  return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
2136
2235
  }
2137
- return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
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;
2138
2241
  }
2139
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;
2140
2250
  function savedUsd(usage, returnedTokens, price = primaryPrice()) {
2141
- const wouldCost = (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
2142
2251
  const readingTheReport = returnedTokens * price.input / 1e6;
2143
- return Math.max(0, wouldCost - (usage.cost ?? 0) - readingTheReport);
2252
+ return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
2144
2253
  }
2145
2254
  function readLedger(sinceMs) {
2146
- if (!fs15.existsSync(ledgerFile())) return [];
2147
- return fs15.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2255
+ if (!fs16.existsSync(ledgerFile())) return [];
2256
+ return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2148
2257
  try {
2149
2258
  return [JSON.parse(l)];
2150
2259
  } catch {
@@ -2220,10 +2329,10 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2220
2329
 
2221
2330
  // src/core/process.ts
2222
2331
  import { spawn } from "node:child_process";
2223
- import fs16 from "node:fs";
2332
+ import fs17 from "node:fs";
2224
2333
  async function spawnWorker(inv, opts) {
2225
- const out = fs16.openSync(opts.stdoutFile, "w");
2226
- const err = fs16.openSync(opts.stderrFile, "w");
2334
+ const out = fs17.openSync(opts.stdoutFile, "w");
2335
+ const err = fs17.openSync(opts.stderrFile, "w");
2227
2336
  const res = { code: null, timedOut: false, stopped: false };
2228
2337
  const child = spawn(inv.command, inv.args, {
2229
2338
  cwd: opts.cwd,
@@ -2246,7 +2355,7 @@ async function spawnWorker(inv, opts) {
2246
2355
  }, opts.timeoutSec * 1e3);
2247
2356
  res.code = await new Promise((resolve2) => {
2248
2357
  child.on("error", (e) => {
2249
- res.spawnError = e.code === "ENOENT" ? fs16.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2358
+ res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2250
2359
  resolve2(127);
2251
2360
  });
2252
2361
  child.on("close", (c) => resolve2(c));
@@ -2255,8 +2364,8 @@ async function spawnWorker(inv, opts) {
2255
2364
  if (killer) clearTimeout(killer);
2256
2365
  process.off("SIGINT", stop);
2257
2366
  process.off("SIGTERM", stop);
2258
- fs16.closeSync(out);
2259
- fs16.closeSync(err);
2367
+ fs17.closeSync(out);
2368
+ fs17.closeSync(err);
2260
2369
  return res;
2261
2370
  }
2262
2371
  export {
@@ -2269,8 +2378,10 @@ export {
2269
2378
  badgeUrl,
2270
2379
  brief,
2271
2380
  card,
2381
+ catalogPrice,
2272
2382
  compact,
2273
2383
  cooldownMs,
2384
+ costAt,
2274
2385
  describeTarget,
2275
2386
  estimateTokens,
2276
2387
  extractRefs,
@@ -2291,12 +2402,14 @@ export {
2291
2402
  newRunId,
2292
2403
  parseAudit,
2293
2404
  parsePlan,
2405
+ parsePriceSpec,
2294
2406
  parseStatus,
2295
2407
  parseTarget,
2296
2408
  parseVerdict,
2297
2409
  planName,
2298
2410
  planTask,
2299
2411
  primaryPrice,
2412
+ readCatalog,
2300
2413
  readLedger,
2301
2414
  readMeta,
2302
2415
  releaseSlot,
@@ -2310,11 +2423,14 @@ export {
2310
2423
  savedUsd,
2311
2424
  secretWarning,
2312
2425
  shimDir,
2426
+ shimReady,
2313
2427
  slotHolders,
2314
2428
  spawnWorker,
2315
2429
  totals,
2430
+ trim,
2316
2431
  tryAcquireSlot,
2317
2432
  usd,
2318
2433
  verifyRefs,
2434
+ workerCostOf,
2319
2435
  writeMeta
2320
2436
  };