cartethyia 2.7.9 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -0,0 +1,1018 @@
1
+ import { asCoercedNumber as sharedAsNumber, asRecord as sharedAsRecord } from "../../../shared/coerce";
2
+ import { providerBaseUrl, BUDDY_PROVIDER_IDS } from "../../provider-metadata";
3
+ import { sleep } from "../../../shared/sleep";
4
+ import { QODER_CHECKIN_PROVIDER_IDS } from "../qoder/qoder-checkin";
5
+ import { buildCodeBuddyUserAgent } from "../../operations/client-versions";
6
+ import { codebuddyDomain, type CodeBuddyVariant } from "./codebuddy/shared";
7
+ import { randomUUID } from "node:crypto";
8
+ import type { FetchLike } from "../../quota/quota-contracts";
9
+ import { text as asText } from "../../quota/quota-contracts";
10
+ import type { ToolDefinition } from "../../../protocol/canonical-model";
11
+
12
+ // ── buddy-checkin ──────────────────────────────────────────
13
+
14
+ export const DAILY_CHECKIN_PROVIDER_IDS: readonly string[] = [
15
+ ...BUDDY_PROVIDER_IDS,
16
+ ...QODER_CHECKIN_PROVIDER_IDS,
17
+ ];
18
+
19
+ const TIMEOUT_MS = 15_000;
20
+
21
+ export const CHECKIN_STATUS_PATH = "/billing/meter/checkin-activity-status";
22
+
23
+ export const CHECKIN_CLAIM_PATH = "/billing/meter/daily-checkin";
24
+
25
+ export type DailyCheckinState =
26
+ | "claimed"
27
+ | "already_claimed"
28
+ | "not_eligible"
29
+ | "event_ended"
30
+ | "unavailable"
31
+ | "error";
32
+
33
+ export interface DailyCheckinResult {
34
+ readonly providerId: string;
35
+ readonly accountId: string;
36
+ readonly state: DailyCheckinState;
37
+ /** Credits granted by this claim; `null` when nothing was granted. */
38
+ readonly credit: number | null;
39
+ readonly streakDays: number | null;
40
+ /** Provider-facing detail for the audit log; `null` on success. */
41
+ readonly error: string | null;
42
+ }
43
+
44
+ export interface BuddyActivityReportEvent {
45
+ readonly eventCode: "chat_request_send";
46
+ readonly timestamp: number;
47
+ readonly reportDelay: number;
48
+ readonly mode: string;
49
+ readonly conversationId: string;
50
+ readonly requestId: string;
51
+ readonly inputLength: number;
52
+ readonly requestModelId: string;
53
+ readonly requestModelName: string;
54
+ readonly isPlan: boolean;
55
+ readonly isAutoExecuteTerminal: boolean;
56
+ readonly isAutoModify: boolean;
57
+ readonly codebaseEnable: boolean;
58
+ readonly maxToken: number;
59
+ readonly maxSteps: number;
60
+ readonly temperature: number;
61
+ readonly maxRetries: number;
62
+ readonly mentionContexts: readonly unknown[];
63
+ readonly knowledgeId: readonly unknown[];
64
+ readonly knowledgeName: readonly unknown[];
65
+ readonly codebaseId: string;
66
+ readonly mentionContextCount: number;
67
+ readonly command: string;
68
+ readonly recommendId: string;
69
+ readonly skillId: string;
70
+ readonly skillCount: number;
71
+ readonly totalCount: number;
72
+ readonly presentAt: number;
73
+ readonly rootRequestId: string;
74
+ readonly parentConversationId: string;
75
+ readonly agentName: string;
76
+ readonly agentType: string;
77
+ readonly product: string;
78
+ readonly userId: string;
79
+ }
80
+
81
+ export const ACTIVITY_REPORT_PATH = "/report";
82
+
83
+ export type BuddyActivityReportState = "reported" | "unavailable" | "error";
84
+
85
+ export interface BuddyActivityReportResult {
86
+ readonly providerId: string;
87
+ readonly accountId: string;
88
+ readonly state: BuddyActivityReportState;
89
+ /** Provider-facing detail for the audit log; `null` on success. */
90
+ readonly error: string | null;
91
+ }
92
+
93
+ export function buildActivityReportEvent(args: {
94
+ readonly conversationId: string;
95
+ readonly userId: string;
96
+ readonly requestId?: string;
97
+ readonly now?: number;
98
+ }): BuddyActivityReportEvent {
99
+ const now = args.now ?? Date.now();
100
+ const requestId = args.requestId ?? args.conversationId;
101
+ return {
102
+ eventCode: "chat_request_send",
103
+ timestamp: now,
104
+ reportDelay: 0,
105
+ mode: "unknown",
106
+ conversationId: args.conversationId,
107
+ requestId,
108
+ inputLength: 0,
109
+ requestModelId: "auto",
110
+ requestModelName: "Auto",
111
+ isPlan: false,
112
+ isAutoExecuteTerminal: false,
113
+ isAutoModify: false,
114
+ codebaseEnable: false,
115
+ maxToken: 0,
116
+ maxSteps: 0,
117
+ temperature: 0,
118
+ maxRetries: 0,
119
+ mentionContexts: [],
120
+ knowledgeId: [],
121
+ knowledgeName: [],
122
+ codebaseId: "",
123
+ mentionContextCount: 0,
124
+ command: "",
125
+ recommendId: "",
126
+ skillId: "",
127
+ skillCount: 0,
128
+ totalCount: 0,
129
+ presentAt: now,
130
+ rootRequestId: args.conversationId,
131
+ parentConversationId: args.conversationId,
132
+ agentName: "default",
133
+ agentType: "main",
134
+ product: "SaaS",
135
+ userId: args.userId,
136
+ };
137
+ }
138
+
139
+ export const ACTIVITY_REPORT_EVENTS_PER_BATCH = 4;
140
+
141
+ export const ACTIVITY_REPORT_ROUNDS = 2;
142
+
143
+ export const ACTIVITY_REPORT_ROUND_DELAY_MS = 1_500;
144
+
145
+ export async function fetchBuddyActivityReport(args: {
146
+ readonly providerId: string;
147
+ readonly accountId: string;
148
+ readonly credential: string;
149
+ readonly userId: string;
150
+ readonly fetcher: typeof fetch;
151
+ readonly conversationId?: string;
152
+ /** Pause between rounds. Injectable so a test does not wait on the real delay. */
153
+ readonly roundDelayMs?: number;
154
+ readonly sleep?: (ms: number) => Promise<void>;
155
+ }): Promise<BuddyActivityReportResult> {
156
+ const { providerId, accountId, credential, userId, fetcher } = args;
157
+ const base = { providerId, accountId, error: null };
158
+ if (!credential) {
159
+ return { ...base, state: "error", error: "Account has no stored credential." };
160
+ }
161
+ if (!userId) {
162
+ return { ...base, state: "error", error: "Account uid is unavailable; cannot report activity." };
163
+ }
164
+ const headers = {
165
+ ...(await checkinHeaders(providerId)),
166
+ Authorization: `Bearer ${credential}`,
167
+ "X-User-Id": userId,
168
+ };
169
+ const url = `${providerBaseUrl(providerId)}${VERSION_SEGMENT[providerId] ?? ""}${ACTIVITY_REPORT_PATH}`;
170
+ const delayMs = args.roundDelayMs ?? ACTIVITY_REPORT_ROUND_DELAY_MS;
171
+ const sleep = args.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
172
+ const conversationId = args.conversationId ?? `activity-${Date.now()}`;
173
+
174
+ for (let round = 0; round < ACTIVITY_REPORT_ROUNDS; round += 1) {
175
+ if (round > 0 && delayMs > 0) await sleep(delayMs);
176
+ const events = Array.from({ length: ACTIVITY_REPORT_EVENTS_PER_BATCH }, (_, index) => {
177
+ const sequence = round * ACTIVITY_REPORT_EVENTS_PER_BATCH + index;
178
+ return buildActivityReportEvent({
179
+ conversationId: `${conversationId}-${sequence}`,
180
+ requestId: `${conversationId}-req-${sequence}`,
181
+ userId,
182
+ });
183
+ });
184
+ let payload: unknown;
185
+ try {
186
+ const response = await fetcher(url, {
187
+ method: "POST",
188
+ headers,
189
+ body: JSON.stringify(events),
190
+ signal: AbortSignal.timeout(TIMEOUT_MS),
191
+ });
192
+ if (response.status === 401 || response.status === 403) {
193
+ return { ...base, state: "error", error: "Credential invalid or expired." };
194
+ }
195
+ if (!response.ok) {
196
+ return { ...base, state: "error", error: `Activity report API error (${response.status}).` };
197
+ }
198
+ payload = await response.text().then((text) => (text ? JSON.parse(text) : null));
199
+ } catch (error) {
200
+ if (error instanceof SyntaxError) {
201
+ return { ...base, state: "error", error: "Activity report response returned invalid JSON." };
202
+ }
203
+ return {
204
+ ...base,
205
+ state: "error",
206
+ error: error instanceof Error ? error.message : "Activity report request failed.",
207
+ };
208
+ }
209
+ const code = envelopeCode(payload);
210
+ if (code !== null && code !== 0) {
211
+ return { ...base, state: "error", error: `Activity report error (code ${code}).` };
212
+ }
213
+ }
214
+ return { ...base, state: "reported" };
215
+ }
216
+
217
+ const ALREADY_CLAIMED_CODE = 1001;
218
+
219
+ const NOT_ELIGIBLE_CODE = 1002;
220
+
221
+ const EVENT_ENDED_CODE = 1003;
222
+
223
+ const CLAIM_RETRY_DELAYS_MS = [1_000, 2_000] as const;
224
+
225
+ const VERSION_SEGMENT: Readonly<Record<string, string>> = {
226
+ cb: "",
227
+ cbcn: "",
228
+ };
229
+
230
+ function statusUrl(providerId: string): string {
231
+ return `${providerBaseUrl(providerId)}${VERSION_SEGMENT[providerId] ?? ""}${CHECKIN_STATUS_PATH}`;
232
+ }
233
+
234
+ function claimUrl(providerId: string): string {
235
+ return `${providerBaseUrl(providerId)}${VERSION_SEGMENT[providerId] ?? ""}${CHECKIN_CLAIM_PATH}`;
236
+ }
237
+
238
+ async function checkinHeaders(providerId: string): Promise<Record<string, string>> {
239
+ const common = {
240
+ "X-Product": "SaaS",
241
+ "X-Requested-With": "XMLHttpRequest",
242
+ "X-CodeBuddy-Request": "1",
243
+ "Content-Type": "application/json",
244
+ Accept: "application/json",
245
+ };
246
+ const variant: CodeBuddyVariant = providerId === "cb" ? "IDE" : "CLI";
247
+ return {
248
+ ...common,
249
+ "User-Agent": buildCodeBuddyUserAgent(variant),
250
+ "X-IDE-Type": variant,
251
+ "X-IDE-Name": variant,
252
+ "X-Domain": codebuddyDomain(variant),
253
+ };
254
+ }
255
+
256
+ function alreadyClaimedMessage(payload: unknown, rawBody: string): string | null {
257
+ const message = asTextField(payload) || rawBody.trim();
258
+ if (!message) return null;
259
+ if (message.includes("已签到")) return message;
260
+ const lower = message.toLowerCase();
261
+ if (lower.includes("already") && lower.includes("check")) return message;
262
+ return null;
263
+ }
264
+
265
+ function asTextField(payload: unknown): string | null {
266
+ const data = asRecord(payload);
267
+ if (!data) return null;
268
+ const direct = data["msg"] ?? data["message"];
269
+ if (typeof direct === "string" && direct.trim()) return direct.trim();
270
+ return null;
271
+ }
272
+
273
+ function isRetryableClaim(error: unknown, status: number | null): boolean {
274
+ if (status !== null) return status >= 500;
275
+ return !(error instanceof DOMException && error.name === "AbortError");
276
+ }
277
+
278
+ function asRecord(value: unknown): Record<string, unknown> | null {
279
+ return sharedAsRecord(value) ?? null;
280
+ }
281
+
282
+ function asNumber(value: unknown): number | null {
283
+ return sharedAsNumber(value) ?? null;
284
+ }
285
+
286
+ function envelopeCode(payload: unknown): number | null {
287
+ const envelope = asRecord(payload);
288
+ if (!envelope || !("code" in envelope)) return null;
289
+ return asNumber(envelope["code"]);
290
+ }
291
+
292
+ function envelopeData(payload: unknown): Record<string, unknown> | null {
293
+ const data = asRecord(asRecord(payload)?.["data"]);
294
+ if (!data) return null;
295
+ if ("today_checked_in" in data || "active" in data || "streak_days" in data) return data;
296
+ return asRecord(asRecord(data["Response"])?.["Data"]);
297
+ }
298
+
299
+ export async function fetchDailyCheckin(args: {
300
+ readonly providerId: string;
301
+ readonly accountId: string;
302
+ readonly credential: string;
303
+ readonly fetcher: typeof fetch;
304
+ /** Overrides the status probe when the caller already knows the state. */
305
+ readonly forceClaim?: boolean;
306
+ }): Promise<DailyCheckinResult> {
307
+ const { providerId, accountId, credential, fetcher } = args;
308
+ const base = { providerId, accountId, credit: null, streakDays: null, error: null };
309
+
310
+ if (!credential) {
311
+ return { ...base, state: "error", error: "Account has no stored credential." };
312
+ }
313
+
314
+ // `checkinHeaders()` already carries Content-Type/Accept; only auth is
315
+ // layered on here so the per-provider identity stays in one place.
316
+ const headers = {
317
+ ...(await checkinHeaders(providerId)),
318
+ Authorization: `Bearer ${credential}`,
319
+ };
320
+
321
+ let status: unknown;
322
+ try {
323
+ const response = await fetcher(statusUrl(providerId), {
324
+ method: "POST",
325
+ headers,
326
+ body: "{}",
327
+ signal: AbortSignal.timeout(TIMEOUT_MS),
328
+ });
329
+ if (response.status === 401 || response.status === 403) {
330
+ return { ...base, state: "error", error: "Credential invalid or expired." };
331
+ }
332
+ if (!response.ok) {
333
+ return { ...base, state: "error", error: `Check-in status API error (${response.status}).` };
334
+ }
335
+ status = await response.json().catch(() => null);
336
+ } catch (error) {
337
+ return {
338
+ ...base,
339
+ state: "error",
340
+ error: error instanceof Error ? error.message : "Check-in status request failed.",
341
+ };
342
+ }
343
+
344
+ const statusCode = envelopeCode(status);
345
+ if (statusCode !== null && statusCode !== 0) {
346
+ return { ...base, state: "error", error: `Check-in status error (code ${statusCode}).` };
347
+ }
348
+
349
+ const data = envelopeData(status);
350
+ if (!data) return { ...base, state: "unavailable", error: "Check-in activity unavailable." };
351
+ if (data["active"] === false) {
352
+ return { ...base, state: "unavailable", error: "No check-in activity running." };
353
+ }
354
+ const streak = asNumber(data["streak_days"]);
355
+ if (!args.forceClaim && data["today_checked_in"] === true) {
356
+ return { ...base, state: "already_claimed", streakDays: streak };
357
+ }
358
+
359
+ let claim: unknown;
360
+ let claimRaw = "";
361
+ let attempt = 0;
362
+ for (;;) {
363
+ try {
364
+ const response = await fetcher(claimUrl(providerId), {
365
+ method: "POST",
366
+ headers,
367
+ body: "{}",
368
+ signal: AbortSignal.timeout(TIMEOUT_MS),
369
+ });
370
+ if (response.status === 401 || response.status === 403) {
371
+ return { ...base, state: "error", error: "Credential invalid or expired." };
372
+ }
373
+ if (response.status >= 500 && attempt < CLAIM_RETRY_DELAYS_MS.length) {
374
+ await sleep(CLAIM_RETRY_DELAYS_MS[attempt] as number);
375
+ attempt += 1;
376
+ continue;
377
+ }
378
+ if (!response.ok) {
379
+ return { ...base, state: "error", error: `Daily check-in API error (${response.status}).` };
380
+ }
381
+ claimRaw = await response.text().catch(() => "");
382
+ claim = claimRaw ? JSON.parse(claimRaw) : null;
383
+ break;
384
+ } catch (error) {
385
+ if (error instanceof SyntaxError) {
386
+ return { ...base, state: "error", error: "Daily check-in response returned invalid JSON." };
387
+ }
388
+ if (isRetryableClaim(error, null) && attempt < CLAIM_RETRY_DELAYS_MS.length) {
389
+ await sleep(CLAIM_RETRY_DELAYS_MS[attempt] as number);
390
+ attempt += 1;
391
+ continue;
392
+ }
393
+ return {
394
+ ...base,
395
+ state: "error",
396
+ error: error instanceof Error ? error.message : "Daily check-in request failed.",
397
+ };
398
+ }
399
+ }
400
+
401
+ const claimCode = envelopeCode(claim);
402
+ if (claimCode !== null && claimCode !== 0) {
403
+ const alreadyText = alreadyClaimedMessage(claim, claimRaw);
404
+ if (claimCode === ALREADY_CLAIMED_CODE || alreadyText) {
405
+ return { ...base, state: "already_claimed", streakDays: streak };
406
+ }
407
+ if (claimCode === NOT_ELIGIBLE_CODE) return { ...base, state: "not_eligible" };
408
+ if (claimCode === EVENT_ENDED_CODE) return { ...base, state: "event_ended" };
409
+ return { ...base, state: "error", error: `Daily check-in error (code ${claimCode}).` };
410
+ }
411
+ // Some gateway variants answer the duplicate-claim state as prose with a
412
+ // success envelope; catch that before reporting a fresh claim.
413
+ if (claimCode === 0) {
414
+ const alreadyText = alreadyClaimedMessage(claim, claimRaw);
415
+ if (alreadyText) return { ...base, state: "already_claimed", streakDays: streak };
416
+ }
417
+
418
+ const claimData = envelopeData(claim);
419
+ return {
420
+ ...base,
421
+ state: "claimed",
422
+ credit: asNumber(claimData?.["credit"]) ?? asNumber(claimData?.["today_credit"]) ?? null,
423
+ streakDays: asNumber(claimData?.["streak_days"]) ?? streak,
424
+ };
425
+ }
426
+
427
+ export function checkinDayKey(now: Date = new Date()): string {
428
+ return now.toISOString().slice(0, 10);
429
+ }
430
+
431
+ // ── buddy-growth-shared ──────────────────────────────────────────
432
+
433
+ // (TIMEOUT_MS declared once above)
434
+
435
+ function growthBase(baseUrl: string): string {
436
+ return baseUrl.replace(/\/+$/, "").replace(/\/v2$/, "");
437
+ }
438
+
439
+ export type BuddyGrowthState = "claimed" | "already" | "not_eligible" | "skipped" | "error";
440
+
441
+ export interface BuddyGrowthResult {
442
+ readonly state: BuddyGrowthState;
443
+ /** Provider-facing detail for the log; `null` on success. */
444
+ readonly error: string | null;
445
+ /** Credits granted, when the upstream reported them. */
446
+ readonly credit: number | null;
447
+ }
448
+
449
+ export interface BuddyGrowthCall {
450
+ readonly baseUrl: string;
451
+ readonly headers: Record<string, string>;
452
+ readonly credential: string;
453
+ readonly fetcher: FetchLike;
454
+ /** Account UID for the `X-User-Id` identity header. */
455
+ readonly uid?: string | undefined;
456
+ /** Which account and provider the call belongs to — needed by the activity report. */
457
+ readonly accountId: string;
458
+ readonly providerId: string;
459
+ /** Injectable idempotency token generator (tests). */
460
+ readonly token?: (prefix: string) => string;
461
+ }
462
+
463
+ function clientToken(call: BuddyGrowthCall, prefix: string): string {
464
+ return (call.token ?? ((name: string) => `${name}-${randomUUID()}`))(prefix);
465
+ }
466
+
467
+ interface Reply {
468
+ readonly status: number;
469
+ readonly payload: Record<string, unknown> | null;
470
+ readonly data: Record<string, unknown> | null;
471
+ readonly code: number | null;
472
+ readonly message: string;
473
+ readonly ok: boolean;
474
+ readonly transport: string | null;
475
+ }
476
+
477
+ async function call(
478
+ options: BuddyGrowthCall,
479
+ method: "GET" | "POST",
480
+ path: string,
481
+ body?: Record<string, unknown>,
482
+ ): Promise<Reply> {
483
+ const url = `${growthBase(options.baseUrl)}${path}`;
484
+ let response: Response;
485
+ try {
486
+ response = await options.fetcher(url, {
487
+ method,
488
+ headers: {
489
+ ...options.headers,
490
+ ...(options.uid === undefined || options.uid.length === 0
491
+ ? {}
492
+ : { "X-User-Id": options.uid }),
493
+ Authorization: `Bearer ${options.credential}`,
494
+ "Content-Type": "application/json",
495
+ Accept: "application/json",
496
+ },
497
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
498
+ signal: AbortSignal.timeout(TIMEOUT_MS),
499
+ });
500
+ } catch (error) {
501
+ return {
502
+ status: 0,
503
+ payload: null,
504
+ data: null,
505
+ code: null,
506
+ message: error instanceof Error ? error.message : "request failed",
507
+ ok: false,
508
+ transport: error instanceof Error ? error.message : "request failed",
509
+ };
510
+ }
511
+ let payload: Record<string, unknown> | null = null;
512
+ let text = "";
513
+ try {
514
+ text = await response.text();
515
+ payload = text.length > 0 ? asRecord(JSON.parse(text)) : null;
516
+ } catch {
517
+ payload = null;
518
+ }
519
+ const code = payload === null || !("code" in payload) ? null : Number(payload["code"]);
520
+ const message = asText(payload?.["msg"]) ?? asText(payload?.["message"]) ?? text.slice(0, 200);
521
+ const data = asRecord(payload?.["data"]) ?? (payload !== null && !("code" in payload) ? payload : null);
522
+ if (!response.ok) {
523
+ return {
524
+ status: response.status,
525
+ payload,
526
+ data,
527
+ code: Number.isFinite(code) ? code : null,
528
+ message,
529
+ ok: false,
530
+ // A 409/403/400 here is the upstream's own answer, not a transport fault.
531
+ transport: null,
532
+ };
533
+ }
534
+ if (Number.isFinite(code) && code !== 0) {
535
+ return { status: response.status, payload, data, code, message, ok: false, transport: null };
536
+ }
537
+ return { status: response.status, payload, data, code, message, ok: true, transport: null };
538
+ }
539
+
540
+ function refused(reply: Reply, status: number, markers: readonly string[]): boolean {
541
+ if (reply.ok || reply.status !== status) return false;
542
+ const lower = reply.message.toLowerCase();
543
+ return markers.some((marker) => lower.includes(marker));
544
+ }
545
+
546
+ function coded(reply: Reply, code: number): boolean {
547
+ return !reply.ok && reply.code === code;
548
+ }
549
+
550
+ function done(reply: Reply, credit?: number | null): BuddyGrowthResult {
551
+ if (reply.ok) {
552
+ return { state: "claimed", error: null, credit: credit ?? creditOf(reply) };
553
+ }
554
+ return { state: "error", error: reply.message || `HTTP ${reply.status}`, credit: null };
555
+ }
556
+
557
+ function creditOf(reply: Reply): number | null {
558
+ if (reply.data === null) return null;
559
+ const raw = reply.data["credit"] ?? reply.data["credit_granted"] ?? reply.data["reward_credit"];
560
+ const value = Number(raw);
561
+ return Number.isFinite(value) ? value : null;
562
+ }
563
+
564
+ const STREAK_PATH = "/activity/growth/streak";
565
+
566
+ const HEATMAP_PATH = "/activity/growth/heatmap";
567
+
568
+ const MAKEUP_PATH = "/activity/growth/makeup-cards/use";
569
+
570
+ export interface BuddyStreakState {
571
+ readonly days: number;
572
+ readonly makeupCards: number;
573
+ }
574
+
575
+ export async function readBuddyStreak(
576
+ options: BuddyGrowthCall,
577
+ ): Promise<BuddyStreakState | null> {
578
+ const reply = await call(options, "GET", STREAK_PATH);
579
+ if (!reply.ok) return null;
580
+ const streak = asRecord(reply.data?.["streak"]);
581
+ const cards = asRecord(reply.data?.["makeup_cards"]);
582
+ return {
583
+ days: Number(streak?.["days"] ?? 0) || 0,
584
+ makeupCards: Number(cards?.["balance"] ?? 0) || 0,
585
+ };
586
+ }
587
+
588
+ export interface BuddyHeatmapCell {
589
+ readonly date: string;
590
+ readonly score: number;
591
+ }
592
+
593
+ export async function readBuddyHeatmap(
594
+ options: BuddyGrowthCall,
595
+ ): Promise<readonly BuddyHeatmapCell[] | null> {
596
+ const reply = await call(options, "GET", HEATMAP_PATH);
597
+ if (!reply.ok || reply.data === null) return null;
598
+ const list = reply.data["cells"];
599
+ if (!Array.isArray(list)) return null;
600
+ return list.flatMap((entry) => {
601
+ const row = asRecord(entry);
602
+ const date = asText(row?.["date"])?.slice(0, 10);
603
+ if (date === undefined || date.length === 0) return [];
604
+ return [{ date, score: Number(row?.["score"] ?? 0) || 0 }];
605
+ });
606
+ }
607
+
608
+ export function cstYesterday(now: Date = new Date()): string {
609
+ const shifted = new Date(now.getTime() + 8 * 3600_000);
610
+ return new Date(shifted.getTime() - 86_400_000).toISOString().slice(0, 10);
611
+ }
612
+
613
+ export async function useBuddyMakeupCard(
614
+ options: BuddyGrowthCall,
615
+ targetDate: string = cstYesterday(),
616
+ ): Promise<BuddyGrowthResult> {
617
+ const [heatmap, streak] = await Promise.all([
618
+ readBuddyHeatmap(options),
619
+ readBuddyStreak(options),
620
+ ]);
621
+ if (heatmap === null) return { state: "skipped", error: "heatmap unavailable", credit: null };
622
+ const cell = heatmap.find((entry) => entry.date === targetDate);
623
+ // No cell for the day means the heatmap does not cover it — no evidence of a
624
+ // miss, so nothing is spent.
625
+ if (cell === undefined || cell.score !== 0) {
626
+ return { state: "skipped", error: null, credit: null };
627
+ }
628
+ if (streak === null || streak.makeupCards <= 0) {
629
+ return { state: "skipped", error: null, credit: null };
630
+ }
631
+ const reply = await call(options, "POST", MAKEUP_PATH, { target_date: targetDate });
632
+ // No card, no miss, or already made up — all normal.
633
+ if (refused(reply, 400, ["no card", "insufficient", "already", "无卡", "已补"])) {
634
+ return { state: "already", error: null, credit: null };
635
+ }
636
+ return done(reply);
637
+ }
638
+
639
+ const REDEEM_PATH = "/activity/growth/redeem";
640
+
641
+ export const BUDDY_REDEEM_TIERS = ["7d", "14d", "28d"] as const;
642
+
643
+ export type BuddyRedeemTier = (typeof BUDDY_REDEEM_TIERS)[number];
644
+
645
+ export type BuddyTierStatus = "available" | "claimed" | "locked" | "unknown";
646
+
647
+ export interface BuddyRedemptionStatus {
648
+ readonly tiers: Readonly<Record<BuddyRedeemTier, BuddyTierStatus>>;
649
+ readonly remainingDays: number | null;
650
+ }
651
+
652
+ const TIER_FIELDS: Readonly<Record<BuddyRedeemTier, string>> = {
653
+ "7d": "tier_7d_status",
654
+ "14d": "tier_14d_status",
655
+ "28d": "tier_28d_status",
656
+ };
657
+
658
+ export async function readBuddyRedemption(
659
+ options: BuddyGrowthCall,
660
+ ): Promise<BuddyRedemptionStatus | null> {
661
+ const reply = await call(options, "GET", STREAK_PATH);
662
+ if (!reply.ok || reply.data === null) return null;
663
+ const status = asRecord(reply.data["redemption_status"]) ?? {};
664
+ const tiers = {} as Record<BuddyRedeemTier, BuddyTierStatus>;
665
+ for (const tier of BUDDY_REDEEM_TIERS) {
666
+ const value = asText(status[TIER_FIELDS[tier]])?.toLowerCase();
667
+ tiers[tier] =
668
+ value === "available" || value === "claimed" || value === "locked" ? value : "unknown";
669
+ }
670
+ const remaining = Number(status["remaining_days"]);
671
+ return { tiers, remainingDays: Number.isFinite(remaining) ? remaining : null };
672
+ }
673
+
674
+ export async function redeemBuddyStreakTier(
675
+ options: BuddyGrowthCall,
676
+ tier: BuddyRedeemTier,
677
+ ): Promise<BuddyGrowthResult> {
678
+ const reply = await call(options, "POST", REDEEM_PATH, {
679
+ tier,
680
+ client_token: clientToken(options, `redeem-${tier}`),
681
+ });
682
+ if (coded(reply, 409) || refused(reply, 409, ["duplicate", "已领取"])) {
683
+ return { state: "already", error: null, credit: null };
684
+ }
685
+ if (refused(reply, 403, ["not enough", "insufficient", "未达标", "天数不足"])) {
686
+ return { state: "not_eligible", error: null, credit: null };
687
+ }
688
+ return done(reply);
689
+ }
690
+
691
+ const TRAVEL_STATUS_PATH = "/activity/growth/buddy/travel/status";
692
+
693
+ const TRAVEL_CONFIG_PATH = "/activity/growth/buddy/travel/config";
694
+
695
+ const TRAVEL_DEPART_PATH = "/activity/growth/buddy/travel/depart";
696
+
697
+ const TRAVEL_CLAIM_PATH = "/activity/growth/buddy/travel/claim";
698
+
699
+ const TRAVEL_LOCATION_ID = 4;
700
+
701
+ export type BuddyTravelState = "idle" | "traveling" | "arrived" | "unknown";
702
+
703
+ export interface BuddyTravelStatus {
704
+ readonly state: BuddyTravelState;
705
+ readonly dailyLimitReached: boolean;
706
+ readonly recordId: number | null;
707
+ readonly rewardCredit: number | null;
708
+ }
709
+
710
+ export async function readBuddyTravelStatus(
711
+ options: BuddyGrowthCall,
712
+ ): Promise<BuddyTravelStatus | null> {
713
+ const reply = await call(options, "GET", TRAVEL_STATUS_PATH);
714
+ if (!reply.ok || reply.data === null) return null;
715
+ const raw = asText(reply.data["state"])?.toLowerCase();
716
+ const state: BuddyTravelState =
717
+ raw === "idle" || raw === "traveling" || raw === "arrived" ? raw : "unknown";
718
+ const record = Number(reply.data["record_id"]);
719
+ const reward = Number(reply.data["reward_credit"]);
720
+ return {
721
+ state,
722
+ dailyLimitReached: reply.data["daily_limit_reached"] === true,
723
+ recordId: Number.isFinite(record) ? record : null,
724
+ rewardCredit: Number.isFinite(reward) ? reward : null,
725
+ };
726
+ }
727
+
728
+ export async function readBuddyTravelConfig(
729
+ options: BuddyGrowthCall,
730
+ ): Promise<Record<string, unknown> | null> {
731
+ const reply = await call(options, "GET", TRAVEL_CONFIG_PATH);
732
+ return reply.ok ? reply.data : null;
733
+ }
734
+
735
+ export async function departBuddyTravel(
736
+ options: BuddyGrowthCall,
737
+ locationId: number = TRAVEL_LOCATION_ID,
738
+ ): Promise<BuddyGrowthResult> {
739
+ const reply = await call(options, "POST", TRAVEL_DEPART_PATH, { location_id: locationId });
740
+ if (refused(reply, 400, ["already traveling", "location not available", "limit"])) {
741
+ return { state: "already", error: null, credit: null };
742
+ }
743
+ return done(reply);
744
+ }
745
+
746
+ export async function claimBuddyTravel(
747
+ options: BuddyGrowthCall,
748
+ recordId: number,
749
+ ): Promise<BuddyGrowthResult> {
750
+ const reply = await call(options, "POST", TRAVEL_CLAIM_PATH, { record_id: recordId });
751
+ return done(reply);
752
+ }
753
+
754
+ const BUDDY_INFO_PATH = "/activity/growth/buddy/info";
755
+
756
+ const BUDDY_TASKS_PATH = "/v2/activity/growth/tasks";
757
+
758
+ const BUDDY_AGREEMENT_PATH = "/activity/growth/buddy/agreement";
759
+
760
+ const BUDDY_FIRST_PATH = "/activity/growth/buddy/first";
761
+
762
+ const FIRST_BUDDY_TASK = "first_buddy";
763
+
764
+ const ACCEPTED_STATUSES: readonly string[] = [
765
+ "not_accepted",
766
+ "accepted",
767
+ "in_progress",
768
+ "completed",
769
+ ];
770
+
771
+ export async function readBuddyInfo(
772
+ options: BuddyGrowthCall,
773
+ ): Promise<{ readonly hasBuddy: boolean } | null> {
774
+ const reply = await call(options, "GET", BUDDY_INFO_PATH);
775
+ if (!reply.ok || reply.data === null || !("buddy" in reply.data)) return null;
776
+ const buddy = reply.data["buddy"];
777
+ return { hasBuddy: buddy !== null && typeof buddy === "object" };
778
+ }
779
+
780
+ export async function readBuddyFirstTask(
781
+ options: BuddyGrowthCall,
782
+ ): Promise<Record<string, unknown> | null> {
783
+ const reply = await call(options, "GET", BUDDY_TASKS_PATH);
784
+ if (!reply.ok || reply.data === null) return null;
785
+ const rows = reply.data["tasks"];
786
+ if (!Array.isArray(rows)) return null;
787
+ const matches = rows.filter(
788
+ (row) => asRecord(row)?.["task_code"] === FIRST_BUDDY_TASK,
789
+ );
790
+ if (matches.length !== 1) return null;
791
+ const task = asRecord(matches[0]);
792
+ if (task === null) return null;
793
+ if (task["locked"] !== false || task["reward_buddy"] !== true) return null;
794
+ const status = asText(task["accept_status"])?.toLowerCase();
795
+ if (status === undefined || !ACCEPTED_STATUSES.includes(status)) return null;
796
+ return task;
797
+ }
798
+
799
+ export async function readBuddyAgreement(
800
+ options: BuddyGrowthCall,
801
+ ): Promise<Record<string, unknown> | null> {
802
+ const reply = await call(options, "GET", BUDDY_AGREEMENT_PATH);
803
+ return reply.ok ? reply.data : null;
804
+ }
805
+
806
+ export async function agreeBuddyAgreement(
807
+ options: BuddyGrowthCall,
808
+ ): Promise<BuddyGrowthResult> {
809
+ const reply = await call(options, "POST", BUDDY_AGREEMENT_PATH, { agree: true });
810
+ return done(reply);
811
+ }
812
+
813
+ export async function adoptFirstBuddy(
814
+ options: BuddyGrowthCall,
815
+ ): Promise<BuddyGrowthResult> {
816
+ const reply = await call(options, "POST", BUDDY_FIRST_PATH, {});
817
+ if (refused(reply, 400, ["first_buddy task not completed", "task not completed"])) {
818
+ return { state: "not_eligible", error: null, credit: null };
819
+ }
820
+ return done(reply);
821
+ }
822
+
823
+ // ── buddy-server-tools ──────────────────────────────────────────
824
+
825
+ export type ServerToolKind = "web_search" | "web_fetch";
826
+
827
+ export interface ServerToolDeclaration {
828
+ readonly kind: ServerToolKind;
829
+ readonly name: string;
830
+ }
831
+
832
+ export interface ServerToolInvocation {
833
+ readonly id: string;
834
+ readonly kind: ServerToolKind;
835
+ readonly rawArguments: string;
836
+ }
837
+
838
+ export interface ServerToolRunResult {
839
+ /** Text handed back upstream as the tool's result message. */
840
+ readonly content: string;
841
+ readonly toolCallId: string;
842
+ }
843
+
844
+ export interface ServerToolExecutor {
845
+ /** Runs one web fetch; `null` means the backend is unavailable. */
846
+ readonly fetch?: ((input: { url: string }) => Promise<string>) | undefined;
847
+ /** Runs one web search; `null` means the backend is unavailable. */
848
+ readonly search?: ((input: { query: string }) => Promise<string>) | undefined;
849
+ }
850
+
851
+ const SEARCH_TYPE_PREFIX = "web_search_";
852
+
853
+ const FETCH_TYPE_PREFIX = "web_fetch_";
854
+
855
+ function normalizeTypeName(value: string): string {
856
+ return value.trim().toLowerCase().replace(/[_-]+/g, " ");
857
+ }
858
+
859
+ function classifyByType(type: string): ServerToolKind | null {
860
+ const normalized = normalizeTypeName(type);
861
+ if (normalized.startsWith(normalizeTypeName(SEARCH_TYPE_PREFIX))) return "web_search";
862
+ if (normalized.startsWith(normalizeTypeName(FETCH_TYPE_PREFIX))) return "web_fetch";
863
+ return null;
864
+ }
865
+
866
+ export function classifyServerToolDeclaration(tool: ToolDefinition): ServerToolKind | null {
867
+ const raw = tool.native_type?.trim();
868
+ if (raw !== undefined && raw.length > 0) {
869
+ const byRaw = classifyByType(raw);
870
+ if (byRaw !== null) return byRaw;
871
+ }
872
+ if (tool.tool_type === "web_search") return "web_search";
873
+ if (tool.tool_type !== undefined && tool.tool_type !== "function") {
874
+ const byType = classifyByType(tool.tool_type);
875
+ if (byType !== null) return byType;
876
+ }
877
+ return null;
878
+ }
879
+
880
+ export function findServerToolDeclarations(
881
+ tools: readonly ToolDefinition[] | undefined,
882
+ ): Map<string, ServerToolKind> {
883
+ const out = new Map<string, ServerToolKind>();
884
+ for (const tool of tools ?? []) {
885
+ const kind = classifyServerToolDeclaration(tool);
886
+ if (kind !== null) out.set(tool.name, kind);
887
+ }
888
+ return out;
889
+ }
890
+
891
+ export function isServerToolCall(
892
+ name: string,
893
+ declared: ReadonlyMap<string, ServerToolKind>,
894
+ ): boolean {
895
+ return declared.has(name);
896
+ }
897
+
898
+ function parseJsonArguments(raw: string): Record<string, unknown> {
899
+ try {
900
+ const parsed: unknown = JSON.parse(raw);
901
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
902
+ return parsed as Record<string, unknown>;
903
+ }
904
+ } catch {
905
+ // fall through
906
+ }
907
+ return {};
908
+ }
909
+
910
+ export function buildServerToolInvocation(
911
+ callId: string,
912
+ name: string,
913
+ kind: ServerToolKind,
914
+ rawArguments: string,
915
+ ): ServerToolInvocation {
916
+ return { id: callId || `server_tool_${name}`, kind, rawArguments };
917
+ }
918
+
919
+ function textOf(value: unknown): string {
920
+ return typeof value === "string" ? value.trim() : "";
921
+ }
922
+
923
+ function safeFetchUrl(url: string): string | null {
924
+ let parsed: URL;
925
+ try {
926
+ parsed = new URL(url);
927
+ } catch {
928
+ return null;
929
+ }
930
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
931
+ return parsed.toString();
932
+ }
933
+
934
+ async function runOne(
935
+ executor: ServerToolExecutor,
936
+ invocation: ServerToolInvocation,
937
+ ): Promise<ServerToolRunResult> {
938
+ const args = parseJsonArguments(invocation.rawArguments);
939
+ if (invocation.kind === "web_fetch") {
940
+ const url = safeFetchUrl(textOf(args["url"] ?? args["target"]));
941
+ if (url === null) {
942
+ return {
943
+ toolCallId: invocation.id,
944
+ content: "web_fetch failed: the model supplied an invalid or unsupported URL and nothing was fetched.",
945
+ };
946
+ }
947
+ if (executor.fetch === undefined) {
948
+ return {
949
+ toolCallId: invocation.id,
950
+ content: "web_fetch unavailable: the gateway has no fetch backend configured.",
951
+ };
952
+ }
953
+ try {
954
+ const content = await executor.fetch({ url });
955
+ return { toolCallId: invocation.id, content };
956
+ } catch (error) {
957
+ return {
958
+ toolCallId: invocation.id,
959
+ content: `web_fetch failed: ${error instanceof Error ? error.message : String(error)}`,
960
+ };
961
+ }
962
+ }
963
+ const query = textOf(args["query"]);
964
+ if (query.length === 0) {
965
+ return {
966
+ toolCallId: invocation.id,
967
+ content: "web_search failed: the model supplied an empty query and nothing was searched.",
968
+ };
969
+ }
970
+ if (executor.search === undefined) {
971
+ return {
972
+ toolCallId: invocation.id,
973
+ content: "web_search unavailable: the gateway has no search backend configured.",
974
+ };
975
+ }
976
+ try {
977
+ const content = await executor.search({ query });
978
+ return { toolCallId: invocation.id, content };
979
+ } catch (error) {
980
+ return {
981
+ toolCallId: invocation.id,
982
+ content: `web_search failed: ${error instanceof Error ? error.message : String(error)}`,
983
+ };
984
+ }
985
+ }
986
+
987
+ export async function executeServerToolInvocations(
988
+ executor: ServerToolExecutor,
989
+ invocations: readonly ServerToolInvocation[],
990
+ ): Promise<readonly ServerToolRunResult[]> {
991
+ return Promise.all(invocations.map((invocation) => runOne(executor, invocation)));
992
+ }
993
+
994
+ export function buildServerToolFollowUpMessages(
995
+ assistantToolCalls: Array<{ id: string; name: string; arguments: string }>,
996
+ results: readonly ServerToolRunResult[],
997
+ ): Array<Record<string, unknown>> {
998
+ const messages: Array<Record<string, unknown>> = [];
999
+ if (assistantToolCalls.length > 0) {
1000
+ messages.push({
1001
+ role: "assistant",
1002
+ content: "",
1003
+ tool_calls: assistantToolCalls.map((call) => ({
1004
+ id: call.id,
1005
+ type: "function",
1006
+ function: { name: call.name, arguments: call.arguments },
1007
+ })),
1008
+ });
1009
+ }
1010
+ for (const result of results) {
1011
+ messages.push({
1012
+ role: "tool",
1013
+ tool_call_id: result.toolCallId,
1014
+ content: result.content,
1015
+ });
1016
+ }
1017
+ return messages;
1018
+ }