cartethyia 2.5.9 → 2.6.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 (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
package/package.json CHANGED
@@ -1,97 +1,96 @@
1
- {
2
- "name": "cartethyia",
3
- "version": "2.5.9",
4
- "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time \u2014 streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
- "license": "GPL-3.0-only",
6
- "type": "module",
7
- "packageManager": "bun@1.4.0",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/risunCodex/Cartethyia.git"
11
- },
12
- "homepage": "https://github.com/risunCodex/Cartethyia#readme",
13
- "bugs": {
14
- "url": "https://github.com/risunCodex/Cartethyia/issues"
15
- },
16
- "keywords": [
17
- "ai",
18
- "gateway",
19
- "llm",
20
- "bun",
21
- "self-hosted"
22
- ],
23
- "bin": {
24
- "cartethyia": "scripts/cli/bin/cartethyia.cjs",
25
- "carte": "scripts/cli/bin/carte.cjs"
26
- },
27
- "files": [
28
- "scripts/cli/",
29
- "scripts/build/",
30
- "scripts/health.ts",
31
- "src/",
32
- "migrations/",
33
- "dist/dashboard/",
34
- ".env.example",
35
- "README.md"
36
- ],
37
- "workspaces": [
38
- "dashboard"
39
- ],
40
- "engines": {
41
- "bun": ">=1.4.0"
42
- },
43
- "scripts": {
44
- "dev": "bun run dev:stack",
45
- "dev:watch": "bun run scripts/dev/supervisor.ts",
46
- "test": "bun run scripts/test/run.ts",
47
- "build": "bun run scripts/build/build.ts",
48
- "prepack": "bun run dashboard:build",
49
- "setup": "bun run scripts/cli/setup.ts",
50
- "start": "bun run scripts/cli/main.ts start",
51
- "doctor": "bun run scripts/cli/doctor.ts",
52
- "probe:egress": "bun run scripts/tools/probe-egress.ts",
53
- "restart": "bun run scripts/build/restart.ts",
54
- "build:aot": "bun run scripts/build/aot.ts",
55
- "dev:stack": "concurrently -n BACKEND,DASHBOARD -c cyan,magenta --kill-others-on-fail \"bun run dev:backend\" \"bun run dashboard:dev\"",
56
- "typecheck": "tsc --noEmit",
57
- "test:watch": "bun run scripts/test/run.ts --watch",
58
- "test-db:up": "docker compose -f docker-compose.test.yml up -d --wait postgres-test",
59
- "dev:backend": "bun run --hot src/main.ts",
60
- "dev:console-auth": "bun run scripts/test/console-session.ts",
61
- "dev:visual-baseline": "bun run scripts/test/visual-baseline.ts",
62
- "build:binary": "bun run scripts/build/compile.ts",
63
- "test:backend": "bun run scripts/test/run.ts --scope backend",
64
- "test-db:down": "docker compose -f docker-compose.test.yml down",
65
- "dashboard:dev": "bun run --cwd dashboard dev",
66
- "test-db:check": "bun run scripts/test/check-db.ts",
67
- "dashboard:test": "bun run scripts/test/run.ts --scope dashboard",
68
- "test-db:reset": "docker compose -f docker-compose.test.yml down -v && docker compose -f docker-compose.test.yml up -d --wait postgres-test",
69
- "dashboard:build": "bun run --cwd dashboard build",
70
- "dashboard:typecheck": "bun run --cwd dashboard typecheck",
71
- "check:migrations": "bun run scripts/test/check-migrations.ts"
72
- },
73
- "dependencies": {
74
- "drizzle-orm": "0.45.2",
75
- "elysia": "2.0.0-beta.16",
76
- "enquirer": "^2.4.1",
77
- "ioredis": "6.0.0",
78
- "jose": "^6.2.12",
79
- "jsonrepair": "^3.15.0",
80
- "pg": "8.23.0",
81
- "pino": "^10.3.1",
82
- "pino-pretty": "^13.1.3",
83
- "protobufjs": "^7.6.5",
84
- "socks-proxy-agent": "^10.1.0",
85
- "systray2": "2.1.4",
86
- "typebox": "1.3.27",
87
- "ws": "8.22.0"
88
- },
89
- "devDependencies": {
90
- "@types/pg": "8.11.10",
91
- "@types/ws": "8.18.1",
92
- "bun-types": "1.4.0",
93
- "concurrently": "^10.0.5",
94
- "drizzle-kit": "^0.31.11",
95
- "typescript": "5.9.2"
96
- }
97
- }
1
+ {
2
+ "name": "cartethyia",
3
+ "version": "2.6.0",
4
+ "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time \u2014 streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
+ "license": "GPL-3.0-only",
6
+ "type": "module",
7
+ "packageManager": "bun@1.4.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/risunCodex/Cartethyia.git"
11
+ },
12
+ "homepage": "https://github.com/risunCodex/Cartethyia#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/risunCodex/Cartethyia/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "gateway",
19
+ "llm",
20
+ "bun",
21
+ "self-hosted"
22
+ ],
23
+ "bin": {
24
+ "cartethyia": "scripts/cli/bin/cartethyia.cjs",
25
+ "carte": "scripts/cli/bin/carte.cjs"
26
+ },
27
+ "files": [
28
+ "scripts/cli/",
29
+ "src/",
30
+ "migrations/",
31
+ "dist/dashboard/",
32
+ ".env.example",
33
+ "README.md"
34
+ ],
35
+ "workspaces": [
36
+ "dashboard"
37
+ ],
38
+ "engines": {
39
+ "bun": ">=1.4.0"
40
+ },
41
+ "scripts": {
42
+ "dev": "bun run dev:stack",
43
+ "dev:watch": "bun run scripts/dev/supervisor.ts",
44
+ "test": "bun run scripts/test/run.ts",
45
+ "build": "bun run scripts/build/build.ts",
46
+ "prepack": "bun run dashboard:build",
47
+ "setup": "bun run scripts/cli/setup.ts",
48
+ "start": "bun run scripts/cli/main.ts start",
49
+ "doctor": "bun run scripts/cli/doctor.ts",
50
+ "probe:egress": "bun run scripts/tools/probe-egress.ts",
51
+ "restart": "bun run scripts/build/restart.ts",
52
+ "build:aot": "bun run scripts/build/aot.ts",
53
+ "dev:stack": "concurrently -n BACKEND,DASHBOARD -c cyan,magenta --kill-others-on-fail \"bun run dev:backend\" \"bun run dashboard:dev\"",
54
+ "typecheck": "tsc --noEmit",
55
+ "test:watch": "bun run scripts/test/run.ts --watch",
56
+ "test-db:up": "docker compose -f docker-compose.test.yml up -d --wait postgres-test",
57
+ "dev:backend": "bun run --hot src/main.ts",
58
+ "dev:console-auth": "bun run scripts/test/console-session.ts",
59
+ "dev:visual-baseline": "bun run scripts/test/visual-baseline.ts",
60
+ "build:binary": "bun run scripts/build/compile.ts",
61
+ "test:backend": "bun run scripts/test/run.ts --scope backend",
62
+ "test:integration": "bun run scripts/test/run.ts --scope integration",
63
+ "test-db:down": "docker compose -f docker-compose.test.yml down",
64
+ "dashboard:dev": "bun run --cwd dashboard dev",
65
+ "test-db:check": "bun run scripts/test/check-db.ts",
66
+ "dashboard:test": "bun run scripts/test/run.ts --scope dashboard",
67
+ "test-db:reset": "docker compose -f docker-compose.test.yml down -v && docker compose -f docker-compose.test.yml up -d --wait postgres-test",
68
+ "dashboard:build": "bun run --cwd dashboard build",
69
+ "dashboard:typecheck": "bun run --cwd dashboard typecheck",
70
+ "check:migrations": "bun run scripts/test/check-migrations.ts"
71
+ },
72
+ "dependencies": {
73
+ "drizzle-orm": "0.45.2",
74
+ "elysia": "2.0.0-beta.16",
75
+ "enquirer": "^2.4.1",
76
+ "ioredis": "6.0.0",
77
+ "jose": "^6.2.12",
78
+ "jsonrepair": "^3.15.0",
79
+ "pg": "8.23.0",
80
+ "pino": "^10.3.1",
81
+ "pino-pretty": "^13.1.3",
82
+ "protobufjs": "^7.6.5",
83
+ "socks-proxy-agent": "^10.1.0",
84
+ "systray2": "2.1.4",
85
+ "typebox": "1.3.27",
86
+ "ws": "8.22.0"
87
+ },
88
+ "devDependencies": {
89
+ "@types/pg": "8.11.10",
90
+ "@types/ws": "8.18.1",
91
+ "bun-types": "1.4.0",
92
+ "concurrently": "^10.0.5",
93
+ "drizzle-kit": "^0.31.11",
94
+ "typescript": "5.9.2"
95
+ }
96
+ }
@@ -1,125 +1,126 @@
1
- /**
2
- * The two admin actions the menu and the tray both offer.
3
- *
4
- * Both go through the running gateway rather than the database file: the
5
- * gateway holds the connection and the schema is dialect-dependent, so reading
6
- * or writing the tables from this process would need a second implementation
7
- * of both.
8
- */
9
-
10
- import { ENV_PATH, readEnvFile } from "./env";
11
-
12
- /**
13
- * Asks the gateway to write a gzipped config backup into the download
14
- * directory. Returns the written path and counts, or null when it failed.
15
- */
16
- export async function exportBackup(
17
- port: number,
18
- token: string,
19
- ): Promise<{ path: string; bytes: number; counts: Record<string, number> } | null> {
20
- try {
21
- const response = await fetch(`http://127.0.0.1:${port}/admin/export-backup`, {
22
- method: "POST",
23
- headers: { "content-type": "application/json", "x-drain-token": token },
24
- body: JSON.stringify({}),
25
- signal: AbortSignal.timeout(60_000),
26
- });
27
- if (!response.ok) {
28
- const detail = await response.text().catch(() => "");
29
- console.error(`The gateway refused the backup (HTTP ${response.status}). ${detail}`.trim());
30
- return null;
31
- }
32
- return (await response.json()) as {
33
- path: string;
34
- bytes: number;
35
- counts: Record<string, number>;
36
- };
37
- } catch {
38
- console.error("Could not reach the gateway. Is it still running?");
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Posts the console-account reset to the running gateway.
45
- *
46
- * Returns 0 on success and 1 on any failure so the caller can print one line
47
- * either way.
48
- */
49
- export async function resetConsoleAccount(port: number, token: string): Promise<number> {
50
- try {
51
- const response = await fetch(`http://127.0.0.1:${port}/admin/reset-console-account`, {
52
- method: "POST",
53
- headers: { "content-type": "application/json", "x-drain-token": token },
54
- body: JSON.stringify({ confirm: "RESET" }),
55
- signal: AbortSignal.timeout(10_000),
56
- });
57
- if (!response.ok) {
58
- const detail = await response.text().catch(() => "");
59
- console.error(`The gateway refused the reset (HTTP ${response.status}). ${detail}`.trim());
60
- return 1;
61
- }
62
- return 0;
63
- } catch {
64
- console.error("Could not reach the gateway. Is it still running?");
65
- return 1;
66
- }
67
- }
68
-
69
-
70
- /**
71
- * Operator escape hatch: clears the console login lockout bucket.
72
- *
73
- * The lockout is keyed by `<username>:<clientIp>` and lasts an hour, and there
74
- * is no console surface to clear it — a locked-out operator has no way back in
75
- * short of editing the database by hand. This is the documented reset.
76
- *
77
- * Usage:
78
- * cartethyia admin reset-lockout <username> <clientIp>
79
- * cartethyia admin reset-lockout --all
80
- */
81
- export async function resetLockout(args: readonly string[]): Promise<void> {
82
- const all = args.includes("--all");
83
- const positional = args.filter((arg) => !arg.startsWith("--"));
84
- if (!all && positional.length < 2) {
85
- console.error("Usage: cartethyia admin reset-lockout <username> <clientIp>");
86
- console.error(" cartethyia admin reset-lockout --all");
87
- process.exit(1);
88
- }
89
-
90
- const env = await readEnvFile(ENV_PATH);
91
- const databaseUrl = env.DATABASE_URL ?? process.env.DATABASE_URL;
92
- if (!databaseUrl) {
93
- console.error("✗ DATABASE_URL is not set; cannot reach the lockout table.");
94
- process.exit(1);
95
- }
96
- process.env.DATABASE_URL = databaseUrl;
97
-
98
- const { getDb, closeDb } = await import("../../src/persistence/boot");
99
- const { consoleLockouts } = await import("../../src/persistence/schema");
100
- const { eq, or } = await import("drizzle-orm");
101
-
102
- const db = getDb();
103
- try {
104
- if (all) {
105
- const cleared = await db.delete(consoleLockouts).returning({ ip: consoleLockouts.ip });
106
- console.log(`✓ Cleared ${cleared.length} lockout row(s).`);
107
- return;
108
- }
109
- const username = positional[0]!.trim().toLowerCase();
110
- const clientIp = positional[1]!;
111
- // The bucket key is `<identifier>:<ip>`; match either exact spelling so
112
- // an operator does not have to guess how the identifier was normalized.
113
- const cleared = await db
114
- .delete(consoleLockouts)
115
- .where(or(eq(consoleLockouts.ip, `${username}:${clientIp}`), eq(consoleLockouts.ip, clientIp)))
116
- .returning({ ip: consoleLockouts.ip });
117
- if (cleared.length === 0) {
118
- console.log(`• No lockout row for ${username}:${clientIp} (already clear).`);
119
- } else {
120
- console.log(`✓ Cleared lockout for ${cleared.map((row) => row.ip).join(", ")}.`);
121
- }
122
- } finally {
123
- await closeDb();
124
- }
125
- }
1
+ /**
2
+ * The two admin actions the menu and the tray both offer.
3
+ *
4
+ * Both go through the running gateway rather than the database file: the
5
+ * gateway holds the connection and the schema is dialect-dependent, so reading
6
+ * or writing the tables from this process would need a second implementation
7
+ * of both.
8
+ */
9
+
10
+ import { ENV_PATH, readEnvFile } from "./env";
11
+
12
+ /**
13
+ * Asks the gateway to write a gzipped config backup into the download
14
+ * directory. Returns the written path and counts, or null when it failed.
15
+ */
16
+ export async function exportBackup(
17
+ port: number,
18
+ token: string,
19
+ ): Promise<{ path: string; bytes: number; counts: Record<string, number> } | { error: string }> {
20
+ try {
21
+ const response = await fetch(`http://127.0.0.1:${port}/admin/export-backup`, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json", "x-drain-token": token },
24
+ body: JSON.stringify({}),
25
+ signal: AbortSignal.timeout(60_000),
26
+ });
27
+ if (!response.ok) {
28
+ const detail = await response.text().catch(() => "");
29
+ return { error: `The gateway refused the backup (HTTP ${response.status}). ${detail}`.trim() };
30
+ }
31
+ return (await response.json()) as {
32
+ path: string;
33
+ bytes: number;
34
+ counts: Record<string, number>;
35
+ };
36
+ } catch {
37
+ return { error: "Could not reach the gateway. Is it still running?" };
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Posts the console-account reset to the running gateway.
43
+ *
44
+ * Returns 0 on success and 1 on any failure so the caller can print one line
45
+ * either way.
46
+ */
47
+ export async function resetConsoleAccount(port: number, token: string): Promise<number> {
48
+ try {
49
+ const response = await fetch(`http://127.0.0.1:${port}/admin/reset-console-account`, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json", "x-drain-token": token },
52
+ body: JSON.stringify({ confirm: "RESET" }),
53
+ signal: AbortSignal.timeout(10_000),
54
+ });
55
+ if (!response.ok) {
56
+ const detail = await response.text().catch(() => "");
57
+ console.error(`The gateway refused the reset (HTTP ${response.status}). ${detail}`.trim());
58
+ return 1;
59
+ }
60
+ return 0;
61
+ } catch {
62
+ console.error("Could not reach the gateway. Is it still running?");
63
+ return 1;
64
+ }
65
+ }
66
+
67
+
68
+ /**
69
+ * Operator escape hatch: clears the console login lockout bucket.
70
+ *
71
+ * The lockout is keyed by `<username>:<clientIp>` and lasts an hour, and there
72
+ * is no console surface to clear it — a locked-out operator has no way back in
73
+ * short of editing the database by hand. This is the documented reset.
74
+ *
75
+ * Usage:
76
+ * cartethyia admin reset-lockout <username> <clientIp>
77
+ * cartethyia admin reset-lockout --all
78
+ */
79
+ export async function resetLockout(args: readonly string[]): Promise<void> {
80
+ const all = args.includes("--all");
81
+ const positional = args.filter((arg) => !arg.startsWith("--"));
82
+ if (!all && positional.length < 2) {
83
+ console.error("Usage: cartethyia admin reset-lockout <username> <clientIp>");
84
+ console.error(" cartethyia admin reset-lockout --all");
85
+ process.exit(1);
86
+ }
87
+
88
+ const env = await readEnvFile(ENV_PATH);
89
+ const databaseUrl = env.DATABASE_URL ?? process.env.DATABASE_URL;
90
+ if (!databaseUrl) {
91
+ console.error("✗ DATABASE_URL is not set; cannot reach the lockout table.");
92
+ process.exit(1);
93
+ }
94
+ process.env.DATABASE_URL = databaseUrl;
95
+
96
+ // dynamic-import: database driver loaded after DATABASE_URL is published to
97
+ // process.env; static import would initialise the connection pool against
98
+ // whatever (or nothing) is in the environment at module load time.
99
+ const { getDb, closeDb } = await import("../../src/persistence/boot");
100
+ const { consoleLockouts } = await import("../../src/persistence/schema");
101
+ const { eq, or } = await import("drizzle-orm");
102
+
103
+ const db = getDb();
104
+ try {
105
+ if (all) {
106
+ const cleared = await db.delete(consoleLockouts).returning({ ip: consoleLockouts.ip });
107
+ console.log(`✓ Cleared ${cleared.length} lockout row(s).`);
108
+ return;
109
+ }
110
+ const username = positional[0]!.trim().toLowerCase();
111
+ const clientIp = positional[1]!;
112
+ // The bucket key is `<identifier>:<ip>`; match either exact spelling so
113
+ // an operator does not have to guess how the identifier was normalized.
114
+ const cleared = await db
115
+ .delete(consoleLockouts)
116
+ .where(or(eq(consoleLockouts.ip, `${username}:${clientIp}`), eq(consoleLockouts.ip, clientIp)))
117
+ .returning({ ip: consoleLockouts.ip });
118
+ if (cleared.length === 0) {
119
+ console.log(`• No lockout row for ${username}:${clientIp} (already clear).`);
120
+ } else {
121
+ console.log(`✓ Cleared lockout for ${cleared.map((row) => row.ip).join(", ")}.`);
122
+ }
123
+ } finally {
124
+ await closeDb();
125
+ }
126
+ }