whale-code 6.4.0

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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,976 @@
1
+ import { sanitizeFilterValue } from "../lib/utils.js";
2
+ // ============================================================================
3
+ // PRODUCTS — Full product catalog management
4
+ // Products, categories, field schemas, pricing schemas, catalogs, assignments
5
+ // ============================================================================
6
+ export async function handleProducts(sb, args, storeId) {
7
+ const sid = storeId;
8
+ switch (args.action) {
9
+ // ======================== PRODUCTS ========================
10
+ case "browse": {
11
+ // Category summary with product counts, price ranges, stock status
12
+ const { data: cats, error: catErr } = await sb.from("categories")
13
+ .select("id, name, slug, parent_id, display_order, product_count, catalog_id")
14
+ .eq("store_id", sid)
15
+ .eq("is_active", true)
16
+ .order("display_order", { ascending: true });
17
+ if (catErr)
18
+ return { success: false, error: catErr.message };
19
+ // Status distribution via count queries
20
+ const { count: activeCount } = await sb.from("products").select("id", { count: "exact", head: true }).eq("store_id", sid).neq("status", "archived");
21
+ const { count: archivedCount } = await sb.from("products").select("id", { count: "exact", head: true }).eq("store_id", sid).eq("status", "archived");
22
+ return {
23
+ success: true,
24
+ data: {
25
+ categories: cats,
26
+ product_status_summary: { active: activeCount || 0, archived: archivedCount || 0 },
27
+ tip: "Use products.find with category or status filters to drill into a specific set. Use products.get for full product detail."
28
+ }
29
+ };
30
+ }
31
+ case "find": {
32
+ const offset = args.offset || 0;
33
+ let q = sb.from("products")
34
+ .select("id, name, sku, status, type, stock_quantity, category:categories!primary_category_id(name)", { count: "exact" })
35
+ .eq("store_id", sid)
36
+ .order("created_at", { ascending: false });
37
+ if (args.limit) {
38
+ q = q.range(offset, offset + args.limit - 1);
39
+ }
40
+ else if (offset > 0) {
41
+ q = q.range(offset, offset + 999);
42
+ }
43
+ if (args.query) {
44
+ const sq = sanitizeFilterValue(String(args.query));
45
+ q = q.or(`name.ilike.%${sq}%,sku.ilike.%${sq}%,description.ilike.%${sq}%`);
46
+ }
47
+ if (args.category || args.category_id) {
48
+ // Accept both category (name or UUID) and category_id (UUID)
49
+ const catInput = (args.category_id || args.category);
50
+ const catVal = sanitizeFilterValue(catInput);
51
+ let matchedCatId = null;
52
+ if (/^[0-9a-f]{8}-/.test(catVal)) {
53
+ matchedCatId = catVal;
54
+ }
55
+ else {
56
+ const { data: cats } = await sb.from("categories").select("id").ilike("name", `%${catVal}%`).eq("store_id", sid).limit(1);
57
+ if (cats?.length)
58
+ matchedCatId = cats[0].id;
59
+ }
60
+ if (matchedCatId) {
61
+ // Include sub-categories (children of the matched category)
62
+ const { data: children } = await sb.from("categories").select("id").eq("parent_id", matchedCatId).eq("store_id", sid);
63
+ const catIds = [matchedCatId, ...(children || []).map(c => c.id)];
64
+ q = q.in("primary_category_id", catIds);
65
+ }
66
+ else {
67
+ // Category not found — return empty result
68
+ q = q.eq("primary_category_id", "00000000-0000-0000-0000-000000000000");
69
+ }
70
+ }
71
+ if (args.catalog_id)
72
+ q = q.eq("catalog_id", args.catalog_id);
73
+ if (args.status)
74
+ q = q.eq("status", args.status);
75
+ // Exclude archived unless explicitly requested or filtering by status
76
+ if (!args.status && !args.include_archived) {
77
+ q = q.neq("status", "archived");
78
+ }
79
+ if (args.featured !== undefined)
80
+ q = q.eq("featured", args.featured);
81
+ const { data, error, count: totalCount } = await q;
82
+ if (error)
83
+ return { success: false, error: error.message };
84
+ // Flatten nested category for display
85
+ const flattened = (data || []).map((row) => {
86
+ const { category, ...rest } = row;
87
+ return { ...rest, category_name: category?.name || null };
88
+ });
89
+ return { success: true, total: totalCount, count: flattened.length, offset, ...(args.limit ? { limit: args.limit } : {}), data: flattened };
90
+ }
91
+ case "get": {
92
+ const pid = args.product_id;
93
+ const { data: product, error: pErr } = await sb.from("products")
94
+ .select("*, category:categories!primary_category_id(id, name, slug)")
95
+ .eq("id", pid).eq("store_id", sid).single();
96
+ if (pErr)
97
+ return { success: false, error: pErr.message };
98
+ const { data: fieldSchemas } = await sb.from("product_field_schemas")
99
+ .select("field_schema_id, field_schema:field_schemas!field_schema_id(id, name, fields, icon)")
100
+ .eq("product_id", pid);
101
+ const { data: pricingSchemas } = await sb.from("product_pricing_schemas")
102
+ .select("pricing_schema_id, pricing_schema:pricing_schemas!pricing_schema_id(id, name, tiers, quality_tier)")
103
+ .eq("product_id", pid);
104
+ const { data: inventory } = await sb.from("inventory")
105
+ .select("id, quantity, location:locations!location_id(id, name)")
106
+ .eq("product_id", pid).eq("store_id", sid);
107
+ return {
108
+ success: true,
109
+ data: {
110
+ ...product,
111
+ field_schemas: fieldSchemas?.map(fs => fs.field_schema) || [],
112
+ pricing_schemas: pricingSchemas?.map(ps => ps.pricing_schema) || [],
113
+ inventory: inventory || []
114
+ }
115
+ };
116
+ }
117
+ case "create": {
118
+ const name = args.name;
119
+ if (!name)
120
+ return { success: false, error: "name is required" };
121
+ const insert = { store_id: sid, name };
122
+ if (args.sku)
123
+ insert.sku = args.sku;
124
+ if (args.description)
125
+ insert.description = args.description;
126
+ if (args.short_description)
127
+ insert.short_description = args.short_description;
128
+ if (args.type)
129
+ insert.type = args.type;
130
+ if (args.status)
131
+ insert.status = args.status;
132
+ if (args.cost_price !== undefined)
133
+ insert.cost_price = args.cost_price;
134
+ if (args.wholesale_price !== undefined)
135
+ insert.wholesale_price = args.wholesale_price;
136
+ if (args.featured !== undefined)
137
+ insert.featured = args.featured;
138
+ if (args.stock_quantity !== undefined)
139
+ insert.stock_quantity = args.stock_quantity;
140
+ if (args.manage_stock !== undefined)
141
+ insert.manage_stock = args.manage_stock;
142
+ if (args.weight !== undefined)
143
+ insert.weight = args.weight;
144
+ if (args.tax_status)
145
+ insert.tax_status = args.tax_status;
146
+ if (args.tax_class)
147
+ insert.tax_class = args.tax_class;
148
+ if (args.catalog_id)
149
+ insert.catalog_id = args.catalog_id;
150
+ if (args.pricing_data)
151
+ insert.pricing_data = args.pricing_data;
152
+ // custom_fields NOT inserted directly — schema is the source of truth
153
+ // Agent-provided field_values are filtered against schema keys post-insert
154
+ if (args.is_wholesale !== undefined)
155
+ insert.is_wholesale = args.is_wholesale;
156
+ if (args.wholesale_only !== undefined)
157
+ insert.wholesale_only = args.wholesale_only;
158
+ if (args.minimum_wholesale_quantity !== undefined)
159
+ insert.minimum_wholesale_quantity = args.minimum_wholesale_quantity;
160
+ const catArg = (args.category || args.primary_category_id || args.category_id);
161
+ if (catArg) {
162
+ if (/^[0-9a-f]{8}-/.test(catArg)) {
163
+ insert.primary_category_id = catArg;
164
+ }
165
+ else {
166
+ const { data: cats } = await sb.from("categories").select("id").ilike("name", `%${catArg}%`).eq("store_id", sid).limit(1);
167
+ if (cats?.length)
168
+ insert.primary_category_id = cats[0].id;
169
+ }
170
+ }
171
+ if (args.pricing_schema_id)
172
+ insert.pricing_schema_id = args.pricing_schema_id;
173
+ const { data, error } = await sb.from("products").insert(insert).select("id, name, sku, slug, status, primary_category_id, pricing_schema_id, created_at").single();
174
+ if (error)
175
+ return { success: false, error: error.message };
176
+ // Explicit schema assignments from args
177
+ if (args.field_schema_ids && Array.isArray(args.field_schema_ids)) {
178
+ const rows = args.field_schema_ids.map(fsId => ({ product_id: data.id, field_schema_id: fsId }));
179
+ await sb.from("product_field_schemas").insert(rows);
180
+ }
181
+ if (args.pricing_schema_ids && Array.isArray(args.pricing_schema_ids)) {
182
+ const rows = args.pricing_schema_ids.map(psId => ({ product_id: data.id, pricing_schema_id: psId }));
183
+ await sb.from("product_pricing_schemas").insert(rows);
184
+ }
185
+ const productUpdates = {};
186
+ const inherited = [];
187
+ // Auto-inherit field schema from category — ALWAYS merge with schema template
188
+ const categoryId = insert.primary_category_id;
189
+ if (categoryId && !args.field_schema_ids) {
190
+ const { data: cat } = await sb.from("categories").select("field_schema_id").eq("id", categoryId).single();
191
+ if (cat?.field_schema_id) {
192
+ await sb.from("product_field_schemas").upsert({ product_id: data.id, field_schema_id: cat.field_schema_id }, { onConflict: "product_id,field_schema_id" });
193
+ // Schema is source of truth — only schema keys allowed
194
+ const { data: fs } = await sb.from("field_schemas").select("fields").eq("id", cat.field_schema_id).single();
195
+ if (fs?.fields && Array.isArray(fs.fields)) {
196
+ const schemaKeys = new Set();
197
+ const fieldValues = {};
198
+ for (const f of fs.fields) {
199
+ const key = f.key;
200
+ if (key) {
201
+ schemaKeys.add(key);
202
+ fieldValues[key] = f.default ?? null;
203
+ }
204
+ }
205
+ // Only accept agent values for keys that exist in the schema
206
+ const agentValues = args.field_values || {};
207
+ for (const [k, v] of Object.entries(agentValues)) {
208
+ if (schemaKeys.has(k))
209
+ fieldValues[k] = v;
210
+ }
211
+ productUpdates.custom_fields = fieldValues;
212
+ }
213
+ inherited.push(`field_schema:${cat.field_schema_id}`);
214
+ }
215
+ // Also check junction table for additional schemas
216
+ const { data: catFieldSchemas } = await sb.from("category_field_schemas").select("field_schema_id").eq("category_id", categoryId);
217
+ if (catFieldSchemas?.length) {
218
+ const cat2 = await sb.from("categories").select("field_schema_id").eq("id", categoryId).single();
219
+ const rows = catFieldSchemas.filter(r => r.field_schema_id !== cat2?.data?.field_schema_id).map(r => ({ product_id: data.id, field_schema_id: r.field_schema_id }));
220
+ if (rows.length)
221
+ await sb.from("product_field_schemas").insert(rows);
222
+ }
223
+ }
224
+ // If pricing_schema_id provided, hydrate pricing_data from schema
225
+ if (insert.pricing_schema_id && !args.pricing_data) {
226
+ const { data: ps } = await sb.from("pricing_schemas").select("tiers").eq("id", insert.pricing_schema_id).single();
227
+ if (ps?.tiers)
228
+ productUpdates.pricing_data = ps.tiers;
229
+ }
230
+ // Apply any post-insert updates
231
+ if (Object.keys(productUpdates).length > 0) {
232
+ await sb.from("products").update(productUpdates).eq("id", data.id);
233
+ }
234
+ // Re-read the full product for response
235
+ const { data: full } = await sb.from("products")
236
+ .select("id, name, sku, slug, status, primary_category_id, pricing_schema_id, custom_fields, pricing_data, created_at")
237
+ .eq("id", data.id).single();
238
+ if (inherited.length && full)
239
+ full.inherited = inherited;
240
+ return { success: true, data: full || data };
241
+ }
242
+ case "update": {
243
+ const pid = args.product_id;
244
+ if (!pid)
245
+ return { success: false, error: "product_id is required" };
246
+ const updates = {};
247
+ if (args.name !== undefined)
248
+ updates.name = args.name;
249
+ if (args.sku !== undefined)
250
+ updates.sku = args.sku;
251
+ if (args.description !== undefined)
252
+ updates.description = args.description;
253
+ if (args.short_description !== undefined)
254
+ updates.short_description = args.short_description;
255
+ if (args.type !== undefined)
256
+ updates.type = args.type;
257
+ if (args.status !== undefined)
258
+ updates.status = args.status;
259
+ if (args.cost_price !== undefined)
260
+ updates.cost_price = args.cost_price;
261
+ if (args.wholesale_price !== undefined)
262
+ updates.wholesale_price = args.wholesale_price;
263
+ if (args.featured !== undefined)
264
+ updates.featured = args.featured;
265
+ if (args.stock_quantity !== undefined)
266
+ updates.stock_quantity = args.stock_quantity;
267
+ if (args.manage_stock !== undefined)
268
+ updates.manage_stock = args.manage_stock;
269
+ if (args.weight !== undefined)
270
+ updates.weight = args.weight;
271
+ if (args.tax_status !== undefined)
272
+ updates.tax_status = args.tax_status;
273
+ if (args.tax_class !== undefined)
274
+ updates.tax_class = args.tax_class;
275
+ if (args.catalog_id !== undefined)
276
+ updates.catalog_id = args.catalog_id;
277
+ if (args.pricing_schema_id !== undefined)
278
+ updates.pricing_schema_id = args.pricing_schema_id;
279
+ if (args.pricing_data !== undefined)
280
+ updates.pricing_data = args.pricing_data;
281
+ // custom_fields filtered to schema keys only (schema = source of truth)
282
+ if (args.field_values !== undefined) {
283
+ const agentFV = args.field_values;
284
+ // Look up product's linked field schema to get allowed keys
285
+ const { data: pfs } = await sb.from("product_field_schemas").select("field_schema_id").eq("product_id", pid).limit(1);
286
+ if (pfs?.length) {
287
+ const { data: fsDef } = await sb.from("field_schemas").select("fields").eq("id", pfs[0].field_schema_id).single();
288
+ if (fsDef?.fields && Array.isArray(fsDef.fields)) {
289
+ const { data: existing } = await sb.from("products").select("custom_fields").eq("id", pid).single();
290
+ const base = existing?.custom_fields || {};
291
+ const filtered = { ...base };
292
+ const schemaKeys = new Set(fsDef.fields.map((f) => f.key).filter(Boolean));
293
+ for (const [k, v] of Object.entries(agentFV)) {
294
+ if (schemaKeys.has(k))
295
+ filtered[k] = v;
296
+ }
297
+ updates.custom_fields = filtered;
298
+ }
299
+ else {
300
+ updates.custom_fields = agentFV; // no schema definition found, pass through
301
+ }
302
+ }
303
+ else {
304
+ updates.custom_fields = agentFV; // no schema linked, pass through
305
+ }
306
+ }
307
+ if (args.is_wholesale !== undefined)
308
+ updates.is_wholesale = args.is_wholesale;
309
+ if (args.wholesale_only !== undefined)
310
+ updates.wholesale_only = args.wholesale_only;
311
+ if (args.minimum_wholesale_quantity !== undefined)
312
+ updates.minimum_wholesale_quantity = args.minimum_wholesale_quantity;
313
+ if (args.featured_image !== undefined)
314
+ updates.featured_image = args.featured_image;
315
+ const updateCatArg = (args.category ?? args.primary_category_id ?? args.category_id);
316
+ if (updateCatArg !== undefined) {
317
+ if (!updateCatArg) {
318
+ updates.primary_category_id = null;
319
+ }
320
+ else if (/^[0-9a-f]{8}-/.test(updateCatArg)) {
321
+ updates.primary_category_id = updateCatArg;
322
+ }
323
+ else {
324
+ const { data: cats } = await sb.from("categories").select("id").ilike("name", `%${updateCatArg}%`).eq("store_id", sid).limit(1);
325
+ if (cats?.length)
326
+ updates.primary_category_id = cats[0].id;
327
+ }
328
+ }
329
+ const { data, error } = await sb.from("products")
330
+ .update(updates).eq("id", pid).eq("store_id", sid)
331
+ .select("id, name, sku, slug, status, cost_price, pricing_schema_id, updated_at").single();
332
+ return error ? { success: false, error: error.message } : { success: true, data };
333
+ }
334
+ case "delete": {
335
+ const pid = args.product_id;
336
+ if (!pid)
337
+ return { success: false, error: "product_id is required" };
338
+ if (args.hard === true) {
339
+ const { error } = await sb.from("products").delete().eq("id", pid).eq("store_id", sid);
340
+ return error ? { success: false, error: error.message } : { success: true, data: { id: pid, deleted: true } };
341
+ }
342
+ const { data, error } = await sb.from("products")
343
+ .update({ status: "archived" }).eq("id", pid).eq("store_id", sid)
344
+ .select("id, name, status").single();
345
+ return error ? { success: false, error: error.message } : { success: true, data };
346
+ }
347
+ // ======================== CATEGORIES ========================
348
+ case "list_categories": {
349
+ let q = sb.from("categories")
350
+ .select("id, name, slug, description, icon, parent_id, display_order, is_active, featured, product_count, catalog_id, field_schema_id, created_at")
351
+ .eq("store_id", sid)
352
+ .order("display_order", { ascending: true });
353
+ if (args.catalog_id)
354
+ q = q.eq("catalog_id", args.catalog_id);
355
+ if (args.parent_id)
356
+ q = q.eq("parent_id", args.parent_id);
357
+ if (args.active_only !== false)
358
+ q = q.eq("is_active", true);
359
+ const { data, error } = await q.limit(args.limit || 100);
360
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
361
+ }
362
+ case "get_category": {
363
+ const catId = args.category_id;
364
+ const { data: cat, error: catErr } = await sb.from("categories")
365
+ .select("*").eq("id", catId).eq("store_id", sid).single();
366
+ if (catErr)
367
+ return { success: false, error: catErr.message };
368
+ const { data: fieldAssigns } = await sb.from("category_field_schemas")
369
+ .select("sort_order, is_active, field_schema:field_schemas!field_schema_id(id, name, fields, icon)")
370
+ .eq("category_id", catId).eq("is_active", true).order("sort_order");
371
+ const { data: pricingAssigns } = await sb.from("category_pricing_schemas")
372
+ .select("sort_order, is_active, pricing_schema:pricing_schemas!pricing_schema_id(id, name, tiers, quality_tier)")
373
+ .eq("category_id", catId).eq("is_active", true).order("sort_order");
374
+ const { data: children } = await sb.from("categories")
375
+ .select("id, name, slug, display_order, is_active, product_count")
376
+ .eq("parent_id", catId).order("display_order");
377
+ return {
378
+ success: true,
379
+ data: {
380
+ ...cat,
381
+ field_schemas: fieldAssigns?.map(a => ({ ...a.field_schema, sort_order: a.sort_order })) || [],
382
+ pricing_schemas: pricingAssigns?.map(a => ({ ...a.pricing_schema, sort_order: a.sort_order })) || [],
383
+ subcategories: children || []
384
+ }
385
+ };
386
+ }
387
+ case "create_category": {
388
+ const name = args.name;
389
+ if (!name)
390
+ return { success: false, error: "name is required" };
391
+ const insert = { store_id: sid, name, slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") };
392
+ if (args.description)
393
+ insert.description = args.description;
394
+ if (args.icon)
395
+ insert.icon = args.icon;
396
+ if (args.parent_id)
397
+ insert.parent_id = args.parent_id;
398
+ if (args.catalog_id)
399
+ insert.catalog_id = args.catalog_id;
400
+ if (args.display_order !== undefined)
401
+ insert.display_order = args.display_order;
402
+ if (args.field_schema_id)
403
+ insert.field_schema_id = args.field_schema_id;
404
+ const { data, error } = await sb.from("categories").insert(insert)
405
+ .select("id, name, slug, parent_id, catalog_id, display_order, created_at").single();
406
+ if (error)
407
+ return { success: false, error: error.message };
408
+ if (args.field_schema_ids && Array.isArray(args.field_schema_ids)) {
409
+ const rows = args.field_schema_ids.map((fsId, i) => ({ category_id: data.id, field_schema_id: fsId, sort_order: i + 1 }));
410
+ await sb.from("category_field_schemas").insert(rows);
411
+ }
412
+ if (args.pricing_schema_ids && Array.isArray(args.pricing_schema_ids)) {
413
+ const rows = args.pricing_schema_ids.map((psId, i) => ({ category_id: data.id, pricing_schema_id: psId, sort_order: i + 1 }));
414
+ await sb.from("category_pricing_schemas").insert(rows);
415
+ }
416
+ return { success: true, data };
417
+ }
418
+ case "update_category": {
419
+ const catId = args.category_id;
420
+ if (!catId)
421
+ return { success: false, error: "category_id is required" };
422
+ const updates = {};
423
+ if (args.name !== undefined)
424
+ updates.name = args.name;
425
+ if (args.description !== undefined)
426
+ updates.description = args.description;
427
+ if (args.icon !== undefined)
428
+ updates.icon = args.icon;
429
+ if (args.parent_id !== undefined)
430
+ updates.parent_id = args.parent_id;
431
+ if (args.catalog_id !== undefined)
432
+ updates.catalog_id = args.catalog_id;
433
+ if (args.display_order !== undefined)
434
+ updates.display_order = args.display_order;
435
+ if (args.is_active !== undefined)
436
+ updates.is_active = args.is_active;
437
+ if (args.featured !== undefined)
438
+ updates.featured = args.featured;
439
+ if (args.field_schema_id !== undefined)
440
+ updates.field_schema_id = args.field_schema_id;
441
+ const { data, error } = await sb.from("categories")
442
+ .update(updates).eq("id", catId).eq("store_id", sid)
443
+ .select("id, name, slug, is_active, display_order, updated_at").single();
444
+ return error ? { success: false, error: error.message } : { success: true, data };
445
+ }
446
+ case "delete_category": {
447
+ const catId = args.category_id;
448
+ if (!catId)
449
+ return { success: false, error: "category_id is required" };
450
+ if (args.hard === true) {
451
+ const { error } = await sb.from("categories").delete().eq("id", catId).eq("store_id", sid);
452
+ return error ? { success: false, error: error.message } : { success: true, data: { id: catId, deleted: true } };
453
+ }
454
+ const { data, error } = await sb.from("categories")
455
+ .update({ is_active: false }).eq("id", catId).eq("store_id", sid)
456
+ .select("id, name, is_active").single();
457
+ return error ? { success: false, error: error.message } : { success: true, data };
458
+ }
459
+ // ======================== FIELD SCHEMAS ========================
460
+ case "list_field_schemas": {
461
+ let q = sb.from("field_schemas")
462
+ .select("id, name, slug, description, icon, fields, is_public, is_active, catalog_id, store_id, install_count, created_at")
463
+ .eq("is_active", true);
464
+ // Show store-owned schemas AND public schemas (prevent IDOR)
465
+ if (args.public_only === true) {
466
+ q = q.eq("is_public", true);
467
+ }
468
+ else {
469
+ q = q.or(`store_id.eq.${sid},is_public.eq.true`);
470
+ }
471
+ if (args.catalog_id)
472
+ q = q.eq("catalog_id", args.catalog_id);
473
+ if (args.limit)
474
+ q = q.limit(args.limit);
475
+ const { data, error } = await q.order("name");
476
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
477
+ }
478
+ case "get_field_schema": {
479
+ const fsId = (args.field_schema_id || args.schema_id);
480
+ if (!fsId)
481
+ return { success: false, error: "field_schema_id is required" };
482
+ // Allow access to own schemas OR public schemas (prevent IDOR)
483
+ const { data, error } = await sb.from("field_schemas").select("*").eq("id", fsId)
484
+ .or(`store_id.eq.${sid},is_public.eq.true`).single();
485
+ if (error)
486
+ return { success: false, error: error.message };
487
+ const { data: assignments } = await sb.from("category_field_schemas")
488
+ .select("category:categories!category_id(id, name)").eq("field_schema_id", fsId).eq("is_active", true);
489
+ return { success: true, data: { ...data, assigned_categories: assignments?.map(a => a.category) || [] } };
490
+ }
491
+ case "create_field_schema": {
492
+ const name = args.name;
493
+ if (!name)
494
+ return { success: false, error: "name is required" };
495
+ if (!args.fields || !Array.isArray(args.fields))
496
+ return { success: false, error: "fields array is required" };
497
+ const insert = {
498
+ store_id: sid,
499
+ name,
500
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
501
+ fields: args.fields
502
+ };
503
+ if (args.description)
504
+ insert.description = args.description;
505
+ if (args.icon)
506
+ insert.icon = args.icon;
507
+ if (args.catalog_id)
508
+ insert.catalog_id = args.catalog_id;
509
+ if (args.is_public !== undefined)
510
+ insert.is_public = args.is_public;
511
+ const { data, error } = await sb.from("field_schemas").insert(insert)
512
+ .select("id, name, slug, fields, icon, is_active, created_at").single();
513
+ return error ? { success: false, error: error.message } : { success: true, data };
514
+ }
515
+ case "update_field_schema": {
516
+ const fsId = (args.field_schema_id || args.schema_id);
517
+ if (!fsId)
518
+ return { success: false, error: "field_schema_id is required" };
519
+ const updates = {};
520
+ if (args.name !== undefined)
521
+ updates.name = args.name;
522
+ if (args.description !== undefined)
523
+ updates.description = args.description;
524
+ if (args.icon !== undefined)
525
+ updates.icon = args.icon;
526
+ if (args.fields !== undefined)
527
+ updates.fields = args.fields;
528
+ if (args.is_public !== undefined)
529
+ updates.is_public = args.is_public;
530
+ if (args.is_active !== undefined)
531
+ updates.is_active = args.is_active;
532
+ if (args.catalog_id !== undefined)
533
+ updates.catalog_id = args.catalog_id;
534
+ // Only allow modification of own schemas (prevent IDOR)
535
+ const { data, error } = await sb.from("field_schemas")
536
+ .update(updates).eq("id", fsId).eq("store_id", sid)
537
+ .select("id, name, slug, fields, icon, is_active, updated_at").single();
538
+ return error ? { success: false, error: error.message } : { success: true, data };
539
+ }
540
+ case "delete_field_schema": {
541
+ const fsId = (args.field_schema_id || args.schema_id);
542
+ if (!fsId)
543
+ return { success: false, error: "field_schema_id is required" };
544
+ // Only allow deletion of own schemas (prevent IDOR)
545
+ const { data, error } = await sb.from("field_schemas")
546
+ .update({ is_active: false, deleted_at: new Date().toISOString() }).eq("id", fsId).eq("store_id", sid)
547
+ .select("id, name, is_active").single();
548
+ return error ? { success: false, error: error.message } : { success: true, data };
549
+ }
550
+ // ======================== PRICING SCHEMAS ========================
551
+ case "list_pricing_schemas": {
552
+ let q = sb.from("pricing_schemas")
553
+ .select("id, name, slug, description, tiers, quality_tier, is_public, is_active, catalog_id, store_id, install_count, created_at")
554
+ .eq("is_active", true);
555
+ // Show store-owned schemas AND public schemas (prevent IDOR)
556
+ if (args.public_only === true) {
557
+ q = q.eq("is_public", true);
558
+ }
559
+ else {
560
+ q = q.or(`store_id.eq.${sid},is_public.eq.true`);
561
+ }
562
+ if (args.catalog_id)
563
+ q = q.eq("catalog_id", args.catalog_id);
564
+ if (args.limit)
565
+ q = q.limit(args.limit);
566
+ const { data, error } = await q.order("name");
567
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
568
+ }
569
+ case "get_pricing_schema": {
570
+ const psId = (args.pricing_schema_id || args.schema_id);
571
+ if (!psId)
572
+ return { success: false, error: "pricing_schema_id is required" };
573
+ // Allow access to own schemas OR public schemas (prevent IDOR)
574
+ const { data, error } = await sb.from("pricing_schemas").select("*").eq("id", psId)
575
+ .or(`store_id.eq.${sid},is_public.eq.true`).single();
576
+ if (error)
577
+ return { success: false, error: error.message };
578
+ const { data: assignments } = await sb.from("category_pricing_schemas")
579
+ .select("category:categories!category_id(id, name)").eq("pricing_schema_id", psId).eq("is_active", true);
580
+ return { success: true, data: { ...data, assigned_categories: assignments?.map(a => a.category) || [] } };
581
+ }
582
+ case "create_pricing_schema": {
583
+ const name = args.name;
584
+ if (!name)
585
+ return { success: false, error: "name is required" };
586
+ if (!args.tiers || !Array.isArray(args.tiers))
587
+ return { success: false, error: "tiers array is required" };
588
+ const insert = {
589
+ store_id: sid,
590
+ name,
591
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
592
+ tiers: args.tiers
593
+ };
594
+ if (args.description)
595
+ insert.description = args.description;
596
+ if (args.quality_tier)
597
+ insert.quality_tier = args.quality_tier;
598
+ if (args.catalog_id)
599
+ insert.catalog_id = args.catalog_id;
600
+ if (args.is_public !== undefined)
601
+ insert.is_public = args.is_public;
602
+ const { data, error } = await sb.from("pricing_schemas").insert(insert)
603
+ .select("id, name, slug, tiers, quality_tier, is_active, created_at").single();
604
+ return error ? { success: false, error: error.message } : { success: true, data };
605
+ }
606
+ case "update_pricing_schema": {
607
+ const psId = (args.pricing_schema_id || args.schema_id);
608
+ if (!psId)
609
+ return { success: false, error: "pricing_schema_id is required" };
610
+ const updates = {};
611
+ if (args.name !== undefined)
612
+ updates.name = args.name;
613
+ if (args.description !== undefined)
614
+ updates.description = args.description;
615
+ if (args.tiers !== undefined)
616
+ updates.tiers = args.tiers;
617
+ if (args.quality_tier !== undefined)
618
+ updates.quality_tier = args.quality_tier;
619
+ if (args.is_public !== undefined)
620
+ updates.is_public = args.is_public;
621
+ if (args.is_active !== undefined)
622
+ updates.is_active = args.is_active;
623
+ if (args.catalog_id !== undefined)
624
+ updates.catalog_id = args.catalog_id;
625
+ // Only allow modification of own schemas (prevent IDOR)
626
+ const { data, error } = await sb.from("pricing_schemas")
627
+ .update(updates).eq("id", psId).eq("store_id", sid)
628
+ .select("id, name, slug, tiers, quality_tier, is_active, updated_at").single();
629
+ return error ? { success: false, error: error.message } : { success: true, data };
630
+ }
631
+ case "delete_pricing_schema": {
632
+ const psId = (args.pricing_schema_id || args.schema_id);
633
+ if (!psId)
634
+ return { success: false, error: "pricing_schema_id is required" };
635
+ // Only allow deletion of own schemas (prevent IDOR)
636
+ const { data, error } = await sb.from("pricing_schemas")
637
+ .update({ is_active: false, deleted_at: new Date().toISOString() }).eq("id", psId).eq("store_id", sid)
638
+ .select("id, name, is_active").single();
639
+ return error ? { success: false, error: error.message } : { success: true, data };
640
+ }
641
+ // ======================== CATALOGS ========================
642
+ case "list_catalogs": {
643
+ const { data, error } = await sb.from("catalogs")
644
+ .select("id, name, slug, description, vertical, is_active, is_default, display_order, created_at")
645
+ .eq("store_id", sid).order("display_order");
646
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
647
+ }
648
+ case "create_catalog": {
649
+ const name = args.name;
650
+ if (!name)
651
+ return { success: false, error: "name is required" };
652
+ // Resolve owner_user_id from store
653
+ const { data: store } = await sb.from("stores").select("owner_user_id").eq("id", sid).single();
654
+ const insert = {
655
+ store_id: sid, name,
656
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
657
+ owner_user_id: store?.owner_user_id
658
+ };
659
+ if (args.description)
660
+ insert.description = args.description;
661
+ if (args.vertical)
662
+ insert.vertical = args.vertical;
663
+ if (args.is_default !== undefined)
664
+ insert.is_default = args.is_default;
665
+ if (args.settings)
666
+ insert.settings = args.settings;
667
+ const { data, error } = await sb.from("catalogs").insert(insert)
668
+ .select("id, name, slug, vertical, is_default, created_at").single();
669
+ return error ? { success: false, error: error.message } : { success: true, data };
670
+ }
671
+ case "update_catalog": {
672
+ const catId = args.catalog_id;
673
+ if (!catId)
674
+ return { success: false, error: "catalog_id is required" };
675
+ const updates = {};
676
+ if (args.name !== undefined)
677
+ updates.name = args.name;
678
+ if (args.description !== undefined)
679
+ updates.description = args.description;
680
+ if (args.vertical !== undefined)
681
+ updates.vertical = args.vertical;
682
+ if (args.is_active !== undefined)
683
+ updates.is_active = args.is_active;
684
+ if (args.is_default !== undefined)
685
+ updates.is_default = args.is_default;
686
+ if (args.settings !== undefined)
687
+ updates.settings = args.settings;
688
+ if (args.display_order !== undefined)
689
+ updates.display_order = args.display_order;
690
+ const { data, error } = await sb.from("catalogs")
691
+ .update(updates).eq("id", catId).eq("store_id", sid)
692
+ .select("id, name, slug, vertical, is_active, is_default, updated_at").single();
693
+ return error ? { success: false, error: error.message } : { success: true, data };
694
+ }
695
+ // ======================== SCHEMA ASSIGNMENTS ========================
696
+ case "assign_schema": {
697
+ const target = args.target; // "category" or "product"
698
+ const schemaType = args.schema_type; // "field" or "pricing"
699
+ const targetId = args.target_id;
700
+ const schemaId = args.schema_id;
701
+ if (!target || !schemaType || !targetId || !schemaId) {
702
+ return { success: false, error: "target (category|product), schema_type (field|pricing), target_id, and schema_id are required" };
703
+ }
704
+ // Verify target product/category belongs to this store (prevent IDOR)
705
+ const targetTable = target === "product" ? "products" : "categories";
706
+ const { data: ownerCheck, error: ownerErr } = await sb.from(targetTable)
707
+ .select("id").eq("id", targetId).eq("store_id", sid).single();
708
+ if (ownerErr || !ownerCheck) {
709
+ return { success: false, error: `${target} not found or does not belong to this store` };
710
+ }
711
+ // Verify schema is accessible (own schema or public)
712
+ const schemaTable = schemaType === "field" ? "field_schemas" : "pricing_schemas";
713
+ const { data: schemaCheck, error: schemaErr } = await sb.from(schemaTable)
714
+ .select("id").eq("id", schemaId).or(`store_id.eq.${sid},is_public.eq.true`).single();
715
+ if (schemaErr || !schemaCheck) {
716
+ return { success: false, error: `${schemaType} schema not found or not accessible` };
717
+ }
718
+ const table = target === "product"
719
+ ? (schemaType === "field" ? "product_field_schemas" : "product_pricing_schemas")
720
+ : (schemaType === "field" ? "category_field_schemas" : "category_pricing_schemas");
721
+ const fkCol = target === "product" ? "product_id" : "category_id";
722
+ const schemaCol = schemaType === "field" ? "field_schema_id" : "pricing_schema_id";
723
+ const row = { [fkCol]: targetId, [schemaCol]: schemaId };
724
+ if (args.sort_order !== undefined)
725
+ row.sort_order = args.sort_order;
726
+ const { data, error } = await sb.from(table).upsert(row, { onConflict: `${fkCol},${schemaCol}` }).select().single();
727
+ if (error)
728
+ return { success: false, error: error.message };
729
+ // Hydrate product with schema data
730
+ if (target === "product") {
731
+ const productUpdates = {};
732
+ if (schemaType === "pricing") {
733
+ const { data: schema } = await sb.from("pricing_schemas").select("tiers, quality_tier").eq("id", schemaId).single();
734
+ productUpdates.pricing_schema_id = schemaId;
735
+ if (schema?.tiers)
736
+ productUpdates.pricing_data = schema.tiers;
737
+ }
738
+ if (schemaType === "field") {
739
+ // Schema is source of truth — rebuild custom_fields from schema keys only
740
+ const { data: schema } = await sb.from("field_schemas").select("fields").eq("id", schemaId).single();
741
+ if (schema?.fields && Array.isArray(schema.fields)) {
742
+ const { data: product } = await sb.from("products").select("custom_fields").eq("id", targetId).eq("store_id", sid).single();
743
+ const existing = product?.custom_fields || {};
744
+ const rebuilt = {};
745
+ for (const f of schema.fields) {
746
+ const key = f.key;
747
+ if (key)
748
+ rebuilt[key] = (key in existing) ? existing[key] : (f.default ?? null);
749
+ }
750
+ productUpdates.custom_fields = rebuilt;
751
+ }
752
+ }
753
+ if (Object.keys(productUpdates).length > 0) {
754
+ await sb.from("products").update(productUpdates).eq("id", targetId).eq("store_id", sid);
755
+ }
756
+ }
757
+ return { success: true, data };
758
+ }
759
+ case "unassign_schema": {
760
+ const target = args.target;
761
+ const schemaType = args.schema_type;
762
+ const targetId = args.target_id;
763
+ const schemaId = args.schema_id;
764
+ if (!target || !schemaType || !targetId || !schemaId) {
765
+ return { success: false, error: "target (category|product), schema_type (field|pricing), target_id, and schema_id are required" };
766
+ }
767
+ // Verify target product/category belongs to this store (prevent IDOR)
768
+ const targetTable = target === "product" ? "products" : "categories";
769
+ const { data: ownerCheck, error: ownerErr } = await sb.from(targetTable)
770
+ .select("id").eq("id", targetId).eq("store_id", sid).single();
771
+ if (ownerErr || !ownerCheck) {
772
+ return { success: false, error: `${target} not found or does not belong to this store` };
773
+ }
774
+ const table = target === "product"
775
+ ? (schemaType === "field" ? "product_field_schemas" : "product_pricing_schemas")
776
+ : (schemaType === "field" ? "category_field_schemas" : "category_pricing_schemas");
777
+ const fkCol = target === "product" ? "product_id" : "category_id";
778
+ const schemaCol = schemaType === "field" ? "field_schema_id" : "pricing_schema_id";
779
+ const { error } = await sb.from(table).delete().eq(fkCol, targetId).eq(schemaCol, schemaId);
780
+ return error ? { success: false, error: error.message } : { success: true, data: { removed: true, target, schema_type: schemaType, target_id: targetId, schema_id: schemaId } };
781
+ }
782
+ // ======================== PRODUCT DEDUP / MERGE ========================
783
+ case "find_duplicates": {
784
+ // Find products with similar names (potential duplicates)
785
+ const { data: all, error: allErr } = await sb.from("products")
786
+ .select("id, name, sku, status, stock_quantity, cost_price, created_at")
787
+ .eq("store_id", sid).neq("status", "archived")
788
+ .order("name");
789
+ if (allErr)
790
+ return { success: false, error: allErr.message };
791
+ // Group by normalized name (lowercase, trim, strip common suffixes like "(TEMP)", "(OLD)", "(COPY)")
792
+ const nameMap = new Map();
793
+ for (const p of all || []) {
794
+ const normalized = (p.name || "").toLowerCase().trim()
795
+ .replace(/\s*\((temp|old|copy|duplicate|dup|new|v2)\)\s*/gi, "")
796
+ .replace(/\s+/g, " ");
797
+ if (!normalized)
798
+ continue;
799
+ if (!nameMap.has(normalized))
800
+ nameMap.set(normalized, []);
801
+ nameMap.get(normalized).push(p);
802
+ }
803
+ const duplicates = Array.from(nameMap.entries())
804
+ .filter(([_, products]) => products.length > 1)
805
+ .map(([normalizedName, products]) => ({
806
+ normalized_name: normalizedName,
807
+ count: products.length,
808
+ products: products.map(p => ({
809
+ id: p.id, name: p.name, sku: p.sku, status: p.status,
810
+ stock_quantity: p.stock_quantity, cost_price: p.cost_price, created_at: p.created_at,
811
+ })),
812
+ }));
813
+ // Also check by exact SKU match
814
+ const skuMap = new Map();
815
+ for (const p of all || []) {
816
+ if (!p.sku)
817
+ continue;
818
+ const key = p.sku.toLowerCase().trim();
819
+ if (!skuMap.has(key))
820
+ skuMap.set(key, []);
821
+ skuMap.get(key).push(p);
822
+ }
823
+ const skuDupes = Array.from(skuMap.entries())
824
+ .filter(([_, products]) => products.length > 1)
825
+ .map(([sku, products]) => ({ sku, count: products.length, products }));
826
+ // Pre-format as markdown — formatter drops nested products arrays
827
+ const totalNameDupes = duplicates.reduce((s, d) => s + d.count, 0);
828
+ const totalSkuDupes = skuDupes.reduce((s, d) => s + d.count, 0);
829
+ const lines = [
830
+ `## Duplicate Products`,
831
+ `**By Name**: ${duplicates.length} groups (${totalNameDupes} products) | **By SKU**: ${skuDupes.length} groups (${totalSkuDupes} products)\n`,
832
+ ];
833
+ if (duplicates.length > 0) {
834
+ lines.push("### Name Duplicates");
835
+ for (const group of duplicates.slice(0, 20)) {
836
+ lines.push(`\n**"${group.normalized_name}"** (${group.count} products):`);
837
+ lines.push("| Name | SKU | Status | Stock | Created |");
838
+ lines.push("| --- | --- | --- | ---: | --- |");
839
+ for (const p of group.products) {
840
+ lines.push(`| ${p.name} | ${p.sku || "—"} | ${p.status} | ${p.stock_quantity ?? "—"} | ${p.created_at?.slice(0, 10) || "—"} |`);
841
+ }
842
+ }
843
+ }
844
+ if (skuDupes.length > 0) {
845
+ lines.push("\n### SKU Duplicates");
846
+ for (const group of skuDupes.slice(0, 20)) {
847
+ lines.push(`\n**SKU "${group.sku}"** (${group.count} products):`);
848
+ lines.push("| Name | SKU | Status | Stock | Created |");
849
+ lines.push("| --- | --- | --- | ---: | --- |");
850
+ for (const p of group.products) {
851
+ lines.push(`| ${p.name} | ${p.sku || "—"} | ${p.status} | ${p.stock_quantity ?? "—"} | ${p.created_at?.slice(0, 10) || "—"} |`);
852
+ }
853
+ }
854
+ }
855
+ if (duplicates.length === 0 && skuDupes.length === 0) {
856
+ lines.push("\nNo duplicates found.");
857
+ }
858
+ return { success: true, data: lines.join("\n") };
859
+ }
860
+ case "merge": {
861
+ const primaryId = args.primary_product_id;
862
+ const secondaryId = args.secondary_product_id;
863
+ if (!primaryId || !secondaryId)
864
+ return { success: false, error: "primary_product_id and secondary_product_id required" };
865
+ if (primaryId === secondaryId)
866
+ return { success: false, error: "Cannot merge a product with itself" };
867
+ // Verify both exist and belong to this store
868
+ const { data: primary } = await sb.from("products").select("*").eq("id", primaryId).eq("store_id", sid).single();
869
+ const { data: secondary } = await sb.from("products").select("*").eq("id", secondaryId).eq("store_id", sid).single();
870
+ if (!primary)
871
+ return { success: false, error: `Primary product ${primaryId} not found` };
872
+ if (!secondary)
873
+ return { success: false, error: `Secondary product ${secondaryId} not found` };
874
+ const reassignResults = {};
875
+ // 1. Consolidate inventory — sum quantities per location
876
+ const { data: secInv } = await sb.from("inventory")
877
+ .select("product_id, location_id, quantity")
878
+ .eq("product_id", secondaryId).eq("store_id", sid);
879
+ if (secInv?.length) {
880
+ for (const row of secInv) {
881
+ if (!row.quantity || row.quantity <= 0)
882
+ continue;
883
+ // Try to add to existing primary inventory at same location
884
+ const { data: priRow } = await sb.from("inventory")
885
+ .select("id, quantity")
886
+ .eq("product_id", primaryId).eq("location_id", row.location_id).eq("store_id", sid)
887
+ .maybeSingle();
888
+ if (priRow) {
889
+ await sb.from("inventory").update({ quantity: (priRow.quantity || 0) + row.quantity, updated_at: new Date().toISOString() }).eq("id", priRow.id);
890
+ }
891
+ else {
892
+ await sb.from("inventory").insert({ store_id: sid, product_id: primaryId, location_id: row.location_id, quantity: row.quantity });
893
+ }
894
+ }
895
+ // Zero out secondary inventory
896
+ await sb.from("inventory").update({ quantity: 0 }).eq("product_id", secondaryId).eq("store_id", sid);
897
+ reassignResults.inventory = `consolidated ${secInv.length} location(s)`;
898
+ }
899
+ else {
900
+ reassignResults.inventory = "no inventory to consolidate";
901
+ }
902
+ // 2. Reassign order_items
903
+ const { error: oiErr, count: oiCount } = await sb.from("order_items")
904
+ .update({ product_id: primaryId }).eq("product_id", secondaryId);
905
+ reassignResults.order_items = oiErr ? `error: ${oiErr.message}` : `moved ${oiCount ?? 0} rows`;
906
+ // 3. Reassign cart_items
907
+ const { error: ciErr, count: ciCount } = await sb.from("cart_items")
908
+ .update({ product_id: primaryId }).eq("product_id", secondaryId);
909
+ reassignResults.cart_items = ciErr ? `error: ${ciErr.message}` : `moved ${ciCount ?? 0} rows`;
910
+ // 4. Reassign purchase_order_items
911
+ const { error: poiErr, count: poiCount } = await sb.from("purchase_order_items")
912
+ .update({ product_id: primaryId }).eq("product_id", secondaryId);
913
+ reassignResults.purchase_order_items = poiErr ? `error: ${poiErr.message}` : `moved ${poiCount ?? 0} rows`;
914
+ // 5. Reassign product_reviews
915
+ const { error: prErr, count: prCount } = await sb.from("product_reviews")
916
+ .update({ product_id: primaryId }).eq("product_id", secondaryId);
917
+ reassignResults.product_reviews = prErr ? `error: ${prErr.message}` : `moved ${prCount ?? 0} rows`;
918
+ // 6. Fill in missing fields on primary from secondary
919
+ const fillFields = ["description", "short_description", "cost_price", "wholesale_price", "weight", "featured_image"];
920
+ const fills = {};
921
+ for (const field of fillFields) {
922
+ if (!primary[field] && secondary[field])
923
+ fills[field] = secondary[field];
924
+ }
925
+ // Merge custom_fields (secondary fills gaps)
926
+ if (secondary.custom_fields && typeof secondary.custom_fields === "object") {
927
+ const merged = { ...secondary.custom_fields, ...(primary.custom_fields || {}) };
928
+ fills.custom_fields = merged;
929
+ }
930
+ if (Object.keys(fills).length > 0) {
931
+ await sb.from("products").update(fills).eq("id", primaryId).eq("store_id", sid);
932
+ reassignResults.field_fills = Object.keys(fills).join(", ");
933
+ }
934
+ // 7. Archive the secondary product
935
+ await sb.from("products").update({
936
+ status: "archived",
937
+ name: `[MERGED] ${secondary.name}`,
938
+ }).eq("id", secondaryId).eq("store_id", sid);
939
+ // Re-read primary
940
+ const { data: merged } = await sb.from("products")
941
+ .select("id, name, sku, status, stock_quantity, cost_price, created_at, updated_at")
942
+ .eq("id", primaryId).single();
943
+ return {
944
+ success: true,
945
+ data: {
946
+ merged_product: merged,
947
+ archived_product: { id: secondaryId, name: secondary.name },
948
+ reassign_results: reassignResults,
949
+ }
950
+ };
951
+ }
952
+ default:
953
+ return { success: false, error: `Unknown products action: ${args.action}. Valid: browse, find, get, create, update, delete, find_duplicates, merge, list_categories, get_category, create_category, update_category, delete_category, list_field_schemas, get_field_schema, create_field_schema, update_field_schema, delete_field_schema, list_pricing_schemas, get_pricing_schema, create_pricing_schema, update_pricing_schema, delete_pricing_schema, list_catalogs, create_catalog, update_catalog, assign_schema, unassign_schema` };
954
+ }
955
+ }
956
+ export async function handleCollections(sb, args, storeId) {
957
+ const sid = storeId;
958
+ switch (args.action) {
959
+ case "find": {
960
+ let q = sb.from("creation_collections").select("*").eq("store_id", sid);
961
+ if (args.name) {
962
+ const sn = sanitizeFilterValue(args.name);
963
+ q = q.ilike("name", `%${sn}%`);
964
+ }
965
+ const { data, error } = await q.limit(100);
966
+ return error ? { success: false, error: error.message } : { success: true, data };
967
+ }
968
+ case "create": {
969
+ const { data, error } = await sb.from("creation_collections")
970
+ .insert({ store_id: sid, name: args.name }).select().single();
971
+ return error ? { success: false, error: error.message } : { success: true, data };
972
+ }
973
+ default:
974
+ return { success: false, error: `Unknown collections action: ${args.action}` };
975
+ }
976
+ }