@cad0p/pi-usage 0.11.4 → 0.11.5-20261103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.11.5] - 2026-11-03
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ A fullscreen `/usage` with browsable history, and a refresh about three times faster.
9
+
10
+ - **Fullscreen `/usage`:** bare `/usage` opens a view like `/context`. It paints at once from the last report (`refreshing …` until the new one lands, then `updated HH:MM`), keeps an always-visible key legend, folds provider sections, and scrolls with the arrows, `j`/`k`, PgUp/PgDn, `g`/`G` and the mouse wheel. Rows wrap instead of being clipped, so resets stay visible at 64 columns. Folds, page and month persist for the pi session; `esc`/`q` leaves a clean editor.
11
+ - **History:** `tab` (or `1`/`2`) switches to month history: days, models and per-account tokens, `←`/`→` and `[`/`]` walk the months, and today merges from the live report exactly once. Backed by the new read-only `piquota stats --json [--month YYYY-MM]`.
12
+ - **No invented `#1`:** the unnumbered base account carries no suffix on any surface.
13
+ - **Faster refresh:** a provider's accounts are queried in parallel (default 8, `PI_QUOTA_ACCOUNT_CONCURRENCY` 1–32): a forced refresh went from ~2.9 s to ~1.0 s, OpenCode Go's 14 keys from ~3.8 s to ~0.9 s.
14
+ - **User-Agent:** every request sends `pi-usage/<version> (+https://github.com/cad0p/pi-usage)` instead of Node's generic `node` (Antigravity keeps its CLI agent).
15
+ <!-- USER-EDITABLE SECTION END -->
16
+
17
+ ### 🚀 Features
18
+
19
+ - Fullscreen /usage with month history; no invented "#1" (closes #21, closes #22)
20
+
21
+ ### ⚡ Performance
22
+
23
+ - Query a provider's accounts in parallel; send the package User-Agent (closes #23)
24
+
25
+
5
26
  ## [0.11.4] - 2026-11-03
6
27
 
7
28
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -6,7 +6,7 @@ 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
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.
12
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.
@@ -40,7 +40,9 @@ shuvquota, and it never writes to a credential file.
40
40
  * Antigravity's access token **may** be refreshed, but only in memory. Google
41
41
  does not rotate that refresh token, so Pi's stored copy stays valid. Claude and
42
42
  Codex are never refreshed, because they **do** rotate and persisting a rotated
43
- token would break Pi.
43
+ token would break Pi. Every surface reports the token the run *used*: a renewal
44
+ that succeeded clears the stored "token expired" claim, and only a failed one
45
+ leaves the expiry (and the warning) on screen.
44
46
  * The Claude Code store (`~/.claude/.credentials.json`) is opened read-only too, and
45
47
  its refresh token is never even read into memory, so nothing here can rotate a
46
48
  token the installed `claude` owns. `~/.claude.json` is read only for the account
@@ -223,6 +225,9 @@ piquota auth opencode --wait 300
223
225
 
224
226
  piquota omarchy # write pi-*.json records for Omarchy's Agents panel
225
227
  piquota stats # durable token history: last frozen day, families, totals
228
+ piquota stats --json --month 2026-11
229
+ # read-only history feed: months, one month's days
230
+ # per family/slot/model, all-time totals, notes
226
231
  piquota stats --rebuild-totals # recompute totals.json from the frozen month files
227
232
  ```
228
233
 
@@ -242,7 +247,7 @@ Inside Pi:
242
247
 
243
248
  | Command | Effect |
244
249
  | --- | --- |
245
- | `/usage` | one on-demand refresh, show the full panel until the next prompt (or a second `/usage`), notify a one-line summary |
250
+ | `/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 |
246
251
  | `/usage box` | pin the five-row box above the editor |
247
252
  | `/usage line` | pin the compact line above the editor, in its own row |
248
253
  | `/usage panel` | full panel above the editor, pinned across prompts |
@@ -253,18 +258,92 @@ Inside Pi:
253
258
 
254
259
  ### The Pi surface
255
260
 
256
- Nothing is pinned by default: `/usage` is a one-shot command, so it claims no row
257
- above the editor and leaves the footer alone. Turn a surface on with `/usage box`,
258
- `/usage line`, `/usage panel` or `/usage status`; only then does the extension keep
259
- it fresh with a background refresh (and a gentle refresh after each agent turn).
260
- `/usage hide` gives the row back.
261
+ Nothing is pinned by default: `/usage` opens a **fullscreen overlay** that takes
262
+ the keyboard, so it claims no row above the editor and leaves the footer alone.
263
+ It is the same `ctx.ui.custom` overlay pattern pi's `/context` uses: it fills the
264
+ terminal, scrolls, folds and closes on Esc (or `q`), leaving nothing behind.
265
+ Turn a persistent surface on with `/usage box`, `/usage line`, `/usage panel` or
266
+ `/usage status`; only then does the extension keep it fresh with a background
267
+ refresh (and a gentle refresh after each agent turn). `/usage hide` gives the row
268
+ back. `/usage panel` still pins the same panel flat above the editor.
269
+
270
+ The overlay paints on the same keystroke: it renders the report the extension
271
+ already holds, or the CLI's on-disk report cache (`~/.cache/pi-quota/usage.json`,
272
+ read-only) when memory is empty, and never waits for the CLI before the first
273
+ frame. The same forced refresh bare `/usage` always ran then runs behind it: the
274
+ header says `refreshing …` while it does, and the update time when it lands. A
275
+ failed refresh is a line in the view, never a thrown error. Every provider is a section:
276
+ the header is the brand-coloured name line, the body lists every window with its
277
+ used-fraction bar, reset countdown and the token rows for today, the last seven
278
+ days, the prompts and the heaviest models. All sections start expanded; the fold
279
+ state and the page you left live for the pi session, so reopening `/usage`
280
+ resumes them, and no file is written.
281
+
282
+ | Key | Action |
283
+ | --- | --- |
284
+ | `↑`/`↓`, `j`/`k` | move the header cursor; a section taller than the viewport scrolls line by line first |
285
+ | `enter` / `space` | fold or unfold the section under the cursor |
286
+ | `→`/`l`, `←`/`h` | expand, collapse |
287
+ | `PgUp`/`PgDn`, `g`/`Home`, `G`/`End` | page, first section, last section |
288
+ | mouse wheel | scroll |
289
+ | `r` | refresh in place (on History: the month and the live report) |
290
+ | `tab`, `1` / `2` | switch between Now and History |
291
+ | `esc` / `q` | close |
292
+
293
+ The legend at the bottom lists every key the current view answers, and wraps
294
+ into a second or third row on a narrow pane instead of dropping pairs — as few
295
+ rows as fit, with the pairs balanced across them. The frame
296
+ fits from about 60 columns up: a line wider than the terminal wraps at its
297
+ ` · ` separators with a hanging indent instead of being cut, so a row never
298
+ loses the datum it ends with, and a resize rebuilds it. The wheel is handled by the component itself, like
299
+ pi-context-view: pi routes fullscreen wheel events to the focused component but
300
+ has no scrolling API for a custom screen, and this extension has no runtime
301
+ dependencies to wrap the body in a `ScrollView`.
302
+
303
+ `tab` (or `2`) opens **History**, the second page: the durable token history for
304
+ one month, with the same foldable sections. A provider section lists the month's
305
+ days, the prompt count for that month and for all time, the token split by
306
+ model, and — for a multi-account provider — one row per account with its month
307
+ and all-time totals (every account the family has in the month *or* all-time, so
308
+ one busy key does not hide the others). `←`/`→` (or
309
+ `[`/`]`) walk the browsable months — those with a file, plus the live month,
310
+ which is where History opens even before its first freeze — and `tab` (or `1`)
311
+ comes back to Now. The all-time
312
+ figure is `totals.json` (frozen through its watermark) plus, when it is merged,
313
+ the same open day the month counts, so today is never missing from it. A rollup
314
+ that cannot cover the month on screen — a freeze whose totals write failed, a
315
+ month file restored behind `totals.json` — is still printed, dimmed, with
316
+ `~ all-time lags this month; run piquota stats --rebuild-totals` above the rows:
317
+ the figure is the only answer to "how much has this burned ever", so it is
318
+ explained rather than dropped. A family's open day
319
+ (today) is merged from the live report only when the report was *generated* today
320
+ and the frozen month has no row for that family's day — per family, so one family
321
+ frozen early never hides another's live row — and a day is counted exactly once.
322
+ A report generated before midnight, or the older report cache painted on the first
323
+ frame, is skipped: its `todayTotalTokens` belong to another day, and merging them
324
+ under today would count a frozen yesterday twice. The tail then names the report's
325
+ day and says today is not merged. A missing or quarantined month is a note instead
326
+ of rows, a failed `stats` run shows the CLI's own stderr line (redacted) or its
327
+ exit status with a retry hint, and a slow fetch never moves the page off the month
328
+ the last keypress asked for.
329
+
330
+ History reads `piquota stats --json [--month YYYY-MM]`: the month list, one
331
+ month's days per family/slot/model, the all-time rollup and notes. That command
332
+ never takes the writer lock and never writes, so it is safe to run while a scan
333
+ is freezing a day. Everything it reports is frozen: `lastFrozenDay` is its
334
+ watermark, and neither `piquota stats` nor its JSON moves an open day into a
335
+ total. The History page merges the live day itself, for the month and for the
336
+ all-time figures alike, and prints the same "comes from the live report" note
337
+ under both.
261
338
 
262
339
  A provider with several accounts is shown as one header (`OP-Go · 7 of 14 accounts
263
340
  ok`) plus one line per account with every window and its reset, e.g.
264
341
  `#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
266
- cap on plain widgets never truncates it, and the summary names the account with the
267
- most room: `OP-Go: 7/14 ok · lowest 24% (#7)`.
342
+ (`key rejected`, `no Go plan`). The base entry in `auth.json` (`opencode-go`) has
343
+ no number, so its label column is blank; only real sibling slots (`opencode-go-2`
344
+ …) are numbered `#2` … `#14`. The leading glyph is the fullest window's band. The
345
+ overlay and the pinned panel are rendered as components, so Pi's ten-line cap on
346
+ plain widgets never truncates them.
268
347
 
269
348
  `/usage box` pins the **five-row box above the editor**:
270
349
 
@@ -373,6 +452,12 @@ Rp suite) without external network daemons or closed-source intermediaries:
373
452
  * **Percentages and Countdown**: `usedPercent` and `remainingPercent` report
374
453
  percentages (0..100); `resetsInSec` provides the exact integer seconds until
375
454
  reset, immune to local clock skew.
455
+ * **`expiresInMin` and `tokenRenewedInMemory`**: the freshness of the credential the
456
+ run actually used. Antigravity's token is renewed in memory, so its result
457
+ carries `tokenRenewedInMemory: true` and `expiresInMin: null` instead of Pi's
458
+ stale stored countdown; a renewal that failed keeps the stored value, which is
459
+ what the panel's `token EXPIRED` badge and the warning line describe. The field
460
+ is only present when a renewal happened.
376
461
  * **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
377
462
  allowing consumers to cleanly omit them without displaying false-positive
378
463
  error indicators.
@@ -440,7 +525,7 @@ seven entries, oldest first, today last):
440
525
  "activeDays": 11,
441
526
  "activeDates": ["2026-10-27"],
442
527
  "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 } }
528
+ "byAccount": { "": { "todayTokens": 14172, "weekTokens": 118422, "todayPrompts": 3, "totalPrompts": 1200, "totalTokens": 48812210 } }
444
529
  }
445
530
  },
446
531
  "localStatsMeta": { "files": 604, "scanned": 1, "reused": 603, "bytesRead": 3042, "durationMs": 9, "partial": false, "budgetMs": 12000, "frozenDays": 0, "locked": false }
@@ -488,7 +573,9 @@ piquota omarchy # write pi-claude.json, pi-codex.json, ... once
488
573
  `auth.json`) still gets **one** record: every healthy account contributes only
489
574
  the window that constrains it, titled `#<slot> · Session|Weekly|Monthly`, and
490
575
  failed accounts add no rows but are summarised by reason, e.g.
491
- `#8–#10 key rejected · #11–#14 no Go plan`. The same record also carries
576
+ `#8–#10 key rejected · #11–#14 no Go plan`. The unnumbered base entry keeps a
577
+ blank `id` and its limits are untitled; only real sibling slots carry
578
+ `#<slot>`. The same record also carries
492
579
  `accounts[{id, ok, status, limits}]` with every window of every account
493
580
  (`status` is a short reason such as `Key rejected` for a failed one), which
494
581
  Omarchy's built-in panel ignores and this package's own plugin draws.
@@ -510,7 +597,8 @@ is a collapsible section of one scrolling panel.
510
597
  with its window and reset (`Weekly · 1d`), a chip when something is wrong
511
598
  (`Unavailable`, `7 down`), and the headline meter while folded. The headline is
512
599
  the fullest window; for a multi-account provider it is the healthy account
513
- with the most room left, the `lowest` figure `/usage` prints.
600
+ with the most room left. `/usage` does not collapse a family like that: its
601
+ panel lists one line per account, so this headline is the bar's own summary.
514
602
  - **Expanded**: the status card, LIMITS with the built-in meters and countdowns,
515
603
  then for a multi-account record one row per account with every window
516
604
  (`#2 session 2% 10m · week 10% 6d · month 90% 19d`, failed accounts dimmed
@@ -582,6 +670,8 @@ shows them next to the quota:
582
670
  | `/usage` panel | a `tokens` row per provider: today, 7 days, prompts, then the top 3 models of the week |
583
671
  | `piquota omarchy` | the optional fields Omarchy's Agents panel draws as "Tokens by day" and "Tokens by model" |
584
672
  | `piquota stats` | the durable history: last frozen day, families, prompts, session-days |
673
+ | `/usage` → `tab` | History: per provider, the month's days and models, account totals for a multi-account family, and the all-time rollup |
674
+ | `piquota stats --json` | the same history as a read-only JSON feed (`--month YYYY-MM` selects a month) |
585
675
 
586
676
  What counts: one assistant message with a non-zero token count is one prompt,
587
677
  and its tokens are `input + output + cacheRead + cacheWrite` — the same sum the
@@ -755,7 +845,7 @@ extensions/quota-panel.ts Pi TUI line and /usage
755
845
  ## Tests
756
846
 
757
847
  ```bash
758
- node --test tests/*.test.mjs # 354 tests, fake tokens only, no network
848
+ node --test tests/*.test.mjs # 413 tests, fake tokens only, no network
759
849
  ```
760
850
 
761
851
  Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
package/bin/piquota.js CHANGED
@@ -57,7 +57,7 @@ Usage:
57
57
  piquota auth <opencode|status> [flags]
58
58
  piquota gentle-pi <status|apply|revert>
59
59
  piquota omarchy
60
- piquota stats [--rebuild-totals]
60
+ piquota stats [--json] [--month YYYY-MM] [--rebuild-totals]
61
61
 
62
62
  Quota:
63
63
  --json Emit the normalized report as JSON (for rp-host and automations)
@@ -73,6 +73,7 @@ Quota:
73
73
  --explain Show which stores/fields are read (names only)
74
74
  --clear-cache Delete the local cache and exit
75
75
  --rebuild-totals With \`stats\`: rebuild the token-history totals from its month files
76
+ --month YYYY-MM With \`stats\`: read one month (default: the current local month)
76
77
  --version Print the version and exit
77
78
 
78
79
  Auth (OpenCode Go session, the only credential Pi does not store):
@@ -86,6 +87,9 @@ Auth (OpenCode Go session, the only credential Pi does not store):
86
87
  Stats:
87
88
  piquota stats summarise the durable token history
88
89
  (dir: \${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage)
90
+ piquota stats --json [--month] read-only JSON: months (the live month always),
91
+ one month's days per family/slot/model, all-time
92
+ totals, notes
89
93
  piquota stats --rebuild-totals recompute totals.json from the frozen month files
90
94
 
91
95
  Omarchy:
@@ -96,6 +100,7 @@ Omarchy:
96
100
  Environment:
97
101
  PI_QUOTA_LOCAL_STATS=0 skip the token scan of Pi's session files
98
102
  PI_QUOTA_LOCAL_STATS_BUDGET_MS wall-clock budget for that scan (default 12000)
103
+ PI_QUOTA_ACCOUNT_CONCURRENCY accounts of one provider queried at once (1-32, default 8)
99
104
 
100
105
  Guarantees:
101
106
  * ~/.pi/agent/auth.json is opened read-only. Never written, synced or refreshed.
@@ -161,8 +166,14 @@ async function main() {
161
166
  return 2;
162
167
  }
163
168
 
169
+ if (args.month && command !== "stats") {
170
+ err("--month only works with \`piquota stats\`\n");
171
+ process.stdout.write(HELP);
172
+ return 2;
173
+ }
174
+
164
175
  if (command === "stats") {
165
- return runStatsCommand({ rebuild: args.rebuildTotals, env: process.env });
176
+ return runStatsCommand({ rebuild: args.rebuildTotals, json: args.json, month: args.month, env: process.env });
166
177
  }
167
178
 
168
179
  if (command === "omarchy") {