cartethyia 2.7.0 → 2.7.2

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 (330) hide show
  1. package/.env.example +107 -106
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-XaUmiBrC.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
  66. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js +1 -0
  70. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-DFVFrnWW.js} +2 -2
  74. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
  86. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  89. package/dist/dashboard/assets/providers/inception.svg +5 -0
  90. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
  94. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-KIT0rC6G.js → select-DBNVLdUu.js} +1 -1
  96. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
  98. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
  100. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
  102. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
  104. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  107. package/dist/dashboard/index.html +1 -1
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +54 -53
  110. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  111. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  112. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  114. package/package.json +96 -96
  115. package/scripts/cli/console-menu.ts +5 -2
  116. package/scripts/cli/tray.ts +21 -6
  117. package/src/boot/app.ts +684 -714
  118. package/src/boot/dependencies.ts +65 -62
  119. package/src/config/cooldowns.ts +16 -0
  120. package/src/config/providers.ts +13 -1
  121. package/src/config/resources.ts +70 -70
  122. package/src/config/security.ts +37 -37
  123. package/src/config/spec.ts +371 -358
  124. package/src/console/cli-tools/contracts.ts +10 -4
  125. package/src/console/console-router.ts +233 -225
  126. package/src/console/domain-registration.ts +398 -381
  127. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  128. package/src/console/providers/catalog/contracts.ts +19 -0
  129. package/src/console/providers/catalog/provider-operations.ts +38 -0
  130. package/src/console/providers/catalog/routes.ts +14 -0
  131. package/src/console/providers/catalog/store-helpers.ts +326 -0
  132. package/src/console/providers/catalog/store.ts +1292 -1563
  133. package/src/console/quota/account-quota-global.ts +2 -2
  134. package/src/console/quota/account-quota-shared.ts +1 -1
  135. package/src/console/quota/account-quota-tenant.ts +35 -12
  136. package/src/console/quota/quota-view.ts +2 -2
  137. package/src/console/routing/pools/contracts.ts +41 -1
  138. package/src/console/routing/pools/operations.ts +183 -4
  139. package/src/console/routing/pools/relay-deploy.ts +17 -4
  140. package/src/console/routing/pools/routes.ts +30 -0
  141. package/src/console/routing/pools/store.ts +98 -8
  142. package/src/main.ts +183 -167
  143. package/src/network/outbound-fetch.ts +124 -208
  144. package/src/network/pool/agent.ts +15 -0
  145. package/src/network/pool/pool-state.ts +7 -0
  146. package/src/network/pool/selector.ts +110 -2
  147. package/src/network/pool-cooldown-store.ts +252 -0
  148. package/src/network/pool-health-machine.ts +5 -3
  149. package/src/network/pool-health.ts +36 -9
  150. package/src/observability/event-query.ts +57 -0
  151. package/src/observability/metrics.ts +467 -467
  152. package/src/observability/runtime-metrics.ts +269 -269
  153. package/src/persistence/health-event-flush.ts +2 -0
  154. package/src/persistence/schema.ts +19 -0
  155. package/src/persistence/usage-flush.ts +8 -3
  156. package/src/protocol/canonical-model.ts +1 -1
  157. package/src/protocol/dsml.ts +377 -0
  158. package/src/protocol/gateway-error.ts +232 -232
  159. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  160. package/src/protocol/request/chat.ts +7 -7
  161. package/src/protocol/request/codex.ts +2 -2
  162. package/src/protocol/request/gemini.ts +1 -1
  163. package/src/protocol/request/messages.ts +6 -6
  164. package/src/protocol/request/responses.ts +7 -7
  165. package/src/protocol/response/dsml.ts +12 -133
  166. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  167. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  168. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  169. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  170. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  171. package/src/{transport → protocol}/translation/search.ts +1 -1
  172. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  173. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  174. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream-parsing.ts +176 -0
  176. package/src/providers/adapters/compatible-adapter.ts +11 -2
  177. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  178. package/src/providers/default-registry.ts +22 -5
  179. package/src/providers/discovery/probe-phases.ts +16 -3
  180. package/src/providers/discovery/probing-service.ts +4 -1
  181. package/src/providers/integrations/anthropic.ts +1 -1
  182. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  184. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  185. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  186. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  187. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  188. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  189. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  190. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  191. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  192. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  193. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  194. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  195. package/src/providers/integrations/claude/claude.ts +1 -1
  196. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  197. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  198. package/src/providers/integrations/codex/codex.ts +1 -1
  199. package/src/providers/integrations/factory/factory.ts +1 -1
  200. package/src/providers/integrations/github/github.ts +1 -1
  201. package/src/providers/integrations/grok/grok.ts +1 -1
  202. package/src/providers/integrations/inception/provider.ts +91 -0
  203. package/src/providers/integrations/inferhub.ts +1 -1
  204. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  205. package/src/providers/integrations/mistral.ts +193 -19
  206. package/src/providers/integrations/ollamacloud-models.ts +50 -0
  207. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  208. package/src/providers/integrations/opencode/prompt.ts +111 -0
  209. package/src/providers/integrations/opencode/provider.ts +101 -23
  210. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  211. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  212. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -1
  213. package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
  214. package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
  215. package/src/providers/integrations/qoder/qoder.ts +315 -400
  216. package/src/providers/integrations/shared/install-id.ts +2 -1
  217. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  218. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  219. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  220. package/src/providers/integrations/trae/trae.ts +106 -23
  221. package/src/providers/operations/account-error-classification.ts +1684 -0
  222. package/src/providers/operations/account-health-service.ts +27 -1503
  223. package/src/providers/operations/account-reset-service.ts +2 -0
  224. package/src/providers/operations/provider-catalog-service.ts +11 -1
  225. package/src/providers/provider-contracts.ts +7 -4
  226. package/src/providers/provider-metadata.ts +25 -4
  227. package/src/providers/provider-probe.ts +2 -2
  228. package/src/{console → providers}/quota/cache.ts +1 -1
  229. package/src/{console → providers}/quota/refresh.ts +5 -5
  230. package/src/security/cli-client-fingerprint.ts +9 -2
  231. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  232. package/src/transport/dispatch/attempt-loop.ts +3 -0
  233. package/src/transport/dispatch/empty-stream.ts +9 -2
  234. package/src/transport/dispatch/generation.ts +37 -55
  235. package/src/transport/dispatch/handler-deps.ts +32 -0
  236. package/src/transport/dispatch/leases.ts +9 -4
  237. package/src/transport/dispatch/proxy-request.ts +6 -30
  238. package/src/transport/dispatch/responses-compact.ts +22 -43
  239. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  240. package/src/transport/dispatch/streaming/stream.ts +0 -8
  241. package/src/transport/dispatch/systemone.ts +22 -42
  242. package/src/transport/dispatch/token-count.ts +23 -39
  243. package/src/transport/dispatch/upstream.ts +1 -3
  244. package/src/transport/dispatch/websearch.ts +22 -41
  245. package/src/transport/failure-policy.ts +92 -178
  246. package/src/transport/guards/abuse-guard.ts +10 -0
  247. package/src/transport/guards/auth-guard.ts +109 -109
  248. package/src/transport/guards/error-lifecycle.ts +1 -1
  249. package/src/transport/guards/pipeline.ts +220 -220
  250. package/src/transport/guards/request-context.ts +245 -238
  251. package/src/transport/request/degradation.ts +20 -1
  252. package/src/transport/request/inflight.ts +4 -114
  253. package/src/transport/request/preparer.ts +6 -6
  254. package/src/transport/request/state.ts +66 -38
  255. package/src/transport/request/tenant-preferences.ts +1 -1
  256. package/src/transport/routing/route-catalog.ts +75 -43
  257. package/src/transport/routing/router.ts +30 -2
  258. package/src/transport/surface/messages/parse.ts +1 -1
  259. package/src/transport/surface/responses/encode.ts +2 -3
  260. package/src/transport/surface/responses/parse.ts +49 -7
  261. package/src/types/undici.d.ts +33 -0
  262. package/src/workers/buddy-growth-tasks.ts +346 -425
  263. package/src/workers/daily-checkin.ts +17 -6
  264. package/src/workers/quota-refresh-sweep.ts +2 -2
  265. package/src/workers/scheduler.ts +17 -1
  266. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
  271. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  272. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  273. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  275. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
  276. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  277. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  278. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
  279. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  280. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
  281. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  282. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  283. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  284. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
  285. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  286. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
  287. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  288. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  289. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  290. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
  291. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  292. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  293. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  294. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
  295. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  296. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  297. package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
  298. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  299. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  300. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  301. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  302. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  303. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  304. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  305. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  306. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  307. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  308. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  309. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  310. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  311. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  312. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  313. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  314. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  315. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  316. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  317. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  318. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  319. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  320. package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
  321. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  322. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  323. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  324. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  325. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  326. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  327. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  328. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  329. /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
  330. /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The minimal system prompt injected into free-tier OpenCode payloads.
3
+ *
4
+ * The full CLI prompt is unnecessary: live trimming showed the free-tier gate
5
+ * accepts any system content that carries the `<env>` block with a
6
+ * `Working directory:` line — even a ~30-character placeholder passes. The
7
+ * marker line below is kept so `carriesOpenCodePrompt` can detect an already-
8
+ * injected prompt and avoid duplication.
9
+ */
10
+
11
+ /** The marker that identifies a body already carrying the injected prompt. */
12
+ export const OPENCODE_PROMPT_MARKER = "You are an interactive coding agent that helps users";
13
+
14
+ /**
15
+ * Assembles the minimal prompt: the marker line plus the `<env>` block the
16
+ * gate requires. No model identity, no skills, no style rules — the model is
17
+ * a gateway proxy and should not claim an upstream identity.
18
+ */
19
+ export function opencodeSystemPrompt(): string {
20
+ const directory = process.cwd().trim() || "/workspace";
21
+ return `You are an interactive coding agent that helps users with software engineering tasks.\n<env> Working directory: ${directory}</env>`;
22
+ }
23
+
24
+ /** Flattens a system/instruction value into its text fragments. */
25
+ export function textFragments(value: unknown): string[] {
26
+ if (typeof value === "string") {
27
+ return value.trim() === "" ? [] : [value];
28
+ }
29
+ if (Array.isArray(value)) return value.flatMap((item) => textFragments(item));
30
+ if (typeof value === "object" && value !== null) {
31
+ const record = value as Record<string, unknown>;
32
+ if (typeof record["text"] === "string") return textFragments(record["text"]);
33
+ if (record["content"] !== undefined) return textFragments(record["content"]);
34
+ }
35
+ return [];
36
+ }
37
+
38
+ /** Whether any text inside `value` already contains the injected prompt marker. */
39
+ export function carriesOpenCodePrompt(value: unknown): boolean {
40
+ return textFragments(value).some((part) => part.includes(OPENCODE_PROMPT_MARKER));
41
+ }
42
+
43
+ /**
44
+ * Appends the `<env>` block to the first existing system/developer message
45
+ * instead of prepending a new leading system message. The tool's own system
46
+ * content (Claude Code, DSH, …) keeps its place; the gate-required env block
47
+ * is tacked on at the end of it.
48
+ */
49
+ export function injectChatPrompt(payload: Record<string, unknown>, prompt: string): boolean {
50
+ const messages = payload["messages"];
51
+ if (!Array.isArray(messages)) return false;
52
+ const firstSystem = messages.findIndex((raw) => {
53
+ if (typeof raw !== "object" || raw === null) return false;
54
+ const role = (raw as Record<string, unknown>)["role"];
55
+ return role === "system" || role === "developer";
56
+ });
57
+ if (firstSystem === -1) {
58
+ payload["messages"] = [{ role: "system", content: prompt }, ...messages];
59
+ return true;
60
+ }
61
+ const message = messages[firstSystem] as Record<string, unknown>;
62
+ if (carriesOpenCodePrompt(message["content"])) return false;
63
+ const existing = textFragments(message["content"]).join("\n");
64
+ message["content"] = existing === "" ? prompt : `${existing}\n\n${prompt}`;
65
+ return true;
66
+ }
67
+
68
+ /** Appends the prompt to the top-level Anthropic `system` value. */
69
+ export function injectAnthropicPrompt(payload: Record<string, unknown>, prompt: string): boolean {
70
+ const system = payload["system"];
71
+ if (system !== undefined && carriesOpenCodePrompt(system)) return false;
72
+ if (system === undefined) {
73
+ payload["system"] = prompt;
74
+ return true;
75
+ }
76
+ if (typeof system === "string") {
77
+ payload["system"] = system.trim() === "" ? prompt : system + "\n\n" + prompt;
78
+ } else if (Array.isArray(system)) {
79
+ payload["system"] = [...system, { type: "text", text: prompt }];
80
+ } else {
81
+ payload["system"] = prompt;
82
+ }
83
+ return true;
84
+ }
85
+
86
+ /** Appends the prompt to the Responses `instructions` value. */
87
+ export function injectResponsesPrompt(payload: Record<string, unknown>, prompt: string): boolean {
88
+ const instructions = payload["instructions"];
89
+ if (instructions !== undefined && carriesOpenCodePrompt(instructions)) return false;
90
+ if (typeof instructions === "string") {
91
+ payload["instructions"] = instructions.trim() === "" ? prompt : instructions + "\n\n" + prompt;
92
+ } else {
93
+ payload["instructions"] = prompt;
94
+ }
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * Injects the prompt into a free-tier payload for the given wire family.
100
+ * System One payloads are never touched. Reports whether changed.
101
+ */
102
+ export function injectOpenCodePrompt(
103
+ payload: Record<string, unknown>,
104
+ wireFamily: "chat" | "responses" | "messages" | "systemone",
105
+ ): boolean {
106
+ if (wireFamily === "systemone") return false;
107
+ const prompt = opencodeSystemPrompt();
108
+ if (wireFamily === "chat") return injectChatPrompt(payload, prompt);
109
+ if (wireFamily === "messages") return injectAnthropicPrompt(payload, prompt);
110
+ return injectResponsesPrompt(payload, prompt);
111
+ }
@@ -9,6 +9,7 @@ import type { ApiKeyProviderSpec } from "../../configured-provider";
9
9
  import { resolveOpenCodeUnavailableFree } from "../../../config/providers";
10
10
  import { buildOpenCodeHeaders } from "./fingerprint";
11
11
  import { OPENCODE_TOOL_DEFINITIONS } from "./tool-definitions";
12
+ import { injectOpenCodePrompt } from "./prompt";
12
13
  /**
13
14
  * OpenCode's three API-key tiers, all served from `opencode.ai`:
14
15
  *
@@ -19,8 +20,10 @@ import { OPENCODE_TOOL_DEFINITIONS } from "./tool-definitions";
19
20
  * - `opencode-go` (OpenCode Go): billed API-key route on the separate
20
21
  * `/zen/go/v1` base — a bundled, higher-tier model set.
21
22
  *
22
- * Free and Zen dispatch on `/inference/v1`, NOT `/zen/v1`. The catalog is the
23
- * other way round: `/zen/v1/models` publishes the listing while
23
+ * Free and Zen dispatch on the `/inference` origin using the 2.1 family
24
+ * segment: `/inference/openai/v1/...` for chat/responses and
25
+ * `/inference/anthropic/v1/messages?beta=true` for Anthropic. The catalog is
26
+ * the other way round: `/zen/v1/models` publishes the listing while
24
27
  * `/inference/v1/models` answers 404. A replay that hit `/inference/v1` directly
25
28
  * returned 200 while the gateway kept failing, because every model row carried
26
29
  * an explicit `/zen/v1` endpoint — which outranks the per-provider wire-family
@@ -30,8 +33,28 @@ import { OPENCODE_TOOL_DEFINITIONS } from "./tool-definitions";
30
33
  * (`x-opencode-*` correlation IDs, `user-agent: opencode`). Go is
31
34
  * intentionally API-key-only.
32
35
  */
33
- /** Dispatch base for the Free and Zen tiers. */
34
- const ZEN_PATH_PREFIX = "/inference/v1";
36
+ /**
37
+ * The v2 upstream path for a wire family on the `/inference` origin, matching
38
+ * the reference implementation's `v2EndpointPath`: the 2.1 client
39
+ * groups `/inference` routes by provider family, so chat/responses sit under
40
+ * `/openai/v1/...` and Anthropic under `/anthropic/v1/messages?beta=true`.
41
+ * Go keeps the flat `/zen/go/v1/...` shape.
42
+ */
43
+ function zenEndpoint(wireFamily: string): string {
44
+ if (wireFamily === "messages") return "/inference/anthropic/v1/messages?beta=true";
45
+ const protocol = wireFamily === "responses" ? "responses" : "chat/completions";
46
+ return `/inference/openai/v1/${protocol}`;
47
+ }
48
+
49
+ /**
50
+ * The System One decision endpoint on the `/inference` origin. Verified live:
51
+ * System One does NOT take the v2 family segment — `/inference/openai/v1/systemone`
52
+ * answers 404 while the flat `/inference/v1/systemone` answers 200 with a valid
53
+ * `{state, questions}` body.
54
+ */
55
+ function zenSystemoneEndpoint(): string {
56
+ return "/inference/v1/systemone";
57
+ }
35
58
  const GO_PATH_PREFIX = "/zen/go/v1";
36
59
 
37
60
  /**
@@ -121,7 +144,10 @@ export function isFreeTierZenModel(modelId: string): boolean {
121
144
  // id rather than the prefixed one.
122
145
  const bare = modelId.slice(modelId.indexOf("/") + 1);
123
146
  if (unavailableFreeZenIds().includes(bare)) return false;
124
- return bare.endsWith("-free") || KNOWN_FREE_ZEN_IDS.includes(bare);
147
+ // The reference gateway's name rule admits any id containing "free" (any
148
+ // case) in addition to the suffix convention; the explicit lists keep the
149
+ // measured exclusions and the one free id that carries no marker.
150
+ return bare.endsWith("-free") || KNOWN_FREE_ZEN_IDS.includes(bare) || bare.toLowerCase().includes("free");
125
151
  }
126
152
 
127
153
  /**
@@ -156,6 +182,7 @@ function opencodeDesktopHeaders(
156
182
  undefined,
157
183
  context.conversation_affinity,
158
184
  context.request_headers?.["x-opencode-project"],
185
+ context.request_headers?.["x-opencode-session-id"],
159
186
  ),
160
187
  // The gate reads the anonymous sentinel: a free dispatch with no
161
188
  // Authorization is refused regardless of headers, tools, or session shape.
@@ -327,6 +354,34 @@ function clampOpenCodeTools(payload: Record<string, unknown>, request: Canonical
327
354
  if (kept.length > OPENCODE_MAX_TOOLS) return;
328
355
  payload["tools"] = [...kept, ...rest.slice(0, OPENCODE_MAX_TOOLS - kept.length)];
329
356
  }
357
+ /**
358
+ * The completion ceiling a 2.x client puts on every v2 chat body, matching the
359
+ * reference gateway's `v2CompletionLimit`. The upstream's free gate expects the
360
+ * same ceiling; an explicit `max_tokens`/`max_completion_tokens` from the
361
+ * caller always wins.
362
+ */
363
+ const OPENCODE_V2_COMPLETION_LIMIT = 32_000;
364
+
365
+ /** The key order a 2.x client's JSON.stringify emits for a chat body. */
366
+ const V2_CHAT_KEY_ORDER = ["model", "messages", "tools", "stream", "stream_options", "max_completion_tokens"] as const;
367
+
368
+ /**
369
+ * Reorders a chat payload's keys to the 2.x client's byte layout: the skeleton
370
+ * keys first in the client's order, then every remaining key sorted. Values are
371
+ * untouched; JSON.stringify preserves this insertion order, so the serialized
372
+ * body matches the reference gateway's `marshalV2ChatBody`.
373
+ */
374
+ function reorderV2ChatKeys(payload: Record<string, unknown>): void {
375
+ const keys = [
376
+ ...V2_CHAT_KEY_ORDER.filter((key) => key in payload),
377
+ ...Object.keys(payload).filter((key) => !(V2_CHAT_KEY_ORDER as readonly string[]).includes(key)).sort(),
378
+ ];
379
+ const next: Record<string, unknown> = {};
380
+ for (const key of keys) next[key] = payload[key];
381
+ for (const key of Object.keys(payload)) delete payload[key];
382
+ Object.assign(payload, next);
383
+ }
384
+
330
385
  function opencodeSpec(providerId: "opencode" | "opencode-zen" | "opencode-go"): ApiKeyProviderSpec {
331
386
  // Free-tier gate facts (verified live 2026-10-14 against opencode.ai/zen/v1):
332
387
  // a request for a free model with stream:false is ALWAYS refused with 403
@@ -352,19 +407,42 @@ function opencodeSpec(providerId: "opencode" | "opencode-zen" | "opencode-go"):
352
407
  if (candidate.wire_family === "responses") {
353
408
  payload.store = false;
354
409
  }
355
- if (candidate.wire_family === "chat" && request.stream) {
356
- payload.stream_options = {
357
- ...(typeof payload.stream_options === "object" && payload.stream_options !== null
358
- ? (payload.stream_options as Record<string, unknown>)
359
- : {}),
360
- include_usage: true,
361
- };
362
- }
363
- // Free model: force the upstream stream flag regardless of what the
364
- // caller asked for. The gate refuses stream:false on free models with
365
- // 403 FreeTierError; here it only patches the wire body, never the
410
+ // Free model: match the reference gateway's free-tier shaping 1:1 —
411
+ // the official CLI system prompt, the v2 completion ceiling, the
412
+ // include_usage flag, and the forced upstream stream. The gate refuses
413
+ // tool-less, non-streaming, or non-agent-shaped free dispatches with 403
414
+ // FreeTierError; here it only patches the wire body, never the
366
415
  // client-visible stream contract.
367
- if (isFreeTierZenModel(candidate.model_id)) payload.stream = true;
416
+ if (isFreeTierZenModel(candidate.model_id)) {
417
+ if (candidate.wire_family === "chat") {
418
+ // The wire encoder injects the v2 ceiling (32000) as the default
419
+ // max_completion_tokens when the caller set no limit. Treat that
420
+ // sentinel as "unset" so the model's own declared output ceiling can
421
+ // take its place; an explicit user limit is never touched.
422
+ const isV2Default =
423
+ payload["max_completion_tokens"] === OPENCODE_V2_COMPLETION_LIMIT &&
424
+ payload["max_tokens"] === undefined;
425
+ if (isV2Default || (payload["max_completion_tokens"] === undefined && payload["max_tokens"] === undefined)) {
426
+ const ceiling =
427
+ typeof candidate.output_limit === "number" && candidate.output_limit > 0
428
+ ? candidate.output_limit
429
+ : OPENCODE_V2_COMPLETION_LIMIT;
430
+ payload["max_completion_tokens"] = ceiling;
431
+ }
432
+ payload.stream_options = {
433
+ ...(typeof payload.stream_options === "object" && payload.stream_options !== null
434
+ ? (payload.stream_options as Record<string, unknown>)
435
+ : {}),
436
+ include_usage: true,
437
+ };
438
+ }
439
+ const family = candidate.wire_family;
440
+ if (family === "chat" || family === "responses" || family === "messages") {
441
+ injectOpenCodePrompt(payload, family);
442
+ }
443
+ payload.stream = true;
444
+ if (candidate.wire_family === "chat") reorderV2ChatKeys(payload);
445
+ }
368
446
  },
369
447
  // OpenCode Free is a genuinely public, unauthenticated endpoint: no
370
448
  // credential is ever forwarded, whatever the account configuration says.
@@ -424,7 +502,7 @@ export async function discoverOpenCodeFreeModels(options: {
424
502
  ...systemoneLimits(),
425
503
  freeTier: true,
426
504
  serviceKind: "systemone" as const,
427
- endpointPath: `${ZEN_PATH_PREFIX}/systemone`,
505
+ endpointPath: zenSystemoneEndpoint(),
428
506
  };
429
507
  });
430
508
  }
@@ -436,12 +514,12 @@ export async function discoverOpenCodeFreeModels(options: {
436
514
  // OpenCode row understates this model's 1M context limit, so keep the corrected
437
515
  // limit explicit for both tiers that serve it.
438
516
  const sharedZenChat: readonly ModelDefinition[] = [
439
- defineModel({ id: "big-pickle", providerId: "opencode", wireFamily: "chat", endpoint: `${ZEN_PATH_PREFIX}/chat/completions`, vision: true, reasoning: true }),
517
+ defineModel({ id: "big-pickle", providerId: "opencode", wireFamily: "chat", endpoint: zenEndpoint("chat"), vision: true, reasoning: true }),
440
518
  defineModel({
441
519
  id: "mimo-v2.6-flash-free",
442
520
  providerId: "opencode",
443
521
  wireFamily: "chat",
444
- endpoint: `${ZEN_PATH_PREFIX}/chat/completions`,
522
+ endpoint: zenEndpoint("chat"),
445
523
  ctx: 1_000_000,
446
524
  out: 32_000,
447
525
  vision: true,
@@ -478,7 +556,7 @@ const systemoneZen = (id: string, free: boolean): ModelDefinition =>
478
556
  providerId: "opencode",
479
557
  serviceKind: "systemone",
480
558
  wireFamily: "chat",
481
- endpoint: `${ZEN_PATH_PREFIX}/systemone`,
559
+ endpoint: zenSystemoneEndpoint(),
482
560
  ctx: SYSTEMONE_LIMITS.context,
483
561
  out: SYSTEMONE_LIMITS.output,
484
562
  toolCall: false,
@@ -507,8 +585,8 @@ export const OPENCODE_FREE_MODELS: readonly ModelDefinition[] = [
507
585
 
508
586
  export const OPENCODE_ZEN_MODELS: readonly ModelDefinition[] = [
509
587
  ...sharedZenChat,
510
- defineModel({ id: "muse-spark-1.2", providerId: "opencode", wireFamily: "responses", endpoint: `${ZEN_PATH_PREFIX}/responses`, vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
511
- defineModel({ id: "muse-spark-1.3", providerId: "opencode", wireFamily: "responses", endpoint: `${ZEN_PATH_PREFIX}/responses`, vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
588
+ defineModel({ id: "muse-spark-1.2", providerId: "opencode", wireFamily: "responses", endpoint: zenEndpoint("responses"), vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
589
+ defineModel({ id: "muse-spark-1.3", providerId: "opencode", wireFamily: "responses", endpoint: zenEndpoint("responses"), vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
512
590
  systemoneZen("jev-1.13", false),
513
591
  ];
514
592
 
@@ -6,10 +6,10 @@
6
6
  * even one carrying the right name — is refused with `403 FreeTierError`.
7
7
  * That is why these are verbatim rather than minimal.
8
8
  *
9
- * Source: the client's own tool registry as embedded by the `opencode2api`
10
- * gateway (`internal/gateway/opencode_tools.json`), captured from a real
11
- * dispatch. `patch` is absent from that capture; its description and single
12
- * `patchText` parameter come from the 2.0.32 CLI binary's own registration.
9
+ * Source: the client's own tool registry as embedded by the reference
10
+ * gateway's tool asset, captured from a real dispatch. `patch` is absent from
11
+ * that capture; its description and single `patchText` parameter come from the
12
+ * 2.0.32 CLI binary's own registration.
13
13
  *
14
14
  * Thirteen entries, matching the CLI's internal tool-name set.
15
15
  */
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Qoder daily check-in (streak / 100 Credits per day), ported from the
3
+ * reference bridge (`qoder_accounts.py` in `qoder2api-hub`).
4
+ *
5
+ * Two upstream surfaces, tried in order:
6
+ *
7
+ * 1. Legacy sash: `GET /sash/api/v1/me/daily-check-in/status` then
8
+ * `POST /sash/api/v1/me/daily-check-in/claim`. The status exposes the
9
+ * streak (`currentStreakDays`) and the reward (`rewardCredits`, 100).
10
+ * 2. Activity-platform campaign: when the legacy surface is absent (INTL often
11
+ * answers 404), `GET /sash/api/v1/me/campaigns` lists daily rewards and the
12
+ * CREDITS-kind claimable one is claimed via
13
+ * `POST /sash/api/v1/me/campaigns/{id}/claim`.
14
+ *
15
+ * The result shape is the same `DailyCheckinResult` the buddy family uses, so
16
+ * the shared sweep slot ledger (`daily_checkin_ledger`), the log phrasing, and
17
+ * the console surface treat Qoder like any other daily-grant provider.
18
+ */
19
+ import { QODER_REALMS, qoderRealmForProvider } from "./qoder-realm";
20
+
21
+ /** Structural fetch bound; Bun's own fetch type carries extra host props. */
22
+ type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
23
+ import type { DailyCheckinResult, DailyCheckinState } from "../buddy/buddy-checkin";
24
+
25
+ export const QODER_CHECKIN_PROVIDER_IDS: readonly string[] = ["qoder", "qodercn"];
26
+
27
+ /** True when this provider id runs the Qoder check-in surface. */
28
+ export function isQoderCheckinProvider(providerId: string): boolean {
29
+ return (QODER_CHECKIN_PROVIDER_IDS as readonly string[]).includes(providerId);
30
+ }
31
+
32
+ /** Abort budget for one upstream check-in request. */
33
+ const TIMEOUT_MS = 15_000;
34
+
35
+ const CHECKIN_STATUS_PATH = "/sash/api/v1/me/daily-check-in/status";
36
+ const CHECKIN_CLAIM_PATH = "/sash/api/v1/me/daily-check-in/claim";
37
+ const CAMPAIGNS_PATH = "/sash/api/v1/me/campaigns";
38
+ const CAMPAIGN_CLAIM_PATH = "/sash/api/v1/me/campaigns/%s/claim";
39
+
40
+ function asRecord(value: unknown): Record<string, unknown> | null {
41
+ if (typeof value !== "object" || value === null) return null;
42
+ return value as Record<string, unknown>;
43
+ }
44
+
45
+ function asNumber(value: unknown): number | null {
46
+ if (typeof value === "number" && Number.isFinite(value)) return value;
47
+ if (typeof value === "string" && value.trim() !== "") {
48
+ const parsed = Number(value);
49
+ return Number.isFinite(parsed) ? parsed : null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function safeParse(raw: string): unknown {
55
+ try {
56
+ return JSON.parse(raw);
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function baseOutcome(
63
+ providerId: string,
64
+ accountId: string,
65
+ ): { providerId: string; accountId: string; credit: null; streakDays: null; error: null } {
66
+ return { providerId, accountId, credit: null, streakDays: null, error: null };
67
+ }
68
+
69
+ function asState(state: string, today: string, lastClaimedAt: unknown): { active: boolean; todayCheckedIn: boolean } {
70
+ const status = state.toUpperCase();
71
+ const active = status === "CLAIMABLE" || status === "CLAIMED";
72
+ let claimedToday = false;
73
+ if (status === "CLAIMED") {
74
+ const stamp = asNumber(lastClaimedAt);
75
+ if (stamp !== null) {
76
+ const day = new Date(stamp * 1000).toISOString().slice(0, 10);
77
+ claimedToday = day === today;
78
+ }
79
+ }
80
+ return { active, todayCheckedIn: claimedToday };
81
+ }
82
+
83
+ /** GET campaign list; returns `null` when the surface is unreachable/unauthorized. */
84
+ async function fetchCampaigns(
85
+ base: string,
86
+ credential: string,
87
+ fetcher: Fetcher,
88
+ ): Promise<Record<string, unknown>[] | null> {
89
+ try {
90
+ const response = await fetcher(`${base}${CAMPAIGNS_PATH}`, {
91
+ method: "GET",
92
+ headers: {
93
+ Authorization: `Bearer ${credential}`,
94
+ Accept: "application/json",
95
+ },
96
+ signal: AbortSignal.timeout(TIMEOUT_MS),
97
+ });
98
+ if (response.status === 401 || response.status === 403) return null;
99
+ if (!response.ok) return null;
100
+ const payload: unknown = await response.json().catch(() => null);
101
+ const rec = asRecord(payload);
102
+ const list = Array.isArray(rec?.["campaigns"]) ? rec!["campaigns"] : Array.isArray(payload) ? payload : null;
103
+ return Array.isArray(list) ? (list as Record<string, unknown>[]) : null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Claims one campaign and returns the resulting state. Mirrors the donor's
111
+ * `claim_campaign` idempotence handling: a 409 or `ALREADY_CLAIMED` body is
112
+ * an already-claimed grant, `SAME_PERSON_ALREADY_CLAIMED`/`BLOCKED` is a
113
+ * same-person block (not a failure of this account's day), and anything else
114
+ * is an error.
115
+ */
116
+ async function claimCampaign(
117
+ base: string,
118
+ campaignId: string,
119
+ credential: string,
120
+ fetcher: Fetcher,
121
+ ): Promise<{ state: DailyCheckinState; amount: number }> {
122
+ const url = base + CAMPAIGN_CLAIM_PATH.replace("%s", encodeURIComponent(campaignId));
123
+ let response: Response;
124
+ try {
125
+ response = await fetcher(url, {
126
+ method: "POST",
127
+ headers: {
128
+ Authorization: `Bearer ${credential}`,
129
+ "Content-Type": "application/json",
130
+ Accept: "application/json",
131
+ },
132
+ body: "{}",
133
+ signal: AbortSignal.timeout(TIMEOUT_MS),
134
+ });
135
+ } catch {
136
+ return { state: "error", amount: 0 };
137
+ }
138
+ const raw = await response.text().catch(() => "");
139
+ const payload: unknown = raw ? safeParse(raw) : null;
140
+ const rec = asRecord(payload);
141
+ const status = String(rec?.["status"] ?? "").toUpperCase();
142
+ const failure = String(rec?.["failureCode"] ?? "").toUpperCase();
143
+ const success = rec?.["success"] !== false;
144
+ const benefit = asRecord(rec?.["benefit"]);
145
+ const amount = asNumber(rec?.["amount"] ?? benefit?.["amount"]) ?? 0;
146
+ if (response.status === 409 || failure === "ALREADY_CLAIMED" || status === "CLAIMED" && failure === "REPLAYED") {
147
+ return { state: "already_claimed", amount };
148
+ }
149
+ if (failure === "SAME_PERSON_ALREADY_CLAIMED" || status === "BLOCKED") {
150
+ return { state: "not_eligible", amount: 0 };
151
+ }
152
+ if (response.ok && success && (status === "CLAIMED" || status === "GRANTED" || status === "SUCCESS")) {
153
+ return { state: "claimed", amount };
154
+ }
155
+ return { state: "error", amount: 0 };
156
+ }
157
+
158
+ /** Legacy sash status + claim. Returns `null` when the surface is absent (404/405/410). */
159
+ async function legacySashCheckin(args: {
160
+ readonly base: string;
161
+ readonly credential: string;
162
+ readonly fetcher: Fetcher;
163
+ readonly now?: () => Date;
164
+ }): Promise<{ state: DailyCheckinState; credit: number; streakDays: number } | null> {
165
+ const { base, credential, fetcher } = args;
166
+ const today = (args.now?.() ?? new Date()).toISOString().slice(0, 10);
167
+ let statusResponse: Response;
168
+ try {
169
+ statusResponse = await fetcher(`${base}${CHECKIN_STATUS_PATH}`, {
170
+ method: "GET",
171
+ headers: {
172
+ Authorization: `Bearer ${credential}`,
173
+ Accept: "application/json",
174
+ },
175
+ signal: AbortSignal.timeout(TIMEOUT_MS),
176
+ });
177
+ } catch {
178
+ return { state: "error", credit: 0, streakDays: 0 };
179
+ }
180
+ if (statusResponse.status === 401 || statusResponse.status === 403) {
181
+ return { state: "error", credit: 0, streakDays: 0 };
182
+ }
183
+ if (statusResponse.status === 404 || statusResponse.status === 405 || statusResponse.status === 410) {
184
+ return null; // surface absent — caller falls back to campaign
185
+ }
186
+ if (!statusResponse.ok) {
187
+ return { state: "error", credit: 0, streakDays: 0 };
188
+ }
189
+ const payload: unknown = await statusResponse.json().catch(() => null);
190
+ const rec = asRecord(payload);
191
+ if (!rec) return { state: "error", credit: 0, streakDays: 0 };
192
+ const { active, todayCheckedIn } = asState(String(rec["status"] ?? ""), today, rec["lastClaimedAt"]);
193
+ const streakDays = asNumber(rec["currentStreakDays"]) ?? 0;
194
+ const credit = asNumber(rec["rewardCredits"]) ?? 100;
195
+ if (todayCheckedIn) return { state: "already_claimed", credit: 0, streakDays };
196
+ if (!active) return { state: "not_eligible", credit: 0, streakDays };
197
+ // Claim.
198
+ let claimResponse: Response;
199
+ try {
200
+ claimResponse = await fetcher(`${base}${CHECKIN_CLAIM_PATH}`, {
201
+ method: "POST",
202
+ headers: {
203
+ Authorization: `Bearer ${credential}`,
204
+ "Content-Type": "application/json",
205
+ Accept: "application/json",
206
+ },
207
+ body: "{}",
208
+ signal: AbortSignal.timeout(TIMEOUT_MS),
209
+ });
210
+ } catch {
211
+ return { state: "error", credit: 0, streakDays };
212
+ }
213
+ const raw = await claimResponse.text().catch(() => "");
214
+ const lower = raw.toLowerCase();
215
+ if (claimResponse.status === 409 || lower.includes("already_claimed") || lower.includes("已签到")) {
216
+ return { state: "already_claimed", credit: 0, streakDays };
217
+ }
218
+ const body: unknown = raw ? safeParse(raw) : null;
219
+ const bodyRec = asRecord(body);
220
+ const ok = claimResponse.ok && bodyRec?.["success"] !== false;
221
+ if (!ok) return { state: "error", credit: 0, streakDays };
222
+ return { state: "claimed", credit, streakDays };
223
+ }
224
+
225
+ /**
226
+ * Performs one account's Qoder daily check-in: legacy sash first, campaign
227
+ * fallback when the sash surface is absent. Returns the same
228
+ * `DailyCheckinResult` shape the shared sweep consumes.
229
+ */
230
+ export async function fetchQoderDailyCheckin(args: {
231
+ readonly providerId: string;
232
+ readonly accountId: string;
233
+ readonly credential: string;
234
+ readonly fetcher: Fetcher;
235
+ readonly now?: () => Date;
236
+ }): Promise<DailyCheckinResult> {
237
+ const { providerId, accountId, credential, fetcher } = args;
238
+ const baseResult = baseOutcome(providerId, accountId);
239
+ if (!credential) {
240
+ return { ...baseResult, state: "error", error: "Account has no stored credential." };
241
+ }
242
+ const realm = qoderRealmForProvider(providerId);
243
+ const base = QODER_REALMS[realm].openapi;
244
+
245
+ const sash = await legacySashCheckin({
246
+ base,
247
+ credential,
248
+ fetcher,
249
+ ...(args.now === undefined ? {} : { now: args.now }),
250
+ });
251
+ if (sash !== null) {
252
+ return {
253
+ ...baseResult,
254
+ state: sash.state,
255
+ credit: sash.state === "claimed" ? sash.credit : null,
256
+ streakDays: sash.streakDays,
257
+ ...(sash.state === "error" ? { error: "Upstream daily check-in failed." } : {}),
258
+ };
259
+ }
260
+
261
+ // Legacy surface absent: campaign fallback.
262
+ const campaigns = await fetchCampaigns(base, credential, fetcher);
263
+ if (campaigns === null) {
264
+ return { ...baseResult, state: "error", error: "Check-in surface unavailable (legacy 404 and campaign list failed)." };
265
+ }
266
+ let alreadySeen = false;
267
+ for (const campaign of campaigns) {
268
+ const rec = asRecord(campaign);
269
+ if (!rec) continue;
270
+ const benefit = asRecord(rec["benefit"]);
271
+ const kind = String(benefit?.["kind"] ?? "").toUpperCase();
272
+ if (kind !== "CREDITS" && kind !== "") continue;
273
+ const actionType = String(rec["action_type"] ?? "");
274
+ if (actionType !== "" && actionType !== "CLAIM_BENEFIT") continue;
275
+ const claimStatus = String(rec["claim_status"] ?? "").toUpperCase();
276
+ if (claimStatus === "CLAIMED") {
277
+ alreadySeen = true;
278
+ continue;
279
+ }
280
+ if (claimStatus !== "CLAIMABLE") continue;
281
+ const campaignId = String(rec["campaign_id"] ?? "");
282
+ if (!campaignId) continue;
283
+ const result = await claimCampaign(base, campaignId, credential, fetcher);
284
+ if (result.state === "claimed") {
285
+ const amount = asNumber(benefit?.["amount"]) ?? result.amount;
286
+ return { ...baseResult, state: "claimed", credit: amount, streakDays: null };
287
+ }
288
+ if (result.state === "already_claimed") {
289
+ alreadySeen = true;
290
+ continue;
291
+ }
292
+ if (result.state === "error") {
293
+ return { ...baseResult, state: "error", error: "Campaign claim failed." };
294
+ }
295
+ // not_eligible (same-person block): stop trying further campaigns.
296
+ return { ...baseResult, state: "not_eligible", error: "Same-person claim already used today." };
297
+ }
298
+ return {
299
+ ...baseResult,
300
+ state: alreadySeen ? "already_claimed" : "unavailable",
301
+ error: alreadySeen ? null : "No daily CREDITS campaign claimable.",
302
+ };
303
+ }
@@ -150,7 +150,18 @@ export interface QoderOAuthState {
150
150
 
151
151
  /** Reads the persisted Qoder identity; `undefined` when the account predates it. */
152
152
  export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefined {
153
- const root = record(value);
153
+ let root = record(value);
154
+ if (root === undefined && typeof value === "string") {
155
+ // The credential loader stores `auth_state` as JSON text in SQLite, so
156
+ // the resolver hands the adapter a string when the cache was cold. Parse
157
+ // it before giving up — a login that works once must work again after the
158
+ // cache is evicted.
159
+ try {
160
+ root = record(JSON.parse(value));
161
+ } catch {
162
+ return undefined;
163
+ }
164
+ }
154
165
  if (root === undefined) return undefined;
155
166
  const userId = nonEmptyTrimmedString(root.userId);
156
167
  const machineId = nonEmptyTrimmedString(root.machineId);