clanker-usage 0.4.0__tar.gz

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.
Files changed (40) hide show
  1. clanker_usage-0.4.0/.gitignore +4 -0
  2. clanker_usage-0.4.0/LICENSE +21 -0
  3. clanker_usage-0.4.0/PKG-INFO +228 -0
  4. clanker_usage-0.4.0/README.md +214 -0
  5. clanker_usage-0.4.0/build_wrappers.py +159 -0
  6. clanker_usage-0.4.0/pyproject.toml +35 -0
  7. clanker_usage-0.4.0/src/clanker_usage/__init__.py +5 -0
  8. clanker_usage-0.4.0/src/clanker_usage/__main__.py +3 -0
  9. clanker_usage-0.4.0/src/clanker_usage/cache.py +58 -0
  10. clanker_usage-0.4.0/src/clanker_usage/cli.py +400 -0
  11. clanker_usage-0.4.0/src/clanker_usage/errors.py +10 -0
  12. clanker_usage-0.4.0/src/clanker_usage/http.py +68 -0
  13. clanker_usage-0.4.0/src/clanker_usage/oauth.py +29 -0
  14. clanker_usage-0.4.0/src/clanker_usage/paths.py +38 -0
  15. clanker_usage-0.4.0/src/clanker_usage/providers/__init__.py +13 -0
  16. clanker_usage-0.4.0/src/clanker_usage/providers/agy.py +189 -0
  17. clanker_usage-0.4.0/src/clanker_usage/providers/claude.py +236 -0
  18. clanker_usage-0.4.0/src/clanker_usage/providers/codex.py +243 -0
  19. clanker_usage-0.4.0/src/clanker_usage/providers/cursor.py +122 -0
  20. clanker_usage-0.4.0/src/clanker_usage/providers/deepseek.py +409 -0
  21. clanker_usage-0.4.0/src/clanker_usage/providers/devin.py +119 -0
  22. clanker_usage-0.4.0/src/clanker_usage/providers/gemini.py +192 -0
  23. clanker_usage-0.4.0/src/clanker_usage/providers/google.py +63 -0
  24. clanker_usage-0.4.0/src/clanker_usage/providers/opencode.py +96 -0
  25. clanker_usage-0.4.0/src/clanker_usage/providers/zcode.py +117 -0
  26. clanker_usage-0.4.0/src/clanker_usage/shapes/agy/agy-2026-10-19.json +59 -0
  27. clanker_usage-0.4.0/src/clanker_usage/shapes/claude/claude-2026-10-19.json +54 -0
  28. clanker_usage-0.4.0/src/clanker_usage/shapes/codex/api-2026-10-19.json +37 -0
  29. clanker_usage-0.4.0/src/clanker_usage/shapes/codex/app-server-2026-10-19.json +56 -0
  30. clanker_usage-0.4.0/src/clanker_usage/shapes/cursor/cursor-2026-10-19.json +35 -0
  31. clanker_usage-0.4.0/src/clanker_usage/shapes/deepseek/deepseek-2026-10-19.json +11 -0
  32. clanker_usage-0.4.0/src/clanker_usage/shapes/devin/devin-2026-10-19.json +17 -0
  33. clanker_usage-0.4.0/src/clanker_usage/shapes/gemini/from-gemini-cli-0.56-source.json +12 -0
  34. clanker_usage-0.4.0/src/clanker_usage/shapes/opencode/opencode-2026-10-19.json +30 -0
  35. clanker_usage-0.4.0/src/clanker_usage/shapes/zcode/no-plan-2026-10-18.json +5 -0
  36. clanker_usage-0.4.0/src/clanker_usage/shapes/zcode/quota-2026-09-05.json +29 -0
  37. clanker_usage-0.4.0/src/clanker_usage/shapes.py +88 -0
  38. clanker_usage-0.4.0/tests/test_core.py +207 -0
  39. clanker_usage-0.4.0/tests/test_install.py +62 -0
  40. clanker_usage-0.4.0/tests/test_zcode.py +195 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ .venv/
3
+ dist/
4
+ dist-wrappers/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mihai
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.
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.5
2
+ Name: clanker-usage
3
+ Version: 0.4.0
4
+ Summary: Plan and quota of coding-agent accounts, with a statusline, a cache daemon and a check for provider changes
5
+ Project-URL: Homepage, https://github.com/wakamex/clanker-usage
6
+ Project-URL: Source, https://github.com/wakamex/clanker-usage
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai-agents,cli,quota,statusline,usage,z.ai
10
+ Requires-Python: >=3.12
11
+ Provides-Extra: deepseek
12
+ Requires-Dist: pyyaml>=6.0.3; extra == 'deepseek'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # clanker-usage
16
+
17
+ Monitor the plan and quota of your coding-agent accounts from the terminal: current usage, a one-line statusline, a daemon that keeps a cache fresh, and a check that fails loudly when a provider changes its API. Every provider has the same commands and the same JSON document.
18
+
19
+ Providers: Antigravity (`agy-usage`), Claude Code (`ccusage`), Codex (`codex-cli-usage`), Cursor (`cursor-cli-usage`), DeepSeek (`deepseek-harness-usage`), Devin (`devin-cli-usage`), Gemini CLI (`gemini-cli-usage`), OpenCode Go (`opencode-cli-usage`) and Z.ai's Coding Plan (`zcode-cli-usage`). Each package name and command is a former standalone tool, now one interface over one codebase.
20
+
21
+ clanker-usage reports what the provider says about the account: plan, quota windows and balance. Token use and cost from local session logs are the job of [clanker-analytics](https://github.com/wakamex/clanker-analytics).
22
+
23
+ ```text
24
+ $ zcode-cli-usage
25
+ Z.ai: lite
26
+ 5-hour: 0% used
27
+ weekly: 1% used, resets 2026-09-10 23:38 EDT
28
+ ```
29
+
30
+ The numbers above are synthetic.
31
+
32
+ ## Install
33
+
34
+ Requires Python 3.12 or newer and [uv](https://docs.astral.sh/uv/). Install the command for your provider:
35
+
36
+ ```sh
37
+ uv tool install zcode-cli-usage
38
+ ```
39
+
40
+ `zcode-cli-usage` installs `clanker-usage` at the same version. `zcode-cli-usage COMMAND` and `clanker-usage zcode COMMAND` are the same command.
41
+
42
+ ## Commands
43
+
44
+ Every provider command takes the same subcommands:
45
+
46
+ ```sh
47
+ zcode-cli-usage # status: the plan and each window
48
+ zcode-cli-usage json # the usage-v1 document
49
+ zcode-cli-usage statusline # one line for a status bar
50
+ zcode-cli-usage refresh # fetch and update the cache; fail if unavailable
51
+ zcode-cli-usage daemon # refresh the cache every five minutes
52
+ zcode-cli-usage check # compare the live response with the recorded response shapes
53
+ clanker-usage # every account in one table; --json for every document
54
+ clanker-usage check # check every provider
55
+ ```
56
+
57
+ - `--max-age SECONDS` on `status`, `json`, `statusline` and `clanker-usage` itself reuses a cached document younger than that. It defaults to 0, always fetching, for `status` and `json`, and to 300 for `statusline` and the all-accounts table.
58
+ - `--interval SECONDS` on `daemon` sets the refresh interval. A daemon skips a refresh when another process updated the same account's cache within the interval, so several daemons or statuslines on one account do not multiply requests. After a failure it backs off, doubling up to an hour.
59
+ - When a fetch fails, the last cached document for the same account is shown as `stale`. With no cache to fall back on, the result is `unavailable`, which exits with status 1; `json` still prints the document.
60
+ - A provider can add a setup command, such as Z.ai's `configure`.
61
+
62
+ ## The usage-v1 document
63
+
64
+ `json` prints one document per account, and the cache stores the same document:
65
+
66
+ ```json
67
+ {
68
+ "schema": "usage-v1",
69
+ "provider": "zcode",
70
+ "account": "3f2a9c1e5b7d4a60",
71
+ "plan": "lite",
72
+ "status": "live",
73
+ "error": null,
74
+ "retrieved_at": "2026-09-05T17:00:00+00:00",
75
+ "windows": [
76
+ {"name": "5-hour", "seconds": 18000, "scope": null, "used_pct": 0, "resets_at": null},
77
+ {"name": "weekly", "seconds": 604800, "scope": null, "used_pct": 1, "resets_at": "2026-09-11T03:38:39.995000+00:00"}
78
+ ],
79
+ "balance": null,
80
+ "details": {"limits": []}
81
+ }
82
+ ```
83
+
84
+ | Field | Meaning |
85
+ | --- | --- |
86
+ | `provider` | the provider, such as `zcode` |
87
+ | `account` | a stable account id from the provider, or a one-way fingerprint of the credential when the provider gives none; null when no credential was found |
88
+ | `plan` | the plan name as the provider reports it, or null without a plan |
89
+ | `status` | `live` (fetched now), `cached` (reused under `--max-age`), `stale` (the fetch failed and this is the last cached document) or `unavailable` |
90
+ | `error` | why the fetch failed, for `stale` and `unavailable`; it never contains a credential or response body |
91
+ | `retrieved_at` | when the provider returned this data, in ISO 8601 UTC |
92
+ | `balance` | for an account billed from a balance, one entry per currency with its `currency` and `amount`, which goes below zero when usage outruns the balance; null for accounts with quota windows |
93
+ | `windows` | each quota window: `name`, length in `seconds` (null when the window is not a fixed length), `scope` (the models it covers, or null for the whole account), `used_pct` from 0 to 100, as exact as the provider allows, and `resets_at` in ISO 8601 UTC or null |
94
+ | `details` | what only this provider reports |
95
+
96
+ ## Files
97
+
98
+ | File | Path |
99
+ | --- | --- |
100
+ | Cache | `~/.cache/clanker-usage/PROVIDER/ACCOUNT.json` |
101
+ | Check state | `~/.local/state/clanker-usage/check/PROVIDER.json` |
102
+ | Configuration | `~/.config/clanker-usage/config.json`, one section per provider |
103
+
104
+ The `XDG_CACHE_HOME`, `XDG_STATE_HOME` and `XDG_CONFIG_HOME` variables move them. Caches never contain a credential.
105
+
106
+ ## Checking for provider changes
107
+
108
+ `check` fetches without the cache and reports each provider as one of:
109
+
110
+ - `ok`: the response matches a recorded shape and every window the previous check saw is present.
111
+ - `new-shape`: the response has a field or type that no recorded shape has. The new paths are listed, such as `$.data.limits[].rollover: number`.
112
+ - `missing`: a window that the previous check saw is absent.
113
+ - `error`: the request failed or the response did not parse.
114
+
115
+ A provider whose login has no quota, such as Gemini CLI on an API key, reports `expected-unavailable`. `check` exits with status 1 unless every provider is `ok` or `expected-unavailable`, so it can run from a timer or CI and notify on failure. Its state file keeps paths and window names but no values. `check --record DIR` also saves a synthetic copy of each live response in `DIR`.
116
+
117
+ ## Antigravity
118
+
119
+ Sign in with the Antigravity CLI, `agy`. The tool reads its Google login from `~/.gemini/antigravity-cli/antigravity-oauth-token`. When agy has left the access token expired, the tool renews it with the OAuth client compiled into `agy` and saves it back, but only if agy has not renewed it meanwhile, so a renewal agy made is never undone. Requests carry the installed agy version, because the quota service leaves out the Gemini group for older clients. The account id is a fingerprint of the Google account in the login's id token.
120
+
121
+ Quota comes from Google's Code Assist `retrieveUserQuotaSummary`. Each quota group, such as Gemini or Claude and GPT, has a `5-hour` and a `weekly` window, with the group as the window's `scope`; the plan is the paid tier's name, such as Google AI Ultra.
122
+
123
+ ## Claude Code
124
+
125
+ Sign in with Claude Code. The tool reads its login from `.credentials.json` in Claude's config directory, `~/.claude` or `$CLAUDE_CONFIG_DIR`, and the account from Claude Code's `.claude.json`. When Claude Code has left the access token expired, the tool renews it and writes the rotated refresh token back, because Anthropic rotates refresh tokens and Claude Code would otherwise be signed out; it replaces the file only if Claude Code has not renewed it meanwhile. A request during which `/login` switched accounts is discarded. The account id is a fingerprint of Claude's account UUID, so config directories signed into the same account share one cache and one daemon's refreshes.
126
+
127
+ The windows are the `5-hour` session and the `weekly` limit, plus a `weekly` window scoped to each model with its own weekly limit, such as Fable. An account without a subscription, or in an organization that turned off OAuth, is `unavailable`, which replaces its old windows in the cache and which `check` reports as expected.
128
+
129
+ As Claude Code's status line, `ccusage statusline` reads the status JSON Claude Code pipes to it and starts the line with the directory, model and session cost. In `~/.claude/settings.json`:
130
+
131
+ ```json
132
+ {"statusLine": {"type": "command", "command": "ccusage statusline"}}
133
+ ```
134
+
135
+ To see several accounts in `clanker-usage`, add each extra config directory once: `ccusage configure --config-dir ~/.claude-b`.
136
+
137
+ ## Codex
138
+
139
+ Sign in with Codex. The tool asks Codex itself: it starts `codex app-server` and reads `account/rateLimits/read` over its JSON-RPC protocol, so Codex handles its own login and token renewal. When Codex is not installed or does not answer, the tool reads the token Codex saved in `$CODEX_HOME/auth.json` (`~/.codex` by default) and calls the usage API directly; it never renews that token, and rereads it once if Codex renewed it during the request. The account id is a fingerprint of Codex's ChatGPT account id.
140
+
141
+ Codex's own limits are the `5-hour` and `weekly` windows. Each additional limit, such as one for a single model, adds its windows with the limit's name as their `scope`, and the API's code review limit appears as a `code review` window. `details` keeps the credits balance when the account has credits.
142
+
143
+ ## Cursor
144
+
145
+ Sign in with Cursor's CLI (`agent login`) or its IDE. The tool reads the CLI's access token from `~/.config/cursor/auth.json`, and otherwise the IDE's from its `state.vscdb` database, and never refreshes or changes either. The account id is a fingerprint of the token's subject, so it stays the same when Cursor renews the token.
146
+
147
+ Usage comes from Cursor's `DashboardService/GetCurrentPeriodUsage` and the plan name from `DashboardService/GetPlanInfo`. The one window is the `billing cycle`, with the share of the plan's included usage already used; `details` keeps the spend, the limit and the separate shares for automatic model selection and for API pricing.
148
+
149
+ ## DeepSeek
150
+
151
+ The tool finds the API key the way DeepSeek Harness does: `DEEPSEEK_API_KEY` in the environment, then Harness's credential store `$DSH_HOME/.credentials.yaml` (`~/.dsh` by default), then a `.env` in the current directory, then `$DSH_HOME/.env`. It parses those files as Harness does, refuses a credential store readable by other users, and never changes them. When Harness routes DeepSeek to a custom endpoint or credential name, the tool will not send that key to DeepSeek; set `CLANKER_USAGE_DEEPSEEK_API_KEY` to the official account key instead. The account id is a fingerprint of the key.
152
+
153
+ The balance comes from DeepSeek's `GET /user/balance`. DeepSeek reports no quota windows or token history, so the document has a `balance` per currency and `details` keeps the granted and topped-up parts and whether the balance still allows requests. Install `deepseek-harness-usage`, or `clanker-usage[deepseek]`, since reading Harness's YAML store needs [PyYAML](https://pyyaml.org/).
154
+
155
+ ## Devin
156
+
157
+ Sign in with the Devin CLI. The tool reads its local API key and server from `~/.local/share/devin/credentials.toml` (under `$XDG_DATA_HOME` when set), sends requests only to that server over HTTPS, and never changes the file. The account id is a fingerprint of the key.
158
+
159
+ Usage comes from the server's `SeatManagementService/GetUserStatus`. The `daily` and `weekly` windows come from the remaining quota it reports; `details.credits` keeps the prompt, flow and flex credits and the overage balance. The tool keeps only the plan and quota parts of the response, leaving out the account's name, email and model lists.
160
+
161
+ ## Gemini CLI
162
+
163
+ Quota exists only for a Google login. The tool chooses the login as Gemini CLI does: the `GOOGLE_GENAI_USE_GCA`, `GOOGLE_GENAI_USE_VERTEXAI`, `GEMINI_API_KEY` and Cloud Shell variables, then `.gemini/settings.json` in the current directory, then `~/.gemini/settings.json` (under `$GEMINI_CLI_HOME` when set). With any other login the document is `unavailable` and `check` reports `expected-unavailable`, which does not fail it. With a Google login it reads `~/.gemini/oauth_creds.json` and renews an expired token the same way as for Antigravity, using the OAuth client in the credentials file or in the installed Gemini CLI.
164
+
165
+ Quota comes from Code Assist `retrieveUserQuota`, with one window per model, scoped to that model. `GOOGLE_CLOUD_PROJECT` selects the Code Assist project for accounts that need one, as in Gemini CLI.
166
+
167
+ ## OpenCode Go
168
+
169
+ Connect OpenCode Go in OpenCode with `/connect`. The tool reads the Go key OpenCode stores in `~/.local/share/opencode/auth.json` (under `$XDG_DATA_HOME` when set) and never changes the file. The account id is a fingerprint of the key.
170
+
171
+ The limits come from the OpenCode Console's `/console/api/go/status`, the call the console's Go page makes. It is not part of the documented Console API, so `check` is what notices when its format changes. Go's limits are dollar amounts: the `5-hour` and `weekly` windows allow 20% and 50% of the `monthly` amount, and `details.spend` keeps each window's used and total dollars. The plan is the subscription product, such as `go`.
172
+
173
+ ## Z.ai Coding Plan
174
+
175
+ Export `ZAI_API_KEY`, or point the tool at an existing `.env` file once:
176
+
177
+ ```sh
178
+ zcode-cli-usage configure --env-file /path/to/.env
179
+ ```
180
+
181
+ Configuration stores only the file path. The tool reads `ZAI_API_KEY` from that file without executing shell code: plain or quoted assignments, optional `export`, and comments work, and shell variable expansion does not. An exported `ZAI_API_KEY` takes precedence. The account id is a fingerprint of the key, since the API names no account.
182
+
183
+ Quota data comes from `GET https://api.z.ai/api/monitor/usage/quota/limit` with `Authorization: Bearer <ZAI_API_KEY>`. Each quota becomes a window: unit `3` is hours and `6` is weeks, so the current quotas are `5-hour` and `weekly`. `used_pct` is computed from the used and total credits, since the API's own percentage is rounded; `details.limits` keeps each quota's `type`, `unit`, `number`, `used`, `limit` and `remaining` credits. An account without an active Coding Plan has a null `plan` and no windows.
184
+
185
+ ## Background refresh on Linux
186
+
187
+ Save this as `~/.config/systemd/user/zcode-cli-usage.service`, adjusting the executable path if needed:
188
+
189
+ ```ini
190
+ [Unit]
191
+ Description=Z.ai Coding Plan quota monitor
192
+ After=network-online.target
193
+
194
+ [Service]
195
+ ExecStart=%h/.local/bin/zcode-cli-usage daemon
196
+ Restart=on-failure
197
+ RestartSec=30
198
+
199
+ [Install]
200
+ WantedBy=default.target
201
+ ```
202
+
203
+ ```sh
204
+ systemctl --user daemon-reload
205
+ systemctl --user enable --now zcode-cli-usage.service
206
+ ```
207
+
208
+ ## Reliability and privacy
209
+
210
+ - Requests never follow redirects, so a credential is only sent to the provider's own URL. Responses over 1 MiB are rejected, and errors name only the HTTP status or failure kind.
211
+ - Caches, state and configuration are written through a private temporary file and atomically replaced, with mode `0600` in directories the tool creates with mode `0700`.
212
+
213
+ ## Development
214
+
215
+ The provider packages, such as `zcode-cli-usage`, are generated by `build_wrappers.py`. Each depends on the same version of `clanker-usage` and contains only a module named after itself that runs its provider, so its command and `python -m` module work.
216
+
217
+ A provider module in `src/clanker_usage/providers/` supplies its account id, a fetch of the raw response, and a `normalize` that turns the response into the document's `plan`, `windows` and `details`; the core does everything else. Recorded response shapes live in `src/clanker_usage/shapes/<provider>/`. Every value in them is replaced by a fixed one of the same type, except under keys that describe the format, such as a window's unit, which each provider lists in `KEEP`. Every provider is tested against every recorded shape. When `check` reports a new shape with the same meaning, save it with `check --record DIR` and add the file to that folder; when the meaning changed, fix the provider.
218
+
219
+ ```sh
220
+ uv run --locked python -m unittest discover -s tests
221
+ uv run --locked python build_wrappers.py # writes dist-wrappers/
222
+ ```
223
+
224
+ `tests/test_install.py` builds the core and every provider package, installs each provider package alone with uv, and runs its command and module.
225
+
226
+ ## License
227
+
228
+ MIT
@@ -0,0 +1,214 @@
1
+ # clanker-usage
2
+
3
+ Monitor the plan and quota of your coding-agent accounts from the terminal: current usage, a one-line statusline, a daemon that keeps a cache fresh, and a check that fails loudly when a provider changes its API. Every provider has the same commands and the same JSON document.
4
+
5
+ Providers: Antigravity (`agy-usage`), Claude Code (`ccusage`), Codex (`codex-cli-usage`), Cursor (`cursor-cli-usage`), DeepSeek (`deepseek-harness-usage`), Devin (`devin-cli-usage`), Gemini CLI (`gemini-cli-usage`), OpenCode Go (`opencode-cli-usage`) and Z.ai's Coding Plan (`zcode-cli-usage`). Each package name and command is a former standalone tool, now one interface over one codebase.
6
+
7
+ clanker-usage reports what the provider says about the account: plan, quota windows and balance. Token use and cost from local session logs are the job of [clanker-analytics](https://github.com/wakamex/clanker-analytics).
8
+
9
+ ```text
10
+ $ zcode-cli-usage
11
+ Z.ai: lite
12
+ 5-hour: 0% used
13
+ weekly: 1% used, resets 2026-09-10 23:38 EDT
14
+ ```
15
+
16
+ The numbers above are synthetic.
17
+
18
+ ## Install
19
+
20
+ Requires Python 3.12 or newer and [uv](https://docs.astral.sh/uv/). Install the command for your provider:
21
+
22
+ ```sh
23
+ uv tool install zcode-cli-usage
24
+ ```
25
+
26
+ `zcode-cli-usage` installs `clanker-usage` at the same version. `zcode-cli-usage COMMAND` and `clanker-usage zcode COMMAND` are the same command.
27
+
28
+ ## Commands
29
+
30
+ Every provider command takes the same subcommands:
31
+
32
+ ```sh
33
+ zcode-cli-usage # status: the plan and each window
34
+ zcode-cli-usage json # the usage-v1 document
35
+ zcode-cli-usage statusline # one line for a status bar
36
+ zcode-cli-usage refresh # fetch and update the cache; fail if unavailable
37
+ zcode-cli-usage daemon # refresh the cache every five minutes
38
+ zcode-cli-usage check # compare the live response with the recorded response shapes
39
+ clanker-usage # every account in one table; --json for every document
40
+ clanker-usage check # check every provider
41
+ ```
42
+
43
+ - `--max-age SECONDS` on `status`, `json`, `statusline` and `clanker-usage` itself reuses a cached document younger than that. It defaults to 0, always fetching, for `status` and `json`, and to 300 for `statusline` and the all-accounts table.
44
+ - `--interval SECONDS` on `daemon` sets the refresh interval. A daemon skips a refresh when another process updated the same account's cache within the interval, so several daemons or statuslines on one account do not multiply requests. After a failure it backs off, doubling up to an hour.
45
+ - When a fetch fails, the last cached document for the same account is shown as `stale`. With no cache to fall back on, the result is `unavailable`, which exits with status 1; `json` still prints the document.
46
+ - A provider can add a setup command, such as Z.ai's `configure`.
47
+
48
+ ## The usage-v1 document
49
+
50
+ `json` prints one document per account, and the cache stores the same document:
51
+
52
+ ```json
53
+ {
54
+ "schema": "usage-v1",
55
+ "provider": "zcode",
56
+ "account": "3f2a9c1e5b7d4a60",
57
+ "plan": "lite",
58
+ "status": "live",
59
+ "error": null,
60
+ "retrieved_at": "2026-09-05T17:00:00+00:00",
61
+ "windows": [
62
+ {"name": "5-hour", "seconds": 18000, "scope": null, "used_pct": 0, "resets_at": null},
63
+ {"name": "weekly", "seconds": 604800, "scope": null, "used_pct": 1, "resets_at": "2026-09-11T03:38:39.995000+00:00"}
64
+ ],
65
+ "balance": null,
66
+ "details": {"limits": []}
67
+ }
68
+ ```
69
+
70
+ | Field | Meaning |
71
+ | --- | --- |
72
+ | `provider` | the provider, such as `zcode` |
73
+ | `account` | a stable account id from the provider, or a one-way fingerprint of the credential when the provider gives none; null when no credential was found |
74
+ | `plan` | the plan name as the provider reports it, or null without a plan |
75
+ | `status` | `live` (fetched now), `cached` (reused under `--max-age`), `stale` (the fetch failed and this is the last cached document) or `unavailable` |
76
+ | `error` | why the fetch failed, for `stale` and `unavailable`; it never contains a credential or response body |
77
+ | `retrieved_at` | when the provider returned this data, in ISO 8601 UTC |
78
+ | `balance` | for an account billed from a balance, one entry per currency with its `currency` and `amount`, which goes below zero when usage outruns the balance; null for accounts with quota windows |
79
+ | `windows` | each quota window: `name`, length in `seconds` (null when the window is not a fixed length), `scope` (the models it covers, or null for the whole account), `used_pct` from 0 to 100, as exact as the provider allows, and `resets_at` in ISO 8601 UTC or null |
80
+ | `details` | what only this provider reports |
81
+
82
+ ## Files
83
+
84
+ | File | Path |
85
+ | --- | --- |
86
+ | Cache | `~/.cache/clanker-usage/PROVIDER/ACCOUNT.json` |
87
+ | Check state | `~/.local/state/clanker-usage/check/PROVIDER.json` |
88
+ | Configuration | `~/.config/clanker-usage/config.json`, one section per provider |
89
+
90
+ The `XDG_CACHE_HOME`, `XDG_STATE_HOME` and `XDG_CONFIG_HOME` variables move them. Caches never contain a credential.
91
+
92
+ ## Checking for provider changes
93
+
94
+ `check` fetches without the cache and reports each provider as one of:
95
+
96
+ - `ok`: the response matches a recorded shape and every window the previous check saw is present.
97
+ - `new-shape`: the response has a field or type that no recorded shape has. The new paths are listed, such as `$.data.limits[].rollover: number`.
98
+ - `missing`: a window that the previous check saw is absent.
99
+ - `error`: the request failed or the response did not parse.
100
+
101
+ A provider whose login has no quota, such as Gemini CLI on an API key, reports `expected-unavailable`. `check` exits with status 1 unless every provider is `ok` or `expected-unavailable`, so it can run from a timer or CI and notify on failure. Its state file keeps paths and window names but no values. `check --record DIR` also saves a synthetic copy of each live response in `DIR`.
102
+
103
+ ## Antigravity
104
+
105
+ Sign in with the Antigravity CLI, `agy`. The tool reads its Google login from `~/.gemini/antigravity-cli/antigravity-oauth-token`. When agy has left the access token expired, the tool renews it with the OAuth client compiled into `agy` and saves it back, but only if agy has not renewed it meanwhile, so a renewal agy made is never undone. Requests carry the installed agy version, because the quota service leaves out the Gemini group for older clients. The account id is a fingerprint of the Google account in the login's id token.
106
+
107
+ Quota comes from Google's Code Assist `retrieveUserQuotaSummary`. Each quota group, such as Gemini or Claude and GPT, has a `5-hour` and a `weekly` window, with the group as the window's `scope`; the plan is the paid tier's name, such as Google AI Ultra.
108
+
109
+ ## Claude Code
110
+
111
+ Sign in with Claude Code. The tool reads its login from `.credentials.json` in Claude's config directory, `~/.claude` or `$CLAUDE_CONFIG_DIR`, and the account from Claude Code's `.claude.json`. When Claude Code has left the access token expired, the tool renews it and writes the rotated refresh token back, because Anthropic rotates refresh tokens and Claude Code would otherwise be signed out; it replaces the file only if Claude Code has not renewed it meanwhile. A request during which `/login` switched accounts is discarded. The account id is a fingerprint of Claude's account UUID, so config directories signed into the same account share one cache and one daemon's refreshes.
112
+
113
+ The windows are the `5-hour` session and the `weekly` limit, plus a `weekly` window scoped to each model with its own weekly limit, such as Fable. An account without a subscription, or in an organization that turned off OAuth, is `unavailable`, which replaces its old windows in the cache and which `check` reports as expected.
114
+
115
+ As Claude Code's status line, `ccusage statusline` reads the status JSON Claude Code pipes to it and starts the line with the directory, model and session cost. In `~/.claude/settings.json`:
116
+
117
+ ```json
118
+ {"statusLine": {"type": "command", "command": "ccusage statusline"}}
119
+ ```
120
+
121
+ To see several accounts in `clanker-usage`, add each extra config directory once: `ccusage configure --config-dir ~/.claude-b`.
122
+
123
+ ## Codex
124
+
125
+ Sign in with Codex. The tool asks Codex itself: it starts `codex app-server` and reads `account/rateLimits/read` over its JSON-RPC protocol, so Codex handles its own login and token renewal. When Codex is not installed or does not answer, the tool reads the token Codex saved in `$CODEX_HOME/auth.json` (`~/.codex` by default) and calls the usage API directly; it never renews that token, and rereads it once if Codex renewed it during the request. The account id is a fingerprint of Codex's ChatGPT account id.
126
+
127
+ Codex's own limits are the `5-hour` and `weekly` windows. Each additional limit, such as one for a single model, adds its windows with the limit's name as their `scope`, and the API's code review limit appears as a `code review` window. `details` keeps the credits balance when the account has credits.
128
+
129
+ ## Cursor
130
+
131
+ Sign in with Cursor's CLI (`agent login`) or its IDE. The tool reads the CLI's access token from `~/.config/cursor/auth.json`, and otherwise the IDE's from its `state.vscdb` database, and never refreshes or changes either. The account id is a fingerprint of the token's subject, so it stays the same when Cursor renews the token.
132
+
133
+ Usage comes from Cursor's `DashboardService/GetCurrentPeriodUsage` and the plan name from `DashboardService/GetPlanInfo`. The one window is the `billing cycle`, with the share of the plan's included usage already used; `details` keeps the spend, the limit and the separate shares for automatic model selection and for API pricing.
134
+
135
+ ## DeepSeek
136
+
137
+ The tool finds the API key the way DeepSeek Harness does: `DEEPSEEK_API_KEY` in the environment, then Harness's credential store `$DSH_HOME/.credentials.yaml` (`~/.dsh` by default), then a `.env` in the current directory, then `$DSH_HOME/.env`. It parses those files as Harness does, refuses a credential store readable by other users, and never changes them. When Harness routes DeepSeek to a custom endpoint or credential name, the tool will not send that key to DeepSeek; set `CLANKER_USAGE_DEEPSEEK_API_KEY` to the official account key instead. The account id is a fingerprint of the key.
138
+
139
+ The balance comes from DeepSeek's `GET /user/balance`. DeepSeek reports no quota windows or token history, so the document has a `balance` per currency and `details` keeps the granted and topped-up parts and whether the balance still allows requests. Install `deepseek-harness-usage`, or `clanker-usage[deepseek]`, since reading Harness's YAML store needs [PyYAML](https://pyyaml.org/).
140
+
141
+ ## Devin
142
+
143
+ Sign in with the Devin CLI. The tool reads its local API key and server from `~/.local/share/devin/credentials.toml` (under `$XDG_DATA_HOME` when set), sends requests only to that server over HTTPS, and never changes the file. The account id is a fingerprint of the key.
144
+
145
+ Usage comes from the server's `SeatManagementService/GetUserStatus`. The `daily` and `weekly` windows come from the remaining quota it reports; `details.credits` keeps the prompt, flow and flex credits and the overage balance. The tool keeps only the plan and quota parts of the response, leaving out the account's name, email and model lists.
146
+
147
+ ## Gemini CLI
148
+
149
+ Quota exists only for a Google login. The tool chooses the login as Gemini CLI does: the `GOOGLE_GENAI_USE_GCA`, `GOOGLE_GENAI_USE_VERTEXAI`, `GEMINI_API_KEY` and Cloud Shell variables, then `.gemini/settings.json` in the current directory, then `~/.gemini/settings.json` (under `$GEMINI_CLI_HOME` when set). With any other login the document is `unavailable` and `check` reports `expected-unavailable`, which does not fail it. With a Google login it reads `~/.gemini/oauth_creds.json` and renews an expired token the same way as for Antigravity, using the OAuth client in the credentials file or in the installed Gemini CLI.
150
+
151
+ Quota comes from Code Assist `retrieveUserQuota`, with one window per model, scoped to that model. `GOOGLE_CLOUD_PROJECT` selects the Code Assist project for accounts that need one, as in Gemini CLI.
152
+
153
+ ## OpenCode Go
154
+
155
+ Connect OpenCode Go in OpenCode with `/connect`. The tool reads the Go key OpenCode stores in `~/.local/share/opencode/auth.json` (under `$XDG_DATA_HOME` when set) and never changes the file. The account id is a fingerprint of the key.
156
+
157
+ The limits come from the OpenCode Console's `/console/api/go/status`, the call the console's Go page makes. It is not part of the documented Console API, so `check` is what notices when its format changes. Go's limits are dollar amounts: the `5-hour` and `weekly` windows allow 20% and 50% of the `monthly` amount, and `details.spend` keeps each window's used and total dollars. The plan is the subscription product, such as `go`.
158
+
159
+ ## Z.ai Coding Plan
160
+
161
+ Export `ZAI_API_KEY`, or point the tool at an existing `.env` file once:
162
+
163
+ ```sh
164
+ zcode-cli-usage configure --env-file /path/to/.env
165
+ ```
166
+
167
+ Configuration stores only the file path. The tool reads `ZAI_API_KEY` from that file without executing shell code: plain or quoted assignments, optional `export`, and comments work, and shell variable expansion does not. An exported `ZAI_API_KEY` takes precedence. The account id is a fingerprint of the key, since the API names no account.
168
+
169
+ Quota data comes from `GET https://api.z.ai/api/monitor/usage/quota/limit` with `Authorization: Bearer <ZAI_API_KEY>`. Each quota becomes a window: unit `3` is hours and `6` is weeks, so the current quotas are `5-hour` and `weekly`. `used_pct` is computed from the used and total credits, since the API's own percentage is rounded; `details.limits` keeps each quota's `type`, `unit`, `number`, `used`, `limit` and `remaining` credits. An account without an active Coding Plan has a null `plan` and no windows.
170
+
171
+ ## Background refresh on Linux
172
+
173
+ Save this as `~/.config/systemd/user/zcode-cli-usage.service`, adjusting the executable path if needed:
174
+
175
+ ```ini
176
+ [Unit]
177
+ Description=Z.ai Coding Plan quota monitor
178
+ After=network-online.target
179
+
180
+ [Service]
181
+ ExecStart=%h/.local/bin/zcode-cli-usage daemon
182
+ Restart=on-failure
183
+ RestartSec=30
184
+
185
+ [Install]
186
+ WantedBy=default.target
187
+ ```
188
+
189
+ ```sh
190
+ systemctl --user daemon-reload
191
+ systemctl --user enable --now zcode-cli-usage.service
192
+ ```
193
+
194
+ ## Reliability and privacy
195
+
196
+ - Requests never follow redirects, so a credential is only sent to the provider's own URL. Responses over 1 MiB are rejected, and errors name only the HTTP status or failure kind.
197
+ - Caches, state and configuration are written through a private temporary file and atomically replaced, with mode `0600` in directories the tool creates with mode `0700`.
198
+
199
+ ## Development
200
+
201
+ The provider packages, such as `zcode-cli-usage`, are generated by `build_wrappers.py`. Each depends on the same version of `clanker-usage` and contains only a module named after itself that runs its provider, so its command and `python -m` module work.
202
+
203
+ A provider module in `src/clanker_usage/providers/` supplies its account id, a fetch of the raw response, and a `normalize` that turns the response into the document's `plan`, `windows` and `details`; the core does everything else. Recorded response shapes live in `src/clanker_usage/shapes/<provider>/`. Every value in them is replaced by a fixed one of the same type, except under keys that describe the format, such as a window's unit, which each provider lists in `KEEP`. Every provider is tested against every recorded shape. When `check` reports a new shape with the same meaning, save it with `check --record DIR` and add the file to that folder; when the meaning changed, fix the provider.
204
+
205
+ ```sh
206
+ uv run --locked python -m unittest discover -s tests
207
+ uv run --locked python build_wrappers.py # writes dist-wrappers/
208
+ ```
209
+
210
+ `tests/test_install.py` builds the core and every provider package, installs each provider package alone with uv, and runs its command and module.
211
+
212
+ ## License
213
+
214
+ MIT
@@ -0,0 +1,159 @@
1
+ """Generate the wrapper packages that keep each provider's original package name and command.
2
+
3
+ clanker-usage is the only codebase. A wrapper depends on the same version of clanker-usage and
4
+ contains one generated module named after itself, so `NAME` and `python -m MODULE` run that
5
+ provider. The module only calls clanker-usage's `run` for its provider.
6
+
7
+ Usage: python build_wrappers.py [OUT_DIR] [PACKAGE ...] (default OUT_DIR: dist-wrappers)
8
+ """
9
+
10
+ import re
11
+ import shutil
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parent
16
+ CORE = "clanker-usage"
17
+ REPOSITORY = "https://github.com/wakamex/clanker-usage"
18
+
19
+ WRAPPERS = {
20
+ "agy-usage": {
21
+ "provider": "agy", "label": "Antigravity",
22
+ "description": "Antigravity quota monitor",
23
+ "keywords": ["antigravity", "agy", "gemini", "usage", "quota", "statusline", "cli"],
24
+ },
25
+ "ccusage": {
26
+ "provider": "claude", "label": "Claude Code",
27
+ "description": "Claude Code usage limit monitor",
28
+ "keywords": ["claude", "claude-code", "anthropic", "usage", "rate-limits", "statusline", "cli"],
29
+ },
30
+ "codex-cli-usage": {
31
+ "provider": "codex", "label": "Codex",
32
+ "description": "Codex rate limit monitor",
33
+ "keywords": ["codex", "openai", "chatgpt", "usage", "rate-limits", "statusline", "cli"],
34
+ },
35
+ "cursor-cli-usage": {
36
+ "provider": "cursor", "label": "Cursor",
37
+ "description": "Cursor plan usage monitor",
38
+ "keywords": ["cursor", "cursor-agent", "usage", "quota", "statusline", "cli"],
39
+ },
40
+ "deepseek-harness-usage": {
41
+ "provider": "deepseek", "label": "DeepSeek", "extra": "deepseek",
42
+ "description": "DeepSeek account balance monitor for DeepSeek Harness",
43
+ "keywords": ["deepseek", "deepseek-harness", "balance", "usage", "statusline", "cli"],
44
+ },
45
+ "devin-cli-usage": {
46
+ "provider": "devin", "label": "Devin",
47
+ "description": "Devin quota and credits monitor",
48
+ "keywords": ["devin", "windsurf", "usage", "quota", "statusline", "cli"],
49
+ },
50
+ "gemini-cli-usage": {
51
+ "provider": "gemini", "label": "Gemini CLI",
52
+ "description": "Gemini CLI model quota monitor",
53
+ "keywords": ["gemini", "gemini-cli", "google", "usage", "quota", "statusline", "cli"],
54
+ },
55
+ "opencode-cli-usage": {
56
+ "provider": "opencode", "label": "OpenCode Go",
57
+ "description": "OpenCode Go subscription limit monitor",
58
+ "keywords": ["opencode", "opencode-go", "usage", "quota", "statusline", "cli"],
59
+ },
60
+ "zcode-cli-usage": {
61
+ "provider": "zcode", "label": "Z.ai Coding Plan",
62
+ "description": "Z.ai Coding Plan quota monitor",
63
+ "keywords": ["z.ai", "zai", "glm", "coding-plan", "usage", "quota", "statusline", "cli"],
64
+ },
65
+ }
66
+
67
+ PYPROJECT = '''[build-system]
68
+ requires = ["hatchling==1.32.4"]
69
+ build-backend = "hatchling.build"
70
+
71
+ [project]
72
+ name = "{name}"
73
+ version = "{version}"
74
+ description = "{description}"
75
+ readme = "README.md"
76
+ keywords = [{keywords}]
77
+ requires-python = ">=3.12"
78
+ license = "MIT"
79
+ license-files = ["LICENSE"]
80
+ dependencies = ["{core}{extras}=={version}"]
81
+
82
+ [project.urls]
83
+ Homepage = "{repository}"
84
+ Source = "{repository}"
85
+
86
+ [project.scripts]
87
+ {name} = "{module}:main"
88
+
89
+ [tool.hatch.build.targets.wheel]
90
+ packages = ["{module}"]
91
+ '''
92
+
93
+ INIT = '''"""{name}: the {label} command of {core}."""
94
+
95
+ from clanker_usage import run
96
+
97
+
98
+ def main():
99
+ return run("{provider}")
100
+ '''
101
+
102
+ MAIN = '''from . import main
103
+
104
+ raise SystemExit(main())
105
+ '''
106
+
107
+ README = '''# {name}
108
+
109
+ Monitor your {label} quota from the terminal: current usage, a one-line statusline, and a daemon that keeps a cache fresh.
110
+
111
+ ```sh
112
+ uv tool install {name}
113
+ {name} # current quotas
114
+ {name} statusline # one line, from a cache less than five minutes old
115
+ {name} json # the usage-v1 document
116
+ ```
117
+
118
+ `{name}` is the {label} command of [{core}]({repository}), so it installs {core} at the same version. The documentation lives there.
119
+ '''
120
+
121
+
122
+ def core_version():
123
+ match = re.search(r'(?m)^version = "([^"]+)"', (ROOT / "pyproject.toml").read_text())
124
+ if not match:
125
+ raise SystemExit("no version in pyproject.toml")
126
+ return match[1]
127
+
128
+
129
+ def build(name, out_root):
130
+ if name not in WRAPPERS:
131
+ raise SystemExit(f"unknown wrapper package: {name}")
132
+ spec = WRAPPERS[name]
133
+ values = {
134
+ "name": name, "module": name.replace("-", "_"), "version": core_version(), "core": CORE,
135
+ "repository": REPOSITORY, **spec,
136
+ }
137
+ values["keywords"] = ", ".join(f'"{word}"' for word in spec["keywords"])
138
+ values["extras"] = f"[{spec['extra']}]" if spec.get("extra") else ""
139
+ out = out_root / name
140
+ if out.exists():
141
+ shutil.rmtree(out)
142
+ package = out / values["module"]
143
+ package.mkdir(parents=True)
144
+ (out / "pyproject.toml").write_text(PYPROJECT.format(**values))
145
+ (out / "README.md").write_text(README.format(**values))
146
+ (package / "__init__.py").write_text(INIT.format(**values))
147
+ (package / "__main__.py").write_text(MAIN)
148
+ shutil.copy(ROOT / "LICENSE", out / "LICENSE")
149
+ return out
150
+
151
+
152
+ def main(argv):
153
+ out_root = Path(argv[0]) if argv else ROOT / "dist-wrappers"
154
+ for name in argv[1:] or WRAPPERS:
155
+ print(build(name, out_root))
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main(sys.argv[1:])