@cad0p/pi-usage 0.11.5 โ†’ 0.11.6

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,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.11.6] - 2026-11-03
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ `/usage` refreshes inside pi with no CLI child, each provider on its own, and every key works under the kitty keyboard protocol.
9
+
10
+ - **In-process refresh:** `/usage` runs the engine inside pi instead of spawning `piquota`, so it paints in about 0.35 s and `r` finishes in under a second. Each provider updates on its own, so a slow one never holds the others back. The token-stats scan runs in a worker thread and never blocks the TUI. The systemd timer still uses the read-only CLI.
11
+ - **Logins stay valid:** inside pi, Claude and Codex tokens come from pi's own `getProviderAuth`, which renews them under pi's lock exactly as when you use the model. Antigravity, which pi 1.5 no longer resolves, keeps its renewed token in memory for its lifetime; nothing is written back.
12
+ - **Kitty keyboard protocol (Ghostty, kitty, foot):** arrows, Esc, Enter, Tab, Space and every legend key work in their CSI-u form on both tabs. Key releases never act, repeats do. A drift test compares the decoder with the installed pi-tui matcher.
13
+ - **History all-time counts today once**, and an all-time figure that lags behind the month files is shown dimmed with a note instead of disappearing.
14
+ - **Antigravity header:** no repeated project, and no false "token expired" while the account works.
15
+ <!-- USER-EDITABLE SECTION END -->
16
+
17
+ ### ๐Ÿš€ Features
18
+
19
+ - In-process /usage refresh, each provider on its own (closes #26)
20
+
21
+ ### ๐Ÿ› Bug Fixes
22
+
23
+ - History all-time counts today once; Antigravity header without the false expiry (closes #29)
24
+ - *(extension)* /usage keys work under the kitty keyboard protocol (closes #33)
25
+
26
+
5
27
  ## [0.11.5] - 2026-11-03
6
28
 
7
29
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -32,15 +32,22 @@ shuvquota, and it never writes to a credential file.
32
32
 
33
33
  ## Hard guarantees
34
34
 
35
- * `~/.pi/agent/auth.json` is opened with `flag: "r"`. Never written, synced or
36
- refreshed. Verified with a checksum before and after every run.
35
+ * pi-usage opens `~/.pi/agent/auth.json` with `flag: "r"` and never writes it.
36
+ CLI and systemd-timer runs are strictly read-only. In the Pi extension only,
37
+ `getProviderAuth` may ask Pi to renew Anthropic/Codex OAuth under Pi's own
38
+ cross-process auth lock; pi-usage never writes auth storage itself. Check the
39
+ auth.json checksum around live runs.
37
40
  * No token, refresh token, API key, session cookie or `sessionKey` is printed,
38
- logged, cached, or sent anywhere. E-mail identities are redacted in every
39
- artifact.
40
- * Antigravity's access token **may** be refreshed, but only in memory. Google
41
- does not rotate that refresh token, so Pi's stored copy stays valid. Claude and
42
- Codex are never refreshed, because they **do** rotate and persisting a rotated
43
- token would break Pi.
41
+ logged, or serialized to a file. Tokens are sent only to their provider; the
42
+ extension holds resolved access tokens in memory for the request. Antigravity's
43
+ renewed access token stays in a process-local cache until shortly before expiry.
44
+ E-mail identities are redacted in every artifact.
45
+ * Antigravity may be renewed in memory because Google does not rotate its refresh
46
+ token. The CLI and timer never refresh Claude/Codex; inside Pi, the extension
47
+ calls Pi's `getProviderAuth`, which returns fresh OAuth directly or lets Pi
48
+ renew and persist it under Pi's auth-store lock. pi-usage itself never writes
49
+ auth.json. Every surface reports the token the run *used*: an Antigravity
50
+ renewal clears the stale stored-expiry claim; only a failed renewal leaves it.
44
51
  * The Claude Code store (`~/.claude/.credentials.json`) is opened read-only too, and
45
52
  its refresh token is never even read into memory, so nothing here can rotate a
46
53
  token the installed `claude` owns. `~/.claude.json` is read only for the account
@@ -60,7 +67,7 @@ shuvquota, and it never writes to a credential file.
60
67
 
61
68
  | Path | What |
62
69
  | --- | --- |
63
- | `~/.cache/pi-quota/usage.json` | the report cache |
70
+ | `~/.cache/pi-quota/usage.json` | the report cache, with each family's own fetch time |
64
71
  | `~/.cache/pi-quota/local-sessions.json` | per-session-file offsets and open token days |
65
72
  | `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-usage/totals.json`, `tokens-YYYY-MM.json` | frozen token history, plus `.corrupt-<ts>` backups for corrupt files |
66
73
  | `~/.cache/pi-quota/refresh-state.json` | each family's own refresh clock |
@@ -107,14 +114,17 @@ There are two ways to be signed in to Claude, and a user may have either or both
107
114
  | Claude Code CLI | `~/.claude/.credentials.json` (honours `CLAUDE_CONFIG_DIR`); on macOS, when that file is absent, the `Claude Code-credentials` login Keychain item (read-only, via `security find-generic-password`; macOS may ask once to allow access) | What `npm:pi-claude-code-provider` drives. A subscription with usage credits disabled answers `400` on Pi's own entry for every request, and this is the source that still works. |
108
115
  | Pi's own entry | `~/.pi/agent/auth.json` โ†’ `anthropic` | `/login anthropic` in Pi, without the plugin installed. |
109
116
 
110
- **The Claude Code CLI wins when both are present and its token is still valid.** The
111
- choice is a *preference, not a fallback*: only the selected source is tried, so a
112
- broken preferred source is reported rather than silently masked by the other one.
113
- The one exception is an expired Claude Code token next to a fresh Pi login: a store
114
- left behind by an uninstalled CLI never refreshes again, so `auto` reads Pi's login
115
- instead and adds a warning naming the switch. Override it with
116
- `PI_QUOTA_CLAUDE_SOURCE=pi` or `=claude-code`; `--explain` prints which one was used
117
- and why.
117
+ For `piquota` CLI and timer runs, the Claude Code CLI wins when both are
118
+ present and its token is still valid. The choice is a *preference, not a fallback*:
119
+ only the selected source is tried, so a broken preferred source is reported rather
120
+ than silently masked by the other one. An expired Claude Code token next to a fresh
121
+ Pi login is passed over in `auto` mode with a warning. Override CLI selection with
122
+ `PI_QUOTA_CLAUDE_SOURCE=pi` or `=claude-code`; `--explain` prints what was used.
123
+
124
+ Inside Pi, the extension asks Pi's `getProviderAuth("anthropic")` for OAuth first.
125
+ A fresh token is returned as-is; when it is near expiry, Pi renews and persists it
126
+ under its auth-store lock, then the extension uses that returned token in memory.
127
+ If Pi has no OAuth login, the configured source preference applies.
118
128
 
119
129
  The Claude Code store is opened read-only, and its refresh token is deliberately
120
130
  **never carried into memory**: Anthropic rotates refresh tokens, and a rotation
@@ -174,10 +184,10 @@ When you also run `install.sh` for the terminal CLI, pass
174
184
  | `piquota` on your `PATH` for terminal use | no | yes, symlinked |
175
185
  | Omarchy sync while Pi is closed | no | yes, with `--omarchy-timer` |
176
186
 
177
- If you live inside Pi and use Omarchy, the npm route covers it: the extension
178
- runs `piquota omarchy` on every quota refresh, so the bar fills without any
179
- setup. Choose `install.sh` when you want the CLI in a terminal, or background
180
- sync while Pi is not running.
187
+ If you live inside Pi and use Omarchy, the extension publishes each
188
+ provider's record in-process as soon as that family finishes, then prunes stale
189
+ records once the complete refresh settles. Choose `install.sh` when you want the
190
+ CLI in a terminal, or background sync while Pi is not running.
181
191
 
182
192
  ### Manual Install / Development
183
193
 
@@ -266,11 +276,19 @@ refresh (and a gentle refresh after each agent turn). `/usage hide` gives the ro
266
276
  back. `/usage panel` still pins the same panel flat above the editor.
267
277
 
268
278
  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:
279
+ already holds, or the on-disk report cache (`~/.cache/pi-quota/usage.json`) when
280
+ memory is empty. That cache is keyed by the Claude source the numbers in it came
281
+ from, so a report fetched with Pi's own OAuth is stored under Pi's key and a
282
+ reader that asked for Claude Code is not served it. It never waits for network work before the first frame. Opening
283
+ `/usage` refreshes only families older than their own clock (60 seconds by
284
+ default; Claude's 300-second throttle-safe interval is preserved); `r` forces
285
+ all enabled families. Families run independently and repaint as they finish. A
286
+ failed family is a line in the view โ€” one line per family, kept until that family
287
+ succeeds again, so an out-of-order success cannot hide another one's failure โ€”
288
+ never a thrown error. The local session
289
+ scan runs in a Node worker thread, not on the TUI event loop, and a shutdown or
290
+ `/reload` stops it mid-scan: it releases its history lock and writes nothing.
291
+ Every provider is a section:
274
292
  the header is the brand-coloured name line, the body lists every window with its
275
293
  used-fraction bar, reset countdown and the token rows for today, the last seven
276
294
  days, the prompts and the heaviest models. All sections start expanded; the fold
@@ -288,6 +306,13 @@ resumes them, and no file is written.
288
306
  | `tab`, `1` / `2` | switch between Now and History |
289
307
  | `esc` / `q` | close |
290
308
 
309
+ Every key works in a terminal that sends the legacy escape sequences and in one
310
+ that negotiates the kitty keyboard protocol (Ghostty among them): the decoder
311
+ reads both encodings, plus kitty press, repeat and release events, so a press
312
+ moves once and its release never moves again. A repeat is a press โ€” a held arrow
313
+ keeps moving, exactly as it does in tmux, where the OS repeat is the same bytes
314
+ as the first press.
315
+
291
316
  The legend at the bottom lists every key the current view answers, and wraps
292
317
  into a second or third row on a narrow pane instead of dropping pairs โ€” as few
293
318
  rows as fit, with the pairs balanced across them. The frame
@@ -300,13 +325,20 @@ dependencies to wrap the body in a `ScrollView`.
300
325
 
301
326
  `tab` (or `2`) opens **History**, the second page: the durable token history for
302
327
  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
328
+ days, the prompt count for that month and for all time, the token split by
329
+ model, and โ€” for a multi-account provider โ€” one row per account with its month
330
+ and all-time totals (every account the family has in the month *or* all-time, so
331
+ one busy key does not hide the others). `โ†`/`โ†’` (or
306
332
  `[`/`]`) walk the browsable months โ€” those with a file, plus the live month,
307
333
  which is where History opens even before its first freeze โ€” and `tab` (or `1`)
308
334
  comes back to Now. The all-time
309
- figure is `totals.json` (frozen through its watermark). A family's open day
335
+ figure is `totals.json` (frozen through its watermark) plus, when it is merged,
336
+ the same open day the month counts, so today is never missing from it. A rollup
337
+ that cannot cover the month on screen โ€” a freeze whose totals write failed, a
338
+ month file restored behind `totals.json` โ€” is still printed, dimmed, with
339
+ `~ all-time lags this month; run piquota stats --rebuild-totals` above the rows:
340
+ the figure is the only answer to "how much has this burned ever", so it is
341
+ explained rather than dropped. A family's open day
310
342
  (today) is merged from the live report only when the report was *generated* today
311
343
  and the frozen month has no row for that family's day โ€” per family, so one family
312
344
  frozen early never hides another's live row โ€” and a day is counted exactly once.
@@ -314,14 +346,17 @@ A report generated before midnight, or the older report cache painted on the fir
314
346
  frame, is skipped: its `todayTotalTokens` belong to another day, and merging them
315
347
  under today would count a frozen yesterday twice. The tail then names the report's
316
348
  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.
349
+ of rows, a failed history read is redacted and shown with a retry hint, and a slow
350
+ read never moves the page off the month the last keypress asked for.
351
+
352
+ The extension reads the same `historyJson` payload in-process; the CLI exposes
353
+ it as `piquota stats --json [--month YYYY-MM]`: the month list, one month's days
354
+ per family/slot/model, the all-time rollup and notes. The read path never takes
355
+ the writer lock and never writes, so it is safe while a scan is freezing a day. Everything it reports is frozen: `lastFrozenDay` is its
356
+ watermark, and neither `piquota stats` nor its JSON moves an open day into a
357
+ total. The History page merges the live day itself, for the month and for the
358
+ all-time figures alike, and prints the same "comes from the live report" note
359
+ under both.
325
360
 
326
361
  A provider with several accounts is shown as one header (`OP-Go ยท 7 of 14 accounts
327
362
  ok`) plus one line per account with every window and its reset, e.g.
@@ -439,6 +474,12 @@ Rp suite) without external network daemons or closed-source intermediaries:
439
474
  * **Percentages and Countdown**: `usedPercent` and `remainingPercent` report
440
475
  percentages (0..100); `resetsInSec` provides the exact integer seconds until
441
476
  reset, immune to local clock skew.
477
+ * **`expiresInMin` and `tokenRenewedInMemory`**: the freshness of the credential the
478
+ run actually used. Antigravity's token is renewed in memory, so its result
479
+ carries `tokenRenewedInMemory: true` and `expiresInMin: null` instead of Pi's
480
+ stale stored countdown; a renewal that failed keeps the stored value, which is
481
+ what the panel's `token EXPIRED` badge and the warning line describe. The field
482
+ is only present when a renewal happened.
442
483
  * **`notConfigured` vs `ok`**: unconfigured providers report `notConfigured: true`,
443
484
  allowing consumers to cleanly omit them without displaying false-positive
444
485
  error indicators.
@@ -793,6 +834,7 @@ src/local/history.js frozen local-day history: month files + all-time
793
834
  src/model.js window normalization, percent and reset parsing
794
835
  src/render/{theme,panel}.js colors, thresholds, rings, bars, boxed panel
795
836
  src/cache.js report cache at ~/.cache/pi-quota/usage.json
837
+ src/overlay-keys.js raw key decoding for /usage: legacy and kitty keyboard encodings
796
838
  src/omarchy/record.js pure mapping from the report to Omarchy usage records
797
839
  src/omarchy/publish.js atomic writer for pi-*.json, and stale-record cleanup
798
840
  src/refresh.js per-family refresh clocks, and the merge back into one report
@@ -826,7 +868,7 @@ extensions/quota-panel.ts Pi TUI line and /usage
826
868
  ## Tests
827
869
 
828
870
  ```bash
829
- node --test tests/*.test.mjs # 401 tests, fake tokens only, no network
871
+ node --test tests/*.test.mjs # 455 tests, fake tokens only, no network
830
872
  ```
831
873
 
832
874
  Modules covered: `auth.json` parsing and de-duplication, the Claude Code store
package/bin/piquota.js CHANGED
@@ -177,10 +177,20 @@ async function main() {
177
177
  }
178
178
 
179
179
  if (command === "omarchy") {
180
- const load = async () => {
180
+ const load = async ({ onFamily } = {}) => {
181
181
  const everyFamily = enabledFamilies({ env: process.env });
182
+ const publishFamily = onFamily
183
+ ? async (family, fragment) => onFamily(family, mergeLastGood(fragment, {}).report)
184
+ : undefined;
182
185
  const loadFresh = () =>
183
- collectQuota({ families: everyFamily, timeoutMs: args.timeoutMs, refresh, force: args.force, localStats: LOCAL_STATS });
186
+ collectQuota({
187
+ families: everyFamily,
188
+ timeoutMs: args.timeoutMs,
189
+ refresh,
190
+ force: args.force,
191
+ localStats: LOCAL_STATS,
192
+ onFamily: publishFamily,
193
+ });
184
194
  const fresh = args.noCache
185
195
  ? await loadFresh()
186
196
  : (await withCache({ ttlMs: args.ttlMs, force: args.force, families: everyFamily, key: CACHE_KEY }, loadFresh)).report;