cartethyia 2.5.4 → 2.5.7

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 (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-DE2607Tb.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DE2607Tb.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-C2PNYWzk.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-C2PNYWzk.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-B-9xnAQN.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-B-9xnAQN.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-xeax9nJt.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-xeax9nJt.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-Df4jjaHJ.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-Df4jjaHJ.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-Dyojsm84.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-Dyojsm84.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-DeWSOEUO.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-DeWSOEUO.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-C_klIZom.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-C_klIZom.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-BYph-InK.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-BYph-InK.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-CvyQCYWJ.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-CvyQCYWJ.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-Cm1JYilI.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-Cm1JYilI.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-Bog0p925.js +4 -0
  66. package/dist/dashboard/assets/entry-Bog0p925.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CZQIrdoX.js} +2 -2
  72. package/dist/dashboard/assets/index-CZQIrdoX.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BPpSsBgL.js} +1 -1
  94. package/dist/dashboard/assets/select-BPpSsBgL.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +95 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -1,310 +1,309 @@
1
- // Cursor quota.
2
- //
3
- // Two surfaces, and the second exists because the first is no longer
4
- // authoritative on its own:
5
- //
6
- // - `{baseUrl}/auth/usage` is Cursor's request-count API from before
7
- // usage-based plans. Current plans answer it with an uncapped, always-zero
8
- // `gpt-4` bucket, so a naive read of it reports "no usage" for an account
9
- // that is in fact spending against a dollar cap.
10
- // - `cursor.com/api/usage-summary` is the current surface: `individualUsage`
11
- // with `overall`, `plan` and `onDemand` buckets, in **USD cents**.
12
- //
13
- // The donor therefore reads both and prefers the summary when it answers. That
14
- // preference is kept here: the legacy response is still the only one an api-key
15
- // account can get, so it cannot simply be dropped, but it must not be allowed
16
- // to overwrite a real dollar balance with a zero.
17
- //
18
- // The summary is only reachable for an OAuth account on the default host, and
19
- // it needs the user id out of the access token — which is why it is attempted
20
- // conditionally rather than assumed.
21
-
22
- import type {
23
- FetchLike,
24
- ProviderQuotaResult,
25
- ProviderQuotaWindow,
26
- } from "../../quota/quota-contracts";
27
- import {
28
- cleanError,
29
- isoDate,
30
- number,
31
- percentWindow,
32
- quotaRecord,
33
- record,
34
- text,
35
- } from "../../quota/quota-contracts";
36
- import type { QuotaCollectionContext } from "../../quota/quota-support";
37
- import { cursorAccountIdentity } from "./cursor-auth";
38
- import { CURSOR_API_HOST, CURSOR_WEBSITE_HOST } from "./cursor-shared";
39
-
40
- /** Timestamp keys naming the end of the current window. */
41
- const RESET_END_KEYS: readonly string[] = [
42
- "billingCycleEnd",
43
- "endOfMonth",
44
- "resetsAt",
45
- "nextReset",
46
- ];
47
-
48
- /** Timestamp keys naming the start, which imply a window one month long. */
49
- const RESET_START_KEYS: readonly string[] = [
50
- "startOfMonth",
51
- "billingCycleStart",
52
- "startOfBillingCycle",
53
- ];
54
-
55
- /**
56
- * The window's reset, or `null`.
57
- *
58
- * A start timestamp implies a monthly window, so it is advanced by one month
59
- * rather than reported as-is — reporting the start as the reset would say the
60
- * window had already lapsed.
61
- */
62
- function resetsAt(source: Record<string, unknown>): string | null {
63
- for (const key of RESET_END_KEYS) {
64
- const value = isoDate(source[key]);
65
- if (value) return value;
66
- }
67
- for (const key of RESET_START_KEYS) {
68
- const value = isoDate(source[key]);
69
- if (!value) continue;
70
- const date = new Date(value);
71
- if (Number.isNaN(date.getTime())) continue;
72
- date.setUTCMonth(date.getUTCMonth() + 1);
73
- return date.toISOString();
74
- }
75
- return null;
76
- }
77
-
78
- /**
79
- * A Cursor cents bucket: `used`/`limit`/`remaining` in USD cents.
80
- *
81
- * An absent limit is a real shape, not a broken one — some plans carry no cap
82
- * and only report what has been spent. Those are emitted used-only rather than
83
- * dropped, which is what used to collapse an account to "no usage data".
84
- */
85
- function centsWindow(
86
- kind: string,
87
- label: string,
88
- source: Record<string, unknown>,
89
- reset: string | null,
90
- ): ProviderQuotaWindow | null {
91
- const limit = number(source["limit"] ?? source["maxRequestUsage"] ?? source["amountLimit"] ?? source["usdLimit"]);
92
- const used = number(source["used"] ?? source["numRequests"] ?? source["amountUsed"] ?? source["usdUsed"]);
93
- const remaining = number(source["remaining"]);
94
- if (used === null && remaining === null && limit === null) return null;
95
- const usedValue = used ?? (remaining !== null && limit !== null ? limit - remaining : null);
96
- return percentWindow(kind, label, null, reset, usedValue, limit, "independent");
97
- }
98
-
99
- /** The legacy `/auth/usage` shape: one object per request bucket. */
100
- function parseLegacyUsage(payload: unknown): readonly ProviderQuotaWindow[] {
101
- const root = quotaRecord(payload);
102
- const reset = resetsAt(root);
103
- const windows: ProviderQuotaWindow[] = [];
104
- for (const [key, value] of Object.entries(root)) {
105
- const bucket = record(value);
106
- if (!bucket) continue;
107
- const window = centsWindow(`cursor:requests:${key.toLowerCase()}`, `${key} requests`, bucket, reset);
108
- if (!window) continue;
109
- // A bucket with no cap and nothing used is not a balance. Usage-based plans
110
- // answer this surface with `gpt-4: {numRequests: 0, maxRequestUsage: null}`
111
- // while their real spend sits on the summary — showing the empty row put a
112
- // meaningless "gpt-4 requests" window above the number that matters, on
113
- // exactly the accounts denominated in dollars.
114
- if (window.limit === null && (window.used ?? 0) === 0) continue;
115
- windows.push(window);
116
- }
117
- return windows;
118
- }
119
-
120
- /**
121
- * The `usage-summary` shape.
122
- *
123
- * `overall` is the spend against the plan; `plan` carries the included rails;
124
- * `onDemand` is spend beyond them. All three are kept when present — dropping
125
- * on-demand because the plan bucket answered would hide the part of the bill
126
- * that is actually growing.
127
- */
128
- function parseUsageSummary(payload: unknown): {
129
- readonly windows: readonly ProviderQuotaWindow[];
130
- readonly plan: string | null;
131
- } {
132
- const root = record(payload) ?? {};
133
- const individual = record(root["individualUsage"]);
134
- if (!individual) return { windows: [], plan: null };
135
- const reset = resetsAt(root);
136
- const windows: ProviderQuotaWindow[] = [];
137
-
138
- // The plan bucket is denominated two ways and the reference reads the
139
- // percentages, not the dollar figures: an included plan carries `limit: 0`
140
- // because there is no dollar cap to spend against, so reading `used`/`limit`
141
- // reported every such account as `0 / 0` — a balance that looks empty and
142
- // means nothing.
143
- const plan = record(individual["plan"]);
144
- const limitCents = number(plan?.["limit"]);
145
- const limitUsd = limitCents !== null && limitCents > 0 ? limitCents / 100 : null;
146
-
147
- const percentWindowFor = (
148
- kind: string,
149
- label: string,
150
- percent: number | null,
151
- withLimit: boolean,
152
- ): ProviderQuotaWindow | null => {
153
- if (percent === null) return null;
154
- const fraction = Math.max(0, percent) / 100;
155
- if (withLimit && limitUsd !== null) {
156
- return percentWindow(kind, label, percent, reset, limitUsd * fraction, limitUsd, "independent");
157
- }
158
- // No dollar cap, so the percentage is the whole reading: reported against a
159
- // 100-point scale rather than as a dollar figure the plan does not have.
160
- return percentWindow(kind, label, percent, reset, percent, 100, "independent");
161
- };
162
-
163
- const auto = percentWindowFor(
164
- "cursor:usd:individual-auto",
165
- "Cursor Models",
166
- number(plan?.["autoPercentUsed"]),
167
- false,
168
- );
169
- if (auto) windows.push(auto);
170
- const api = percentWindowFor(
171
- "cursor:usd:individual-api",
172
- "Other Models",
173
- number(plan?.["apiPercentUsed"]),
174
- true,
175
- );
176
- if (api) windows.push(api);
177
- if (!auto && !api) {
178
- const fallback =
179
- percentWindowFor(
180
- "cursor:usd:individual-plan",
181
- "Personal Usage",
182
- number(plan?.["totalPercentUsed"]),
183
- true,
184
- ) ?? centsWindow("cursor:usd:individual-plan", "Personal Usage", plan ?? {}, reset);
185
- if (fallback) windows.push(fallback);
186
- }
187
-
188
- // On-demand is spend beyond the included rails. Kept even when the plan
189
- // bucket answered, because it is the part of the bill that grows.
190
- const onDemand = record(individual["onDemand"]);
191
- if (onDemand && onDemand["enabled"] !== false) {
192
- const window = centsWindow("cursor:usd:individual-on-demand", "On-demand usage", onDemand, reset);
193
- if (window) windows.push(window);
194
- }
195
-
196
- return { windows, plan: text(individual["planName"] ?? root["membershipType"]) };
197
- }
198
-
199
- /**
200
- * Reads one Cursor account's quota.
201
- *
202
- * The summary is preferred when it answers, because the legacy surface reports
203
- * a permanently-zero bucket on exactly the plans that have a dollar cap. The
204
- * legacy read is still attempted: it is the only surface an api-key account
205
- * can reach.
206
- */
207
- function authStateUserId(context: QuotaCollectionContext | undefined): string | undefined {
208
- const raw = context?.auth_state ?? {};
209
- for (const key of ["userId", "user_id", "uid", "sub"] as const) {
210
- const value = raw[key];
211
- if (typeof value === "string" && value.trim()) return value.trim();
212
- }
213
- return undefined;
214
- }
215
-
216
- /**
217
- * A stable display identity derived from the credential.
218
- *
219
- * Cursor's access token carries the account's real subject
220
- * (`google-oauth2|user_01K3...`), and that was being shown as the account
221
- * label — an identity, not a name. It is reduced to a short opaque tag here so
222
- * the console can tell two Cursor accounts apart without publishing either
223
- * one's subject.
224
- *
225
- * Derived rather than random, so the same account keeps the same tag across
226
- * refreshes; a tag that changed every read would be worse than the identity it
227
- * replaced.
228
- */
229
- function cursorAccountTag(credential: string): string {
230
- let hash = 0x811c9dc5;
231
- for (let index = 0; index < credential.length; index += 1) {
232
- hash ^= credential.charCodeAt(index);
233
- hash = Math.imul(hash, 0x01000193) >>> 0;
234
- }
235
- return `cursor-${hash.toString(36).padStart(7, "0").slice(0, 7)}`;
236
- }
237
-
238
- export async function fetchCursorQuota(
239
- credential: string,
240
- fetcher: FetchLike,
241
- context?: QuotaCollectionContext,
242
- ): Promise<ProviderQuotaResult> {
243
- const raw = context?.auth_state ?? {};
244
- const baseUrl =
245
- typeof raw["apiEndpoint"] === "string" && raw["apiEndpoint"].trim()
246
- ? raw["apiEndpoint"].trim().replace(/\/+$/, "")
247
- : `https://${CURSOR_API_HOST}`;
248
- const errors: string[] = [];
249
-
250
- const call = async (url: string, headers: Record<string, string>): Promise<unknown> => {
251
- const response = await fetcher(url, { method: "GET", headers });
252
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
253
- return await response.json();
254
- };
255
-
256
- const legacy = await call(`${baseUrl}/auth/usage`, {
257
- accept: "application/json",
258
- authorization: `Bearer ${credential}`,
259
- }).then(
260
- (payload) => parseLegacyUsage(payload),
261
- (error: unknown) => {
262
- errors.push(`auth/usage: ${cleanError(error)}`);
263
- return null;
264
- },
265
- );
266
-
267
- // Attempted for every credential kind, not just OAuth. The legacy surface is
268
- // the only one an api-key account is *guaranteed*, but gating the summary on
269
- // `credential_kind` left api-key accounts showing only `gpt-4 requests` — a
270
- // bucket that reads zero on exactly the plans that have a dollar cap. If the
271
- // summary answers, it is the better reading whatever kind the row claims.
272
- const userId = cursorAccountIdentity(credential) ?? authStateUserId(context);
273
- let summary: { readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null } | null = null;
274
- if (userId && baseUrl === `https://${CURSOR_API_HOST}`) {
275
- // This surface authenticates by session cookie, not bearer: it answers 401
276
- // `not_authenticated` for every Authorization header, including a valid
277
- // one. The cookie is the user id and the token joined by `::`.
278
- summary = await call(`https://${CURSOR_WEBSITE_HOST}/api/usage-summary`, {
279
- accept: "application/json",
280
- cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${credential}`)}`,
281
- }).then(
282
- (payload) => parseUsageSummary(payload),
283
- (error: unknown) => {
284
- errors.push(`usage-summary: ${cleanError(error)}`);
285
- return null;
286
- },
287
- );
288
- }
289
-
290
- const summaryWindows = summary?.windows ?? [];
291
- const windows = summaryWindows.length > 0 ? summaryWindows : (legacy ?? []);
292
- if (windows.length === 0) {
293
- return {
294
- source: "cursor",
295
- plan: summary?.plan ?? null,
296
- windows: [],
297
- accountLabel: cursorAccountTag(credential),
298
- error: errors.join("; ") || "Cursor quota endpoints did not answer.",
299
- };
300
- }
301
- return {
302
- source: "cursor",
303
- plan: summary?.plan ?? null,
304
- windows,
305
- accountLabel: cursorAccountTag(credential),
306
- // Partial reads keep the error so the console can name the quiet surface,
307
- // but a summary that answered is enough to report a balance.
308
- error: summaryWindows.length > 0 && errors.length > 0 ? errors.join("; ") : null,
309
- };
310
- }
1
+ // Cursor quota.
2
+ //
3
+ // Two surfaces, and the second exists because the first is not authoritative
4
+ // on its own:
5
+ //
6
+ // - `{baseUrl}/auth/usage` is Cursor's request-count API. Current plans answer
7
+ // it with an uncapped, always-zero `gpt-4` bucket, so a naive read of it
8
+ // reports "no usage" for an account that is in fact spending against a
9
+ // dollar cap.
10
+ // - `cursor.com/api/usage-summary` is the current surface: `individualUsage`
11
+ // with `overall`, `plan` and `onDemand` buckets, in **USD cents**.
12
+ //
13
+ // The donor therefore reads both and prefers the summary when it answers. That
14
+ // preference is kept here: the legacy response is still the only one an api-key
15
+ // account can get, so it cannot simply be dropped, but it must not be allowed
16
+ // to overwrite a real dollar balance with a zero.
17
+ //
18
+ // The summary is only reachable for an OAuth account on the default host, and
19
+ // it needs the user id out of the access token — which is why it is attempted
20
+ // conditionally rather than assumed.
21
+
22
+ import type {
23
+ FetchLike,
24
+ ProviderQuotaResult,
25
+ ProviderQuotaWindow,
26
+ } from "../../quota/quota-contracts";
27
+ import {
28
+ cleanError,
29
+ isoDate,
30
+ number,
31
+ percentWindow,
32
+ quotaRecord,
33
+ record,
34
+ text,
35
+ } from "../../quota/quota-contracts";
36
+ import type { QuotaCollectionContext } from "../../quota/quota-support";
37
+ import { cursorAccountIdentity } from "./cursor-auth";
38
+ import { CURSOR_API_HOST, CURSOR_WEBSITE_HOST } from "./cursor-shared";
39
+
40
+ /** Timestamp keys naming the end of the current window. */
41
+ const RESET_END_KEYS: readonly string[] = [
42
+ "billingCycleEnd",
43
+ "endOfMonth",
44
+ "resetsAt",
45
+ "nextReset",
46
+ ];
47
+
48
+ /** Timestamp keys naming the start, which imply a window one month long. */
49
+ const RESET_START_KEYS: readonly string[] = [
50
+ "startOfMonth",
51
+ "billingCycleStart",
52
+ "startOfBillingCycle",
53
+ ];
54
+
55
+ /**
56
+ * The window's reset, or `null`.
57
+ *
58
+ * A start timestamp implies a monthly window, so it is advanced by one month
59
+ * rather than reported as-is — reporting the start as the reset would say the
60
+ * window had already lapsed.
61
+ */
62
+ function resetsAt(source: Record<string, unknown>): string | null {
63
+ for (const key of RESET_END_KEYS) {
64
+ const value = isoDate(source[key]);
65
+ if (value) return value;
66
+ }
67
+ for (const key of RESET_START_KEYS) {
68
+ const value = isoDate(source[key]);
69
+ if (!value) continue;
70
+ const date = new Date(value);
71
+ if (Number.isNaN(date.getTime())) continue;
72
+ date.setUTCMonth(date.getUTCMonth() + 1);
73
+ return date.toISOString();
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * A Cursor cents bucket: `used`/`limit`/`remaining` in USD cents.
80
+ *
81
+ * An absent limit is a real shape, not a broken one — some plans carry no cap
82
+ * and only report what has been spent. Those are emitted used-only rather than
83
+ * dropped, which would collapse an account to "no usage data".
84
+ */
85
+ function centsWindow(
86
+ kind: string,
87
+ label: string,
88
+ source: Record<string, unknown>,
89
+ reset: string | null,
90
+ ): ProviderQuotaWindow | null {
91
+ const limit = number(source["limit"] ?? source["maxRequestUsage"] ?? source["amountLimit"] ?? source["usdLimit"]);
92
+ const used = number(source["used"] ?? source["numRequests"] ?? source["amountUsed"] ?? source["usdUsed"]);
93
+ const remaining = number(source["remaining"]);
94
+ if (used === null && remaining === null && limit === null) return null;
95
+ const usedValue = used ?? (remaining !== null && limit !== null ? limit - remaining : null);
96
+ return percentWindow(kind, label, null, reset, usedValue, limit, "independent");
97
+ }
98
+
99
+ /** The legacy `/auth/usage` shape: one object per request bucket. */
100
+ function parseLegacyUsage(payload: unknown): readonly ProviderQuotaWindow[] {
101
+ const root = quotaRecord(payload);
102
+ const reset = resetsAt(root);
103
+ const windows: ProviderQuotaWindow[] = [];
104
+ for (const [key, value] of Object.entries(root)) {
105
+ const bucket = record(value);
106
+ if (!bucket) continue;
107
+ const window = centsWindow(`cursor:requests:${key.toLowerCase()}`, `${key} requests`, bucket, reset);
108
+ if (!window) continue;
109
+ // A bucket with no cap and nothing used is not a balance. Usage-based plans
110
+ // answer this surface with `gpt-4: {numRequests: 0, maxRequestUsage: null}`
111
+ // while their real spend sits on the summary — showing the empty row would
112
+ // put a meaningless "gpt-4 requests" window above the number that matters,
113
+ // on exactly the accounts denominated in dollars.
114
+ if (window.limit === null && (window.used ?? 0) === 0) continue;
115
+ windows.push(window);
116
+ }
117
+ return windows;
118
+ }
119
+
120
+ /**
121
+ * The `usage-summary` shape.
122
+ *
123
+ * `overall` is the spend against the plan; `plan` carries the included rails;
124
+ * `onDemand` is spend beyond them. All three are kept when present — dropping
125
+ * on-demand because the plan bucket answered would hide the part of the bill
126
+ * that is actually growing.
127
+ */
128
+ function parseUsageSummary(payload: unknown): {
129
+ readonly windows: readonly ProviderQuotaWindow[];
130
+ readonly plan: string | null;
131
+ } {
132
+ const root = record(payload) ?? {};
133
+ const individual = record(root["individualUsage"]);
134
+ if (!individual) return { windows: [], plan: null };
135
+ const reset = resetsAt(root);
136
+ const windows: ProviderQuotaWindow[] = [];
137
+
138
+ // The plan bucket is denominated two ways and the reference reads the
139
+ // percentages, not the dollar figures: an included plan carries `limit: 0`
140
+ // because there is no dollar cap to spend against, so reading `used`/`limit`
141
+ // reports every such account as `0 / 0` — a balance that looks empty and
142
+ // means nothing.
143
+ const plan = record(individual["plan"]);
144
+ const limitCents = number(plan?.["limit"]);
145
+ const limitUsd = limitCents !== null && limitCents > 0 ? limitCents / 100 : null;
146
+
147
+ const percentWindowFor = (
148
+ kind: string,
149
+ label: string,
150
+ percent: number | null,
151
+ withLimit: boolean,
152
+ ): ProviderQuotaWindow | null => {
153
+ if (percent === null) return null;
154
+ const fraction = Math.max(0, percent) / 100;
155
+ if (withLimit && limitUsd !== null) {
156
+ return percentWindow(kind, label, percent, reset, limitUsd * fraction, limitUsd, "independent");
157
+ }
158
+ // No dollar cap, so the percentage is the whole reading: reported against a
159
+ // 100-point scale rather than as a dollar figure the plan does not have.
160
+ return percentWindow(kind, label, percent, reset, percent, 100, "independent");
161
+ };
162
+
163
+ const auto = percentWindowFor(
164
+ "cursor:usd:individual-auto",
165
+ "Cursor Models",
166
+ number(plan?.["autoPercentUsed"]),
167
+ false,
168
+ );
169
+ if (auto) windows.push(auto);
170
+ const api = percentWindowFor(
171
+ "cursor:usd:individual-api",
172
+ "Other Models",
173
+ number(plan?.["apiPercentUsed"]),
174
+ true,
175
+ );
176
+ if (api) windows.push(api);
177
+ if (!auto && !api) {
178
+ const fallback =
179
+ percentWindowFor(
180
+ "cursor:usd:individual-plan",
181
+ "Personal Usage",
182
+ number(plan?.["totalPercentUsed"]),
183
+ true,
184
+ ) ?? centsWindow("cursor:usd:individual-plan", "Personal Usage", plan ?? {}, reset);
185
+ if (fallback) windows.push(fallback);
186
+ }
187
+
188
+ // On-demand is spend beyond the included rails. Kept even when the plan
189
+ // bucket answered, because it is the part of the bill that grows.
190
+ const onDemand = record(individual["onDemand"]);
191
+ if (onDemand && onDemand["enabled"] !== false) {
192
+ const window = centsWindow("cursor:usd:individual-on-demand", "On-demand usage", onDemand, reset);
193
+ if (window) windows.push(window);
194
+ }
195
+
196
+ return { windows, plan: text(individual["planName"] ?? root["membershipType"]) };
197
+ }
198
+
199
+ /**
200
+ * Reads one Cursor account's quota.
201
+ *
202
+ * The summary is preferred when it answers, because the legacy surface reports
203
+ * a permanently-zero bucket on exactly the plans that have a dollar cap. The
204
+ * legacy read is still attempted: it is the only surface an api-key account
205
+ * can reach.
206
+ */
207
+ function authStateUserId(context: QuotaCollectionContext | undefined): string | undefined {
208
+ const raw = context?.auth_state ?? {};
209
+ for (const key of ["userId", "user_id", "uid", "sub"] as const) {
210
+ const value = raw[key];
211
+ if (typeof value === "string" && value.trim()) return value.trim();
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ /**
217
+ * A stable display identity derived from the credential.
218
+ *
219
+ * Cursor's access token carries the account's real subject
220
+ * (`google-oauth2|user_01K3...`), which is an identity, not a name. It is
221
+ * reduced to a short opaque tag here so the console can tell two Cursor
222
+ * accounts apart without publishing either one's subject.
223
+ *
224
+ * Derived rather than random, so the same account keeps the same tag across
225
+ * refreshes; a tag that changed every read would be worse than showing the
226
+ * subject it replaces.
227
+ */
228
+ function cursorAccountTag(credential: string): string {
229
+ let hash = 0x811c9dc5;
230
+ for (let index = 0; index < credential.length; index += 1) {
231
+ hash ^= credential.charCodeAt(index);
232
+ hash = Math.imul(hash, 0x01000193) >>> 0;
233
+ }
234
+ return `cursor-${hash.toString(36).padStart(7, "0").slice(0, 7)}`;
235
+ }
236
+
237
+ export async function fetchCursorQuota(
238
+ credential: string,
239
+ fetcher: FetchLike,
240
+ context?: QuotaCollectionContext,
241
+ ): Promise<ProviderQuotaResult> {
242
+ const raw = context?.auth_state ?? {};
243
+ const baseUrl =
244
+ typeof raw["apiEndpoint"] === "string" && raw["apiEndpoint"].trim()
245
+ ? raw["apiEndpoint"].trim().replace(/\/+$/, "")
246
+ : `https://${CURSOR_API_HOST}`;
247
+ const errors: string[] = [];
248
+
249
+ const call = async (url: string, headers: Record<string, string>): Promise<unknown> => {
250
+ const response = await fetcher(url, { method: "GET", headers });
251
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
252
+ return await response.json();
253
+ };
254
+
255
+ const legacy = await call(`${baseUrl}/auth/usage`, {
256
+ accept: "application/json",
257
+ authorization: `Bearer ${credential}`,
258
+ }).then(
259
+ (payload) => parseLegacyUsage(payload),
260
+ (error: unknown) => {
261
+ errors.push(`auth/usage: ${cleanError(error)}`);
262
+ return null;
263
+ },
264
+ );
265
+
266
+ // Attempted for every credential kind, not just OAuth. The legacy surface is
267
+ // the only one an api-key account is *guaranteed*, but gating the summary on
268
+ // `credential_kind` leaves api-key accounts showing only `gpt-4 requests` — a
269
+ // bucket that reads zero on exactly the plans that have a dollar cap. If the
270
+ // summary answers, it is the better reading whatever kind the row claims.
271
+ const userId = cursorAccountIdentity(credential) ?? authStateUserId(context);
272
+ let summary: { readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null } | null = null;
273
+ if (userId && baseUrl === `https://${CURSOR_API_HOST}`) {
274
+ // This surface authenticates by session cookie, not bearer: it answers 401
275
+ // `not_authenticated` for every Authorization header, including a valid
276
+ // one. The cookie is the user id and the token joined by `::`.
277
+ summary = await call(`https://${CURSOR_WEBSITE_HOST}/api/usage-summary`, {
278
+ accept: "application/json",
279
+ cookie: `WorkosCursorSessionToken=${encodeURIComponent(`${userId}::${credential}`)}`,
280
+ }).then(
281
+ (payload) => parseUsageSummary(payload),
282
+ (error: unknown) => {
283
+ errors.push(`usage-summary: ${cleanError(error)}`);
284
+ return null;
285
+ },
286
+ );
287
+ }
288
+
289
+ const summaryWindows = summary?.windows ?? [];
290
+ const windows = summaryWindows.length > 0 ? summaryWindows : (legacy ?? []);
291
+ if (windows.length === 0) {
292
+ return {
293
+ source: "cursor",
294
+ plan: summary?.plan ?? null,
295
+ windows: [],
296
+ accountLabel: cursorAccountTag(credential),
297
+ error: errors.join("; ") || "Cursor quota endpoints did not answer.",
298
+ };
299
+ }
300
+ return {
301
+ source: "cursor",
302
+ plan: summary?.plan ?? null,
303
+ windows,
304
+ accountLabel: cursorAccountTag(credential),
305
+ // Partial reads keep the error so the console can name the quiet surface,
306
+ // but a summary that answered is enough to report a balance.
307
+ error: summaryWindows.length > 0 && errors.length > 0 ? errors.join("; ") : null,
308
+ };
309
+ }