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,823 +1,1114 @@
1
1
  import { sanitizeFilterValue } from "../lib/utils.js";
2
2
  import { handleEmail } from "./comms.js";
3
+
3
4
  /** Strip internal infrastructure fields that stores should never see */
4
5
  function stripInternal(row) {
5
- if (!row)
6
- return null;
7
- const { platform_user_id, store_id, password_hash, ...clean } = row;
8
- return clean;
6
+ if (!row) return null;
7
+ const {
8
+ platform_user_id,
9
+ store_id,
10
+ password_hash,
11
+ ...clean
12
+ } = row;
13
+ return clean;
9
14
  }
10
15
  function stripInternalArray(rows) {
11
- return (rows || []).map(r => stripInternal(r));
16
+ return (rows || []).map(r => stripInternal(r));
12
17
  }
13
18
  export async function handleCustomers(sb, args, storeId) {
14
- const sid = storeId;
15
- switch (args.action) {
16
- // ---- FIND: search customers via v_segment_customers view ----
17
- case "find": {
18
- const sortField = args.sort_by || "created_at";
19
- const sortAsc = args.sort_order === "asc";
20
- let q = sb.from("v_segment_customers")
21
- .select("id, first_name, last_name, email, phone, loyalty_points, loyalty_tier, total_spent, total_orders, lifetime_value, is_active, rfm_segment, is_vip_customer, is_at_risk, is_churned, reorder_due, ai_churn_risk, days_since_last_order, engagement_score, age_bracket, created_at")
22
- .eq("store_id", sid)
23
- .order(sortField, { ascending: sortAsc });
24
- if (args.limit)
25
- q = q.limit(args.limit);
26
- if (args.query) {
27
- const raw = sanitizeFilterValue(String(args.query).trim());
28
- const words = raw.split(/\s+/).filter(Boolean);
29
- if (words.length > 1) {
30
- const clauses = words.map(w => { const sw = sanitizeFilterValue(w); return `first_name.ilike.%${sw}%,last_name.ilike.%${sw}%`; }).join(",");
31
- q = q.or(`${clauses},email.ilike.%${raw}%,phone.ilike.%${raw}%`);
32
- }
33
- else {
34
- const term = `%${raw}%`;
35
- q = q.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term},phone.ilike.${term}`);
36
- }
37
- }
38
- if (args.status === "active")
39
- q = q.eq("is_active", true);
40
- if (args.status === "inactive")
41
- q = q.eq("is_active", false);
42
- if (args.loyalty_tier)
43
- q = q.eq("loyalty_tier", args.loyalty_tier);
44
- if (args.rfm_segment)
45
- q = q.eq("rfm_segment", args.rfm_segment);
46
- if (args.is_vip !== undefined)
47
- q = q.eq("is_vip_customer", args.is_vip);
48
- if (args.is_at_risk !== undefined)
49
- q = q.eq("is_at_risk", args.is_at_risk);
50
- if (args.is_churned !== undefined)
51
- q = q.eq("is_churned", args.is_churned);
52
- if (args.reorder_due !== undefined)
53
- q = q.eq("reorder_due", args.reorder_due);
54
- if (args.age_bracket)
55
- q = q.eq("age_bracket", args.age_bracket);
56
- if (args.min_orders !== undefined)
57
- q = q.gte("total_orders", args.min_orders);
58
- if (args.max_orders !== undefined)
59
- q = q.lte("total_orders", args.max_orders);
60
- if (args.min_spent !== undefined)
61
- q = q.gte("total_spent", args.min_spent);
62
- if (args.max_spent !== undefined)
63
- q = q.lte("total_spent", args.max_spent);
64
- const { data, error } = await q;
65
- return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
19
+ const sid = storeId;
20
+ switch (args.action) {
21
+ // ---- FIND: search customers via v_segment_customers view ----
22
+ case "find":
23
+ {
24
+ const sortField = args.sort_by || "created_at";
25
+ const sortAsc = args.sort_order === "asc";
26
+ let q = sb.from("v_segment_customers").select("id, first_name, last_name, email, phone, loyalty_points, loyalty_tier, total_spent, total_orders, lifetime_value, is_active, rfm_segment, is_vip_customer, is_at_risk, is_churned, reorder_due, ai_churn_risk, days_since_last_order, engagement_score, age_bracket, created_at").eq("store_id", sid).order(sortField, {
27
+ ascending: sortAsc
28
+ });
29
+ if (args.limit) q = q.limit(args.limit);
30
+ if (args.query) {
31
+ const raw = sanitizeFilterValue(String(args.query).trim());
32
+ const words = raw.split(/\s+/).filter(Boolean);
33
+ if (words.length > 1) {
34
+ const clauses = words.map(w => {
35
+ const sw = sanitizeFilterValue(w);
36
+ return `first_name.ilike.%${sw}%,last_name.ilike.%${sw}%`;
37
+ }).join(",");
38
+ q = q.or(`${clauses},email.ilike.%${raw}%,phone.ilike.%${raw}%`);
39
+ } else {
40
+ const term = `%${raw}%`;
41
+ q = q.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term},phone.ilike.${term}`);
42
+ }
66
43
  }
67
- // ---- GET: full 360° customer detail with orders, activity, notes, loyalty, segments ----
68
- case "get": {
69
- const custId = args.customer_id;
70
- const { data: customer, error: custErr } = await sb.from("v_segment_customers")
71
- .select("*").eq("id", custId).eq("store_id", sid).single();
72
- if (custErr)
73
- return { success: false, error: custErr.message };
74
- const [{ data: orders }, { data: notes }, { data: activity }, { data: profile }, { data: loyaltyHistory }, { data: segmentRows }] = await Promise.all([
75
- sb.from("orders")
76
- .select("id, order_number, status, total_amount, payment_status, fulfillment_status, created_at")
77
- .eq("customer_id", custId).eq("store_id", sid)
78
- .order("created_at", { ascending: false })
79
- .limit(args.orders_limit || 10),
80
- sb.from("customer_notes")
81
- .select("id, note, created_by, created_at")
82
- .eq("customer_id", custId)
83
- .order("created_at", { ascending: false }).limit(10),
84
- sb.from("customer_activity")
85
- .select("id, activity_type, description, created_at")
86
- .eq("customer_id", custId)
87
- .order("created_at", { ascending: false }).limit(10),
88
- sb.from("store_customer_profiles")
89
- .select("*").eq("relationship_id", custId).maybeSingle(),
90
- sb.from("loyalty_transactions")
91
- .select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
92
- .eq("customer_id", custId)
93
- .order("created_at", { ascending: false }).limit(20),
94
- sb.from("customer_segment_memberships")
95
- .select("added_at, segment:customer_segments(id, name, type)")
96
- .eq("customer_id", custId),
97
- ]);
98
- // Flatten profile fields into customer object (skip sensitive + computed-only fields)
99
- const prof = profile;
100
- const profileFields = {};
101
- if (prof) {
102
- const skip = new Set([
103
- "id", "relationship_id", "created_at", "updated_at",
104
- "password_hash", // never expose
105
- // These are recomputed by refresh_metrics — read from the view instead
106
- "total_spent", "total_orders", "lifetime_value", "average_order_value",
107
- "first_order_at", "last_order_at",
108
- ]);
109
- for (const [k, v] of Object.entries(prof)) {
110
- if (!skip.has(k))
111
- profileFields[k] = v ?? null;
112
- }
113
- }
114
- // Flatten segment memberships
115
- const segments = (segmentRows || []).map((r) => ({
116
- segment_id: r.segment?.id, name: r.segment?.name, type: r.segment?.type, added_at: r.added_at,
117
- }));
118
- return { success: true, data: stripInternal({ ...customer, ...profileFields, orders, notes, activity, loyalty_history: loyaltyHistory || [], segments }) };
44
+ if (args.status === "active") q = q.eq("is_active", true);
45
+ if (args.status === "inactive") q = q.eq("is_active", false);
46
+ if (args.loyalty_tier) q = q.eq("loyalty_tier", args.loyalty_tier);
47
+ if (args.rfm_segment) q = q.eq("rfm_segment", args.rfm_segment);
48
+ if (args.is_vip !== undefined) q = q.eq("is_vip_customer", args.is_vip);
49
+ if (args.is_at_risk !== undefined) q = q.eq("is_at_risk", args.is_at_risk);
50
+ if (args.is_churned !== undefined) q = q.eq("is_churned", args.is_churned);
51
+ if (args.reorder_due !== undefined) q = q.eq("reorder_due", args.reorder_due);
52
+ if (args.age_bracket) q = q.eq("age_bracket", args.age_bracket);
53
+ if (args.min_orders !== undefined) q = q.gte("total_orders", args.min_orders);
54
+ if (args.max_orders !== undefined) q = q.lte("total_orders", args.max_orders);
55
+ if (args.min_spent !== undefined) q = q.gte("total_spent", args.min_spent);
56
+ if (args.max_spent !== undefined) q = q.lte("total_spent", args.max_spent);
57
+ const {
58
+ data,
59
+ error
60
+ } = await q;
61
+ return error ? {
62
+ success: false,
63
+ error: error.message
64
+ } : {
65
+ success: true,
66
+ count: data?.length,
67
+ data
68
+ };
69
+ }
70
+
71
+ // ---- GET: full 360° customer detail with orders, activity, notes, loyalty, segments ----
72
+ case "get":
73
+ {
74
+ const custId = args.customer_id;
75
+ const {
76
+ data: customer,
77
+ error: custErr
78
+ } = await sb.from("v_segment_customers").select("*").eq("id", custId).eq("store_id", sid).single();
79
+ if (custErr) return {
80
+ success: false,
81
+ error: custErr.message
82
+ };
83
+ const [{
84
+ data: orders
85
+ }, {
86
+ data: notes
87
+ }, {
88
+ data: activity
89
+ }, {
90
+ data: profile
91
+ }, {
92
+ data: loyaltyHistory
93
+ }, {
94
+ data: segmentRows
95
+ }] = await Promise.all([sb.from("orders").select("id, order_number, status, total_amount, payment_status, fulfillment_status, created_at").eq("customer_id", custId).eq("store_id", sid).order("created_at", {
96
+ ascending: false
97
+ }).limit(args.orders_limit || 10), sb.from("customer_notes").select("id, note, created_by, created_at").eq("customer_id", custId).order("created_at", {
98
+ ascending: false
99
+ }).limit(10), sb.from("customer_activity").select("id, activity_type, description, created_at").eq("customer_id", custId).order("created_at", {
100
+ ascending: false
101
+ }).limit(10), sb.from("store_customer_profiles").select("*").eq("relationship_id", custId).maybeSingle(), sb.from("loyalty_transactions").select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at").eq("customer_id", custId).order("created_at", {
102
+ ascending: false
103
+ }).limit(20), sb.from("customer_segment_memberships").select("added_at, segment:customer_segments(id, name, type)").eq("customer_id", custId)]);
104
+
105
+ // Flatten profile fields into customer object (skip sensitive + computed-only fields)
106
+ const prof = profile;
107
+ const profileFields = {};
108
+ if (prof) {
109
+ const skip = new Set(["id", "relationship_id", "created_at", "updated_at", "password_hash",
110
+ // never expose
111
+ // These are recomputed by refresh_metrics — read from the view instead
112
+ "total_spent", "total_orders", "lifetime_value", "average_order_value", "first_order_at", "last_order_at"]);
113
+ for (const [k, v] of Object.entries(prof)) {
114
+ if (!skip.has(k)) profileFields[k] = v ?? null;
115
+ }
119
116
  }
120
- // ---- CREATE: new customer (platform_user + relationship + profile) ----
121
- // Matching priority: email → phone → drivers_license → name+DOB → create new
122
- case "create": {
123
- const email = args.email;
124
- const phone = args.phone;
125
- const firstName = args.first_name;
126
- const lastName = args.last_name;
127
- const dob = args.date_of_birth;
128
- const dl = args.drivers_license_number;
129
- if (!firstName && !lastName)
130
- return { success: false, error: "first_name or last_name is required" };
131
- // Multi-tier matching to find existing platform_user (mirrors POS verify flow)
132
- let platformUserId = null;
133
- let matchedVia = null;
134
- // Tier 1: email
135
- if (!platformUserId && email) {
136
- const { data: existing } = await sb.from("platform_users")
137
- .select("id").eq("email", email).maybeSingle();
138
- if (existing) {
139
- platformUserId = existing.id;
140
- matchedVia = "email";
141
- }
142
- }
143
- // Tier 2: phone
144
- if (!platformUserId && phone) {
145
- const { data: existing } = await sb.from("platform_users")
146
- .select("id").eq("phone", phone).maybeSingle();
147
- if (existing) {
148
- platformUserId = existing.id;
149
- matchedVia = "phone";
150
- }
151
- }
152
- // Tier 3: drivers license (look up via store_customer_profiles → relationship → platform_user)
153
- if (!platformUserId && dl) {
154
- const { data: profMatch } = await sb.from("store_customer_profiles")
155
- .select("relationship_id, relationship:user_creation_relationships!relationship_id(user_id, store_id)")
156
- .eq("drivers_license_number", dl).limit(1).maybeSingle();
157
- if (profMatch) {
158
- const rel = profMatch.relationship;
159
- if (rel?.user_id) {
160
- platformUserId = rel.user_id;
161
- matchedVia = "drivers_license";
162
- }
163
- }
164
- }
165
- // Tier 4: exact name + date of birth (only when all 3 are provided)
166
- if (!platformUserId && firstName && lastName && dob) {
167
- const { data: nameMatch } = await sb.from("platform_users")
168
- .select("id").ilike("first_name", firstName).ilike("last_name", lastName)
169
- .eq("date_of_birth", dob).limit(1).maybeSingle();
170
- if (nameMatch) {
171
- platformUserId = nameMatch.id;
172
- matchedVia = "name+dob";
173
- }
174
- }
175
- // Create platform_user if no match found
176
- const isNameOnly = !email && !phone;
177
- if (!platformUserId) {
178
- const puInsert = { first_name: firstName, last_name: lastName };
179
- if (email)
180
- puInsert.email = email;
181
- if (phone)
182
- puInsert.phone = phone;
183
- if (dob)
184
- puInsert.date_of_birth = dob;
185
- const { data: newPu, error: puErr } = await sb.from("platform_users")
186
- .insert(puInsert).select("id").single();
187
- if (puErr)
188
- return { success: false, error: `Failed to create platform user: ${puErr.message}` };
189
- platformUserId = newPu.id;
190
- }
191
- // Check if relationship already exists for this store
192
- const { data: existingRel } = await sb.from("user_creation_relationships")
193
- .select("id").eq("user_id", platformUserId).eq("store_id", sid).maybeSingle();
194
- if (existingRel) {
195
- const { data: existing } = await sb.from("v_store_customers")
196
- .select("*").eq("id", existingRel.id).single();
197
- return { success: true, data: stripInternal(existing), note: `Customer already exists (matched via ${matchedVia || "existing record"})` };
198
- }
199
- // Create relationship
200
- const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
201
- .insert({
202
- user_id: platformUserId, creation_id: sid, creation_type: "store",
203
- store_id: sid, role: "user", status: "active",
204
- email_consent: args.email_consent ?? false,
205
- sms_consent: args.sms_consent ?? false,
206
- }).select("id").single();
207
- if (relErr)
208
- return { success: false, error: `Failed to create customer relationship: ${relErr.message}` };
209
- // Create store profile
210
- const profileInsert = { relationship_id: rel.id };
211
- if (args.street_address)
212
- profileInsert.street_address = args.street_address;
213
- if (args.city)
214
- profileInsert.city = args.city;
215
- if (args.state)
216
- profileInsert.state = args.state;
217
- if (args.postal_code)
218
- profileInsert.postal_code = args.postal_code;
219
- if (dl)
220
- profileInsert.drivers_license_number = dl;
221
- if (args.medical_card_number)
222
- profileInsert.medical_card_number = args.medical_card_number;
223
- if (args.medical_card_expiry)
224
- profileInsert.medical_card_expiry = args.medical_card_expiry;
225
- if (args.gender)
226
- profileInsert.gender = args.gender;
227
- if (args.eye_color)
228
- profileInsert.eye_color = args.eye_color;
229
- if (args.hair_color)
230
- profileInsert.hair_color = args.hair_color;
231
- if (args.height_raw)
232
- profileInsert.height_raw = args.height_raw;
233
- if (args.weight)
234
- profileInsert.weight = args.weight;
235
- if (args.suffix)
236
- profileInsert.suffix = args.suffix;
237
- if (args.is_staff !== undefined)
238
- profileInsert.is_staff = args.is_staff;
239
- await sb.from("store_customer_profiles").insert(profileInsert);
240
- const { data: created } = await sb.from("v_store_customers")
241
- .select("*").eq("id", rel.id).single();
242
- return {
243
- success: true,
244
- data: stripInternal(created),
245
- ...(matchedVia ? { note: `Linked to existing customer record (matched via ${matchedVia})` } : {}),
246
- ...(isNameOnly ? { warning: "Created with no email or phone — add contact info to prevent duplicates." } : {}),
247
- };
117
+
118
+ // Flatten segment memberships
119
+ const segments = (segmentRows || []).map(r => ({
120
+ segment_id: r.segment?.id,
121
+ name: r.segment?.name,
122
+ type: r.segment?.type,
123
+ added_at: r.added_at
124
+ }));
125
+ return {
126
+ success: true,
127
+ data: stripInternal({
128
+ ...customer,
129
+ ...profileFields,
130
+ orders,
131
+ notes,
132
+ activity,
133
+ loyalty_history: loyaltyHistory || [],
134
+ segments
135
+ })
136
+ };
137
+ }
138
+
139
+ // ---- CREATE: new customer (platform_user + relationship + profile) ----
140
+ // Matching priority: email → phone → drivers_license → name+DOB → create new
141
+ case "create":
142
+ {
143
+ const email = args.email;
144
+ const phone = args.phone;
145
+ const firstName = args.first_name;
146
+ const lastName = args.last_name;
147
+ const dob = args.date_of_birth;
148
+ const dl = args.drivers_license_number;
149
+ if (!firstName && !lastName) return {
150
+ success: false,
151
+ error: "first_name or last_name is required"
152
+ };
153
+
154
+ // Multi-tier matching to find existing platform_user (mirrors POS verify flow)
155
+ let platformUserId = null;
156
+ let matchedVia = null;
157
+
158
+ // Tier 1: email
159
+ if (!platformUserId && email) {
160
+ const {
161
+ data: existing
162
+ } = await sb.from("platform_users").select("id").eq("email", email).maybeSingle();
163
+ if (existing) {
164
+ platformUserId = existing.id;
165
+ matchedVia = "email";
166
+ }
248
167
  }
249
- // ---- UPDATE: modify any customer fields ----
250
- case "update": {
251
- const custId = args.customer_id;
252
- const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
253
- .select("id, user_id").eq("id", custId).eq("store_id", sid).single();
254
- if (relErr)
255
- return { success: false, error: `Customer not found: ${relErr.message}` };
256
- // Customer identity (name, email, phone, DOB)
257
- const identityUpdates = {};
258
- if (args.first_name !== undefined)
259
- identityUpdates.first_name = args.first_name;
260
- if (args.last_name !== undefined)
261
- identityUpdates.last_name = args.last_name;
262
- if (args.email !== undefined)
263
- identityUpdates.email = args.email;
264
- if (args.phone !== undefined)
265
- identityUpdates.phone = args.phone;
266
- if (args.date_of_birth !== undefined)
267
- identityUpdates.date_of_birth = args.date_of_birth;
268
- if (Object.keys(identityUpdates).length > 0) {
269
- identityUpdates.updated_at = new Date().toISOString();
270
- const { error: puErr } = await sb.from("platform_users")
271
- .update(identityUpdates).eq("id", rel.user_id);
272
- if (puErr)
273
- return { success: false, error: `Failed to update customer: ${puErr.message}` };
274
- }
275
- // Consent & status
276
- const relUpdates = {};
277
- if (args.status !== undefined)
278
- relUpdates.status = args.status;
279
- if (args.email_consent !== undefined)
280
- relUpdates.email_consent = args.email_consent;
281
- if (args.sms_consent !== undefined)
282
- relUpdates.sms_consent = args.sms_consent;
283
- if (args.push_consent !== undefined)
284
- relUpdates.push_consent = args.push_consent;
285
- if (Object.keys(relUpdates).length > 0) {
286
- relUpdates.updated_at = new Date().toISOString();
287
- await sb.from("user_creation_relationships").update(relUpdates).eq("id", custId);
288
- }
289
- // Profile fields
290
- const profUpdates = {};
291
- if (args.loyalty_points !== undefined)
292
- profUpdates.loyalty_points = args.loyalty_points;
293
- if (args.loyalty_tier !== undefined)
294
- profUpdates.loyalty_tier = args.loyalty_tier;
295
- if (args.street_address !== undefined)
296
- profUpdates.street_address = args.street_address;
297
- if (args.city !== undefined)
298
- profUpdates.city = args.city;
299
- if (args.state !== undefined)
300
- profUpdates.state = args.state;
301
- if (args.postal_code !== undefined)
302
- profUpdates.postal_code = args.postal_code;
303
- if (args.billing_address !== undefined)
304
- profUpdates.billing_address = args.billing_address;
305
- if (args.shipping_addresses !== undefined)
306
- profUpdates.shipping_addresses = args.shipping_addresses;
307
- if (args.default_shipping_address_index !== undefined)
308
- profUpdates.default_shipping_address_index = args.default_shipping_address_index;
309
- if (args.drivers_license_number !== undefined)
310
- profUpdates.drivers_license_number = args.drivers_license_number;
311
- if (args.id_verified !== undefined) {
312
- profUpdates.id_verified = args.id_verified;
313
- if (args.id_verified === true)
314
- profUpdates.id_verified_at = new Date().toISOString();
315
- }
316
- if (args.license_expiration_date !== undefined)
317
- profUpdates.license_expiration_date = args.license_expiration_date;
318
- if (args.license_issue_date !== undefined)
319
- profUpdates.license_issue_date = args.license_issue_date;
320
- if (args.medical_card_number !== undefined)
321
- profUpdates.medical_card_number = args.medical_card_number;
322
- if (args.medical_card_expiry !== undefined)
323
- profUpdates.medical_card_expiry = args.medical_card_expiry;
324
- if (args.gender !== undefined)
325
- profUpdates.gender = args.gender;
326
- if (args.eye_color !== undefined)
327
- profUpdates.eye_color = args.eye_color;
328
- if (args.height_raw !== undefined)
329
- profUpdates.height_raw = args.height_raw;
330
- if (args.weight !== undefined)
331
- profUpdates.weight = args.weight;
332
- if (args.hair_color !== undefined)
333
- profUpdates.hair_color = args.hair_color;
334
- if (args.suffix !== undefined)
335
- profUpdates.suffix = args.suffix;
336
- if (args.is_staff !== undefined)
337
- profUpdates.is_staff = args.is_staff;
338
- if (args.has_wallet_pass !== undefined)
339
- profUpdates.has_wallet_pass = args.has_wallet_pass;
340
- if (args.is_wholesale_approved !== undefined)
341
- profUpdates.is_wholesale_approved = args.is_wholesale_approved;
342
- if (args.wholesale_tier !== undefined)
343
- profUpdates.wholesale_tier = args.wholesale_tier;
344
- if (args.wholesale_business_name !== undefined)
345
- profUpdates.wholesale_business_name = args.wholesale_business_name;
346
- if (args.wholesale_business_type !== undefined)
347
- profUpdates.wholesale_business_type = args.wholesale_business_type;
348
- if (args.wholesale_license_number !== undefined)
349
- profUpdates.wholesale_license_number = args.wholesale_license_number;
350
- if (args.wholesale_tax_id !== undefined)
351
- profUpdates.wholesale_tax_id = args.wholesale_tax_id;
352
- if (args.wholesale_discount_percent !== undefined)
353
- profUpdates.wholesale_discount_percent = args.wholesale_discount_percent;
354
- if (args.wholesale_payment_terms !== undefined)
355
- profUpdates.wholesale_payment_terms = args.wholesale_payment_terms;
356
- if (args.wholesale_credit_limit !== undefined)
357
- profUpdates.wholesale_credit_limit = args.wholesale_credit_limit;
358
- if (args.wholesale_resale_certificate !== undefined)
359
- profUpdates.wholesale_resale_certificate = args.wholesale_resale_certificate;
360
- if (args.wholesale_notes !== undefined)
361
- profUpdates.wholesale_notes = args.wholesale_notes;
362
- if (Object.keys(profUpdates).length > 0) {
363
- profUpdates.updated_at = new Date().toISOString();
364
- const { error: profErr } = await sb.from("store_customer_profiles")
365
- .update(profUpdates).eq("relationship_id", custId);
366
- if (profErr)
367
- return { success: false, error: `Failed to update profile: ${profErr.message}` };
368
- }
369
- const { data: updated } = await sb.from("v_segment_customers")
370
- .select("*").eq("id", custId).eq("store_id", sid).single();
371
- return { success: true, data: stripInternal(updated) };
168
+ // Tier 2: phone
169
+ if (!platformUserId && phone) {
170
+ const {
171
+ data: existing
172
+ } = await sb.from("platform_users").select("id").eq("phone", phone).maybeSingle();
173
+ if (existing) {
174
+ platformUserId = existing.id;
175
+ matchedVia = "phone";
176
+ }
372
177
  }
373
- // ---- FIND_DUPLICATES: identify potential duplicate customer accounts ----
374
- case "find_duplicates": {
375
- // Try RPC first (may not exist yet)
376
- const { data: dupes, error: dupeErr } = await sb.rpc("find_duplicate_customers", { p_store_id: sid });
377
- if (!dupeErr && dupes)
378
- return { success: true, data: dupes };
379
- // Fallback: manual duplicate detection by phone
380
- const { data: byPhone, error: phErr } = await sb.from("v_store_customers")
381
- .select("id, first_name, last_name, email, phone, total_spent, total_orders, created_at")
382
- .eq("store_id", sid).not("phone", "is", null).order("phone");
383
- if (phErr)
384
- return { success: false, error: phErr.message };
385
- const phoneMap = new Map();
386
- for (const c of byPhone || []) {
387
- if (!c.phone)
388
- continue;
389
- const normalized = c.phone.replace(/\D/g, "").slice(-10);
390
- if (!phoneMap.has(normalized))
391
- phoneMap.set(normalized, []);
392
- phoneMap.get(normalized).push(c);
393
- }
394
- const phoneDupes = Array.from(phoneMap.entries())
395
- .filter(([_, custs]) => custs.length > 1)
396
- .map(([phone, custs]) => ({ phone, count: custs.length, customers: custs }));
397
- // Also check by exact name match
398
- const { data: byName } = await sb.from("v_store_customers")
399
- .select("id, first_name, last_name, email, phone, total_spent, total_orders, created_at")
400
- .eq("store_id", sid).order("last_name").order("first_name");
401
- const nameMap = new Map();
402
- for (const c of byName || []) {
403
- const key = `${(c.first_name || "").toLowerCase().trim()} ${(c.last_name || "").toLowerCase().trim()}`;
404
- if (!key.trim())
405
- continue;
406
- if (!nameMap.has(key))
407
- nameMap.set(key, []);
408
- nameMap.get(key).push(c);
409
- }
410
- const nameDupes = Array.from(nameMap.entries())
411
- .filter(([_, custs]) => custs.length > 1)
412
- .map(([name, custs]) => ({ name, count: custs.length, customers: custs }));
413
- // Pre-format as markdown — formatter drops nested customer arrays
414
- const totalPhoneDupes = phoneDupes.reduce((s, d) => s + d.count, 0);
415
- const totalNameDupes = nameDupes.reduce((s, d) => s + d.count, 0);
416
- const lines = [
417
- `## Duplicate Customers`,
418
- `**By Phone**: ${phoneDupes.length} groups (${totalPhoneDupes} customers) | **By Name**: ${nameDupes.length} groups (${totalNameDupes} customers)\n`,
419
- ];
420
- if (phoneDupes.length > 0) {
421
- lines.push("### Phone Duplicates");
422
- for (const group of phoneDupes.slice(0, 20)) {
423
- lines.push(`\n**Phone ${group.phone}** (${group.count} customers):`);
424
- lines.push("| Name | Email | Phone | Orders | Spent | Created |");
425
- lines.push("| --- | --- | --- | ---: | ---: | --- |");
426
- for (const c of group.customers) {
427
- lines.push(`| ${c.first_name || ""} ${c.last_name || ""} | ${c.email || "—"} | ${c.phone || "—"} | ${c.total_orders ?? 0} | $${c.total_spent ?? 0} | ${c.created_at?.slice(0, 10) || "—"} |`);
428
- }
429
- }
430
- }
431
- if (nameDupes.length > 0) {
432
- lines.push("\n### Name Duplicates");
433
- for (const group of nameDupes.slice(0, 20)) {
434
- lines.push(`\n**"${group.name}"** (${group.count} customers):`);
435
- lines.push("| Name | Email | Phone | Orders | Spent | Created |");
436
- lines.push("| --- | --- | --- | ---: | ---: | --- |");
437
- for (const c of group.customers) {
438
- lines.push(`| ${c.first_name || ""} ${c.last_name || ""} | ${c.email || "—"} | ${c.phone || "—"} | ${c.total_orders ?? 0} | $${c.total_spent ?? 0} | ${c.created_at?.slice(0, 10) || "—"} |`);
439
- }
440
- }
178
+ // Tier 3: drivers license (look up via store_customer_profiles → relationship → platform_user)
179
+ if (!platformUserId && dl) {
180
+ const {
181
+ data: profMatch
182
+ } = await sb.from("store_customer_profiles").select("relationship_id, relationship:user_creation_relationships!relationship_id(user_id, store_id)").eq("drivers_license_number", dl).limit(1).maybeSingle();
183
+ if (profMatch) {
184
+ const rel = profMatch.relationship;
185
+ if (rel?.user_id) {
186
+ platformUserId = rel.user_id;
187
+ matchedVia = "drivers_license";
441
188
  }
442
- if (phoneDupes.length === 0 && nameDupes.length === 0) {
443
- lines.push("\nNo duplicates found.");
444
- }
445
- return { success: true, data: lines.join("\n") };
189
+ }
446
190
  }
447
- // ---- MERGE: merge two customer records into one ----
448
- case "merge": {
449
- const primaryId = args.primary_customer_id;
450
- const secondaryId = args.secondary_customer_id;
451
- if (!primaryId || !secondaryId)
452
- return { success: false, error: "primary_customer_id and secondary_customer_id required" };
453
- if (primaryId === secondaryId)
454
- return { success: false, error: "Cannot merge a customer with itself" };
455
- // Verify both exist and belong to this store
456
- const { data: primary } = await sb.from("v_store_customers").select("*").eq("id", primaryId).eq("store_id", sid).single();
457
- const { data: secondary } = await sb.from("v_store_customers").select("*").eq("id", secondaryId).eq("store_id", sid).single();
458
- if (!primary)
459
- return { success: false, error: `Primary customer ${primaryId} not found in this store` };
460
- if (!secondary)
461
- return { success: false, error: `Secondary customer ${secondaryId} not found in this store` };
462
- // Reassign all child records from secondary to primary
463
- // Tables with store_id column get scoped; tables without rely on customer ownership verification above
464
- const storeScoped = ["orders"];
465
- const customerOnly = ["customer_activity", "customer_notes", "customer_loyalty", "customer_sessions", "customer_segment_memberships"];
466
- const reassignResults = {};
467
- for (const table of storeScoped) {
468
- const { error, count } = await sb.from(table)
469
- .update({ customer_id: primaryId }).eq("customer_id", secondaryId).eq("store_id", sid);
470
- reassignResults[table] = error ? `error: ${error.message}` : `moved ${count ?? "?"} rows`;
471
- }
472
- for (const table of customerOnly) {
473
- const { error, count } = await sb.from(table)
474
- .update({ customer_id: primaryId }).eq("customer_id", secondaryId);
475
- reassignResults[table] = error ? `error: ${error.message}` : `moved ${count ?? "?"} rows`;
476
- }
477
- // Merge profile stats
478
- const { data: primaryProf } = await sb.from("store_customer_profiles")
479
- .select("*").eq("relationship_id", primaryId).maybeSingle();
480
- const { data: secondaryProf } = await sb.from("store_customer_profiles")
481
- .select("*").eq("relationship_id", secondaryId).maybeSingle();
482
- if (primaryProf && secondaryProf) {
483
- await sb.from("store_customer_profiles").update({
484
- total_spent: parseFloat(primaryProf.total_spent || 0) + parseFloat(secondaryProf.total_spent || 0),
485
- total_orders: (primaryProf.total_orders || 0) + (secondaryProf.total_orders || 0),
486
- loyalty_points: (primaryProf.loyalty_points || 0) + (secondaryProf.loyalty_points || 0),
487
- lifetime_points_earned: (primaryProf.lifetime_points_earned || 0) + (secondaryProf.lifetime_points_earned || 0),
488
- first_order_at: primaryProf.first_order_at && secondaryProf.first_order_at
489
- ? (primaryProf.first_order_at < secondaryProf.first_order_at ? primaryProf.first_order_at : secondaryProf.first_order_at)
490
- : primaryProf.first_order_at || secondaryProf.first_order_at,
491
- last_order_at: primaryProf.last_order_at && secondaryProf.last_order_at
492
- ? (primaryProf.last_order_at > secondaryProf.last_order_at ? primaryProf.last_order_at : secondaryProf.last_order_at)
493
- : primaryProf.last_order_at || secondaryProf.last_order_at,
494
- updated_at: new Date().toISOString(),
495
- }).eq("relationship_id", primaryId);
496
- await sb.from("store_customer_profiles").delete().eq("relationship_id", secondaryId);
497
- }
498
- // Mark secondary as merged
499
- await sb.from("user_creation_relationships").update({
500
- status: "merged",
501
- relationship_data: { merged_into: primaryId, merged_at: new Date().toISOString() },
502
- updated_at: new Date().toISOString(),
503
- }).eq("id", secondaryId).eq("store_id", sid);
504
- // Fill in missing identity fields from secondary
505
- const { data: primaryRel } = await sb.from("user_creation_relationships").select("user_id").eq("id", primaryId).eq("store_id", sid).single();
506
- const { data: secondaryRel } = await sb.from("user_creation_relationships").select("user_id").eq("id", secondaryId).eq("store_id", sid).single();
507
- if (primaryRel && secondaryRel) {
508
- const { data: pPu } = await sb.from("platform_users").select("*").eq("id", primaryRel.user_id).single();
509
- const { data: sPu } = await sb.from("platform_users").select("*").eq("id", secondaryRel.user_id).single();
510
- if (pPu && sPu) {
511
- const fills = {};
512
- if (!pPu.email && sPu.email && !sPu.email.startsWith("merged."))
513
- fills.email = sPu.email;
514
- if (!pPu.phone && sPu.phone && !sPu.phone.startsWith("merged_"))
515
- fills.phone = sPu.phone;
516
- if (!pPu.date_of_birth && sPu.date_of_birth)
517
- fills.date_of_birth = sPu.date_of_birth;
518
- if (Object.keys(fills).length > 0)
519
- await sb.from("platform_users").update(fills).eq("id", primaryRel.user_id);
520
- // Mark secondary identity as merged
521
- const marks = {};
522
- if (sPu.email && !sPu.email.startsWith("merged."))
523
- marks.email = `merged.${sPu.email}`;
524
- if (sPu.phone && !sPu.phone.startsWith("merged_"))
525
- marks.phone = `merged_${sPu.phone}`;
526
- if (Object.keys(marks).length > 0)
527
- await sb.from("platform_users").update(marks).eq("id", secondaryRel.user_id);
528
- }
529
- }
530
- const { data: merged } = await sb.from("v_store_customers").select("*").eq("id", primaryId).single();
531
- return { success: true, data: { merged_customer: stripInternal(merged), reassign_results: reassignResults } };
191
+ // Tier 4: exact name + date of birth (only when all 3 are provided)
192
+ if (!platformUserId && firstName && lastName && dob) {
193
+ const {
194
+ data: nameMatch
195
+ } = await sb.from("platform_users").select("id").ilike("first_name", firstName).ilike("last_name", lastName).eq("date_of_birth", dob).limit(1).maybeSingle();
196
+ if (nameMatch) {
197
+ platformUserId = nameMatch.id;
198
+ matchedVia = "name+dob";
199
+ }
532
200
  }
533
- // ---- ADD_NOTE ----
534
- case "add_note": {
535
- // Verify customer belongs to this store before adding note
536
- const { data: custCheck } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
537
- if (!custCheck)
538
- return { success: false, error: "Customer not found in this store" };
539
- const { data, error } = await sb.from("customer_notes")
540
- .insert({ customer_id: args.customer_id, note: args.note, created_by: args.created_by || "agent" })
541
- .select().single();
542
- return error ? { success: false, error: error.message } : { success: true, data };
201
+
202
+ // Create platform_user if no match found
203
+ const isNameOnly = !email && !phone;
204
+ if (!platformUserId) {
205
+ const puInsert = {
206
+ first_name: firstName,
207
+ last_name: lastName
208
+ };
209
+ if (email) puInsert.email = email;
210
+ if (phone) puInsert.phone = phone;
211
+ if (dob) puInsert.date_of_birth = dob;
212
+ const {
213
+ data: newPu,
214
+ error: puErr
215
+ } = await sb.from("platform_users").insert(puInsert).select("id").single();
216
+ if (puErr) return {
217
+ success: false,
218
+ error: `Failed to create platform user: ${puErr.message}`
219
+ };
220
+ platformUserId = newPu.id;
543
221
  }
544
- // ---- NOTES ----
545
- case "notes": {
546
- // Verify customer belongs to this store
547
- const { data: custCheck } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
548
- if (!custCheck)
549
- return { success: false, error: "Customer not found in this store" };
550
- const { data, error } = await sb.from("customer_notes")
551
- .select("id, note, created_by, created_at")
552
- .eq("customer_id", args.customer_id)
553
- .order("created_at", { ascending: false }).limit(args.limit || 25);
554
- return error ? { success: false, error: error.message } : { success: true, data };
222
+
223
+ // Check if relationship already exists for this store
224
+ const {
225
+ data: existingRel
226
+ } = await sb.from("user_creation_relationships").select("id").eq("user_id", platformUserId).eq("store_id", sid).maybeSingle();
227
+ if (existingRel) {
228
+ const {
229
+ data: existing
230
+ } = await sb.from("v_store_customers").select("*").eq("id", existingRel.id).single();
231
+ return {
232
+ success: true,
233
+ data: stripInternal(existing),
234
+ note: `Customer already exists (matched via ${matchedVia || "existing record"})`
235
+ };
555
236
  }
556
- // ---- ACTIVITY ----
557
- case "activity": {
558
- // Verify customer belongs to this store
559
- const { data: custCheck } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
560
- if (!custCheck)
561
- return { success: false, error: "Customer not found in this store" };
562
- const { data, error } = await sb.from("customer_activity")
563
- .select("id, activity_type, description, metadata, created_at")
564
- .eq("customer_id", args.customer_id)
565
- .order("created_at", { ascending: false }).limit(args.limit || 25);
566
- return error ? { success: false, error: error.message } : { success: true, data };
237
+
238
+ // Create relationship
239
+ const {
240
+ data: rel,
241
+ error: relErr
242
+ } = await sb.from("user_creation_relationships").insert({
243
+ user_id: platformUserId,
244
+ creation_id: sid,
245
+ creation_type: "store",
246
+ store_id: sid,
247
+ role: "user",
248
+ status: "active",
249
+ email_consent: args.email_consent ?? false,
250
+ sms_consent: args.sms_consent ?? false
251
+ }).select("id").single();
252
+ if (relErr) return {
253
+ success: false,
254
+ error: `Failed to create customer relationship: ${relErr.message}`
255
+ };
256
+
257
+ // Create store profile
258
+ const profileInsert = {
259
+ relationship_id: rel.id
260
+ };
261
+ if (args.street_address) profileInsert.street_address = args.street_address;
262
+ if (args.city) profileInsert.city = args.city;
263
+ if (args.state) profileInsert.state = args.state;
264
+ if (args.postal_code) profileInsert.postal_code = args.postal_code;
265
+ if (dl) profileInsert.drivers_license_number = dl;
266
+ if (args.medical_card_number) profileInsert.medical_card_number = args.medical_card_number;
267
+ if (args.medical_card_expiry) profileInsert.medical_card_expiry = args.medical_card_expiry;
268
+ if (args.gender) profileInsert.gender = args.gender;
269
+ if (args.eye_color) profileInsert.eye_color = args.eye_color;
270
+ if (args.hair_color) profileInsert.hair_color = args.hair_color;
271
+ if (args.height_raw) profileInsert.height_raw = args.height_raw;
272
+ if (args.weight) profileInsert.weight = args.weight;
273
+ if (args.suffix) profileInsert.suffix = args.suffix;
274
+ if (args.is_staff !== undefined) profileInsert.is_staff = args.is_staff;
275
+ await sb.from("store_customer_profiles").insert(profileInsert);
276
+ const {
277
+ data: created
278
+ } = await sb.from("v_store_customers").select("*").eq("id", rel.id).single();
279
+ return {
280
+ success: true,
281
+ data: stripInternal(created),
282
+ ...(matchedVia ? {
283
+ note: `Linked to existing customer record (matched via ${matchedVia})`
284
+ } : {}),
285
+ ...(isNameOnly ? {
286
+ warning: "Created with no email or phone — add contact info to prevent duplicates."
287
+ } : {})
288
+ };
289
+ }
290
+
291
+ // ---- UPDATE: modify any customer fields ----
292
+ case "update":
293
+ {
294
+ const custId = args.customer_id;
295
+ const {
296
+ data: rel,
297
+ error: relErr
298
+ } = await sb.from("user_creation_relationships").select("id, user_id").eq("id", custId).eq("store_id", sid).single();
299
+ if (relErr) return {
300
+ success: false,
301
+ error: `Customer not found: ${relErr.message}`
302
+ };
303
+
304
+ // Customer identity (name, email, phone, DOB)
305
+ const identityUpdates = {};
306
+ if (args.first_name !== undefined) identityUpdates.first_name = args.first_name;
307
+ if (args.last_name !== undefined) identityUpdates.last_name = args.last_name;
308
+ if (args.email !== undefined) identityUpdates.email = args.email;
309
+ if (args.phone !== undefined) identityUpdates.phone = args.phone;
310
+ if (args.date_of_birth !== undefined) identityUpdates.date_of_birth = args.date_of_birth;
311
+ if (Object.keys(identityUpdates).length > 0) {
312
+ identityUpdates.updated_at = new Date().toISOString();
313
+ const {
314
+ error: puErr
315
+ } = await sb.from("platform_users").update(identityUpdates).eq("id", rel.user_id);
316
+ if (puErr) return {
317
+ success: false,
318
+ error: `Failed to update customer: ${puErr.message}`
319
+ };
567
320
  }
568
- // ---- ORDERS: customer order history ----
569
- case "orders": {
570
- const { data, error } = await sb.from("orders")
571
- .select("id, order_number, status, total_amount, subtotal, tax_amount, payment_status, fulfillment_status, payment_method, created_at")
572
- .eq("customer_id", args.customer_id).eq("store_id", sid)
573
- .order("created_at", { ascending: false }).limit(args.limit || 25);
574
- return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
321
+
322
+ // Consent & status
323
+ const relUpdates = {};
324
+ if (args.status !== undefined) relUpdates.status = args.status;
325
+ if (args.email_consent !== undefined) relUpdates.email_consent = args.email_consent;
326
+ if (args.sms_consent !== undefined) relUpdates.sms_consent = args.sms_consent;
327
+ if (args.push_consent !== undefined) relUpdates.push_consent = args.push_consent;
328
+ if (Object.keys(relUpdates).length > 0) {
329
+ relUpdates.updated_at = new Date().toISOString();
330
+ await sb.from("user_creation_relationships").update(relUpdates).eq("id", custId);
575
331
  }
576
- // ---- LINK_CHANNEL_IDENTITY: connect a sender_id on any channel to a customer ----
577
- case "link_channel_identity": {
578
- const { customer_id, channel_type, sender_id, sender_name } = args;
579
- if (!customer_id || !channel_type || !sender_id) {
580
- return { success: false, error: "customer_id, channel_type, and sender_id are required" };
581
- }
582
- // Verify customer belongs to this store
583
- const { data: custCheck } = await sb.from("v_store_customers")
584
- .select("id").eq("id", customer_id).eq("store_id", sid).single();
585
- if (!custCheck)
586
- return { success: false, error: "Customer not found in this store" };
587
- const { error } = await sb.from("customer_channel_identities").upsert({
588
- store_id: sid,
589
- customer_id,
590
- channel_type,
591
- sender_id,
592
- sender_name: sender_name || null,
593
- verified: true,
594
- last_seen_at: new Date().toISOString(),
595
- }, { onConflict: "store_id,channel_type,sender_id" });
596
- return error
597
- ? { success: false, error: error.message }
598
- : { success: true, linked: { customer_id, channel_type, sender_id } };
332
+
333
+ // Profile fields
334
+ const profUpdates = {};
335
+ if (args.loyalty_points !== undefined) profUpdates.loyalty_points = args.loyalty_points;
336
+ if (args.loyalty_tier !== undefined) profUpdates.loyalty_tier = args.loyalty_tier;
337
+ if (args.street_address !== undefined) profUpdates.street_address = args.street_address;
338
+ if (args.city !== undefined) profUpdates.city = args.city;
339
+ if (args.state !== undefined) profUpdates.state = args.state;
340
+ if (args.postal_code !== undefined) profUpdates.postal_code = args.postal_code;
341
+ if (args.billing_address !== undefined) profUpdates.billing_address = args.billing_address;
342
+ if (args.shipping_addresses !== undefined) profUpdates.shipping_addresses = args.shipping_addresses;
343
+ if (args.default_shipping_address_index !== undefined) profUpdates.default_shipping_address_index = args.default_shipping_address_index;
344
+ if (args.drivers_license_number !== undefined) profUpdates.drivers_license_number = args.drivers_license_number;
345
+ if (args.id_verified !== undefined) {
346
+ profUpdates.id_verified = args.id_verified;
347
+ if (args.id_verified === true) profUpdates.id_verified_at = new Date().toISOString();
348
+ }
349
+ if (args.license_expiration_date !== undefined) profUpdates.license_expiration_date = args.license_expiration_date;
350
+ if (args.license_issue_date !== undefined) profUpdates.license_issue_date = args.license_issue_date;
351
+ if (args.medical_card_number !== undefined) profUpdates.medical_card_number = args.medical_card_number;
352
+ if (args.medical_card_expiry !== undefined) profUpdates.medical_card_expiry = args.medical_card_expiry;
353
+ if (args.gender !== undefined) profUpdates.gender = args.gender;
354
+ if (args.eye_color !== undefined) profUpdates.eye_color = args.eye_color;
355
+ if (args.height_raw !== undefined) profUpdates.height_raw = args.height_raw;
356
+ if (args.weight !== undefined) profUpdates.weight = args.weight;
357
+ if (args.hair_color !== undefined) profUpdates.hair_color = args.hair_color;
358
+ if (args.suffix !== undefined) profUpdates.suffix = args.suffix;
359
+ if (args.is_staff !== undefined) profUpdates.is_staff = args.is_staff;
360
+ if (args.has_wallet_pass !== undefined) profUpdates.has_wallet_pass = args.has_wallet_pass;
361
+ if (args.is_wholesale_approved !== undefined) profUpdates.is_wholesale_approved = args.is_wholesale_approved;
362
+ if (args.wholesale_tier !== undefined) profUpdates.wholesale_tier = args.wholesale_tier;
363
+ if (args.wholesale_business_name !== undefined) profUpdates.wholesale_business_name = args.wholesale_business_name;
364
+ if (args.wholesale_business_type !== undefined) profUpdates.wholesale_business_type = args.wholesale_business_type;
365
+ if (args.wholesale_license_number !== undefined) profUpdates.wholesale_license_number = args.wholesale_license_number;
366
+ if (args.wholesale_tax_id !== undefined) profUpdates.wholesale_tax_id = args.wholesale_tax_id;
367
+ if (args.wholesale_discount_percent !== undefined) profUpdates.wholesale_discount_percent = args.wholesale_discount_percent;
368
+ if (args.wholesale_payment_terms !== undefined) profUpdates.wholesale_payment_terms = args.wholesale_payment_terms;
369
+ if (args.wholesale_credit_limit !== undefined) profUpdates.wholesale_credit_limit = args.wholesale_credit_limit;
370
+ if (args.wholesale_resale_certificate !== undefined) profUpdates.wholesale_resale_certificate = args.wholesale_resale_certificate;
371
+ if (args.wholesale_notes !== undefined) profUpdates.wholesale_notes = args.wholesale_notes;
372
+ if (Object.keys(profUpdates).length > 0) {
373
+ profUpdates.updated_at = new Date().toISOString();
374
+ const {
375
+ error: profErr
376
+ } = await sb.from("store_customer_profiles").update(profUpdates).eq("relationship_id", custId);
377
+ if (profErr) return {
378
+ success: false,
379
+ error: `Failed to update profile: ${profErr.message}`
380
+ };
381
+ }
382
+ const {
383
+ data: updated
384
+ } = await sb.from("v_segment_customers").select("*").eq("id", custId).eq("store_id", sid).single();
385
+ return {
386
+ success: true,
387
+ data: stripInternal(updated)
388
+ };
389
+ }
390
+
391
+ // ---- FIND_DUPLICATES: identify potential duplicate customer accounts ----
392
+ case "find_duplicates":
393
+ {
394
+ // Try RPC first (may not exist yet)
395
+ const {
396
+ data: dupes,
397
+ error: dupeErr
398
+ } = await sb.rpc("find_duplicate_customers", {
399
+ p_store_id: sid
400
+ });
401
+ if (!dupeErr && dupes) return {
402
+ success: true,
403
+ data: dupes
404
+ };
405
+
406
+ // Fallback: manual duplicate detection by phone
407
+ const {
408
+ data: byPhone,
409
+ error: phErr
410
+ } = await sb.from("v_store_customers").select("id, first_name, last_name, email, phone, total_spent, total_orders, created_at").eq("store_id", sid).not("phone", "is", null).order("phone");
411
+ if (phErr) return {
412
+ success: false,
413
+ error: phErr.message
414
+ };
415
+ const phoneMap = new Map();
416
+ for (const c of byPhone || []) {
417
+ if (!c.phone) continue;
418
+ const normalized = c.phone.replace(/\D/g, "").slice(-10);
419
+ if (!phoneMap.has(normalized)) phoneMap.set(normalized, []);
420
+ phoneMap.get(normalized).push(c);
599
421
  }
600
- // ---- GET_CHANNEL_IDENTITIES: list all channel identities for a customer ----
601
- case "get_channel_identities": {
602
- const custId = args.customer_id;
603
- if (!custId)
604
- return { success: false, error: "customer_id is required" };
605
- const { data, error } = await sb.from("customer_channel_identities")
606
- .select("id, channel_type, sender_id, sender_name, verified, last_seen_at, created_at")
607
- .eq("store_id", sid).eq("customer_id", custId)
608
- .order("last_seen_at", { ascending: false });
609
- return error ? { success: false, error: error.message } : { success: true, data };
422
+ const phoneDupes = Array.from(phoneMap.entries()).filter(([_, custs]) => custs.length > 1).map(([phone, custs]) => ({
423
+ phone,
424
+ count: custs.length,
425
+ customers: custs
426
+ }));
427
+
428
+ // Also check by exact name match
429
+ const {
430
+ data: byName
431
+ } = await sb.from("v_store_customers").select("id, first_name, last_name, email, phone, total_spent, total_orders, created_at").eq("store_id", sid).order("last_name").order("first_name");
432
+ const nameMap = new Map();
433
+ for (const c of byName || []) {
434
+ const key = `${(c.first_name || "").toLowerCase().trim()} ${(c.last_name || "").toLowerCase().trim()}`;
435
+ if (!key.trim()) continue;
436
+ if (!nameMap.has(key)) nameMap.set(key, []);
437
+ nameMap.get(key).push(c);
610
438
  }
611
- // ---- INVITE: create auth account for customer and send branded invite email ----
612
- case "invite": {
613
- const custId = args.customer_id;
614
- if (!custId)
615
- return { success: false, error: "customer_id is required" };
616
- // Verify customer belongs to this store
617
- const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
618
- .select("id, user_id, store_id")
619
- .eq("id", custId).eq("store_id", sid).single();
620
- if (relErr || !rel)
621
- return { success: false, error: "Customer not found for this store" };
622
- // Get platform user + store name in parallel
623
- const [{ data: pu }, { data: store }] = await Promise.all([
624
- sb.from("platform_users").select("id, auth_id, email, first_name, last_name").eq("id", rel.user_id).single(),
625
- sb.from("stores").select("store_name").eq("id", sid).single(),
626
- ]);
627
- if (!pu)
628
- return { success: false, error: "Platform user not found" };
629
- if (!pu.email)
630
- return { success: false, error: "Customer has no email address — add one first" };
631
- const storeName = store?.store_name || "our platform";
632
- const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ") || "there";
633
- const portalUrl = args.portal_url || args.redirect_to || null;
634
- // If already has an auth account, just resend the email with a new magic link
635
- let authId = pu.auth_id;
636
- if (!authId) {
637
- // Create auth account silently (no Supabase built-in email)
638
- const { data: newUser, error: createErr } = await sb.auth.admin.createUser({
639
- email: pu.email,
640
- email_confirm: true, // skip verification — store is vouching for this email
641
- user_metadata: {
642
- first_name: pu.first_name,
643
- last_name: pu.last_name,
644
- invited_by_store: sid,
645
- },
646
- });
647
- if (createErr)
648
- return { success: false, error: `Account creation failed: ${createErr.message}` };
649
- authId = newUser.user.id;
650
- // Link auth account to platform user
651
- await sb.from("platform_users").update({ auth_id: authId }).eq("id", pu.id);
439
+ const nameDupes = Array.from(nameMap.entries()).filter(([_, custs]) => custs.length > 1).map(([name, custs]) => ({
440
+ name,
441
+ count: custs.length,
442
+ customers: custs
443
+ }));
444
+
445
+ // Pre-format as markdown formatter drops nested customer arrays
446
+ const totalPhoneDupes = phoneDupes.reduce((s, d) => s + d.count, 0);
447
+ const totalNameDupes = nameDupes.reduce((s, d) => s + d.count, 0);
448
+ const lines = [`## Duplicate Customers`, `**By Phone**: ${phoneDupes.length} groups (${totalPhoneDupes} customers) | **By Name**: ${nameDupes.length} groups (${totalNameDupes} customers)\n`];
449
+ if (phoneDupes.length > 0) {
450
+ lines.push("### Phone Duplicates");
451
+ for (const group of phoneDupes.slice(0, 20)) {
452
+ lines.push(`\n**Phone ${group.phone}** (${group.count} customers):`);
453
+ lines.push("| Name | Email | Phone | Orders | Spent | Created |");
454
+ lines.push("| --- | --- | --- | ---: | ---: | --- |");
455
+ for (const c of group.customers) {
456
+ lines.push(`| ${c.first_name || ""} ${c.last_name || ""} | ${c.email || "—"} | ${c.phone || "—"} | ${c.total_orders ?? 0} | $${c.total_spent ?? 0} | ${c.created_at?.slice(0, 10) || "—"} |`);
652
457
  }
653
- // Generate a magic link for the customer to set up / log in
654
- const { data: linkData, error: linkErr } = await sb.auth.admin.generateLink({
655
- type: "magiclink",
656
- email: pu.email,
657
- options: {
658
- ...(portalUrl ? { redirectTo: portalUrl } : {}),
659
- },
660
- });
661
- if (linkErr)
662
- return { success: false, error: `Link generation failed: ${linkErr.message}` };
663
- const magicLink = linkData?.properties?.action_link || "";
664
- // Send branded invite email through our email system
665
- const emailResult = await handleEmail(sb, {
666
- action: "send_template",
667
- to: pu.email,
668
- template: "customer-portal-invite",
669
- template_data: {
670
- customer_name: customerName,
671
- store_name: storeName,
672
- magic_link: magicLink,
673
- portal_url: portalUrl || magicLink,
674
- },
675
- }, sid);
676
- return {
677
- success: true,
678
- data: {
679
- email: pu.email,
680
- name: customerName,
681
- invited: true,
682
- email_sent: emailResult.success,
683
- ...(emailResult.success ? {} : { email_error: emailResult.error }),
684
- },
685
- };
458
+ }
686
459
  }
687
- // ---- LOYALTY_HISTORY: transaction history for a customer ----
688
- case "loyalty_history": {
689
- const custId = args.customer_id;
690
- if (!custId)
691
- return { success: false, error: "customer_id is required" };
692
- let q = sb.from("loyalty_transactions")
693
- .select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
694
- .eq("customer_id", custId)
695
- .order("created_at", { ascending: false });
696
- if (args.transaction_type)
697
- q = q.eq("transaction_type", args.transaction_type);
698
- q = q.limit(args.limit || 50);
699
- const { data, error } = await q;
700
- return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
460
+ if (nameDupes.length > 0) {
461
+ lines.push("\n### Name Duplicates");
462
+ for (const group of nameDupes.slice(0, 20)) {
463
+ lines.push(`\n**"${group.name}"** (${group.count} customers):`);
464
+ lines.push("| Name | Email | Phone | Orders | Spent | Created |");
465
+ lines.push("| --- | --- | --- | ---: | ---: | --- |");
466
+ for (const c of group.customers) {
467
+ lines.push(`| ${c.first_name || ""} ${c.last_name || ""} | ${c.email || "—"} | ${c.phone || "—"} | ${c.total_orders ?? 0} | $${c.total_spent ?? 0} | ${c.created_at?.slice(0, 10) || "—"} |`);
468
+ }
469
+ }
701
470
  }
702
- // ---- ADJUST_LOYALTY: award or deduct loyalty points via RPC ----
703
- case "adjust_loyalty": {
704
- const custId = args.customer_id;
705
- const points = args.points;
706
- const reason = args.reason;
707
- if (!custId || points === undefined || !reason)
708
- return { success: false, error: "customer_id, points, and reason are required" };
709
- const { data, error } = await sb.rpc("adjust_customer_loyalty_points", {
710
- p_customer_id: custId,
711
- p_points_change: points,
712
- p_reason: reason,
713
- });
714
- if (error)
715
- return { success: false, error: error.message };
716
- const { data: updated } = await sb.from("store_customer_profiles")
717
- .select("loyalty_points, loyalty_tier, lifetime_points_earned")
718
- .eq("relationship_id", custId).maybeSingle();
719
- return { success: true, data: { rpc_result: data, ...updated } };
471
+ if (phoneDupes.length === 0 && nameDupes.length === 0) {
472
+ lines.push("\nNo duplicates found.");
720
473
  }
721
- // ---- SEGMENTS: list all customer segments for the store ----
722
- case "segments": {
723
- let q = sb.from("customer_segments")
724
- .select("id, name, ai_description, type, segment_rules, filter_criteria, customer_count, is_active, color, icon, targeting_tips, created_at")
725
- .eq("store_id", sid)
726
- .order("customer_count", { ascending: false });
727
- if (args.limit)
728
- q = q.limit(args.limit);
729
- const { data, error } = await q;
730
- return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
474
+ return {
475
+ success: true,
476
+ data: lines.join("\n")
477
+ };
478
+ }
479
+
480
+ // ---- MERGE: merge two customer records into one ----
481
+ case "merge":
482
+ {
483
+ const primaryId = args.primary_customer_id;
484
+ const secondaryId = args.secondary_customer_id;
485
+ if (!primaryId || !secondaryId) return {
486
+ success: false,
487
+ error: "primary_customer_id and secondary_customer_id required"
488
+ };
489
+ if (primaryId === secondaryId) return {
490
+ success: false,
491
+ error: "Cannot merge a customer with itself"
492
+ };
493
+
494
+ // Verify both exist and belong to this store
495
+ const {
496
+ data: primary
497
+ } = await sb.from("v_store_customers").select("*").eq("id", primaryId).eq("store_id", sid).single();
498
+ const {
499
+ data: secondary
500
+ } = await sb.from("v_store_customers").select("*").eq("id", secondaryId).eq("store_id", sid).single();
501
+ if (!primary) return {
502
+ success: false,
503
+ error: `Primary customer ${primaryId} not found in this store`
504
+ };
505
+ if (!secondary) return {
506
+ success: false,
507
+ error: `Secondary customer ${secondaryId} not found in this store`
508
+ };
509
+
510
+ // Reassign all child records from secondary to primary
511
+ // Tables with store_id column get scoped; tables without rely on customer ownership verification above
512
+ const storeScoped = ["orders"];
513
+ const customerOnly = ["customer_activity", "customer_notes", "customer_loyalty", "customer_sessions", "customer_segment_memberships"];
514
+ const reassignResults = {};
515
+ for (const table of storeScoped) {
516
+ const {
517
+ error,
518
+ count
519
+ } = await sb.from(table).update({
520
+ customer_id: primaryId
521
+ }).eq("customer_id", secondaryId).eq("store_id", sid);
522
+ reassignResults[table] = error ? `error: ${error.message}` : `moved ${count ?? "?"} rows`;
731
523
  }
732
- // ---- SEGMENT_MEMBERS: list customers in a specific segment ----
733
- case "segment_members": {
734
- const segId = args.segment_id;
735
- if (!segId)
736
- return { success: false, error: "segment_id is required" };
737
- const { data, error } = await sb.from("customer_segment_memberships")
738
- .select("added_at, customer:v_segment_customers!customer_id(*)")
739
- .eq("segment_id", segId)
740
- .order("added_at", { ascending: false })
741
- .limit(args.limit || 50);
742
- if (error)
743
- return { success: false, error: error.message };
744
- const members = (data || []).map((r) => ({ added_at: r.added_at, ...r.customer }))
745
- .filter((m) => m.store_id === sid);
746
- return { success: true, count: members.length, data: stripInternalArray(members) };
524
+ for (const table of customerOnly) {
525
+ const {
526
+ error,
527
+ count
528
+ } = await sb.from(table).update({
529
+ customer_id: primaryId
530
+ }).eq("customer_id", secondaryId);
531
+ reassignResults[table] = error ? `error: ${error.message}` : `moved ${count ?? "?"} rows`;
747
532
  }
748
- // ---- REFRESH_METRICS: recalculate all customer metrics for the store ----
749
- case "refresh_metrics": {
750
- const start = Date.now();
751
- const { data, error } = await sb.rpc("refresh_customer_metrics", { p_store_id: sid });
752
- const elapsed = Date.now() - start;
753
- if (error)
754
- return { success: false, error: error.message };
755
- return { success: true, data: { result: data, elapsed_ms: elapsed, message: `Customer metrics refreshed in ${elapsed}ms` } };
533
+
534
+ // Merge profile stats
535
+ const {
536
+ data: primaryProf
537
+ } = await sb.from("store_customer_profiles").select("*").eq("relationship_id", primaryId).maybeSingle();
538
+ const {
539
+ data: secondaryProf
540
+ } = await sb.from("store_customer_profiles").select("*").eq("relationship_id", secondaryId).maybeSingle();
541
+ if (primaryProf && secondaryProf) {
542
+ await sb.from("store_customer_profiles").update({
543
+ total_spent: parseFloat(primaryProf.total_spent || 0) + parseFloat(secondaryProf.total_spent || 0),
544
+ total_orders: (primaryProf.total_orders || 0) + (secondaryProf.total_orders || 0),
545
+ loyalty_points: (primaryProf.loyalty_points || 0) + (secondaryProf.loyalty_points || 0),
546
+ lifetime_points_earned: (primaryProf.lifetime_points_earned || 0) + (secondaryProf.lifetime_points_earned || 0),
547
+ first_order_at: primaryProf.first_order_at && secondaryProf.first_order_at ? primaryProf.first_order_at < secondaryProf.first_order_at ? primaryProf.first_order_at : secondaryProf.first_order_at : primaryProf.first_order_at || secondaryProf.first_order_at,
548
+ last_order_at: primaryProf.last_order_at && secondaryProf.last_order_at ? primaryProf.last_order_at > secondaryProf.last_order_at ? primaryProf.last_order_at : secondaryProf.last_order_at : primaryProf.last_order_at || secondaryProf.last_order_at,
549
+ updated_at: new Date().toISOString()
550
+ }).eq("relationship_id", primaryId);
551
+ await sb.from("store_customer_profiles").delete().eq("relationship_id", secondaryId);
756
552
  }
757
- default:
758
- return { success: false, error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, invite, find_duplicates, merge, add_note, notes, activity, orders, loyalty_history, adjust_loyalty, segments, segment_members, refresh_metrics, link_channel_identity, get_channel_identities` };
759
- }
760
- }
761
- export async function handleOrders(sb, args, storeId) {
762
- const sid = storeId;
763
- switch (args.action) {
764
- case "find": {
765
- let q = sb.from("orders")
766
- .select("id, order_number, status, total_amount, subtotal, tax_amount, created_at, customer_id, customer:v_store_customers!customer_id(id, first_name, last_name, email, phone)")
767
- .eq("store_id", sid).order("created_at", { ascending: false });
768
- if (args.limit)
769
- q = q.limit(args.limit);
770
- if (args.status)
771
- q = q.eq("status", args.status);
772
- if (args.customer_id)
773
- q = q.eq("customer_id", args.customer_id);
774
- if (args.order_number)
775
- q = q.eq("order_number", args.order_number);
776
- if (args.query) {
777
- const sq = sanitizeFilterValue(String(args.query));
778
- q = q.or(`order_number.ilike.%${sq}%`);
553
+
554
+ // Mark secondary as merged
555
+ await sb.from("user_creation_relationships").update({
556
+ status: "merged",
557
+ relationship_data: {
558
+ merged_into: primaryId,
559
+ merged_at: new Date().toISOString()
560
+ },
561
+ updated_at: new Date().toISOString()
562
+ }).eq("id", secondaryId).eq("store_id", sid);
563
+
564
+ // Fill in missing identity fields from secondary
565
+ const {
566
+ data: primaryRel
567
+ } = await sb.from("user_creation_relationships").select("user_id").eq("id", primaryId).eq("store_id", sid).single();
568
+ const {
569
+ data: secondaryRel
570
+ } = await sb.from("user_creation_relationships").select("user_id").eq("id", secondaryId).eq("store_id", sid).single();
571
+ if (primaryRel && secondaryRel) {
572
+ const {
573
+ data: pPu
574
+ } = await sb.from("platform_users").select("*").eq("id", primaryRel.user_id).single();
575
+ const {
576
+ data: sPu
577
+ } = await sb.from("platform_users").select("*").eq("id", secondaryRel.user_id).single();
578
+ if (pPu && sPu) {
579
+ const fills = {};
580
+ if (!pPu.email && sPu.email && !sPu.email.startsWith("merged.")) fills.email = sPu.email;
581
+ if (!pPu.phone && sPu.phone && !sPu.phone.startsWith("merged_")) fills.phone = sPu.phone;
582
+ if (!pPu.date_of_birth && sPu.date_of_birth) fills.date_of_birth = sPu.date_of_birth;
583
+ if (Object.keys(fills).length > 0) await sb.from("platform_users").update(fills).eq("id", primaryRel.user_id);
584
+ // Mark secondary identity as merged
585
+ const marks = {};
586
+ if (sPu.email && !sPu.email.startsWith("merged.")) marks.email = `merged.${sPu.email}`;
587
+ if (sPu.phone && !sPu.phone.startsWith("merged_")) marks.phone = `merged_${sPu.phone}`;
588
+ if (Object.keys(marks).length > 0) await sb.from("platform_users").update(marks).eq("id", secondaryRel.user_id);
589
+ }
590
+ }
591
+ const {
592
+ data: merged
593
+ } = await sb.from("v_store_customers").select("*").eq("id", primaryId).single();
594
+ return {
595
+ success: true,
596
+ data: {
597
+ merged_customer: stripInternal(merged),
598
+ reassign_results: reassignResults
599
+ }
600
+ };
601
+ }
602
+
603
+ // ---- ADD_NOTE ----
604
+ case "add_note":
605
+ {
606
+ // Verify customer belongs to this store before adding note
607
+ const {
608
+ data: custCheck
609
+ } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
610
+ if (!custCheck) return {
611
+ success: false,
612
+ error: "Customer not found in this store"
613
+ };
614
+ const {
615
+ data,
616
+ error
617
+ } = await sb.from("customer_notes").insert({
618
+ customer_id: args.customer_id,
619
+ note: args.note,
620
+ created_by: args.created_by || "agent"
621
+ }).select().single();
622
+ return error ? {
623
+ success: false,
624
+ error: error.message
625
+ } : {
626
+ success: true,
627
+ data
628
+ };
629
+ }
630
+
631
+ // ---- NOTES ----
632
+ case "notes":
633
+ {
634
+ // Verify customer belongs to this store
635
+ const {
636
+ data: custCheck
637
+ } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
638
+ if (!custCheck) return {
639
+ success: false,
640
+ error: "Customer not found in this store"
641
+ };
642
+ const {
643
+ data,
644
+ error
645
+ } = await sb.from("customer_notes").select("id, note, created_by, created_at").eq("customer_id", args.customer_id).order("created_at", {
646
+ ascending: false
647
+ }).limit(args.limit || 25);
648
+ return error ? {
649
+ success: false,
650
+ error: error.message
651
+ } : {
652
+ success: true,
653
+ data
654
+ };
655
+ }
656
+
657
+ // ---- ACTIVITY ----
658
+ case "activity":
659
+ {
660
+ // Verify customer belongs to this store
661
+ const {
662
+ data: custCheck
663
+ } = await sb.from("v_store_customers").select("id").eq("id", args.customer_id).eq("store_id", sid).single();
664
+ if (!custCheck) return {
665
+ success: false,
666
+ error: "Customer not found in this store"
667
+ };
668
+ const {
669
+ data,
670
+ error
671
+ } = await sb.from("customer_activity").select("id, activity_type, description, metadata, created_at").eq("customer_id", args.customer_id).order("created_at", {
672
+ ascending: false
673
+ }).limit(args.limit || 25);
674
+ return error ? {
675
+ success: false,
676
+ error: error.message
677
+ } : {
678
+ success: true,
679
+ data
680
+ };
681
+ }
682
+
683
+ // ---- ORDERS: customer order history ----
684
+ case "orders":
685
+ {
686
+ const {
687
+ data,
688
+ error
689
+ } = await sb.from("orders").select("id, order_number, status, total_amount, subtotal, tax_amount, payment_status, fulfillment_status, payment_method, created_at").eq("customer_id", args.customer_id).eq("store_id", sid).order("created_at", {
690
+ ascending: false
691
+ }).limit(args.limit || 25);
692
+ return error ? {
693
+ success: false,
694
+ error: error.message
695
+ } : {
696
+ success: true,
697
+ count: data?.length,
698
+ data
699
+ };
700
+ }
701
+
702
+ // ---- LINK_CHANNEL_IDENTITY: connect a sender_id on any channel to a customer ----
703
+ case "link_channel_identity":
704
+ {
705
+ const {
706
+ customer_id,
707
+ channel_type,
708
+ sender_id,
709
+ sender_name
710
+ } = args;
711
+ if (!customer_id || !channel_type || !sender_id) {
712
+ return {
713
+ success: false,
714
+ error: "customer_id, channel_type, and sender_id are required"
715
+ };
716
+ }
717
+ // Verify customer belongs to this store
718
+ const {
719
+ data: custCheck
720
+ } = await sb.from("v_store_customers").select("id").eq("id", customer_id).eq("store_id", sid).single();
721
+ if (!custCheck) return {
722
+ success: false,
723
+ error: "Customer not found in this store"
724
+ };
725
+ const {
726
+ error
727
+ } = await sb.from("customer_channel_identities").upsert({
728
+ store_id: sid,
729
+ customer_id,
730
+ channel_type,
731
+ sender_id,
732
+ sender_name: sender_name || null,
733
+ verified: true,
734
+ last_seen_at: new Date().toISOString()
735
+ }, {
736
+ onConflict: "store_id,channel_type,sender_id"
737
+ });
738
+ return error ? {
739
+ success: false,
740
+ error: error.message
741
+ } : {
742
+ success: true,
743
+ linked: {
744
+ customer_id,
745
+ channel_type,
746
+ sender_id
747
+ }
748
+ };
749
+ }
750
+
751
+ // ---- GET_CHANNEL_IDENTITIES: list all channel identities for a customer ----
752
+ case "get_channel_identities":
753
+ {
754
+ const custId = args.customer_id;
755
+ if (!custId) return {
756
+ success: false,
757
+ error: "customer_id is required"
758
+ };
759
+ const {
760
+ data,
761
+ error
762
+ } = await sb.from("customer_channel_identities").select("id, channel_type, sender_id, sender_name, verified, last_seen_at, created_at").eq("store_id", sid).eq("customer_id", custId).order("last_seen_at", {
763
+ ascending: false
764
+ });
765
+ return error ? {
766
+ success: false,
767
+ error: error.message
768
+ } : {
769
+ success: true,
770
+ data
771
+ };
772
+ }
773
+
774
+ // ---- INVITE: create auth account for customer and send branded invite email ----
775
+ case "invite":
776
+ {
777
+ const custId = args.customer_id;
778
+ if (!custId) return {
779
+ success: false,
780
+ error: "customer_id is required"
781
+ };
782
+
783
+ // Verify customer belongs to this store
784
+ const {
785
+ data: rel,
786
+ error: relErr
787
+ } = await sb.from("user_creation_relationships").select("id, user_id, store_id").eq("id", custId).eq("store_id", sid).single();
788
+ if (relErr || !rel) return {
789
+ success: false,
790
+ error: "Customer not found for this store"
791
+ };
792
+
793
+ // Get platform user + store name in parallel
794
+ const [{
795
+ data: pu
796
+ }, {
797
+ data: store
798
+ }] = await Promise.all([sb.from("platform_users").select("id, auth_id, email, first_name, last_name").eq("id", rel.user_id).single(), sb.from("stores").select("store_name").eq("id", sid).single()]);
799
+ if (!pu) return {
800
+ success: false,
801
+ error: "Platform user not found"
802
+ };
803
+ if (!pu.email) return {
804
+ success: false,
805
+ error: "Customer has no email address — add one first"
806
+ };
807
+ const storeName = store?.store_name || "our platform";
808
+ const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ") || "there";
809
+ const portalUrl = args.portal_url || args.redirect_to || null;
810
+
811
+ // If already has an auth account, just resend the email with a new magic link
812
+ let authId = pu.auth_id;
813
+ if (!authId) {
814
+ // Create auth account silently (no Supabase built-in email)
815
+ const {
816
+ data: newUser,
817
+ error: createErr
818
+ } = await sb.auth.admin.createUser({
819
+ email: pu.email,
820
+ email_confirm: true,
821
+ // skip verification — store is vouching for this email
822
+ user_metadata: {
823
+ first_name: pu.first_name,
824
+ last_name: pu.last_name,
825
+ invited_by_store: sid
779
826
  }
780
- const { data, error } = await q;
781
- if (error)
782
- return { success: false, error: error.message };
783
- // Flatten customer join so name/email appear as table columns
784
- const flattened = (data || []).map((row) => {
785
- const { customer, ...rest } = row;
786
- return {
787
- ...rest,
788
- customer_name: customer ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : "—",
789
- customer_email: customer?.email || null,
790
- };
791
- });
792
- return { success: true, count: flattened.length, data: flattened };
827
+ });
828
+ if (createErr) return {
829
+ success: false,
830
+ error: `Account creation failed: ${createErr.message}`
831
+ };
832
+ authId = newUser.user.id;
833
+
834
+ // Link auth account to platform user
835
+ await sb.from("platform_users").update({
836
+ auth_id: authId
837
+ }).eq("id", pu.id);
793
838
  }
794
- case "get": {
795
- const { data, error } = await sb.from("orders")
796
- .select("*, customer:v_store_customers!customer_id(id, first_name, last_name, email, phone, loyalty_points, total_spent, total_orders), items:order_items(id, product_id, quantity, line_total, cost_per_unit, product_sku, product:products(id, name, sku))")
797
- .eq("id", args.order_id).eq("store_id", sid).single();
798
- if (error)
799
- return { success: false, error: error.message };
800
- // Flatten customer and item product joins for readable display
801
- const order = data;
802
- const { customer, items: rawItems, ...orderFields } = order;
803
- const flatItems = (rawItems || []).map((item) => ({
804
- id: item.id, product_id: item.product_id,
805
- product_name: item.product?.name || "—", product_sku: item.product_sku || item.product?.sku || "—",
806
- quantity: item.quantity, line_total: item.line_total, cost_per_unit: item.cost_per_unit,
807
- }));
808
- return {
809
- success: true,
810
- data: {
811
- ...orderFields,
812
- customer_name: customer ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : "—",
813
- customer_email: customer?.email || null,
814
- customer_phone: customer?.phone || null,
815
- customer_id: customer?.id || orderFields.customer_id,
816
- items: flatItems,
817
- }
818
- };
839
+
840
+ // Generate a magic link for the customer to set up / log in
841
+ const {
842
+ data: linkData,
843
+ error: linkErr
844
+ } = await sb.auth.admin.generateLink({
845
+ type: "magiclink",
846
+ email: pu.email,
847
+ options: {
848
+ ...(portalUrl ? {
849
+ redirectTo: portalUrl
850
+ } : {})
851
+ }
852
+ });
853
+ if (linkErr) return {
854
+ success: false,
855
+ error: `Link generation failed: ${linkErr.message}`
856
+ };
857
+ const magicLink = linkData?.properties?.action_link || "";
858
+
859
+ // Send branded invite email through our email system
860
+ const emailResult = await handleEmail(sb, {
861
+ action: "send_template",
862
+ to: pu.email,
863
+ template: "customer-portal-invite",
864
+ template_data: {
865
+ customer_name: customerName,
866
+ store_name: storeName,
867
+ magic_link: magicLink,
868
+ portal_url: portalUrl || magicLink
869
+ }
870
+ }, sid);
871
+ return {
872
+ success: true,
873
+ data: {
874
+ email: pu.email,
875
+ name: customerName,
876
+ invited: true,
877
+ email_sent: emailResult.success,
878
+ ...(emailResult.success ? {} : {
879
+ email_error: emailResult.error
880
+ })
881
+ }
882
+ };
883
+ }
884
+
885
+ // ---- LOYALTY_HISTORY: transaction history for a customer ----
886
+ case "loyalty_history":
887
+ {
888
+ const custId = args.customer_id;
889
+ if (!custId) return {
890
+ success: false,
891
+ error: "customer_id is required"
892
+ };
893
+ let q = sb.from("loyalty_transactions").select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at").eq("customer_id", custId).order("created_at", {
894
+ ascending: false
895
+ });
896
+ if (args.transaction_type) q = q.eq("transaction_type", args.transaction_type);
897
+ q = q.limit(args.limit || 50);
898
+ const {
899
+ data,
900
+ error
901
+ } = await q;
902
+ return error ? {
903
+ success: false,
904
+ error: error.message
905
+ } : {
906
+ success: true,
907
+ count: data?.length,
908
+ data
909
+ };
910
+ }
911
+
912
+ // ---- ADJUST_LOYALTY: award or deduct loyalty points via RPC ----
913
+ case "adjust_loyalty":
914
+ {
915
+ const custId = args.customer_id;
916
+ const points = args.points;
917
+ const reason = args.reason;
918
+ if (!custId || points === undefined || !reason) return {
919
+ success: false,
920
+ error: "customer_id, points, and reason are required"
921
+ };
922
+ const {
923
+ data,
924
+ error
925
+ } = await sb.rpc("adjust_customer_loyalty_points", {
926
+ p_customer_id: custId,
927
+ p_points_change: points,
928
+ p_reason: reason
929
+ });
930
+ if (error) return {
931
+ success: false,
932
+ error: error.message
933
+ };
934
+ const {
935
+ data: updated
936
+ } = await sb.from("store_customer_profiles").select("loyalty_points, loyalty_tier, lifetime_points_earned").eq("relationship_id", custId).maybeSingle();
937
+ return {
938
+ success: true,
939
+ data: {
940
+ rpc_result: data,
941
+ ...updated
942
+ }
943
+ };
944
+ }
945
+
946
+ // ---- SEGMENTS: list all customer segments for the store ----
947
+ case "segments":
948
+ {
949
+ let q = sb.from("customer_segments").select("id, name, ai_description, type, segment_rules, filter_criteria, customer_count, is_active, color, icon, targeting_tips, created_at").eq("store_id", sid).order("customer_count", {
950
+ ascending: false
951
+ });
952
+ if (args.limit) q = q.limit(args.limit);
953
+ const {
954
+ data,
955
+ error
956
+ } = await q;
957
+ return error ? {
958
+ success: false,
959
+ error: error.message
960
+ } : {
961
+ success: true,
962
+ count: data?.length,
963
+ data
964
+ };
965
+ }
966
+
967
+ // ---- SEGMENT_MEMBERS: list customers in a specific segment ----
968
+ case "segment_members":
969
+ {
970
+ const segId = args.segment_id;
971
+ if (!segId) return {
972
+ success: false,
973
+ error: "segment_id is required"
974
+ };
975
+ const {
976
+ data,
977
+ error
978
+ } = await sb.from("customer_segment_memberships").select("added_at, customer:v_segment_customers!customer_id(*)").eq("segment_id", segId).order("added_at", {
979
+ ascending: false
980
+ }).limit(args.limit || 50);
981
+ if (error) return {
982
+ success: false,
983
+ error: error.message
984
+ };
985
+ const members = (data || []).map(r => ({
986
+ added_at: r.added_at,
987
+ ...r.customer
988
+ })).filter(m => m.store_id === sid);
989
+ return {
990
+ success: true,
991
+ count: members.length,
992
+ data: stripInternalArray(members)
993
+ };
994
+ }
995
+
996
+ // ---- REFRESH_METRICS: recalculate all customer metrics for the store ----
997
+ case "refresh_metrics":
998
+ {
999
+ const start = Date.now();
1000
+ const {
1001
+ data,
1002
+ error
1003
+ } = await sb.rpc("refresh_customer_metrics", {
1004
+ p_store_id: sid
1005
+ });
1006
+ const elapsed = Date.now() - start;
1007
+ if (error) return {
1008
+ success: false,
1009
+ error: error.message
1010
+ };
1011
+ return {
1012
+ success: true,
1013
+ data: {
1014
+ result: data,
1015
+ elapsed_ms: elapsed,
1016
+ message: `Customer metrics refreshed in ${elapsed}ms`
1017
+ }
1018
+ };
1019
+ }
1020
+ default:
1021
+ return {
1022
+ success: false,
1023
+ error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, invite, find_duplicates, merge, add_note, notes, activity, orders, loyalty_history, adjust_loyalty, segments, segment_members, refresh_metrics, link_channel_identity, get_channel_identities`
1024
+ };
1025
+ }
1026
+ }
1027
+ export async function handleOrders(sb, args, storeId) {
1028
+ const sid = storeId;
1029
+ switch (args.action) {
1030
+ case "find":
1031
+ {
1032
+ let q = sb.from("orders").select("id, order_number, status, total_amount, subtotal, tax_amount, created_at, customer_id, customer:v_store_customers!customer_id(id, first_name, last_name, email, phone)").eq("store_id", sid).order("created_at", {
1033
+ ascending: false
1034
+ });
1035
+ if (args.limit) q = q.limit(args.limit);
1036
+ if (args.status) q = q.eq("status", args.status);
1037
+ if (args.customer_id) q = q.eq("customer_id", args.customer_id);
1038
+ if (args.order_number) q = q.eq("order_number", args.order_number);
1039
+ if (args.query) {
1040
+ const sq = sanitizeFilterValue(String(args.query));
1041
+ q = q.or(`order_number.ilike.%${sq}%`);
819
1042
  }
820
- default:
821
- return { success: false, error: `Unknown orders action: ${args.action}` };
822
- }
1043
+ const {
1044
+ data,
1045
+ error
1046
+ } = await q;
1047
+ if (error) return {
1048
+ success: false,
1049
+ error: error.message
1050
+ };
1051
+ // Flatten customer join so name/email appear as table columns
1052
+ const flattened = (data || []).map(row => {
1053
+ const {
1054
+ customer,
1055
+ ...rest
1056
+ } = row;
1057
+ return {
1058
+ ...rest,
1059
+ customer_name: customer ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : "—",
1060
+ customer_email: customer?.email || null
1061
+ };
1062
+ });
1063
+ return {
1064
+ success: true,
1065
+ count: flattened.length,
1066
+ data: flattened
1067
+ };
1068
+ }
1069
+ case "get":
1070
+ {
1071
+ const {
1072
+ data,
1073
+ error
1074
+ } = await sb.from("orders").select("*, customer:v_store_customers!customer_id(id, first_name, last_name, email, phone, loyalty_points, total_spent, total_orders), items:order_items(id, product_id, quantity, line_total, cost_per_unit, product_sku, product:products(id, name, sku))").eq("id", args.order_id).eq("store_id", sid).single();
1075
+ if (error) return {
1076
+ success: false,
1077
+ error: error.message
1078
+ };
1079
+ // Flatten customer and item product joins for readable display
1080
+ const order = data;
1081
+ const {
1082
+ customer,
1083
+ items: rawItems,
1084
+ ...orderFields
1085
+ } = order;
1086
+ const flatItems = (rawItems || []).map(item => ({
1087
+ id: item.id,
1088
+ product_id: item.product_id,
1089
+ product_name: item.product?.name || "—",
1090
+ product_sku: item.product_sku || item.product?.sku || "—",
1091
+ quantity: item.quantity,
1092
+ line_total: item.line_total,
1093
+ cost_per_unit: item.cost_per_unit
1094
+ }));
1095
+ return {
1096
+ success: true,
1097
+ data: {
1098
+ ...orderFields,
1099
+ customer_name: customer ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim() : "—",
1100
+ customer_email: customer?.email || null,
1101
+ customer_phone: customer?.phone || null,
1102
+ customer_id: customer?.id || orderFields.customer_id,
1103
+ items: flatItems
1104
+ }
1105
+ };
1106
+ }
1107
+ default:
1108
+ return {
1109
+ success: false,
1110
+ error: `Unknown orders action: ${args.action}`
1111
+ };
1112
+ }
823
1113
  }
1114
+ //# sourceMappingURL=crm.js.map