@cad0p/pi-usage 0.0.0-stage → 0.11.2-20261102.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +519 -2
  3. package/bin/piquota.js +207 -0
  4. package/extensions/quota-panel.ts +1074 -0
  5. package/package.json +59 -4
  6. package/src/auth/claude-code-auth.js +490 -0
  7. package/src/auth/jwt.js +84 -0
  8. package/src/auth/pi-auth.js +424 -0
  9. package/src/browser/cookies.js +557 -0
  10. package/src/browser/history.js +173 -0
  11. package/src/cache.js +235 -0
  12. package/src/cli/args.js +154 -0
  13. package/src/cli/commands/auth.js +216 -0
  14. package/src/cli/commands/gentle-pi.js +44 -0
  15. package/src/cli/commands/omarchy.js +75 -0
  16. package/src/cli/explain.js +99 -0
  17. package/src/cli/output.js +34 -0
  18. package/src/engine.js +426 -0
  19. package/src/exec.js +31 -0
  20. package/src/gentle-pi/rail-patch.js +335 -0
  21. package/src/http.js +182 -0
  22. package/src/model.js +332 -0
  23. package/src/omarchy/assets/antigravity.svg +3 -0
  24. package/src/omarchy/assets/claude.svg +1 -0
  25. package/src/omarchy/assets/codex.svg +1 -0
  26. package/src/omarchy/assets/github-actions.svg +3 -0
  27. package/src/omarchy/assets/opencode-go.svg +3 -0
  28. package/src/omarchy/publish.js +152 -0
  29. package/src/omarchy/record.js +462 -0
  30. package/src/opencode/dashboard.js +337 -0
  31. package/src/opencode/session.js +485 -0
  32. package/src/providers/antigravity-oauth.js +173 -0
  33. package/src/providers/antigravity.js +225 -0
  34. package/src/providers/backoff.js +146 -0
  35. package/src/providers/claude.js +194 -0
  36. package/src/providers/codex.js +151 -0
  37. package/src/providers/error-kind.js +128 -0
  38. package/src/providers/github-actions.js +379 -0
  39. package/src/providers/opencode-go.js +274 -0
  40. package/src/refresh.js +292 -0
  41. package/src/render/panel.js +411 -0
  42. package/src/render/theme.js +102 -0
  43. package/src/sticky.js +319 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 J3fp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,520 @@
1
- # Temporary Holding Version
1
+ # pi-usage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One source of truth for quota: **the credentials Pi already owns**.
4
+
5
+ > [!NOTE]
6
+ > `@cad0p/pi-usage` is a fork of [J3pf/piQuota](https://github.com/J3pf/piQuota) (`@j3pf/piquota`). It adds:
7
+ >
8
+ > * **OpenCode Go from the API key**: `GET https://opencode.ai/zen/go/v1/usage` returns the rolling (5h), weekly and monthly windows for the key Pi already stores, so no browser session is needed.
9
+ > * **Numbered accounts**: `auth.json` entries such as `opencode-go-2` … `opencode-go-14` (the [pi-fallback-provider](https://github.com/cad0p/pi-fallback-provider) sibling convention) are read as extra accounts of their base provider.
10
+ > * **One Omarchy tab per provider**: a multi-account provider shows each account's binding window (`#2 · Monthly`) in a single tab, and summarises failed accounts.
11
+ > * **`/usage` on demand**: the Pi command is `/usage` only, and nothing is pinned above the editor or in the footer unless you ask for it. A multi-account provider gets one line per account in the panel.
12
+ > * **No dead Claude store**: `auto` passes over an expired Claude Code token when Pi's own `anthropic` login is fresh, and says so in the warnings.
13
+ >
14
+ > Everything below still applies; the CLI is still called `piquota`.
15
+
16
+ `pi-quota` is a standalone project. It does not import, invoke or modify
17
+ shuvquota, and it never writes to a credential file.
18
+
19
+ > [!TIP]
20
+ > **Working with AI Agents?** See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for architectural boundaries, test runner commands, and hard invariants for AI coding agents.
21
+
22
+ | Surface | Command |
23
+ | --- | --- |
24
+ | Terminal | `piquota` · `piquota --json` · `piquota --status` · `piquota --compact` |
25
+ | Pi TUI | `/usage` |
26
+ | External / `rp-host` / automation | `piquota --json` (structured JSON feed with `primaryWindowId` and countdowns) |
27
+ | Omarchy Bar Panel | `piquota omarchy` (or systemd user timer via `--omarchy-timer`) |
28
+
29
+ > [!NOTE]
30
+ > **Moshi Integration Removed in v0.11.0**: Previous versions included an integration with the Moshi mobile app and `moshi-hook`. Starting with v0.11.0, Moshi-specific commands and approval mirroring are removed in favor of direct local integrations such as `piquota --json` for `rp-host` / Rp-app and `piquota omarchy` for Omarchy.
31
+
32
+ ## Hard guarantees
33
+
34
+ * `~/.pi/agent/auth.json` is opened with `flag: "r"`. Never written, synced or
35
+ refreshed. Verified with a checksum before and after every run.
36
+ * No token, refresh token, API key, session cookie or `sessionKey` is printed,
37
+ logged, cached, or sent anywhere. E-mail identities are redacted in every
38
+ artifact.
39
+ * Antigravity's access token **may** be refreshed, but only in memory. Google
40
+ does not rotate that refresh token, so Pi's stored copy stays valid. Claude and
41
+ Codex are never refreshed, because they **do** rotate and persisting a rotated
42
+ token would break Pi.
43
+ * The Claude Code store (`~/.claude/.credentials.json`) is opened read-only too, and
44
+ its refresh token is never even read into memory, so nothing here can rotate a
45
+ token the installed `claude` owns. `~/.claude.json` is read only for the account
46
+ e-mail and display name; its project history is never touched.
47
+ * Browser cookie databases are copied to a private temp dir and opened
48
+ read-only. This is only used for the OpenCode Go fallback, when the key usage
49
+ endpoint is gone and no session cookie is configured.
50
+ * `/usage` never injects quota into the LLM context: the extension only calls
51
+ `ui.setStatus`, `ui.setWidget` and `ui.notify`.
52
+ * Nothing is written outside these paths:
53
+
54
+ | Path | What |
55
+ | --- | --- |
56
+ | `~/.cache/pi-quota/usage.json` | the report cache |
57
+ | `~/.cache/pi-quota/refresh-state.json` | each family's own refresh clock |
58
+ | `~/.cache/pi-quota/last-published.json`, `last-good.json`, `backoff.json` | sticky snapshots and throttle state |
59
+ | `~/.config/pi-quota/opencode-cookie` | only after `piquota auth opencode`, mode `600` |
60
+ | `~/.pi/agent/extensions/*.ts` | only by `install.sh` |
61
+
62
+ ## Status, verified against the live APIs
63
+
64
+ | Provider | Source | Endpoint | Result |
65
+ | --- | --- | --- | --- |
66
+ | Claude | Claude Code CLI store, else `anthropic.access` | `GET api.anthropic.com/api/oauth/usage` | ✅ 5h + weekly + plan |
67
+ | Codex | `openai-codex.access` + `accountId` | `GET chatgpt.com/backend-api/wham/usage` | ✅ 5h + weekly + plan |
68
+ | Antigravity | `antigravity.access` + `projectId` | `POST cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` | ✅ Gemini + Claude/GPT buckets, **with in-memory refresh** |
69
+ | OpenCode Go | `opencode-go.key`, numbered siblings included | `GET opencode.ai/zen/go/v1/usage` | ✅ 5h + weekly + monthly per account; the session cookie and SSR dashboard are a base-account fallback |
70
+ | GitHub Actions (opt-in) | Authenticated `gh` CLI | `gh api /organizations/<org>/settings/billing/usage/summary?product=Actions` | ✅ org-wide monthly Linux minutes, plan-derived allowance |
71
+
72
+ Four findings worth recording, each of which cost a wrong hypothesis:
73
+
74
+ 1. **Antigravity needs the CLI `User-Agent`.** Without
75
+ `User-Agent: antigravity/cli/...` the backend answers
76
+ `403 You do not have a valid license of this product` for a healthy token.
77
+ 2. **Pi's `opencode-go` entry is an OpenCode Zen API key, and that key reads its
78
+ own Go usage.** `GET /zen/go/v1/usage` answers with the rolling (5h), weekly
79
+ and monthly windows for the key's workspace; `401` means the key was revoked
80
+ and `403 Workspace access denied` means the key is valid but its workspace has
81
+ no Go plan. Only a `404` (endpoint gone) or an entry without a key falls back
82
+ to the session cookie and the authenticated SSR dashboard, and only for the
83
+ base account, because one browser session belongs to one workspace.
84
+ 3. **`primaryWindowId` identifies the most urgent window.** The CLI computes the
85
+ shortest, most pressing window (e.g. 5h session before weekly) so downstream UI
86
+ consumers like `rp-host` and the Pi TUI render priority pills without reimplementing
87
+ window precedence.
88
+ 4. **Both Claude OAuth stores answer the same endpoint.** Pi's own `anthropic`
89
+ entry and the Claude Code CLI store return identical windows, which is what
90
+ makes the second source a drop-in replacement rather than a separate feature.
91
+
92
+ ### Claude has two sources
93
+
94
+ There are two ways to be signed in to Claude, and a user may have either or both:
95
+
96
+ | Source | Store | Why it exists |
97
+ | --- | --- | --- |
98
+ | Claude Code CLI | `~/.claude/.credentials.json` (honours `CLAUDE_CONFIG_DIR`); on macOS, when that file is absent, the `Claude Code-credentials` login Keychain item (read-only, via `security find-generic-password`; macOS may ask once to allow access) | What `npm:pi-claude-code-provider` drives. A subscription with usage credits disabled answers `400` on Pi's own entry for every request, and this is the source that still works. |
99
+ | Pi's own entry | `~/.pi/agent/auth.json` → `anthropic` | `/login anthropic` in Pi, without the plugin installed. |
100
+
101
+ **The Claude Code CLI wins when both are present and its token is still valid.** The
102
+ choice is a *preference, not a fallback*: only the selected source is tried, so a
103
+ broken preferred source is reported rather than silently masked by the other one.
104
+ The one exception is an expired Claude Code token next to a fresh Pi login: a store
105
+ left behind by an uninstalled CLI never refreshes again, so `auto` reads Pi's login
106
+ instead and adds a warning naming the switch. Override it with
107
+ `PI_QUOTA_CLAUDE_SOURCE=pi` or `=claude-code`; `--explain` prints which one was used
108
+ and why.
109
+
110
+ The Claude Code store is opened read-only, and its refresh token is deliberately
111
+ **never carried into memory**: Anthropic rotates refresh tokens, and a rotation
112
+ performed behind the CLI's back would sign the installed `claude` out. Refresh, if
113
+ any, stays the CLI's business.
114
+
115
+ ### GitHub Actions credential source
116
+
117
+ GitHub Actions does not use a Pi-store credential. Quota requests are delegated to
118
+ GitHub CLI, which owns authentication; piQuota never reads the token value.
119
+
120
+ | Source | Credential | Why it exists |
121
+ |---|---|---|
122
+ | GitHub CLI | Its authenticated session, or `GH_TOKEN` / `GITHUB_TOKEN` passed to `gh` | Reads organization billing usage without putting the token in piQuota's report or cache. |
123
+
124
+ Enable the family with `PI_QUOTA_GITHUB_ACTIONS=1` or select it explicitly with
125
+ `piquota github-actions` (`piquota gh` and `piquota actions` are aliases). Use
126
+ `gh auth login` if the CLI is not authenticated; organization billing access and
127
+ the `read:org`, `repo`, and `workflow` scopes may be required.
128
+
129
+ | Variable | Default | Effect |
130
+ |---|---|---|
131
+ | `PI_QUOTA_GITHUB_ACTIONS` | off | Enables the family for default runs; truthy values are `1`, `true`, `yes`, and `on`. |
132
+ | `PI_QUOTA_GITHUB_ORG` | `KoralisSoft` | Organization queried for its plan and usage. |
133
+ | `PI_QUOTA_GITHUB_ACTIONS_MINUTES` | plan-derived | Positive integer monthly allowance override; unsupported plans require this setting. |
134
+ | `PI_QUOTA_GITHUB_ACTIONS_REPOS` | empty | Optional comma-separated repo names used only to filter attribution; quota remains org-wide. |
135
+
136
+ ## Prerequisites & Installation
137
+
138
+ * **Node.js >= 22.19.0** (native `node:sqlite` and global `fetch` support; zero npm dependencies).
139
+ * **[Pi Coding Agent](https://github.com/earendil-works/pi)** (`pi`).
140
+ * **[Gentle AI](https://github.com/Gentleman-Programming/gentle-pi)** (`gentle-pi`).
141
+ * Platform support: Linux, WSL2, macOS. See [docs/OS-COMPATIBILITY.md](docs/OS-COMPATIBILITY.md) for OS-specific details.
142
+
143
+ ### 1-Line Quick Install
144
+
145
+ ```bash
146
+ curl -fsSL https://raw.githubusercontent.com/cad0p/pi-usage/main/install.sh | bash
147
+ ```
148
+
149
+ ### Install the Pi extension from npm
150
+
151
+ ```bash
152
+ pi install npm:@cad0p/pi-usage
153
+ ```
154
+
155
+ Loads both Pi extensions straight from the package. The quota panel resolves
156
+ the CLI from inside the package, so nothing else is required to see quotas in
157
+ the TUI.
158
+ When you also run `install.sh` for the terminal CLI, pass
159
+ `--no-extension` so the two copies cannot drift apart.
160
+
161
+ | Capability | `pi install npm:` | `install.sh` |
162
+ | --- | --- | --- |
163
+ | TUI panel, sidebar and `/usage` | yes | yes |
164
+ | Omarchy bar records | yes, written on every refresh | yes |
165
+ | `piquota` on your `PATH` for terminal use | no | yes, symlinked |
166
+ | Omarchy sync while Pi is closed | no | yes, with `--omarchy-timer` |
167
+
168
+ If you live inside Pi and use Omarchy, the npm route covers it: the extension
169
+ runs `piquota omarchy` on every quota refresh, so the bar fills without any
170
+ setup. Choose `install.sh` when you want the CLI in a terminal, or background
171
+ sync while Pi is not running.
172
+
173
+ ### Manual Install / Development
174
+
175
+ ```bash
176
+ git clone https://github.com/cad0p/pi-usage.git
177
+ cd pi-usage
178
+ ./install.sh # --link for development, --uninstall to remove
179
+ ./install.sh --no-extension # CLI only; keep the Pi extension installed as a package
180
+ ```
181
+
182
+ No sudo. It validates Node, Pi and Gentle AI, reports which Claude source it found,
183
+ copies the project to `~/.local/share/pi-quota`, symlinks `~/.local/bin/piquota`,
184
+ installs the Pi TUI extension (and its gentle-pi rail slot) unless `--no-extension`
185
+ is given,
186
+ and removes the previous generation's `shuvquota` shim so upstream `/usr/bin/shuvquota`
187
+ is reachable again.
188
+
189
+ ## Commands
190
+
191
+ ```bash
192
+ piquota # boxed panel: ring, bar, % left, "reset in 3h 12m"
193
+ piquota claude codex # only these families
194
+ piquota github-actions # GitHub Actions minutes (explicit opt-in; aliases: gh, actions)
195
+ piquota --json # normalized report (for rp-host, scripts, and automations)
196
+ piquota --compact # one line per provider
197
+ piquota --status # single line with rings, for status bars
198
+ piquota --explain # which stores and fields are read (names only)
199
+ piquota --clear-cache # delete the cache and exit
200
+ piquota --no-color # plain output
201
+ piquota --ttl 300 # cache TTL in seconds (default 60)
202
+ piquota --timeout 5000 # per-request timeout in ms (default 15000)
203
+ piquota --no-cache # ignore the cache entirely
204
+ piquota --force # ignore a still-fresh cache entry
205
+ piquota --no-refresh # never refresh Antigravity's token in memory
206
+ piquota --version # the version this binary reports
207
+
208
+ piquota auth status # every credential source, including opencode.ai
209
+ piquota auth opencode # open the login in Firefox and capture the session
210
+ piquota auth opencode --paste # read the cookie from stdin instead
211
+ piquota auth opencode --no-browser
212
+ piquota auth opencode --wait 300
213
+
214
+ piquota omarchy # write pi-*.json records for Omarchy's Agents panel
215
+ ```
216
+
217
+ Colours: green above 50% remaining, yellow 20–50%, red below 20%.
218
+
219
+ **Which window the ring shows:** always the **shortest** one (5h/session first,
220
+ then daily, weekly, monthly), because that is the limit you hit first. A weekly
221
+ window at 31% left is more consumed, but a 5h window at 97% is what answers "can
222
+ I keep working right now". Ties inside the same rank go to the tightest window,
223
+ so Antigravity's two 5h groups resolve to whichever is more consumed. The rule
224
+ lives in `selectPrimaryWindow()` (`src/model.js`), the CLI stamps
225
+ `primaryWindowId` into the report, and the Pi extension reads that field, so the
226
+ terminal and the TUI cannot disagree. The full panel still lists every window,
227
+ so nothing is hidden.
228
+
229
+ Inside Pi:
230
+
231
+ | Command | Effect |
232
+ | --- | --- |
233
+ | `/usage` | one on-demand refresh, show the full panel, notify a one-line summary |
234
+ | `/usage box` | pin the five-row box above the editor |
235
+ | `/usage line` | pin the compact line above the editor, in its own row |
236
+ | `/usage panel` | full boxed panel above the editor |
237
+ | `/usage hide` | hide the panel/line and stop background refresh until a surface is on again |
238
+ | `/usage status` / `nostatus` | enable/disable the footer status |
239
+ | `/usage refresh` | refresh only |
240
+ | `/usage json` | where the cached report lives |
241
+
242
+ ### The Pi surface
243
+
244
+ Nothing is pinned by default: `/usage` is a one-shot command, so it claims no row
245
+ above the editor and leaves the footer alone. Turn a surface on with `/usage box`,
246
+ `/usage line`, `/usage panel` or `/usage status`; only then does the extension keep
247
+ it fresh with a background refresh (and a gentle refresh after each agent turn).
248
+ `/usage hide` gives the row back.
249
+
250
+ A provider with several accounts is shown as one header (`OP-Go · 7 of 14 accounts
251
+ ok`) plus one line per account: its fullest window, or the reason it failed (`key
252
+ rejected`, `no Go plan`). The panel is rendered as a component, so Pi's ten-line
253
+ cap on plain widgets never truncates it, and the summary names the account with the
254
+ most room: `OP-Go: 7/14 ok · lowest 24% (#7)`.
255
+
256
+ `/usage box` pins the **five-row box above the editor**:
257
+
258
+ ```
259
+ ╭─ Quota ──────────────────╮
260
+ │ Claude: ○ 0% R:3h 12m │
261
+ │ Codex: ○ 8% R:2h 40m │
262
+ │ Agy: ○ 4% R:41m │
263
+ │ Agy C/G: ? n/a R:unknown │
264
+ │ OP-Go: ○ 4% R:1h 05m │
265
+ ╰───────────────────────────╯
266
+ ```
267
+
268
+ When gentle-pi's fullscreen sidebar is active, the same card is painted in its
269
+ right rail instead, under Status and TODO, and the above-editor box stays quiet so
270
+ the numbers are not shown twice. See [the rail slot](#the-gentle-pi-rail-slot).
271
+
272
+ * each name is painted with its **own brand colour** — Claude `#D97757`, Codex
273
+ `#10A37F`, Antigravity `#4285F4`, OpenCode `#007AFF`;
274
+ * the numbers are **used**, not remaining, and the semaphore is both colour and
275
+ shape (`○` plenty left → `◔` → `◕` → `●` nearly spent), so it still reads on a
276
+ colourblind or mono terminal;
277
+ * **only active providers are shown:** if you only have Claude and Codex configured
278
+ in Pi, only Claude and Codex appear in the line and on Moshi (no empty warning
279
+ icons for providers you don't use);
280
+ * `NO_COLOR` / `TERM=dumb` drops the colours and keeps the glyphs;
281
+ * the window shown is the **shortest** one (5h first), and `/usage` expands every
282
+ window with a used-fraction bar and the reset countdown.
283
+
284
+ Pi's theme only exposes a fixed palette, so brand colours are emitted as
285
+ truecolor ANSI. Widget string arrays are wrapped in `Text` components by Pi,
286
+ which are ANSI-aware, so the codes are measured correctly and `theme.fg()` keeps
287
+ working in the same strings.
288
+
289
+ ### Why the footer is off by default
290
+
291
+ gentle-pi replaces the footer with its own shell bar (`setFooter`) and that bar
292
+ drops segments **from the end** when the line overflows:
293
+
294
+ ```js
295
+ while (segments.length > 1 && visibleWidth(left) > width) segments.pop();
296
+ ```
297
+
298
+ Extension statuses render last, so a footer status is the first thing discarded —
299
+ which is why a long status line appears and then vanishes once the bar fills with
300
+ cost, branch and usage data. The quota line therefore lives in its own row, where
301
+ nothing competes for it. `/usage status` still enables the footer variant for
302
+ anyone who wants it.
303
+
304
+ ### The gentle-pi rail slot
305
+
306
+ gentle-pi paints its right rail from a **hardcoded allowlist**, so a part that
307
+ another extension registers is silently never drawn:
308
+
309
+ ```ts
310
+ const sections = ["footer", "agents", "todo"].map((key) => { /* ... */ });
311
+ ```
312
+
313
+ piQuota registers its card under `quota`, which that list does not include. There is
314
+ no injection point, so the card cannot appear in the rail on its own. The fix is one
315
+ anchored edit that appends the part:
316
+
317
+ ```ts
318
+ const sections = ["footer", "agents", "todo", "quota"].map((key) => { /* ... */ });
319
+ ```
320
+
321
+ `piquota gentle-pi` owns that edit:
322
+
323
+ * `piquota gentle-pi status` — report whether the rail renders the quota part;
324
+ * `piquota gentle-pi apply` — append the part (idempotent);
325
+ * `piquota gentle-pi revert` — remove the part and restore the original bytes.
326
+
327
+ Because a gentle-pi update replaces the package, the patch would otherwise vanish.
328
+ The extension therefore **validates the slot on every session start** and re-applies
329
+ it when it is gone, reporting the repair instead of doing it silently. `install.sh`
330
+ applies it at install time and reverts it on `--uninstall`.
331
+
332
+ Safety properties, all covered by `tests/rail-patch.test.mjs`:
333
+
334
+ * exactly one array literal is rewritten; every other byte of the file is untouched;
335
+ * an unrecognized shape is **refused**, never guessed at, because a wrong rewrite of
336
+ another extension's layout would break the whole rail;
337
+ * the write is atomic, the pre-patch revision is kept as a one-time
338
+ `.pi-quota-backup` next to the file, and the result is re-read after writing with a
339
+ restore from that backup when it does not read back as patched;
340
+ * `PI_QUOTA_GENTLE_PI_DIR` points the lookup elsewhere, which is what keeps the test
341
+ suite away from a real installation.
342
+
343
+ If gentle-pi is absent, or the slot cannot be applied, nothing breaks: the card stays
344
+ in the box above the editor. The rail only exists in fullscreen at 140 columns or
345
+ wider, so the suppression of that box is conditional on gentle-pi reporting its rail
346
+ as active — in regular mode or a narrow terminal the box is the only surface.
347
+
348
+ ## JSON feed & rp-host integration (`piquota --json`)
349
+
350
+ `piquota --json` provides a structured, machine-readable feed of all configured
351
+ provider quotas. It is designed for consumption by local status bars, dynamic
352
+ islands, background pollers, and client applications such as `rp-host` (from the
353
+ Rp suite) without external network daemons or closed-source intermediaries:
354
+
355
+ * **Predictable exit status**: exits `0` if at least one configured provider
356
+ answered successfully, or `1` if all providers failed or are unconfigured.
357
+ * **`primaryWindowId`**: the CLI pre-calculates the most urgent quota window
358
+ (e.g., short 5h session window over weekly or monthly allowances) so consumer
359
+ pills or widgets do not have to implement provider-specific priority hierarchies.
360
+ * **Percentages and Countdown**: `usedPercent` and `remainingPercent` report
361
+ percentages (0..100); `resetsInSec` provides the exact integer seconds until
362
+ reset, immune to local clock skew.
363
+ * **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
364
+ allowing consumers to cleanly omit them without displaying false-positive
365
+ error indicators.
366
+
367
+ ```json
368
+ {
369
+ "engine": "pi-quota",
370
+ "schemaVersion": 1,
371
+ "readOnly": true,
372
+ "generatedAt": "2026-10-15T05:35:21.210Z",
373
+ "sources": ["/home/user/.pi/agent/auth.json"],
374
+ "warnings": [],
375
+ "providers": [
376
+ {
377
+ "family": "codex",
378
+ "label": "Codex (Pi)",
379
+ "account": "user@example.com",
380
+ "plan": "plus",
381
+ "source": "/home/user/.pi/agent/auth.json",
382
+ "sourceKind": "pi",
383
+ "expiresInMin": 12650,
384
+ "ok": true,
385
+ "notConfigured": false,
386
+ "error": null,
387
+ "updatedAt": "2026-10-15T05:35:21.210Z",
388
+ "primaryWindowId": "5h",
389
+ "windows": [
390
+ {
391
+ "id": "5h",
392
+ "label": "5h window",
393
+ "usedPercent": 14.5,
394
+ "remainingPercent": 85.5,
395
+ "resetsAt": "2026-10-15T10:35:24.000Z",
396
+ "resetsInSec": 18003,
397
+ "windowSeconds": 18000,
398
+ "note": null
399
+ }
400
+ ]
401
+ }
402
+ ]
403
+ }
404
+ ```
405
+
406
+ ### Each provider refreshes on its own clock
407
+
408
+ Anthropic's usage endpoint answers `429` when polled every minute; Codex,
409
+ Antigravity and OpenCode Go tolerate 60 s intervals. To prevent cards from looking
410
+ stale without triggering rate limits:
411
+
412
+ | Family | Clock |
413
+ | --- | --- |
414
+ | Claude | **300 s** (`--claude-ttl`) |
415
+ | Codex, Antigravity, OpenCode Go | **60 s** (`--fetch-ttl`) |
416
+
417
+ `src/refresh.js` keeps a timestamp per family, requests only what is past its own
418
+ clock, and merges the result back into the canonical report. `src/sticky.js`
419
+ keeps the last healthy snapshot during transient upstream 429 throttles so the
420
+ user never loses visibility. Permanent failures (expired sign-in, missing
421
+ credential) are never masked.
422
+
423
+ ## Omarchy integration
424
+
425
+ Omarchy 4.0.4's **Agents** bar panel draws every `*.json` record in
426
+ `${XDG_STATE_HOME:-~/.local/state}/omarchy/agents/usage/`, whoever wrote it.
427
+ `piquota omarchy` (opt-in, one shot) writes one record per provider piQuota can
428
+ read, so Claude, Codex, Antigravity and OpenCode Go all get a tab:
429
+
430
+ ```bash
431
+ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
432
+ ./install.sh --omarchy-timer # also run it every 5 minutes (systemd user timer)
433
+ ```
434
+
435
+ - Records are named `pi-<family>.json`, so they never overwrite Omarchy's own
436
+ `claude.json` / `codex.json`. They hold plan names, used percentages, window
437
+ labels and reset times only; no tokens, accounts or e-mail addresses.
438
+ - A provider with a failing sign-in shows a status and a help line instead of
439
+ limits. A provider that is not configured gets no record, and a stale
440
+ `pi-*.json` is removed on the next run.
441
+ - A family with several accounts (numbered `<base>-<slot>` entries in
442
+ `auth.json`) still gets **one** record: every healthy account contributes only
443
+ the window that constrains it, titled `#<slot> · Session|Weekly|Monthly`, and
444
+ failed accounts add no rows but are summarised by reason, e.g.
445
+ `#8–#10 key rejected · #11–#14 no Go plan`.
446
+ - Omarchy's native collector already tracks Claude with richer local statistics, so
447
+ `piquota omarchy` automatically skips `claude` and cleans up duplicate `pi-claude.json`
448
+ files when `claude.json` exists in the usage directory (override with `PI_QUOTA_OMARCHY_SKIP`).
449
+ - `PI_QUOTA_OMARCHY_DIR` overrides the output directory. `./install.sh --uninstall`
450
+ removes the timer and the `pi-*.json` records.
451
+
452
+ ## Layout
453
+
454
+ ```
455
+ src/auth/pi-auth.js read-only auth.json reader, WSL + Linux, deduplicated
456
+ src/auth/claude-code-auth.js read-only Claude Code store; never carries its refresh token
457
+ src/auth/jwt.js Codex JWT payload reader (claim keys contain dots)
458
+ src/browser/cookies.js read-only Firefox/Chromium cookie access via a temp copy
459
+ src/browser/history.js workspace ids recovered from a copied places.sqlite
460
+ src/opencode/session.js cookie + workspace resolution, dashboard fetch
461
+ src/opencode/dashboard.js three-strategy parser for the Go plan page
462
+ src/providers/*.js one file per provider; each degrades instead of throwing
463
+ src/providers/github-actions.js plan-derived org Actions minutes via authenticated gh CLI
464
+ src/providers/antigravity-oauth.js in-memory refresh with Google's public client
465
+ src/providers/backoff.js per-family throttle state
466
+ src/sticky.js last published / last good snapshots and throttle recovery
467
+ src/engine.js collectQuota() -> one normalized report
468
+ src/model.js window normalization, percent and reset parsing
469
+ src/render/{theme,panel}.js colors, thresholds, rings, bars, boxed panel
470
+ src/cache.js report cache at ~/.cache/pi-quota/usage.json
471
+ src/omarchy/record.js pure mapping from the report to Omarchy usage records
472
+ src/omarchy/publish.js atomic writer for pi-*.json, and stale-record cleanup
473
+ src/refresh.js per-family refresh clocks, and the merge back into one report
474
+ src/cli/args.js argument parsing, and the flag/positional split
475
+ src/exec.js the one place that spawns gh CLI binary
476
+ src/http.js fetch wrapper: timeouts, JSON, redaction
477
+ contrib/systemd/ user service and timer behind `install.sh --omarchy-timer`
478
+ bin/piquota.js the only CLI
479
+ extensions/quota-panel.ts Pi TUI line and /usage
480
+ ```
481
+
482
+ ## Troubleshooting
483
+
484
+ | Symptom | Cause and fix |
485
+ | --- | --- |
486
+ | `no <family> credential in the Pi store` | Not logged in to Pi for that provider. `/login <provider>` in Pi. |
487
+ | `no claude credential in the Pi store or from the Claude Code CLI` | Neither Claude source is configured. `/login anthropic` in Pi, or install Claude Code and run `claude` once. |
488
+ | `Claude Code CLI has no readable credentials; looked for …` | The store is missing, empty or corrupt. `piquota --explain` prints every path that was tried. |
489
+ | `Claude Code token expired or rejected; run \`claude\` once` | Claude Code's own access token lapsed. Run any `claude` command; piQuota never refreshes it. |
490
+ | Claude reports `plan pro` but the window looks wrong | The store is chosen by preference, not by success. `PI_QUOTA_CLAUDE_SOURCE=pi piquota` forces the other source; the cache is keyed by that setting, so the switch refetches. |
491
+ | `rate limited (HTTP 429); retry in Ns` | The vendor throttled the usage endpoint. Wait, or raise `--ttl`. |
492
+ | `Antigravity token rejected` | Refresh failed; `/login antigravity` in Pi. `--no-refresh` disables the attempt. |
493
+ | `no "auth" cookie for opencode.ai in N readable store(s)` | Run `piquota auth opencode`, or paste the cookie. |
494
+ | `only encrypted Chromium stores found` | Log in with Firefox; Chrome on Windows uses DPAPI. |
495
+ | `unknown flag: --x` | The flag is a typo or was removed. `piquota --help` lists every flag. |
496
+ | `unknown argument` | `piquota --help` lists every flag. |
497
+
498
+ ## Tests
499
+
500
+ ```bash
501
+ node --test tests/*.test.mjs # 265 tests, fake tokens only, no network
502
+ ```
503
+
504
+ Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
505
+ (including that the refresh token never leaves it and that reading leaves the file
506
+ byte-identical), Claude source precedence, the five providers (including GitHub
507
+ Actions plan resolution, opt-in gating and degradation, the two Antigravity
508
+ failures and the OpenCode degradation), the dashboard parser's three
509
+ strategies, the Firefox cookie reader against a synthetic SQLite database, the
510
+ Antigravity refresh (in-memory only), sticky and last-good snapshots resilience,
511
+ the Omarchy record mapping and atomic writer, the renderers, the Pi
512
+ extension contract, the per-family refresh clocks, argument parsing, and the
513
+ structure of these documents themselves (tables, fences, links, anchors).
514
+
515
+ ## Acknowledgments & Prior Art
516
+
517
+ * **[shuvquota](https://github.com/shuv1337/shuvquota)** by [@shuv1337](https://github.com/shuv1337):
518
+ Huge credit to `shuvquota` for pioneering multi-provider terminal quota monitoring across AI services. Its approach to rate-limit endpoints, dashboard parsing patterns for OpenCode Go, and terminal quota concepts served as inspiration for this project.
519
+
520
+ `piQuota` was built as a standalone, strictly read-only implementation designed specifically around [Pi Coding Agent](https://github.com/earendil-works/pi) credentials (`~/.pi/agent/auth.json`), Gentle AI orchestration, and zero-dependency JSON and desktop integrations.