cartethyia 2.5.2 → 2.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +2 -0
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -54
- package/package.json +1 -1
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/command/console-menu.ts +244 -246
- package/scripts/cli/command/tray.ts +208 -204
- package/scripts/cli/command/update.ts +27 -10
- package/scripts/cli/main.ts +28 -1
- package/src/boot/app.ts +693 -693
- package/src/boot/dependencies.ts +646 -616
- package/src/config/spec.ts +81 -2
- package/src/config/telemetry.ts +46 -0
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +93 -0
- package/src/console/data-management/operations.ts +229 -0
- package/src/console/data-management/routes.ts +104 -0
- package/src/console/domain-registration.ts +39 -0
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/store.ts +1115 -1018
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/provider-operations.ts +20 -0
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +22 -1
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/settings/contracts.ts +37 -0
- package/src/console/settings/store.ts +51 -0
- package/src/console/share/share-router.ts +6 -9
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/observability/event-compaction.ts +315 -0
- package/src/observability/event-day-index.ts +442 -0
- package/src/observability/event-log.ts +152 -3
- package/src/observability/payload-capture.ts +373 -344
- package/src/observability/payload-store.ts +440 -366
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- package/src/persistence/stores/telemetry-store.ts +237 -214
- package/src/protocol/codecs.ts +1 -2
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +14 -2
- package/src/protocol/response/gemini.ts +18 -4
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +136 -134
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/cline/cline-quota.ts +132 -128
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1485 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/opencode/provider.ts +531 -532
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1457
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +13 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +33 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/streaming/stream.ts +30 -2
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/request/preparer.ts +7 -6
- package/src/transport/request/state.ts +9 -0
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/route-catalog.ts +854 -799
- package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
- package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
}
|