quotacap 0.0.20 → 0.0.22
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/README.md +59 -18
- package/dist/adapters/agy.d.ts +3 -0
- package/dist/adapters/agy.js +81 -0
- package/dist/adapters/claude.d.ts +7 -5
- package/dist/adapters/claude.js +6 -6
- package/dist/adapters/codex.d.ts +3 -3
- package/dist/adapters/codex.js +128 -49
- package/dist/adapters/grok.d.ts +3 -3
- package/dist/adapters/grok.js +114 -70
- package/dist/adapters/index.d.ts +5 -2
- package/dist/adapters/index.js +36 -9
- package/dist/adapters/kimi.d.ts +3 -3
- package/dist/adapters/kimi.js +64 -86
- package/dist/adapters/manual.d.ts +4 -4
- package/dist/adapters/manual.js +2 -2
- package/dist/adapters/pty.d.ts +23 -0
- package/dist/adapters/pty.js +487 -0
- package/dist/adapters/types.d.ts +7 -3
- package/dist/advisory/engine.d.ts +19 -2
- package/dist/advisory/engine.js +130 -10
- package/dist/advisory/snapshot.d.ts +16 -0
- package/dist/advisory/snapshot.js +213 -0
- package/dist/advisory/types.d.ts +46 -4
- package/dist/advisory/validate.d.ts +5 -0
- package/dist/advisory/validate.js +18 -0
- package/dist/cli/advise.d.ts +3 -0
- package/dist/cli/advise.js +62 -0
- package/dist/cli/clients.d.ts +18 -0
- package/dist/cli/clients.js +11 -0
- package/dist/cli/index.js +4 -84
- package/dist/cli/ingest.d.ts +3 -0
- package/dist/cli/ingest.js +31 -0
- package/dist/cli/runtime.d.ts +12 -0
- package/dist/cli/runtime.js +125 -0
- package/dist/cli/snapshot-source.d.ts +25 -0
- package/dist/cli/snapshot-source.js +223 -0
- package/dist/cli/status.d.ts +3 -0
- package/dist/cli/status.js +84 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +126 -1
- package/dist/daemon.d.ts +4 -29
- package/dist/daemon.js +12 -67
- package/dist/format/markdown.d.ts +3 -0
- package/dist/format/markdown.js +24 -0
- package/dist/format/rows.d.ts +27 -0
- package/dist/format/rows.js +241 -0
- package/dist/format/terminal.d.ts +12 -0
- package/dist/format/terminal.js +144 -0
- package/dist/http/server.d.ts +24 -3
- package/dist/http/server.js +236 -106
- package/dist/mcp/server.d.ts +0 -6
- package/dist/mcp/server.js +78 -25
- package/dist/runtime/client.d.ts +24 -0
- package/dist/runtime/client.js +72 -0
- package/dist/runtime/owner.d.ts +35 -0
- package/dist/runtime/owner.js +393 -0
- package/dist/runtime/poll.d.ts +48 -0
- package/dist/runtime/poll.js +222 -0
- package/dist/runtime/service.d.ts +24 -0
- package/dist/runtime/service.js +251 -0
- package/dist/runtime/spawn.d.ts +17 -0
- package/dist/runtime/spawn.js +97 -0
- package/dist/runtime/token.d.ts +3 -0
- package/dist/runtime/token.js +64 -0
- package/dist/service/index.d.ts +4 -0
- package/dist/service/index.js +47 -0
- package/dist/service/macos.d.ts +96 -0
- package/dist/service/macos.js +551 -0
- package/dist/src/adapters/agy.d.ts +3 -0
- package/dist/src/adapters/agy.js +81 -0
- package/dist/src/adapters/claude.d.ts +7 -5
- package/dist/src/adapters/claude.js +6 -6
- package/dist/src/adapters/codex.d.ts +3 -3
- package/dist/src/adapters/codex.js +128 -49
- package/dist/src/adapters/grok.d.ts +3 -3
- package/dist/src/adapters/grok.js +114 -70
- package/dist/src/adapters/index.d.ts +5 -2
- package/dist/src/adapters/index.js +36 -9
- package/dist/src/adapters/kimi.d.ts +3 -3
- package/dist/src/adapters/kimi.js +64 -86
- package/dist/src/adapters/manual.d.ts +4 -4
- package/dist/src/adapters/manual.js +2 -2
- package/dist/src/adapters/pty.d.ts +23 -0
- package/dist/src/adapters/pty.js +487 -0
- package/dist/src/adapters/types.d.ts +7 -3
- package/dist/src/advisory/engine.d.ts +19 -2
- package/dist/src/advisory/engine.js +130 -10
- package/dist/src/advisory/snapshot.d.ts +16 -0
- package/dist/src/advisory/snapshot.js +213 -0
- package/dist/src/advisory/types.d.ts +46 -4
- package/dist/src/advisory/validate.d.ts +5 -0
- package/dist/src/advisory/validate.js +18 -0
- package/dist/src/cli/advise.d.ts +3 -0
- package/dist/src/cli/advise.js +62 -0
- package/dist/src/cli/clients.d.ts +18 -0
- package/dist/src/cli/clients.js +11 -0
- package/dist/src/cli/index.js +4 -84
- package/dist/src/cli/ingest.d.ts +3 -0
- package/dist/src/cli/ingest.js +31 -0
- package/dist/src/cli/runtime.d.ts +12 -0
- package/dist/src/cli/runtime.js +125 -0
- package/dist/src/cli/snapshot-source.d.ts +25 -0
- package/dist/src/cli/snapshot-source.js +223 -0
- package/dist/src/cli/status.d.ts +3 -0
- package/dist/src/cli/status.js +84 -0
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +126 -1
- package/dist/src/daemon.d.ts +4 -29
- package/dist/src/daemon.js +12 -67
- package/dist/src/format/markdown.d.ts +3 -0
- package/dist/src/format/markdown.js +24 -0
- package/dist/src/format/rows.d.ts +27 -0
- package/dist/src/format/rows.js +241 -0
- package/dist/src/format/terminal.d.ts +12 -0
- package/dist/src/format/terminal.js +144 -0
- package/dist/src/http/server.d.ts +24 -3
- package/dist/src/http/server.js +236 -106
- package/dist/src/mcp/server.d.ts +0 -6
- package/dist/src/mcp/server.js +78 -25
- package/dist/src/runtime/client.d.ts +24 -0
- package/dist/src/runtime/client.js +72 -0
- package/dist/src/runtime/owner.d.ts +35 -0
- package/dist/src/runtime/owner.js +393 -0
- package/dist/src/runtime/poll.d.ts +48 -0
- package/dist/src/runtime/poll.js +222 -0
- package/dist/src/runtime/service.d.ts +24 -0
- package/dist/src/runtime/service.js +251 -0
- package/dist/src/runtime/spawn.d.ts +17 -0
- package/dist/src/runtime/spawn.js +97 -0
- package/dist/src/runtime/token.d.ts +3 -0
- package/dist/src/runtime/token.js +64 -0
- package/dist/src/service/index.d.ts +4 -0
- package/dist/src/service/index.js +47 -0
- package/dist/src/service/macos.d.ts +96 -0
- package/dist/src/service/macos.js +551 -0
- package/dist/src/store/attempts.d.ts +12 -0
- package/dist/src/store/attempts.js +29 -0
- package/dist/src/store/db.d.ts +1 -1
- package/dist/src/store/db.js +58 -2
- package/dist/src/store/quotas.js +52 -10
- package/dist/src/version.js +1 -1
- package/dist/src/webAssets.js +1 -1
- package/dist/src/webHtml.js +1 -1
- package/dist/store/attempts.d.ts +12 -0
- package/dist/store/attempts.js +29 -0
- package/dist/store/db.d.ts +1 -1
- package/dist/store/db.js +58 -2
- package/dist/store/quotas.js +52 -10
- package/dist/tests/adapters/agy.test.js +337 -0
- package/dist/tests/adapters/claude.test.js +41 -1
- package/dist/tests/adapters/codex.test.js +231 -91
- package/dist/tests/adapters/credential-free.test.js +211 -0
- package/dist/tests/adapters/grok.test.js +239 -135
- package/dist/tests/adapters/kimi.test.js +219 -104
- package/dist/tests/adapters/pty.test.d.ts +1 -0
- package/dist/tests/adapters/pty.test.js +240 -0
- package/dist/tests/adapters/registry.test.js +5 -5
- package/dist/tests/advisory/engine.test.js +194 -6
- package/dist/tests/advisory/pace.test.d.ts +1 -0
- package/dist/tests/advisory/pace.test.js +60 -0
- package/dist/tests/advisory/snapshot.test.d.ts +1 -0
- package/dist/tests/advisory/snapshot.test.js +65 -0
- package/dist/tests/advisory/validate.test.d.ts +1 -0
- package/dist/tests/advisory/validate.test.js +19 -0
- package/dist/tests/cli/advise.test.d.ts +1 -0
- package/dist/tests/cli/advise.test.js +294 -0
- package/dist/tests/cli/cli.test.js +39 -19
- package/dist/tests/cli/helpers.d.ts +9 -0
- package/dist/tests/cli/helpers.js +93 -0
- package/dist/tests/cli/ingest.test.d.ts +1 -0
- package/dist/tests/cli/ingest.test.js +188 -0
- package/dist/tests/cli/parity.test.d.ts +1 -0
- package/dist/tests/cli/parity.test.js +225 -0
- package/dist/tests/cli/snapshot-source.test.d.ts +1 -0
- package/dist/tests/cli/snapshot-source.test.js +305 -0
- package/dist/tests/cli/status.test.d.ts +1 -0
- package/dist/tests/cli/status.test.js +423 -0
- package/dist/tests/daemon.test.js +140 -38
- package/dist/tests/fixtures/stable-state.d.ts +22 -0
- package/dist/tests/fixtures/stable-state.js +152 -0
- package/dist/tests/fixtures/stable-state.test.d.ts +1 -0
- package/dist/tests/fixtures/stable-state.test.js +17 -0
- package/dist/tests/format/markdown.test.d.ts +1 -0
- package/dist/tests/format/markdown.test.js +49 -0
- package/dist/tests/format/terminal.test.d.ts +1 -0
- package/dist/tests/format/terminal.test.js +476 -0
- package/dist/tests/http/api.test.js +224 -2
- package/dist/tests/integration.test.js +20 -7
- package/dist/tests/mcp/protocol.test.d.ts +1 -0
- package/dist/tests/mcp/protocol.test.js +134 -0
- package/dist/tests/mcp/quotas.test.js +38 -6
- package/dist/tests/mcp/tools.test.d.ts +1 -0
- package/dist/tests/mcp/tools.test.js +291 -0
- package/dist/tests/runtime/cancel.test.d.ts +1 -0
- package/dist/tests/runtime/cancel.test.js +264 -0
- package/dist/tests/runtime/cli.test.d.ts +1 -0
- package/dist/tests/runtime/cli.test.js +226 -0
- package/dist/tests/runtime/config.test.d.ts +1 -0
- package/dist/tests/runtime/config.test.js +144 -0
- package/dist/tests/runtime/http.test.d.ts +1 -0
- package/dist/tests/runtime/http.test.js +267 -0
- package/dist/tests/runtime/lifecycle.test.d.ts +1 -0
- package/dist/tests/runtime/lifecycle.test.js +303 -0
- package/dist/tests/runtime/ownership.test.d.ts +1 -0
- package/dist/tests/runtime/ownership.test.js +355 -0
- package/dist/tests/runtime/poll.test.d.ts +1 -0
- package/dist/tests/runtime/poll.test.js +410 -0
- package/dist/tests/runtime/status.test.d.ts +1 -0
- package/dist/tests/runtime/status.test.js +232 -0
- package/dist/tests/service/macos.test.d.ts +1 -0
- package/dist/tests/service/macos.test.js +349 -0
- package/dist/tests/store/attempts.test.d.ts +1 -0
- package/dist/tests/store/attempts.test.js +29 -0
- package/dist/tests/store/db.test.js +61 -6
- package/dist/tests/store/migration.test.d.ts +1 -0
- package/dist/tests/store/migration.test.js +41 -0
- package/dist/tests/web/advice-drawer.test.d.ts +1 -0
- package/dist/tests/web/advice-drawer.test.js +45 -0
- package/dist/tests/web/dashboard.e2e.d.ts +1 -0
- package/dist/tests/web/dashboard.e2e.js +670 -0
- package/dist/tests/web/header.test.d.ts +1 -0
- package/dist/tests/web/header.test.js +56 -0
- package/dist/tests/web/names.test.d.ts +1 -0
- package/dist/tests/web/names.test.js +19 -0
- package/dist/tests/web/onboarding.test.d.ts +1 -0
- package/dist/tests/web/onboarding.test.js +21 -0
- package/dist/tests/web/pacing.test.d.ts +1 -0
- package/dist/tests/web/pacing.test.js +62 -0
- package/dist/tests/web/playwright.config.d.ts +2 -0
- package/dist/tests/web/playwright.config.js +10 -0
- package/dist/tests/web/provider-drawer.test.d.ts +1 -0
- package/dist/tests/web/provider-drawer.test.js +68 -0
- package/dist/tests/web/recommendation.test.d.ts +1 -0
- package/dist/tests/web/recommendation.test.js +38 -0
- package/dist/tests/web/reset-rail.test.d.ts +1 -0
- package/dist/tests/web/reset-rail.test.js +63 -0
- package/dist/tests/web/responsive.test.d.ts +1 -0
- package/dist/tests/web/responsive.test.js +21 -0
- package/dist/tests/web/settings.test.d.ts +1 -0
- package/dist/tests/web/settings.test.js +32 -0
- package/dist/tests/web/state-catalog.test.d.ts +1 -0
- package/dist/tests/web/state-catalog.test.js +206 -0
- package/dist/tests/web/state.test.d.ts +1 -0
- package/dist/tests/web/state.test.js +27 -0
- package/dist/tests/web/stub-server.d.ts +37 -0
- package/dist/tests/web/stub-server.js +132 -0
- package/dist/tests/web/theme.test.d.ts +1 -0
- package/dist/tests/web/theme.test.js +12 -0
- package/dist/version.js +1 -1
- package/dist/web/src/App.d.ts +1 -0
- package/dist/web/src/App.js +106 -78
- package/dist/web/src/Theme.d.ts +11 -0
- package/dist/web/src/Theme.js +53 -0
- package/dist/web/src/api.d.ts +27 -2
- package/dist/web/src/api.js +102 -6
- package/dist/web/src/components/AdviceDrawer.d.ts +9 -0
- package/dist/web/src/components/AdviceDrawer.js +77 -0
- package/dist/web/src/components/FaultBanner.d.ts +17 -0
- package/dist/web/src/components/FaultBanner.js +60 -0
- package/dist/web/src/components/Header.d.ts +22 -0
- package/dist/web/src/components/Header.js +46 -0
- package/dist/web/src/components/PaceBar.d.ts +51 -0
- package/dist/web/src/components/PaceBar.js +251 -0
- package/dist/web/src/components/PacingLegend.d.ts +3 -0
- package/dist/web/src/components/PacingLegend.js +43 -0
- package/dist/web/src/components/ProviderCard.d.ts +8 -0
- package/dist/web/src/components/ProviderCard.js +27 -0
- package/dist/web/src/components/ProviderDrawer.d.ts +52 -0
- package/dist/web/src/components/ProviderDrawer.js +270 -0
- package/dist/web/src/components/ProviderIcon.d.ts +7 -0
- package/dist/web/src/components/ProviderIcon.js +27 -0
- package/dist/web/src/components/ProviderRow.d.ts +8 -0
- package/dist/web/src/components/ProviderRow.js +14 -0
- package/dist/web/src/components/Recommendation.d.ts +27 -0
- package/dist/web/src/components/Recommendation.js +77 -0
- package/dist/web/src/components/ResetRail.d.ts +38 -0
- package/dist/web/src/components/ResetRail.js +188 -0
- package/dist/web/src/components/SubscriptionList.d.ts +19 -0
- package/dist/web/src/components/SubscriptionList.js +107 -0
- package/dist/web/src/components/settings/CliIntegrationPanel.d.ts +2 -0
- package/dist/web/src/components/settings/CliIntegrationPanel.js +49 -0
- package/dist/web/src/components/settings/DaemonPanel.d.ts +6 -0
- package/dist/web/src/components/settings/DaemonPanel.js +45 -0
- package/dist/web/src/components/settings/ProvidersPanel.d.ts +14 -0
- package/dist/web/src/components/settings/ProvidersPanel.js +69 -0
- package/dist/web/src/components/settings/SettingsDrawer.d.ts +9 -0
- package/dist/web/src/components/settings/SettingsDrawer.js +55 -0
- package/dist/web/src/names.d.ts +3 -0
- package/dist/web/src/names.js +19 -0
- package/dist/web/src/pages/Onboarding.d.ts +17 -0
- package/dist/web/src/pages/Onboarding.js +96 -0
- package/dist/web/src/router.d.ts +11 -0
- package/dist/web/src/router.js +31 -0
- package/dist/web/src/state.d.ts +97 -0
- package/dist/web/src/state.js +80 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +28 -9
- package/scripts/fix-pty-perms.mjs +21 -0
- package/web/dist/assets/index-CyUkWdwJ.css +1 -0
- package/web/dist/assets/index-PN4RpMKG.js +12 -0
- package/web/dist/index.html +19 -2
- package/dist/adapters/core.d.ts +0 -9
- package/dist/adapters/core.js +0 -90
- package/dist/format/table.d.ts +0 -2
- package/dist/format/table.js +0 -56
- package/dist/src/adapters/core.d.ts +0 -9
- package/dist/src/adapters/core.js +0 -90
- package/dist/src/format/table.d.ts +0 -2
- package/dist/src/format/table.js +0 -56
- package/dist/tests/adapters/core.test.js +0 -110
- package/dist/tests/format/table.test.js +0 -98
- package/web/dist/assets/index-Dpiqzcc8.js +0 -40
- /package/dist/tests/adapters/{core.test.d.ts → agy.test.d.ts} +0 -0
- /package/dist/tests/{format/table.test.d.ts → adapters/credential-free.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# QuotaCap
|
|
2
2
|
|
|
3
|
-
QuotaCap helps you get more from the AI coding subscriptions you already pay for: Claude Code, Codex, Kimi Code, and
|
|
3
|
+
QuotaCap helps you get more from the AI coding subscriptions you already pay for: Claude Code, Codex, Kimi Code, Grok, and Antigravity. It tracks one current usage window and reset time for each plan, then estimates which plan to use next from remaining usage and recent pace when available.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/quotacap)
|
|
6
6
|
[](https://github.com/carlosboeing/quotacap/actions)
|
|
@@ -16,15 +16,17 @@ QuotaCap helps you get more from the AI coding subscriptions you already pay for
|
|
|
16
16
|
|
|
17
17
|
## Providers
|
|
18
18
|
|
|
19
|
-
| Provider | Source |
|
|
20
|
-
|
|
21
|
-
| Claude Code | Live |
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
19
|
+
| Provider | Mechanism | Source |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Claude Code | `exec` — `claude -p /usage --output-format json` | Live |
|
|
22
|
+
| Antigravity | `exec` — `agy -p /usage --output-format json` (two rows: `agy` Gemini, `agy:3p` 3p) | Live |
|
|
23
|
+
| Codex | `pty` — `codex --no-alt-screen` then `/status`, parse `Weekly/5h limit: X% left` | Live |
|
|
24
|
+
| Kimi Code | `pty` — `kimi` then `/usage`, parse `Weekly/5h limit: Y% used` | Live |
|
|
25
|
+
| Grok | `pty` — `grok` then `/usage`, parse `Weekly limit (plan)` + `Credits: $X` | Live |
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Exec adapters run via `execFile` with an argv list. PTY adapters run via `node-pty` (`src/adapters/pty.ts`). They are TUI-fragile: a vendor text change breaks the parser and the row degrades fail-closed until the regex is fixed. Poll latency is 2–10 s per PTY provider (settle plus completion). It dominates `POST /api/refresh` and the first poll, not the steady-state 15 m timer.
|
|
28
|
+
|
|
29
|
+
Live adapters invoke the CLIs you already logged into. No API keys. No token files are read.
|
|
28
30
|
|
|
29
31
|
## Install
|
|
30
32
|
|
|
@@ -36,7 +38,7 @@ curl -fsSL https://raw.githubusercontent.com/carlosboeing/quotacap/main/install.
|
|
|
36
38
|
npm install -g quotacap
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
The binary is a self-contained executable from GitHub Releases.
|
|
41
|
+
The binary is a self-contained executable from GitHub Releases (shipped as `quotacap-<os>-<arch>.tar.gz` with a `pty` sidecar for Kimi/Codex/Grok); `install.sh` handles the tarball and sidecar transparently.
|
|
40
42
|
The npm package runs the same CLI on Node. To run without a global install, replace `quotacap` with `npx quotacap` in any command, for example `npx quotacap web`.
|
|
41
43
|
|
|
42
44
|
## Quick start
|
|
@@ -53,12 +55,6 @@ quotacap advise
|
|
|
53
55
|
|
|
54
56
|
`web` stays in the foreground and starts the daemon.
|
|
55
57
|
|
|
56
|
-
For a provider without a live adapter:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
quotacap ingest --provider agy --text "65% used · resets Sep 1"
|
|
60
|
-
```
|
|
61
|
-
|
|
62
58
|
## MCP
|
|
63
59
|
|
|
64
60
|
```json
|
|
@@ -73,15 +69,51 @@ Use `"command": "npx", "args": ["quotacap", "mcp"]` when installed via npm only.
|
|
|
73
69
|
|
|
74
70
|
Tools: `get_quotas`, `get_recommendation`, `forecast`.
|
|
75
71
|
|
|
76
|
-
##
|
|
72
|
+
## HTTP API
|
|
73
|
+
|
|
74
|
+
The local web server listens on `127.0.0.1:8787` (configured via `QUOTACAP_URL` or `~/.quotacap/config.json`). All endpoints enforce loopback `Host` (`127.0.0.1`, `localhost`, `[::1]`) and reject foreign `Origin` headers (403) to prevent DNS rebinding and cross-origin requests.
|
|
75
|
+
|
|
76
|
+
| Method | Path | Auth | Description |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `GET` | `/health` | None | Health check and uptime |
|
|
79
|
+
| `GET` | `/api/quotas` | None | Current quotas for all providers |
|
|
80
|
+
| `GET` | `/api/recommendation` | None | Current advisory on which provider to use next |
|
|
81
|
+
| `GET` | `/api/token` | Same-origin | Shared secret token for the dashboard |
|
|
82
|
+
| `POST` | `/api/refresh` | `X-QuotaCap-Token` | Trigger an immediate adapter poll (debounced to 60s) |
|
|
83
|
+
|
|
84
|
+
### Curl examples
|
|
85
|
+
|
|
86
|
+
Read current quotas:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl http://localhost:8787/api/quotas
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Trigger an immediate refresh (requires the shared secret in `~/.quotacap/token` with mode `0600`):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -X POST http://localhost:8787/api/refresh \
|
|
96
|
+
-H "X-QuotaCap-Token: $(cat ~/.quotacap/token)"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Security
|
|
100
|
+
|
|
101
|
+
QuotaCap is a local daemon. It binds to `127.0.0.1` only (`src/cli/index.ts:56`). It does not listen on `0.0.0.0`. There is no LAN surface.
|
|
77
102
|
|
|
78
|
-
|
|
103
|
+
It owns no tokens. It never reads `~/.codex/auth.json`, `~/.kimi-code/credentials/kimi-code.json`, `~/.kimi/credentials/kimi-code.json`, `~/.grok/auth.json`, or `~/.gemini/oauth_creds.json`. It never uses `refresh_token` or `grant_type=refresh_token`. It has no hardcoded client ids. Those OAuth paths and the `.qc-bak` and `.qc-lock` helpers were removed in #14. This is asserted by `tests/adapters/credential-free.test.ts`. Each CLI owns its own session. QuotaCap only spawns the CLI and reads its stdout via `exec` (`claude`, `agy`) or PTY (`codex`, `kimi`, `grok`).
|
|
104
|
+
|
|
105
|
+
It stores no `raw` provider payload. The `raw` column was dropped and migrated in `src/store/db.ts:37-48`. `GET /api/quotas` and MCP `get_quotas` never return `raw` (`tests/http/api.test.ts`). History and the token live under `~/.quotacap/` with `0700` on the directory and `0600` on files.
|
|
106
|
+
|
|
107
|
+
Every request checks `Host` and `Origin`. `Host` must be loopback (`127.0.0.1`, `localhost`, `[::1]`), otherwise `403`. `Origin` when present must be loopback, otherwise `403`. Absent `Origin` passes for `curl` and MCP. `POST /api/refresh` requires `X-QuotaCap-Token` matching `~/.quotacap/token` with `crypto.timingSafeEqual` (`src/http/server.ts:isValidToken`), otherwise `401`. `GET /assets/*` is rooted with `path.resolve` and a prefix check (`tests/http/api.test.ts`).
|
|
108
|
+
|
|
109
|
+
Adapters fail closed. A timeout or unmatched TUI text becomes a stale row, never `0% used`. See `SECURITY.md` for the full "does and does not touch" list and how to report a vulnerability. See `docs/architecture.md` for the detailed security model.
|
|
79
110
|
|
|
80
111
|
## Docs
|
|
81
112
|
|
|
82
113
|
- [Architecture](docs/architecture.md)
|
|
83
114
|
- [Changelog](docs/CHANGELOG.md)
|
|
84
115
|
- [Roadmap](docs/ROADMAP.md)
|
|
116
|
+
- [Security](SECURITY.md)
|
|
85
117
|
|
|
86
118
|
## License
|
|
87
119
|
|
|
@@ -100,4 +132,13 @@ npm run build:bin
|
|
|
100
132
|
The store uses `node:sqlite` on Node and `bun:sqlite` on Bun.
|
|
101
133
|
The same code is an npm package and a compiled binary.
|
|
102
134
|
|
|
135
|
+
`node-pty` is a native addon for the PTY-based adapters (Kimi, Codex, Grok) and is an `optionalDependency`. Prebuilt binaries are provided where available (macOS and Linux). If no prebuild matches your Node version or platform, `npm install` compiles it from source — this requires Xcode (macOS) or `build-essential` + `python3` (Linux). If the compile fails the install still succeeds and exec-based adapters (Claude, Agy) continue to work; PTY adapters will report `node-pty not available` until you install the toolchain and run `npm rebuild node-pty`.
|
|
136
|
+
|
|
137
|
+
The Kimi adapter spawns `kimi` in your home directory so the quota modal does not depend on the project path. If Kimi shows `Trust this folder?`, QuotaCap fails closed with `untrusted workspace — run \`kimi\` there and select Trust this folder` and does not auto-trust; trust remains an explicit interactive decision.
|
|
138
|
+
|
|
139
|
+
The compiled binaries are built with `bun build --compile`.
|
|
140
|
+
Each `dist-bin/quotacap-<os>-<arch>.tar.gz` bundles a `pty` sidecar.
|
|
141
|
+
The sidecar is `pty/node-pty` with the matching prebuild.
|
|
142
|
+
`install.sh` verifies the tarball and installs the sidecar to `~/.local/bin/pty` and `~/.local/share/quotacap/pty`.
|
|
143
|
+
|
|
103
144
|
</details>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { trackedExecFile } from "../runtime/spawn.js";
|
|
2
|
+
function usedPctFromRemaining(remaining) {
|
|
3
|
+
if (typeof remaining !== "number" || !Number.isFinite(remaining))
|
|
4
|
+
return null;
|
|
5
|
+
const pct = Math.round((1 - remaining) * 100);
|
|
6
|
+
if (!Number.isFinite(pct) || pct < 0 || pct > 100)
|
|
7
|
+
return null;
|
|
8
|
+
return pct;
|
|
9
|
+
}
|
|
10
|
+
function parseGroup(g, provider, rawJsonString, now) {
|
|
11
|
+
if (!Array.isArray(g.buckets))
|
|
12
|
+
return null;
|
|
13
|
+
const weekly = g.buckets.find((b) => b.window === "weekly");
|
|
14
|
+
if (!weekly)
|
|
15
|
+
return null;
|
|
16
|
+
const usedPct = usedPctFromRemaining(weekly.remaining_fraction);
|
|
17
|
+
if (usedPct === null)
|
|
18
|
+
return null;
|
|
19
|
+
const rawReset = weekly.reset_time;
|
|
20
|
+
const resetDate = rawReset ? new Date(rawReset) : null;
|
|
21
|
+
if (!resetDate || Number.isNaN(resetDate.getTime()))
|
|
22
|
+
throw new Error("agy: bad weekly reset_time");
|
|
23
|
+
const resetsAt = resetDate.toISOString();
|
|
24
|
+
const periodStart = new Date(resetDate.getTime() - 7 * 86400000).toISOString();
|
|
25
|
+
const fiveHour = g.buckets.find((b) => b.window === "5h");
|
|
26
|
+
const sessionPct = fiveHour
|
|
27
|
+
? usedPctFromRemaining(fiveHour.remaining_fraction) ?? undefined
|
|
28
|
+
: undefined;
|
|
29
|
+
return {
|
|
30
|
+
provider,
|
|
31
|
+
plan: "unknown",
|
|
32
|
+
usedPct,
|
|
33
|
+
sessionPct,
|
|
34
|
+
resetsAt,
|
|
35
|
+
periodStart,
|
|
36
|
+
source: "cli",
|
|
37
|
+
fetchedAt: now.toISOString(),
|
|
38
|
+
raw: rawJsonString,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function parseAgyUsage(parsedJson, now = new Date()) {
|
|
42
|
+
if (parsedJson?.status !== "SUCCESS")
|
|
43
|
+
throw new Error("agy: status is not SUCCESS");
|
|
44
|
+
const groups = parsedJson?.command?.data?.groups;
|
|
45
|
+
if (!Array.isArray(groups))
|
|
46
|
+
throw new Error("agy: no usage groups");
|
|
47
|
+
const raw = JSON.stringify(parsedJson);
|
|
48
|
+
const rows = [];
|
|
49
|
+
const geminiGroup = groups.find((g) => /gemini/i.test(g.name ?? "") || g.buckets?.some((b) => b.id?.startsWith("gemini")));
|
|
50
|
+
if (geminiGroup) {
|
|
51
|
+
const q = parseGroup(geminiGroup, "agy", raw, now);
|
|
52
|
+
if (q)
|
|
53
|
+
rows.push(q);
|
|
54
|
+
}
|
|
55
|
+
const threePGroup = groups.find((g) => /claude|gpt|3p/i.test(g.name ?? "") || g.buckets?.some((b) => b.id?.startsWith("3p")));
|
|
56
|
+
if (threePGroup) {
|
|
57
|
+
const q = parseGroup(threePGroup, "agy:3p", raw, now);
|
|
58
|
+
if (q)
|
|
59
|
+
rows.push(q);
|
|
60
|
+
}
|
|
61
|
+
if (rows.length === 0) {
|
|
62
|
+
for (const g of groups) {
|
|
63
|
+
const q = parseGroup(g, "agy", raw, now);
|
|
64
|
+
if (q) {
|
|
65
|
+
rows.push(q);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (rows.length === 0)
|
|
71
|
+
throw new Error("agy: no weekly bucket");
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
export const agyAdapter = {
|
|
75
|
+
id: "agy",
|
|
76
|
+
requiresAuth: "agy login (CLI owns credentials)",
|
|
77
|
+
async poll() {
|
|
78
|
+
const { stdout } = await trackedExecFile("agy", "agy", ["-p", "/usage", "--output-format", "json"], { timeout: 20000 });
|
|
79
|
+
return parseAgyUsage(JSON.parse(stdout));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function parseClaudeUsage(result: string, now?: Date):
|
|
3
|
-
export
|
|
1
|
+
import type { ParsedQuota } from "./types.js";
|
|
2
|
+
export declare function parseClaudeUsage(result: string, now?: Date): ParsedQuota;
|
|
3
|
+
export interface ClaudeAdapter {
|
|
4
4
|
id: string;
|
|
5
5
|
requiresAuth: string;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
execPath: string;
|
|
7
|
+
poll(execPath?: string): Promise<ParsedQuota>;
|
|
8
|
+
}
|
|
9
|
+
export declare const claudeAdapter: ClaudeAdapter;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { trackedExecFile } from "../runtime/spawn.js";
|
|
3
2
|
import { parseResetText } from "./parse.js";
|
|
4
|
-
const exec = promisify(execFile);
|
|
5
3
|
export function parseClaudeUsage(result, now = new Date()) {
|
|
6
4
|
const sessionMatch = result.match(/Current session:\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
7
5
|
const weeklyMatch = result.match(/Current week \(all models\):\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
@@ -15,14 +13,16 @@ export function parseClaudeUsage(result, now = new Date()) {
|
|
|
15
13
|
const periodStart = (parsedReset ? new Date(resets - 7 * 86400000) : new Date(now.getTime() - 7 * 86400000)).toISOString();
|
|
16
14
|
return {
|
|
17
15
|
provider: "claude", plan: "max", usedPct, sessionPct,
|
|
18
|
-
resetsAt, periodStart,
|
|
16
|
+
resetsAt, periodStart, source: "cli", fetchedAt: now.toISOString(), raw: result,
|
|
19
17
|
};
|
|
20
18
|
}
|
|
21
19
|
export const claudeAdapter = {
|
|
22
20
|
id: "claude",
|
|
23
21
|
requiresAuth: "keychain:Claude Code-credentials",
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
execPath: "claude",
|
|
23
|
+
async poll(execPath) {
|
|
24
|
+
const bin = execPath ?? claudeAdapter.execPath ?? "claude";
|
|
25
|
+
const { stdout } = await trackedExecFile("claude", bin, ["-p", "/usage", "--output-format", "json"], { timeout: 8000 });
|
|
26
26
|
const parsed = JSON.parse(stdout);
|
|
27
27
|
const result = parsed.result ?? stdout;
|
|
28
28
|
return parseClaudeUsage(result);
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { ParsedQuota } from "./types.js";
|
|
2
|
+
export declare function parseCodexTui(text: string, now?: Date): ParsedQuota;
|
|
3
3
|
export declare const codexAdapter: {
|
|
4
4
|
id: string;
|
|
5
5
|
requiresAuth: string;
|
|
6
|
-
poll(): Promise<
|
|
6
|
+
poll(): Promise<ParsedQuota>;
|
|
7
7
|
};
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1,61 +1,140 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
2
|
+
import { runPty, stripAnsi } from "./pty.js";
|
|
3
|
+
import { adapterSignal } from "../runtime/spawn.js";
|
|
4
|
+
const MONTHS = {
|
|
5
|
+
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
|
|
6
|
+
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
|
|
7
|
+
};
|
|
8
|
+
function parseWeeklyReset(raw, now) {
|
|
9
|
+
const m = raw.trim().match(/^(\d{1,2}):(\d{2})\s+on\s+(\d{1,2})\s+([A-Za-z]{3,9})$/);
|
|
10
|
+
if (!m)
|
|
11
|
+
return null;
|
|
12
|
+
const hours = parseInt(m[1], 10);
|
|
13
|
+
const mins = parseInt(m[2], 10);
|
|
14
|
+
const day = parseInt(m[3], 10);
|
|
15
|
+
const monStr = m[4].slice(0, 3).toLowerCase();
|
|
16
|
+
const month = MONTHS[monStr];
|
|
17
|
+
if (month === undefined || !Number.isFinite(hours) || !Number.isFinite(mins) || !Number.isFinite(day))
|
|
18
|
+
return null;
|
|
19
|
+
if (hours < 0 || hours > 23 || mins < 0 || mins > 59 || day < 1 || day > 31)
|
|
20
|
+
return null;
|
|
21
|
+
let year = now.getFullYear();
|
|
22
|
+
let dt = new Date(year, month, day, hours, mins, 0, 0);
|
|
23
|
+
if (Number.isNaN(dt.getTime()))
|
|
24
|
+
return null;
|
|
25
|
+
if (dt.getTime() < now.getTime()) {
|
|
26
|
+
dt = new Date(year + 1, month, day, hours, mins, 0, 0);
|
|
27
|
+
if (Number.isNaN(dt.getTime()))
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (dt.getTime() < now.getTime())
|
|
31
|
+
return null;
|
|
32
|
+
return dt.toISOString();
|
|
33
|
+
}
|
|
34
|
+
function parseFiveReset(raw, now) {
|
|
35
|
+
const m = raw.trim().match(/^(\d{1,2}):(\d{2})$/);
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
const hours = parseInt(m[1], 10);
|
|
39
|
+
const mins = parseInt(m[2], 10);
|
|
40
|
+
if (hours < 0 || hours > 23 || mins < 0 || mins > 59)
|
|
41
|
+
return null;
|
|
42
|
+
let dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, mins, 0, 0);
|
|
43
|
+
if (dt.getTime() < now.getTime()) {
|
|
44
|
+
dt = new Date(dt.getTime() + 86400000);
|
|
45
|
+
}
|
|
46
|
+
return dt.toISOString();
|
|
47
|
+
}
|
|
48
|
+
export function parseCodexTui(text, now = new Date()) {
|
|
49
|
+
const cleaned = stripAnsi(text);
|
|
50
|
+
let weeklyLeft = null;
|
|
51
|
+
let fiveLeft = null;
|
|
52
|
+
let weeklyRaw = null;
|
|
53
|
+
let fiveRaw = null;
|
|
54
|
+
const weeklyRe = /Weekly limit:\s*(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
55
|
+
const fiveRe = /5h limit:\s*(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
56
|
+
const w = cleaned.match(weeklyRe);
|
|
57
|
+
const f = cleaned.match(fiveRe);
|
|
58
|
+
if (w) {
|
|
59
|
+
weeklyLeft = parseInt(w[1], 10);
|
|
60
|
+
weeklyRaw = w[2].trim();
|
|
61
|
+
}
|
|
62
|
+
if (f) {
|
|
63
|
+
fiveLeft = parseInt(f[1], 10);
|
|
64
|
+
fiveRaw = f[2].trim();
|
|
65
|
+
}
|
|
66
|
+
if (weeklyLeft === null) {
|
|
67
|
+
const m = cleaned.match(/weekly[^\d%]*(\d+)%\s*left/i);
|
|
68
|
+
if (m)
|
|
69
|
+
weeklyLeft = parseInt(m[1], 10);
|
|
70
|
+
}
|
|
71
|
+
if (fiveLeft === null) {
|
|
72
|
+
const m = cleaned.match(/5h[^\d%]*(\d+)%\s*left/i);
|
|
73
|
+
if (m)
|
|
74
|
+
fiveLeft = parseInt(m[1], 10);
|
|
75
|
+
}
|
|
76
|
+
if (weeklyLeft === null)
|
|
77
|
+
throw new Error("codex: weekly limit not found in TUI output");
|
|
78
|
+
if (fiveLeft === null)
|
|
79
|
+
throw new Error("codex: 5h limit not found in TUI output");
|
|
80
|
+
if (!Number.isFinite(weeklyLeft) || weeklyLeft < 0 || weeklyLeft > 100)
|
|
81
|
+
throw new Error("codex: bad weekly pct");
|
|
82
|
+
if (!Number.isFinite(fiveLeft) || fiveLeft < 0 || fiveLeft > 100)
|
|
83
|
+
throw new Error("codex: bad 5h pct");
|
|
84
|
+
let weeklyIso = null;
|
|
85
|
+
let fiveIso = null;
|
|
86
|
+
let estimated = false;
|
|
87
|
+
if (weeklyRaw) {
|
|
88
|
+
weeklyIso = parseWeeklyReset(weeklyRaw, now);
|
|
89
|
+
if (!weeklyIso)
|
|
90
|
+
throw new Error(`codex: bad weekly reset "${weeklyRaw}"`);
|
|
91
|
+
}
|
|
92
|
+
if (fiveRaw) {
|
|
93
|
+
fiveIso = parseFiveReset(fiveRaw, now);
|
|
94
|
+
if (!fiveIso)
|
|
95
|
+
throw new Error(`codex: bad 5h reset "${fiveRaw}"`);
|
|
96
|
+
}
|
|
97
|
+
if (!weeklyIso) {
|
|
98
|
+
weeklyIso = new Date(now.getTime() + 7 * 86400000).toISOString();
|
|
99
|
+
estimated = true;
|
|
100
|
+
}
|
|
101
|
+
if (!fiveIso)
|
|
102
|
+
fiveIso = new Date(now.getTime() + 5 * 3600000).toISOString();
|
|
103
|
+
const usedPct = 100 - weeklyLeft;
|
|
104
|
+
const sessionPct = 100 - fiveLeft;
|
|
105
|
+
const periodStart = new Date(new Date(weeklyIso).getTime() - 7 * 86400000).toISOString();
|
|
11
106
|
return {
|
|
12
107
|
provider: "codex",
|
|
13
|
-
plan:
|
|
14
|
-
usedPct
|
|
15
|
-
|
|
108
|
+
plan: "unknown",
|
|
109
|
+
usedPct,
|
|
110
|
+
sessionPct,
|
|
111
|
+
resetsAt: weeklyIso,
|
|
16
112
|
periodStart,
|
|
17
|
-
|
|
18
|
-
source: "api",
|
|
113
|
+
source: "tui",
|
|
19
114
|
fetchedAt: now.toISOString(),
|
|
115
|
+
resetsAtEstimated: estimated || undefined,
|
|
116
|
+
raw: cleaned.slice(0, 4096),
|
|
20
117
|
};
|
|
21
118
|
}
|
|
22
|
-
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
23
|
-
const REFRESH_URL = "https://auth.openai.com/oauth/token";
|
|
24
|
-
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
25
|
-
function codexHome() {
|
|
26
|
-
return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
27
|
-
}
|
|
28
119
|
export const codexAdapter = {
|
|
29
120
|
id: "codex",
|
|
30
|
-
requiresAuth: "
|
|
121
|
+
requiresAuth: "codex login (CLI owns credentials)",
|
|
31
122
|
async poll() {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw e;
|
|
49
|
-
const tok = await postForm(REFRESH_URL, { grant_type: "refresh_token", client_id: CLIENT_ID, refresh_token: rf });
|
|
50
|
-
await persistCreds(authFile, (cur) => {
|
|
51
|
-
const tokens = { ...(cur.tokens ?? {}), };
|
|
52
|
-
if (tok.access_token)
|
|
53
|
-
tokens.access_token = tok.access_token;
|
|
54
|
-
if (tok.refresh_token)
|
|
55
|
-
tokens.refresh_token = tok.refresh_token;
|
|
56
|
-
return { ...cur, tokens, last_refresh: new Date().toISOString() };
|
|
57
|
-
});
|
|
58
|
-
return parseCodexUsage(await getJson(USAGE_URL, { ...headers, Authorization: `Bearer ${tok.access_token}` }));
|
|
59
|
-
}
|
|
123
|
+
const transcript = await runPty({
|
|
124
|
+
file: "codex",
|
|
125
|
+
args: ["--no-alt-screen"],
|
|
126
|
+
cwd: os.homedir(),
|
|
127
|
+
cols: 140,
|
|
128
|
+
rows: 50,
|
|
129
|
+
settleDelayMs: 2000,
|
|
130
|
+
input: "/status\r",
|
|
131
|
+
completionRegex: /\d+%\s*left/i,
|
|
132
|
+
abortOn: /Do you trust|Trust.*folder|trust the files in this folder/i,
|
|
133
|
+
timeoutMs: 12000,
|
|
134
|
+
maxBytes: 256 * 1024,
|
|
135
|
+
signal: adapterSignal("codex"),
|
|
136
|
+
label: "codex",
|
|
137
|
+
});
|
|
138
|
+
return parseCodexTui(transcript);
|
|
60
139
|
},
|
|
61
140
|
};
|
package/dist/adapters/grok.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { ParsedQuota } from "./types.js";
|
|
2
|
+
export declare function parseGrokTui(text: string, now?: Date): ParsedQuota;
|
|
3
3
|
export declare const grokAdapter: {
|
|
4
4
|
id: string;
|
|
5
5
|
requiresAuth: string;
|
|
6
|
-
poll(): Promise<
|
|
6
|
+
poll(): Promise<ParsedQuota>;
|
|
7
7
|
};
|