cartethyia 2.4.6 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CytxSeGP.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-CU7S1wJs.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.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-DOXfuzfL.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-38JfiZ0H.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--yj84zk3.js +4 -0
  63. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  65. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  66. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  67. package/dist/dashboard/assets/entry-DKvr5Q9K.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-CcD1eSJ6.js} +2 -2
  71. package/dist/dashboard/assets/index-CcD1eSJ6.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-XPJ-dIw3.js +41 -0
  89. package/dist/dashboard/assets/select-XPJ-dIw3.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/tray.ts +47 -8
  119. package/scripts/cli/command/update.ts +1 -1
  120. package/scripts/cli/gateway/instance-lock.ts +105 -0
  121. package/scripts/cli/gateway/spawn.ts +173 -101
  122. package/scripts/cli/ui/background.ts +110 -49
  123. package/src/boot/app.ts +20 -6
  124. package/src/boot/dependencies.ts +19 -0
  125. package/src/boot/elysia-typebox-bridge.ts +37 -0
  126. package/src/boot/lifecycle.ts +2 -0
  127. package/src/config/cooldowns.ts +138 -138
  128. package/src/config/providers.ts +5 -5
  129. package/src/config/runtime.ts +19 -3
  130. package/src/config/spec.ts +9 -0
  131. package/src/console/api-keys/contracts.ts +414 -391
  132. package/src/console/api-keys/routes.ts +880 -870
  133. package/src/console/auth/account-reset-store.ts +14 -0
  134. package/src/console/auth/account-reset.ts +3 -2
  135. package/src/console/auth/setup-service.ts +6 -3
  136. package/src/console/domain-registration.ts +9 -2
  137. package/src/console/observability/contracts.ts +475 -455
  138. package/src/console/observability/store.ts +1015 -952
  139. package/src/console/providers/catalog/contracts.ts +31 -0
  140. package/src/console/providers/catalog/provider-operations.ts +8 -0
  141. package/src/console/providers/catalog/store.ts +1532 -1525
  142. package/src/console/providers/oauth/callback-listener.ts +18 -3
  143. package/src/console/providers/oauth/routes.ts +38 -15
  144. package/src/console/quota/refresh.ts +19 -0
  145. package/src/console/updates/routes.ts +162 -4
  146. package/src/main.ts +3 -0
  147. package/src/network/http2-bidi.ts +85 -0
  148. package/src/network/outbound-fetch.ts +1 -26
  149. package/src/network/pool/agent.ts +478 -460
  150. package/src/network/pool/resolver.ts +1104 -544
  151. package/src/network/pool-health-machine.ts +12 -5
  152. package/src/observability/payload-capture.ts +344 -324
  153. package/src/persistence/db-pg.ts +9 -3
  154. package/src/persistence/db-sqlite.ts +497 -495
  155. package/src/persistence/health-event-flush.ts +137 -0
  156. package/src/persistence/migrate.ts +181 -129
  157. package/src/persistence/schema.ts +1360 -1295
  158. package/src/persistence/settings-revision.ts +25 -0
  159. package/src/persistence/shared-cache.ts +217 -0
  160. package/src/persistence/stores/share-store.ts +556 -528
  161. package/src/persistence/usage-flush.ts +61 -1
  162. package/src/protocol/canonical-model.ts +23 -1
  163. package/src/protocol/response/chat.ts +143 -10
  164. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  165. package/src/providers/default-registry.ts +173 -1
  166. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  167. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  168. package/src/providers/discovery/probing-service.ts +1022 -1001
  169. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  170. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  171. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  172. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  173. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  174. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  175. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  176. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  177. package/src/providers/integrations/claude/claude.ts +606 -582
  178. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  179. package/src/providers/integrations/cline/cline.ts +539 -468
  180. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  181. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  182. package/src/providers/integrations/commandcode/quota.ts +160 -0
  183. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  184. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  185. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  186. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  187. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  188. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  189. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  190. package/src/providers/integrations/cursor/cursor.ts +960 -0
  191. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  192. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  193. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  194. package/src/providers/integrations/github/github.ts +4 -4
  195. package/src/providers/integrations/hermes.ts +79 -8
  196. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  197. package/src/providers/integrations/kilo/kilo.ts +153 -146
  198. package/src/providers/integrations/opencode/provider.ts +532 -538
  199. package/src/providers/integrations/openrouter/provider.ts +139 -281
  200. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  201. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  202. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  203. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  204. package/src/providers/integrations/qoder/qoder.ts +865 -730
  205. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  206. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  207. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  208. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  209. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  210. package/src/providers/integrations/trae/trae.ts +458 -0
  211. package/src/providers/operations/account-health-service.ts +107 -10
  212. package/src/providers/operations/client-versions.ts +133 -6
  213. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  214. package/src/providers/operations/provider-credential-service.ts +46 -7
  215. package/src/providers/provider-contracts.ts +76 -0
  216. package/src/providers/provider-metadata.ts +78 -6
  217. package/src/providers/provider-model-definition.ts +373 -349
  218. package/src/security/api-key-auth.ts +126 -8
  219. package/src/transport/contracts.ts +3 -0
  220. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  221. package/src/transport/dispatch/attempt-loop.ts +530 -515
  222. package/src/transport/dispatch/generation.ts +88 -21
  223. package/src/transport/dispatch/leases.ts +99 -2
  224. package/src/transport/dispatch/proxy-request.ts +60 -3
  225. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  226. package/src/transport/dispatch/streaming/stream.ts +22 -2
  227. package/src/transport/dispatch/upstream.ts +19 -1
  228. package/src/transport/guards/auth-guard.ts +26 -1
  229. package/src/transport/guards/body-policy.ts +5 -4
  230. package/src/transport/request/preparer.ts +103 -0
  231. package/src/transport/request/state.ts +14 -0
  232. package/src/transport/routing/plan-gate.ts +45 -0
  233. package/src/transport/routing/route-catalog.ts +799 -777
  234. package/src/transport/routing/route-model.ts +52 -1
  235. package/src/transport/routing/router.ts +51 -0
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  237. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  239. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  241. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  242. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  243. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  244. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  245. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  246. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  248. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  250. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  252. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  254. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  256. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  258. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  259. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  260. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  262. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  263. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  265. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  266. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  267. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  268. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  270. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  271. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  272. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  273. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  274. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  275. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  276. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  277. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  279. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  280. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  281. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  282. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  283. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  284. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  285. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  286. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  287. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  288. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  289. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  291. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  293. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  295. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  296. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  297. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  298. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  299. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  300. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  301. package/src/network/tls/bun-client-hello.ts +0 -112
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  303. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
@@ -0,0 +1,960 @@
1
+ // The canonical Cursor adapter.
2
+ //
3
+ // Cursor speaks Connect-RPC over protobuf on a bidirectional HTTP/2 stream, so
4
+ // this adapter frames its own protocol instead of going through a wire codec. It
5
+ // owns the whole tool loop in both directions:
6
+ //
7
+ // - a Cursor executable tool request becomes a client `tool_call_delta`;
8
+ // - the client's next request carries that tool's result and the *same*
9
+ // upstream Run answers it, so the model continues where it left off.
10
+ //
11
+ // Two rules govern what is executed. Only Cursor's authoritative
12
+ // `exec_server_message.mcp_args` is a tool call — `interaction_update`'s
13
+ // `tool_call_started` / `tool_call_completed` are display notifications, and
14
+ // Cursor's native shell/filesystem/workspace tools are never run here. And an
15
+ // `mcp_args` request is executable only when its namespace and function name
16
+ // match a tool the current request declared; anything else is answered
17
+ // `rejected` so the model can continue without the gateway ever touching a
18
+ // filesystem or a shell on the caller's behalf.
19
+ import { GatewayError } from "../../../protocol/gateway-error";
20
+ import type {
21
+ CanonicalEvent,
22
+ CanonicalRequest,
23
+ CanonicalStopReason,
24
+ ContentPart,
25
+ UsageRecord,
26
+ } from "../../../protocol/canonical-model";
27
+ import { resolveImageSource, splitDataUrl } from "../../../protocol/codecs";
28
+ import { IMAGE_CLAMP_MAX_BYTES } from "../../../protocol/image-clamp";
29
+ import type { ProviderAdapter, ProviderDispatchContext, ProviderDispatchTarget } from "../../provider-contracts";
30
+ import { readCredentialSecret } from "../../provider-contracts";
31
+ import type { Http2BidiEndReason, ValidatedHttp2BidiStream } from "../../../network/http2-bidi";
32
+ import type { ProviderContinuationScope } from "../../provider-contracts";
33
+ import { CURSOR_PROVIDER_ID, RUN_IDLE_TTL_MS, isAllowedAgentHost } from "./cursor-shared";
34
+ import {
35
+ CURSOR_MCP_NAMESPACE as MCP_NAMESPACE,
36
+ buildCancelAction,
37
+ buildKvGetBlobResult,
38
+ buildKvSetBlobResult,
39
+ buildMcpRejected,
40
+ buildMcpResult,
41
+ buildMcpStateResult,
42
+ buildHeartbeat,
43
+ buildRequestContextResult,
44
+ buildRunRequest,
45
+ historyFromRequest,
46
+ liveUserText,
47
+ systemPromptFrom,
48
+ toolDescriptorsFor,
49
+ type CursorImageInput,
50
+ type CursorToolDescriptor,
51
+ } from "./cursor-request";
52
+ import { cursorRunTerminationError, decodeServerMessages, openCursorRun } from "./cursor-http2";
53
+ import { cursorRunSessions } from "./cursor-session-manager";
54
+ import type { CursorContinuationScope, CursorRunSession } from "./cursor-session-manager";
55
+ import { fetchCursorRunOrigin } from "./cursor-catalog";
56
+ import { defaultParameterValues, fetchCursorModels } from "./cursor-catalog";
57
+ import type { CursorModelInfo } from "./cursor-catalog";
58
+ import { decodeValueToJson, decodeStructEntriesToJson } from "./protocol/struct";
59
+
60
+ /**
61
+ * Client-heartbeat cadence for every Cursor Run.
62
+ *
63
+ * The edge answers a turn only after it has seen at least one client
64
+ * heartbeat, and the donor pins this to 5s (`heartbeatMs` in its continuation
65
+ * policy). A longer cadence is a Run that streams heartbeats and never
66
+ * answers; a shorter one is traffic the upstream does not ask for.
67
+ */
68
+ const CURSOR_HEARTBEAT_INTERVAL_MS = 5_000;
69
+
70
+ /**
71
+ * How long one account's model roster is reused for `parameters` lookups.
72
+ *
73
+ * A Run needs the model's declared default parameter tuple, but the roster is
74
+ * per-account and moves rarely, so it is fetched once and cached rather than on
75
+ * every dispatch.
76
+ */
77
+ const CURSOR_ROSTER_TTL_MS = 300_000;
78
+ /** credential → { roster, fetchedAt } for the parameter lookup. */
79
+ const rosterCache = new Map<string, { roster: readonly CursorModelInfo[]; fetchedAt: number }>();
80
+ /** Client-heartbeat cadence the Cursor edge expects (donor `heartbeatMs`). */
81
+ const CURSOR_HEARTBEAT_INTERVAL_OVERRIDE = (() => {
82
+ const raw = process.env.CARTETHYIA_CURSOR_HEARTBEAT_MS?.trim();
83
+ if (raw === undefined || raw === "") return undefined;
84
+ const parsed = Number(raw);
85
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;
86
+ })();
87
+
88
+ /** Image MIME types Cursor accepts in `selected_images`. */
89
+ const ALLOWED_IMAGE_MIME_TYPES: Record<string, true> = {
90
+ "image/png": true,
91
+ "image/jpeg": true,
92
+ "image/webp": true,
93
+ "image/gif": true,
94
+ };
95
+
96
+ /** Largest image payload accepted, matching the shared pre-send clamp. */
97
+ const MAX_IMAGE_BYTES = IMAGE_CLAMP_MAX_BYTES;
98
+
99
+ /** Identifies an allowed image payload from its magic bytes. */
100
+ function sniffImageMime(bytes: Uint8Array): string | undefined {
101
+ if (bytes.byteLength >= 4 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
102
+ return "image/png";
103
+ }
104
+ if (bytes.byteLength >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
105
+ return "image/jpeg";
106
+ }
107
+ if (
108
+ bytes.byteLength >= 6 &&
109
+ bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38
110
+ ) {
111
+ return "image/gif";
112
+ }
113
+ if (
114
+ bytes.byteLength >= 12 &&
115
+ bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
116
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
117
+ ) {
118
+ return "image/webp";
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ function base64ToBytes(data: string): Uint8Array | undefined {
124
+ try {
125
+ const binary = atob(data);
126
+ const out = new Uint8Array(binary.length);
127
+ for (let index = 0; index < binary.length; index += 1) out[index] = binary.charCodeAt(index);
128
+ return out;
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Validates one decoded image payload.
136
+ *
137
+ * The MIME type is verified by sniffing and only then compared against the
138
+ * declared type: a caller-supplied `content-type` or data-URL media type is a
139
+ * claim, and sending bytes the upstream will reject — or bytes that are not an
140
+ * image at all — fails the whole request rather than being dropped silently.
141
+ */
142
+ function finishImage(bytes: Uint8Array): CursorImageInput {
143
+ const sniffed = sniffImageMime(bytes);
144
+ const mimeType = sniffed;
145
+ if (mimeType === undefined || ALLOWED_IMAGE_MIME_TYPES[mimeType] !== true) {
146
+ throw new GatewayError(
147
+ "invalid_request",
148
+ 400,
149
+ "Cursor accepts PNG, JPEG, WebP and GIF image input only",
150
+ );
151
+ }
152
+ if (bytes.byteLength > MAX_IMAGE_BYTES) {
153
+ throw new GatewayError(
154
+ "invalid_request",
155
+ 400,
156
+ `Cursor image input exceeds the ${MAX_IMAGE_BYTES}-byte limit`,
157
+ );
158
+ }
159
+ return { data: bytes, mimeType };
160
+ }
161
+
162
+ /** Reads one canonical image part into Cursor `selected_images` bytes. */
163
+ async function resolveImageInput(
164
+ part: Extract<ContentPart, { kind: "image" }>,
165
+ context: ProviderDispatchContext,
166
+ ): Promise<CursorImageInput> {
167
+ const source = resolveImageSource(part.payload);
168
+ const url = source?.url;
169
+ if (url === undefined) {
170
+ throw new GatewayError(
171
+ "invalid_request",
172
+ 400,
173
+ "Cursor image input requires inline image data or an HTTPS image URL",
174
+ );
175
+ }
176
+ const dataUrl = splitDataUrl(url);
177
+ if (dataUrl !== undefined) {
178
+ const bytes = base64ToBytes(dataUrl.data);
179
+ if (bytes === undefined) {
180
+ throw new GatewayError("invalid_request", 400, "Cursor image attachment is not valid base64");
181
+ }
182
+ return finishImage(bytes);
183
+ }
184
+ const parsed = new URL(url);
185
+ if (parsed.protocol !== "https:") {
186
+ throw new GatewayError(
187
+ "invalid_request",
188
+ 400,
189
+ "Cursor image URLs must be HTTPS; private and non-TLS sources are not fetched",
190
+ );
191
+ }
192
+ const fetcher = context.outbound_attachment_fetch;
193
+ if (fetcher === undefined) {
194
+ throw new GatewayError(
195
+ "invalid_request",
196
+ 400,
197
+ "Cursor has no validated egress to fetch this image URL",
198
+ );
199
+ }
200
+ // Redirects are not followed: reject the response rather than letting a
201
+ // public URL redirect the attachment fetch to a private address.
202
+ const response = await fetcher(url, {
203
+ method: "GET",
204
+ redirect: "manual",
205
+ signal: context.abort_signal,
206
+ });
207
+ if (!response.ok) {
208
+ throw new GatewayError(
209
+ "invalid_request",
210
+ 400,
211
+ `Cursor could not fetch this image URL (${response.status})`,
212
+ );
213
+ }
214
+ const body = new Uint8Array(await response.arrayBuffer());
215
+ if (body.byteLength > MAX_IMAGE_BYTES) {
216
+ throw new GatewayError(
217
+ "invalid_request",
218
+ 400,
219
+ `Cursor image input exceeds the ${MAX_IMAGE_BYTES}-byte limit`,
220
+ );
221
+ }
222
+ return finishImage(body);
223
+ }
224
+
225
+ /** Collects every canonical image part of a request into Cursor images. */
226
+ async function collectImages(
227
+ request: CanonicalRequest,
228
+ context: ProviderDispatchContext,
229
+ ): Promise<CursorImageInput[]> {
230
+ const images: CursorImageInput[] = [];
231
+ for (const message of request.messages) {
232
+ for (const part of message.content) {
233
+ if (part.kind !== "image") continue;
234
+ images.push(await resolveImageInput(part, context));
235
+ }
236
+ }
237
+ return images;
238
+ }
239
+
240
+ function usageFromTurnEnded(turnEnded: Record<string, unknown>): UsageRecord {
241
+ const count = (value: unknown): number =>
242
+ typeof value === "number" && Number.isFinite(value) ? value : 0;
243
+ const input = count(turnEnded["input_tokens"]);
244
+ const output = count(turnEnded["output_tokens"]);
245
+ const cacheRead = count(turnEnded["cache_read"]);
246
+ return {
247
+ input_tokens: input,
248
+ cached_input_tokens: cacheRead,
249
+ cache_write_tokens: count(turnEnded["cache_write"]),
250
+ uncached_input_tokens: Math.max(0, input - cacheRead),
251
+ output_tokens: output,
252
+ reasoning_tokens: count(turnEnded["reasoning_tokens"]),
253
+ estimated_cost: null,
254
+ total_tokens: input + output,
255
+ };
256
+ }
257
+
258
+ function recordOf(value: unknown): Record<string, unknown> | undefined {
259
+ return value !== null && typeof value === "object" && !Array.isArray(value)
260
+ ? (value as Record<string, unknown>)
261
+ : undefined;
262
+ }
263
+
264
+ function nonEmptyStringOf(value: unknown): string | undefined {
265
+ return typeof value === "string" && value.length > 0 ? value : undefined;
266
+ }
267
+
268
+ /**
269
+ * Reads one inbound `mcp_args` request.
270
+ *
271
+ * Returns `undefined` for anything that is not an executable call. Cursor's own
272
+ * host tools arrive under their own server identifiers (or none at all) and are
273
+ * never executed here — refusing them is what keeps a model's request from
274
+ * turning into a shell command on the gateway's host.
275
+ */
276
+ function readExecutableMcpCall(
277
+ mcpArgs: Record<string, unknown>,
278
+ descriptors: readonly CursorToolDescriptor[],
279
+ ): { toolName: string; arguments: string } | undefined {
280
+ if (nonEmptyStringOf(mcpArgs["provider_identifier"]) !== MCP_NAMESPACE) return undefined;
281
+ const toolName = nonEmptyStringOf(mcpArgs["tool_name"]);
282
+ if (toolName === undefined) return undefined;
283
+ if (!descriptors.some((descriptor) => descriptor.toolName === toolName)) return undefined;
284
+ return { toolName, arguments: decodeMcpArguments(mcpArgs) };
285
+ }
286
+
287
+ /**
288
+ * Decodes Cursor's `McpArgs.args` — a `map<string, google.protobuf.Value>`
289
+ * carried as repeated map-entry messages — into a canonical arguments string.
290
+ */
291
+ function decodeMcpArguments(mcpArgs: Record<string, unknown>): string {
292
+ const entries = mcpArgs["args"];
293
+ const raw = Array.isArray(entries)
294
+ ? entries.filter((entry): entry is Uint8Array => entry instanceof Uint8Array)
295
+ : [];
296
+ if (raw.length === 0) return "{}";
297
+ try {
298
+ return JSON.stringify(decodeStructEntriesToJson(raw));
299
+ } catch {
300
+ return "{}";
301
+ }
302
+ }
303
+
304
+ /** One image-bearing block of a tool result, ready for `McpToolResultContentItem`. */
305
+ interface ResultImage {
306
+ readonly data: Uint8Array;
307
+ readonly mimeType: string;
308
+ }
309
+
310
+ /** Reads the images out of one canonical tool result. */
311
+ function resultImages(part: Extract<ContentPart, { kind: "toolResult" }>): ResultImage[] {
312
+ if (typeof part.content === "string") return [];
313
+ const images: ResultImage[] = [];
314
+ for (const child of part.content) {
315
+ if (child.kind !== "image") continue;
316
+ const source = resolveImageSource(child.payload);
317
+ const url = source?.url;
318
+ if (url === undefined) continue;
319
+ const dataUrl = splitDataUrl(url);
320
+ if (dataUrl === undefined) continue;
321
+ const bytes = base64ToBytes(dataUrl.data);
322
+ if (bytes === undefined) continue;
323
+ const mimeType = sniffImageMime(bytes) ?? dataUrl.mediaType;
324
+ if (ALLOWED_IMAGE_MIME_TYPES[mimeType] !== true) continue;
325
+ if (bytes.byteLength > MAX_IMAGE_BYTES) continue;
326
+ images.push({ data: bytes, mimeType });
327
+ }
328
+ return images;
329
+ }
330
+
331
+ /** One Run's live state while this adapter is pumping it. */
332
+ interface RunHandle {
333
+ readonly stream: ValidatedHttp2BidiStream;
334
+ readonly write: (message: Uint8Array) => Promise<void>;
335
+ close(reason?: string): void;
336
+ }
337
+
338
+ class CursorAdapter implements ProviderAdapter {
339
+ readonly provider_id = CURSOR_PROVIDER_ID;
340
+
341
+ /**
342
+ * Dispatches one canonical request.
343
+ *
344
+ * Two entry shapes, decided before anything is dialed:
345
+ *
346
+ * - **Continuation** — the request carries a tool result for a call this
347
+ * process is holding. The already-open Run is resumed: no new Run, no new
348
+ * account lease, no new pool slot.
349
+ * - **Fresh turn** — a new Run. If the request carries complete assistant +
350
+ * tool history but no live session (a restart, or an expired idle Run), the
351
+ * whole history is rebased into the new Run so the model still sees the
352
+ * conversation.
353
+ */
354
+ async *dispatch(
355
+ request: CanonicalRequest,
356
+ candidate: ProviderDispatchTarget,
357
+ context: ProviderDispatchContext,
358
+ ): AsyncIterable<CanonicalEvent> {
359
+ const pending = readPendingToolResult(request, context);
360
+ if (pending !== undefined && pending.length > 0) {
361
+ yield* this.#continueRun(request, candidate, context, pending);
362
+ return;
363
+ }
364
+ yield* this.#startRun(request, candidate, context);
365
+ }
366
+
367
+ /**
368
+ * Resumes the held Run for one tool result.
369
+ *
370
+ * Every pending call must resolve to the *same* Run and the same account. A
371
+ * request whose calls resolve to different Runs — or to no live Run at all —
372
+ * is refused rather than partially applied: writing one result to another
373
+ * account's Run would corrupt both conversations, and silently starting a new
374
+ * Run would answer a tool call the model is still waiting on.
375
+ */
376
+ async *#continueRun(
377
+ request: CanonicalRequest,
378
+ candidate: ProviderDispatchTarget,
379
+ context: ProviderDispatchContext,
380
+ pending: readonly PendingToolResult[],
381
+ ): AsyncIterable<CanonicalEvent> {
382
+ const scope = context.continuation_scope;
383
+ if (scope === undefined) {
384
+ throw new GatewayError(
385
+ "invalid_request",
386
+ 400,
387
+ "This tool result cannot be matched to a live Cursor Run; resend the complete conversation.",
388
+ );
389
+ }
390
+ const resolved = pending.map((item) => cursorRunSessions.resolveCall(item.callId, scope));
391
+ if (resolved.some((entry) => entry === undefined)) {
392
+ throw new GatewayError(
393
+ "invalid_request",
394
+ 400,
395
+ "This tool result refers to a Cursor tool call that is no longer pending. Resend the complete conversation.",
396
+ { callIds: pending.map((item) => item.callId) },
397
+ );
398
+ }
399
+ const runId = resolved[0]?.session.runId;
400
+ if (runId === undefined) {
401
+ throw new GatewayError("invalid_request", 400, "Cursor Run is no longer live");
402
+ }
403
+ if (resolved.some((entry) => entry?.session.runId !== runId)) {
404
+ throw new GatewayError(
405
+ "invalid_request",
406
+ 400,
407
+ "These tool results belong to different Cursor Runs; resend them one Run at a time.",
408
+ );
409
+ }
410
+ const session = cursorRunSessions.get(runId);
411
+ if (session === undefined) {
412
+ throw new GatewayError("invalid_request", 400, "Cursor Run is no longer live");
413
+ }
414
+ const handle = this.#handles.get(runId);
415
+ if (handle === undefined) {
416
+ // The Run's socket is gone even though the record survived (a transport
417
+ // failure the pump did not observe). Rebase rather than answer into a
418
+ // dead stream.
419
+ cursorRunSessions.terminate(runId, "Cursor Run transport is gone");
420
+ yield* this.#startRun(request, candidate, context);
421
+ return;
422
+ }
423
+ let sequence = 0;
424
+ const next = (): number => (sequence += 1);
425
+ try {
426
+ for (const item of pending) {
427
+ const entry = cursorRunSessions.resolveCall(item.callId, scope);
428
+ if (entry === undefined) continue;
429
+ await handle.write(
430
+ buildMcpResult({
431
+ execId: entry.call.execId,
432
+ text: item.text,
433
+ isError: item.isError,
434
+ ...(item.images.length > 0 ? { images: item.images } : {}),
435
+ }),
436
+ );
437
+ cursorRunSessions.clearPendingCall(runId, item.callId);
438
+ }
439
+ yield* this.#pump({
440
+ request,
441
+ candidate,
442
+ context,
443
+ handle,
444
+ runId,
445
+ session,
446
+ next,
447
+ continuation: true,
448
+ });
449
+ } finally {
450
+ // The Run stays open only while it still owes an answer. A terminal close
451
+ // is the session manager's single release path.
452
+ if (cursorRunSessions.get(runId) === undefined) this.#handles.delete(runId);
453
+ else if (!this.#awaitsToolResult && session.pending.size === 0) {
454
+ cursorRunSessions.terminate(runId);
455
+ this.#handles.delete(runId);
456
+ }
457
+ }
458
+ }
459
+
460
+ async *#startRun(
461
+ request: CanonicalRequest,
462
+ candidate: ProviderDispatchTarget,
463
+ context: ProviderDispatchContext,
464
+ ): AsyncIterable<CanonicalEvent> {
465
+ const outboundHttp2 = context.outbound_http2;
466
+ if (outboundHttp2 === undefined) {
467
+ throw new GatewayError(
468
+ "admission_unavailable",
469
+ 503,
470
+ "Cursor requires validated HTTP/2 egress; none is configured for this route",
471
+ { providerId: CURSOR_PROVIDER_ID },
472
+ );
473
+ }
474
+ const credential = readCredentialSecret(
475
+ context.credential,
476
+ "Cursor account credential is missing or empty; reconnect the account",
477
+ );
478
+ const origin = await this.#resolveOrigin(credential, context);
479
+ const rawModelId = candidate.model_id.length > 0 ? candidate.model_id : request.model;
480
+ const modelId = resolveCursorWireModelId(rawModelId);
481
+ // Cursor declares per-model parameter tuples (effort / context / fast). A
482
+ // fresh Run must carry the model's declared default tuple: with an empty
483
+ // `parameters` the Run streams only heartbeats and never turns.
484
+ const parameterValues = defaultParameterValues(
485
+ await this.#rosterFor(credential, context).then((roster) =>
486
+ roster.find((model) => model.id === modelId),
487
+ ),
488
+ );
489
+ const images = await collectImages(request, context);
490
+ const descriptors = toolDescriptorsFor(request.tools);
491
+ const systemPrompt = systemPromptFrom(request);
492
+ const conversationId = crypto.randomUUID();
493
+ const runId = crypto.randomUUID();
494
+ const opened = await openCursorRun({
495
+ origin,
496
+ credential,
497
+ outboundHttp2,
498
+ signal: context.abort_signal,
499
+ });
500
+ let closed = false;
501
+ const handle: RunHandle = {
502
+ stream: opened.stream,
503
+ write: opened.write,
504
+ close: (reason) => {
505
+ if (closed) return;
506
+ closed = true;
507
+ if (reason !== undefined && reason.length > 0) {
508
+ void opened.write(buildCancelAction(reason)).catch(() => undefined);
509
+ }
510
+ opened.stream.abort();
511
+ },
512
+ };
513
+ this.#handles.set(runId, handle);
514
+ const session: CursorRunSession = {
515
+ runId,
516
+ conversationId,
517
+ scope: sessionScope(context),
518
+ descriptors,
519
+ pending: new Map(),
520
+ calls: new Map(),
521
+ usage: undefined,
522
+ failure: undefined,
523
+ };
524
+ let sequence = 0;
525
+ const next = (): number => (sequence += 1);
526
+ try {
527
+ await opened.write(
528
+ buildRunRequest({
529
+ text: liveUserText(request),
530
+ modelId,
531
+ conversationId,
532
+ runId,
533
+ ...(images.length > 0 ? { images } : {}),
534
+ ...(descriptors.length > 0 ? { descriptors } : {}),
535
+ ...(systemPrompt === undefined ? {} : { systemPrompt }),
536
+ ...(parameterValues.length > 0 ? { parameterValues } : {}),
537
+ // A fresh Run always rebases the client's history into its user text:
538
+ // Cursor's own checkpoint cannot be reconstructed after a restart, and
539
+ // `root_prompt_messages_json` would suppress the RequestContext rules.
540
+ history: historyFromRequest(request),
541
+ }),
542
+ );
543
+ // Registered before the first frame is pumped: a tool call can arrive on
544
+ // the very first frame, and the Run must already be claimable.
545
+ cursorRunSessions.register({
546
+ session,
547
+ releaseLease: context.lease_ownership?.transferToProviderSession(),
548
+ close: (reason) => handle.close(reason),
549
+ idleTtlMs: RUN_IDLE_TTL_MS,
550
+ });
551
+ yield* this.#pump({
552
+ request,
553
+ candidate,
554
+ context,
555
+ handle,
556
+ runId,
557
+ session,
558
+ next,
559
+ continuation: false,
560
+ });
561
+ } catch (error) {
562
+ cursorRunSessions.terminate(runId, error instanceof Error ? error.message : undefined);
563
+ this.#handles.delete(runId);
564
+ throw error;
565
+ } finally {
566
+ if (cursorRunSessions.get(runId) === undefined) this.#handles.delete(runId);
567
+ // A Run whose tool call is registered and unanswered is *held*, not
568
+ // finished. The consumer may stop reading before the pump reaches its
569
+ // terminal (a stream the client already has enough of), and tearing the
570
+ // Run down there loses the call it just handed out — the next request's
571
+ // tool result then resolves nothing.
572
+ else if (!this.#awaitsToolResult && session.pending.size === 0) {
573
+ cursorRunSessions.terminate(runId);
574
+ this.#handles.delete(runId);
575
+ }
576
+ }
577
+ }
578
+
579
+ /** Live Run handles, keyed by run id. Process-local, like the sessions. */
580
+ readonly #handles = new Map<string, RunHandle>();
581
+ /** Set by the pump when the last terminal it emitted still owes a tool result. */
582
+ #awaitsToolResult = false;
583
+
584
+ /**
585
+ * Resolves the Run origin for one account.
586
+ *
587
+ * Cursor routes an account to a region-specific host, and the wrong host
588
+ * silently refuses the Run; the API host serves auth and catalog calls but is
589
+ * not the origin `GetServerConfig` names for Run. A resolution failure is a
590
+ * transport fault, never a fallback to the API host.
591
+ */
592
+ async #resolveOrigin(credential: string, context: ProviderDispatchContext): Promise<string> {
593
+ try {
594
+ const origin = await fetchCursorRunOrigin({
595
+ credential,
596
+ ...(context.outbound_fetch === undefined
597
+ ? {}
598
+ : { fetcher: context.outbound_fetch as unknown as typeof fetch }),
599
+ signal: context.abort_signal,
600
+ });
601
+ if (!isAllowedAgentHost(new URL(origin).hostname)) {
602
+ throw new Error("resolved origin is not a Cursor host");
603
+ }
604
+ return origin;
605
+ } catch (error) {
606
+ throw new GatewayError(
607
+ "platform_unavailable",
608
+ 502,
609
+ `Cursor Run origin could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
610
+ { providerId: CURSOR_PROVIDER_ID },
611
+ "upstream",
612
+ );
613
+ }
614
+ }
615
+
616
+ /**
617
+ * The account's model roster, for the default parameter tuple.
618
+ *
619
+ * Cached per credential and never fatal: a Run whose model is not in the
620
+ * roster (or whose roster fetch failed) still runs with no parameters rather
621
+ * than failing the dispatch. The tuple is an optimization, not a requirement.
622
+ */
623
+ async #rosterFor(
624
+ credential: string,
625
+ context: ProviderDispatchContext,
626
+ ): Promise<readonly CursorModelInfo[]> {
627
+ const cached = rosterCache.get(credential);
628
+ if (cached !== undefined && Date.now() - cached.fetchedAt < CURSOR_ROSTER_TTL_MS) {
629
+ return cached.roster;
630
+ }
631
+ try {
632
+ const roster = await fetchCursorModels({
633
+ credential,
634
+ ...(context.outbound_fetch === undefined
635
+ ? {}
636
+ : { fetcher: context.outbound_fetch as unknown as typeof fetch }),
637
+ signal: context.abort_signal,
638
+ });
639
+ rosterCache.set(credential, { roster, fetchedAt: Date.now() });
640
+ return roster;
641
+ } catch {
642
+ return cached?.roster ?? [];
643
+ }
644
+ }
645
+
646
+ async *#pump(input: {
647
+ readonly request: CanonicalRequest;
648
+ readonly candidate: ProviderDispatchTarget;
649
+ readonly context: ProviderDispatchContext;
650
+ readonly handle: RunHandle;
651
+ readonly runId: string;
652
+ readonly session: CursorRunSession;
653
+ readonly next: () => number;
654
+ readonly continuation: boolean;
655
+ }): AsyncIterable<CanonicalEvent> {
656
+ const { context, handle, runId, session } = input;
657
+ let sawToolCall = false;
658
+ let terminalUsage: UsageRecord | undefined;
659
+ let stopReason: CanonicalStopReason = "stop";
660
+ let failure: GatewayError | undefined;
661
+ const unsubscribe = handle.stream.onTerminal((reason: Http2BidiEndReason) => {
662
+ if (reason.kind === "local-close") return;
663
+ const mapped = cursorRunTerminationError({ stream: handle.stream, reason });
664
+ if (mapped !== undefined) failure = mapped;
665
+ });
666
+ // The Cursor edge answers a Run only after it sees a client heartbeat (the
667
+ // donor's continuation policy pins `heartbeatMs` to 5s). One heartbeat is
668
+ // sent eagerly on open here and then on the interval; clearing it when the
669
+ // body reader ends is safe because the reader's end already means the Run
670
+ // is over.
671
+ const beat = (): void =>
672
+ void handle.write(buildHeartbeat()).catch(() => undefined);
673
+ const onAbort = (): void => clearInterval(heartbeatTimer);
674
+ const heartbeatTimer = setInterval(
675
+ beat,
676
+ CURSOR_HEARTBEAT_INTERVAL_OVERRIDE ?? CURSOR_HEARTBEAT_INTERVAL_MS,
677
+ );
678
+ void beat();
679
+ context.abort_signal.addEventListener("abort", onAbort, { once: true });
680
+ if (context.abort_signal.aborted) onAbort();
681
+ heartbeatTimer.unref();
682
+ try {
683
+ for await (const message of decodeServerMessages(handle.stream.body)) {
684
+ if (context.abort_signal.aborted) {
685
+ failure = new GatewayError("transport_closed", 499, "request was cancelled");
686
+ break;
687
+ }
688
+ const execServer = recordOf(message["exec_server_message"]);
689
+ if (execServer !== undefined) {
690
+ const handled = await this.#handleExecServerMessage(execServer, session, handle);
691
+ if (handled.kind !== "tool_call") continue;
692
+ const callId = `call_cursor_${runId}_${handled.execId}`;
693
+ const scope = context.continuation_scope;
694
+ if (scope === undefined) {
695
+ // Without a scope, no later request could be authorized to answer
696
+ // this call, so the Run must not be held open for one.
697
+ await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable"));
698
+ continue;
699
+ }
700
+ if (!cursorRunSessions.registerPendingCall(runId, { callId, execId: handled.execId, toolName: handled.toolName })) {
701
+ await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable"));
702
+ continue;
703
+ }
704
+ sawToolCall = true;
705
+ yield {
706
+ type: "tool_call_delta",
707
+ sequence_number: input.next(),
708
+ call_id: callId,
709
+ name: handled.toolName,
710
+ arguments_delta: handled.arguments,
711
+ };
712
+ continue;
713
+ }
714
+
715
+ const kv = recordOf(message["kv_server_message"]);
716
+ if (kv !== undefined && (await this.#handleKvMessage(kv, handle))) continue;
717
+
718
+ const interaction = recordOf(message["interaction_update"]);
719
+ if (interaction === undefined) continue;
720
+
721
+ const textDelta = recordOf(interaction["text_delta"]);
722
+ if (textDelta !== undefined) {
723
+ const text = typeof textDelta["text"] === "string" ? textDelta["text"] : "";
724
+ if (text.length > 0) {
725
+ yield {
726
+ type: "content_delta",
727
+ sequence_number: input.next(),
728
+ content: { kind: "text", text },
729
+ };
730
+ }
731
+ continue;
732
+ }
733
+ const thinking = recordOf(interaction["thinking_delta"]);
734
+ if (thinking !== undefined) {
735
+ const text = typeof thinking["text"] === "string" ? thinking["text"] : "";
736
+ if (text.length > 0) {
737
+ yield {
738
+ type: "content_delta",
739
+ sequence_number: input.next(),
740
+ content: { kind: "reasoning", payload: null, summary: text },
741
+ };
742
+ }
743
+ continue;
744
+ }
745
+ // `tool_call_started` / `tool_call_completed` are display notifications:
746
+ // they report what Cursor's host did, not a call this gateway executes.
747
+ // Only `turn_ended` is authoritative for the turn's end and its usage.
748
+ const turnEnded = recordOf(interaction["turn_ended"]);
749
+ if (turnEnded !== undefined) {
750
+ terminalUsage = usageFromTurnEnded(turnEnded);
751
+ break;
752
+ }
753
+ }
754
+ } catch (error) {
755
+ failure =
756
+ error instanceof GatewayError
757
+ ? error
758
+ : new GatewayError(
759
+ "platform_unavailable",
760
+ 502,
761
+ `Cursor Run failed: ${error instanceof Error ? error.message : String(error)}`,
762
+ { providerId: CURSOR_PROVIDER_ID },
763
+ "upstream",
764
+ );
765
+ } finally {
766
+ unsubscribe();
767
+ clearInterval(heartbeatTimer);
768
+ context.abort_signal.removeEventListener("abort", onAbort);
769
+ }
770
+
771
+ if (failure !== undefined) {
772
+ this.#awaitsToolResult = false;
773
+ cursorRunSessions.terminate(runId, failure.message);
774
+ yield {
775
+ type: "terminal",
776
+ sequence_number: input.next(),
777
+ state: "failed",
778
+ stop_reason: "error",
779
+ // The refusal text, so the caller sees "you hit your usage limit"
780
+ // instead of a terminal with no reason attached.
781
+ stop_details: { code: failure.code, status: failure.status, message: failure.message },
782
+ ...(terminalUsage === undefined ? {} : { usage: terminalUsage }),
783
+ };
784
+ return;
785
+ }
786
+ if (sawToolCall) {
787
+ // The interim terminal: the client must answer the tool call, and this Run
788
+ // stays open for it. `usage_deferred` tells the settle path not to charge
789
+ // estimated tokens for a turn whose real usage arrives with the Run's
790
+ // `turn_ended` on the continuation request.
791
+ this.#awaitsToolResult = true;
792
+ cursorRunSessions.touch(runId);
793
+ yield {
794
+ type: "terminal",
795
+ sequence_number: input.next(),
796
+ state: "complete",
797
+ stop_reason: "tool_use",
798
+ usage_deferred: true,
799
+ };
800
+ return;
801
+ }
802
+ this.#awaitsToolResult = false;
803
+ cursorRunSessions.terminate(runId);
804
+ yield {
805
+ type: "terminal",
806
+ sequence_number: input.next(),
807
+ state: "complete",
808
+ stop_reason: stopReason,
809
+ ...(terminalUsage === undefined ? {} : { usage: terminalUsage }),
810
+ };
811
+ }
812
+
813
+ /**
814
+ * Handles one Cursor exec request.
815
+ *
816
+ * The control paths are mandatory, not optional: `request_context_args`,
817
+ * `mcp_state_exec_args`, and the KV channel all stall the Run forever if the
818
+ * client never answers. Only `mcp_args` — and only for a declared tool —
819
+ * becomes a tool call; every other variant is refused rather than executed.
820
+ */
821
+ async #handleExecServerMessage(
822
+ execServer: Record<string, unknown>,
823
+ session: CursorRunSession,
824
+ handle: RunHandle,
825
+ ): Promise<{ kind: "handled" } | { kind: "tool_call"; execId: number; toolName: string; arguments: string }> {
826
+ const execId = typeof execServer["id"] === "number" ? execServer["id"] : 0;
827
+ if (recordOf(execServer["request_context_args"]) !== undefined) {
828
+ await handle.write(buildRequestContextResult(execId, probeContextFor(session.descriptors)));
829
+ return { kind: "handled" };
830
+ }
831
+ if (recordOf(execServer["mcp_state_exec_args"]) !== undefined) {
832
+ await handle.write(buildMcpStateResult(execId, session.descriptors));
833
+ return { kind: "handled" };
834
+ }
835
+ const mcpArgs = recordOf(execServer["mcp_args"]);
836
+ if (mcpArgs !== undefined) {
837
+ const call = readExecutableMcpCall(mcpArgs, session.descriptors);
838
+ if (call === undefined) {
839
+ await handle.write(
840
+ buildMcpRejected(execId, "this tool is not one the request declared"),
841
+ );
842
+ return { kind: "handled" };
843
+ }
844
+ return { kind: "tool_call", execId, toolName: call.toolName, arguments: call.arguments };
845
+ }
846
+ // Every other exec variant is a Cursor host tool (shell, read, edit, glob,
847
+ // subagent, …). Refusing it is the security boundary: the gateway does not
848
+ // run filesystem or shell operations the client never asked for.
849
+ await handle.write(buildMcpRejected(execId, "Cursor host tools are not executed by this gateway"));
850
+ return { kind: "handled" };
851
+ }
852
+
853
+ /** Answers one KV blob request; `false` when the message carried none. */
854
+ async #handleKvMessage(kv: Record<string, unknown>, handle: RunHandle): Promise<boolean> {
855
+ const id = typeof kv["id"] === "number" ? kv["id"] : 0;
856
+ if (recordOf(kv["set_blob_args"]) !== undefined) {
857
+ await handle.write(buildKvSetBlobResult(id));
858
+ return true;
859
+ }
860
+ const getArgs = recordOf(kv["get_blob_args"]);
861
+ if (getArgs !== undefined) {
862
+ const blobId = getArgs["blob_id"];
863
+ const data = blobId instanceof Uint8Array ? blobId : new Uint8Array(0);
864
+ await handle.write(buildKvGetBlobResult(id, data));
865
+ return true;
866
+ }
867
+ return false;
868
+ }
869
+ }
870
+
871
+ /** RequestContext Cursor receives in reply to a context probe. */
872
+ function probeContextFor(descriptors: readonly CursorToolDescriptor[]): Record<string, unknown> {
873
+ return {
874
+ tools: descriptors.map((descriptor) => ({
875
+ name: descriptor.name,
876
+ description: descriptor.description,
877
+ input_schema: descriptor.inputSchema,
878
+ provider_identifier: descriptor.providerIdentifier,
879
+ tool_name: descriptor.toolName,
880
+ })),
881
+ mcp_info_complete: true,
882
+ rules_info_complete: true,
883
+ env_info_complete: true,
884
+ };
885
+ }
886
+
887
+ /**
888
+ * The continuation scope of one dispatch.
889
+ *
890
+ * Derived from the route's own account identity plus the caller's tenant and API
891
+ * key. A route with no account cannot scope a continuation, and a Run that
892
+ * cannot be scoped must not be held open across requests at all.
893
+ */
894
+ function sessionScope(context: ProviderDispatchContext): CursorContinuationScope {
895
+ const scope: ProviderContinuationScope | undefined = context.continuation_scope;
896
+ return {
897
+ tenantId: scope?.tenantId ?? "",
898
+ apiKeyId: scope?.apiKeyId ?? "",
899
+ providerId: CURSOR_PROVIDER_ID,
900
+ providerAccountId: scope?.providerAccountId ?? context.credential.account_id ?? "",
901
+ };
902
+ }
903
+
904
+ /** One tool result the client sent back for a call this process is holding. */
905
+ interface PendingToolResult {
906
+ readonly callId: string;
907
+ readonly text: string;
908
+ readonly isError: boolean;
909
+ readonly images: readonly ResultImage[];
910
+ }
911
+
912
+ /**
913
+ * Reads the tool results this request wants delivered to a held Run.
914
+ *
915
+ * Only ids in the `call_cursor_<run-id>_<exec-id>` shape are considered: any
916
+ * other id is the client's own, belongs to a different provider's conversation,
917
+ * and must not be sent to Cursor.
918
+ */
919
+ function readPendingToolResult(
920
+ request: CanonicalRequest,
921
+ _context: ProviderDispatchContext,
922
+ ): PendingToolResult[] | undefined {
923
+ const results: PendingToolResult[] = [];
924
+ for (const message of request.messages) {
925
+ for (const part of message.content) {
926
+ if (part.kind !== "toolResult") continue;
927
+ if (!part.call_id.startsWith("call_cursor_")) continue;
928
+ const text =
929
+ typeof part.content === "string"
930
+ ? part.content
931
+ : part.content.map((child) => (child.kind === "text" ? child.text : "")).join("\n");
932
+ results.push({
933
+ callId: part.call_id,
934
+ text,
935
+ isError: part.is_error === true,
936
+ images: resultImages(part),
937
+ });
938
+ }
939
+ }
940
+ return results.length > 0 ? results : undefined;
941
+ }
942
+
943
+ /** Reconstructs one tool result's text and images for the adapter's tests. */
944
+ export { decodeStructEntriesToJson, decodeValueToJson };
945
+
946
+ export function createCursorAdapter(): ProviderAdapter {
947
+ return new CursorAdapter();
948
+ }
949
+
950
+ export const cursorAdapter: ProviderAdapter = createCursorAdapter();
951
+ /** Maps virtual/canonical client model IDs to upstream Cursor wire IDs. */
952
+ function resolveCursorWireModelId(rawModelId: string): string {
953
+ const id = rawModelId.trim().toLowerCase();
954
+ // Only `auto` is an alias: Cursor's own roster names its Auto model
955
+ // `default`. `grok-4.6` is already the upstream id, and rewriting it to
956
+ // `cursor-grok-4.6` made every Grok Run answer "AI Model Not Found".
957
+ if (id === "auto") return "default";
958
+ return rawModelId;
959
+ }
960
+