cartethyia 2.8.0 → 2.8.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/.env.example +3 -3
- package/dist/dashboard/assets/{BannedPage-Cc0vKpMG.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bqj2aa1T.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-CXsaOAO2.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BeLpJ4ma.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DnV0uZ3m.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-CBxAjo63.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-B5yEE0k6.js → HealthEventsModal-Clq8E9cW.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BWFLECmz.js → InjectSystemPromptPanel-6Teoq9Se.js} +1 -1
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CgoPc5B9.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-D9bFQern.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-B_p4tL8k.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-D_0PiZtg.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-CLI2bJTK.js → ProvidersPage-C3fYuu6i.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-DA-a1iSN.js → QuotaPage-D1RV_2BX.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CctTga2C.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-febLU4r1.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-M6fvR5Wz.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-nyeuRqz9.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BFOlvrJC.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CqTLw5e7.js → assert-BMcj1xZE.js} +1 -1
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-C83EqLB0.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-D1xn7pIe.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-DU3Lq5Lm.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-Dw1U_drK.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CEL5Sg_J.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BjFR5UHy.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-HvBW0AjI.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CECVELyy.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DYYofQ7W.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BSu3gFlt.js → entry-DScKYvZj.js} +3 -3
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DLW4i8zK.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-1-fCqe64.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DGvU2oLK.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-iNnO1X2K.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-0q8UGypb.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-C0w2bQjJ.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-M5aJ_xCa.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-BKeQGT3K.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-CivAk3da.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-CpzvI5ep.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/{routing-BFQX2VBc.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-CXbck0jG.js → select-oO1FxMVb.js} +1 -1
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_XvMHh9.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DggRiCxE.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-ClFzfbYp.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CTIFxKqS.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BClG3yWj.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +1 -1
- package/src/boot/dependencies.ts +4 -6
- package/src/config/providers.ts +6 -5
- package/src/config/spec.ts +8 -8
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -2
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/observability/live.ts +2 -2
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +2 -2
- package/src/console/providers/catalog/store.ts +1914 -1907
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-tenant.ts +5 -5
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +1 -1
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +7 -7
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +2 -2
- package/src/network/ssrf.ts +1 -1
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/observability/telemetry-buffer.ts +1 -1
- package/src/persistence/schema.ts +9 -0
- package/src/persistence/ttl-cache.ts +1 -1
- package/src/protocol/codecs.ts +58 -1
- package/src/protocol/request/messages.ts +31 -18
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +5 -0
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +7 -3
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -739
- package/src/providers/default-registry.ts +76 -0
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +6 -2
- package/src/providers/discovery/probing-service.ts +7 -0
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +11 -0
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +10 -10
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +5 -5
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +4 -4
- package/src/providers/integrations/buddy/{buddy-identity.ts → headers.ts} +138 -140
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +3 -4
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -1
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-shared.ts +1 -1
- package/src/providers/integrations/cursor/cursor.ts +485 -70
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/factory/factory-oauth.ts +16 -7
- package/src/providers/integrations/freebuff/freebuff.ts +2 -2
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +4 -4
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/hermes/hermes.ts +34 -5
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-protocol.ts +0 -3
- package/src/providers/integrations/perch/perch.ts +3 -3
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
- package/src/providers/integrations/qoder/qoder-adapter.ts +19 -113
- package/src/providers/integrations/qoder/qoder-auth.ts +8 -1
- package/src/providers/integrations/qoder/qoder-catalog.ts +1 -1
- package/src/providers/integrations/qoder/qoder-chat.ts +25 -8
- package/src/providers/integrations/qoder/qoder-checkin.ts +1 -1
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -25
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-quota.ts +15 -1
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zro/zro.ts +2 -1
- package/src/providers/operations/account-refresh-status.ts +14 -0
- package/src/providers/operations/client-versions.ts +95 -30
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/provider-caches.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +22 -25
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +2 -2
- package/src/providers/provider-metadata.ts +30 -0
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/refresh.ts +25 -3
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +1 -1
- package/src/transport/dispatch/attempt-loop.ts +3 -3
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +1 -1
- package/src/transport/dispatch/upstream.ts +1 -1
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +0 -2
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/src/console/providers/catalog/discover-constants.ts +0 -6
- package/src/network/pool-health.ts +0 -198
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -188
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -179
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -870
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -481
- /package/dist/dashboard/assets/{app-DIXLjojg.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-DIXLjojg.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{byte-accounting.ts → accounting.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
- /package/src/network/{rate-limit-verdict.ts → verdict.ts} +0 -0
|
@@ -6,19 +6,21 @@
|
|
|
6
6
|
// It never calls `node:http2.connect` itself — the binding owns the socket, the
|
|
7
7
|
// DNS pinning, the SSRF policy, and the pool tunnel.
|
|
8
8
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
9
|
-
import type { ValidatedHttp2BidiStream, Http2BidiEndReason } from "../../../network/
|
|
9
|
+
import type { ValidatedHttp2BidiStream, Http2BidiEndReason } from "../../../network/bidi";
|
|
10
10
|
import type { ValidatedOutboundHttp2Bidi } from "../../provider-contracts";
|
|
11
11
|
import { decodeFramePayload, encodeFrame, streamFrames, FLAG_END_STREAM } from "./protocol/framing";
|
|
12
12
|
import { decodeMessage } from "./protocol/messages";
|
|
13
13
|
import {
|
|
14
14
|
CONTENT_TYPE_CONNECT_PROTO,
|
|
15
15
|
CONNECT_PROTOCOL_VERSION,
|
|
16
|
+
CURSOR_API_BASE_URL,
|
|
16
17
|
FALLBACK_CLIENT_VERSION,
|
|
17
18
|
RUN_PATH,
|
|
18
19
|
isAllowedAgentHost,
|
|
19
20
|
resolveCursorTimezone,
|
|
20
21
|
} from "./cursor-shared";
|
|
21
22
|
import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
|
|
23
|
+
import { fnv1a32 } from "../shared/hash";
|
|
22
24
|
|
|
23
25
|
import { isRecord } from "../../../protocol/codecs";
|
|
24
26
|
|
|
@@ -88,7 +90,11 @@ export interface CursorServerMessage {
|
|
|
88
90
|
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
89
91
|
|
|
90
92
|
/** Cursor identity headers for the Run stream. */
|
|
91
|
-
async function runHeaders(
|
|
93
|
+
async function runHeaders(
|
|
94
|
+
credential: string,
|
|
95
|
+
clientVersion: string,
|
|
96
|
+
requestId: string,
|
|
97
|
+
): Promise<Record<string, string>> {
|
|
92
98
|
const { machineId, macMachineId } = await resolveCursorDeviceIds();
|
|
93
99
|
return {
|
|
94
100
|
authorization: `Bearer ${credential}`,
|
|
@@ -102,7 +108,7 @@ async function runHeaders(credential: string, clientVersion: string): Promise<Re
|
|
|
102
108
|
// Without it Cursor may treat the stream as non-streaming.
|
|
103
109
|
"x-cursor-streaming": "true",
|
|
104
110
|
"x-ghost-mode": "true",
|
|
105
|
-
"x-request-id":
|
|
111
|
+
"x-request-id": requestId,
|
|
106
112
|
...(resolveCursorTimezone() === "" ? {} : { "x-cursor-timezone": resolveCursorTimezone() }),
|
|
107
113
|
"user-agent": "connect-es/1.6.1",
|
|
108
114
|
};
|
|
@@ -122,7 +128,8 @@ export async function openCursorRun(input: {
|
|
|
122
128
|
readonly signal: AbortSignal;
|
|
123
129
|
readonly clientVersion?: string;
|
|
124
130
|
readonly readIdleMs?: number;
|
|
125
|
-
|
|
131
|
+
readonly requestId?: string;
|
|
132
|
+
}): Promise<{ stream: ValidatedHttp2BidiStream; requestId: string; write: (message: Uint8Array) => Promise<void> }> {
|
|
126
133
|
const url = new URL(`${input.origin}${RUN_PATH}`);
|
|
127
134
|
if (url.protocol !== "https:" || !isAllowedAgentHost(url.hostname)) {
|
|
128
135
|
throw new GatewayError(
|
|
@@ -132,7 +139,8 @@ export async function openCursorRun(input: {
|
|
|
132
139
|
{ origin: input.origin },
|
|
133
140
|
);
|
|
134
141
|
}
|
|
135
|
-
const
|
|
142
|
+
const requestId = input.requestId ?? crypto.randomUUID();
|
|
143
|
+
const headers = await runHeaders(input.credential, input.clientVersion ?? FALLBACK_CLIENT_VERSION, requestId);
|
|
136
144
|
const stream = await input.outboundHttp2(url, headers, {
|
|
137
145
|
signal: input.signal,
|
|
138
146
|
readIdleMs: input.readIdleMs ?? DEFAULT_READ_IDLE_MS,
|
|
@@ -140,6 +148,7 @@ export async function openCursorRun(input: {
|
|
|
140
148
|
});
|
|
141
149
|
return {
|
|
142
150
|
stream,
|
|
151
|
+
requestId,
|
|
143
152
|
write: async (message: Uint8Array): Promise<void> => {
|
|
144
153
|
await stream.write(encodeFrame(0, message));
|
|
145
154
|
},
|
|
@@ -275,7 +284,13 @@ export async function* decodeServerMessages(
|
|
|
275
284
|
const envelope = connectErrorEnvelope(payload);
|
|
276
285
|
if (envelope !== undefined) throw envelope;
|
|
277
286
|
try {
|
|
278
|
-
|
|
287
|
+
const decoded = decodeMessage("AgentServerMessage", payload);
|
|
288
|
+
// Shape-only wire traces and checkpoint retention need the raw frame,
|
|
289
|
+
// but the decoded message must stay a plain protobuf object for the
|
|
290
|
+
// adapter (and for encodeMessage round-trips). Non-enumerable keeps it
|
|
291
|
+
// out of Object.keys/JSON, so no existing consumer sees a new field.
|
|
292
|
+
Object.defineProperty(decoded, "__bytes", { value: payload, enumerable: false });
|
|
293
|
+
yield decoded;
|
|
279
294
|
} catch {
|
|
280
295
|
// One undecodable frame is not a protocol failure to abort on: Cursor
|
|
281
296
|
// extends this schema with fields this vendored copy does not model, and
|
|
@@ -297,3 +312,587 @@ function concat(left: Uint8Array, right: Uint8Array): Uint8Array {
|
|
|
297
312
|
out.set(right, left.byteLength);
|
|
298
313
|
return out;
|
|
299
314
|
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Normalized Run transport, and the HTTP/1.1 fallback.
|
|
318
|
+
//
|
|
319
|
+
// HTTP/2 is the Run's native shape, but a network that cannot carry a
|
|
320
|
+
// bidirectional HTTP/2 stream (a tunneling proxy, an ALPN-restricted middlebox)
|
|
321
|
+
// must not turn Cursor into a provider that cannot serve at all: Cursor's own
|
|
322
|
+
// clients run the same conversation over HTTP/1.1, with the server's frames
|
|
323
|
+
// down one `AgentService/RunSSE` stream and each client message up as a
|
|
324
|
+
// numbered `BidiService/BidiAppend` call (donor wire, verified: the append
|
|
325
|
+
// body is raw protobuf — hex message, the request id, the sequence number —
|
|
326
|
+
// not a Connect frame).
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/** Why one Run transport ended, with the failure the pump should surface. */
|
|
330
|
+
export interface CursorRunTerminal {
|
|
331
|
+
readonly kind: "remote-error" | "remote-clean-close" | "local-close";
|
|
332
|
+
readonly failure: GatewayError | undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* One Run's wire, normalized across transports.
|
|
337
|
+
*
|
|
338
|
+
* `body` yields raw response bytes (Connect-framed server messages) and
|
|
339
|
+
* `write` sends one framed client message; the frame pump in the adapter
|
|
340
|
+
* consumes both without knowing which transport is underneath.
|
|
341
|
+
*/
|
|
342
|
+
export interface CursorRunTransport {
|
|
343
|
+
readonly mode: "http2" | "http1";
|
|
344
|
+
readonly body: AsyncIterable<Uint8Array>;
|
|
345
|
+
write(message: Uint8Array): Promise<void>;
|
|
346
|
+
abort(): void;
|
|
347
|
+
/** Terminal listener; at most one terminal is delivered per transport. */
|
|
348
|
+
onTerminal(listener: (terminal: CursorRunTerminal) => void): () => void;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Wraps the validated HTTP/2 stream in the normalized transport shape. */
|
|
352
|
+
export function wrapHttp2Run(
|
|
353
|
+
stream: ValidatedHttp2BidiStream,
|
|
354
|
+
write: (message: Uint8Array) => Promise<void>,
|
|
355
|
+
): CursorRunTransport {
|
|
356
|
+
const listeners = new Set<(terminal: CursorRunTerminal) => void>();
|
|
357
|
+
let settled = false;
|
|
358
|
+
const settle = (terminal: CursorRunTerminal): void => {
|
|
359
|
+
if (settled) return;
|
|
360
|
+
settled = true;
|
|
361
|
+
for (const listener of [...listeners]) {
|
|
362
|
+
try {
|
|
363
|
+
listener(terminal);
|
|
364
|
+
} catch {
|
|
365
|
+
// A listener failure must not skip the remaining listeners.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
listeners.clear();
|
|
369
|
+
};
|
|
370
|
+
stream.onTerminal((reason) => {
|
|
371
|
+
if (reason.kind === "local-close") {
|
|
372
|
+
settle({ kind: "local-close", failure: undefined });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (reason.kind === "remote-clean-close") {
|
|
376
|
+
// A clean close carries no failure of its own: the pump decides whether
|
|
377
|
+
// a turn that ended without `turn_ended` is complete (text/step/tool
|
|
378
|
+
// batch) or quiet. `cursorRunTerminationError` is for status/trailer
|
|
379
|
+
// refusals, not for a stream the server simply closed.
|
|
380
|
+
settle({ kind: "remote-clean-close", failure: undefined });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
settle({
|
|
384
|
+
kind: "remote-error",
|
|
385
|
+
failure: cursorRunTerminationError({ stream, reason }) ?? undefined,
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
mode: "http2",
|
|
390
|
+
body: stream.body,
|
|
391
|
+
write,
|
|
392
|
+
abort: () => stream.abort(),
|
|
393
|
+
onTerminal: (listener) => {
|
|
394
|
+
listeners.add(listener);
|
|
395
|
+
return () => listeners.delete(listener);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** HTTP/1.1 Run constants, as the donor's clients pin them. */
|
|
401
|
+
const H1 = {
|
|
402
|
+
/** ms a BidiAppend may take before the Run fails (donor appendWait). */
|
|
403
|
+
appendWaitMs: 60_000,
|
|
404
|
+
/** BidiAppends allowed on the wire at once (donor atOnce). */
|
|
405
|
+
atOnce: 16,
|
|
406
|
+
} as const;
|
|
407
|
+
|
|
408
|
+
/** ms after one h2-blocked open that fresh Runs go straight to HTTP/1.1. */
|
|
409
|
+
const H2_RETRY_AFTER_MS = 10 * 60_000;
|
|
410
|
+
/** ms an HTTP/2 open may take before the transport itself is declared blocked. */
|
|
411
|
+
const H2_HEAD_WAIT_MS = 15_000;
|
|
412
|
+
|
|
413
|
+
/** Per-credential HTTP/1.1 window after one h2-blocked open, digest-keyed. */
|
|
414
|
+
const h1UntilByCredential = new Map<string, number>();
|
|
415
|
+
|
|
416
|
+
function credentialDigest(credential: string): string {
|
|
417
|
+
return String(fnv1a32(credential));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** True when the failure to open an HTTP/2 Run says HTTP/2 itself is blocked. */
|
|
421
|
+
export function isH2BlockedOpen(error: unknown): boolean {
|
|
422
|
+
if (!(error instanceof Error)) return false;
|
|
423
|
+
const code = String((error as Error & { code?: string }).code ?? "");
|
|
424
|
+
if (/^ERR_HTTP2|^EPROTO/.test(code)) return true;
|
|
425
|
+
const message = `${error.message} ${code}`.toLowerCase();
|
|
426
|
+
return (
|
|
427
|
+
/http\/?2|nghttp2|alpn|protocol error|settings frame|flow control/.test(message) &&
|
|
428
|
+
!/refused|enotfound|econn|enetunreach|ehostunreach|epipe/.test(message)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Whether a failed Run *open* is worth retrying on a fresh transport.
|
|
434
|
+
*
|
|
435
|
+
* Connection and deadline failures are retried; auth, quota, and request-shape
|
|
436
|
+
* refusals are not — retrying those just burns the account's remaining window.
|
|
437
|
+
*/
|
|
438
|
+
export function isRetryableOpenFailure(error: unknown): boolean {
|
|
439
|
+
if (!(error instanceof GatewayError)) {
|
|
440
|
+
// Raw transport throws (DNS, dial, TLS) come through as plain errors.
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (error.code === "authentication_failed") return false;
|
|
444
|
+
if (error.code === "quota_exceeded") return false;
|
|
445
|
+
if (error.code === "invalid_request") return false;
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Backoff for open retries: 500 ms doubling, capped at 8 s (donor policy). */
|
|
450
|
+
export function openRetryDelayMs(attempt: number): number {
|
|
451
|
+
return Math.min(500 * 2 ** attempt, 8_000);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const timer = setTimeout(resolve, ms);
|
|
457
|
+
timer.unref?.();
|
|
458
|
+
signal.addEventListener(
|
|
459
|
+
"abort",
|
|
460
|
+
() => {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
463
|
+
},
|
|
464
|
+
{ once: true },
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Applies the open-retry backoff; exported as part of the transport policy. */
|
|
470
|
+
export { sleep as cursorSleep };
|
|
471
|
+
|
|
472
|
+
/** Connect's code for an HTTP status that carried no Connect error of its own. */
|
|
473
|
+
function connectCodeForStatus(status: number): string {
|
|
474
|
+
return { 400: "invalid_argument", 401: "unauthenticated", 403: "permission_denied", 429: "resource_exhausted" }[
|
|
475
|
+
status
|
|
476
|
+
] ?? "unavailable";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Maps an HTTP/1.1 Run failure onto the gateway's error taxonomy. */
|
|
480
|
+
function h1OpenFailure(status: number, message: string): GatewayError {
|
|
481
|
+
if (status === 401 || status === 403) {
|
|
482
|
+
return new GatewayError(
|
|
483
|
+
"authentication_failed",
|
|
484
|
+
401,
|
|
485
|
+
`Cursor refused the credential over HTTP/1.1: ${message} (request {requestId}). Wrong key, right door — check the credential 🔑`,
|
|
486
|
+
{},
|
|
487
|
+
"upstream",
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (status === 429) {
|
|
491
|
+
return new GatewayError(
|
|
492
|
+
"quota_exceeded",
|
|
493
|
+
429,
|
|
494
|
+
`Cursor refused the Run over HTTP/1.1: ${message} (request {requestId}). You've maxed this window — pace yourself ⏳`,
|
|
495
|
+
{},
|
|
496
|
+
"upstream",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return new GatewayError(
|
|
500
|
+
"platform_unavailable",
|
|
501
|
+
502,
|
|
502
|
+
`Cursor Run failed over HTTP/1.1: ${message} (request {requestId}). The upstream came back empty-handed — retry 🌙`,
|
|
503
|
+
{},
|
|
504
|
+
"upstream",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** varint encoding for the BidiAppend body builder. */
|
|
509
|
+
function appendVarint(out: number[], value: number): void {
|
|
510
|
+
let v = value;
|
|
511
|
+
while (v > 0x7f) {
|
|
512
|
+
out.push((v & 0x7f) | 0x80);
|
|
513
|
+
v >>>= 7;
|
|
514
|
+
}
|
|
515
|
+
out.push(v);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Builds one raw `aiserver.v1.BidiAppendRequest` body:
|
|
520
|
+
* field 1 `data` (hex string), field 2 `requestId` (message, field 1 string),
|
|
521
|
+
* field 3 `appendSeqno` (int64). Verified against the donor's wire encoder.
|
|
522
|
+
*/
|
|
523
|
+
function bidiAppendBody(message: Uint8Array, requestId: string, seqno: number): Uint8Array {
|
|
524
|
+
const hex = Buffer.from(message).toString("hex");
|
|
525
|
+
const data = new TextEncoder().encode(hex);
|
|
526
|
+
const id = new TextEncoder().encode(requestId);
|
|
527
|
+
const out: number[] = [];
|
|
528
|
+
out.push(0x0a);
|
|
529
|
+
appendVarint(out, data.byteLength);
|
|
530
|
+
for (const byte of data) out.push(byte);
|
|
531
|
+
out.push(0x12, 2 + id.byteLength, 0x0a);
|
|
532
|
+
appendVarint(out, id.byteLength);
|
|
533
|
+
for (const byte of id) out.push(byte);
|
|
534
|
+
out.push(0x18);
|
|
535
|
+
appendVarint(out, seqno);
|
|
536
|
+
return new Uint8Array(out);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** True when a client message is a KV blob reply, which no append waits on. */
|
|
540
|
+
function isKvReply(message: Uint8Array): boolean {
|
|
541
|
+
return message.byteLength > 0 && message[0] === 0x1a;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Opens one Run over HTTP/1.1: the server's frames come down one RunSSE
|
|
546
|
+
* stream, every client message goes up as a numbered BidiAppend call.
|
|
547
|
+
*
|
|
548
|
+
* Failures of an append or of the down stream end the Run with a synthesized
|
|
549
|
+
* Connect error envelope (END_STREAM), so the shared decoder surfaces it as
|
|
550
|
+
* the Run's own refusal — the pump cannot tell the transports apart.
|
|
551
|
+
*/
|
|
552
|
+
export async function openCursorRunH1(input: {
|
|
553
|
+
readonly requestId: string;
|
|
554
|
+
readonly credential: string;
|
|
555
|
+
readonly fetcher: typeof fetch;
|
|
556
|
+
readonly signal: AbortSignal;
|
|
557
|
+
readonly clientVersion?: string;
|
|
558
|
+
readonly readIdleMs?: number;
|
|
559
|
+
}): Promise<CursorRunTransport> {
|
|
560
|
+
const ac = new AbortController();
|
|
561
|
+
const forwardAbort = (): void => ac.abort();
|
|
562
|
+
input.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
563
|
+
if (input.signal.aborted) ac.abort();
|
|
564
|
+
|
|
565
|
+
const headers = await runHeaders(input.credential, input.clientVersion ?? FALLBACK_CLIENT_VERSION, input.requestId);
|
|
566
|
+
const wireHeaders = {
|
|
567
|
+
...headers,
|
|
568
|
+
"accept-encoding": "identity",
|
|
569
|
+
"x-cursor-streaming": "true",
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const listeners = new Set<(terminal: CursorRunTerminal) => void>();
|
|
573
|
+
let ended = false;
|
|
574
|
+
let settled = false;
|
|
575
|
+
let seqno = 0;
|
|
576
|
+
const inflight = new Set<Promise<void>>();
|
|
577
|
+
|
|
578
|
+
const queue: Array<Uint8Array | null> = [];
|
|
579
|
+
let waiter: (() => void) | undefined;
|
|
580
|
+
const pushDown = (chunk: Uint8Array | null): void => {
|
|
581
|
+
queue.push(chunk);
|
|
582
|
+
const wake = waiter;
|
|
583
|
+
waiter = undefined;
|
|
584
|
+
wake?.();
|
|
585
|
+
};
|
|
586
|
+
const settle = (terminal: CursorRunTerminal): void => {
|
|
587
|
+
if (settled) return;
|
|
588
|
+
settled = true;
|
|
589
|
+
for (const listener of [...listeners]) {
|
|
590
|
+
try {
|
|
591
|
+
listener(terminal);
|
|
592
|
+
} catch {
|
|
593
|
+
// A listener failure must not skip the remaining listeners.
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
listeners.clear();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const failStream = (code: string, message: string, status: number): void => {
|
|
600
|
+
if (ended) return;
|
|
601
|
+
ended = true;
|
|
602
|
+
// Synthesized Connect error envelope: the shared decoder reads it as the
|
|
603
|
+
// Run's own refusal, exactly as it does over HTTP/2.
|
|
604
|
+
const json = new TextEncoder().encode(JSON.stringify({ error: { code, message } }));
|
|
605
|
+
pushDown(encodeFrame(FLAG_END_STREAM, json));
|
|
606
|
+
pushDown(null);
|
|
607
|
+
settle({ kind: "remote-error", failure: h1OpenFailure(status, message) });
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const endCleanly = (): void => {
|
|
611
|
+
if (ended || inflight.size > 0) return;
|
|
612
|
+
ended = true;
|
|
613
|
+
pushDown(null);
|
|
614
|
+
settle({ kind: "remote-clean-close", failure: undefined });
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const append = async (message: Uint8Array): Promise<void> => {
|
|
618
|
+
const seq = seqno;
|
|
619
|
+
seqno += 1;
|
|
620
|
+
const body = bidiAppendBody(message, input.requestId, seq);
|
|
621
|
+
const deadlineMs = H1.appendWaitMs + Math.ceil(body.byteLength / 65_536) * 1_000;
|
|
622
|
+
const deadline = AbortSignal.timeout(deadlineMs);
|
|
623
|
+
try {
|
|
624
|
+
const response = await input.fetcher(`${CURSOR_API_BASE_URL}/aiserver.v1.BidiService/BidiAppend`, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: { ...wireHeaders, "content-type": "application/proto" },
|
|
627
|
+
body: body as unknown as BodyInit,
|
|
628
|
+
signal: AbortSignal.any([ac.signal, deadline]),
|
|
629
|
+
});
|
|
630
|
+
if (!response.ok) {
|
|
631
|
+
const text = await response.text().catch(() => "");
|
|
632
|
+
failStream(connectCodeForStatus(response.status), text.trim() || `HTTP ${response.status}`, response.status);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
await response.arrayBuffer().catch(() => undefined);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (ac.signal.aborted || input.signal.aborted) return;
|
|
638
|
+
if (deadline.aborted) {
|
|
639
|
+
failStream("deadline_exceeded", `BidiAppend got no answer in ${Math.round(deadlineMs / 1_000)}s`, 504);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
failStream("unavailable", `BidiAppend: ${error instanceof Error ? error.message : String(error)}`, 502);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const track = (promise: Promise<void>): void => {
|
|
647
|
+
inflight.add(promise);
|
|
648
|
+
void promise.finally(() => {
|
|
649
|
+
inflight.delete(promise);
|
|
650
|
+
// A down stream that already ended cleanly while appends were on the
|
|
651
|
+
// wire becomes terminal only once they have all been answered.
|
|
652
|
+
if (inflight.size === 0 && sseDone && !ended) endCleanly();
|
|
653
|
+
});
|
|
654
|
+
};
|
|
655
|
+
let sseDone = false;
|
|
656
|
+
/** One append whose completion the write path waits on. */
|
|
657
|
+
const appendTracked = (message: Uint8Array): Promise<void> => {
|
|
658
|
+
const promise = append(message);
|
|
659
|
+
track(promise);
|
|
660
|
+
return promise;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const transport: CursorRunTransport = {
|
|
664
|
+
mode: "http1",
|
|
665
|
+
body: {
|
|
666
|
+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
|
|
667
|
+
return {
|
|
668
|
+
next: async (): Promise<IteratorResult<Uint8Array>> => {
|
|
669
|
+
for (;;) {
|
|
670
|
+
const front = queue.shift();
|
|
671
|
+
if (front === undefined) {
|
|
672
|
+
if (ended) return { done: true, value: undefined };
|
|
673
|
+
await new Promise<void>((resolve) => {
|
|
674
|
+
waiter = resolve;
|
|
675
|
+
});
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (front === null) return { done: true, value: undefined };
|
|
679
|
+
return { done: false, value: front };
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
write: async (message: Uint8Array): Promise<void> => {
|
|
686
|
+
if (ended || ac.signal.aborted) {
|
|
687
|
+
throw new GatewayError(
|
|
688
|
+
"transport_unavailable",
|
|
689
|
+
502,
|
|
690
|
+
"Cursor Run transport is closed (request {requestId}). The connection ghosted us — come back in a moment 👻",
|
|
691
|
+
{},
|
|
692
|
+
"network",
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
// KV blob replies never wait: nothing else depends on them, and Cursor
|
|
696
|
+
// asks for a fresh Run's whole history at once (donor kvReply rule).
|
|
697
|
+
if (isKvReply(message)) {
|
|
698
|
+
track(append(message));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// Every other message serializes: it goes up only after the appends
|
|
702
|
+
// before it were answered, in sequence order (donor ordering rule).
|
|
703
|
+
while (inflight.size > 0 && !ended && !ac.signal.aborted) {
|
|
704
|
+
await Promise.race([...inflight]);
|
|
705
|
+
}
|
|
706
|
+
if (ended || ac.signal.aborted) {
|
|
707
|
+
throw new GatewayError(
|
|
708
|
+
"transport_unavailable",
|
|
709
|
+
502,
|
|
710
|
+
"Cursor Run transport is closed (request {requestId}). The connection ghosted us — come back in a moment 👻",
|
|
711
|
+
{},
|
|
712
|
+
"network",
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
await appendTracked(message);
|
|
716
|
+
},
|
|
717
|
+
abort: () => {
|
|
718
|
+
ac.abort();
|
|
719
|
+
if (!ended) {
|
|
720
|
+
ended = true;
|
|
721
|
+
pushDown(null);
|
|
722
|
+
settle({ kind: "local-close", failure: undefined });
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
onTerminal: (listener) => {
|
|
726
|
+
listeners.add(listener);
|
|
727
|
+
return () => listeners.delete(listener);
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Down stream: RunSSE, opened with one Connect frame carrying the request
|
|
732
|
+
// id (field 1, string) — the linkage every append repeats. The opener is
|
|
733
|
+
// a regular message frame (flags 0), not an END_STREAM frame: END_STREAM
|
|
734
|
+
// ends the *response*, not the request body.
|
|
735
|
+
void (async () => {
|
|
736
|
+
try {
|
|
737
|
+
const id = new TextEncoder().encode(input.requestId);
|
|
738
|
+
const runOpen = new Uint8Array(2 + id.byteLength);
|
|
739
|
+
runOpen[0] = 0x0a;
|
|
740
|
+
runOpen[1] = id.byteLength;
|
|
741
|
+
runOpen.set(id, 2);
|
|
742
|
+
const response = await input.fetcher(`${CURSOR_API_BASE_URL}/agent.v1.AgentService/RunSSE`, {
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: wireHeaders,
|
|
745
|
+
body: encodeFrame(0, runOpen) as unknown as BodyInit,
|
|
746
|
+
signal: ac.signal,
|
|
747
|
+
});
|
|
748
|
+
if (!response.ok || response.body === null) {
|
|
749
|
+
const text = response.body === null ? "no response body" : await response.text().catch(() => "");
|
|
750
|
+
failStream(
|
|
751
|
+
connectCodeForStatus(response.status),
|
|
752
|
+
text.trim() || `HTTP ${response.status}`,
|
|
753
|
+
response.status,
|
|
754
|
+
);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const reader = response.body.getReader();
|
|
758
|
+
const readIdleMs = input.readIdleMs ?? DEFAULT_READ_IDLE_MS;
|
|
759
|
+
for (;;) {
|
|
760
|
+
if (ended) {
|
|
761
|
+
await reader.cancel().catch(() => undefined);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const chunk = await Promise.race([
|
|
765
|
+
reader.read(),
|
|
766
|
+
new Promise<never>((_, reject) => {
|
|
767
|
+
const timer = setTimeout(() => reject(new Error(`RunSSE idle past ${readIdleMs}ms`)), readIdleMs);
|
|
768
|
+
timer.unref?.();
|
|
769
|
+
}),
|
|
770
|
+
]);
|
|
771
|
+
if (chunk.done) {
|
|
772
|
+
sseDone = true;
|
|
773
|
+
endCleanly();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
pushDown(chunk.value);
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (ac.signal.aborted || input.signal.aborted) {
|
|
780
|
+
if (!ended) {
|
|
781
|
+
ended = true;
|
|
782
|
+
pushDown(null);
|
|
783
|
+
settle({ kind: "local-close", failure: undefined });
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
failStream("unavailable", `RunSSE: ${error instanceof Error ? error.message : String(error)}`, 502);
|
|
788
|
+
}
|
|
789
|
+
})();
|
|
790
|
+
|
|
791
|
+
return transport;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Opens one Cursor Run with the donor's transport policy: HTTP/2 first, and —
|
|
796
|
+
* when HTTP/2 itself is blocked here — HTTP/1.1 immediately, with fresh Runs
|
|
797
|
+
* staying on HTTP/1.1 for a window. A plain connection failure leaves the next
|
|
798
|
+
* Run on HTTP/2 again; it says nothing about HTTP/2 being unavailable.
|
|
799
|
+
*/
|
|
800
|
+
export async function openCursorRunWithFallback(input: {
|
|
801
|
+
readonly origin: string;
|
|
802
|
+
readonly credential: string;
|
|
803
|
+
readonly outboundHttp2: ValidatedOutboundHttp2Bidi | undefined;
|
|
804
|
+
readonly outboundFetch: typeof fetch | undefined;
|
|
805
|
+
readonly signal: AbortSignal;
|
|
806
|
+
readonly clientVersion?: string;
|
|
807
|
+
readonly readIdleMs?: number;
|
|
808
|
+
/** Stable request id across H2 → H1 fallback and stream retries. */
|
|
809
|
+
readonly requestId?: string;
|
|
810
|
+
/** Force HTTP/1.1 after a mid-stream HTTP/2 failure. */
|
|
811
|
+
readonly forceH1?: boolean;
|
|
812
|
+
}): Promise<{ transport: CursorRunTransport; requestId: string }> {
|
|
813
|
+
const digest = credentialDigest(input.credential);
|
|
814
|
+
const h1Until = h1UntilByCredential.get(digest) ?? 0;
|
|
815
|
+
if (input.forceH1 === true) h1UntilByCredential.set(digest, Date.now() + H2_RETRY_AFTER_MS);
|
|
816
|
+
if (input.forceH1 === true || Date.now() < h1Until || input.outboundHttp2 === undefined) {
|
|
817
|
+
return openH1OrThrow(input, input.requestId ?? crypto.randomUUID(), digest);
|
|
818
|
+
}
|
|
819
|
+
let openedPromise: ReturnType<typeof openCursorRun> | undefined;
|
|
820
|
+
try {
|
|
821
|
+
openedPromise = openCursorRun({
|
|
822
|
+
origin: input.origin,
|
|
823
|
+
credential: input.credential,
|
|
824
|
+
outboundHttp2: input.outboundHttp2,
|
|
825
|
+
signal: input.signal,
|
|
826
|
+
...(input.requestId === undefined ? {} : { requestId: input.requestId }),
|
|
827
|
+
...(input.clientVersion === undefined ? {} : { clientVersion: input.clientVersion }),
|
|
828
|
+
...(input.readIdleMs === undefined ? {} : { readIdleMs: input.readIdleMs }),
|
|
829
|
+
});
|
|
830
|
+
let headTimedOut = false;
|
|
831
|
+
let headTimer: ReturnType<typeof setTimeout> | undefined;
|
|
832
|
+
let opened: Awaited<typeof openedPromise>;
|
|
833
|
+
try {
|
|
834
|
+
opened = await Promise.race([
|
|
835
|
+
openedPromise,
|
|
836
|
+
new Promise<never>((_, reject) => {
|
|
837
|
+
headTimer = setTimeout(() => {
|
|
838
|
+
headTimedOut = true;
|
|
839
|
+
reject(Object.assign(new Error("HTTP/2 Run got no response head in 15s"), { name: "H2HeadTimeout" }));
|
|
840
|
+
}, H2_HEAD_WAIT_MS);
|
|
841
|
+
headTimer.unref?.();
|
|
842
|
+
}),
|
|
843
|
+
]);
|
|
844
|
+
} finally {
|
|
845
|
+
clearTimeout(headTimer);
|
|
846
|
+
// A head that arrived after the timeout is a live stream nobody owns:
|
|
847
|
+
// abort it instead of leaking the socket.
|
|
848
|
+
if (headTimedOut) void openedPromise.then((o) => o.stream.abort(), () => undefined);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
transport: wrapHttp2Run(opened.stream, opened.write),
|
|
852
|
+
requestId: opened.requestId,
|
|
853
|
+
};
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (!isH2BlockedOpen(error)) throw error;
|
|
856
|
+
h1UntilByCredential.set(digest, Date.now() + H2_RETRY_AFTER_MS);
|
|
857
|
+
// The donor keeps one request id across the fallback: RunSSE's opener and
|
|
858
|
+
// every BidiAppend repeat the id the HTTP/2 Run was opened with.
|
|
859
|
+
return openH1OrThrow(
|
|
860
|
+
input,
|
|
861
|
+
input.requestId ?? openedPromise?.then((o) => o.requestId).catch(() => crypto.randomUUID()) ?? crypto.randomUUID(),
|
|
862
|
+
digest,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Opens the HTTP/1.1 fallback, refusing when no validated egress can carry it. */
|
|
868
|
+
async function openH1OrThrow(
|
|
869
|
+
input: {
|
|
870
|
+
readonly credential: string;
|
|
871
|
+
readonly outboundFetch: typeof fetch | undefined;
|
|
872
|
+
readonly signal: AbortSignal;
|
|
873
|
+
readonly clientVersion?: string;
|
|
874
|
+
readonly readIdleMs?: number;
|
|
875
|
+
},
|
|
876
|
+
requestId: string | Promise<string>,
|
|
877
|
+
digest: string,
|
|
878
|
+
): Promise<{ transport: CursorRunTransport; requestId: string }> {
|
|
879
|
+
const id = await requestId;
|
|
880
|
+
if (input.outboundFetch === undefined) {
|
|
881
|
+
throw new GatewayError(
|
|
882
|
+
"admission_unavailable",
|
|
883
|
+
503,
|
|
884
|
+
"Cursor fell back to HTTP/1.1 but no validated egress is configured for this route (request {requestId}). No bridge to cross — configure a pool first 🏗️",
|
|
885
|
+
{ providerId: "cursor" },
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const transport = await openCursorRunH1({
|
|
889
|
+
requestId: id,
|
|
890
|
+
credential: input.credential,
|
|
891
|
+
fetcher: input.outboundFetch,
|
|
892
|
+
signal: input.signal,
|
|
893
|
+
...(input.clientVersion === undefined ? {} : { clientVersion: input.clientVersion }),
|
|
894
|
+
...(input.readIdleMs === undefined ? {} : { readIdleMs: input.readIdleMs }),
|
|
895
|
+
});
|
|
896
|
+
void digest;
|
|
897
|
+
return { transport, requestId: id };
|
|
898
|
+
}
|
|
@@ -33,7 +33,7 @@ export const CURSOR_OAUTH_CLIENT_ID = "KbZUR41cY7W6zRSdpSUJ7I7mLYBKOCmB";
|
|
|
33
33
|
* operator was not what the request carried. Discovery supersedes both on its
|
|
34
34
|
* first successful fetch.
|
|
35
35
|
*/
|
|
36
|
-
export const FALLBACK_CLIENT_VERSION = "cli-3.29.
|
|
36
|
+
export const FALLBACK_CLIENT_VERSION = "cli-3.29.15-86776fc";
|
|
37
37
|
|
|
38
38
|
/** Connect-RPC paths. Run is served by the discovered agent origin, not the API host. */
|
|
39
39
|
export const RUN_PATH = "/agent.v1.AgentService/Run";
|