@cad0p/pi-usage 0.11.3 โ 0.11.4
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/CHANGELOG.md +28 -0
- package/README.md +274 -12
- package/bin/piquota.js +49 -4
- package/extensions/quota-panel.ts +188 -31
- package/omarchy/cad0p.pi-usage/Agent.qml +33 -0
- package/omarchy/cad0p.pi-usage/Main.qml +333 -0
- package/omarchy/cad0p.pi-usage/Panel.qml +136 -0
- package/omarchy/cad0p.pi-usage/README.md +40 -0
- package/omarchy/cad0p.pi-usage/UsageContent.qml +1001 -0
- package/omarchy/cad0p.pi-usage/UsageLogic.js +603 -0
- package/omarchy/cad0p.pi-usage/assets/antigravity.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/claude.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex-light.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/fireworks.svg +7 -0
- package/omarchy/cad0p.pi-usage/assets/github-actions.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/opencode-go.svg +3 -0
- package/omarchy/cad0p.pi-usage/manifest.json +56 -0
- package/package.json +2 -1
- package/src/auth/pi-auth.js +3 -0
- package/src/cli/args.js +5 -0
- package/src/cli/commands/stats.js +76 -0
- package/src/engine.js +44 -0
- package/src/local/history.js +1160 -0
- package/src/local/pi-sessions.js +1511 -0
- package/src/omarchy/publish.js +1 -65
- package/src/omarchy/record.js +82 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.11.4] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
<!-- USER-EDITABLE SECTION START -->
|
|
8
|
+
Token history, a scrolling bar panel, and Antigravity from Pi's own login.
|
|
9
|
+
|
|
10
|
+
- **Token stats from Pi's sessions:** tokens by day and by model per provider and account, read from `~/.pi/agent/sessions` with no extra credential. Incremental (a warm run reads only appended bytes) and durable: finished days are frozen into `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/` (`totals.json` + `tokens-YYYY-MM.json`) and survive session deletion and cache loss. Shown in `/usage` and in the Omarchy records; `piquota stats` inspects the history.
|
|
11
|
+
- **AI Usage bar plugin (`cad0p.pi-usage`):** looks like Omarchy's Agents panel, but every provider is a collapsible section of one scrolling panel, multi-account providers list every window of every account, and tokens by day and by model are drawn as in the built-in. `install.sh --omarchy-plugin` links it from the pi package, so `pi update` updates it.
|
|
12
|
+
- **No more writes into other Omarchy plugins:** `piquota omarchy` stopped copying provider marks into every `~/.config/omarchy/plugins/*/assets`.
|
|
13
|
+
- **`/usage` glance:** bare `/usage` closes on the next prompt or a second `/usage` (`/usage panel` still pins), and each account line shows session, week and month.
|
|
14
|
+
- **Antigravity:** read from Pi's `google-antigravity` login (the legacy `antigravity` key still works).
|
|
15
|
+
- `docs/verification.md`: the checklist every change follows.
|
|
16
|
+
<!-- USER-EDITABLE SECTION END -->
|
|
17
|
+
|
|
18
|
+
### ๐ Features
|
|
19
|
+
|
|
20
|
+
- *(local)* Token stats by day and by model from Pi's session files (closes #16)
|
|
21
|
+
- *(omarchy)* AI Usage bar plugin, every provider a collapsible section (closes #18)
|
|
22
|
+
|
|
23
|
+
### ๐ Bug Fixes
|
|
24
|
+
|
|
25
|
+
- *(extension)* /usage glance closes on the next prompt; every window per account (closes #9)
|
|
26
|
+
- *(auth)* Read Antigravity from Pi's google-antigravity login (closes #12)
|
|
27
|
+
|
|
28
|
+
### ๐ Documentation
|
|
29
|
+
|
|
30
|
+
- Add the verification checklist every change follows (closes #14)
|
|
31
|
+
|
|
32
|
+
|
|
5
33
|
## [0.11.3] - 2026-11-02
|
|
6
34
|
|
|
7
35
|
<!-- USER-EDITABLE SECTION START -->
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ One source of truth for quota: **the credentials Pi already owns**.
|
|
|
8
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
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
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
|
+
> * **Tokens by day and by model**: the tokens Pi's own session files recorded are folded per provider and shown in `/usage`, in `--json` (`localStats`) and in the Omarchy records, at a bounded scan cost.
|
|
11
12
|
> * **`/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
13
|
> * **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
|
>
|
|
@@ -17,7 +18,7 @@ One source of truth for quota: **the credentials Pi already owns**.
|
|
|
17
18
|
shuvquota, and it never writes to a credential file.
|
|
18
19
|
|
|
19
20
|
> [!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
|
+
> **Working with AI Agents?** See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for architectural boundaries, test runner commands, and hard invariants for AI coding agents, and [docs/verification.md](docs/verification.md) for the verification checklist every change follows.
|
|
21
22
|
|
|
22
23
|
| Surface | Command |
|
|
23
24
|
| --- | --- |
|
|
@@ -49,11 +50,19 @@ shuvquota, and it never writes to a credential file.
|
|
|
49
50
|
endpoint is gone and no session cookie is configured.
|
|
50
51
|
* `/usage` never injects quota into the LLM context: the extension only calls
|
|
51
52
|
`ui.setStatus`, `ui.setWidget` and `ui.notify`.
|
|
53
|
+
* Pi's session files (`~/.pi/agent/sessions/**/*.jsonl`) are opened read-only, to
|
|
54
|
+
count tokens per day and per model. The scan cache keys each file by its path,
|
|
55
|
+
size and byte offset and stores open days; the durable history under
|
|
56
|
+
`~/.pi/agent/pi-usage/` stores frozen days. Both hold only aggregate counts and
|
|
57
|
+
model names โ no prompt text or tool output reaches a cache, a record or the
|
|
58
|
+
report.
|
|
52
59
|
* Nothing is written outside these paths:
|
|
53
60
|
|
|
54
61
|
| Path | What |
|
|
55
62
|
| --- | --- |
|
|
56
63
|
| `~/.cache/pi-quota/usage.json` | the report cache |
|
|
64
|
+
| `~/.cache/pi-quota/local-sessions.json` | per-session-file offsets and open token days |
|
|
65
|
+
| `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/totals.json`, `tokens-YYYY-MM.json` | frozen token history, plus `.corrupt-<ts>` backups for corrupt files |
|
|
57
66
|
| `~/.cache/pi-quota/refresh-state.json` | each family's own refresh clock |
|
|
58
67
|
| `~/.cache/pi-quota/last-published.json`, `last-good.json`, `backoff.json` | sticky snapshots and throttle state |
|
|
59
68
|
| `~/.config/pi-quota/opencode-cookie` | only after `piquota auth opencode`, mode `600` |
|
|
@@ -65,7 +74,7 @@ shuvquota, and it never writes to a credential file.
|
|
|
65
74
|
| --- | --- | --- | --- |
|
|
66
75
|
| Claude | Claude Code CLI store, else `anthropic.access` | `GET api.anthropic.com/api/oauth/usage` | โ
5h + weekly + plan |
|
|
67
76
|
| 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** |
|
|
77
|
+
| Antigravity | `google-antigravity.access` (or legacy `antigravity`) + `projectId` | `POST cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` | โ
Gemini + Claude/GPT buckets, **with in-memory refresh** |
|
|
69
78
|
| 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
79
|
| 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
80
|
|
|
@@ -177,6 +186,7 @@ git clone https://github.com/cad0p/pi-usage.git
|
|
|
177
186
|
cd pi-usage
|
|
178
187
|
./install.sh # --link for development, --uninstall to remove
|
|
179
188
|
./install.sh --no-extension # CLI only; keep the Pi extension installed as a package
|
|
189
|
+
./install.sh --omarchy-plugin # also link the AI Usage bar plugin (see Omarchy integration)
|
|
180
190
|
```
|
|
181
191
|
|
|
182
192
|
No sudo. It validates Node, Pi and Gentle AI, reports which Claude source it found,
|
|
@@ -212,6 +222,8 @@ piquota auth opencode --no-browser
|
|
|
212
222
|
piquota auth opencode --wait 300
|
|
213
223
|
|
|
214
224
|
piquota omarchy # write pi-*.json records for Omarchy's Agents panel
|
|
225
|
+
piquota stats # durable token history: last frozen day, families, totals
|
|
226
|
+
piquota stats --rebuild-totals # recompute totals.json from the frozen month files
|
|
215
227
|
```
|
|
216
228
|
|
|
217
229
|
Colours: green above 50% remaining, yellow 20โ50%, red below 20%.
|
|
@@ -230,10 +242,10 @@ Inside Pi:
|
|
|
230
242
|
|
|
231
243
|
| Command | Effect |
|
|
232
244
|
| --- | --- |
|
|
233
|
-
| `/usage` | one on-demand refresh, show the full panel, notify a one-line summary |
|
|
245
|
+
| `/usage` | one on-demand refresh, show the full panel until the next prompt (or a second `/usage`), notify a one-line summary |
|
|
234
246
|
| `/usage box` | pin the five-row box above the editor |
|
|
235
247
|
| `/usage line` | pin the compact line above the editor, in its own row |
|
|
236
|
-
| `/usage panel` | full
|
|
248
|
+
| `/usage panel` | full panel above the editor, pinned across prompts |
|
|
237
249
|
| `/usage hide` | hide the panel/line and stop background refresh until a surface is on again |
|
|
238
250
|
| `/usage status` / `nostatus` | enable/disable the footer status |
|
|
239
251
|
| `/usage refresh` | refresh only |
|
|
@@ -248,8 +260,9 @@ it fresh with a background refresh (and a gentle refresh after each agent turn).
|
|
|
248
260
|
`/usage hide` gives the row back.
|
|
249
261
|
|
|
250
262
|
A provider with several accounts is shown as one header (`OP-Go ยท 7 of 14 accounts
|
|
251
|
-
ok`) plus one line per account
|
|
252
|
-
|
|
263
|
+
ok`) plus one line per account with every window and its reset, e.g.
|
|
264
|
+
`#2 โ session 2% (10m) ยท week 10% (6d) ยท month 90% (19d)`, or the reason it failed
|
|
265
|
+
(`key rejected`, `no Go plan`). The leading glyph is the fullest window's band. The panel is rendered as a component, so Pi's ten-line
|
|
253
266
|
cap on plain widgets never truncates it, and the summary names the account with the
|
|
254
267
|
most room: `OP-Go: 7/14 ok ยท lowest 24% (#7)`.
|
|
255
268
|
|
|
@@ -363,6 +376,10 @@ Rp suite) without external network daemons or closed-source intermediaries:
|
|
|
363
376
|
* **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
|
|
364
377
|
allowing consumers to cleanly omit them without displaying false-positive
|
|
365
378
|
error indicators.
|
|
379
|
+
* **`localStats`**: the tokens Pi's own session files recorded, per family,
|
|
380
|
+
shaped exactly like the fields Omarchy's Agents panel reads (today, the last
|
|
381
|
+
seven local days, all-time per-model buckets) plus a `byAccount` breakdown for
|
|
382
|
+
numbered siblings. `localStatsMeta` says how the scan went.
|
|
366
383
|
|
|
367
384
|
```json
|
|
368
385
|
{
|
|
@@ -403,6 +420,33 @@ Rp suite) without external network daemons or closed-source intermediaries:
|
|
|
403
420
|
}
|
|
404
421
|
```
|
|
405
422
|
|
|
423
|
+
The token part of the same report is abridged here (`recentDays` always carries
|
|
424
|
+
seven entries, oldest first, today last):
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"localStats": {
|
|
429
|
+
"codex": {
|
|
430
|
+
"hasLocalStats": true,
|
|
431
|
+
"hasPromptStats": true,
|
|
432
|
+
"todayTotalTokens": 14172,
|
|
433
|
+
"todayPrompts": 3,
|
|
434
|
+
"todaySessions": 1,
|
|
435
|
+
"todayTokensByModel": { "gpt-5.1-codex": 14172 },
|
|
436
|
+
"weekTokensByModel": { "gpt-5.1-codex": 118422 },
|
|
437
|
+
"recentDays": [{ "date": "2026-10-27", "messageCount": 0 }],
|
|
438
|
+
"totalPrompts": 1200,
|
|
439
|
+
"totalSessions": 42,
|
|
440
|
+
"activeDays": 11,
|
|
441
|
+
"activeDates": ["2026-10-27"],
|
|
442
|
+
"modelUsage": { "gpt-5.1-codex": { "inputTokens": 4, "outputTokens": 425, "cacheReadInputTokens": 6838, "cacheCreationInputTokens": 6905 } },
|
|
443
|
+
"byAccount": { "#1": { "todayTokens": 14172, "weekTokens": 118422, "todayPrompts": 3, "totalPrompts": 1200, "totalTokens": 48812210 } }
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
"localStatsMeta": { "files": 604, "scanned": 1, "reused": 603, "bytesRead": 3042, "durationMs": 9, "partial": false, "budgetMs": 12000, "frozenDays": 0, "locked": false }
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
406
450
|
### Each provider refreshes on its own clock
|
|
407
451
|
|
|
408
452
|
Anthropic's usage endpoint answers `429` when polled every minute; Codex,
|
|
@@ -434,7 +478,9 @@ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
|
|
|
434
478
|
|
|
435
479
|
- Records are named `pi-<family>.json`, so they never overwrite Omarchy's own
|
|
436
480
|
`claude.json` / `codex.json`. They hold plan names, used percentages, window
|
|
437
|
-
labels
|
|
481
|
+
labels, reset times, and โ when the token scan has run โ aggregate token counts
|
|
482
|
+
per local day and per model (`hasLocalStats`, `recentDays`, `modelUsage`).
|
|
483
|
+
No prompt text, session path, account or e-mail address is copied.
|
|
438
484
|
- A provider with a failing sign-in shows a status and a help line instead of
|
|
439
485
|
limits. A provider that is not configured gets no record, and a stale
|
|
440
486
|
`pi-*.json` is removed on the next run.
|
|
@@ -442,13 +488,219 @@ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
|
|
|
442
488
|
`auth.json`) still gets **one** record: every healthy account contributes only
|
|
443
489
|
the window that constrains it, titled `#<slot> ยท Session|Weekly|Monthly`, and
|
|
444
490
|
failed accounts add no rows but are summarised by reason, e.g.
|
|
445
|
-
`#8โ#10 key rejected ยท #11โ#14 no Go plan`.
|
|
491
|
+
`#8โ#10 key rejected ยท #11โ#14 no Go plan`. The same record also carries
|
|
492
|
+
`accounts[{id, ok, status, limits}]` with every window of every account
|
|
493
|
+
(`status` is a short reason such as `Key rejected` for a failed one), which
|
|
494
|
+
Omarchy's built-in panel ignores and this package's own plugin draws.
|
|
446
495
|
- Omarchy's native collector already tracks Claude with richer local statistics, so
|
|
447
496
|
`piquota omarchy` automatically skips `claude` and cleans up duplicate `pi-claude.json`
|
|
448
497
|
files when `claude.json` exists in the usage directory (override with `PI_QUOTA_OMARCHY_SKIP`).
|
|
449
498
|
- `PI_QUOTA_OMARCHY_DIR` overrides the output directory. `./install.sh --uninstall`
|
|
450
499
|
removes the timer and the `pi-*.json` records.
|
|
451
500
|
|
|
501
|
+
### The AI Usage bar plugin
|
|
502
|
+
|
|
503
|
+
The package also ships its own Omarchy bar widget, `cad0p.pi-usage` ("AI
|
|
504
|
+
Usage", in [`omarchy/cad0p.pi-usage/`](omarchy/cad0p.pi-usage/)). It is derived
|
|
505
|
+
from Omarchy's built-in Agents plugin (MIT, by Omarchy) and reuses the shell's
|
|
506
|
+
own panel components, so it looks the same, but it has no tabs: every provider
|
|
507
|
+
is a collapsible section of one scrolling panel.
|
|
508
|
+
|
|
509
|
+
- **Header row**: the provider's mark, name and plan, the headline percentage
|
|
510
|
+
with its window and reset (`Weekly ยท 1d`), a chip when something is wrong
|
|
511
|
+
(`Unavailable`, `7 down`), and the headline meter while folded. The headline is
|
|
512
|
+
the fullest window; for a multi-account provider it is the healthy account
|
|
513
|
+
with the most room left, the `lowest` figure `/usage` prints.
|
|
514
|
+
- **Expanded**: the status card, LIMITS with the built-in meters and countdowns,
|
|
515
|
+
then for a multi-account record one row per account with every window
|
|
516
|
+
(`#2 session 2% 10m ยท week 10% 6d ยท month 90% 19d`, failed accounts dimmed
|
|
517
|
+
with their reason), then TOKENS BY DAY and TOKENS BY MODEL whenever the record
|
|
518
|
+
carries local statistics, drawn exactly as the built-in draws them.
|
|
519
|
+
- **Which section is open**: on first use, only the provider closest to a limit
|
|
520
|
+
(ties go to the earliest reset). Every expand or collapse you make sticks,
|
|
521
|
+
across popup opens and shell restarts, in
|
|
522
|
+
`~/.local/state/omarchy/settings/cad0p.pi-usage.json`.
|
|
523
|
+
- **Keys**: `j`/`k` or the arrows walk the section headers (a tall open section
|
|
524
|
+
scrolls first), Enter or Space toggles, `l`/`h` expand/collapse, `r` refreshes,
|
|
525
|
+
Tab moves to the neighbouring bar panel, Esc closes. The mouse wheel scrolls;
|
|
526
|
+
a click on a header toggles it.
|
|
527
|
+
- **Records**: every `*.json` in the usage directory, Omarchy's own included.
|
|
528
|
+
A provider is hidden by `providers.<id>.enabled = false` in the widget's
|
|
529
|
+
settings, the same switch the built-in uses. The plugin does **not** support
|
|
530
|
+
the built-in Agents widget's synced cross-device aggregation
|
|
531
|
+
(`syncMode`, `syncDir`, `syncFileName`, `syncDeviceId`). Users who rely on
|
|
532
|
+
synced aggregation should keep the built-in widget; this plugin is not a full
|
|
533
|
+
replacement for it.
|
|
534
|
+
- **Refresh**: the systemd timer and the Pi extension already publish the
|
|
535
|
+
`pi-*.json` records, so the plugin never schedules `piquota`. `r`, a middle
|
|
536
|
+
click or `omarchy-shell cad0p.pi-usage refresh` starts
|
|
537
|
+
`piquota-omarchy.service` (or runs the package's `piquota omarchy` when there is
|
|
538
|
+
no unit). Omarchy's collectors (`omarchy-agent-usage-update`) run on the
|
|
539
|
+
`refreshIntervalSec` timer and for a record's `retryAdvised` retry, and only
|
|
540
|
+
while the built-in Agents widget is off the bar (`omarchyCollectors`: `Auto`,
|
|
541
|
+
`On` or `Off`, where `Off` never runs them), so keeping both never doubles the
|
|
542
|
+
work.
|
|
543
|
+
|
|
544
|
+
Install it by linking it into Omarchy's plugin directory; the link points into
|
|
545
|
+
the pi package when there is one, so `pi update` updates the plugin too:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
./install.sh --omarchy-plugin # symlink ~/.config/omarchy/plugins/cad0p.pi-usage (idempotent)
|
|
549
|
+
omarchy plugin enable cad0p.pi-usage # put it on the bar (right section)
|
|
550
|
+
omarchy bar put cad0p.pi-usage --after omarchy.agents # or: right next to the built-in Agents icon
|
|
551
|
+
omarchy plugin disable omarchy.agents # optional: drop the built-in widget
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Without the installer, the link is
|
|
555
|
+
`ln -s ~/.pi/agent/npm/node_modules/@cad0p/pi-usage/omarchy/cad0p.pi-usage ~/.config/omarchy/plugins/cad0p.pi-usage`
|
|
556
|
+
followed by `omarchy-shell shell rescanPlugins`. To hide providers, pass the whole
|
|
557
|
+
object, as with the built-in:
|
|
558
|
+
`omarchy bar set cad0p.pi-usage providers '{"fireworks":{"enabled":false}}' --json`.
|
|
559
|
+
Remove it with `omarchy plugin disable cad0p.pi-usage` and
|
|
560
|
+
`rm ~/.config/omarchy/plugins/cad0p.pi-usage` (`./install.sh --uninstall` removes
|
|
561
|
+
the link too); `omarchy plugin enable omarchy.agents` brings the built-in back.
|
|
562
|
+
|
|
563
|
+
IPC: `omarchy-shell cad0p.pi-usage <open|close|toggle|refresh>`, plus
|
|
564
|
+
`expand <id>` / `collapse <id>` (e.g. `expand pi-opencode-go`).
|
|
565
|
+
|
|
566
|
+
`tests/omarchy-harness/render.sh [out-dir] [--light]` renders the panel offscreen
|
|
567
|
+
to PNGs against fixture records, with Omarchy's real components and the current
|
|
568
|
+
theme, next to the built-in panel for comparison. It never touches the running
|
|
569
|
+
shell.
|
|
570
|
+
|
|
571
|
+
## Token stats from Pi's own sessions
|
|
572
|
+
|
|
573
|
+
Pi writes every assistant message's `usage` block into
|
|
574
|
+
`~/.pi/agent/sessions/**.jsonl` (plus `~/.omp/agent/sessions` when the omp fork
|
|
575
|
+
is installed; `PI_CODING_AGENT_DIR` relocates the Pi agent directory). piQuota
|
|
576
|
+
folds those messages into tokens per local day and per model, per provider, and
|
|
577
|
+
shows them next to the quota:
|
|
578
|
+
|
|
579
|
+
| Where | What |
|
|
580
|
+
| --- | --- |
|
|
581
|
+
| `piquota --json` | `localStats.<family>` โ the Omarchy-shaped counters plus `byAccount` |
|
|
582
|
+
| `/usage` panel | a `tokens` row per provider: today, 7 days, prompts, then the top 3 models of the week |
|
|
583
|
+
| `piquota omarchy` | the optional fields Omarchy's Agents panel draws as "Tokens by day" and "Tokens by model" |
|
|
584
|
+
| `piquota stats` | the durable history: last frozen day, families, prompts, session-days |
|
|
585
|
+
|
|
586
|
+
What counts: one assistant message with a non-zero token count is one prompt,
|
|
587
|
+
and its tokens are `input + output + cacheRead + cacheWrite` โ the same sum the
|
|
588
|
+
Omarchy panel renders. Messages are bucketed by **local** date (the panel calls
|
|
589
|
+
the last row "Today"). Only providers piQuota can bill are counted:
|
|
590
|
+
`anthropic`, `openai-codex`, `antigravity`, `opencode-go` and their numbered
|
|
591
|
+
siblings, so `opencode-go-7` is account `#7`. Providers it does not manage
|
|
592
|
+
(`deepinfra`, `xai`, ...) are ignored, and so is the custom `claude-bridge`
|
|
593
|
+
provider: its tokens are not a Pi subscription.
|
|
594
|
+
|
|
595
|
+
**Two stores, two lifetimes.** Open days (normally just today) live in the
|
|
596
|
+
disposable scan cache. Once a local day is over it is frozen into a durable
|
|
597
|
+
history that no rescan, no cache wipe and no session deletion can rewrite. The
|
|
598
|
+
history is Pi data, so it lives next to Pi's agent directory:
|
|
599
|
+
|
|
600
|
+
| Path | What |
|
|
601
|
+
| --- | --- |
|
|
602
|
+
| `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/totals.json` | all-time rollup per family and account (models, prompts, session-days, active dates) plus `lastFrozenDay` |
|
|
603
|
+
| `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/tokens-YYYY-MM.json` | the frozen days of one local month, keyed day โ family โ slot โ model |
|
|
604
|
+
| `~/.cache/pi-quota/local-sessions.json` | per-session-file size, mtime, byte offset and copy fingerprint, open days, and pending late deltas for unreadable months |
|
|
605
|
+
|
|
606
|
+
> `~/.pi` is often a git repository. Add `agent/pi-usage/` to that repo's
|
|
607
|
+
> `.gitignore` if you do not want your token counts committed.
|
|
608
|
+
|
|
609
|
+
Freezing: every run moves each day before today that is not frozen yet into its
|
|
610
|
+
month file and into `totals.json`, then drops it from the per-file cache. The
|
|
611
|
+
month file is written first and the totals second; the `lastFrozenDay` watermark
|
|
612
|
+
makes a restart re-freeze a half-written day idempotently (the month write is a
|
|
613
|
+
SET per day) instead of adding it twice. A late write to an already-frozen day โ
|
|
614
|
+
a session that appends just past midnight โ increments both stores directly; if
|
|
615
|
+
its month is temporarily unreadable, the delta stays in the cache until a later
|
|
616
|
+
run can write it. Such a held delta lives only in the disposable cache: if the
|
|
617
|
+
cache is lost before the month becomes writable, that delta is dropped, because
|
|
618
|
+
the bytes of a frozen day are never read again. A crash between the month and totals writes can double that
|
|
619
|
+
one small delta. A budget-truncated scan never freezes: history is only written
|
|
620
|
+
from complete runs.
|
|
621
|
+
|
|
622
|
+
Consequences, all covered by tests: history survives session deletion and cache
|
|
623
|
+
loss. A lost or corrupt `totals.json` is moved aside as
|
|
624
|
+
`totals.json.corrupt-<ts>` and **rebuilt from the month files**, which are the
|
|
625
|
+
authoritative record of frozen days โ not from the session files, because a
|
|
626
|
+
session deleted since then would otherwise shrink history. Frozen days are never
|
|
627
|
+
recomputed, so a replaced session file cannot rewrite them either.
|
|
628
|
+
|
|
629
|
+
Damaged month files are handled without rebuilding frozen days from sessions. A
|
|
630
|
+
month that cannot be *read* (permissions, I/O) is left alone: the run warns, keeps
|
|
631
|
+
its open days in the cache, and does not freeze them until the month is readable.
|
|
632
|
+
Other history proceeds normally, and this alone does not force a full session
|
|
633
|
+
rescan. A rollup rebuild never writes `totals.json` from an incomplete month set.
|
|
634
|
+
|
|
635
|
+
A *corrupt* or foreign-version month is moved aside by the lock owner (never by a
|
|
636
|
+
read-only run) to `.corrupt-<ts>`. Its warning names the file and directs you to copy the
|
|
637
|
+
`.corrupt-<ts>` backup back to the original month path if recoverable, or to delete
|
|
638
|
+
the backup to abandon those counts. Until one of the two happens, freezing stops at
|
|
639
|
+
that month for *every* later day too (the frozen watermark only moves forward), so
|
|
640
|
+
later days stay open in the cache and each run repeats the warning; once the backup
|
|
641
|
+
is restored or deleted, the held days freeze on the next run. Frozen days are never rebuilt from sessions: their
|
|
642
|
+
daily rows are missing (shown as 0) in `recentDays`, while `totals.json` retains
|
|
643
|
+
the all-time counts, so the two views may disagree. `piquota stats --rebuild-totals`
|
|
644
|
+
refuses while a quarantined month has not been restored. A lost *month* file
|
|
645
|
+
loses that month's daily detail; normal runs keep the all-time totals, and a
|
|
646
|
+
rebuild can recover counts from every remaining month file even if sessions are
|
|
647
|
+
gone. Rebuild refuses when there is no month file at all while `totals.json`
|
|
648
|
+
still has data, because replacing that rollup with an empty one would turn a
|
|
649
|
+
missing directory into permanently lost history. A normal run reads only
|
|
650
|
+
`totals.json` plus the month files covering the last seven days (current month,
|
|
651
|
+
and the previous one early in a month), so years of history never slow a run: on
|
|
652
|
+
this machine 45 frozen days cost ~28 KB in total โ `totals.json` 6.9 KB and two
|
|
653
|
+
month files of 5.4 KB and 16 KB. A day inside that window is read from its month
|
|
654
|
+
file once frozen, so late writes appear in "tokens by day" without waiting for
|
|
655
|
+
the week to roll over.
|
|
656
|
+
|
|
657
|
+
Cost: the first run streams each session file once โ 1.56 GB across 608 files
|
|
658
|
+
in 3.0 s here; later runs read only the bytes appended since the previous scan
|
|
659
|
+
(59 ms of scanning for the whole store, ~20 KB read), and every byte is parsed
|
|
660
|
+
once in normal operation. The CLI's own wall time is dominated by the provider
|
|
661
|
+
calls either way.
|
|
662
|
+
|
|
663
|
+
What forces a file to be read from its first byte (a *rescan*, in which frozen
|
|
664
|
+
days are skipped and only open days are rebuilt): the cache missing or corrupt,
|
|
665
|
+
the file replaced or now shorter, a path the cache has never seen (a copy, a
|
|
666
|
+
move, a restored backup โ its old days were already frozen under its previous
|
|
667
|
+
path), or the same size with a new mtime (an in-place rewrite of the same
|
|
668
|
+
length). An append only reads the new bytes. The one rewrite this cannot see is
|
|
669
|
+
one that restores both the size *and* the mtime, which no stat-based check can
|
|
670
|
+
detect. The cache is atomic and `0600`.
|
|
671
|
+
|
|
672
|
+
Two writers are routine here (the systemd timer and `/usage`), so a run takes an
|
|
673
|
+
exclusive lock โ `local-sessions.json.lock`, with a 2-minute stale timeout โ
|
|
674
|
+
before it reads the history. The file holds an owner token, and a live run
|
|
675
|
+
refreshes its mtime as it walks the store, so a slow scan is never mistaken for a
|
|
676
|
+
dead one. A run that cannot take the lock reports the numbers already on disk and
|
|
677
|
+
writes nothing; a run that is taken over mid-scan notices on its next refresh and
|
|
678
|
+
also writes nothing (it never deletes its successor's lock).
|
|
679
|
+
|
|
680
|
+
Duplicate *sessions* are handled two ways. A byte-identical copy of a session
|
|
681
|
+
file (a copy, a restored backup, a sync tool) is recognised by a (size, first
|
|
682
|
+
32 KB, last 32 KB) fingerprint and contributes nothing; anything appended to it
|
|
683
|
+
later is counted normally, and if the original is deleted the copy is adopted and
|
|
684
|
+
read on the next run. A fingerprint can collide โ two same-size sessions that
|
|
685
|
+
differ only in the middle โ so a candidate is verified by streaming both files
|
|
686
|
+
before it is called a copy; that full comparison runs only on the rare duplicate.
|
|
687
|
+
A *fork* that copies a prefix and then diverges has its own size and tail, so it
|
|
688
|
+
is counted as a session of its own, duplicated prefix included: on this machine
|
|
689
|
+
(611 files, 186,443 assistant messages) only 11 message ids appeared in more than
|
|
690
|
+
one file (0.006%), none of them on today, so message-id de-duplication would cost
|
|
691
|
+
megabytes to correct a handful of tokens and is not implemented.
|
|
692
|
+
|
|
693
|
+
The scan has a wall-clock budget (12 s, or `PI_QUOTA_LOCAL_STATS_BUDGET_MS`) so
|
|
694
|
+
the first run cannot push `/usage` past its 30 s exec timeout; when it runs out
|
|
695
|
+
the report carries `localStatsMeta.partial`, the panel marks the numbers
|
|
696
|
+
`scan partial`, and nothing is frozen that run. `PI_QUOTA_LOCAL_STATS=0` turns
|
|
697
|
+
the scan off.
|
|
698
|
+
|
|
699
|
+
Session counts are **session-days**, not distinct session files: a day is frozen
|
|
700
|
+
as aggregate rows, without the list of files that fed it, so a session that runs
|
|
701
|
+
past midnight counts once on each day it appears in. `todaySessions` is exact
|
|
702
|
+
(those files are still open); `totalSessions` is the sum of the per-day counts.
|
|
703
|
+
|
|
452
704
|
## Layout
|
|
453
705
|
|
|
454
706
|
```
|
|
@@ -465,6 +717,8 @@ src/providers/antigravity-oauth.js in-memory refresh with Google's public clien
|
|
|
465
717
|
src/providers/backoff.js per-family throttle state
|
|
466
718
|
src/sticky.js last published / last good snapshots and throttle recovery
|
|
467
719
|
src/engine.js collectQuota() -> one normalized report
|
|
720
|
+
src/local/pi-sessions.js token counts from Pi's session files, incremental scan
|
|
721
|
+
src/local/history.js frozen local-day history: month files + all-time rollup
|
|
468
722
|
src/model.js window normalization, percent and reset parsing
|
|
469
723
|
src/render/{theme,panel}.js colors, thresholds, rings, bars, boxed panel
|
|
470
724
|
src/cache.js report cache at ~/.cache/pi-quota/usage.json
|
|
@@ -472,9 +726,12 @@ src/omarchy/record.js pure mapping from the report to Omarchy usage rec
|
|
|
472
726
|
src/omarchy/publish.js atomic writer for pi-*.json, and stale-record cleanup
|
|
473
727
|
src/refresh.js per-family refresh clocks, and the merge back into one report
|
|
474
728
|
src/cli/args.js argument parsing, and the flag/positional split
|
|
729
|
+
src/cli/commands/stats.js `piquota stats` and `--rebuild-totals`
|
|
475
730
|
src/exec.js the one place that spawns gh CLI binary
|
|
476
731
|
src/http.js fetch wrapper: timeouts, JSON, redaction
|
|
477
732
|
contrib/systemd/ user service and timer behind `install.sh --omarchy-timer`
|
|
733
|
+
omarchy/cad0p.pi-usage/ the AI Usage Omarchy bar plugin (QML; UsageLogic.js is its pure logic)
|
|
734
|
+
tests/omarchy-harness/ offscreen renderer for the plugin, fixtures included
|
|
478
735
|
bin/piquota.js the only CLI
|
|
479
736
|
extensions/quota-panel.ts Pi TUI line and /usage
|
|
480
737
|
```
|
|
@@ -498,7 +755,7 @@ extensions/quota-panel.ts Pi TUI line and /usage
|
|
|
498
755
|
## Tests
|
|
499
756
|
|
|
500
757
|
```bash
|
|
501
|
-
node --test tests/*.test.mjs #
|
|
758
|
+
node --test tests/*.test.mjs # 354 tests, fake tokens only, no network
|
|
502
759
|
```
|
|
503
760
|
|
|
504
761
|
Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
|
|
@@ -508,9 +765,14 @@ Actions plan resolution, opt-in gating and degradation, the two Antigravity
|
|
|
508
765
|
failures and the OpenCode degradation), the dashboard parser's three
|
|
509
766
|
strategies, the Firefox cookie reader against a synthetic SQLite database, the
|
|
510
767
|
Antigravity refresh (in-memory only), sticky and last-good snapshots resilience,
|
|
511
|
-
the Omarchy record mapping and atomic writer, the
|
|
512
|
-
|
|
513
|
-
|
|
768
|
+
the Omarchy record mapping and atomic writer, the Omarchy plugin's logic,
|
|
769
|
+
manifest and marks, the local token scan (family and slot mapping, incremental
|
|
770
|
+
append, shrink, deletion, local-day bucketing, cache corruption), the durable
|
|
771
|
+
history (month partitioning, freezing at midnight, late appends to frozen days,
|
|
772
|
+
cache loss, a crash between the month and totals writes, corrupt-history
|
|
773
|
+
quarantine, rebuild-totals), the renderers, the Pi extension contract, the
|
|
774
|
+
per-family refresh clocks, argument parsing, and the structure of these
|
|
775
|
+
documents themselves (tables, fences, links, anchors).
|
|
514
776
|
|
|
515
777
|
## Acknowledgments & Prior Art
|
|
516
778
|
|
package/bin/piquota.js
CHANGED
|
@@ -19,6 +19,7 @@ import { parseArgs } from "../src/cli/args.js";
|
|
|
19
19
|
import { runAuthCommand } from "../src/cli/commands/auth.js";
|
|
20
20
|
import { runGentlePiCommand } from "../src/cli/commands/gentle-pi.js";
|
|
21
21
|
import { runOmarchyCommand } from "../src/cli/commands/omarchy.js";
|
|
22
|
+
import { runStatsCommand } from "../src/cli/commands/stats.js";
|
|
22
23
|
import { explainLines, resolveFamilies } from "../src/cli/explain.js";
|
|
23
24
|
import { err, out } from "../src/cli/output.js";
|
|
24
25
|
import { collectQuota, enabledFamilies, normalizeClaudeSourceMode } from "../src/engine.js";
|
|
@@ -35,6 +36,20 @@ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.u
|
|
|
35
36
|
*/
|
|
36
37
|
const CACHE_KEY = `claude-source:${normalizeClaudeSourceMode(process.env.PI_QUOTA_CLAUDE_SOURCE)}`;
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Local token statistics from Pi's session files are on for the CLI: the Omarchy
|
|
41
|
+
* records and /usage both show them. `PI_QUOTA_LOCAL_STATS=0` turns the scan
|
|
42
|
+
* off (e.g. on a machine where the session store is huge and network quota is
|
|
43
|
+
* all that matters), and the budget keeps a first full scan from delaying a
|
|
44
|
+
* report past the 30 s the /usage extension allows.
|
|
45
|
+
*/
|
|
46
|
+
const LOCAL_STATS_BUDGET_MS = Number(process.env.PI_QUOTA_LOCAL_STATS_BUDGET_MS);
|
|
47
|
+
const LOCAL_STATS = process.env.PI_QUOTA_LOCAL_STATS === "0"
|
|
48
|
+
? false
|
|
49
|
+
: LOCAL_STATS_BUDGET_MS > 0
|
|
50
|
+
? { budgetMs: LOCAL_STATS_BUDGET_MS }
|
|
51
|
+
: {};
|
|
52
|
+
|
|
38
53
|
const HELP = `piquota ${VERSION} โ read-only quota from Pi's provider credentials
|
|
39
54
|
|
|
40
55
|
Usage:
|
|
@@ -42,6 +57,7 @@ Usage:
|
|
|
42
57
|
piquota auth <opencode|status> [flags]
|
|
43
58
|
piquota gentle-pi <status|apply|revert>
|
|
44
59
|
piquota omarchy
|
|
60
|
+
piquota stats [--rebuild-totals]
|
|
45
61
|
|
|
46
62
|
Quota:
|
|
47
63
|
--json Emit the normalized report as JSON (for rp-host and automations)
|
|
@@ -56,6 +72,7 @@ Quota:
|
|
|
56
72
|
--no-refresh Never refresh Antigravity's token in memory
|
|
57
73
|
--explain Show which stores/fields are read (names only)
|
|
58
74
|
--clear-cache Delete the local cache and exit
|
|
75
|
+
--rebuild-totals With \`stats\`: rebuild the token-history totals from its month files
|
|
59
76
|
--version Print the version and exit
|
|
60
77
|
|
|
61
78
|
Auth (OpenCode Go session, the only credential Pi does not store):
|
|
@@ -66,18 +83,31 @@ Auth (OpenCode Go session, the only credential Pi does not store):
|
|
|
66
83
|
piquota auth opencode --wait <s> how long to wait for the login (default 180)
|
|
67
84
|
piquota auth status report every credential source
|
|
68
85
|
|
|
86
|
+
Stats:
|
|
87
|
+
piquota stats summarise the durable token history
|
|
88
|
+
(dir: \${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage)
|
|
89
|
+
piquota stats --rebuild-totals recompute totals.json from the frozen month files
|
|
90
|
+
|
|
69
91
|
Omarchy:
|
|
70
92
|
piquota omarchy write pi-<family>.json records for Omarchy's Agents bar panel
|
|
71
93
|
(dir: $PI_QUOTA_OMARCHY_DIR or $XDG_STATE_HOME/omarchy/agents/usage)
|
|
72
94
|
run it on a timer: ./install.sh --omarchy-timer
|
|
73
95
|
|
|
96
|
+
Environment:
|
|
97
|
+
PI_QUOTA_LOCAL_STATS=0 skip the token scan of Pi's session files
|
|
98
|
+
PI_QUOTA_LOCAL_STATS_BUDGET_MS wall-clock budget for that scan (default 12000)
|
|
99
|
+
|
|
74
100
|
Guarantees:
|
|
75
101
|
* ~/.pi/agent/auth.json is opened read-only. Never written, synced or refreshed.
|
|
102
|
+
* Pi's session files are opened read-only too; the token scan keeps only
|
|
103
|
+
aggregate counts per day and model, never prompt text or session paths.
|
|
76
104
|
* Antigravity's access token may be refreshed in memory; it is never persisted.
|
|
77
105
|
* Browser cookie databases are copied and opened read-only; values are never logged.
|
|
78
|
-
* Only percentages, window labels, reset times
|
|
106
|
+
* Only percentages, window labels, reset times, plan names and aggregate token
|
|
107
|
+
counts leave this machine.
|
|
79
108
|
* \`omarchy\` (opt-in) writes only pi-*.json records, atomically, into Omarchy's usage dir;
|
|
80
|
-
records hold plan names, percentages, window labels
|
|
109
|
+
records hold plan names, percentages, window labels, reset times and aggregate
|
|
110
|
+
token counts, never prompt text or accounts.
|
|
81
111
|
* \`gentle-pi\` is the one file this tool edits outside its own state: it appends
|
|
82
112
|
piQuota's part to gentle-pi's rail allowlist so the card can be painted there.
|
|
83
113
|
The edit is one array literal, it is reversible with \`piquota gentle-pi revert\`,
|
|
@@ -122,10 +152,24 @@ async function main() {
|
|
|
122
152
|
return runGentlePiCommand(sub);
|
|
123
153
|
}
|
|
124
154
|
|
|
155
|
+
if (args.rebuildTotals && command !== "stats") {
|
|
156
|
+
// The flag repairs one thing (the all-time rollup) and reads nothing else,
|
|
157
|
+
// so running it beside `piquota` or `piquota omarchy` would silently skip
|
|
158
|
+
// the command the user asked for.
|
|
159
|
+
err("--rebuild-totals only works with `piquota stats`\n");
|
|
160
|
+
process.stdout.write(HELP);
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (command === "stats") {
|
|
165
|
+
return runStatsCommand({ rebuild: args.rebuildTotals, env: process.env });
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
if (command === "omarchy") {
|
|
126
169
|
const load = async () => {
|
|
127
170
|
const everyFamily = enabledFamilies({ env: process.env });
|
|
128
|
-
const loadFresh = () =>
|
|
171
|
+
const loadFresh = () =>
|
|
172
|
+
collectQuota({ families: everyFamily, timeoutMs: args.timeoutMs, refresh, force: args.force, localStats: LOCAL_STATS });
|
|
129
173
|
const fresh = args.noCache
|
|
130
174
|
? await loadFresh()
|
|
131
175
|
: (await withCache({ ttlMs: args.ttlMs, force: args.force, families: everyFamily, key: CACHE_KEY }, loadFresh)).report;
|
|
@@ -149,7 +193,8 @@ async function main() {
|
|
|
149
193
|
requested: families.length > 0 ? families : undefined,
|
|
150
194
|
explicit,
|
|
151
195
|
});
|
|
152
|
-
const load = () =>
|
|
196
|
+
const load = () =>
|
|
197
|
+
collectQuota({ families: selected, explicit, timeoutMs: args.timeoutMs, refresh, force: args.force, localStats: LOCAL_STATS });
|
|
153
198
|
|
|
154
199
|
let report;
|
|
155
200
|
let cached = false;
|