cartethyia 2.4.6 → 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.
Files changed (302) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-D3xqZbN-.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-u6W1UXSc.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-CTTpCb25.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-C5XpWlvy.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-DHoB3Agn.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  25. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-CvjZxHE4.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-D3nza7LH.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-Dtt12WT1.js +1 -0
  37. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-CbK3hXvU.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  44. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  45. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  47. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
  49. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
  51. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
  53. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
  61. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  63. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  64. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  65. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  67. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-D70pChfn.js → index-CXm3FIyt.js} +2 -2
  71. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
  73. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
  75. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
  77. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
  81. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  84. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  85. package/dist/dashboard/assets/providers/trae.webp +0 -0
  86. package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
  87. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  88. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  89. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  90. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  91. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  92. package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
  93. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  94. package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
  95. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  96. package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
  97. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  98. package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
  99. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -52
  105. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  106. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  107. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  108. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  109. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  110. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  111. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  112. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  113. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  114. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  115. package/package.json +3 -2
  116. package/scripts/cli/command/console-menu.ts +30 -11
  117. package/scripts/cli/command/start.ts +77 -13
  118. package/scripts/cli/command/update.ts +1 -1
  119. package/scripts/cli/gateway/instance-lock.ts +105 -0
  120. package/scripts/cli/gateway/spawn.ts +173 -101
  121. package/scripts/cli/ui/background.ts +110 -49
  122. package/src/boot/app.ts +20 -6
  123. package/src/boot/dependencies.ts +19 -0
  124. package/src/boot/elysia-typebox-bridge.ts +37 -0
  125. package/src/boot/lifecycle.ts +2 -0
  126. package/src/config/cooldowns.ts +138 -138
  127. package/src/config/providers.ts +5 -5
  128. package/src/config/runtime.ts +19 -3
  129. package/src/config/spec.ts +9 -0
  130. package/src/console/api-keys/contracts.ts +414 -391
  131. package/src/console/api-keys/routes.ts +880 -870
  132. package/src/console/auth/account-reset-store.ts +14 -0
  133. package/src/console/auth/account-reset.ts +3 -2
  134. package/src/console/auth/setup-service.ts +6 -3
  135. package/src/console/domain-registration.ts +9 -2
  136. package/src/console/observability/contracts.ts +475 -455
  137. package/src/console/observability/store.ts +1015 -952
  138. package/src/console/providers/catalog/contracts.ts +31 -0
  139. package/src/console/providers/catalog/provider-operations.ts +8 -0
  140. package/src/console/providers/catalog/store.ts +1532 -1525
  141. package/src/console/providers/oauth/callback-listener.ts +18 -3
  142. package/src/console/providers/oauth/routes.ts +38 -15
  143. package/src/console/quota/refresh.ts +19 -0
  144. package/src/console/updates/routes.ts +162 -4
  145. package/src/main.ts +3 -0
  146. package/src/network/http2-bidi.ts +85 -0
  147. package/src/network/outbound-fetch.ts +1 -26
  148. package/src/network/pool/agent.ts +478 -460
  149. package/src/network/pool/resolver.ts +1104 -544
  150. package/src/network/pool-health-machine.ts +12 -5
  151. package/src/observability/payload-capture.ts +344 -324
  152. package/src/persistence/db-pg.ts +9 -3
  153. package/src/persistence/db-sqlite.ts +497 -495
  154. package/src/persistence/health-event-flush.ts +137 -0
  155. package/src/persistence/migrate.ts +181 -129
  156. package/src/persistence/schema.ts +1360 -1295
  157. package/src/persistence/settings-revision.ts +25 -0
  158. package/src/persistence/shared-cache.ts +217 -0
  159. package/src/persistence/stores/share-store.ts +556 -528
  160. package/src/persistence/usage-flush.ts +61 -1
  161. package/src/protocol/canonical-model.ts +23 -1
  162. package/src/protocol/response/chat.ts +143 -10
  163. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  164. package/src/providers/default-registry.ts +173 -1
  165. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  166. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  167. package/src/providers/discovery/probing-service.ts +1022 -1001
  168. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  169. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  170. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  171. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  172. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  173. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  174. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  175. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  176. package/src/providers/integrations/claude/claude.ts +606 -582
  177. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  178. package/src/providers/integrations/cline/cline.ts +539 -468
  179. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  180. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  181. package/src/providers/integrations/commandcode/quota.ts +160 -0
  182. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  183. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  184. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  185. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  186. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  187. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  188. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  189. package/src/providers/integrations/cursor/cursor.ts +960 -0
  190. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  191. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  192. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  193. package/src/providers/integrations/github/github.ts +4 -4
  194. package/src/providers/integrations/hermes.ts +79 -8
  195. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  196. package/src/providers/integrations/kilo/kilo.ts +153 -146
  197. package/src/providers/integrations/opencode/provider.ts +532 -538
  198. package/src/providers/integrations/openrouter/provider.ts +139 -281
  199. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  200. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  201. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  202. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  203. package/src/providers/integrations/qoder/qoder.ts +865 -730
  204. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  205. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  206. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  207. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  208. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  209. package/src/providers/integrations/trae/trae.ts +458 -0
  210. package/src/providers/operations/account-health-service.ts +107 -10
  211. package/src/providers/operations/client-versions.ts +133 -6
  212. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  213. package/src/providers/operations/provider-credential-service.ts +46 -7
  214. package/src/providers/provider-contracts.ts +76 -0
  215. package/src/providers/provider-metadata.ts +78 -6
  216. package/src/providers/provider-model-definition.ts +373 -349
  217. package/src/security/api-key-auth.ts +126 -8
  218. package/src/transport/contracts.ts +3 -0
  219. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  220. package/src/transport/dispatch/attempt-loop.ts +530 -515
  221. package/src/transport/dispatch/generation.ts +88 -21
  222. package/src/transport/dispatch/leases.ts +99 -2
  223. package/src/transport/dispatch/proxy-request.ts +60 -3
  224. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  225. package/src/transport/dispatch/streaming/stream.ts +22 -2
  226. package/src/transport/dispatch/upstream.ts +19 -1
  227. package/src/transport/guards/auth-guard.ts +26 -1
  228. package/src/transport/guards/body-policy.ts +5 -4
  229. package/src/transport/request/preparer.ts +103 -0
  230. package/src/transport/request/state.ts +14 -0
  231. package/src/transport/routing/plan-gate.ts +45 -0
  232. package/src/transport/routing/route-catalog.ts +799 -777
  233. package/src/transport/routing/route-model.ts +52 -1
  234. package/src/transport/routing/router.ts +51 -0
  235. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  237. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  239. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  241. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  242. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  243. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  244. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  245. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  246. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  248. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  250. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  252. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  254. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  256. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  258. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  259. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  260. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  262. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  263. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  265. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  266. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  267. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  268. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  270. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  271. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  272. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  273. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  274. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  275. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  276. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  277. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  279. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  280. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  281. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  282. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  283. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  284. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  285. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  286. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  287. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  288. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  289. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  291. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  293. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  295. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  296. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  297. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  298. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  299. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  300. package/src/network/tls/bun-client-hello.ts +0 -112
  301. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +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();