cartethyia 2.4.5 → 2.5.0
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 +4 -1
- package/README.md +63 -13
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.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-Df77V5I5.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
- 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/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
- package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.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/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
- package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
- package/dist/dashboard/assets/select-C0Rer3gF.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/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.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 +122 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- 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/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -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 +26 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +246 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +77 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +110 -0
- package/src/boot/app.ts +23 -7
- package/src/boot/dependencies.ts +55 -13
- 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 +31 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +20 -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/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -423
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +1015 -912
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +1532 -1521
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- 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 +87 -24
- 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/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/payload-capture.ts +344 -324
- package/src/observability/runtime-metrics.ts +7 -0
- 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 -1257
- 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/codex.ts +9 -75
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +177 -1
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -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 +115 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- 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/codex/codex.ts +5 -8
- 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/factory/factory-oauth.ts +7 -11
- 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/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +532 -499
- 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 -263
- 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/shared/install-id.ts +29 -2
- 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 +143 -6
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- 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 +39 -2
- package/src/transport/dispatch/attempt-loop.ts +530 -513
- package/src/transport/dispatch/empty-stream.ts +17 -26
- 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/failure-policy.ts +20 -0
- 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 -738
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
- package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Code model discovery, read from the Provider API.
|
|
3
|
+
*
|
|
4
|
+
* The adapter used to carry a hand-copied list of 36 ids with no limits and one
|
|
5
|
+
* wire for all of them. The Provider API publishes the real roster — 95 rows,
|
|
6
|
+
* each stating `context_length` and `supported_endpoints` — so this replaces
|
|
7
|
+
* that snapshot with what the provider actually serves.
|
|
8
|
+
*
|
|
9
|
+
* Two facts the snapshot could not express, and both change what is routable:
|
|
10
|
+
*
|
|
11
|
+
* * Every `claude-*` id is served on `/messages` only. A catalog that filed
|
|
12
|
+
* them as chat posted to an endpoint that does not answer them, so all nine
|
|
13
|
+
* were unusable.
|
|
14
|
+
*
|
|
15
|
+
* * Many ids also serve `/responses`. The wire is chosen per model from
|
|
16
|
+
* `supported_endpoints`, which is the provider's own advertisement, rather
|
|
17
|
+
* than guessed from the id.
|
|
18
|
+
*
|
|
19
|
+
* `:free` is deliberately NOT read as a free tier here. Verified against the
|
|
20
|
+
* live API: `minimax/minimax-m3.1-flash-preview:free`, `stealth/cat-jam:free`,
|
|
21
|
+
* `stealth/glyph-cluster:free` and `reflection/beam:free` all answer 400
|
|
22
|
+
* "insufficient credits" on an account whose `freeCredits` is 0. The suffix is
|
|
23
|
+
* part of the upstream id, not a pricing statement — the opposite of
|
|
24
|
+
* OpenRouter, Kilo and Cline, where `:free` is the tier. Marking it here would
|
|
25
|
+
* publish paid models as free, which is the defect fixed on Cline.
|
|
26
|
+
*/
|
|
27
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
28
|
+
import { isRecord } from "../../../protocol/codecs";
|
|
29
|
+
import { boundedUpstreamNumber, sanitizeUpstreamLabel } from "../../provider-sanitize";
|
|
30
|
+
import type { ProviderModelDiscovery } from "../../discovery/discovery-types";
|
|
31
|
+
import { defineModel } from "../../provider-model-definition";
|
|
32
|
+
import { COMMANDCODE_PROVIDER_ID } from "./commandcode";
|
|
33
|
+
|
|
34
|
+
/** The Provider API's OpenAI-shaped model listing. */
|
|
35
|
+
export const COMMANDCODE_MODELS_PATH = "/models" as const;
|
|
36
|
+
|
|
37
|
+
/** The route that selects the OpenAI Responses wire when a model lists it. */
|
|
38
|
+
const RESPONSES_ENDPOINT = "/responses" as const;
|
|
39
|
+
const ANTHROPIC_MESSAGES_ENDPOINT = "/messages" as const;
|
|
40
|
+
|
|
41
|
+
const MAX_CONTEXT = 1_000_000_000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The wire a model is served on.
|
|
45
|
+
*
|
|
46
|
+
* `claude-*` answers `/messages` only. Everything else answers
|
|
47
|
+
* `/chat/completions`, and adds `/responses` when the listing advertises it.
|
|
48
|
+
* Without endpoint metadata a model keeps chat, which every non-Claude model
|
|
49
|
+
* serves, so a listing that omits the field never breaks.
|
|
50
|
+
*/
|
|
51
|
+
export function commandCodeWireFor(
|
|
52
|
+
modelId: string,
|
|
53
|
+
supportedEndpoints?: readonly string[],
|
|
54
|
+
): { readonly wireFamily: "messages" | "responses" | "chat"; readonly endpointPath: string } {
|
|
55
|
+
if (modelId.startsWith("claude-")) {
|
|
56
|
+
return { wireFamily: "messages", endpointPath: ANTHROPIC_MESSAGES_ENDPOINT };
|
|
57
|
+
}
|
|
58
|
+
if (supportedEndpoints?.includes(RESPONSES_ENDPOINT)) {
|
|
59
|
+
return { wireFamily: "responses", endpointPath: RESPONSES_ENDPOINT };
|
|
60
|
+
}
|
|
61
|
+
return { wireFamily: "chat", endpointPath: "/chat/completions" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reads `supported_endpoints` tolerantly — unknown rows keep the chat wire. */
|
|
65
|
+
function endpointList(value: unknown): readonly string[] {
|
|
66
|
+
if (!Array.isArray(value)) return [];
|
|
67
|
+
return value.filter((entry): entry is string => typeof entry === "string");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const discoverCommandCodeModels: ProviderModelDiscovery = async (
|
|
71
|
+
input,
|
|
72
|
+
): Promise<readonly ModelDefinition[] | null> => {
|
|
73
|
+
const fetcher = input.fetcher ?? fetch;
|
|
74
|
+
const signal = AbortSignal.timeout(10_000);
|
|
75
|
+
try {
|
|
76
|
+
const base = input.baseUrl.replace(/\/+$/, "");
|
|
77
|
+
const response = await fetcher(`${base}${COMMANDCODE_MODELS_PATH}`, {
|
|
78
|
+
headers: { accept: "application/json", authorization: `Bearer ${input.credential}` },
|
|
79
|
+
signal,
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) return null;
|
|
82
|
+
const body: unknown = await response.json();
|
|
83
|
+
if (!isRecord(body)) return null;
|
|
84
|
+
const rows = Array.isArray(body["data"]) ? body["data"] : [];
|
|
85
|
+
const byId = new Map<string, ModelDefinition>();
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
if (!isRecord(row)) continue;
|
|
88
|
+
const id = sanitizeUpstreamLabel(row["id"]);
|
|
89
|
+
if (id === undefined) continue;
|
|
90
|
+
const contextLimit = boundedUpstreamNumber(row["context_length"], { min: 1, max: MAX_CONTEXT });
|
|
91
|
+
const { wireFamily, endpointPath } = commandCodeWireFor(
|
|
92
|
+
id,
|
|
93
|
+
endpointList(row["supported_endpoints"]),
|
|
94
|
+
);
|
|
95
|
+
byId.set(
|
|
96
|
+
id,
|
|
97
|
+
defineModel({
|
|
98
|
+
id,
|
|
99
|
+
providerId: COMMANDCODE_PROVIDER_ID,
|
|
100
|
+
wireFamily,
|
|
101
|
+
endpoint: endpointPath,
|
|
102
|
+
// `null` when the row states no limit, which is not a guess: the
|
|
103
|
+
// routing ladder keeps its own fallback.
|
|
104
|
+
ctx: contextLimit ?? null,
|
|
105
|
+
reasoning: true,
|
|
106
|
+
toolCall: true,
|
|
107
|
+
vision: true,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (byId.size === 0) return null;
|
|
112
|
+
return [...byId.values()].sort((a, b) => a.modelId.localeCompare(b.modelId));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Code quota, read from the four billing endpoints its client reads.
|
|
3
|
+
*
|
|
4
|
+
* Command Code bills in credits, not in rate-limit windows, so a quota row here
|
|
5
|
+
* is a credit balance rather than a 5-hour/weekly pair. The shape the console
|
|
6
|
+
* renders still expects windows, so the balance is reported as one independent
|
|
7
|
+
* window — the honest reading, since credits are the only thing consumed.
|
|
8
|
+
*
|
|
9
|
+
* The endpoints, all on `https://api.commandcode.ai` (not the Provider API
|
|
10
|
+
* origin the model catalog lives on):
|
|
11
|
+
*
|
|
12
|
+
* `/alpha/whoami` identity, and the `orgId` when one exists
|
|
13
|
+
* `/alpha/billing/credits` the balance and any window limits
|
|
14
|
+
* `/alpha/billing/subscriptions` the plan, when there is one
|
|
15
|
+
* `/alpha/usage/summary` spend in the period, for context
|
|
16
|
+
*
|
|
17
|
+
* Every call is optional to the result: a failure reports what is still known
|
|
18
|
+
* rather than failing the whole read, because an operator who can see a zero
|
|
19
|
+
* balance has already got the answer even if the usage summary never came back.
|
|
20
|
+
*/
|
|
21
|
+
import type { FetchLike, ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
22
|
+
|
|
23
|
+
const COMMANDCODE_API_BASE = "https://api.commandcode.ai" as const;
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
25
|
+
|
|
26
|
+
interface RequestOk {
|
|
27
|
+
readonly ok: true;
|
|
28
|
+
readonly value: unknown;
|
|
29
|
+
}
|
|
30
|
+
interface RequestFailed {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
}
|
|
33
|
+
type RequestResult = RequestOk | RequestFailed;
|
|
34
|
+
|
|
35
|
+
async function readJson(
|
|
36
|
+
path: string,
|
|
37
|
+
credential: string,
|
|
38
|
+
fetcher: FetchLike,
|
|
39
|
+
): Promise<RequestResult> {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetcher(`${COMMANDCODE_API_BASE}${path}`, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
headers: { accept: "application/json", authorization: `Bearer ${credential}` },
|
|
44
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) return { ok: false };
|
|
47
|
+
return { ok: true, value: await response.json() };
|
|
48
|
+
} catch {
|
|
49
|
+
return { ok: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
55
|
+
? (value as Record<string, unknown>)
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function numberOrNull(value: unknown): number | null {
|
|
60
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The credit block: three balances that are summed, plus any window limits. */
|
|
64
|
+
function creditWindow(credits: Record<string, unknown>): ProviderQuotaWindow | null {
|
|
65
|
+
const monthly = numberOrNull(credits["monthlyCredits"]) ?? 0;
|
|
66
|
+
const purchased = numberOrNull(credits["purchasedCredits"]) ?? 0;
|
|
67
|
+
const free = numberOrNull(credits["freeCredits"]) ?? 0;
|
|
68
|
+
const remaining = monthly + purchased + free;
|
|
69
|
+
// Command Code reports no ceiling for a credit balance, so there is no
|
|
70
|
+
// percentage to compute — unlike a rate-limit window, where the upstream
|
|
71
|
+
// states both sides. Reporting null is the truthful answer.
|
|
72
|
+
return {
|
|
73
|
+
kind: "credits",
|
|
74
|
+
label: "Credits",
|
|
75
|
+
accounting: "independent",
|
|
76
|
+
usedPercent: null,
|
|
77
|
+
remainingPercent: null,
|
|
78
|
+
resetsAt: null,
|
|
79
|
+
remaining,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function windowLimits(
|
|
84
|
+
limits: Record<string, unknown>,
|
|
85
|
+
): readonly ProviderQuotaWindow[] {
|
|
86
|
+
const out: ProviderQuotaWindow[] = [];
|
|
87
|
+
const fiveHour = recordValue(limits["fiveHour"]);
|
|
88
|
+
const weekly = recordValue(limits["weekly"]);
|
|
89
|
+
const push = (source: Record<string, unknown> | undefined, kind: string, label: string) => {
|
|
90
|
+
if (source === undefined) return;
|
|
91
|
+
out.push({
|
|
92
|
+
kind,
|
|
93
|
+
label,
|
|
94
|
+
accounting: "independent",
|
|
95
|
+
usedPercent: numberOrNull(source["usedPercent"]),
|
|
96
|
+
remainingPercent: numberOrNull(source["remainingPercent"]),
|
|
97
|
+
resetsAt: typeof source["resetsAt"] === "string" ? source["resetsAt"] : null,
|
|
98
|
+
remaining: numberOrNull(source["remaining"]),
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
push(fiveHour, "five_hour", "5 Hour");
|
|
102
|
+
push(weekly, "weekly", "Weekly");
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function fetchCommandCodeQuota(
|
|
107
|
+
credential: string,
|
|
108
|
+
fetcher: FetchLike,
|
|
109
|
+
): Promise<ProviderQuotaResult> {
|
|
110
|
+
const source = "commandcode";
|
|
111
|
+
const [whoami, creditsRaw, subscriptionRaw] = await Promise.all([
|
|
112
|
+
readJson("/alpha/whoami", credential, fetcher),
|
|
113
|
+
readJson("/alpha/billing/credits", credential, fetcher),
|
|
114
|
+
readJson("/alpha/billing/subscriptions", credential, fetcher),
|
|
115
|
+
]);
|
|
116
|
+
if (!whoami.ok && !creditsRaw.ok) {
|
|
117
|
+
return {
|
|
118
|
+
source,
|
|
119
|
+
plan: null,
|
|
120
|
+
windows: [],
|
|
121
|
+
error: "Command Code did not answer its billing endpoints",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const whoamiBody = whoami.ok ? recordValue(whoami.value) : undefined;
|
|
126
|
+
const user = recordValue(whoamiBody?.["user"]);
|
|
127
|
+
const accountLabel =
|
|
128
|
+
(typeof user?.["email"] === "string" && user["email"]) ||
|
|
129
|
+
(typeof user?.["userName"] === "string" && user["userName"]) ||
|
|
130
|
+
undefined;
|
|
131
|
+
|
|
132
|
+
const creditsBody = creditsRaw.ok ? recordValue(creditsRaw.value) : undefined;
|
|
133
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
134
|
+
if (creditsBody !== undefined) {
|
|
135
|
+
const credits = recordValue(creditsBody["credits"]);
|
|
136
|
+
if (credits !== undefined) {
|
|
137
|
+
const window = creditWindow(credits);
|
|
138
|
+
if (window !== null) windows.push(window);
|
|
139
|
+
}
|
|
140
|
+
const limits = recordValue(creditsBody["windowLimits"]);
|
|
141
|
+
if (limits !== undefined) windows.push(...windowLimits(limits));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const subscriptionBody = subscriptionRaw.ok ? recordValue(subscriptionRaw.value) : undefined;
|
|
145
|
+
const planRecord = recordValue(subscriptionBody?.["data"]);
|
|
146
|
+
const plan =
|
|
147
|
+
typeof planRecord?.["name"] === "string"
|
|
148
|
+
? planRecord["name"]
|
|
149
|
+
: creditsRaw.ok
|
|
150
|
+
? "Credits"
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
source,
|
|
155
|
+
plan,
|
|
156
|
+
windows,
|
|
157
|
+
error: null,
|
|
158
|
+
...(accountLabel === undefined ? {} : { accountLabel }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor OAuth client and token refresher.
|
|
3
|
+
*
|
|
4
|
+
* Cursor's login is its own PKCE device flow, not RFC 8628: the verifier and a
|
|
5
|
+
* UUID are generated locally, the operator opens a `loginDeepControl` URL in a
|
|
6
|
+
* browser, and the gateway polls `/auth/poll` until the tokens land. There is no
|
|
7
|
+
* user code — approval is bound to the browser session — so `userCode` is empty
|
|
8
|
+
* and the private verifier rides in `providerState`, which the console persists
|
|
9
|
+
* server-side and never returns to the browser.
|
|
10
|
+
*
|
|
11
|
+
* Two credential families reach dispatch:
|
|
12
|
+
*
|
|
13
|
+
* - **Browser session** (`/oauth/token` refresh grant, as Cursor's IDE performs
|
|
14
|
+
* it). A rejected token is not an HTTP error: Cursor answers 200 with
|
|
15
|
+
* `{"access_token":"","shouldLogout":true}`, so the body decides the outcome.
|
|
16
|
+
* - **Raw API key** (`crsr_`, exchanged at `/auth/exchange_user_api_key`). The
|
|
17
|
+
* exchanged JWT is the dispatch secret; the raw key is kept in the encrypted
|
|
18
|
+
* refresh field so the refresher can re-exchange it, which is how Cursor CLI
|
|
19
|
+
* renews an API-key login.
|
|
20
|
+
*
|
|
21
|
+
* A raw key never reaches `auth_state` and is never logged: `auth_state` is
|
|
22
|
+
* operator-visible in the console.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
25
|
+
import { OAuthDeviceFlow } from "../../authentication/oauth-device-flow";
|
|
26
|
+
import type {
|
|
27
|
+
OAuthDeviceFlowContext,
|
|
28
|
+
OAuthDevicePollResult,
|
|
29
|
+
OAuthDeviceStartResult,
|
|
30
|
+
OAuthExchangeResult,
|
|
31
|
+
OAuthImportInput,
|
|
32
|
+
OAuthLoginField,
|
|
33
|
+
} from "../../authentication/oauth-flow-store";
|
|
34
|
+
import { base64UrlEncode, nonEmptyTrimmedString, record } from "../../authentication/oauth-flow-store";
|
|
35
|
+
import type { FetchLike } from "../../authentication/oauth-client";
|
|
36
|
+
import type { OAuthRefreshContext, OAuthTokenRefreshResult } from "../../authentication/oauth-refresh-service";
|
|
37
|
+
import {
|
|
38
|
+
AUTH_REQUEST_TIMEOUT_MS,
|
|
39
|
+
CURSOR_API_BASE_URL,
|
|
40
|
+
CURSOR_OAUTH_CLIENT_ID,
|
|
41
|
+
CURSOR_WEBSITE_URL,
|
|
42
|
+
SESSION_REFRESH_TIMEOUT_MS,
|
|
43
|
+
SIGN_IN_POLICY_VIOLATION,
|
|
44
|
+
TOKEN_EXPIRY_THRESHOLD_S,
|
|
45
|
+
isExchangeableApiKey,
|
|
46
|
+
} from "./cursor-shared";
|
|
47
|
+
|
|
48
|
+
/** Device authorizations live 5 minutes before the flow is abandoned. */
|
|
49
|
+
const DEVICE_EXPIRY_SECONDS = 300;
|
|
50
|
+
/** Dashboard poll cadence for the device authorization. */
|
|
51
|
+
const DEVICE_INTERVAL_SECONDS = 5;
|
|
52
|
+
/** Token lifetime assumed when the upstream omits every expiry hint. */
|
|
53
|
+
const EXPIRY_FALLBACK_MS = 30 * 24 * 60 * 60 * 1000;
|
|
54
|
+
|
|
55
|
+
/** Fields the import flow collects: one raw Cursor API key. */
|
|
56
|
+
const IMPORT_FIELDS: readonly OAuthLoginField[] = [
|
|
57
|
+
{
|
|
58
|
+
key: "credential",
|
|
59
|
+
label: "Cursor API key",
|
|
60
|
+
placeholder: "crsr_…",
|
|
61
|
+
secret: true,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function base64urlDecode(segment: string): string | undefined {
|
|
67
|
+
try {
|
|
68
|
+
return Buffer.from(segment, "base64url").toString("utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Decodes a JWT payload without verifying; the trust boundary stays at TLS. */
|
|
75
|
+
export function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
76
|
+
const segment = token.split(".")[1];
|
|
77
|
+
if (segment === undefined) return undefined;
|
|
78
|
+
const json = base64urlDecode(segment);
|
|
79
|
+
if (json === undefined) return undefined;
|
|
80
|
+
try {
|
|
81
|
+
return record(JSON.parse(json) as unknown);
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** JWT `exp` claim as epoch milliseconds, or `undefined` when absent/malformed. */
|
|
88
|
+
export function decodeJwtExpiryMs(token: string): number | undefined {
|
|
89
|
+
const payload = decodeJwtPayload(token);
|
|
90
|
+
const exp = payload?.["exp"];
|
|
91
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return undefined;
|
|
92
|
+
return exp * 1000;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a JWT expires within the refresh threshold. */
|
|
96
|
+
export function isExpiringSoon(token: string, thresholdSeconds = TOKEN_EXPIRY_THRESHOLD_S): boolean {
|
|
97
|
+
const expiry = decodeJwtExpiryMs(token);
|
|
98
|
+
if (expiry === undefined) return false;
|
|
99
|
+
return expiry - Date.now() < thresholdSeconds * 1000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The account identity Cursor itself asserts, from the access token's `sub`.
|
|
104
|
+
*
|
|
105
|
+
* Cursor publishes no email on this grant, so the login used to fall back to the
|
|
106
|
+
* provider id — every account of the provider then looked identical, and adding
|
|
107
|
+
* a second one replaced the first. `sub` (`auth0|user_01…`) is stable across
|
|
108
|
+
* every login of one account and differs between accounts, which is exactly
|
|
109
|
+
* what an account identity has to do.
|
|
110
|
+
*/
|
|
111
|
+
export function cursorAccountIdentity(token: string): string | undefined {
|
|
112
|
+
const payload = decodeJwtPayload(token);
|
|
113
|
+
const subject = payload?.["sub"];
|
|
114
|
+
return typeof subject === "string" && subject.trim().length > 0 ? subject.trim() : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Private device-flow state. Never returned to the browser. */
|
|
118
|
+
interface CursorDeviceState {
|
|
119
|
+
readonly verifier: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseDeviceState(value: string | undefined): CursorDeviceState | undefined {
|
|
123
|
+
if (value === undefined || value.length === 0) return undefined;
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(value) as unknown;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const root = record(parsed);
|
|
131
|
+
const verifier = root === undefined ? undefined : nonEmptyTrimmedString(root["verifier"]);
|
|
132
|
+
return verifier === undefined ? undefined : { verifier };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Minimal fetch surface accepted by the token request helpers. */
|
|
136
|
+
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
137
|
+
|
|
138
|
+
/** The base OAuth client's fetch surface; widened to `FetchFn` at each call. */
|
|
139
|
+
function asFetchFn(fetchFn: FetchLike): FetchFn {
|
|
140
|
+
// Structural identity: the base class's `FetchLike` is the same signature as
|
|
141
|
+
// `FetchFn`, so this only reconciles the two declarations.
|
|
142
|
+
return fetchFn as FetchFn;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type JsonOutcome =
|
|
146
|
+
| { ok: true; body: Record<string, unknown>; status: number }
|
|
147
|
+
| { ok: false; status?: number; message: string };
|
|
148
|
+
|
|
149
|
+
async function fetchJson(
|
|
150
|
+
fetchFn: FetchFn,
|
|
151
|
+
url: string,
|
|
152
|
+
init: RequestInit,
|
|
153
|
+
timeoutMs: number,
|
|
154
|
+
label: string,
|
|
155
|
+
): Promise<JsonOutcome> {
|
|
156
|
+
let response: Response;
|
|
157
|
+
try {
|
|
158
|
+
response = await fetchFn(url, {
|
|
159
|
+
...init,
|
|
160
|
+
signal: signalOrTimeout(init.signal, timeoutMs),
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
message: `${label} request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const text = await response.text().catch(() => "");
|
|
169
|
+
let body: Record<string, unknown> | undefined;
|
|
170
|
+
try {
|
|
171
|
+
body = record(JSON.parse(text) as unknown) ?? undefined;
|
|
172
|
+
} catch {
|
|
173
|
+
body = undefined;
|
|
174
|
+
}
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
status: response.status,
|
|
179
|
+
message: `${label} failed (${response.status}): ${text.slice(0, 200)}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (body === undefined) {
|
|
183
|
+
return { ok: false, status: response.status, message: `${label} returned malformed JSON` };
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, body, status: response.status };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Composes an outer signal with a bounded timeout; absent signal → timeout alone. */
|
|
189
|
+
function signalOrTimeout(signal: AbortSignal | null | undefined, timeoutMs: number): AbortSignal {
|
|
190
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
191
|
+
return signal === null || signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Exchanges a raw `crsr_` key for a short-lived JWT.
|
|
196
|
+
*
|
|
197
|
+
* Also how an API-key login is renewed: Cursor CLI re-runs the exchange and
|
|
198
|
+
* never uses the refresh token the response carries.
|
|
199
|
+
*/
|
|
200
|
+
async function exchangeApiKey(
|
|
201
|
+
fetchFn: FetchFn,
|
|
202
|
+
apiKey: string,
|
|
203
|
+
): Promise<{ accessToken: string; refreshToken: string; expiresAt: Date }> {
|
|
204
|
+
const outcome = await fetchJson(
|
|
205
|
+
fetchFn,
|
|
206
|
+
`${CURSOR_API_BASE_URL}/auth/exchange_user_api_key`,
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
210
|
+
body: "{}",
|
|
211
|
+
},
|
|
212
|
+
AUTH_REQUEST_TIMEOUT_MS,
|
|
213
|
+
"Cursor API key exchange",
|
|
214
|
+
);
|
|
215
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
216
|
+
const accessToken = nonEmptyTrimmedString(outcome.body["accessToken"]);
|
|
217
|
+
if (accessToken === undefined) throw new Error("Cursor API key exchange returned no access token");
|
|
218
|
+
const refreshToken = nonEmptyTrimmedString(outcome.body["refreshToken"]) ?? apiKey;
|
|
219
|
+
const expiry = decodeJwtExpiryMs(accessToken);
|
|
220
|
+
return {
|
|
221
|
+
accessToken,
|
|
222
|
+
refreshToken,
|
|
223
|
+
expiresAt: new Date(expiry ?? Date.now() + EXPIRY_FALLBACK_MS),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export class CursorOAuthClient extends OAuthDeviceFlow {
|
|
228
|
+
override readonly supportsDeviceCode = true;
|
|
229
|
+
override readonly supportsBrowserCode = false;
|
|
230
|
+
readonly importFields = IMPORT_FIELDS;
|
|
231
|
+
|
|
232
|
+
protected override readonly providerLabel = "Cursor";
|
|
233
|
+
// Cursor's poll endpoint takes no client id; the abstract field stays empty
|
|
234
|
+
// rather than inventing one, and the poll request omits it.
|
|
235
|
+
protected override readonly clientId = "";
|
|
236
|
+
protected override readonly tokenUrl = `${CURSOR_API_BASE_URL}/oauth/token`;
|
|
237
|
+
protected override readonly scopes = "";
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Starts the browser login.
|
|
241
|
+
*
|
|
242
|
+
* The PKCE verifier is the flow's private half and is returned only as
|
|
243
|
+
* `providerState` — the console persists it in Redis, and the browser sees the
|
|
244
|
+
* URL and the UUID alone. Publishing it would let anyone who can read the
|
|
245
|
+
* start response mint the tokens the operator is about to authorize.
|
|
246
|
+
*/
|
|
247
|
+
override async startDeviceAuth(
|
|
248
|
+
_context?: OAuthDeviceFlowContext,
|
|
249
|
+
): Promise<OAuthDeviceStartResult> {
|
|
250
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
251
|
+
const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
|
|
252
|
+
const uuid = randomUUID();
|
|
253
|
+
const params = new URLSearchParams({
|
|
254
|
+
challenge,
|
|
255
|
+
uuid,
|
|
256
|
+
mode: "login",
|
|
257
|
+
redirectTarget: "cli",
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
verificationUri: `${CURSOR_WEBSITE_URL}/loginDeepControl?${params.toString()}`,
|
|
261
|
+
// No user code: approval happens against the browser session.
|
|
262
|
+
userCode: "",
|
|
263
|
+
deviceAuthId: uuid,
|
|
264
|
+
intervalSeconds: DEVICE_INTERVAL_SECONDS,
|
|
265
|
+
expiresInSeconds: DEVICE_EXPIRY_SECONDS,
|
|
266
|
+
providerState: JSON.stringify({ verifier } satisfies CursorDeviceState),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Polls `/auth/poll` with the private verifier.
|
|
272
|
+
*
|
|
273
|
+
* 404 is Cursor's "not approved yet" answer, not a failure. A 429 is a
|
|
274
|
+
* backoff request rather than a dead credential, so the dashboard widens its
|
|
275
|
+
* cadence and keeps waiting. A 200 without tokens is still pending: the
|
|
276
|
+
* operator may not have finished approving, and ending the flow there would
|
|
277
|
+
* abandon a login in progress (a genuinely changed shape surfaces as the
|
|
278
|
+
* flow's own expiry).
|
|
279
|
+
*/
|
|
280
|
+
override async pollDeviceAuth(
|
|
281
|
+
deviceAuthId: string,
|
|
282
|
+
context?: OAuthDeviceFlowContext,
|
|
283
|
+
): Promise<OAuthDevicePollResult> {
|
|
284
|
+
const state = parseDeviceState(context?.providerState);
|
|
285
|
+
if (state === undefined) return { status: "failed", reason: "unknown Cursor device authorization" };
|
|
286
|
+
const url =
|
|
287
|
+
`${CURSOR_API_BASE_URL}/auth/poll?uuid=${encodeURIComponent(deviceAuthId)}` +
|
|
288
|
+
`&verifier=${encodeURIComponent(state.verifier)}`;
|
|
289
|
+
const outcome = await fetchJson(
|
|
290
|
+
asFetchFn(this.fetchFn),
|
|
291
|
+
url,
|
|
292
|
+
{ method: "GET", headers: { accept: "application/json" } },
|
|
293
|
+
AUTH_REQUEST_TIMEOUT_MS,
|
|
294
|
+
"Cursor device polling",
|
|
295
|
+
);
|
|
296
|
+
if (!outcome.ok) {
|
|
297
|
+
if (outcome.status === 404) return { status: "pending" };
|
|
298
|
+
if (outcome.status === 429) {
|
|
299
|
+
return { status: "slow_down", retryAfterSeconds: DEVICE_INTERVAL_SECONDS * 2 };
|
|
300
|
+
}
|
|
301
|
+
return { status: "failed", reason: outcome.message };
|
|
302
|
+
}
|
|
303
|
+
const accessToken = nonEmptyTrimmedString(outcome.body["accessToken"]);
|
|
304
|
+
if (accessToken === undefined) return { status: "pending" };
|
|
305
|
+
const refreshToken = nonEmptyTrimmedString(outcome.body["refreshToken"]) ?? accessToken;
|
|
306
|
+
const expiry = decodeJwtExpiryMs(accessToken);
|
|
307
|
+
const identity = cursorAccountIdentity(accessToken);
|
|
308
|
+
return {
|
|
309
|
+
status: "complete",
|
|
310
|
+
result: {
|
|
311
|
+
access: accessToken,
|
|
312
|
+
refresh: refreshToken,
|
|
313
|
+
expiresAt: new Date(expiry ?? Date.now() + EXPIRY_FALLBACK_MS),
|
|
314
|
+
...(identity === undefined ? {} : { accountLabel: identity }),
|
|
315
|
+
// `method` distinguishes a browser session (renewed by grant) from an
|
|
316
|
+
// imported key (renewed by re-exchange). It carries no secret.
|
|
317
|
+
auth_state: { method: "login", ...(identity === undefined ? {} : { subject: identity }) },
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Imports a raw Cursor API key.
|
|
324
|
+
*
|
|
325
|
+
* The key is exchanged before it is returned, so an import cannot persist
|
|
326
|
+
* material that never worked. The exchanged JWT becomes the account secret and
|
|
327
|
+
* the raw key rides in the encrypted refresh field: the refresher needs it to
|
|
328
|
+
* renew the login, and that field is the only place it is stored.
|
|
329
|
+
*/
|
|
330
|
+
async importCredential(input: OAuthImportInput): Promise<OAuthExchangeResult> {
|
|
331
|
+
const credential = input.credential.trim();
|
|
332
|
+
if (credential.length === 0) throw new Error("Cursor credential material is required");
|
|
333
|
+
if (!isExchangeableApiKey(credential)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
"Cursor API keys start with 'crsr_'; sign in with Cursor for a browser session instead.",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const exchanged = await exchangeApiKey(asFetchFn(this.fetchFn), credential);
|
|
339
|
+
const identity = cursorAccountIdentity(exchanged.accessToken);
|
|
340
|
+
return {
|
|
341
|
+
access: exchanged.accessToken,
|
|
342
|
+
refresh: credential,
|
|
343
|
+
expiresAt: exchanged.expiresAt,
|
|
344
|
+
...(identity === undefined ? {} : { accountLabel: identity }),
|
|
345
|
+
auth_state: { method: "api_key", ...(identity === undefined ? {} : { subject: identity }) },
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Renews one Cursor account credential.
|
|
351
|
+
*
|
|
352
|
+
* An API-key account re-exchanges its raw key (the refresh field holds it); a
|
|
353
|
+
* browser session posts the IDE's refresh grant. A sign-in policy block and a
|
|
354
|
+
* `shouldLogout` answer are terminal for this credential, while a network or
|
|
355
|
+
* 5xx failure is transient and worth retrying.
|
|
356
|
+
*/
|
|
357
|
+
override async refresh(
|
|
358
|
+
refreshToken: string,
|
|
359
|
+
signal?: AbortSignal,
|
|
360
|
+
context?: OAuthRefreshContext,
|
|
361
|
+
): Promise<OAuthTokenRefreshResult> {
|
|
362
|
+
const method = nonEmptyTrimmedString(context?.auth_state?.["method"]);
|
|
363
|
+
if (isExchangeableApiKey(refreshToken)) {
|
|
364
|
+
const exchanged = await exchangeApiKey(asFetchFn(this.fetchFn), refreshToken);
|
|
365
|
+
return {
|
|
366
|
+
access: exchanged.accessToken,
|
|
367
|
+
refresh: refreshToken,
|
|
368
|
+
expiresAt: exchanged.expiresAt,
|
|
369
|
+
auth_state: { method: method ?? "api_key" },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (refreshToken.length === 0) throw new Error("Cursor refresh token is missing");
|
|
373
|
+
const outcome = await fetchJson(
|
|
374
|
+
asFetchFn(this.fetchFn),
|
|
375
|
+
`${CURSOR_API_BASE_URL}/oauth/token`,
|
|
376
|
+
{
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: { "content-type": "application/json", "x-cursor-client-type": "cli" },
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
grant_type: "refresh_token",
|
|
381
|
+
client_id: CURSOR_OAUTH_CLIENT_ID,
|
|
382
|
+
refresh_token: refreshToken,
|
|
383
|
+
}),
|
|
384
|
+
...(signal === undefined ? {} : { signal }),
|
|
385
|
+
},
|
|
386
|
+
SESSION_REFRESH_TIMEOUT_MS,
|
|
387
|
+
"Cursor session refresh",
|
|
388
|
+
);
|
|
389
|
+
if (!outcome.ok) {
|
|
390
|
+
// A bare transport failure is transient; a refusal is not.
|
|
391
|
+
if (outcome.status === 401 || outcome.status === 403) {
|
|
392
|
+
throw new Error(`Cursor session refresh was rejected (${outcome.status})`);
|
|
393
|
+
}
|
|
394
|
+
throw new Error(outcome.message);
|
|
395
|
+
}
|
|
396
|
+
if (outcome.body["shouldLogout"] === true) {
|
|
397
|
+
const code = outcome.body["error"];
|
|
398
|
+
throw new Error(
|
|
399
|
+
code === SIGN_IN_POLICY_VIOLATION
|
|
400
|
+
? `Cursor session blocked by sign-in policy (${SIGN_IN_POLICY_VIOLATION})`
|
|
401
|
+
: "Cursor ended the session (shouldLogout) — sign in again",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const access = nonEmptyTrimmedString(outcome.body["access_token"]);
|
|
405
|
+
if (access === undefined) throw new Error("Cursor session refresh returned no access token");
|
|
406
|
+
const expiry = decodeJwtExpiryMs(access);
|
|
407
|
+
return {
|
|
408
|
+
access,
|
|
409
|
+
// The grant issues no new refresh token: the IDE stores the access token
|
|
410
|
+
// as both, and so does this refresher.
|
|
411
|
+
refresh: refreshToken,
|
|
412
|
+
expiresAt: new Date(expiry ?? Date.now() + EXPIRY_FALLBACK_MS),
|
|
413
|
+
auth_state: { method: method ?? "login" },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export const cursorOAuthClient = new CursorOAuthClient();
|