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
@@ -1,107 +1,107 @@
1
- # Cartethyia tray icon for Windows using NotifyIcon.
2
- #
3
- # Why PowerShell instead of a Node tray library: `systray2` and friends bundle a
4
- # helper binary and still leave the spawning process attached to a console, so
5
- # Windows Terminal shows an empty tab for the tray's whole lifetime. A
6
- # PowerShell NotifyIcon owns no console of its own when started with
7
- # -WindowStyle Hidden, and the protocol here is plain JSON lines on
8
- # stdin/stdout, so the parent stays in charge.
9
- #
10
- # Protocol: the parent writes one JSON object per line.
11
- # {"action":"add-item","index":0,"title":"...","enabled":true}
12
- # {"action":"update-item","index":0,"title":"...","enabled":true}
13
- # {"action":"set-tooltip","text":"..."}
14
- # {"action":"kill"}
15
- # This script writes one JSON object per line:
16
- # {"type":"started"} {"type":"ready"} {"type":"click","index":0}
17
- param([string]$IconPath, [string]$Tooltip)
18
-
19
- $ErrorActionPreference = "Stop"
20
-
21
- Add-Type -AssemblyName System.Windows.Forms
22
- Add-Type -AssemblyName System.Drawing
23
-
24
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
25
- [Console]::InputEncoding = [System.Text.Encoding]::UTF8
26
- $OutputEncoding = [System.Text.Encoding]::UTF8
27
-
28
- [System.Windows.Forms.Application]::EnableVisualStyles()
29
- [System.Windows.Forms.Application]::SetCompatibleTextRenderingDefault($false)
30
-
31
- $script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
32
- $script:notifyIcon.Icon = New-Object System.Drawing.Icon($IconPath)
33
- $script:notifyIcon.Text = $Tooltip
34
- $script:notifyIcon.Visible = $true
35
-
36
- $script:menu = New-Object System.Windows.Forms.ContextMenuStrip
37
- $script:notifyIcon.ContextMenuStrip = $script:menu
38
- $script:items = @()
39
-
40
- function Write-Event($obj) {
41
- $json = $obj | ConvertTo-Json -Compress
42
- [Console]::Out.WriteLine($json)
43
- [Console]::Out.Flush()
44
- }
45
-
46
- function Add-MenuItem($index, $title, $enabled) {
47
- $item = New-Object System.Windows.Forms.ToolStripMenuItem
48
- $item.Text = $title
49
- $item.Enabled = $enabled
50
- $idx = $index
51
- $item.Add_Click({ Write-Event @{ type = "click"; index = $idx } }.GetNewClosure())
52
- $script:menu.Items.Add($item) | Out-Null
53
- $script:items += $item
54
- }
55
-
56
- # A real ToolStripSeparator draws the divider. Sending a "-" title instead
57
- # renders a literal dash row, which is what the menu showed before.
58
- function Add-MenuSeparator($index) {
59
- $item = New-Object System.Windows.Forms.ToolStripSeparator
60
- $script:menu.Items.Add($item) | Out-Null
61
- $script:items += $item
62
- }
63
-
64
- function Update-MenuItem($index, $title, $enabled) {
65
- if ($index -lt $script:items.Count) {
66
- $script:items[$index].Text = $title
67
- $script:items[$index].Enabled = $enabled
68
- }
69
- }
70
-
71
- function Set-Tooltip($text) {
72
- # NotifyIcon.Text is capped at 63 characters.
73
- if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
74
- $script:notifyIcon.Text = $text
75
- }
76
-
77
- # Stdin is polled from the UI thread: a background thread reading Console.In
78
- # races the message loop and deadlocks when the menu is open.
79
- $script:timer = New-Object System.Windows.Forms.Timer
80
- $script:timer.Interval = 100
81
- $script:timer.Add_Tick({
82
- try {
83
- while ([Console]::In.Peek() -ne -1) {
84
- $line = [Console]::In.ReadLine()
85
- if ([string]::IsNullOrWhiteSpace($line)) { continue }
86
- $cmd = $line | ConvertFrom-Json
87
- switch ($cmd.action) {
88
- "add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
89
- "add-separator" { Add-MenuSeparator $cmd.index }
90
- "update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
91
- "set-tooltip" { Set-Tooltip $cmd.text }
92
- "ready" { Write-Event @{ type = "ready" } }
93
- "kill" {
94
- $script:notifyIcon.Visible = $false
95
- $script:notifyIcon.Dispose()
96
- [System.Windows.Forms.Application]::Exit()
97
- }
98
- }
99
- }
100
- } catch {
101
- Write-Event @{ type = "error"; message = $_.Exception.Message }
102
- }
103
- })
104
- $script:timer.Start()
105
-
106
- Write-Event @{ type = "started" }
107
- [System.Windows.Forms.Application]::Run()
1
+ # Cartethyia tray icon for Windows using NotifyIcon.
2
+ #
3
+ # Why PowerShell instead of a Node tray library: `systray2` and friends bundle a
4
+ # helper binary and still leave the spawning process attached to a console, so
5
+ # Windows Terminal shows an empty tab for the tray's whole lifetime. A
6
+ # PowerShell NotifyIcon owns no console of its own when started with
7
+ # -WindowStyle Hidden, and the protocol here is plain JSON lines on
8
+ # stdin/stdout, so the parent stays in charge.
9
+ #
10
+ # Protocol: the parent writes one JSON object per line.
11
+ # {"action":"add-item","index":0,"title":"...","enabled":true}
12
+ # {"action":"update-item","index":0,"title":"...","enabled":true}
13
+ # {"action":"set-tooltip","text":"..."}
14
+ # {"action":"kill"}
15
+ # This script writes one JSON object per line:
16
+ # {"type":"started"} {"type":"ready"} {"type":"click","index":0}
17
+ param([string]$IconPath, [string]$Tooltip)
18
+
19
+ $ErrorActionPreference = "Stop"
20
+
21
+ Add-Type -AssemblyName System.Windows.Forms
22
+ Add-Type -AssemblyName System.Drawing
23
+
24
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
25
+ [Console]::InputEncoding = [System.Text.Encoding]::UTF8
26
+ $OutputEncoding = [System.Text.Encoding]::UTF8
27
+
28
+ [System.Windows.Forms.Application]::EnableVisualStyles()
29
+ [System.Windows.Forms.Application]::SetCompatibleTextRenderingDefault($false)
30
+
31
+ $script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
32
+ $script:notifyIcon.Icon = New-Object System.Drawing.Icon($IconPath)
33
+ $script:notifyIcon.Text = $Tooltip
34
+ $script:notifyIcon.Visible = $true
35
+
36
+ $script:menu = New-Object System.Windows.Forms.ContextMenuStrip
37
+ $script:notifyIcon.ContextMenuStrip = $script:menu
38
+ $script:items = @()
39
+
40
+ function Write-Event($obj) {
41
+ $json = $obj | ConvertTo-Json -Compress
42
+ [Console]::Out.WriteLine($json)
43
+ [Console]::Out.Flush()
44
+ }
45
+
46
+ function Add-MenuItem($index, $title, $enabled) {
47
+ $item = New-Object System.Windows.Forms.ToolStripMenuItem
48
+ $item.Text = $title
49
+ $item.Enabled = $enabled
50
+ $idx = $index
51
+ $item.Add_Click({ Write-Event @{ type = "click"; index = $idx } }.GetNewClosure())
52
+ $script:menu.Items.Add($item) | Out-Null
53
+ $script:items += $item
54
+ }
55
+
56
+ # A real ToolStripSeparator draws the divider. Sending a "-" title instead
57
+ # renders a literal dash row, which is what the menu showed before.
58
+ function Add-MenuSeparator($index) {
59
+ $item = New-Object System.Windows.Forms.ToolStripSeparator
60
+ $script:menu.Items.Add($item) | Out-Null
61
+ $script:items += $item
62
+ }
63
+
64
+ function Update-MenuItem($index, $title, $enabled) {
65
+ if ($index -lt $script:items.Count) {
66
+ $script:items[$index].Text = $title
67
+ $script:items[$index].Enabled = $enabled
68
+ }
69
+ }
70
+
71
+ function Set-Tooltip($text) {
72
+ # NotifyIcon.Text is capped at 63 characters.
73
+ if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
74
+ $script:notifyIcon.Text = $text
75
+ }
76
+
77
+ # Stdin is polled from the UI thread: a background thread reading Console.In
78
+ # races the message loop and deadlocks when the menu is open.
79
+ $script:timer = New-Object System.Windows.Forms.Timer
80
+ $script:timer.Interval = 100
81
+ $script:timer.Add_Tick({
82
+ try {
83
+ while ([Console]::In.Peek() -ne -1) {
84
+ $line = [Console]::In.ReadLine()
85
+ if ([string]::IsNullOrWhiteSpace($line)) { continue }
86
+ $cmd = $line | ConvertFrom-Json
87
+ switch ($cmd.action) {
88
+ "add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
89
+ "add-separator" { Add-MenuSeparator $cmd.index }
90
+ "update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
91
+ "set-tooltip" { Set-Tooltip $cmd.text }
92
+ "ready" { Write-Event @{ type = "ready" } }
93
+ "kill" {
94
+ $script:notifyIcon.Visible = $false
95
+ $script:notifyIcon.Dispose()
96
+ [System.Windows.Forms.Application]::Exit()
97
+ }
98
+ }
99
+ }
100
+ } catch {
101
+ Write-Event @{ type = "error"; message = $_.Exception.Message }
102
+ }
103
+ })
104
+ $script:timer.Start()
105
+
106
+ Write-Event @{ type = "started" }
107
+ [System.Windows.Forms.Application]::Run()
@@ -1,208 +1,192 @@
1
- import { isNewerRelease } from "../../src/console/updates/version-compare";
2
- import { resolvePackagePath } from "../../src/config/package-paths";
3
- import { resolvePort } from "../../src/config/runtime";
4
- import { ensureDrainToken } from "./instance";
5
- import { startGateway } from "./gateway";
6
- import { stopGateway } from "./gateway";
7
- import { readInstalledVersion, readLatestPublishedVersion } from "./version";
8
- import { reportTrayStartup } from "./startup-signal";
9
- import { createTray } from "./tray/index";
10
- import { openUrl } from "./background";
11
- import { exportBackup, resetConsoleAccount } from "./admin";
12
- import { printUpdateInstructions } from "./update";
13
-
14
- /**
15
- * Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
16
- * on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
17
- * an empty console tab open for the tray's whole lifetime, and that window is
18
- * owned by Windows Terminal — not by this process, so it cannot be found and
19
- * hidden from inside.
20
- *
21
- * The gateway it starts is detached, which is what makes the health poll below
22
- * necessary — see {@link TRAY_HEALTH_POLL_MS}.
23
- */
24
-
25
- /**
26
- * How often the tray asks whether the gateway is still answering.
27
- *
28
- * 10 s: often enough that an operator who stops the gateway by hand sees the
29
- * icon go away, and far below the cost of a request. A detached gateway can no
30
- * longer be waited on, so this poll is the only signal left.
31
- */
32
- const TRAY_HEALTH_POLL_MS = 10_000;
33
-
34
- /**
35
- * Consecutive misses to forgive before a dead gateway closes the tray.
36
- *
37
- * One refusal is not a stopped gateway — a probe can lose a race with startup
38
- * or a slow request — and closing the icon on one would make startup feel
39
- * random.
40
- */
41
- const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
42
-
43
- export async function startTray(): Promise<void> {
44
- const port = resolvePort();
45
- const drainToken = ensureDrainToken();
46
- const dashboardUrl = `http://127.0.0.1:${port}/console`;
47
- const repositoryUrl = "https://github.com/risunCode/cartethyia";
48
- const currentVersion = await readInstalledVersion();
49
-
50
- let gateway: Bun.Subprocess | null = null;
51
- let closing = false;
52
- let busy = false;
53
- let resetArmed = false;
54
- let latestVersion: string | null = null;
55
- let closePromise: Promise<void> | undefined;
56
-
57
- const tray = createTray({
58
- iconPath: resolvePackagePath(
59
- "scripts",
60
- "cli",
61
- "tray",
62
- "assets",
63
- process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
64
- ),
65
- tooltip: `Cartethyia ${currentVersion}`,
66
- items: [
67
- { kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: true },
68
- { kind: "separator" },
69
- { kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
70
- { kind: "action", id: "reset", title: "Reset console password" },
71
- { kind: "action", id: "backup", title: "Backup" },
72
- { kind: "separator" },
73
- { kind: "action", id: "exit", title: "Exit" },
74
- ],
75
- onSelect: (id: string) => void handleSelect(id),
76
- onExit: () => void close(1),
77
- });
78
-
79
- const close = (exitCode = 0): Promise<void> => {
80
- if (closePromise) return closePromise;
81
- closing = true;
82
- closePromise = (async () => {
83
- const current: Bun.Subprocess | null = gateway;
84
- gateway = null;
85
- if (current) await stopGateway(current, port, drainToken);
86
- await tray.close();
87
- process.exitCode = exitCode;
88
- })();
89
- return closePromise;
90
- };
91
-
92
- const refreshVersion = async (): Promise<void> => {
93
- const fetched = await readLatestPublishedVersion();
94
- if (closing) return;
95
- latestVersion = fetched;
96
- if (fetched === null) {
97
- tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
98
- return;
99
- }
100
- const newer = isNewerRelease(currentVersion, fetched);
101
- tray.update(
102
- "update",
103
- newer
104
- ? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
105
- : `Cartethyia ${currentVersion} \u00b7 up to date`,
106
- // Always clickable: the row opens the repository, and only offers the
107
- // update command when there is something newer.
108
- true,
109
- );
110
- };
111
-
112
- const runUpdate = async (): Promise<void> => {
113
- if (busy) return;
114
- busy = true;
115
- const latest = await readLatestPublishedVersion();
116
- await close();
117
- printUpdateInstructions(currentVersion, latest);
118
- // The tray has no terminal of its own, so these instructions are only
119
- // visible when it was started from one. Hold that terminal open long
120
- // enough to copy the command, then exit.
121
- await new Promise<void>((resolve) => setTimeout(resolve, 180_000));
122
- process.exit(0);
123
- };
124
-
125
- async function handleSelect(id: string): Promise<void> {
126
- if (closing) return;
127
- if (id === "exit") return void (await close());
128
- if (id === "console") return void (await openUrl(dashboardUrl));
129
- if (id === "backup") {
130
- const result = await exportBackup(port, drainToken);
131
- if (result !== null) {
132
- console.log(
133
- `Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
134
- );
135
- }
136
- return;
137
- }
138
- if (id === "reset") {
139
- // A tray click cannot ask for the typed confirmation the menu uses, so
140
- // the first click arms the action and the second one runs it.
141
- if (!resetArmed) {
142
- resetArmed = true;
143
- tray.update("reset", "Reset console password \u00b7 click again to delete", true);
144
- return;
145
- }
146
- resetArmed = false;
147
- tray.update("reset", "Reset console password", true);
148
- await resetConsoleAccount(port, drainToken);
149
- return;
150
- }
151
- if (id === "update") {
152
- // Nothing to install: the row is the way to the repository instead.
153
- if (latestVersion === null || !isNewerRelease(currentVersion, latestVersion)) {
154
- await openUrl(repositoryUrl);
155
- return;
156
- }
157
- await runUpdate();
158
- }
159
- }
160
-
161
- process.once("SIGINT", () => void close());
162
- process.once("SIGTERM", () => void close());
163
-
164
- try {
165
- await tray.ready();
166
- // Detached, unlike every other caller: the gateway must outlive this
167
- // process. Attached, it dies with the tray — and the tray dies with the
168
- // terminal that launched it, so closing that terminal took the gateway
169
- // down too and "Hide to tray" left nothing serving. `stopGateway` drains
170
- // over HTTP before it ever reaches for `kill()`, so detaching costs the
171
- // exit path nothing: the tray can still stop the gateway on Exit.
172
- gateway = await startGateway(port, drainToken, { detached: true });
173
- // The launcher that spawned this tray is blocked on a signal file: it
174
- // cannot read our stdio, because the tray is detached. Without this write
175
- // it waits out its 60 s deadline and reports a failure for a tray that is
176
- // running fine.
177
- reportTrayStartup({ status: "ready", url: dashboardUrl });
178
- console.log(`Cartethyia is running at ${dashboardUrl}`);
179
- void refreshVersion();
180
-
181
- let misses = 0;
182
- // A detached child's `exited` never settles on Windows, so the old
183
- // `await current.exited` would have parked here forever and the tray would
184
- // never notice the gateway going away. Poll its health endpoint instead:
185
- // that answers for a process we no longer own, which is the whole point of
186
- // detaching it. The probe is the same one startup used, so a gateway that
187
- // is up reads as up regardless of who spawned it.
188
- while (!closing) {
189
- await Bun.sleep(TRAY_HEALTH_POLL_MS);
190
- if (closing || gateway === null) continue;
191
- const alive = await fetch(`http://127.0.0.1:${port}/health`, {
192
- signal: AbortSignal.timeout(2_000),
193
- }).then((response) => response.ok).catch(() => false);
194
- if (alive) {
195
- misses = 0;
196
- continue;
197
- }
198
- misses += 1;
199
- if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
200
- gateway = null;
201
- console.error("Cartethyia stopped answering; the tray is closing.");
202
- await close(1);
203
- }
204
- } catch (error) {
205
- await close(1);
206
- throw error;
207
- }
208
- }
1
+ import { resolvePackagePath } from "../../src/config/package-paths";
2
+ import { resolvePort } from "../../src/config/runtime";
3
+ import { ensureDrainToken, attachGateway, stopAttachedGateway, type RunningGateway } from "./instance";
4
+ import { startGateway } from "./gateway";
5
+ import { stopGateway } from "./gateway";
6
+ import { readInstalledVersion } from "./version";
7
+ import { reportTrayStartup } from "./startup-signal";
8
+ import { createTray } from "./tray/index";
9
+ import { openUrl } from "./background";
10
+ import { exportBackup, resetConsoleAccount } from "./admin";
11
+ import { acquireController, inheritedControllerHandoff } from "./controller";
12
+ import { restoreControllerAfterFailedHandoff } from "./controller";
13
+
14
+ /**
15
+ * Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
16
+ * on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
17
+ * an empty console tab open for the tray's whole lifetime, and that window is
18
+ * owned by Windows Terminal — not by this process, so it cannot be found and
19
+ * hidden from inside.
20
+ *
21
+ * The gateway it starts is detached, which is what makes the health poll below
22
+ * necessary — see {@link TRAY_HEALTH_POLL_MS}.
23
+ */
24
+
25
+ /**
26
+ * How often the tray asks whether the gateway is still answering.
27
+ *
28
+ * 10 s: often enough that an operator who stops the gateway by hand sees the
29
+ * icon go away, and far below the cost of a request. A detached gateway can no
30
+ * longer be waited on, so this poll is the only signal left.
31
+ */
32
+ const TRAY_HEALTH_POLL_MS = 10_000;
33
+
34
+ /**
35
+ * Consecutive misses to forgive before a dead gateway closes the tray.
36
+ *
37
+ * One refusal is not a stopped gateway — a probe can lose a race with startup
38
+ * or a slow request — and closing the icon on one would make startup feel
39
+ * random.
40
+ */
41
+ const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
42
+
43
+ export async function startTray(): Promise<void> {
44
+ const handoff = inheritedControllerHandoff();
45
+ const controller = acquireController("tray", handoff);
46
+ const releaseController = (): void => controller.release();
47
+ const failedHandoff = (): void => {
48
+ if (handoff) restoreControllerAfterFailedHandoff(handoff);
49
+ };
50
+ process.once("exit", failedHandoff);
51
+ process.once("exit", releaseController);
52
+ try {
53
+ await runTray();
54
+ } finally {
55
+ process.removeListener("exit", failedHandoff);
56
+ controller.release();
57
+ process.removeListener("exit", releaseController);
58
+ }
59
+ }
60
+
61
+ async function runTray(): Promise<void> {
62
+ const port = resolvePort();
63
+ const drainToken = ensureDrainToken();
64
+ const dashboardUrl = `http://127.0.0.1:${port}/console`;
65
+ const releasesUrl = "https://github.com/risunCodex/Cartethyia/releases";
66
+ const currentVersion = await readInstalledVersion();
67
+
68
+ let gateway: Bun.Subprocess | null = null;
69
+ let adoptedGateway: RunningGateway | null = null;
70
+ let closing = false;
71
+ let resetArmed = false;
72
+ let closePromise: Promise<void> | undefined;
73
+
74
+ const tray = createTray({
75
+ iconPath: resolvePackagePath(
76
+ "scripts",
77
+ "cli",
78
+ "tray",
79
+ "assets",
80
+ process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
81
+ ),
82
+ tooltip: `Cartethyia ${currentVersion}`,
83
+ items: [
84
+ // Version row is informational; clicking it opens the releases page so
85
+ // the operator can decide for themselves. No background check: the tray
86
+ // used to poll the registry on open and the result was a cue the user
87
+ // did not ask for.
88
+ { kind: "action", id: "version", title: `Cartethyia ${currentVersion}`, enabled: true },
89
+ { kind: "separator" },
90
+ { kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
91
+ { kind: "action", id: "reset", title: "Reset console password" },
92
+ { kind: "action", id: "backup", title: "Backup" },
93
+ { kind: "separator" },
94
+ { kind: "action", id: "exit", title: "Exit" },
95
+ ],
96
+ onSelect: (id: string) => void handleSelect(id),
97
+ onExit: () => void close(1),
98
+ });
99
+
100
+ const close = (exitCode = 0): Promise<void> => {
101
+ if (closePromise) return closePromise;
102
+ closing = true;
103
+ closePromise = (async () => {
104
+ const current: Bun.Subprocess | null = gateway;
105
+ gateway = null;
106
+ if (current) await stopGateway(current, port, drainToken);
107
+ else if (adoptedGateway !== null) await stopAttachedGateway(adoptedGateway, drainToken);
108
+ await tray.close();
109
+ process.exitCode = exitCode;
110
+ })();
111
+ return closePromise;
112
+ };
113
+
114
+ async function handleSelect(id: string): Promise<void> {
115
+ if (closing) return;
116
+ if (id === "exit") return void (await close());
117
+ if (id === "console") return void (await openUrl(dashboardUrl));
118
+ if (id === "version") return void (await openUrl(releasesUrl));
119
+ if (id === "backup") {
120
+ const result = await exportBackup(port, drainToken);
121
+ if ("error" in result) {
122
+ console.error(result.error);
123
+ } else {
124
+ console.log(
125
+ `Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
126
+ );
127
+ }
128
+ return;
129
+ }
130
+ if (id === "reset") {
131
+ // A tray click cannot ask for the typed confirmation the menu uses, so
132
+ // the first click arms the action and the second one runs it.
133
+ if (!resetArmed) {
134
+ resetArmed = true;
135
+ tray.update("reset", "Reset console password \u00b7 click again to delete", true);
136
+ return;
137
+ }
138
+ resetArmed = false;
139
+ tray.update("reset", "Reset console password", true);
140
+ await resetConsoleAccount(port, drainToken);
141
+ return;
142
+ }
143
+ }
144
+
145
+ process.once("SIGINT", () => void close());
146
+ process.once("SIGTERM", () => void close());
147
+
148
+ try {
149
+ await tray.ready();
150
+ // Detached, unlike every other caller: the gateway must outlive this
151
+ // process. Attached, it dies with the tray — and the tray dies with the
152
+ // terminal that launched it, so closing that terminal took the gateway
153
+ // down too and "Hide to tray" left nothing serving. `stopGateway` drains
154
+ // over HTTP before it ever reaches for `kill()`, so detaching costs the
155
+ // exit path nothing: the tray can still stop the gateway on Exit.
156
+ adoptedGateway = await attachGateway(port, drainToken);
157
+ if (adoptedGateway === null) gateway = await startGateway(port, drainToken, { detached: true });
158
+ // The launcher that spawned this tray is blocked on a signal file: it
159
+ // cannot read our stdio, because the tray is detached. Without this write
160
+ // it waits out its 60 s deadline and reports a failure for a tray that is
161
+ // running fine.
162
+ reportTrayStartup({ status: "ready", url: dashboardUrl });
163
+ console.log(`Cartethyia is running at ${dashboardUrl}`);
164
+
165
+ let misses = 0;
166
+ // A detached child's `exited` never settles on Windows, so the old
167
+ // `await current.exited` would have parked here forever and the tray would
168
+ // never notice the gateway going away. Poll its health endpoint instead:
169
+ // that answers for a process we no longer own, which is the whole point of
170
+ // detaching it. The probe is the same one startup used, so a gateway that
171
+ // is up reads as up regardless of who spawned it.
172
+ while (!closing) {
173
+ await Bun.sleep(TRAY_HEALTH_POLL_MS);
174
+ if (closing || gateway === null && adoptedGateway === null) continue;
175
+ const alive = await fetch(`http://127.0.0.1:${port}/health`, {
176
+ signal: AbortSignal.timeout(2_000),
177
+ }).then((response) => response.ok).catch(() => false);
178
+ if (alive) {
179
+ misses = 0;
180
+ continue;
181
+ }
182
+ misses += 1;
183
+ if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
184
+ gateway = null;
185
+ console.error("Cartethyia stopped answering; the tray is closing.");
186
+ await close(1);
187
+ }
188
+ } catch (error) {
189
+ await close(1);
190
+ throw error;
191
+ }
192
+ }