pitroom 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -0
- package/PRIVACY.md +3 -1
- package/README.md +4 -2
- package/dist/lib.mjs +144 -28
- package/dist/pitroom.mjs +589 -312
- package/dist/ui/app.js +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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;
|
|
@@ -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
|
|
756
|
-
usage.cacheRead +=
|
|
757
|
-
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);
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
2252
|
+
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2144
2253
|
}
|
|
2145
2254
|
function readLedger(sinceMs) {
|
|
2146
|
-
if (!
|
|
2147
|
-
return
|
|
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
|
|
2332
|
+
import fs17 from "node:fs";
|
|
2224
2333
|
async function spawnWorker(inv, opts) {
|
|
2225
|
-
const out =
|
|
2226
|
-
const err =
|
|
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" ?
|
|
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
|
-
|
|
2259
|
-
|
|
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
|
};
|