cartethyia 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -1,258 +1,257 @@
1
- import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
2
- import {
3
- ENV_PATH,
4
- getLocalPlatform,
5
- getOsHints,
6
- hasPlaceholderSecrets,
7
- parseServiceUrl,
8
- probeTcpService,
9
- readEnvFile,
10
- } from "../shared/env";
11
- import type { ProbeResult } from "../shared/env";
12
- import { resolveDataDir, resolveDbMode, type DbMode } from "../../src/persistence/db-mode";
13
-
14
- interface DoctorConfig {
15
- envPath: string;
16
- probeTimeoutMs: number;
17
- }
18
-
19
- function loadConfig(): DoctorConfig {
20
- return {
21
- envPath: ENV_PATH,
22
- probeTimeoutMs: 3000,
23
- };
24
- }
25
-
26
- /**
27
- * Operator escape hatch: clears the console login lockout bucket.
28
- *
29
- * The lockout is keyed by `<username>:<clientIp>` and lasts an hour, and there
30
- * is no console surface to clear it — a locked-out operator has no way back in
31
- * short of editing the database by hand. This is the documented reset.
32
- *
33
- * Usage:
34
- * bun run doctor --reset-lockout <username> <clientIp>
35
- * bun run doctor --reset-lockout --all
36
- */
37
- async function resetLockout(args: readonly string[]): Promise<void> {
38
- const all = args.includes("--all");
39
- const positional = args.filter((arg) => !arg.startsWith("--"));
40
- if (!all && positional.length < 2) {
41
- console.error("Usage: bun run doctor --reset-lockout <username> <clientIp>");
42
- console.error(" bun run doctor --reset-lockout --all");
43
- process.exit(1);
44
- }
45
-
46
- const env = await readEnvFile(ENV_PATH);
47
- const databaseUrl = env.DATABASE_URL ?? process.env.DATABASE_URL;
48
- if (!databaseUrl) {
49
- console.error("✗ DATABASE_URL is not set; cannot reach the lockout table.");
50
- process.exit(1);
51
- }
52
- process.env.DATABASE_URL = databaseUrl;
53
-
54
- const { getDb, closeDb } = await import("../../src/persistence/boot");
55
- const { consoleLockouts } = await import("../../src/persistence/schema");
56
- const { eq, or } = await import("drizzle-orm");
57
-
58
- const db = getDb();
59
- try {
60
- if (all) {
61
- const cleared = await db.delete(consoleLockouts).returning({ ip: consoleLockouts.ip });
62
- console.log(`✓ Cleared ${cleared.length} lockout row(s).`);
63
- return;
64
- }
65
- const username = positional[0]!.trim().toLowerCase();
66
- const clientIp = positional[1]!;
67
- // The bucket key is `<identifier>:<ip>`; match either exact spelling so
68
- // an operator does not have to guess how the identifier was normalized.
69
- const cleared = await db
70
- .delete(consoleLockouts)
71
- .where(or(eq(consoleLockouts.ip, `${username}:${clientIp}`), eq(consoleLockouts.ip, clientIp)))
72
- .returning({ ip: consoleLockouts.ip });
73
- if (cleared.length === 0) {
74
- console.log(`• No lockout row for ${username}:${clientIp} (already clear).`);
75
- } else {
76
- console.log(`✓ Cleared lockout for ${cleared.map((row) => row.ip).join(", ")}.`);
77
- }
78
- } finally {
79
- await closeDb();
80
- }
81
- }
82
-
83
-
84
- async function probeReadiness(port: number, timeoutMs: number): Promise<ProbeResult> {
85
- const controller = new AbortController();
86
- const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
87
-
88
- try {
89
- const response = await fetch(`http://localhost:${port}/health/ready`, {
90
- signal: controller.signal,
91
- });
92
-
93
- clearTimeout(timeoutHandle);
94
-
95
- if (response.ok) {
96
- return { success: true };
97
- }
98
- return { success: false, error: `HTTP ${response.status}` };
99
- } catch (err) {
100
- clearTimeout(timeoutHandle);
101
- return { success: false, error: String(err) };
102
- }
103
- }
104
-
105
- async function doctor(): Promise<void> {
106
- const config = loadConfig();
107
-
108
- const resetIndex = process.argv.indexOf("--reset-lockout");
109
- if (resetIndex !== -1) {
110
- await resetLockout(process.argv.slice(resetIndex + 1));
111
- return;
112
- }
113
-
114
- console.log("🩺 Cartethyia Health Check\n");
115
- console.log(`🖥️ Host platform: ${getLocalPlatform()}`);
116
-
117
- // Step 1: Check .env exists
118
- console.log("📋 Environment Configuration");
119
- if (!existsSync(config.envPath)) {
120
- console.log(" ✗ .env file not found");
121
- console.log(" Run: bun setup\n");
122
- process.exit(1);
123
- }
124
- console.log(" ✓ .env file exists");
125
-
126
- // Step 2: Check required environment variables. DATABASE_URL is required in
127
- // full mode only; lite keeps its database in the data dir. Redis is never
128
- // required — unset means the in-memory backend.
129
- const envVars = await readEnvFile(config.envPath);
130
- // `CARTETHYIA_DB_MODE` has no default and throws when absent, which is the
131
- // right answer at boot but the wrong one here: this command exists to report
132
- // a broken environment readably, so the missing key joins the checklist
133
- // instead of crashing before it prints.
134
- const missing: string[] = [];
135
- let dbMode: DbMode | null = null;
136
- try {
137
- dbMode = resolveDbMode({ ...process.env, ...envVars });
138
- } catch {
139
- console.log(" ✗ CARTETHYIA_DB_MODE is not set — run: bun setup");
140
- // Counted as missing so this command exits non-zero. A boot would throw on
141
- // the same key, so a green doctor here would be a false all-clear.
142
- missing.push("CARTETHYIA_DB_MODE");
143
- }
144
- const required =
145
- dbMode === "full"
146
- ? ["DATABASE_URL", "CARTETHYIA_ENCRYPTION_KEY"]
147
- : ["CARTETHYIA_ENCRYPTION_KEY"];
148
-
149
- for (const key of required) {
150
- const value = envVars[key] ?? process.env[key];
151
- if (!value) {
152
- missing.push(key);
153
- console.log(` ✗ ${key} is not set`);
154
- } else if (hasPlaceholderSecrets(value)) {
155
- console.log(` ⚠️ ${key} has placeholder value — update before production`);
156
- } else {
157
- console.log(` ✓ ${key} is set`);
158
- }
159
- }
160
-
161
- if (missing.length > 0) {
162
- console.log(
163
- `\nFailed: ${missing.length} required environment variable(s) missing or using placeholders\n`,
164
- );
165
- process.exit(1);
166
- }
167
-
168
- console.log();
169
-
170
- // Step 3: Check database. Full mode probes the TCP service; lite verifies
171
- // the data dir is writable (the SQLite backend creates the file at boot too).
172
- if (dbMode === "lite") {
173
- const dataDir = resolveDataDir({ ...process.env, ...envVars });
174
- try {
175
- mkdirSync(dataDir, { recursive: true });
176
- accessSync(dataDir, constants.W_OK);
177
- console.log(` ✓ Lite data dir is writable (${dataDir})`);
178
- } catch {
179
- console.log(` ✗ Lite data dir is not writable (${dataDir})`);
180
- process.exit(1);
181
- }
182
- } else {
183
- const dbUrl = envVars.DATABASE_URL ?? process.env.DATABASE_URL;
184
- const dbParsed = parseServiceUrl(dbUrl ?? "");
185
-
186
- if (!dbParsed) {
187
- console.log(" ✗ Invalid DATABASE_URL format");
188
- process.exit(1);
189
- }
190
-
191
- const dbResult = await probeTcpService(dbParsed.host, dbParsed.port, config.probeTimeoutMs);
192
- if (dbResult.success) {
193
- console.log(` ✓ Reachable at ${dbParsed.host}:${dbParsed.port}`);
194
- } else {
195
- console.log(` ✗ Not reachable at ${dbParsed.host}:${dbParsed.port}`);
196
- console.log(` Error: ${dbResult.error || "connection refused"}`);
197
- for (const hint of getOsHints("postgres")) console.log(` ${hint}`);
198
- process.exit(1);
199
- }
200
- }
201
-
202
- console.log();
203
-
204
- // Step 4: Check Redis connectivity — informational only. A configured URL
205
- // is probed; unset means the in-memory backend, which is always up.
206
- console.log("🔴 Redis");
207
- const redisUrl = envVars.REDIS_URL ?? process.env.REDIS_URL;
208
- if (!redisUrl) {
209
- console.log(" ✓ in-memory backend (REDIS_URL not set)");
210
- } else {
211
- const redisParsed = parseServiceUrl(redisUrl);
212
- if (!redisParsed) {
213
- console.log(" ✗ REDIS_URL is invalid");
214
- process.exit(1);
215
- }
216
- const redisResult = await probeTcpService(
217
- redisParsed.host,
218
- redisParsed.port,
219
- config.probeTimeoutMs,
220
- );
221
- if (redisResult.success) {
222
- console.log(` ✓ Reachable at ${redisParsed.host}:${redisParsed.port}`);
223
- } else {
224
- console.log(` ✗ Not reachable at ${redisParsed.host}:${redisParsed.port}`);
225
- console.log(` Error: ${redisResult.error || "connection refused"}`);
226
- for (const hint of getOsHints("redis")) console.log(` ${hint}`);
227
- process.exit(1);
228
- }
229
- }
230
-
231
- console.log();
232
-
233
- // Step 5: Check application readiness
234
- console.log("🏥 Application Readiness");
235
- // Use the same explicit PORT resolution as the application.
236
- const port = Number(envVars.PORT ?? process.env.PORT ?? "12800");
237
- console.log(` Checking http://localhost:${port}/health/ready...`);
238
-
239
- const readinessResult = await probeReadiness(port, config.probeTimeoutMs);
240
- if (readinessResult.success) {
241
- console.log(` ✓ Application is ready`);
242
- } else {
243
- console.log(` ✗ Application is not ready`);
244
- console.log(` Error: ${readinessResult.error || "connection refused"}`);
245
- console.log("");
246
- console.log("Make sure the application is running:");
247
- console.log(` bun run dev`);
248
- process.exit(1);
249
- }
250
-
251
- console.log();
252
- console.log("✅ All systems operational!");
253
- }
254
-
255
- doctor().catch((err) => {
256
- console.error("❌ Health check failed:", err);
257
- process.exit(1);
258
- });
1
+ import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
2
+ import { ENV_PATH, hasPlaceholderSecrets, readEnvFile } from "../support/env";
3
+ import { getLocalPlatform, getOsHints } from "../support/platform";
4
+ import { parseServiceUrl, probeTcpService } from "../support/services";
5
+ import type { ProbeResult } from "../support/services";
6
+ import { resolveDataDir, resolveDbMode, type DbMode } from "../../../src/persistence/db-mode";
7
+
8
+ interface DoctorConfig {
9
+ envPath: string;
10
+ probeTimeoutMs: number;
11
+ }
12
+
13
+ function loadConfig(): DoctorConfig {
14
+ return {
15
+ envPath: ENV_PATH,
16
+ probeTimeoutMs: 3000,
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Operator escape hatch: clears the console login lockout bucket.
22
+ *
23
+ * The lockout is keyed by `<username>:<clientIp>` and lasts an hour, and there
24
+ * is no console surface to clear it — a locked-out operator has no way back in
25
+ * short of editing the database by hand. This is the documented reset.
26
+ *
27
+ * Usage:
28
+ * bun run doctor --reset-lockout <username> <clientIp>
29
+ * bun run doctor --reset-lockout --all
30
+ */
31
+ async function resetLockout(args: readonly string[]): Promise<void> {
32
+ const all = args.includes("--all");
33
+ const positional = args.filter((arg) => !arg.startsWith("--"));
34
+ if (!all && positional.length < 2) {
35
+ console.error("Usage: bun run doctor --reset-lockout <username> <clientIp>");
36
+ console.error(" bun run doctor --reset-lockout --all");
37
+ process.exit(1);
38
+ }
39
+
40
+ const env = await readEnvFile(ENV_PATH);
41
+ const databaseUrl = env.DATABASE_URL ?? process.env.DATABASE_URL;
42
+ if (!databaseUrl) {
43
+ console.error("✗ DATABASE_URL is not set; cannot reach the lockout table.");
44
+ process.exit(1);
45
+ }
46
+ process.env.DATABASE_URL = databaseUrl;
47
+
48
+ const { getDb, closeDb } = await import("../../../src/persistence/boot");
49
+ const { consoleLockouts } = await import("../../../src/persistence/schema");
50
+ const { eq, or } = await import("drizzle-orm");
51
+
52
+ const db = getDb();
53
+ try {
54
+ if (all) {
55
+ const cleared = await db.delete(consoleLockouts).returning({ ip: consoleLockouts.ip });
56
+ console.log(`✓ Cleared ${cleared.length} lockout row(s).`);
57
+ return;
58
+ }
59
+ const username = positional[0]!.trim().toLowerCase();
60
+ const clientIp = positional[1]!;
61
+ // The bucket key is `<identifier>:<ip>`; match either exact spelling so
62
+ // an operator does not have to guess how the identifier was normalized.
63
+ const cleared = await db
64
+ .delete(consoleLockouts)
65
+ .where(or(eq(consoleLockouts.ip, `${username}:${clientIp}`), eq(consoleLockouts.ip, clientIp)))
66
+ .returning({ ip: consoleLockouts.ip });
67
+ if (cleared.length === 0) {
68
+ console.log(`• No lockout row for ${username}:${clientIp} (already clear).`);
69
+ } else {
70
+ console.log(`✓ Cleared lockout for ${cleared.map((row) => row.ip).join(", ")}.`);
71
+ }
72
+ } finally {
73
+ await closeDb();
74
+ }
75
+ }
76
+
77
+
78
+ async function probeReadiness(port: number, timeoutMs: number): Promise<ProbeResult> {
79
+ const controller = new AbortController();
80
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
81
+
82
+ try {
83
+ const response = await fetch(`http://localhost:${port}/health/ready`, {
84
+ signal: controller.signal,
85
+ });
86
+
87
+ clearTimeout(timeoutHandle);
88
+
89
+ if (response.ok) {
90
+ return { success: true };
91
+ }
92
+ return { success: false, error: `HTTP ${response.status}` };
93
+ } catch (err) {
94
+ clearTimeout(timeoutHandle);
95
+ return { success: false, error: String(err) };
96
+ }
97
+ }
98
+
99
+ /** Runs the health check and the lockout reset; the CLI and `bun run doctor` both call this. */
100
+ export async function doctor(): Promise<void> {
101
+ const config = loadConfig();
102
+
103
+ const resetIndex = process.argv.indexOf("--reset-lockout");
104
+ if (resetIndex !== -1) {
105
+ await resetLockout(process.argv.slice(resetIndex + 1));
106
+ return;
107
+ }
108
+
109
+ console.log("🩺 Cartethyia Health Check\n");
110
+ console.log(`🖥️ Host platform: ${getLocalPlatform()}`);
111
+
112
+ // Step 1: Check .env exists
113
+ console.log("📋 Environment Configuration");
114
+ if (!existsSync(config.envPath)) {
115
+ console.log(" ✗ .env file not found");
116
+ console.log(" Run: bun setup\n");
117
+ process.exit(1);
118
+ }
119
+ console.log(" ✓ .env file exists");
120
+
121
+ // Step 2: Check required environment variables. DATABASE_URL is required in
122
+ // full mode only; lite keeps its database in the data dir. Redis is never
123
+ // required — unset means the in-memory backend.
124
+ const envVars = await readEnvFile(config.envPath);
125
+ // `CARTETHYIA_DB_MODE` has no default and throws when absent, which is the
126
+ // right answer at boot but the wrong one here: this command exists to report
127
+ // a broken environment readably, so the missing key joins the checklist
128
+ // instead of crashing before it prints.
129
+ const missing: string[] = [];
130
+ let dbMode: DbMode | null = null;
131
+ try {
132
+ dbMode = resolveDbMode({ ...process.env, ...envVars });
133
+ } catch {
134
+ console.log(" ✗ CARTETHYIA_DB_MODE is not set — run: bun setup");
135
+ // Counted as missing so this command exits non-zero. A boot would throw on
136
+ // the same key, so a green doctor here would be a false all-clear.
137
+ missing.push("CARTETHYIA_DB_MODE");
138
+ }
139
+ const required =
140
+ dbMode === "full"
141
+ ? ["DATABASE_URL", "CARTETHYIA_ENCRYPTION_KEY"]
142
+ : ["CARTETHYIA_ENCRYPTION_KEY"];
143
+
144
+ for (const key of required) {
145
+ const value = envVars[key] ?? process.env[key];
146
+ if (!value) {
147
+ missing.push(key);
148
+ console.log(` ✗ ${key} is not set`);
149
+ } else if (hasPlaceholderSecrets(value)) {
150
+ console.log(` ⚠️ ${key} has placeholder value — update before production`);
151
+ } else {
152
+ console.log(` ✓ ${key} is set`);
153
+ }
154
+ }
155
+
156
+ if (missing.length > 0) {
157
+ console.log(
158
+ `\nFailed: ${missing.length} required environment variable(s) missing or using placeholders\n`,
159
+ );
160
+ process.exit(1);
161
+ }
162
+
163
+ console.log();
164
+
165
+ // Step 3: Check database. Full mode probes the TCP service; lite verifies
166
+ // the data dir is writable (the SQLite backend creates the file at boot too).
167
+ if (dbMode === "lite") {
168
+ const dataDir = resolveDataDir({ ...process.env, ...envVars });
169
+ try {
170
+ mkdirSync(dataDir, { recursive: true });
171
+ accessSync(dataDir, constants.W_OK);
172
+ console.log(` ✓ Lite data dir is writable (${dataDir})`);
173
+ } catch {
174
+ console.log(` ✗ Lite data dir is not writable (${dataDir})`);
175
+ process.exit(1);
176
+ }
177
+ } else {
178
+ const dbUrl = envVars.DATABASE_URL ?? process.env.DATABASE_URL;
179
+ const dbParsed = parseServiceUrl(dbUrl ?? "");
180
+
181
+ if (!dbParsed) {
182
+ console.log(" ✗ Invalid DATABASE_URL format");
183
+ process.exit(1);
184
+ }
185
+
186
+ const dbResult = await probeTcpService(dbParsed.host, dbParsed.port, config.probeTimeoutMs);
187
+ if (dbResult.success) {
188
+ console.log(` ✓ Reachable at ${dbParsed.host}:${dbParsed.port}`);
189
+ } else {
190
+ console.log(` ✗ Not reachable at ${dbParsed.host}:${dbParsed.port}`);
191
+ console.log(` Error: ${dbResult.error || "connection refused"}`);
192
+ for (const hint of getOsHints("postgres")) console.log(` ${hint}`);
193
+ process.exit(1);
194
+ }
195
+ }
196
+
197
+ console.log();
198
+
199
+ // Step 4: Check Redis connectivity — informational only. A configured URL
200
+ // is probed; unset means the in-memory backend, which is always up.
201
+ console.log("🔴 Redis");
202
+ const redisUrl = envVars.REDIS_URL ?? process.env.REDIS_URL;
203
+ if (!redisUrl) {
204
+ console.log(" ✓ in-memory backend (REDIS_URL not set)");
205
+ } else {
206
+ const redisParsed = parseServiceUrl(redisUrl);
207
+ if (!redisParsed) {
208
+ console.log(" ✗ REDIS_URL is invalid");
209
+ process.exit(1);
210
+ }
211
+ const redisResult = await probeTcpService(
212
+ redisParsed.host,
213
+ redisParsed.port,
214
+ config.probeTimeoutMs,
215
+ );
216
+ if (redisResult.success) {
217
+ console.log(` ✓ Reachable at ${redisParsed.host}:${redisParsed.port}`);
218
+ } else {
219
+ console.log(` ✗ Not reachable at ${redisParsed.host}:${redisParsed.port}`);
220
+ console.log(` Error: ${redisResult.error || "connection refused"}`);
221
+ for (const hint of getOsHints("redis")) console.log(` ${hint}`);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ console.log();
227
+
228
+ // Step 5: Check application readiness
229
+ console.log("🏥 Application Readiness");
230
+ // Use the same explicit PORT resolution as the application.
231
+ const port = Number(envVars.PORT ?? process.env.PORT ?? "12800");
232
+ console.log(` Checking http://localhost:${port}/health/ready...`);
233
+
234
+ const readinessResult = await probeReadiness(port, config.probeTimeoutMs);
235
+ if (readinessResult.success) {
236
+ console.log(` ✓ Application is ready`);
237
+ } else {
238
+ console.log(` ✗ Application is not ready`);
239
+ console.log(` Error: ${readinessResult.error || "connection refused"}`);
240
+ console.log("");
241
+ console.log("Make sure the application is running:");
242
+ console.log(` bun run dev`);
243
+ process.exit(1);
244
+ }
245
+
246
+ console.log();
247
+ console.log("✅ All systems operational!");
248
+ }
249
+
250
+ // `bun run doctor` still works: the module is a script when run directly and a
251
+ // command when imported by the CLI.
252
+ if (import.meta.main) {
253
+ doctor().catch((err) => {
254
+ console.error("❌ Health check failed:", err);
255
+ process.exit(1);
256
+ });
257
+ }
@@ -4,23 +4,19 @@ import { existsSync, mkdirSync } from "node:fs";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { createServer } from "node:net";
7
+ import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../support/bun";
7
8
  import {
8
- MINIMUM_BUN_VERSION,
9
- bunVersionAtLeast,
10
- detectLocalPostgres,
9
+ ENV_PATH,
10
+ PROJECT_ROOT,
11
11
  generateCartethyiaEncryptionKey,
12
- getLocalPlatform,
13
- getOsHints,
14
12
  hasPlaceholderSecrets,
15
13
  mandatoryEnvBody,
16
- parseServiceUrl,
17
- probeTcpService,
18
14
  readEnvFile,
19
- ENV_PATH,
20
- PROJECT_ROOT,
21
- } from "../shared/env";
22
- import type { ProbeResult } from "../shared/env";
23
- import { resolveDataDir, resolveDbMode } from "../../src/persistence/db-mode";
15
+ } from "../support/env";
16
+ import { getLocalPlatform, getOsHints } from "../support/platform";
17
+ import { detectLocalPostgres, parseServiceUrl, probeTcpService } from "../support/services";
18
+ import type { ProbeResult } from "../support/services";
19
+ import { resolveDataDir, resolveDbMode } from "../../../src/persistence/db-mode";
24
20
 
25
21
  async function ask(question: string, defaultValue = ""): Promise<string> {
26
22
  const rl = createInterface({ input, output });
@@ -0,0 +1,77 @@
1
+ import { resolvePort } from "../../../src/config/runtime";
2
+ import { ensureDrainToken } from "../gateway/drain-token";
3
+ import { clearInstanceLock, pidAlive, readInstanceLock, writeInstanceLock } from "../gateway/instance-lock";
4
+
5
+ /**
6
+ * Runs the gateway in the foreground, in this process.
7
+ *
8
+ * A foreground run still needs a drain token on disk: the gateway registers
9
+ * `/admin/drain` only when one is set, and "Hide to tray" hands this run to a
10
+ * tray that drains it through that route.
11
+ *
12
+ * Unlike the menu and tray paths this one does not spawn a child, so the
13
+ * port-busy check has to happen here too — otherwise a stale gateway makes a
14
+ * plain `cartethyia start` fail from inside `src/main` with a message that says
15
+ * nothing about what to do.
16
+ */
17
+ export async function startForeground(): Promise<void> {
18
+ const token = ensureDrainToken();
19
+ process.env.CARTETHYIA_DRAIN_TOKEN ??= token;
20
+ const port = resolvePort();
21
+ const blocker = await inspectPortHolder(port, token);
22
+ if (blocker !== null) throw new Error(blocker);
23
+ writeInstanceLock(port, process.pid, token);
24
+ await import("../../../src/main");
25
+ }
26
+
27
+ /**
28
+ * What to tell the operator when the port is already taken, or `null` when the
29
+ * port is free (or was freed).
30
+ *
31
+ * Same rule as the spawned path: only ever stop a gateway that proves it is
32
+ * this install's own. A drain request is token-gated, so a 401 means another
33
+ * Cartethyia install and anything else is not a gateway at all — neither may be
34
+ * touched, and both are reported instead.
35
+ */
36
+ async function inspectPortHolder(port: number, token: string): Promise<string | null> {
37
+ const up = await fetch(`http://127.0.0.1:${port}/health`, {
38
+ signal: AbortSignal.timeout(1_000),
39
+ }).catch(() => null);
40
+ if (up === null) {
41
+ // Port is free. Drop a stale marker from a crashed run so the next start
42
+ // does not read a dead pid as a live gateway.
43
+ const lock = readInstanceLock();
44
+ if (lock !== null && lock.port === port && !pidAlive(lock.pid)) clearInstanceLock();
45
+ return null;
46
+ }
47
+ const response = await fetch(`http://127.0.0.1:${port}/admin/drain`, {
48
+ method: "POST",
49
+ headers: { "x-drain-token": token },
50
+ signal: AbortSignal.timeout(3_000),
51
+ }).catch(() => null);
52
+ if (response === null) {
53
+ return `Port ${port} is busy and nothing there answers a drain request. Stop whatever holds it, or set a different PORT.`;
54
+ }
55
+ if (response.status === 401) {
56
+ return `Another Cartethyia gateway is already running on port ${port} under a different install. Stop it from its own terminal or tray — this install will not stop a gateway it does not own.`;
57
+ }
58
+ if (response.status === 403) {
59
+ return `Port ${port} refused a drain from loopback. Stop what holds it, then start again.`;
60
+ }
61
+ if (response.status !== 202) {
62
+ return `Port ${port} is busy and refused to drain (HTTP ${response.status}). Stop it manually, then start again.`;
63
+ }
64
+ // Drain accepted; wait for the port to actually close.
65
+ const deadline = Date.now() + 30_000;
66
+ while (Date.now() < deadline) {
67
+ const stillUp = await fetch(`http://127.0.0.1:${port}/health`, {
68
+ signal: AbortSignal.timeout(500),
69
+ }).catch(() => null);
70
+ if (stillUp === null) {
71
+ clearInstanceLock();
72
+ return null;
73
+ }
74
+ await Bun.sleep(200);
75
+ }
76
+ return `The gateway on port ${port} accepted a drain but did not release the port within 30 seconds. Stop it manually, then start again.`;
77
+ }