@maci0/dsh-quota-check 0.12.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-quota-check contributors
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 ADDED
@@ -0,0 +1,220 @@
1
+ # dsh-quota-check
2
+
3
+ One statusbar figure for the provider the current session is using: the remaining
4
+ balance on a pure API-billing route, or the plan quota on a subscription route.
5
+ The chip sits in the composer statusbar (the row under the composer card), flush
6
+ right so it lands directly beneath the model selector, and the provider key never
7
+ leaves the host process.
8
+
9
+ ## What you get
10
+
11
+ - **One statusbar figure, no provider setup.** The chip follows the session's own
12
+ model selection; nothing has to be pointed at a provider. The row's own
13
+ cadences and deadline are edited from the **Plugins** page's card (see
14
+ [Configure](#configure)).
15
+ - **The key never reaches the browser.** The host half owns the credential and the
16
+ outbound request, and the browser half only draws the formatted text it returns.
17
+ - **One read per provider, cached.** A rerender, a session switch, or a second tab
18
+ shares one cached reading instead of multiplying provider traffic.
19
+ - **Subscription plans are read where the CLI left them.** Claude, Codex, Grok, and
20
+ Cursor meters come from the credentials already on this machine, and a rotated
21
+ token is written back the way the CLI writes it.
22
+
23
+ ## Install
24
+
25
+ > **Install it as a bundle.** `dsh plugin add …` mounts the row from the
26
+ > package's own patch layer, which is what the settings editor can write to. A
27
+ > row added with `--patch` is an overlay: it disappears at the next start, and
28
+ > the Plugins card cannot save into it (the editor refuses a write an overlay
29
+ > would win).
30
+
31
+ ```sh
32
+ dsh plugin --profile web add @maci0/dsh-quota-check@0.12.2
33
+ ```
34
+
35
+ This installs the public npm package; no GitHub token or `~/.secrets` setup is needed.
36
+ The version is pinned. To upgrade, run the same command with a newer version,
37
+ then restart `dsh web` (bundle layers compose at boot).
38
+
39
+ Do **not** also paste the `id: quota-check` row from `cordis.patch.yml` into the
40
+ profile's own patch: `insert` does not dedupe ids.
41
+
42
+ ## Configure
43
+
44
+ Three fields, all editable from the Web client: open **Plugins**, then the
45
+ **quota-check** row, then **Configure**. The card validates the schema's bounds
46
+ before the write (an emptied box is refused, not saved as `0`), saves every
47
+ changed field in one update, and marks the fields you have overridden with a
48
+ **Reset to defaults** control. Every field is `volatile()`, so a save reaches the
49
+ running route: the host re-reads the row per request and drops its served
50
+ readings, which is why a new cache window or cadence applies to the next poll
51
+ instead of waiting for a restart.
52
+
53
+ The same row can be set by hand in the profile's own `cordis.patch.yml`:
54
+
55
+ ```yaml
56
+ - id: quota-check
57
+ config:
58
+ cacheSeconds: 60 # seconds one reading is served; 0 re-asks every time
59
+ timeoutMs: 10000 # per-provider request deadline
60
+ refreshSeconds: 300 # browser re-read cadence; reported to the chip
61
+ ```
62
+
63
+ | Field | Default | Bounds | Meaning |
64
+ |---|---|---|---|
65
+ | `cacheSeconds` | `60` | 0–3600 | How long one reading is served before the provider is asked again. `0` asks every time. |
66
+ | `timeoutMs` | `10000` | 1–60000 | Per-request deadline for one provider call, including a subscription token refresh. |
67
+ | `refreshSeconds` | `300` | 10–3600 | The cadence the host reports to the chip, which re-reads at that interval. |
68
+
69
+ ## The chip
70
+
71
+ It reads:
72
+
73
+ - **`$91.81`**: DeepSeek API balance (granted / topped-up breakdown in the tooltip)
74
+ - **`$12.34`**: OpenRouter remaining credits (bought / used in the tooltip)
75
+ - **`GLM 27%`**: z.ai / BigModel Coding Plan quota, fullest window (5h, weekly, and
76
+ MCP windows with their reset times in the tooltip)
77
+ - **`$8.75`**: LiteLLM key budget (`/key/info`: budget, spend, remaining), which is
78
+ also the fallback shape tried for any other route that has a base URL
79
+ - **`Go 32%`**: OpenCode Go subscription quota, lowest remaining window. The
80
+ tooltip lists the rolling 5-hour, weekly, and monthly usage with reset times.
81
+ Uses the route's configured API key (or `OPENCODE_GO_API_KEY` /
82
+ `OPENCODE_API_KEY`) to read `https://opencode.ai/zen/go/v1/usage`.
83
+ Recognized by an `opencode-go` route id with no base URL, or a base URL at
84
+ `https://opencode.ai/zen/go/…`; Zen pay-as-you-go routes are separate.
85
+ - **`Claude 100%`**, **`Codex 32%`**, **`Grok 92%`**, **`Cursor 64%`**: the
86
+ subscription plans' own meters, read from the credential the CLI already left on
87
+ this machine: `~/.claude/.credentials.json`, `~/.codex/auth.json`,
88
+ `~/.grok/auth.json`, and Cursor's `~/.config/cursor/auth.json` or IDE database.
89
+ The tooltip lists every window (session, weekly, credits, on-demand) with its
90
+ reset time.
91
+ - **`OmniRoute 100%`**: the fullest window across the upstream connections an
92
+ OmniRoute deployment holds, read from OmniRoute's own `GET /api/providers` and
93
+ `GET /api/usage/<connectionId>`. The tooltip names each connection's plan and
94
+ its windows (`DeepSeek · Credits $91.27 left`, `SuperGrokPro · Weekly 7% used`,
95
+ each with its reset time).
96
+
97
+ Metered quota figures count **down**: the chip shows what is left, not what is
98
+ spent, and colors itself by headroom: green at ≥50% remaining, yellow at ≥25%,
99
+ orange at ≥10%, red below.
100
+
101
+ Any other provider renders nothing: the statusbar never grows a "no data" row.
102
+ That is the case for local inference (vLLM) and for per-request API billing with
103
+ no balance route.
104
+
105
+ A reading that **failed** (a rejected key, an unreachable endpoint, a provider
106
+ that answered with an error) is the one exception: the chip renders dimmed as
107
+ `quota ?` with the reason in its tooltip, because silence there would hide a
108
+ broken setup behind "nothing to show". Clicking it retries.
109
+
110
+ ## How it works
111
+
112
+ - **Host half** (`src/index.ts`, built to `lib/index.js`) serves
113
+ `GET /quota-check?provider=<id>`. It resolves the provider's profile from the
114
+ LLM registry plus the settings document, resolves the credential through
115
+ `ctx.credentials` (falling back to `process.env`), calls the
116
+ provider, and returns one JSON report. The provider key never leaves this
117
+ process; readings are cached for `cacheSeconds` per provider, so a rerender, a
118
+ session switch, or a second tab never multiplies provider traffic.
119
+ `?refresh=1` forces a fresh read. A `provider` that is not a route id (ASCII
120
+ letters, digits, `.`, `_` or `-`, starting with a letter or digit, at most 128
121
+ characters) is answered 400 before it keys the cache or reaches any lookup.
122
+ A settings write drops the cache, and a read
123
+ that was in flight during the write answers its caller but is neither cached
124
+ nor handed to later polls.
125
+ - **Browser half** (`lib/client.js`) reads `modelSelection` from the session's
126
+ projections, asks that route for the selected provider, and draws the returned
127
+ text in `conversation.composer.dock`. It is pushed to the right edge of the
128
+ dock and then pulled back by the composer card's own inset plus the send
129
+ control, so the figure sits under the model selector at any viewport width. A
130
+ click re-reads; otherwise the chip refreshes on the host's configured cadence.
131
+ After a provider switch the chip draws nothing until the new provider's
132
+ reading lands, so the previous figure never reads as the new one.
133
+ - **Probes** (`src/probes.ts`) are the pure rules: which endpoint answers for a
134
+ route, and how its payload becomes the chip text and tooltip lines. A route's
135
+ configured base URL decides when it names a known host; the provider id only
136
+ decides when no base URL is configured. A configured base URL that is not an
137
+ absolute http(s) URL (`box:20128`, `api.deepseek.com`) is reported as a
138
+ configuration error before any probe is chosen, so no origin is guessed and no
139
+ credential is resolved or sent. That distinction is what keeps
140
+ `google-vertex-anthropic` (host `*.googleapis.com`) away from the Claude Code
141
+ subscription credential while a bare `anthropic` route reads it. A route that
142
+ names no known vendor and still has a base URL is asked for a LiteLLM key
143
+ budget, which is how a proxy deployment gets recognized at all; a host without
144
+ that route answers 404 and renders no chip. OmniRoute is the one probe with two
145
+ rounds: it is recognized by route id, because the host is whatever machine runs
146
+ it, then asks `/api/providers` for the connection ids and `/api/usage/<id>` for
147
+ each connection that is live and publishes its quota (an id of `.` or `..` is
148
+ skipped, since no usage path can address it). A chip for an OmniRoute route is
149
+ therefore one listing plus one request per visible connection.
150
+ - **Local credentials** (`src/local-usage.ts`) are the port of the `quota-widget`
151
+ fetchers: file paths, expiry skews, refresh bodies, and the atomic 0600
152
+ write-back that keeps the CLI signed in. Only Claude, Codex, and Grok ever
153
+ rotate a token; Cursor's session token is long-lived and rotates in the IDE.
154
+ Every token request is bounded by `timeoutMs`, and a refresh that fails or
155
+ times out falls back to the token on disk. Grok's token endpoint is discovered
156
+ at runtime, and its two billing meters (weekly credits, monthly spend) are two
157
+ requests, so a probe may read more than one URL and still report when only one
158
+ answered.
159
+
160
+ Adding a key-based probe is one function in `src/probes.ts` plus a case in
161
+ `tests/probes.test.ts`; adding a subscription provider is that plus a credential
162
+ reader in `src/local-usage.ts`. A probe whose endpoint names ids the route
163
+ configuration cannot carry (OmniRoute's per-connection usage) declares
164
+ `requests` instead of `url`, and reads its own listing before the host asks each
165
+ id.
166
+
167
+ ## Security
168
+
169
+ The plugin injects the composition's trust fence (`connection`) and the route
170
+ asks `requestRejection` before anything else, so it answers only same-origin,
171
+ authenticated callers, exactly like the harness's own browser routes; a
172
+ composition without that service never mounts the route. Responses carry
173
+ formatted figures only: never a key, never a raw provider body, never a token. A
174
+ failure thrown inside another service (credentials, settings) is logged on the
175
+ host, and the report carries a fixed sentence instead of its text.
176
+
177
+ Subscription credentials stay on this machine: they are read from the CLI's own
178
+ files and sent only to the vendor that issued them (`api.anthropic.com`,
179
+ `chatgpt.com`, `cli-chat-proxy.grok.com`, `cursor.com`). A rotated token is
180
+ written back the way the CLI writes it (same file, same fields, mode 0600, one
181
+ atomic rename), so the plugin never signs a CLI out. The Claude usage request
182
+ deliberately wears Claude Code's own User-Agent, because Anthropic rate-limits
183
+ that endpoint per agent.
184
+
185
+ ## Limits
186
+
187
+ - **OmniRoute has no per-route figure.** The router holds the upstream accounts, so a quota is only knowable per connection. The chip shows the fullest window across them and the tooltip names each plan; which account one request spends is the router's decision, so the plugin does not attribute it to a DSH route. A read is one listing plus one request per visible connection (21 on the reference box), cached for `cacheSeconds`.
188
+ - **The subscription probes follow route identity.** A route must be named after the vendor or answer on the vendor's own host. A reseller that proxies Claude on `omniroute`'s host therefore shows nothing rather than the local Claude Code plan, and Vertex-hosted Claude (`google-vertex-anthropic`) is deliberately excluded.
189
+ - **A rotated token is written to the CLI's own file.** The plugin refreshes Claude, Codex, and Grok credentials and writes them back atomically at mode 0600. That keeps the CLI signed in, but it means this plugin is a writer in `~/.claude`, `~/.codex`, and `~/.grok`. Cursor's session token has no refresh path at all. Reads sharing a CLI credential are serialized within this process. Refresh write-back preserves edits made while the token request runs and skips a newer or removed login; separate CLI processes do not share a locking protocol.
190
+ - **Cursor credential reading needs `node:sqlite`.** The IDE database fallback imports it dynamically, so a runtime without that built-in reads only `~/.config/cursor/auth.json` and reports no Cursor chip from the database alone.
191
+
192
+ ## Development
193
+
194
+ dsh loads plugins on Node `^22.19.0 || >=24.0.0`; development and tests run on bun.
195
+
196
+ ```sh
197
+ bun install # first run only (typescript, @types/node, cordis, carrier)
198
+ bun run typecheck
199
+ bun test # probes, credentials, route, and the real-composition boot
200
+ bun run build # tsc -> lib/*.js
201
+ ```
202
+
203
+ For local development, `dsh plugin --profile <name> add <path-to-checkout>`
204
+ (after `bun run build`), then restart `dsh web`.
205
+
206
+ `bun test` includes the real-composition case: the plugin mounts into a real
207
+ Cordis `Context` beside the real HTTP carrier on an OS-assigned port, the route
208
+ is driven over real HTTP, disposing the fiber must withdraw it, and no route
209
+ exists until the trust fence is mounted.
210
+
211
+ Verify the route by hand once the plugin is mounted, from the same browser that
212
+ has the Web client open (the route is behind the session cookie):
213
+
214
+ ```
215
+ http://127.0.0.1:3080/quota-check?provider=deepseek-official
216
+ ```
217
+
218
+ ## Licence
219
+
220
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,13 @@
1
+ # The dsh-quota-check bundle patch: applied automatically when a profile lists
2
+ # this bundle (`dsh plugin add`/`update` appends the package to
3
+ # dsh.profile.bundles). Users override this row from their profile's own
4
+ # cordis.patch.yml (live-watched; dsh.profile.bundles is frozen at boot) with a
5
+ # `- id: quota-check` row, which replaces the row's whole `config`.
6
+ # `name` stays a bare package specifier: the browser half is served by the
7
+ # client module system, which resolves the Loader entry's package, reads its
8
+ # `dsh.client` manifest, and serves `exports["./client"]`.
9
+ # Do not also insert this same row into the profile patch: insert does not
10
+ # dedupe ids, and a second row would register the plugin twice.
11
+ - insert:
12
+ - id: quota-check
13
+ name: '@maci0/dsh-quota-check'
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="7" y="20" width="5" height="8" rx="1" fill="#9BE7C4"/>
3
+ <rect x="15.5" y="14" width="5" height="14" rx="1" fill="#3DDC97"/>
4
+ <rect x="24" y="8" width="5" height="20" rx="1" fill="#0F9F6E"/>
5
+ </svg>