pitroom 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/PRIVACY.md +3 -1
- package/README.md +5 -3
- package/dist/lib.mjs +146 -29
- package/dist/pitroom.mjs +595 -315
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +30 -30
- package/docs/backends.md +2 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -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.22.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.22.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,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Workers
|
|
6
|
+
- **`--continue` works with Gemini.** It was refused ("cannot continue sessions") because Gemini CLI's `--resume` was known to take an index or `latest`, which is not safe with parallel workers. It takes the session id as well (checked live on 0.62: a follow-up remembered what the first run read), and the id is in the stream's first event, so Pitroom resumes by it like with the other workers: `pitroom run --continue last "…"`, or `continue` in `pitroom_run`.
|
|
7
|
+
|
|
8
|
+
### Dashboard
|
|
9
|
+
- **A crew is one card.** On the Live tab, runs that share a group (two or more: `pitroom run -g name`, a `pitroom_run` with `tasks`) now show as one card: the group name, the workers that are in it (`opencode ×3`), how many runs, how many are running, done or need attention, a progress bar, the tokens and the estimated saving of the crew, and when it started. It opens onto its runs, which are the usual cards; a crew that is still working is open by itself. A group of one run, and runs without a group, stay plain cards; the group picker still lists one crew's runs flat. A crew with a run among the newest is returned whole, so "Show older runs" never leaves it with fewer runs than it has.
|
|
10
|
+
|
|
11
|
+
## 0.21.0
|
|
12
|
+
|
|
13
|
+
### Windows
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
16
|
+
### Costs
|
|
17
|
+
- **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).
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
3
20
|
## 0.20.0
|
|
4
21
|
|
|
5
22
|
### 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`.
|
|
@@ -662,7 +662,7 @@ pitroom run -m nvidia/z-ai/glm-5.3 "…" # another model on th
|
|
|
662
662
|
| OpenCode (v2+) | ✅ | per-run permission rules | reported | private `--standalone` server per run |
|
|
663
663
|
| Codex CLI | ✅ | OS sandbox (`read-only` / `workspace-write`) | tokens only | `codex login`; your `~/.codex/config.toml` is ignored for workers (its MCP servers run outside the sandbox); models take `#effort` |
|
|
664
664
|
| Claude Code | ✅ | tool allowlist (`--restricted --safe-mode`, `dontAsk`) | reported | `claude auth login`; its default is often Opus, so prefer `-W claude:haiku` |
|
|
665
|
-
| Gemini CLI | beta | `--approval-mode plan` plus Pitroom's policy rules | tokens only | an API key from Google AI Studio (sign in with `gemini` or set `GEMINI_API_KEY`; Google account sign-in is refused by Google); workers run in a private Gemini home without your hooks, MCP servers and skills; secret-file rules are not enforced by Gemini 0.62, but a read run with secret-looking files reads a [clean snapshot](#modes) without them; pin a model, e.g. `-W gemini:gemini-3.8-flash`;
|
|
665
|
+
| Gemini CLI | beta | `--approval-mode plan` plus Pitroom's policy rules | tokens only | an API key from Google AI Studio (sign in with `gemini` or set `GEMINI_API_KEY`; Google account sign-in is refused by Google); workers run in a private Gemini home without your hooks, MCP servers and skills; secret-file rules are not enforced by Gemini 0.62, but a read run with secret-looking files reads a [clean snapshot](#modes) without them; pin a model, e.g. `-W gemini:gemini-3.8-flash`; `--continue` works (by session id; see [the notes](docs/backends.md#gemini-cli-notes)) |
|
|
666
666
|
|
|
667
667
|
### Models, costs and effort
|
|
668
668
|
|
|
@@ -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));
|
|
@@ -827,6 +827,7 @@ function invocation3(req) {
|
|
|
827
827
|
"none"
|
|
828
828
|
];
|
|
829
829
|
for (const p of policies) args.push("--policy", path5.join(dir, p));
|
|
830
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
830
831
|
if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
|
|
831
832
|
const [model] = (req.model ?? "").split("#");
|
|
832
833
|
if (model) args.push("--model", model);
|
|
@@ -913,7 +914,7 @@ function catalog3() {
|
|
|
913
914
|
var gemini = {
|
|
914
915
|
id: "gemini",
|
|
915
916
|
name: "Gemini CLI",
|
|
916
|
-
capabilities: { readOnly: "approval-mode", resume: "
|
|
917
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
917
918
|
binary: binary3,
|
|
918
919
|
catalog: catalog3,
|
|
919
920
|
invocation: invocation3,
|
|
@@ -1725,7 +1726,9 @@ var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
|
1725
1726
|
// src/vcs/guard.ts
|
|
1726
1727
|
import fs11 from "node:fs";
|
|
1727
1728
|
import path11 from "node:path";
|
|
1728
|
-
var VERSION =
|
|
1729
|
+
var VERSION = 2;
|
|
1730
|
+
var WINDOWS = process.platform === "win32";
|
|
1731
|
+
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
1729
1732
|
var SHIM = `#!/bin/sh
|
|
1730
1733
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
1731
1734
|
# index or discard work. The primary agent reviews and commits, not the worker.
|
|
@@ -1802,9 +1805,10 @@ function shimDir() {
|
|
|
1802
1805
|
return path11.join(home(), "shim", `v${VERSION}`);
|
|
1803
1806
|
}
|
|
1804
1807
|
function findRealGit(skip) {
|
|
1808
|
+
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1805
1809
|
for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
|
|
1806
|
-
if (path11.resolve(dir)
|
|
1807
|
-
const p = path11.join(dir, "git");
|
|
1810
|
+
if (same(path11.resolve(dir), skip)) continue;
|
|
1811
|
+
const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
|
|
1808
1812
|
try {
|
|
1809
1813
|
fs11.accessSync(p, fs11.constants.X_OK);
|
|
1810
1814
|
if (fs11.statSync(p).isFile()) return p;
|
|
@@ -1813,6 +1817,24 @@ function findRealGit(skip) {
|
|
|
1813
1817
|
}
|
|
1814
1818
|
return void 0;
|
|
1815
1819
|
}
|
|
1820
|
+
function findGitSh(realGit) {
|
|
1821
|
+
let dir = path11.dirname(realGit);
|
|
1822
|
+
for (let i = 0; i < 4; i++) {
|
|
1823
|
+
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
1824
|
+
const p = path11.join(dir, rel);
|
|
1825
|
+
if (fs11.existsSync(p)) return p;
|
|
1826
|
+
}
|
|
1827
|
+
const up = path11.dirname(dir);
|
|
1828
|
+
if (up === dir) break;
|
|
1829
|
+
dir = up;
|
|
1830
|
+
}
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
var cmdShim = (sh) => `@echo off\r
|
|
1834
|
+
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
1835
|
+
"${sh}" "%~dp0git" %*\r
|
|
1836
|
+
exit /b %errorlevel%\r
|
|
1837
|
+
`;
|
|
1816
1838
|
var HOOK_VERSION = 1;
|
|
1817
1839
|
var REF_HOOK = `#!/bin/sh
|
|
1818
1840
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -1826,7 +1848,17 @@ function writeIfChanged(file, content) {
|
|
|
1826
1848
|
fs11.mkdirSync(path11.dirname(file), { recursive: true });
|
|
1827
1849
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1828
1850
|
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
1829
|
-
|
|
1851
|
+
renameOver(tmp, file);
|
|
1852
|
+
}
|
|
1853
|
+
function pathKey(env) {
|
|
1854
|
+
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
1855
|
+
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
1856
|
+
}
|
|
1857
|
+
function onePath(env) {
|
|
1858
|
+
const key = pathKey(env);
|
|
1859
|
+
const out = { ...env };
|
|
1860
|
+
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
1861
|
+
return out;
|
|
1830
1862
|
}
|
|
1831
1863
|
function withGitConfig(env, entries) {
|
|
1832
1864
|
const out = { ...env };
|
|
@@ -1847,17 +1879,27 @@ function guardEnv(env) {
|
|
|
1847
1879
|
GIT_PAGER: "cat",
|
|
1848
1880
|
PAGER: "cat"
|
|
1849
1881
|
};
|
|
1850
|
-
if (process.platform === "win32") return { ...env, ...quiet };
|
|
1851
1882
|
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
1852
|
-
env = withGitConfig(env, [
|
|
1853
|
-
["core.hooksPath", hooksDir()],
|
|
1883
|
+
env = withGitConfig(onePath(env), [
|
|
1884
|
+
["core.hooksPath", fwd(hooksDir())],
|
|
1854
1885
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
1855
1886
|
]);
|
|
1856
1887
|
const dir = shimDir();
|
|
1857
1888
|
const real = findRealGit(dir);
|
|
1858
1889
|
if (!real) return { ...env, ...quiet };
|
|
1890
|
+
let sh;
|
|
1891
|
+
if (WINDOWS) {
|
|
1892
|
+
sh = findGitSh(real);
|
|
1893
|
+
if (!sh) return { ...env, ...quiet };
|
|
1894
|
+
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
1895
|
+
}
|
|
1859
1896
|
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
1860
|
-
|
|
1897
|
+
const key = pathKey(env);
|
|
1898
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
|
|
1899
|
+
}
|
|
1900
|
+
function shimReady() {
|
|
1901
|
+
const env = guardEnv({ ...process.env });
|
|
1902
|
+
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
1861
1903
|
}
|
|
1862
1904
|
|
|
1863
1905
|
// src/core/slots.ts
|
|
@@ -1994,7 +2036,7 @@ function secretWarning(files, mode) {
|
|
|
1994
2036
|
var slash = (p) => p.split(path13.sep).join("/");
|
|
1995
2037
|
|
|
1996
2038
|
// src/core/receipt.ts
|
|
1997
|
-
import
|
|
2039
|
+
import fs16 from "node:fs";
|
|
1998
2040
|
|
|
1999
2041
|
// src/core/config.ts
|
|
2000
2042
|
import fs14 from "node:fs";
|
|
@@ -2019,7 +2061,11 @@ var SCHEMA = {
|
|
|
2019
2061
|
mcpDash: "boolean",
|
|
2020
2062
|
notify: "boolean",
|
|
2021
2063
|
notifyCommand: "string",
|
|
2022
|
-
notifyAfter: "number"
|
|
2064
|
+
notifyAfter: "number",
|
|
2065
|
+
workerPrices: "record",
|
|
2066
|
+
priceFeed: "boolean",
|
|
2067
|
+
priceFeedUrl: "string",
|
|
2068
|
+
priceFeedHours: "number"
|
|
2023
2069
|
};
|
|
2024
2070
|
function configPath() {
|
|
2025
2071
|
if (process.env.PITROOM_CONFIG) return path14.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -2117,10 +2163,64 @@ function effective(flags = {}) {
|
|
|
2117
2163
|
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2118
2164
|
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2119
2165
|
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)
|
|
2166
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2167
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2168
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2169
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2170
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
2121
2171
|
};
|
|
2122
2172
|
}
|
|
2123
2173
|
|
|
2174
|
+
// src/core/prices.ts
|
|
2175
|
+
import fs15 from "node:fs";
|
|
2176
|
+
import path15 from "node:path";
|
|
2177
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2178
|
+
var catalogFile = () => path15.join(home(), "prices.json");
|
|
2179
|
+
function trim(raw) {
|
|
2180
|
+
const out = {};
|
|
2181
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2182
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2183
|
+
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2184
|
+
const c = m?.cost;
|
|
2185
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2186
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2187
|
+
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return out;
|
|
2191
|
+
}
|
|
2192
|
+
var cached2;
|
|
2193
|
+
function readCatalog() {
|
|
2194
|
+
const file = catalogFile();
|
|
2195
|
+
try {
|
|
2196
|
+
const { mtimeMs } = fs15.statSync(file);
|
|
2197
|
+
if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2198
|
+
const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2199
|
+
const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2200
|
+
cached2 = { file, mtimeMs, catalog: catalog5 };
|
|
2201
|
+
return catalog5;
|
|
2202
|
+
} catch {
|
|
2203
|
+
return void 0;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2207
|
+
function catalogPrice(backend, ...models) {
|
|
2208
|
+
const catalog5 = readCatalog();
|
|
2209
|
+
if (!catalog5) return void 0;
|
|
2210
|
+
for (const raw of models) {
|
|
2211
|
+
const id = raw?.split("#")[0];
|
|
2212
|
+
if (!id) continue;
|
|
2213
|
+
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2214
|
+
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2215
|
+
const triple = any ? catalog5.models[any] : void 0;
|
|
2216
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2217
|
+
const [input, output, cachedInput] = triple;
|
|
2218
|
+
return { input, output, cachedInput, key: any };
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return void 0;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2124
2224
|
// src/core/receipt.ts
|
|
2125
2225
|
var PRESETS = {
|
|
2126
2226
|
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
@@ -2134,17 +2234,27 @@ function primaryPrice() {
|
|
|
2134
2234
|
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2135
2235
|
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2136
2236
|
}
|
|
2137
|
-
|
|
2237
|
+
const named = eff.primary.value;
|
|
2238
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2239
|
+
if (preset) return preset;
|
|
2240
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2241
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2138
2242
|
}
|
|
2139
2243
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2244
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2245
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2246
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2247
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2248
|
+
}
|
|
2249
|
+
var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
|
|
2250
|
+
var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
|
|
2140
2251
|
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
2252
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2143
|
-
return Math.max(0,
|
|
2253
|
+
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2144
2254
|
}
|
|
2145
2255
|
function readLedger(sinceMs) {
|
|
2146
|
-
if (!
|
|
2147
|
-
return
|
|
2256
|
+
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2257
|
+
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2148
2258
|
try {
|
|
2149
2259
|
return [JSON.parse(l)];
|
|
2150
2260
|
} catch {
|
|
@@ -2220,10 +2330,10 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2220
2330
|
|
|
2221
2331
|
// src/core/process.ts
|
|
2222
2332
|
import { spawn } from "node:child_process";
|
|
2223
|
-
import
|
|
2333
|
+
import fs17 from "node:fs";
|
|
2224
2334
|
async function spawnWorker(inv, opts) {
|
|
2225
|
-
const out =
|
|
2226
|
-
const err =
|
|
2335
|
+
const out = fs17.openSync(opts.stdoutFile, "w");
|
|
2336
|
+
const err = fs17.openSync(opts.stderrFile, "w");
|
|
2227
2337
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2228
2338
|
const child = spawn(inv.command, inv.args, {
|
|
2229
2339
|
cwd: opts.cwd,
|
|
@@ -2246,7 +2356,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2246
2356
|
}, opts.timeoutSec * 1e3);
|
|
2247
2357
|
res.code = await new Promise((resolve2) => {
|
|
2248
2358
|
child.on("error", (e) => {
|
|
2249
|
-
res.spawnError = e.code === "ENOENT" ?
|
|
2359
|
+
res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2250
2360
|
resolve2(127);
|
|
2251
2361
|
});
|
|
2252
2362
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2255,8 +2365,8 @@ async function spawnWorker(inv, opts) {
|
|
|
2255
2365
|
if (killer) clearTimeout(killer);
|
|
2256
2366
|
process.off("SIGINT", stop);
|
|
2257
2367
|
process.off("SIGTERM", stop);
|
|
2258
|
-
|
|
2259
|
-
|
|
2368
|
+
fs17.closeSync(out);
|
|
2369
|
+
fs17.closeSync(err);
|
|
2260
2370
|
return res;
|
|
2261
2371
|
}
|
|
2262
2372
|
export {
|
|
@@ -2269,8 +2379,10 @@ export {
|
|
|
2269
2379
|
badgeUrl,
|
|
2270
2380
|
brief,
|
|
2271
2381
|
card,
|
|
2382
|
+
catalogPrice,
|
|
2272
2383
|
compact,
|
|
2273
2384
|
cooldownMs,
|
|
2385
|
+
costAt,
|
|
2274
2386
|
describeTarget,
|
|
2275
2387
|
estimateTokens,
|
|
2276
2388
|
extractRefs,
|
|
@@ -2291,12 +2403,14 @@ export {
|
|
|
2291
2403
|
newRunId,
|
|
2292
2404
|
parseAudit,
|
|
2293
2405
|
parsePlan,
|
|
2406
|
+
parsePriceSpec,
|
|
2294
2407
|
parseStatus,
|
|
2295
2408
|
parseTarget,
|
|
2296
2409
|
parseVerdict,
|
|
2297
2410
|
planName,
|
|
2298
2411
|
planTask,
|
|
2299
2412
|
primaryPrice,
|
|
2413
|
+
readCatalog,
|
|
2300
2414
|
readLedger,
|
|
2301
2415
|
readMeta,
|
|
2302
2416
|
releaseSlot,
|
|
@@ -2310,11 +2424,14 @@ export {
|
|
|
2310
2424
|
savedUsd,
|
|
2311
2425
|
secretWarning,
|
|
2312
2426
|
shimDir,
|
|
2427
|
+
shimReady,
|
|
2313
2428
|
slotHolders,
|
|
2314
2429
|
spawnWorker,
|
|
2315
2430
|
totals,
|
|
2431
|
+
trim,
|
|
2316
2432
|
tryAcquireSlot,
|
|
2317
2433
|
usd,
|
|
2318
2434
|
verifyRefs,
|
|
2435
|
+
workerCostOf,
|
|
2319
2436
|
writeMeta
|
|
2320
2437
|
};
|