quotacap 0.0.19 → 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.
Files changed (315) hide show
  1. package/README.md +93 -68
  2. package/dist/adapters/agy.d.ts +3 -0
  3. package/dist/adapters/agy.js +81 -0
  4. package/dist/adapters/claude.d.ts +7 -5
  5. package/dist/adapters/claude.js +6 -6
  6. package/dist/adapters/codex.d.ts +3 -3
  7. package/dist/adapters/codex.js +128 -49
  8. package/dist/adapters/grok.d.ts +3 -3
  9. package/dist/adapters/grok.js +114 -70
  10. package/dist/adapters/index.d.ts +5 -2
  11. package/dist/adapters/index.js +36 -9
  12. package/dist/adapters/kimi.d.ts +3 -3
  13. package/dist/adapters/kimi.js +64 -86
  14. package/dist/adapters/manual.d.ts +4 -4
  15. package/dist/adapters/manual.js +2 -2
  16. package/dist/adapters/pty.d.ts +23 -0
  17. package/dist/adapters/pty.js +487 -0
  18. package/dist/adapters/types.d.ts +7 -3
  19. package/dist/advisory/engine.d.ts +19 -2
  20. package/dist/advisory/engine.js +130 -10
  21. package/dist/advisory/snapshot.d.ts +16 -0
  22. package/dist/advisory/snapshot.js +213 -0
  23. package/dist/advisory/types.d.ts +46 -4
  24. package/dist/advisory/validate.d.ts +5 -0
  25. package/dist/advisory/validate.js +18 -0
  26. package/dist/cli/advise.d.ts +3 -0
  27. package/dist/cli/advise.js +62 -0
  28. package/dist/cli/clients.d.ts +18 -0
  29. package/dist/cli/clients.js +11 -0
  30. package/dist/cli/index.js +4 -84
  31. package/dist/cli/ingest.d.ts +3 -0
  32. package/dist/cli/ingest.js +31 -0
  33. package/dist/cli/runtime.d.ts +12 -0
  34. package/dist/cli/runtime.js +125 -0
  35. package/dist/cli/snapshot-source.d.ts +25 -0
  36. package/dist/cli/snapshot-source.js +223 -0
  37. package/dist/cli/status.d.ts +3 -0
  38. package/dist/cli/status.js +84 -0
  39. package/dist/config.d.ts +20 -0
  40. package/dist/config.js +126 -1
  41. package/dist/daemon.d.ts +4 -29
  42. package/dist/daemon.js +12 -67
  43. package/dist/format/markdown.d.ts +3 -0
  44. package/dist/format/markdown.js +24 -0
  45. package/dist/format/rows.d.ts +27 -0
  46. package/dist/format/rows.js +241 -0
  47. package/dist/format/terminal.d.ts +12 -0
  48. package/dist/format/terminal.js +144 -0
  49. package/dist/http/server.d.ts +24 -3
  50. package/dist/http/server.js +236 -106
  51. package/dist/mcp/server.d.ts +0 -6
  52. package/dist/mcp/server.js +78 -25
  53. package/dist/runtime/client.d.ts +24 -0
  54. package/dist/runtime/client.js +72 -0
  55. package/dist/runtime/owner.d.ts +35 -0
  56. package/dist/runtime/owner.js +393 -0
  57. package/dist/runtime/poll.d.ts +48 -0
  58. package/dist/runtime/poll.js +222 -0
  59. package/dist/runtime/service.d.ts +24 -0
  60. package/dist/runtime/service.js +251 -0
  61. package/dist/runtime/spawn.d.ts +17 -0
  62. package/dist/runtime/spawn.js +97 -0
  63. package/dist/runtime/token.d.ts +3 -0
  64. package/dist/runtime/token.js +64 -0
  65. package/dist/service/index.d.ts +4 -0
  66. package/dist/service/index.js +47 -0
  67. package/dist/service/macos.d.ts +96 -0
  68. package/dist/service/macos.js +551 -0
  69. package/dist/src/adapters/agy.d.ts +3 -0
  70. package/dist/src/adapters/agy.js +81 -0
  71. package/dist/src/adapters/claude.d.ts +7 -5
  72. package/dist/src/adapters/claude.js +6 -6
  73. package/dist/src/adapters/codex.d.ts +3 -3
  74. package/dist/src/adapters/codex.js +128 -49
  75. package/dist/src/adapters/grok.d.ts +3 -3
  76. package/dist/src/adapters/grok.js +114 -70
  77. package/dist/src/adapters/index.d.ts +5 -2
  78. package/dist/src/adapters/index.js +36 -9
  79. package/dist/src/adapters/kimi.d.ts +3 -3
  80. package/dist/src/adapters/kimi.js +64 -86
  81. package/dist/src/adapters/manual.d.ts +4 -4
  82. package/dist/src/adapters/manual.js +2 -2
  83. package/dist/src/adapters/pty.d.ts +23 -0
  84. package/dist/src/adapters/pty.js +487 -0
  85. package/dist/src/adapters/types.d.ts +7 -3
  86. package/dist/src/advisory/engine.d.ts +19 -2
  87. package/dist/src/advisory/engine.js +130 -10
  88. package/dist/src/advisory/snapshot.d.ts +16 -0
  89. package/dist/src/advisory/snapshot.js +213 -0
  90. package/dist/src/advisory/types.d.ts +46 -4
  91. package/dist/src/advisory/validate.d.ts +5 -0
  92. package/dist/src/advisory/validate.js +18 -0
  93. package/dist/src/cli/advise.d.ts +3 -0
  94. package/dist/src/cli/advise.js +62 -0
  95. package/dist/src/cli/clients.d.ts +18 -0
  96. package/dist/src/cli/clients.js +11 -0
  97. package/dist/src/cli/index.js +4 -84
  98. package/dist/src/cli/ingest.d.ts +3 -0
  99. package/dist/src/cli/ingest.js +31 -0
  100. package/dist/src/cli/runtime.d.ts +12 -0
  101. package/dist/src/cli/runtime.js +125 -0
  102. package/dist/src/cli/snapshot-source.d.ts +25 -0
  103. package/dist/src/cli/snapshot-source.js +223 -0
  104. package/dist/src/cli/status.d.ts +3 -0
  105. package/dist/src/cli/status.js +84 -0
  106. package/dist/src/config.d.ts +20 -0
  107. package/dist/src/config.js +126 -1
  108. package/dist/src/daemon.d.ts +4 -29
  109. package/dist/src/daemon.js +12 -67
  110. package/dist/src/format/markdown.d.ts +3 -0
  111. package/dist/src/format/markdown.js +24 -0
  112. package/dist/src/format/rows.d.ts +27 -0
  113. package/dist/src/format/rows.js +241 -0
  114. package/dist/src/format/terminal.d.ts +12 -0
  115. package/dist/src/format/terminal.js +144 -0
  116. package/dist/src/http/server.d.ts +24 -3
  117. package/dist/src/http/server.js +236 -106
  118. package/dist/src/mcp/server.d.ts +0 -6
  119. package/dist/src/mcp/server.js +78 -25
  120. package/dist/src/runtime/client.d.ts +24 -0
  121. package/dist/src/runtime/client.js +72 -0
  122. package/dist/src/runtime/owner.d.ts +35 -0
  123. package/dist/src/runtime/owner.js +393 -0
  124. package/dist/src/runtime/poll.d.ts +48 -0
  125. package/dist/src/runtime/poll.js +222 -0
  126. package/dist/src/runtime/service.d.ts +24 -0
  127. package/dist/src/runtime/service.js +251 -0
  128. package/dist/src/runtime/spawn.d.ts +17 -0
  129. package/dist/src/runtime/spawn.js +97 -0
  130. package/dist/src/runtime/token.d.ts +3 -0
  131. package/dist/src/runtime/token.js +64 -0
  132. package/dist/src/service/index.d.ts +4 -0
  133. package/dist/src/service/index.js +47 -0
  134. package/dist/src/service/macos.d.ts +96 -0
  135. package/dist/src/service/macos.js +551 -0
  136. package/dist/src/store/attempts.d.ts +12 -0
  137. package/dist/src/store/attempts.js +29 -0
  138. package/dist/src/store/db.d.ts +1 -1
  139. package/dist/src/store/db.js +58 -2
  140. package/dist/src/store/quotas.js +52 -10
  141. package/dist/src/version.js +1 -1
  142. package/dist/src/webAssets.js +1 -1
  143. package/dist/src/webHtml.js +1 -1
  144. package/dist/store/attempts.d.ts +12 -0
  145. package/dist/store/attempts.js +29 -0
  146. package/dist/store/db.d.ts +1 -1
  147. package/dist/store/db.js +58 -2
  148. package/dist/store/quotas.js +52 -10
  149. package/dist/tests/adapters/agy.test.js +337 -0
  150. package/dist/tests/adapters/claude.test.js +41 -1
  151. package/dist/tests/adapters/codex.test.js +231 -91
  152. package/dist/tests/adapters/credential-free.test.js +211 -0
  153. package/dist/tests/adapters/grok.test.js +239 -135
  154. package/dist/tests/adapters/kimi.test.js +219 -104
  155. package/dist/tests/adapters/pty.test.d.ts +1 -0
  156. package/dist/tests/adapters/pty.test.js +240 -0
  157. package/dist/tests/adapters/registry.test.js +5 -5
  158. package/dist/tests/advisory/engine.test.js +194 -6
  159. package/dist/tests/advisory/pace.test.d.ts +1 -0
  160. package/dist/tests/advisory/pace.test.js +60 -0
  161. package/dist/tests/advisory/snapshot.test.d.ts +1 -0
  162. package/dist/tests/advisory/snapshot.test.js +65 -0
  163. package/dist/tests/advisory/validate.test.d.ts +1 -0
  164. package/dist/tests/advisory/validate.test.js +19 -0
  165. package/dist/tests/cli/advise.test.d.ts +1 -0
  166. package/dist/tests/cli/advise.test.js +294 -0
  167. package/dist/tests/cli/cli.test.js +39 -19
  168. package/dist/tests/cli/helpers.d.ts +9 -0
  169. package/dist/tests/cli/helpers.js +93 -0
  170. package/dist/tests/cli/ingest.test.d.ts +1 -0
  171. package/dist/tests/cli/ingest.test.js +188 -0
  172. package/dist/tests/cli/parity.test.d.ts +1 -0
  173. package/dist/tests/cli/parity.test.js +225 -0
  174. package/dist/tests/cli/snapshot-source.test.d.ts +1 -0
  175. package/dist/tests/cli/snapshot-source.test.js +305 -0
  176. package/dist/tests/cli/status.test.d.ts +1 -0
  177. package/dist/tests/cli/status.test.js +423 -0
  178. package/dist/tests/daemon.test.js +140 -38
  179. package/dist/tests/fixtures/stable-state.d.ts +22 -0
  180. package/dist/tests/fixtures/stable-state.js +152 -0
  181. package/dist/tests/fixtures/stable-state.test.d.ts +1 -0
  182. package/dist/tests/fixtures/stable-state.test.js +17 -0
  183. package/dist/tests/format/markdown.test.d.ts +1 -0
  184. package/dist/tests/format/markdown.test.js +49 -0
  185. package/dist/tests/format/terminal.test.d.ts +1 -0
  186. package/dist/tests/format/terminal.test.js +476 -0
  187. package/dist/tests/http/api.test.js +224 -2
  188. package/dist/tests/integration.test.js +20 -7
  189. package/dist/tests/mcp/protocol.test.d.ts +1 -0
  190. package/dist/tests/mcp/protocol.test.js +134 -0
  191. package/dist/tests/mcp/quotas.test.js +38 -6
  192. package/dist/tests/mcp/tools.test.d.ts +1 -0
  193. package/dist/tests/mcp/tools.test.js +291 -0
  194. package/dist/tests/runtime/cancel.test.d.ts +1 -0
  195. package/dist/tests/runtime/cancel.test.js +264 -0
  196. package/dist/tests/runtime/cli.test.d.ts +1 -0
  197. package/dist/tests/runtime/cli.test.js +226 -0
  198. package/dist/tests/runtime/config.test.d.ts +1 -0
  199. package/dist/tests/runtime/config.test.js +144 -0
  200. package/dist/tests/runtime/http.test.d.ts +1 -0
  201. package/dist/tests/runtime/http.test.js +267 -0
  202. package/dist/tests/runtime/lifecycle.test.d.ts +1 -0
  203. package/dist/tests/runtime/lifecycle.test.js +303 -0
  204. package/dist/tests/runtime/ownership.test.d.ts +1 -0
  205. package/dist/tests/runtime/ownership.test.js +355 -0
  206. package/dist/tests/runtime/poll.test.d.ts +1 -0
  207. package/dist/tests/runtime/poll.test.js +410 -0
  208. package/dist/tests/runtime/status.test.d.ts +1 -0
  209. package/dist/tests/runtime/status.test.js +232 -0
  210. package/dist/tests/service/macos.test.d.ts +1 -0
  211. package/dist/tests/service/macos.test.js +349 -0
  212. package/dist/tests/store/attempts.test.d.ts +1 -0
  213. package/dist/tests/store/attempts.test.js +29 -0
  214. package/dist/tests/store/db.test.js +61 -6
  215. package/dist/tests/store/migration.test.d.ts +1 -0
  216. package/dist/tests/store/migration.test.js +41 -0
  217. package/dist/tests/web/advice-drawer.test.d.ts +1 -0
  218. package/dist/tests/web/advice-drawer.test.js +45 -0
  219. package/dist/tests/web/dashboard.e2e.d.ts +1 -0
  220. package/dist/tests/web/dashboard.e2e.js +670 -0
  221. package/dist/tests/web/header.test.d.ts +1 -0
  222. package/dist/tests/web/header.test.js +56 -0
  223. package/dist/tests/web/names.test.d.ts +1 -0
  224. package/dist/tests/web/names.test.js +19 -0
  225. package/dist/tests/web/onboarding.test.d.ts +1 -0
  226. package/dist/tests/web/onboarding.test.js +21 -0
  227. package/dist/tests/web/pacing.test.d.ts +1 -0
  228. package/dist/tests/web/pacing.test.js +62 -0
  229. package/dist/tests/web/playwright.config.d.ts +2 -0
  230. package/dist/tests/web/playwright.config.js +10 -0
  231. package/dist/tests/web/provider-drawer.test.d.ts +1 -0
  232. package/dist/tests/web/provider-drawer.test.js +68 -0
  233. package/dist/tests/web/recommendation.test.d.ts +1 -0
  234. package/dist/tests/web/recommendation.test.js +38 -0
  235. package/dist/tests/web/reset-rail.test.d.ts +1 -0
  236. package/dist/tests/web/reset-rail.test.js +63 -0
  237. package/dist/tests/web/responsive.test.d.ts +1 -0
  238. package/dist/tests/web/responsive.test.js +21 -0
  239. package/dist/tests/web/settings.test.d.ts +1 -0
  240. package/dist/tests/web/settings.test.js +32 -0
  241. package/dist/tests/web/state-catalog.test.d.ts +1 -0
  242. package/dist/tests/web/state-catalog.test.js +206 -0
  243. package/dist/tests/web/state.test.d.ts +1 -0
  244. package/dist/tests/web/state.test.js +27 -0
  245. package/dist/tests/web/stub-server.d.ts +37 -0
  246. package/dist/tests/web/stub-server.js +132 -0
  247. package/dist/tests/web/theme.test.d.ts +1 -0
  248. package/dist/tests/web/theme.test.js +12 -0
  249. package/dist/version.js +1 -1
  250. package/dist/web/src/App.d.ts +1 -0
  251. package/dist/web/src/App.js +106 -78
  252. package/dist/web/src/Theme.d.ts +11 -0
  253. package/dist/web/src/Theme.js +53 -0
  254. package/dist/web/src/api.d.ts +27 -2
  255. package/dist/web/src/api.js +102 -6
  256. package/dist/web/src/components/AdviceDrawer.d.ts +9 -0
  257. package/dist/web/src/components/AdviceDrawer.js +77 -0
  258. package/dist/web/src/components/FaultBanner.d.ts +17 -0
  259. package/dist/web/src/components/FaultBanner.js +60 -0
  260. package/dist/web/src/components/Header.d.ts +22 -0
  261. package/dist/web/src/components/Header.js +46 -0
  262. package/dist/web/src/components/PaceBar.d.ts +51 -0
  263. package/dist/web/src/components/PaceBar.js +251 -0
  264. package/dist/web/src/components/PacingLegend.d.ts +3 -0
  265. package/dist/web/src/components/PacingLegend.js +43 -0
  266. package/dist/web/src/components/ProviderCard.d.ts +8 -0
  267. package/dist/web/src/components/ProviderCard.js +27 -0
  268. package/dist/web/src/components/ProviderDrawer.d.ts +52 -0
  269. package/dist/web/src/components/ProviderDrawer.js +270 -0
  270. package/dist/web/src/components/ProviderIcon.d.ts +7 -0
  271. package/dist/web/src/components/ProviderIcon.js +27 -0
  272. package/dist/web/src/components/ProviderRow.d.ts +8 -0
  273. package/dist/web/src/components/ProviderRow.js +14 -0
  274. package/dist/web/src/components/Recommendation.d.ts +27 -0
  275. package/dist/web/src/components/Recommendation.js +77 -0
  276. package/dist/web/src/components/ResetRail.d.ts +38 -0
  277. package/dist/web/src/components/ResetRail.js +188 -0
  278. package/dist/web/src/components/SubscriptionList.d.ts +19 -0
  279. package/dist/web/src/components/SubscriptionList.js +107 -0
  280. package/dist/web/src/components/settings/CliIntegrationPanel.d.ts +2 -0
  281. package/dist/web/src/components/settings/CliIntegrationPanel.js +49 -0
  282. package/dist/web/src/components/settings/DaemonPanel.d.ts +6 -0
  283. package/dist/web/src/components/settings/DaemonPanel.js +45 -0
  284. package/dist/web/src/components/settings/ProvidersPanel.d.ts +14 -0
  285. package/dist/web/src/components/settings/ProvidersPanel.js +69 -0
  286. package/dist/web/src/components/settings/SettingsDrawer.d.ts +9 -0
  287. package/dist/web/src/components/settings/SettingsDrawer.js +55 -0
  288. package/dist/web/src/names.d.ts +3 -0
  289. package/dist/web/src/names.js +19 -0
  290. package/dist/web/src/pages/Onboarding.d.ts +17 -0
  291. package/dist/web/src/pages/Onboarding.js +96 -0
  292. package/dist/web/src/router.d.ts +11 -0
  293. package/dist/web/src/router.js +31 -0
  294. package/dist/web/src/state.d.ts +97 -0
  295. package/dist/web/src/state.js +80 -0
  296. package/dist/webAssets.js +1 -1
  297. package/dist/webHtml.js +1 -1
  298. package/package.json +33 -8
  299. package/scripts/fix-pty-perms.mjs +21 -0
  300. package/web/dist/assets/index-CyUkWdwJ.css +1 -0
  301. package/web/dist/assets/index-PN4RpMKG.js +12 -0
  302. package/web/dist/index.html +19 -2
  303. package/dist/adapters/core.d.ts +0 -9
  304. package/dist/adapters/core.js +0 -90
  305. package/dist/format/table.d.ts +0 -2
  306. package/dist/format/table.js +0 -56
  307. package/dist/src/adapters/core.d.ts +0 -9
  308. package/dist/src/adapters/core.js +0 -90
  309. package/dist/src/format/table.d.ts +0 -2
  310. package/dist/src/format/table.js +0 -56
  311. package/dist/tests/adapters/core.test.js +0 -110
  312. package/dist/tests/format/table.test.js +0 -98
  313. package/web/dist/assets/index-Dpiqzcc8.js +0 -40
  314. /package/dist/tests/adapters/{core.test.d.ts → agy.test.d.ts} +0 -0
  315. /package/dist/tests/{format/table.test.d.ts → adapters/credential-free.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,69 +1,59 @@
1
1
  # QuotaCap
2
2
 
3
- **Cross-harness AI quota dashboard + harness-callable advice.**
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
- One place to see how much of each AI subscription you've used. Shows when it resets and what to burn next. Goal is 100% use at reset.
5
+ [![npm](https://img.shields.io/npm/v/quotacap)](https://www.npmjs.com/package/quotacap)
6
+ [![CI](https://github.com/carlosboeing/quotacap/actions/workflows/test.yml/badge.svg)](https://github.com/carlosboeing/quotacap/actions)
7
+ [![license](https://img.shields.io/github/license/carlosboeing/quotacap)](LICENSE)
8
+ [![node](https://img.shields.io/node/v/quotacap)](package.json)
6
9
 
7
- Polls pluggable adapters (Claude, Codex, Gemini, Kimi, Grok, OpenCode). Stores history in SQLite. Serves:
10
+ ## Features
8
11
 
9
- * **Web dashboard** at `http://localhost:8787` banner + 7-day strip + table with burn vs ideal. Includes stale badges and degraded banner.
10
- * **CLI** `quotacap` / `npx quotacap` `status`, `advise --json`, `ingest`, `web`, `init`, `daemon`
11
- * **MCP server** `get_quotas`, `get_recommendation`, `forecast` for any harness
12
+ - **Visibility**: remaining usage and reset time for one current window per connected plan
13
+ - **Pacing**: recent usage when history is available, or an estimated pace while QuotaCap collects it
14
+ - **Advice**: an estimate of which plan to use next so you can use more of each allowance without exhausting one early
15
+ - **Dashboard, CLI, and MCP**: the same data and advice on every surface
12
16
 
13
- Runs local-only by default. No prompts or file contents leave your machine.
17
+ ## Providers
18
+
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
+
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.
14
30
 
15
31
  ## Install
16
32
 
17
33
  ```bash
18
- # Binary — no Node needed (macOS + Linux, arm64/x64)
34
+ # Binary: macOS and Linux, arm64 and x64. No Node.
19
35
  curl -fsSL https://raw.githubusercontent.com/carlosboeing/quotacap/main/install.sh | sh
20
36
 
21
- # Or via npm (requires Node 22.13+; node:sqlite needs --experimental-sqlite before 22.13)
22
- npm install -g quotacap # global install
23
- npx quotacap # run without install
37
+ # Or install with npm (Node 22.13+)
38
+ npm install -g quotacap
24
39
  ```
25
40
 
26
- The binary is a self-contained Bun-compiled executable from GitHub Releases.
27
- The npm package runs the same CLI on Node. Both put `quotacap` on your PATH.
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.
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`.
28
43
 
29
44
  ## Quick start
30
45
 
31
46
  ```bash
32
- quotacap init
33
- quotacap ingest --provider kimi --text "Current week: 22% used · resets Aug 29 at 11am"
34
- quotacap status --json
35
- quotacap web # http://localhost:8787
36
- quotacap advise --json # tries HTTP, falls back to local DB
37
- quotacap advise --json --task heavy
38
- ```
47
+ # Terminal 1: leave this running
48
+ quotacap init # writes ~/.quotacap/config.json
49
+ quotacap web # dashboard at http://localhost:8787
39
50
 
40
- ## HTTP API
41
-
42
- ```bash
43
- curl http://localhost:8787/health
44
- curl http://localhost:8787/api/quotas
45
- curl "http://localhost:8787/api/recommendation?task=any"
46
- curl -X POST http://localhost:8787/api/refresh
51
+ # Terminal 2: after the dashboard table fills
52
+ quotacap status
53
+ quotacap advise
47
54
  ```
48
55
 
49
- * `GET /health` `{ok, uptime, lastPollAt}`
50
- * `GET /api/quotas` → `Quota[]` with `stale`, `ageMs`
51
- * `GET /api/recommendation?task=any` → `{use, reason, advisories}`
52
- * `POST /api/refresh` → `{fulfilled, rejected, lastPollAt, degraded}` — debounced 60s, always 200
53
- * `GET /` → `web/dist/index.html` if built, else `web/index.html`
54
-
55
- ## CLI
56
-
57
- ```
58
- quotacap status [--json]
59
- quotacap advise [--json] [--task any|heavy|light]
60
- quotacap ingest --provider <id> --text "..."
61
- quotacap web [--port 8787]
62
- quotacap daemon [--foreground]
63
- quotacap init
64
- ```
65
-
66
- `advise` fetches `http://localhost:$port/api/recommendation` with 2s timeout. Falls back to local `recommend()` when daemon is down.
56
+ `web` stays in the foreground and starts the daemon.
67
57
 
68
58
  ## MCP
69
59
 
@@ -76,44 +66,79 @@ quotacap init
76
66
  ```
77
67
 
78
68
  Use `"command": "npx", "args": ["quotacap", "mcp"]` when installed via npm only.
79
- Tools: `get_quotas`, `get_recommendation`, `forecast`. Wrapper over HTTP. Set `QUOTACAP_URL` to override.
80
69
 
81
- ## How it works
70
+ Tools: `get_quotas`, `get_recommendation`, `forecast`.
71
+
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) |
82
83
 
84
+ ### Curl examples
85
+
86
+ Read current quotas:
87
+
88
+ ```bash
89
+ curl http://localhost:8787/api/quotas
83
90
  ```
84
- Adapters (claude-cli ✓, manual ✓, others → manual-paste)
85
- daemon (poll 15m + jitter, SQLite ~/.quotacap/quotacap.db)
86
- → HTTP :8787
87
- → dashboard + CLI + MCP (same handler)
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)"
88
97
  ```
89
98
 
90
- Adapters are isolated. One failure does not block others. Uses `Promise.allSettled` and per-adapter catch. `POST /api/refresh` always 200.
99
+ ## Security
91
100
 
92
- ## Config
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.
93
102
 
94
- `~/.quotacap/config.json`:
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`).
95
104
 
96
- ```json
97
- { "port": 8787, "pollMinutes": 15, "enabledProviders": ["claude"] }
98
- ```
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.
99
106
 
100
- DB is `~/.quotacap/quotacap.db` (`quotas`, `snapshots`).
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`).
101
108
 
102
- ## Development
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.
110
+
111
+ ## Docs
112
+
113
+ - [Architecture](docs/architecture.md)
114
+ - [Changelog](docs/CHANGELOG.md)
115
+ - [Roadmap](docs/ROADMAP.md)
116
+ - [Security](SECURITY.md)
117
+
118
+ ## License
119
+
120
+ MIT. See [LICENSE](LICENSE).
121
+
122
+ <details>
123
+ <summary>Development</summary>
103
124
 
104
125
  ```bash
105
- npm test # vitest run (10 files) — builds first via pretest
126
+ npm test # vitest (builds first via pretest)
106
127
  bun test tests/bun/ # bun-runtime tests (sqlite adapter, MCP translation)
107
- npm run build # vite build → embed web assets → tsc → flatten dist → chmod bin
108
- npm run build:bin # bun build --compile (current platform, or pass targets)
109
- npx tsc --noEmit
128
+ npm run build
129
+ npm run build:bin
110
130
  ```
111
131
 
112
- The `store/db.ts` adapter picks `node:sqlite` on Node and `bun:sqlite` on Bun, so
113
- the same code runs as an npm package and as a compiled binary.
132
+ The store uses `node:sqlite` on Node and `bun:sqlite` on Bun.
133
+ The same code is an npm package and a compiled binary.
114
134
 
115
- See `.workbench/2-design/2026-08-28-quotacap-design.md` for spec. See `docs/ROADMAP.md` for next steps.
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`.
116
136
 
117
- ## License
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`.
118
143
 
119
- MIT — see [LICENSE](LICENSE).
144
+ </details>
@@ -0,0 +1,3 @@
1
+ import type { Adapter, ParsedQuota } from "./types.js";
2
+ export declare function parseAgyUsage(parsedJson: any, now?: Date): ParsedQuota[];
3
+ export declare const agyAdapter: Adapter;
@@ -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 { Quota } from "./types.js";
2
- export declare function parseClaudeUsage(result: string, now?: Date): Quota;
3
- export declare const claudeAdapter: {
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
- poll(): Promise<Quota>;
7
- };
6
+ execPath: string;
7
+ poll(execPath?: string): Promise<ParsedQuota>;
8
+ }
9
+ export declare const claudeAdapter: ClaudeAdapter;
@@ -1,7 +1,5 @@
1
- import { execFile } from "node:child_process";
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, raw: result, source: "cli", fetchedAt: now.toISOString()
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
- async poll() {
25
- const { stdout } = await exec("claude", ["-p", "/usage", "--output-format", "json"], { timeout: 8000 });
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);
@@ -1,7 +1,7 @@
1
- import type { Quota } from "./types.js";
2
- export declare function parseCodexUsage(body: any, now?: Date): Quota;
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<Quota>;
6
+ poll(): Promise<ParsedQuota>;
7
7
  };
@@ -1,61 +1,140 @@
1
1
  import os from "node:os";
2
- import path from "node:path";
3
- import { readJsonFile, getJson, postForm, persistCreds } from "./core.js";
4
- export function parseCodexUsage(body, now = new Date()) {
5
- const rl = body?.rate_limit ?? {};
6
- const win = rl.secondary_window ?? rl.primary_window;
7
- if (!win)
8
- throw new Error("codex: no rate-limit window in response");
9
- const resetsAt = new Date(win.reset_at * 1000).toISOString();
10
- const periodStart = new Date((win.reset_at - win.limit_window_seconds) * 1000).toISOString();
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: body.plan_type ?? "unknown",
14
- usedPct: win.used_percent ?? 0,
15
- resetsAt,
108
+ plan: "unknown",
109
+ usedPct,
110
+ sessionPct,
111
+ resetsAt: weeklyIso,
16
112
  periodStart,
17
- raw: JSON.stringify(body),
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: "~/.codex/auth.json (codex login)",
121
+ requiresAuth: "codex login (CLI owns credentials)",
31
122
  async poll() {
32
- const authFile = path.join(codexHome(), "auth.json");
33
- const auth = await readJsonFile(authFile);
34
- const at = auth.tokens?.access_token;
35
- if (!at)
36
- throw new Error("codex: no access_token in auth.json — run codex login");
37
- const headers = { Authorization: `Bearer ${at}`, "User-Agent": "quotacap" };
38
- if (auth.tokens?.account_id)
39
- headers["ChatGPT-Account-Id"] = auth.tokens.account_id;
40
- try {
41
- return parseCodexUsage(await getJson(USAGE_URL, headers));
42
- }
43
- catch (e) {
44
- if (e.status !== 401)
45
- throw e;
46
- const rf = auth.tokens?.refresh_token;
47
- if (!rf)
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
  };
@@ -1,7 +1,7 @@
1
- import type { Quota } from "./types.js";
2
- export declare function parseGrokUsage(body: any, now?: Date): Quota;
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<Quota>;
6
+ poll(): Promise<ParsedQuota>;
7
7
  };