@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 +21 -0
- package/README.md +220 -0
- package/cordis.patch.yml +13 -0
- package/icon.svg +5 -0
- package/lib/client.js +471 -0
- package/lib/host.js +12 -0
- package/lib/index.js +469 -0
- package/lib/local-usage.js +453 -0
- package/lib/probes.js +848 -0
- package/lib/types/host.d.ts +128 -0
- package/lib/types/index.d.ts +113 -0
- package/lib/types/local-usage.d.ts +45 -0
- package/lib/types/probes.d.ts +126 -0
- package/lib/types/util.d.ts +14 -0
- package/lib/util.js +28 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +104 -0
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).
|
package/cordis.patch.yml
ADDED
|
@@ -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>
|