@johpaz/hive-agents 0.0.35 → 0.0.36

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 (440) hide show
  1. package/README.md +64 -39
  2. package/dist/hive.js +3127 -3104
  3. package/dist/ui/assets/{AgentCreateForm-B4eK7efF.js → AgentCreateForm-tJZv9FZC.js} +1 -1
  4. package/dist/ui/assets/{AgentDetailPage-BD2uoJWk.js → AgentDetailPage-Du-mRcAX.js} +1 -1
  5. package/dist/ui/assets/AgentNewPage-DIFYd_Ys.js +1 -0
  6. package/dist/ui/assets/{AgentsPage-4JUZXvkA.js → AgentsPage-YvSgWRiw.js} +6 -6
  7. package/dist/ui/assets/CanvasPage-DtMwGvxf.js +33 -0
  8. package/dist/ui/assets/{ChannelsPage-BUn7-nhV.js → ChannelsPage-BdBXWHjj.js} +1 -1
  9. package/dist/ui/assets/DashboardPage-ghl1ZguH.js +6 -0
  10. package/dist/ui/assets/{LoginPage-C8j_urUD.js → LoginPage-CAmSI9Vy.js} +1 -1
  11. package/dist/ui/assets/LogsPage-DAPBHkwK.js +1 -0
  12. package/dist/ui/assets/MeetingPage-WjjGOqqU.js +1 -0
  13. package/dist/ui/assets/{NotFound-Drh-sJPN.js → NotFound-BMeQSGcG.js} +1 -1
  14. package/dist/ui/assets/ProvidersPage-Ct6HsAi1.js +1 -0
  15. package/dist/ui/assets/{RecoverPage-DNb1Pr8h.js → RecoverPage-DpW3l-yv.js} +1 -1
  16. package/dist/ui/assets/SettingsPage-DBJ7_E6C.js +9 -0
  17. package/dist/ui/assets/SetupPage-DKmLVUaj.js +1 -0
  18. package/dist/ui/assets/{WebChatPage-R-YOwA4F.js → WebChatPage-CVRcKept.js} +2 -2
  19. package/dist/ui/assets/accordion-C5d5Rm5z.js +1 -0
  20. package/dist/ui/assets/{alert-U8FsgWi7.js → alert-C-NE-P3s.js} +1 -1
  21. package/dist/ui/assets/{alert-dialog-CRdMkkmk.js → alert-dialog-C5mzbHdP.js} +1 -1
  22. package/dist/ui/assets/{badge-Cli1jnH5.js → badge-ChpACfWO.js} +1 -1
  23. package/dist/ui/assets/chevron-up-BYhk0K2J.js +1 -0
  24. package/dist/ui/assets/{dialog-DQ3s-LuO.js → dialog-QnZ0ad8O.js} +1 -1
  25. package/dist/ui/assets/dropdown-menu-BK-CO3Od.js +1 -0
  26. package/dist/ui/assets/{es-DcMjrpbA.js → es-NQNoaWDx.js} +1 -1
  27. package/dist/ui/assets/index-B2fCYtTS.css +2 -0
  28. package/dist/ui/assets/index-DMCjjdqf.js +116 -0
  29. package/dist/ui/assets/{label-0BvGVXvZ.js → label-D2H1IR_J.js} +1 -1
  30. package/dist/ui/assets/progress-BherYzY6.js +1 -0
  31. package/dist/ui/assets/scroll-area-DkeyX32e.js +1 -0
  32. package/dist/ui/assets/{slider-D47dOrRa.js → slider-CsiUDxc3.js} +1 -1
  33. package/dist/ui/assets/switch-BDwN8RYV.js +1 -0
  34. package/dist/ui/assets/{table-DhowbNxQ.js → table-CSc8ubon.js} +1 -1
  35. package/dist/ui/assets/terminal-DN38Q456.js +1 -0
  36. package/dist/ui/assets/useProviders-C6_QHsEi.js +1 -0
  37. package/dist/ui/assets/{vendor-radix-JY4ncZrD.js → vendor-radix-cw1bQaVC.js} +4 -4
  38. package/dist/ui/assets/{vendor-react-CscwQerf.js → vendor-react-D4s9E-zj.js} +1 -1
  39. package/dist/ui/dist/assets/AgentCreateForm-tJZv9FZC.js +1 -0
  40. package/dist/ui/dist/assets/AgentDetailPage-Du-mRcAX.js +1 -0
  41. package/dist/ui/dist/assets/AgentNewPage-DIFYd_Ys.js +1 -0
  42. package/dist/ui/dist/assets/AgentsPage-YvSgWRiw.js +10 -0
  43. package/dist/ui/dist/assets/CanvasPage-DtMwGvxf.js +33 -0
  44. package/dist/ui/dist/assets/ChannelsPage-BdBXWHjj.js +8 -0
  45. package/dist/ui/dist/assets/DashboardPage-ghl1ZguH.js +6 -0
  46. package/dist/ui/dist/assets/LoginPage-CAmSI9Vy.js +1 -0
  47. package/dist/ui/dist/assets/LogsPage-DAPBHkwK.js +1 -0
  48. package/dist/ui/dist/assets/MeetingPage-WjjGOqqU.js +1 -0
  49. package/dist/ui/dist/assets/NotFound-BMeQSGcG.js +1 -0
  50. package/dist/ui/dist/assets/ProvidersPage-Ct6HsAi1.js +1 -0
  51. package/dist/ui/dist/assets/RecoverPage-DpW3l-yv.js +1 -0
  52. package/dist/ui/dist/assets/SettingsPage-DBJ7_E6C.js +9 -0
  53. package/dist/ui/dist/assets/SetupPage-DKmLVUaj.js +1 -0
  54. package/dist/ui/dist/assets/WebChatPage-CVRcKept.js +16 -0
  55. package/dist/ui/dist/assets/accordion-C5d5Rm5z.js +1 -0
  56. package/dist/ui/dist/assets/activity-c3pNngT_.js +1 -0
  57. package/dist/ui/dist/assets/alert-C-NE-P3s.js +1 -0
  58. package/dist/ui/dist/assets/alert-dialog-C5mzbHdP.js +1 -0
  59. package/dist/ui/dist/assets/arrow-left-CBcbX5EZ.js +1 -0
  60. package/dist/ui/dist/assets/badge-ChpACfWO.js +1 -0
  61. package/dist/ui/dist/assets/calendar-B-KZ9RQO.js +1 -0
  62. package/dist/ui/dist/assets/card-CNf6BS2e.js +1 -0
  63. package/dist/ui/dist/assets/chevron-left-D4U-5A27.js +1 -0
  64. package/dist/ui/dist/assets/chevron-right-CR4Skrf3.js +1 -0
  65. package/dist/ui/dist/assets/chevron-up-BYhk0K2J.js +1 -0
  66. package/dist/ui/dist/assets/circle-alert-CyHDwUj8.js +1 -0
  67. package/dist/ui/dist/assets/circle-check-Bb54Ebmu.js +1 -0
  68. package/dist/ui/dist/assets/cpu-Cdgc_B1K.js +1 -0
  69. package/dist/ui/dist/assets/dialog-QnZ0ad8O.js +1 -0
  70. package/dist/ui/dist/assets/download-C3ifGMjJ.js +1 -0
  71. package/dist/ui/dist/assets/dropdown-menu-BK-CO3Od.js +1 -0
  72. package/dist/ui/dist/assets/es-NQNoaWDx.js +1 -0
  73. package/dist/ui/dist/assets/external-link-BvxYeTP1.js +1 -0
  74. package/dist/ui/dist/assets/eye-DqNTU_GD.js +1 -0
  75. package/dist/ui/dist/assets/file-text-BT_9S9SM.js +1 -0
  76. package/dist/ui/dist/assets/folder-open-BhH8y9ac.js +1 -0
  77. package/dist/ui/dist/assets/format-GVHeOyWI.js +1 -0
  78. package/dist/ui/dist/assets/gateway-url-COCbW0IR.js +1 -0
  79. package/dist/ui/dist/assets/gauge-D_TMa4i9.js +1 -0
  80. package/dist/ui/dist/assets/globe-DeCQTCDJ.js +1 -0
  81. package/dist/ui/dist/assets/hexagon-DsGOUl-H.js +1 -0
  82. package/dist/ui/dist/assets/history-BSG-Ypqf.js +1 -0
  83. package/dist/ui/dist/assets/index-B2fCYtTS.css +2 -0
  84. package/dist/ui/dist/assets/index-DMCjjdqf.js +116 -0
  85. package/dist/ui/dist/assets/info-NwLoa2Mj.js +1 -0
  86. package/dist/ui/dist/assets/key-3EP0dhkT.js +1 -0
  87. package/dist/ui/dist/assets/label-D2H1IR_J.js +1 -0
  88. package/dist/ui/dist/assets/loader-circle-CZNax6kS.js +1 -0
  89. package/dist/ui/dist/assets/lock-Ei1_J-Nq.js +1 -0
  90. package/dist/ui/dist/assets/pause-BUqah9Bi.js +1 -0
  91. package/dist/ui/dist/assets/play-NcZ4swwL.js +1 -0
  92. package/dist/ui/dist/assets/plus-CX1xyhp5.js +1 -0
  93. package/dist/ui/dist/assets/progress-BherYzY6.js +1 -0
  94. package/dist/ui/dist/assets/refresh-cw-DaYdjQFk.js +1 -0
  95. package/dist/ui/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
  96. package/dist/ui/dist/assets/save-CUdYyHNy.js +1 -0
  97. package/dist/ui/dist/assets/scroll-area-DkeyX32e.js +1 -0
  98. package/dist/ui/dist/assets/send-B0H5SEIE.js +1 -0
  99. package/dist/ui/dist/assets/settings-Ds4SqD8s.js +1 -0
  100. package/dist/ui/dist/assets/slider-CsiUDxc3.js +14 -0
  101. package/dist/ui/dist/assets/sparkles-yUEb-7oH.js +1 -0
  102. package/dist/ui/dist/assets/square-BD81nFtN.js +1 -0
  103. package/dist/ui/dist/assets/switch-BDwN8RYV.js +1 -0
  104. package/dist/ui/dist/assets/table-CSc8ubon.js +1 -0
  105. package/dist/ui/dist/assets/terminal-DN38Q456.js +1 -0
  106. package/dist/ui/dist/assets/textarea-CXgXWKrT.js +1 -0
  107. package/dist/ui/dist/assets/trash-2-CNjMkoq6.js +1 -0
  108. package/dist/ui/dist/assets/triangle-alert-C9Y8Ub4X.js +1 -0
  109. package/dist/ui/dist/assets/useProviders-C6_QHsEi.js +1 -0
  110. package/dist/ui/dist/assets/utils-3pnRFmFe.js +1 -0
  111. package/dist/ui/dist/assets/vendor-charts-Bu2lyBKP.js +65 -0
  112. package/dist/ui/dist/assets/vendor-query-DsWPbQdG.js +1 -0
  113. package/dist/ui/dist/assets/vendor-radix-cw1bQaVC.js +63 -0
  114. package/dist/ui/dist/assets/vendor-react-D4s9E-zj.js +1 -0
  115. package/dist/ui/dist/assets/vendor-router-C9pIYwbJ.js +3 -0
  116. package/dist/ui/dist/assets/volume-2-CeSXNDv4.js +1 -0
  117. package/dist/ui/dist/assets/zap-hlXjpSeA.js +1 -0
  118. package/dist/ui/dist/favicon.ico +0 -0
  119. package/dist/ui/dist/index.html +40 -0
  120. package/dist/ui/dist/placeholder.svg +1 -0
  121. package/dist/ui/index.html +6 -6
  122. package/package.json +137 -13
  123. package/packages/cli/src/adapters/binary.ts +461 -0
  124. package/packages/cli/src/adapters/bun-global.ts +378 -0
  125. package/packages/cli/src/adapters/config.ts +314 -0
  126. package/packages/cli/src/adapters/docker.ts +308 -0
  127. package/packages/cli/src/adapters/factory.ts +168 -0
  128. package/packages/cli/src/adapters/index.ts +80 -0
  129. package/packages/cli/src/adapters/types.ts +218 -0
  130. package/packages/cli/src/commands/agent-run.ts +168 -0
  131. package/packages/cli/src/commands/agents.ts +398 -0
  132. package/packages/cli/src/commands/chat.ts +142 -0
  133. package/packages/cli/src/commands/config.ts +49 -0
  134. package/packages/cli/src/commands/cron.ts +487 -0
  135. package/packages/cli/src/commands/dev.ts +58 -0
  136. package/packages/cli/src/commands/doctor.ts +320 -0
  137. package/packages/cli/src/commands/gateway.ts +719 -0
  138. package/packages/cli/src/commands/logs.ts +57 -0
  139. package/packages/cli/src/commands/mcp.ts +175 -0
  140. package/packages/cli/src/commands/message.ts +77 -0
  141. package/packages/cli/src/commands/migrate.ts +90 -0
  142. package/packages/cli/src/commands/onboard.ts +1656 -0
  143. package/packages/cli/src/commands/security.ts +144 -0
  144. package/packages/cli/src/commands/service.ts +50 -0
  145. package/packages/cli/src/commands/sessions.ts +116 -0
  146. package/packages/cli/src/commands/skills.ts +215 -0
  147. package/packages/cli/src/commands/update.ts +203 -0
  148. package/packages/cli/src/index.ts +210 -0
  149. package/packages/cli/src/ui-bundle.generated.ts +3 -0
  150. package/packages/cli/src/utils/token.ts +6 -0
  151. package/packages/core/src/agent/agent-loop.ts +691 -0
  152. package/packages/core/src/agent/compaction.ts +240 -0
  153. package/packages/core/src/agent/context-compiler.ts +467 -0
  154. package/packages/core/src/agent/context-guard.ts +91 -0
  155. package/packages/core/src/agent/conversation-store.ts +244 -0
  156. package/packages/core/src/agent/curator.ts +158 -0
  157. package/packages/core/src/agent/hooks.ts +166 -0
  158. package/packages/core/src/agent/llm-client.ts +167 -0
  159. package/packages/core/src/agent/llm-providers/anthropic.ts +212 -0
  160. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  161. package/packages/core/src/agent/llm-providers/gemini.ts +215 -0
  162. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  163. package/packages/core/src/agent/llm-providers/interface.ts +195 -0
  164. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  165. package/packages/core/src/agent/llm-providers/local-llama.ts +37 -0
  166. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  167. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  168. package/packages/core/src/agent/llm-providers/ollama.ts +175 -0
  169. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +379 -0
  170. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  171. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  172. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  173. package/packages/core/src/agent/native-tools.ts +31 -0
  174. package/packages/core/src/agent/playbook-selector.ts +147 -0
  175. package/packages/core/src/agent/prompt-builder.ts +169 -0
  176. package/packages/core/src/agent/providers/index.ts +204 -0
  177. package/packages/core/src/agent/providers.ts +1 -0
  178. package/packages/core/src/agent/reflector.ts +200 -0
  179. package/packages/core/src/agent/service.ts +267 -0
  180. package/packages/core/src/agent/skill-selector.ts +479 -0
  181. package/packages/core/src/agent/stuck-loop.ts +133 -0
  182. package/packages/core/src/agent/tool-selector.ts +569 -0
  183. package/packages/core/src/agent/tracer.ts +100 -0
  184. package/packages/core/src/auth/auth.ts +108 -0
  185. package/packages/core/src/auth/index.ts +1 -0
  186. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  187. package/packages/core/src/canvas/canvas-manager.ts +390 -0
  188. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  189. package/packages/core/src/canvas/emitter.ts +149 -0
  190. package/packages/core/src/canvas/index.ts +3 -0
  191. package/packages/core/src/channels/base.ts +154 -0
  192. package/packages/core/src/channels/discord.ts +273 -0
  193. package/packages/core/src/channels/index.ts +7 -0
  194. package/packages/core/src/channels/manager.ts +450 -0
  195. package/packages/core/src/channels/slack.ts +323 -0
  196. package/packages/core/src/channels/telegram.ts +612 -0
  197. package/packages/core/src/channels/webchat.ts +139 -0
  198. package/packages/core/src/channels/whatsapp.ts +548 -0
  199. package/packages/core/src/config/index.ts +12 -0
  200. package/packages/core/src/config/loader.ts +569 -0
  201. package/packages/core/src/events/agent-bus.ts +460 -0
  202. package/packages/core/src/events/event-bus.ts +169 -0
  203. package/packages/core/src/gateway/channel-notify.ts +64 -0
  204. package/packages/core/src/gateway/helpers/cors.ts +32 -0
  205. package/packages/core/src/gateway/helpers/index.ts +4 -0
  206. package/packages/core/src/gateway/helpers/narration.ts +57 -0
  207. package/packages/core/src/gateway/helpers/path.ts +13 -0
  208. package/packages/core/src/gateway/helpers/redact.ts +61 -0
  209. package/packages/core/src/gateway/index.ts +5 -0
  210. package/packages/core/src/gateway/initializer.ts +363 -0
  211. package/packages/core/src/gateway/lane-queue.ts +169 -0
  212. package/packages/core/src/gateway/llm-local/client.ts +94 -0
  213. package/packages/core/src/gateway/llm-local/detector.ts +321 -0
  214. package/packages/core/src/gateway/llm-local/downloader.ts +216 -0
  215. package/packages/core/src/gateway/llm-local/index.ts +34 -0
  216. package/packages/core/src/gateway/llm-local/manager.ts +186 -0
  217. package/packages/core/src/gateway/llm-local/models.ts +149 -0
  218. package/packages/core/src/gateway/llm-local/server.ts +179 -0
  219. package/packages/core/src/gateway/resolver.ts +108 -0
  220. package/packages/core/src/gateway/router.ts +124 -0
  221. package/packages/core/src/gateway/routes/agents.ts +210 -0
  222. package/packages/core/src/gateway/routes/auth.ts +244 -0
  223. package/packages/core/src/gateway/routes/channels.ts +484 -0
  224. package/packages/core/src/gateway/routes/chat.ts +241 -0
  225. package/packages/core/src/gateway/routes/config.ts +12 -0
  226. package/packages/core/src/gateway/routes/cron-api.ts +544 -0
  227. package/packages/core/src/gateway/routes/ethics.ts +46 -0
  228. package/packages/core/src/gateway/routes/llm-local.ts +271 -0
  229. package/packages/core/src/gateway/routes/mcp.ts +319 -0
  230. package/packages/core/src/gateway/routes/meeting.ts +232 -0
  231. package/packages/core/src/gateway/routes/models.ts +163 -0
  232. package/packages/core/src/gateway/routes/multimodal.ts +93 -0
  233. package/packages/core/src/gateway/routes/providers.ts +220 -0
  234. package/packages/core/src/gateway/routes/setup.ts +441 -0
  235. package/packages/core/src/gateway/routes/skills.ts +115 -0
  236. package/packages/core/src/gateway/routes/system.ts +469 -0
  237. package/packages/core/src/gateway/routes/tasks.ts +44 -0
  238. package/packages/core/src/gateway/routes/tools.ts +59 -0
  239. package/packages/core/src/gateway/routes/tts-local.ts +388 -0
  240. package/packages/core/src/gateway/routes/users.ts +122 -0
  241. package/packages/core/src/gateway/routes/voice.ts +189 -0
  242. package/packages/core/src/gateway/routes/workspace.ts +281 -0
  243. package/packages/core/src/gateway/server.ts +2744 -0
  244. package/packages/core/src/gateway/session.ts +95 -0
  245. package/packages/core/src/gateway/slash-commands.ts +207 -0
  246. package/packages/core/src/gateway/tts/README.md +94 -0
  247. package/packages/core/src/gateway/tts/package.json +25 -0
  248. package/packages/core/src/gateway/tts/src/client.ts +59 -0
  249. package/packages/core/src/gateway/tts/src/detect.ts +42 -0
  250. package/packages/core/src/gateway/tts/src/index.ts +15 -0
  251. package/packages/core/src/gateway/tts/src/install.ts +129 -0
  252. package/packages/core/src/gateway/tts/src/models.ts +50 -0
  253. package/packages/core/src/gateway/tts/src/server.ts +252 -0
  254. package/packages/core/src/gateway/tts/voices/.gitkeep +0 -0
  255. package/packages/core/src/heartbeat/index.ts +157 -0
  256. package/packages/core/src/index.ts +56 -0
  257. package/packages/core/src/mcp/hot-reload.ts +148 -0
  258. package/packages/core/src/mcp/singleton.ts +21 -0
  259. package/packages/core/src/mcp/tool-sync.ts +176 -0
  260. package/packages/core/src/multimodal/index.ts +2 -0
  261. package/packages/core/src/multimodal/types.ts +28 -0
  262. package/packages/core/src/multimodal/vision-service.ts +283 -0
  263. package/packages/core/src/plugins/api.ts +128 -0
  264. package/packages/core/src/plugins/index.ts +2 -0
  265. package/packages/core/src/plugins/loader.ts +365 -0
  266. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  267. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  268. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  269. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  270. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  271. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  272. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  273. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  274. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  275. package/packages/core/src/scheduler/dag/index.ts +26 -0
  276. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  277. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  278. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  279. package/packages/core/src/scheduler/index.ts +22 -0
  280. package/packages/core/src/scheduler/integration.ts +237 -0
  281. package/packages/core/src/scheduler/types.ts +164 -0
  282. package/packages/core/src/security/google-chat.ts +269 -0
  283. package/packages/core/src/security/index.ts +192 -0
  284. package/packages/core/src/security/pairing.ts +250 -0
  285. package/packages/core/src/security/rate-limit.ts +270 -0
  286. package/packages/core/src/security/signal.ts +321 -0
  287. package/packages/core/src/state/store.ts +312 -0
  288. package/packages/core/src/storage/crypto.ts +197 -0
  289. package/packages/core/src/storage/migrate.ts +147 -0
  290. package/packages/core/src/storage/onboarding.ts +1506 -0
  291. package/packages/core/src/storage/schema.ts +666 -0
  292. package/packages/core/src/storage/seed.ts +628 -0
  293. package/packages/core/src/storage/sqlite.ts +407 -0
  294. package/packages/core/src/storage/usage.ts +374 -0
  295. package/packages/core/src/tool-runtime/index.ts +476 -0
  296. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  297. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  298. package/packages/core/src/tools/agents/index.ts +610 -0
  299. package/packages/core/src/tools/canvas/index.ts +420 -0
  300. package/packages/core/src/tools/cli/index.ts +142 -0
  301. package/packages/core/src/tools/core/index.ts +478 -0
  302. package/packages/core/src/tools/cron/index.ts +635 -0
  303. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  304. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  305. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  306. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  307. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  308. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  309. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  310. package/packages/core/src/tools/filesystem/index.ts +34 -0
  311. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  312. package/packages/core/src/tools/index.ts +197 -0
  313. package/packages/core/src/tools/meeting/index.ts +363 -0
  314. package/packages/core/src/tools/office/index.ts +47 -0
  315. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  316. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  317. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  318. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  319. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  320. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  321. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  322. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  323. package/packages/core/src/tools/types.ts +39 -0
  324. package/packages/core/src/tools/voice/index.ts +104 -0
  325. package/packages/core/src/tools/web/browser-click.ts +78 -0
  326. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  327. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  328. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  329. package/packages/core/src/tools/web/browser-script.ts +88 -0
  330. package/packages/core/src/tools/web/browser-service.ts +554 -0
  331. package/packages/core/src/tools/web/browser-type.ts +101 -0
  332. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  333. package/packages/core/src/tools/web/index.ts +41 -0
  334. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  335. package/packages/core/src/tools/web/web-search.ts +123 -0
  336. package/packages/core/src/utils/benchmark.ts +80 -0
  337. package/packages/core/src/utils/crypto.ts +73 -0
  338. package/packages/core/src/utils/date.ts +42 -0
  339. package/packages/core/src/utils/index.ts +5 -0
  340. package/packages/core/src/utils/logger.ts +389 -0
  341. package/packages/core/src/utils/retry.ts +70 -0
  342. package/packages/core/src/utils/toon.ts +253 -0
  343. package/packages/core/src/voice/index.ts +643 -0
  344. package/packages/mcp/src/config.ts +13 -0
  345. package/packages/mcp/src/index.ts +1 -0
  346. package/packages/mcp/src/logger.ts +47 -0
  347. package/packages/mcp/src/manager.ts +439 -0
  348. package/packages/mcp/src/transports/index.ts +67 -0
  349. package/packages/mcp/src/transports/sse.ts +238 -0
  350. package/packages/mcp/src/transports/websocket.ts +159 -0
  351. package/packages/skills/src/bundled/agents/agent_spawner/SKILL.md +167 -0
  352. package/packages/skills/src/bundled/agents/code_delegator/SKILL.md +156 -0
  353. package/packages/skills/src/bundled/agents/memory_manager/SKILL.md +143 -0
  354. package/packages/skills/src/bundled/agents/research_and_remember/SKILL.md +139 -0
  355. package/packages/skills/src/bundled/agents/task_orchestrator/SKILL.md +198 -0
  356. package/packages/skills/src/bundled/canvas/a2ui_dashboard/SKILL.md +176 -0
  357. package/packages/skills/src/bundled/canvas/a2ui_form/SKILL.md +202 -0
  358. package/packages/skills/src/bundled/canvas/a2ui_interactive/SKILL.md +206 -0
  359. package/packages/skills/src/bundled/canvas/canvas_dashboard/SKILL.md +146 -0
  360. package/packages/skills/src/bundled/canvas/canvas_interact/SKILL.md +148 -0
  361. package/packages/skills/src/bundled/canvas/canvas_report/SKILL.md +146 -0
  362. package/packages/skills/src/bundled/cli/cli_pipeline/SKILL.md +136 -0
  363. package/packages/skills/src/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  364. package/packages/skills/src/bundled/cron_manager/SKILL.md +188 -0
  365. package/packages/skills/src/bundled/cron_reminder/SKILL.md +112 -0
  366. package/packages/skills/src/bundled/filesystem/file_manager/SKILL.md +118 -0
  367. package/packages/skills/src/bundled/filesystem/file_read_and_summarize/SKILL.md +108 -0
  368. package/packages/skills/src/bundled/filesystem/file_writer/SKILL.md +135 -0
  369. package/packages/skills/src/bundled/meeting/meeting_transcription/SKILL.md +213 -0
  370. package/packages/skills/src/bundled/office/office_document_manager/SKILL.md +262 -0
  371. package/packages/skills/src/bundled/search_knowledge/busqueda_fts5/SKILL.md +74 -0
  372. package/packages/skills/src/bundled/voice/voice_assistant/SKILL.md +174 -0
  373. package/packages/skills/src/bundled/voice/voice_input/SKILL.md +146 -0
  374. package/packages/skills/src/bundled/voice/voice_output/SKILL.md +151 -0
  375. package/packages/skills/src/bundled/web/browser_automate/SKILL.md +120 -0
  376. package/packages/skills/src/bundled/web/browser_scrape/SKILL.md +109 -0
  377. package/packages/skills/src/bundled/web/web_monitor/SKILL.md +127 -0
  378. package/packages/skills/src/bundled/web/web_research/SKILL.md +119 -0
  379. package/packages/skills/src/bundled-data.generated.ts +1964 -0
  380. package/packages/skills/src/index.ts +1 -0
  381. package/packages/skills/src/loader.ts +388 -0
  382. package/dist/ui/assets/AgentNewPage-GB-tVN50.js +0 -1
  383. package/dist/ui/assets/BridgePage-DDcDILKu.js +0 -1
  384. package/dist/ui/assets/CanvasPage-oOk2sGOD.js +0 -33
  385. package/dist/ui/assets/DashboardPage-DV_2qWYJ.js +0 -6
  386. package/dist/ui/assets/LogsPage-DayYjh01.js +0 -1
  387. package/dist/ui/assets/MeetingPage-C01uPuqj.js +0 -1
  388. package/dist/ui/assets/ProjectsPage-B8_am_Ib.js +0 -1
  389. package/dist/ui/assets/ProvidersPage-DBzi66e4.js +0 -1
  390. package/dist/ui/assets/SettingsPage-CFA_Tknl.js +0 -9
  391. package/dist/ui/assets/SetupPage-BrUWbhvT.js +0 -1
  392. package/dist/ui/assets/accordion-DdAEfIXR.js +0 -1
  393. package/dist/ui/assets/chevron-down-DIosfU_U.js +0 -1
  394. package/dist/ui/assets/chevron-up-CI-W21Fy.js +0 -1
  395. package/dist/ui/assets/circle-S0-ouLz-.js +0 -1
  396. package/dist/ui/assets/circle-minus-CE0iJrl8.js +0 -1
  397. package/dist/ui/assets/circle-x-jUJ5zZvQ.js +0 -1
  398. package/dist/ui/assets/dropdown-menu-C2CXM1VE.js +0 -1
  399. package/dist/ui/assets/index-BN0875JH.css +0 -2
  400. package/dist/ui/assets/index-CH6sBa3Q.js +0 -116
  401. package/dist/ui/assets/pencil-5VdSj-h5.js +0 -1
  402. package/dist/ui/assets/progress-JN30I5fF.js +0 -1
  403. package/dist/ui/assets/scroll-area-BQQPitM8.js +0 -1
  404. package/dist/ui/assets/search-ChPgnVKj.js +0 -1
  405. package/dist/ui/assets/switch-C7W2-KEx.js +0 -1
  406. package/dist/ui/assets/terminal-C-R5Fckz.js +0 -1
  407. package/dist/ui/assets/useProviders-TBnWn-Hq.js +0 -1
  408. /package/dist/ui/assets/{card-DFKnZ6ky.js → card-CNf6BS2e.js} +0 -0
  409. /package/dist/ui/assets/{circle-alert-KuAm2FWh.js → circle-alert-CyHDwUj8.js} +0 -0
  410. /package/dist/ui/assets/{circle-check-6Ard1-2z.js → circle-check-Bb54Ebmu.js} +0 -0
  411. /package/dist/ui/assets/{cpu-KDy6-FAI.js → cpu-Cdgc_B1K.js} +0 -0
  412. /package/dist/ui/assets/{download-Cjbk4Rek.js → download-C3ifGMjJ.js} +0 -0
  413. /package/dist/ui/assets/{external-link-HtrFM63g.js → external-link-BvxYeTP1.js} +0 -0
  414. /package/dist/ui/assets/{eye-D1dB40_o.js → eye-DqNTU_GD.js} +0 -0
  415. /package/dist/ui/assets/{file-text-CE58EfH0.js → file-text-BT_9S9SM.js} +0 -0
  416. /package/dist/ui/assets/{folder-open-DIPKeiI_.js → folder-open-BhH8y9ac.js} +0 -0
  417. /package/dist/ui/assets/{format-BwdV8bB5.js → format-GVHeOyWI.js} +0 -0
  418. /package/dist/ui/assets/{gateway-url-D5uj6Nxg.js → gateway-url-COCbW0IR.js} +0 -0
  419. /package/dist/ui/assets/{gauge-DmQmJHEg.js → gauge-D_TMa4i9.js} +0 -0
  420. /package/dist/ui/assets/{globe-_hUGxQF4.js → globe-DeCQTCDJ.js} +0 -0
  421. /package/dist/ui/assets/{hexagon-BaNGQlQj.js → hexagon-DsGOUl-H.js} +0 -0
  422. /package/dist/ui/assets/{history-BfZVGlZa.js → history-BSG-Ypqf.js} +0 -0
  423. /package/dist/ui/assets/{info-CBZ5-AlC.js → info-NwLoa2Mj.js} +0 -0
  424. /package/dist/ui/assets/{key-Bv5DdTPh.js → key-3EP0dhkT.js} +0 -0
  425. /package/dist/ui/assets/{loader-circle-C4hhXLgp.js → loader-circle-CZNax6kS.js} +0 -0
  426. /package/dist/ui/assets/{lock-CkZYexqw.js → lock-Ei1_J-Nq.js} +0 -0
  427. /package/dist/ui/assets/{pause-Bpy1_s7y.js → pause-BUqah9Bi.js} +0 -0
  428. /package/dist/ui/assets/{play-Cj4osqJZ.js → play-NcZ4swwL.js} +0 -0
  429. /package/dist/ui/assets/{plus-BQhgZN3A.js → plus-CX1xyhp5.js} +0 -0
  430. /package/dist/ui/assets/{refresh-cw-BfREHVQM.js → refresh-cw-DaYdjQFk.js} +0 -0
  431. /package/dist/ui/assets/{save-FFTD4dMp.js → save-CUdYyHNy.js} +0 -0
  432. /package/dist/ui/assets/{settings-BdHKUL92.js → settings-Ds4SqD8s.js} +0 -0
  433. /package/dist/ui/assets/{sparkles-r4uJbJAl.js → sparkles-yUEb-7oH.js} +0 -0
  434. /package/dist/ui/assets/{square-G7Hyufqm.js → square-BD81nFtN.js} +0 -0
  435. /package/dist/ui/assets/{textarea-5kyuD04X.js → textarea-CXgXWKrT.js} +0 -0
  436. /package/dist/ui/assets/{trash-2-DXVBRWfh.js → trash-2-CNjMkoq6.js} +0 -0
  437. /package/dist/ui/assets/{triangle-alert-Bu5seg9O.js → triangle-alert-C9Y8Ub4X.js} +0 -0
  438. /package/dist/ui/assets/{vendor-router-CCECILJ0.js → vendor-router-C9pIYwbJ.js} +0 -0
  439. /package/dist/ui/assets/{volume-2-s9DuS696.js → volume-2-CeSXNDv4.js} +0 -0
  440. /package/dist/ui/assets/{zap-BPHZzXKV.js → zap-hlXjpSeA.js} +0 -0
@@ -0,0 +1,2744 @@
1
+ import type { Config } from "../config/loader";
2
+ import { loadConfig, getHiveDir } from "../config/loader";
3
+ import { logger, onLogEntry } from "../utils/logger";
4
+ import { sessionManager, parseSessionId } from "./session";
5
+ import { laneQueue } from "./lane-queue";
6
+ import {
7
+ type InboundMessage,
8
+ type OutboundMessage,
9
+ isSlashCommand,
10
+ executeSlashCommand,
11
+ } from "./slash-commands";
12
+ import { ChannelManager } from "../channels/manager";
13
+ import { AgentService } from "../agent/service";
14
+ import { AgentRunner } from "../agent/providers/index";
15
+ import type { IncomingMessage } from "../channels/base";
16
+ import { mkdirSync, rmSync, unlinkSync, watch, existsSync, writeFileSync, readFileSync } from "node:fs";
17
+ import * as path from "node:path";
18
+
19
+ // Read version from package.json at module load time
20
+ const _pkgVersion = (() => {
21
+ try {
22
+ const pkgPath = path.join(import.meta.dir, "../../../package.json");
23
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version as string;
24
+ } catch {
25
+ return "0.0.27";
26
+ }
27
+ })();
28
+ import { cpus as osCpus } from "node:os";
29
+ import { getDb, getDbPathLazy, initializeDatabase } from "../storage/sqlite";
30
+ import { seedAllData } from "../storage/seed";
31
+ import { canvasManager } from "../canvas/canvas-manager.ts";
32
+ import { subscribeCanvas, unsubscribeCanvas, emitCanvas, getCanvasSnapshot, removeCanvasComponent } from "../canvas/emitter";
33
+ import { resolveCanvasInteraction } from "../tools/canvas/index";
34
+ import { randomUUID } from "crypto";
35
+ import { legacyDecryptAES, loadMcpHeaders } from "../storage/crypto.ts";
36
+ import { resolveContext } from "./resolver";
37
+ import { voiceService } from "../voice/index";
38
+ import { multimodalService } from "../multimodal/index";
39
+ import { initializeGateway, type GatewayInitializationResult } from "./initializer";
40
+ import { handleSetupStatus, handleVerifyProvider, handleCompleteSetup, handleSetupProviders, handleSetupEthics, handleSetupOllamaModels } from "./routes/setup";
41
+ import { handleAuthStatus, handleLogin, handleSetupCredentials, handleChangePassword, handleRecover, handleDisableAuth, handleRecoveryKey } from "./routes/auth";
42
+ import { resolveUserId } from "../storage/onboarding";
43
+ import { handleGetAgents, handleCreateAgent, handleUpdateAgent, handleDeleteAgent } from "./routes/agents";
44
+ import { handleGetProviders, handleCreateProvider, handleToggleProvider, handleUpdateProvider, handleSyncProviderModels } from "./routes/providers";
45
+ import { handleGetUsers, handleCreateUser, handleUpdateUserSettings, handleGetUserChannels, handleLinkUserChannel } from "./routes/users";
46
+ import { handleGetSkills, handleActivateSkill, handleUpdateSkill, handleDeleteSkill, handleCreateSkill } from "./routes/skills";
47
+ import { handleGetEthics, handleActivateEthics, handleDeleteEthics } from "./routes/ethics";
48
+ import { handleGetTools, handleActivateTool, handleUpdateTool } from "./routes/tools";
49
+ import { handleGetTasks, handleUpdateTask } from "./routes/tasks";
50
+ import { setChannelSendFn } from "./channel-notify";
51
+ import { CronScheduler } from "../scheduler/CronScheduler";
52
+ import { DAGScheduler, ParallelStrategy } from "../scheduler/dag/index";
53
+ import { createTaskHandler, setSchedulerForCleanup } from "../scheduler/integration";
54
+
55
+ import { setSchedulerInstance as setScheduleToolsInstance } from "../tools/cron/index.ts";
56
+ import { setSchedulerInstance as setCronApiInstance } from "./routes/cron-api";
57
+ import {
58
+ handleGetCronJobs,
59
+ handleGetCronJob,
60
+ handleCreateCronJob,
61
+ handleUpdateCronJob,
62
+ handleDeleteCronJob,
63
+ handlePauseCronJob,
64
+ handleResumeCronJob,
65
+ handleTriggerCronJob,
66
+ handleGetCronJobHistory,
67
+ handleGetCronStatus,
68
+ handleGetCronChannels,
69
+ } from "./routes/cron-api";
70
+ import { handleGetChannels, handleGetChannelConfig, handleActivateChannel, handleDeactivateChannel, handleCreateChannel, handleGetChannelAccount, handleUpdateChannelAccount, handleDeleteChannelAccount, handleChannelAction, handleUpdateChannelSettings, handleToggleChannel, handleGetChannelStatus, handleReconnectChannel, handleGetWhatsAppDetails, handleDisconnectWhatsApp, handleUpdateWhatsAppConfig } from "./routes/channels";
71
+ import { handleGetMcpServers, handleGetMcpServerDetail, handleCreateMcpServer, handleUpdateMcpServer, handleDeleteMcpServer, handleToggleMcpServer, handleGetMCPServerTools } from "./routes/mcp";
72
+ import { handleGetModels, handleCreateModel, handleToggleModel, handleGetModelsConfig, handleUpdateModelsConfig, handleDeleteModel, handleUpdateModel } from "./routes/models";
73
+ import { handleGetVoiceProviders, handleGetConfiguredVoiceProviders, handleSaveVoiceProviderKey, handleTestVoice, handleGetChannelVoice, handleUpdateChannelVoice, handleGetVoiceProviderVoices } from "./routes/voice";
74
+ import { handleGetVisionProviders, handleGetChannelVision, handleUpdateChannelVision, handleOcrImage } from "./routes/multimodal";
75
+ import { handleGetLocalTTSStatus, handleGetLocalTTSLogs, handleInstallLocalTTS, handleStartLocalTTS, handleStopLocalTTS, handleSpeakLocalTTS, handleGetAvailableModels, handleGetInstalledVoices, handleDownloadModel, handleGetDownloadLogs, initializeLocalTTS } from "./routes/tts-local";
76
+ import { handleGetLocalLLMStatus, handleGetLocalLLMLogs, handleInstallLocalLLM, handleStartLocalLLM, handleStopLocalLLM, handleDownloadLLMModel, initializeLocalLLM } from "./routes/llm-local";
77
+ import { handleCreateMeeting, handleListMeetings, handleGetMeeting, handleAddMeetingSegment, handleStopMeeting } from "./routes/meeting";
78
+ import { handleGetActivityStats, handleGetSystemStats, handleGetUsageStats, handleSystemReload, handleApiReload, handleGetVersion, handleTriggerUpdate } from "./routes/system";
79
+ import { handleGetChatHistory, handleGetCanvas, handleGetNotes, handleUpdateNote } from "./routes/chat";
80
+ import { handleChat as handlePostChat } from "./routes/chat";
81
+ import { handleGetConfig } from "./routes/config";
82
+ import { handleGetWorkspace, handleUpdateWorkspace, handleValidateWorkspace, handleCreateWorkspace, handleOpenWorkspace } from "./routes/workspace";
83
+ import { getNarration, expandPath, addCorsHeaders, CORS_ORIGINS } from "./helpers";
84
+ import { redactConfig } from "./helpers";
85
+
86
+ const logSubscribers = new Set<string>();
87
+
88
+ // Helpers imported from ./helpers/index.ts
89
+ // - getNarration, TOOL_NARRATIONS
90
+ // - expandPath
91
+ // - addCorsHeaders, CORS_ORIGINS
92
+ // - redactConfig, redactValue
93
+
94
+ interface WebSocketData {
95
+ sessionId: string;
96
+ authenticatedAt: number;
97
+ providerId?: string;
98
+ modelId?: string;
99
+ meetingSessionId?: string;
100
+ }
101
+
102
+ export async function startGateway(config: Config): Promise<void> {
103
+ const host = config.gateway?.host ?? "127.0.0.1";
104
+ const port = config.gateway?.port ?? 18790;
105
+ const pidFile = expandPath(config.gateway?.pidFile ?? "~/.hive/gateway.pid");
106
+
107
+ // FIX 2 — startTime para calcular uptime en /status y /api/agents
108
+ const startTime = Date.now();
109
+
110
+ // CPU delta sampling — process.cpuUsage() is cumulative; we diff between calls
111
+ const numCores = osCpus().length || 1;
112
+ let lastCpuSample = process.cpuUsage();
113
+ let lastCpuSampleTime = Date.now();
114
+ const log = logger.child("gateway");
115
+
116
+ log.info(`Starting gateway on ${host}:${port}`);
117
+
118
+ // ── Auto-generate auth token if not provided ─────────────────────────────
119
+ // Priority: HIVE_AUTH_TOKEN env var > persisted token file > generate new
120
+ const tokenFile = path.join(getHiveDir(), ".auth_token");
121
+ if (!process.env.HIVE_AUTH_TOKEN) {
122
+ if (existsSync(tokenFile)) {
123
+ process.env.HIVE_AUTH_TOKEN = readFileSync(tokenFile, "utf-8").trim();
124
+ log.info("🔑 Auth token loaded from persistent storage");
125
+ } else {
126
+ const generated = randomUUID().replace(/-/g, "");
127
+ process.env.HIVE_AUTH_TOKEN = generated;
128
+ mkdirSync(path.dirname(tokenFile), { recursive: true });
129
+ writeFileSync(tokenFile, generated, { mode: 0o600 });
130
+ log.info("🔑 Auth token auto-generated and persisted");
131
+ }
132
+ } else {
133
+ // User provided token via env — persist it so it's visible in the file too
134
+ writeFileSync(tokenFile, process.env.HIVE_AUTH_TOKEN, { mode: 0o600 });
135
+ log.info("🔑 Auth token loaded from environment variable");
136
+ }
137
+
138
+ // ── Inicialización modular con manejo de errores ──────────────────────────
139
+ let agent: AgentService;
140
+ let runner: AgentRunner;
141
+ let channelManager: ChannelManager;
142
+ let dbProvider: string;
143
+ let dbModel: string;
144
+ // ── Bind port immediately so parent health-check doesn't timeout ──────────
145
+ // The full handler is loaded via server.reload() once initialization finishes
146
+ let server = Bun.serve<WebSocketData>({
147
+ port,
148
+ hostname: host,
149
+ idleTimeout: 0, // Disable 10s idle timeout — SSE streams can run for minutes
150
+ fetch: (req) => {
151
+ const origin = req.headers.get("Origin") ?? ""
152
+ const isLocalhost = origin.includes("localhost") || origin.includes("127.0.0.1") || origin.includes("0.0.0.0")
153
+ const corsHeaders = isLocalhost ? {
154
+ "Access-Control-Allow-Origin": origin,
155
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
156
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, X-Requested-With",
157
+ "Access-Control-Allow-Credentials": "true",
158
+ } : {}
159
+ if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders })
160
+ const pathname = new URL(req.url).pathname
161
+ if (pathname === "/health" || pathname === "/health/") {
162
+ return Response.json({ status: "starting" }, { headers: corsHeaders })
163
+ }
164
+ return Response.json({ status: "starting" }, { status: 503, headers: corsHeaders })
165
+ },
166
+ websocket: { open() { }, message() { }, close() { } },
167
+ });
168
+ log.info(`Port ${port} bound (initializing gateway...)`);
169
+
170
+ // Inicializar DB siempre (en setup mode crea la DB vacía, los endpoints retornan [] en vez de 500)
171
+ try {
172
+ const db = initializeDatabase();
173
+ // Seed providers/models/hive_capabilities so setup wizard has data before onboarding completes
174
+ seedAllData();
175
+ } catch { /* si falla, los endpoints manejarán el error */ }
176
+
177
+ // Setup mode: no DB file OR DB existe pero tiene 0 usuarios (primera ejecución interrumpida)
178
+ let gatewaySetupMode = false;
179
+ try {
180
+ const count = (getDb().query("SELECT COUNT(*) as count FROM users").get() as { count: number }).count;
181
+ gatewaySetupMode = count === 0;
182
+ } catch {
183
+ gatewaySetupMode = true;
184
+ }
185
+
186
+ try {
187
+ // Usar el inicializador modular para todos los componentes críticos
188
+ const init = await initializeGateway(config, pidFile);
189
+
190
+ agent = init.agent;
191
+ runner = init.runner;
192
+ channelManager = init.channelManager;
193
+ dbProvider = init.provider;
194
+ dbModel = init.model;
195
+
196
+ // Auto-iniciar TTS y LLM local si están instalados
197
+ await initializeLocalTTS();
198
+ await initializeLocalLLM();
199
+
200
+ // Conectar channel-notify singleton para que las tools (notify, report_progress) puedan enviar mensajes
201
+ setChannelSendFn(async (channel, sessionId, content) => {
202
+ await channelManager.send(channel, sessionId, { content, type: "progress" });
203
+ });
204
+
205
+ if (gatewaySetupMode) {
206
+ log.info("🎉 Setup mode: gateway running — open http://localhost:" + port + "/setup to configure");
207
+ } else {
208
+ log.info("✅ Gateway initialization completed successfully");
209
+
210
+ // ── Initialize New Cron Scheduler (Croner-based) ───────────────────────
211
+ try {
212
+ const db = getDb();
213
+
214
+ // Repair orphaned task_runs (status='running' from previous crash)
215
+ db.query(`
216
+ UPDATE task_runs
217
+ SET status = 'timeout', finished_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
218
+ WHERE status = 'running'
219
+ `).run();
220
+
221
+ // Create and boot scheduler
222
+ const handler = createTaskHandler();
223
+ const scheduler = new CronScheduler(db, handler);
224
+ scheduler.boot();
225
+
226
+ // Register scheduler globally for tools, routes, and internal cleanup
227
+ setScheduleToolsInstance(scheduler);
228
+ setCronApiInstance(scheduler);
229
+ setSchedulerForCleanup(scheduler);
230
+
231
+ log.info(`📅 CronScheduler initialized with ${scheduler.getStatus().length} task(s)`);
232
+
233
+ // Register DAGScheduler as a global service (opt-in by swarms)
234
+ const dagScheduler = new DAGScheduler({ strategy: new ParallelStrategy(), maxConcurrentWorkers: 2 });
235
+ (globalThis as any).__dagScheduler = dagScheduler;
236
+ log.info("🔀 DAGScheduler ready");
237
+ } catch (err) {
238
+ log.error(`❌ CronScheduler initialization failed: ${(err as Error).message}`);
239
+ }
240
+
241
+ }
242
+ } catch (error) {
243
+ log.error(`❌ Gateway initialization failed: ${(error as Error).message}`);
244
+ log.error("Stack trace:", (error as Error).stack);
245
+ process.exit(1);
246
+ }
247
+
248
+ // Check for insecure binding
249
+ if (host === "0.0.0.0" && config.security?.warnOnInsecureConfig !== false) {
250
+ log.warn("Gateway binding to 0.0.0.0 exposes server to all network interfaces!");
251
+ }
252
+
253
+ // ── CRON Handler setup ─────────────────────────────────────────────────────
254
+ function prepareTools(agentInstance: AgentService, sessionId: string) {
255
+ // Tools are now handled by the native agent-loop internally
256
+ return undefined;
257
+ }
258
+
259
+ // Set up hot reload watchers
260
+ const watchers: Array<() => void> = [];
261
+
262
+ // Note: Context store, Ethics, Agent Loop, LLM runner, and Channel Manager
263
+ // are now initialized by initializeGateway() above
264
+
265
+ // Handle messages from channels (Telegram, Discord, WhatsApp, Slack)
266
+ if (!gatewaySetupMode) channelManager.onMessage(async (message: IncomingMessage) => {
267
+ log.info(`📥 Message from ${message.channel}:${message.accountId}`);
268
+ log.info(` Session: ${message.sessionId}`);
269
+
270
+ const voiceConfig = voiceService.getChannelVoiceConfig(message.channel);
271
+ const visionConfig = multimodalService.getChannelVisionConfig(message.channel);
272
+ let messageContent = message.content;
273
+
274
+ let preferAudioResponse = false;
275
+ let inputType: "text" | "audio_transcribed" | "image" | "document" = "text";
276
+ let sttProviderUsed: string | null = null;
277
+ let contentParts: import("../multimodal/types").ContentPart[] | undefined;
278
+
279
+ if (voiceConfig.voiceEnabled && message.audio) {
280
+ log.info(`🎙️ Voice enabled, processing audio...`);
281
+
282
+ if (!voiceConfig.sttProvider) {
283
+ log.warn(`⚠️ STT provider not configured for channel ${message.channel}`);
284
+ await channelManager.send(message.channel, message.sessionId, {
285
+ content: `🎙️ Para usar notas de voz, necesitas configurar el proveedor STT en la configuración del canal. Ve a Configuración > Canales > [Tu canal] y configura "Prov. STT" (ej: groq-whisper o openai)`,
286
+ });
287
+ return;
288
+ }
289
+
290
+ try {
291
+ const audioInput = voiceService.normalizeAudioFromChannel(message.channel, message.audio);
292
+ sttProviderUsed = voiceConfig.sttProvider || "groq-whisper";
293
+ messageContent = await voiceService.transcribe(audioInput, sttProviderUsed);
294
+ log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
295
+
296
+ inputType = "audio_transcribed";
297
+ // If user sent audio and TTS is available, always respond in audio
298
+ preferAudioResponse = !!voiceConfig.ttsProvider;
299
+
300
+ await channelManager.send(message.channel, message.sessionId, {
301
+ content: `🎙️ Transcripción: ${messageContent}`,
302
+ type: "message"
303
+ });
304
+ } catch (error) {
305
+ log.error(`❌ Transcription failed: ${(error as Error).message}`);
306
+ await channelManager.send(message.channel, message.sessionId, {
307
+ content: `Error al transcribir audio: ${(error as Error).message}`,
308
+ });
309
+ return;
310
+ }
311
+ }
312
+
313
+ // ── Multimodal: image/document processing ──
314
+ if (message.image || message.document) {
315
+ log.info(`🖼️ Multimodal content detected on channel ${message.channel}`);
316
+
317
+ if (message.image) {
318
+ try {
319
+ const imageInput = multimodalService.normalizeImageFromChannel(message.channel, message.image);
320
+ const activeModelId = dbModel;
321
+ const activeProviderId = dbProvider;
322
+ const modelHasVision = activeModelId && activeProviderId
323
+ ? multimodalService.modelSupportsVision(activeProviderId, activeModelId)
324
+ : false;
325
+
326
+ if (visionConfig.visionEnabled && modelHasVision) {
327
+ contentParts = await multimodalService.processImage(imageInput, visionConfig.visionModelId || undefined);
328
+ inputType = "image";
329
+ log.info(`🖼️ Image sent as vision ContentParts (model supports vision)`);
330
+ } else {
331
+ const ocrProvider = visionConfig.ocrProvider || "openai";
332
+ log.info(`🖼️ Model lacks vision, using OCR via ${ocrProvider}...`);
333
+ const ocrText = await multimodalService.ocrImage(imageInput, ocrProvider);
334
+ messageContent = ocrText
335
+ ? `[Imagen adjunta — contenido extraído por OCR]\n${ocrText}\n\n${messageContent || ""}`
336
+ : messageContent || "";
337
+ inputType = "image";
338
+ log.info(`🖼️ OCR result: ${ocrText.substring(0, 100)}...`);
339
+ }
340
+ } catch (imgError) {
341
+ log.error(`❌ Image processing failed: ${(imgError as Error).message}`);
342
+ await channelManager.send(message.channel, message.sessionId, {
343
+ content: `⚠️ Error al procesar la imagen: ${(imgError as Error).message}`,
344
+ });
345
+ }
346
+ }
347
+
348
+ if (message.document) {
349
+ try {
350
+ const docInput = multimodalService.normalizeDocumentFromChannel(message.channel, message.document);
351
+ const ocrProvider = visionConfig.ocrProvider || "openai";
352
+ log.info(`📄 Document detected, extracting text via OCR (${ocrProvider})...`);
353
+ const docImage: import("../multimodal/types").ImageInput = {
354
+ type: docInput.type,
355
+ data: docInput.data,
356
+ mimeType: docInput.mimeType,
357
+ caption: docInput.fileName,
358
+ };
359
+ const ocrText = await multimodalService.ocrImage(docImage, ocrProvider);
360
+ messageContent = ocrText
361
+ ? `[Documento adjunto: ${docInput.fileName || "unknown"}]\n${ocrText}\n\n${messageContent || ""}`
362
+ : messageContent || "";
363
+ inputType = "document";
364
+ log.info(`📄 Document OCR result: ${ocrText.substring(0, 100)}...`);
365
+ } catch (docError) {
366
+ log.error(`❌ Document processing failed: ${(docError as Error).message}`);
367
+ await channelManager.send(message.channel, message.sessionId, {
368
+ content: `⚠️ Error al procesar el documento: ${(docError as Error).message}`,
369
+ });
370
+ }
371
+ }
372
+ }
373
+
374
+ log.info(` Content: ${messageContent.substring(0, 150)}${messageContent.length > 150 ? "..." : ""}`);
375
+
376
+ const { userId } = resolveContext({
377
+ channel: message.channel,
378
+ channelUserId: message.sessionId,
379
+ });
380
+
381
+ const telegramMeta = message.metadata?.telegram as { messageId?: number } | undefined;
382
+ const messageId = telegramMeta?.messageId?.toString();
383
+ await Promise.all([
384
+ channelManager.markAsRead(message.channel, message.sessionId, messageId),
385
+ channelManager.startTyping(message.channel, message.sessionId),
386
+ ]);
387
+
388
+ // unifiedSessionId = userId del onboarding → historial y thread LangGraph unificados
389
+ const unifiedSessionId = userId;
390
+ // routingSessionId = peerId del canal → para enviar respuestas de vuelta al canal correcto
391
+ const routingSessionId = message.sessionId;
392
+
393
+ const userMetadata = inputType === "audio_transcribed"
394
+ ? { input_type: "audio_transcribed", stt_provider: sttProviderUsed, channel: message.channel }
395
+ : inputType === "image" || inputType === "document"
396
+ ? { input_type: inputType, ocr_provider: visionConfig.ocrProvider, channel: message.channel }
397
+ : { input_type: "text", channel: message.channel };
398
+
399
+ // Obtener la zona horaria del usuario para el timestamp exacto
400
+ const userRow = getDb()
401
+ .query<any, [string]>("SELECT * FROM users WHERE id = ?")
402
+ .get(userId);
403
+ const userTimezone = userRow?.timezone || "UTC";
404
+ const now = new Date();
405
+ let exactTime = "";
406
+ try {
407
+ exactTime = now.toLocaleString("en-US", {
408
+ timeZone: userTimezone,
409
+ dateStyle: "full",
410
+ timeStyle: "long",
411
+ });
412
+ } catch (e) {
413
+ exactTime = now.toISOString();
414
+ }
415
+ const messageContentWithTime = `[Timestamp: ${exactTime} (${userTimezone})]\n${messageContent}`;
416
+
417
+ const messages = contentParts
418
+ ? [{ role: "user" as const, content: [{ type: "text" as const, text: messageContentWithTime }, ...contentParts] as import("../multimodal/types").ContentPart[] }]
419
+ : [{ role: "user" as const, content: messageContentWithTime }];
420
+
421
+ try {
422
+ log.info(`🤖 Routing to agent loop...`);
423
+
424
+ const response = await runner.generate({
425
+ provider: dbProvider as any,
426
+ messages,
427
+ rawUserMessage: messageContent,
428
+ maxTokens: 4096,
429
+ tools: prepareTools(agent, unifiedSessionId),
430
+ maxSteps: 15,
431
+ threadId: unifiedSessionId,
432
+ userId,
433
+ channel: message.channel,
434
+ onStep: async (step) => {
435
+ // "text" = el agente narra lo que está pensando/haciendo antes de un tool_call
436
+ if (step.type === "text" && step.message) {
437
+ const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
438
+ if (trimmedMessage) {
439
+ log.debug(`[NARRATION] ${trimmedMessage.substring(0, 100)}`);
440
+ try {
441
+ await channelManager.send(message.channel, routingSessionId, {
442
+ content: trimmedMessage,
443
+ type: "progress",
444
+ });
445
+ } catch (err) {
446
+ log.warn(`[onStep] Narration send failed: ${(err as Error).message}`);
447
+ }
448
+ }
449
+ return;
450
+ }
451
+
452
+ // "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
453
+ if (step.type === "tool_call" && step.toolName) {
454
+ const narration = getNarration(step.toolName);
455
+ log.debug(`[TOOL] ${step.toolName} → "${narration}"`);
456
+ try {
457
+ await channelManager.send(message.channel, routingSessionId, {
458
+ content: narration,
459
+ type: "progress",
460
+ });
461
+ } catch (err) {
462
+ log.warn(`[onStep] Tool narration send failed: ${(err as Error).message}`);
463
+ }
464
+ return;
465
+ }
466
+
467
+ // "tool_result" = resultado de la herramienta
468
+ // Solo enviamos al usuario si el resultado lo pide explícitamente
469
+ if (step.type === "tool_result" && step.message) {
470
+ try {
471
+ const result = JSON.parse(step.message);
472
+ if (result._sendToUser) {
473
+ const userMessage = result.message || result.status || step.message;
474
+ try {
475
+ await channelManager.send(message.channel, routingSessionId, {
476
+ content: userMessage,
477
+ type: "progress",
478
+ });
479
+ } catch (err) {
480
+ log.warn(`[onStep] Tool result send failed: ${(err as Error).message}`);
481
+ }
482
+ }
483
+ } catch {
484
+ // No es JSON estructurado — no enviamos resultados crudos al usuario
485
+ }
486
+ return;
487
+ }
488
+ },
489
+ });
490
+
491
+ const responseContent = response.content?.trim() || "";
492
+ if (!responseContent) {
493
+ log.warn(`📤 LLM response: empty — skipping send`);
494
+ return;
495
+ }
496
+ log.info(`📤 LLM response: ${responseContent.substring(0, 100)}${responseContent.length > 100 ? "..." : ""}`);
497
+
498
+ const shouldSpeak = preferAudioResponse;
499
+ let responseType: "text" | "audio" = "text";
500
+ let ttsProviderUsed: string | null = null;
501
+ let ttsMimeType: string | null = null;
502
+
503
+ if (responseContent) {
504
+ if (shouldSpeak) {
505
+ if (!voiceConfig.ttsProvider) {
506
+ log.warn(`⚠️ TTS provider not configured, user requested audio`);
507
+ await channelManager.send(message.channel, routingSessionId, {
508
+ content: `${responseContent}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > [Tu canal] (ej: elevenlabs, openai-tts)`
509
+ });
510
+ } else {
511
+ try {
512
+ log.info(`🔊 TTS enabled, synthesizing audio...`);
513
+ const audioOutput = await voiceService.speak(responseContent, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
514
+ ttsProviderUsed = voiceConfig.ttsProvider;
515
+ ttsMimeType = audioOutput.mimeType;
516
+ responseType = "audio";
517
+
518
+ try {
519
+ const channel = channelManager.getChannel(message.channel);
520
+ if (channel?.sendAudio) {
521
+ await channel.sendAudio(routingSessionId, audioOutput.data as Buffer, audioOutput.mimeType);
522
+ log.info(`✅ Audio sent to ${routingSessionId}`);
523
+ } else {
524
+ log.warn(`Channel ${message.channel} does not support audio, sending text`);
525
+ await channelManager.send(message.channel, routingSessionId, { content: responseContent });
526
+ }
527
+ } catch (audioError) {
528
+ log.error(`❌ Audio send failed: ${(audioError as Error).message}, sending text instead`);
529
+ // Fallback to text
530
+ await channelManager.send(message.channel, routingSessionId, { content: responseContent });
531
+ }
532
+ } catch (ttsError) {
533
+ log.error(`❌ TTS failed: ${(ttsError as Error).message}, sending text instead`);
534
+ await channelManager.send(message.channel, routingSessionId, { content: responseContent });
535
+ }
536
+ }
537
+ } else {
538
+ await channelManager.send(message.channel, routingSessionId, { content: responseContent });
539
+ }
540
+ }
541
+
542
+ const assistantMetadata = {
543
+ response_type: responseType,
544
+ tts_provider: ttsProviderUsed,
545
+ mime_type: ttsMimeType,
546
+ channel: message.channel
547
+ };
548
+
549
+ await channelManager.stopTyping(message.channel, routingSessionId);
550
+ log.info(`✅ Response sent to ${routingSessionId} via ${message.channel}`);
551
+ } catch (error) {
552
+ await channelManager.stopTyping(message.channel, routingSessionId);
553
+ log.error(`❌ Error: ${(error as Error).message} `);
554
+ await channelManager.send(message.channel, routingSessionId, {
555
+ content: `Error: ${(error as Error).message} `,
556
+ });
557
+ }
558
+ });
559
+
560
+ // ── Auth helper ──────────────────────────────────────────────────────────
561
+ // Dev mode when HIVE_DEV is set to "true" or "1".
562
+ // Set HIVE_DEV=true in your development environment.
563
+ const isDev = process.env.HIVE_DEV === "true" || process.env.HIVE_DEV === "1";
564
+
565
+ function checkAuth(req: Request, url: URL): boolean {
566
+ // En modo desarrollo, permitir todo
567
+ if (isDev) return true;
568
+
569
+ // En setup mode (sin usuarios), bypass total — el wizard no tiene token aún
570
+ if (gatewaySetupMode) return true;
571
+
572
+ // Setup endpoints are always public — needed before the client has a token
573
+ if (url.pathname.startsWith("/api/setup/")) return true;
574
+
575
+ // Auth endpoints: status, login, recover are public; others require token
576
+ if (url.pathname === "/api/auth/status") return true;
577
+ if (url.pathname === "/api/auth/login") return true;
578
+ if (url.pathname === "/api/auth/recover") return true;
579
+
580
+ // Users endpoint is public when no credentials configured (matches /api/auth/status behavior)
581
+ // This allows the UI to load user data when login is not configured yet
582
+ if (url.pathname === "/api/users" && req.method === "GET") {
583
+ try {
584
+ const user = getDb().query(
585
+ `SELECT email, password_hash FROM users LIMIT 1`
586
+ ).get() as { email: string | null; password_hash: string | null } | null;
587
+ const hasCredentials = !!(user?.email && user?.password_hash);
588
+ // Allow access if no credentials configured
589
+ if (!hasCredentials) return true;
590
+ } catch {
591
+ // If DB query fails, fall through to token check
592
+ }
593
+ }
594
+
595
+ // Si no hay credenciales configuradas (modo open), bypass total — el UI
596
+ // no tiene token en localStorage porque nunca pasó por login.
597
+ // Coincide con el comportamiento de AuthGuard: status.hasCredentials === false → open.
598
+ try {
599
+ const user = getDb().query(
600
+ `SELECT email, password_hash FROM users LIMIT 1`
601
+ ).get() as { email: string | null; password_hash: string | null } | null;
602
+ const hasCredentials = !!(user?.email && user?.password_hash);
603
+ if (!hasCredentials) return true;
604
+ } catch {
605
+ // Si falla la consulta, caemos al chequeo de token
606
+ }
607
+
608
+ const activeToken = process.env.HIVE_AUTH_TOKEN;
609
+ if (!activeToken) return true;
610
+ const authHeader = req.headers.get("authorization");
611
+ const provided = authHeader?.replace(/^Bearer\s+/i, "") ?? url.searchParams.get("token");
612
+ return provided === activeToken;
613
+ }
614
+
615
+ // Reload with full handler now that initialization is complete
616
+ server.reload({
617
+ async fetch(req, server) {
618
+ const start = Date.now();
619
+ const url = new URL(req.url);
620
+ const method = req.method;
621
+
622
+ const logRequest = (status: number, duration: number) => {
623
+ // Skip health checks from spamming logs unless debug
624
+ if (url.pathname === "/health" || url.pathname === "/health/") {
625
+ log.debug(`${method} ${url.pathname} - ${status} (${duration}ms)`);
626
+ } else {
627
+ log.info(`${method} ${url.pathname} - ${status} (${duration}ms)`);
628
+ }
629
+ };
630
+
631
+ const handleRequest = async (): Promise<Response | undefined> => {
632
+
633
+ // ── CORS preflight ────────────────────────────────────────────────────
634
+ if (req.method === "OPTIONS") {
635
+ const origin = req.headers.get("Origin");
636
+ if (origin && (origin.includes("localhost") || origin.includes("127.0.0.1") || origin.includes("0.0.0.0") || CORS_ORIGINS.some(o => origin.includes(o.replace("http://", ""))))) {
637
+ return new Response(null, {
638
+ status: 204,
639
+ headers: {
640
+ "Access-Control-Allow-Origin": origin,
641
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
642
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, X-Requested-With",
643
+ "Access-Control-Allow-Credentials": "true",
644
+ "Access-Control-Max-Age": "86400",
645
+ },
646
+ });
647
+ }
648
+ return new Response(null, { status: 204 });
649
+ }
650
+
651
+ // ── WebSocket upgrade ────────────────────────────────────────────────
652
+ if (url.pathname === "/ws" || url.pathname === "/ws/") {
653
+ let sessionId = url.searchParams.get("session") || resolveUserId({}) || "default";
654
+ // Auth: accept ?token=<authToken> (same as REST Bearer) as alternative to ?session=<userId>
655
+ if (!isDev && !gatewaySetupMode) {
656
+ const tokenParam = url.searchParams.get("token");
657
+ const activeToken = process.env.HIVE_AUTH_TOKEN;
658
+ if (tokenParam && activeToken && tokenParam === activeToken) {
659
+ // Token auth — resolve the real userId from DB
660
+ const user = getDb().query("SELECT id FROM users LIMIT 1").get() as { id: string } | undefined;
661
+ if (user) sessionId = user.id;
662
+ }
663
+ try {
664
+ const userExists = getDb().query("SELECT 1 FROM users WHERE id = ? LIMIT 1").get(sessionId);
665
+ if (!userExists) {
666
+ return new Response("Unauthorized", { status: 401 });
667
+ }
668
+ } catch {
669
+ return new Response("Unauthorized", { status: 401 });
670
+ }
671
+ }
672
+ if (!sessionId) {
673
+ return new Response("Missing session or user ID", { status: 400 });
674
+ }
675
+ const success = server.upgrade(req, {
676
+ data: { sessionId, authenticatedAt: Date.now() },
677
+ });
678
+ if (success) return undefined;
679
+ return new Response("WebSocket upgrade failed", { status: 400 });
680
+ }
681
+
682
+
683
+
684
+
685
+ // ── Meeting Stream WebSocket upgrade ───────────────────────────────────
686
+ if (url.pathname === "/meeting-stream" || url.pathname === "/meeting-stream/") {
687
+ const meetingSessionId = url.searchParams.get("meetingSessionId") ?? "";
688
+ const sessionId = `meeting:${meetingSessionId || crypto.randomUUID()}`;
689
+ const success = server.upgrade(req, {
690
+ data: { sessionId, meetingSessionId, authenticatedAt: Date.now() },
691
+ });
692
+ if (success) return undefined;
693
+ return new Response("Meeting stream WebSocket upgrade failed", { status: 400 });
694
+ }
695
+
696
+ // ── Health (must be before UI routing so it works in dev mode too) ───
697
+ if (url.pathname === "/health" || url.pathname === "/health/") {
698
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
699
+ return addCorsHeaders(Response.json({ status: "ok", version: _pkgVersion, uptime }), req);
700
+ }
701
+
702
+ // ── Dashboard / UI ────────────────────────────────────────────────────
703
+ // In development: UI is served by Vite on port 5173, Gateway only handles /api and /ws
704
+ // In production: serve static files from packages/hive-ui/dist
705
+
706
+ // Check if this is an API or WebSocket request
707
+ const isApiRequest = url.pathname.startsWith("/api");
708
+ const isWsRequest = url.pathname.startsWith("/ws");
709
+ const isUiRequest = url.pathname === "/ui" || url.pathname === "/ui/" || url.pathname.startsWith("/ui/") || url.pathname.startsWith("/ui?");
710
+ const isSetupRequest = url.pathname === "/setup" || url.pathname === "/setup/" || url.pathname.startsWith("/setup/") || url.pathname.startsWith("/setup?");
711
+
712
+ // In development mode, serve static files with HMR support
713
+ // In production, serve static files from dist folder
714
+ if (!isApiRequest && !isWsRequest) {
715
+ // In development: serve from packages/hive-ui/dist with HMR injection
716
+ if (isDev) {
717
+ const uiDir = path.join(process.cwd(), "packages/hive-ui/dist");
718
+
719
+ // Verificar si existe el build de la UI
720
+ const indexPath = path.join(uiDir, "index.html");
721
+ if (!existsSync(indexPath)) {
722
+ return new Response(
723
+ "UI build not found. Please run: cd packages/hive-ui && bun run build\n\n" +
724
+ "Or use: bun run dev (from root) which builds automatically.",
725
+ { status: 503, headers: { "Content-Type": "text/plain" } }
726
+ );
727
+ }
728
+
729
+ let subPath = url.pathname;
730
+ if (subPath === "/" || subPath === "/setup" || subPath === "/ui" || subPath === "/ui/") {
731
+ subPath = "/index.html";
732
+ } else if (subPath.startsWith("/ui/")) {
733
+ subPath = subPath.replace(/^\/ui/, "");
734
+ } else if (subPath.startsWith("/setup/")) {
735
+ subPath = subPath.replace(/^\/setup/, "");
736
+ }
737
+
738
+ const filePath = path.join(uiDir, subPath);
739
+
740
+ // Para index.html, inyectar script de HMR de Vite
741
+ if (subPath === "/index.html") {
742
+ const indexFile = Bun.file(filePath);
743
+ if (await indexFile.exists()) {
744
+ let html = await indexFile.text();
745
+ // Inyectar script de HMR de Vite antes de </head>
746
+ const hmrScript = `<script type="module" src="http://localhost:5173/@vite/client"></script>`;
747
+ html = html.replace("</head>", `${hmrScript}</head>`);
748
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
749
+ }
750
+ }
751
+
752
+ const uiFile = Bun.file(filePath);
753
+ if (await uiFile.exists()) {
754
+ return new Response(uiFile);
755
+ }
756
+
757
+ // SPA fallback: servir index.html para rutas de React Router
758
+ const fallbackFile = Bun.file(path.join(uiDir, "index.html"));
759
+ if (await fallbackFile.exists()) {
760
+ let html = await fallbackFile.text();
761
+ // Inyectar script de HMR de Vite
762
+ const hmrScript = `<script type="module" src="http://localhost:5173/@vite/client"></script>`;
763
+ html = html.replace("</head>", `${hmrScript}</head>`);
764
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
765
+ }
766
+
767
+ return new Response("Not found", { status: 404 });
768
+ }
769
+
770
+ // In production: serve from dist folder
771
+ // Priority: HIVE_UI_DIR (Docker) > ~/.hive/ui > HIVE_DIST_DIR/ui (global npm) > cwd/packages/hive-ui/dist (monorepo)
772
+ const uiDirFromEnv = process.env.HIVE_UI_DIR;
773
+ const uiDirFromHive = path.join(getHiveDir(), "ui");
774
+ const uiDirFromDist = process.env.HIVE_DIST_DIR ? path.join(process.env.HIVE_DIST_DIR, "ui") : null;
775
+ const uiDirFromCwd = path.join(process.cwd(), "packages/hive-ui/dist");
776
+ const uiDir = uiDirFromEnv
777
+ || (existsSync(path.join(uiDirFromHive, "index.html")) ? uiDirFromHive
778
+ : uiDirFromDist && existsSync(path.join(uiDirFromDist, "index.html")) ? uiDirFromDist
779
+ : uiDirFromCwd);
780
+ let subPath = url.pathname;
781
+
782
+ // En setup mode: / y /ui redirigen a /setup
783
+ if (gatewaySetupMode && (subPath === "/" || subPath === "/ui" || subPath === "/ui/")) {
784
+ const _publicBase = process.env.HIVE_PUBLIC_URL?.replace(/\/$/, "")
785
+ ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
786
+ return Response.redirect(`${_publicBase}/setup`, 302);
787
+ }
788
+
789
+ // Normalize path for /ui routes
790
+ if (subPath === "/ui" || subPath === "/ui/") {
791
+ subPath = "/index.html";
792
+ } else if (subPath.startsWith("/ui/")) {
793
+ subPath = subPath.replace(/^\/ui/, "");
794
+ if (!subPath) subPath = "/index.html";
795
+ } else if (subPath === "/") {
796
+ subPath = "/index.html";
797
+ }
798
+
799
+ // Normalize path for /setup routes
800
+ if (subPath === "/setup" || subPath === "/setup/") {
801
+ subPath = "/index.html";
802
+ } else if (subPath.startsWith("/setup/")) {
803
+ subPath = subPath.replace(/^\/setup/, "");
804
+ if (!subPath) subPath = "/index.html";
805
+ }
806
+
807
+ const filePath = path.join(uiDir, subPath);
808
+ const uiFile = Bun.file(filePath);
809
+ if (await uiFile.exists()) {
810
+ return new Response(uiFile);
811
+ }
812
+
813
+ // SPA fallback: paths without a file extension are React Router routes — serve index.html
814
+ if (!path.extname(subPath)) {
815
+ const indexFile = Bun.file(path.join(uiDir, "index.html"));
816
+ if (await indexFile.exists()) {
817
+ return new Response(indexFile);
818
+ }
819
+ }
820
+
821
+ // If UI is not available, show helpful message for any non-API route
822
+ return new Response(
823
+ "UI not found.\n\n" +
824
+ "Options:\n" +
825
+ " 1. Place the UI in ~/.hive/ui/ (copy hive-ui/dist contents there)\n" +
826
+ " 2. Set HIVE_UI_DIR=/path/to/ui\n" +
827
+ " 3. Build from source: cd packages/hive-ui && bun run build\n",
828
+ { status: 404, headers: { "Content-Type": "text/plain" } }
829
+ );
830
+ }
831
+
832
+ // Handle /dashboard redirect for backwards compatibility
833
+ if (url.pathname.startsWith("/dashboard")) {
834
+ const tokenParam = url.searchParams.get("token") ? `? token = ${url.searchParams.get("token")} ` : "";
835
+ return Response.redirect(`/ ui${tokenParam} `, 301);
836
+ }
837
+
838
+ // ── Rutas que requieren autenticación ────────────────────────────────
839
+ if (!checkAuth(req, url)) {
840
+ log.warn(`[AUTH] Unauthorized request to ${url.pathname} from ${req.headers.get("origin")} `);
841
+ return addCorsHeaders(new Response("Unauthorized", { status: 401 }), req);
842
+ }
843
+
844
+ // ── Setup API ────────────────────────────────────────────────────────
845
+ // GET /api/setup/status
846
+ if (url.pathname === "/api/setup/status" || url.pathname === "/api/setup/status/") {
847
+ return addCorsHeaders(await handleSetupStatus(), req)
848
+ }
849
+
850
+ // GET /api/setup/providers
851
+ if (url.pathname === "/api/setup/providers" && req.method === "GET") {
852
+ return handleSetupProviders(addCorsHeaders, req)
853
+ }
854
+
855
+ // GET /api/setup/ollama-models
856
+ if (url.pathname === "/api/setup/ollama-models" && req.method === "GET") {
857
+ return handleSetupOllamaModels(addCorsHeaders, req)
858
+ }
859
+
860
+ // GET /api/setup/ethics
861
+ if (url.pathname === "/api/setup/ethics" && req.method === "GET") {
862
+ return handleSetupEthics(addCorsHeaders, req)
863
+ }
864
+
865
+ // POST /api/setup/verify-provider
866
+ if (url.pathname === "/api/setup/verify-provider" && req.method === "POST") {
867
+ return addCorsHeaders(await handleVerifyProvider(req), req)
868
+ }
869
+
870
+ // POST /api/setup/complete
871
+ if (url.pathname === "/api/setup/complete" && req.method === "POST") {
872
+ return await handleCompleteSetup(req, config, addCorsHeaders)
873
+ }
874
+
875
+ // ── Auth API ─────────────────────────────────────────────────────────
876
+ // GET /api/auth/status
877
+ if (url.pathname === "/api/auth/status" && req.method === "GET") {
878
+ return handleAuthStatus(req, addCorsHeaders);
879
+ }
880
+ // POST /api/auth/login
881
+ if (url.pathname === "/api/auth/login" && req.method === "POST") {
882
+ return handleLogin(req, addCorsHeaders);
883
+ }
884
+ // POST /api/auth/setup-credentials
885
+ if (url.pathname === "/api/auth/setup-credentials" && req.method === "POST") {
886
+ return handleSetupCredentials(req, addCorsHeaders);
887
+ }
888
+ // POST /api/auth/change-password
889
+ if (url.pathname === "/api/auth/change-password" && req.method === "POST") {
890
+ return handleChangePassword(req, addCorsHeaders);
891
+ }
892
+ // POST /api/auth/recover
893
+ if (url.pathname === "/api/auth/recover" && req.method === "POST") {
894
+ return handleRecover(req, addCorsHeaders);
895
+ }
896
+ // POST /api/auth/disable
897
+ if (url.pathname === "/api/auth/disable" && req.method === "POST") {
898
+ return handleDisableAuth(req, addCorsHeaders);
899
+ }
900
+ // GET /api/auth/recovery-key
901
+ if (url.pathname === "/api/auth/recovery-key" && req.method === "GET") {
902
+ return handleRecoveryKey(req, addCorsHeaders);
903
+ }
904
+
905
+ // ── Status ───────────────────────────────────────────────────────────
906
+ if (url.pathname === "/status" || url.pathname === "/status/") {
907
+ return addCorsHeaders(new Response(
908
+ JSON.stringify({
909
+ status: "ok",
910
+ version: "0.1.7",
911
+ uptime: Math.floor((Date.now() - startTime) / 1000),
912
+ gateway: { host, port },
913
+ sessions: sessionManager.list().map((s) => ({
914
+ id: s.id,
915
+ createdAt: s.createdAt,
916
+ messageCount: s.messageCount,
917
+ })),
918
+ channels: channelManager?.listChannels() ?? [],
919
+ queue: { activeSessions: 0 },
920
+ }),
921
+ { headers: { "Content-Type": "application/json", "Cache-Control": "max-age=5" } }
922
+ ), req);
923
+ }
924
+
925
+ // ── Activity Stats ─────────────────────────────────────────────────
926
+ if (url.pathname === "/api/activity-stats" || url.pathname === "/api/activity-stats/") {
927
+ return await handleGetActivityStats(req, addCorsHeaders)
928
+ }
929
+
930
+ // ── System Stats ───────────────────────────────────────────────────
931
+ if (url.pathname === "/api/system-stats" || url.pathname === "/api/system-stats/") {
932
+ return await handleGetSystemStats(req, addCorsHeaders, startTime)
933
+ }
934
+
935
+ // ── Version Check ──────────────────────────────────────────────────
936
+ if (url.pathname === "/api/version" || url.pathname === "/api/version/") {
937
+ return await handleGetVersion(req, addCorsHeaders)
938
+ }
939
+
940
+ // ── Trigger Update ─────────────────────────────────────────────────
941
+ if (url.pathname === "/api/update" || url.pathname === "/api/update/") {
942
+ if (req.method === "POST") {
943
+ return await handleTriggerUpdate(req, addCorsHeaders)
944
+ }
945
+ }
946
+
947
+ // ── Usage Stats ─────────────────────────────────────────────────────
948
+ if (url.pathname === "/api/usage-stats" || url.pathname === "/api/usage-stats/") {
949
+ return await handleGetUsageStats(req, addCorsHeaders)
950
+ }
951
+
952
+ // ── System Reload ─────────────────────────────────────────────────
953
+ if (url.pathname === "/api/system/reload" || url.pathname === "/api/system/reload/") {
954
+ return await handleSystemReload(req, addCorsHeaders)
955
+ }
956
+
957
+ // ── Config ─────────────────────────────────────────────────────────
958
+ if (url.pathname === "/api/config") {
959
+ if (req.method === "GET") {
960
+ return await handleGetConfig(req, addCorsHeaders, config);
961
+ }
962
+ }
963
+
964
+ // ── Tasks API ─────────────────────────────────────────────────────
965
+ if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/") && req.method === "GET") {
966
+ return await handleGetTasks(req, addCorsHeaders)
967
+ }
968
+
969
+ const taskDetailMatch = url.pathname.match(/^\/api\/tasks\/(\d+)$/)
970
+ if (taskDetailMatch && req.method === "PATCH") {
971
+ return await handleUpdateTask(req, addCorsHeaders)
972
+ }
973
+ const channelDetailMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/([^/]+)$/);
974
+ if (channelDetailMatch) {
975
+ const name = channelDetailMatch[1];
976
+ const accountId = channelDetailMatch[2];
977
+
978
+ if (req.method === "GET") {
979
+ return await handleGetChannelAccount(req, addCorsHeaders, name, accountId);
980
+ }
981
+ if (req.method === "PUT") {
982
+ const body = await req.json().catch(() => ({}));
983
+ if (!body.config) return new Response("Missing config", { status: 400 });
984
+
985
+ config.channels = config.channels || {};
986
+ config.channels[name] = config.channels[name] || { enabled: true, accounts: {} };
987
+ const channelEntry = config.channels[name] as any;
988
+ channelEntry.accounts = channelEntry.accounts || {};
989
+ channelEntry.accounts[accountId] = body.config;
990
+ return await handleUpdateChannelAccount(req, addCorsHeaders, name, accountId, channelManager);
991
+ }
992
+ if (req.method === "DELETE") {
993
+ // Config update handled by caller
994
+ if (config.channels?.[name]) {
995
+ const channelEntry = config.channels[name] as any;
996
+ if (channelEntry.accounts) {
997
+ delete channelEntry.accounts[accountId];
998
+ if (Object.keys(channelEntry.accounts).length === 0) {
999
+ delete config.channels[name];
1000
+ }
1001
+ }
1002
+ }
1003
+ return await handleDeleteChannelAccount(req, addCorsHeaders, name, accountId, config, channelManager);
1004
+ }
1005
+ }
1006
+
1007
+ const channelActionMatch = url.pathname.match(
1008
+ /^\/api\/channels\/([^/]+)\/([^/]+)\/(start|stop)$/
1009
+ );
1010
+ if (channelActionMatch) {
1011
+ const [, name, accountId, action] = channelActionMatch;
1012
+ if (req.method === "POST") {
1013
+ return await handleChannelAction(req, addCorsHeaders, name, accountId, action as "start" | "stop", channelManager);
1014
+ }
1015
+ }
1016
+
1017
+ // ── Skills API ───────────────────────────────────────────────────────
1018
+ if ((url.pathname === "/api/skills" || url.pathname === "/api/skills/") && req.method === "POST") {
1019
+ return await handleCreateSkill(req, addCorsHeaders);
1020
+ }
1021
+
1022
+ // ── Model Config API ─────────────────────────────────────────────────
1023
+ if (url.pathname === "/api/config/models") {
1024
+ if (req.method === "GET") {
1025
+ return await handleGetModelsConfig(req, addCorsHeaders, config);
1026
+ }
1027
+ if (req.method === "POST") {
1028
+ return await handleUpdateModelsConfig(req, addCorsHeaders, config, agent);
1029
+ }
1030
+ }
1031
+
1032
+ // ── MCP API ──────────────────────────────────────────────────────────
1033
+ // Note: Full MCP route handlers are in routes/mcp.ts
1034
+ if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
1035
+ const mcpManager = agent?.getMCPManager() ?? null;
1036
+ return await handleGetMcpServers(req, addCorsHeaders, mcpManager)
1037
+ }
1038
+
1039
+ // GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
1040
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
1041
+ const serverId = url.pathname.split("/")[4];
1042
+ return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
1043
+ }
1044
+
1045
+ if (url.pathname === "/api/mcp/servers" && req.method === "POST") {
1046
+ const response = await handleCreateMcpServer(req, addCorsHeaders)
1047
+
1048
+ // Hot reload will auto-connect the server within 2 seconds
1049
+ // No manual connection needed
1050
+
1051
+ return response
1052
+ }
1053
+
1054
+ // PUT /api/mcp/servers/:id — update server config
1055
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "PUT") {
1056
+ return await handleUpdateMcpServer(req, addCorsHeaders)
1057
+ }
1058
+
1059
+ // DELETE /api/mcp/servers/:id — remove server
1060
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "DELETE") {
1061
+ return await handleDeleteMcpServer(req, addCorsHeaders)
1062
+ }
1063
+
1064
+ if (url.pathname.match(/^\/api\/mcp\/servers\/[^/]+\/toggle$/)) {
1065
+ const mcpId = url.pathname.split("/")[4];
1066
+ if (req.method === "POST") {
1067
+ return await handleToggleMcpServer(req, addCorsHeaders, mcpId)
1068
+ }
1069
+ }
1070
+
1071
+ // ── Workspace API ────────────────────────────────────────────────────
1072
+ // Validate workspace path
1073
+ if (url.pathname === "/api/workspace/validate" && req.method === "POST") {
1074
+ return await handleValidateWorkspace(req, addCorsHeaders);
1075
+ }
1076
+
1077
+ // Create workspace directory
1078
+ if (url.pathname === "/api/workspace/create" && req.method === "POST") {
1079
+ return await handleCreateWorkspace(req, addCorsHeaders);
1080
+ }
1081
+
1082
+ // Open workspace in file explorer
1083
+ if (url.pathname === "/api/workspace/open" && req.method === "GET") {
1084
+ return await handleOpenWorkspace(req, addCorsHeaders);
1085
+ }
1086
+
1087
+ // Get/Update workspace files (soul, user, ethics)
1088
+ for (const wsType of ["soul", "user", "ethics"] as const) {
1089
+ if (url.pathname === `/api/workspace/${wsType}`) {
1090
+ const coordinatorRow = getDb().query<{ workspace: string | null }, []>(
1091
+ "SELECT workspace FROM agents WHERE role = 'coordinator' LIMIT 1"
1092
+ ).get();
1093
+ const liveWorkspacePath = coordinatorRow?.workspace
1094
+ ? expandPath(coordinatorRow.workspace)
1095
+ : expandPath("~/.hive/workspace");
1096
+ if (req.method === "GET") {
1097
+ return await handleGetWorkspace(req, addCorsHeaders, liveWorkspacePath, wsType);
1098
+ }
1099
+ if (req.method === "POST") {
1100
+ const reloadFn = async (type: string) => {
1101
+ if (type === "soul") agent.reloadSoul();
1102
+ if (type === "user") agent.reloadUser();
1103
+ if (type === "ethics") await agent.reloadEthics();
1104
+ };
1105
+ return await handleUpdateWorkspace(req, addCorsHeaders, liveWorkspacePath, wsType, reloadFn);
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ // ── Reload API ───────────────────────────────────────────────────────
1111
+ if (url.pathname === "/api/reload" && req.method === "POST") {
1112
+ return await handleApiReload(req, addCorsHeaders, agent);
1113
+ }
1114
+
1115
+ // ── User Channel Linking API ────────────────────────────────────────────
1116
+ if (url.pathname === "/api/user/channels" && req.method === "POST") {
1117
+ return await handleLinkUserChannel(req, addCorsHeaders, config, log);
1118
+ }
1119
+
1120
+ if (url.pathname === "/api/user/channels" && req.method === "GET") {
1121
+ return await handleGetUserChannels(req, addCorsHeaders, config);
1122
+ }
1123
+
1124
+ // ── Agents API ─────────────────────────────────────────────────────
1125
+ if (url.pathname === "/api/agents" && req.method === "GET") {
1126
+ return await handleGetAgents(req, addCorsHeaders)
1127
+ }
1128
+
1129
+ if (url.pathname === "/api/agents" && req.method === "POST") {
1130
+ return await handleCreateAgent(req, addCorsHeaders)
1131
+ }
1132
+
1133
+ if (url.pathname.startsWith("/api/agents/") && (req.method === "PATCH" || req.method === "PUT")) {
1134
+ return await handleUpdateAgent(req, addCorsHeaders)
1135
+ }
1136
+
1137
+ if (url.pathname.match(/^\/api\/agents\/[^/]+$/) && req.method === "DELETE") {
1138
+ return await handleDeleteAgent(req, addCorsHeaders)
1139
+ }
1140
+
1141
+ // ── Providers API ───────────────────────────────────────────────────
1142
+ if (url.pathname === "/api/providers" && req.method === "GET") {
1143
+ return await handleGetProviders(req, addCorsHeaders)
1144
+ }
1145
+
1146
+ if (url.pathname === "/api/providers" && req.method === "POST") {
1147
+ return await handleCreateProvider(req, addCorsHeaders)
1148
+ }
1149
+
1150
+ if (url.pathname.match(/^\/api\/providers\/[^/]+\/toggle$/) && req.method === "POST") {
1151
+ return await handleToggleProvider(req, addCorsHeaders)
1152
+ }
1153
+
1154
+ const providerIdMatch = url.pathname.match(/^\/api\/providers\/([^/]+)$/)
1155
+ if (providerIdMatch && (req.method === "PUT" || req.method === "PATCH")) {
1156
+ return await handleUpdateProvider(req, addCorsHeaders)
1157
+ }
1158
+
1159
+ // ── Models API ───────────────────────────────────────────────────
1160
+ // GET /api/models?provider_id=xxx - Get models filtered by provider
1161
+ if (url.pathname === "/api/models" && req.method === "GET") {
1162
+ return await handleGetModels(req, addCorsHeaders)
1163
+ }
1164
+
1165
+ // POST /api/providers/:id/sync-models — sincroniza modelos desde la API local del provider
1166
+ const syncModelsMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/sync-models$/)
1167
+ if (syncModelsMatch && req.method === "POST") {
1168
+ const providerId = syncModelsMatch[1]
1169
+ return await handleSyncProviderModels(req, addCorsHeaders, providerId)
1170
+ }
1171
+
1172
+ // POST /api/models - Create a new model
1173
+ if (url.pathname === "/api/models" && req.method === "POST") {
1174
+ return await handleCreateModel(req, addCorsHeaders)
1175
+ }
1176
+
1177
+ if (url.pathname.match(/^\/api\/models\/[^/]+\/toggle$/) && req.method === "POST") {
1178
+ return await handleToggleModel(req, addCorsHeaders)
1179
+ }
1180
+
1181
+ // DELETE /api/models/:id
1182
+ if (url.pathname.match(/^\/api\/models\/[^/]+$/) && req.method === "DELETE") {
1183
+ return await handleDeleteModel(req, addCorsHeaders)
1184
+ }
1185
+
1186
+ // PUT /api/models/:id
1187
+ if (url.pathname.match(/^\/api\/models\/[^/]+$/) && req.method === "PUT") {
1188
+ return await handleUpdateModel(req, addCorsHeaders)
1189
+ }
1190
+
1191
+ // ── Skills API ─────────────────────────────────────────────────────
1192
+ if (url.pathname === "/api/skills" && req.method === "GET") {
1193
+ return await handleGetSkills(req, addCorsHeaders)
1194
+ }
1195
+
1196
+ if (url.pathname === "/api/skills" && req.method === "POST") {
1197
+ const body = await req.json().catch(() => ({}))
1198
+ const { name, description, category, tools, triggers, preferred_agents, body: bodyContent } = body
1199
+ if (!name) return addCorsHeaders(new Response("Missing name", { status: 400 }), req)
1200
+ const id = randomUUID()
1201
+ getDb().query(`INSERT INTO skills(id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active) VALUES(?, ?, ?, ?, ?, ?, ?, ?, '0.0.1', 1, 1)`).run(id, name, description || "", category || "", tools || "", triggers || "", typeof preferred_agents === 'object' ? JSON.stringify(preferred_agents || []) : (preferred_agents || "[]"), bodyContent || "")
1202
+ return addCorsHeaders(Response.json({ success: true, id }), req)
1203
+ }
1204
+
1205
+ if (url.pathname.match(/^\/api\/skills\/[^/]+\/toggle$/) && req.method === "POST") {
1206
+ return await handleActivateSkill(req, addCorsHeaders)
1207
+ }
1208
+
1209
+ if (url.pathname.match(/^\/api\/skills\/[^/]+$/) && req.method === "PUT") {
1210
+ return await handleUpdateSkill(req, addCorsHeaders)
1211
+ }
1212
+
1213
+ if (url.pathname.match(/^\/api\/skills\/[^/]+$/) && req.method === "DELETE") {
1214
+ return await handleDeleteSkill(req, addCorsHeaders)
1215
+ }
1216
+
1217
+ // ── Tools API ────────────────────────────────────────────────────────
1218
+ if (url.pathname === "/api/tools" && req.method === "GET") {
1219
+ return await handleGetTools(req, addCorsHeaders)
1220
+ }
1221
+
1222
+ if (url.pathname.match(/^\/api\/tools\/[^/]+\/toggle$/) && req.method === "POST") {
1223
+ return await handleActivateTool(req, addCorsHeaders)
1224
+ }
1225
+
1226
+ if (url.pathname.match(/^\/api\/tools\/[^/]+$/) && req.method === "PUT") {
1227
+ return await handleUpdateTool(req, addCorsHeaders)
1228
+ }
1229
+
1230
+ // ── Ethics API ──────────────────────────────────────────────────────
1231
+ if (url.pathname === "/api/ethics" && req.method === "GET") {
1232
+ return await handleGetEthics(req, addCorsHeaders)
1233
+ }
1234
+
1235
+ if (url.pathname === "/api/ethics" && req.method === "POST") {
1236
+ const body = await req.json().catch(() => ({}))
1237
+ const { name, description, content, is_default } = body
1238
+ if (!name || !content) return addCorsHeaders(Response.json({ success: false, error: "Missing name or content" }, { status: 400 }), req)
1239
+ const id = randomUUID()
1240
+ getDb().query(`INSERT INTO ethics(id, name, description, content, is_default, enabled, active) VALUES(?, ?, ?, ?, ?, 1, 1)`).run(id, name, description || "", content, is_default ? 1 : 0)
1241
+ return addCorsHeaders(Response.json({ success: true, id }), req)
1242
+ }
1243
+
1244
+ if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "PUT") {
1245
+ return await handleActivateEthics(req, addCorsHeaders)
1246
+ }
1247
+
1248
+ if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "DELETE") {
1249
+ return await handleDeleteEthics(req, addCorsHeaders)
1250
+ }
1251
+
1252
+ // ── Users API ───────────────────────────────────────────────────────
1253
+ if (url.pathname === "/api/users" && req.method === "GET") {
1254
+ return await handleGetUsers(req, addCorsHeaders)
1255
+ }
1256
+
1257
+ if (url.pathname === "/api/users" && req.method === "POST") {
1258
+ return await handleCreateUser(req, addCorsHeaders)
1259
+ }
1260
+
1261
+ if (url.pathname === "/api/user/settings" && req.method === "PATCH") {
1262
+ return await handleUpdateUserSettings(req, addCorsHeaders)
1263
+ }
1264
+
1265
+ // ── MCP Servers API ──────────────────────────────────────────────────
1266
+ if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
1267
+ return await handleGetMcpServers(req, addCorsHeaders, agent?.getMCPManager() ?? null)
1268
+ }
1269
+
1270
+ // GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
1271
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
1272
+ const serverId = url.pathname.split("/")[4];
1273
+ return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
1274
+ }
1275
+
1276
+ // GET /api/mcp/servers/:id/tools - Get tools for a specific MCP server
1277
+ // Note: Tools are loaded from MCP Manager at runtime, not from DB
1278
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/) && req.method === "GET") {
1279
+ const serverId = url.pathname.split("/")[4];
1280
+ const mcpManager = agent?.getMCPManager() ?? null;
1281
+ return await handleGetMCPServerTools(req, addCorsHeaders, serverId, mcpManager)
1282
+ }
1283
+
1284
+ // Note: /api/mcp/tools/:id/toggle and /api/mcp/tools/:id DELETE removed
1285
+ // MCP tools are not stored in DB - they are loaded at runtime from servers
1286
+
1287
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/toggle$/)) {
1288
+ const mcpName = url.pathname.split("/")[4];
1289
+ if (req.method === "POST") {
1290
+ const body = await req.json().catch(() => ({}))
1291
+ // Support both { active: boolean } and { action: "connect"|"disconnect" }
1292
+ let active = body.active
1293
+ if (active === undefined && body.action !== undefined) {
1294
+ active = body.action === "connect"
1295
+ }
1296
+ if (active === undefined) {
1297
+ return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
1298
+ }
1299
+
1300
+ log.info(`[MCP] Toggle connection for ${mcpName}, active=${active}`)
1301
+
1302
+ // Update DB
1303
+ getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ? OR name = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName, mcpName)
1304
+
1305
+ // Connect/Disconnect MCP server in real-time (no restart needed)
1306
+ try {
1307
+ const mcp = agent?.getMCPManager() ?? null;
1308
+ if (mcp) {
1309
+ log.info(`[MCP] Manager found, connecting ${mcpName}...`)
1310
+ if (active) {
1311
+ const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ? OR name = ?`).get(mcpName, mcpName) as Record<string, any> | undefined;
1312
+ if (server) {
1313
+ log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
1314
+
1315
+ // Build MCP server config
1316
+ const mcpServerConfig: any = {
1317
+ transport: server.transport as string,
1318
+ command: server.command as string | null,
1319
+ args: server.args ? JSON.parse(server.args as string) : [],
1320
+ url: server.url as string | null,
1321
+ enabled: true,
1322
+ }
1323
+
1324
+ // Load headers from keychain (modern approach), fall back to legacy AES
1325
+ const keychainHeaders = await loadMcpHeaders(server.id as string);
1326
+ if (Object.keys(keychainHeaders).length > 0) {
1327
+ mcpServerConfig.headers = keychainHeaders;
1328
+ } else if (server.headers_encrypted && server.headers_iv) {
1329
+ try {
1330
+ mcpServerConfig.headers = legacyDecryptAES(server.headers_encrypted, server.headers_iv);
1331
+ } catch (e) {
1332
+ log.warn(`Failed to decrypt legacy headers for ${mcpName}`);
1333
+ }
1334
+ }
1335
+
1336
+ // Get current MCP config and add/update this server
1337
+ const currentConfig = (mcp as any).config || { servers: {} }
1338
+ const newServersConfig = { ...currentConfig.servers }
1339
+ newServersConfig[mcpName] = mcpServerConfig
1340
+
1341
+ // Update MCP Manager config (this will register and auto-connect the server)
1342
+ await mcp.updateConfig({
1343
+ ...currentConfig,
1344
+ servers: newServersConfig,
1345
+ });
1346
+
1347
+ log.info(`[MCP] Server registered in MCP Manager`)
1348
+
1349
+ // Get tools after connection
1350
+ const tools = mcp.getServerTools(mcpName) || [];
1351
+ const serverDetails = mcp.getServerDetails?.(mcpName);
1352
+ const serverStatus = serverDetails?.status ?? mcp.getServerStatus(mcpName);
1353
+ if (serverStatus === "error" && serverDetails?.error) {
1354
+ log.error(`[MCP] Connection error for ${mcpName}: ${serverDetails.error}`);
1355
+ }
1356
+ log.info(`[MCP] Connected! Tools: ${tools.length}, status: ${serverStatus}`);
1357
+ getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ? OR name = ?`).run(serverStatus === "connected" ? "connected" : "error", tools.length, mcpName, mcpName);
1358
+ } else {
1359
+ log.error(`[MCP] Server not found in DB: ${mcpName}`)
1360
+ }
1361
+ } else {
1362
+ await mcp.disconnectServer(mcpName);
1363
+ getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ? OR name = ?`).run("disconnected", mcpName, mcpName);
1364
+ }
1365
+ } else {
1366
+ log.error(`[MCP] No MCP Manager found`)
1367
+ }
1368
+ } catch (error) {
1369
+ log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
1370
+ }
1371
+
1372
+ return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
1373
+ }
1374
+ }
1375
+
1376
+ // GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
1377
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
1378
+ const serverId = url.pathname.split("/")[4];
1379
+ return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
1380
+ }
1381
+
1382
+ // Support /api/mcp/servers/{name} with POST for connect (frontend uses this)
1383
+ if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/)) {
1384
+ const mcpName = url.pathname.split("/")[4];
1385
+ if (req.method === "POST") {
1386
+ const body = await req.json().catch(() => ({}))
1387
+ // Support both { active: boolean } and { action: "connect"|"disconnect" }
1388
+ let active = body.active
1389
+ if (active === undefined && body.action !== undefined) {
1390
+ active = body.action === "connect"
1391
+ }
1392
+ if (active === undefined) {
1393
+ return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
1394
+ }
1395
+
1396
+ // Update DB
1397
+ getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ? OR name = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName, mcpName)
1398
+
1399
+ // Connect/Disconnect MCP server in real-time (no restart needed)
1400
+ try {
1401
+ const mcp = agent?.getMCPManager() ?? null;
1402
+ if (mcp) {
1403
+ if (active) {
1404
+ const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ? OR name = ?`).get(mcpName, mcpName) as Record<string, any> | undefined;
1405
+ if (server) {
1406
+ log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
1407
+
1408
+ // Build MCP server config
1409
+ const mcpServerConfig: any = {
1410
+ transport: server.transport as string,
1411
+ command: server.command as string | null,
1412
+ args: server.args ? JSON.parse(server.args as string) : [],
1413
+ url: server.url as string | null,
1414
+ enabled: true,
1415
+ }
1416
+
1417
+ // Load headers from keychain (modern approach), fall back to legacy AES
1418
+ const keychainHeaders2 = await loadMcpHeaders(server.id as string);
1419
+ if (Object.keys(keychainHeaders2).length > 0) {
1420
+ mcpServerConfig.headers = keychainHeaders2;
1421
+ } else if (server.headers_encrypted && server.headers_iv) {
1422
+ try {
1423
+ mcpServerConfig.headers = legacyDecryptAES(server.headers_encrypted, server.headers_iv);
1424
+ } catch (e) {
1425
+ log.warn(`Failed to decrypt legacy headers for ${mcpName}`);
1426
+ }
1427
+ }
1428
+
1429
+ // Get current MCP config and add/update this server
1430
+ const currentConfig = (mcp as any).config || { servers: {} }
1431
+ const newServersConfig = { ...currentConfig.servers }
1432
+ newServersConfig[mcpName] = mcpServerConfig
1433
+
1434
+ // Update MCP Manager config (this will register and auto-connect the server)
1435
+ await mcp.updateConfig({
1436
+ ...currentConfig,
1437
+ servers: newServersConfig,
1438
+ });
1439
+
1440
+ log.info(`[MCP] Server registered in MCP Manager`)
1441
+
1442
+ // Get tools after connection
1443
+ const tools = mcp.getServerTools(mcpName) || [];
1444
+ const serverDetails2 = mcp.getServerDetails?.(mcpName);
1445
+ const serverStatus2 = serverDetails2?.status ?? mcp.getServerStatus(mcpName);
1446
+ if (serverStatus2 === "error" && serverDetails2?.error) {
1447
+ log.error(`[MCP] Connection error for ${mcpName}: ${serverDetails2.error}`);
1448
+ }
1449
+ log.info(`[MCP] Connected! Tools: ${tools.length}, status: ${serverStatus2}`);
1450
+
1451
+ // Update DB with status and tools
1452
+ getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ? OR name = ?`).run(serverStatus2 === "connected" ? "connected" : "error", tools.length, mcpName, mcpName);
1453
+ } else {
1454
+ log.error(`[MCP] Server not found in DB: ${mcpName}`)
1455
+ }
1456
+ } else {
1457
+ await mcp.disconnectServer(mcpName);
1458
+ getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ? OR name = ?`).run("disconnected", mcpName, mcpName);
1459
+ }
1460
+ }
1461
+ } catch (error) {
1462
+ log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
1463
+ }
1464
+
1465
+ return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
1466
+ }
1467
+ }
1468
+
1469
+ // ── Channels API ───────────────────────────────────────────────────
1470
+ if (url.pathname === "/api/channels" && req.method === "GET") {
1471
+ return await handleGetChannels(req, addCorsHeaders, channelManager);
1472
+ }
1473
+
1474
+ // PUT /api/channels/:id - Update channel settings
1475
+ const channelIdMatch = url.pathname.match(/^\/api\/channels\/([^/]+)$/);
1476
+ if (channelIdMatch && req.method === "PUT") {
1477
+ const channelId = channelIdMatch[1];
1478
+ return await handleUpdateChannelSettings(req, addCorsHeaders, channelId);
1479
+ }
1480
+
1481
+ if (url.pathname.match(/^\/api\/channels\/[^/]+\/toggle$/)) {
1482
+ const channelId = url.pathname.split("/")[3];
1483
+ if (req.method === "POST") {
1484
+ return await handleToggleChannel(req, addCorsHeaders, channelId);
1485
+ }
1486
+ }
1487
+
1488
+ // GET /api/channels/:type/:id/status — connection state + QR for WhatsApp
1489
+ if (url.pathname.match(/^\/api\/channels\/[^/]+\/[^/]+\/status$/) && req.method === "GET") {
1490
+ return await handleGetChannelStatus(req, addCorsHeaders, channelManager);
1491
+ }
1492
+
1493
+ // WhatsApp-specific endpoints
1494
+ // GET /api/channels/whatsapp/:id/details
1495
+ if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/details$/) && req.method === "GET") {
1496
+ const accountId = url.pathname.split("/")[3];
1497
+ return await handleGetWhatsAppDetails(req, addCorsHeaders, accountId, channelManager);
1498
+ }
1499
+
1500
+ // POST /api/channels/whatsapp/:id/disconnect
1501
+ if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/disconnect$/) && req.method === "POST") {
1502
+ const accountId = url.pathname.split("/")[3];
1503
+ return await handleDisconnectWhatsApp(req, addCorsHeaders, accountId, channelManager);
1504
+ }
1505
+
1506
+ // PUT /api/channels/whatsapp/:id/config
1507
+ if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/config$/) && req.method === "PUT") {
1508
+ const accountId = url.pathname.split("/")[3];
1509
+ return await handleUpdateWhatsAppConfig(req, addCorsHeaders, accountId, channelManager);
1510
+ }
1511
+
1512
+ // POST /api/channels/:id/reconnect — restart channel (with optional new credentials)
1513
+ if (url.pathname.match(/^\/api\/channels\/[^/]+\/reconnect$/) && req.method === "POST") {
1514
+ const channelId = url.pathname.split("/")[3];
1515
+ return await handleReconnectChannel(req, addCorsHeaders, channelId, channelManager);
1516
+ }
1517
+
1518
+ // ── Voice API ───────────────────────────────────────────────────────
1519
+ if (url.pathname === "/api/voice/providers" && req.method === "GET") {
1520
+ return await handleGetVoiceProviders(req, addCorsHeaders)
1521
+ }
1522
+
1523
+ if (url.pathname === "/api/voice/configured-providers" && req.method === "GET") {
1524
+ return await handleGetConfiguredVoiceProviders(req, addCorsHeaders)
1525
+ }
1526
+
1527
+ // POST /api/voice/providers/:providerId/key - Save API key for a voice provider
1528
+ const voiceProviderKeyMatch = url.pathname.match(/^\/api\/voice\/providers\/([^/]+)\/key$/)
1529
+ if (voiceProviderKeyMatch && req.method === "POST") {
1530
+ return await handleSaveVoiceProviderKey(req, addCorsHeaders)
1531
+ }
1532
+
1533
+ if (url.pathname === "/api/voice/test" && req.method === "POST") {
1534
+ return await handleTestVoice(req, addCorsHeaders)
1535
+ }
1536
+
1537
+ // GET /api/voice/:provider/voices - Get available voices for a provider
1538
+ const voiceProviderVoicesMatch = url.pathname.match(/^\/api\/voice\/([^/]+)\/voices$/)
1539
+ if (voiceProviderVoicesMatch && req.method === "GET") {
1540
+ const providerId = voiceProviderVoicesMatch[1]
1541
+ return await handleGetVoiceProviderVoices(req, addCorsHeaders, providerId)
1542
+ }
1543
+
1544
+ // GET /api/channels/:id/voice - Get voice config for a channel
1545
+ const channelVoiceMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/voice$/)
1546
+ if (channelVoiceMatch && req.method === "GET") {
1547
+ const channelId = channelVoiceMatch[1]
1548
+ return await handleGetChannelVoice(req, addCorsHeaders, channelId)
1549
+ }
1550
+
1551
+ // PATCH /api/channels/:id/voice - Update voice config for a channel
1552
+ if (channelVoiceMatch && req.method === "PATCH") {
1553
+ const channelId = channelVoiceMatch[1]
1554
+ return await handleUpdateChannelVoice(req, addCorsHeaders, channelId)
1555
+ }
1556
+
1557
+ // ── Multimodal / Vision API ─────────────────────────────────────────
1558
+ if (url.pathname === "/api/multimodal/vision-providers" && req.method === "GET") {
1559
+ return await handleGetVisionProviders(req, addCorsHeaders)
1560
+ }
1561
+
1562
+ if (url.pathname === "/api/multimodal/ocr" && req.method === "POST") {
1563
+ return await handleOcrImage(req, addCorsHeaders)
1564
+ }
1565
+
1566
+ const channelVisionMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/vision$/)
1567
+ if (channelVisionMatch && req.method === "GET") {
1568
+ const channelId = channelVisionMatch[1]
1569
+ return await handleGetChannelVision(req, addCorsHeaders, channelId)
1570
+ }
1571
+
1572
+ if (channelVisionMatch && req.method === "PATCH") {
1573
+ const channelId = channelVisionMatch[1]
1574
+ return await handleUpdateChannelVision(req, addCorsHeaders, channelId)
1575
+ }
1576
+
1577
+ // ── Piper TTS Local ──────────────────────────────────────────────────
1578
+ if (url.pathname === "/api/tts-local/status" && req.method === "GET") {
1579
+ return await handleGetLocalTTSStatus(req, addCorsHeaders)
1580
+ }
1581
+ if (url.pathname === "/api/tts-local/logs" && req.method === "GET") {
1582
+ return await handleGetLocalTTSLogs(req, addCorsHeaders)
1583
+ }
1584
+ if (url.pathname === "/api/tts-local/install" && req.method === "POST") {
1585
+ return await handleInstallLocalTTS(req, addCorsHeaders)
1586
+ }
1587
+ if (url.pathname === "/api/tts-local/start" && req.method === "POST") {
1588
+ return await handleStartLocalTTS(req, addCorsHeaders)
1589
+ }
1590
+ if (url.pathname === "/api/tts-local/stop" && req.method === "POST") {
1591
+ return await handleStopLocalTTS(req, addCorsHeaders)
1592
+ }
1593
+ if (url.pathname === "/api/tts-local/speak" && req.method === "POST") {
1594
+ return await handleSpeakLocalTTS(req, addCorsHeaders)
1595
+ }
1596
+ // Modelos
1597
+ if (url.pathname === "/api/tts-local/models" && req.method === "GET") {
1598
+ return await handleGetAvailableModels(req, addCorsHeaders)
1599
+ }
1600
+ if (url.pathname === "/api/tts-local/models/download" && req.method === "POST") {
1601
+ return await handleDownloadModel(req, addCorsHeaders)
1602
+ }
1603
+ if (url.pathname === "/api/tts-local/models/logs" && req.method === "GET") {
1604
+ return await handleGetDownloadLogs(req, addCorsHeaders)
1605
+ }
1606
+ if (url.pathname === "/api/tts-local/voices" && req.method === "GET") {
1607
+ return await handleGetInstalledVoices(req, addCorsHeaders)
1608
+ }
1609
+
1610
+ // ── LLM Local ────────────────────────────────────────────────────────
1611
+ if (url.pathname === "/api/llm-local/status" && req.method === "GET") {
1612
+ return await handleGetLocalLLMStatus(req, addCorsHeaders)
1613
+ }
1614
+ if (url.pathname === "/api/llm-local/logs" && req.method === "GET") {
1615
+ return await handleGetLocalLLMLogs(req, addCorsHeaders)
1616
+ }
1617
+ if (url.pathname === "/api/llm-local/install" && req.method === "POST") {
1618
+ return await handleInstallLocalLLM(req, addCorsHeaders)
1619
+ }
1620
+ if (url.pathname === "/api/llm-local/start" && req.method === "POST") {
1621
+ return await handleStartLocalLLM(req, addCorsHeaders)
1622
+ }
1623
+ if (url.pathname === "/api/llm-local/stop" && req.method === "POST") {
1624
+ return await handleStopLocalLLM(req, addCorsHeaders)
1625
+ }
1626
+ if (url.pathname === "/api/llm-local/download-model" && req.method === "POST") {
1627
+ return await handleDownloadLLMModel(req, addCorsHeaders)
1628
+ }
1629
+
1630
+ // ── Meeting Transcription API ────────────────────────────────────────
1631
+ if (url.pathname === "/api/meetings" && req.method === "POST") {
1632
+ return await handleCreateMeeting(req, addCorsHeaders);
1633
+ }
1634
+
1635
+ if (url.pathname === "/api/meetings" && req.method === "GET") {
1636
+ return await handleListMeetings(req, addCorsHeaders);
1637
+ }
1638
+
1639
+ const meetingIdMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)$/);
1640
+ if (meetingIdMatch && req.method === "GET") {
1641
+ return await handleGetMeeting(req, addCorsHeaders, meetingIdMatch[1]);
1642
+ }
1643
+
1644
+ const meetingSegmentMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)\/segments$/);
1645
+ if (meetingSegmentMatch && req.method === "POST") {
1646
+ return await handleAddMeetingSegment(req, addCorsHeaders, meetingSegmentMatch[1]);
1647
+ }
1648
+
1649
+ const meetingStopMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)\/stop$/);
1650
+ if (meetingStopMatch && req.method === "POST") {
1651
+ return await handleStopMeeting(req, addCorsHeaders, meetingStopMatch[1]);
1652
+ }
1653
+
1654
+ // ── Chat / Canvas / Notes API ───────────────────────────────────────
1655
+ if (url.pathname === "/api/chat/history" && req.method === "GET") {
1656
+ return await handleGetChatHistory(req, addCorsHeaders)
1657
+ }
1658
+
1659
+ if (url.pathname === "/api/chat" && req.method === "POST") {
1660
+ return await handlePostChat(req, addCorsHeaders)
1661
+ }
1662
+
1663
+ if (url.pathname === "/api/canvas" && req.method === "GET") {
1664
+ return await handleGetCanvas(req, addCorsHeaders)
1665
+ }
1666
+
1667
+ if (url.pathname === "/api/notes" && req.method === "GET") {
1668
+ return await handleGetNotes(req, addCorsHeaders)
1669
+ }
1670
+
1671
+ // ── Cron Jobs API ──────────────────────────────────────────────────
1672
+ const cronMatch = url.pathname.match(/^\/api\/cron(\/[^/]+)?(\/[^/]+)?$/);
1673
+ if (cronMatch && req.method === "GET" && !cronMatch[2]) {
1674
+ if (cronMatch[1] === "/status") {
1675
+ return await handleGetCronStatus(req, addCorsHeaders);
1676
+ }
1677
+ if (cronMatch[1] === "/channels") {
1678
+ return await handleGetCronChannels(req, addCorsHeaders);
1679
+ }
1680
+ if (cronMatch[1]) {
1681
+ const taskId = cronMatch[1].slice(1);
1682
+ return await handleGetCronJob(req, addCorsHeaders, taskId);
1683
+ }
1684
+ return await handleGetCronJobs(req, addCorsHeaders);
1685
+ }
1686
+
1687
+ if (cronMatch && req.method === "POST" && !cronMatch[2]) {
1688
+ return await handleCreateCronJob(req, addCorsHeaders);
1689
+ }
1690
+
1691
+ if (cronMatch && req.method === "GET" && cronMatch[2] === "/history") {
1692
+ const taskId = cronMatch[1]?.slice(1);
1693
+ return await handleGetCronJobHistory(req, addCorsHeaders, taskId || "");
1694
+ }
1695
+
1696
+ if (cronMatch && req.method === "POST" && cronMatch[2] === "/pause") {
1697
+ const taskId = cronMatch[1]?.slice(1);
1698
+ return await handlePauseCronJob(req, addCorsHeaders, taskId || "");
1699
+ }
1700
+
1701
+ if (cronMatch && req.method === "POST" && cronMatch[2] === "/resume") {
1702
+ const taskId = cronMatch[1]?.slice(1);
1703
+ return await handleResumeCronJob(req, addCorsHeaders, taskId || "");
1704
+ }
1705
+
1706
+ if (cronMatch && req.method === "POST" && cronMatch[2] === "/trigger") {
1707
+ const taskId = cronMatch[1]?.slice(1);
1708
+ return await handleTriggerCronJob(req, addCorsHeaders, taskId || "");
1709
+ }
1710
+
1711
+ if (cronMatch && req.method === "PATCH" && cronMatch[1] && !cronMatch[2]) {
1712
+ const taskId = cronMatch[1].slice(1);
1713
+ return await handleUpdateCronJob(req, addCorsHeaders, taskId);
1714
+ }
1715
+
1716
+ if (cronMatch && req.method === "DELETE" && cronMatch[1] && !cronMatch[2]) {
1717
+ const taskId = cronMatch[1].slice(1);
1718
+ return await handleDeleteCronJob(req, addCorsHeaders, taskId);
1719
+ }
1720
+
1721
+ return addCorsHeaders(new Response("Not Found", { status: 404 }), req)
1722
+ };
1723
+
1724
+ try {
1725
+ const response = await handleRequest();
1726
+ const duration = Date.now() - start;
1727
+ if (response) {
1728
+ logRequest(response.status, duration);
1729
+ } else {
1730
+ // Bun upgrade returns undefined on success
1731
+ log.info(`${method} ${url.pathname} - 101 Switching Protocols(${duration}ms)`);
1732
+ }
1733
+ return response;
1734
+ } catch (error) {
1735
+ const duration = Date.now() - start;
1736
+ log.error(`${method} ${url.pathname} - Internal Error(${duration}ms): ${(error as Error).message} `);
1737
+ return addCorsHeaders(Response.json({ success: false, error: (error as Error).message, message: "Error interno del servidor" }, { status: 500 }), req);
1738
+ }
1739
+ },
1740
+
1741
+ websocket: {
1742
+ open(ws) {
1743
+ const data = ws.data;
1744
+ // ── Meeting Stream ─────────────────────────────────────────────────────
1745
+ if (data.sessionId.startsWith("meeting:")) {
1746
+ log.info(`Meeting stream client connected: ${data.sessionId}`);
1747
+ ws.send(JSON.stringify({ type: "meeting:connected", sessionId: data.sessionId, meetingSessionId: (data as any).meetingSessionId }));
1748
+ return;
1749
+ }
1750
+
1751
+ // ── LLM Local ─────────────────────────────────────────────────────────
1752
+ if (data.sessionId.startsWith("llm-local:")) {
1753
+ log.info(`Local LLM client connected: ${data.sessionId}`);
1754
+ ws.send(JSON.stringify({ type: "llm-local:connected", sessionId: data.sessionId }));
1755
+ return;
1756
+ }
1757
+
1758
+ log.debug(`WebSocket connected: ${data.sessionId} `);
1759
+
1760
+ sessionManager.create(data.sessionId, ws);
1761
+
1762
+ const channel = channelManager?.getChannel("webchat") as any;
1763
+ if (channel?.registerConnection) channel.registerConnection(ws);
1764
+
1765
+ // Send status message
1766
+ ws.send(JSON.stringify({
1767
+ type: "status",
1768
+ sessionId: data.sessionId,
1769
+ status: { state: "connected", model: `${dbProvider}/${dbModel}` },
1770
+ } as OutboundMessage));
1771
+
1772
+ // Send welcome message with real user data
1773
+ try {
1774
+ const db = getDb();
1775
+ const user = db.query("SELECT id, name, language FROM users LIMIT 1").get() as { id: string; name: string; language: string } | undefined;
1776
+ const agent = db.query("SELECT id, name, provider_id, model_id FROM agents WHERE role = 'coordinator' LIMIT 1").get() as { id: string; name: string; provider_id: string; model_id: string } | undefined;
1777
+
1778
+ // Get channels
1779
+ const channels = db.query("SELECT id FROM channels WHERE active = 1").all() as Array<{ id: string }>;
1780
+
1781
+ // Get voice config from webchat channel
1782
+ const voiceConfig = db.query("SELECT voice_enabled, stt_provider, tts_provider FROM channels WHERE id = 'webchat'").get() as { voice_enabled: number; stt_provider: string; tts_provider: string } | undefined;
1783
+
1784
+ ws.send(JSON.stringify({
1785
+ type: "welcome",
1786
+ sessionId: data.sessionId,
1787
+ user: user ? { id: user.id, name: user.name, language: user.language } : null,
1788
+ agent: agent ? { id: agent.id, name: agent.name, provider: agent.provider_id, model: agent.model_id } : null,
1789
+ channels: channels.map(c => c.id),
1790
+ voice: voiceConfig ? {
1791
+ enabled: voiceConfig.voice_enabled === 1,
1792
+ sttProvider: voiceConfig.stt_provider,
1793
+ ttsProvider: voiceConfig.tts_provider
1794
+ } : { enabled: false, sttProvider: null, ttsProvider: null },
1795
+ } as OutboundMessage));
1796
+ } catch (err) {
1797
+ log.error("Error sending welcome message:", err);
1798
+ }
1799
+ },
1800
+
1801
+ async message(ws, message) {
1802
+ const data = ws.data;
1803
+
1804
+ // LLM Local
1805
+ if (data.sessionId.startsWith("llm-local:")) {
1806
+ const { handleLLMWebSocket } = await import("./llm-local/server");
1807
+ await handleLLMWebSocket(ws as any, message.toString());
1808
+ return;
1809
+ }
1810
+
1811
+ // Bridge events clients are read-only; only respond to ping keepalive
1812
+ if (data.sessionId.startsWith("bridge:")) {
1813
+ try {
1814
+ const m = JSON.parse(message.toString());
1815
+ if (m?.type === "ping") ws.send(JSON.stringify({ type: "pong" }));
1816
+ } catch { /* ignore */ }
1817
+ return;
1818
+ }
1819
+
1820
+ // Meeting stream: handle audio chunks and stop commands
1821
+ if (data.sessionId.startsWith("meeting:")) {
1822
+ try {
1823
+ const m = JSON.parse(message.toString()) as Record<string, unknown>;
1824
+ if (m?.type === "ping") {
1825
+ ws.send(JSON.stringify({ type: "pong" }));
1826
+ return;
1827
+ }
1828
+ if (m?.type === "audio_chunk") {
1829
+ const meetingSessionId = (data as any).meetingSessionId as string;
1830
+ const audioBase64 = m.audio as string;
1831
+ const speaker = (m.speaker as string) || null;
1832
+ const mimeType = (m.mime_type as string) || "audio/webm";
1833
+ (async () => {
1834
+ try {
1835
+ const db = getDb();
1836
+ const session = db.query(
1837
+ `SELECT id, stt_model, status FROM meeting_sessions WHERE id = ?`
1838
+ ).get(meetingSessionId) as { id: string; stt_model: string; status: string } | undefined;
1839
+ if (!session || session.status !== "active") {
1840
+ ws.send(JSON.stringify({ type: "error", error: "Sesión no activa o no encontrada" }));
1841
+ return;
1842
+ }
1843
+ const transcription = await voiceService.transcribe(
1844
+ { type: "base64", data: audioBase64, mimeType },
1845
+ session.stt_model
1846
+ );
1847
+ const seqResult = db.query(
1848
+ `SELECT COALESCE(MAX(seq) + 1, 0) as next_seq FROM meeting_segments WHERE session_id = ?`
1849
+ ).get(meetingSessionId) as { next_seq: number };
1850
+ const seq = seqResult.next_seq;
1851
+ db.query(
1852
+ `INSERT INTO meeting_segments (session_id, seq, speaker, text) VALUES (?, ?, ?, ?)`
1853
+ ).run(meetingSessionId, seq, speaker, transcription);
1854
+ ws.send(JSON.stringify({ type: "transcript_segment", seq, speaker, text: transcription }));
1855
+ } catch (err) {
1856
+ ws.send(JSON.stringify({ type: "error", error: (err as Error).message }));
1857
+ }
1858
+ })();
1859
+ return;
1860
+ }
1861
+ if (m?.type === "meeting_stop") {
1862
+ const meetingSessionId = (data as any).meetingSessionId as string;
1863
+ try {
1864
+ const db = getDb();
1865
+ db.query(
1866
+ `UPDATE meeting_sessions SET status = 'stopped', stopped_at = unixepoch() WHERE id = ? AND status = 'active'`
1867
+ ).run(meetingSessionId);
1868
+ const countResult = db.query(
1869
+ `SELECT COUNT(*) as count FROM meeting_segments WHERE session_id = ?`
1870
+ ).get(meetingSessionId) as { count: number };
1871
+ ws.send(JSON.stringify({ type: "meeting_stopped", session_id: meetingSessionId, segment_count: countResult.count }));
1872
+ } catch (err) {
1873
+ ws.send(JSON.stringify({ type: "error", error: (err as Error).message }));
1874
+ }
1875
+ return;
1876
+ }
1877
+ } catch { /* ignore malformed messages */ }
1878
+ return;
1879
+ }
1880
+
1881
+
1882
+
1883
+
1884
+
1885
+ let msg: InboundMessage;
1886
+ try {
1887
+ msg = JSON.parse(message.toString()) as InboundMessage;
1888
+ } catch {
1889
+ ws.send(JSON.stringify({
1890
+ type: "error",
1891
+ sessionId: data.sessionId,
1892
+ error: "Invalid JSON message",
1893
+ } as OutboundMessage));
1894
+ return;
1895
+ }
1896
+
1897
+ msg.sessionId = msg.sessionId ?? data.sessionId;
1898
+ sessionManager.touch(msg.sessionId);
1899
+
1900
+ if (msg.type === "ping") {
1901
+ ws.send(JSON.stringify({ type: "pong", sessionId: msg.sessionId } as OutboundMessage));
1902
+ return;
1903
+ }
1904
+
1905
+ // Canvas subscribe
1906
+ if (msg.type === "canvas_subscribe") {
1907
+ subscribeCanvas(ws);
1908
+ canvasManager.registerSession(`canvas:${data.sessionId}`, ws);
1909
+ ws.send(JSON.stringify({
1910
+ type: "canvas:snapshot",
1911
+ data: getCanvasSnapshot(),
1912
+ }));
1913
+ return;
1914
+ }
1915
+
1916
+ // Canvas unsubscribe
1917
+ if (msg.type === "canvas_unsubscribe") {
1918
+ unsubscribeCanvas(ws);
1919
+ canvasManager.unregisterSession(`canvas:${data.sessionId}`);
1920
+ return;
1921
+ }
1922
+
1923
+ // A2UI actions — user interacted with an A2UI surface component
1924
+ if (msg.type === "a2ui:action") {
1925
+ const actionData = (msg.data ?? msg) as Record<string, unknown>;
1926
+ const actionName = actionData.name as string ?? "action";
1927
+ const surfaceId = actionData.surfaceId as string ?? "unknown";
1928
+ const sourceComponentId = actionData.sourceComponentId as string ?? "unknown";
1929
+ const context = actionData.context as Record<string, unknown> ?? {};
1930
+
1931
+ const interactionMsg = `[a2ui:action] surface=${surfaceId} action=${actionName} component=${sourceComponentId}${Object.keys(context).length > 0 ? ` context=${JSON.stringify(context)}` : ""}`;
1932
+ log.info(`A2UI action forwarded to agent: ${interactionMsg}`);
1933
+
1934
+ const sessionId = data.sessionId;
1935
+ ws.send(JSON.stringify({ type: "typing", isTyping: true, sessionId } as OutboundMessage));
1936
+
1937
+ laneQueue.enqueue(sessionId, async (_task, signal) => {
1938
+ if (signal.aborted) {
1939
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
1940
+ return;
1941
+ }
1942
+ try {
1943
+ const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
1944
+ const messages = [{ role: "user" as const, content: interactionMsg }];
1945
+ let streamedContent = "";
1946
+ const messageId = crypto.randomUUID();
1947
+
1948
+ const response = await runner.generate({
1949
+ provider: dbProvider as any,
1950
+ messages,
1951
+ maxTokens: 4096,
1952
+ tools: prepareTools(agent, sessionId),
1953
+ maxSteps: 15,
1954
+ threadId: sessionId,
1955
+ userId,
1956
+ onToken: async (token: string) => {
1957
+ if (signal.aborted) return;
1958
+ streamedContent += token;
1959
+ ws.send(JSON.stringify({ type: "message", id: messageId, sessionId, content: token, isChunk: true, isStep: false } as OutboundMessage));
1960
+ },
1961
+ onStep: async (step) => {
1962
+ if (signal.aborted) return;
1963
+ log.debug(`[a2ui:action TOOL] ${step.type}: ${step.toolName || ""}`);
1964
+ },
1965
+ });
1966
+
1967
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
1968
+
1969
+ const content = streamedContent || response.content?.trim() || "";
1970
+ if (content && streamedContent.length === 0) {
1971
+ ws.send(JSON.stringify({ type: "message", sessionId, content, isStep: false } as OutboundMessage));
1972
+ }
1973
+ } catch (error) {
1974
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
1975
+ ws.send(JSON.stringify({ type: "error", sessionId, error: (error as Error).message } as OutboundMessage));
1976
+ log.error(`A2UI action agent error: ${(error as Error).message}`);
1977
+ }
1978
+ });
1979
+
1980
+ return;
1981
+ }
1982
+
1983
+ // Canvas interactions from the main session — route to canvasManager and local resolver
1984
+ if (msg.type === "canvas:interact") {
1985
+ const { componentId } = msg;
1986
+ let resolvedByPending = false;
1987
+ if (componentId) {
1988
+ removeCanvasComponent(componentId);
1989
+ // Resolve local pending interactions (canvas_confirm tool)
1990
+ resolvedByPending = resolveCanvasInteraction(componentId, msg.data);
1991
+ }
1992
+ const canvasSessionId = `canvas:${data.sessionId}`;
1993
+ canvasManager.handleMessage(canvasSessionId, JSON.stringify(msg));
1994
+
1995
+ // If no tool was waiting for this interaction, forward it to the agent as a new turn
1996
+ if (!resolvedByPending) {
1997
+ const interactionData = msg.data as Record<string, unknown> | undefined;
1998
+ const action = (msg as any).action as string | undefined;
1999
+
2000
+ // Build a human-readable message describing what the user clicked
2001
+ let interactionMsg = `[canvas:interact] componentId=${componentId ?? "unknown"}, action=${action ?? "click"}`;
2002
+ if (interactionData && Object.keys(interactionData).length > 0) {
2003
+ interactionMsg += `, data=${JSON.stringify(interactionData)}`;
2004
+ }
2005
+
2006
+ log.info(`Canvas interaction forwarded to agent: ${interactionMsg}`);
2007
+
2008
+ const sessionId = data.sessionId;
2009
+
2010
+ ws.send(JSON.stringify({ type: "typing", isTyping: true, sessionId } as OutboundMessage));
2011
+
2012
+ laneQueue.enqueue(sessionId, async (_task, signal) => {
2013
+ if (signal.aborted) {
2014
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
2015
+ return;
2016
+ }
2017
+ try {
2018
+ const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
2019
+ const messages = [{ role: "user" as const, content: interactionMsg }];
2020
+ let streamedContent = "";
2021
+ const messageId = crypto.randomUUID();
2022
+
2023
+ const response = await runner.generate({
2024
+ provider: dbProvider as any,
2025
+ messages,
2026
+ maxTokens: 4096,
2027
+ tools: prepareTools(agent, sessionId),
2028
+ maxSteps: 15,
2029
+ threadId: sessionId,
2030
+ userId,
2031
+ onToken: async (token: string) => {
2032
+ if (signal.aborted) return;
2033
+ streamedContent += token;
2034
+ ws.send(JSON.stringify({ type: "message", id: messageId, sessionId, content: token, isChunk: true, isStep: false } as OutboundMessage));
2035
+ },
2036
+ onStep: async (step) => {
2037
+ if (signal.aborted) return;
2038
+ log.debug(`[canvas:interact TOOL] ${step.type}: ${step.toolName || ""}`);
2039
+ },
2040
+ });
2041
+
2042
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
2043
+
2044
+ const content = streamedContent || response.content?.trim() || "";
2045
+ if (content && streamedContent.length === 0) {
2046
+ ws.send(JSON.stringify({ type: "message", sessionId, content, isStep: false } as OutboundMessage));
2047
+ }
2048
+ } catch (error) {
2049
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
2050
+ ws.send(JSON.stringify({ type: "error", sessionId, error: (error as Error).message } as OutboundMessage));
2051
+ log.error(`Canvas interact agent error: ${(error as Error).message}`);
2052
+ }
2053
+ });
2054
+ }
2055
+
2056
+ return;
2057
+ }
2058
+
2059
+ // Canvas session - handle interactions
2060
+ if (msg.type === "command" || (msg.content && isSlashCommand(msg.content))) {
2061
+ const result = await executeSlashCommand(msg.sessionId, msg.content ?? `/${msg.command}`, ws);
2062
+ if (result) {
2063
+ ws.send(JSON.stringify(result));
2064
+ return;
2065
+ }
2066
+ }
2067
+
2068
+ // Logs subscription
2069
+ if (msg.type === "logs_subscribe") {
2070
+ logSubscribers.add(data.sessionId);
2071
+ log.debug(`Session ${data.sessionId} subscribed to logs`);
2072
+ return;
2073
+ }
2074
+
2075
+ if (msg.type === "logs_unsubscribe") {
2076
+ logSubscribers.delete(data.sessionId);
2077
+ log.debug(`Session ${data.sessionId} unsubscribed from logs`);
2078
+ return;
2079
+ }
2080
+
2081
+ // Stop generation (like ChatGPT/Claude stop button)
2082
+ if (msg.type === "stop") {
2083
+ const cancelled = laneQueue.cancel(msg.sessionId);
2084
+ log.info(`[stop] Session ${msg.sessionId} — cancelled: ${cancelled}`);
2085
+ ws.send(JSON.stringify({
2086
+ type: "typing",
2087
+ isTyping: false,
2088
+ sessionId: msg.sessionId,
2089
+ } as OutboundMessage));
2090
+ if (cancelled) {
2091
+ ws.send(JSON.stringify({
2092
+ type: "status",
2093
+ sessionId: msg.sessionId,
2094
+ status: { state: "cancelled" },
2095
+ } as OutboundMessage));
2096
+ }
2097
+ return;
2098
+ }
2099
+
2100
+ // Handle audio messages from WebChat
2101
+ let webchatPreferAudio = false;
2102
+ if (msg.type === "audio" && msg.audio) {
2103
+ log.info(`WebChat audio from session ${msg.sessionId}`);
2104
+
2105
+ const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
2106
+
2107
+ if (!voiceConfig.voiceEnabled) {
2108
+ ws.send(JSON.stringify({
2109
+ type: "error",
2110
+ sessionId: msg.sessionId,
2111
+ error: "Voice input not enabled for this channel"
2112
+ } as OutboundMessage));
2113
+ return;
2114
+ }
2115
+
2116
+ if (!voiceConfig.sttProvider) {
2117
+ ws.send(JSON.stringify({
2118
+ type: "message",
2119
+ sessionId: msg.sessionId,
2120
+ content: "🎙️ Para usar notas de voz, configura el proveedor STT en Configuración > Canales > WebChat (ej: groq-whisper)"
2121
+ } as OutboundMessage));
2122
+ return;
2123
+ }
2124
+
2125
+ ws.send(JSON.stringify({
2126
+ type: "typing",
2127
+ isTyping: true,
2128
+ sessionId: msg.sessionId,
2129
+ } as OutboundMessage));
2130
+
2131
+ try {
2132
+ const audioInput = { type: "base64" as const, data: msg.audio, mimeType: "audio/webm" };
2133
+ const sttProvider = voiceConfig.sttProvider || "groq-whisper";
2134
+ const messageContent = await voiceService.transcribe(audioInput, sttProvider);
2135
+
2136
+ log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
2137
+
2138
+ webchatPreferAudio = true;
2139
+
2140
+ ws.send(JSON.stringify({
2141
+ type: "message",
2142
+ sessionId: msg.sessionId,
2143
+ content: `🎙️ Transcripción: ${messageContent}`
2144
+ } as OutboundMessage));
2145
+
2146
+ ws.send(JSON.stringify({
2147
+ type: "typing",
2148
+ isTyping: false,
2149
+ sessionId: msg.sessionId,
2150
+ } as OutboundMessage));
2151
+
2152
+ laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
2153
+ if (signal.aborted) {
2154
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
2155
+ ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
2156
+ return;
2157
+ }
2158
+
2159
+ try {
2160
+ const unifiedSessionId = msg.sessionId;
2161
+ const messages = [{ role: "user" as const, content: messageContent }];
2162
+ log.info(`Generating response for session ${unifiedSessionId}...`);
2163
+
2164
+ const { userId } = resolveContext({
2165
+ channel: "webchat",
2166
+ channelUserId: msg.sessionId,
2167
+ });
2168
+
2169
+ // Streaming: send tokens as they arrive
2170
+ let streamedContent = "";
2171
+ let messageId = crypto.randomUUID();
2172
+
2173
+ const response = await runner.generate({
2174
+ provider: dbProvider as any,
2175
+ messages,
2176
+ maxTokens: 4096,
2177
+ tools: prepareTools(agent, unifiedSessionId),
2178
+ maxSteps: 15,
2179
+ threadId: unifiedSessionId,
2180
+ userId,
2181
+ onToken: async (token: string) => {
2182
+ if (signal.aborted) return;
2183
+ streamedContent += token;
2184
+ // Send chunk to client
2185
+ ws.send(JSON.stringify({
2186
+ type: "message",
2187
+ id: messageId,
2188
+ sessionId: unifiedSessionId,
2189
+ content: token,
2190
+ isChunk: true,
2191
+ isStep: false,
2192
+ } as OutboundMessage));
2193
+ },
2194
+ onStep: async (step) => {
2195
+ if (signal.aborted) return;
2196
+
2197
+ // "text" = el agente narra lo que esta pensando/haciendo
2198
+ if (step.type === "text" && step.message) {
2199
+ const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
2200
+ if (trimmedMessage) {
2201
+ ws.send(JSON.stringify({
2202
+ type: "progress",
2203
+ sessionId: unifiedSessionId,
2204
+ content: trimmedMessage,
2205
+ } as OutboundMessage));
2206
+ }
2207
+ return;
2208
+ }
2209
+
2210
+ // "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
2211
+ if (step.type === "tool_call" && step.toolName) {
2212
+ const narration = getNarration(step.toolName);
2213
+ ws.send(JSON.stringify({
2214
+ type: "progress",
2215
+ sessionId: unifiedSessionId,
2216
+ content: narration,
2217
+ } as OutboundMessage));
2218
+ return;
2219
+ }
2220
+
2221
+ // "tool_result" = resultado de herramienta → solo si pide enviarse al usuario
2222
+ if (step.type === "tool_result" && step.message) {
2223
+ try {
2224
+ const result = JSON.parse(step.message);
2225
+ if (result._sendToUser || result.status) {
2226
+ const userMessage = result.message || result.status || "";
2227
+ if (userMessage) {
2228
+ ws.send(JSON.stringify({
2229
+ type: "progress",
2230
+ sessionId: unifiedSessionId,
2231
+ content: userMessage,
2232
+ } as OutboundMessage));
2233
+ }
2234
+ return;
2235
+ }
2236
+ } catch { }
2237
+ }
2238
+ },
2239
+ });
2240
+
2241
+ // Use streamed content from onToken, fallback to response.content
2242
+ const content = streamedContent || response.content?.trim() || "";
2243
+ log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
2244
+
2245
+ const voiceCfg = voiceService.getChannelVoiceConfig("webchat");
2246
+ const shouldSpeak = webchatPreferAudio;
2247
+ let responseType: "text" | "audio" = "text";
2248
+ let ttsProviderUsed: string | null = null;
2249
+ let ttsMimeType: string | null = null;
2250
+
2251
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2252
+
2253
+ // Don't send text message if already streamed (content came via onToken)
2254
+ const alreadyStreamed = streamedContent.length > 0;
2255
+
2256
+ if (content && !alreadyStreamed) {
2257
+ if (shouldSpeak) {
2258
+ if (!voiceCfg.ttsProvider) {
2259
+ ws.send(JSON.stringify({
2260
+ type: "message",
2261
+ sessionId: unifiedSessionId,
2262
+ content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
2263
+ isStep: false,
2264
+ } as OutboundMessage));
2265
+ } else {
2266
+ try {
2267
+ log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
2268
+ const audioOutput = await voiceService.speak(content, voiceCfg.ttsProvider, voiceCfg.ttsVoiceId || undefined);
2269
+ ttsProviderUsed = voiceCfg.ttsProvider;
2270
+ ttsMimeType = audioOutput.mimeType;
2271
+ responseType = "audio";
2272
+ const base64Audio = (audioOutput.data as Buffer).toString("base64");
2273
+ log.info(`Audio generated: ${base64Audio.length} bytes, mimeType: ${audioOutput.mimeType}`);
2274
+ ws.send(JSON.stringify({
2275
+ type: "message",
2276
+ sessionId: unifiedSessionId,
2277
+ content,
2278
+ audio: base64Audio,
2279
+ mimeType: audioOutput.mimeType,
2280
+ isStep: false
2281
+ } as OutboundMessage));
2282
+ } catch (ttsError) {
2283
+ log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
2284
+ ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2285
+ }
2286
+ }
2287
+ } else {
2288
+ ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2289
+ }
2290
+ } else if (alreadyStreamed && shouldSpeak && voiceCfg.ttsProvider) {
2291
+ try {
2292
+ log.info(`🔊 TTS enabled, synthesizing audio after streaming...`);
2293
+ const audioOutput = await voiceService.speak(content, voiceCfg.ttsProvider, voiceCfg.ttsVoiceId || undefined);
2294
+ const base64Audio = (audioOutput.data as Buffer).toString("base64");
2295
+ log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
2296
+ ws.send(JSON.stringify({
2297
+ type: "message",
2298
+ sessionId: unifiedSessionId,
2299
+ content,
2300
+ audio: base64Audio,
2301
+ mimeType: audioOutput.mimeType,
2302
+ isStep: false
2303
+ } as OutboundMessage));
2304
+ } catch (ttsError) {
2305
+ log.error(`TTS after streaming failed: ${(ttsError as Error).message}), skipping audio`);
2306
+ }
2307
+ }
2308
+ } catch (error) {
2309
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
2310
+ ws.send(JSON.stringify({
2311
+ type: "error",
2312
+ sessionId: msg.sessionId,
2313
+ error: (error as Error).message,
2314
+ } as OutboundMessage));
2315
+ log.error(`Error for session ${msg.sessionId}: ${(error as Error).message}`);
2316
+ }
2317
+ });
2318
+ } catch (error) {
2319
+ ws.send(JSON.stringify({
2320
+ type: "typing",
2321
+ isTyping: false,
2322
+ sessionId: msg.sessionId,
2323
+ } as OutboundMessage));
2324
+ ws.send(JSON.stringify({
2325
+ type: "error",
2326
+ sessionId: msg.sessionId,
2327
+ error: `Transcription failed: ${(error as Error).message}`
2328
+ } as OutboundMessage));
2329
+ }
2330
+ return;
2331
+ }
2332
+
2333
+ if (msg.type === "message" && msg.content) {
2334
+ log.info(`WebChat message from session ${msg.sessionId}: ${msg.content.substring(0, 100)}`);
2335
+
2336
+ // FIX 6 — typing indicator inmediato ANTES de encolar
2337
+ // El usuario ve "escribiendo..." de inmediato, no después del queue
2338
+ ws.send(JSON.stringify({
2339
+ type: "typing",
2340
+ isTyping: true,
2341
+ sessionId: msg.sessionId,
2342
+ } as OutboundMessage));
2343
+
2344
+ laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
2345
+ if (signal.aborted) {
2346
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
2347
+ ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
2348
+ return;
2349
+ }
2350
+
2351
+ try {
2352
+ const unifiedSessionId = msg.sessionId;
2353
+
2354
+ // Multimodal: process image/document if present
2355
+ let finalMessageContent = msg.content;
2356
+ let contentParts: any[] | undefined = undefined;
2357
+ const visionConfig = multimodalService.getChannelVisionConfig("webchat");
2358
+
2359
+ if (msg.image || msg.document) {
2360
+ log.info(`🖼️ Multimodal content detected from WebChat session ${unifiedSessionId}`);
2361
+
2362
+ if (msg.image) {
2363
+ try {
2364
+ const imageInput = {
2365
+ type: "base64" as const,
2366
+ data: msg.image.base64,
2367
+ mimeType: msg.image.mimeType || "image/jpeg",
2368
+ caption: msg.image.caption
2369
+ };
2370
+
2371
+ const activeModelId = dbModel;
2372
+ const activeProviderId = dbProvider;
2373
+ const modelHasVision = activeModelId && activeProviderId
2374
+ ? multimodalService.modelSupportsVision(activeProviderId, activeModelId)
2375
+ : false;
2376
+
2377
+ if (visionConfig.visionEnabled && modelHasVision) {
2378
+ contentParts = await multimodalService.processImage(imageInput, visionConfig.visionModelId || undefined);
2379
+ log.info(`🖼️ Image sent as vision ContentParts (model supports vision)`);
2380
+ } else {
2381
+ const ocrProvider = visionConfig.ocrProvider || (["openai", "gemini", "anthropic"].includes(dbProvider) ? dbProvider : "openai");
2382
+ log.info(`🖼️ Model lacks vision or vision disabled, using OCR via ${ocrProvider}...`);
2383
+ const ocrText = await multimodalService.ocrImage(imageInput, ocrProvider);
2384
+ finalMessageContent = ocrText
2385
+ ? `[Imagen adjunta — contenido extraído por OCR]\n${ocrText}\n\n${finalMessageContent || ""}`
2386
+ : finalMessageContent || "";
2387
+ log.info(`🖼️ OCR result: ${ocrText.substring(0, 100)}...`);
2388
+ }
2389
+ } catch (imgError) {
2390
+ log.error(`❌ Image processing failed: ${(imgError as Error).message}`);
2391
+ }
2392
+ }
2393
+
2394
+ if (msg.document) {
2395
+ try {
2396
+ const ocrProvider = visionConfig.ocrProvider || (["openai", "gemini", "anthropic"].includes(dbProvider) ? dbProvider : "openai");
2397
+ log.info(`📄 Document detected from WebChat, extracting text via OCR (${ocrProvider})...`);
2398
+ const docImage = {
2399
+ type: "base64" as const,
2400
+ data: msg.document.base64,
2401
+ mimeType: msg.document.mimeType || "application/pdf",
2402
+ caption: (msg.document as any).fileName || (msg.document as any).caption
2403
+ };
2404
+ const ocrText = await multimodalService.ocrImage(docImage, ocrProvider);
2405
+ finalMessageContent = ocrText
2406
+ ? `[Documento adjunto]\n${ocrText}\n\n${finalMessageContent || ""}`
2407
+ : finalMessageContent || "";
2408
+ log.info(`📄 Document OCR result: ${ocrText.substring(0, 100)}...`);
2409
+ } catch (docError) {
2410
+ log.error(`❌ Document processing failed: ${(docError as Error).message}`);
2411
+ }
2412
+ }
2413
+ }
2414
+
2415
+ const messages: any[] = contentParts
2416
+ ? [{ role: "user" as const, content: contentParts }]
2417
+ : [{ role: "user" as const, content: finalMessageContent }];
2418
+
2419
+ log.info(`Generating response for session ${unifiedSessionId} (multimodal: ${!!(msg.image || msg.document)})...`);
2420
+
2421
+ const { userId } = resolveContext({
2422
+ channel: "webchat",
2423
+ channelUserId: msg.sessionId,
2424
+ });
2425
+
2426
+ // Streaming: send tokens as they arrive
2427
+ let streamedContent = "";
2428
+ let messageId = crypto.randomUUID();
2429
+
2430
+ const response = await runner.generate({
2431
+ provider: dbProvider as any,
2432
+ messages,
2433
+ maxTokens: 4096,
2434
+ tools: prepareTools(agent, unifiedSessionId),
2435
+ maxSteps: 15,
2436
+ threadId: unifiedSessionId,
2437
+ userId,
2438
+ signal,
2439
+ onToken: async (token: string) => {
2440
+ if (signal.aborted) return;
2441
+ streamedContent += token;
2442
+ // Send chunk to client
2443
+ ws.send(JSON.stringify({
2444
+ type: "message",
2445
+ id: messageId,
2446
+ sessionId: unifiedSessionId,
2447
+ content: token,
2448
+ isChunk: true,
2449
+ isStep: false,
2450
+ } as OutboundMessage));
2451
+ },
2452
+ onStep: async (step) => {
2453
+ if (signal.aborted) return;
2454
+
2455
+ // "text" = el agente narra lo que esta pensando/haciendo
2456
+ if (step.type === "text" && step.message) {
2457
+ const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
2458
+ if (trimmedMessage) {
2459
+ ws.send(JSON.stringify({
2460
+ type: "progress",
2461
+ sessionId: unifiedSessionId,
2462
+ content: trimmedMessage,
2463
+ } as OutboundMessage));
2464
+ }
2465
+ return;
2466
+ }
2467
+
2468
+ // "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
2469
+ if (step.type === "tool_call" && step.toolName) {
2470
+ const narration = getNarration(step.toolName);
2471
+ ws.send(JSON.stringify({
2472
+ type: "progress",
2473
+ sessionId: unifiedSessionId,
2474
+ content: narration,
2475
+ } as OutboundMessage));
2476
+ return;
2477
+ }
2478
+
2479
+ // "tool_result" = resultado de herramienta → solo si pide enviarse al usuario
2480
+ if (step.type === "tool_result" && step.message) {
2481
+ try {
2482
+ const result = JSON.parse(step.message);
2483
+ if (result._sendToUser || result.status) {
2484
+ const userMessage = result.message || result.status || "";
2485
+ if (userMessage) {
2486
+ ws.send(JSON.stringify({
2487
+ type: "progress",
2488
+ sessionId: unifiedSessionId,
2489
+ content: userMessage,
2490
+ } as OutboundMessage));
2491
+ }
2492
+ return;
2493
+ }
2494
+ } catch { }
2495
+ }
2496
+ },
2497
+ });
2498
+
2499
+ // Use streamed content from onToken, fallback to response.content
2500
+ const content = streamedContent || response.content?.trim() || "";
2501
+ log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
2502
+
2503
+ const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
2504
+ const shouldSpeak = webchatPreferAudio;
2505
+ let responseType: "text" | "audio" = "text";
2506
+ let ttsProviderUsed: string | null = null;
2507
+ let ttsMimeType: string | null = null;
2508
+
2509
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2510
+
2511
+ // Don't send text message if already streamed (content came via onToken)
2512
+ const alreadyStreamed = streamedContent.length > 0;
2513
+
2514
+ if (content && !alreadyStreamed) {
2515
+ if (shouldSpeak) {
2516
+ if (!voiceConfig.ttsProvider) {
2517
+ ws.send(JSON.stringify({
2518
+ type: "message",
2519
+ sessionId: unifiedSessionId,
2520
+ content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
2521
+ isStep: false
2522
+ } as OutboundMessage));
2523
+ } else {
2524
+ try {
2525
+ log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
2526
+ const audioOutput = await voiceService.speak(content, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
2527
+ ttsProviderUsed = voiceConfig.ttsProvider;
2528
+ ttsMimeType = audioOutput.mimeType;
2529
+ responseType = "audio";
2530
+ const base64Audio = (audioOutput.data as Buffer).toString("base64");
2531
+ ws.send(JSON.stringify({
2532
+ type: "message",
2533
+ sessionId: unifiedSessionId,
2534
+ content,
2535
+ audio: base64Audio,
2536
+ mimeType: audioOutput.mimeType,
2537
+ isStep: false
2538
+ } as OutboundMessage));
2539
+ } catch (ttsError) {
2540
+ log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
2541
+ ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2542
+ }
2543
+ }
2544
+ } else {
2545
+ ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2546
+ }
2547
+ } else if (alreadyStreamed && shouldSpeak && voiceConfig.ttsProvider) {
2548
+ try {
2549
+ log.info(`🔊 TTS enabled, synthesizing audio after streaming...`);
2550
+ const audioOutput = await voiceService.speak(content, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
2551
+ const base64Audio = (audioOutput.data as Buffer).toString("base64");
2552
+ log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
2553
+ ws.send(JSON.stringify({
2554
+ type: "message",
2555
+ sessionId: unifiedSessionId,
2556
+ content,
2557
+ audio: base64Audio,
2558
+ mimeType: audioOutput.mimeType,
2559
+ isStep: false
2560
+ } as OutboundMessage));
2561
+ } catch (ttsError) {
2562
+ log.error(`TTS after streaming failed: ${(ttsError as Error).message}), skipping audio`);
2563
+ }
2564
+ }
2565
+ } catch (error) {
2566
+ const unifiedSessionId = msg.sessionId;
2567
+ // Detener typing aunque falle — nunca dejar el spinner infinito
2568
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2569
+ ws.send(JSON.stringify({
2570
+ type: "error",
2571
+ sessionId: unifiedSessionId,
2572
+ error: (error as Error).message,
2573
+ } as OutboundMessage));
2574
+ log.error(`Error for session ${unifiedSessionId}: ${(error as Error).message}`);
2575
+ }
2576
+ });
2577
+
2578
+ return;
2579
+ }
2580
+
2581
+ ws.send(JSON.stringify({
2582
+ type: "error",
2583
+ sessionId: msg.sessionId,
2584
+ error: "Unknown message type",
2585
+ } as OutboundMessage));
2586
+ },
2587
+
2588
+ close(ws) {
2589
+ const data = ws.data;
2590
+ if (data.sessionId.startsWith("meeting:")) {
2591
+ log.info(`Meeting stream client disconnected: ${data.sessionId}`);
2592
+ return;
2593
+ }
2594
+
2595
+ log.debug(`WebSocket disconnected: ${data.sessionId}`);
2596
+ logSubscribers.delete(data.sessionId);
2597
+ sessionManager.delete(data.sessionId);
2598
+ laneQueue.cancel(data.sessionId);
2599
+ unsubscribeCanvas(ws);
2600
+ canvasManager.unregisterSession(`canvas:${data.sessionId}`);
2601
+
2602
+ const channel = channelManager?.getChannel("webchat") as any;
2603
+ if (channel?.unregisterConnection) channel.unregisterConnection(data.sessionId);
2604
+ },
2605
+ },
2606
+ });
2607
+
2608
+ onLogEntry((entry) => {
2609
+ if (logSubscribers.size === 0) return;
2610
+
2611
+ const payload = JSON.stringify({
2612
+ type: "log",
2613
+ sessionId: entry.meta?.sessionId || "system",
2614
+ logEntry: entry,
2615
+ });
2616
+
2617
+ for (const sessionId of logSubscribers) {
2618
+ const session = sessionManager.get(sessionId);
2619
+ if (session?.ws && session.ws.readyState === 1) {
2620
+ try {
2621
+ session.ws.send(payload);
2622
+ } catch {
2623
+ logSubscribers.delete(sessionId);
2624
+ }
2625
+ } else {
2626
+ logSubscribers.delete(sessionId);
2627
+ }
2628
+ }
2629
+ });
2630
+
2631
+ log.info(`Gateway started successfully`);
2632
+
2633
+ // Check if running as child process in dev mode (parent handles browser open)
2634
+ const isGatewayChild = process.env.HIVE_GATEWAY_CHILD === "1";
2635
+
2636
+ // Print URLs based on mode
2637
+ if (isDev) {
2638
+ // In development: Gateway serves UI on port 18790 (same as production), Vite provides HMR on 5173
2639
+ const devUrl = gatewaySetupMode ? `http://localhost:${port}/setup` : `http://localhost:${port}`;
2640
+ log.info(`[gateway] UI: ${devUrl}`);
2641
+ log.info(`[gateway] API: http://${host}:${port}`);
2642
+ log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
2643
+ log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
2644
+ log.info(`[gateway] Modo: desarrollo`);
2645
+ if (!isGatewayChild) {
2646
+ log.info(gatewaySetupMode ? `🎉 Primer arranque — abriendo setup...` : `🐝 Administra tu Hive aquí: ${devUrl}`);
2647
+ }
2648
+ } else {
2649
+ // In production: Gateway serves UI from dist/
2650
+ const isSetupMode = gatewaySetupMode;
2651
+ const baseUrl = process.env.HIVE_PUBLIC_URL?.replace(/\/$/, "") ?? `http://${host}:${port}`;
2652
+ const uiUrl = isSetupMode ? `${baseUrl}/setup` : `${baseUrl}/ui`;
2653
+
2654
+ log.info(`[gateway] UI: ${uiUrl}`);
2655
+ log.info(`[gateway] API: http://${host}:${port}`);
2656
+ log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
2657
+ log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
2658
+
2659
+ // Always open browser on startup (setup and normal mode).
2660
+ // Set NO_BROWSER=1 to skip in headless/server environments (e.g. CLI parent manages the browser).
2661
+ if (!process.env.NO_BROWSER) {
2662
+ log.info(isSetupMode ? `🎉 Primer arranque — abriendo wizard de configuración...` : `🐝 Administra tu Hive aquí: ${uiUrl}`);
2663
+ try {
2664
+ const platform = process.platform;
2665
+ let shellCmd: string;
2666
+ if (platform === "win32") {
2667
+ shellCmd = `start "" "${uiUrl}"`;
2668
+ } else if (platform === "darwin") {
2669
+ shellCmd = `open "${uiUrl}"`;
2670
+ } else {
2671
+ // Linux: gio open first (GNOME/Wayland native), then xdg-open fallbacks
2672
+ shellCmd = `gio open "${uiUrl}" 2>/dev/null || xdg-open "${uiUrl}" 2>/dev/null || sensible-browser "${uiUrl}" 2>/dev/null || x-www-browser "${uiUrl}" 2>/dev/null || true`;
2673
+ }
2674
+ const shell = platform === "win32" ? "cmd" : "/bin/sh";
2675
+ const shellArg = platform === "win32" ? "/c" : "-c";
2676
+ // Use Bun.spawn (native Bun API) for reliable detached subprocess
2677
+ const proc = Bun.spawn([shell, shellArg, shellCmd], {
2678
+ stdout: "ignore",
2679
+ stderr: "ignore",
2680
+ stdin: "ignore",
2681
+ });
2682
+ proc.unref();
2683
+ } catch (err) {
2684
+ log.warn(`Could not open browser: ${(err as Error).message}`);
2685
+ }
2686
+ }
2687
+ }
2688
+ if (!gatewaySetupMode) log.info(`Channels: ${channelManager.listChannels().map((c) => c.name).join(", ") || "none"}`);
2689
+
2690
+ // FIX 7 — SIGTERM: graceful shutdown with full cleanup
2691
+ process.on("SIGTERM", async () => {
2692
+ log.info("Received SIGTERM, shutting down gracefully...");
2693
+ watchers.forEach((close) => close());
2694
+
2695
+ const mcp = agent?.getMCPManager();
2696
+ if (mcp) {
2697
+ log.info("Disconnecting MCP servers...");
2698
+ await mcp.disconnectAll().catch(() => { });
2699
+ }
2700
+
2701
+ if (channelManager) {
2702
+ log.info("Stopping channels...");
2703
+ await channelManager.stopAll();
2704
+ }
2705
+
2706
+ // BrowserService — kill any running browser processes
2707
+ try {
2708
+ const mod = await import("../tools/web/browser-service");
2709
+ mod.CDPClient.closeAll();
2710
+ log.info("Browser processes cleaned up");
2711
+ } catch { }
2712
+
2713
+ // CanvasManager — stop heartbeat intervals
2714
+ try {
2715
+ canvasManager.clearAll();
2716
+ log.info("Canvas sessions cleaned up");
2717
+ } catch { }
2718
+
2719
+ // MCP hot-reload — stop polling interval
2720
+ try {
2721
+ const { stopMCPHotReload } = await import("../mcp/hot-reload");
2722
+ stopMCPHotReload();
2723
+ log.info("MCP hot-reload stopped");
2724
+ } catch { }
2725
+
2726
+ server.stop();
2727
+
2728
+ try { unlinkSync(pidFile); } catch { }
2729
+ log.info("Gateway shutdown complete");
2730
+ process.exit(0);
2731
+ });
2732
+
2733
+ process.on("SIGHUP", async () => {
2734
+ log.info("Received SIGHUP, reloading configuration...");
2735
+ try {
2736
+ const newConfig = await loadConfig();
2737
+ await agent.updateConfig(newConfig);
2738
+ await agent.reload();
2739
+ log.info("Configuration reloaded successfully");
2740
+ } catch (error) {
2741
+ log.error(`Failed to reload configuration: ${(error as Error).message}`);
2742
+ }
2743
+ });
2744
+ }