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,1021 +1,1494 @@
1
1
  import { sanitizeFilterValue, escapeCSV, fillTemplate, groupBy } from "../lib/utils.js";
2
2
  import { validateUrl } from "../lib/ssrf-guard.js";
3
- import { applyGenerationRules, generateCannabinoidData, applyCalculations, runValidation, generateFullPanelData, } from "../lib/pdf-renderer.js";
3
+ import { applyGenerationRules, generateCannabinoidData, applyCalculations, runValidation, generateFullPanelData } from "../lib/pdf-renderer.js";
4
4
  import { renderLayoutToPdf, renderHtmlToPdf, renderLabelToPdf } from "../lib/react-pdf-layout.js";
5
5
  import { renderCOAToPdf } from "../lib/coa-renderer.js";
6
6
  import QRCode from "qrcode";
7
+ import { handleBrowser } from "./browser.js";
7
8
  const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
8
9
  const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
9
10
  const MAX_ATTACHMENT_COUNT = 10;
11
+
12
+ /** Generate a PNG thumbnail for a PDF document via Playwright screenshot and store it in Supabase. */
13
+ async function generateThumbnail(sb, docId, pdfUrl) {
14
+ // Wrap in Google Docs viewer — headless Chromium downloads raw PDFs instead of rendering them
15
+ const viewerUrl = `https://docs.google.com/gview?url=${encodeURIComponent(pdfUrl)}&embedded=true`;
16
+ const result = await handleBrowser(sb, {
17
+ action: "screenshot",
18
+ url: viewerUrl
19
+ });
20
+ const b64 = result.data?.screenshot_base64;
21
+ if (!b64) return;
22
+ const thumbBuffer = Buffer.from(b64, 'base64');
23
+ const thumbPath = `thumbs/${docId}.png`;
24
+ await sb.storage.from('screenshots').upload(thumbPath, thumbBuffer, {
25
+ contentType: 'image/png',
26
+ upsert: true
27
+ });
28
+ const {
29
+ data: thumbUrlData
30
+ } = sb.storage.from('screenshots').getPublicUrl(thumbPath);
31
+ await sb.from('store_documents').update({
32
+ thumbnail_url: thumbUrlData.publicUrl
33
+ }).eq('id', docId);
34
+ }
10
35
  export async function handleEmail(sb, args, storeId) {
11
- if (!storeId)
12
- return { success: false, error: "store_id required" };
13
- const sid = storeId;
14
- switch (args.action) {
15
- case "inbox": {
16
- let q = sb.from("email_threads").select("*, latest_message:email_inbox(subject, from_email, created_at)")
17
- .eq("store_id", sid).order("updated_at", { ascending: false }).limit(args.limit || 25);
18
- if (args.status)
19
- q = q.eq("status", args.status);
20
- if (args.mailbox)
21
- q = q.eq("mailbox", args.mailbox);
22
- if (args.priority)
23
- q = q.eq("priority", args.priority);
24
- const { data, error } = await q;
25
- if (error)
26
- return { success: false, error: error.message };
27
- // Flatten latest_message join so subject/from appear as table columns
28
- const flattened = (data || []).map((row) => {
29
- const { latest_message, ...rest } = row;
30
- return { ...rest, subject: latest_message?.subject || null, from_email: latest_message?.from_email || null };
31
- });
32
- return { success: true, data: flattened };
36
+ if (!storeId) return {
37
+ success: false,
38
+ error: "store_id required"
39
+ };
40
+ const sid = storeId;
41
+ switch (args.action) {
42
+ case "inbox":
43
+ {
44
+ let q = sb.from("email_threads").select("*, latest_message:email_inbox(subject, from_email, created_at)").eq("store_id", sid).order("updated_at", {
45
+ ascending: false
46
+ }).limit(args.limit || 25);
47
+ if (args.status) q = q.eq("status", args.status);
48
+ if (args.mailbox) q = q.eq("mailbox", args.mailbox);
49
+ if (args.priority) q = q.eq("priority", args.priority);
50
+ const {
51
+ data,
52
+ error
53
+ } = await q;
54
+ if (error) return {
55
+ success: false,
56
+ error: error.message
57
+ };
58
+ // Flatten latest_message join so subject/from appear as table columns
59
+ const flattened = (data || []).map(row => {
60
+ const {
61
+ latest_message,
62
+ ...rest
63
+ } = row;
64
+ return {
65
+ ...rest,
66
+ subject: latest_message?.subject || null,
67
+ from_email: latest_message?.from_email || null
68
+ };
69
+ });
70
+ return {
71
+ success: true,
72
+ data: flattened
73
+ };
74
+ }
75
+ case "inbox_get":
76
+ {
77
+ const {
78
+ data,
79
+ error
80
+ } = await sb.from("email_threads").select("*, messages:email_inbox(id, subject, from_email, to_email, body_text, created_at)").eq("id", args.thread_id).eq("store_id", sid).single();
81
+ if (error) return {
82
+ success: false,
83
+ error: error.message
84
+ };
85
+ // Pre-format so messages are visible (formatter handles sub-tables but body gets truncated)
86
+ const {
87
+ messages,
88
+ ...thread
89
+ } = data;
90
+ const lines = [`## Email Thread`, `**Status**: ${thread.status || "—"} | **Mailbox**: ${thread.mailbox || "—"} | **Priority**: ${thread.priority || "—"}`, ""];
91
+ for (const msg of (messages || []).sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())) {
92
+ lines.push(`### ${msg.from_email} → ${msg.to_email || "—"} (${msg.created_at?.slice(0, 16) || "—"})`);
93
+ lines.push(`**Subject**: ${msg.subject || "(no subject)"}`);
94
+ const body = (msg.body_text || "").slice(0, 500);
95
+ lines.push(body + (msg.body_text?.length > 500 ? "..." : ""));
96
+ lines.push("");
33
97
  }
34
- case "inbox_get": {
35
- const { data, error } = await sb.from("email_threads")
36
- .select("*, messages:email_inbox(id, subject, from_email, to_email, body_text, created_at)").eq("id", args.thread_id).eq("store_id", sid).single();
37
- if (error)
38
- return { success: false, error: error.message };
39
- // Pre-format so messages are visible (formatter handles sub-tables but body gets truncated)
40
- const { messages, ...thread } = data;
41
- const lines = [
42
- `## Email Thread`,
43
- `**Status**: ${thread.status || "—"} | **Mailbox**: ${thread.mailbox || "—"} | **Priority**: ${thread.priority || "—"}`,
44
- "",
45
- ];
46
- for (const msg of (messages || []).sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())) {
47
- lines.push(`### ${msg.from_email} → ${msg.to_email || "—"} (${msg.created_at?.slice(0, 16) || "—"})`);
48
- lines.push(`**Subject**: ${msg.subject || "(no subject)"}`);
49
- const body = (msg.body_text || "").slice(0, 500);
50
- lines.push(body + (msg.body_text?.length > 500 ? "..." : ""));
51
- lines.push("");
52
- }
53
- return { success: true, data: lines.join("\n") };
98
+ return {
99
+ success: true,
100
+ data: lines.join("\n")
101
+ };
102
+ }
103
+ case "send":
104
+ {
105
+ // P0 FIX: Per-store email rate limit (100 emails/hour)
106
+ const oneHourAgo = new Date(Date.now() - 3600_000).toISOString();
107
+ const {
108
+ count: recentSends
109
+ } = await sb.from("email_sends").select("id", {
110
+ count: "exact",
111
+ head: true
112
+ }).eq("store_id", sid).gte("created_at", oneHourAgo);
113
+ if ((recentSends || 0) >= 100) {
114
+ return {
115
+ success: false,
116
+ error: "Email rate limit exceeded (100/hour). Try again later."
117
+ };
54
118
  }
55
- case "send": {
56
- // P0 FIX: Per-store email rate limit (100 emails/hour)
57
- const oneHourAgo = new Date(Date.now() - 3600_000).toISOString();
58
- const { count: recentSends } = await sb.from("email_sends")
59
- .select("id", { count: "exact", head: true })
60
- .eq("store_id", sid)
61
- .gte("created_at", oneHourAgo);
62
- if ((recentSends || 0) >= 100) {
63
- return { success: false, error: "Email rate limit exceeded (100/hour). Try again later." };
119
+
120
+ // Invoke send-email edge function
121
+ const sbUrl = process.env["SUPABASE_URL"];
122
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
123
+ try {
124
+ // Resolve URL-based attachments to base64 for Resend API
125
+ let attachments;
126
+ const rawAttachments = args.attachments;
127
+ if (rawAttachments && rawAttachments.length > 0) {
128
+ // P0 FIX: Enforce attachment count limit
129
+ if (rawAttachments.length > MAX_ATTACHMENT_COUNT) {
130
+ throw new Error(`Too many attachments: ${rawAttachments.length} (max ${MAX_ATTACHMENT_COUNT})`);
64
131
  }
65
- // Invoke send-email edge function
66
- const sbUrl = process.env["SUPABASE_URL"];
67
- const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
68
- try {
69
- // Resolve URL-based attachments to base64 for Resend API
70
- let attachments;
71
- const rawAttachments = args.attachments;
72
- if (rawAttachments && rawAttachments.length > 0) {
73
- // P0 FIX: Enforce attachment count limit
74
- if (rawAttachments.length > MAX_ATTACHMENT_COUNT) {
75
- throw new Error(`Too many attachments: ${rawAttachments.length} (max ${MAX_ATTACHMENT_COUNT})`);
76
- }
77
- attachments = [];
78
- let totalBytes = 0;
79
- for (const att of rawAttachments) {
80
- if (att.url) {
81
- // P0 FIX: Validate URL to prevent SSRF via attachment URLs
82
- const ssrfErr = await validateUrl(att.url);
83
- if (ssrfErr)
84
- throw new Error(`Attachment URL blocked: ${ssrfErr}`);
85
- // Fetch the file and convert to base64
86
- const fileResp = await fetch(att.url);
87
- if (!fileResp.ok)
88
- throw new Error(`Failed to fetch attachment: ${att.url} (${fileResp.status})`);
89
- const buffer = await fileResp.arrayBuffer();
90
- // P0 FIX: Enforce 10MB per-attachment size limit
91
- if (buffer.byteLength > MAX_ATTACHMENT_BYTES) {
92
- throw new Error(`Attachment too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
93
- }
94
- // P0 FIX: Enforce 25MB total attachment size limit
95
- totalBytes += buffer.byteLength;
96
- if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
97
- throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
98
- }
99
- const base64 = Buffer.from(buffer).toString("base64");
100
- const filename = att.filename || att.url.split("/").pop() || "attachment";
101
- attachments.push({ filename, content: base64 });
102
- }
103
- else if (att.content && att.filename) {
104
- // Already base64 — decode length to check size
105
- const contentBytes = Math.ceil((att.content.length * 3) / 4);
106
- if (contentBytes > MAX_ATTACHMENT_BYTES) {
107
- throw new Error(`Attachment too large: ${(contentBytes / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
108
- }
109
- totalBytes += contentBytes;
110
- if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
111
- throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
112
- }
113
- attachments.push({ filename: att.filename, content: att.content });
114
- }
115
- }
132
+ attachments = [];
133
+ let totalBytes = 0;
134
+ for (const att of rawAttachments) {
135
+ if (att.url) {
136
+ // P0 FIX: Validate URL to prevent SSRF via attachment URLs
137
+ const ssrfErr = await validateUrl(att.url);
138
+ if (ssrfErr) throw new Error(`Attachment URL blocked: ${ssrfErr}`);
139
+ // Fetch the file and convert to base64
140
+ const fileResp = await fetch(att.url);
141
+ if (!fileResp.ok) throw new Error(`Failed to fetch attachment: ${att.url} (${fileResp.status})`);
142
+ const buffer = await fileResp.arrayBuffer();
143
+ // P0 FIX: Enforce 10MB per-attachment size limit
144
+ if (buffer.byteLength > MAX_ATTACHMENT_BYTES) {
145
+ throw new Error(`Attachment too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
116
146
  }
117
- // Resolve {{variable}} placeholders in email fields against template_data
118
- let emailHtml = args.html;
119
- let emailSubject = args.subject;
120
- let emailText = args.text;
121
- if (args.template_data && typeof args.template_data === "object") {
122
- const tplData = args.template_data;
123
- if (emailHtml)
124
- emailHtml = fillTemplate(emailHtml, tplData);
125
- if (emailSubject)
126
- emailSubject = fillTemplate(emailSubject, tplData);
127
- if (emailText)
128
- emailText = fillTemplate(emailText, tplData);
147
+ // P0 FIX: Enforce 25MB total attachment size limit
148
+ totalBytes += buffer.byteLength;
149
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
150
+ throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
129
151
  }
130
- const payload = { to: args.to, subject: emailSubject, html: emailHtml, text: emailText, storeId: sid };
131
- if (attachments && attachments.length > 0)
132
- payload.attachments = attachments;
133
- const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
134
- method: "POST",
135
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
136
- body: JSON.stringify(payload)
152
+ const base64 = Buffer.from(buffer).toString("base64");
153
+ const filename = att.filename || att.url.split("/").pop() || "attachment";
154
+ attachments.push({
155
+ filename,
156
+ content: base64
137
157
  });
138
- const result = await resp.json();
139
- return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Send failed" };
140
- }
141
- catch (err) {
142
- return { success: false, error: `Email send failed: ${err}` };
143
- }
144
- }
145
- case "send_template": {
146
- const sbUrl = process.env["SUPABASE_URL"];
147
- const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
148
- try {
149
- const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
150
- method: "POST",
151
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
152
- body: JSON.stringify({ to: args.to, template: args.template, template_data: args.template_data, storeId: sid })
153
- });
154
- const result = await resp.json();
155
- return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Send failed" };
156
- }
157
- catch (err) {
158
- return { success: false, error: `Template send failed: ${err}` };
159
- }
160
- }
161
- case "list": {
162
- const { data, error } = await sb.from("email_sends").select("*")
163
- .eq("store_id", sid).order("created_at", { ascending: false }).limit(args.limit || 50);
164
- return error ? { success: false, error: error.message } : { success: true, data };
165
- }
166
- case "get": {
167
- const { data, error } = await sb.from("email_sends")
168
- .select("*").eq("id", args.email_id).eq("store_id", sid).single();
169
- return error ? { success: false, error: error.message } : { success: true, data };
170
- }
171
- case "templates": {
172
- const { data, error } = await sb.from("email_templates").select("*")
173
- .eq("store_id", sid).eq("is_active", true).limit(100);
174
- return error ? { success: false, error: error.message } : { success: true, data };
175
- }
176
- case "inbox_reply": {
177
- const sbUrl = process.env["SUPABASE_URL"];
178
- const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
179
- try {
180
- const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
181
- method: "POST",
182
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
183
- body: JSON.stringify({ to: args.to, subject: args.subject, html: args.html, text: args.text, thread_id: args.thread_id, storeId: sid })
158
+ } else if (att.content && att.filename) {
159
+ // Already base64 decode length to check size
160
+ const contentBytes = Math.ceil(att.content.length * 3 / 4);
161
+ if (contentBytes > MAX_ATTACHMENT_BYTES) {
162
+ throw new Error(`Attachment too large: ${(contentBytes / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
163
+ }
164
+ totalBytes += contentBytes;
165
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
166
+ throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
167
+ }
168
+ attachments.push({
169
+ filename: att.filename,
170
+ content: att.content
184
171
  });
185
- const result = await resp.json();
186
- return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Reply failed" };
187
- }
188
- catch (err) {
189
- return { success: false, error: `Reply failed: ${err}` };
172
+ }
190
173
  }
174
+ }
175
+
176
+ // Resolve {{variable}} placeholders in email fields against template_data
177
+ let emailHtml = args.html;
178
+ let emailSubject = args.subject;
179
+ let emailText = args.text;
180
+ if (args.template_data && typeof args.template_data === "object") {
181
+ const tplData = args.template_data;
182
+ if (emailHtml) emailHtml = fillTemplate(emailHtml, tplData);
183
+ if (emailSubject) emailSubject = fillTemplate(emailSubject, tplData);
184
+ if (emailText) emailText = fillTemplate(emailText, tplData);
185
+ }
186
+ const payload = {
187
+ to: args.to,
188
+ subject: emailSubject,
189
+ html: emailHtml,
190
+ text: emailText,
191
+ storeId: sid
192
+ };
193
+ if (attachments && attachments.length > 0) payload.attachments = attachments;
194
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ "Authorization": `Bearer ${sbKey}`
199
+ },
200
+ body: JSON.stringify(payload)
201
+ });
202
+ const result = await resp.json();
203
+ return resp.ok ? {
204
+ success: true,
205
+ data: result
206
+ } : {
207
+ success: false,
208
+ error: result.error || "Send failed"
209
+ };
210
+ } catch (err) {
211
+ return {
212
+ success: false,
213
+ error: `Email send failed: ${err}`
214
+ };
191
215
  }
192
- case "inbox_update": {
193
- const updates = {};
194
- if (args.status)
195
- updates.status = args.status;
196
- if (args.priority)
197
- updates.priority = args.priority;
198
- if (args.intent)
199
- updates.ai_intent = args.intent;
200
- if (args.ai_summary)
201
- updates.ai_summary = args.ai_summary;
202
- const { data, error } = await sb.from("email_threads")
203
- .update(updates).eq("id", args.thread_id).eq("store_id", sid).select().single();
204
- return error ? { success: false, error: error.message } : { success: true, data };
216
+ }
217
+ case "send_template":
218
+ {
219
+ const sbUrl = process.env["SUPABASE_URL"];
220
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
221
+ try {
222
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
223
+ method: "POST",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ "Authorization": `Bearer ${sbKey}`
227
+ },
228
+ body: JSON.stringify({
229
+ to: args.to,
230
+ template: args.template,
231
+ template_data: args.template_data,
232
+ storeId: sid
233
+ })
234
+ });
235
+ const result = await resp.json();
236
+ return resp.ok ? {
237
+ success: true,
238
+ data: result
239
+ } : {
240
+ success: false,
241
+ error: result.error || "Send failed"
242
+ };
243
+ } catch (err) {
244
+ return {
245
+ success: false,
246
+ error: `Template send failed: ${err}`
247
+ };
205
248
  }
206
- case "inbox_stats": {
207
- const { data, error } = await sb.from("email_threads")
208
- .select("status, mailbox, priority").eq("store_id", sid).limit(1000);
209
- if (error)
210
- return { success: false, error: error.message };
211
- const stats = {
212
- total: data.length,
213
- by_status: groupBy(data, "status"),
214
- by_mailbox: groupBy(data, "mailbox"),
215
- by_priority: groupBy(data, "priority")
216
- };
217
- return { success: true, data: stats };
249
+ }
250
+ case "list":
251
+ {
252
+ const {
253
+ data,
254
+ error
255
+ } = await sb.from("email_sends").select("*").eq("store_id", sid).order("created_at", {
256
+ ascending: false
257
+ }).limit(args.limit || 50);
258
+ return error ? {
259
+ success: false,
260
+ error: error.message
261
+ } : {
262
+ success: true,
263
+ data
264
+ };
265
+ }
266
+ case "get":
267
+ {
268
+ const {
269
+ data,
270
+ error
271
+ } = await sb.from("email_sends").select("*").eq("id", args.email_id).eq("store_id", sid).single();
272
+ return error ? {
273
+ success: false,
274
+ error: error.message
275
+ } : {
276
+ success: true,
277
+ data
278
+ };
279
+ }
280
+ case "templates":
281
+ {
282
+ const {
283
+ data,
284
+ error
285
+ } = await sb.from("email_templates").select("*").eq("store_id", sid).eq("is_active", true).limit(100);
286
+ return error ? {
287
+ success: false,
288
+ error: error.message
289
+ } : {
290
+ success: true,
291
+ data
292
+ };
293
+ }
294
+ case "inbox_reply":
295
+ {
296
+ const sbUrl = process.env["SUPABASE_URL"];
297
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
298
+ try {
299
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
300
+ method: "POST",
301
+ headers: {
302
+ "Content-Type": "application/json",
303
+ "Authorization": `Bearer ${sbKey}`
304
+ },
305
+ body: JSON.stringify({
306
+ to: args.to,
307
+ subject: args.subject,
308
+ html: args.html,
309
+ text: args.text,
310
+ thread_id: args.thread_id,
311
+ storeId: sid
312
+ })
313
+ });
314
+ const result = await resp.json();
315
+ return resp.ok ? {
316
+ success: true,
317
+ data: result
318
+ } : {
319
+ success: false,
320
+ error: result.error || "Reply failed"
321
+ };
322
+ } catch (err) {
323
+ return {
324
+ success: false,
325
+ error: `Reply failed: ${err}`
326
+ };
218
327
  }
219
- case "create_template": {
220
- if (!args.name)
221
- return { success: false, error: "name is required" };
222
- if (!args.subject)
223
- return { success: false, error: "subject is required" };
224
- const slug = args.slug || args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
225
- const { data, error } = await sb.from("email_templates").insert({
226
- store_id: sid,
227
- name: args.name,
228
- slug,
229
- subject: args.subject,
230
- category: args.category || "general",
231
- description: args.description || null,
232
- html_content: args.html_content || null,
233
- preview_text: args.preview_text || null,
234
- text_content: args.text_content || null,
235
- is_active: true,
236
- }).select("id, name, slug, subject, category, created_at").single();
237
- if (error)
238
- return { success: false, error: error.message };
239
- return { success: true, data };
240
- }
241
- case "update_template": {
242
- if (!args.template_id)
243
- return { success: false, error: "template_id is required" };
244
- const updates = {};
245
- if (args.name !== undefined)
246
- updates.name = args.name;
247
- if (args.slug !== undefined)
248
- updates.slug = args.slug;
249
- if (args.subject !== undefined)
250
- updates.subject = args.subject;
251
- if (args.category !== undefined)
252
- updates.category = args.category;
253
- if (args.description !== undefined)
254
- updates.description = args.description;
255
- if (args.html_content !== undefined)
256
- updates.html_content = args.html_content;
257
- if (args.preview_text !== undefined)
258
- updates.preview_text = args.preview_text;
259
- if (args.text_content !== undefined)
260
- updates.text_content = args.text_content;
261
- if (Object.keys(updates).length === 0)
262
- return { success: false, error: "No fields to update" };
263
- const { data, error } = await sb.from("email_templates")
264
- .update(updates).eq("id", args.template_id).eq("store_id", sid)
265
- .select("id, name, slug, subject, category, updated_at").single();
266
- if (error)
267
- return { success: false, error: error.message };
268
- return { success: true, data };
269
- }
270
- case "delete_template": {
271
- if (!args.template_id)
272
- return { success: false, error: "template_id is required" };
273
- const { data, error } = await sb.from("email_templates")
274
- .update({ is_active: false }).eq("id", args.template_id).eq("store_id", sid)
275
- .select("id, name").single();
276
- if (error)
277
- return { success: false, error: error.message };
278
- return { success: true, data: { ...data, deleted: true } };
279
- }
280
- default:
281
- return { success: false, error: `Unknown email action: ${args.action}. Valid: send, send_template, list, get, templates, inbox, inbox_get, inbox_reply, inbox_update, inbox_stats, create_template, update_template, delete_template` };
282
- }
328
+ }
329
+ case "inbox_update":
330
+ {
331
+ const updates = {};
332
+ if (args.status) updates.status = args.status;
333
+ if (args.priority) updates.priority = args.priority;
334
+ if (args.intent) updates.ai_intent = args.intent;
335
+ if (args.ai_summary) updates.ai_summary = args.ai_summary;
336
+ const {
337
+ data,
338
+ error
339
+ } = await sb.from("email_threads").update(updates).eq("id", args.thread_id).eq("store_id", sid).select().single();
340
+ return error ? {
341
+ success: false,
342
+ error: error.message
343
+ } : {
344
+ success: true,
345
+ data
346
+ };
347
+ }
348
+ case "inbox_stats":
349
+ {
350
+ const {
351
+ data,
352
+ error
353
+ } = await sb.from("email_threads").select("status, mailbox, priority").eq("store_id", sid).limit(1000);
354
+ if (error) return {
355
+ success: false,
356
+ error: error.message
357
+ };
358
+ const stats = {
359
+ total: data.length,
360
+ by_status: groupBy(data, "status"),
361
+ by_mailbox: groupBy(data, "mailbox"),
362
+ by_priority: groupBy(data, "priority")
363
+ };
364
+ return {
365
+ success: true,
366
+ data: stats
367
+ };
368
+ }
369
+ case "create_template":
370
+ {
371
+ if (!args.name) return {
372
+ success: false,
373
+ error: "name is required"
374
+ };
375
+ if (!args.subject) return {
376
+ success: false,
377
+ error: "subject is required"
378
+ };
379
+ const slug = args.slug || args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
380
+ const {
381
+ data,
382
+ error
383
+ } = await sb.from("email_templates").insert({
384
+ store_id: sid,
385
+ name: args.name,
386
+ slug,
387
+ subject: args.subject,
388
+ category: args.category || "general",
389
+ description: args.description || null,
390
+ html_content: args.html_content || null,
391
+ preview_text: args.preview_text || null,
392
+ text_content: args.text_content || null,
393
+ is_active: true
394
+ }).select("id, name, slug, subject, category, created_at").single();
395
+ if (error) return {
396
+ success: false,
397
+ error: error.message
398
+ };
399
+ return {
400
+ success: true,
401
+ data
402
+ };
403
+ }
404
+ case "update_template":
405
+ {
406
+ if (!args.template_id) return {
407
+ success: false,
408
+ error: "template_id is required"
409
+ };
410
+ const updates = {};
411
+ if (args.name !== undefined) updates.name = args.name;
412
+ if (args.slug !== undefined) updates.slug = args.slug;
413
+ if (args.subject !== undefined) updates.subject = args.subject;
414
+ if (args.category !== undefined) updates.category = args.category;
415
+ if (args.description !== undefined) updates.description = args.description;
416
+ if (args.html_content !== undefined) updates.html_content = args.html_content;
417
+ if (args.preview_text !== undefined) updates.preview_text = args.preview_text;
418
+ if (args.text_content !== undefined) updates.text_content = args.text_content;
419
+ if (Object.keys(updates).length === 0) return {
420
+ success: false,
421
+ error: "No fields to update"
422
+ };
423
+ const {
424
+ data,
425
+ error
426
+ } = await sb.from("email_templates").update(updates).eq("id", args.template_id).eq("store_id", sid).select("id, name, slug, subject, category, updated_at").single();
427
+ if (error) return {
428
+ success: false,
429
+ error: error.message
430
+ };
431
+ return {
432
+ success: true,
433
+ data
434
+ };
435
+ }
436
+ case "delete_template":
437
+ {
438
+ if (!args.template_id) return {
439
+ success: false,
440
+ error: "template_id is required"
441
+ };
442
+ const {
443
+ data,
444
+ error
445
+ } = await sb.from("email_templates").update({
446
+ is_active: false
447
+ }).eq("id", args.template_id).eq("store_id", sid).select("id, name").single();
448
+ if (error) return {
449
+ success: false,
450
+ error: error.message
451
+ };
452
+ return {
453
+ success: true,
454
+ data: {
455
+ ...data,
456
+ deleted: true
457
+ }
458
+ };
459
+ }
460
+ default:
461
+ return {
462
+ success: false,
463
+ error: `Unknown email action: ${args.action}. Valid: send, send_template, list, get, templates, inbox, inbox_get, inbox_reply, inbox_update, inbox_stats, create_template, update_template, delete_template`
464
+ };
465
+ }
283
466
  }
284
467
  export async function handleDocuments(sb, args, storeId) {
285
- if (!storeId)
286
- return { success: false, error: "store_id required" };
287
- const sid = storeId;
288
- const action = args.action;
289
- switch (action) {
290
- case "create": {
291
- const docType = args.document_type || "text";
292
- const name = args.name;
293
- if (!name)
294
- return { success: false, error: "name is required" };
295
- const extMap = { csv: "csv", json: "json", text: "txt", markdown: "md", html: "html", pdf: "pdf" };
296
- const mimeMap = {
297
- csv: "text/csv", json: "application/json", text: "text/plain",
298
- markdown: "text/markdown", html: "text/html", pdf: "application/pdf",
468
+ if (!storeId) return {
469
+ success: false,
470
+ error: "store_id required"
471
+ };
472
+ const sid = storeId;
473
+ const action = args.action;
474
+ switch (action) {
475
+ case "create":
476
+ {
477
+ const docType = args.document_type || "text";
478
+ const name = args.name;
479
+ if (!name) return {
480
+ success: false,
481
+ error: "name is required"
482
+ };
483
+ const extMap = {
484
+ csv: "csv",
485
+ json: "json",
486
+ text: "txt",
487
+ markdown: "md",
488
+ html: "html",
489
+ pdf: "pdf"
490
+ };
491
+ const mimeMap = {
492
+ csv: "text/csv",
493
+ json: "application/json",
494
+ text: "text/plain",
495
+ markdown: "text/markdown",
496
+ html: "text/html",
497
+ pdf: "application/pdf"
498
+ };
499
+ const ext = extMap[docType] || "txt";
500
+ const mime = mimeMap[docType] || "text/plain";
501
+ const safeName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
502
+ const fileName = `${safeName}_${Date.now()}.${ext}`;
503
+ const storagePath = `${sid}/${fileName}`;
504
+ let uploadBuffer;
505
+ if (docType === "pdf") {
506
+ // PDF generation from HTML content via React-PDF
507
+ const htmlContent = args.html || args.content;
508
+ if (!htmlContent) return {
509
+ success: false,
510
+ error: "html or content (HTML string) is required for PDF documents"
511
+ };
512
+ try {
513
+ const pdfBuffer = await renderHtmlToPdf(htmlContent, {
514
+ format: args.format || "A4",
515
+ landscape: args.landscape || false
516
+ });
517
+ uploadBuffer = new Uint8Array(pdfBuffer);
518
+ } catch (err) {
519
+ return {
520
+ success: false,
521
+ error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}`
299
522
  };
300
- const ext = extMap[docType] || "txt";
301
- const mime = mimeMap[docType] || "text/plain";
302
- const safeName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
303
- const fileName = `${safeName}_${Date.now()}.${ext}`;
304
- const storagePath = `${sid}/${fileName}`;
305
- let uploadBuffer;
306
- if (docType === "pdf") {
307
- // PDF generation from HTML content via React-PDF
308
- const htmlContent = args.html || args.content;
309
- if (!htmlContent)
310
- return { success: false, error: "html or content (HTML string) is required for PDF documents" };
311
- try {
312
- const pdfBuffer = await renderHtmlToPdf(htmlContent, {
313
- format: args.format || "A4",
314
- landscape: args.landscape || false,
315
- });
316
- uploadBuffer = new Uint8Array(pdfBuffer);
317
- }
318
- catch (err) {
319
- return { success: false, error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}` };
320
- }
321
- }
322
- else {
323
- let content;
324
- if (docType === "csv") {
325
- const headers = args.headers;
326
- const rows = args.rows;
327
- if (!headers || !rows)
328
- return { success: false, error: "headers and rows required for CSV" };
329
- const lines = [headers.map(escapeCSV).join(",")];
330
- for (const row of rows) {
331
- if (Array.isArray(row)) {
332
- lines.push(row.map(escapeCSV).join(","));
333
- }
334
- else {
335
- lines.push(headers.map(h => escapeCSV(row[h])).join(","));
336
- }
337
- }
338
- content = lines.join("\n");
339
- }
340
- else if (docType === "json") {
341
- const jsonData = args.data || args.content;
342
- content = typeof jsonData === "string" ? jsonData : JSON.stringify(jsonData, null, 2);
343
- }
344
- else {
345
- content = args.content || "";
346
- }
347
- uploadBuffer = new TextEncoder().encode(content);
523
+ }
524
+ } else {
525
+ let content;
526
+ if (docType === "csv") {
527
+ const headers = args.headers;
528
+ const rows = args.rows;
529
+ if (!headers || !rows) return {
530
+ success: false,
531
+ error: "headers and rows required for CSV"
532
+ };
533
+ const lines = [headers.map(escapeCSV).join(",")];
534
+ for (const row of rows) {
535
+ if (Array.isArray(row)) {
536
+ lines.push(row.map(escapeCSV).join(","));
537
+ } else {
538
+ lines.push(headers.map(h => escapeCSV(row[h])).join(","));
539
+ }
348
540
  }
349
- const { error: uploadErr } = await sb.storage
350
- .from("documents")
351
- .upload(storagePath, uploadBuffer, { contentType: mime, upsert: true });
352
- if (uploadErr)
353
- return { success: false, error: `Upload failed: ${uploadErr.message}` };
354
- const { data: urlData } = sb.storage.from("documents").getPublicUrl(storagePath);
355
- const fileUrl = urlData.publicUrl;
356
- const sizeBytes = uploadBuffer.length;
357
- const { data: record, error: insertErr } = await sb.from("store_documents").insert({
358
- store_id: sid,
359
- document_type: docType,
360
- file_name: fileName,
361
- file_url: fileUrl,
362
- file_size: sizeBytes,
363
- file_type: mime,
364
- document_name: name,
365
- source_name: "Documents Edge Function",
366
- document_date: new Date().toISOString().split("T")[0],
367
- data: { document_type: docType },
368
- metadata: { size_bytes: sizeBytes },
369
- }).select("id, document_name, file_url, created_at").single();
370
- if (insertErr)
371
- return { success: false, error: insertErr.message };
372
- return { success: true, data: { id: record.id, name: record.document_name, type: docType, url: record.file_url, file_name: fileName, size: sizeBytes } };
541
+ content = lines.join("\n");
542
+ } else if (docType === "json") {
543
+ const jsonData = args.data || args.content;
544
+ content = typeof jsonData === "string" ? jsonData : JSON.stringify(jsonData, null, 2);
545
+ } else {
546
+ content = args.content || "";
547
+ }
548
+ uploadBuffer = new TextEncoder().encode(content);
373
549
  }
374
- case "find": {
375
- let query = sb.from("store_documents")
376
- .select("id, document_type, document_name, reference_number, file_url, file_type, file_size, created_at, metadata");
377
- query = query.eq("store_id", sid);
378
- if (args.document_type)
379
- query = query.eq("document_type", args.document_type);
380
- if (args.name) {
381
- const sn = sanitizeFilterValue(args.name);
382
- query = query.ilike("document_name", `%${sn}%`);
383
- }
384
- query = query.order("created_at", { ascending: false }).limit(args.limit || 50);
385
- const { data, error } = await query;
386
- if (error)
387
- return { success: false, error: error.message };
388
- return {
389
- success: true,
390
- data: {
391
- count: data?.length || 0,
392
- documents: (data || []).map(d => ({
393
- id: d.id, type: d.document_type, name: d.document_name,
394
- reference: d.reference_number, url: d.file_url,
395
- size: d.file_size, created: d.created_at,
396
- })),
397
- },
398
- };
550
+ const {
551
+ error: uploadErr
552
+ } = await sb.storage.from("documents").upload(storagePath, uploadBuffer, {
553
+ contentType: mime,
554
+ upsert: true
555
+ });
556
+ if (uploadErr) return {
557
+ success: false,
558
+ error: `Upload failed: ${uploadErr.message}`
559
+ };
560
+ const {
561
+ data: urlData
562
+ } = sb.storage.from("documents").getPublicUrl(storagePath);
563
+ const fileUrl = urlData.publicUrl;
564
+ const sizeBytes = uploadBuffer.length;
565
+ const {
566
+ data: record,
567
+ error: insertErr
568
+ } = await sb.from("store_documents").insert({
569
+ store_id: sid,
570
+ document_type: docType,
571
+ file_name: fileName,
572
+ file_url: fileUrl,
573
+ file_size: sizeBytes,
574
+ file_type: mime,
575
+ document_name: name,
576
+ source_name: "Documents Edge Function",
577
+ document_date: new Date().toISOString().split("T")[0],
578
+ data: {
579
+ document_type: docType
580
+ },
581
+ metadata: {
582
+ size_bytes: sizeBytes
583
+ }
584
+ }).select("id, document_name, file_url, created_at").single();
585
+ if (insertErr) return {
586
+ success: false,
587
+ error: insertErr.message
588
+ };
589
+ if (fileUrl.endsWith('.pdf')) await generateThumbnail(sb, record.id, fileUrl).catch(() => {});
590
+ return {
591
+ success: true,
592
+ data: {
593
+ id: record.id,
594
+ name: record.document_name,
595
+ type: docType,
596
+ url: record.file_url,
597
+ file_name: fileName,
598
+ size: sizeBytes
599
+ }
600
+ };
601
+ }
602
+ case "find":
603
+ {
604
+ let query = sb.from("store_documents").select("id, document_type, document_name, reference_number, file_url, file_type, file_size, created_at, metadata");
605
+ query = query.eq("store_id", sid);
606
+ if (args.document_type) query = query.eq("document_type", args.document_type);
607
+ if (args.name) {
608
+ const sn = sanitizeFilterValue(args.name);
609
+ query = query.ilike("document_name", `%${sn}%`);
399
610
  }
400
- case "delete": {
401
- if (!args.confirm)
402
- return { success: false, error: "Set confirm: true to delete" };
403
- let query = sb.from("store_documents").delete().eq("store_id", sid);
404
- if (args.document_type)
405
- query = query.eq("document_type", args.document_type);
406
- if (args.name) {
407
- const sn = sanitizeFilterValue(args.name);
408
- query = query.ilike("document_name", `%${sn}%`);
409
- }
410
- const { data, error } = await query.select("id");
411
- if (error)
412
- return { success: false, error: error.message };
413
- return { success: true, data: { deleted: data?.length || 0 } };
611
+ query = query.order("created_at", {
612
+ ascending: false
613
+ }).limit(args.limit || 50);
614
+ const {
615
+ data,
616
+ error
617
+ } = await query;
618
+ if (error) return {
619
+ success: false,
620
+ error: error.message
621
+ };
622
+ return {
623
+ success: true,
624
+ data: {
625
+ count: data?.length || 0,
626
+ documents: (data || []).map(d => ({
627
+ id: d.id,
628
+ type: d.document_type,
629
+ name: d.document_name,
630
+ reference: d.reference_number,
631
+ url: d.file_url,
632
+ size: d.file_size,
633
+ created: d.created_at
634
+ }))
635
+ }
636
+ };
637
+ }
638
+ case "delete":
639
+ {
640
+ if (!args.confirm) return {
641
+ success: false,
642
+ error: "Set confirm: true to delete"
643
+ };
644
+ let query = sb.from("store_documents").delete().eq("store_id", sid);
645
+ if (args.document_type) query = query.eq("document_type", args.document_type);
646
+ if (args.name) {
647
+ const sn = sanitizeFilterValue(args.name);
648
+ query = query.ilike("document_name", `%${sn}%`);
414
649
  }
415
- case "create_template": {
416
- if (!args.name)
417
- return { success: false, error: "name is required" };
418
- if (!args.document_type)
419
- return { success: false, error: "document_type is required" };
420
- const { data, error } = await sb.from("document_templates").insert({
421
- store_id: sid || null,
422
- name: args.name,
423
- description: args.description || null,
424
- document_type: args.document_type,
425
- content: args.content || null,
426
- headers: args.headers || null,
427
- schema: args.schema || [],
428
- metadata: args.data || {},
429
- }).select("id, name, document_type, created_at").single();
430
- if (error)
431
- return { success: false, error: error.message };
432
- return { success: true, data: { template_id: data.id, name: data.name, type: data.document_type } };
650
+ const {
651
+ data,
652
+ error
653
+ } = await query.select("id");
654
+ if (error) return {
655
+ success: false,
656
+ error: error.message
657
+ };
658
+ return {
659
+ success: true,
660
+ data: {
661
+ deleted: data?.length || 0
662
+ }
663
+ };
664
+ }
665
+ case "create_template":
666
+ {
667
+ if (!args.name) return {
668
+ success: false,
669
+ error: "name is required"
670
+ };
671
+ if (!args.document_type) return {
672
+ success: false,
673
+ error: "document_type is required"
674
+ };
675
+ const {
676
+ data,
677
+ error
678
+ } = await sb.from("document_templates").insert({
679
+ store_id: sid || null,
680
+ name: args.name,
681
+ description: args.description || null,
682
+ document_type: args.document_type,
683
+ content: args.content || null,
684
+ headers: args.headers || null,
685
+ schema: args.schema || [],
686
+ metadata: args.data || {}
687
+ }).select("id, name, document_type, created_at").single();
688
+ if (error) return {
689
+ success: false,
690
+ error: error.message
691
+ };
692
+ return {
693
+ success: true,
694
+ data: {
695
+ template_id: data.id,
696
+ name: data.name,
697
+ type: data.document_type
698
+ }
699
+ };
700
+ }
701
+ case "list_templates":
702
+ {
703
+ let query = sb.from("document_templates").select("id, name, description, document_type, headers, schema, created_at").eq("is_active", true);
704
+ if (sid) query = query.or(`store_id.eq.${sid},store_id.is.null`);
705
+ if (args.document_type) query = query.eq("document_type", args.document_type);
706
+ if (args.limit) query = query.limit(args.limit);
707
+ query = query.order("created_at", {
708
+ ascending: false
709
+ });
710
+ const {
711
+ data,
712
+ error
713
+ } = await query;
714
+ if (error) return {
715
+ success: false,
716
+ error: error.message
717
+ };
718
+ return {
719
+ success: true,
720
+ data: {
721
+ count: data?.length || 0,
722
+ templates: (data || []).map(t => ({
723
+ id: t.id,
724
+ name: t.name,
725
+ description: t.description,
726
+ type: t.document_type,
727
+ headers: t.headers,
728
+ fields: t.schema?.length || 0
729
+ }))
730
+ }
731
+ };
732
+ }
733
+ case "from_template":
734
+ {
735
+ const templateId = args.template_id;
736
+ if (!templateId) return {
737
+ success: false,
738
+ error: "template_id is required"
739
+ };
740
+ const {
741
+ data: template,
742
+ error: tErr
743
+ } = await sb.from("document_templates").select("*").eq("id", templateId).or(`store_id.eq.${sid},store_id.is.null`).single();
744
+ if (tErr || !template) return {
745
+ success: false,
746
+ error: "Template not found"
747
+ };
748
+ const tData = {
749
+ ...template.metadata,
750
+ ...(args.data || {}),
751
+ date: new Date().toISOString().split("T")[0]
752
+ };
753
+ const docType = template.document_type;
754
+ const docName = args.name || fillTemplate(template.name, tData);
755
+ let content;
756
+ if (docType === "csv") {
757
+ const headers = template.headers || [];
758
+ const rows = args.rows;
759
+ if (!rows) return {
760
+ success: false,
761
+ error: "rows required for CSV template"
762
+ };
763
+ const lines = [headers.map(escapeCSV).join(",")];
764
+ for (const row of rows) {
765
+ if (Array.isArray(row)) lines.push(row.map(escapeCSV).join(","));else lines.push(headers.map(h => escapeCSV(row[h])).join(","));
766
+ }
767
+ content = lines.join("\n");
768
+ } else if (docType === "json") {
769
+ content = template.content ? fillTemplate(template.content, tData) : JSON.stringify(tData, null, 2);
770
+ } else {
771
+ content = template.content ? fillTemplate(template.content, tData) : "";
433
772
  }
434
- case "list_templates": {
435
- let query = sb.from("document_templates")
436
- .select("id, name, description, document_type, headers, schema, created_at")
437
- .eq("is_active", true);
438
- if (sid)
439
- query = query.or(`store_id.eq.${sid},store_id.is.null`);
440
- if (args.document_type)
441
- query = query.eq("document_type", args.document_type);
442
- if (args.limit)
443
- query = query.limit(args.limit);
444
- query = query.order("created_at", { ascending: false });
445
- const { data, error } = await query;
446
- if (error)
447
- return { success: false, error: error.message };
448
- return {
449
- success: true,
450
- data: {
451
- count: data?.length || 0,
452
- templates: (data || []).map(t => ({
453
- id: t.id, name: t.name, description: t.description,
454
- type: t.document_type, headers: t.headers,
455
- fields: t.schema?.length || 0,
456
- })),
457
- },
458
- };
773
+ const extMap = {
774
+ csv: "csv",
775
+ json: "json",
776
+ text: "txt",
777
+ markdown: "md",
778
+ html: "html"
779
+ };
780
+ const mimeMap = {
781
+ csv: "text/csv",
782
+ json: "application/json",
783
+ text: "text/plain",
784
+ markdown: "text/markdown",
785
+ html: "text/html"
786
+ };
787
+ const ext = extMap[docType] || "txt";
788
+ const mime = mimeMap[docType] || "text/plain";
789
+ const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
790
+ const fileName = `${safeName}_${Date.now()}.${ext}`;
791
+ const storagePath = `${sid}/${fileName}`;
792
+ const {
793
+ error: uploadErr
794
+ } = await sb.storage.from("documents").upload(storagePath, new TextEncoder().encode(content), {
795
+ contentType: mime,
796
+ upsert: true
797
+ });
798
+ if (uploadErr) return {
799
+ success: false,
800
+ error: `Upload failed: ${uploadErr.message}`
801
+ };
802
+ const {
803
+ data: urlData
804
+ } = sb.storage.from("documents").getPublicUrl(storagePath);
805
+ const sizeBytes = new TextEncoder().encode(content).length;
806
+ const {
807
+ data: record,
808
+ error: insertErr
809
+ } = await sb.from("store_documents").insert({
810
+ store_id: sid,
811
+ document_type: docType,
812
+ file_name: fileName,
813
+ file_url: urlData.publicUrl,
814
+ file_size: sizeBytes,
815
+ file_type: mime,
816
+ document_name: docName,
817
+ source_name: "Documents Edge Function",
818
+ document_date: new Date().toISOString().split("T")[0],
819
+ data: {
820
+ template_id: template.id,
821
+ template_name: template.name
822
+ },
823
+ metadata: {
824
+ size_bytes: sizeBytes,
825
+ from_template: true
826
+ }
827
+ }).select("id, document_name, file_url, created_at").single();
828
+ if (insertErr) return {
829
+ success: false,
830
+ error: insertErr.message
831
+ };
832
+ if (urlData.publicUrl.endsWith('.pdf')) await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => {});
833
+ return {
834
+ success: true,
835
+ data: {
836
+ id: record.id,
837
+ name: record.document_name,
838
+ type: docType,
839
+ template: template.name,
840
+ url: record.file_url,
841
+ size: sizeBytes
842
+ }
843
+ };
844
+ }
845
+ case "list_stores":
846
+ {
847
+ const {
848
+ data,
849
+ error
850
+ } = await sb.from("stores").select("id, store_name, slug, status, created_at").eq("id", sid).order("store_name");
851
+ if (error) return {
852
+ success: false,
853
+ error: error.message
854
+ };
855
+ return {
856
+ success: true,
857
+ data: {
858
+ count: data?.length || 0,
859
+ stores: data
860
+ }
861
+ };
862
+ }
863
+ case "list_profiles":
864
+ {
865
+ const {
866
+ data,
867
+ error
868
+ } = await sb.from("document_profiles").select("id, name, category, sample_type, config, template_id, is_active, created_at").eq("is_active", true).eq("owner_store_id", sid).order("name");
869
+ if (error) return {
870
+ success: false,
871
+ error: error.message
872
+ };
873
+ return {
874
+ success: true,
875
+ data: {
876
+ count: data?.length || 0,
877
+ profiles: (data || []).map(p => ({
878
+ id: p.id,
879
+ name: p.name,
880
+ category: p.category,
881
+ sample_type: p.sample_type,
882
+ template_id: p.template_id,
883
+ has_config: Object.keys(p.config || {}).length > 0
884
+ }))
885
+ }
886
+ };
887
+ }
888
+ case "generate":
889
+ {
890
+ // Generate a document from a template profile with store data
891
+ const templateId = args.template_id;
892
+ if (!templateId) return {
893
+ success: false,
894
+ error: "template_id is required. Use list_profiles to find available templates."
895
+ };
896
+ // Route to from_template with the same args
897
+ return handleDocuments(sb, {
898
+ ...args,
899
+ action: "from_template"
900
+ }, storeId);
901
+ }
902
+ case "bulk_generate":
903
+ {
904
+ const templateId = args.template_id;
905
+ if (!templateId) return {
906
+ success: false,
907
+ error: "template_id required"
908
+ };
909
+ const items = args.items;
910
+ if (!items || !Array.isArray(items) || items.length === 0) {
911
+ return {
912
+ success: false,
913
+ error: "items required: array of variable sets for each document"
914
+ };
459
915
  }
460
- case "from_template": {
461
- const templateId = args.template_id;
462
- if (!templateId)
463
- return { success: false, error: "template_id is required" };
464
- const { data: template, error: tErr } = await sb.from("document_templates")
465
- .select("*").eq("id", templateId).or(`store_id.eq.${sid},store_id.is.null`).single();
466
- if (tErr || !template)
467
- return { success: false, error: "Template not found" };
468
- const tData = { ...template.metadata, ...(args.data || {}), date: new Date().toISOString().split("T")[0] };
469
- const docType = template.document_type;
470
- const docName = args.name || fillTemplate(template.name, tData);
471
- let content;
472
- if (docType === "csv") {
473
- const headers = template.headers || [];
474
- const rows = args.rows;
475
- if (!rows)
476
- return { success: false, error: "rows required for CSV template" };
477
- const lines = [headers.map(escapeCSV).join(",")];
478
- for (const row of rows) {
479
- if (Array.isArray(row))
480
- lines.push(row.map(escapeCSV).join(","));
481
- else
482
- lines.push(headers.map(h => escapeCSV(row[h])).join(","));
483
- }
484
- content = lines.join("\n");
916
+ if (items.length > 50) {
917
+ return {
918
+ success: false,
919
+ error: "Maximum 50 documents per bulk_generate call"
920
+ };
921
+ }
922
+ const results = [];
923
+ for (const item of items) {
924
+ const itemArgs = {
925
+ ...args,
926
+ action: "from_template",
927
+ data: item,
928
+ name: item.name || undefined
929
+ };
930
+ const result = await handleDocuments(sb, itemArgs, storeId);
931
+ if (result.success && result.data) {
932
+ results.push({
933
+ name: result.data.name || "",
934
+ url: result.data.url || ""
935
+ });
936
+ } else {
937
+ results.push({
938
+ name: String(item.name || ""),
939
+ url: "",
940
+ error: result.error || "Generation failed"
941
+ });
942
+ }
943
+ }
944
+ const succeeded = results.filter(r => !r.error).length;
945
+ return {
946
+ success: true,
947
+ data: {
948
+ total: items.length,
949
+ succeeded,
950
+ failed: items.length - succeeded,
951
+ results
952
+ }
953
+ };
954
+ }
955
+
956
+ // ================================================================
957
+ // PDF TEMPLATE SYSTEM — full layout rendering, calculations, profiles
958
+ // ================================================================
959
+
960
+ case "generate_pdf":
961
+ {
962
+ const templateId = args.template_id;
963
+ const templateSlug = args.template_slug;
964
+ if (!templateId && !templateSlug) return {
965
+ success: false,
966
+ error: "template_id or template_slug required"
967
+ };
968
+
969
+ // 1. Load pdf_template
970
+ let tplQuery = sb.from("pdf_templates").select("*").eq("is_active", true);
971
+ if (templateId) tplQuery = tplQuery.eq("id", templateId);else tplQuery = tplQuery.eq("slug", templateSlug);
972
+ tplQuery = tplQuery.or(`store_id.eq.${sid},store_id.is.null`);
973
+ const {
974
+ data: tpl,
975
+ error: tplErr
976
+ } = await tplQuery.limit(1).single();
977
+ if (tplErr || !tpl) return {
978
+ success: false,
979
+ error: `Template not found: ${templateId || templateSlug}`
980
+ };
981
+
982
+ // 2. Optionally load document_profile + its client store
983
+ let profileConfig = {};
984
+ let profileConstants = {};
985
+ let profileClientData = {};
986
+ let resolvedClientStoreId = null;
987
+ if (args.profile_id) {
988
+ const {
989
+ data: profile
990
+ } = await sb.from("document_profiles").select("*").eq("id", args.profile_id).eq("is_active", true).single();
991
+ if (profile) {
992
+ const cfg = profile.config || {};
993
+ profileConfig = cfg;
994
+ profileConstants = cfg.constants || {};
995
+ resolvedClientStoreId = profile.client_store_id || null;
996
+ // Load client info from the profile's linked store
997
+ if (profile.client_store_id) {
998
+ const {
999
+ data: clientStore
1000
+ } = await sb.from("stores").select("store_name, legal_name, address, city, state, zip, distributor_license_number, phone, email").eq("id", profile.client_store_id).single();
1001
+ if (clientStore) {
1002
+ profileClientData = {
1003
+ clientName: clientStore.legal_name || clientStore.store_name,
1004
+ clientAddress: [clientStore.address, clientStore.city, clientStore.state, clientStore.zip].filter(Boolean).join(", ") || null,
1005
+ licenseNumber: clientStore.distributor_license_number || null,
1006
+ clientPhone: clientStore.phone || null,
1007
+ clientEmail: clientStore.email || null
1008
+ };
1009
+ }
485
1010
  }
486
- else if (docType === "json") {
487
- content = template.content ? fillTemplate(template.content, tData) : JSON.stringify(tData, null, 2);
1011
+ // Use profile name as sampleName if not already set
1012
+ if (profile.name && !profileConstants.sampleName) {
1013
+ profileClientData.sampleName = profile.name;
488
1014
  }
489
- else {
490
- content = template.content ? fillTemplate(template.content, tData) : "";
1015
+ if (profile.sample_type) {
1016
+ profileClientData.sampleType = profile.sample_type;
491
1017
  }
492
- const extMap = { csv: "csv", json: "json", text: "txt", markdown: "md", html: "html" };
493
- const mimeMap = {
494
- csv: "text/csv", json: "application/json", text: "text/plain",
495
- markdown: "text/markdown", html: "text/html",
496
- };
497
- const ext = extMap[docType] || "txt";
498
- const mime = mimeMap[docType] || "text/plain";
499
- const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
500
- const fileName = `${safeName}_${Date.now()}.${ext}`;
501
- const storagePath = `${sid}/${fileName}`;
502
- const { error: uploadErr } = await sb.storage
503
- .from("documents")
504
- .upload(storagePath, new TextEncoder().encode(content), { contentType: mime, upsert: true });
505
- if (uploadErr)
506
- return { success: false, error: `Upload failed: ${uploadErr.message}` };
507
- const { data: urlData } = sb.storage.from("documents").getPublicUrl(storagePath);
508
- const sizeBytes = new TextEncoder().encode(content).length;
509
- const { data: record, error: insertErr } = await sb.from("store_documents").insert({
510
- store_id: sid,
511
- document_type: docType,
512
- file_name: fileName,
513
- file_url: urlData.publicUrl,
514
- file_size: sizeBytes,
515
- file_type: mime,
516
- document_name: docName,
517
- source_name: "Documents Edge Function",
518
- document_date: new Date().toISOString().split("T")[0],
519
- data: { template_id: template.id, template_name: template.name },
520
- metadata: { size_bytes: sizeBytes, from_template: true },
521
- }).select("id, document_name, file_url, created_at").single();
522
- if (insertErr)
523
- return { success: false, error: insertErr.message };
524
- return { success: true, data: { id: record.id, name: record.document_name, type: docType, template: template.name, url: record.file_url, size: sizeBytes } };
525
- }
526
- case "list_stores": {
527
- const { data, error } = await sb.from("stores")
528
- .select("id, store_name, slug, status, created_at")
529
- .eq("id", sid)
530
- .order("store_name");
531
- if (error)
532
- return { success: false, error: error.message };
533
- return { success: true, data: { count: data?.length || 0, stores: data } };
1018
+ if (profile.default_size) {
1019
+ profileClientData.sampleSize = profile.default_size;
1020
+ }
1021
+ }
534
1022
  }
535
- case "list_profiles": {
536
- const { data, error } = await sb.from("document_profiles")
537
- .select("id, name, category, sample_type, config, template_id, is_active, created_at")
538
- .eq("is_active", true)
539
- .eq("owner_store_id", sid)
540
- .order("name");
541
- if (error)
542
- return { success: false, error: error.message };
543
- return {
544
- success: true,
545
- data: {
546
- count: data?.length || 0,
547
- profiles: (data || []).map(p => ({
548
- id: p.id, name: p.name, category: p.category,
549
- sample_type: p.sample_type, template_id: p.template_id,
550
- has_config: Object.keys(p.config || {}).length > 0,
551
- })),
552
- },
1023
+
1024
+ // 3. Optionally load customer (via store_customer_profiles + platform_users) — overrides profile client
1025
+ let clientData = {};
1026
+ if (args.customer_id) {
1027
+ const {
1028
+ data: rel
1029
+ } = await sb.from("user_creation_relationships").select("id, store_id, platform_users!inner(first_name, last_name, email, phone)").eq("id", args.customer_id).single();
1030
+ if (rel) {
1031
+ const pu = rel.platform_users;
1032
+ const {
1033
+ data: prof
1034
+ } = await sb.from("store_customer_profiles").select("street_address, city, state, postal_code, drivers_license_number, medical_card_number").eq("relationship_id", rel.id).maybeSingle();
1035
+ clientData = {
1036
+ clientName: [pu.first_name, pu.last_name].filter(Boolean).join(" "),
1037
+ clientEmail: pu.email,
1038
+ clientPhone: pu.phone,
1039
+ clientAddress: prof ? [prof.street_address, prof.city, prof.state, prof.postal_code].filter(Boolean).join(", ") : null,
1040
+ clientLicense: prof?.drivers_license_number || null,
1041
+ clientMedicalCard: prof?.medical_card_number || null
553
1042
  };
1043
+ }
554
1044
  }
555
- case "generate": {
556
- // Generate a document from a template profile with store data
557
- const templateId = args.template_id;
558
- if (!templateId)
559
- return { success: false, error: "template_id is required. Use list_profiles to find available templates." };
560
- // Route to from_template with the same args
561
- return handleDocuments(sb, { ...args, action: "from_template" }, storeId);
1045
+
1046
+ // 4. Merge data: constants profile config ← profile client explicit client ← user data
1047
+ const tplConstants = tpl.constants || {};
1048
+ const mergedConstants = {
1049
+ ...tplConstants,
1050
+ ...profileConstants
1051
+ };
1052
+ const cannabinoidCfg = profileConfig.cannabinoids ?? undefined;
1053
+ const {
1054
+ cannabinoids: _cfgRanges,
1055
+ ...profileDataOnly
1056
+ } = profileConfig;
1057
+ let mergedData = {
1058
+ ...mergedConstants,
1059
+ ...profileDataOnly,
1060
+ ...profileClientData,
1061
+ ...clientData,
1062
+ ...(args.data || {}),
1063
+ date: new Date().toISOString().slice(0, 10),
1064
+ approvalDate: new Date().toISOString().slice(0, 10)
1065
+ };
1066
+
1067
+ // 5. Apply generation rules — auto-fill sampleId, dates, etc.
1068
+ mergedData = applyGenerationRules(tpl.generation_rules, mergedData);
1069
+
1070
+ // 6. Generate cannabinoid data if profile has cannabinoid config
1071
+ if (cannabinoidCfg && !mergedData.cannabinoids) {
1072
+ mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
562
1073
  }
563
- case "bulk_generate": {
564
- const templateId = args.template_id;
565
- if (!templateId)
566
- return { success: false, error: "template_id required" };
567
- const items = args.items;
568
- if (!items || !Array.isArray(items) || items.length === 0) {
569
- return { success: false, error: "items required: array of variable sets for each document" };
570
- }
571
- if (items.length > 50) {
572
- return { success: false, error: "Maximum 50 documents per bulk_generate call" };
573
- }
574
- const results = [];
575
- for (const item of items) {
576
- const itemArgs = { ...args, action: "from_template", data: item, name: item.name || undefined };
577
- const result = await handleDocuments(sb, itemArgs, storeId);
578
- if (result.success && result.data) {
579
- results.push({ name: result.data.name || "", url: result.data.url || "" });
580
- }
581
- else {
582
- results.push({ name: String(item.name || ""), url: "", error: result.error || "Generation failed" });
583
- }
1074
+
1075
+ // 6b. Flatten cannabinoid rows into top-level keys for calculation formulas
1076
+ // e.g. { name: "D9-THC", percentWeight: 0.22 } → D9_THC: 0.22
1077
+ if (Array.isArray(mergedData.cannabinoids)) {
1078
+ for (const row of mergedData.cannabinoids) {
1079
+ const key = row.name.replace(/-/g, "_").replace(/[^a-zA-Z0-9_]/g, "");
1080
+ if (key && row.percentWeight !== undefined) {
1081
+ mergedData[key] = row.percentWeight;
584
1082
  }
585
- const succeeded = results.filter(r => !r.error).length;
586
- return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
1083
+ }
587
1084
  }
588
- // ================================================================
589
- // PDF TEMPLATE SYSTEM — full layout rendering, calculations, profiles
590
- // ================================================================
591
- case "generate_pdf": {
592
- const templateId = args.template_id;
593
- const templateSlug = args.template_slug;
594
- if (!templateId && !templateSlug)
595
- return { success: false, error: "template_id or template_slug required" };
596
- // 1. Load pdf_template
597
- let tplQuery = sb.from("pdf_templates").select("*").eq("is_active", true);
598
- if (templateId)
599
- tplQuery = tplQuery.eq("id", templateId);
600
- else
601
- tplQuery = tplQuery.eq("slug", templateSlug);
602
- tplQuery = tplQuery.or(`store_id.eq.${sid},store_id.is.null`);
603
- const { data: tpl, error: tplErr } = await tplQuery.limit(1).single();
604
- if (tplErr || !tpl)
605
- return { success: false, error: `Template not found: ${templateId || templateSlug}` };
606
- // 2. Optionally load document_profile + its client store
607
- let profileConfig = {};
608
- let profileConstants = {};
609
- let profileClientData = {};
610
- let resolvedClientStoreId = null;
611
- if (args.profile_id) {
612
- const { data: profile } = await sb.from("document_profiles")
613
- .select("*").eq("id", args.profile_id).eq("is_active", true).single();
614
- if (profile) {
615
- const cfg = (profile.config || {});
616
- profileConfig = cfg;
617
- profileConstants = (cfg.constants || {});
618
- resolvedClientStoreId = profile.client_store_id || null;
619
- // Load client info from the profile's linked store
620
- if (profile.client_store_id) {
621
- const { data: clientStore } = await sb.from("stores")
622
- .select("store_name, legal_name, address, city, state, zip, distributor_license_number, phone, email")
623
- .eq("id", profile.client_store_id).single();
624
- if (clientStore) {
625
- profileClientData = {
626
- clientName: clientStore.legal_name || clientStore.store_name,
627
- clientAddress: [clientStore.address, clientStore.city, clientStore.state, clientStore.zip].filter(Boolean).join(", ") || null,
628
- licenseNumber: clientStore.distributor_license_number || null,
629
- clientPhone: clientStore.phone || null,
630
- clientEmail: clientStore.email || null,
631
- };
632
- }
633
- }
634
- // Use profile name as sampleName if not already set
635
- if (profile.name && !profileConstants.sampleName) {
636
- profileClientData.sampleName = profile.name;
637
- }
638
- if (profile.sample_type) {
639
- profileClientData.sampleType = profile.sample_type;
640
- }
641
- if (profile.default_size) {
642
- profileClientData.sampleSize = profile.default_size;
643
- }
644
- }
645
- }
646
- // 3. Optionally load customer (via store_customer_profiles + platform_users) — overrides profile client
647
- let clientData = {};
648
- if (args.customer_id) {
649
- const { data: rel } = await sb.from("user_creation_relationships")
650
- .select("id, store_id, platform_users!inner(first_name, last_name, email, phone)")
651
- .eq("id", args.customer_id).single();
652
- if (rel) {
653
- const pu = rel.platform_users;
654
- const { data: prof } = await sb.from("store_customer_profiles")
655
- .select("street_address, city, state, postal_code, drivers_license_number, medical_card_number")
656
- .eq("relationship_id", rel.id).maybeSingle();
657
- clientData = {
658
- clientName: [pu.first_name, pu.last_name].filter(Boolean).join(" "),
659
- clientEmail: pu.email,
660
- clientPhone: pu.phone,
661
- clientAddress: prof ? [prof.street_address, prof.city, prof.state, prof.postal_code].filter(Boolean).join(", ") : null,
662
- clientLicense: prof?.drivers_license_number || null,
663
- clientMedicalCard: prof?.medical_card_number || null,
664
- };
665
- }
666
- }
667
- // 4. Merge data: constants ← profile config ← profile client ← explicit client ← user data
668
- const tplConstants = (tpl.constants || {});
669
- const mergedConstants = { ...tplConstants, ...profileConstants };
670
- const cannabinoidCfg = (profileConfig.cannabinoids ?? undefined);
671
- const { cannabinoids: _cfgRanges, ...profileDataOnly } = profileConfig;
672
- let mergedData = {
673
- ...mergedConstants,
674
- ...profileDataOnly,
675
- ...profileClientData,
676
- ...clientData,
677
- ...(args.data || {}),
678
- date: new Date().toISOString().slice(0, 10),
679
- approvalDate: new Date().toISOString().slice(0, 10),
1085
+
1086
+ // 7. Apply calculations
1087
+ mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
1088
+
1089
+ // Also run per-row calculations if cannabinoids exist
1090
+ if (Array.isArray(mergedData.cannabinoids)) {
1091
+ const rows = mergedData.cannabinoids;
1092
+ for (const row of rows) {
1093
+ const rowCtx = {
1094
+ ...mergedConstants,
1095
+ ...row
680
1096
  };
681
- // 5. Apply generation rules — auto-fill sampleId, dates, etc.
682
- mergedData = applyGenerationRules(tpl.generation_rules, mergedData);
683
- // 6. Generate cannabinoid data if profile has cannabinoid config
684
- if (cannabinoidCfg && !mergedData.cannabinoids) {
685
- mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
686
- }
687
- // 7. Apply calculations
688
- mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
689
- // Also run per-row calculations if cannabinoids exist
690
- if (Array.isArray(mergedData.cannabinoids)) {
691
- const rows = mergedData.cannabinoids;
692
- for (const row of rows) {
693
- const rowCtx = { ...mergedConstants, ...row };
694
- const rowCalcs = (tpl.row_calculations || tpl.calculations);
695
- if (rowCalcs) {
696
- const computed = applyCalculations(rowCalcs, mergedConstants, rowCtx);
697
- Object.assign(row, computed);
698
- }
699
- }
700
- }
701
- // 7b. Generate full panel data if requested (safety tests, pesticides, solvents)
702
- if (mergedData.fullPanel && !mergedData.microbialResults) {
703
- const tplConstants = (tpl.constants || {});
704
- const fullPanel = generateFullPanelData(tplConstants);
705
- mergedData.microbialResults = fullPanel.microbialResults;
706
- mergedData.heavyMetalsResults = fullPanel.heavyMetalsResults;
707
- mergedData.mycotoxinResults = fullPanel.mycotoxinResults;
708
- mergedData.pesticidesCat1 = fullPanel.pesticidesCat1;
709
- mergedData.pesticidesCat2 = fullPanel.pesticidesCat2;
710
- mergedData.testsResidualSolvents = fullPanel.residualSolventsResults;
711
- mergedData.totalPages = 5;
712
- mergedData.complianceResults = mergedData.complianceResults || {
713
- heavyMetals: { status: "PASS" }, microbial: { status: "PASS" },
714
- pesticides: { status: "PASS" }, mycotoxins: { status: "PASS" },
715
- residualSolvents: { status: "PASS" },
716
- };
717
- }
718
- // 7c. Generate QR code for COA templates — links to Quantix COA landing page
719
- if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
720
- const productSlug = (mergedData.sampleName || "certificate")
721
- .toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
722
- const coaUrl = `https://quantixanalytics.com/coa/${sid}/${productSlug}`;
723
- try {
724
- mergedData.qrCodeDataUrl = await QRCode.toDataURL(coaUrl, { width: 120, margin: 1 });
725
- }
726
- catch { /* skip QR if generation fails */ }
727
- }
728
- // 8. Validate
729
- const validationResults = runValidation(tpl.validation_rules, mergedData);
730
- const errors = validationResults.filter(v => v.severity === "error" && !v.passed);
731
- const warnings = validationResults.filter(v => v.severity === "warning" && !v.passed);
732
- if (errors.length > 0 && !args.force) {
733
- return {
734
- success: false,
735
- error: `Validation failed: ${errors.map(e => e.message).join("; ")}`,
736
- data: { errors, warnings },
737
- };
738
- }
739
- // 9-10. Render PDF — all templates use React-PDF
740
- let pdfBuffer;
741
- try {
742
- if (tpl.slug?.startsWith("cannabis-coa")) {
743
- // COA templates use dedicated renderer
744
- pdfBuffer = await renderCOAToPdf(mergedData);
745
- }
746
- else {
747
- // Detect label layout vs generic layout
748
- const layout = tpl.layout;
749
- const isLabel = layout?.version === 1 && Array.isArray(layout?.elements) && layout.elements[0]?.field;
750
- if (isLabel) {
751
- const items = Array.isArray(mergedData.items)
752
- ? mergedData.items
753
- : [mergedData];
754
- pdfBuffer = await renderLabelToPdf(layout, tpl.page_config, items);
755
- }
756
- else {
757
- pdfBuffer = await renderLayoutToPdf(tpl.layout, tpl.page_config, tpl.styles, mergedData);
758
- }
759
- }
760
- }
761
- catch (err) {
762
- return { success: false, error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}` };
1097
+ const rowCalcs = tpl.row_calculations || tpl.calculations;
1098
+ if (rowCalcs) {
1099
+ const computed = applyCalculations(rowCalcs, mergedConstants, rowCtx);
1100
+ Object.assign(row, computed);
763
1101
  }
764
- // 11. Upload to Supabase storage
765
- const sampleName = mergedData.sampleName || mergedData.productName || "";
766
- const docName = args.name || (sampleName ? `${sampleName} COA` : fillTemplate(tpl.name || "Document", mergedData));
767
- const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
768
- const fileName = `${safeName}_${Date.now()}.pdf`;
769
- const storagePath = `${sid}/${fileName}`;
770
- const bucketName = "store-documents";
771
- const { error: uploadErr } = await sb.storage
772
- .from(bucketName)
773
- .upload(storagePath, new Uint8Array(pdfBuffer), { contentType: "application/pdf", upsert: true });
774
- if (uploadErr)
775
- return { success: false, error: `Upload failed: ${uploadErr.message}` };
776
- const { data: urlData } = sb.storage.from(bucketName).getPublicUrl(storagePath);
777
- // 12. Build COA metadata for the Quantix landing page
778
- const coaMetadata = {
779
- size_bytes: pdfBuffer.length,
780
- from_pdf_template: true,
781
- template_version: tpl.version,
782
- };
783
- if (tpl.slug?.startsWith("cannabis-coa")) {
784
- coaMetadata.sample_name = sampleName;
785
- coaMetadata.sample_id = mergedData.sampleId || null;
786
- coaMetadata.sample_type = mergedData.sampleType || mergedData.strain || null;
787
- coaMetadata.sample_size = mergedData.sampleSize || null;
788
- coaMetadata.batch_number = mergedData.batchId || null;
789
- coaMetadata.lab_name = mergedData.labName || null;
790
- coaMetadata.lab_contact = mergedData.labContact || null;
791
- coaMetadata.lab_website = mergedData.labWebsite || null;
792
- coaMetadata.lab_director = mergedData.labDirector || null;
793
- coaMetadata.director_title = mergedData.directorTitle || null;
794
- coaMetadata.logo_url = mergedData.logoUrl || null;
795
- coaMetadata.signature_url = mergedData.signatureUrl || null;
796
- coaMetadata.client_name = mergedData.clientName || null;
797
- coaMetadata.client_address = mergedData.clientAddress || null;
798
- coaMetadata.date_collected = mergedData.dateCollected || null;
799
- coaMetadata.date_received = mergedData.dateReceived || null;
800
- coaMetadata.date_tested = mergedData.dateTested || null;
801
- coaMetadata.date_reported = mergedData.dateReported || null;
802
- coaMetadata.status = "Pass";
803
- coaMetadata.thc_total = mergedData.totalTHC || null;
804
- coaMetadata.cbd_total = mergedData.totalCBD || null;
805
- coaMetadata.cannabinoids_total = mergedData.totalCannabinoids || null;
806
- coaMetadata.moisture = mergedData.moisture || null;
807
- // Detailed cannabinoid data for the interactive landing page
808
- const cannabinoids = mergedData.cannabinoids;
809
- if (Array.isArray(cannabinoids)) {
810
- coaMetadata.cannabinoids = Object.fromEntries(cannabinoids.filter((c) => typeof c.percentWeight === "number" && c.percentWeight > 0)
811
- .map((c) => [c.name, c.percentWeight]));
812
- coaMetadata.cannabinoids_detailed = cannabinoids.map((c) => ({
813
- name: c.name, percent: c.percentWeight, mg_per_g: c.mgPerG, lod: c.lod, loq: c.loq, result: String(c.result ?? c.percentWeight),
814
- }));
815
- }
816
- // Safety test panels
817
- if (mergedData.fullPanel) {
818
- coaMetadata.test_panels = {
819
- cannabinoids: true, microbial: true, heavy_metals: true,
820
- mycotoxins: true, pesticides: true, residual_solvents: true,
821
- };
822
- coaMetadata.safety_tests = {
823
- microbial: "Pass", heavy_metals: "Pass", mycotoxins: "Pass",
824
- pesticides: "Pass", residual_solvents: "Pass",
825
- };
826
- }
1102
+ }
1103
+ }
1104
+
1105
+ // 7b. Generate full panel data if requested (safety tests, pesticides, solvents)
1106
+ if (mergedData.fullPanel && !mergedData.microbialResults) {
1107
+ const tplConstants = tpl.constants || {};
1108
+ const fullPanel = generateFullPanelData(tplConstants);
1109
+ mergedData.microbialResults = fullPanel.microbialResults;
1110
+ mergedData.heavyMetalsResults = fullPanel.heavyMetalsResults;
1111
+ mergedData.mycotoxinResults = fullPanel.mycotoxinResults;
1112
+ mergedData.pesticidesCat1 = fullPanel.pesticidesCat1;
1113
+ mergedData.pesticidesCat2 = fullPanel.pesticidesCat2;
1114
+ mergedData.testsResidualSolvents = fullPanel.residualSolventsResults;
1115
+ mergedData.totalPages = 5;
1116
+ mergedData.complianceResults = mergedData.complianceResults || {
1117
+ heavyMetals: {
1118
+ status: "PASS"
1119
+ },
1120
+ microbial: {
1121
+ status: "PASS"
1122
+ },
1123
+ pesticides: {
1124
+ status: "PASS"
1125
+ },
1126
+ mycotoxins: {
1127
+ status: "PASS"
1128
+ },
1129
+ residualSolvents: {
1130
+ status: "PASS"
827
1131
  }
828
- // 13. Insert store_documents record
829
- const { data: record, error: insertErr } = await sb.from("store_documents").insert({
830
- store_id: sid,
831
- document_type: tpl.document_type || "pdf",
832
- file_name: fileName,
833
- file_url: urlData.publicUrl,
834
- file_size: pdfBuffer.length,
835
- file_type: "application/pdf",
836
- document_name: docName,
837
- source_name: "PDF Template Engine",
838
- document_date: new Date().toISOString().split("T")[0],
839
- customer_id: args.customer_id || null,
840
- client_store_id: resolvedClientStoreId,
841
- data: { template_id: tpl.id, template_slug: tpl.slug, profile_id: args.profile_id || null },
842
- metadata: coaMetadata,
843
- }).select("id, document_name, file_url, created_at").single();
844
- if (insertErr)
845
- return { success: false, error: insertErr.message };
846
- return {
847
- success: true,
848
- data: {
849
- id: record.id,
850
- name: record.document_name,
851
- url: record.file_url,
852
- template: tpl.name,
853
- size: pdfBuffer.length,
854
- warnings: warnings.map(w => w.message),
855
- },
856
- };
1132
+ };
857
1133
  }
858
- case "list_pdf_templates": {
859
- let query = sb.from("pdf_templates")
860
- .select("id, slug, name, description, document_type, version, created_at")
861
- .eq("is_active", true)
862
- .or(`store_id.eq.${sid},store_id.is.null`)
863
- .order("name");
864
- if (args.document_type)
865
- query = query.eq("document_type", args.document_type);
866
- if (args.limit)
867
- query = query.limit(args.limit);
868
- const { data, error } = await query;
869
- if (error)
870
- return { success: false, error: error.message };
871
- return {
872
- success: true,
873
- data: {
874
- count: data?.length || 0,
875
- templates: (data || []).map(t => ({
876
- id: t.id, slug: t.slug, name: t.name,
877
- description: t.description, type: t.document_type, version: t.version,
878
- })),
879
- },
880
- };
1134
+
1135
+ // 7b2. Auto-set test status flags for summary section
1136
+ if (mergedData.cannabinoids && mergedData.cannabinoids.length > 0) {
1137
+ mergedData.testsCannabinoids = true;
881
1138
  }
882
- case "list_document_profiles": {
883
- let query = sb.from("document_profiles")
884
- .select("id, name, category, sample_type, default_size, client_store_id, template_id, created_at")
885
- .eq("is_active", true)
886
- .eq("owner_store_id", sid)
887
- .order("name");
888
- if (args.template_id)
889
- query = query.eq("template_id", args.template_id);
890
- if (args.category)
891
- query = query.eq("category", args.category);
892
- if (args.limit)
893
- query = query.limit(args.limit);
894
- const { data, error } = await query;
895
- if (error)
896
- return { success: false, error: error.message };
897
- return {
898
- success: true,
899
- data: {
900
- count: data?.length || 0,
901
- profiles: (data || []).map(p => ({
902
- id: p.id, name: p.name, category: p.category,
903
- sample_type: p.sample_type, default_size: p.default_size,
904
- client_store_id: p.client_store_id,
905
- })),
906
- },
907
- };
1139
+ if (mergedData.moisture !== undefined && mergedData.moisture !== null) {
1140
+ mergedData.testsMoisture = true;
1141
+ }
1142
+ if (mergedData.dateTested) {
1143
+ mergedData.testsBatch = true;
908
1144
  }
909
- case "list_clients":
910
- case "list_customers": {
911
- let query = sb.from("v_store_customers")
912
- .select("id, first_name, last_name, email, phone, loyalty_tier, total_orders, is_active, created_at")
913
- .eq("store_id", sid)
914
- .order("created_at", { ascending: false });
915
- if (args.query) {
916
- const term = `%${sanitizeFilterValue(String(args.query).trim())}%`;
917
- query = query.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term}`);
1145
+
1146
+ // 7c. Generate QR code for COA templates — links to Quantix COA landing page
1147
+ if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
1148
+ const productSlug = (mergedData.sampleName || "certificate").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1149
+ const coaUrl = `https://quantixanalytics.com/coa/${sid}/${productSlug}`;
1150
+ try {
1151
+ mergedData.qrCodeDataUrl = await QRCode.toDataURL(coaUrl, {
1152
+ width: 120,
1153
+ margin: 1
1154
+ });
1155
+ } catch {/* skip QR if generation fails */}
1156
+ }
1157
+
1158
+ // 8. Validate
1159
+ const validationResults = runValidation(tpl.validation_rules, mergedData);
1160
+ const errors = validationResults.filter(v => v.severity === "error" && !v.passed);
1161
+ const warnings = validationResults.filter(v => v.severity === "warning" && !v.passed);
1162
+ if (errors.length > 0 && !args.force) {
1163
+ return {
1164
+ success: false,
1165
+ error: `Validation failed: ${errors.map(e => e.message).join("; ")}`,
1166
+ data: {
1167
+ errors,
1168
+ warnings
918
1169
  }
919
- if (args.limit)
920
- query = query.limit(args.limit);
921
- const { data, error } = await query;
922
- if (error)
923
- return { success: false, error: error.message };
924
- return {
925
- success: true,
926
- data: {
927
- count: data?.length || 0,
928
- customers: (data || []).map(c => ({
929
- id: c.id, name: [c.first_name, c.last_name].filter(Boolean).join(" "),
930
- email: c.email, phone: c.phone, loyalty_tier: c.loyalty_tier,
931
- total_orders: c.total_orders, is_active: c.is_active,
932
- })),
933
- },
934
- };
1170
+ };
935
1171
  }
936
- case "deliver_documents": {
937
- // Batch-assign documents to a customer and optionally send notification email
938
- const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
939
- const customerId = args.customer_id;
940
- if (!docIds.length)
941
- return { success: false, error: "document_ids (array) or document_id (string) is required" };
942
- if (!customerId)
943
- return { success: false, error: "customer_id is required" };
944
- // Verify customer belongs to this store
945
- const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
946
- .select("id, store_id, platform_users!inner(first_name, last_name, email)")
947
- .eq("id", customerId).eq("store_id", sid).single();
948
- if (relErr || !rel)
949
- return { success: false, error: "Customer not found for this store" };
950
- const pu = rel.platform_users;
951
- const customerEmail = pu.email;
952
- const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ");
953
- // Assign all documents to the customer
954
- const { error: updateErr } = await sb.from("store_documents")
955
- .update({ customer_id: customerId })
956
- .in("id", docIds)
957
- .eq("store_id", sid);
958
- if (updateErr)
959
- return { success: false, error: `Failed to assign documents: ${updateErr.message}` };
960
- // Get document names for the email
961
- const { data: docs } = await sb.from("store_documents")
962
- .select("id, document_name, file_url")
963
- .in("id", docIds);
964
- let emailSent = false;
965
- if (!args.skip_email && customerEmail) {
966
- const portalUrl = args.portal_url || null;
967
- const docList = (docs || []).map(d => `<li><a href="${d.file_url}">${d.document_name || "Document"}</a></li>`).join("");
968
- // Get store name for template
969
- const { data: store } = await sb.from("stores").select("store_name").eq("id", sid).single();
970
- const emailResult = await handleEmail(sb, {
971
- action: "send_template",
972
- to: customerEmail,
973
- template: "documents-delivered",
974
- template_data: {
975
- customer_name: customerName || "there",
976
- store_name: store?.store_name || "",
977
- document_count: String(docIds.length),
978
- document_list: docList,
979
- portal_url: portalUrl || "",
980
- },
981
- }, storeId);
982
- emailSent = emailResult.success;
1172
+
1173
+ // 9-10. Render PDF all templates use React-PDF
1174
+ let pdfBuffer;
1175
+ try {
1176
+ if (tpl.slug?.startsWith("cannabis-coa")) {
1177
+ // COA templates use dedicated renderer
1178
+ pdfBuffer = await renderCOAToPdf(mergedData);
1179
+ } else {
1180
+ // Detect label layout vs generic layout
1181
+ const layout = tpl.layout;
1182
+ const isLabel = layout?.version === 1 && Array.isArray(layout?.elements) && layout.elements[0]?.field;
1183
+ if (isLabel) {
1184
+ const items = Array.isArray(mergedData.items) ? mergedData.items : [mergedData];
1185
+ pdfBuffer = await renderLabelToPdf(layout, tpl.page_config, items);
1186
+ } else {
1187
+ pdfBuffer = await renderLayoutToPdf(tpl.layout, tpl.page_config, tpl.styles, mergedData);
983
1188
  }
984
- return {
985
- success: true,
986
- data: {
987
- document_ids: docIds,
988
- customer_id: customerId,
989
- customer_email: customerEmail,
990
- email_sent: emailSent,
991
- count: docIds.length,
992
- },
993
- };
1189
+ }
1190
+ } catch (err) {
1191
+ return {
1192
+ success: false,
1193
+ error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}`
1194
+ };
994
1195
  }
995
- case "list_customer_documents": {
996
- const customerId = args.customer_id;
997
- if (!customerId)
998
- return { success: false, error: "customer_id is required" };
999
- let query = sb.from("store_documents")
1000
- .select("id, document_name, file_url, file_type, created_at, thumbnail_url, metadata, document_type")
1001
- .eq("store_id", sid)
1002
- .eq("customer_id", customerId)
1003
- .eq("is_active", true)
1004
- .order("created_at", { ascending: false });
1005
- if (args.limit)
1006
- query = query.limit(args.limit);
1007
- const { data, error } = await query;
1008
- if (error)
1009
- return { success: false, error: error.message };
1010
- return {
1011
- success: true,
1012
- data: {
1013
- count: data?.length || 0,
1014
- documents: data || [],
1015
- },
1196
+
1197
+ // 11. Upload to Supabase storage
1198
+ const sampleName = mergedData.sampleName || mergedData.productName || "";
1199
+ const docName = args.name || (sampleName ? `${sampleName} COA` : fillTemplate(tpl.name || "Document", mergedData));
1200
+ const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
1201
+ const fileName = `${safeName}_${Date.now()}.pdf`;
1202
+ const storagePath = `${sid}/${fileName}`;
1203
+ const bucketName = "store-documents";
1204
+ const {
1205
+ error: uploadErr
1206
+ } = await sb.storage.from(bucketName).upload(storagePath, new Uint8Array(pdfBuffer), {
1207
+ contentType: "application/pdf",
1208
+ upsert: true
1209
+ });
1210
+ if (uploadErr) return {
1211
+ success: false,
1212
+ error: `Upload failed: ${uploadErr.message}`
1213
+ };
1214
+ const {
1215
+ data: urlData
1216
+ } = sb.storage.from(bucketName).getPublicUrl(storagePath);
1217
+
1218
+ // 12. Build COA metadata for the Quantix landing page
1219
+ const coaMetadata = {
1220
+ size_bytes: pdfBuffer.length,
1221
+ from_pdf_template: true,
1222
+ template_version: tpl.version
1223
+ };
1224
+ if (tpl.slug?.startsWith("cannabis-coa")) {
1225
+ coaMetadata.sample_name = sampleName;
1226
+ coaMetadata.sample_id = mergedData.sampleId || null;
1227
+ coaMetadata.sample_type = mergedData.sampleType || mergedData.strain || null;
1228
+ coaMetadata.sample_size = mergedData.sampleSize || null;
1229
+ coaMetadata.batch_number = mergedData.batchId || null;
1230
+ coaMetadata.lab_name = mergedData.labName || null;
1231
+ coaMetadata.lab_contact = mergedData.labContact || null;
1232
+ coaMetadata.lab_website = mergedData.labWebsite || null;
1233
+ coaMetadata.lab_director = mergedData.labDirector || null;
1234
+ coaMetadata.director_title = mergedData.directorTitle || null;
1235
+ coaMetadata.logo_url = mergedData.logoUrl || null;
1236
+ coaMetadata.signature_url = mergedData.signatureUrl || null;
1237
+ coaMetadata.client_name = mergedData.clientName || null;
1238
+ coaMetadata.client_address = mergedData.clientAddress || null;
1239
+ coaMetadata.date_collected = mergedData.dateCollected || null;
1240
+ coaMetadata.date_received = mergedData.dateReceived || null;
1241
+ coaMetadata.date_tested = mergedData.dateTested || null;
1242
+ coaMetadata.date_reported = mergedData.dateReported || null;
1243
+ coaMetadata.status = "Pass";
1244
+ coaMetadata.thc_total = mergedData.totalTHC || null;
1245
+ coaMetadata.cbd_total = mergedData.totalCBD || null;
1246
+ coaMetadata.cannabinoids_total = mergedData.totalCannabinoids || null;
1247
+ coaMetadata.moisture = mergedData.moisture || null;
1248
+ // Detailed cannabinoid data for the interactive landing page
1249
+ const cannabinoids = mergedData.cannabinoids;
1250
+ if (Array.isArray(cannabinoids)) {
1251
+ coaMetadata.cannabinoids = Object.fromEntries(cannabinoids.filter(c => typeof c.percentWeight === "number" && c.percentWeight > 0).map(c => [c.name, c.percentWeight]));
1252
+ coaMetadata.cannabinoids_detailed = cannabinoids.map(c => ({
1253
+ name: c.name,
1254
+ percent: c.percentWeight,
1255
+ mg_per_g: c.mgPerG,
1256
+ lod: c.lod,
1257
+ loq: c.loq,
1258
+ result: String(c.result ?? c.percentWeight)
1259
+ }));
1260
+ }
1261
+ // Safety test panels
1262
+ if (mergedData.fullPanel) {
1263
+ coaMetadata.test_panels = {
1264
+ cannabinoids: true,
1265
+ microbial: true,
1266
+ heavy_metals: true,
1267
+ mycotoxins: true,
1268
+ pesticides: true,
1269
+ residual_solvents: true
1270
+ };
1271
+ coaMetadata.safety_tests = {
1272
+ microbial: "Pass",
1273
+ heavy_metals: "Pass",
1274
+ mycotoxins: "Pass",
1275
+ pesticides: "Pass",
1276
+ residual_solvents: "Pass"
1016
1277
  };
1278
+ }
1279
+ }
1280
+
1281
+ // 13. Insert store_documents record
1282
+ const {
1283
+ data: record,
1284
+ error: insertErr
1285
+ } = await sb.from("store_documents").insert({
1286
+ store_id: sid,
1287
+ document_type: tpl.document_type || "pdf",
1288
+ file_name: fileName,
1289
+ file_url: urlData.publicUrl,
1290
+ file_size: pdfBuffer.length,
1291
+ file_type: "application/pdf",
1292
+ document_name: docName,
1293
+ source_name: "PDF Template Engine",
1294
+ document_date: new Date().toISOString().split("T")[0],
1295
+ customer_id: args.customer_id || null,
1296
+ client_store_id: resolvedClientStoreId,
1297
+ data: {
1298
+ template_id: tpl.id,
1299
+ template_slug: tpl.slug,
1300
+ profile_id: args.profile_id || null
1301
+ },
1302
+ metadata: coaMetadata
1303
+ }).select("id, document_name, file_url, created_at").single();
1304
+ if (insertErr) return {
1305
+ success: false,
1306
+ error: insertErr.message
1307
+ };
1308
+ await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => {});
1309
+ return {
1310
+ success: true,
1311
+ data: {
1312
+ id: record.id,
1313
+ name: record.document_name,
1314
+ url: record.file_url,
1315
+ template: tpl.name,
1316
+ size: pdfBuffer.length,
1317
+ warnings: warnings.map(w => w.message)
1318
+ }
1319
+ };
1320
+ }
1321
+ case "list_pdf_templates":
1322
+ {
1323
+ let query = sb.from("pdf_templates").select("id, slug, name, description, document_type, version, created_at").eq("is_active", true).or(`store_id.eq.${sid},store_id.is.null`).order("name");
1324
+ if (args.document_type) query = query.eq("document_type", args.document_type);
1325
+ if (args.limit) query = query.limit(args.limit);
1326
+ const {
1327
+ data,
1328
+ error
1329
+ } = await query;
1330
+ if (error) return {
1331
+ success: false,
1332
+ error: error.message
1333
+ };
1334
+ return {
1335
+ success: true,
1336
+ data: {
1337
+ count: data?.length || 0,
1338
+ templates: (data || []).map(t => ({
1339
+ id: t.id,
1340
+ slug: t.slug,
1341
+ name: t.name,
1342
+ description: t.description,
1343
+ type: t.document_type,
1344
+ version: t.version
1345
+ }))
1346
+ }
1347
+ };
1348
+ }
1349
+ case "list_document_profiles":
1350
+ {
1351
+ let query = sb.from("document_profiles").select("id, name, category, sample_type, default_size, client_store_id, template_id, created_at").eq("is_active", true).eq("owner_store_id", sid).order("name");
1352
+ if (args.template_id) query = query.eq("template_id", args.template_id);
1353
+ if (args.category) query = query.eq("category", args.category);
1354
+ if (args.limit) query = query.limit(args.limit);
1355
+ const {
1356
+ data,
1357
+ error
1358
+ } = await query;
1359
+ if (error) return {
1360
+ success: false,
1361
+ error: error.message
1362
+ };
1363
+ return {
1364
+ success: true,
1365
+ data: {
1366
+ count: data?.length || 0,
1367
+ profiles: (data || []).map(p => ({
1368
+ id: p.id,
1369
+ name: p.name,
1370
+ category: p.category,
1371
+ sample_type: p.sample_type,
1372
+ default_size: p.default_size,
1373
+ client_store_id: p.client_store_id
1374
+ }))
1375
+ }
1376
+ };
1377
+ }
1378
+ case "list_clients":
1379
+ case "list_customers":
1380
+ return {
1381
+ success: false,
1382
+ error: "Use the 'customers' tool instead. Clients are customers."
1383
+ };
1384
+ case "deliver_documents":
1385
+ {
1386
+ // Batch-assign documents to a customer and optionally send notification email
1387
+ const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
1388
+ const customerId = args.customer_id;
1389
+ if (!docIds.length) return {
1390
+ success: false,
1391
+ error: "document_ids (array) or document_id (string) is required"
1392
+ };
1393
+ if (!customerId) return {
1394
+ success: false,
1395
+ error: "customer_id is required"
1396
+ };
1397
+
1398
+ // Verify customer belongs to this store
1399
+ const {
1400
+ data: rel,
1401
+ error: relErr
1402
+ } = await sb.from("user_creation_relationships").select("id, store_id, platform_users!inner(first_name, last_name, email)").eq("id", customerId).eq("store_id", sid).single();
1403
+ if (relErr || !rel) return {
1404
+ success: false,
1405
+ error: "Customer not found for this store"
1406
+ };
1407
+ const pu = rel.platform_users;
1408
+ const customerEmail = pu.email;
1409
+ const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ");
1410
+
1411
+ // Assign all documents to the customer
1412
+ const {
1413
+ error: updateErr
1414
+ } = await sb.from("store_documents").update({
1415
+ customer_id: customerId
1416
+ }).in("id", docIds).eq("store_id", sid);
1417
+ if (updateErr) return {
1418
+ success: false,
1419
+ error: `Failed to assign documents: ${updateErr.message}`
1420
+ };
1421
+
1422
+ // Get document names for the email
1423
+ const {
1424
+ data: docs
1425
+ } = await sb.from("store_documents").select("id, document_name, file_url").in("id", docIds);
1426
+ let emailSent = false;
1427
+ if (!args.skip_email && customerEmail) {
1428
+ const portalUrl = args.portal_url || null;
1429
+ const docList = (docs || []).map(d => `<li><a href="${d.file_url}">${d.document_name || "Document"}</a></li>`).join("");
1430
+
1431
+ // Get store name for template
1432
+ const {
1433
+ data: store
1434
+ } = await sb.from("stores").select("store_name").eq("id", sid).single();
1435
+ const emailResult = await handleEmail(sb, {
1436
+ action: "send_template",
1437
+ to: customerEmail,
1438
+ template: "documents-delivered",
1439
+ template_data: {
1440
+ customer_name: customerName || "there",
1441
+ store_name: store?.store_name || "",
1442
+ document_count: String(docIds.length),
1443
+ document_list: docList,
1444
+ portal_url: portalUrl || ""
1445
+ }
1446
+ }, storeId);
1447
+ emailSent = emailResult.success;
1017
1448
  }
1018
- default:
1019
- return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, list_customers, deliver_documents, list_customer_documents` };
1020
- }
1449
+ return {
1450
+ success: true,
1451
+ data: {
1452
+ document_ids: docIds,
1453
+ customer_id: customerId,
1454
+ customer_email: customerEmail,
1455
+ email_sent: emailSent,
1456
+ count: docIds.length
1457
+ }
1458
+ };
1459
+ }
1460
+ case "list_customer_documents":
1461
+ {
1462
+ const customerId = args.customer_id;
1463
+ if (!customerId) return {
1464
+ success: false,
1465
+ error: "customer_id is required"
1466
+ };
1467
+ let query = sb.from("store_documents").select("id, document_name, file_url, file_type, created_at, thumbnail_url, metadata, document_type").eq("store_id", sid).eq("customer_id", customerId).eq("is_active", true).order("created_at", {
1468
+ ascending: false
1469
+ });
1470
+ if (args.limit) query = query.limit(args.limit);
1471
+ const {
1472
+ data,
1473
+ error
1474
+ } = await query;
1475
+ if (error) return {
1476
+ success: false,
1477
+ error: error.message
1478
+ };
1479
+ return {
1480
+ success: true,
1481
+ data: {
1482
+ count: data?.length || 0,
1483
+ documents: data || []
1484
+ }
1485
+ };
1486
+ }
1487
+ default:
1488
+ return {
1489
+ success: false,
1490
+ error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, deliver_documents, list_customer_documents`
1491
+ };
1492
+ }
1021
1493
  }
1494
+ //# sourceMappingURL=comms.js.map