@cad0p/pi-usage 0.0.0-stage → 0.11.2

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