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