@cad0p/pi-usage 0.11.3 โ†’ 0.11.4-20261103.1

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 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
@@ -6,8 +6,9 @@ One source of truth for quota: **the credentials Pi already owns**.
6
6
  > `@cad0p/pi-usage` is a fork of [J3pf/piQuota](https://github.com/J3pf/piQuota) (`@j3pf/piquota`). It adds:
7
7
  >
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
- > * **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.
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. The accounts of one provider are queried in parallel (8 at a time, `PI_QUOTA_ACCOUNT_CONCURRENCY` from 1 to 32), so 14 OpenCode Go keys cost about one round trip instead of fourteen. Every request carries `User-Agent: pi-usage/<version> (+https://github.com/cad0p/pi-usage)` unless a provider needs its own client string: OpenCode Go asks clients not to use a generic HTTP-library name, and Node's default is `node`.
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,11 @@ 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 --json --month 2026-11
227
+ # read-only history feed: months, one month's days
228
+ # per family/slot/model, all-time totals, notes
229
+ piquota stats --rebuild-totals # recompute totals.json from the frozen month files
215
230
  ```
216
231
 
217
232
  Colours: green above 50% remaining, yellow 20โ€“50%, red below 20%.
@@ -230,10 +245,10 @@ Inside Pi:
230
245
 
231
246
  | Command | Effect |
232
247
  | --- | --- |
233
- | `/usage` | one on-demand refresh, show the full panel, notify a one-line summary |
248
+ | `/usage` | open the fullscreen quota overlay: every provider, window, account and token row, folded by section, refreshed in the background; `tab` opens the month history; `esc` closes it |
234
249
  | `/usage box` | pin the five-row box above the editor |
235
250
  | `/usage line` | pin the compact line above the editor, in its own row |
236
- | `/usage panel` | full boxed panel above the editor |
251
+ | `/usage panel` | full panel above the editor, pinned across prompts |
237
252
  | `/usage hide` | hide the panel/line and stop background refresh until a surface is on again |
238
253
  | `/usage status` / `nostatus` | enable/disable the footer status |
239
254
  | `/usage refresh` | refresh only |
@@ -241,17 +256,81 @@ Inside Pi:
241
256
 
242
257
  ### The Pi surface
243
258
 
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.
259
+ Nothing is pinned by default: `/usage` opens a **fullscreen overlay** that takes
260
+ the keyboard, so it claims no row above the editor and leaves the footer alone.
261
+ It is the same `ctx.ui.custom` overlay pattern pi's `/context` uses: it fills the
262
+ terminal, scrolls, folds and closes on Esc (or `q`), leaving nothing behind.
263
+ Turn a persistent surface on with `/usage box`, `/usage line`, `/usage panel` or
264
+ `/usage status`; only then does the extension keep it fresh with a background
265
+ refresh (and a gentle refresh after each agent turn). `/usage hide` gives the row
266
+ back. `/usage panel` still pins the same panel flat above the editor.
267
+
268
+ The overlay paints on the same keystroke: it renders the report the extension
269
+ already holds, or the CLI's on-disk report cache (`~/.cache/pi-quota/usage.json`,
270
+ read-only) when memory is empty, and never waits for the CLI before the first
271
+ frame. The same forced refresh bare `/usage` always ran then runs behind it: the
272
+ header says `refreshing โ€ฆ` while it does, and the update time when it lands. A
273
+ failed refresh is a line in the view, never a thrown error. Every provider is a section:
274
+ the header is the brand-coloured name line, the body lists every window with its
275
+ used-fraction bar, reset countdown and the token rows for today, the last seven
276
+ days, the prompts and the heaviest models. All sections start expanded; the fold
277
+ state and the page you left live for the pi session, so reopening `/usage`
278
+ resumes them, and no file is written.
279
+
280
+ | Key | Action |
281
+ | --- | --- |
282
+ | `โ†‘`/`โ†“`, `j`/`k` | move the header cursor; a section taller than the viewport scrolls line by line first |
283
+ | `enter` / `space` | fold or unfold the section under the cursor |
284
+ | `โ†’`/`l`, `โ†`/`h` | expand, collapse |
285
+ | `PgUp`/`PgDn`, `g`/`Home`, `G`/`End` | page, first section, last section |
286
+ | mouse wheel | scroll |
287
+ | `r` | refresh in place (on History: the month and the live report) |
288
+ | `tab`, `1` / `2` | switch between Now and History |
289
+ | `esc` / `q` | close |
290
+
291
+ The legend at the bottom lists every key the current view answers, and wraps
292
+ into a second or third row on a narrow pane instead of dropping pairs โ€” as few
293
+ rows as fit, with the pairs balanced across them. The frame
294
+ fits from about 60 columns up: a line wider than the terminal wraps at its
295
+ ` ยท ` separators with a hanging indent instead of being cut, so a row never
296
+ loses the datum it ends with, and a resize rebuilds it. The wheel is handled by the component itself, like
297
+ pi-context-view: pi routes fullscreen wheel events to the focused component but
298
+ has no scrolling API for a custom screen, and this extension has no runtime
299
+ dependencies to wrap the body in a `ScrollView`.
300
+
301
+ `tab` (or `2`) opens **History**, the second page: the durable token history for
302
+ one month, with the same foldable sections. A provider section lists the month's
303
+ days, the token split by model, and โ€” for a multi-account provider โ€” one row per
304
+ account with its month and all-time totals (every account the family has in the
305
+ month *or* all-time, so one busy key does not hide the others). `โ†`/`โ†’` (or
306
+ `[`/`]`) walk the browsable months โ€” those with a file, plus the live month,
307
+ which is where History opens even before its first freeze โ€” and `tab` (or `1`)
308
+ comes back to Now. The all-time
309
+ figure is `totals.json` (frozen through its watermark). A family's open day
310
+ (today) is merged from the live report only when the report was *generated* today
311
+ and the frozen month has no row for that family's day โ€” per family, so one family
312
+ frozen early never hides another's live row โ€” and a day is counted exactly once.
313
+ A report generated before midnight, or the older report cache painted on the first
314
+ frame, is skipped: its `todayTotalTokens` belong to another day, and merging them
315
+ under today would count a frozen yesterday twice. The tail then names the report's
316
+ day and says today is not merged. A missing or quarantined month is a note instead
317
+ of rows, a failed `stats` run shows the CLI's own stderr line (redacted) or its
318
+ exit status with a retry hint, and a slow fetch never moves the page off the month
319
+ the last keypress asked for.
320
+
321
+ History reads `piquota stats --json [--month YYYY-MM]`: the month list, one
322
+ month's days per family/slot/model, the all-time rollup and notes. That command
323
+ never takes the writer lock and never writes, so it is safe to run while a scan
324
+ is freezing a day.
249
325
 
250
326
  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)`.
327
+ ok`) plus one line per account with every window and its reset, e.g.
328
+ `#2 โ— session 2% (10m) ยท week 10% (6d) ยท month 90% (19d)`, or the reason it failed
329
+ (`key rejected`, `no Go plan`). The base entry in `auth.json` (`opencode-go`) has
330
+ no number, so its label column is blank; only real sibling slots (`opencode-go-2`
331
+ โ€ฆ) are numbered `#2` โ€ฆ `#14`. The leading glyph is the fullest window's band. The
332
+ overlay and the pinned panel are rendered as components, so Pi's ten-line cap on
333
+ plain widgets never truncates them.
255
334
 
256
335
  `/usage box` pins the **five-row box above the editor**:
257
336
 
@@ -363,6 +442,10 @@ Rp suite) without external network daemons or closed-source intermediaries:
363
442
  * **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
364
443
  allowing consumers to cleanly omit them without displaying false-positive
365
444
  error indicators.
445
+ * **`localStats`**: the tokens Pi's own session files recorded, per family,
446
+ shaped exactly like the fields Omarchy's Agents panel reads (today, the last
447
+ seven local days, all-time per-model buckets) plus a `byAccount` breakdown for
448
+ numbered siblings. `localStatsMeta` says how the scan went.
366
449
 
367
450
  ```json
368
451
  {
@@ -403,6 +486,33 @@ Rp suite) without external network daemons or closed-source intermediaries:
403
486
  }
404
487
  ```
405
488
 
489
+ The token part of the same report is abridged here (`recentDays` always carries
490
+ seven entries, oldest first, today last):
491
+
492
+ ```json
493
+ {
494
+ "localStats": {
495
+ "codex": {
496
+ "hasLocalStats": true,
497
+ "hasPromptStats": true,
498
+ "todayTotalTokens": 14172,
499
+ "todayPrompts": 3,
500
+ "todaySessions": 1,
501
+ "todayTokensByModel": { "gpt-5.1-codex": 14172 },
502
+ "weekTokensByModel": { "gpt-5.1-codex": 118422 },
503
+ "recentDays": [{ "date": "2026-10-27", "messageCount": 0 }],
504
+ "totalPrompts": 1200,
505
+ "totalSessions": 42,
506
+ "activeDays": 11,
507
+ "activeDates": ["2026-10-27"],
508
+ "modelUsage": { "gpt-5.1-codex": { "inputTokens": 4, "outputTokens": 425, "cacheReadInputTokens": 6838, "cacheCreationInputTokens": 6905 } },
509
+ "byAccount": { "": { "todayTokens": 14172, "weekTokens": 118422, "todayPrompts": 3, "totalPrompts": 1200, "totalTokens": 48812210 } }
510
+ }
511
+ },
512
+ "localStatsMeta": { "files": 604, "scanned": 1, "reused": 603, "bytesRead": 3042, "durationMs": 9, "partial": false, "budgetMs": 12000, "frozenDays": 0, "locked": false }
513
+ }
514
+ ```
515
+
406
516
  ### Each provider refreshes on its own clock
407
517
 
408
518
  Anthropic's usage endpoint answers `429` when polled every minute; Codex,
@@ -434,7 +544,9 @@ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
434
544
 
435
545
  - Records are named `pi-<family>.json`, so they never overwrite Omarchy's own
436
546
  `claude.json` / `codex.json`. They hold plan names, used percentages, window
437
- labels and reset times only; no tokens, accounts or e-mail addresses.
547
+ labels, reset times, and โ€” when the token scan has run โ€” aggregate token counts
548
+ per local day and per model (`hasLocalStats`, `recentDays`, `modelUsage`).
549
+ No prompt text, session path, account or e-mail address is copied.
438
550
  - A provider with a failing sign-in shows a status and a help line instead of
439
551
  limits. A provider that is not configured gets no record, and a stale
440
552
  `pi-*.json` is removed on the next run.
@@ -442,13 +554,224 @@ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
442
554
  `auth.json`) still gets **one** record: every healthy account contributes only
443
555
  the window that constrains it, titled `#<slot> ยท Session|Weekly|Monthly`, and
444
556
  failed accounts add no rows but are summarised by reason, e.g.
445
- `#8โ€“#10 key rejected ยท #11โ€“#14 no Go plan`.
557
+ `#8โ€“#10 key rejected ยท #11โ€“#14 no Go plan`. The unnumbered base entry keeps a
558
+ blank `id` and its limits are untitled; only real sibling slots carry
559
+ `#<slot>`. The same record also carries
560
+ `accounts[{id, ok, status, limits}]` with every window of every account
561
+ (`status` is a short reason such as `Key rejected` for a failed one), which
562
+ Omarchy's built-in panel ignores and this package's own plugin draws.
446
563
  - Omarchy's native collector already tracks Claude with richer local statistics, so
447
564
  `piquota omarchy` automatically skips `claude` and cleans up duplicate `pi-claude.json`
448
565
  files when `claude.json` exists in the usage directory (override with `PI_QUOTA_OMARCHY_SKIP`).
449
566
  - `PI_QUOTA_OMARCHY_DIR` overrides the output directory. `./install.sh --uninstall`
450
567
  removes the timer and the `pi-*.json` records.
451
568
 
569
+ ### The AI Usage bar plugin
570
+
571
+ The package also ships its own Omarchy bar widget, `cad0p.pi-usage` ("AI
572
+ Usage", in [`omarchy/cad0p.pi-usage/`](omarchy/cad0p.pi-usage/)). It is derived
573
+ from Omarchy's built-in Agents plugin (MIT, by Omarchy) and reuses the shell's
574
+ own panel components, so it looks the same, but it has no tabs: every provider
575
+ is a collapsible section of one scrolling panel.
576
+
577
+ - **Header row**: the provider's mark, name and plan, the headline percentage
578
+ with its window and reset (`Weekly ยท 1d`), a chip when something is wrong
579
+ (`Unavailable`, `7 down`), and the headline meter while folded. The headline is
580
+ the fullest window; for a multi-account provider it is the healthy account
581
+ with the most room left. `/usage` does not collapse a family like that: its
582
+ panel lists one line per account, so this headline is the bar's own summary.
583
+ - **Expanded**: the status card, LIMITS with the built-in meters and countdowns,
584
+ then for a multi-account record one row per account with every window
585
+ (`#2 session 2% 10m ยท week 10% 6d ยท month 90% 19d`, failed accounts dimmed
586
+ with their reason), then TOKENS BY DAY and TOKENS BY MODEL whenever the record
587
+ carries local statistics, drawn exactly as the built-in draws them.
588
+ - **Which section is open**: on first use, only the provider closest to a limit
589
+ (ties go to the earliest reset). Every expand or collapse you make sticks,
590
+ across popup opens and shell restarts, in
591
+ `~/.local/state/omarchy/settings/cad0p.pi-usage.json`.
592
+ - **Keys**: `j`/`k` or the arrows walk the section headers (a tall open section
593
+ scrolls first), Enter or Space toggles, `l`/`h` expand/collapse, `r` refreshes,
594
+ Tab moves to the neighbouring bar panel, Esc closes. The mouse wheel scrolls;
595
+ a click on a header toggles it.
596
+ - **Records**: every `*.json` in the usage directory, Omarchy's own included.
597
+ A provider is hidden by `providers.<id>.enabled = false` in the widget's
598
+ settings, the same switch the built-in uses. The plugin does **not** support
599
+ the built-in Agents widget's synced cross-device aggregation
600
+ (`syncMode`, `syncDir`, `syncFileName`, `syncDeviceId`). Users who rely on
601
+ synced aggregation should keep the built-in widget; this plugin is not a full
602
+ replacement for it.
603
+ - **Refresh**: the systemd timer and the Pi extension already publish the
604
+ `pi-*.json` records, so the plugin never schedules `piquota`. `r`, a middle
605
+ click or `omarchy-shell cad0p.pi-usage refresh` starts
606
+ `piquota-omarchy.service` (or runs the package's `piquota omarchy` when there is
607
+ no unit). Omarchy's collectors (`omarchy-agent-usage-update`) run on the
608
+ `refreshIntervalSec` timer and for a record's `retryAdvised` retry, and only
609
+ while the built-in Agents widget is off the bar (`omarchyCollectors`: `Auto`,
610
+ `On` or `Off`, where `Off` never runs them), so keeping both never doubles the
611
+ work.
612
+
613
+ Install it by linking it into Omarchy's plugin directory; the link points into
614
+ the pi package when there is one, so `pi update` updates the plugin too:
615
+
616
+ ```bash
617
+ ./install.sh --omarchy-plugin # symlink ~/.config/omarchy/plugins/cad0p.pi-usage (idempotent)
618
+ omarchy plugin enable cad0p.pi-usage # put it on the bar (right section)
619
+ omarchy bar put cad0p.pi-usage --after omarchy.agents # or: right next to the built-in Agents icon
620
+ omarchy plugin disable omarchy.agents # optional: drop the built-in widget
621
+ ```
622
+
623
+ Without the installer, the link is
624
+ `ln -s ~/.pi/agent/npm/node_modules/@cad0p/pi-usage/omarchy/cad0p.pi-usage ~/.config/omarchy/plugins/cad0p.pi-usage`
625
+ followed by `omarchy-shell shell rescanPlugins`. To hide providers, pass the whole
626
+ object, as with the built-in:
627
+ `omarchy bar set cad0p.pi-usage providers '{"fireworks":{"enabled":false}}' --json`.
628
+ Remove it with `omarchy plugin disable cad0p.pi-usage` and
629
+ `rm ~/.config/omarchy/plugins/cad0p.pi-usage` (`./install.sh --uninstall` removes
630
+ the link too); `omarchy plugin enable omarchy.agents` brings the built-in back.
631
+
632
+ IPC: `omarchy-shell cad0p.pi-usage <open|close|toggle|refresh>`, plus
633
+ `expand <id>` / `collapse <id>` (e.g. `expand pi-opencode-go`).
634
+
635
+ `tests/omarchy-harness/render.sh [out-dir] [--light]` renders the panel offscreen
636
+ to PNGs against fixture records, with Omarchy's real components and the current
637
+ theme, next to the built-in panel for comparison. It never touches the running
638
+ shell.
639
+
640
+ ## Token stats from Pi's own sessions
641
+
642
+ Pi writes every assistant message's `usage` block into
643
+ `~/.pi/agent/sessions/**.jsonl` (plus `~/.omp/agent/sessions` when the omp fork
644
+ is installed; `PI_CODING_AGENT_DIR` relocates the Pi agent directory). piQuota
645
+ folds those messages into tokens per local day and per model, per provider, and
646
+ shows them next to the quota:
647
+
648
+ | Where | What |
649
+ | --- | --- |
650
+ | `piquota --json` | `localStats.<family>` โ€” the Omarchy-shaped counters plus `byAccount` |
651
+ | `/usage` panel | a `tokens` row per provider: today, 7 days, prompts, then the top 3 models of the week |
652
+ | `piquota omarchy` | the optional fields Omarchy's Agents panel draws as "Tokens by day" and "Tokens by model" |
653
+ | `piquota stats` | the durable history: last frozen day, families, prompts, session-days |
654
+ | `/usage` โ†’ `tab` | History: per provider, the month's days and models, account totals for a multi-account family, and the all-time rollup |
655
+ | `piquota stats --json` | the same history as a read-only JSON feed (`--month YYYY-MM` selects a month) |
656
+
657
+ What counts: one assistant message with a non-zero token count is one prompt,
658
+ and its tokens are `input + output + cacheRead + cacheWrite` โ€” the same sum the
659
+ Omarchy panel renders. Messages are bucketed by **local** date (the panel calls
660
+ the last row "Today"). Only providers piQuota can bill are counted:
661
+ `anthropic`, `openai-codex`, `antigravity`, `opencode-go` and their numbered
662
+ siblings, so `opencode-go-7` is account `#7`. Providers it does not manage
663
+ (`deepinfra`, `xai`, ...) are ignored, and so is the custom `claude-bridge`
664
+ provider: its tokens are not a Pi subscription.
665
+
666
+ **Two stores, two lifetimes.** Open days (normally just today) live in the
667
+ disposable scan cache. Once a local day is over it is frozen into a durable
668
+ history that no rescan, no cache wipe and no session deletion can rewrite. The
669
+ history is Pi data, so it lives next to Pi's agent directory:
670
+
671
+ | Path | What |
672
+ | --- | --- |
673
+ | `${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` |
674
+ | `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/tokens-YYYY-MM.json` | the frozen days of one local month, keyed day โ†’ family โ†’ slot โ†’ model |
675
+ | `~/.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 |
676
+
677
+ > `~/.pi` is often a git repository. Add `agent/pi-usage/` to that repo's
678
+ > `.gitignore` if you do not want your token counts committed.
679
+
680
+ Freezing: every run moves each day before today that is not frozen yet into its
681
+ month file and into `totals.json`, then drops it from the per-file cache. The
682
+ month file is written first and the totals second; the `lastFrozenDay` watermark
683
+ makes a restart re-freeze a half-written day idempotently (the month write is a
684
+ SET per day) instead of adding it twice. A late write to an already-frozen day โ€”
685
+ a session that appends just past midnight โ€” increments both stores directly; if
686
+ its month is temporarily unreadable, the delta stays in the cache until a later
687
+ run can write it. Such a held delta lives only in the disposable cache: if the
688
+ cache is lost before the month becomes writable, that delta is dropped, because
689
+ the bytes of a frozen day are never read again. A crash between the month and totals writes can double that
690
+ one small delta. A budget-truncated scan never freezes: history is only written
691
+ from complete runs.
692
+
693
+ Consequences, all covered by tests: history survives session deletion and cache
694
+ loss. A lost or corrupt `totals.json` is moved aside as
695
+ `totals.json.corrupt-<ts>` and **rebuilt from the month files**, which are the
696
+ authoritative record of frozen days โ€” not from the session files, because a
697
+ session deleted since then would otherwise shrink history. Frozen days are never
698
+ recomputed, so a replaced session file cannot rewrite them either.
699
+
700
+ Damaged month files are handled without rebuilding frozen days from sessions. A
701
+ month that cannot be *read* (permissions, I/O) is left alone: the run warns, keeps
702
+ its open days in the cache, and does not freeze them until the month is readable.
703
+ Other history proceeds normally, and this alone does not force a full session
704
+ rescan. A rollup rebuild never writes `totals.json` from an incomplete month set.
705
+
706
+ A *corrupt* or foreign-version month is moved aside by the lock owner (never by a
707
+ read-only run) to `.corrupt-<ts>`. Its warning names the file and directs you to copy the
708
+ `.corrupt-<ts>` backup back to the original month path if recoverable, or to delete
709
+ the backup to abandon those counts. Until one of the two happens, freezing stops at
710
+ that month for *every* later day too (the frozen watermark only moves forward), so
711
+ later days stay open in the cache and each run repeats the warning; once the backup
712
+ is restored or deleted, the held days freeze on the next run. Frozen days are never rebuilt from sessions: their
713
+ daily rows are missing (shown as 0) in `recentDays`, while `totals.json` retains
714
+ the all-time counts, so the two views may disagree. `piquota stats --rebuild-totals`
715
+ refuses while a quarantined month has not been restored. A lost *month* file
716
+ loses that month's daily detail; normal runs keep the all-time totals, and a
717
+ rebuild can recover counts from every remaining month file even if sessions are
718
+ gone. Rebuild refuses when there is no month file at all while `totals.json`
719
+ still has data, because replacing that rollup with an empty one would turn a
720
+ missing directory into permanently lost history. A normal run reads only
721
+ `totals.json` plus the month files covering the last seven days (current month,
722
+ and the previous one early in a month), so years of history never slow a run: on
723
+ this machine 45 frozen days cost ~28 KB in total โ€” `totals.json` 6.9 KB and two
724
+ month files of 5.4 KB and 16 KB. A day inside that window is read from its month
725
+ file once frozen, so late writes appear in "tokens by day" without waiting for
726
+ the week to roll over.
727
+
728
+ Cost: the first run streams each session file once โ€” 1.56 GB across 608 files
729
+ in 3.0 s here; later runs read only the bytes appended since the previous scan
730
+ (59 ms of scanning for the whole store, ~20 KB read), and every byte is parsed
731
+ once in normal operation. The CLI's own wall time is dominated by the provider
732
+ calls either way.
733
+
734
+ What forces a file to be read from its first byte (a *rescan*, in which frozen
735
+ days are skipped and only open days are rebuilt): the cache missing or corrupt,
736
+ the file replaced or now shorter, a path the cache has never seen (a copy, a
737
+ move, a restored backup โ€” its old days were already frozen under its previous
738
+ path), or the same size with a new mtime (an in-place rewrite of the same
739
+ length). An append only reads the new bytes. The one rewrite this cannot see is
740
+ one that restores both the size *and* the mtime, which no stat-based check can
741
+ detect. The cache is atomic and `0600`.
742
+
743
+ Two writers are routine here (the systemd timer and `/usage`), so a run takes an
744
+ exclusive lock โ€” `local-sessions.json.lock`, with a 2-minute stale timeout โ€”
745
+ before it reads the history. The file holds an owner token, and a live run
746
+ refreshes its mtime as it walks the store, so a slow scan is never mistaken for a
747
+ dead one. A run that cannot take the lock reports the numbers already on disk and
748
+ writes nothing; a run that is taken over mid-scan notices on its next refresh and
749
+ also writes nothing (it never deletes its successor's lock).
750
+
751
+ Duplicate *sessions* are handled two ways. A byte-identical copy of a session
752
+ file (a copy, a restored backup, a sync tool) is recognised by a (size, first
753
+ 32 KB, last 32 KB) fingerprint and contributes nothing; anything appended to it
754
+ later is counted normally, and if the original is deleted the copy is adopted and
755
+ read on the next run. A fingerprint can collide โ€” two same-size sessions that
756
+ differ only in the middle โ€” so a candidate is verified by streaming both files
757
+ before it is called a copy; that full comparison runs only on the rare duplicate.
758
+ A *fork* that copies a prefix and then diverges has its own size and tail, so it
759
+ is counted as a session of its own, duplicated prefix included: on this machine
760
+ (611 files, 186,443 assistant messages) only 11 message ids appeared in more than
761
+ one file (0.006%), none of them on today, so message-id de-duplication would cost
762
+ megabytes to correct a handful of tokens and is not implemented.
763
+
764
+ The scan has a wall-clock budget (12 s, or `PI_QUOTA_LOCAL_STATS_BUDGET_MS`) so
765
+ the first run cannot push `/usage` past its 30 s exec timeout; when it runs out
766
+ the report carries `localStatsMeta.partial`, the panel marks the numbers
767
+ `scan partial`, and nothing is frozen that run. `PI_QUOTA_LOCAL_STATS=0` turns
768
+ the scan off.
769
+
770
+ Session counts are **session-days**, not distinct session files: a day is frozen
771
+ as aggregate rows, without the list of files that fed it, so a session that runs
772
+ past midnight counts once on each day it appears in. `todaySessions` is exact
773
+ (those files are still open); `totalSessions` is the sum of the per-day counts.
774
+
452
775
  ## Layout
453
776
 
454
777
  ```
@@ -465,6 +788,8 @@ src/providers/antigravity-oauth.js in-memory refresh with Google's public clien
465
788
  src/providers/backoff.js per-family throttle state
466
789
  src/sticky.js last published / last good snapshots and throttle recovery
467
790
  src/engine.js collectQuota() -> one normalized report
791
+ src/local/pi-sessions.js token counts from Pi's session files, incremental scan
792
+ src/local/history.js frozen local-day history: month files + all-time rollup
468
793
  src/model.js window normalization, percent and reset parsing
469
794
  src/render/{theme,panel}.js colors, thresholds, rings, bars, boxed panel
470
795
  src/cache.js report cache at ~/.cache/pi-quota/usage.json
@@ -472,9 +797,12 @@ src/omarchy/record.js pure mapping from the report to Omarchy usage rec
472
797
  src/omarchy/publish.js atomic writer for pi-*.json, and stale-record cleanup
473
798
  src/refresh.js per-family refresh clocks, and the merge back into one report
474
799
  src/cli/args.js argument parsing, and the flag/positional split
800
+ src/cli/commands/stats.js `piquota stats` and `--rebuild-totals`
475
801
  src/exec.js the one place that spawns gh CLI binary
476
802
  src/http.js fetch wrapper: timeouts, JSON, redaction
477
803
  contrib/systemd/ user service and timer behind `install.sh --omarchy-timer`
804
+ omarchy/cad0p.pi-usage/ the AI Usage Omarchy bar plugin (QML; UsageLogic.js is its pure logic)
805
+ tests/omarchy-harness/ offscreen renderer for the plugin, fixtures included
478
806
  bin/piquota.js the only CLI
479
807
  extensions/quota-panel.ts Pi TUI line and /usage
480
808
  ```
@@ -498,7 +826,7 @@ extensions/quota-panel.ts Pi TUI line and /usage
498
826
  ## Tests
499
827
 
500
828
  ```bash
501
- node --test tests/*.test.mjs # 265 tests, fake tokens only, no network
829
+ node --test tests/*.test.mjs # 401 tests, fake tokens only, no network
502
830
  ```
503
831
 
504
832
  Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
@@ -508,9 +836,14 @@ Actions plan resolution, opt-in gating and degradation, the two Antigravity
508
836
  failures and the OpenCode degradation), the dashboard parser's three
509
837
  strategies, the Firefox cookie reader against a synthetic SQLite database, the
510
838
  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).
839
+ the Omarchy record mapping and atomic writer, the Omarchy plugin's logic,
840
+ manifest and marks, the local token scan (family and slot mapping, incremental
841
+ append, shrink, deletion, local-day bucketing, cache corruption), the durable
842
+ history (month partitioning, freezing at midnight, late appends to frozen days,
843
+ cache loss, a crash between the month and totals writes, corrupt-history
844
+ quarantine, rebuild-totals), the renderers, the Pi extension contract, the
845
+ per-family refresh clocks, argument parsing, and the structure of these
846
+ documents themselves (tables, fences, links, anchors).
514
847
 
515
848
  ## Acknowledgments & Prior Art
516
849