cartethyia 2.5.2 → 2.5.4

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 (277) hide show
  1. package/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  3. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
  26. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
  38. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  39. package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
  40. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  41. package/dist/dashboard/assets/app-COlK95oi.css +2 -0
  42. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  43. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
  44. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
  46. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
  48. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  49. package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
  50. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  51. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
  52. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  55. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  56. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  57. package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
  58. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  59. package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
  60. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
  62. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
  64. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
  66. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
  70. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
  72. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
  74. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
  76. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
  78. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  79. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
  80. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  81. package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
  82. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
  84. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  85. package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
  86. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  87. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
  88. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
  89. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
  90. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  91. package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
  92. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  93. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
  94. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  95. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
  96. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  97. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
  98. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  99. package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
  100. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  101. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  102. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  105. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  106. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  107. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  108. package/dist/dashboard/index.html +1 -1
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -54
  111. package/package.json +1 -1
  112. package/scripts/cli/autostart.ts +237 -0
  113. package/scripts/cli/command/console-menu.ts +244 -246
  114. package/scripts/cli/command/tray.ts +208 -204
  115. package/scripts/cli/command/update.ts +27 -10
  116. package/scripts/cli/main.ts +28 -1
  117. package/src/boot/app.ts +693 -693
  118. package/src/boot/dependencies.ts +646 -616
  119. package/src/config/spec.ts +81 -2
  120. package/src/config/telemetry.ts +46 -0
  121. package/src/console/api-keys/bucket-spend.ts +15 -4
  122. package/src/console/backup/contracts.ts +15 -2
  123. package/src/console/backup/routes.ts +52 -1
  124. package/src/console/backup/service.ts +60 -4
  125. package/src/console/console-router.ts +8 -0
  126. package/src/console/data-management/contracts.ts +93 -0
  127. package/src/console/data-management/operations.ts +229 -0
  128. package/src/console/data-management/routes.ts +104 -0
  129. package/src/console/domain-registration.ts +39 -0
  130. package/src/console/observability/contracts.ts +524 -485
  131. package/src/console/observability/store.ts +1115 -1018
  132. package/src/console/providers/catalog/contracts.ts +6 -0
  133. package/src/console/providers/catalog/provider-operations.ts +20 -0
  134. package/src/console/providers/catalog/routes.ts +7 -0
  135. package/src/console/providers/catalog/store.ts +22 -1
  136. package/src/console/quota/account-quota-shared.ts +52 -39
  137. package/src/console/quota/refresh.ts +441 -429
  138. package/src/console/routing/pools/import-parse.ts +206 -0
  139. package/src/console/routing/pools/operations.ts +543 -501
  140. package/src/console/routing/pools/routes.ts +181 -164
  141. package/src/console/settings/contracts.ts +37 -0
  142. package/src/console/settings/store.ts +51 -0
  143. package/src/console/share/share-router.ts +6 -9
  144. package/src/console/updates/routes.ts +16 -0
  145. package/src/main.ts +10 -0
  146. package/src/observability/event-compaction.ts +315 -0
  147. package/src/observability/event-day-index.ts +442 -0
  148. package/src/observability/event-log.ts +152 -3
  149. package/src/observability/payload-capture.ts +373 -344
  150. package/src/observability/payload-store.ts +440 -366
  151. package/src/observability/telemetry-status.ts +28 -16
  152. package/src/persistence/health-event-flush.ts +36 -0
  153. package/src/persistence/schema.ts +1402 -1360
  154. package/src/persistence/stores/api-key-store.ts +6 -0
  155. package/src/persistence/stores/share-store.ts +583 -556
  156. package/src/persistence/stores/telemetry-store.ts +237 -214
  157. package/src/protocol/codecs.ts +1 -2
  158. package/src/protocol/image-clamp.ts +19 -1
  159. package/src/protocol/registry.ts +8 -0
  160. package/src/protocol/request/gemini.ts +9 -2
  161. package/src/protocol/request/messages.ts +14 -2
  162. package/src/protocol/response/gemini.ts +18 -4
  163. package/src/providers/adapters/compatible-adapter.ts +3 -0
  164. package/src/providers/bundled-discovery-providers.ts +122 -119
  165. package/src/providers/configured-openai-providers.ts +136 -134
  166. package/src/providers/default-registry.ts +5 -4
  167. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  168. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  169. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  170. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  171. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  172. package/src/providers/integrations/cline/cline-quota.ts +132 -128
  173. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  174. package/src/providers/integrations/codex/codex.ts +2 -29
  175. package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
  176. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  177. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  178. package/src/providers/integrations/cursor/cursor.ts +1485 -960
  179. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  180. package/src/providers/integrations/factory/factory.ts +8 -1
  181. package/src/providers/integrations/gemini.ts +6 -1
  182. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  183. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  184. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  185. package/src/providers/integrations/opencode/provider.ts +531 -532
  186. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  187. package/src/providers/integrations/tokenharbor.ts +2 -2
  188. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  189. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  190. package/src/providers/operations/account-health-service.ts +1523 -1457
  191. package/src/providers/operations/client-versions.ts +9 -2
  192. package/src/providers/provider-contracts.ts +7 -0
  193. package/src/providers/provider-metadata.ts +13 -1
  194. package/src/providers/quota/quota-support.ts +8 -0
  195. package/src/security/api-key-auth.ts +504 -494
  196. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  197. package/src/transport/dispatch/generation.ts +6 -0
  198. package/src/transport/dispatch/proxy-request.ts +33 -1
  199. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  200. package/src/transport/dispatch/streaming/stream.ts +30 -2
  201. package/src/transport/dispatch/upstream.ts +9 -0
  202. package/src/transport/request/preparer.ts +7 -6
  203. package/src/transport/request/state.ts +9 -0
  204. package/src/transport/request/tenant-preferences.ts +60 -0
  205. package/src/transport/routing/route-catalog.ts +854 -799
  206. package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
  207. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
  208. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
  209. package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
  210. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
  211. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
  212. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
  213. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
  214. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
  215. package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
  216. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  217. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
  218. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
  219. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
  220. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
  221. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
  222. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
  223. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
  224. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
  225. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
  226. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
  227. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
  228. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
  229. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
  230. package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
  231. package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
  232. package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
  233. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  234. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  235. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  236. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  237. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  238. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  239. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  240. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  241. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  242. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  243. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  244. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  245. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  246. package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
  247. package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
  248. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  249. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  250. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  251. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  252. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  253. package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
  254. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  255. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  256. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  257. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  258. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  259. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  260. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  261. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  262. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  263. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  264. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  265. package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
  266. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  267. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  268. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  269. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  270. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  271. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  272. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  273. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  274. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  275. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  276. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
  277. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
@@ -1,471 +1,616 @@
1
- // Cartethyia-owned Cursor request builder.
2
- //
3
- // Translates a `CanonicalRequest` into Cursor's `AgentClientMessage` protobuf
4
- // messages. Everything here is wire construction — no transport, no session
5
- // state, no credentials — so it is testable against the schema without a socket.
6
- //
7
- // Two Cursor behaviours drive the shape:
8
- //
9
- // - **Nothing goes in `root_prompt_messages_json`.** A client-written entry
10
- // stands in for the conversation's rendered root prompt, and Cursor then stops
11
- // rendering RequestContext rules — including the host system-instructions rule
12
- // the system prompt travels in. Prior history therefore rides in an escaped
13
- // `<conversation_history>` prefix of the live user text instead.
14
- // - **Tools are advertised through `mcp_meta_tool_options`.** `AgentRunRequest.mcp_tools`
15
- // stays empty on real turns; the model calls an advertised tool through the
16
- // MCP exec channel, which is the only path this gateway executes.
17
- import type { CanonicalMessage, CanonicalRequest, ContentPart, ToolDefinition } from "../../../protocol/canonical-model";
18
- import { encodeMessage } from "./protocol/messages";
19
- import { encodeJsonAsValue } from "./protocol/struct";
20
-
21
- /**
22
- * MCP namespace Cartethyia advertises client tools under.
23
- *
24
- * Cursor namespaces MCP tools by server, and an inbound `mcp_args` request
25
- * carries both the server and the function name. Advertising under one fixed
26
- * namespace is what makes "is this a tool the client declared?" decidable: a
27
- * Cursor-native tool (shell, read, edit, …) arrives under a different server or
28
- * with no server at all and is never executed by the gateway.
29
- */
30
- export const CURSOR_MCP_NAMESPACE = "cartethyia";
31
-
32
- /** One tools advertisement Cursor receives in `RequestContext.mcp_meta_tool_options`. */
33
- export interface CursorToolDescriptor {
34
- /** Cursor composite `name`: `<namespace>-<tool name>`. */
35
- readonly name: string;
36
- readonly description: string;
37
- /** Server identifier every inbound call must echo to be executable. */
38
- readonly providerIdentifier: string;
39
- /** Bare function name the client declared. */
40
- readonly toolName: string;
41
- /** JSON Schema, encoded as a `google.protobuf.Value` on the wire. */
42
- readonly inputSchema: unknown;
43
- }
44
-
45
- /** One image accepted from a canonical request, ready for `selected_images`. */
46
- export interface CursorImageInput {
47
- readonly data: Uint8Array;
48
- readonly mimeType: string;
49
- readonly filename?: string;
50
- }
51
-
52
- /** Prior turn carried in the escaped history prefix of a rebased Run. */
53
- export interface CursorHistoryEntry {
54
- readonly role: "user" | "assistant";
55
- readonly content: string;
56
- }
57
-
58
- /** Closing tags that would let one history message end its own block early. */
59
- const SEED_HISTORY_TAGS = /<\/(conversation_history|user|assistant)>/gi;
60
-
61
- /**
62
- * Prior turns of a Run without a checkpoint, as a transcript ahead of the live
63
- * user text. `system` entries never appear: host system context travels as the
64
- * system-instructions rule in RequestContext.
65
- */
66
- export function seedHistoryUserText(
67
- text: string,
68
- history: readonly CursorHistoryEntry[] | undefined,
69
- ): string {
70
- const entries = (history ?? []).filter((entry) => entry.content.length > 0);
71
- if (entries.length === 0) return text;
72
- const transcript = entries
73
- .map(
74
- (entry) =>
75
- `<${entry.role}>\n${entry.content.replace(SEED_HISTORY_TAGS, "<\\/$1>")}\n</${entry.role}>`,
76
- )
77
- .join("\n\n");
78
- return `<conversation_history>\n${transcript}\n</conversation_history>\n\n${text}`;
79
- }
80
-
81
- /** Empty `ConversationStateStructure` for a Run with no checkpoint. */
82
- export function buildSeedConversationState(): Uint8Array {
83
- return encodeMessage("ConversationStateStructure", {});
84
- }
85
-
86
- /**
87
- * Advertises one request's declared function tools as Cursor MCP descriptors.
88
- *
89
- * A tool with no usable name is skipped rather than advertised under an
90
- * invented one: an unnameable tool cannot be matched back to an inbound call,
91
- * and advertising it would let the model call something the gateway cannot
92
- * execute or the client cannot answer.
93
- */
94
- export function toolDescriptorsFor(
95
- tools: readonly ToolDefinition[] | undefined,
96
- ): CursorToolDescriptor[] {
97
- const descriptors: CursorToolDescriptor[] = [];
98
- const seen = new Set<string>();
99
- for (const tool of tools ?? []) {
100
- const name = tool.name?.trim();
101
- if (name === undefined || name.length === 0) continue;
102
- if (seen.has(name)) continue;
103
- seen.add(name);
104
- descriptors.push({
105
- name: `${CURSOR_MCP_NAMESPACE}-${name}`,
106
- description: typeof tool.description === "string" ? tool.description : "",
107
- providerIdentifier: CURSOR_MCP_NAMESPACE,
108
- toolName: name,
109
- inputSchema: tool.jsonSchema ?? { type: "object", properties: {} },
110
- });
111
- }
112
- return descriptors;
113
- }
114
-
115
- /** `McpToolDefinition` protobuf messages for one descriptor set. */
116
- function mcpToolDefinitions(
117
- descriptors: readonly CursorToolDescriptor[],
118
- ): Record<string, unknown>[] {
119
- return descriptors.map((descriptor) => ({
120
- name: descriptor.name,
121
- description: descriptor.description,
122
- input_schema: encodeJsonAsValue(descriptor.inputSchema),
123
- provider_identifier: descriptor.providerIdentifier,
124
- tool_name: descriptor.toolName,
125
- }));
126
- }
127
-
128
- /**
129
- * Builds the RequestContext Cartethyia sends.
130
- *
131
- * The system prompt is a `global` rule: an untyped Cursor rule is attachable
132
- * only on request, so its content would never reach the model on its own. Tools
133
- * are advertised both in `tools` (#7) and in `mcp_meta_tool_options` (#34) —
134
- * the meta-tool channel is the one Cursor's model actually draws its callable
135
- * catalog from on a live turn.
136
- */
137
- export function buildCursorRequestContext(input: {
138
- readonly systemPrompt?: string;
139
- readonly descriptors: readonly CursorToolDescriptor[];
140
- }): Record<string, unknown> {
141
- const tools = mcpToolDefinitions(input.descriptors);
142
- const rules =
143
- input.systemPrompt !== undefined && input.systemPrompt.trim().length > 0
144
- ? [
145
- {
146
- full_path: "cartethyia/system-instructions",
147
- content: input.systemPrompt,
148
- type: { global: {} },
149
- },
150
- ]
151
- : [];
152
- return {
153
- ...(rules.length > 0 ? { rules } : {}),
154
- ...(tools.length > 0 ? { tools } : {}),
155
- // The host's own filesystem/web/search surface. A headless gateway has
156
- // none; advertising none up front keeps Cursor from routing a tool call to
157
- // a channel the gateway must then refuse.
158
- mcp_file_system_options: { enabled: false },
159
- web_search_enabled: false,
160
- web_fetch_enabled: false,
161
- ...(tools.length > 0
162
- ? {
163
- mcp_meta_tool_options: {
164
- enabled: true,
165
- mcp_descriptors: [
166
- {
167
- server_name: CURSOR_MCP_NAMESPACE,
168
- server_identifier: CURSOR_MCP_NAMESPACE,
169
- tools: tools.map((tool) => ({
170
- tool_name: tool["tool_name"],
171
- ...(typeof tool["description"] === "string"
172
- ? { description: tool["description"] }
173
- : {}),
174
- input_schema: tool["input_schema"],
175
- })),
176
- },
177
- ],
178
- },
179
- }
180
- : {}),
181
- // Every context section the gateway does not populate is marked complete so
182
- // Cursor does not wait for a follow-up the client will never send.
183
- mcp_info_complete: true,
184
- rules_info_complete: true,
185
- env_info_complete: true,
186
- repository_info_complete: true,
187
- custom_subagents_info_complete: true,
188
- agent_skills_info_complete: true,
189
- mcp_file_system_info_complete: true,
190
- git_status_info_complete: true,
191
- };
192
- }
193
-
194
- /** Joins a request's system and instruction content into one prompt. */
195
- export function systemPromptFrom(request: CanonicalRequest): string | undefined {
196
- const parts = [...(request.system ?? []), ...(request.instructions ?? [])];
197
- const text = parts
198
- .map((part) => (part.kind === "text" ? part.text : ""))
199
- .filter((value) => value.length > 0)
200
- .join("\n\n");
201
- return text.length > 0 ? text : undefined;
202
- }
203
-
204
- /**
205
- * Prior turns of a rebased Run, as escaped history entries.
206
- *
207
- * `system` turns never appear: host system context travels as the
208
- * system-instructions rule, and `tool` turns become `user` entries because
209
- * Cursor's seed transcript has no tool role.
210
- */
211
- export function historyFromRequest(request: CanonicalRequest): CursorHistoryEntry[] {
212
- const entries: CursorHistoryEntry[] = [];
213
- for (const message of request.messages) {
214
- const text = messageText(message);
215
- if (text.trim().length === 0) continue;
216
- if (message.role === "assistant") entries.push({ role: "assistant", content: text });
217
- else if (message.role === "user" || message.role === "tool") {
218
- entries.push({ role: "user", content: text });
219
- }
220
- }
221
- return entries;
222
- }
223
-
224
- /** Builds `AgentClientMessage{run_request}` for one Cursor Run. */
225
- export function buildRunRequest(input: {
226
- readonly text: string;
227
- readonly modelId: string;
228
- readonly conversationId: string;
229
- /** Stable parent group; unlike the conversation id it survives a rebase. */
230
- readonly conversationGroupId?: string;
231
- readonly runId?: string;
232
- readonly messageId?: string;
233
- readonly history?: readonly CursorHistoryEntry[];
234
- /** Cursor-declared parameter tuple (effort/context/fast) for this model. */
235
- readonly parameterValues?: readonly { id: string; value: string }[];
236
- readonly maxMode?: boolean;
237
- readonly images?: readonly CursorImageInput[];
238
- readonly descriptors?: readonly CursorToolDescriptor[];
239
- readonly systemPrompt?: string;
240
- /** Opaque checkpoint bytes; absent sends an empty seed state. */
241
- readonly conversationState?: Uint8Array;
242
- /** Resume a held Run instead of submitting another user turn. */
243
- readonly action?: "user" | "resume";
244
- }): Uint8Array {
245
- const messageId = input.messageId ?? crypto.randomUUID();
246
- const seeded = !(input.conversationState && input.conversationState.length > 0);
247
- const userMessage: Record<string, unknown> = {
248
- text: seeded ? seedHistoryUserText(input.text, input.history) : input.text,
249
- message_id: messageId,
250
- // AGENT_MODE_AGENT: the default agent turn, as Cursor CLI sends it.
251
- mode: 1,
252
- };
253
- if (input.images !== undefined && input.images.length > 0) {
254
- userMessage["selected_context"] = {
255
- selected_images: input.images.map((image) => ({
256
- data: image.data,
257
- uuid: crypto.randomUUID(),
258
- ...(image.filename === undefined ? {} : { path: image.filename }),
259
- mime_type: image.mimeType,
260
- })),
261
- };
262
- }
263
- const userMessageAction: Record<string, unknown> = { user_message: userMessage };
264
- const requestContext = buildCursorRequestContext({
265
- ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),
266
- descriptors: input.descriptors ?? [],
267
- });
268
- userMessageAction["request_context"] = requestContext;
269
- const runRequest: Record<string, unknown> = {
270
- conversation_id: input.conversationId,
271
- conversation_group_id: input.conversationGroupId ?? input.conversationId,
272
- run_id: input.runId ?? messageId,
273
- action:
274
- input.action === "resume"
275
- ? { resume_action: { request_context: requestContext } }
276
- : { user_message_action: userMessageAction },
277
- requested_model: {
278
- model_id: input.modelId,
279
- max_mode: input.maxMode ?? false,
280
- parameters: (input.parameterValues ?? []).map((parameter) => ({ ...parameter })),
281
- },
282
- conversation_state: seeded ? buildSeedConversationState() : input.conversationState,
283
- mcp_tools: { mcp_tools: [] },
284
- unknown_flag: 0,
285
- field_12: 0,
286
- };
287
- return encodeMessage("AgentClientMessage", { run_request: runRequest });
288
- }
289
-
290
- /** `AgentClientMessage{client_heartbeat}`. */
291
- export function buildHeartbeat(): Uint8Array {
292
- return encodeMessage("AgentClientMessage", { client_heartbeat: {} });
293
- }
294
-
295
- /**
296
- * `ACM#5 exec_client_control_message.heartbeat{id}`: Cursor CLI sends one every
297
- * few seconds for each exec it is still running. A zero id is left out, as
298
- * proto3 does for a default scalar.
299
- */
300
- export function buildExecHeartbeat(execId: number): Uint8Array {
301
- return encodeMessage("AgentClientMessage", {
302
- exec_client_control_message: { heartbeat: execId === 0 ? {} : { id: execId } },
303
- });
304
- }
305
-
306
- /** `ACM#5 exec_client_control_message.stream_close{id}` after an exec result. */
307
- export function buildExecStreamClose(execId: number): Uint8Array {
308
- return encodeMessage("AgentClientMessage", {
309
- exec_client_control_message: { stream_close: { id: execId } },
310
- });
311
- }
312
-
313
- /** Stops the live Run, as Cursor CLI does before closing the stream. */
314
- export function buildCancelAction(reason: string): Uint8Array {
315
- return encodeMessage("AgentClientMessage", {
316
- conversation_action: { cancel_action: { reason } },
317
- });
318
- }
319
-
320
- /** `ACM#3 kv_client_message` reply to a server `set_blob`. */
321
- export function buildKvSetBlobResult(id: number): Uint8Array {
322
- return encodeMessage("AgentClientMessage", {
323
- kv_client_message: { id, set_blob_result: {} },
324
- });
325
- }
326
-
327
- /** `ACM#3 kv_client_message` reply to a server `get_blob`. */
328
- export function buildKvGetBlobResult(id: number, blobData: Uint8Array): Uint8Array {
329
- return encodeMessage("AgentClientMessage", {
330
- kv_client_message: { id, get_blob_result: { blob_data: blobData } },
331
- });
332
- }
333
-
334
- /** `ACM#2 exec_client_message{request_context_result}` reply to a context probe. */
335
- export function buildRequestContextResult(
336
- execId: number,
337
- requestContext: Record<string, unknown>,
338
- ): Uint8Array {
339
- return encodeMessage("AgentClientMessage", {
340
- exec_client_message: {
341
- id: execId,
342
- request_context_result: { success: { request_context: requestContext } },
343
- },
344
- });
345
- }
346
-
347
- /**
348
- * `ACM#2 exec_client_message{mcp_state_exec_result}` reply to Cursor's MCP
349
- * availability probe.
350
- *
351
- * Cursor probes the servers behind an advertised tool before it emits a call,
352
- * and a Run that never gets an answer stalls on heartbeats. Answering from the
353
- * descriptors this Run advertised is what keeps the model's catalog intact.
354
- */
355
- export function buildMcpStateResult(
356
- execId: number,
357
- descriptors: readonly CursorToolDescriptor[],
358
- ): Uint8Array {
359
- return encodeMessage("AgentClientMessage", {
360
- exec_client_message: {
361
- id: execId,
362
- mcp_state_exec_result: {
363
- success: {
364
- servers: [
365
- {
366
- server_name: CURSOR_MCP_NAMESPACE,
367
- server_identifier: CURSOR_MCP_NAMESPACE,
368
- status: "connected",
369
- tools: mcpToolDefinitions(descriptors),
370
- },
371
- ],
372
- },
373
- },
374
- },
375
- });
376
- }
377
-
378
- /** One `McpToolResultContentItem` for an image-bearing tool result. */
379
- function imageContentItem(data: Uint8Array, mimeType: string): Record<string, unknown> {
380
- return { image: { data, mime_type: mimeType } };
381
- }
382
-
383
- /**
384
- * `ACM#2 exec_client_message{mcp_result}` carrying one external tool result.
385
- *
386
- * `is_error` is set for a tool the client reported as failed: Cursor relays the
387
- * distinction to the model, so a failure is not silently recast as a successful
388
- * observation.
389
- */
390
- export function buildMcpResult(input: {
391
- readonly execId: number;
392
- readonly text: string;
393
- readonly isError: boolean;
394
- readonly images?: readonly { data: Uint8Array; mimeType: string }[];
395
- }): Uint8Array {
396
- const content: Record<string, unknown>[] = [];
397
- if (input.text.length > 0) content.push({ text: { text: input.text } });
398
- for (const image of input.images ?? []) {
399
- content.push(imageContentItem(image.data, image.mimeType));
400
- }
401
- if (content.length === 0) content.push({ text: { text: "" } });
402
- return encodeMessage("AgentClientMessage", {
403
- exec_client_message: {
404
- id: input.execId,
405
- mcp_result: { success: { content, is_error: input.isError } },
406
- },
407
- });
408
- }
409
-
410
- /**
411
- * `ACM#2 exec_client_message{mcp_result}` rejecting a tool the client never
412
- * declared.
413
- *
414
- * A refusal is not an execution: Cursor asked for a host tool this gateway does
415
- * not run, and answering `rejected` (rather than executing it or stalling) is
416
- * what lets the model continue without the gateway ever touching a filesystem
417
- * or a shell on the caller's behalf.
418
- */
419
- export function buildMcpRejected(execId: number, reason: string): Uint8Array {
420
- return encodeMessage("AgentClientMessage", {
421
- exec_client_message: {
422
- id: execId,
423
- mcp_result: { rejected: { reason, is_readonly: true } },
424
- },
425
- });
426
- }
427
-
428
- /** Text of one canonical message, for the escaped history prefix. */
429
- export function messageText(message: CanonicalMessage): string {
430
- const parts: string[] = [];
431
- for (const part of message.content) {
432
- if (part.kind === "text") parts.push(part.text);
433
- else if (part.kind === "toolCall") parts.push(`[tool call ${part.name} ${stringifyArgs(part.arguments)}]`);
434
- else if (part.kind === "toolResult") parts.push(`[tool result ${part.call_id} ${toolResultText(part)}]`);
435
- else if (part.kind === "reasoning" && part.summary !== undefined) parts.push(part.summary);
436
- }
437
- return parts.join("\n");
438
- }
439
-
440
- function stringifyArgs(arguments_: unknown): string {
441
- if (typeof arguments_ === "string") return arguments_;
442
- try {
443
- return JSON.stringify(arguments_ ?? {});
444
- } catch {
445
- return "{}";
446
- }
447
- }
448
-
449
- function toolResultText(part: Extract<ContentPart, { kind: "toolResult" }>): string {
450
- if (typeof part.content === "string") return part.content;
451
- return part.content
452
- .map((child) => (child.kind === "text" ? child.text : `[${child.kind}]`))
453
- .join("\n");
454
- }
455
-
456
- /**
457
- * The live user text of one canonical request: the last user turn.
458
- *
459
- * Everything before it is history, and Cursor's own `user_message.text` carries
460
- * only the current prompt.
461
- */
462
- export function liveUserText(request: CanonicalRequest): string {
463
- for (let index = request.messages.length - 1; index >= 0; index -= 1) {
464
- const message = request.messages[index];
465
- if (message === undefined) continue;
466
- if (message.role !== "user" && message.role !== "tool") continue;
467
- const text = messageText(message);
468
- if (text.trim().length > 0) return text;
469
- }
470
- return "";
471
- }
1
+ // Cartethyia-owned Cursor request builder.
2
+ //
3
+ // Translates a `CanonicalRequest` into Cursor's `AgentClientMessage` protobuf
4
+ // messages. Everything here is wire construction — no transport, no session
5
+ // state, no credentials — so it is testable against the schema without a socket.
6
+ //
7
+ // Two Cursor behaviours drive the shape:
8
+ //
9
+ // - **Nothing goes in `root_prompt_messages_json`.** A client-written entry
10
+ // stands in for the conversation's rendered root prompt, and Cursor then stops
11
+ // rendering RequestContext rules — including the host system-instructions rule
12
+ // the system prompt travels in. Prior history therefore rides in an escaped
13
+ // `<conversation_history>` prefix of the live user text instead.
14
+ // - **Tools are advertised through `mcp_meta_tool_options`.** `AgentRunRequest.mcp_tools`
15
+ // stays empty on real turns; the model calls an advertised tool through the
16
+ // MCP exec channel, which is the only path this gateway executes.
17
+ import type { CanonicalMessage, CanonicalRequest, ContentPart, ToolDefinition } from "../../../protocol/canonical-model";
18
+ import { encodeMessage } from "./protocol/messages";
19
+ import { encodeJsonAsValue } from "./protocol/struct";
20
+
21
+ /**
22
+ * MCP namespace Cartethyia advertises client tools under.
23
+ *
24
+ * Cursor namespaces MCP tools by server, and an inbound `mcp_args` request
25
+ * carries both the server and the function name. Advertising under one fixed
26
+ * namespace is what makes "is this a tool the client declared?" decidable: a
27
+ * Cursor-native tool (shell, read, edit, …) arrives under a different server or
28
+ * with no server at all and is never executed by the gateway.
29
+ */
30
+ export const CURSOR_MCP_NAMESPACE = "cartethyia";
31
+
32
+ /** One tools advertisement Cursor receives in `RequestContext.mcp_meta_tool_options`. */
33
+ export interface CursorToolDescriptor {
34
+ /** Cursor composite `name`: `<namespace>-<tool name>`. */
35
+ readonly name: string;
36
+ readonly description: string;
37
+ /** Server identifier every inbound call must echo to be executable. */
38
+ readonly providerIdentifier: string;
39
+ /** Bare function name the client declared. */
40
+ readonly toolName: string;
41
+ /** JSON Schema, encoded as a `google.protobuf.Value` on the wire. */
42
+ readonly inputSchema: unknown;
43
+ }
44
+
45
+ /** One image accepted from a canonical request, ready for `selected_images`. */
46
+ export interface CursorImageInput {
47
+ readonly data: Uint8Array;
48
+ readonly mimeType: string;
49
+ readonly filename?: string;
50
+ }
51
+
52
+ /** Prior turn carried in the escaped history prefix of a rebased Run. */
53
+ export interface CursorHistoryEntry {
54
+ readonly role: "user" | "assistant";
55
+ readonly content: string;
56
+ }
57
+
58
+ /** Closing tags that would let one history message end its own block early. */
59
+ const SEED_HISTORY_TAGS = /<\/(conversation_history|user|assistant)>/gi;
60
+
61
+ /**
62
+ * Prior turns of a Run without a checkpoint, as a transcript ahead of the live
63
+ * user text. `system` entries never appear: host system context travels as the
64
+ * system-instructions rule in RequestContext.
65
+ */
66
+ export function seedHistoryUserText(
67
+ text: string,
68
+ history: readonly CursorHistoryEntry[] | undefined,
69
+ ): string {
70
+ const entries = (history ?? []).filter((entry) => entry.content.length > 0);
71
+ if (entries.length === 0) return text;
72
+ const transcript = entries
73
+ .map(
74
+ (entry) =>
75
+ `<${entry.role}>\n${entry.content.replace(SEED_HISTORY_TAGS, "<\\/$1>")}\n</${entry.role}>`,
76
+ )
77
+ .join("\n\n");
78
+ return `<conversation_history>\n${transcript}\n</conversation_history>\n\n${text}`;
79
+ }
80
+
81
+ /** Empty `ConversationStateStructure` for a Run with no checkpoint. */
82
+ export function buildSeedConversationState(): Uint8Array {
83
+ return encodeMessage("ConversationStateStructure", {});
84
+ }
85
+
86
+ /**
87
+ * Advertises one request's declared function tools as Cursor MCP descriptors.
88
+ *
89
+ * A tool with no usable name is skipped rather than advertised under an
90
+ * invented one: an unnameable tool cannot be matched back to an inbound call,
91
+ * and advertising it would let the model call something the gateway cannot
92
+ * execute or the client cannot answer.
93
+ */
94
+ export function toolDescriptorsFor(
95
+ tools: readonly ToolDefinition[] | undefined,
96
+ ): CursorToolDescriptor[] {
97
+ const descriptors: CursorToolDescriptor[] = [];
98
+ const seen = new Set<string>();
99
+ for (const tool of tools ?? []) {
100
+ const name = tool.name?.trim();
101
+ if (name === undefined || name.length === 0) continue;
102
+ if (seen.has(name)) continue;
103
+ seen.add(name);
104
+ descriptors.push({
105
+ name: `${CURSOR_MCP_NAMESPACE}-${name}`,
106
+ description: typeof tool.description === "string" ? tool.description : "",
107
+ providerIdentifier: CURSOR_MCP_NAMESPACE,
108
+ toolName: name,
109
+ inputSchema: tool.jsonSchema ?? { type: "object", properties: {} },
110
+ });
111
+ }
112
+ return descriptors;
113
+ }
114
+
115
+ /** `McpToolDefinition` protobuf messages for one descriptor set. */
116
+ function mcpToolDefinitions(
117
+ descriptors: readonly CursorToolDescriptor[],
118
+ ): Record<string, unknown>[] {
119
+ return descriptors.map((descriptor) => ({
120
+ name: descriptor.name,
121
+ description: descriptor.description,
122
+ input_schema: encodeJsonAsValue(descriptor.inputSchema),
123
+ provider_identifier: descriptor.providerIdentifier,
124
+ tool_name: descriptor.toolName,
125
+ }));
126
+ }
127
+
128
+ /**
129
+ * Builds the RequestContext Cartethyia sends.
130
+ *
131
+ * The system prompt is a `global` rule: an untyped Cursor rule is attachable
132
+ * only on request, so its content would never reach the model on its own. Tools
133
+ * are advertised both in `tools` (#7) and in `mcp_meta_tool_options` (#34) —
134
+ * the meta-tool channel is the one Cursor's model actually draws its callable
135
+ * catalog from on a live turn.
136
+ */
137
+ export function buildCursorRequestContext(input: {
138
+ readonly systemPrompt?: string;
139
+ readonly descriptors: readonly CursorToolDescriptor[];
140
+ }): Record<string, unknown> {
141
+ const tools = mcpToolDefinitions(input.descriptors);
142
+ const rules =
143
+ input.systemPrompt !== undefined && input.systemPrompt.trim().length > 0
144
+ ? [
145
+ {
146
+ full_path: "cartethyia/system-instructions",
147
+ content: input.systemPrompt,
148
+ type: { global: {} },
149
+ },
150
+ ]
151
+ : [];
152
+ return {
153
+ ...(rules.length > 0 ? { rules } : {}),
154
+ ...(tools.length > 0 ? { tools } : {}),
155
+ // The host's own filesystem/web/search surface. A headless gateway has
156
+ // none; advertising none up front keeps Cursor from routing a tool call to
157
+ // a channel the gateway must then refuse.
158
+ mcp_file_system_options: { enabled: false },
159
+ web_search_enabled: false,
160
+ web_fetch_enabled: false,
161
+ ...(tools.length > 0
162
+ ? {
163
+ mcp_meta_tool_options: {
164
+ enabled: true,
165
+ mcp_descriptors: [
166
+ {
167
+ server_name: CURSOR_MCP_NAMESPACE,
168
+ server_identifier: CURSOR_MCP_NAMESPACE,
169
+ tools: tools.map((tool) => ({
170
+ tool_name: tool["tool_name"],
171
+ ...(typeof tool["description"] === "string"
172
+ ? { description: tool["description"] }
173
+ : {}),
174
+ input_schema: tool["input_schema"],
175
+ })),
176
+ },
177
+ ],
178
+ },
179
+ }
180
+ : {}),
181
+ // Every context section the gateway does not populate is marked complete so
182
+ // Cursor does not wait for a follow-up the client will never send.
183
+ mcp_info_complete: true,
184
+ rules_info_complete: true,
185
+ env_info_complete: true,
186
+ repository_info_complete: true,
187
+ custom_subagents_info_complete: true,
188
+ agent_skills_info_complete: true,
189
+ mcp_file_system_info_complete: true,
190
+ git_status_info_complete: true,
191
+ };
192
+ }
193
+
194
+ /** Joins a request's system and instruction content into one prompt. */
195
+ export function systemPromptFrom(request: CanonicalRequest): string | undefined {
196
+ const parts = [...(request.system ?? []), ...(request.instructions ?? [])];
197
+ const text = parts
198
+ .map((part) => (part.kind === "text" ? part.text : ""))
199
+ .filter((value) => value.length > 0)
200
+ .join("\n\n");
201
+ return text.length > 0 ? text : undefined;
202
+ }
203
+
204
+ /**
205
+ * Prior turns of a rebased Run, as escaped history entries.
206
+ *
207
+ * `system` turns never appear: host system context travels as the
208
+ * system-instructions rule, and `tool` turns become `user` entries because
209
+ * Cursor's seed transcript has no tool role.
210
+ */
211
+ export function historyFromRequest(request: CanonicalRequest): CursorHistoryEntry[] {
212
+ const entries: CursorHistoryEntry[] = [];
213
+ for (const message of request.messages) {
214
+ const text = messageText(message);
215
+ if (text.trim().length === 0) continue;
216
+ if (message.role === "assistant") entries.push({ role: "assistant", content: text });
217
+ else if (message.role === "user" || message.role === "tool") {
218
+ entries.push({ role: "user", content: text });
219
+ }
220
+ }
221
+ return entries;
222
+ }
223
+
224
+ /** Builds `AgentClientMessage{run_request}` for one Cursor Run. */
225
+ export function buildRunRequest(input: {
226
+ readonly text: string;
227
+ readonly modelId: string;
228
+ readonly conversationId: string;
229
+ /** Stable parent group; unlike the conversation id it survives a rebase. */
230
+ readonly conversationGroupId?: string;
231
+ readonly runId?: string;
232
+ readonly messageId?: string;
233
+ readonly history?: readonly CursorHistoryEntry[];
234
+ /** Cursor-declared parameter tuple (effort/context/fast) for this model. */
235
+ readonly parameterValues?: readonly { id: string; value: string }[];
236
+ readonly maxMode?: boolean;
237
+ readonly images?: readonly CursorImageInput[];
238
+ readonly descriptors?: readonly CursorToolDescriptor[];
239
+ readonly systemPrompt?: string;
240
+ /** Opaque checkpoint bytes; absent sends an empty seed state. */
241
+ readonly conversationState?: Uint8Array;
242
+ /** Resume a held Run instead of submitting another user turn. */
243
+ readonly action?: "user" | "resume";
244
+ }): Uint8Array {
245
+ const messageId = input.messageId ?? crypto.randomUUID();
246
+ const seeded = !(input.conversationState && input.conversationState.length > 0);
247
+ const userMessage: Record<string, unknown> = {
248
+ text: seeded ? seedHistoryUserText(input.text, input.history) : input.text,
249
+ message_id: messageId,
250
+ // AGENT_MODE_AGENT: the default agent turn, as Cursor CLI sends it.
251
+ mode: 1,
252
+ };
253
+ if (input.images !== undefined && input.images.length > 0) {
254
+ userMessage["selected_context"] = {
255
+ selected_images: input.images.map((image) => ({
256
+ data: image.data,
257
+ uuid: crypto.randomUUID(),
258
+ ...(image.filename === undefined ? {} : { path: image.filename }),
259
+ mime_type: image.mimeType,
260
+ })),
261
+ };
262
+ }
263
+ const userMessageAction: Record<string, unknown> = { user_message: userMessage };
264
+ const requestContext = buildCursorRequestContext({
265
+ ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),
266
+ descriptors: input.descriptors ?? [],
267
+ });
268
+ userMessageAction["request_context"] = requestContext;
269
+ const runRequest: Record<string, unknown> = {
270
+ conversation_id: input.conversationId,
271
+ conversation_group_id: input.conversationGroupId ?? input.conversationId,
272
+ run_id: input.runId ?? messageId,
273
+ action:
274
+ input.action === "resume"
275
+ ? { resume_action: { request_context: requestContext } }
276
+ : { user_message_action: userMessageAction },
277
+ requested_model: {
278
+ model_id: input.modelId,
279
+ max_mode: input.maxMode ?? false,
280
+ parameters: (input.parameterValues ?? []).map((parameter) => ({ ...parameter })),
281
+ },
282
+ conversation_state: seeded ? buildSeedConversationState() : input.conversationState,
283
+ mcp_tools: { mcp_tools: [] },
284
+ unknown_flag: 0,
285
+ field_12: 0,
286
+ };
287
+ return encodeMessage("AgentClientMessage", { run_request: runRequest });
288
+ }
289
+
290
+ /** `AgentClientMessage{client_heartbeat}`. */
291
+ export function buildHeartbeat(): Uint8Array {
292
+ return encodeMessage("AgentClientMessage", { client_heartbeat: {} });
293
+ }
294
+
295
+ /**
296
+ * `ACM#5 exec_client_control_message.heartbeat{id}`: Cursor CLI sends one every
297
+ * few seconds for each exec it is still running. A zero id is left out, as
298
+ * proto3 does for a default scalar.
299
+ */
300
+ export function buildExecHeartbeat(execId: number): Uint8Array {
301
+ return encodeMessage("AgentClientMessage", {
302
+ exec_client_control_message: { heartbeat: execId === 0 ? {} : { id: execId } },
303
+ });
304
+ }
305
+
306
+ /** `ACM#5 exec_client_control_message.stream_close{id}` after an exec result. */
307
+ export function buildExecStreamClose(execId: number, execIdString?: string): Uint8Array {
308
+ return encodeMessage("AgentClientMessage", {
309
+ exec_client_control_message: { stream_close: { id: execId, ...(execIdString === undefined ? {} : { exec_id: execIdString }) } },
310
+ });
311
+ }
312
+
313
+ /** Stops the live Run, as Cursor CLI does before closing the stream. */
314
+ export function buildCancelAction(reason: string): Uint8Array {
315
+ return encodeMessage("AgentClientMessage", {
316
+ conversation_action: { cancel_action: { reason } },
317
+ });
318
+ }
319
+
320
+ /** `ACM#3 kv_client_message` reply to a server `set_blob`. */
321
+ export function buildKvSetBlobResult(id: number): Uint8Array {
322
+ return encodeMessage("AgentClientMessage", {
323
+ kv_client_message: { id, set_blob_result: {} },
324
+ });
325
+ }
326
+
327
+ /** `ACM#3 kv_client_message` reply to a server `get_blob`. */
328
+ export function buildKvGetBlobResult(id: number, blobData: Uint8Array): Uint8Array {
329
+ return encodeMessage("AgentClientMessage", {
330
+ kv_client_message: { id, get_blob_result: { blob_data: blobData } },
331
+ });
332
+ }
333
+
334
+ /** `ACM#2 exec_client_message{request_context_result}` reply to a context probe. */
335
+ export function buildRequestContextResult(
336
+ execId: number,
337
+ requestContext: Record<string, unknown>,
338
+ execIdString?: string,
339
+ ): Uint8Array {
340
+ return encodeMessage("AgentClientMessage", {
341
+ exec_client_message: {
342
+ id: execId,
343
+ exec_id: execIdString ?? "",
344
+ request_context_result: { success: { request_context: requestContext } },
345
+ },
346
+ });
347
+ }
348
+
349
+ /**
350
+ * `ACM#2 exec_client_message{mcp_state_exec_result}` reply to Cursor's MCP
351
+ * availability probe.
352
+ *
353
+ * Cursor probes the servers behind an advertised tool before it emits a call,
354
+ * and a Run that never gets an answer stalls on heartbeats. Answering from the
355
+ * descriptors this Run advertised is what keeps the model's catalog intact.
356
+ */
357
+ export function buildMcpStateResult(
358
+ execId: number,
359
+ descriptors: readonly CursorToolDescriptor[],
360
+ execIdString?: string,
361
+ ): Uint8Array {
362
+ return encodeMessage("AgentClientMessage", {
363
+ exec_client_message: {
364
+ id: execId,
365
+ exec_id: execIdString ?? "",
366
+ mcp_state_exec_result: {
367
+ success: {
368
+ servers: [
369
+ {
370
+ server_name: CURSOR_MCP_NAMESPACE,
371
+ server_identifier: CURSOR_MCP_NAMESPACE,
372
+ status: "connected",
373
+ tools: mcpToolDefinitions(descriptors),
374
+ },
375
+ ],
376
+ },
377
+ },
378
+ },
379
+ });
380
+ }
381
+
382
+ /** One `McpToolResultContentItem` for an image-bearing tool result. */
383
+ function imageContentItem(data: Uint8Array, mimeType: string): Record<string, unknown> {
384
+ return { image: { data, mime_type: mimeType } };
385
+ }
386
+
387
+ /**
388
+ * `ACM#2 exec_client_message{mcp_result}` carrying one external tool result.
389
+ *
390
+ * `is_error` is set for a tool the client reported as failed: Cursor relays the
391
+ * distinction to the model, so a failure is not silently recast as a successful
392
+ * observation.
393
+ */
394
+ export function buildMcpResult(input: {
395
+ readonly execId: number;
396
+ readonly execIdString?: string;
397
+ readonly text: string;
398
+ readonly isError: boolean;
399
+ readonly images?: readonly { data: Uint8Array; mimeType: string }[];
400
+ }): Uint8Array {
401
+ const content: Record<string, unknown>[] = [];
402
+ if (input.text.length > 0) content.push({ text: { text: input.text } });
403
+ for (const image of input.images ?? []) {
404
+ content.push(imageContentItem(image.data, image.mimeType));
405
+ }
406
+ if (content.length === 0) content.push({ text: { text: "" } });
407
+ return encodeMessage("AgentClientMessage", {
408
+ exec_client_message: {
409
+ id: input.execId,
410
+ exec_id: input.execIdString ?? "",
411
+ mcp_result: { success: { content, is_error: input.isError } },
412
+ },
413
+ });
414
+ }
415
+
416
+ /**
417
+ * `ACM#2 exec_client_message{shell_result|write_result|read_result}` for a host
418
+ * exec the canonical client executed locally.
419
+ *
420
+ * Cursor's executor replies to its own host requests with the variant's typed
421
+ * result frame (the generic `mcp_result` leaves the server unmatched on a frame
422
+ * it did rank), and after every exec result it sends `stream_close` — without
423
+ * it the cloud waits forever on shell execs. `is_error` keeps the failure as
424
+ * the variant's error/failure case rather than a crafted success.
425
+ */
426
+ export type CursorHostExecResultVariant = "shell" | "write" | "read";
427
+
428
+ export function buildHostExecResult(input: {
429
+ readonly execId: number;
430
+ readonly execIdString?: string;
431
+ readonly variant: CursorHostExecResultVariant;
432
+ readonly text: string;
433
+ readonly isError: boolean;
434
+ readonly host: {
435
+ readonly command?: string;
436
+ readonly workingDirectory?: string;
437
+ readonly path?: string;
438
+ readonly lines?: number;
439
+ readonly fileSize?: number;
440
+ };
441
+ }): Uint8Array[] {
442
+ const id = { id: input.execId, exec_id: input.execIdString ?? "" };
443
+ const variant = input.variant;
444
+ const text = input.text;
445
+ let result: Record<string, unknown>;
446
+ if (variant === "shell") {
447
+ result = {
448
+ shell_result: input.isError
449
+ ? { failure: { command: input.host.command ?? "", exit_code: 1, stderr: text } }
450
+ : {
451
+ success: {
452
+ command: input.host.command ?? "",
453
+ exit_code: 0,
454
+ stdout: text,
455
+ ...(input.host.workingDirectory === undefined ? {} : { working_directory: input.host.workingDirectory }),
456
+ },
457
+ },
458
+ };
459
+ } else if (variant === "write") {
460
+ result = {
461
+ write_result: input.isError
462
+ ? { error: { path: input.host.path ?? "", error: text } }
463
+ : {
464
+ success: {
465
+ path: input.host.path ?? "",
466
+ lines_created: input.host.lines ?? 0,
467
+ file_size: input.host.fileSize ?? 0,
468
+ },
469
+ },
470
+ };
471
+ } else {
472
+ result = {
473
+ read_result: input.isError
474
+ ? { error: { path: input.host.path ?? "", error: text } }
475
+ : {
476
+ success: {
477
+ path: input.host.path ?? "",
478
+ content: text,
479
+ total_lines: text.length > 0 ? text.split("\n").length : 0,
480
+ },
481
+ },
482
+ };
483
+ }
484
+ return [
485
+ encodeMessage("AgentClientMessage", { exec_client_message: { ...id, ...result } }),
486
+ // The real CLI pairs every typed exec result with stream_close — the cloud
487
+ // waits on it for shell execs specifically.
488
+ buildExecStreamClose(input.execId, input.execIdString),
489
+ ];
490
+ }
491
+
492
+ /**
493
+ * `ACM#2 exec_client_message{mcp_result}` rejecting a tool the client never
494
+ * declared.
495
+ *
496
+ * A refusal is not an execution: Cursor asked for a host tool this gateway does
497
+ * not run, and answering `rejected` (rather than executing it or stalling) is
498
+ * what lets the model continue without the gateway ever touching a filesystem
499
+ * or a shell on the caller's behalf.
500
+ */
501
+ export function buildMcpRejected(
502
+ execId: number,
503
+ reason: string,
504
+ execIdString?: string,
505
+ ): Uint8Array {
506
+ return encodeMessage("AgentClientMessage", {
507
+ exec_client_message: {
508
+ id: execId,
509
+ exec_id: execIdString ?? "",
510
+ mcp_result: { rejected: { reason, is_readonly: true } },
511
+ },
512
+ });
513
+ }
514
+
515
+ /**
516
+ * `ACM#2 exec_client_message{*_allowlist_precheck_result}` for frames 41/42/43.
517
+ *
518
+ * Cursor asks whether a shell command, MCP tool, or web fetch would be
519
+ * permitted *before* it emits the real call. This gateway executes none of its
520
+ * own surface, so every precheck is answered `allowlisted: false` — the model
521
+ * never learns otherwise than that the tool must be routed through the
522
+ * advertised MCP channel it was told about.
523
+ */
524
+ export function buildAllowlistPrecheckResult(
525
+ execId: number,
526
+ kind: "shell" | "mcp" | "web_fetch",
527
+ execIdString?: string,
528
+ ): Uint8Array {
529
+ const frame =
530
+ kind === "shell"
531
+ ? "shell_allowlist_precheck_result"
532
+ : kind === "mcp"
533
+ ? "mcp_allowlist_precheck_result"
534
+ : "web_fetch_allowlist_precheck_result";
535
+ return encodeMessage("AgentClientMessage", {
536
+ exec_client_message: { id: execId, exec_id: execIdString ?? "", [frame]: { allowlisted: false } },
537
+ });
538
+ }
539
+
540
+ /**
541
+ * `ACM#5 exec_client_control_message.throw{id}`: fails an exec frame in band.
542
+ *
543
+ * An unrecognized (or unimplementable) exec variant must still be answered —
544
+ * silently returning strands the exec id and the server waits forever on a
545
+ * reply that never comes. Cursor's own executor recovers exactly this way, so
546
+ * the server surfaces the error to the model instead of blocking the turn.
547
+ */
548
+ export function buildExecThrow(execId: number, error: string, execIdString?: string): Uint8Array {
549
+ return encodeMessage("AgentClientMessage", {
550
+ exec_client_control_message: {
551
+ throw: { id: execId, error, ...(execIdString === undefined ? {} : { exec_id: execIdString }) },
552
+ },
553
+ });
554
+ }
555
+
556
+ /**
557
+ * `ACM#6 interaction_response{id}` answering one permission gate.
558
+ *
559
+ * Result names must match the queried variant's response frame; bodies stay
560
+ * opaque so every gate is answered as a policy object rather than a decoded
561
+ * args blob.
562
+ */
563
+ export function buildInteractionResponse(
564
+ queryId: number,
565
+ resultName: string,
566
+ result: Record<string, unknown>,
567
+ ): Uint8Array {
568
+ return encodeMessage("AgentClientMessage", {
569
+ interaction_response: { id: queryId, [resultName]: result },
570
+ });
571
+ }
572
+
573
+ /** Text of one canonical message, for the escaped history prefix. */
574
+ export function messageText(message: CanonicalMessage): string {
575
+ const parts: string[] = [];
576
+ for (const part of message.content) {
577
+ if (part.kind === "text") parts.push(part.text);
578
+ else if (part.kind === "toolCall") parts.push(`[tool call ${part.name} ${stringifyArgs(part.arguments)}]`);
579
+ else if (part.kind === "toolResult") parts.push(`[tool result ${part.call_id} ${toolResultText(part)}]`);
580
+ else if (part.kind === "reasoning" && part.summary !== undefined) parts.push(part.summary);
581
+ }
582
+ return parts.join("\n");
583
+ }
584
+
585
+ function stringifyArgs(arguments_: unknown): string {
586
+ if (typeof arguments_ === "string") return arguments_;
587
+ try {
588
+ return JSON.stringify(arguments_ ?? {});
589
+ } catch {
590
+ return "{}";
591
+ }
592
+ }
593
+
594
+ function toolResultText(part: Extract<ContentPart, { kind: "toolResult" }>): string {
595
+ if (typeof part.content === "string") return part.content;
596
+ return part.content
597
+ .map((child) => (child.kind === "text" ? child.text : `[${child.kind}]`))
598
+ .join("\n");
599
+ }
600
+
601
+ /**
602
+ * The live user text of one canonical request: the last user turn.
603
+ *
604
+ * Everything before it is history, and Cursor's own `user_message.text` carries
605
+ * only the current prompt.
606
+ */
607
+ export function liveUserText(request: CanonicalRequest): string {
608
+ for (let index = request.messages.length - 1; index >= 0; index -= 1) {
609
+ const message = request.messages[index];
610
+ if (message === undefined) continue;
611
+ if (message.role !== "user" && message.role !== "tool") continue;
612
+ const text = messageText(message);
613
+ if (text.trim().length > 0) return text;
614
+ }
615
+ return "";
616
+ }