cartethyia 2.4.6 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
- package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
- package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -52
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/tray.ts +47 -8
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +1 -26
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/payload-capture.ts +344 -324
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1295
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/provider.ts +532 -538
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Cursor live model catalog and Run-origin discovery.
|
|
2
|
+
//
|
|
3
|
+
// Two unary Connect-RPC calls on the API host back this module:
|
|
4
|
+
// `/aiserver.v1.AiService/AvailableModels` for the roster and
|
|
5
|
+
// `/aiserver.v1.ServerConfigService/GetServerConfig` for the region-specific Run
|
|
6
|
+
// origin. Both go through the validated outbound fetch the dispatch path hands
|
|
7
|
+
// the adapter, so a catalog refresh obeys the same SSRF policy and the same
|
|
8
|
+
// network pool as a dispatch.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here invents a capability. A model is marked image-capable, tool-
|
|
11
|
+
// capable, or reasoning-capable only because Cursor declared it; limits come
|
|
12
|
+
// from Cursor's own variant parameter or its own context fields.
|
|
13
|
+
import { defineModel } from "../../provider-model-definition";
|
|
14
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
15
|
+
import {
|
|
16
|
+
AVAILABLE_MODELS_PATH,
|
|
17
|
+
CURSOR_API_BASE_URL,
|
|
18
|
+
CURSOR_PROVIDER_ID,
|
|
19
|
+
FALLBACK_CLIENT_VERSION,
|
|
20
|
+
SERVER_CONFIG_PATH,
|
|
21
|
+
normalizeAgentRunOrigin,
|
|
22
|
+
} from "./cursor-shared";
|
|
23
|
+
import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
|
|
24
|
+
|
|
25
|
+
/** One parameter value of a Cursor-declared model variant. */
|
|
26
|
+
export interface CursorParameterValue {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly value: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One variant (parameter tuple) Cursor advertises for a model. */
|
|
32
|
+
export interface CursorModelVariant {
|
|
33
|
+
readonly displayName: string;
|
|
34
|
+
readonly parameterValues: readonly CursorParameterValue[];
|
|
35
|
+
readonly isDefaultNonMax: boolean;
|
|
36
|
+
readonly isDefaultMax: boolean;
|
|
37
|
+
readonly isMaxMode: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One model entry as Cursor declares it. */
|
|
41
|
+
export interface CursorModelInfo {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly displayName: string;
|
|
44
|
+
readonly maxContext?: number;
|
|
45
|
+
readonly maxContextForMaxMode?: number;
|
|
46
|
+
readonly supportsImages?: boolean;
|
|
47
|
+
readonly supportsThinking?: boolean;
|
|
48
|
+
readonly supportsAgent?: boolean;
|
|
49
|
+
readonly supportsMaxMode?: boolean;
|
|
50
|
+
readonly variants: readonly CursorModelVariant[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Headers every Cursor unary call carries, matching the CLI's identity. */
|
|
54
|
+
async function unaryHeaders(
|
|
55
|
+
credential: string,
|
|
56
|
+
clientVersion: string,
|
|
57
|
+
): Promise<Record<string, string>> {
|
|
58
|
+
const { machineId, macMachineId } = await resolveCursorDeviceIds();
|
|
59
|
+
return {
|
|
60
|
+
authorization: `Bearer ${credential}`,
|
|
61
|
+
"connect-protocol-version": "1",
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
accept: "application/json",
|
|
64
|
+
"x-cursor-client-type": "cli",
|
|
65
|
+
"x-cursor-client-version": clientVersion,
|
|
66
|
+
"x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
|
|
67
|
+
"x-ghost-mode": "true",
|
|
68
|
+
"x-request-id": crypto.randomUUID(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function postUnary(
|
|
73
|
+
fetcher: typeof fetch,
|
|
74
|
+
path: string,
|
|
75
|
+
credential: string,
|
|
76
|
+
body: unknown,
|
|
77
|
+
clientVersion: string,
|
|
78
|
+
signal: AbortSignal | undefined,
|
|
79
|
+
): Promise<Record<string, unknown>> {
|
|
80
|
+
const response = await fetcher(`${CURSOR_API_BASE_URL}${path}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: await unaryHeaders(credential, clientVersion),
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
...(signal === undefined ? {} : { signal }),
|
|
85
|
+
});
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Cursor ${path} failed (${response.status}): ${text.slice(0, 200)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(`Cursor ${path} returned malformed JSON`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
100
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
101
|
+
const prototype = Object.getPrototypeOf(value);
|
|
102
|
+
return prototype === Object.prototype || prototype === null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function optionalBoolean(
|
|
106
|
+
record: Record<string, unknown>,
|
|
107
|
+
names: readonly string[],
|
|
108
|
+
): boolean | undefined {
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
if (!Object.hasOwn(record, name)) continue;
|
|
111
|
+
const value = record[name];
|
|
112
|
+
return typeof value === "boolean" ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function positiveNumber(value: unknown): number | undefined {
|
|
118
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Parses Cursor's `200k` / `1m` context spelling into tokens. */
|
|
122
|
+
export function parseCursorContextLimit(value: unknown): number | undefined {
|
|
123
|
+
if (typeof value !== "string") return undefined;
|
|
124
|
+
const text = value.trim();
|
|
125
|
+
const match = /^(\d+(?:\.\d+)?)\s*([km])$/i.exec(text);
|
|
126
|
+
if (match !== null) {
|
|
127
|
+
const multiplier = match[2]!.toLowerCase() === "k" ? 1_000 : 1_000_000;
|
|
128
|
+
const limit = Number(match[1]) * multiplier;
|
|
129
|
+
return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined;
|
|
130
|
+
}
|
|
131
|
+
const numeric = Number(text);
|
|
132
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeParameterValues(value: unknown): CursorParameterValue[] | undefined {
|
|
136
|
+
if (value === undefined) return [];
|
|
137
|
+
if (!Array.isArray(value)) return undefined;
|
|
138
|
+
const parameters: CursorParameterValue[] = [];
|
|
139
|
+
for (const entry of value) {
|
|
140
|
+
if (!isPlainRecord(entry)) return undefined;
|
|
141
|
+
const id = typeof entry["id"] === "string" ? entry["id"].trim() : "";
|
|
142
|
+
const parameterValue = entry["value"] ?? entry["value"];
|
|
143
|
+
if (id.length === 0 || typeof parameterValue !== "string") return undefined;
|
|
144
|
+
parameters.push({ id, value: parameterValue });
|
|
145
|
+
}
|
|
146
|
+
return parameters;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Maps a live `AvailableModels` response onto Cursor model entries.
|
|
151
|
+
*
|
|
152
|
+
* Every entry's capability is Cursor's own declaration. A malformed variant or
|
|
153
|
+
* a non-boolean capability throws rather than being coerced: a catalog that
|
|
154
|
+
* guesses "supports images" where Cursor said nothing routes image traffic to a
|
|
155
|
+
* model that will refuse it.
|
|
156
|
+
*/
|
|
157
|
+
export function mapAvailableModelsResponse(raw: Record<string, unknown>): CursorModelInfo[] {
|
|
158
|
+
const entries = Array.isArray(raw["models"]) ? raw["models"] : [];
|
|
159
|
+
const models: CursorModelInfo[] = [];
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!isPlainRecord(entry)) continue;
|
|
162
|
+
const id = typeof entry["name"] === "string" ? entry["name"].trim() : "";
|
|
163
|
+
if (id.length === 0) continue;
|
|
164
|
+
const rawVariants = entry["variants"] ?? [];
|
|
165
|
+
if (!Array.isArray(rawVariants)) {
|
|
166
|
+
throw new Error(`Cursor AvailableModels returned invalid variants for ${id}`);
|
|
167
|
+
}
|
|
168
|
+
const variants: CursorModelVariant[] = [];
|
|
169
|
+
for (const variant of rawVariants) {
|
|
170
|
+
if (!isPlainRecord(variant)) {
|
|
171
|
+
throw new Error(`Cursor AvailableModels returned an invalid variant for ${id}`);
|
|
172
|
+
}
|
|
173
|
+
const parameterValues = normalizeParameterValues(
|
|
174
|
+
variant["parameterValues"] ?? variant["parameter_values"],
|
|
175
|
+
);
|
|
176
|
+
if (parameterValues === undefined) {
|
|
177
|
+
throw new Error(`Cursor AvailableModels returned invalid parameter values for ${id}`);
|
|
178
|
+
}
|
|
179
|
+
const isMaxMode = optionalBoolean(variant, ["isMaxMode", "is_max_mode"]) ?? false;
|
|
180
|
+
variants.push({
|
|
181
|
+
displayName: (typeof variant["displayName"] === "string"
|
|
182
|
+
? variant["displayName"]
|
|
183
|
+
: typeof variant["display_name"] === "string"
|
|
184
|
+
? variant["display_name"]
|
|
185
|
+
: id) as string,
|
|
186
|
+
parameterValues,
|
|
187
|
+
isDefaultNonMax:
|
|
188
|
+
optionalBoolean(variant, ["isDefaultNonMaxConfig", "is_default_non_max_config"]) ?? false,
|
|
189
|
+
isDefaultMax:
|
|
190
|
+
optionalBoolean(variant, ["isDefaultMaxConfig", "is_default_max_config"]) ?? false,
|
|
191
|
+
isMaxMode,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const contextOf = (variant: CursorModelVariant | undefined): number | undefined =>
|
|
195
|
+
parseCursorContextLimit(variant?.parameterValues.find((p) => p.id === "context")?.value);
|
|
196
|
+
const baseContext =
|
|
197
|
+
contextOf(variants.find((variant) => variant.isDefaultNonMax)) ??
|
|
198
|
+
contextOf(variants.find((variant) => variant.isDefaultMax) ?? variants[0]) ??
|
|
199
|
+
positiveNumber(entry["contextTokenLimit"] ?? entry["context_token_limit"]);
|
|
200
|
+
const maxContext =
|
|
201
|
+
contextOf(variants.find((variant) => variant.isDefaultMax)) ??
|
|
202
|
+
positiveNumber(entry["contextTokenLimitForMaxMode"] ?? entry["context_token_limit_for_max_mode"]);
|
|
203
|
+
models.push({
|
|
204
|
+
id,
|
|
205
|
+
displayName: (typeof entry["clientDisplayName"] === "string"
|
|
206
|
+
? entry["clientDisplayName"]
|
|
207
|
+
: typeof entry["client_display_name"] === "string"
|
|
208
|
+
? entry["client_display_name"]
|
|
209
|
+
: id) as string,
|
|
210
|
+
...(baseContext === undefined ? {} : { maxContext: baseContext }),
|
|
211
|
+
...(maxContext === undefined ? {} : { maxContextForMaxMode: maxContext }),
|
|
212
|
+
...(() => {
|
|
213
|
+
const images = optionalBoolean(entry, ["supportsImages", "supports_images"]);
|
|
214
|
+
return images === undefined ? {} : { supportsImages: images };
|
|
215
|
+
})(),
|
|
216
|
+
supportsThinking: optionalBoolean(entry, ["supportsThinking", "supports_thinking"]) ?? false,
|
|
217
|
+
supportsAgent: optionalBoolean(entry, ["supportsAgent", "supports_agent"]) ?? false,
|
|
218
|
+
supportsMaxMode: optionalBoolean(entry, ["supportsMaxMode", "supports_max_mode"]) ?? false,
|
|
219
|
+
variants,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return models;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Cursor's declared default parameter tuple for one model.
|
|
227
|
+
*
|
|
228
|
+
* The public catalog lists one row per model and sends Cursor's own default:
|
|
229
|
+
* no variant picker is part of this integration, so the tuple is the one Cursor
|
|
230
|
+
* marked default (its max default when the model runs in max mode, otherwise its
|
|
231
|
+
* non-max default). An empty tuple is correct for a model that declares none.
|
|
232
|
+
*/
|
|
233
|
+
export function defaultParameterValues(
|
|
234
|
+
model: CursorModelInfo | undefined,
|
|
235
|
+
maxMode = false,
|
|
236
|
+
): CursorParameterValue[] {
|
|
237
|
+
if (model === undefined || model.variants.length === 0) return [];
|
|
238
|
+
const tier = model.variants.filter((variant) => variant.isMaxMode === maxMode);
|
|
239
|
+
const candidates = tier.length > 0 ? tier : model.variants;
|
|
240
|
+
const declared = candidates.find((variant) =>
|
|
241
|
+
maxMode ? variant.isDefaultMax : variant.isDefaultNonMax,
|
|
242
|
+
);
|
|
243
|
+
const chosen = declared ?? candidates.find((variant) => variant.isMaxMode === maxMode) ?? candidates[0];
|
|
244
|
+
return chosen === undefined ? [] : chosen.parameterValues.map((parameter) => ({ ...parameter }));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Projects one Cursor model entry onto a Cartethyia catalog row. */
|
|
248
|
+
export function cursorModelDefinition(model: CursorModelInfo): ModelDefinition {
|
|
249
|
+
return cursorModel(model.id, model.maxContext, {
|
|
250
|
+
vision: model.supportsImages === true,
|
|
251
|
+
reasoning: model.supportsThinking === true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cursorModel(
|
|
256
|
+
modelId: string,
|
|
257
|
+
ctx?: number,
|
|
258
|
+
options: { vision?: boolean; reasoning?: boolean; out?: number } = {},
|
|
259
|
+
): ModelDefinition {
|
|
260
|
+
return defineModel({
|
|
261
|
+
id: modelId,
|
|
262
|
+
providerId: CURSOR_PROVIDER_ID,
|
|
263
|
+
wireFamily: "chat",
|
|
264
|
+
endpoint: "/agent.v1.AgentService/Run",
|
|
265
|
+
...(ctx === undefined ? {} : { ctx }),
|
|
266
|
+
...(options.out === undefined ? {} : { out: options.out }),
|
|
267
|
+
// Tools are served through Cursor's MCP exec channel, which this adapter
|
|
268
|
+
// answers for every request that declares tools — so the row advertises
|
|
269
|
+
// them and the adapter refuses anything it did not advertise.
|
|
270
|
+
toolCall: true,
|
|
271
|
+
vision: options.vision ?? true,
|
|
272
|
+
reasoning: options.reasoning ?? false,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Static fallback catalog for Cursor.
|
|
278
|
+
*
|
|
279
|
+
* The authoritative roster is resolved per account from `AvailableModels`,
|
|
280
|
+
* while these static rows give the provider offline floor visibility and
|
|
281
|
+
* establish canonical IDs for Auto and Grok 4.6/4.7.
|
|
282
|
+
*
|
|
283
|
+
* `auto` is the only id surfaced for Cursor's Auto model: the upstream wire id
|
|
284
|
+
* is `default` and the two are the same model, not two entries. Dispatch maps
|
|
285
|
+
* `auto` is an alias: Cursor's own roster names its Auto model `default`, and
|
|
286
|
+
* dispatch maps `auto` onto it, so seeding the upstream id as well would list
|
|
287
|
+
* the same model twice. Grok ids are already upstream ids and are not rewritten.
|
|
288
|
+
*
|
|
289
|
+
* Output is stated explicitly: `defineModel` would otherwise inherit a
|
|
290
|
+
* base-catalog output equal to the whole 256k window, which is not a limit
|
|
291
|
+
* Cursor serves and makes admission reserve the entire context as headroom.
|
|
292
|
+
*/
|
|
293
|
+
export const CURSOR_MODELS: readonly ModelDefinition[] = [
|
|
294
|
+
cursorModel("auto", 256_000, { vision: true, reasoning: false, out: 64_000 }),
|
|
295
|
+
cursorModel("grok-4.6", 256_000, { vision: true, reasoning: true, out: 64_000 }),
|
|
296
|
+
cursorModel("grok-4.7", 256_000, { vision: true, reasoning: true, out: 64_000 }),
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
/** Fetches Cursor's live model roster through validated egress. */
|
|
300
|
+
export async function fetchCursorModels(input: {
|
|
301
|
+
readonly credential: string;
|
|
302
|
+
readonly fetcher?: typeof fetch;
|
|
303
|
+
readonly signal?: AbortSignal;
|
|
304
|
+
readonly clientVersion?: string;
|
|
305
|
+
}): Promise<CursorModelInfo[]> {
|
|
306
|
+
const fetcher = input.fetcher ?? globalThis.fetch;
|
|
307
|
+
const body = await postUnary(
|
|
308
|
+
fetcher,
|
|
309
|
+
AVAILABLE_MODELS_PATH,
|
|
310
|
+
input.credential,
|
|
311
|
+
// Parameterized effort/context/fast variants, as Cursor IDE requests them.
|
|
312
|
+
{
|
|
313
|
+
includeLongContextModels: true,
|
|
314
|
+
useModelParameters: true,
|
|
315
|
+
useCloudAgentEffortModes: true,
|
|
316
|
+
},
|
|
317
|
+
input.clientVersion ?? FALLBACK_CLIENT_VERSION,
|
|
318
|
+
input.signal,
|
|
319
|
+
);
|
|
320
|
+
return mapAvailableModelsResponse(body);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolves the Run-stream origin Cursor routes this account to.
|
|
325
|
+
*
|
|
326
|
+
* Fails closed: a region-routed account is silently rejected by the wrong
|
|
327
|
+
* regional host, so a missing, malformed, non-HTTPS, or non-`*.cursor.sh` value
|
|
328
|
+
* is an error rather than a fallback to the API host. The API origin does serve
|
|
329
|
+
* auth and catalog calls, but it is not the origin `GetServerConfig` names for
|
|
330
|
+
* Run, and assuming it does is what makes a Run land on a host that refuses it.
|
|
331
|
+
*/
|
|
332
|
+
export async function fetchCursorRunOrigin(input: {
|
|
333
|
+
readonly credential: string;
|
|
334
|
+
readonly fetcher?: typeof fetch;
|
|
335
|
+
readonly signal?: AbortSignal;
|
|
336
|
+
readonly clientVersion?: string;
|
|
337
|
+
}): Promise<string> {
|
|
338
|
+
const fetcher = input.fetcher ?? globalThis.fetch;
|
|
339
|
+
const body = await postUnary(
|
|
340
|
+
fetcher,
|
|
341
|
+
SERVER_CONFIG_PATH,
|
|
342
|
+
input.credential,
|
|
343
|
+
{ telem_enabled: false },
|
|
344
|
+
input.clientVersion ?? FALLBACK_CLIENT_VERSION,
|
|
345
|
+
input.signal,
|
|
346
|
+
);
|
|
347
|
+
const config = isPlainRecord(body["agentUrlConfig"]) ? body["agentUrlConfig"] : undefined;
|
|
348
|
+
const raw = config?.["agentnUrl"] ?? config?.["agentUrl"];
|
|
349
|
+
const origin = normalizeAgentRunOrigin(typeof raw === "string" ? raw : undefined);
|
|
350
|
+
if (origin === null) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
raw === undefined
|
|
353
|
+
? "Cursor GetServerConfig response missing agentUrlConfig.agentnUrl"
|
|
354
|
+
: "Cursor GetServerConfig returned an invalid Cursor agent URL",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return origin;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Discovery entry point: the live roster, as catalog rows. */
|
|
361
|
+
export async function discoverCursorModels(input: {
|
|
362
|
+
readonly credential: string;
|
|
363
|
+
readonly fetcher?: typeof fetch;
|
|
364
|
+
readonly signal?: AbortSignal;
|
|
365
|
+
}): Promise<readonly ModelDefinition[]> {
|
|
366
|
+
const models = await fetchCursorModels(input);
|
|
367
|
+
return models.map(cursorModelDefinition);
|
|
368
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Cursor Run transport: one validated HTTP/2 bidi stream, framed.
|
|
2
|
+
//
|
|
3
|
+
// This module owns the wire and nothing else. It opens the stream through the
|
|
4
|
+
// validated HTTP/2 binding, writes Connect frames, decodes response frames back
|
|
5
|
+
// into protobuf messages, and maps stream/HTTP/gRPC outcomes onto typed errors.
|
|
6
|
+
// It never calls `node:http2.connect` itself — the binding owns the socket, the
|
|
7
|
+
// DNS pinning, the SSRF policy, and the pool tunnel.
|
|
8
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
9
|
+
import type { ValidatedHttp2BidiStream, Http2BidiEndReason } from "../../../network/http2-bidi";
|
|
10
|
+
import type { ValidatedOutboundHttp2Bidi } from "../../provider-contracts";
|
|
11
|
+
import { decodeFramePayload, encodeFrame, streamFrames, FLAG_END_STREAM } from "./protocol/framing";
|
|
12
|
+
import { decodeMessage } from "./protocol/messages";
|
|
13
|
+
import {
|
|
14
|
+
CONTENT_TYPE_CONNECT_PROTO,
|
|
15
|
+
CONNECT_PROTOCOL_VERSION,
|
|
16
|
+
FALLBACK_CLIENT_VERSION,
|
|
17
|
+
RUN_PATH,
|
|
18
|
+
isAllowedAgentHost,
|
|
19
|
+
} from "./cursor-shared";
|
|
20
|
+
import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
|
|
21
|
+
|
|
22
|
+
import { isRecord } from "../../../protocol/codecs";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The refusal Cursor sends inside an otherwise-successful 200 Run stream.
|
|
26
|
+
*
|
|
27
|
+
* It is a Connect JSON error envelope, not protobuf, and it is where a real
|
|
28
|
+
* account-level refusal lands — a rate-limited or exhausted account streams a
|
|
29
|
+
* heartbeat and then this. Without it the Run looks like a deadlock: the
|
|
30
|
+
* stream opened, no text ever arrived, and nothing named the reason.
|
|
31
|
+
*/
|
|
32
|
+
function connectErrorEnvelope(payload: Uint8Array): GatewayError | undefined {
|
|
33
|
+
const text = new TextDecoder().decode(payload);
|
|
34
|
+
if (!text.trimStart().startsWith("{\"error\"")) return undefined;
|
|
35
|
+
let parsed: unknown;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!isRecord(parsed) || !isRecord(parsed["error"])) return undefined;
|
|
42
|
+
const error = parsed["error"];
|
|
43
|
+
const code = typeof error["code"] === "string" ? error["code"] : "";
|
|
44
|
+
const detail = cursorErrorDetail(error);
|
|
45
|
+
if (code === "resource_exhausted") {
|
|
46
|
+
return new GatewayError(
|
|
47
|
+
"quota_exceeded",
|
|
48
|
+
429,
|
|
49
|
+
`Cursor refused the Run: ${detail}`,
|
|
50
|
+
{ providerId: "cursor", upstreamCode: code },
|
|
51
|
+
"upstream",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return new GatewayError(
|
|
55
|
+
"platform_unavailable",
|
|
56
|
+
502,
|
|
57
|
+
`Cursor refused the Run: ${detail}`,
|
|
58
|
+
{ providerId: "cursor", upstreamCode: code },
|
|
59
|
+
"upstream",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The human-readable refusal, preferring Cursor's own `chatMessage`/`detail`. */
|
|
64
|
+
function cursorErrorDetail(error: Record<string, unknown>): string {
|
|
65
|
+
const raw = typeof error["message"] === "string" ? error["message"] : "";
|
|
66
|
+
for (const detail of Array.isArray(error["details"]) ? error["details"] : []) {
|
|
67
|
+
if (!isRecord(detail)) continue;
|
|
68
|
+
const debug = detail["debug"];
|
|
69
|
+
if (!isRecord(debug)) continue;
|
|
70
|
+
const info = debug["details"];
|
|
71
|
+
if (isRecord(info)) {
|
|
72
|
+
const chat = info["chatMessage"];
|
|
73
|
+
if (typeof chat === "string" && chat.length > 0) return chat;
|
|
74
|
+
const title = info["title"];
|
|
75
|
+
if (typeof title === "string" && title.length > 0) return title;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return raw.length > 0 ? raw : "Cursor refused the Run";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One decoded Cursor server message. */
|
|
82
|
+
export interface CursorServerMessage {
|
|
83
|
+
readonly raw: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read-idle bound for one Run stream. */
|
|
87
|
+
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
88
|
+
|
|
89
|
+
/** Cursor identity headers for the Run stream. */
|
|
90
|
+
async function runHeaders(credential: string, clientVersion: string): Promise<Record<string, string>> {
|
|
91
|
+
const { machineId, macMachineId } = await resolveCursorDeviceIds();
|
|
92
|
+
return {
|
|
93
|
+
authorization: `Bearer ${credential}`,
|
|
94
|
+
"content-type": CONTENT_TYPE_CONNECT_PROTO,
|
|
95
|
+
"connect-protocol-version": CONNECT_PROTOCOL_VERSION,
|
|
96
|
+
"connect-accept-encoding": "gzip,br",
|
|
97
|
+
"x-cursor-client-type": "cli",
|
|
98
|
+
"x-cursor-client-version": clientVersion,
|
|
99
|
+
"x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
|
|
100
|
+
// The CLI's streaming interceptor sets this on the bidi Run stream.
|
|
101
|
+
// Without it Cursor may treat the stream as non-streaming.
|
|
102
|
+
"x-cursor-streaming": "true",
|
|
103
|
+
"x-ghost-mode": "true",
|
|
104
|
+
"x-request-id": crypto.randomUUID(),
|
|
105
|
+
"user-agent": "connect-es/1.6.1",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Opens one Cursor Run stream.
|
|
111
|
+
*
|
|
112
|
+
* The host is re-validated here as well as in the binding: the Run origin comes
|
|
113
|
+
* from `GetServerConfig`, which is upstream data, so it is checked against the
|
|
114
|
+
* Cursor allowlist before a credentialed connection is opened to it.
|
|
115
|
+
*/
|
|
116
|
+
export async function openCursorRun(input: {
|
|
117
|
+
readonly origin: string;
|
|
118
|
+
readonly credential: string;
|
|
119
|
+
readonly outboundHttp2: ValidatedOutboundHttp2Bidi;
|
|
120
|
+
readonly signal: AbortSignal;
|
|
121
|
+
readonly clientVersion?: string;
|
|
122
|
+
readonly readIdleMs?: number;
|
|
123
|
+
}): Promise<{ stream: ValidatedHttp2BidiStream; write: (message: Uint8Array) => Promise<void> }> {
|
|
124
|
+
const url = new URL(`${input.origin}${RUN_PATH}`);
|
|
125
|
+
if (url.protocol !== "https:" || !isAllowedAgentHost(url.hostname)) {
|
|
126
|
+
throw new GatewayError(
|
|
127
|
+
"invalid_request",
|
|
128
|
+
400,
|
|
129
|
+
"Cursor Run requires an allowlisted Cursor agent origin",
|
|
130
|
+
{ origin: input.origin },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const headers = await runHeaders(input.credential, input.clientVersion ?? FALLBACK_CLIENT_VERSION);
|
|
134
|
+
const stream = await input.outboundHttp2(url, headers, {
|
|
135
|
+
signal: input.signal,
|
|
136
|
+
readIdleMs: input.readIdleMs ?? DEFAULT_READ_IDLE_MS,
|
|
137
|
+
allowHostname: isAllowedAgentHost,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
stream,
|
|
141
|
+
write: async (message: Uint8Array): Promise<void> => {
|
|
142
|
+
await stream.write(encodeFrame(0, message));
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Map of gRPC status codes onto gateway errors. */
|
|
148
|
+
function grpcError(status: string, message: string | undefined): GatewayError {
|
|
149
|
+
const code = Number(status);
|
|
150
|
+
const detail = message === undefined || message.length === 0 ? "" : `: ${message}`;
|
|
151
|
+
if (code === 16 || code === 7) {
|
|
152
|
+
// UNAUTHENTICATED / PERMISSION_DENIED: the credential, not the request.
|
|
153
|
+
return new GatewayError(
|
|
154
|
+
"authentication_failed",
|
|
155
|
+
401,
|
|
156
|
+
`Cursor refused the credential${detail}`,
|
|
157
|
+
{ grpcStatus: status },
|
|
158
|
+
"upstream",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (code === 8 || code === 14) {
|
|
162
|
+
return new GatewayError(
|
|
163
|
+
"quota_exceeded",
|
|
164
|
+
429,
|
|
165
|
+
`Cursor refused the Run${detail}`,
|
|
166
|
+
{ grpcStatus: status },
|
|
167
|
+
"upstream",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (code === 4 || code === 10) {
|
|
171
|
+
return new GatewayError(
|
|
172
|
+
"deadline_exceeded",
|
|
173
|
+
504,
|
|
174
|
+
`Cursor Run ended before it finished${detail}`,
|
|
175
|
+
{ grpcStatus: status },
|
|
176
|
+
"upstream",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return new GatewayError(
|
|
180
|
+
"platform_unavailable",
|
|
181
|
+
502,
|
|
182
|
+
`Cursor Run failed (gRPC ${status})${detail}`,
|
|
183
|
+
{ grpcStatus: status },
|
|
184
|
+
"upstream",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The terminal error for one ended Run stream.
|
|
190
|
+
*
|
|
191
|
+
* Status, trailers, and transport error are read in that order because that is
|
|
192
|
+
* the order of authority: an HTTP status is what the edge answered, a gRPC
|
|
193
|
+
* trailer is what the server decided, and a transport error is what the socket
|
|
194
|
+
* did. A clean close with none of them is still a failure — a Run that ends
|
|
195
|
+
* without `turn_ended` produced no answer.
|
|
196
|
+
*/
|
|
197
|
+
export function cursorRunTerminationError(input: {
|
|
198
|
+
readonly stream: ValidatedHttp2BidiStream;
|
|
199
|
+
readonly reason: Http2BidiEndReason;
|
|
200
|
+
}): GatewayError | undefined {
|
|
201
|
+
if (input.reason.kind === "local-close") return undefined;
|
|
202
|
+
if (input.reason.kind === "remote-error") {
|
|
203
|
+
return input.reason.error instanceof GatewayError
|
|
204
|
+
? input.reason.error
|
|
205
|
+
: new GatewayError(
|
|
206
|
+
"transport_unavailable",
|
|
207
|
+
502,
|
|
208
|
+
`Cursor Run transport interrupted: ${input.reason.error.message}`,
|
|
209
|
+
{},
|
|
210
|
+
"network",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const { status, headers } = input.stream.response;
|
|
214
|
+
if (status !== 0 && status !== 200) {
|
|
215
|
+
const message = headers["grpc-message"];
|
|
216
|
+
return new GatewayError(
|
|
217
|
+
status === 401 || status === 403 ? "authentication_failed" : "platform_unavailable",
|
|
218
|
+
status === 401 || status === 403 ? 401 : 502,
|
|
219
|
+
`Cursor Run failed by remote: ${status}`,
|
|
220
|
+
{ httpStatus: status, ...(message === undefined ? {} : { upstreamMessage: message }) },
|
|
221
|
+
"upstream",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const trailers = input.stream.trailers;
|
|
225
|
+
const grpcStatus = trailers["grpc-status"] ?? headers["grpc-status"];
|
|
226
|
+
if (grpcStatus !== undefined && grpcStatus !== "0") {
|
|
227
|
+
return grpcError(grpcStatus, trailers["grpc-message"] ?? headers["grpc-message"]);
|
|
228
|
+
}
|
|
229
|
+
return new GatewayError(
|
|
230
|
+
"platform_unavailable",
|
|
231
|
+
502,
|
|
232
|
+
"Cursor Run ended before turn_ended",
|
|
233
|
+
{},
|
|
234
|
+
"upstream",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Decodes a response byte stream into Cursor server messages.
|
|
240
|
+
*
|
|
241
|
+
* Buffering is bounded and the leftover is carried forward rather than
|
|
242
|
+
* re-merged from scratch: a Run frame can be many megabytes, and copying the
|
|
243
|
+
* whole pending buffer on every chunk is what turns a long Run into a stall.
|
|
244
|
+
*/
|
|
245
|
+
export async function* decodeServerMessages(
|
|
246
|
+
body: AsyncIterable<Uint8Array>,
|
|
247
|
+
maxBufferedBytes = 64 * 1024 * 1024,
|
|
248
|
+
): AsyncGenerator<Record<string, unknown>, void, void> {
|
|
249
|
+
let pending: Uint8Array = new Uint8Array(0);
|
|
250
|
+
for await (const chunk of body) {
|
|
251
|
+
pending = pending.byteLength === 0 ? chunk : concat(pending, chunk);
|
|
252
|
+
if (pending.byteLength > maxBufferedBytes) {
|
|
253
|
+
throw new GatewayError(
|
|
254
|
+
"platform_unavailable",
|
|
255
|
+
502,
|
|
256
|
+
"Cursor Run exceeded the frame buffer bound",
|
|
257
|
+
{},
|
|
258
|
+
"upstream",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const frames = [...streamFrames(pending)];
|
|
262
|
+
if (frames.length === 0) continue;
|
|
263
|
+
let consumed = 0;
|
|
264
|
+
for (const frame of frames) consumed += 5 + frame.payload.byteLength;
|
|
265
|
+
const leftover = pending.subarray(consumed);
|
|
266
|
+
pending = leftover.byteLength === 0 ? new Uint8Array(0) : new Uint8Array(leftover);
|
|
267
|
+
for (const frame of frames) {
|
|
268
|
+
const payload = decodeFramePayload(frame);
|
|
269
|
+
// A refusal is a Connect JSON error envelope, not protobuf: an exhausted
|
|
270
|
+
// account answers with one inside a normal 200 stream. Surfacing it is
|
|
271
|
+
// the difference between "the model said nothing" and "you hit your
|
|
272
|
+
// usage limit".
|
|
273
|
+
const envelope = connectErrorEnvelope(payload);
|
|
274
|
+
if (envelope !== undefined) throw envelope;
|
|
275
|
+
try {
|
|
276
|
+
yield decodeMessage("AgentServerMessage", payload);
|
|
277
|
+
} catch {
|
|
278
|
+
// One undecodable frame is not a protocol failure to abort on: Cursor
|
|
279
|
+
// extends this schema with fields this vendored copy does not model, and
|
|
280
|
+
// a Run that produced usable output must not die on one unknown field.
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// `END_STREAM` ends the *response*, not the payload: Cursor sets it on the
|
|
284
|
+
// last frame of a Run, which is exactly the frame carrying `turn_ended`
|
|
285
|
+
// and the model's final text. Returning before decoding it dropped every
|
|
286
|
+
// answer and left the Run looking like a heartbeat-only deadlock.
|
|
287
|
+
if ((frame.flags & FLAG_END_STREAM) !== 0) return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function concat(left: Uint8Array, right: Uint8Array): Uint8Array {
|
|
293
|
+
const out = new Uint8Array(left.byteLength + right.byteLength);
|
|
294
|
+
out.set(left, 0);
|
|
295
|
+
out.set(right, left.byteLength);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Heartbeat cadence while a Run is held open waiting for a tool result. */
|
|
300
|
+
export const RUN_HEARTBEAT_INTERVAL_MS = 20_000;
|