@wrongstack/cli 0.295.1 → 0.296.3

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 (398) hide show
  1. package/data/providers.json +0 -24
  2. package/dist/acp-A5OSIU2P.js +824 -0
  3. package/dist/acp-A5OSIU2P.js.map +7 -0
  4. package/dist/acp-server-agent.d.ts.map +1 -1
  5. package/dist/arg-parser.d.ts.map +1 -1
  6. package/dist/audit-2ZSFGNIF.js +102 -0
  7. package/dist/audit-2ZSFGNIF.js.map +7 -0
  8. package/dist/auth-CJWQJVRH.js +504 -0
  9. package/dist/auth-CJWQJVRH.js.map +7 -0
  10. package/dist/auth-menu/auth-menu-audit.d.ts +0 -4
  11. package/dist/auth-menu/auth-menu-audit.d.ts.map +1 -1
  12. package/dist/bench-7ZDWSNEF.js +278 -0
  13. package/dist/bench-7ZDWSNEF.js.map +7 -0
  14. package/dist/boot/brain-decision-log.d.ts +7 -5
  15. package/dist/boot/brain-decision-log.d.ts.map +1 -1
  16. package/dist/boot/dispatch-webui.d.ts +5 -8
  17. package/dist/boot/dispatch-webui.d.ts.map +1 -1
  18. package/dist/boot/system-prompt-builder.d.ts +3 -0
  19. package/dist/boot/system-prompt-builder.d.ts.map +1 -1
  20. package/dist/boot/tui-coordinator-callbacks.d.ts +45 -0
  21. package/dist/boot/tui-coordinator-callbacks.d.ts.map +1 -0
  22. package/dist/boot/tui-project-switch.d.ts +2 -3
  23. package/dist/boot/tui-project-switch.d.ts.map +1 -1
  24. package/dist/boot/tui-runtime-state.d.ts +5 -6
  25. package/dist/boot/tui-runtime-state.d.ts.map +1 -1
  26. package/dist/boot/tui-session-resume.d.ts.map +1 -1
  27. package/dist/boot/tui-settings-adapter.d.ts +2 -3
  28. package/dist/boot/tui-settings-adapter.d.ts.map +1 -1
  29. package/dist/boot.d.ts.map +1 -1
  30. package/dist/brain-menu/panel-service.d.ts.map +1 -1
  31. package/dist/chimera-review-task.d.ts +6 -0
  32. package/dist/chimera-review-task.d.ts.map +1 -0
  33. package/dist/chimera-reviewer-policy.d.ts +34 -0
  34. package/dist/chimera-reviewer-policy.d.ts.map +1 -0
  35. package/dist/chronicle-QQEQFSSE.js +256 -0
  36. package/dist/chronicle-QQEQFSSE.js.map +7 -0
  37. package/dist/chunk-5N34EWJR.js +418 -0
  38. package/dist/chunk-5N34EWJR.js.map +7 -0
  39. package/dist/chunk-66T43Q4Y.js +90 -0
  40. package/dist/chunk-66T43Q4Y.js.map +7 -0
  41. package/dist/chunk-7OCVIDC7.js +11 -0
  42. package/dist/chunk-7OCVIDC7.js.map +7 -0
  43. package/dist/chunk-ABXK4VVY.js +84 -0
  44. package/dist/chunk-ABXK4VVY.js.map +7 -0
  45. package/dist/chunk-AXVZMEWG.js +306 -0
  46. package/dist/chunk-AXVZMEWG.js.map +7 -0
  47. package/dist/chunk-AZESNNIC.js +216 -0
  48. package/dist/chunk-AZESNNIC.js.map +7 -0
  49. package/dist/chunk-BGFM3PD7.js +41 -0
  50. package/dist/chunk-BGFM3PD7.js.map +7 -0
  51. package/dist/chunk-BQCAJ7FI.js +425 -0
  52. package/dist/chunk-BQCAJ7FI.js.map +7 -0
  53. package/dist/chunk-DPJZQCVP.js +13 -0
  54. package/dist/chunk-DPJZQCVP.js.map +7 -0
  55. package/dist/chunk-F4PEGAJZ.js +176 -0
  56. package/dist/chunk-F4PEGAJZ.js.map +7 -0
  57. package/dist/chunk-IHBYBYGA.js +443 -0
  58. package/dist/chunk-IHBYBYGA.js.map +7 -0
  59. package/dist/chunk-JBSQ7ONN.js +1023 -0
  60. package/dist/chunk-JBSQ7ONN.js.map +7 -0
  61. package/dist/chunk-JBTFWWKV.js +1325 -0
  62. package/dist/chunk-JBTFWWKV.js.map +7 -0
  63. package/dist/chunk-JUHRI6OV.js +100 -0
  64. package/dist/chunk-JUHRI6OV.js.map +7 -0
  65. package/dist/chunk-OI7GWJDS.js +3303 -0
  66. package/dist/chunk-OI7GWJDS.js.map +7 -0
  67. package/dist/chunk-OP47B3IX.js +142 -0
  68. package/dist/chunk-OP47B3IX.js.map +7 -0
  69. package/dist/chunk-PJHET45A.js +158 -0
  70. package/dist/chunk-PJHET45A.js.map +7 -0
  71. package/dist/chunk-Q5GTM25S.js +17 -0
  72. package/dist/chunk-Q5GTM25S.js.map +7 -0
  73. package/dist/chunk-QD544J2B.js +19 -0
  74. package/dist/chunk-QD544J2B.js.map +7 -0
  75. package/dist/chunk-QUNHS6O3.js +258 -0
  76. package/dist/chunk-QUNHS6O3.js.map +7 -0
  77. package/dist/chunk-TADVX644.js +480 -0
  78. package/dist/chunk-TADVX644.js.map +7 -0
  79. package/dist/chunk-TGDHN4LM.js +81 -0
  80. package/dist/chunk-TGDHN4LM.js.map +7 -0
  81. package/dist/chunk-TYO2OVAD.js +50 -0
  82. package/dist/chunk-TYO2OVAD.js.map +7 -0
  83. package/dist/chunk-VCYZ3WZV.js +14 -0
  84. package/dist/chunk-VCYZ3WZV.js.map +7 -0
  85. package/dist/chunk-VJUEZZYB.js +29 -0
  86. package/dist/chunk-VJUEZZYB.js.map +7 -0
  87. package/dist/chunk-WHOIR577.js +1 -0
  88. package/dist/chunk-WHOIR577.js.map +7 -0
  89. package/dist/chunk-WY7KKOUK.js +460 -0
  90. package/dist/chunk-WY7KKOUK.js.map +7 -0
  91. package/dist/chunk-XJXDOF63.js +28 -0
  92. package/dist/chunk-XJXDOF63.js.map +7 -0
  93. package/dist/chunk-XWEGWYDU.js +560 -0
  94. package/dist/chunk-XWEGWYDU.js.map +7 -0
  95. package/dist/chunk-YWUPHRGB.js +346 -0
  96. package/dist/chunk-YWUPHRGB.js.map +7 -0
  97. package/dist/chunk-Z7YGASQP.js +313 -0
  98. package/dist/chunk-Z7YGASQP.js.map +7 -0
  99. package/dist/cli-entry-main.d.ts +2 -0
  100. package/dist/cli-entry-main.d.ts.map +1 -0
  101. package/dist/cli-entry-point.d.ts +3 -3
  102. package/dist/cli-entry-point.d.ts.map +1 -1
  103. package/dist/cli-main-RTY35OPD.js +29268 -0
  104. package/dist/cli-main-RTY35OPD.js.map +7 -0
  105. package/dist/cli-main-helpers.d.ts +21 -0
  106. package/dist/cli-main-helpers.d.ts.map +1 -0
  107. package/dist/cli-main.d.ts +25 -1
  108. package/dist/cli-main.d.ts.map +1 -1
  109. package/dist/context-limit.d.ts +48 -0
  110. package/dist/context-limit.d.ts.map +1 -1
  111. package/dist/diag-doctor-OQ3AA63O.js +170 -0
  112. package/dist/diag-doctor-OQ3AA63O.js.map +7 -0
  113. package/dist/execute-deps.d.ts +11 -12
  114. package/dist/execute-deps.d.ts.map +1 -1
  115. package/dist/execution-O7I5DYE6.js +4682 -0
  116. package/dist/execution-O7I5DYE6.js.map +7 -0
  117. package/dist/execution-chimera-ask.d.ts +10 -0
  118. package/dist/execution-chimera-ask.d.ts.map +1 -0
  119. package/dist/execution-chimera-cascade.d.ts +15 -0
  120. package/dist/execution-chimera-cascade.d.ts.map +1 -0
  121. package/dist/execution-cleanup.d.ts +23 -0
  122. package/dist/execution-cleanup.d.ts.map +1 -0
  123. package/dist/execution-kanban-dispatch.d.ts +10 -0
  124. package/dist/execution-kanban-dispatch.d.ts.map +1 -0
  125. package/dist/execution-repl-fleet-callbacks.d.ts +11 -0
  126. package/dist/execution-repl-fleet-callbacks.d.ts.map +1 -0
  127. package/dist/execution-storage-observability.d.ts +3 -0
  128. package/dist/execution-storage-observability.d.ts.map +1 -0
  129. package/dist/execution-tui-next-step-callbacks.d.ts +20 -0
  130. package/dist/execution-tui-next-step-callbacks.d.ts.map +1 -0
  131. package/dist/execution.d.ts +2 -139
  132. package/dist/execution.d.ts.map +1 -1
  133. package/dist/export-TPORYVRZ.js +64 -0
  134. package/dist/export-TPORYVRZ.js.map +7 -0
  135. package/dist/fleet/host-acp-runner-cache.d.ts +9 -0
  136. package/dist/fleet/host-acp-runner-cache.d.ts.map +1 -0
  137. package/dist/fleet/host-acp.d.ts +3 -0
  138. package/dist/fleet/host-acp.d.ts.map +1 -0
  139. package/dist/fleet/host-concurrency.d.ts +2 -0
  140. package/dist/fleet/host-concurrency.d.ts.map +1 -0
  141. package/dist/fleet/host-context.d.ts +5 -0
  142. package/dist/fleet/host-context.d.ts.map +1 -0
  143. package/dist/fleet/host-director-builder.d.ts +24 -0
  144. package/dist/fleet/host-director-builder.d.ts.map +1 -0
  145. package/dist/fleet/host-director-event-bridges.d.ts +37 -0
  146. package/dist/fleet/host-director-event-bridges.d.ts.map +1 -0
  147. package/dist/fleet/host-director-services.d.ts +20 -0
  148. package/dist/fleet/host-director-services.d.ts.map +1 -0
  149. package/dist/fleet/host-event-bridge.d.ts +17 -0
  150. package/dist/fleet/host-event-bridge.d.ts.map +1 -0
  151. package/dist/fleet/host-helpers.d.ts +22 -0
  152. package/dist/fleet/host-helpers.d.ts.map +1 -0
  153. package/dist/fleet/host-learning-tracker.d.ts +11 -0
  154. package/dist/fleet/host-learning-tracker.d.ts.map +1 -0
  155. package/dist/fleet/host-learning.d.ts +4 -0
  156. package/dist/fleet/host-learning.d.ts.map +1 -0
  157. package/dist/fleet/host-lifecycle-events.d.ts +4 -0
  158. package/dist/fleet/host-lifecycle-events.d.ts.map +1 -0
  159. package/dist/fleet/host-paths.d.ts +3 -0
  160. package/dist/fleet/host-paths.d.ts.map +1 -0
  161. package/dist/fleet/host-provider.d.ts +22 -0
  162. package/dist/fleet/host-provider.d.ts.map +1 -0
  163. package/dist/fleet/host-session-writer.d.ts +3 -0
  164. package/dist/fleet/host-session-writer.d.ts.map +1 -0
  165. package/dist/fleet/host-shadow-pass.d.ts +21 -0
  166. package/dist/fleet/host-shadow-pass.d.ts.map +1 -0
  167. package/dist/fleet/host-spawn-types.d.ts +25 -0
  168. package/dist/fleet/host-spawn-types.d.ts.map +1 -0
  169. package/dist/fleet/host-status.d.ts +48 -0
  170. package/dist/fleet/host-status.d.ts.map +1 -0
  171. package/dist/fleet/host-subagent-factory.d.ts +16 -0
  172. package/dist/fleet/host-subagent-factory.d.ts.map +1 -0
  173. package/dist/fleet/host-supervisor.d.ts +16 -0
  174. package/dist/fleet/host-supervisor.d.ts.map +1 -0
  175. package/dist/fleet/host-task-result-report.d.ts +11 -0
  176. package/dist/fleet/host-task-result-report.d.ts.map +1 -0
  177. package/dist/fleet/host-types.d.ts +89 -0
  178. package/dist/fleet/host-types.d.ts.map +1 -0
  179. package/dist/fleet/host.d.ts +10 -357
  180. package/dist/fleet/host.d.ts.map +1 -1
  181. package/dist/hq-JUYKUNJX.js +547 -0
  182. package/dist/hq-JUYKUNJX.js.map +7 -0
  183. package/dist/hq-command-controller.d.ts +2 -2
  184. package/dist/hq-command-controller.d.ts.map +1 -1
  185. package/dist/hq-server/auth.d.ts +15 -4
  186. package/dist/hq-server/auth.d.ts.map +1 -1
  187. package/dist/hq-server/project-root.d.ts +5 -0
  188. package/dist/hq-server/project-root.d.ts.map +1 -0
  189. package/dist/hq-server/routes/auth-handlers.d.ts +25 -0
  190. package/dist/hq-server/routes/auth-handlers.d.ts.map +1 -0
  191. package/dist/hq-server/routes/session-handlers.d.ts +8 -0
  192. package/dist/hq-server/routes/session-handlers.d.ts.map +1 -0
  193. package/dist/hq-server/routes.d.ts +8 -6
  194. package/dist/hq-server/routes.d.ts.map +1 -1
  195. package/dist/hq-server/snapshot.d.ts +44 -0
  196. package/dist/hq-server/snapshot.d.ts.map +1 -1
  197. package/dist/hq-server/types.d.ts +22 -4
  198. package/dist/hq-server/types.d.ts.map +1 -1
  199. package/dist/hq-server/utils.d.ts +8 -0
  200. package/dist/hq-server/utils.d.ts.map +1 -1
  201. package/dist/hq-server/ws.d.ts +1 -1
  202. package/dist/hq-server/ws.d.ts.map +1 -1
  203. package/dist/hq-server-6AQ3BXZA.js +39 -0
  204. package/dist/hq-server-6AQ3BXZA.js.map +7 -0
  205. package/dist/hq-server.d.ts +3 -3
  206. package/dist/hq-server.d.ts.map +1 -1
  207. package/dist/hq-ttl-U2FAYFAT.js +8 -0
  208. package/dist/hq-ttl-U2FAYFAT.js.map +7 -0
  209. package/dist/hq-tunnel-RFKGC4HI.js +114 -0
  210. package/dist/hq-tunnel-RFKGC4HI.js.map +7 -0
  211. package/dist/hq-tunnel.d.ts +1 -1
  212. package/dist/hq-tunnel.d.ts.map +1 -1
  213. package/dist/index.d.ts +2 -1
  214. package/dist/index.d.ts.map +1 -1
  215. package/dist/index.js +2503 -53026
  216. package/dist/index.js.map +4 -4
  217. package/dist/init-E2NDDOHI.js +27 -0
  218. package/dist/init-E2NDDOHI.js.map +7 -0
  219. package/dist/kanban-dispatch-route.d.ts +14 -0
  220. package/dist/kanban-dispatch-route.d.ts.map +1 -0
  221. package/dist/kanban-hq-sync.d.ts +2 -0
  222. package/dist/kanban-hq-sync.d.ts.map +1 -1
  223. package/dist/kanban-run-mirror-EKVNGXWW.js +12 -0
  224. package/dist/kanban-run-mirror-EKVNGXWW.js.map +7 -0
  225. package/dist/live-settings-input.d.ts +69 -0
  226. package/dist/live-settings-input.d.ts.map +1 -0
  227. package/dist/mailbox-serve-2UWHEN3M.js +302 -0
  228. package/dist/mailbox-serve-2UWHEN3M.js.map +7 -0
  229. package/dist/mcp-CYLPUEHC.js +413 -0
  230. package/dist/mcp-CYLPUEHC.js.map +7 -0
  231. package/dist/modeldiag-HENYJ4N7.js +1290 -0
  232. package/dist/modeldiag-HENYJ4N7.js.map +7 -0
  233. package/dist/permissions-FNYLZIL4.js +115 -0
  234. package/dist/permissions-FNYLZIL4.js.map +7 -0
  235. package/dist/picker-key-state.d.ts +7 -0
  236. package/dist/picker-key-state.d.ts.map +1 -0
  237. package/dist/picker-model-picker.d.ts +6 -0
  238. package/dist/picker-model-picker.d.ts.map +1 -0
  239. package/dist/picker-ui.d.ts +17 -0
  240. package/dist/picker-ui.d.ts.map +1 -0
  241. package/dist/picker.d.ts +4 -37
  242. package/dist/picker.d.ts.map +1 -1
  243. package/dist/plugin-usage-OU6X6LM3.js +40 -0
  244. package/dist/plugin-usage-OU6X6LM3.js.map +7 -0
  245. package/dist/plugins-4LJ5IUBI.js +24 -0
  246. package/dist/plugins-4LJ5IUBI.js.map +7 -0
  247. package/dist/pre-launch.d.ts +0 -1
  248. package/dist/pre-launch.d.ts.map +1 -1
  249. package/dist/project-BLQ3ALVL.js +76 -0
  250. package/dist/project-BLQ3ALVL.js.map +7 -0
  251. package/dist/project-manifest-GE25PW4S.js +18 -0
  252. package/dist/project-manifest-GE25PW4S.js.map +7 -0
  253. package/dist/project-picker-BOCWHBXP.js +17 -0
  254. package/dist/project-picker-BOCWHBXP.js.map +7 -0
  255. package/dist/projects-XE76NFNZ.js +38 -0
  256. package/dist/projects-XE76NFNZ.js.map +7 -0
  257. package/dist/provider-helpers.d.ts +22 -6
  258. package/dist/provider-helpers.d.ts.map +1 -1
  259. package/dist/providers-models-GDELBHZK.js +589 -0
  260. package/dist/providers-models-GDELBHZK.js.map +7 -0
  261. package/dist/quick-VIO2BEHS.js +10 -0
  262. package/dist/quick-VIO2BEHS.js.map +7 -0
  263. package/dist/repl-auto-proceed.d.ts +19 -0
  264. package/dist/repl-auto-proceed.d.ts.map +1 -0
  265. package/dist/repl-client-registration.d.ts +7 -0
  266. package/dist/repl-client-registration.d.ts.map +1 -0
  267. package/dist/repl-input.d.ts +11 -0
  268. package/dist/repl-input.d.ts.map +1 -0
  269. package/dist/repl-options.d.ts +54 -0
  270. package/dist/repl-options.d.ts.map +1 -0
  271. package/dist/repl-rendering.d.ts +4 -0
  272. package/dist/repl-rendering.d.ts.map +1 -0
  273. package/dist/repl-suggestions.d.ts +21 -0
  274. package/dist/repl-suggestions.d.ts.map +1 -0
  275. package/dist/repl.d.ts +3 -162
  276. package/dist/repl.d.ts.map +1 -1
  277. package/dist/replay-KCTXNMZQ.js +75 -0
  278. package/dist/replay-KCTXNMZQ.js.map +7 -0
  279. package/dist/rewind-EJMVFVLS.js +185 -0
  280. package/dist/rewind-EJMVFVLS.js.map +7 -0
  281. package/dist/sdd-runtime-JG4EEKL6.js +54 -0
  282. package/dist/sdd-runtime-JG4EEKL6.js.map +7 -0
  283. package/dist/services/sdd/project-context.d.ts +1 -1
  284. package/dist/services/sdd/project-context.d.ts.map +1 -1
  285. package/dist/services/sdd/task-manager.d.ts.map +1 -1
  286. package/dist/services/statusline-config.d.ts +1 -1
  287. package/dist/services/statusline-config.d.ts.map +1 -1
  288. package/dist/session-event-wiring.d.ts +2 -2
  289. package/dist/session-event-wiring.d.ts.map +1 -1
  290. package/dist/sessions-config-NLCMK3BY.js +409 -0
  291. package/dist/sessions-config-NLCMK3BY.js.map +7 -0
  292. package/dist/settings-menu.d.ts +3 -18
  293. package/dist/settings-menu.d.ts.map +1 -1
  294. package/dist/simpleui-dist-JIFGUJO7.js +93 -0
  295. package/dist/simpleui-dist-JIFGUJO7.js.map +7 -0
  296. package/dist/slash-commands/brain.d.ts.map +1 -1
  297. package/dist/slash-commands/clear.d.ts.map +1 -1
  298. package/dist/slash-commands/fallback.d.ts.map +1 -1
  299. package/dist/slash-commands/kanban-agent-helpers.d.ts +18 -0
  300. package/dist/slash-commands/kanban-agent-helpers.d.ts.map +1 -0
  301. package/dist/slash-commands/kanban-format.d.ts +16 -0
  302. package/dist/slash-commands/kanban-format.d.ts.map +1 -0
  303. package/dist/slash-commands/kanban-help.d.ts +2 -0
  304. package/dist/slash-commands/kanban-help.d.ts.map +1 -0
  305. package/dist/slash-commands/kanban.d.ts.map +1 -1
  306. package/dist/slash-commands/mailbox-demo.d.ts.map +1 -1
  307. package/dist/slash-commands/mailbox.d.ts +2 -1
  308. package/dist/slash-commands/mailbox.d.ts.map +1 -1
  309. package/dist/slash-commands/memory-compact.d.ts +5 -0
  310. package/dist/slash-commands/memory-compact.d.ts.map +1 -0
  311. package/dist/slash-commands/memory-flags.d.ts +36 -0
  312. package/dist/slash-commands/memory-flags.d.ts.map +1 -0
  313. package/dist/slash-commands/memory-formatters.d.ts +16 -0
  314. package/dist/slash-commands/memory-formatters.d.ts.map +1 -0
  315. package/dist/slash-commands/memory.d.ts.map +1 -1
  316. package/dist/slash-commands/mouse.d.ts +2 -2
  317. package/dist/slash-commands/review.d.ts.map +1 -1
  318. package/dist/slash-commands/sdd-command-helpers.d.ts +23 -0
  319. package/dist/slash-commands/sdd-command-helpers.d.ts.map +1 -0
  320. package/dist/slash-commands/sdd-format.d.ts +32 -0
  321. package/dist/slash-commands/sdd-format.d.ts.map +1 -0
  322. package/dist/slash-commands/sdd.d.ts.map +1 -1
  323. package/dist/slash-commands/settings.d.ts.map +1 -1
  324. package/dist/subcommands/handlers/chronicle.d.ts.map +1 -1
  325. package/dist/subcommands/handlers/mailbox-serve.d.ts.map +1 -1
  326. package/dist/subcommands/handlers/model-smoke-test.d.ts +51 -0
  327. package/dist/subcommands/handlers/model-smoke-test.d.ts.map +1 -0
  328. package/dist/subcommands/handlers/modeldiag-profiles.d.ts +64 -0
  329. package/dist/subcommands/handlers/modeldiag-profiles.d.ts.map +1 -0
  330. package/dist/subcommands/handlers/modeldiag.d.ts.map +1 -1
  331. package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts +3 -0
  332. package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts.map +1 -0
  333. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +3 -0
  334. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts.map +1 -0
  335. package/dist/subcommands/handlers/per-subcommand-help-table.d.ts +36 -0
  336. package/dist/subcommands/handlers/per-subcommand-help-table.d.ts.map +1 -0
  337. package/dist/subcommands/handlers/per-subcommand-help-types.d.ts +18 -0
  338. package/dist/subcommands/handlers/per-subcommand-help-types.d.ts.map +1 -0
  339. package/dist/subcommands/handlers/per-subcommand-help.d.ts +2 -80
  340. package/dist/subcommands/handlers/per-subcommand-help.d.ts.map +1 -1
  341. package/dist/subcommands/handlers/rewind.d.ts.map +1 -1
  342. package/dist/subcommands/index.d.ts +7 -0
  343. package/dist/subcommands/index.d.ts.map +1 -1
  344. package/dist/tools-skills-UNKLOB7M.js +29 -0
  345. package/dist/tools-skills-UNKLOB7M.js.map +7 -0
  346. package/dist/update-YC4TYW2Y.js +16 -0
  347. package/dist/update-YC4TYW2Y.js.map +7 -0
  348. package/dist/update-check-UMYTOH7Z.js +18 -0
  349. package/dist/update-check-UMYTOH7Z.js.map +7 -0
  350. package/dist/version-help-DROHZRW6.js +11 -0
  351. package/dist/version-help-DROHZRW6.js.map +7 -0
  352. package/dist/webui-server/connection-handler.d.ts.map +1 -1
  353. package/dist/webui-server/kanban-host-adapter.d.ts +5 -1
  354. package/dist/webui-server/kanban-host-adapter.d.ts.map +1 -1
  355. package/dist/webui-server/listen-helpers.d.ts +18 -0
  356. package/dist/webui-server/listen-helpers.d.ts.map +1 -0
  357. package/dist/webui-server/node-pty-loader.d.ts +6 -0
  358. package/dist/webui-server/node-pty-loader.d.ts.map +1 -0
  359. package/dist/webui-server-ZF5KVGC5.js +1460 -0
  360. package/dist/webui-server-ZF5KVGC5.js.map +7 -0
  361. package/dist/webui-server-options.d.ts +191 -0
  362. package/dist/webui-server-options.d.ts.map +1 -0
  363. package/dist/webui-server.d.ts +2 -178
  364. package/dist/webui-server.d.ts.map +1 -1
  365. package/dist/wiring/brain-and-orchestration.d.ts.map +1 -1
  366. package/dist/wiring/codebase-index.d.ts.map +1 -1
  367. package/dist/wiring/command-host-state.d.ts +4 -4
  368. package/dist/wiring/dep-watcher-bridge.d.ts +23 -0
  369. package/dist/wiring/dep-watcher-bridge.d.ts.map +1 -0
  370. package/dist/wiring/dep-watcher.d.ts.map +1 -1
  371. package/dist/wiring/hq-telemetry.d.ts +2 -2
  372. package/dist/wiring/lifecycle-plugins.d.ts +4 -3
  373. package/dist/wiring/lifecycle-plugins.d.ts.map +1 -1
  374. package/dist/wiring/mailbox-bridge-bootstrap.d.ts +1 -1
  375. package/dist/wiring/management-tools.d.ts +15 -0
  376. package/dist/wiring/management-tools.d.ts.map +1 -0
  377. package/dist/wiring/pipeline.d.ts +2 -1
  378. package/dist/wiring/pipeline.d.ts.map +1 -1
  379. package/dist/wiring/plugins.d.ts +1 -1
  380. package/dist/wiring/plugins.d.ts.map +1 -1
  381. package/dist/wiring/provider-runtime-setup.d.ts +14 -1
  382. package/dist/wiring/provider-runtime-setup.d.ts.map +1 -1
  383. package/dist/wiring/provider.d.ts.map +1 -1
  384. package/dist/wiring/runtime-controller-deps.d.ts +1 -1
  385. package/dist/wiring/runtime-controller-deps.d.ts.map +1 -1
  386. package/dist/wiring/sage.d.ts.map +1 -1
  387. package/dist/wiring/session-registry.d.ts +38 -0
  388. package/dist/wiring/session-registry.d.ts.map +1 -0
  389. package/dist/wiring/session.d.ts +10 -6
  390. package/dist/wiring/session.d.ts.map +1 -1
  391. package/dist/wiring/tools.d.ts.map +1 -1
  392. package/package.json +22 -22
  393. package/dist/cli-recovery-prompt.d.ts +0 -19
  394. package/dist/cli-recovery-prompt.d.ts.map +0 -1
  395. package/dist/first-run-auth-gate.d.ts +0 -20
  396. package/dist/first-run-auth-gate.d.ts.map +0 -1
  397. package/dist/pre-launch/indexing-question.d.ts +0 -28
  398. package/dist/pre-launch/indexing-question.d.ts.map +0 -1
@@ -0,0 +1,3303 @@
1
+ import {
2
+ resolveAuditActor
3
+ } from "./chunk-Q5GTM25S.js";
4
+
5
+ // src/hq-server.ts
6
+ import { randomUUID as randomUUID4 } from "node:crypto";
7
+ import * as fs4 from "node:fs/promises";
8
+ import * as http from "node:http";
9
+ import * as path6 from "node:path";
10
+ import {
11
+ createMailboxHttpRouter,
12
+ createProjectMailbox,
13
+ getSharedProjectMailbox,
14
+ MAILBOX_HTTP_DEFAULT_MAX_AGE_MS,
15
+ MailboxEventEmitter,
16
+ MailboxHttpRateLimiter
17
+ } from "@wrongstack/core/coordination";
18
+ import {
19
+ createHqPersistence,
20
+ HqAlertEngine,
21
+ HqCommandAuditLog,
22
+ HqBootstrapCodeStore,
23
+ mintHqCookieSecret as mintHqCookieSecret2,
24
+ mutateHqAuthFile as mutateHqAuthFile2,
25
+ toAlertMessage,
26
+ watchHqAuthFile
27
+ } from "@wrongstack/core/hq";
28
+ import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
29
+ import { WebSocket as WebSocket3, WebSocketServer } from "ws";
30
+
31
+ // src/hq-recovery-html.ts
32
+ var HQ_RECOVERY_HTML = `<!doctype html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ <meta name="viewport" content="width=device-width,initial-scale=1">
37
+ <title>WrongStack HQ \u2014 assets unavailable</title>
38
+ <style>
39
+ :root{color-scheme:dark;background:#0c111b;color:#e7edf7;font:16px/1.5 system-ui,sans-serif}
40
+ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;box-sizing:border-box}
41
+ main{max-width:680px;border:1px solid #334155;background:#111827;padding:28px}
42
+ h1{margin-top:0;font-size:1.5rem}code{color:#93c5fd}p{color:#cbd5e1}
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <main data-hq-recovery-shell>
47
+ <h1>HQ frontend assets are unavailable</h1>
48
+ <p>The HQ backend is running, but the packaged React application could not be resolved.</p>
49
+ <p>Rebuild or reinstall <code>@wrongstack/webui-hq</code>, then restart HQ. API routes remain available for diagnostics.</p>
50
+ </main>
51
+ </body>
52
+ </html>`;
53
+ var HQ_HTML = HQ_RECOVERY_HTML;
54
+
55
+ // src/hq-server/auth.ts
56
+ import { createHmac, timingSafeEqual } from "node:crypto";
57
+ import { isIP } from "node:net";
58
+ var HQ_SESSION_COOKIE = "hq.session";
59
+ var HQ_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
60
+ function setHqSecurityHeaders(res) {
61
+ res.setHeader("X-Content-Type-Options", "nosniff");
62
+ res.setHeader("X-Frame-Options", "DENY");
63
+ res.setHeader("Referrer-Policy", "no-referrer");
64
+ res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
65
+ res.setHeader(
66
+ "Content-Security-Policy",
67
+ [
68
+ "default-src 'self'",
69
+ "base-uri 'none'",
70
+ "object-src 'none'",
71
+ "frame-ancestors 'none'",
72
+ "form-action 'self'",
73
+ "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",
74
+ "style-src 'self' 'unsafe-inline'",
75
+ "font-src 'self' data:",
76
+ "img-src 'self' data:",
77
+ "connect-src 'self'"
78
+ ].join("; ")
79
+ );
80
+ }
81
+ function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins) {
82
+ const rawHost = req.headers.host?.trim();
83
+ if (!rawHost) return false;
84
+ try {
85
+ const requestUrl = new URL(`http://${rawHost}`);
86
+ if (requestUrl.username || requestUrl.password || requestUrl.pathname !== "/" || requestUrl.search || requestUrl.hash) {
87
+ return false;
88
+ }
89
+ for (const origin of trustedPublicOrigins) {
90
+ const trusted = new URL(origin);
91
+ const normalizedRequest = new URL(`${trusted.protocol}//${rawHost}`);
92
+ if (trusted.host.toLowerCase() === normalizedRequest.host.toLowerCase()) return true;
93
+ }
94
+ const hostname = requestUrl.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
95
+ const requestPort = requestUrl.port || "80";
96
+ const portMatches = boundPort === void 0 || boundPort === 0 || requestPort === String(boundPort);
97
+ const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
98
+ if (isLoopback) return portMatches;
99
+ if (isIP(hostname) !== 0) {
100
+ const normalizedBoundHost = boundHost?.toLowerCase().replace(/^\[(.*)\]$/, "$1");
101
+ const permitsIpHost = normalizedBoundHost === void 0 || normalizedBoundHost === "0.0.0.0" || normalizedBoundHost === "::" || normalizedBoundHost === hostname;
102
+ return permitsIpHost && portMatches;
103
+ }
104
+ return boundHost?.toLowerCase() === hostname && portMatches;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set()) {
110
+ if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
111
+ const origin = req.headers.origin;
112
+ if (origin === void 0) return true;
113
+ try {
114
+ const parsed = new URL(origin);
115
+ if (parsed.protocol === "file:") return true;
116
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
117
+ const rawRequestHost = req.headers.host?.trim();
118
+ if (!rawRequestHost) return false;
119
+ const requestHost = new URL(`${parsed.protocol}//${rawRequestHost}`).host.toLowerCase();
120
+ if (parsed.host.toLowerCase() !== requestHost) return false;
121
+ const registeredPublicOrigin = [...trustedPublicOrigins].some(
122
+ (trustedOrigin) => new URL(trustedOrigin).host.toLowerCase() === requestHost
123
+ );
124
+ return !registeredPublicOrigin || trustedPublicOrigins.has(parsed.origin.toLowerCase());
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ function signHqSession(sessionId, secret) {
130
+ return createHmac("sha256", secret).update(sessionId).digest("hex");
131
+ }
132
+ function serializeHqSessionCookie(sessionId, secret) {
133
+ return `${sessionId}.${signHqSession(sessionId, secret)}`;
134
+ }
135
+ function parseHqSessionCookie(value, secret) {
136
+ const dot = value.indexOf(".");
137
+ if (dot === -1) return void 0;
138
+ const sessionId = value.slice(0, dot);
139
+ const sig = value.slice(dot + 1);
140
+ if (!sessionId || !sig) return void 0;
141
+ const expected = signHqSession(sessionId, secret);
142
+ try {
143
+ if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return void 0;
144
+ } catch {
145
+ return void 0;
146
+ }
147
+ return sessionId;
148
+ }
149
+ function parseCookieHeader(cookieHeader) {
150
+ const out = {};
151
+ if (typeof cookieHeader !== "string") return out;
152
+ for (const part of cookieHeader.split(";")) {
153
+ const idx = part.indexOf("=");
154
+ if (idx === -1) continue;
155
+ const key = part.slice(0, idx).trim();
156
+ const value = part.slice(idx + 1).trim();
157
+ if (key) {
158
+ try {
159
+ out[key] = decodeURIComponent(value);
160
+ } catch {
161
+ out[key] = value;
162
+ }
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+ function setHqSessionCookie(res, value, secure) {
168
+ const parts = [
169
+ `${HQ_SESSION_COOKIE}=${encodeURIComponent(value)}`,
170
+ "Path=/",
171
+ "HttpOnly",
172
+ "SameSite=Lax",
173
+ `Max-Age=${Math.floor(HQ_SESSION_MAX_AGE_MS / 1e3)}`
174
+ ];
175
+ if (secure) parts.push("Secure");
176
+ res.setHeader("Set-Cookie", parts.join("; "));
177
+ }
178
+ function clearHqSessionCookie(res, secure) {
179
+ const parts = [`${HQ_SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`];
180
+ if (secure) parts.push("Secure");
181
+ res.setHeader("Set-Cookie", parts.join("; "));
182
+ }
183
+ function isTokenAuth(auth) {
184
+ return auth !== void 0 && auth.kind === "token";
185
+ }
186
+ function isCookieAuth(auth) {
187
+ return auth !== void 0 && auth.kind === "cookie";
188
+ }
189
+ function extractBrowserToken(req, url) {
190
+ const queryToken = url.searchParams.get("token");
191
+ if (queryToken) {
192
+ console.warn(
193
+ JSON.stringify({
194
+ level: "warn",
195
+ event: "hq.token_from_query_param",
196
+ message: "Browser token accepted from URL query parameter \u2014 token can leak through browser history, server access logs, and Referer headers.",
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
198
+ })
199
+ );
200
+ return queryToken;
201
+ }
202
+ const auth = req.headers.authorization;
203
+ if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
204
+ return auth.slice(7).trim();
205
+ }
206
+ return void 0;
207
+ }
208
+ function timingSafeTokenMatch(tokens, supplied) {
209
+ if (!supplied) return void 0;
210
+ const b = Buffer.from(supplied);
211
+ for (const candidate of tokens) {
212
+ const a = Buffer.from(candidate);
213
+ if (a.length === b.length && timingSafeEqual(a, b)) return candidate;
214
+ }
215
+ return void 0;
216
+ }
217
+ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
218
+ const token = extractBrowserToken(req, url);
219
+ if (token) {
220
+ const matchedToken = timingSafeTokenMatch(mutableAuth.browserTokens, token);
221
+ if (matchedToken) {
222
+ const obj = mutableAuth.browserTokenObjs.get(matchedToken);
223
+ const ctx = {
224
+ kind: "token",
225
+ token: matchedToken,
226
+ id: obj?.id ?? "unknown"
227
+ };
228
+ if (obj?.capabilities !== void 0) ctx.capabilities = obj.capabilities;
229
+ return ctx;
230
+ }
231
+ }
232
+ if (mutableAuth.cookieSecret) {
233
+ const cookies = parseCookieHeader(req.headers.cookie);
234
+ const raw = cookies[HQ_SESSION_COOKIE];
235
+ if (raw) {
236
+ const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret);
237
+ if (sessionId) {
238
+ const session = sessions.get(sessionId);
239
+ if (session && Date.now() - session.createdAt < HQ_SESSION_MAX_AGE_MS) {
240
+ if (session.kind === "token" && session.tokenId !== void 0) {
241
+ const liveToken = [...mutableAuth.browserTokenObjs.values()].find(
242
+ (obj) => obj.id === session.tokenId
243
+ );
244
+ if (!liveToken) {
245
+ sessions.delete(sessionId);
246
+ return void 0;
247
+ }
248
+ const ctx = { kind: "cookie", tokenId: session.tokenId };
249
+ if (liveToken.capabilities !== void 0) ctx.capabilities = liveToken.capabilities;
250
+ return ctx;
251
+ }
252
+ return { kind: "cookie" };
253
+ }
254
+ if (session) sessions.delete(sessionId);
255
+ }
256
+ }
257
+ }
258
+ return void 0;
259
+ }
260
+
261
+ // src/hq-server/auth-state.ts
262
+ import { DEFAULT_HQ_REDACTION_POLICY, hqAuthContentHash, isTokenExpired, logHqAuthAudit } from "@wrongstack/core/hq";
263
+ var TOKEN_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
264
+ function liveTokens(list) {
265
+ return (list ?? []).filter((token) => !isTokenExpired(token));
266
+ }
267
+ function createHqAuthState(authFile, dataDir) {
268
+ let rawBrowserTokens = authFile.browserTokens ?? [];
269
+ let rawClientTokens = authFile.clientTokens ?? [];
270
+ const mutableAuth = {
271
+ operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY, ...authFile.redactionPolicy ?? {} },
272
+ operatorPolicyOverride: authFile.redactionPolicy,
273
+ browserTokens: new Set(liveTokens(authFile.browserTokens).map(({ token }) => token)),
274
+ clientTokens: new Set(liveTokens(authFile.clientTokens).map(({ token }) => token)),
275
+ browserTokenObjs: browserTokenMap(authFile.browserTokens),
276
+ clientTokenObjs: new Map(liveTokens(authFile.clientTokens).map((token) => [token.token, token])),
277
+ passwordHash: authFile.passwordHash,
278
+ cookieSecret: authFile.cookieSecret,
279
+ alertRules: authFile.alertRules
280
+ };
281
+ return {
282
+ mutableAuth,
283
+ tokenStats: () => {
284
+ const now = Date.now();
285
+ const all = [...rawBrowserTokens, ...rawClientTokens];
286
+ const live = all.filter((token) => !isTokenExpired(token, now));
287
+ return {
288
+ browserTotal: mutableAuth.browserTokens.size,
289
+ clientTotal: mutableAuth.clientTokens.size,
290
+ expired: all.length - live.length,
291
+ expiringSoon: live.filter((token) => {
292
+ if (token.expiresAt === void 0) return false;
293
+ const expiry = Date.parse(token.expiresAt);
294
+ return Number.isFinite(expiry) && expiry - now <= TOKEN_EXPIRY_WARNING_MS;
295
+ }).length
296
+ };
297
+ },
298
+ apply: (next) => {
299
+ const newBrowser = next.browserTokens ?? [];
300
+ const newClient = next.clientTokens ?? [];
301
+ auditPrunedTokens("browser", rawBrowserTokens, newBrowser, next, dataDir);
302
+ auditPrunedTokens("client", rawClientTokens, newClient, next, dataDir);
303
+ rawBrowserTokens = newBrowser;
304
+ rawClientTokens = newClient;
305
+ mutableAuth.operatorPolicy = {
306
+ ...DEFAULT_HQ_REDACTION_POLICY,
307
+ ...next.redactionPolicy ?? {}
308
+ };
309
+ mutableAuth.operatorPolicyOverride = next.redactionPolicy;
310
+ mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(({ token }) => token));
311
+ mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(({ token }) => token));
312
+ mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
313
+ mutableAuth.clientTokenObjs = new Map(
314
+ liveTokens(next.clientTokens).map((token) => [token.token, token])
315
+ );
316
+ mutableAuth.passwordHash = next.passwordHash;
317
+ mutableAuth.cookieSecret = next.cookieSecret;
318
+ mutableAuth.alertRules = next.alertRules;
319
+ }
320
+ };
321
+ }
322
+ function browserTokenMap(tokens) {
323
+ return new Map(
324
+ liveTokens(tokens).map((token) => [
325
+ token.token,
326
+ {
327
+ id: token.id,
328
+ ...token.capabilities !== void 0 ? { capabilities: token.capabilities } : {},
329
+ ...token.expiresAt !== void 0 ? { expiresAt: token.expiresAt } : {}
330
+ }
331
+ ])
332
+ );
333
+ }
334
+ function auditPrunedTokens(scope, previous, next, authFile, dataDir) {
335
+ const previouslyLive = new Set(previous.filter((token) => !isTokenExpired(token)).map(({ id }) => id));
336
+ const prunedCount = next.filter((token) => isTokenExpired(token) && previouslyLive.has(token.id)).length;
337
+ if (prunedCount === 0) return;
338
+ const contentHash = hqAuthContentHash(authFile);
339
+ logHqAuthAudit(dataDir, {
340
+ kind: "expired-prune",
341
+ scope,
342
+ tokenId: "(aggregate)",
343
+ prunedCount,
344
+ ...contentHash !== void 0 ? { contentHash } : {}
345
+ });
346
+ }
347
+
348
+ // src/hq-server/preflight.ts
349
+ import { assessHqExposure, ensureHqFirstRunAuthFile, resolveHqDataDir } from "@wrongstack/core/hq";
350
+ import { HqInsecureExposureError } from "@wrongstack/core/hq";
351
+ async function prepareHqServerStart(options, defaults) {
352
+ const host = options.host ?? defaults.host;
353
+ const port = options.port ?? defaults.port;
354
+ const dataDir = resolveHqDataDir(options.dataDir);
355
+ const firstRunAuth = await ensureHqFirstRunAuthFile(dataDir, {
356
+ warn: (message) => console.warn(
357
+ JSON.stringify({
358
+ level: "warn",
359
+ event: "hq.auth_load_failed",
360
+ message,
361
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
362
+ })
363
+ ),
364
+ actor: resolveAuditActor(),
365
+ ...options.password !== void 0 ? { password: options.password } : {},
366
+ ...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
367
+ });
368
+ const { authFile } = firstRunAuth;
369
+ if (options.requireBrowserAuth && (authFile.browserTokens ?? []).length === 0 && authFile.passwordHash === void 0) {
370
+ throw new HqInsecureExposureError(
371
+ "HQ public relay requires browser authentication. Set --password or create a browser token first."
372
+ );
373
+ }
374
+ const exposure = assessHqExposure({
375
+ host,
376
+ hasBrowserTokens: (authFile.browserTokens ?? []).length > 0,
377
+ hasPassword: authFile.passwordHash !== void 0,
378
+ allowInsecure: options.allowInsecureOpen
379
+ });
380
+ if (exposure.kind === "refuse") throw new HqInsecureExposureError(exposure.message);
381
+ if (exposure.kind === "warn") {
382
+ console.warn(
383
+ JSON.stringify({
384
+ level: "warn",
385
+ event: "hq.insecure_exposure",
386
+ message: exposure.message,
387
+ host,
388
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
389
+ })
390
+ );
391
+ }
392
+ return { host, port, dataDir, firstRunAuth };
393
+ }
394
+
395
+ // src/hq-server/routes.ts
396
+ import { randomUUID as randomUUID2 } from "node:crypto";
397
+ import * as path4 from "node:path";
398
+ import { tokenHasCapability, validateHqCommand } from "@wrongstack/core/hq";
399
+ import {
400
+ resolveProjectDir
401
+ } from "@wrongstack/core/coordination";
402
+
403
+ // src/hq-static-serve.ts
404
+ import * as fs from "node:fs";
405
+ import { createRequire } from "node:module";
406
+ import * as path from "node:path";
407
+ var requireFromHere = createRequire(import.meta.url);
408
+ var MIME = {
409
+ ".html": "text/html; charset=utf-8",
410
+ ".js": "application/javascript; charset=utf-8",
411
+ ".mjs": "application/javascript; charset=utf-8",
412
+ ".css": "text/css; charset=utf-8",
413
+ ".json": "application/json; charset=utf-8",
414
+ ".svg": "image/svg+xml",
415
+ ".png": "image/png",
416
+ ".jpg": "image/jpeg",
417
+ ".woff": "font/woff",
418
+ ".woff2": "font/woff2",
419
+ ".ico": "image/x-icon"
420
+ };
421
+ var cachedDistDir;
422
+ function resolveHqDistDir() {
423
+ if (cachedDistDir !== void 0) return cachedDistDir;
424
+ try {
425
+ const pkgJson = requireFromHere.resolve("@wrongstack/webui-hq/package.json");
426
+ const dist = path.join(path.dirname(pkgJson), "dist");
427
+ if (fs.existsSync(path.join(dist, "index.html"))) {
428
+ cachedDistDir = dist;
429
+ return dist;
430
+ }
431
+ } catch {
432
+ }
433
+ try {
434
+ let dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
435
+ for (let i = 0; i < 8; i++) {
436
+ const candidate = path.join(dir, "node_modules", "@wrongstack", "webui-hq", "dist");
437
+ if (fs.existsSync(path.join(candidate, "index.html"))) {
438
+ cachedDistDir = candidate;
439
+ return candidate;
440
+ }
441
+ const parent = path.dirname(dir);
442
+ if (parent === dir) break;
443
+ dir = parent;
444
+ }
445
+ } catch {
446
+ }
447
+ cachedDistDir = null;
448
+ return null;
449
+ }
450
+ function isInsideDist(filePath, distDir) {
451
+ const rel = path.relative(distDir, filePath);
452
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
453
+ }
454
+ function isMissingPath(error) {
455
+ const code = error.code;
456
+ return code === "ENOENT" || code === "ENOTDIR";
457
+ }
458
+ async function readRegularFile(filePath) {
459
+ try {
460
+ const info = await fs.promises.stat(filePath);
461
+ if (!info.isFile()) return null;
462
+ return await fs.promises.readFile(filePath);
463
+ } catch (error) {
464
+ if (isMissingPath(error)) return null;
465
+ throw error;
466
+ }
467
+ }
468
+ async function serveHqStatic(_req, res, urlPath, distDir) {
469
+ const relPath = urlPath === "/" || urlPath === "" ? "/index.html" : urlPath;
470
+ let decodedPath;
471
+ try {
472
+ decodedPath = decodeURIComponent(relPath);
473
+ } catch {
474
+ return { handled: false };
475
+ }
476
+ const filePath = path.join(distDir, decodedPath);
477
+ if (!isInsideDist(filePath, distDir)) {
478
+ return { handled: false };
479
+ }
480
+ const data = await readRegularFile(filePath);
481
+ if (data === null) {
482
+ const indexPath = path.join(distDir, "index.html");
483
+ const html = await readRegularFile(indexPath);
484
+ if (html !== null) {
485
+ res.writeHead(200, {
486
+ "Content-Type": MIME[".html"],
487
+ "Cache-Control": "no-cache",
488
+ "Content-Length": html.byteLength
489
+ });
490
+ res.end(html);
491
+ return { handled: true };
492
+ }
493
+ return { handled: false };
494
+ }
495
+ const ext = path.extname(filePath).toLowerCase();
496
+ const mime = MIME[ext] ?? "application/octet-stream";
497
+ const cacheControl = decodedPath.startsWith("/assets/") ? "public, max-age=31536000, immutable" : ext === ".html" ? "no-cache" : "public, max-age=3600";
498
+ res.writeHead(200, {
499
+ "Content-Type": mime,
500
+ "Cache-Control": cacheControl,
501
+ "Content-Length": data.byteLength
502
+ });
503
+ res.end(data);
504
+ return { handled: true };
505
+ }
506
+
507
+ // src/hq-server/snapshot.ts
508
+ import { WebSocket } from "ws";
509
+
510
+ // src/hq-server/utils.ts
511
+ import * as fs2 from "node:fs/promises";
512
+ import * as os from "node:os";
513
+ import * as path2 from "node:path";
514
+ import { resolveHqDataDir as resolveHqDataDir2 } from "@wrongstack/core/hq";
515
+ var TRANSCRIPT_RING_MAX = 4e3;
516
+ var MAX_TRANSCRIPT_SESSIONS = 400;
517
+ var MAX_AGENT_RINGS = 800;
518
+ function evictOldest(map, max) {
519
+ while (map.size > max) {
520
+ const oldest = map.keys().next().value;
521
+ if (oldest === void 0) break;
522
+ map.delete(oldest);
523
+ }
524
+ }
525
+ function agentRingKey(sessionId, subId) {
526
+ return sessionId ? `${sessionId}::${subId}` : subId;
527
+ }
528
+ function agentMessageToEntry(p) {
529
+ const kind = typeof p["kind"] === "string" ? p["kind"] : "text";
530
+ const role = kind === "thinking" ? "thinking" : kind === "tool_use" || kind === "tool_result" ? "tool" : kind === "error" ? "error" : kind === "status" || kind === "system" ? "system" : "assistant";
531
+ return {
532
+ ts: typeof p["ts"] === "string" ? p["ts"] : (/* @__PURE__ */ new Date()).toISOString(),
533
+ role,
534
+ text: typeof p["content"] === "string" ? p["content"] : "",
535
+ ...typeof p["toolName"] === "string" ? { tool: p["toolName"] } : {},
536
+ ...kind === "error" ? { isError: true } : {}
537
+ };
538
+ }
539
+ async function readLocalSubagentTranscript(sessionId, subagentId) {
540
+ try {
541
+ const { SessionRegistry } = await import("@wrongstack/core/storage");
542
+ const { resolveWstackPaths, sessionScopedPath } = await import("@wrongstack/core/utils");
543
+ const globalRoot = path2.dirname(resolveHqDataDir2());
544
+ const registry = new SessionRegistry(globalRoot);
545
+ const entry = await registry.get(sessionId).catch(() => null);
546
+ if (!entry) return null;
547
+ const paths = resolveWstackPaths({ projectRoot: entry.projectRoot, globalRoot });
548
+ const sessionDir = sessionScopedPath(paths.projectSessions, sessionId, "");
549
+ const file = path2.join(sessionDir, "subagents", "transcripts", subagentId, "transcript.jsonl");
550
+ const raw = await fs2.readFile(file, "utf8").catch(() => null);
551
+ if (raw === null) return null;
552
+ const out = [];
553
+ for (const line of raw.split("\n")) {
554
+ const trimmed = line.trim();
555
+ if (trimmed.length === 0) continue;
556
+ try {
557
+ out.push(agentMessageToEntry(JSON.parse(trimmed)));
558
+ } catch {
559
+ }
560
+ }
561
+ return out;
562
+ } catch {
563
+ return null;
564
+ }
565
+ }
566
+ function decodePathSegment(value) {
567
+ try {
568
+ return decodeURIComponent(value);
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ function displayHost(host) {
574
+ return host === "0.0.0.0" ? "127.0.0.1" : host;
575
+ }
576
+ var RequestBodyTooLargeError = class extends Error {
577
+ };
578
+ function readRequestBody(req, maxBytes = 1e6) {
579
+ return new Promise((resolve, reject) => {
580
+ const chunks = [];
581
+ let size = 0;
582
+ let settled = false;
583
+ req.on("data", (chunk) => {
584
+ if (settled) return;
585
+ size += chunk.length;
586
+ if (size > maxBytes) {
587
+ settled = true;
588
+ chunks.length = 0;
589
+ reject(new RequestBodyTooLargeError("request body too large"));
590
+ return;
591
+ }
592
+ chunks.push(chunk);
593
+ });
594
+ req.on("end", () => {
595
+ if (!settled) resolve(Buffer.concat(chunks).toString("utf8"));
596
+ });
597
+ req.on("error", (error) => {
598
+ if (!settled) reject(error);
599
+ });
600
+ });
601
+ }
602
+ function writeInvalidBody(res, error) {
603
+ const tooLarge = error instanceof RequestBodyTooLargeError;
604
+ res.writeHead(tooLarge ? 413 : 400, { "Content-Type": "application/json" });
605
+ res.end(JSON.stringify({ error: tooLarge ? "request body too large" : "invalid json body" }));
606
+ }
607
+ function buildHttpUrl(host, port, token) {
608
+ const url = new URL(`http://${displayHost(host)}:${port}/`);
609
+ if (token) url.searchParams.set("token", token);
610
+ return url.toString();
611
+ }
612
+ function buildBootstrapHttpUrl(host, port, bootstrapCode) {
613
+ const url = new URL(`http://${displayHost(host)}:${port}/`);
614
+ url.hash = `bootstrap=${bootstrapCode}`;
615
+ return url.toString();
616
+ }
617
+ function buildClientWsUrl(host, port, token) {
618
+ const url = new URL(`ws://${displayHost(host)}:${port}/ws/client`);
619
+ if (token) url.searchParams.set("token", token);
620
+ return url.toString();
621
+ }
622
+ function hqRuntimeMarkerPath(dataDir) {
623
+ return path2.join(dataDir, "runtime.json");
624
+ }
625
+ function lanIPv4Addresses() {
626
+ const out = [];
627
+ try {
628
+ const ifaces = os.networkInterfaces();
629
+ for (const name of Object.keys(ifaces)) {
630
+ for (const ni of ifaces[name] ?? []) {
631
+ if (ni.family === "IPv4" && !ni.internal) out.push(ni.address);
632
+ }
633
+ }
634
+ } catch {
635
+ }
636
+ return out;
637
+ }
638
+ function truncateHqSummary(value, maxLength) {
639
+ if (typeof value !== "string" || value.length === 0) return void 0;
640
+ if (value.length <= maxLength) return value;
641
+ return `${value.slice(0, maxLength)}\u2026[truncated:${value.length - maxLength}]`;
642
+ }
643
+ function hqMachineKey(hostname, machineId) {
644
+ const hn = hostname?.trim();
645
+ return hn ? `host:${hn.toLowerCase()}` : `mid:${machineId || "local"}`;
646
+ }
647
+ function recordTimeseriesSignal(persistence, event) {
648
+ try {
649
+ if (event.type === "session.usage") {
650
+ const p = event.payload;
651
+ persistence.timeseries.record({
652
+ ts: Date.parse(event.timestamp) || Date.now(),
653
+ ...typeof p.costUsd === "number" ? { costUsd: p.costUsd } : {},
654
+ ...typeof p.inputTokens === "number" ? { inputTokens: p.inputTokens } : {},
655
+ ...typeof p.outputTokens === "number" ? { outputTokens: p.outputTokens } : {},
656
+ ...typeof p.model === "string" && p.model.length > 0 ? { model: p.model } : {},
657
+ ...typeof p.provider === "string" && p.provider.length > 0 ? { provider: p.provider } : {},
658
+ ...typeof p.cacheRead === "number" ? { cacheRead: p.cacheRead } : {},
659
+ ...typeof p.cacheWrite === "number" ? { cacheWrite: p.cacheWrite } : {}
660
+ });
661
+ } else if (event.type === "tool.completed") {
662
+ persistence.timeseries.record({
663
+ ts: Date.parse(event.timestamp) || Date.now(),
664
+ toolCalls: 1
665
+ });
666
+ }
667
+ } catch {
668
+ }
669
+ }
670
+ function sanitizeApiError(err) {
671
+ const message = err instanceof Error ? err.message : String(err);
672
+ const lower = message.toLowerCase();
673
+ if (lower.includes("enoent") || lower.includes("no such file")) {
674
+ return "resource not found";
675
+ }
676
+ if (lower.includes("eacces") || lower.includes("permission")) {
677
+ return "permission denied";
678
+ }
679
+ if (lower.includes("json") && (lower.includes("parse") || lower.includes("unexpected"))) {
680
+ return "malformed data";
681
+ }
682
+ return "internal error";
683
+ }
684
+
685
+ // src/hq-server/snapshot.ts
686
+ var HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS = 100;
687
+ var HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS = 3e3;
688
+ var HQ_BROWSER_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
689
+ function sendGuarded(ws, data) {
690
+ if (ws.readyState !== WebSocket.OPEN) return false;
691
+ const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
692
+ if (buffered + Buffer.byteLength(data, "utf8") > HQ_BROWSER_MAX_BUFFERED_BYTES) {
693
+ try {
694
+ ws.terminate();
695
+ } catch {
696
+ try {
697
+ ws.close(1013, "client cannot keep up");
698
+ } catch {
699
+ }
700
+ }
701
+ return false;
702
+ }
703
+ try {
704
+ ws.send(data);
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
710
+ var HQ_STALE_SNAPSHOT_MS = 5 * 6e4;
711
+ function reapStaleClientState(clients, nowMs = Date.now()) {
712
+ for (const client of clients.values()) {
713
+ for (const [sessionId, tracked] of client.sessions) {
714
+ if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.sessions.delete(sessionId);
715
+ }
716
+ for (const [runId, fleet] of client.fleets) {
717
+ if (nowMs - fleet.receivedAt > HQ_STALE_SNAPSHOT_MS) client.fleets.delete(runId);
718
+ }
719
+ for (const [sessionId, tracked] of client.mcpSnapshots) {
720
+ if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.mcpSnapshots.delete(sessionId);
721
+ }
722
+ }
723
+ }
724
+ function buildSnapshot(clients, options) {
725
+ reapStaleClientState(clients);
726
+ const now = (/* @__PURE__ */ new Date()).toISOString();
727
+ const clientRecordById = /* @__PURE__ */ new Map();
728
+ const projectMap = /* @__PURE__ */ new Map();
729
+ const mailboxSummaries = [];
730
+ const sessionById = /* @__PURE__ */ new Map();
731
+ const fleetByRunId = /* @__PURE__ */ new Map();
732
+ for (const client of clients.values()) {
733
+ const machineId = client.machineId || client.project.machineId || "";
734
+ if (!clientRecordById.has(client.clientId)) {
735
+ clientRecordById.set(client.clientId, {
736
+ clientId: client.clientId,
737
+ kind: client.kind,
738
+ machineId,
739
+ ...client.hostname ? { hostname: client.hostname } : {},
740
+ ...client.pid ? { pid: client.pid } : {},
741
+ ...client.version ? { version: client.version } : {},
742
+ connected: true,
743
+ connectedAt: client.connectedAt,
744
+ lastSeenAt: client.lastSeenAt,
745
+ projectId: client.projectId,
746
+ capabilities: client.capabilities
747
+ });
748
+ }
749
+ let project = projectMap.get(client.projectId);
750
+ if (!project) {
751
+ project = {
752
+ projectId: client.projectId,
753
+ projectName: client.project.projectName || client.projectId,
754
+ projectRootDisplay: client.project.projectRoot,
755
+ machineIds: [machineId],
756
+ ...client.project.gitBranch ? { gitBranch: client.project.gitBranch } : {},
757
+ activeClients: 0,
758
+ activeSessions: 0,
759
+ activeSubagents: 0,
760
+ totalCostUsd: 0,
761
+ lastActivityAt: now,
762
+ status: "active"
763
+ };
764
+ projectMap.set(client.projectId, project);
765
+ } else if (machineId && !project.machineIds.includes(machineId)) {
766
+ project.machineIds = [...project.machineIds, machineId];
767
+ }
768
+ for (const tracked of client.sessions.values()) {
769
+ const cutoff = Date.now() - 5 * 6e4;
770
+ const agents = tracked.payload.agents.filter((agent) => {
771
+ if (agent.id === "leader") return true;
772
+ const lastActivityAt = Date.parse(agent.lastActivityAt);
773
+ return Number.isFinite(lastActivityAt) && lastActivityAt >= cutoff;
774
+ });
775
+ sessionById.set(tracked.payload.sessionId, {
776
+ ...tracked.payload,
777
+ clientId: client.clientId,
778
+ agents,
779
+ agentCount: agents.length
780
+ });
781
+ }
782
+ for (const snapshot of client.mailboxes.values()) {
783
+ mailboxSummaries.push({
784
+ mailboxId: snapshot.mailboxId,
785
+ projectId: client.projectId,
786
+ scope: snapshot.scope,
787
+ messageCount: snapshot.totals.messages,
788
+ unreadCount: snapshot.totals.unread,
789
+ incompleteCount: snapshot.totals.incomplete,
790
+ highPriorityCount: snapshot.totals.highPriority,
791
+ onlineAgentCount: snapshot.totals.onlineAgents,
792
+ lastActivityAt: now
793
+ });
794
+ }
795
+ for (const fleet of client.fleets.values()) {
796
+ fleetByRunId.set(fleet.payload.runId, {
797
+ payload: fleet.payload,
798
+ clientId: client.clientId,
799
+ projectId: client.projectId,
800
+ lastActivityAt: new Date(fleet.receivedAt).toISOString()
801
+ });
802
+ }
803
+ }
804
+ const processKeyOf = (rec) => rec.pid !== void 0 ? `${rec.machineId}:${rec.pid}` : rec.clientId;
805
+ const countedProjectProcesses = /* @__PURE__ */ new Set();
806
+ for (const rec of clientRecordById.values()) {
807
+ const key = `${rec.projectId}|${processKeyOf(rec)}`;
808
+ if (countedProjectProcesses.has(key)) continue;
809
+ countedProjectProcesses.add(key);
810
+ const project = projectMap.get(rec.projectId);
811
+ if (project) project.activeClients++;
812
+ }
813
+ const liveSessions = Array.from(sessionById.values());
814
+ const machineMap = /* @__PURE__ */ new Map();
815
+ let totalAgents = 0;
816
+ let totalSubagents = 0;
817
+ let totalCostUsd = 0;
818
+ for (const session of liveSessions) {
819
+ let project = projectMap.get(session.projectId);
820
+ if (!project) {
821
+ project = {
822
+ projectId: session.projectId,
823
+ projectName: session.projectName || session.projectId,
824
+ projectRootDisplay: session.projectRoot,
825
+ machineIds: [session.machineId],
826
+ ...session.gitBranch ? { gitBranch: session.gitBranch } : {},
827
+ activeClients: 0,
828
+ activeSessions: 0,
829
+ activeSubagents: 0,
830
+ totalCostUsd: 0,
831
+ lastActivityAt: session.lastActivityAt,
832
+ status: "active"
833
+ };
834
+ projectMap.set(session.projectId, project);
835
+ } else if (session.machineId && !project.machineIds.includes(session.machineId)) {
836
+ project.machineIds = [...project.machineIds, session.machineId];
837
+ }
838
+ project.activeSessions++;
839
+ let sessionCost = 0;
840
+ for (const agent of session.agents) {
841
+ totalAgents++;
842
+ if (agent.id !== "leader") totalSubagents++;
843
+ if (typeof agent.costUsd === "number") {
844
+ sessionCost += agent.costUsd;
845
+ }
846
+ }
847
+ project.activeSubagents += session.agents.filter((a) => a.id !== "leader").length;
848
+ project.totalCostUsd += sessionCost;
849
+ totalCostUsd += sessionCost;
850
+ const mKey = hqMachineKey(session.hostname, session.machineId);
851
+ let machine = machineMap.get(mKey);
852
+ if (!machine) {
853
+ machine = {
854
+ record: {
855
+ machineId: session.machineId,
856
+ ...session.hostname ? { hostname: session.hostname } : {},
857
+ clientCount: 0,
858
+ sessionCount: 0,
859
+ agentCount: 0,
860
+ projectIds: [],
861
+ lastActivityAt: session.lastActivityAt
862
+ },
863
+ projects: /* @__PURE__ */ new Set()
864
+ };
865
+ machineMap.set(mKey, machine);
866
+ }
867
+ machine.record.sessionCount++;
868
+ machine.record.agentCount += session.agents.length;
869
+ machine.projects.add(session.projectId);
870
+ if (session.lastActivityAt > machine.record.lastActivityAt) {
871
+ machine.record.lastActivityAt = session.lastActivityAt;
872
+ }
873
+ }
874
+ const countedMachineProcesses = /* @__PURE__ */ new Set();
875
+ for (const rec of clientRecordById.values()) {
876
+ if (!rec.machineId && !rec.hostname) continue;
877
+ const rKey = hqMachineKey(rec.hostname, rec.machineId);
878
+ let machine = machineMap.get(rKey);
879
+ if (!machine) {
880
+ machine = {
881
+ record: {
882
+ machineId: rec.machineId,
883
+ ...rec.hostname ? { hostname: rec.hostname } : {},
884
+ clientCount: 0,
885
+ sessionCount: 0,
886
+ agentCount: 0,
887
+ projectIds: [],
888
+ lastActivityAt: rec.lastSeenAt
889
+ },
890
+ projects: /* @__PURE__ */ new Set()
891
+ };
892
+ machineMap.set(rKey, machine);
893
+ }
894
+ const processKey = `${rKey}|${processKeyOf(rec)}`;
895
+ if (!countedMachineProcesses.has(processKey)) {
896
+ countedMachineProcesses.add(processKey);
897
+ machine.record.clientCount++;
898
+ }
899
+ machine.projects.add(rec.projectId);
900
+ if (rec.hostname && !machine.record.hostname) machine.record.hostname = rec.hostname;
901
+ }
902
+ const machines = Array.from(machineMap.values()).map((m) => ({
903
+ ...m.record,
904
+ projectIds: Array.from(m.projects)
905
+ }));
906
+ const clientRecords = Array.from(clientRecordById.values());
907
+ const projects = Array.from(projectMap.values());
908
+ let unread = 0;
909
+ let incomplete = 0;
910
+ for (const m of mailboxSummaries) {
911
+ unread += m.unreadCount;
912
+ incomplete += m.incompleteCount;
913
+ }
914
+ const sessions = liveSessions.map((s) => {
915
+ let sessionCost = 0;
916
+ for (const agent of s.agents) {
917
+ if (typeof agent.costUsd === "number") sessionCost += agent.costUsd;
918
+ }
919
+ const provider = s.agents.find((a) => a.model !== void 0)?.model;
920
+ return {
921
+ sessionId: s.sessionId,
922
+ projectId: s.projectId,
923
+ clientId: s.clientId ?? `${s.machineId}:${s.clientKind}`,
924
+ status: s.status === "active" ? "running" : "idle",
925
+ ...provider !== void 0 ? { model: provider } : {},
926
+ startedAt: s.startedAt,
927
+ lastActivityAt: s.lastActivityAt,
928
+ ...sessionCost > 0 ? { costUsd: sessionCost } : {}
929
+ };
930
+ });
931
+ const fleets = Array.from(fleetByRunId.values()).map((f) => ({
932
+ runId: f.payload.runId,
933
+ projectId: f.projectId,
934
+ clientId: f.clientId,
935
+ activeSubagents: f.payload.activeSubagents,
936
+ queuedTasks: f.payload.queuedTasks,
937
+ completedTasks: f.payload.completedTasks,
938
+ failedTasks: f.payload.failedTasks,
939
+ ...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
940
+ lastActivityAt: f.lastActivityAt
941
+ }));
942
+ const mcpServers = [];
943
+ const seenMcp = /* @__PURE__ */ new Set();
944
+ for (const client of clients.values()) {
945
+ for (const [sessionId, tracked] of client.mcpSnapshots.entries()) {
946
+ for (const server of tracked.servers) {
947
+ const key = `${client.clientId}:${sessionId}:${server.name}`;
948
+ if (seenMcp.has(key)) continue;
949
+ seenMcp.add(key);
950
+ mcpServers.push(server);
951
+ }
952
+ }
953
+ }
954
+ return {
955
+ generatedAt: now,
956
+ clients: clientRecords,
957
+ projects,
958
+ sessions,
959
+ fleets,
960
+ mailboxes: mailboxSummaries,
961
+ machines,
962
+ liveSessions,
963
+ mcpServers,
964
+ totals: {
965
+ activeProjects: projects.length,
966
+ activeClients: new Set(clientRecords.map(processKeyOf)).size,
967
+ activeSessions: liveSessions.length,
968
+ activeSubagents: totalSubagents,
969
+ unreadMailboxMessages: unread,
970
+ incompleteMailboxMessages: incomplete,
971
+ totalCostUsd,
972
+ activeMachines: machines.length,
973
+ activeAgents: totalAgents,
974
+ ...options?.tokenStats !== void 0 ? { tokenStats: options.tokenStats } : {}
975
+ }
976
+ };
977
+ }
978
+ function createSnapshotBroadcaster(clients, browsers, persistence) {
979
+ let cached = "";
980
+ let dirty = true;
981
+ let timer = null;
982
+ let closed = false;
983
+ let lastPersistAt = 0;
984
+ const persistCheckpoint = (snapshot) => {
985
+ if (persistence === void 0) return;
986
+ const now = Date.now();
987
+ if (now - lastPersistAt < HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) return;
988
+ lastPersistAt = now;
989
+ persistence.snapshotStore.save(snapshot);
990
+ };
991
+ const serialize = () => {
992
+ if (!dirty && cached.length > 0) return cached;
993
+ const snapshot = buildSnapshot(clients);
994
+ const msg = { type: "hq.snapshot", snapshot };
995
+ cached = JSON.stringify(msg);
996
+ dirty = false;
997
+ persistCheckpoint(snapshot);
998
+ return cached;
999
+ };
1000
+ const flush = () => {
1001
+ timer = null;
1002
+ if (browsers.size === 0) {
1003
+ if (persistence !== void 0 && Date.now() - lastPersistAt >= HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) {
1004
+ persistCheckpoint(buildSnapshot(clients));
1005
+ }
1006
+ return;
1007
+ }
1008
+ const data = serialize();
1009
+ for (const ws of browsers) sendGuarded(ws, data);
1010
+ };
1011
+ return {
1012
+ currentSerialized: serialize,
1013
+ broadcast: () => {
1014
+ if (closed) return;
1015
+ dirty = true;
1016
+ if (timer !== null) return;
1017
+ timer = setTimeout(flush, HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS);
1018
+ timer.unref?.();
1019
+ },
1020
+ close: () => {
1021
+ closed = true;
1022
+ if (timer !== null) {
1023
+ clearTimeout(timer);
1024
+ timer = null;
1025
+ }
1026
+ }
1027
+ };
1028
+ }
1029
+ function buildProjectDetail(clients, projectId) {
1030
+ const projectClients = [];
1031
+ for (const c of clients.values()) {
1032
+ if (c.projectId === projectId) projectClients.push(c);
1033
+ }
1034
+ if (projectClients.length === 0) return null;
1035
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1036
+ const clientRecords = projectClients.map((c) => ({
1037
+ clientId: c.clientId,
1038
+ kind: c.kind,
1039
+ machineId: "",
1040
+ ...c.hostname ? { hostname: c.hostname } : {},
1041
+ ...c.pid ? { pid: c.pid } : {},
1042
+ ...c.version ? { version: c.version } : {},
1043
+ connected: true,
1044
+ connectedAt: c.connectedAt,
1045
+ lastSeenAt: c.lastSeenAt,
1046
+ projectId: c.projectId,
1047
+ capabilities: c.capabilities
1048
+ }));
1049
+ const mailboxPayloads = [];
1050
+ let latestActivity = now;
1051
+ for (const c of projectClients) {
1052
+ for (const snap of c.mailboxes.values()) {
1053
+ mailboxPayloads.push(snap);
1054
+ if (snap.totals.messages > 0) latestActivity = now;
1055
+ }
1056
+ }
1057
+ const primaryProject = projectClients[0].project;
1058
+ const machineIds = Array.from(new Set(projectClients.map((client) => client.project.machineId)));
1059
+ const project = {
1060
+ projectId,
1061
+ projectName: primaryProject.projectName || projectId,
1062
+ projectRootDisplay: primaryProject.projectRoot,
1063
+ machineIds,
1064
+ ...primaryProject.gitBranch ? { gitBranch: primaryProject.gitBranch } : {},
1065
+ activeClients: projectClients.length,
1066
+ activeSessions: 0,
1067
+ activeSubagents: 0,
1068
+ totalCostUsd: 0,
1069
+ lastActivityAt: latestActivity,
1070
+ status: "active"
1071
+ };
1072
+ return {
1073
+ generatedAt: now,
1074
+ project,
1075
+ clients: clientRecords,
1076
+ mailboxes: mailboxPayloads
1077
+ };
1078
+ }
1079
+ function broadcastEvent(event, browsers) {
1080
+ const msg = { type: "hq.event", event };
1081
+ const data = JSON.stringify(msg);
1082
+ for (const ws of browsers) sendGuarded(ws, data);
1083
+ }
1084
+ function broadcastCommandStatus(command, browsers) {
1085
+ const data = JSON.stringify({ type: "hq.command_status", command });
1086
+ for (const ws of browsers) sendGuarded(ws, data);
1087
+ }
1088
+
1089
+ // src/hq-server/trust-boundary.ts
1090
+ import { isTrustDecisionAllowed } from "@wrongstack/core/security";
1091
+ async function authorizeHqCommand(input) {
1092
+ const { boundary, command, commandId, enqueuedBy, authMethod, target } = input;
1093
+ const subject = command.type === "run-command" ? {
1094
+ kind: "command",
1095
+ id: command.command,
1096
+ attributes: { targetClientId: target.clientId }
1097
+ } : {
1098
+ kind: "custom",
1099
+ id: command.type,
1100
+ attributes: { targetClientId: target.clientId }
1101
+ };
1102
+ const request = {
1103
+ version: 1,
1104
+ requestId: commandId,
1105
+ // This request is initiated by the authenticated HQ operator. Browser and
1106
+ // target-client capability checks have already passed before this adapter
1107
+ // is called, so classifying it as an unauthenticated remote client would
1108
+ // make the compatibility policy deny every legitimate run-command.
1109
+ actor: { kind: "user", id: enqueuedBy },
1110
+ surface: "hq",
1111
+ capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
1112
+ subject,
1113
+ risk: command.type === "run-command" ? "critical" : "high",
1114
+ scope: { projectId: target.projectId },
1115
+ authContext: { method: authMethod, principalId: enqueuedBy },
1116
+ metadata: { commandType: command.type, targetKind: target.kind }
1117
+ };
1118
+ const decision = await boundary.evaluate(request);
1119
+ console.warn(
1120
+ JSON.stringify({
1121
+ level: "info",
1122
+ event: "hq.trust_boundary.decision",
1123
+ requestId: commandId,
1124
+ capability: request.capability,
1125
+ decision: decision.kind,
1126
+ policyId: decision.policyId,
1127
+ actor: enqueuedBy,
1128
+ clientId: target.clientId,
1129
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1130
+ })
1131
+ );
1132
+ return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason };
1133
+ }
1134
+
1135
+ // src/hq-server/routes/auth-handlers.ts
1136
+ import { randomUUID } from "node:crypto";
1137
+ import {
1138
+ DEFAULT_HQ_REDACTION_POLICY as DEFAULT_HQ_REDACTION_POLICY2,
1139
+ hashHqPassword,
1140
+ isLoopbackHost,
1141
+ mintHqCookieSecret,
1142
+ mutateHqAuthFile,
1143
+ verifyHqPassword
1144
+ } from "@wrongstack/core/hq";
1145
+ function isLoopbackRequest(req) {
1146
+ const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
1147
+ return address !== void 0 && isLoopbackHost(address);
1148
+ }
1149
+ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
1150
+ const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1151
+ const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0;
1152
+ const localOpenMode = openMode && !requireBrowserAuth && isLoopbackRequest(req);
1153
+ const publicOrigin = trustedPublicOrigins.values().next().value;
1154
+ res.writeHead(200, { "Content-Type": "application/json" });
1155
+ res.end(
1156
+ JSON.stringify({
1157
+ tokenMode: mutableAuth.browserTokens.size > 0,
1158
+ passwordMode: mutableAuth.passwordHash !== void 0,
1159
+ publicRelay: requireBrowserAuth === true || publicOrigin !== void 0,
1160
+ ...publicOrigin !== void 0 ? { publicOrigin } : {},
1161
+ secureCookies: secureCookies === true,
1162
+ loggedIn: auth !== void 0 || localOpenMode,
1163
+ authKind: isCookieAuth(auth) ? "password" : isTokenAuth(auth) ? "token" : localOpenMode ? "open" : void 0
1164
+ })
1165
+ );
1166
+ }
1167
+ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies) {
1168
+ if (!mutableAuth.passwordHash) {
1169
+ res.writeHead(403, { "Content-Type": "application/json" });
1170
+ res.end(
1171
+ JSON.stringify({
1172
+ error: {
1173
+ code: "PASSWORD_NOT_CONFIGURED",
1174
+ message: "Password login is not enabled on this HQ server."
1175
+ }
1176
+ })
1177
+ );
1178
+ return;
1179
+ }
1180
+ const clientIp = req.socket.remoteAddress ?? "unknown";
1181
+ const existing = loginAttempts.get(clientIp);
1182
+ if (existing && existing.blockedUntil > Date.now()) {
1183
+ const retryAfter = Math.ceil((existing.blockedUntil - Date.now()) / 1e3);
1184
+ res.writeHead(429, {
1185
+ "Content-Type": "application/json",
1186
+ "Retry-After": String(retryAfter)
1187
+ });
1188
+ res.end(
1189
+ JSON.stringify({
1190
+ error: {
1191
+ code: "RATE_LIMITED",
1192
+ message: `Too many failed login attempts. Retry after ${retryAfter} seconds.`
1193
+ }
1194
+ })
1195
+ );
1196
+ return;
1197
+ }
1198
+ let body;
1199
+ try {
1200
+ body = JSON.parse(await readRequestBody(req));
1201
+ } catch (error) {
1202
+ writeInvalidBody(res, error);
1203
+ return;
1204
+ }
1205
+ if (typeof body.password !== "string" || body.password.length === 0) {
1206
+ res.writeHead(400, { "Content-Type": "application/json" });
1207
+ res.end(
1208
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } })
1209
+ );
1210
+ return;
1211
+ }
1212
+ const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
1213
+ if (!ok || !mutableAuth.cookieSecret) {
1214
+ const prev = loginAttempts.get(clientIp);
1215
+ const count = (prev?.count ?? 0) + 1;
1216
+ const backoffMs = Math.min(2 ** count * 1e3, 16e3);
1217
+ loginAttempts.set(clientIp, {
1218
+ count,
1219
+ blockedUntil: Date.now() + backoffMs,
1220
+ lastAttempt: Date.now()
1221
+ });
1222
+ res.writeHead(401, { "Content-Type": "application/json" });
1223
+ res.end(
1224
+ JSON.stringify({ error: { code: "INVALID_PASSWORD", message: "Invalid password." } })
1225
+ );
1226
+ return;
1227
+ }
1228
+ loginAttempts.delete(clientIp);
1229
+ const sessionId = randomUUID();
1230
+ sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
1231
+ setHqSessionCookie(
1232
+ res,
1233
+ serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
1234
+ secureCookies
1235
+ );
1236
+ res.writeHead(200, { "Content-Type": "application/json" });
1237
+ res.end(JSON.stringify({ loggedIn: true }));
1238
+ }
1239
+ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
1240
+ const auth = authenticateBrowserRequest(
1241
+ req,
1242
+ new URL(req.url ?? "/", "http://localhost"),
1243
+ mutableAuth,
1244
+ sessions
1245
+ );
1246
+ if (isCookieAuth(auth)) {
1247
+ const cookies = parseCookieHeader(req.headers.cookie);
1248
+ const raw = cookies[HQ_SESSION_COOKIE];
1249
+ if (raw) {
1250
+ const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret ?? "");
1251
+ if (sessionId) sessions.delete(sessionId);
1252
+ }
1253
+ }
1254
+ clearHqSessionCookie(res, secureCookies);
1255
+ res.writeHead(200, { "Content-Type": "application/json" });
1256
+ res.end(JSON.stringify({ loggedIn: false }));
1257
+ }
1258
+ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth) {
1259
+ const auth = authenticateBrowserRequest(
1260
+ req,
1261
+ new URL(req.url ?? "/", "http://localhost"),
1262
+ mutableAuth,
1263
+ sessions
1264
+ );
1265
+ const localOpenBootstrap = auth === void 0 && req.method === "POST" && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && isLoopbackRequest(req);
1266
+ if (auth === void 0 && !localOpenBootstrap) {
1267
+ res.writeHead(401, { "Content-Type": "application/json" });
1268
+ res.end(
1269
+ JSON.stringify({
1270
+ error: {
1271
+ code: "AUTH_REQUIRED",
1272
+ message: "A browser token or password session is required to manage the password."
1273
+ }
1274
+ })
1275
+ );
1276
+ return;
1277
+ }
1278
+ let body = {};
1279
+ try {
1280
+ body = JSON.parse(await readRequestBody(req));
1281
+ } catch {
1282
+ res.writeHead(400, { "Content-Type": "application/json" });
1283
+ res.end(
1284
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } })
1285
+ );
1286
+ return;
1287
+ }
1288
+ if (isCookieAuth(auth) && mutableAuth.passwordHash !== void 0) {
1289
+ const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
1290
+ if (!currentPassword || !await verifyHqPassword(currentPassword, mutableAuth.passwordHash)) {
1291
+ res.writeHead(403, { "Content-Type": "application/json" });
1292
+ res.end(
1293
+ JSON.stringify({
1294
+ error: { code: "INVALID_CURRENT_PASSWORD", message: "Current password is invalid." }
1295
+ })
1296
+ );
1297
+ return;
1298
+ }
1299
+ }
1300
+ if (req.method === "DELETE") {
1301
+ if (requireBrowserAuth && mutableAuth.browserTokens.size === 0) {
1302
+ res.writeHead(409, { "Content-Type": "application/json" });
1303
+ res.end(
1304
+ JSON.stringify({
1305
+ error: {
1306
+ code: "BROWSER_AUTH_REQUIRED",
1307
+ message: "Cannot remove the only browser authentication method while a public relay is active."
1308
+ }
1309
+ })
1310
+ );
1311
+ return;
1312
+ }
1313
+ const next2 = await mutateHqAuthFile(dataDir, (current) => {
1314
+ const updated = { ...current };
1315
+ delete updated.passwordHash;
1316
+ delete updated.cookieSecret;
1317
+ return updated;
1318
+ });
1319
+ applyAuthFile(mutableAuth, next2);
1320
+ sessions.clear();
1321
+ clearHqSessionCookie(res, secureCookies);
1322
+ res.writeHead(200, { "Content-Type": "application/json" });
1323
+ res.end(
1324
+ JSON.stringify({
1325
+ passwordMode: false,
1326
+ tokenMode: mutableAuth.browserTokens.size > 0,
1327
+ loggedIn: !isCookieAuth(auth) && isTokenAuth(auth)
1328
+ })
1329
+ );
1330
+ return;
1331
+ }
1332
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
1333
+ if (newPassword.length < 8 || newPassword.length > 1024) {
1334
+ res.writeHead(400, { "Content-Type": "application/json" });
1335
+ res.end(
1336
+ JSON.stringify({
1337
+ error: { code: "INVALID_PASSWORD", message: "New password must be between 8 and 1024 characters." }
1338
+ })
1339
+ );
1340
+ return;
1341
+ }
1342
+ const passwordHash = await hashHqPassword(newPassword);
1343
+ const cookieSecret = mintHqCookieSecret();
1344
+ const next = await mutateHqAuthFile(dataDir, (current) => ({
1345
+ ...current,
1346
+ passwordHash,
1347
+ cookieSecret
1348
+ }));
1349
+ applyAuthFile(mutableAuth, next);
1350
+ sessions.clear();
1351
+ if (isCookieAuth(auth) || localOpenBootstrap) {
1352
+ const sessionId = randomUUID();
1353
+ sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
1354
+ setHqSessionCookie(
1355
+ res,
1356
+ serializeHqSessionCookie(sessionId, cookieSecret),
1357
+ secureCookies
1358
+ );
1359
+ }
1360
+ res.writeHead(200, { "Content-Type": "application/json" });
1361
+ res.end(
1362
+ JSON.stringify({
1363
+ passwordMode: true,
1364
+ tokenMode: mutableAuth.browserTokens.size > 0,
1365
+ loggedIn: true
1366
+ })
1367
+ );
1368
+ }
1369
+ function applyAuthFile(mutableAuth, next) {
1370
+ mutableAuth.operatorPolicy = {
1371
+ ...DEFAULT_HQ_REDACTION_POLICY2,
1372
+ ...next.redactionPolicy ?? {}
1373
+ };
1374
+ mutableAuth.operatorPolicyOverride = next.redactionPolicy;
1375
+ mutableAuth.browserTokens = new Set((next.browserTokens ?? []).map((t) => t.token));
1376
+ mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map((t) => t.token));
1377
+ mutableAuth.browserTokenObjs = new Map(
1378
+ (next.browserTokens ?? []).map((t) => [
1379
+ t.token,
1380
+ { id: t.id, ...t.capabilities !== void 0 ? { capabilities: t.capabilities } : {} }
1381
+ ])
1382
+ );
1383
+ mutableAuth.clientTokenObjs = new Map(
1384
+ (next.clientTokens ?? []).map((token) => [token.token, token])
1385
+ );
1386
+ mutableAuth.passwordHash = next.passwordHash;
1387
+ mutableAuth.cookieSecret = next.cookieSecret;
1388
+ mutableAuth.alertRules = next.alertRules;
1389
+ }
1390
+ async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore) {
1391
+ if (!mutableAuth.cookieSecret) {
1392
+ res.writeHead(503, { "Content-Type": "application/json" });
1393
+ res.end(
1394
+ JSON.stringify({
1395
+ error: {
1396
+ code: "BOOTSTRAP_UNAVAILABLE",
1397
+ message: "Cookie signing is not configured on this HQ server."
1398
+ }
1399
+ })
1400
+ );
1401
+ return;
1402
+ }
1403
+ let body;
1404
+ try {
1405
+ body = JSON.parse(await readRequestBody(req));
1406
+ } catch (error) {
1407
+ writeInvalidBody(res, error);
1408
+ return;
1409
+ }
1410
+ if (typeof body.code !== "string" || body.code.length === 0) {
1411
+ res.writeHead(400, { "Content-Type": "application/json" });
1412
+ res.end(
1413
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "code is required" } })
1414
+ );
1415
+ return;
1416
+ }
1417
+ const entry = bootstrapStore.consume(body.code);
1418
+ if (!entry) {
1419
+ res.writeHead(401, { "Content-Type": "application/json" });
1420
+ res.end(
1421
+ JSON.stringify({
1422
+ error: {
1423
+ code: "INVALID_OR_EXPIRED_CODE",
1424
+ message: "The bootstrap code is invalid, already used, or expired."
1425
+ }
1426
+ })
1427
+ );
1428
+ return;
1429
+ }
1430
+ const tokenLive = [...mutableAuth.browserTokenObjs.values()].some(
1431
+ (obj) => obj.id === entry.tokenId
1432
+ );
1433
+ if (!tokenLive) {
1434
+ res.writeHead(401, { "Content-Type": "application/json" });
1435
+ res.end(
1436
+ JSON.stringify({
1437
+ error: {
1438
+ code: "TOKEN_REVOKED",
1439
+ message: "The token that issued this bootstrap code has been revoked."
1440
+ }
1441
+ })
1442
+ );
1443
+ return;
1444
+ }
1445
+ const sessionId = randomUUID();
1446
+ sessions.set(sessionId, {
1447
+ createdAt: Date.now(),
1448
+ kind: "token",
1449
+ tokenId: entry.tokenId,
1450
+ ...entry.capabilities !== void 0 ? { capabilities: entry.capabilities } : {}
1451
+ });
1452
+ setHqSessionCookie(
1453
+ res,
1454
+ serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
1455
+ secureCookies
1456
+ );
1457
+ res.writeHead(200, { "Content-Type": "application/json" });
1458
+ res.end(JSON.stringify({ loggedIn: true }));
1459
+ }
1460
+
1461
+ // src/hq-server/routes/system-handlers.ts
1462
+ async function handleApiSystemUpdate(res) {
1463
+ const [{ checkForUpdate }, { detectUpdatePackageName }] = await Promise.all([
1464
+ import("./update-check-UMYTOH7Z.js"),
1465
+ import("./update-YC4TYW2Y.js")
1466
+ ]);
1467
+ const packageName = detectUpdatePackageName();
1468
+ const info = await checkForUpdate({ packageName });
1469
+ res.writeHead(200, {
1470
+ "Content-Type": "application/json",
1471
+ "Cache-Control": "no-store"
1472
+ });
1473
+ res.end(
1474
+ JSON.stringify({
1475
+ ...info,
1476
+ packageName,
1477
+ command: "wstack update"
1478
+ })
1479
+ );
1480
+ }
1481
+ async function handleApiSystemHealth(res, clients, persistence, eventLog) {
1482
+ const now = Date.now();
1483
+ const eventCount = eventLog.length;
1484
+ let timeseriesStoreStatus = "unknown";
1485
+ let kanbanStoreStatus = "unknown";
1486
+ try {
1487
+ const tsOk = await persistence.timeseries.read(now - 3e5);
1488
+ timeseriesStoreStatus = Array.isArray(tsOk) ? "ok" : "degraded";
1489
+ } catch {
1490
+ timeseriesStoreStatus = "degraded";
1491
+ }
1492
+ try {
1493
+ await persistence.kanban.load("__hq_health_probe__");
1494
+ kanbanStoreStatus = "ok";
1495
+ } catch {
1496
+ kanbanStoreStatus = "degraded";
1497
+ }
1498
+ const inMemoryEventLogActive = true;
1499
+ const allOk = inMemoryEventLogActive && timeseriesStoreStatus === "ok" && kanbanStoreStatus === "ok";
1500
+ const clientCount = clients.size;
1501
+ const connectedClients = [...clients.values()].filter(
1502
+ (c) => Date.now() - new Date(c.lastSeenAt).getTime() < 6e4
1503
+ ).length;
1504
+ res.writeHead(200, {
1505
+ "Content-Type": "application/json",
1506
+ "Cache-Control": "no-store"
1507
+ });
1508
+ res.end(
1509
+ JSON.stringify({
1510
+ status: allOk ? "healthy" : "degraded",
1511
+ uptime: {
1512
+ serverTime: (/* @__PURE__ */ new Date()).toISOString(),
1513
+ eventLogSize: eventCount
1514
+ },
1515
+ stores: {
1516
+ events: "ok",
1517
+ timeseries: timeseriesStoreStatus,
1518
+ kanban: kanbanStoreStatus
1519
+ },
1520
+ connections: {
1521
+ total: clientCount,
1522
+ active: connectedClients,
1523
+ stale: clientCount - connectedClients
1524
+ }
1525
+ })
1526
+ );
1527
+ }
1528
+ async function handleApiCommandsAudit(req, res, auditLog) {
1529
+ const url = new URL(req.url ?? "/", "http://localhost");
1530
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
1531
+ const limit = Math.min(1e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
1532
+ res.writeHead(200, { "Content-Type": "application/json" });
1533
+ res.end(JSON.stringify({ commands: auditLog.recent(limit) }));
1534
+ }
1535
+ async function handleApiAlerts(req, res, alertEngine) {
1536
+ const url = new URL(req.url ?? "/", "http://localhost");
1537
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "100", 10);
1538
+ const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 100));
1539
+ res.writeHead(200, { "Content-Type": "application/json" });
1540
+ res.end(
1541
+ JSON.stringify({
1542
+ active: alertEngine.activeAlerts(),
1543
+ history: alertEngine.recentAlerts(limit)
1544
+ })
1545
+ );
1546
+ }
1547
+
1548
+ // src/hq-server/routes/session-handlers.ts
1549
+ import * as path3 from "node:path";
1550
+ import {
1551
+ buildTranscriptFromEvents,
1552
+ resolveHqDataDir as resolveHqDataDir3
1553
+ } from "@wrongstack/core/hq";
1554
+ async function handleApiSessions(res) {
1555
+ const { SessionRegistry } = await import("@wrongstack/core/storage");
1556
+ const globalRoot = path3.dirname(resolveHqDataDir3());
1557
+ try {
1558
+ const registry = new SessionRegistry(globalRoot);
1559
+ const sessions = await registry.list();
1560
+ const result = sessions.filter((s) => s.status !== "stale").map((s) => ({
1561
+ sessionId: s.sessionId,
1562
+ projectSlug: s.projectSlug,
1563
+ projectName: s.projectName,
1564
+ projectRoot: s.projectRoot,
1565
+ workingDir: s.workingDir,
1566
+ status: s.status,
1567
+ pid: s.pid,
1568
+ startedAt: s.startedAt,
1569
+ lastHeartbeatAt: s.lastHeartbeatAt,
1570
+ agentCount: s.agentCount,
1571
+ agents: s.agents.map((a) => ({
1572
+ id: a.id,
1573
+ name: a.name,
1574
+ status: a.status,
1575
+ currentTool: a.currentTool,
1576
+ iterations: a.iterations,
1577
+ toolCalls: a.toolCalls,
1578
+ lastActivityAt: a.lastActivityAt
1579
+ }))
1580
+ }));
1581
+ res.writeHead(200, { "Content-Type": "application/json" });
1582
+ res.end(JSON.stringify(result));
1583
+ } catch (err) {
1584
+ res.writeHead(500, { "Content-Type": "application/json" });
1585
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
1586
+ }
1587
+ }
1588
+ async function handleApiSessionEvents(req, res, match, transcripts) {
1589
+ const { SessionRegistry, DefaultSessionStore } = await import("@wrongstack/core/storage");
1590
+ const { resolveWstackPaths } = await import("@wrongstack/core/utils");
1591
+ const url = new URL(req.url ?? "/", "http://localhost");
1592
+ const full = url.searchParams.get("full") === "1";
1593
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
1594
+ const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
1595
+ const sessionId = decodePathSegment(match[1]);
1596
+ if (sessionId === null) {
1597
+ res.writeHead(400, { "Content-Type": "application/json" });
1598
+ res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
1599
+ return;
1600
+ }
1601
+ const globalRoot = path3.dirname(resolveHqDataDir3());
1602
+ try {
1603
+ const registry = new SessionRegistry(globalRoot);
1604
+ const entry = await registry.get(sessionId).catch(() => null);
1605
+ let entries = [];
1606
+ let source = "stream";
1607
+ let status;
1608
+ let clientType;
1609
+ let projectName;
1610
+ if (entry && "projectRoot" in entry) {
1611
+ const paths = resolveWstackPaths({
1612
+ projectRoot: entry.projectRoot,
1613
+ globalRoot
1614
+ });
1615
+ const store = new DefaultSessionStore({ dir: paths.projectSessions });
1616
+ const data = await store.load(sessionId).catch(() => null);
1617
+ if (data) {
1618
+ entries = buildTranscriptFromEvents(
1619
+ data.events.map((e) => e)
1620
+ );
1621
+ source = "disk";
1622
+ status = entry.status;
1623
+ clientType = entry.clientType;
1624
+ projectName = entry.projectName;
1625
+ }
1626
+ }
1627
+ if (entries.length === 0) {
1628
+ const ring = transcripts.get(sessionId);
1629
+ if (!ring && !entry) {
1630
+ res.writeHead(404, { "Content-Type": "application/json" });
1631
+ res.end(JSON.stringify({ error: "Session not found" }));
1632
+ return;
1633
+ }
1634
+ entries = ring ? ring.entries : [];
1635
+ source = "stream";
1636
+ if (entry) {
1637
+ status = entry.status;
1638
+ clientType = entry.clientType;
1639
+ projectName = entry.projectName;
1640
+ }
1641
+ }
1642
+ const total = entries.length;
1643
+ const tail = full ? entries : entries.slice(-limit);
1644
+ res.writeHead(200, { "Content-Type": "application/json" });
1645
+ res.end(
1646
+ JSON.stringify({
1647
+ sessionId,
1648
+ source,
1649
+ ...status !== void 0 ? { status } : {},
1650
+ ...clientType !== void 0 ? { clientType } : {},
1651
+ ...projectName !== void 0 ? { projectName } : {},
1652
+ total,
1653
+ entries: tail
1654
+ })
1655
+ );
1656
+ } catch (err) {
1657
+ res.writeHead(500, { "Content-Type": "application/json" });
1658
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
1659
+ }
1660
+ }
1661
+ async function handleApiSessionAgentMessages(req, res, match, agentMessages) {
1662
+ const url = new URL(req.url ?? "/", "http://localhost");
1663
+ const sid = decodePathSegment(match[1]);
1664
+ const aid = decodePathSegment(match[2]);
1665
+ if (sid === null || aid === null) {
1666
+ res.writeHead(400, { "Content-Type": "application/json" });
1667
+ res.end(JSON.stringify({ error: "invalid session or agent id encoding" }));
1668
+ return;
1669
+ }
1670
+ const full = url.searchParams.get("full") === "1";
1671
+ const disk = await readLocalSubagentTranscript(sid, aid);
1672
+ const source = disk !== null ? "disk" : "stream";
1673
+ const all = disk !== null ? disk : agentMessages.get(agentRingKey(sid, aid)) ?? agentMessages.get(aid) ?? [];
1674
+ const entries = full ? all : all.slice(-200);
1675
+ res.writeHead(200, { "Content-Type": "application/json" });
1676
+ res.end(JSON.stringify({ subagentId: aid, sessionId: sid, source, total: all.length, entries }));
1677
+ }
1678
+ async function handleApiAgentMessages(req, res, match, agentMessages) {
1679
+ const url = new URL(req.url ?? "/", "http://localhost");
1680
+ const id = decodePathSegment(match[1]);
1681
+ if (id === null) {
1682
+ res.writeHead(400, { "Content-Type": "application/json" });
1683
+ res.end(JSON.stringify({ error: "invalid agent id encoding" }));
1684
+ return;
1685
+ }
1686
+ const full = url.searchParams.get("full") === "1";
1687
+ const merged = [];
1688
+ for (const [key, ring] of agentMessages) {
1689
+ if (key === id || key.endsWith(`::${id}`)) merged.push(...ring);
1690
+ }
1691
+ merged.sort((a, b) => a.ts.localeCompare(b.ts));
1692
+ const entries = full ? merged : merged.slice(-200);
1693
+ res.writeHead(200, { "Content-Type": "application/json" });
1694
+ res.end(JSON.stringify({ subagentId: id, total: merged.length, entries }));
1695
+ }
1696
+
1697
+ // src/hq-server/project-root.ts
1698
+ import { deriveHqProjectId } from "@wrongstack/core/hq";
1699
+ function resolveHqProjectRoot(globalRoot, ids) {
1700
+ const fn = async () => {
1701
+ const { SessionRegistry } = await import("@wrongstack/core/storage");
1702
+ try {
1703
+ const registry = new SessionRegistry(globalRoot);
1704
+ if (typeof ids.sessionId === "string") {
1705
+ const entry = await registry.get(ids.sessionId).catch(() => null);
1706
+ if (entry?.projectRoot) return entry.projectRoot;
1707
+ }
1708
+ if (typeof ids.projectId === "string") {
1709
+ const { createHash } = await import("node:crypto");
1710
+ const all = await registry.list().catch(() => []);
1711
+ const projectIds = /* @__PURE__ */ new Map();
1712
+ const projectIdForRoot = (projectRoot) => {
1713
+ const cached = projectIds.get(projectRoot);
1714
+ if (cached !== void 0) return cached;
1715
+ const derived = deriveHqProjectId(projectRoot);
1716
+ projectIds.set(projectRoot, derived);
1717
+ return derived;
1718
+ };
1719
+ const match = all.find(
1720
+ (e) => e.projectSlug === ids.projectId || projectIdForRoot(e.projectRoot) === ids.projectId || createHash("sha256").update(e.projectRoot).digest("hex").slice(0, 12) === ids.projectId
1721
+ );
1722
+ if (match) return match.projectRoot;
1723
+ }
1724
+ } catch {
1725
+ }
1726
+ return void 0;
1727
+ };
1728
+ return fn();
1729
+ }
1730
+
1731
+ // src/hq-server/routes.ts
1732
+ var setHqSecurityHeaders2 = setHqSecurityHeaders;
1733
+ var hasTrustedBrowserOrigin2 = hasTrustedBrowserOrigin;
1734
+ var authenticateBrowserRequest2 = authenticateBrowserRequest;
1735
+ var isTokenAuth2 = isTokenAuth;
1736
+ var isCookieAuth2 = isCookieAuth;
1737
+ var parseHqSessionCookie2 = parseHqSessionCookie;
1738
+ var parseCookieHeader2 = parseCookieHeader;
1739
+ var HQ_SESSION_COOKIE2 = HQ_SESSION_COOKIE;
1740
+ var decodePathSegment2 = decodePathSegment;
1741
+ var readRequestBody2 = readRequestBody;
1742
+ var writeInvalidBody2 = writeInvalidBody;
1743
+ var sanitizeApiError2 = sanitizeApiError;
1744
+ var buildHttpUrl2 = buildHttpUrl;
1745
+ var buildBootstrapHttpUrl2 = buildBootstrapHttpUrl;
1746
+ var buildClientWsUrl2 = buildClientWsUrl;
1747
+ var agentRingKey2 = agentRingKey;
1748
+ var agentMessageToEntry2 = agentMessageToEntry;
1749
+ var readLocalSubagentTranscript2 = readLocalSubagentTranscript;
1750
+ function createHqRouter(deps) {
1751
+ const {
1752
+ host,
1753
+ listeningPort,
1754
+ trustedPublicOrigins,
1755
+ mutableAuth,
1756
+ sessions,
1757
+ loginAttempts,
1758
+ clients,
1759
+ browsers,
1760
+ eventLog,
1761
+ transcripts,
1762
+ agentMessages,
1763
+ alertEngine,
1764
+ auditLog,
1765
+ persistence,
1766
+ dataDir,
1767
+ hqSessionTag,
1768
+ requireBrowserAuth,
1769
+ secureCookies,
1770
+ authorizeMailboxGateway,
1771
+ getMailboxGateway,
1772
+ getTokenStats,
1773
+ trustBoundary,
1774
+ bootstrapStore
1775
+ } = deps;
1776
+ return async (req, res) => {
1777
+ try {
1778
+ const url = new URL(req.url ?? "/", `http://${host}:${listeningPort}`);
1779
+ setHqSecurityHeaders2(res);
1780
+ if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
1781
+ res.writeHead(403, { "Content-Type": "application/json" });
1782
+ res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
1783
+ return;
1784
+ }
1785
+ if (url.pathname.startsWith("/api/") && url.pathname !== "/api/auth/status" && url.pathname !== "/api/login" && url.pathname !== "/api/auth/bootstrap" && (requireBrowserAuth || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash)) {
1786
+ const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
1787
+ if (!auth) {
1788
+ const tokenOnly = mutableAuth.browserTokens.size > 0 && !mutableAuth.passwordHash;
1789
+ res.writeHead(401, { "Content-Type": "application/json" });
1790
+ res.end(
1791
+ JSON.stringify({
1792
+ error: {
1793
+ code: tokenOnly ? "INVALID_TOKEN" : "UNAUTHORIZED",
1794
+ message: tokenOnly ? "A valid ?token= or Authorization: Bearer is required for HTTP access in browser token mode." : "A valid browser token or password session is required."
1795
+ }
1796
+ })
1797
+ );
1798
+ return;
1799
+ }
1800
+ }
1801
+ const hqDistDir = resolveHqDistDir();
1802
+ const isApiOrWsPath = url.pathname.startsWith("/api/") || url.pathname.startsWith("/ws/");
1803
+ if (hqDistDir !== null && !isApiOrWsPath) {
1804
+ const served = await serveHqStatic(req, res, url.pathname, hqDistDir);
1805
+ if (served.handled) return;
1806
+ }
1807
+ if (url.pathname === "/" || url.pathname === "/index.html") {
1808
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1809
+ res.end(HQ_HTML);
1810
+ return;
1811
+ }
1812
+ if (url.pathname === "/api/auth/status" && req.method === "GET") {
1813
+ await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
1814
+ return;
1815
+ }
1816
+ if (url.pathname === "/api/login" && req.method === "POST") {
1817
+ await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies);
1818
+ return;
1819
+ }
1820
+ if (url.pathname === "/api/auth/bootstrap" && req.method === "POST") {
1821
+ if (bootstrapStore) {
1822
+ await handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore);
1823
+ } else {
1824
+ res.writeHead(503, { "Content-Type": "application/json" });
1825
+ res.end(
1826
+ JSON.stringify({
1827
+ error: {
1828
+ code: "BOOTSTRAP_UNAVAILABLE",
1829
+ message: "Bootstrap exchange is not enabled on this HQ server."
1830
+ }
1831
+ })
1832
+ );
1833
+ }
1834
+ return;
1835
+ }
1836
+ if (url.pathname === "/api/logout" && req.method === "POST") {
1837
+ await handleApiLogout(req, res, mutableAuth, sessions, secureCookies);
1838
+ return;
1839
+ }
1840
+ if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
1841
+ await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth);
1842
+ return;
1843
+ }
1844
+ if (url.pathname === "/api/snapshot" && req.method === "GET") {
1845
+ res.writeHead(200, { "Content-Type": "application/json" });
1846
+ res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
1847
+ return;
1848
+ }
1849
+ if (url.pathname === "/api/system/update" && req.method === "GET") {
1850
+ await handleApiSystemUpdate(res);
1851
+ return;
1852
+ }
1853
+ if (url.pathname === "/api/system/health" && req.method === "GET") {
1854
+ await handleApiSystemHealth(res, clients, persistence, eventLog);
1855
+ return;
1856
+ }
1857
+ const mailboxGatewayMatch = url.pathname.match(
1858
+ /^\/api\/projects\/([^/]+)\/mailbox(?:\/(.*))?$/
1859
+ );
1860
+ if (mailboxGatewayMatch) {
1861
+ await handleMailboxGateway(req, res, mailboxGatewayMatch, authorizeMailboxGateway, getMailboxGateway, dataDir);
1862
+ return;
1863
+ }
1864
+ const projectKanbanMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/kanban$/);
1865
+ if (projectKanbanMatch && req.method === "GET") {
1866
+ const projectId = decodePathSegment2(projectKanbanMatch[1]);
1867
+ if (projectId === null) {
1868
+ res.writeHead(400, { "Content-Type": "application/json" });
1869
+ res.end(JSON.stringify({ error: { code: "VALIDATION_ERROR", message: "Invalid project id." } }));
1870
+ return;
1871
+ }
1872
+ const snapshot = await persistence.kanban.load(projectId);
1873
+ res.writeHead(200, { "Content-Type": "application/json" });
1874
+ res.end(JSON.stringify(snapshot));
1875
+ return;
1876
+ }
1877
+ if (url.pathname.startsWith("/api/projects/") && req.method === "GET") {
1878
+ await handleApiProjectDetail(req, res, url, clients);
1879
+ return;
1880
+ }
1881
+ if (url.pathname === "/api/fleet" && req.method === "GET") {
1882
+ res.writeHead(200, { "Content-Type": "application/json" });
1883
+ res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
1884
+ return;
1885
+ }
1886
+ if (url.pathname === "/api/events" && req.method === "GET") {
1887
+ await handleApiEvents(req, res, persistence);
1888
+ return;
1889
+ }
1890
+ if (url.pathname === "/api/trends/cost" && req.method === "GET") {
1891
+ await handleApiTrendsCost(req, res, persistence);
1892
+ return;
1893
+ }
1894
+ if (url.pathname === "/api/command" && req.method === "POST") {
1895
+ await handleApiCommand(
1896
+ req,
1897
+ res,
1898
+ url,
1899
+ mutableAuth,
1900
+ sessions,
1901
+ clients,
1902
+ browsers,
1903
+ auditLog,
1904
+ trustBoundary
1905
+ );
1906
+ return;
1907
+ }
1908
+ if (url.pathname === "/api/mailbox-send" && req.method === "POST") {
1909
+ await handleApiMailboxSend(req, res, mutableAuth, sessions, dataDir, getMailboxGateway, hqSessionTag);
1910
+ return;
1911
+ }
1912
+ const mailboxActionMatch = url.pathname.match(
1913
+ /^\/api\/mailbox\/messages\/([^/]+)\/action$/
1914
+ );
1915
+ if (mailboxActionMatch && req.method === "POST") {
1916
+ await handleMailboxAction(req, res, mailboxActionMatch, mutableAuth, sessions, dataDir, getMailboxGateway);
1917
+ return;
1918
+ }
1919
+ if (url.pathname === "/api/commands" && req.method === "GET") {
1920
+ await handleApiCommandsAudit(req, res, auditLog);
1921
+ return;
1922
+ }
1923
+ if (url.pathname === "/api/alerts" && req.method === "GET") {
1924
+ await handleApiAlerts(req, res, alertEngine);
1925
+ return;
1926
+ }
1927
+ if (url.pathname === "/api/sessions" && req.method === "GET") {
1928
+ await handleApiSessions(res);
1929
+ return;
1930
+ }
1931
+ const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
1932
+ if (eventsMatch && req.method === "GET") {
1933
+ await handleApiSessionEvents(req, res, eventsMatch, transcripts);
1934
+ return;
1935
+ }
1936
+ const sessionAgentMsgMatch = url.pathname.match(
1937
+ /^\/api\/sessions\/([^/]+)\/agents\/([^/]+)\/messages$/
1938
+ );
1939
+ if (sessionAgentMsgMatch && req.method === "GET") {
1940
+ await handleApiSessionAgentMessages(req, res, sessionAgentMsgMatch, agentMessages);
1941
+ return;
1942
+ }
1943
+ const agentMsgMatch = url.pathname.match(/^\/api\/agents\/([^/]+)\/messages$/);
1944
+ if (agentMsgMatch && req.method === "GET") {
1945
+ await handleApiAgentMessages(req, res, agentMsgMatch, agentMessages);
1946
+ return;
1947
+ }
1948
+ res.writeHead(404, { "Content-Type": "text/plain" });
1949
+ res.end("Not found");
1950
+ } catch (err) {
1951
+ console.error(
1952
+ JSON.stringify({
1953
+ level: "error",
1954
+ event: "hq.http_handler_error",
1955
+ message: String(err),
1956
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1957
+ })
1958
+ );
1959
+ if (!res.headersSent) {
1960
+ res.writeHead(500, { "Content-Type": "application/json" });
1961
+ res.end(JSON.stringify({ error: "Internal server error" }));
1962
+ }
1963
+ }
1964
+ };
1965
+ }
1966
+ async function handleMailboxGateway(_req, res, match, authorizeMailboxGateway, getMailboxGateway, dataDir) {
1967
+ const projectId = decodePathSegment2(match[1]);
1968
+ if (projectId === null || projectId.length === 0) {
1969
+ res.writeHead(400, { "Content-Type": "application/json" });
1970
+ res.end(
1971
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "invalid projectId encoding" } })
1972
+ );
1973
+ return;
1974
+ }
1975
+ const gatewayGlobalRoot = path4.dirname(dataDir);
1976
+ const preliminaryAccess = authorizeMailboxGateway(_req, `project:${projectId}`);
1977
+ if (!preliminaryAccess.allowed) {
1978
+ res.writeHead(preliminaryAccess.status ?? 401, {
1979
+ "Content-Type": "application/json; charset=utf-8",
1980
+ "Cache-Control": "no-store"
1981
+ });
1982
+ res.end(JSON.stringify(preliminaryAccess.body));
1983
+ return;
1984
+ }
1985
+ const projectRoot = await resolveHqProjectRoot(gatewayGlobalRoot, { projectId });
1986
+ if (!projectRoot) {
1987
+ res.writeHead(404, { "Content-Type": "application/json" });
1988
+ res.end(
1989
+ JSON.stringify({ error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` } })
1990
+ );
1991
+ return;
1992
+ }
1993
+ const suffix = match[2];
1994
+ const rawUrl = _req.url ?? "";
1995
+ const queryIndex = rawUrl.indexOf("?");
1996
+ const querySuffix = queryIndex === -1 ? "" : rawUrl.slice(queryIndex);
1997
+ const canonicalPath = suffix ? `/mailbox/${suffix}${querySuffix}` : `/mailbox${querySuffix}`;
1998
+ const projectDir = resolveProjectDir(projectRoot, gatewayGlobalRoot);
1999
+ await getMailboxGateway(projectDir).router.handle(_req, res, canonicalPath);
2000
+ }
2001
+ async function handleApiProjectDetail(_req, res, url, clients) {
2002
+ const projectId = decodePathSegment2(url.pathname.slice("/api/projects/".length));
2003
+ if (projectId === null) {
2004
+ res.writeHead(400, { "Content-Type": "application/json" });
2005
+ res.end(
2006
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "invalid projectId encoding" } })
2007
+ );
2008
+ return;
2009
+ }
2010
+ if (!projectId) {
2011
+ res.writeHead(400, { "Content-Type": "application/json" });
2012
+ res.end(
2013
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } })
2014
+ );
2015
+ return;
2016
+ }
2017
+ const detail = buildProjectDetail(clients, projectId);
2018
+ if (!detail) {
2019
+ res.writeHead(404, { "Content-Type": "application/json" });
2020
+ res.end(
2021
+ JSON.stringify({ error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` } })
2022
+ );
2023
+ return;
2024
+ }
2025
+ res.writeHead(200, { "Content-Type": "application/json" });
2026
+ res.end(JSON.stringify(detail));
2027
+ }
2028
+ async function handleApiEvents(req, res, persistence) {
2029
+ const url = new URL(req.url ?? "/", "http://localhost");
2030
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
2031
+ const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
2032
+ const typeFilter = url.searchParams.get("type") ?? void 0;
2033
+ const events = await persistence.eventLog.recent(limit, typeFilter);
2034
+ res.writeHead(200, { "Content-Type": "application/json" });
2035
+ res.end(JSON.stringify({ events, total: events.length }));
2036
+ }
2037
+ async function handleApiTrendsCost(req, res, persistence) {
2038
+ const url = new URL(req.url ?? "/", "http://localhost");
2039
+ const rawSince = Number.parseInt(url.searchParams.get("since") ?? "0", 10);
2040
+ const since = Number.isFinite(rawSince) ? rawSince : 0;
2041
+ const samples = await persistence.timeseries.read(since);
2042
+ res.writeHead(200, { "Content-Type": "application/json" });
2043
+ res.end(JSON.stringify({ samples }));
2044
+ }
2045
+ async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, browsers, auditLog, trustBoundary) {
2046
+ const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
2047
+ const inBrowserTokenMode = mutableAuth.browserTokens.size > 0;
2048
+ const inPasswordMode = mutableAuth.passwordHash !== void 0;
2049
+ if ((inBrowserTokenMode || inPasswordMode) && !auth) {
2050
+ res.writeHead(401, { "Content-Type": "application/json" });
2051
+ res.end(JSON.stringify({ error: "unauthorized" }));
2052
+ return;
2053
+ }
2054
+ const canEnqueue = isCookieAuth2(auth) ? auth.tokenId === void 0 || auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : !inBrowserTokenMode;
2055
+ if (!canEnqueue) {
2056
+ res.writeHead(403, { "Content-Type": "application/json" });
2057
+ res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
2058
+ return;
2059
+ }
2060
+ let body;
2061
+ try {
2062
+ body = JSON.parse(await readRequestBody2(req));
2063
+ } catch (error) {
2064
+ writeInvalidBody2(res, error);
2065
+ return;
2066
+ }
2067
+ if (typeof body.clientId !== "string" || typeof body.type !== "string") {
2068
+ res.writeHead(400, { "Content-Type": "application/json" });
2069
+ res.end(JSON.stringify({ error: "missing clientId or type" }));
2070
+ return;
2071
+ }
2072
+ let target;
2073
+ for (const c of clients.values()) {
2074
+ if (c.clientId === body.clientId) {
2075
+ target = c;
2076
+ break;
2077
+ }
2078
+ }
2079
+ if (target === void 0) {
2080
+ res.writeHead(404, { "Content-Type": "application/json" });
2081
+ res.end(JSON.stringify({ error: "client not connected", clientId: body.clientId }));
2082
+ return;
2083
+ }
2084
+ if (!target.capabilities.includes("control.receive")) {
2085
+ res.writeHead(409, { "Content-Type": "application/json" });
2086
+ res.end(
2087
+ JSON.stringify({
2088
+ error: "client does not accept control commands",
2089
+ clientId: body.clientId
2090
+ })
2091
+ );
2092
+ return;
2093
+ }
2094
+ const commandId = randomUUID2();
2095
+ const queued = {
2096
+ commandId,
2097
+ type: body.type,
2098
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2099
+ payload: body.payload ?? {},
2100
+ requiresAck: true
2101
+ };
2102
+ const validated = validateHqCommand(queued);
2103
+ if (validated === null) {
2104
+ res.writeHead(400, { "Content-Type": "application/json" });
2105
+ res.end(
2106
+ JSON.stringify({ error: "unrecognized or malformed command", type: body.type })
2107
+ );
2108
+ return;
2109
+ }
2110
+ if (validated.type === "run-command" && !tokenHasCapability(target.authToken, "control.execute")) {
2111
+ res.writeHead(403, { "Content-Type": "application/json" });
2112
+ res.end(
2113
+ JSON.stringify({
2114
+ error: "forbidden: target client token lacks control.execute capability"
2115
+ })
2116
+ );
2117
+ return;
2118
+ }
2119
+ const enqueuedBy = isCookieAuth2(auth) ? auth.tokenId ?? "password-session" : isTokenAuth2(auth) ? auth.id : "open-mode";
2120
+ const authorization = await authorizeHqCommand({
2121
+ boundary: trustBoundary,
2122
+ command: validated,
2123
+ commandId,
2124
+ enqueuedBy,
2125
+ authMethod: isCookieAuth2(auth) ? "session" : "bearer-token",
2126
+ target
2127
+ });
2128
+ if (!authorization.allowed) {
2129
+ res.writeHead(403, { "Content-Type": "application/json" });
2130
+ res.end(JSON.stringify({ error: `command denied: ${authorization.reason}` }));
2131
+ return;
2132
+ }
2133
+ target.commandQueue.push(queued);
2134
+ if (target.commandQueue.length > 200)
2135
+ target.commandQueue.splice(0, target.commandQueue.length - 200);
2136
+ const auditEntry = {
2137
+ commandId,
2138
+ type: validated.type,
2139
+ clientId: target.clientId,
2140
+ enqueuedBy,
2141
+ enqueuedAt: queued.createdAt,
2142
+ status: "queued"
2143
+ };
2144
+ auditLog.record(auditEntry);
2145
+ broadcastCommandStatus(auditEntry, browsers);
2146
+ res.writeHead(202, { "Content-Type": "application/json" });
2147
+ res.end(JSON.stringify({ commandId, queued: true, clientId: target.clientId }));
2148
+ }
2149
+ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, getMailboxGateway, hqSessionTag) {
2150
+ const auth = authenticateBrowserRequest2(
2151
+ _req,
2152
+ new URL(_req.url ?? "/", "http://localhost"),
2153
+ mutableAuth,
2154
+ sessions
2155
+ );
2156
+ const canEnqueue = isCookieAuth2(auth) ? auth.tokenId === void 0 || auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : mutableAuth.browserTokens.size === 0;
2157
+ if (!canEnqueue) {
2158
+ res.writeHead(403, { "Content-Type": "application/json" });
2159
+ res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
2160
+ return;
2161
+ }
2162
+ let mbody;
2163
+ try {
2164
+ mbody = JSON.parse(await readRequestBody2(_req));
2165
+ } catch (error) {
2166
+ writeInvalidBody2(res, error);
2167
+ return;
2168
+ }
2169
+ if (typeof mbody.type !== "string" || typeof mbody.body !== "string") {
2170
+ res.writeHead(400, { "Content-Type": "application/json" });
2171
+ res.end(JSON.stringify({ error: "missing type or body" }));
2172
+ return;
2173
+ }
2174
+ if (typeof mbody.sessionId !== "string" && typeof mbody.projectId !== "string") {
2175
+ res.writeHead(400, { "Content-Type": "application/json" });
2176
+ res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
2177
+ return;
2178
+ }
2179
+ if (mbody.audience !== void 0 && mbody.audience !== "all" && mbody.audience !== "leaders") {
2180
+ res.writeHead(400, { "Content-Type": "application/json" });
2181
+ res.end(JSON.stringify({ error: "audience must be all or leaders" }));
2182
+ return;
2183
+ }
2184
+ const mbGlobalRoot = path4.dirname(dataDir);
2185
+ const projectRoot = await resolveHqProjectRoot(mbGlobalRoot, {
2186
+ sessionId: mbody.sessionId,
2187
+ projectId: mbody.projectId
2188
+ });
2189
+ if (typeof projectRoot !== "string" || projectRoot.length === 0) {
2190
+ res.writeHead(404, { "Content-Type": "application/json" });
2191
+ res.end(JSON.stringify({ error: "could not resolve target project mailbox" }));
2192
+ return;
2193
+ }
2194
+ const to = typeof mbody.to === "string" ? mbody.to : "leader";
2195
+ const subject = typeof mbody.subject === "string" ? mbody.subject : "HQ prompt";
2196
+ const priority = mbody.priority === "high" ? "high" : mbody.priority === "low" ? "low" : "normal";
2197
+ const audience = mbody.audience === "leaders" ? "leaders" : "all";
2198
+ const mailboxType = mbody.type === "queue" ? "note" : ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review"].includes(
2199
+ mbody.type
2200
+ ) ? mbody.type : void 0;
2201
+ if (mailboxType === void 0) {
2202
+ res.writeHead(400, { "Content-Type": "application/json" });
2203
+ res.end(
2204
+ JSON.stringify({
2205
+ error: "unrecognized or malformed mailbox message",
2206
+ type: mbody.type
2207
+ })
2208
+ );
2209
+ return;
2210
+ }
2211
+ if ((mailboxType === "assign" || mailboxType === "steer") && (to === "*" || to.trim().toLowerCase() === "all")) {
2212
+ res.writeHead(400, { "Content-Type": "application/json" });
2213
+ res.end(JSON.stringify({ error: `${mailboxType} requires a specific recipient` }));
2214
+ return;
2215
+ }
2216
+ try {
2217
+ const projectDir = resolveProjectDir(projectRoot, mbGlobalRoot);
2218
+ const mailbox = getMailboxGateway(projectDir).mailbox;
2219
+ const from = `hq@${hqSessionTag}`;
2220
+ const deliveryTo = mailboxType === "broadcast" ? "all" : to;
2221
+ const sent = await mailbox.send({
2222
+ from,
2223
+ to: deliveryTo,
2224
+ type: mailboxType,
2225
+ subject,
2226
+ body: mbody.body,
2227
+ priority,
2228
+ audience
2229
+ });
2230
+ res.writeHead(202, { "Content-Type": "application/json" });
2231
+ res.end(
2232
+ JSON.stringify({
2233
+ delivered: true,
2234
+ messageId: sent?.id,
2235
+ to: sent?.to ?? deliveryTo,
2236
+ type: mailboxType,
2237
+ audience
2238
+ })
2239
+ );
2240
+ } catch (err) {
2241
+ res.writeHead(500, { "Content-Type": "application/json" });
2242
+ res.end(JSON.stringify({ error: "mailbox write failed", detail: String(err) }));
2243
+ }
2244
+ }
2245
+ async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, dataDir, getMailboxGateway) {
2246
+ const MAILBOX_ACTIONS = [
2247
+ "mark-read",
2248
+ "acknowledge",
2249
+ "reopen",
2250
+ "soft-delete",
2251
+ "restore"
2252
+ ];
2253
+ let abody;
2254
+ try {
2255
+ abody = JSON.parse(await readRequestBody2(_req));
2256
+ } catch (error) {
2257
+ writeInvalidBody2(res, error);
2258
+ return;
2259
+ }
2260
+ const mailId = decodePathSegment2(match[1]);
2261
+ if (mailId === null) {
2262
+ res.writeHead(400, { "Content-Type": "application/json" });
2263
+ res.end(JSON.stringify({ error: "invalid mailId encoding" }));
2264
+ return;
2265
+ }
2266
+ const action = MAILBOX_ACTIONS.find((a) => a === abody.action);
2267
+ if (action === void 0) {
2268
+ res.writeHead(400, { "Content-Type": "application/json" });
2269
+ res.end(JSON.stringify({ error: "unrecognized action", action: abody.action }));
2270
+ return;
2271
+ }
2272
+ if (typeof abody.readerId !== "string" || abody.readerId.length === 0) {
2273
+ res.writeHead(400, { "Content-Type": "application/json" });
2274
+ res.end(JSON.stringify({ error: "missing readerId" }));
2275
+ return;
2276
+ }
2277
+ if (typeof abody.sessionId !== "string" && typeof abody.projectId !== "string") {
2278
+ res.writeHead(400, { "Content-Type": "application/json" });
2279
+ res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
2280
+ return;
2281
+ }
2282
+ const actGlobalRoot = path4.dirname(dataDir);
2283
+ const projectRoot = await resolveHqProjectRoot(actGlobalRoot, {
2284
+ sessionId: abody.sessionId,
2285
+ projectId: abody.projectId
2286
+ });
2287
+ if (typeof projectRoot !== "string" || projectRoot.length === 0) {
2288
+ res.writeHead(404, { "Content-Type": "application/json" });
2289
+ res.end(JSON.stringify({ error: "could not resolve target project mailbox" }));
2290
+ return;
2291
+ }
2292
+ try {
2293
+ const { actionToAckInput } = await import("@wrongstack/core/coordination");
2294
+ const projectDir = resolveProjectDir(projectRoot, actGlobalRoot);
2295
+ const mailbox = getMailboxGateway(projectDir).mailbox;
2296
+ const readerId = abody.readerId;
2297
+ const message = action === "soft-delete" ? await mailbox.softDelete(mailId, readerId) : action === "restore" ? await mailbox.restore(mailId) : await mailbox.ack(actionToAckInput(action, { action, mailId, readerId }));
2298
+ if (message === null) {
2299
+ res.writeHead(404, { "Content-Type": "application/json" });
2300
+ res.end(JSON.stringify({ error: "message not found", mailId }));
2301
+ return;
2302
+ }
2303
+ res.writeHead(200, { "Content-Type": "application/json" });
2304
+ res.end(JSON.stringify({ action, mailId, message: null, changed: true }));
2305
+ } catch (err) {
2306
+ res.writeHead(500, { "Content-Type": "application/json" });
2307
+ res.end(
2308
+ JSON.stringify({
2309
+ error: "mailbox action failed",
2310
+ detail: sanitizeApiError2(err)
2311
+ })
2312
+ );
2313
+ }
2314
+ }
2315
+
2316
+ // src/hq-server/startup.ts
2317
+ import * as fs3 from "node:fs/promises";
2318
+ import * as path5 from "node:path";
2319
+ var hqRuntimeMarkerPath2 = hqRuntimeMarkerPath;
2320
+ async function writeHqRuntimeMarker(dataDir, url) {
2321
+ const file = hqRuntimeMarkerPath2(dataDir);
2322
+ const payload = JSON.stringify(
2323
+ { url, pid: process.pid, updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
2324
+ null,
2325
+ 2
2326
+ );
2327
+ await fs3.mkdir(path5.dirname(file), { recursive: true });
2328
+ await fs3.writeFile(file, `${payload}
2329
+ `, { encoding: "utf8", mode: 384 });
2330
+ }
2331
+ async function clearHqRuntimeMarker(dataDir, url) {
2332
+ const file = hqRuntimeMarkerPath2(dataDir);
2333
+ try {
2334
+ const raw = await fs3.readFile(file, "utf8");
2335
+ const parsed = JSON.parse(raw);
2336
+ if (parsed.url === url && parsed.pid === process.pid) {
2337
+ await fs3.rm(file, { force: true });
2338
+ }
2339
+ } catch {
2340
+ }
2341
+ }
2342
+ function writeHqStartupInfo(write, handle) {
2343
+ const startup = handle.firstRunSetup;
2344
+ write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
2345
+ `);
2346
+ if (!startup) {
2347
+ write(`Browser endpoint: ${buildHttpUrl(handle.host, handle.port)}
2348
+ `);
2349
+ write(`Client endpoint: ${buildClientWsUrl(handle.host, handle.port)}
2350
+ `);
2351
+ writeHqLanEndpoints(write, handle, void 0);
2352
+ return;
2353
+ }
2354
+ write(`Browser endpoint: ${startup.browserUrl}
2355
+ `);
2356
+ write(`Client endpoint: ${startup.clientUrl}
2357
+ `);
2358
+ if (startup.createdAuth) {
2359
+ write(`
2360
+ First-run HQ auth created in ${startup.dataDir}
2361
+ `);
2362
+ } else {
2363
+ write(`
2364
+ HQ auth loaded from ${startup.dataDir}
2365
+ `);
2366
+ }
2367
+ write("Start clients with:\n");
2368
+ write(` WRONGSTACK_HQ_URL=${startup.clientEnv.WRONGSTACK_HQ_URL}
2369
+ `);
2370
+ if (startup.clientEnv.WRONGSTACK_HQ_TOKEN) {
2371
+ write(` WRONGSTACK_HQ_TOKEN=${startup.clientEnv.WRONGSTACK_HQ_TOKEN}
2372
+ `);
2373
+ write(`Credentials are stored in ${path5.join(startup.dataDir, "auth.json")}
2374
+ `);
2375
+ }
2376
+ writeHqLanEndpoints(write, handle, void 0);
2377
+ }
2378
+ function writeHqLanEndpoints(write, handle, browserToken) {
2379
+ if (handle.host !== "0.0.0.0" && handle.host !== "::") return;
2380
+ const ips = lanIPv4Addresses();
2381
+ if (ips.length === 0) return;
2382
+ write("\nReachable from other machines on your network:\n");
2383
+ for (const ip of ips) {
2384
+ write(` ${buildHttpUrl(ip, handle.port, browserToken)}
2385
+ `);
2386
+ }
2387
+ write(` On another machine, set WRONGSTACK_HQ_URL=http://${ips[0]}:${handle.port}
2388
+ `);
2389
+ }
2390
+
2391
+ // src/hq-server/ws.ts
2392
+ import { randomUUID as randomUUID3 } from "node:crypto";
2393
+ import {
2394
+ HQ_PROTOCOL_VERSION,
2395
+ HQ_TRANSCRIPT_TEXT_CAP,
2396
+ parseHqEventPayload,
2397
+ parseHqFrame,
2398
+ redactHqEvent,
2399
+ resolveHqRedactionPolicy,
2400
+ tightenHqRedactionPolicy,
2401
+ tokenHasCapability as tokenHasCapability2
2402
+ } from "@wrongstack/core/hq";
2403
+ import { WebSocket as WebSocket2 } from "ws";
2404
+ var MAX_EVENT_LOG = 5e3;
2405
+ function handleBrowser(ws, snapshotBroadcaster, browsers) {
2406
+ browsers.add(ws);
2407
+ ws.on("error", (err) => {
2408
+ console.warn(
2409
+ JSON.stringify({
2410
+ level: "warn",
2411
+ event: "hq.browser_socket_error",
2412
+ message: err instanceof Error ? err.message : String(err),
2413
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2414
+ })
2415
+ );
2416
+ });
2417
+ sendGuarded(ws, snapshotBroadcaster.currentSerialized());
2418
+ ws.on("close", () => {
2419
+ browsers.delete(ws);
2420
+ });
2421
+ }
2422
+ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster, transcripts, agentMessages, persistence, auditLog) {
2423
+ let registered = false;
2424
+ function persistEvent(event) {
2425
+ eventLog.push(event);
2426
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
2427
+ if (persistence !== void 0) {
2428
+ persistence.eventLog.append(event);
2429
+ recordTimeseriesSignal(persistence, event);
2430
+ }
2431
+ }
2432
+ ws.on("error", (err) => {
2433
+ console.warn(
2434
+ JSON.stringify({
2435
+ level: "warn",
2436
+ event: "hq.client_socket_error",
2437
+ message: err instanceof Error ? err.message : String(err),
2438
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2439
+ })
2440
+ );
2441
+ });
2442
+ ws.on("message", (data) => {
2443
+ const raw = typeof data === "string" ? data : Buffer.isBuffer(data) ? data : new TextDecoder().decode(data);
2444
+ const parsed = parseHqFrame(raw);
2445
+ if (!parsed.ok) {
2446
+ const code = parsed.reason === "invalid-json" ? 1003 : 1008;
2447
+ ws.close(code, `invalid frame: ${parsed.reason}`);
2448
+ return;
2449
+ }
2450
+ const frame = parsed.frame;
2451
+ if (frame.type === "client.hello") {
2452
+ if (registered) {
2453
+ ws.close(1008, "duplicate client.hello");
2454
+ return;
2455
+ }
2456
+ const payload = frame.payload;
2457
+ if (payload.protocolVersion !== HQ_PROTOCOL_VERSION) {
2458
+ ws.close(1008, "protocol version mismatch");
2459
+ return;
2460
+ }
2461
+ const canPublishTelemetry = auth.token === void 0 || tokenHasCapability2(auth.token, "telemetry.publish");
2462
+ const acceptedCapabilities = payload.capabilities.filter(
2463
+ (capability) => capability === "control.receive" || canPublishTelemetry
2464
+ );
2465
+ const declaredRedactionPolicy = resolveHqRedactionPolicy(payload.redactionPolicy);
2466
+ for (const [otherWs, otherClient] of clients) {
2467
+ const samePublisher = otherClient.projectId === payload.project.projectId && otherClient.kind === payload.client.kind && otherClient.pid !== void 0 && payload.client.pid !== void 0 && otherClient.pid === payload.client.pid && (otherClient.machineId || otherClient.project.machineId) === (payload.client.machineId || payload.project.machineId);
2468
+ if (otherWs !== ws && (otherClient.clientId === payload.client.clientId || samePublisher && otherClient.sessions.size === 0)) {
2469
+ clients.delete(otherWs);
2470
+ otherWs.close(4001, "superseded by a newer HQ connection");
2471
+ }
2472
+ }
2473
+ const client = {
2474
+ ws,
2475
+ clientId: payload.client.clientId,
2476
+ projectId: payload.project.projectId,
2477
+ project: payload.project,
2478
+ kind: payload.client.kind,
2479
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
2480
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
2481
+ ...payload.client.hostname ? { hostname: payload.client.hostname } : {},
2482
+ ...payload.client.pid ? { pid: payload.client.pid } : {},
2483
+ ...payload.client.version ? { version: payload.client.version } : {},
2484
+ capabilities: acceptedCapabilities,
2485
+ ...auth.token !== void 0 ? { authToken: auth.token } : {},
2486
+ declaredRedactionPolicy,
2487
+ lastEventSeq: 0,
2488
+ mailboxes: /* @__PURE__ */ new Map(),
2489
+ machineId: payload.client.machineId || payload.project.machineId,
2490
+ sessions: /* @__PURE__ */ new Map(),
2491
+ fleets: /* @__PURE__ */ new Map(),
2492
+ mcpSnapshots: /* @__PURE__ */ new Map(),
2493
+ commandQueue: []
2494
+ };
2495
+ clients.set(ws, client);
2496
+ registered = true;
2497
+ const welcome = {
2498
+ type: "hq.welcome",
2499
+ protocolVersion: HQ_PROTOCOL_VERSION,
2500
+ serverTime: (/* @__PURE__ */ new Date()).toISOString(),
2501
+ acceptedCapabilities,
2502
+ // The operator-configured override (from <dataDir>/auth.json) wins
2503
+ // over the default. The client learns the *effective* policy.
2504
+ redactionPolicy: tightenHqRedactionPolicy(
2505
+ declaredRedactionPolicy,
2506
+ auth.getOperatorPolicy()
2507
+ )
2508
+ };
2509
+ ws.send(JSON.stringify(welcome));
2510
+ if (persistence !== void 0) {
2511
+ void persistence.kanban.load(client.projectId).then((payload2) => {
2512
+ sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: payload2 }));
2513
+ }).catch(() => void 0);
2514
+ }
2515
+ const event = {
2516
+ id: randomUUID3(),
2517
+ type: "client.hello",
2518
+ schemaVersion: HQ_PROTOCOL_VERSION,
2519
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2520
+ clientId: payload.client.clientId,
2521
+ projectId: payload.project.projectId,
2522
+ seq: 0,
2523
+ payload: { client: payload.client, project: payload.project }
2524
+ };
2525
+ persistEvent(event);
2526
+ snapshotBroadcaster.broadcast();
2527
+ broadcastEvent(event, browsers);
2528
+ return;
2529
+ }
2530
+ if (!registered) return;
2531
+ if (frame.type === "client.command_poll") {
2532
+ const client = clients.get(ws);
2533
+ if (client) {
2534
+ if (frame.clientId !== client.clientId || frame.projectId !== client.projectId) {
2535
+ ws.close(1008, "command poll identity mismatch");
2536
+ return;
2537
+ }
2538
+ client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
2539
+ const afterId = frame.afterCommandId;
2540
+ const limit = frame.limit ?? 25;
2541
+ let toSend;
2542
+ if (afterId === void 0) {
2543
+ toSend = client.commandQueue.slice(0, limit);
2544
+ } else {
2545
+ const idx = client.commandQueue.findIndex((c) => c.commandId === afterId);
2546
+ toSend = (idx >= 0 ? client.commandQueue.slice(idx + 1) : client.commandQueue).slice(
2547
+ 0,
2548
+ limit
2549
+ );
2550
+ }
2551
+ if (toSend.length > 0) {
2552
+ const batch = JSON.stringify({ type: "hq.command_batch", commands: toSend });
2553
+ if (ws.readyState === WebSocket2.OPEN) ws.send(batch);
2554
+ for (const cmd of toSend) {
2555
+ auditLog?.update(cmd.commandId, { status: "delivered" });
2556
+ const updated = auditLog?.get(cmd.commandId);
2557
+ if (updated !== void 0) broadcastCommandStatus(updated, browsers);
2558
+ }
2559
+ }
2560
+ }
2561
+ return;
2562
+ }
2563
+ if (frame.type === "client.command_ack") {
2564
+ const client = clients.get(ws);
2565
+ if (client) {
2566
+ if (frame.clientId !== client.clientId || frame.projectId !== client.projectId) {
2567
+ ws.close(1008, "command ack identity mismatch");
2568
+ return;
2569
+ }
2570
+ client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
2571
+ }
2572
+ if (client) {
2573
+ const updated = auditLog?.updateForClient(frame.commandId, client.clientId, {
2574
+ status: "acked",
2575
+ ackStatus: frame.status,
2576
+ ...frame.message !== void 0 ? { ackMessage: frame.message } : {},
2577
+ ackedAt: (/* @__PURE__ */ new Date()).toISOString()
2578
+ });
2579
+ if (updated === true) {
2580
+ const entry = auditLog?.get(frame.commandId);
2581
+ if (entry !== void 0) broadcastCommandStatus(entry, browsers);
2582
+ }
2583
+ }
2584
+ return;
2585
+ }
2586
+ if (frame.type === "client.event") {
2587
+ const client = clients.get(ws);
2588
+ if (!client) return;
2589
+ const incomingEvent = frame.event;
2590
+ if (incomingEvent.clientId !== client.clientId || incomingEvent.projectId !== client.projectId) {
2591
+ ws.close(1008, "event identity mismatch");
2592
+ return;
2593
+ }
2594
+ if (!tokenHasCapability2(client.authToken, "telemetry.publish") && auth.token !== void 0) {
2595
+ ws.close(1008, "client token lacks telemetry.publish capability");
2596
+ return;
2597
+ }
2598
+ if (incomingEvent.seq <= client.lastEventSeq) {
2599
+ ws.close(1008, "event sequence must increase monotonically");
2600
+ return;
2601
+ }
2602
+ const parsedPayload = parseHqEventPayload(incomingEvent.type, incomingEvent.payload);
2603
+ if (!parsedPayload.ok) {
2604
+ return;
2605
+ }
2606
+ client.lastEventSeq = incomingEvent.seq;
2607
+ client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
2608
+ const event = redactHqEvent(
2609
+ { ...incomingEvent, payload: parsedPayload.payload },
2610
+ {
2611
+ policy: tightenHqRedactionPolicy(
2612
+ client.declaredRedactionPolicy,
2613
+ auth.getOperatorPolicy()
2614
+ ),
2615
+ projectRoot: client.project.projectRoot,
2616
+ // Chat-transcript events carry full turns — the generic 500-char
2617
+ // summary cap would truncate them a second time after the
2618
+ // publisher already applied the transcript cap.
2619
+ ...incomingEvent.type === "session.transcript" || incomingEvent.type === "agent.message" ? { maxSummaryLength: HQ_TRANSCRIPT_TEXT_CAP } : {}
2620
+ }
2621
+ ).value;
2622
+ if (event.type === "client.heartbeat") {
2623
+ return;
2624
+ }
2625
+ if (event.type === "kanban.snapshot") {
2626
+ const payload = event.payload;
2627
+ if (payload.projectId !== client.projectId || persistence === void 0) return;
2628
+ void persistence.kanban.merge(payload).then((merged) => {
2629
+ const touched = /* @__PURE__ */ new Set();
2630
+ for (const record of payload.boards) touched.add(record.boardId);
2631
+ for (const record of payload.tombstones) touched.add(record.boardId);
2632
+ const delta = {
2633
+ projectId: merged.projectId,
2634
+ generatedAt: merged.generatedAt,
2635
+ boards: merged.boards.filter((record) => touched.has(record.boardId)),
2636
+ tombstones: merged.tombstones.filter((record) => touched.has(record.boardId))
2637
+ };
2638
+ const message = JSON.stringify({ type: "hq.kanban_snapshot", payload: delta });
2639
+ for (const peer of clients.values()) {
2640
+ if (peer.projectId === client.projectId) sendGuarded(peer.ws, message);
2641
+ }
2642
+ }).catch((error) => {
2643
+ console.warn(
2644
+ JSON.stringify({
2645
+ level: "warn",
2646
+ event: "hq.kanban_merge_failed",
2647
+ message: error instanceof Error ? error.message : String(error),
2648
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2649
+ })
2650
+ );
2651
+ });
2652
+ persistEvent(event);
2653
+ broadcastEvent(event, browsers);
2654
+ return;
2655
+ }
2656
+ if (event.type === "mailbox.snapshot" && client !== void 0) {
2657
+ const payloadResult = parseHqEventPayload(event.type, event.payload);
2658
+ if (payloadResult.ok) {
2659
+ const payload = payloadResult.payload;
2660
+ client.mailboxes.set(client.projectId + ":" + payload.mailboxId, payload);
2661
+ persistEvent(event);
2662
+ snapshotBroadcaster.broadcast();
2663
+ broadcastEvent(event, browsers);
2664
+ return;
2665
+ }
2666
+ return;
2667
+ }
2668
+ if (event.type === "mailbox.event") {
2669
+ const payloadResult = parseHqEventPayload(event.type, event.payload);
2670
+ if (!payloadResult.ok) {
2671
+ return;
2672
+ }
2673
+ const payload = payloadResult.payload;
2674
+ const sanitizedSummary = truncateHqSummary(payload.summary, 280);
2675
+ const sanitizedEvent = sanitizedSummary === void 0 ? event : { ...event, payload: { ...payload, summary: sanitizedSummary } };
2676
+ persistEvent(sanitizedEvent);
2677
+ broadcastEvent(sanitizedEvent, browsers);
2678
+ return;
2679
+ }
2680
+ if (event.type === "session.snapshot" && client !== void 0) {
2681
+ const result = parseHqEventPayload(event.type, event.payload);
2682
+ if (result.ok) {
2683
+ const payload = result.payload;
2684
+ client.sessions.set(payload.sessionId, { payload, receivedAt: Date.now() });
2685
+ snapshotBroadcaster.broadcast();
2686
+ }
2687
+ return;
2688
+ }
2689
+ if (event.type === "session.ended" && client !== void 0) {
2690
+ const result = parseHqEventPayload(event.type, event.payload);
2691
+ if (result.ok) {
2692
+ const payload = result.payload;
2693
+ client.sessions.delete(payload.sessionId);
2694
+ client.mcpSnapshots.delete(payload.sessionId);
2695
+ for (const [runId, fleet] of client.fleets) {
2696
+ if (fleet.sessionId === payload.sessionId) client.fleets.delete(runId);
2697
+ }
2698
+ snapshotBroadcaster.broadcast();
2699
+ }
2700
+ return;
2701
+ }
2702
+ if (event.type === "fleet.snapshot" && client !== void 0) {
2703
+ const result = parseHqEventPayload(event.type, event.payload);
2704
+ if (result.ok) {
2705
+ const payload = result.payload;
2706
+ client.fleets.set(payload.runId, {
2707
+ payload,
2708
+ ...event.sessionId !== void 0 ? { sessionId: event.sessionId } : {},
2709
+ receivedAt: Date.now()
2710
+ });
2711
+ snapshotBroadcaster.broadcast();
2712
+ }
2713
+ return;
2714
+ }
2715
+ if (event.type === "mcp.health.snapshot" && client !== void 0) {
2716
+ const result = parseHqEventPayload(event.type, event.payload);
2717
+ if (result.ok) {
2718
+ const payload = result.payload;
2719
+ const sessionId = event.sessionId || "unknown";
2720
+ const stamped = payload.servers.map((s) => ({
2721
+ ...s,
2722
+ projectId: client.projectId,
2723
+ clientId: client.clientId
2724
+ }));
2725
+ client.mcpSnapshots.set(sessionId, { servers: stamped, receivedAt: Date.now() });
2726
+ snapshotBroadcaster.broadcast();
2727
+ }
2728
+ return;
2729
+ }
2730
+ if (event.type === "session.transcript" && client !== void 0) {
2731
+ const result = parseHqEventPayload(event.type, event.payload);
2732
+ if (result.ok) {
2733
+ const payload = result.payload;
2734
+ let ring = transcripts.get(payload.sessionId);
2735
+ if (!ring) {
2736
+ ring = { entries: [], ...client.machineId ? { machineId: client.machineId } : {} };
2737
+ }
2738
+ for (const entry of payload.entries) ring.entries.push(entry);
2739
+ if (ring.entries.length > TRANSCRIPT_RING_MAX) {
2740
+ ring.entries.splice(0, ring.entries.length - TRANSCRIPT_RING_MAX);
2741
+ }
2742
+ transcripts.delete(payload.sessionId);
2743
+ transcripts.set(payload.sessionId, ring);
2744
+ evictOldest(transcripts, MAX_TRANSCRIPT_SESSIONS);
2745
+ broadcastEvent(event, browsers);
2746
+ }
2747
+ return;
2748
+ }
2749
+ if (event.type === "agent.message") {
2750
+ const p = event.payload;
2751
+ const subId = p && typeof p["subagentId"] === "string" ? p["subagentId"] : void 0;
2752
+ if (subId) {
2753
+ const key = agentRingKey(event.sessionId, subId);
2754
+ let ring = agentMessages.get(key);
2755
+ if (!ring) ring = [];
2756
+ ring.push(agentMessageToEntry(p));
2757
+ if (ring.length > TRANSCRIPT_RING_MAX) ring.splice(0, ring.length - TRANSCRIPT_RING_MAX);
2758
+ agentMessages.delete(key);
2759
+ agentMessages.set(key, ring);
2760
+ evictOldest(agentMessages, MAX_AGENT_RINGS);
2761
+ }
2762
+ persistEvent(event);
2763
+ broadcastEvent(event, browsers);
2764
+ return;
2765
+ }
2766
+ persistEvent(event);
2767
+ broadcastEvent(event, browsers);
2768
+ }
2769
+ });
2770
+ ws.on("close", () => {
2771
+ clients.delete(ws);
2772
+ snapshotBroadcaster.broadcast();
2773
+ });
2774
+ }
2775
+
2776
+ // src/hq-server.ts
2777
+ import { HqInsecureExposureError as HqInsecureExposureError2 } from "@wrongstack/core/hq";
2778
+ var DEFAULT_HOST = "127.0.0.1";
2779
+ var DEFAULT_PORT = 3499;
2780
+ var MAX_EVENT_LOG2 = 5e3;
2781
+ var MAX_NON_STRICT_PORT_SCAN = 50;
2782
+ var CLIENT_TTL_MS = 6e4;
2783
+ var CLIENT_CLEANUP_INTERVAL_MS = 3e4;
2784
+ var BROWSER_HEARTBEAT_INTERVAL_MS = 15e3;
2785
+ var SESSION_SNAPSHOT_TTL_MS = 3e4;
2786
+ async function startHqServer(options = {}) {
2787
+ const { host, port, dataDir, firstRunAuth } = await prepareHqServerStart(options, {
2788
+ host: DEFAULT_HOST,
2789
+ port: DEFAULT_PORT
2790
+ });
2791
+ return startHqServerWithAuth(options, host, port, dataDir, firstRunAuth);
2792
+ }
2793
+ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
2794
+ const trustBoundary = options.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "hq-trusted-host-compat-v1" });
2795
+ const authFile = firstRunAuth.authFile;
2796
+ const authState = createHqAuthState(authFile, dataDir);
2797
+ const { mutableAuth } = authState;
2798
+ if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
2799
+ const secret = mintHqCookieSecret2();
2800
+ const next = await mutateHqAuthFile2(dataDir, (current) => ({
2801
+ ...current,
2802
+ cookieSecret: secret
2803
+ }));
2804
+ mutableAuth.cookieSecret = next.cookieSecret;
2805
+ console.warn(
2806
+ JSON.stringify({
2807
+ level: "info",
2808
+ event: "hq.cookie_secret_provisioned",
2809
+ message: "Cookie signing secret provisioned for token-mode bootstrap exchange.",
2810
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2811
+ })
2812
+ );
2813
+ }
2814
+ const bootstrapStore = new HqBootstrapCodeStore();
2815
+ console.warn(
2816
+ JSON.stringify({
2817
+ level: "info",
2818
+ event: "hq.startup",
2819
+ message: "WrongStack HQ starting",
2820
+ dataDir,
2821
+ host,
2822
+ port,
2823
+ operatorPolicyActive: authFile.redactionPolicy !== void 0,
2824
+ browserTokenMode: mutableAuth.browserTokens.size > 0,
2825
+ clientTokenMode: mutableAuth.clientTokens.size > 0,
2826
+ passwordMode: mutableAuth.passwordHash !== void 0,
2827
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2828
+ })
2829
+ );
2830
+ return new Promise((resolve, reject) => {
2831
+ const trustedPublicOrigins = /* @__PURE__ */ new Set();
2832
+ let listeningPort = port;
2833
+ const clients = /* @__PURE__ */ new Map();
2834
+ const browsers = /* @__PURE__ */ new Set();
2835
+ const sessions = /* @__PURE__ */ new Map();
2836
+ const eventLog = [];
2837
+ const transcripts = /* @__PURE__ */ new Map();
2838
+ const agentMessages = /* @__PURE__ */ new Map();
2839
+ const mailboxGateways = /* @__PURE__ */ new Map();
2840
+ const mailboxGatewayRateLimiter = new MailboxHttpRateLimiter();
2841
+ const mailboxGatewayRateLimitCleanup = setInterval(
2842
+ () => mailboxGatewayRateLimiter.cleanup(),
2843
+ 12e4
2844
+ );
2845
+ mailboxGatewayRateLimitCleanup.unref?.();
2846
+ const loginAttempts = /* @__PURE__ */ new Map();
2847
+ const LOGIN_ATTEMPT_RETENTION_MS = 15 * 6e4;
2848
+ const HQ_SESSION_MAX_AGE_MS2 = 7 * 24 * 60 * 60 * 1e3;
2849
+ const SESSION_CLEANUP_INTERVAL_MS = 6e4;
2850
+ const sessionCleanupTimer = setInterval(() => {
2851
+ const cutoff = Date.now() - HQ_SESSION_MAX_AGE_MS2;
2852
+ for (const [id, session] of sessions) {
2853
+ if (session.createdAt < cutoff) sessions.delete(id);
2854
+ }
2855
+ const now = Date.now();
2856
+ for (const [ip, entry] of loginAttempts) {
2857
+ if (now - entry.lastAttempt > LOGIN_ATTEMPT_RETENTION_MS) loginAttempts.delete(ip);
2858
+ }
2859
+ }, SESSION_CLEANUP_INTERVAL_MS);
2860
+ sessionCleanupTimer.unref?.();
2861
+ const persistence = createHqPersistence(dataDir);
2862
+ const auditLog = new HqCommandAuditLog(1e3, (entry) => persistence.commandLog.append(entry));
2863
+ void (async () => {
2864
+ const projectsDir = path6.join(path6.dirname(dataDir), "projects");
2865
+ const entries = await fs4.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
2866
+ await Promise.all(
2867
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
2868
+ const projectDir = path6.join(projectsDir, entry.name);
2869
+ const metadataPath = path6.join(projectDir, ".mailbox-server.json");
2870
+ const ownerPid = await fs4.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
2871
+ if (ownerPid === void 0) return;
2872
+ let ownerAlive = false;
2873
+ try {
2874
+ process.kill(ownerPid, 0);
2875
+ ownerAlive = true;
2876
+ } catch {
2877
+ ownerAlive = false;
2878
+ }
2879
+ if (!ownerAlive) {
2880
+ await fs4.rm(metadataPath, { force: true }).catch(() => {
2881
+ });
2882
+ return;
2883
+ }
2884
+ const mailbox = getSharedProjectMailbox(projectDir);
2885
+ try {
2886
+ await Promise.all([mailbox.getAgentStatuses(), mailbox.purgeClients()]);
2887
+ } finally {
2888
+ await mailbox.close().catch(() => {
2889
+ });
2890
+ }
2891
+ })
2892
+ );
2893
+ })().catch(() => {
2894
+ });
2895
+ const hqSessionTag = randomUUID4().slice(0, 8);
2896
+ const authorizeMailboxGateway = (req, projectDir) => {
2897
+ const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
2898
+ const auth = authenticateBrowserRequest2(req, requestUrl, mutableAuth, sessions);
2899
+ const tokenMode = mutableAuth.browserTokens.size > 0;
2900
+ const passwordMode = mutableAuth.passwordHash !== void 0;
2901
+ if ((tokenMode || passwordMode) && auth === void 0) {
2902
+ return {
2903
+ allowed: false,
2904
+ status: 401,
2905
+ body: { error: { code: "UNAUTHORIZED", message: "unauthorized" } }
2906
+ };
2907
+ }
2908
+ const token = isTokenAuth2(auth) ? mutableAuth.browserTokenObjs.get(auth.token) : void 0;
2909
+ const canUseMailbox = isCookieAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? token?.capabilities === void 0 || !!token?.capabilities.includes("control.enqueue") : !tokenMode;
2910
+ if (!canUseMailbox) {
2911
+ return {
2912
+ allowed: false,
2913
+ status: 403,
2914
+ body: {
2915
+ error: {
2916
+ code: "FORBIDDEN",
2917
+ message: "forbidden: token lacks control.enqueue capability"
2918
+ }
2919
+ }
2920
+ };
2921
+ }
2922
+ const identity = isTokenAuth2(auth) ? auth.id : isCookieAuth2(auth) ? "cookie" : "open";
2923
+ return { allowed: true, rateLimitKey: `hq:${identity}:${projectDir}` };
2924
+ };
2925
+ const getMailboxGateway = (projectDir) => {
2926
+ const existing = mailboxGateways.get(projectDir);
2927
+ if (existing) return existing;
2928
+ const eventEmitter = new MailboxEventEmitter();
2929
+ const mailbox = createProjectMailbox({ projectDir, eventEmitter });
2930
+ const router = createMailboxHttpRouter({
2931
+ mailbox,
2932
+ eventEmitter,
2933
+ rateLimiter: mailboxGatewayRateLimiter,
2934
+ authorize: (request) => authorizeMailboxGateway(request, projectDir),
2935
+ // Wire the 1h look-back that the router's docs promise at
2936
+ // mailbox-http-router.ts:25-32 / :47-62. The router itself is
2937
+ // opt-in (L146-152): without this option, every retained
2938
+ // message would be returned. Per-request opt-in to the full
2939
+ // history remains available via `?sinceMs=0`.
2940
+ defaultMaxAgeMs: MAILBOX_HTTP_DEFAULT_MAX_AGE_MS
2941
+ });
2942
+ const gateway = { mailbox, router };
2943
+ mailboxGateways.set(projectDir, gateway);
2944
+ return gateway;
2945
+ };
2946
+ const alertEngine = new HqAlertEngine({
2947
+ onAlert: (alert) => {
2948
+ const msg = toAlertMessage(alert);
2949
+ const data = JSON.stringify(msg);
2950
+ for (const ws of browsers) {
2951
+ if (ws.readyState === WebSocket3.OPEN) ws.send(data);
2952
+ }
2953
+ },
2954
+ onPersist: (alert) => persistence.alertLog.append(alert)
2955
+ });
2956
+ void persistence.eventLog.hydrate().catch(() => void 0);
2957
+ void persistence.timeseries.load().catch(() => void 0);
2958
+ persistence.eventLog.recent(MAX_EVENT_LOG2).then((prior) => {
2959
+ for (let i = prior.length - 1; i >= 0; i--) eventLog.push(prior[i]);
2960
+ }).catch(() => {
2961
+ });
2962
+ persistence.alertLog.readAll().then((prior) => {
2963
+ alertEngine.seed(prior);
2964
+ }).catch(() => {
2965
+ });
2966
+ persistence.commandLog.readAll().then((prior) => {
2967
+ auditLog.seed(prior);
2968
+ }).catch(() => {
2969
+ });
2970
+ const timeseriesFlushTimer = setInterval(() => {
2971
+ persistence.timeseries.flush();
2972
+ }, 6e4);
2973
+ timeseriesFlushTimer.unref?.();
2974
+ const snapshotBroadcaster = createSnapshotBroadcaster(
2975
+ clients,
2976
+ browsers,
2977
+ persistence
2978
+ );
2979
+ snapshotBroadcaster.currentSerialized();
2980
+ const stopAlertEngine = alertEngine.startPeriodic(
2981
+ () => buildSnapshot(clients, { tokenStats: authState.tokenStats() }),
2982
+ () => mutableAuth.alertRules
2983
+ );
2984
+ const clientTtlMs = options.clientTtlMs ?? CLIENT_TTL_MS;
2985
+ const sessionSnapshotTtlMs = options.sessionSnapshotTtlMs ?? SESSION_SNAPSHOT_TTL_MS;
2986
+ const cleanupTimer = setInterval(() => {
2987
+ const cutoff = Date.now() - clientTtlMs;
2988
+ const sessionCutoff = Date.now() - sessionSnapshotTtlMs;
2989
+ let changed = false;
2990
+ for (const [ws, client] of clients.entries()) {
2991
+ if (new Date(client.lastSeenAt).getTime() < cutoff) {
2992
+ ws.terminate();
2993
+ clients.delete(ws);
2994
+ changed = true;
2995
+ continue;
2996
+ }
2997
+ const expiredSessions = /* @__PURE__ */ new Set();
2998
+ for (const [sessionId, tracked] of client.sessions.entries()) {
2999
+ if (tracked.receivedAt < sessionCutoff) {
3000
+ client.sessions.delete(sessionId);
3001
+ client.mcpSnapshots.delete(sessionId);
3002
+ expiredSessions.add(sessionId);
3003
+ changed = true;
3004
+ }
3005
+ }
3006
+ for (const [runId, fleet] of client.fleets) {
3007
+ if (fleet.sessionId !== void 0 && expiredSessions.has(fleet.sessionId)) {
3008
+ client.fleets.delete(runId);
3009
+ changed = true;
3010
+ }
3011
+ }
3012
+ if (client.capabilities.includes("session.summary") && client.sessions.size === 0 && Date.now() - Date.parse(client.connectedAt) > sessionSnapshotTtlMs) {
3013
+ ws.terminate();
3014
+ clients.delete(ws);
3015
+ changed = true;
3016
+ }
3017
+ }
3018
+ if (changed) snapshotBroadcaster.broadcast();
3019
+ }, options.clientCleanupIntervalMs ?? CLIENT_CLEANUP_INTERVAL_MS);
3020
+ cleanupTimer.unref?.();
3021
+ const routerDeps = {
3022
+ trustBoundary,
3023
+ host,
3024
+ listeningPort,
3025
+ trustedPublicOrigins,
3026
+ mutableAuth,
3027
+ sessions,
3028
+ loginAttempts,
3029
+ clients,
3030
+ browsers,
3031
+ eventLog,
3032
+ transcripts,
3033
+ agentMessages,
3034
+ mailboxGateways,
3035
+ mailboxGatewayRateLimiter,
3036
+ alertEngine,
3037
+ auditLog,
3038
+ persistence,
3039
+ dataDir,
3040
+ hqSessionTag,
3041
+ requireBrowserAuth: options.requireBrowserAuth,
3042
+ secureCookies: options.secureCookies,
3043
+ authorizeMailboxGateway,
3044
+ getMailboxGateway,
3045
+ getTokenStats: () => authState.tokenStats(),
3046
+ bootstrapStore
3047
+ };
3048
+ const handleRequest = createHqRouter(routerDeps);
3049
+ const httpServer = http.createServer((req, res) => {
3050
+ void handleRequest(req, res);
3051
+ });
3052
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 1 * 1024 * 1024 });
3053
+ const browserHeartbeatTimer = setInterval(() => {
3054
+ const heartbeat = JSON.stringify({
3055
+ type: "hq.heartbeat",
3056
+ serverTime: (/* @__PURE__ */ new Date()).toISOString()
3057
+ });
3058
+ for (const browser of browsers) {
3059
+ if (browser.readyState === WebSocket3.OPEN) browser.send(heartbeat);
3060
+ }
3061
+ }, options.browserHeartbeatIntervalMs ?? BROWSER_HEARTBEAT_INTERVAL_MS);
3062
+ browserHeartbeatTimer.unref?.();
3063
+ httpServer.on("upgrade", (req, socket, head) => {
3064
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
3065
+ const pathname = url.pathname;
3066
+ if (pathname !== "/ws/client" && pathname !== "/ws/browser") {
3067
+ socket.destroy();
3068
+ return;
3069
+ }
3070
+ if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
3071
+ socket.write(
3072
+ "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
3073
+ error: {
3074
+ code: "INVALID_ORIGIN",
3075
+ message: "Cross-origin WebSocket upgrade rejected."
3076
+ }
3077
+ })
3078
+ );
3079
+ socket.destroy();
3080
+ return;
3081
+ }
3082
+ const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
3083
+ const needsAuth = pathname === "/ws/browser" ? options.requireBrowserAuth || tokenSet.size > 0 || mutableAuth.passwordHash !== void 0 : tokenSet.size > 0;
3084
+ if (needsAuth) {
3085
+ const supplied = url.searchParams.get("token") ?? "";
3086
+ const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
3087
+ const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
3088
+ const cookies = parseCookieHeader2(req.headers.cookie);
3089
+ const raw = cookies[HQ_SESSION_COOKIE2];
3090
+ if (!raw) return false;
3091
+ const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
3092
+ if (sessionId === void 0) return false;
3093
+ const session = sessions.get(sessionId);
3094
+ if (!session || Date.now() - session.createdAt >= HQ_SESSION_MAX_AGE_MS)
3095
+ return false;
3096
+ if (session.kind === "token" && session.tokenId !== void 0) {
3097
+ return [...mutableAuth.browserTokenObjs.values()].some(
3098
+ (obj) => obj.id === session.tokenId
3099
+ );
3100
+ }
3101
+ return true;
3102
+ })();
3103
+ if (!tokenValid && !cookieValid) {
3104
+ socket.write(
3105
+ "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
3106
+ error: {
3107
+ code: "UNAUTHORIZED",
3108
+ message: pathname === "/ws/browser" ? "A valid ?token= or password session is required for browser connections." : "A valid ?token= is required for client connections in token mode."
3109
+ }
3110
+ })
3111
+ );
3112
+ socket.destroy();
3113
+ return;
3114
+ }
3115
+ }
3116
+ wss.handleUpgrade(req, socket, head, (ws) => {
3117
+ wss.emit("connection", ws, req, pathname);
3118
+ });
3119
+ });
3120
+ wss.on("connection", (ws, req, pathname) => {
3121
+ if (pathname === "/ws/browser") {
3122
+ handleBrowser(ws, snapshotBroadcaster, browsers);
3123
+ } else {
3124
+ const token = new URL(req.url ?? "/", `http://${host}:${port}`).searchParams.get("token");
3125
+ handleClient(
3126
+ ws,
3127
+ clients,
3128
+ browsers,
3129
+ eventLog,
3130
+ {
3131
+ ...token ? { token: mutableAuth.clientTokenObjs.get(token) } : {},
3132
+ getOperatorPolicy: () => mutableAuth.operatorPolicyOverride
3133
+ },
3134
+ snapshotBroadcaster,
3135
+ transcripts,
3136
+ agentMessages,
3137
+ persistence,
3138
+ auditLog
3139
+ );
3140
+ }
3141
+ });
3142
+ const authWatcher = watchHqAuthFile(
3143
+ dataDir,
3144
+ (next) => {
3145
+ authState.apply(next);
3146
+ if (options.requireBrowserAuth && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
3147
+ sessions.clear();
3148
+ for (const browser of browsers) browser.close(1008, "Browser authentication removed");
3149
+ }
3150
+ console.warn(
3151
+ JSON.stringify({
3152
+ level: "info",
3153
+ event: "hq.auth.reloaded",
3154
+ message: "HQ auth.json reloaded",
3155
+ browserTokenCount: mutableAuth.browserTokens.size,
3156
+ clientTokenCount: mutableAuth.clientTokens.size,
3157
+ passwordMode: mutableAuth.passwordHash !== void 0,
3158
+ alertRulesActive: mutableAuth.alertRules !== void 0,
3159
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3160
+ })
3161
+ );
3162
+ },
3163
+ {
3164
+ warn: (msg) => console.warn(
3165
+ JSON.stringify({
3166
+ level: "warn",
3167
+ event: "hq.auth.reload_failed",
3168
+ message: msg,
3169
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3170
+ })
3171
+ )
3172
+ }
3173
+ );
3174
+ let bindAttempts = 0;
3175
+ const listen = (nextPort) => {
3176
+ httpServer.listen(nextPort, host);
3177
+ };
3178
+ const onError = (err) => {
3179
+ if (err.code === "EADDRINUSE" && !options.strictPort && !options.exactPort && bindAttempts < MAX_NON_STRICT_PORT_SCAN) {
3180
+ bindAttempts += 1;
3181
+ listen(port + bindAttempts);
3182
+ } else {
3183
+ authWatcher.close();
3184
+ snapshotBroadcaster.close();
3185
+ wss.close();
3186
+ reject(err);
3187
+ }
3188
+ };
3189
+ httpServer.on("error", onError);
3190
+ listen(port);
3191
+ httpServer.once("listening", () => {
3192
+ void (async () => {
3193
+ httpServer.removeListener("error", onError);
3194
+ const addr = httpServer.address();
3195
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
3196
+ listeningPort = actualPort;
3197
+ const browserToken = firstRunAuth.browserToken?.token ?? authFile.browserTokens?.find((t) => t.token.trim().length > 0)?.token;
3198
+ const clientToken = firstRunAuth.clientToken?.token ?? authFile.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
3199
+ const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
3200
+ await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
3201
+ });
3202
+ const browserTokenObj = mutableAuth.browserTokenObjs.get(browserToken ?? "");
3203
+ let browserUrl;
3204
+ if (browserToken && browserTokenObj) {
3205
+ const code = bootstrapStore.issue({
3206
+ tokenId: browserTokenObj.id,
3207
+ ...browserTokenObj.capabilities !== void 0 ? { capabilities: browserTokenObj.capabilities } : {}
3208
+ });
3209
+ browserUrl = buildBootstrapHttpUrl2(host, actualPort, code);
3210
+ } else {
3211
+ browserUrl = buildHttpUrl2(host, actualPort);
3212
+ }
3213
+ const startupInfo = {
3214
+ dataDir,
3215
+ browserUrl,
3216
+ clientUrl: buildClientWsUrl2(host, actualPort, clientToken),
3217
+ clientEnv: {
3218
+ WRONGSTACK_HQ_URL: hqUrl,
3219
+ ...clientToken ? { WRONGSTACK_HQ_TOKEN: clientToken } : {}
3220
+ },
3221
+ createdAuth: firstRunAuth.created,
3222
+ browserTokenMode: mutableAuth.browserTokens.size > 0,
3223
+ passwordMode: mutableAuth.passwordHash !== void 0
3224
+ };
3225
+ let closed = false;
3226
+ const handle = {
3227
+ host,
3228
+ port: actualPort,
3229
+ firstRunSetup: startupInfo,
3230
+ trustPublicOrigin: (origin) => {
3231
+ const parsed = new URL(origin);
3232
+ if (parsed.protocol !== "https:" || parsed.origin !== origin) {
3233
+ throw new TypeError(
3234
+ "Trusted HQ public origin must be an exact HTTPS origin without a path."
3235
+ );
3236
+ }
3237
+ trustedPublicOrigins.add(parsed.origin.toLowerCase());
3238
+ },
3239
+ close: () => new Promise((res) => {
3240
+ if (closed) {
3241
+ res();
3242
+ return;
3243
+ }
3244
+ closed = true;
3245
+ clearInterval(cleanupTimer);
3246
+ clearInterval(browserHeartbeatTimer);
3247
+ clearInterval(timeseriesFlushTimer);
3248
+ clearInterval(mailboxGatewayRateLimitCleanup);
3249
+ clearInterval(sessionCleanupTimer);
3250
+ stopAlertEngine();
3251
+ authWatcher.close();
3252
+ snapshotBroadcaster.close();
3253
+ bootstrapStore.clear();
3254
+ persistence.timeseries.flush();
3255
+ for (const ws of browsers) ws.close(1001, "HQ shutting down");
3256
+ for (const ws of clients.keys()) ws.close(1001, "HQ shutting down");
3257
+ wss.close();
3258
+ for (const { router } of mailboxGateways.values()) router.close();
3259
+ httpServer.close(() => {
3260
+ const mailboxCloses = [...mailboxGateways.values()].map(
3261
+ ({ mailbox }) => mailbox.close().catch(() => void 0)
3262
+ );
3263
+ mailboxGateways.clear();
3264
+ void Promise.all([
3265
+ ...mailboxCloses,
3266
+ persistence.eventLog.drain(),
3267
+ persistence.snapshotStore.drain(),
3268
+ persistence.timeseries.drain(),
3269
+ persistence.kanban.drain()
3270
+ ]).then(() => clearHqRuntimeMarker(dataDir, hqUrl)).catch(() => void 0).finally(() => res());
3271
+ });
3272
+ })
3273
+ };
3274
+ writeHqStartupInfo((line) => console.log(line.trimEnd()), {
3275
+ host,
3276
+ port: actualPort,
3277
+ firstRunSetup: startupInfo
3278
+ });
3279
+ resolve(handle);
3280
+ })();
3281
+ });
3282
+ });
3283
+ }
3284
+
3285
+ export {
3286
+ HQ_HTML,
3287
+ decodePathSegment2 as decodePathSegment,
3288
+ sanitizeApiError2 as sanitizeApiError,
3289
+ agentRingKey2 as agentRingKey,
3290
+ agentMessageToEntry2 as agentMessageToEntry,
3291
+ readLocalSubagentTranscript2 as readLocalSubagentTranscript,
3292
+ DEFAULT_HOST,
3293
+ DEFAULT_PORT,
3294
+ MAX_EVENT_LOG2 as MAX_EVENT_LOG,
3295
+ MAX_NON_STRICT_PORT_SCAN,
3296
+ CLIENT_TTL_MS,
3297
+ CLIENT_CLEANUP_INTERVAL_MS,
3298
+ BROWSER_HEARTBEAT_INTERVAL_MS,
3299
+ SESSION_SNAPSHOT_TTL_MS,
3300
+ startHqServer,
3301
+ HqInsecureExposureError2 as HqInsecureExposureError
3302
+ };
3303
+ //# sourceMappingURL=chunk-OI7GWJDS.js.map