cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -3,8 +3,7 @@ import { stdin as input, stdout as output } from "node:process";
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { dirname, resolve } from "node:path";
6
- import { createServer } from "node:net";
7
- import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../support/bun";
6
+ import { readRequiredBunVersion, bunVersionAtLeast } from "./version";
8
7
  import {
9
8
  ENV_PATH,
10
9
  PROJECT_ROOT,
@@ -12,11 +11,12 @@ import {
12
11
  hasPlaceholderSecrets,
13
12
  mandatoryEnvBody,
14
13
  readEnvFile,
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";
14
+ } from "./env";
15
+ import { getLocalPlatform, getOsHints } from "./platform";
16
+ import { detectLocalPostgres, parseServiceUrl, probeTcpService } from "./services";
17
+ import type { ProbeResult } from "./services";
18
+ import { blockedPortMessage, probeListenPort } from "./port";
19
+ import { resolveDataDir, resolveDbMode } from "../../src/persistence/db-mode";
20
20
 
21
21
  async function ask(question: string, defaultValue = ""): Promise<string> {
22
22
  const rl = createInterface({ input, output });
@@ -145,11 +145,12 @@ function setupCommand(): string {
145
145
  * Refuses to continue on a Bun older than the repository declares. Printed as
146
146
  * one sentence: a stack trace here tells a first-time installer nothing.
147
147
  */
148
- function assertBunVersion(): void {
148
+ async function assertLocalBunVersion(): Promise<void> {
149
149
  const actual = Bun.version;
150
- if (bunVersionAtLeast(actual, MINIMUM_BUN_VERSION)) return;
150
+ const minimum = await readRequiredBunVersion();
151
+ if (bunVersionAtLeast(actual, minimum)) return;
151
152
  throw new Error(
152
- `Bun ${MINIMUM_BUN_VERSION} or newer is required (this is ${actual}). Install it from https://bun.sh and run this again.`,
153
+ `Bun ${minimum} or newer is required (this is ${actual}). Install it from https://bun.sh and run this again.`,
153
154
  );
154
155
  }
155
156
 
@@ -179,25 +180,8 @@ async function chooseDbMode(envExisted: boolean): Promise<void> {
179
180
  /** Port the example ships with. A VPS NAT often does not forward it. */
180
181
  const DEFAULT_PORT = "12800";
181
182
 
182
- /** Ports this installer suggests when 12800 is taken. */
183
183
  const PORT_SUGGESTIONS: readonly string[] = ["8080", "3000", "8081"];
184
184
 
185
- /**
186
- * True when nothing is listening on the port yet.
187
- *
188
- * Binding is the test, not connecting: only a real bind proves the port is
189
- * free, and the socket is closed immediately so the probe never holds it.
190
- */
191
- function isPortFree(host: string, port: number): Promise<boolean> {
192
- return new Promise((resolve) => {
193
- const server = createServer();
194
- server.once("error", () => resolve(false));
195
- server.listen(port, host, () => {
196
- server.close(() => resolve(true));
197
- });
198
- });
199
- }
200
-
201
185
  /**
202
186
  * Asks which port the gateway should listen on. Enter keeps 12800. The
203
187
  * candidates are probed before the prompt so the operator is told what is
@@ -205,14 +189,21 @@ function isPortFree(host: string, port: number): Promise<boolean> {
205
189
  * production build; the dashboard dev server stays on 5173 and proxies to it.
206
190
  */
207
191
  async function choosePort(envExisted: boolean): Promise<void> {
208
- const alreadyChosen = envExisted && Boolean(process.env.PORT?.trim());
209
- if (alreadyChosen || nonInteractive()) return;
192
+ if (nonInteractive()) return;
193
+ const currentPort = process.env.PORT?.trim() || DEFAULT_PORT;
194
+ if (envExisted) {
195
+ const answer = await ask(`Keep the current port ${currentPort}? yes / no`, "yes");
196
+ if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") return;
197
+ if (answer.toLowerCase() !== "no" && answer.toLowerCase() !== "n") {
198
+ throw new Error(`Port choice must be yes or no (got "${answer}")`);
199
+ }
200
+ }
210
201
 
211
- const candidates = [DEFAULT_PORT, ...PORT_SUGGESTIONS];
202
+ const candidates = [...new Set([currentPort, DEFAULT_PORT, ...PORT_SUGGESTIONS])];
212
203
  const states = await Promise.all(
213
204
  candidates.map(async (port) => ({
214
205
  port,
215
- free: await isPortFree("127.0.0.1", Number(port)),
206
+ free: port === currentPort || (await probeListenPort(Number(port))).status === "free",
216
207
  })),
217
208
  );
218
209
  const free = states.filter((s) => s.free).map((s) => s.port);
@@ -230,10 +221,16 @@ async function choosePort(envExisted: boolean): Promise<void> {
230
221
  if (taken.length > 0) console.log(`In use: ${taken.join(", ")}`);
231
222
  console.log("Pick 8080 (or any open port) when a VPS does not forward 12800.");
232
223
 
233
- const answer = await ask("Port", DEFAULT_PORT);
224
+ const answer = await ask("Port", currentPort);
234
225
  if (!/^[1-9][0-9]{0,4}$/.test(answer) || Number(answer) > 65535) {
235
226
  throw new Error(`Port must be a number from 1 to 65535 (got "${answer}")`);
236
227
  }
228
+ if (answer !== currentPort) {
229
+ const selected = await probeListenPort(Number(answer));
230
+ if (selected.status === "blocked") {
231
+ throw new Error(`${blockedPortMessage(Number(answer))} Port selection was not saved.`);
232
+ }
233
+ }
237
234
  if (!defaultFree && answer === DEFAULT_PORT) {
238
235
  console.log(`⚠ ${DEFAULT_PORT} is in use — starting will fail unless whatever holds it is stopped first.`);
239
236
  }
@@ -302,7 +299,7 @@ async function ensureFullDatabase(timeoutMs: number): Promise<void> {
302
299
 
303
300
  export async function setup(): Promise<void> {
304
301
  console.log("🚀 Setting up Cartethyia...\n");
305
- assertBunVersion();
302
+ await assertLocalBunVersion();
306
303
 
307
304
  // Step 1: Setup the user config file for npm installs, repo .env for dev.
308
305
  const envPath = ENV_PATH;
@@ -323,6 +320,8 @@ export async function setup(): Promise<void> {
323
320
  // Step 2: Load the file, then choose the mode. A fresh file carries the
324
321
  // example default, which is not a choice the operator made, so it is asked.
325
322
  const fileEnv = await readEnvFile(envPath);
323
+ // See main.ts: Bun's automatic PORT default must not mask a saved choice.
324
+ if (process.env.PORT === "12800") delete process.env.PORT;
326
325
  for (const [key, value] of Object.entries(fileEnv)) {
327
326
  if (!process.env[key]) process.env[key] = value;
328
327
  }
@@ -376,6 +375,8 @@ export async function setup(): Promise<void> {
376
375
  console.log("🗂️ Lite mode: embedded SQLite, no PostgreSQL to install");
377
376
  const dataDir = resolveDataDir();
378
377
  mkdirSync(dataDir, { recursive: true });
378
+ mkdirSync(process.env.CARTETHYIA_TELEMETRY_PAYLOAD_DIR?.trim() || resolve(dataDir, "telemetry-payloads"), { recursive: true });
379
+ mkdirSync(process.env.CARTETHYIA_INSTALL_ID_DIR?.trim() || resolve(dataDir, ".cartethyia"), { recursive: true });
379
380
  console.log(`✓ Data directory ready at ${dataDir}\n`);
380
381
  } else {
381
382
  console.log("🐘 Checking PostgreSQL...");
@@ -429,7 +430,7 @@ export async function setup(): Promise<void> {
429
430
  console.log(" Production. Builds the dashboard, AOT manifest, and binary first.");
430
431
  console.log("");
431
432
  console.log(" bun run dev");
432
- console.log(" Development. No build; hot reload, dashboard on 5173, API on");
433
+ console.log(" Development. No build; hot reload, dashboard on 5173 (or 1931 if busy), API on");
433
434
  console.log(` ${port}. Use this while changing source.`);
434
435
  }
435
436
  console.log("");
@@ -1,6 +1,7 @@
1
- import { resolvePort } from "../../../src/config/runtime";
2
- import { ensureDrainToken } from "../gateway/drain-token";
3
- import { clearInstanceLock, pidAlive, readInstanceLock, writeInstanceLock } from "../gateway/instance-lock";
1
+ import { resolvePort } from "../../src/config/runtime";
2
+ import { ensureDrainToken } from "./instance";
3
+ import { clearInstanceLock, pidAlive, readInstanceLock, writeInstanceLock } from "./instance";
4
+ import { blockedPortMessage, listenPortErrorMessage, probeListenPort } from "./port";
4
5
 
5
6
  /**
6
7
  * Runs the gateway in the foreground, in this process.
@@ -18,10 +19,14 @@ export async function startForeground(): Promise<void> {
18
19
  const token = ensureDrainToken();
19
20
  process.env.CARTETHYIA_DRAIN_TOKEN ??= token;
20
21
  const port = resolvePort();
22
+ const probe = await probeListenPort(port);
23
+ if (probe.status === "blocked" && probe.code !== "EADDRINUSE") {
24
+ throw new Error(listenPortErrorMessage(port, probe));
25
+ }
21
26
  const blocker = await inspectPortHolder(port, token);
22
27
  if (blocker !== null) throw new Error(blocker);
23
28
  writeInstanceLock(port, process.pid, token);
24
- await import("../../../src/main");
29
+ await import("../../src/main");
25
30
  }
26
31
 
27
32
  /**
@@ -50,7 +55,7 @@ async function inspectPortHolder(port: number, token: string): Promise<string |
50
55
  signal: AbortSignal.timeout(3_000),
51
56
  }).catch(() => null);
52
57
  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.`;
58
+ return blockedPortMessage(port);
54
59
  }
55
60
  if (response.status === 401) {
56
61
  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.`;
@@ -1,23 +1,23 @@
1
- import { writeFileSync } from "node:fs";
2
-
3
- /**
4
- * Reports tray readiness to the launcher that is waiting on a signal file.
5
- *
6
- * The tray is spawned detached with no stdio, so the launcher cannot read its
7
- * output; a one-shot file is the only channel that survives the parent exiting.
8
- * `flag: "wx"` makes the write fail rather than overwrite, which keeps a late
9
- * report from replacing the failure the launcher already acted on.
10
- */
11
- export type TrayStartupSignal =
12
- | { readonly status: "ready"; readonly url: string }
13
- | { readonly status: "failed"; readonly message: string };
14
-
15
- export function reportTrayStartup(signal: TrayStartupSignal): void {
16
- const signalPath = process.env.CARTETHYIA_TRAY_STARTUP_SIGNAL;
17
- if (!signalPath) return;
18
- try {
19
- writeFileSync(signalPath, JSON.stringify(signal), { encoding: "utf8", flag: "wx" });
20
- } catch {
21
- // The launcher may have timed out and removed the one-shot signal file.
22
- }
23
- }
1
+ import { writeFileSync } from "node:fs";
2
+
3
+ /**
4
+ * Reports tray readiness to the launcher that is waiting on a signal file.
5
+ *
6
+ * The tray is spawned detached with no stdio, so the launcher cannot read its
7
+ * output; a one-shot file is the only channel that survives the parent exiting.
8
+ * `flag: "wx"` makes the write fail rather than overwrite, which keeps a late
9
+ * report from replacing the failure the launcher already acted on.
10
+ */
11
+ export type TrayStartupSignal =
12
+ | { readonly status: "ready"; readonly url: string }
13
+ | { readonly status: "failed"; readonly message: string };
14
+
15
+ export function reportTrayStartup(signal: TrayStartupSignal): void {
16
+ const signalPath = process.env.CARTETHYIA_TRAY_STARTUP_SIGNAL;
17
+ if (!signalPath) return;
18
+ try {
19
+ writeFileSync(signalPath, JSON.stringify(signal), { encoding: "utf8", flag: "wx" });
20
+ } catch {
21
+ // The launcher may have timed out and removed the one-shot signal file.
22
+ }
23
+ }
@@ -1,8 +1,15 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
- import SysTray from "systray2";
3
+ import SysTray, { type MenuItem } from "systray2";
4
4
  import { resolvePackagePath } from "../../../src/config/package-paths";
5
5
 
6
+ /**
7
+ * A row as systray2 holds it. The helper numbers the rows itself and reports
8
+ * that number back on a click and on an update, but it is absent from the
9
+ * package's typings.
10
+ */
11
+ type IdentifiedItem = MenuItem & { readonly __id?: number };
12
+
6
13
  /**
7
14
  * One tray row. `action` rows report their id on click; separators are drawn
8
15
  * but never fire.
@@ -162,33 +169,69 @@ class WindowsTray implements Tray {
162
169
  /**
163
170
  * macOS and Linux tray: `systray2` runs its own helper, and neither platform
164
171
  * attaches a console window to it, so the bundled tray is used as-is.
172
+ *
173
+ * Clicks arrive only through `onClick`; a menu built without a listener shows
174
+ * every row and reports none of them, which is why the callbacks are wired
175
+ * here rather than left to the default.
165
176
  */
166
177
  class UnixTray implements Tray {
167
178
  readonly #tray: SysTray;
179
+ readonly #onSelect: (id: string) => void;
180
+ readonly #onExit: () => void;
181
+ /** Row objects kept by reference: a click hands back the object, not the id. */
182
+ readonly #items = new Map<string, IdentifiedItem>();
183
+ #closed = false;
168
184
 
169
185
  constructor(options: TrayOptions) {
186
+ this.#onSelect = options.onSelect;
187
+ this.#onExit = options.onExit;
188
+ const items = options.items.map((item) => {
189
+ if (item.kind === "separator") return SysTray.separator;
190
+ const menuItem: IdentifiedItem = {
191
+ title: item.title,
192
+ tooltip: item.title,
193
+ enabled: item.enabled ?? true,
194
+ };
195
+ this.#items.set(item.id, menuItem);
196
+ return menuItem;
197
+ });
170
198
  this.#tray = new SysTray({
171
199
  menu: {
172
200
  icon: options.iconPath,
173
201
  title: "Cartethyia",
174
202
  tooltip: options.tooltip,
175
- items: options.items.map((item) =>
176
- item.kind === "separator"
177
- ? SysTray.separator
178
- : { title: item.title, tooltip: item.title, enabled: item.enabled ?? true },
179
- ),
203
+ items,
180
204
  },
181
205
  copyDir: true,
182
206
  });
183
207
  }
184
208
 
185
- ready(): Promise<void> {
186
- return this.#tray.ready().then(() => undefined);
209
+ async ready(): Promise<void> {
210
+ await this.#tray.ready();
211
+ // Registered after `ready()`: the helper process does not exist before
212
+ // that, and `onExit` attaches to it directly.
213
+ this.#tray.onExit(() => {
214
+ if (!this.#closed) this.#onExit();
215
+ });
216
+ await this.#tray.onClick((action) => {
217
+ for (const [id, item] of this.#items) {
218
+ // Identity first: `onClick` merges the clicked row into the object this
219
+ // class built, so a match needs no assumptions about the numbering.
220
+ if (item === action.item || item.__id === action.__id) {
221
+ this.#onSelect(id);
222
+ return;
223
+ }
224
+ }
225
+ });
187
226
  }
188
227
 
189
228
  update(id: string, title: string, enabled = true): void {
190
- void id;
191
- void this.#tray.sendAction({ type: "update-item", item: { title, tooltip: title, enabled } });
229
+ const item = this.#items.get(id);
230
+ // An update sent before the helper reports ready carries no row number, and
231
+ // systray2 then rewrites the first row instead of this one.
232
+ if (item?.__id === undefined) return;
233
+ const next: IdentifiedItem = { title, tooltip: title, enabled, __id: item.__id };
234
+ void this.#tray.sendAction({ type: "update-item", item: next });
192
235
  }
193
236
 
194
237
  setTooltip(text: string): void {
@@ -198,9 +241,16 @@ class UnixTray implements Tray {
198
241
  }
199
242
 
200
243
  async close(): Promise<void> {
201
- if (this.#tray.killed) return;
244
+ if (this.#closed) return;
245
+ this.#closed = true;
246
+ const helper = this.#tray.process;
247
+ // `kill()` settles on the helper's `exit` event, so a helper that already
248
+ // exited would leave it pending forever and hang the shutdown after it.
249
+ if (helper.exitCode !== null || helper.signalCode !== null) return;
202
250
  try {
203
- this.#tray.kill();
251
+ // `false`: systray2 otherwise calls `process.exit(0)` when the helper
252
+ // exits, which discards the exit code this close was given.
253
+ await this.#tray.kill(false);
204
254
  } catch {
205
255
  // The helper may already be gone.
206
256
  }