whale-code 6.5.4 → 6.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (853) hide show
  1. package/README.md +39 -31
  2. package/bin/{swagmanager-mcp.js → whale-code.js} +17 -2
  3. package/dist/cli/app.js +148 -72
  4. package/dist/cli/app.js.map +1 -0
  5. package/dist/cli/chat/AgentSelector.js +105 -10
  6. package/dist/cli/chat/AgentSelector.js.map +1 -0
  7. package/dist/cli/chat/ChatApp.d.ts +31 -0
  8. package/dist/cli/chat/ChatApp.js +539 -286
  9. package/dist/cli/chat/ChatApp.js.map +1 -0
  10. package/dist/cli/chat/ChatInput.js +1088 -770
  11. package/dist/cli/chat/ChatInput.js.map +1 -0
  12. package/dist/cli/chat/MarkdownText.js +39 -14
  13. package/dist/cli/chat/MarkdownText.js.map +1 -0
  14. package/dist/cli/chat/MemoryManager.js +181 -46
  15. package/dist/cli/chat/MemoryManager.js.map +1 -0
  16. package/dist/cli/chat/MessageList.d.ts +2 -3
  17. package/dist/cli/chat/MessageList.js +186 -45
  18. package/dist/cli/chat/MessageList.js.map +1 -0
  19. package/dist/cli/chat/ModelSelector.js +282 -63
  20. package/dist/cli/chat/ModelSelector.js.map +1 -0
  21. package/dist/cli/chat/NodeManager.js +165 -75
  22. package/dist/cli/chat/NodeManager.js.map +1 -0
  23. package/dist/cli/chat/NodeSelector.js +171 -30
  24. package/dist/cli/chat/NodeSelector.js.map +1 -0
  25. package/dist/cli/chat/PlanApproval.js +281 -57
  26. package/dist/cli/chat/PlanApproval.js.map +1 -0
  27. package/dist/cli/chat/RewindViewer.js +559 -144
  28. package/dist/cli/chat/RewindViewer.js.map +1 -0
  29. package/dist/cli/chat/SessionManager.js +137 -30
  30. package/dist/cli/chat/SessionManager.js.map +1 -0
  31. package/dist/cli/chat/SlashMenu.js +293 -164
  32. package/dist/cli/chat/SlashMenu.js.map +1 -0
  33. package/dist/cli/chat/StatusBar.js +172 -9
  34. package/dist/cli/chat/StatusBar.js.map +1 -0
  35. package/dist/cli/chat/StoreSelector.js +147 -18
  36. package/dist/cli/chat/StoreSelector.js.map +1 -0
  37. package/dist/cli/chat/StreamingText.d.ts +1 -5
  38. package/dist/cli/chat/StreamingText.js +22 -7
  39. package/dist/cli/chat/StreamingText.js.map +1 -0
  40. package/dist/cli/chat/SubagentPanel.d.ts +1 -2
  41. package/dist/cli/chat/SubagentPanel.js +612 -72
  42. package/dist/cli/chat/SubagentPanel.js.map +1 -0
  43. package/dist/cli/chat/TeamPanel.d.ts +1 -0
  44. package/dist/cli/chat/TeamPanel.js +230 -30
  45. package/dist/cli/chat/TeamPanel.js.map +1 -0
  46. package/dist/cli/chat/ThemeSelector.js +84 -24
  47. package/dist/cli/chat/ThemeSelector.js.map +1 -0
  48. package/dist/cli/chat/ToolIndicator.js +1476 -371
  49. package/dist/cli/chat/ToolIndicator.js.map +1 -0
  50. package/dist/cli/chat/hooks/useAgentLoop.d.ts +1 -0
  51. package/dist/cli/chat/hooks/useAgentLoop.js +481 -367
  52. package/dist/cli/chat/hooks/useAgentLoop.js.map +1 -0
  53. package/dist/cli/chat/hooks/useSlashCommands.d.ts +3 -14
  54. package/dist/cli/chat/hooks/useSlashCommands.js +744 -572
  55. package/dist/cli/chat/hooks/useSlashCommands.js.map +1 -0
  56. package/dist/cli/commands/config-cmd.js +56 -57
  57. package/dist/cli/commands/config-cmd.js.map +1 -0
  58. package/dist/cli/commands/db.js +184 -169
  59. package/dist/cli/commands/db.js.map +1 -0
  60. package/dist/cli/commands/doctor.js +212 -122
  61. package/dist/cli/commands/doctor.js.map +1 -0
  62. package/dist/cli/commands/init.js +211 -244
  63. package/dist/cli/commands/init.js.map +1 -0
  64. package/dist/cli/commands/mcp.js +127 -122
  65. package/dist/cli/commands/mcp.js.map +1 -0
  66. package/dist/cli/login/LoginApp.js +355 -141
  67. package/dist/cli/login/LoginApp.js.map +1 -0
  68. package/dist/cli/print-mode.js +196 -177
  69. package/dist/cli/print-mode.js.map +1 -0
  70. package/dist/cli/serve-mode.js +615 -530
  71. package/dist/cli/serve-mode.js.map +1 -0
  72. package/dist/cli/services/agent-config.d.ts +29 -0
  73. package/dist/cli/services/agent-config.js +91 -0
  74. package/dist/cli/services/agent-config.js.map +1 -0
  75. package/dist/cli/services/agent-definitions.d.ts +4 -1
  76. package/dist/cli/services/agent-definitions.js +97 -56
  77. package/dist/cli/services/agent-definitions.js.map +1 -0
  78. package/dist/cli/services/agent-events.js +225 -162
  79. package/dist/cli/services/agent-events.js.map +1 -0
  80. package/dist/cli/services/agent-loop.js +978 -669
  81. package/dist/cli/services/agent-loop.js.map +1 -0
  82. package/dist/cli/services/agent-worker-base.d.ts +35 -5
  83. package/dist/cli/services/agent-worker-base.js +337 -153
  84. package/dist/cli/services/agent-worker-base.js.map +1 -0
  85. package/dist/cli/services/api-retry.js +69 -64
  86. package/dist/cli/services/api-retry.js.map +1 -0
  87. package/dist/cli/services/auth-service.d.ts +3 -3
  88. package/dist/cli/services/auth-service.js +209 -132
  89. package/dist/cli/services/auth-service.js.map +1 -0
  90. package/dist/cli/services/background-processes.js +343 -267
  91. package/dist/cli/services/background-processes.js.map +1 -0
  92. package/dist/cli/services/browser-auth.d.ts +2 -2
  93. package/dist/cli/services/browser-auth.js +159 -118
  94. package/dist/cli/services/browser-auth.js.map +1 -0
  95. package/dist/cli/services/claude-md-loader.js +40 -36
  96. package/dist/cli/services/claude-md-loader.js.map +1 -0
  97. package/dist/cli/services/config-store.d.ts +9 -4
  98. package/dist/cli/services/config-store.js +164 -117
  99. package/dist/cli/services/config-store.js.map +1 -0
  100. package/dist/cli/services/debug-log.d.ts +1 -1
  101. package/dist/cli/services/debug-log.js +34 -35
  102. package/dist/cli/services/debug-log.js.map +1 -0
  103. package/dist/cli/services/env-detect.d.ts +7 -0
  104. package/dist/cli/services/env-detect.js +9 -0
  105. package/dist/cli/services/env-detect.js.map +1 -0
  106. package/dist/cli/services/error-logger.d.ts +2 -3
  107. package/dist/cli/services/error-logger.js +189 -180
  108. package/dist/cli/services/error-logger.js.map +1 -0
  109. package/dist/cli/services/file-history.d.ts +1 -1
  110. package/dist/cli/services/file-history.js +50 -54
  111. package/dist/cli/services/file-history.js.map +1 -0
  112. package/dist/cli/services/format-server-response.js +332 -372
  113. package/dist/cli/services/format-server-response.js.map +1 -0
  114. package/dist/cli/services/git-context.js +61 -45
  115. package/dist/cli/services/git-context.js.map +1 -0
  116. package/dist/cli/services/hooks.d.ts +2 -2
  117. package/dist/cli/services/hooks.js +195 -180
  118. package/dist/cli/services/hooks.js.map +1 -0
  119. package/dist/cli/services/ink-incremental.d.ts +19 -0
  120. package/dist/cli/services/ink-incremental.js +59 -0
  121. package/dist/cli/services/ink-incremental.js.map +1 -0
  122. package/dist/cli/services/ink-resize-fix.js +54 -44
  123. package/dist/cli/services/ink-resize-fix.js.map +1 -0
  124. package/dist/cli/services/ink-sync-output.d.ts +12 -0
  125. package/dist/cli/services/ink-sync-output.js +16 -0
  126. package/dist/cli/services/ink-sync-output.js.map +1 -0
  127. package/dist/cli/services/interactive-tools.js +268 -212
  128. package/dist/cli/services/interactive-tools.js.map +1 -0
  129. package/dist/cli/services/keybinding-manager.d.ts +11 -1
  130. package/dist/cli/services/keybinding-manager.js +126 -63
  131. package/dist/cli/services/keybinding-manager.js.map +1 -0
  132. package/dist/cli/services/local-tools.d.ts +1 -1
  133. package/dist/cli/services/local-tools.js +939 -656
  134. package/dist/cli/services/local-tools.js.map +1 -0
  135. package/dist/cli/services/lsp-manager.js +757 -594
  136. package/dist/cli/services/lsp-manager.js.map +1 -0
  137. package/dist/cli/services/mcp-client.d.ts +1 -1
  138. package/dist/cli/services/mcp-client.js +173 -134
  139. package/dist/cli/services/mcp-client.js.map +1 -0
  140. package/dist/cli/services/memory-manager.js +53 -40
  141. package/dist/cli/services/memory-manager.js.map +1 -0
  142. package/dist/cli/services/model-manager.js +55 -40
  143. package/dist/cli/services/model-manager.js.map +1 -0
  144. package/dist/cli/services/model-router.js +115 -85
  145. package/dist/cli/services/model-router.js.map +1 -0
  146. package/dist/cli/services/paths.d.ts +30 -0
  147. package/dist/cli/services/paths.js +81 -0
  148. package/dist/cli/services/paths.js.map +1 -0
  149. package/dist/cli/services/permission-modes.js +32 -25
  150. package/dist/cli/services/permission-modes.js.map +1 -0
  151. package/dist/cli/services/rewind.js +182 -168
  152. package/dist/cli/services/rewind.js.map +1 -0
  153. package/dist/cli/services/ripgrep.js +115 -115
  154. package/dist/cli/services/ripgrep.js.map +1 -0
  155. package/dist/cli/services/sandbox.d.ts +1 -1
  156. package/dist/cli/services/sandbox.js +58 -37
  157. package/dist/cli/services/sandbox.js.map +1 -0
  158. package/dist/cli/services/server-tools.js +738 -565
  159. package/dist/cli/services/server-tools.js.map +1 -0
  160. package/dist/cli/services/session-persistence.js +69 -74
  161. package/dist/cli/services/session-persistence.js.map +1 -0
  162. package/dist/cli/services/subagent-worker.js +42 -27
  163. package/dist/cli/services/subagent-worker.js.map +1 -0
  164. package/dist/cli/services/subagent.d.ts +2 -0
  165. package/dist/cli/services/subagent.js +606 -430
  166. package/dist/cli/services/subagent.js.map +1 -0
  167. package/dist/cli/services/system-prompt.js +86 -78
  168. package/dist/cli/services/system-prompt.js.map +1 -0
  169. package/dist/cli/services/task-decomposer.d.ts +1 -1
  170. package/dist/cli/services/task-decomposer.js +172 -139
  171. package/dist/cli/services/task-decomposer.js.map +1 -0
  172. package/dist/cli/services/team-lead.d.ts +2 -2
  173. package/dist/cli/services/team-lead.js +727 -529
  174. package/dist/cli/services/team-lead.js.map +1 -0
  175. package/dist/cli/services/team-state.js +319 -319
  176. package/dist/cli/services/team-state.js.map +1 -0
  177. package/dist/cli/services/teammate.d.ts +8 -2
  178. package/dist/cli/services/teammate.js +862 -560
  179. package/dist/cli/services/teammate.js.map +1 -0
  180. package/dist/cli/services/telemetry.d.ts +6 -1
  181. package/dist/cli/services/telemetry.js +180 -157
  182. package/dist/cli/services/telemetry.js.map +1 -0
  183. package/dist/cli/services/tools/agent-tools.d.ts +3 -3
  184. package/dist/cli/services/tools/agent-tools.js +480 -322
  185. package/dist/cli/services/tools/agent-tools.js.map +1 -0
  186. package/dist/cli/services/tools/file-ops.js +563 -450
  187. package/dist/cli/services/tools/file-ops.js.map +1 -0
  188. package/dist/cli/services/tools/search-tools.js +231 -162
  189. package/dist/cli/services/tools/search-tools.js.map +1 -0
  190. package/dist/cli/services/tools/shell-exec.js +197 -151
  191. package/dist/cli/services/tools/shell-exec.js.map +1 -0
  192. package/dist/cli/services/tools/task-manager.js +206 -173
  193. package/dist/cli/services/tools/task-manager.js.map +1 -0
  194. package/dist/cli/services/tools/web-tools.js +388 -341
  195. package/dist/cli/services/tools/web-tools.js.map +1 -0
  196. package/dist/cli/setup/SetupApp.d.ts +2 -2
  197. package/dist/cli/setup/SetupApp.js +608 -160
  198. package/dist/cli/setup/SetupApp.js.map +1 -0
  199. package/dist/cli/shared/ErrorBoundary.d.ts +22 -0
  200. package/dist/cli/shared/ErrorBoundary.js +73 -0
  201. package/dist/cli/shared/ErrorBoundary.js.map +1 -0
  202. package/dist/cli/shared/MatrixIntro.js +66 -69
  203. package/dist/cli/shared/MatrixIntro.js.map +1 -0
  204. package/dist/cli/shared/SpinnerSlot.d.ts +14 -0
  205. package/dist/cli/shared/SpinnerSlot.js +63 -0
  206. package/dist/cli/shared/SpinnerSlot.js.map +1 -0
  207. package/dist/cli/shared/Theme.d.ts +1 -1
  208. package/dist/cli/shared/Theme.js +136 -92
  209. package/dist/cli/shared/Theme.js.map +1 -0
  210. package/dist/cli/shared/WhaleBanner.js +99 -11
  211. package/dist/cli/shared/WhaleBanner.js.map +1 -0
  212. package/dist/cli/shared/markdown.d.ts +3 -1
  213. package/dist/cli/shared/markdown.js +736 -674
  214. package/dist/cli/shared/markdown.js.map +1 -0
  215. package/dist/cli/shared/marked-terminal.d.js +2 -0
  216. package/dist/cli/shared/marked-terminal.d.js.map +1 -0
  217. package/dist/cli/shared/theme-manager.js +99 -90
  218. package/dist/cli/shared/theme-manager.js.map +1 -0
  219. package/dist/cli/shared/theme-presets.js +256 -254
  220. package/dist/cli/shared/theme-presets.js.map +1 -0
  221. package/dist/cli/status/StatusApp.js +235 -86
  222. package/dist/cli/status/StatusApp.js.map +1 -0
  223. package/dist/cli/stores/StoreApp.js +275 -65
  224. package/dist/cli/stores/StoreApp.js.map +1 -0
  225. package/dist/index.d.ts +2 -2
  226. package/dist/index.js +509 -396
  227. package/dist/index.js.map +1 -0
  228. package/dist/local-agent/connection.d.ts +2 -2
  229. package/dist/local-agent/connection.js +352 -293
  230. package/dist/local-agent/connection.js.map +1 -0
  231. package/dist/local-agent/discovery.js +259 -122
  232. package/dist/local-agent/discovery.js.map +1 -0
  233. package/dist/local-agent/executor.js +216 -193
  234. package/dist/local-agent/executor.js.map +1 -0
  235. package/dist/local-agent/index.d.ts +2 -2
  236. package/dist/local-agent/index.js +156 -156
  237. package/dist/local-agent/index.js.map +1 -0
  238. package/dist/node/adapters/base.js +18 -8
  239. package/dist/node/adapters/base.js.map +1 -0
  240. package/dist/node/adapters/discord.js +286 -275
  241. package/dist/node/adapters/discord.js.map +1 -0
  242. package/dist/node/adapters/email.js +189 -202
  243. package/dist/node/adapters/email.js.map +1 -0
  244. package/dist/node/adapters/imessage.js +145 -142
  245. package/dist/node/adapters/imessage.js.map +1 -0
  246. package/dist/node/adapters/slack.js +237 -236
  247. package/dist/node/adapters/slack.js.map +1 -0
  248. package/dist/node/adapters/sms.js +149 -151
  249. package/dist/node/adapters/sms.js.map +1 -0
  250. package/dist/node/adapters/telegram.js +88 -92
  251. package/dist/node/adapters/telegram.js.map +1 -0
  252. package/dist/node/adapters/webchat.js +160 -136
  253. package/dist/node/adapters/webchat.js.map +1 -0
  254. package/dist/node/adapters/whatsapp.js +212 -215
  255. package/dist/node/adapters/whatsapp.js.map +1 -0
  256. package/dist/node/cli.js +884 -653
  257. package/dist/node/cli.js.map +1 -0
  258. package/dist/node/config.js +20 -18
  259. package/dist/node/config.js.map +1 -0
  260. package/dist/node/gateway-client.js +191 -181
  261. package/dist/node/gateway-client.js.map +1 -0
  262. package/dist/node/portal/clipboard.js +161 -130
  263. package/dist/node/portal/clipboard.js.map +1 -0
  264. package/dist/node/portal/discovery.js +51 -45
  265. package/dist/node/portal/discovery.js.map +1 -0
  266. package/dist/node/portal/forward.js +64 -58
  267. package/dist/node/portal/forward.js.map +1 -0
  268. package/dist/node/portal/index.js +246 -221
  269. package/dist/node/portal/index.js.map +1 -0
  270. package/dist/node/portal/multiplexer.js +192 -182
  271. package/dist/node/portal/multiplexer.js.map +1 -0
  272. package/dist/node/portal/permissions.js +102 -70
  273. package/dist/node/portal/permissions.js.map +1 -0
  274. package/dist/node/portal/protocol.js +153 -116
  275. package/dist/node/portal/protocol.js.map +1 -0
  276. package/dist/node/portal/screen.js +80 -69
  277. package/dist/node/portal/screen.js.map +1 -0
  278. package/dist/node/portal/session.js +124 -117
  279. package/dist/node/portal/session.js.map +1 -0
  280. package/dist/node/portal/shell.js +140 -113
  281. package/dist/node/portal/shell.js.map +1 -0
  282. package/dist/node/portal/stream.js +77 -75
  283. package/dist/node/portal/stream.js.map +1 -0
  284. package/dist/node/portal/transfer.js +190 -167
  285. package/dist/node/portal/transfer.js.map +1 -0
  286. package/dist/node/portal/ui.js +124 -99
  287. package/dist/node/portal/ui.js.map +1 -0
  288. package/dist/node/remote-desktop/compile-helper.js +50 -45
  289. package/dist/node/remote-desktop/compile-helper.js.map +1 -0
  290. package/dist/node/remote-desktop/index.js +215 -187
  291. package/dist/node/remote-desktop/index.js.map +1 -0
  292. package/dist/node/remote-desktop/protocol.js +45 -29
  293. package/dist/node/remote-desktop/protocol.js.map +1 -0
  294. package/dist/node/runtime.js +493 -410
  295. package/dist/node/runtime.js.map +1 -0
  296. package/dist/server/handlers/__test-utils__/test-db.js +39 -89
  297. package/dist/server/handlers/__test-utils__/test-db.js.map +1 -0
  298. package/dist/server/handlers/analytics.js +467 -261
  299. package/dist/server/handlers/analytics.js.map +1 -0
  300. package/dist/server/handlers/api-docs.d.ts +6 -0
  301. package/dist/server/handlers/api-docs.js +1613 -0
  302. package/dist/server/handlers/api-docs.js.map +1 -0
  303. package/dist/server/handlers/api-keys.js +295 -232
  304. package/dist/server/handlers/api-keys.js.map +1 -0
  305. package/dist/server/handlers/billing.js +330 -239
  306. package/dist/server/handlers/billing.js.map +1 -0
  307. package/dist/server/handlers/browser.js +468 -395
  308. package/dist/server/handlers/browser.js.map +1 -0
  309. package/dist/server/handlers/catalog.js +1377 -978
  310. package/dist/server/handlers/catalog.js.map +1 -0
  311. package/dist/server/handlers/clickhouse.js +157 -109
  312. package/dist/server/handlers/clickhouse.js.map +1 -0
  313. package/dist/server/handlers/comms.d.ts +0 -53
  314. package/dist/server/handlers/comms.js +1443 -970
  315. package/dist/server/handlers/comms.js.map +1 -0
  316. package/dist/server/handlers/creations.js +461 -394
  317. package/dist/server/handlers/creations.js.map +1 -0
  318. package/dist/server/handlers/crm.js +1082 -791
  319. package/dist/server/handlers/crm.js.map +1 -0
  320. package/dist/server/handlers/discovery.js +251 -232
  321. package/dist/server/handlers/discovery.js.map +1 -0
  322. package/dist/server/handlers/embeddings.js +241 -164
  323. package/dist/server/handlers/embeddings.js.map +1 -0
  324. package/dist/server/handlers/enrichment.js +887 -718
  325. package/dist/server/handlers/enrichment.js.map +1 -0
  326. package/dist/server/handlers/image-gen.js +467 -376
  327. package/dist/server/handlers/image-gen.js.map +1 -0
  328. package/dist/server/handlers/inventory.js +797 -424
  329. package/dist/server/handlers/inventory.js.map +1 -0
  330. package/dist/server/handlers/kali.js +272 -230
  331. package/dist/server/handlers/kali.js.map +1 -0
  332. package/dist/server/handlers/llm-providers.js +803 -580
  333. package/dist/server/handlers/llm-providers.js.map +1 -0
  334. package/dist/server/handlers/local-agent.js +133 -105
  335. package/dist/server/handlers/local-agent.js.map +1 -0
  336. package/dist/server/handlers/media.js +1179 -857
  337. package/dist/server/handlers/media.js.map +1 -0
  338. package/dist/server/handlers/meta-ads.js +2669 -2093
  339. package/dist/server/handlers/meta-ads.js.map +1 -0
  340. package/dist/server/handlers/nodes.js +1321 -913
  341. package/dist/server/handlers/nodes.js.map +1 -0
  342. package/dist/server/handlers/operations.js +183 -157
  343. package/dist/server/handlers/operations.js.map +1 -0
  344. package/dist/server/handlers/platform.js +346 -210
  345. package/dist/server/handlers/platform.js.map +1 -0
  346. package/dist/server/handlers/remove-bg.js +118 -86
  347. package/dist/server/handlers/remove-bg.js.map +1 -0
  348. package/dist/server/handlers/storefront.js +586 -446
  349. package/dist/server/handlers/storefront.js.map +1 -0
  350. package/dist/server/handlers/supply-chain.js +546 -326
  351. package/dist/server/handlers/supply-chain.js.map +1 -0
  352. package/dist/server/handlers/transcription.js +106 -97
  353. package/dist/server/handlers/transcription.js.map +1 -0
  354. package/dist/server/handlers/video-gen.js +593 -424
  355. package/dist/server/handlers/video-gen.js.map +1 -0
  356. package/dist/server/handlers/voice.js +1458 -1017
  357. package/dist/server/handlers/voice.js.map +1 -0
  358. package/dist/server/handlers/workflow-steps.js +2837 -2116
  359. package/dist/server/handlers/workflow-steps.js.map +1 -0
  360. package/dist/server/handlers/workflows.js +1630 -933
  361. package/dist/server/handlers/workflows.js.map +1 -0
  362. package/dist/server/index.js +3166 -2390
  363. package/dist/server/index.js.map +1 -0
  364. package/dist/server/lib/batch-client.js +471 -409
  365. package/dist/server/lib/batch-client.js.map +1 -0
  366. package/dist/server/lib/clickhouse-buffer.js +118 -104
  367. package/dist/server/lib/clickhouse-buffer.js.map +1 -0
  368. package/dist/server/lib/clickhouse-client.js +107 -107
  369. package/dist/server/lib/clickhouse-client.js.map +1 -0
  370. package/dist/server/lib/coa-renderer.js +1786 -356
  371. package/dist/server/lib/coa-renderer.js.map +1 -0
  372. package/dist/server/lib/code-worker-pool.js +227 -177
  373. package/dist/server/lib/code-worker-pool.js.map +1 -0
  374. package/dist/server/lib/code-worker.js +174 -164
  375. package/dist/server/lib/code-worker.js.map +1 -0
  376. package/dist/server/lib/compaction-service.d.ts +2 -12
  377. package/dist/server/lib/compaction-service.js +74 -184
  378. package/dist/server/lib/compaction-service.js.map +1 -0
  379. package/dist/server/lib/logger.js +36 -24
  380. package/dist/server/lib/logger.js.map +1 -0
  381. package/dist/server/lib/otel.js +101 -80
  382. package/dist/server/lib/otel.js.map +1 -0
  383. package/dist/server/lib/pdf-renderer.d.ts +1 -1
  384. package/dist/server/lib/pdf-renderer.js +954 -776
  385. package/dist/server/lib/pdf-renderer.js.map +1 -0
  386. package/dist/server/lib/prompt-sanitizer.js +188 -108
  387. package/dist/server/lib/prompt-sanitizer.js.map +1 -0
  388. package/dist/server/lib/provider-capabilities.js +136 -138
  389. package/dist/server/lib/provider-capabilities.js.map +1 -0
  390. package/dist/server/lib/provider-failover.js +190 -168
  391. package/dist/server/lib/provider-failover.js.map +1 -0
  392. package/dist/server/lib/rate-limiter.js +186 -117
  393. package/dist/server/lib/rate-limiter.js.map +1 -0
  394. package/dist/server/lib/react-pdf-layout.js +551 -382
  395. package/dist/server/lib/react-pdf-layout.js.map +1 -0
  396. package/dist/server/lib/server-agent-loop.d.ts +9 -0
  397. package/dist/server/lib/server-agent-loop.js +906 -624
  398. package/dist/server/lib/server-agent-loop.js.map +1 -0
  399. package/dist/server/lib/server-subagent.d.ts +2 -0
  400. package/dist/server/lib/server-subagent.js +260 -162
  401. package/dist/server/lib/server-subagent.js.map +1 -0
  402. package/dist/server/lib/session-checkpoint.js +105 -96
  403. package/dist/server/lib/session-checkpoint.js.map +1 -0
  404. package/dist/server/lib/ssrf-guard.js +193 -184
  405. package/dist/server/lib/ssrf-guard.js.map +1 -0
  406. package/dist/server/lib/supabase-client.js +94 -82
  407. package/dist/server/lib/supabase-client.js.map +1 -0
  408. package/dist/server/lib/template-resolver.js +154 -176
  409. package/dist/server/lib/template-resolver.js.map +1 -0
  410. package/dist/server/lib/utils.js +242 -133
  411. package/dist/server/lib/utils.js.map +1 -0
  412. package/dist/server/local-agent-gateway.d.ts +2 -2
  413. package/dist/server/local-agent-gateway.js +785 -627
  414. package/dist/server/local-agent-gateway.js.map +1 -0
  415. package/dist/server/providers/anthropic.js +254 -176
  416. package/dist/server/providers/anthropic.js.map +1 -0
  417. package/dist/server/providers/bedrock.js +221 -162
  418. package/dist/server/providers/bedrock.js.map +1 -0
  419. package/dist/server/providers/gemini.js +548 -418
  420. package/dist/server/providers/gemini.js.map +1 -0
  421. package/dist/server/providers/openai.js +571 -437
  422. package/dist/server/providers/openai.js.map +1 -0
  423. package/dist/server/providers/registry.js +23 -18
  424. package/dist/server/providers/registry.js.map +1 -0
  425. package/dist/server/providers/shared.js +123 -95
  426. package/dist/server/providers/shared.js.map +1 -0
  427. package/dist/server/providers/types.js +1 -11
  428. package/dist/server/providers/types.js.map +1 -0
  429. package/dist/server/proxy-handlers.js +209 -165
  430. package/dist/server/proxy-handlers.js.map +1 -0
  431. package/dist/server/tool-router.d.ts +13 -0
  432. package/dist/server/tool-router.js +960 -598
  433. package/dist/server/tool-router.js.map +1 -0
  434. package/dist/server/validation.js +248 -188
  435. package/dist/server/validation.js.map +1 -0
  436. package/dist/server/worker.js +202 -133
  437. package/dist/server/worker.js.map +1 -0
  438. package/dist/setup.d.ts +2 -2
  439. package/dist/setup.js +151 -147
  440. package/dist/setup.js.map +1 -0
  441. package/dist/shared/agent-core.d.ts +191 -24
  442. package/dist/shared/agent-core.js +971 -462
  443. package/dist/shared/agent-core.js.map +1 -0
  444. package/dist/shared/anthropic-types.js +1 -6
  445. package/dist/shared/anthropic-types.js.map +1 -0
  446. package/dist/shared/api-client.d.ts +17 -9
  447. package/dist/shared/api-client.js +419 -327
  448. package/dist/shared/api-client.js.map +1 -0
  449. package/dist/shared/compaction.d.ts +36 -0
  450. package/dist/shared/compaction.js +138 -0
  451. package/dist/shared/compaction.js.map +1 -0
  452. package/dist/shared/constants.js +67 -64
  453. package/dist/shared/constants.js.map +1 -0
  454. package/dist/shared/sse-parser.js +221 -219
  455. package/dist/shared/sse-parser.js.map +1 -0
  456. package/dist/shared/tool-dispatch.d.ts +4 -2
  457. package/dist/shared/tool-dispatch.js +226 -165
  458. package/dist/shared/tool-dispatch.js.map +1 -0
  459. package/dist/shared/types.js +1 -6
  460. package/dist/shared/types.js.map +1 -0
  461. package/dist/types/cli-highlight.d.js +2 -0
  462. package/dist/types/cli-highlight.d.js.map +1 -0
  463. package/dist/types/diff.d.js +2 -0
  464. package/dist/types/diff.d.js.map +1 -0
  465. package/dist/types/pdf-parse.d.js +2 -0
  466. package/dist/types/pdf-parse.d.js.map +1 -0
  467. package/dist/updater.d.ts +1 -1
  468. package/dist/updater.js +118 -92
  469. package/dist/updater.js.map +1 -0
  470. package/dist/webchat/widget.js +227 -380
  471. package/dist/webchat/widget.js.map +1 -0
  472. package/package.json +22 -10
  473. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  474. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  475. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  476. package/vendor/ink/build/apply-styles.js +175 -0
  477. package/vendor/ink/build/build-layout.js +77 -0
  478. package/vendor/ink/build/calculate-wrapped-text.js +53 -0
  479. package/vendor/ink/build/colorize.d.ts +3 -0
  480. package/vendor/ink/build/colorize.js +48 -0
  481. package/vendor/ink/build/colorize.js.map +1 -0
  482. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  483. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  484. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  485. package/vendor/ink/build/components/App.d.ts +18 -0
  486. package/vendor/ink/build/components/App.js +351 -0
  487. package/vendor/ink/build/components/App.js.map +1 -0
  488. package/vendor/ink/build/components/AppContext.d.ts +15 -0
  489. package/vendor/ink/build/components/AppContext.js +11 -0
  490. package/vendor/ink/build/components/AppContext.js.map +1 -0
  491. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  492. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  493. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  494. package/vendor/ink/build/components/Box.d.ts +117 -0
  495. package/vendor/ink/build/components/Box.js +34 -0
  496. package/vendor/ink/build/components/Box.js.map +1 -0
  497. package/vendor/ink/build/components/Color.js +62 -0
  498. package/vendor/ink/build/components/Cursor.d.ts +83 -0
  499. package/vendor/ink/build/components/Cursor.js +53 -0
  500. package/vendor/ink/build/components/Cursor.js.map +1 -0
  501. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  502. package/vendor/ink/build/components/CursorContext.js +8 -0
  503. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  504. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  505. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  506. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  507. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  508. package/vendor/ink/build/components/ErrorOverview.js +84 -0
  509. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  510. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  511. package/vendor/ink/build/components/FocusContext.js +17 -0
  512. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  513. package/vendor/ink/build/components/Newline.d.ts +13 -0
  514. package/vendor/ink/build/components/Newline.js +8 -0
  515. package/vendor/ink/build/components/Newline.js.map +1 -0
  516. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  517. package/vendor/ink/build/components/Spacer.js +11 -0
  518. package/vendor/ink/build/components/Spacer.js.map +1 -0
  519. package/vendor/ink/build/components/Static.d.ts +24 -0
  520. package/vendor/ink/build/components/Static.js +28 -0
  521. package/vendor/ink/build/components/Static.js.map +1 -0
  522. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  523. package/vendor/ink/build/components/StderrContext.js +13 -0
  524. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  525. package/vendor/ink/build/components/StdinContext.d.ts +22 -0
  526. package/vendor/ink/build/components/StdinContext.js +19 -0
  527. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  528. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  529. package/vendor/ink/build/components/StdoutContext.js +13 -0
  530. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  531. package/vendor/ink/build/components/Text.d.ts +55 -0
  532. package/vendor/ink/build/components/Text.js +50 -0
  533. package/vendor/ink/build/components/Text.js.map +1 -0
  534. package/vendor/ink/build/components/Transform.d.ts +16 -0
  535. package/vendor/ink/build/components/Transform.js +15 -0
  536. package/vendor/ink/build/components/Transform.js.map +1 -0
  537. package/vendor/ink/build/cursor-helpers.d.ts +38 -0
  538. package/vendor/ink/build/cursor-helpers.js +56 -0
  539. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  540. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  541. package/vendor/ink/build/devtools-window-polyfill.js +65 -0
  542. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  543. package/vendor/ink/build/devtools.d.ts +1 -0
  544. package/vendor/ink/build/devtools.js +11 -0
  545. package/vendor/ink/build/devtools.js.map +1 -0
  546. package/vendor/ink/build/dom.d.ts +56 -0
  547. package/vendor/ink/build/dom.js +124 -0
  548. package/vendor/ink/build/dom.js.map +1 -0
  549. package/vendor/ink/build/experimental/apply-style.js +140 -0
  550. package/vendor/ink/build/experimental/dom.js +123 -0
  551. package/vendor/ink/build/experimental/output.js +91 -0
  552. package/vendor/ink/build/experimental/reconciler.js +141 -0
  553. package/vendor/ink/build/experimental/renderer.js +81 -0
  554. package/vendor/ink/build/get-max-width.d.ts +3 -0
  555. package/vendor/ink/build/get-max-width.js +10 -0
  556. package/vendor/ink/build/get-max-width.js.map +1 -0
  557. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  558. package/vendor/ink/build/hooks/use-app.js +8 -0
  559. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  560. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  561. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  562. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  563. package/vendor/ink/build/hooks/use-focus-manager.d.ts +28 -0
  564. package/vendor/ink/build/hooks/use-focus-manager.js +17 -0
  565. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  566. package/vendor/ink/build/hooks/use-focus.d.ts +29 -0
  567. package/vendor/ink/build/hooks/use-focus.js +42 -0
  568. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  569. package/vendor/ink/build/hooks/use-input.d.ts +131 -0
  570. package/vendor/ink/build/hooks/use-input.js +124 -0
  571. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  572. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +5 -0
  573. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +11 -0
  574. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  575. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  576. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  577. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  578. package/vendor/ink/build/hooks/use-stdin.d.ts +5 -0
  579. package/vendor/ink/build/hooks/use-stdin.js +8 -0
  580. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  581. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  582. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  583. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  584. package/vendor/ink/build/hooks/useInput.js +38 -0
  585. package/vendor/ink/build/index.d.ts +34 -0
  586. package/vendor/ink/build/index.js +20 -0
  587. package/vendor/ink/build/index.js.map +1 -0
  588. package/vendor/ink/build/ink.d.ts +90 -0
  589. package/vendor/ink/build/ink.js +654 -0
  590. package/vendor/ink/build/ink.js.map +1 -0
  591. package/vendor/ink/build/input-parser.d.ts +7 -0
  592. package/vendor/ink/build/input-parser.js +154 -0
  593. package/vendor/ink/build/input-parser.js.map +1 -0
  594. package/vendor/ink/build/instance.js +205 -0
  595. package/vendor/ink/build/instances.d.ts +3 -0
  596. package/vendor/ink/build/instances.js +8 -0
  597. package/vendor/ink/build/instances.js.map +1 -0
  598. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  599. package/vendor/ink/build/kitty-keyboard.js +32 -0
  600. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  601. package/vendor/ink/build/layout.d.ts +7 -0
  602. package/vendor/ink/build/layout.js +33 -0
  603. package/vendor/ink/build/layout.js.map +1 -0
  604. package/vendor/ink/build/log-update.d.ts +19 -0
  605. package/vendor/ink/build/log-update.js +243 -0
  606. package/vendor/ink/build/log-update.js.map +1 -0
  607. package/vendor/ink/build/measure-element.d.ts +16 -0
  608. package/vendor/ink/build/measure-element.js +9 -0
  609. package/vendor/ink/build/measure-element.js.map +1 -0
  610. package/vendor/ink/build/measure-text.d.ts +6 -0
  611. package/vendor/ink/build/measure-text.js +21 -0
  612. package/vendor/ink/build/measure-text.js.map +1 -0
  613. package/vendor/ink/build/options.d.ts +52 -0
  614. package/vendor/ink/build/options.js +2 -0
  615. package/vendor/ink/build/options.js.map +1 -0
  616. package/vendor/ink/build/output.d.ts +35 -0
  617. package/vendor/ink/build/output.js +183 -0
  618. package/vendor/ink/build/output.js.map +1 -0
  619. package/vendor/ink/build/parse-keypress.d.ts +22 -0
  620. package/vendor/ink/build/parse-keypress.js +493 -0
  621. package/vendor/ink/build/parse-keypress.js.map +1 -0
  622. package/vendor/ink/build/reconciler.d.ts +4 -0
  623. package/vendor/ink/build/reconciler.js +274 -0
  624. package/vendor/ink/build/reconciler.js.map +1 -0
  625. package/vendor/ink/build/render-background.d.ts +4 -0
  626. package/vendor/ink/build/render-background.js +25 -0
  627. package/vendor/ink/build/render-background.js.map +1 -0
  628. package/vendor/ink/build/render-border.d.ts +4 -0
  629. package/vendor/ink/build/render-border.js +73 -0
  630. package/vendor/ink/build/render-border.js.map +1 -0
  631. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  632. package/vendor/ink/build/render-node-to-output.js +147 -0
  633. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  634. package/vendor/ink/build/render-to-string.d.ts +38 -0
  635. package/vendor/ink/build/render-to-string.js +115 -0
  636. package/vendor/ink/build/render-to-string.js.map +1 -0
  637. package/vendor/ink/build/render.d.ts +121 -0
  638. package/vendor/ink/build/render.js +55 -0
  639. package/vendor/ink/build/render.js.map +1 -0
  640. package/vendor/ink/build/renderer.d.ts +8 -0
  641. package/vendor/ink/build/renderer.js +55 -0
  642. package/vendor/ink/build/renderer.js.map +1 -0
  643. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  644. package/vendor/ink/build/sanitize-ansi.js +27 -0
  645. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  646. package/vendor/ink/build/screen-reader-update.d.ts +13 -0
  647. package/vendor/ink/build/screen-reader-update.js +38 -0
  648. package/vendor/ink/build/screen-reader-update.js.map +1 -0
  649. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  650. package/vendor/ink/build/squash-text-nodes.js +36 -0
  651. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  652. package/vendor/ink/build/styles.d.ts +240 -0
  653. package/vendor/ink/build/styles.js +232 -0
  654. package/vendor/ink/build/styles.js.map +1 -0
  655. package/vendor/ink/build/utils.d.ts +2 -0
  656. package/vendor/ink/build/utils.js +4 -0
  657. package/vendor/ink/build/utils.js.map +1 -0
  658. package/vendor/ink/build/wrap-text.d.ts +3 -0
  659. package/vendor/ink/build/wrap-text.js +31 -0
  660. package/vendor/ink/build/wrap-text.js.map +1 -0
  661. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  662. package/vendor/ink/build/write-synchronized.js +7 -0
  663. package/vendor/ink/build/write-synchronized.js.map +1 -0
  664. package/vendor/ink/license +10 -0
  665. package/vendor/ink/node_modules/@types/node/LICENSE +21 -0
  666. package/vendor/ink/node_modules/@types/node/README.md +15 -0
  667. package/vendor/ink/node_modules/@types/node/assert/strict.d.ts +105 -0
  668. package/vendor/ink/node_modules/@types/node/assert.d.ts +955 -0
  669. package/vendor/ink/node_modules/@types/node/async_hooks.d.ts +623 -0
  670. package/vendor/ink/node_modules/@types/node/buffer.buffer.d.ts +466 -0
  671. package/vendor/ink/node_modules/@types/node/buffer.d.ts +1810 -0
  672. package/vendor/ink/node_modules/@types/node/child_process.d.ts +1428 -0
  673. package/vendor/ink/node_modules/@types/node/cluster.d.ts +486 -0
  674. package/vendor/ink/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
  675. package/vendor/ink/node_modules/@types/node/console.d.ts +151 -0
  676. package/vendor/ink/node_modules/@types/node/constants.d.ts +20 -0
  677. package/vendor/ink/node_modules/@types/node/crypto.d.ts +4065 -0
  678. package/vendor/ink/node_modules/@types/node/dgram.d.ts +564 -0
  679. package/vendor/ink/node_modules/@types/node/diagnostics_channel.d.ts +576 -0
  680. package/vendor/ink/node_modules/@types/node/dns/promises.d.ts +503 -0
  681. package/vendor/ink/node_modules/@types/node/dns.d.ts +922 -0
  682. package/vendor/ink/node_modules/@types/node/domain.d.ts +166 -0
  683. package/vendor/ink/node_modules/@types/node/events.d.ts +1054 -0
  684. package/vendor/ink/node_modules/@types/node/fs/promises.d.ts +1329 -0
  685. package/vendor/ink/node_modules/@types/node/fs.d.ts +4676 -0
  686. package/vendor/ink/node_modules/@types/node/globals.d.ts +150 -0
  687. package/vendor/ink/node_modules/@types/node/globals.typedarray.d.ts +101 -0
  688. package/vendor/ink/node_modules/@types/node/http.d.ts +2167 -0
  689. package/vendor/ink/node_modules/@types/node/http2.d.ts +2480 -0
  690. package/vendor/ink/node_modules/@types/node/https.d.ts +405 -0
  691. package/vendor/ink/node_modules/@types/node/index.d.ts +115 -0
  692. package/vendor/ink/node_modules/@types/node/inspector/promises.d.ts +41 -0
  693. package/vendor/ink/node_modules/@types/node/inspector.d.ts +224 -0
  694. package/vendor/ink/node_modules/@types/node/inspector.generated.d.ts +4226 -0
  695. package/vendor/ink/node_modules/@types/node/module.d.ts +819 -0
  696. package/vendor/ink/node_modules/@types/node/net.d.ts +933 -0
  697. package/vendor/ink/node_modules/@types/node/os.d.ts +507 -0
  698. package/vendor/ink/node_modules/@types/node/package.json +155 -0
  699. package/vendor/ink/node_modules/@types/node/path/posix.d.ts +8 -0
  700. package/vendor/ink/node_modules/@types/node/path/win32.d.ts +8 -0
  701. package/vendor/ink/node_modules/@types/node/path.d.ts +187 -0
  702. package/vendor/ink/node_modules/@types/node/perf_hooks.d.ts +643 -0
  703. package/vendor/ink/node_modules/@types/node/process.d.ts +2156 -0
  704. package/vendor/ink/node_modules/@types/node/punycode.d.ts +117 -0
  705. package/vendor/ink/node_modules/@types/node/querystring.d.ts +152 -0
  706. package/vendor/ink/node_modules/@types/node/quic.d.ts +910 -0
  707. package/vendor/ink/node_modules/@types/node/readline/promises.d.ts +161 -0
  708. package/vendor/ink/node_modules/@types/node/readline.d.ts +541 -0
  709. package/vendor/ink/node_modules/@types/node/repl.d.ts +415 -0
  710. package/vendor/ink/node_modules/@types/node/sea.d.ts +162 -0
  711. package/vendor/ink/node_modules/@types/node/sqlite.d.ts +955 -0
  712. package/vendor/ink/node_modules/@types/node/stream/consumers.d.ts +38 -0
  713. package/vendor/ink/node_modules/@types/node/stream/promises.d.ts +211 -0
  714. package/vendor/ink/node_modules/@types/node/stream/web.d.ts +296 -0
  715. package/vendor/ink/node_modules/@types/node/stream.d.ts +1760 -0
  716. package/vendor/ink/node_modules/@types/node/string_decoder.d.ts +67 -0
  717. package/vendor/ink/node_modules/@types/node/test/reporters.d.ts +96 -0
  718. package/vendor/ink/node_modules/@types/node/test.d.ts +2240 -0
  719. package/vendor/ink/node_modules/@types/node/timers/promises.d.ts +108 -0
  720. package/vendor/ink/node_modules/@types/node/timers.d.ts +159 -0
  721. package/vendor/ink/node_modules/@types/node/tls.d.ts +1198 -0
  722. package/vendor/ink/node_modules/@types/node/trace_events.d.ts +197 -0
  723. package/vendor/ink/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +462 -0
  724. package/vendor/ink/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +71 -0
  725. package/vendor/ink/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +36 -0
  726. package/vendor/ink/node_modules/@types/node/ts5.6/index.d.ts +117 -0
  727. package/vendor/ink/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +72 -0
  728. package/vendor/ink/node_modules/@types/node/ts5.7/index.d.ts +117 -0
  729. package/vendor/ink/node_modules/@types/node/tty.d.ts +250 -0
  730. package/vendor/ink/node_modules/@types/node/url.d.ts +519 -0
  731. package/vendor/ink/node_modules/@types/node/util/types.d.ts +558 -0
  732. package/vendor/ink/node_modules/@types/node/util.d.ts +1662 -0
  733. package/vendor/ink/node_modules/@types/node/v8.d.ts +983 -0
  734. package/vendor/ink/node_modules/@types/node/vm.d.ts +1208 -0
  735. package/vendor/ink/node_modules/@types/node/wasi.d.ts +202 -0
  736. package/vendor/ink/node_modules/@types/node/web-globals/abortcontroller.d.ts +59 -0
  737. package/vendor/ink/node_modules/@types/node/web-globals/blob.d.ts +23 -0
  738. package/vendor/ink/node_modules/@types/node/web-globals/console.d.ts +9 -0
  739. package/vendor/ink/node_modules/@types/node/web-globals/crypto.d.ts +39 -0
  740. package/vendor/ink/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
  741. package/vendor/ink/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
  742. package/vendor/ink/node_modules/@types/node/web-globals/events.d.ts +106 -0
  743. package/vendor/ink/node_modules/@types/node/web-globals/fetch.d.ts +69 -0
  744. package/vendor/ink/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
  745. package/vendor/ink/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
  746. package/vendor/ink/node_modules/@types/node/web-globals/navigator.d.ts +25 -0
  747. package/vendor/ink/node_modules/@types/node/web-globals/performance.d.ts +45 -0
  748. package/vendor/ink/node_modules/@types/node/web-globals/storage.d.ts +24 -0
  749. package/vendor/ink/node_modules/@types/node/web-globals/streams.d.ts +115 -0
  750. package/vendor/ink/node_modules/@types/node/web-globals/timers.d.ts +44 -0
  751. package/vendor/ink/node_modules/@types/node/web-globals/url.d.ts +24 -0
  752. package/vendor/ink/node_modules/@types/node/worker_threads.d.ts +717 -0
  753. package/vendor/ink/node_modules/@types/node/zlib.d.ts +618 -0
  754. package/vendor/ink/node_modules/node-pty/LICENSE +69 -0
  755. package/vendor/ink/node_modules/node-pty/README.md +164 -0
  756. package/vendor/ink/node_modules/node-pty/binding.gyp +150 -0
  757. package/vendor/ink/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  758. package/vendor/ink/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  759. package/vendor/ink/node_modules/node-pty/lib/index.js +52 -0
  760. package/vendor/ink/node_modules/node-pty/lib/interfaces.js +7 -0
  761. package/vendor/ink/node_modules/node-pty/lib/shared/conout.js +11 -0
  762. package/vendor/ink/node_modules/node-pty/lib/terminal.js +190 -0
  763. package/vendor/ink/node_modules/node-pty/lib/types.js +7 -0
  764. package/vendor/ink/node_modules/node-pty/lib/unixTerminal.js +349 -0
  765. package/vendor/ink/node_modules/node-pty/lib/utils.js +39 -0
  766. package/vendor/ink/node_modules/node-pty/lib/windowsConoutConnection.js +125 -0
  767. package/vendor/ink/node_modules/node-pty/lib/windowsPtyAgent.js +287 -0
  768. package/vendor/ink/node_modules/node-pty/lib/windowsTerminal.js +201 -0
  769. package/vendor/ink/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  770. package/vendor/ink/node_modules/node-pty/package.json +65 -0
  771. package/vendor/ink/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  772. package/vendor/ink/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  773. package/vendor/ink/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  774. package/vendor/ink/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  775. package/vendor/ink/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  776. package/vendor/ink/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  777. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  778. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  779. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  780. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty.pdb +0 -0
  781. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  782. package/vendor/ink/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.pdb +0 -0
  783. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  784. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  785. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  786. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty.pdb +0 -0
  787. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  788. package/vendor/ink/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.pdb +0 -0
  789. package/vendor/ink/node_modules/node-pty/scripts/post-install.js +76 -0
  790. package/vendor/ink/node_modules/node-pty/scripts/prebuild.js +34 -0
  791. package/vendor/ink/node_modules/node-pty/src/unix/pty.cc +875 -0
  792. package/vendor/ink/node_modules/node-pty/src/unix/spawn-helper.cc +23 -0
  793. package/vendor/ink/node_modules/node-pty/src/win/conpty.cc +582 -0
  794. package/vendor/ink/node_modules/node-pty/src/win/conpty.h +41 -0
  795. package/vendor/ink/node_modules/node-pty/src/win/conpty_console_list.cc +44 -0
  796. package/vendor/ink/node_modules/node-pty/src/win/path_util.cc +95 -0
  797. package/vendor/ink/node_modules/node-pty/src/win/path_util.h +26 -0
  798. package/vendor/ink/node_modules/node-pty/third_party/conpty/1.23.251008001/win10-arm64/OpenConsole.exe +0 -0
  799. package/vendor/ink/node_modules/node-pty/third_party/conpty/1.23.251008001/win10-arm64/conpty.dll +0 -0
  800. package/vendor/ink/node_modules/node-pty/third_party/conpty/1.23.251008001/win10-x64/OpenConsole.exe +0 -0
  801. package/vendor/ink/node_modules/node-pty/third_party/conpty/1.23.251008001/win10-x64/conpty.dll +0 -0
  802. package/vendor/ink/node_modules/node-pty/typings/node-pty.d.ts +215 -0
  803. package/vendor/ink/node_modules/undici-types/LICENSE +21 -0
  804. package/vendor/ink/node_modules/undici-types/README.md +6 -0
  805. package/vendor/ink/node_modules/undici-types/agent.d.ts +32 -0
  806. package/vendor/ink/node_modules/undici-types/api.d.ts +43 -0
  807. package/vendor/ink/node_modules/undici-types/balanced-pool.d.ts +30 -0
  808. package/vendor/ink/node_modules/undici-types/cache-interceptor.d.ts +173 -0
  809. package/vendor/ink/node_modules/undici-types/cache.d.ts +36 -0
  810. package/vendor/ink/node_modules/undici-types/client-stats.d.ts +15 -0
  811. package/vendor/ink/node_modules/undici-types/client.d.ts +108 -0
  812. package/vendor/ink/node_modules/undici-types/connector.d.ts +34 -0
  813. package/vendor/ink/node_modules/undici-types/content-type.d.ts +21 -0
  814. package/vendor/ink/node_modules/undici-types/cookies.d.ts +30 -0
  815. package/vendor/ink/node_modules/undici-types/diagnostics-channel.d.ts +74 -0
  816. package/vendor/ink/node_modules/undici-types/dispatcher.d.ts +276 -0
  817. package/vendor/ink/node_modules/undici-types/env-http-proxy-agent.d.ts +22 -0
  818. package/vendor/ink/node_modules/undici-types/errors.d.ts +161 -0
  819. package/vendor/ink/node_modules/undici-types/eventsource.d.ts +66 -0
  820. package/vendor/ink/node_modules/undici-types/fetch.d.ts +211 -0
  821. package/vendor/ink/node_modules/undici-types/formdata.d.ts +108 -0
  822. package/vendor/ink/node_modules/undici-types/global-dispatcher.d.ts +9 -0
  823. package/vendor/ink/node_modules/undici-types/global-origin.d.ts +7 -0
  824. package/vendor/ink/node_modules/undici-types/h2c-client.d.ts +73 -0
  825. package/vendor/ink/node_modules/undici-types/handlers.d.ts +15 -0
  826. package/vendor/ink/node_modules/undici-types/header.d.ts +160 -0
  827. package/vendor/ink/node_modules/undici-types/index.d.ts +88 -0
  828. package/vendor/ink/node_modules/undici-types/interceptors.d.ts +73 -0
  829. package/vendor/ink/node_modules/undici-types/mock-agent.d.ts +68 -0
  830. package/vendor/ink/node_modules/undici-types/mock-call-history.d.ts +111 -0
  831. package/vendor/ink/node_modules/undici-types/mock-client.d.ts +27 -0
  832. package/vendor/ink/node_modules/undici-types/mock-errors.d.ts +12 -0
  833. package/vendor/ink/node_modules/undici-types/mock-interceptor.d.ts +94 -0
  834. package/vendor/ink/node_modules/undici-types/mock-pool.d.ts +27 -0
  835. package/vendor/ink/node_modules/undici-types/package.json +55 -0
  836. package/vendor/ink/node_modules/undici-types/patch.d.ts +29 -0
  837. package/vendor/ink/node_modules/undici-types/pool-stats.d.ts +19 -0
  838. package/vendor/ink/node_modules/undici-types/pool.d.ts +41 -0
  839. package/vendor/ink/node_modules/undici-types/proxy-agent.d.ts +29 -0
  840. package/vendor/ink/node_modules/undici-types/readable.d.ts +68 -0
  841. package/vendor/ink/node_modules/undici-types/retry-agent.d.ts +8 -0
  842. package/vendor/ink/node_modules/undici-types/retry-handler.d.ts +125 -0
  843. package/vendor/ink/node_modules/undici-types/round-robin-pool.d.ts +41 -0
  844. package/vendor/ink/node_modules/undici-types/snapshot-agent.d.ts +109 -0
  845. package/vendor/ink/node_modules/undici-types/util.d.ts +18 -0
  846. package/vendor/ink/node_modules/undici-types/utility.d.ts +7 -0
  847. package/vendor/ink/node_modules/undici-types/webidl.d.ts +341 -0
  848. package/vendor/ink/node_modules/undici-types/websocket.d.ts +186 -0
  849. package/vendor/ink/package.json +201 -0
  850. package/vendor/ink/readme.md +2636 -0
  851. package/bin/swag-agent.js +0 -9
  852. package/dist/server/lib/pg-rate-limiter.d.ts +0 -21
  853. package/dist/server/lib/pg-rate-limiter.js +0 -86
@@ -1,990 +1,1398 @@
1
1
  // handlers/nodes.ts — Node & Channel management endpoints
2
2
  // Auth: User JWT for management, Node API key for node operations
3
+
3
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
5
  import { checkPlanLimits, incrementUsage } from "./billing.js";
5
6
  import { queueSpan, auditRowToSpan } from "../lib/clickhouse-buffer.js";
7
+
8
+ // ============================================================================
9
+ // TYPES
10
+ // ============================================================================
11
+
12
+ /** Context returned by resolveConversation — carries customer identity + session state */
13
+
14
+ /** Sender context passed through to the agent invoker for customer-aware prompts */
15
+
16
+ // Callback type for agent invocation (injected from index.ts to avoid circular deps)
17
+
6
18
  let agentInvoker = null;
19
+
7
20
  /** Set the agent invoker — called once from index.ts to break circular dependency */
8
21
  export function setNodeAgentInvoker(invoker) {
9
- agentInvoker = invoker;
22
+ agentInvoker = invoker;
10
23
  }
24
+
11
25
  // ============================================================================
12
26
  // HELPERS
13
27
  // ============================================================================
28
+
14
29
  function hashApiKey(key) {
15
- return createHash("sha256").update(key).digest("hex");
30
+ return createHash("sha256").update(key).digest("hex");
16
31
  }
17
32
  function generateNodeApiKey() {
18
- return randomBytes(32).toString("hex");
33
+ return randomBytes(32).toString("hex");
19
34
  }
35
+
20
36
  // 5-minute in-memory cache for node auth (survives transient Supabase 525s)
21
37
  const NODE_AUTH_CACHE_TTL = 5 * 60 * 1000;
22
38
  const nodeAuthCache = new Map();
39
+
23
40
  /** Clear the node auth cache (for tests). */
24
41
  export function clearNodeAuthCache() {
25
- nodeAuthCache.clear();
42
+ nodeAuthCache.clear();
26
43
  }
44
+
27
45
  /** Authenticate a node by API key, returns node row or null.
28
46
  * Retries once on transient error, falls back to cache if both fail. */
29
47
  async function authenticateNode(supabase, apiKey) {
30
- const hash = hashApiKey(apiKey);
31
- // Attempt DB query with one retry on transient errors
32
- for (let attempt = 0; attempt < 2; attempt++) {
33
- try {
34
- const { data, error } = await supabase
35
- .from("nodes")
36
- .select("id, store_id, name")
37
- .eq("api_key_hash", hash)
38
- .single();
39
- if (data) {
40
- // Success — update cache and return
41
- nodeAuthCache.set(hash, { node: data, cachedAt: Date.now() });
42
- return data;
43
- }
44
- // Non-transient error (e.g. row not found) — no retry
45
- if (error && !isTransientError(error)) {
46
- return null;
47
- }
48
- // Transient error on first attempt — retry
49
- if (attempt === 0 && error) {
50
- console.warn(`[node-auth] Transient error (attempt 1): ${error.message}. Retrying...`);
51
- continue;
52
- }
53
- return null;
54
- }
55
- catch (err) {
56
- // Network-level error (fetch failure, timeout)
57
- if (attempt === 0) {
58
- console.warn(`[node-auth] Network error (attempt 1): ${err.message}. Retrying...`);
59
- continue;
60
- }
61
- }
62
- }
63
- // Both attempts failed — fall back to cache
64
- const cached = nodeAuthCache.get(hash);
65
- if (cached && Date.now() - cached.cachedAt < NODE_AUTH_CACHE_TTL) {
66
- console.warn(`[node-auth] Using cached auth for node ${cached.node.id} (DB unavailable)`);
67
- return cached.node;
48
+ const hash = hashApiKey(apiKey);
49
+
50
+ // Attempt DB query with one retry on transient errors
51
+ for (let attempt = 0; attempt < 2; attempt++) {
52
+ try {
53
+ const {
54
+ data,
55
+ error
56
+ } = await supabase.from("nodes").select("id, store_id, name").eq("api_key_hash", hash).single();
57
+ if (data) {
58
+ // Success — update cache and return
59
+ nodeAuthCache.set(hash, {
60
+ node: data,
61
+ cachedAt: Date.now()
62
+ });
63
+ return data;
64
+ }
65
+
66
+ // Non-transient error (e.g. row not found) no retry
67
+ if (error && !isTransientError(error)) {
68
+ return null;
69
+ }
70
+
71
+ // Transient error on first attempt — retry
72
+ if (attempt === 0 && error) {
73
+ console.warn(`[node-auth] Transient error (attempt 1): ${error.message}. Retrying...`);
74
+ continue;
75
+ }
76
+ return null;
77
+ } catch (err) {
78
+ // Network-level error (fetch failure, timeout)
79
+ if (attempt === 0) {
80
+ console.warn(`[node-auth] Network error (attempt 1): ${err.message}. Retrying...`);
81
+ continue;
82
+ }
68
83
  }
69
- return null;
84
+ }
85
+
86
+ // Both attempts failed — fall back to cache
87
+ const cached = nodeAuthCache.get(hash);
88
+ if (cached && Date.now() - cached.cachedAt < NODE_AUTH_CACHE_TTL) {
89
+ console.warn(`[node-auth] Using cached auth for node ${cached.node.id} (DB unavailable)`);
90
+ return cached.node;
91
+ }
92
+ return null;
70
93
  }
94
+
71
95
  /** Check if a user (by auth UUID) has access to a store.
72
96
  * Mirrors the logic in get_user_store_ids():
73
97
  * 1) store_members.auth_user_id = authUserId
74
98
  * 2) stores.owner_user_id = platform_users.id WHERE auth_id = authUserId */
75
99
  async function userHasStoreAccess(supabase, authUserId, storeId) {
76
- // Check store_members first (direct membership)
77
- const { data: membership } = await supabase
78
- .from("store_members")
79
- .select("id")
80
- .eq("auth_user_id", authUserId)
81
- .eq("store_id", storeId)
82
- .limit(1);
83
- if (membership?.length)
84
- return true;
85
- // Check via platform_users → stores.owner_user_id
86
- const { data: platformUser } = await supabase
87
- .from("platform_users")
88
- .select("id")
89
- .eq("auth_id", authUserId)
90
- .limit(1)
91
- .single();
92
- if (platformUser) {
93
- const { data: ownedStore } = await supabase
94
- .from("stores")
95
- .select("id")
96
- .eq("owner_user_id", platformUser.id)
97
- .eq("id", storeId)
98
- .limit(1);
99
- if (ownedStore?.length)
100
- return true;
101
- }
102
- return false;
100
+ // Check store_members first (direct membership)
101
+ const {
102
+ data: membership
103
+ } = await supabase.from("store_members").select("id").eq("auth_user_id", authUserId).eq("store_id", storeId).limit(1);
104
+ if (membership?.length) return true;
105
+
106
+ // Check via platform_users → stores.owner_user_id
107
+ const {
108
+ data: platformUser
109
+ } = await supabase.from("platform_users").select("id").eq("auth_id", authUserId).limit(1).single();
110
+ if (platformUser) {
111
+ const {
112
+ data: ownedStore
113
+ } = await supabase.from("stores").select("id").eq("owner_user_id", platformUser.id).eq("id", storeId).limit(1);
114
+ if (ownedStore?.length) return true;
115
+ }
116
+ return false;
103
117
  }
118
+
104
119
  /** Check if a Supabase error is transient (network, 5xx, Cloudflare) */
105
120
  function isTransientError(error) {
106
- const msg = error.message || "";
107
- return msg.includes("525") || msg.includes("502") || msg.includes("503")
108
- || msg.includes("504") || msg.includes("520") || msg.includes("522")
109
- || msg.includes("524") || msg.includes("timeout") || msg.includes("ECONNRESET")
110
- || msg.includes("fetch failed");
121
+ const msg = error.message || "";
122
+ return msg.includes("525") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("520") || msg.includes("522") || msg.includes("524") || msg.includes("timeout") || msg.includes("ECONNRESET") || msg.includes("fetch failed");
111
123
  }
124
+
112
125
  /** Log a node event to node_events */
113
126
  async function logNodeEvent(supabase, storeId, nodeId, eventType, details = {}) {
114
- // node_events — existing table for node lifecycle
115
- await supabase.from("node_events").insert({
116
- store_id: storeId,
127
+ // node_events — existing table for node lifecycle
128
+ await supabase.from("node_events").insert({
129
+ store_id: storeId,
130
+ node_id: nodeId,
131
+ event_type: eventType,
132
+ details
133
+ });
134
+
135
+ // Telemetry → ClickHouse
136
+ try {
137
+ const now = new Date();
138
+ queueSpan(auditRowToSpan({
139
+ action: `node.${eventType}`,
140
+ severity: "info",
141
+ store_id: storeId,
142
+ resource_type: "whale_node",
143
+ resource_id: nodeId,
144
+ source: "whale-node",
145
+ user_id: details.registered_by || details.deleted_by || null,
146
+ trace_id: randomUUID(),
147
+ span_kind: "INTERNAL",
148
+ service_name: "whale-node",
149
+ status_code: "OK",
150
+ start_time: now.toISOString(),
151
+ end_time: now.toISOString(),
152
+ details: {
153
+ ...details,
117
154
  node_id: nodeId,
118
- event_type: eventType,
119
- details,
120
- });
121
- // Telemetry → ClickHouse
122
- try {
123
- const now = new Date();
124
- queueSpan(auditRowToSpan({
125
- action: `node.${eventType}`,
126
- severity: "info",
127
- store_id: storeId,
128
- resource_type: "whale_node",
129
- resource_id: nodeId,
130
- source: "whale-node",
131
- user_id: (details.registered_by || details.deleted_by || null),
132
- trace_id: randomUUID(),
133
- span_kind: "INTERNAL",
134
- service_name: "whale-node",
135
- status_code: "OK",
136
- start_time: now.toISOString(),
137
- end_time: now.toISOString(),
138
- details: { ...details, node_id: nodeId, event_type: eventType },
139
- }));
140
- }
141
- catch {
142
- // Telemetry must never break node operations
143
- }
155
+ event_type: eventType
156
+ }
157
+ }));
158
+ } catch {
159
+ // Telemetry must never break node operations
160
+ }
144
161
  }
162
+
145
163
  /** Resolve or create a conversation for a sender on a channel.
146
164
  * Reuses conversation if last message from same sender was < 30 min ago.
147
165
  * Creates an ai_conversations row for new sessions so telemetry/history works.
148
166
  * Resolves customer identity from the dynamic bridge table. */
149
167
  async function resolveConversation(supabase, channelId, senderId, storeId, channelType, senderName) {
150
- // ── Resolve customer identity (best-effort, any channel type) ──
151
- let customerId = null;
152
- let customerName = null;
153
- if (channelType) {
154
- try {
155
- const { data } = await supabase.rpc("resolve_sender_to_customer", {
156
- p_store_id: storeId,
157
- p_channel_type: channelType,
158
- p_sender_id: senderId,
159
- p_sender_name: senderName || null,
160
- });
161
- if (data?.length) {
162
- customerId = data[0].customer_id;
163
- customerName = data[0].customer_name;
164
- }
165
- }
166
- catch { /* customer resolution non-critical */ }
167
- }
168
- // ── 30-min window — reuse existing conversation ──
169
- const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
170
- const { data: recent } = await supabase
171
- .from("channel_messages")
172
- .select("conversation_id")
173
- .eq("channel_id", channelId)
174
- .eq("sender_id", senderId)
175
- .gt("created_at", thirtyMinAgo)
176
- .not("conversation_id", "is", null)
177
- .order("created_at", { ascending: false })
178
- .limit(1);
179
- if (recent?.length && recent[0].conversation_id) {
180
- // Backfill customer_id on existing conversation if newly resolved
181
- if (customerId) {
182
- Promise.resolve(supabase.from("ai_conversations")
183
- .update({ customer_id: customerId })
184
- .eq("id", recent[0].conversation_id)
185
- .is("customer_id", null)).catch(() => { });
186
- }
187
- return { conversationId: recent[0].conversation_id, customerId, customerName, isNewSession: false };
188
- }
189
- // ── New session — create ai_conversations row ──
190
- const newId = randomUUID();
191
- const title = senderName
192
- ? `${senderName} via ${channelType || "channel"}`
193
- : `${senderId} via ${channelType || "channel"}`;
168
+ // ── Resolve customer identity (best-effort, any channel type) ──
169
+ let customerId = null;
170
+ let customerName = null;
171
+ if (channelType) {
194
172
  try {
195
- await supabase.from("ai_conversations").insert({
196
- id: newId,
197
- store_id: storeId,
198
- agent_id: null, // set later by invokeAgentForChannel
199
- channel_id: channelId,
200
- sender_id: senderId,
201
- channel_type: channelType || null,
202
- customer_id: customerId,
203
- title,
204
- metadata: { source: "channel", channel_type: channelType || "unknown" },
205
- });
173
+ const {
174
+ data
175
+ } = await supabase.rpc("resolve_sender_to_customer", {
176
+ p_store_id: storeId,
177
+ p_channel_type: channelType,
178
+ p_sender_id: senderId,
179
+ p_sender_name: senderName || null
180
+ });
181
+ if (data?.length) {
182
+ customerId = data[0].customer_id;
183
+ customerName = data[0].customer_name;
184
+ }
185
+ } catch {/* customer resolution non-critical */}
186
+ }
187
+
188
+ // ── 30-min window — reuse existing conversation ──
189
+ const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
190
+ const {
191
+ data: recent
192
+ } = await supabase.from("channel_messages").select("conversation_id").eq("channel_id", channelId).eq("sender_id", senderId).gt("created_at", thirtyMinAgo).not("conversation_id", "is", null).order("created_at", {
193
+ ascending: false
194
+ }).limit(1);
195
+ if (recent?.length && recent[0].conversation_id) {
196
+ // Backfill customer_id on existing conversation if newly resolved
197
+ if (customerId) {
198
+ Promise.resolve(supabase.from("ai_conversations").update({
199
+ customer_id: customerId
200
+ }).eq("id", recent[0].conversation_id).is("customer_id", null)).catch(() => {});
206
201
  }
207
- catch { /* ai_conversations insert non-critical — agent still works without it */ }
208
- return { conversationId: newId, customerId, customerName, isNewSession: true };
202
+ return {
203
+ conversationId: recent[0].conversation_id,
204
+ customerId,
205
+ customerName,
206
+ isNewSession: false
207
+ };
208
+ }
209
+
210
+ // ── New session — create ai_conversations row ──
211
+ const newId = randomUUID();
212
+ const title = senderName ? `${senderName} via ${channelType || "channel"}` : `${senderId} via ${channelType || "channel"}`;
213
+ try {
214
+ await supabase.from("ai_conversations").insert({
215
+ id: newId,
216
+ store_id: storeId,
217
+ agent_id: null,
218
+ // set later by invokeAgentForChannel
219
+ channel_id: channelId,
220
+ sender_id: senderId,
221
+ channel_type: channelType || null,
222
+ customer_id: customerId,
223
+ title,
224
+ metadata: {
225
+ source: "channel",
226
+ channel_type: channelType || "unknown"
227
+ }
228
+ });
229
+ } catch {/* ai_conversations insert non-critical — agent still works without it */}
230
+ return {
231
+ conversationId: newId,
232
+ customerId,
233
+ customerName,
234
+ isNewSession: true
235
+ };
209
236
  }
237
+
210
238
  // ============================================================================
211
239
  // ROUTE HANDLER
212
240
  // ============================================================================
241
+
213
242
  export async function handleNodeRoutes(pathname, method, body, supabase, auth, queryParams) {
214
- // ── POST /nodes/register ──────────────────────────────────────
215
- // User auth required. Creates a node and returns API key (shown once).
216
- if (pathname === "/nodes/register" && method === "POST") {
217
- if (!auth.userId && !auth.isServiceRole) {
218
- return { status: 401, body: { error: "User authentication required" } };
219
- }
220
- if (!body)
221
- return { status: 400, body: { error: "Request body required" } };
222
- const reg = body;
223
- if (!reg.name || !reg.store_id) {
224
- return { status: 400, body: { error: "name and store_id required" } };
225
- }
226
- // Verify user has access to this store
227
- if (auth.userId) {
228
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, reg.store_id);
229
- if (!hasAccess) {
230
- return { status: 403, body: { error: "No access to this store" } };
231
- }
232
- }
233
- // Check node limit against store plan
234
- const { data: planRow } = await supabase
235
- .from("store_plans")
236
- .select("plan, limits")
237
- .eq("store_id", reg.store_id)
238
- .single();
239
- const planLimits = planRow?.limits;
240
- const nodesMax = planLimits?.nodes_max ?? 1; // free plan default
241
- const { count } = await supabase
242
- .from("nodes")
243
- .select("id", { count: "exact", head: true })
244
- .eq("store_id", reg.store_id);
245
- if ((count || 0) >= nodesMax) {
246
- return { status: 429, body: { error: `Node limit reached (${count}/${nodesMax} on ${planRow?.plan || "free"} plan)` } };
247
- }
248
- const apiKey = generateNodeApiKey();
249
- const apiKeyHash = hashApiKey(apiKey);
250
- const { data: node, error } = await supabase
251
- .from("nodes")
252
- .insert({
253
- store_id: reg.store_id,
254
- name: reg.name,
255
- api_key_hash: apiKeyHash,
256
- capabilities: reg.capabilities || [],
257
- hardware: reg.hardware || {},
258
- version: reg.version || "1.0.0",
259
- config: reg.config || {},
260
- status: "offline",
261
- })
262
- .select("id, name, store_id, status, created_at")
263
- .single();
264
- if (error) {
265
- return { status: 500, body: { error: error.message } };
266
- }
267
- await logNodeEvent(supabase, reg.store_id, node.id, "registered", {
268
- name: reg.name,
269
- registered_by: auth.userId,
270
- });
243
+ // ── POST /nodes/register ──────────────────────────────────────
244
+ // User auth required. Creates a node and returns API key (shown once).
245
+ if (pathname === "/nodes/register" && method === "POST") {
246
+ if (!auth.userId && !auth.isServiceRole) {
247
+ return {
248
+ status: 401,
249
+ body: {
250
+ error: "User authentication required"
251
+ }
252
+ };
253
+ }
254
+ if (!body) return {
255
+ status: 400,
256
+ body: {
257
+ error: "Request body required"
258
+ }
259
+ };
260
+ const reg = body;
261
+ if (!reg.name || !reg.store_id) {
262
+ return {
263
+ status: 400,
264
+ body: {
265
+ error: "name and store_id required"
266
+ }
267
+ };
268
+ }
269
+
270
+ // Verify user has access to this store
271
+ if (auth.userId) {
272
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, reg.store_id);
273
+ if (!hasAccess) {
271
274
  return {
272
- status: 201,
273
- body: {
274
- success: true,
275
- node,
276
- api_key: apiKey, // Shown ONCE — user must save this
277
- message: "Save your API key — it cannot be retrieved later.",
278
- },
275
+ status: 403,
276
+ body: {
277
+ error: "No access to this store"
278
+ }
279
279
  };
280
+ }
281
+ }
282
+
283
+ // Check node limit against store plan
284
+ const {
285
+ data: planRow
286
+ } = await supabase.from("store_plans").select("plan, limits").eq("store_id", reg.store_id).single();
287
+ const planLimits = planRow?.limits;
288
+ const nodesMax = planLimits?.nodes_max ?? 1; // free plan default
289
+
290
+ const {
291
+ count
292
+ } = await supabase.from("nodes").select("id", {
293
+ count: "exact",
294
+ head: true
295
+ }).eq("store_id", reg.store_id);
296
+ if ((count || 0) >= nodesMax) {
297
+ return {
298
+ status: 429,
299
+ body: {
300
+ error: `Node limit reached (${count}/${nodesMax} on ${planRow?.plan || "free"} plan)`
301
+ }
302
+ };
303
+ }
304
+ const apiKey = generateNodeApiKey();
305
+ const apiKeyHash = hashApiKey(apiKey);
306
+ const {
307
+ data: node,
308
+ error
309
+ } = await supabase.from("nodes").insert({
310
+ store_id: reg.store_id,
311
+ name: reg.name,
312
+ api_key_hash: apiKeyHash,
313
+ capabilities: reg.capabilities || [],
314
+ hardware: reg.hardware || {},
315
+ version: reg.version || "1.0.0",
316
+ config: reg.config || {},
317
+ status: "offline"
318
+ }).select("id, name, store_id, status, created_at").single();
319
+ if (error) {
320
+ return {
321
+ status: 500,
322
+ body: {
323
+ error: error.message
324
+ }
325
+ };
326
+ }
327
+ await logNodeEvent(supabase, reg.store_id, node.id, "registered", {
328
+ name: reg.name,
329
+ registered_by: auth.userId
330
+ });
331
+ return {
332
+ status: 201,
333
+ body: {
334
+ success: true,
335
+ node,
336
+ api_key: apiKey,
337
+ // Shown ONCE — user must save this
338
+ message: "Save your API key — it cannot be retrieved later."
339
+ }
340
+ };
341
+ }
342
+
343
+ // ── POST /nodes/heartbeat ─────────────────────────────────────
344
+ // Node API key auth. Updates status + hardware info.
345
+ if (pathname === "/nodes/heartbeat" && method === "POST") {
346
+ const node = await authenticateNode(supabase, auth.rawToken);
347
+ if (!node) {
348
+ return {
349
+ status: 401,
350
+ body: {
351
+ error: "Invalid node API key"
352
+ }
353
+ };
354
+ }
355
+ const hb = body;
356
+ const updates = {
357
+ status: "online",
358
+ last_heartbeat: new Date().toISOString(),
359
+ updated_at: new Date().toISOString()
360
+ };
361
+ if (hb.hardware) updates.hardware = hb.hardware;
362
+ if (hb.capabilities) updates.capabilities = hb.capabilities;
363
+ if (hb.version) updates.version = hb.version;
364
+ await supabase.from("nodes").update(updates).eq("id", node.id);
365
+
366
+ // Sync channel statuses if reported
367
+ if (hb.channels?.length) {
368
+ for (const ch of hb.channels) {
369
+ await supabase.from("channels").update({
370
+ status: ch.status,
371
+ updated_at: new Date().toISOString()
372
+ }).eq("node_id", node.id).eq("type", ch.type);
373
+ }
280
374
  }
281
- // ── POST /nodes/heartbeat ─────────────────────────────────────
282
- // Node API key auth. Updates status + hardware info.
283
- if (pathname === "/nodes/heartbeat" && method === "POST") {
284
- const node = await authenticateNode(supabase, auth.rawToken);
285
- if (!node) {
286
- return { status: 401, body: { error: "Invalid node API key" } };
287
- }
288
- const hb = body;
289
- const updates = {
290
- status: "online",
291
- last_heartbeat: new Date().toISOString(),
292
- updated_at: new Date().toISOString(),
375
+
376
+ // Fetch pending commands for this node
377
+ let pendingCommands = [];
378
+ try {
379
+ const {
380
+ data: cmds
381
+ } = await supabase.from("node_commands").select("id, command, payload").eq("node_id", node.id).eq("status", "pending").order("created_at", {
382
+ ascending: true
383
+ }).limit(10);
384
+ if (cmds?.length) {
385
+ pendingCommands = cmds;
386
+ // Mark them as acknowledged
387
+ const cmdIds = cmds.map(c => c.id);
388
+ await supabase.from("node_commands").update({
389
+ status: "acknowledged",
390
+ acknowledged_at: new Date().toISOString()
391
+ }).in("id", cmdIds);
392
+ }
393
+ } catch {
394
+ // Command delivery is best-effort
395
+ }
396
+ return {
397
+ status: 200,
398
+ body: {
399
+ success: true,
400
+ node_id: node.id,
401
+ commands: pendingCommands
402
+ }
403
+ };
404
+ }
405
+
406
+ // ── GET|POST /nodes ────────────────────────────────────────────
407
+ // User auth. Lists nodes for a store.
408
+ if (pathname === "/nodes" && (method === "GET" || method === "POST")) {
409
+ // store_id from body or query params
410
+ const storeId = body?.store_id || queryParams?.get("store_id");
411
+ if (!storeId) {
412
+ return {
413
+ status: 400,
414
+ body: {
415
+ error: "store_id required"
416
+ }
417
+ };
418
+ }
419
+
420
+ // Verify user has access to the requested store
421
+ if (auth.userId) {
422
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, storeId);
423
+ if (!hasAccess) {
424
+ return {
425
+ status: 403,
426
+ body: {
427
+ error: "Not authorized to access this store's nodes"
428
+ }
293
429
  };
294
- if (hb.hardware)
295
- updates.hardware = hb.hardware;
296
- if (hb.capabilities)
297
- updates.capabilities = hb.capabilities;
298
- if (hb.version)
299
- updates.version = hb.version;
300
- await supabase.from("nodes").update(updates).eq("id", node.id);
301
- // Sync channel statuses if reported
302
- if (hb.channels?.length) {
303
- for (const ch of hb.channels) {
304
- await supabase
305
- .from("channels")
306
- .update({ status: ch.status, updated_at: new Date().toISOString() })
307
- .eq("node_id", node.id)
308
- .eq("type", ch.type);
309
- }
310
- }
311
- // Fetch pending commands for this node
312
- let pendingCommands = [];
313
- try {
314
- const { data: cmds } = await supabase
315
- .from("node_commands")
316
- .select("id, command, payload")
317
- .eq("node_id", node.id)
318
- .eq("status", "pending")
319
- .order("created_at", { ascending: true })
320
- .limit(10);
321
- if (cmds?.length) {
322
- pendingCommands = cmds;
323
- // Mark them as acknowledged
324
- const cmdIds = cmds.map((c) => c.id);
325
- await supabase
326
- .from("node_commands")
327
- .update({ status: "acknowledged", acknowledged_at: new Date().toISOString() })
328
- .in("id", cmdIds);
329
- }
330
- }
331
- catch {
332
- // Command delivery is best-effort
430
+ }
431
+ } else if (!auth.isServiceRole) {
432
+ return {
433
+ status: 401,
434
+ body: {
435
+ error: "Authentication required"
436
+ }
437
+ };
438
+ }
439
+ const {
440
+ data: nodes,
441
+ error
442
+ } = await supabase.from("nodes").select("id, name, status, hardware, capabilities, version, ip_address, last_heartbeat, created_at").eq("store_id", storeId).order("created_at", {
443
+ ascending: false
444
+ });
445
+ if (error) {
446
+ return {
447
+ status: 500,
448
+ body: {
449
+ error: error.message
333
450
  }
451
+ };
452
+ }
453
+
454
+ // Also fetch channels per node
455
+ const nodeIds = (nodes || []).map(n => n.id);
456
+ const {
457
+ data: channels
458
+ } = nodeIds.length ? await supabase.from("channels").select("id, node_id, type, name, status, stats").in("node_id", nodeIds) : {
459
+ data: []
460
+ };
461
+
462
+ // Attach channels to nodes
463
+ const result = (nodes || []).map(n => ({
464
+ ...n,
465
+ channels: (channels || []).filter(c => c.node_id === n.id)
466
+ }));
467
+ return {
468
+ status: 200,
469
+ body: {
470
+ success: true,
471
+ nodes: result
472
+ }
473
+ };
474
+ }
475
+
476
+ // ── PATCH /nodes/:id ──────────────────────────────────────────
477
+ // User auth. Update node name, capabilities, config.
478
+ const patchNodeMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)$/);
479
+ if (patchNodeMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
480
+ const nodeId = patchNodeMatch[1];
481
+ if (!auth.userId && !auth.isServiceRole) {
482
+ return {
483
+ status: 401,
484
+ body: {
485
+ error: "User authentication required"
486
+ }
487
+ };
488
+ }
489
+
490
+ // Verify ownership
491
+ const {
492
+ data: nodeRow
493
+ } = await supabase.from("nodes").select("id, store_id, name").eq("id", nodeId).single();
494
+ if (!nodeRow) {
495
+ return {
496
+ status: 404,
497
+ body: {
498
+ error: "Node not found"
499
+ }
500
+ };
501
+ }
502
+ if (auth.userId) {
503
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, nodeRow.store_id);
504
+ if (!hasAccess) {
334
505
  return {
335
- status: 200,
336
- body: { success: true, node_id: node.id, commands: pendingCommands },
506
+ status: 403,
507
+ body: {
508
+ error: "No access to this node"
509
+ }
337
510
  };
511
+ }
338
512
  }
339
- // ── GET|POST /nodes ────────────────────────────────────────────
340
- // User auth. Lists nodes for a store.
341
- if (pathname === "/nodes" && (method === "GET" || method === "POST")) {
342
- // store_id from body or query params
343
- const storeId = body?.store_id || queryParams?.get("store_id");
344
- if (!storeId) {
345
- return { status: 400, body: { error: "store_id required" } };
346
- }
347
- // Verify user has access to the requested store
348
- if (auth.userId) {
349
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, storeId);
350
- if (!hasAccess) {
351
- return { status: 403, body: { error: "Not authorized to access this store's nodes" } };
352
- }
353
- }
354
- else if (!auth.isServiceRole) {
355
- return { status: 401, body: { error: "Authentication required" } };
356
- }
357
- const { data: nodes, error } = await supabase
358
- .from("nodes")
359
- .select("id, name, status, hardware, capabilities, version, ip_address, last_heartbeat, created_at")
360
- .eq("store_id", storeId)
361
- .order("created_at", { ascending: false });
362
- if (error) {
363
- return { status: 500, body: { error: error.message } };
364
- }
365
- // Also fetch channels per node
366
- const nodeIds = (nodes || []).map((n) => n.id);
367
- const { data: channels } = nodeIds.length
368
- ? await supabase
369
- .from("channels")
370
- .select("id, node_id, type, name, status, stats")
371
- .in("node_id", nodeIds)
372
- : { data: [] };
373
- // Attach channels to nodes
374
- const result = (nodes || []).map((n) => ({
375
- ...n,
376
- channels: (channels || []).filter((c) => c.node_id === n.id),
377
- }));
378
- return { status: 200, body: { success: true, nodes: result } };
379
- }
380
- // ── PATCH /nodes/:id ──────────────────────────────────────────
381
- // User auth. Update node name, capabilities, config.
382
- const patchNodeMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)$/);
383
- if (patchNodeMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
384
- const nodeId = patchNodeMatch[1];
385
- if (!auth.userId && !auth.isServiceRole) {
386
- return { status: 401, body: { error: "User authentication required" } };
387
- }
388
- // Verify ownership
389
- const { data: nodeRow } = await supabase
390
- .from("nodes")
391
- .select("id, store_id, name")
392
- .eq("id", nodeId)
393
- .single();
394
- if (!nodeRow) {
395
- return { status: 404, body: { error: "Node not found" } };
396
- }
397
- if (auth.userId) {
398
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, nodeRow.store_id);
399
- if (!hasAccess) {
400
- return { status: 403, body: { error: "No access to this node" } };
401
- }
402
- }
403
- const nodeUpdates = { updated_at: new Date().toISOString() };
404
- if (body.name !== undefined)
405
- nodeUpdates.name = body.name;
406
- if (body.capabilities !== undefined)
407
- nodeUpdates.capabilities = body.capabilities;
408
- if (body.config !== undefined)
409
- nodeUpdates.config = body.config;
410
- const { data: updated, error: updateErr } = await supabase
411
- .from("nodes")
412
- .update(nodeUpdates)
413
- .eq("id", nodeId)
414
- .select("id, name, status, capabilities, config, version, hardware, last_heartbeat, updated_at")
415
- .single();
416
- if (updateErr) {
417
- return { status: 500, body: { error: updateErr.message } };
418
- }
419
- return { status: 200, body: { success: true, node: updated } };
420
- }
421
- // ── POST /nodes/:id/commands ────────────────────────────────
422
- // User auth. Queue a command for a node.
423
- const commandMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/commands$/);
424
- if (commandMatch && method === "POST") {
425
- const nodeId = commandMatch[1];
426
- if (!auth.userId && !auth.isServiceRole) {
427
- return { status: 401, body: { error: "User authentication required" } };
428
- }
429
- if (!body)
430
- return { status: 400, body: { error: "Request body required" } };
431
- const command = body.command;
432
- const payload = body.payload || {};
433
- if (!command) {
434
- return { status: 400, body: { error: "command is required" } };
435
- }
436
- const validCommands = ["restart", "shutdown", "rotate_key", "pause_all_channels", "resume_all_channels", "update"];
437
- if (!validCommands.includes(command)) {
438
- return { status: 400, body: { error: `Invalid command. Valid: ${validCommands.join(", ")}` } };
439
- }
440
- // Verify node exists and get store_id
441
- const { data: targetNode } = await supabase
442
- .from("nodes")
443
- .select("id, store_id")
444
- .eq("id", nodeId)
445
- .single();
446
- if (!targetNode) {
447
- return { status: 404, body: { error: "Node not found" } };
448
- }
449
- if (auth.userId) {
450
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, targetNode.store_id);
451
- if (!hasAccess) {
452
- return { status: 403, body: { error: "No access to this node" } };
453
- }
454
- }
455
- const { data: cmd, error: cmdErr } = await supabase
456
- .from("node_commands")
457
- .insert({
458
- node_id: nodeId,
459
- store_id: targetNode.store_id,
460
- command,
461
- payload,
462
- status: "pending",
463
- })
464
- .select("id, command, status, created_at")
465
- .single();
466
- if (cmdErr) {
467
- return { status: 500, body: { error: cmdErr.message } };
468
- }
469
- await logNodeEvent(supabase, targetNode.store_id, nodeId, "command_queued", {
470
- command,
471
- command_id: cmd.id,
472
- queued_by: auth.userId,
473
- });
474
- return { status: 201, body: { success: true, command: cmd } };
475
- }
476
- // ── POST /nodes/:id/rotate-key ─────────────────────────────
477
- // User auth. Generate a new API key for a node.
478
- const rotateMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/rotate-key$/);
479
- if (rotateMatch && method === "POST") {
480
- const nodeId = rotateMatch[1];
481
- if (!auth.userId && !auth.isServiceRole) {
482
- return { status: 401, body: { error: "User authentication required" } };
483
- }
484
- // Verify ownership
485
- const { data: rotateNode } = await supabase
486
- .from("nodes")
487
- .select("id, store_id, name")
488
- .eq("id", nodeId)
489
- .single();
490
- if (!rotateNode) {
491
- return { status: 404, body: { error: "Node not found" } };
492
- }
493
- if (auth.userId) {
494
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, rotateNode.store_id);
495
- if (!hasAccess) {
496
- return { status: 403, body: { error: "No access to this node" } };
497
- }
498
- }
499
- const newApiKey = generateNodeApiKey();
500
- const newHash = hashApiKey(newApiKey);
501
- const { error: rotateErr } = await supabase
502
- .from("nodes")
503
- .update({ api_key_hash: newHash, updated_at: new Date().toISOString() })
504
- .eq("id", nodeId);
505
- if (rotateErr) {
506
- return { status: 500, body: { error: rotateErr.message } };
507
- }
508
- // Invalidate auth cache for old hash
509
- for (const [hash, cached] of nodeAuthCache) {
510
- if (cached.node.id === nodeId) {
511
- nodeAuthCache.delete(hash);
512
- }
513
- }
514
- await logNodeEvent(supabase, rotateNode.store_id, nodeId, "key_rotated", {
515
- rotated_by: auth.userId,
516
- });
513
+ const nodeUpdates = {
514
+ updated_at: new Date().toISOString()
515
+ };
516
+ if (body.name !== undefined) nodeUpdates.name = body.name;
517
+ if (body.capabilities !== undefined) nodeUpdates.capabilities = body.capabilities;
518
+ if (body.config !== undefined) nodeUpdates.config = body.config;
519
+ const {
520
+ data: updated,
521
+ error: updateErr
522
+ } = await supabase.from("nodes").update(nodeUpdates).eq("id", nodeId).select("id, name, status, capabilities, config, version, hardware, last_heartbeat, updated_at").single();
523
+ if (updateErr) {
524
+ return {
525
+ status: 500,
526
+ body: {
527
+ error: updateErr.message
528
+ }
529
+ };
530
+ }
531
+ return {
532
+ status: 200,
533
+ body: {
534
+ success: true,
535
+ node: updated
536
+ }
537
+ };
538
+ }
539
+
540
+ // ── POST /nodes/:id/commands ────────────────────────────────
541
+ // User auth. Queue a command for a node.
542
+ const commandMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/commands$/);
543
+ if (commandMatch && method === "POST") {
544
+ const nodeId = commandMatch[1];
545
+ if (!auth.userId && !auth.isServiceRole) {
546
+ return {
547
+ status: 401,
548
+ body: {
549
+ error: "User authentication required"
550
+ }
551
+ };
552
+ }
553
+ if (!body) return {
554
+ status: 400,
555
+ body: {
556
+ error: "Request body required"
557
+ }
558
+ };
559
+ const command = body.command;
560
+ const payload = body.payload || {};
561
+ if (!command) {
562
+ return {
563
+ status: 400,
564
+ body: {
565
+ error: "command is required"
566
+ }
567
+ };
568
+ }
569
+ const validCommands = ["restart", "shutdown", "rotate_key", "pause_all_channels", "resume_all_channels", "update"];
570
+ if (!validCommands.includes(command)) {
571
+ return {
572
+ status: 400,
573
+ body: {
574
+ error: `Invalid command. Valid: ${validCommands.join(", ")}`
575
+ }
576
+ };
577
+ }
578
+
579
+ // Verify node exists and get store_id
580
+ const {
581
+ data: targetNode
582
+ } = await supabase.from("nodes").select("id, store_id").eq("id", nodeId).single();
583
+ if (!targetNode) {
584
+ return {
585
+ status: 404,
586
+ body: {
587
+ error: "Node not found"
588
+ }
589
+ };
590
+ }
591
+ if (auth.userId) {
592
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, targetNode.store_id);
593
+ if (!hasAccess) {
517
594
  return {
518
- status: 200,
519
- body: {
520
- success: true,
521
- api_key: newApiKey,
522
- message: "Save your new API key — it cannot be retrieved later. The old key is now invalid.",
523
- },
595
+ status: 403,
596
+ body: {
597
+ error: "No access to this node"
598
+ }
524
599
  };
600
+ }
525
601
  }
526
- // ── DELETE /nodes/:id ─────────────────────────────────────────
527
- // User auth. Deletes a node and all its channels.
528
- const deleteMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)$/);
529
- if (deleteMatch && method === "DELETE") {
530
- const nodeId = deleteMatch[1];
531
- // Verify ownership
532
- const { data: node } = await supabase
533
- .from("nodes")
534
- .select("id, store_id, name")
535
- .eq("id", nodeId)
536
- .single();
537
- if (!node) {
538
- return { status: 404, body: { error: "Node not found" } };
539
- }
540
- if (auth.userId) {
541
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, node.store_id);
542
- if (!hasAccess) {
543
- return { status: 403, body: { error: "No access to this node" } };
544
- }
545
- }
546
- await logNodeEvent(supabase, node.store_id, nodeId, "deleted", {
547
- name: node.name,
548
- deleted_by: auth.userId,
549
- });
550
- const { error } = await supabase.from("nodes").delete().eq("id", nodeId);
551
- if (error) {
552
- return { status: 500, body: { error: error.message } };
553
- }
554
- return { status: 200, body: { success: true, deleted: nodeId } };
555
- }
556
- // ── POST /channels ───────────────────────────────────────────
557
- // User auth OR node API key. Registers a channel on a node.
558
- if (pathname === "/channels" && method === "POST") {
559
- if (!body)
560
- return { status: 400, body: { error: "Request body required" } };
561
- const reg = body;
562
- if (!reg.store_id || !reg.node_id || !reg.type || !reg.name) {
563
- return { status: 400, body: { error: "store_id, node_id, type, and name required" } };
564
- }
565
- // Verify node exists and belongs to store
566
- const { data: node } = await supabase
567
- .from("nodes")
568
- .select("id, store_id")
569
- .eq("id", reg.node_id)
570
- .eq("store_id", reg.store_id)
571
- .single();
572
- if (!node) {
573
- return { status: 404, body: { error: "Node not found in this store" } };
574
- }
575
- // Check channel-per-node limit against store plan
576
- const { data: chanPlanRow } = await supabase
577
- .from("store_plans")
578
- .select("plan, limits")
579
- .eq("store_id", reg.store_id)
580
- .single();
581
- const chanPlanLimits = chanPlanRow?.limits;
582
- const channelsPerNode = chanPlanLimits?.channels_per_node ?? 2; // free plan default
583
- const { count: existingChannels } = await supabase
584
- .from("channels")
585
- .select("id", { count: "exact", head: true })
586
- .eq("node_id", reg.node_id);
587
- if ((existingChannels || 0) >= channelsPerNode) {
588
- return { status: 429, body: { error: `Channel limit reached (${existingChannels}/${channelsPerNode} per node on ${chanPlanRow?.plan || "free"} plan)` } };
589
- }
590
- const { data: channel, error } = await supabase
591
- .from("channels")
592
- .insert({
593
- store_id: reg.store_id,
594
- node_id: reg.node_id,
595
- type: reg.type,
596
- name: reg.name,
597
- config: reg.config || {},
598
- agent_id: reg.agent_id || null,
599
- status: "inactive",
600
- })
601
- .select("id, type, name, status, created_at")
602
- .single();
603
- if (error) {
604
- return { status: 500, body: { error: error.message } };
605
- }
606
- await logNodeEvent(supabase, reg.store_id, reg.node_id, "channel_added", {
607
- channel_id: channel.id,
608
- type: reg.type,
609
- name: reg.name,
610
- });
611
- return { status: 201, body: { success: true, channel } };
612
- }
613
- // ── GET|POST /channels/list ──────────────────────────────────
614
- // User auth. Lists channels for a store.
615
- if (pathname === "/channels/list" && (method === "GET" || method === "POST")) {
616
- const storeId = body?.store_id || queryParams?.get("store_id");
617
- if (!storeId) {
618
- return { status: 400, body: { error: "store_id required" } };
619
- }
620
- // Verify user has access to the requested store
621
- if (auth.userId) {
622
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, storeId);
623
- if (!hasAccess) {
624
- return { status: 403, body: { error: "Not authorized to access this store's channels" } };
625
- }
626
- }
627
- else if (!auth.isServiceRole) {
628
- return { status: 401, body: { error: "Authentication required" } };
629
- }
630
- const { data: channels, error } = await supabase
631
- .from("channels")
632
- .select(`
602
+ const {
603
+ data: cmd,
604
+ error: cmdErr
605
+ } = await supabase.from("node_commands").insert({
606
+ node_id: nodeId,
607
+ store_id: targetNode.store_id,
608
+ command,
609
+ payload,
610
+ status: "pending"
611
+ }).select("id, command, status, created_at").single();
612
+ if (cmdErr) {
613
+ return {
614
+ status: 500,
615
+ body: {
616
+ error: cmdErr.message
617
+ }
618
+ };
619
+ }
620
+ await logNodeEvent(supabase, targetNode.store_id, nodeId, "command_queued", {
621
+ command,
622
+ command_id: cmd.id,
623
+ queued_by: auth.userId
624
+ });
625
+ return {
626
+ status: 201,
627
+ body: {
628
+ success: true,
629
+ command: cmd
630
+ }
631
+ };
632
+ }
633
+
634
+ // ── POST /nodes/:id/rotate-key ─────────────────────────────
635
+ // User auth. Generate a new API key for a node.
636
+ const rotateMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/rotate-key$/);
637
+ if (rotateMatch && method === "POST") {
638
+ const nodeId = rotateMatch[1];
639
+ if (!auth.userId && !auth.isServiceRole) {
640
+ return {
641
+ status: 401,
642
+ body: {
643
+ error: "User authentication required"
644
+ }
645
+ };
646
+ }
647
+
648
+ // Verify ownership
649
+ const {
650
+ data: rotateNode
651
+ } = await supabase.from("nodes").select("id, store_id, name").eq("id", nodeId).single();
652
+ if (!rotateNode) {
653
+ return {
654
+ status: 404,
655
+ body: {
656
+ error: "Node not found"
657
+ }
658
+ };
659
+ }
660
+ if (auth.userId) {
661
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, rotateNode.store_id);
662
+ if (!hasAccess) {
663
+ return {
664
+ status: 403,
665
+ body: {
666
+ error: "No access to this node"
667
+ }
668
+ };
669
+ }
670
+ }
671
+ const newApiKey = generateNodeApiKey();
672
+ const newHash = hashApiKey(newApiKey);
673
+ const {
674
+ error: rotateErr
675
+ } = await supabase.from("nodes").update({
676
+ api_key_hash: newHash,
677
+ updated_at: new Date().toISOString()
678
+ }).eq("id", nodeId);
679
+ if (rotateErr) {
680
+ return {
681
+ status: 500,
682
+ body: {
683
+ error: rotateErr.message
684
+ }
685
+ };
686
+ }
687
+
688
+ // Invalidate auth cache for old hash
689
+ for (const [hash, cached] of nodeAuthCache) {
690
+ if (cached.node.id === nodeId) {
691
+ nodeAuthCache.delete(hash);
692
+ }
693
+ }
694
+ await logNodeEvent(supabase, rotateNode.store_id, nodeId, "key_rotated", {
695
+ rotated_by: auth.userId
696
+ });
697
+ return {
698
+ status: 200,
699
+ body: {
700
+ success: true,
701
+ api_key: newApiKey,
702
+ message: "Save your new API key — it cannot be retrieved later. The old key is now invalid."
703
+ }
704
+ };
705
+ }
706
+
707
+ // ── DELETE /nodes/:id ─────────────────────────────────────────
708
+ // User auth. Deletes a node and all its channels.
709
+ const deleteMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)$/);
710
+ if (deleteMatch && method === "DELETE") {
711
+ const nodeId = deleteMatch[1];
712
+
713
+ // Verify ownership
714
+ const {
715
+ data: node
716
+ } = await supabase.from("nodes").select("id, store_id, name").eq("id", nodeId).single();
717
+ if (!node) {
718
+ return {
719
+ status: 404,
720
+ body: {
721
+ error: "Node not found"
722
+ }
723
+ };
724
+ }
725
+ if (auth.userId) {
726
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, node.store_id);
727
+ if (!hasAccess) {
728
+ return {
729
+ status: 403,
730
+ body: {
731
+ error: "No access to this node"
732
+ }
733
+ };
734
+ }
735
+ }
736
+ await logNodeEvent(supabase, node.store_id, nodeId, "deleted", {
737
+ name: node.name,
738
+ deleted_by: auth.userId
739
+ });
740
+ const {
741
+ error
742
+ } = await supabase.from("nodes").delete().eq("id", nodeId);
743
+ if (error) {
744
+ return {
745
+ status: 500,
746
+ body: {
747
+ error: error.message
748
+ }
749
+ };
750
+ }
751
+ return {
752
+ status: 200,
753
+ body: {
754
+ success: true,
755
+ deleted: nodeId
756
+ }
757
+ };
758
+ }
759
+
760
+ // ── POST /channels ───────────────────────────────────────────
761
+ // User auth OR node API key. Registers a channel on a node.
762
+ if (pathname === "/channels" && method === "POST") {
763
+ if (!body) return {
764
+ status: 400,
765
+ body: {
766
+ error: "Request body required"
767
+ }
768
+ };
769
+ const reg = body;
770
+ if (!reg.store_id || !reg.node_id || !reg.type || !reg.name) {
771
+ return {
772
+ status: 400,
773
+ body: {
774
+ error: "store_id, node_id, type, and name required"
775
+ }
776
+ };
777
+ }
778
+
779
+ // Verify node exists and belongs to store
780
+ const {
781
+ data: node
782
+ } = await supabase.from("nodes").select("id, store_id").eq("id", reg.node_id).eq("store_id", reg.store_id).single();
783
+ if (!node) {
784
+ return {
785
+ status: 404,
786
+ body: {
787
+ error: "Node not found in this store"
788
+ }
789
+ };
790
+ }
791
+
792
+ // Check channel-per-node limit against store plan
793
+ const {
794
+ data: chanPlanRow
795
+ } = await supabase.from("store_plans").select("plan, limits").eq("store_id", reg.store_id).single();
796
+ const chanPlanLimits = chanPlanRow?.limits;
797
+ const channelsPerNode = chanPlanLimits?.channels_per_node ?? 2; // free plan default
798
+
799
+ const {
800
+ count: existingChannels
801
+ } = await supabase.from("channels").select("id", {
802
+ count: "exact",
803
+ head: true
804
+ }).eq("node_id", reg.node_id);
805
+ if ((existingChannels || 0) >= channelsPerNode) {
806
+ return {
807
+ status: 429,
808
+ body: {
809
+ error: `Channel limit reached (${existingChannels}/${channelsPerNode} per node on ${chanPlanRow?.plan || "free"} plan)`
810
+ }
811
+ };
812
+ }
813
+ const {
814
+ data: channel,
815
+ error
816
+ } = await supabase.from("channels").insert({
817
+ store_id: reg.store_id,
818
+ node_id: reg.node_id,
819
+ type: reg.type,
820
+ name: reg.name,
821
+ config: reg.config || {},
822
+ agent_id: reg.agent_id || null,
823
+ status: "inactive"
824
+ }).select("id, type, name, status, created_at").single();
825
+ if (error) {
826
+ return {
827
+ status: 500,
828
+ body: {
829
+ error: error.message
830
+ }
831
+ };
832
+ }
833
+ await logNodeEvent(supabase, reg.store_id, reg.node_id, "channel_added", {
834
+ channel_id: channel.id,
835
+ type: reg.type,
836
+ name: reg.name
837
+ });
838
+ return {
839
+ status: 201,
840
+ body: {
841
+ success: true,
842
+ channel
843
+ }
844
+ };
845
+ }
846
+
847
+ // ── GET|POST /channels/list ──────────────────────────────────
848
+ // User auth. Lists channels for a store.
849
+ if (pathname === "/channels/list" && (method === "GET" || method === "POST")) {
850
+ const storeId = body?.store_id || queryParams?.get("store_id");
851
+ if (!storeId) {
852
+ return {
853
+ status: 400,
854
+ body: {
855
+ error: "store_id required"
856
+ }
857
+ };
858
+ }
859
+
860
+ // Verify user has access to the requested store
861
+ if (auth.userId) {
862
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, storeId);
863
+ if (!hasAccess) {
864
+ return {
865
+ status: 403,
866
+ body: {
867
+ error: "Not authorized to access this store's channels"
868
+ }
869
+ };
870
+ }
871
+ } else if (!auth.isServiceRole) {
872
+ return {
873
+ status: 401,
874
+ body: {
875
+ error: "Authentication required"
876
+ }
877
+ };
878
+ }
879
+ const {
880
+ data: channels,
881
+ error
882
+ } = await supabase.from("channels").select(`
633
883
  id, node_id, type, name, status, config, agent_id, stats,
634
884
  error_message, created_at, updated_at,
635
885
  nodes!inner(id, name, status)
636
- `)
637
- .eq("store_id", storeId)
638
- .order("created_at", { ascending: false });
639
- if (error) {
640
- return { status: 500, body: { error: error.message } };
641
- }
642
- return { status: 200, body: { success: true, channels: channels || [] } };
643
- }
644
- // ── PATCH /channels/:id ────────────────────────────────────────
645
- // User auth. Update channel config/agent assignment.
646
- const channelPatchMatch = pathname.match(/^\/channels\/([a-f0-9-]+)$/);
647
- if (channelPatchMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
648
- const channelId = channelPatchMatch[1];
649
- // Fetch the channel first to verify ownership
650
- const { data: existingChannel } = await supabase
651
- .from("channels")
652
- .select("id, store_id")
653
- .eq("id", channelId)
654
- .single();
655
- if (!existingChannel) {
656
- return { status: 404, body: { error: "Channel not found" } };
657
- }
658
- // Verify the user has access to this channel's store
659
- if (auth.userId) {
660
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, existingChannel.store_id);
661
- if (!hasAccess) {
662
- return { status: 403, body: { error: "Not authorized to modify this channel" } };
663
- }
886
+ `).eq("store_id", storeId).order("created_at", {
887
+ ascending: false
888
+ });
889
+ if (error) {
890
+ return {
891
+ status: 500,
892
+ body: {
893
+ error: error.message
664
894
  }
665
- else if (!auth.isServiceRole) {
666
- return { status: 401, body: { error: "Authentication required" } };
667
- }
668
- const updates = {};
669
- if (body.agent_id !== undefined)
670
- updates.agent_id = body.agent_id;
671
- if (body.config !== undefined)
672
- updates.config = body.config;
673
- if (body.name !== undefined)
674
- updates.name = body.name;
675
- if (body.status !== undefined)
676
- updates.status = body.status;
677
- updates.updated_at = new Date().toISOString();
678
- const { data: channel, error } = await supabase
679
- .from("channels")
680
- .update(updates)
681
- .eq("id", channelId)
682
- .select("id, type, name, status, agent_id, config, updated_at")
683
- .single();
684
- if (error) {
685
- return { status: 500, body: { error: error.message } };
686
- }
687
- return { status: 200, body: { success: true, channel } };
688
- }
689
- // ── POST /channels/:id/messages ──────────────────────────────
690
- // Node API key auth. Ingests a message from a channel.
691
- // If direction=inbound and channel has agent_id, auto-invokes agent.
692
- const messageMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
693
- if (messageMatch && method === "POST") {
694
- const channelId = messageMatch[1];
695
- if (!body)
696
- return { status: 400, body: { error: "Request body required" } };
697
- // Authenticate node (or service role for admin access)
698
- const node = await authenticateNode(supabase, auth.rawToken);
699
- if (!node && !auth.isServiceRole) {
700
- return { status: 401, body: { error: "Invalid node API key" } };
701
- }
702
- // Verify channel exists (and belongs to this node if node auth)
703
- let channelQuery = supabase
704
- .from("channels")
705
- .select("id, store_id, node_id, agent_id, config, type, name")
706
- .eq("id", channelId);
707
- if (node)
708
- channelQuery = channelQuery.eq("node_id", node.id);
709
- const { data: channel } = await channelQuery.single();
710
- if (!channel) {
711
- return { status: 404, body: { error: "Channel not found" } };
712
- }
713
- const msg = body;
714
- const direction = msg.direction || "inbound";
715
- const senderId = msg.sender_id || "unknown";
716
- // Check plan message limits before accepting
717
- const limitCheck = await checkPlanLimits(supabase, channel.store_id, "message");
718
- if (!limitCheck.allowed) {
719
- return { status: 429, body: { error: limitCheck.reason || "Message limit exceeded" } };
720
- }
721
- // Resolve conversation ID (reuse if < 30 min gap from same sender)
722
- // Channel type is dynamic — read from channel.config.type or channel row's type column
723
- const channelType = channel.config?.type || channel.type || "unknown";
724
- const channelName = channel.name || "";
725
- let conversationId = msg.conversation_id;
726
- let senderContext;
727
- if (!conversationId && direction === "inbound") {
728
- const ctx = await resolveConversation(supabase, channelId, senderId, channel.store_id, channelType, msg.sender_name);
729
- conversationId = ctx.conversationId;
730
- senderContext = {
731
- senderId,
732
- senderName: msg.sender_name || undefined,
733
- customerId: ctx.customerId,
734
- customerName: ctx.customerName,
735
- channelType,
736
- channelId,
737
- channelName,
738
- };
739
- }
740
- // Insert inbound message
741
- const { data: message, error } = await supabase
742
- .from("channel_messages")
743
- .insert({
744
- store_id: channel.store_id,
895
+ };
896
+ }
897
+ return {
898
+ status: 200,
899
+ body: {
900
+ success: true,
901
+ channels: channels || []
902
+ }
903
+ };
904
+ }
905
+
906
+ // ── PATCH /channels/:id ────────────────────────────────────────
907
+ // User auth. Update channel config/agent assignment.
908
+ const channelPatchMatch = pathname.match(/^\/channels\/([a-f0-9-]+)$/);
909
+ if (channelPatchMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
910
+ const channelId = channelPatchMatch[1];
911
+
912
+ // Fetch the channel first to verify ownership
913
+ const {
914
+ data: existingChannel
915
+ } = await supabase.from("channels").select("id, store_id").eq("id", channelId).single();
916
+ if (!existingChannel) {
917
+ return {
918
+ status: 404,
919
+ body: {
920
+ error: "Channel not found"
921
+ }
922
+ };
923
+ }
924
+
925
+ // Verify the user has access to this channel's store
926
+ if (auth.userId) {
927
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, existingChannel.store_id);
928
+ if (!hasAccess) {
929
+ return {
930
+ status: 403,
931
+ body: {
932
+ error: "Not authorized to modify this channel"
933
+ }
934
+ };
935
+ }
936
+ } else if (!auth.isServiceRole) {
937
+ return {
938
+ status: 401,
939
+ body: {
940
+ error: "Authentication required"
941
+ }
942
+ };
943
+ }
944
+ const updates = {};
945
+ if (body.agent_id !== undefined) updates.agent_id = body.agent_id;
946
+ if (body.config !== undefined) updates.config = body.config;
947
+ if (body.name !== undefined) updates.name = body.name;
948
+ if (body.status !== undefined) updates.status = body.status;
949
+ updates.updated_at = new Date().toISOString();
950
+ const {
951
+ data: channel,
952
+ error
953
+ } = await supabase.from("channels").update(updates).eq("id", channelId).select("id, type, name, status, agent_id, config, updated_at").single();
954
+ if (error) {
955
+ return {
956
+ status: 500,
957
+ body: {
958
+ error: error.message
959
+ }
960
+ };
961
+ }
962
+ return {
963
+ status: 200,
964
+ body: {
965
+ success: true,
966
+ channel
967
+ }
968
+ };
969
+ }
970
+
971
+ // ── POST /channels/:id/messages ──────────────────────────────
972
+ // Node API key auth. Ingests a message from a channel.
973
+ // If direction=inbound and channel has agent_id, auto-invokes agent.
974
+ const messageMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
975
+ if (messageMatch && method === "POST") {
976
+ const channelId = messageMatch[1];
977
+ if (!body) return {
978
+ status: 400,
979
+ body: {
980
+ error: "Request body required"
981
+ }
982
+ };
983
+
984
+ // Authenticate node (or service role for admin access)
985
+ const node = await authenticateNode(supabase, auth.rawToken);
986
+ if (!node && !auth.isServiceRole) {
987
+ return {
988
+ status: 401,
989
+ body: {
990
+ error: "Invalid node API key"
991
+ }
992
+ };
993
+ }
994
+
995
+ // Verify channel exists (and belongs to this node if node auth)
996
+ let channelQuery = supabase.from("channels").select("id, store_id, node_id, agent_id, config, type, name").eq("id", channelId);
997
+ if (node) channelQuery = channelQuery.eq("node_id", node.id);
998
+ const {
999
+ data: channel
1000
+ } = await channelQuery.single();
1001
+ if (!channel) {
1002
+ return {
1003
+ status: 404,
1004
+ body: {
1005
+ error: "Channel not found"
1006
+ }
1007
+ };
1008
+ }
1009
+ const msg = body;
1010
+ const direction = msg.direction || "inbound";
1011
+ const senderId = msg.sender_id || "unknown";
1012
+
1013
+ // Check plan message limits before accepting
1014
+ const limitCheck = await checkPlanLimits(supabase, channel.store_id, "message");
1015
+ if (!limitCheck.allowed) {
1016
+ return {
1017
+ status: 429,
1018
+ body: {
1019
+ error: limitCheck.reason || "Message limit exceeded"
1020
+ }
1021
+ };
1022
+ }
1023
+
1024
+ // Resolve conversation ID (reuse if < 30 min gap from same sender)
1025
+ // Channel type is dynamic — read from channel.config.type or channel row's type column
1026
+ const channelType = channel.config?.type || channel.type || "unknown";
1027
+ const channelName = channel.name || "";
1028
+ let conversationId = msg.conversation_id;
1029
+ let senderContext;
1030
+ if (!conversationId && direction === "inbound") {
1031
+ const ctx = await resolveConversation(supabase, channelId, senderId, channel.store_id, channelType, msg.sender_name);
1032
+ conversationId = ctx.conversationId;
1033
+ senderContext = {
1034
+ senderId,
1035
+ senderName: msg.sender_name || undefined,
1036
+ customerId: ctx.customerId,
1037
+ customerName: ctx.customerName,
1038
+ channelType,
1039
+ channelId,
1040
+ channelName
1041
+ };
1042
+ }
1043
+
1044
+ // Insert inbound message
1045
+ const {
1046
+ data: message,
1047
+ error
1048
+ } = await supabase.from("channel_messages").insert({
1049
+ store_id: channel.store_id,
1050
+ channel_id: channelId,
1051
+ direction,
1052
+ sender_id: senderId,
1053
+ sender_name: msg.sender_name,
1054
+ content: msg.content,
1055
+ content_type: msg.content_type || "text",
1056
+ metadata: msg.metadata || {},
1057
+ agent_id: channel.agent_id,
1058
+ conversation_id: conversationId
1059
+ }).select("id, direction, content, conversation_id, created_at").single();
1060
+ if (error) {
1061
+ return {
1062
+ status: 500,
1063
+ body: {
1064
+ error: error.message
1065
+ }
1066
+ };
1067
+ }
1068
+
1069
+ // Update channel stats (best-effort)
1070
+ try {
1071
+ await supabase.rpc("increment_channel_stats", {
1072
+ p_channel_id: channelId
1073
+ });
1074
+ } catch {
1075
+ // Stats function may not exist yet — that's fine
1076
+ }
1077
+
1078
+ // Telemetry → ClickHouse for inbound message
1079
+ if (direction === "inbound") {
1080
+ try {
1081
+ queueSpan(auditRowToSpan({
1082
+ action: "node.message.inbound",
1083
+ severity: "info",
1084
+ store_id: channel.store_id,
1085
+ resource_type: "whale_node",
1086
+ resource_id: channelId,
1087
+ source: "whale-node",
1088
+ conversation_id: conversationId || message.conversation_id || null,
1089
+ trace_id: randomUUID(),
1090
+ span_kind: "INTERNAL",
1091
+ service_name: "whale-node",
1092
+ status_code: "OK",
1093
+ start_time: new Date().toISOString(),
1094
+ end_time: new Date().toISOString(),
1095
+ details: {
745
1096
  channel_id: channelId,
746
- direction,
1097
+ channel_type: channelType,
747
1098
  sender_id: senderId,
748
- sender_name: msg.sender_name,
749
- content: msg.content,
750
- content_type: msg.content_type || "text",
751
- metadata: msg.metadata || {},
752
- agent_id: channel.agent_id,
753
- conversation_id: conversationId,
754
- })
755
- .select("id, direction, content, conversation_id, created_at")
756
- .single();
757
- if (error) {
758
- return { status: 500, body: { error: error.message } };
759
- }
760
- // Update channel stats (best-effort)
1099
+ sender_name: msg.sender_name || null,
1100
+ node_id: node?.id || null,
1101
+ has_agent: !!channel.agent_id
1102
+ }
1103
+ }));
1104
+ } catch {
1105
+ // Telemetry must never break message flow
1106
+ }
1107
+ }
1108
+
1109
+ // Track message usage (best-effort, non-blocking)
1110
+ incrementUsage(supabase, channel.store_id, direction === "inbound" ? {
1111
+ messages_in: 1
1112
+ } : {
1113
+ messages_out: 1
1114
+ }).catch(err => console.error("[billing] usage increment failed:", err.message));
1115
+
1116
+ // ── Agent auto-invocation ───────────────────────────────────
1117
+ // If inbound message and channel has an agent assigned, invoke it
1118
+ let agentResponse = null;
1119
+ if (direction === "inbound" && channel.agent_id && agentInvoker) {
1120
+ // Check agent invocation limits before calling
1121
+ const agentLimitCheck = await checkPlanLimits(supabase, channel.store_id, "agent_invocation");
1122
+ if (!agentLimitCheck.allowed) {
1123
+ console.warn(`[channel-agent] Agent invocation blocked: ${agentLimitCheck.reason}`);
1124
+ } else {
761
1125
  try {
762
- await supabase.rpc("increment_channel_stats", {
763
- p_channel_id: channelId,
764
- });
765
- }
766
- catch {
767
- // Stats function may not exist yet — that's fine
768
- }
769
- // Telemetry ClickHouse for inbound message
770
- if (direction === "inbound") {
771
- try {
772
- queueSpan(auditRowToSpan({
773
- action: "node.message.inbound",
774
- severity: "info",
775
- store_id: channel.store_id,
776
- resource_type: "whale_node",
777
- resource_id: channelId,
778
- source: "whale-node",
779
- conversation_id: conversationId || message.conversation_id || null,
780
- trace_id: randomUUID(),
781
- span_kind: "INTERNAL",
782
- service_name: "whale-node",
783
- status_code: "OK",
784
- start_time: new Date().toISOString(),
785
- end_time: new Date().toISOString(),
786
- details: {
787
- channel_id: channelId,
788
- channel_type: channelType,
789
- sender_id: senderId,
790
- sender_name: msg.sender_name || null,
791
- node_id: node?.id || null,
792
- has_agent: !!channel.agent_id,
793
- },
794
- }));
795
- }
796
- catch {
797
- // Telemetry must never break message flow
798
- }
799
- }
800
- // Track message usage (best-effort, non-blocking)
801
- incrementUsage(supabase, channel.store_id, direction === "inbound" ? { messages_in: 1 } : { messages_out: 1 }).catch((err) => console.error("[billing] usage increment failed:", err.message));
802
- // ── Agent auto-invocation ───────────────────────────────────
803
- // If inbound message and channel has an agent assigned, invoke it
804
- let agentResponse = null;
805
- if (direction === "inbound" && channel.agent_id && agentInvoker) {
806
- // Check agent invocation limits before calling
807
- const agentLimitCheck = await checkPlanLimits(supabase, channel.store_id, "agent_invocation");
808
- if (!agentLimitCheck.allowed) {
809
- console.warn(`[channel-agent] Agent invocation blocked: ${agentLimitCheck.reason}`);
810
- }
811
- else {
812
- try {
813
- console.log(`[channel-agent] Invoking agent ${channel.agent_id} for channel ${channelId}`);
814
- const result = await agentInvoker(supabase, channel.agent_id, msg.content, channel.store_id, conversationId || message.conversation_id, senderContext);
815
- // Track agent invocation usage
816
- incrementUsage(supabase, channel.store_id, { agent_invocations: 1 })
817
- .catch((err) => console.error("[billing] agent usage increment failed:", err.message));
818
- if (result.success && result.response) {
819
- // Insert agent response as outbound message
820
- const { data: outMsg, error: outErr } = await supabase
821
- .from("channel_messages")
822
- .insert({
823
- store_id: channel.store_id,
824
- channel_id: channelId,
825
- direction: "outbound",
826
- sender_id: "agent",
827
- sender_name: "AI Agent",
828
- content: result.response,
829
- content_type: "text",
830
- metadata: { agent_id: channel.agent_id, auto_response: true },
831
- agent_id: channel.agent_id,
832
- conversation_id: conversationId || message.conversation_id,
833
- })
834
- .select("id, direction, content, conversation_id, created_at")
835
- .single();
836
- if (!outErr && outMsg) {
837
- agentResponse = outMsg;
838
- // Track outbound message usage
839
- incrementUsage(supabase, channel.store_id, { messages_out: 1 })
840
- .catch((err) => console.error("[billing] outbound usage increment failed:", err.message));
841
- console.log(`[channel-agent] Response stored: ${outMsg.id}`);
842
- }
843
- }
844
- else if (result.error) {
845
- console.error(`[channel-agent] Agent error: ${result.error}`);
846
- }
847
- }
848
- catch (err) {
849
- console.error(`[channel-agent] Invocation failed:`, err.message);
850
- }
1126
+ console.log(`[channel-agent] Invoking agent ${channel.agent_id} for channel ${channelId}`);
1127
+ const result = await agentInvoker(supabase, channel.agent_id, msg.content, channel.store_id, conversationId || message.conversation_id, senderContext);
1128
+
1129
+ // Track agent invocation usage
1130
+ incrementUsage(supabase, channel.store_id, {
1131
+ agent_invocations: 1
1132
+ }).catch(err => console.error("[billing] agent usage increment failed:", err.message));
1133
+ if (result.success && result.response) {
1134
+ // Insert agent response as outbound message
1135
+ const {
1136
+ data: outMsg,
1137
+ error: outErr
1138
+ } = await supabase.from("channel_messages").insert({
1139
+ store_id: channel.store_id,
1140
+ channel_id: channelId,
1141
+ direction: "outbound",
1142
+ sender_id: "agent",
1143
+ sender_name: "AI Agent",
1144
+ content: result.response,
1145
+ content_type: "text",
1146
+ metadata: {
1147
+ agent_id: channel.agent_id,
1148
+ auto_response: true
1149
+ },
1150
+ agent_id: channel.agent_id,
1151
+ conversation_id: conversationId || message.conversation_id
1152
+ }).select("id, direction, content, conversation_id, created_at").single();
1153
+ if (!outErr && outMsg) {
1154
+ agentResponse = outMsg;
1155
+ // Track outbound message usage
1156
+ incrementUsage(supabase, channel.store_id, {
1157
+ messages_out: 1
1158
+ }).catch(err => console.error("[billing] outbound usage increment failed:", err.message));
1159
+ console.log(`[channel-agent] Response stored: ${outMsg.id}`);
851
1160
  }
1161
+ } else if (result.error) {
1162
+ console.error(`[channel-agent] Agent error: ${result.error}`);
1163
+ }
1164
+ } catch (err) {
1165
+ console.error(`[channel-agent] Invocation failed:`, err.message);
852
1166
  }
1167
+ }
1168
+ }
1169
+ return {
1170
+ status: 201,
1171
+ body: {
1172
+ success: true,
1173
+ message,
1174
+ agent_response: agentResponse,
1175
+ conversation_id: conversationId || message.conversation_id
1176
+ }
1177
+ };
1178
+ }
1179
+
1180
+ // ── GET /channels/:id/messages ─────────────────────────────────
1181
+ // Node API key auth. Retrieves messages for a channel.
1182
+ // Query params (via body): direction, undelivered, limit, after
1183
+ const getMessagesMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
1184
+ if (getMessagesMatch && method === "GET") {
1185
+ const channelId = getMessagesMatch[1];
1186
+
1187
+ // Authenticate node (or service role for admin access)
1188
+ const node = await authenticateNode(supabase, auth.rawToken);
1189
+ if (!node && !auth.isServiceRole) {
1190
+ return {
1191
+ status: 401,
1192
+ body: {
1193
+ error: "Invalid node API key"
1194
+ }
1195
+ };
1196
+ }
1197
+
1198
+ // Verify channel exists (and belongs to this node if node auth)
1199
+ let chQuery = supabase.from("channels").select("id, node_id").eq("id", channelId);
1200
+ if (node) chQuery = chQuery.eq("node_id", node.id);
1201
+ const {
1202
+ data: channel
1203
+ } = await chQuery.single();
1204
+ if (!channel) {
1205
+ return {
1206
+ status: 404,
1207
+ body: {
1208
+ error: "Channel not found"
1209
+ }
1210
+ };
1211
+ }
1212
+ let query = supabase.from("channel_messages").select("id, direction, sender_id, sender_name, content, content_type, metadata, conversation_id, delivered_at, created_at").eq("channel_id", channelId);
1213
+
1214
+ // Filter by direction (body or query param)
1215
+ const direction = body?.direction || queryParams?.get("direction");
1216
+ if (direction) {
1217
+ query = query.eq("direction", direction);
1218
+ }
1219
+
1220
+ // Filter undelivered outbound messages (for node polling)
1221
+ const undelivered = body?.undelivered || queryParams?.get("undelivered");
1222
+ if (undelivered === true || undelivered === "true") {
1223
+ query = query.eq("direction", "outbound").is("delivered_at", null);
1224
+ }
1225
+
1226
+ // After a specific timestamp
1227
+ const after = body?.after || queryParams?.get("after");
1228
+ if (after) {
1229
+ query = query.gt("created_at", after);
1230
+ }
1231
+ const rawLimit = body?.limit || queryParams?.get("limit") || 50;
1232
+ const limit = Math.min(Number(rawLimit), 200);
1233
+ query = query.order("created_at", {
1234
+ ascending: true
1235
+ }).limit(limit);
1236
+ const {
1237
+ data: messages,
1238
+ error: msgErr
1239
+ } = await query;
1240
+ if (msgErr) {
1241
+ return {
1242
+ status: 500,
1243
+ body: {
1244
+ error: msgErr.message
1245
+ }
1246
+ };
1247
+ }
1248
+ return {
1249
+ status: 200,
1250
+ body: {
1251
+ success: true,
1252
+ messages: messages || []
1253
+ }
1254
+ };
1255
+ }
1256
+
1257
+ // ── PATCH /channels/:id/messages/:msgId ──────────────────────
1258
+ // Node API key auth. Mark message as delivered.
1259
+ const deliverMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)$/);
1260
+ if (deliverMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
1261
+ const channelId = deliverMatch[1];
1262
+ const msgId = deliverMatch[2];
1263
+ const node = await authenticateNode(supabase, auth.rawToken);
1264
+ if (!node) {
1265
+ return {
1266
+ status: 401,
1267
+ body: {
1268
+ error: "Invalid node API key"
1269
+ }
1270
+ };
1271
+ }
1272
+ const {
1273
+ error
1274
+ } = await supabase.from("channel_messages").update({
1275
+ delivered_at: body?.delivered_at || new Date().toISOString(),
1276
+ metadata: {
1277
+ ...(body?.metadata || {}),
1278
+ delivered_by_node: node.id
1279
+ }
1280
+ }).eq("id", msgId).eq("channel_id", channelId);
1281
+ if (error) {
1282
+ return {
1283
+ status: 500,
1284
+ body: {
1285
+ error: error.message
1286
+ }
1287
+ };
1288
+ }
1289
+ return {
1290
+ status: 200,
1291
+ body: {
1292
+ success: true,
1293
+ delivered: msgId
1294
+ }
1295
+ };
1296
+ }
1297
+
1298
+ // ── POST /channels/:id/messages/:msgId/delivered ─────────────
1299
+ // Simpler delivery marking endpoint (no _method hack needed)
1300
+ const deliverSimpleMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)\/delivered$/);
1301
+ if (deliverSimpleMatch && method === "POST") {
1302
+ const channelId = deliverSimpleMatch[1];
1303
+ const msgId = deliverSimpleMatch[2];
1304
+ const node = await authenticateNode(supabase, auth.rawToken);
1305
+ if (!node) {
1306
+ return {
1307
+ status: 401,
1308
+ body: {
1309
+ error: "Invalid node API key"
1310
+ }
1311
+ };
1312
+ }
1313
+ const {
1314
+ error
1315
+ } = await supabase.from("channel_messages").update({
1316
+ delivered_at: new Date().toISOString()
1317
+ }).eq("id", msgId).eq("channel_id", channelId);
1318
+ if (error) {
1319
+ return {
1320
+ status: 500,
1321
+ body: {
1322
+ error: error.message
1323
+ }
1324
+ };
1325
+ }
1326
+ return {
1327
+ status: 200,
1328
+ body: {
1329
+ success: true,
1330
+ delivered: msgId
1331
+ }
1332
+ };
1333
+ }
1334
+
1335
+ // ── GET /nodes/:id/events ────────────────────────────────────
1336
+ // User auth. Lists recent events for a node.
1337
+ const eventsMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/events$/);
1338
+ if (eventsMatch && method === "GET") {
1339
+ const nodeId = eventsMatch[1];
1340
+ const limit = body?.limit || 50;
1341
+
1342
+ // Verify user has access to this node's store
1343
+ const {
1344
+ data: node
1345
+ } = await supabase.from("nodes").select("store_id").eq("id", nodeId).single();
1346
+ if (!node) {
1347
+ return {
1348
+ status: 404,
1349
+ body: {
1350
+ error: "Node not found"
1351
+ }
1352
+ };
1353
+ }
1354
+ if (auth.userId) {
1355
+ const hasAccess = await userHasStoreAccess(supabase, auth.userId, node.store_id);
1356
+ if (!hasAccess) {
853
1357
  return {
854
- status: 201,
855
- body: {
856
- success: true,
857
- message,
858
- agent_response: agentResponse,
859
- conversation_id: conversationId || message.conversation_id,
860
- },
1358
+ status: 403,
1359
+ body: {
1360
+ error: "Not authorized to view this node's events"
1361
+ }
861
1362
  };
1363
+ }
1364
+ } else if (!auth.isServiceRole) {
1365
+ return {
1366
+ status: 401,
1367
+ body: {
1368
+ error: "Authentication required"
1369
+ }
1370
+ };
862
1371
  }
863
- // ── GET /channels/:id/messages ─────────────────────────────────
864
- // Node API key auth. Retrieves messages for a channel.
865
- // Query params (via body): direction, undelivered, limit, after
866
- const getMessagesMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
867
- if (getMessagesMatch && method === "GET") {
868
- const channelId = getMessagesMatch[1];
869
- // Authenticate node (or service role for admin access)
870
- const node = await authenticateNode(supabase, auth.rawToken);
871
- if (!node && !auth.isServiceRole) {
872
- return { status: 401, body: { error: "Invalid node API key" } };
873
- }
874
- // Verify channel exists (and belongs to this node if node auth)
875
- let chQuery = supabase.from("channels").select("id, node_id").eq("id", channelId);
876
- if (node)
877
- chQuery = chQuery.eq("node_id", node.id);
878
- const { data: channel } = await chQuery.single();
879
- if (!channel) {
880
- return { status: 404, body: { error: "Channel not found" } };
881
- }
882
- let query = supabase
883
- .from("channel_messages")
884
- .select("id, direction, sender_id, sender_name, content, content_type, metadata, conversation_id, delivered_at, created_at")
885
- .eq("channel_id", channelId);
886
- // Filter by direction (body or query param)
887
- const direction = body?.direction || queryParams?.get("direction");
888
- if (direction) {
889
- query = query.eq("direction", direction);
890
- }
891
- // Filter undelivered outbound messages (for node polling)
892
- const undelivered = body?.undelivered || queryParams?.get("undelivered");
893
- if (undelivered === true || undelivered === "true") {
894
- query = query.eq("direction", "outbound").is("delivered_at", null);
895
- }
896
- // After a specific timestamp
897
- const after = body?.after || queryParams?.get("after");
898
- if (after) {
899
- query = query.gt("created_at", after);
900
- }
901
- const rawLimit = body?.limit || queryParams?.get("limit") || 50;
902
- const limit = Math.min(Number(rawLimit), 200);
903
- query = query.order("created_at", { ascending: true }).limit(limit);
904
- const { data: messages, error: msgErr } = await query;
905
- if (msgErr) {
906
- return { status: 500, body: { error: msgErr.message } };
907
- }
908
- return { status: 200, body: { success: true, messages: messages || [] } };
909
- }
910
- // ── PATCH /channels/:id/messages/:msgId ──────────────────────
911
- // Node API key auth. Mark message as delivered.
912
- const deliverMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)$/);
913
- if (deliverMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
914
- const channelId = deliverMatch[1];
915
- const msgId = deliverMatch[2];
916
- const node = await authenticateNode(supabase, auth.rawToken);
917
- if (!node) {
918
- return { status: 401, body: { error: "Invalid node API key" } };
919
- }
920
- const { error } = await supabase
921
- .from("channel_messages")
922
- .update({
923
- delivered_at: body?.delivered_at || new Date().toISOString(),
924
- metadata: { ...(body?.metadata || {}), delivered_by_node: node.id },
925
- })
926
- .eq("id", msgId)
927
- .eq("channel_id", channelId);
928
- if (error) {
929
- return { status: 500, body: { error: error.message } };
930
- }
931
- return { status: 200, body: { success: true, delivered: msgId } };
932
- }
933
- // ── POST /channels/:id/messages/:msgId/delivered ─────────────
934
- // Simpler delivery marking endpoint (no _method hack needed)
935
- const deliverSimpleMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)\/delivered$/);
936
- if (deliverSimpleMatch && method === "POST") {
937
- const channelId = deliverSimpleMatch[1];
938
- const msgId = deliverSimpleMatch[2];
939
- const node = await authenticateNode(supabase, auth.rawToken);
940
- if (!node) {
941
- return { status: 401, body: { error: "Invalid node API key" } };
942
- }
943
- const { error } = await supabase
944
- .from("channel_messages")
945
- .update({ delivered_at: new Date().toISOString() })
946
- .eq("id", msgId)
947
- .eq("channel_id", channelId);
948
- if (error) {
949
- return { status: 500, body: { error: error.message } };
950
- }
951
- return { status: 200, body: { success: true, delivered: msgId } };
952
- }
953
- // ── GET /nodes/:id/events ────────────────────────────────────
954
- // User auth. Lists recent events for a node.
955
- const eventsMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/events$/);
956
- if (eventsMatch && method === "GET") {
957
- const nodeId = eventsMatch[1];
958
- const limit = body?.limit || 50;
959
- // Verify user has access to this node's store
960
- const { data: node } = await supabase
961
- .from("nodes")
962
- .select("store_id")
963
- .eq("id", nodeId)
964
- .single();
965
- if (!node) {
966
- return { status: 404, body: { error: "Node not found" } };
967
- }
968
- if (auth.userId) {
969
- const hasAccess = await userHasStoreAccess(supabase, auth.userId, node.store_id);
970
- if (!hasAccess) {
971
- return { status: 403, body: { error: "Not authorized to view this node's events" } };
972
- }
973
- }
974
- else if (!auth.isServiceRole) {
975
- return { status: 401, body: { error: "Authentication required" } };
976
- }
977
- const { data: events, error } = await supabase
978
- .from("node_events")
979
- .select("id, event_type, details, created_at")
980
- .eq("node_id", nodeId)
981
- .order("created_at", { ascending: false })
982
- .limit(limit);
983
- if (error) {
984
- return { status: 500, body: { error: error.message } };
985
- }
986
- return { status: 200, body: { success: true, events: events || [] } };
1372
+ const {
1373
+ data: events,
1374
+ error
1375
+ } = await supabase.from("node_events").select("id, event_type, details, created_at").eq("node_id", nodeId).order("created_at", {
1376
+ ascending: false
1377
+ }).limit(limit);
1378
+ if (error) {
1379
+ return {
1380
+ status: 500,
1381
+ body: {
1382
+ error: error.message
1383
+ }
1384
+ };
987
1385
  }
988
- // No route matched
989
- return null;
1386
+ return {
1387
+ status: 200,
1388
+ body: {
1389
+ success: true,
1390
+ events: events || []
1391
+ }
1392
+ };
1393
+ }
1394
+
1395
+ // No route matched
1396
+ return null;
990
1397
  }
1398
+ //# sourceMappingURL=nodes.js.map