whale-code 6.4.0 → 6.5.1

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 (187) hide show
  1. package/bin/swagmanager-mcp.js +51 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +65 -8
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +7 -6
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +85 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +46 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +36 -17
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +9 -6
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +25 -2
  180. package/dist/shared/agent-core.js +66 -5
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +15 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -0,0 +1,96 @@
1
+ // server/handlers/remove-bg.ts — Remove.bg background removal
2
+ // Removes background from images, uploads result to Supabase storage + store_media
3
+ async function getRemoveBgKey(sb, storeId) {
4
+ try {
5
+ const { data } = await sb.rpc("decrypt_secret", { p_name: "REMOVE_BG_API_KEY", p_store_id: storeId });
6
+ return data;
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ async function uploadResult(sb, storeId, buffer, sourceUrl) {
13
+ const id = crypto.randomUUID();
14
+ const fileName = `${id}.png`;
15
+ const storagePath = `remove-bg/${storeId.toUpperCase()}/${fileName}`;
16
+ const { error: uploadErr } = await sb.storage
17
+ .from("product-images")
18
+ .upload(storagePath, buffer, { contentType: "image/png", upsert: true });
19
+ if (uploadErr)
20
+ throw new Error(`Storage upload failed: ${uploadErr.message}`);
21
+ const { data: urlData } = sb.storage.from("product-images").getPublicUrl(storagePath);
22
+ const fileUrl = urlData.publicUrl;
23
+ const { data: mediaRow, error: mediaErr } = await sb
24
+ .from("store_media")
25
+ .insert({
26
+ store_id: storeId,
27
+ file_name: fileName,
28
+ file_path: storagePath,
29
+ file_url: fileUrl,
30
+ file_size: buffer.length,
31
+ file_type: "image/png",
32
+ category: "ai_generated",
33
+ ai_tags: ["background-removed", "remove.bg"],
34
+ ai_description: `Background removed from: ${sourceUrl.substring(0, 200)}`,
35
+ source: "remove-bg",
36
+ folder: "remove-bg",
37
+ })
38
+ .select("id")
39
+ .single();
40
+ if (mediaErr)
41
+ console.error("[remove-bg] store_media insert error:", mediaErr.message);
42
+ return { file_url: fileUrl, media_id: mediaRow?.id || id, file_name: fileName };
43
+ }
44
+ export async function handleRemoveBg(sb, args, storeId) {
45
+ if (!storeId)
46
+ return { success: false, error: "store_id is required" };
47
+ const apiKey = await getRemoveBgKey(sb, storeId);
48
+ if (!apiKey)
49
+ return { success: false, error: "Remove.bg API key not configured. Add REMOVE_BG_API_KEY to platform_secrets." };
50
+ const imageUrl = args.image_url;
51
+ const imageBase64 = args.image_base64;
52
+ const size = args.size || "auto";
53
+ const type = args.type || "auto";
54
+ if (!imageUrl && !imageBase64) {
55
+ return { success: false, error: "Provide image_url (public URL), image_base64, or image_path (local file — handled by CLI)" };
56
+ }
57
+ // Reject local file paths passed as image_url
58
+ if (imageUrl && (imageUrl.startsWith("/") || /^[A-Z]:\\/i.test(imageUrl))) {
59
+ return { success: false, error: `Local paths cannot be used as image_url. Use image_path instead: remove_bg(image_path="${imageUrl}")` };
60
+ }
61
+ // Build form data for remove.bg API
62
+ const form = new FormData();
63
+ if (imageUrl) {
64
+ form.append("image_url", imageUrl);
65
+ }
66
+ else {
67
+ // Strip data URI prefix if present
68
+ const b64 = imageBase64.replace(/^data:image\/[a-z]+;base64,/, "");
69
+ const buf = Buffer.from(b64, "base64");
70
+ form.append("image_file", new Blob([buf]), "image.png");
71
+ }
72
+ form.append("size", size);
73
+ form.append("type", type);
74
+ const resp = await fetch("https://api.remove.bg/v1.0/removebg", {
75
+ method: "POST",
76
+ headers: { "X-Api-Key": apiKey },
77
+ body: form,
78
+ });
79
+ if (!resp.ok) {
80
+ const errText = await resp.text().catch(() => resp.statusText);
81
+ return { success: false, error: `remove.bg API error ${resp.status}: ${errText}` };
82
+ }
83
+ const resultBuffer = Buffer.from(await resp.arrayBuffer());
84
+ const sourceRef = imageUrl || "base64-input";
85
+ const { file_url, media_id, file_name } = await uploadResult(sb, storeId, resultBuffer, sourceRef);
86
+ return {
87
+ success: true,
88
+ data: {
89
+ file_url,
90
+ media_id,
91
+ file_name,
92
+ file_size: resultBuffer.length,
93
+ source: sourceRef,
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,6 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export declare function handleStorefront(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
3
+ success: boolean;
4
+ data?: unknown;
5
+ error?: string;
6
+ }>;
@@ -0,0 +1,477 @@
1
+ // handlers/storefront.ts — Consumer-facing commerce tool for AI shopping agents.
2
+ // Read-heavy, limited writes (cart + checkout). Only exposes published/active products.
3
+ import { sanitizeFilterValue } from "../lib/utils.js";
4
+ // ============================================================================
5
+ // HELPERS
6
+ // ============================================================================
7
+ /** Resolve the primary display price from pricing_data JSON.
8
+ * pricing_data is a Record<tierName, { default_price?, price?, ... }>.
9
+ * Returns the first tier's default_price or price, or 0. */
10
+ function resolvePrimaryPrice(pricingData) {
11
+ if (!pricingData || typeof pricingData !== "object")
12
+ return 0;
13
+ const tiers = Object.values(pricingData);
14
+ if (tiers.length === 0)
15
+ return 0;
16
+ return tiers[0].default_price ?? tiers[0].price ?? 0;
17
+ }
18
+ /** Format pricing tiers for consumer display (name + price, no cost data). */
19
+ function formatPricingTiers(pricingData) {
20
+ if (!pricingData || typeof pricingData !== "object")
21
+ return null;
22
+ const entries = Object.entries(pricingData);
23
+ if (entries.length === 0)
24
+ return null;
25
+ return entries.map(([tier, data]) => ({
26
+ tier,
27
+ price: data.default_price ?? data.price ?? 0,
28
+ }));
29
+ }
30
+ /** Strip internal/admin fields from a product row for public display. */
31
+ function filterPublicFields(product) {
32
+ const { cost_price, wholesale_price, is_wholesale, wholesale_only, minimum_wholesale_quantity, manage_stock, tax_status, tax_class, catalog_id, pricing_schema_id, ...publicFields } = product;
33
+ return publicFields;
34
+ }
35
+ // ============================================================================
36
+ // MAIN HANDLER
37
+ // ============================================================================
38
+ export async function handleStorefront(sb, args, storeId) {
39
+ const sid = storeId;
40
+ switch (args.action) {
41
+ // ======================== BROWSE ========================
42
+ case "browse": {
43
+ const page = args.page || 1;
44
+ const perPage = Math.min(args.per_page || 20, 50);
45
+ const offset = (page - 1) * perPage;
46
+ let q = sb.from("products")
47
+ .select("id, name, sku, slug, short_description, featured_image, pricing_data, status, primary_category_id, featured", { count: "exact" })
48
+ .eq("store_id", sid)
49
+ .in("status", ["published", "active"])
50
+ .order("created_at", { ascending: false })
51
+ .range(offset, offset + perPage - 1);
52
+ if (args.category_id) {
53
+ const catId = args.category_id;
54
+ // Include sub-categories
55
+ const { data: children } = await sb.from("categories").select("id").eq("parent_id", catId).eq("store_id", sid);
56
+ const catIds = [catId, ...(children || []).map((c) => c.id)];
57
+ q = q.in("primary_category_id", catIds);
58
+ }
59
+ if (args.featured === true)
60
+ q = q.eq("featured", true);
61
+ const sortBy = args.sort;
62
+ if (sortBy === "price_asc" || sortBy === "price_desc") {
63
+ // price sort not directly supported on JSON column — sort in JS below
64
+ }
65
+ else if (sortBy === "name") {
66
+ q = q.order("name", { ascending: true });
67
+ }
68
+ const { data, error, count } = await q;
69
+ if (error)
70
+ return { success: false, error: error.message };
71
+ let products = (data || []).map((p) => ({
72
+ ...filterPublicFields(p),
73
+ price: resolvePrimaryPrice(p.pricing_data),
74
+ pricing_tiers: formatPricingTiers(p.pricing_data),
75
+ }));
76
+ // Client-side price sort when requested
77
+ if (sortBy === "price_asc")
78
+ products.sort((a, b) => a.price - b.price);
79
+ if (sortBy === "price_desc")
80
+ products.sort((a, b) => b.price - a.price);
81
+ return {
82
+ success: true,
83
+ data: {
84
+ products,
85
+ page,
86
+ per_page: perPage,
87
+ total: count || 0,
88
+ total_pages: Math.ceil((count || 0) / perPage),
89
+ },
90
+ };
91
+ }
92
+ // ======================== PRODUCT DETAIL ========================
93
+ case "product_detail": {
94
+ const pid = args.product_id;
95
+ if (!pid)
96
+ return { success: false, error: "product_id is required" };
97
+ const { data: product, error: pErr } = await sb.from("products")
98
+ .select("*, category:categories!primary_category_id(id, name, slug)")
99
+ .eq("id", pid)
100
+ .eq("store_id", sid)
101
+ .in("status", ["published", "active"])
102
+ .single();
103
+ if (pErr || !product)
104
+ return { success: false, error: "Product not found or not available" };
105
+ // Inventory availability per location
106
+ const { data: inventory } = await sb.from("inventory")
107
+ .select("quantity, location:locations!location_id(id, name)")
108
+ .eq("product_id", pid)
109
+ .eq("store_id", sid)
110
+ .gt("quantity", 0);
111
+ return {
112
+ success: true,
113
+ data: {
114
+ ...filterPublicFields(product),
115
+ price: resolvePrimaryPrice(product.pricing_data),
116
+ pricing_tiers: formatPricingTiers(product.pricing_data),
117
+ availability: (inventory || []).map((inv) => ({
118
+ location_id: inv.location?.id,
119
+ location_name: inv.location?.name,
120
+ in_stock: (inv.quantity ?? 0) > 0,
121
+ quantity: inv.quantity,
122
+ })),
123
+ },
124
+ };
125
+ }
126
+ // ======================== SEARCH ========================
127
+ case "search": {
128
+ const query = args.query;
129
+ if (!query)
130
+ return { success: false, error: "query is required" };
131
+ const sq = sanitizeFilterValue(query);
132
+ const limit = Math.min(args.limit || 20, 50);
133
+ const { data, error } = await sb.from("products")
134
+ .select("id, name, sku, slug, short_description, featured_image, pricing_data, status, primary_category_id")
135
+ .eq("store_id", sid)
136
+ .in("status", ["published", "active"])
137
+ .or(`name.ilike.%${sq}%,description.ilike.%${sq}%,sku.ilike.%${sq}%`)
138
+ .order("name")
139
+ .limit(limit);
140
+ if (error)
141
+ return { success: false, error: error.message };
142
+ const products = (data || []).map((p) => ({
143
+ ...filterPublicFields(p),
144
+ price: resolvePrimaryPrice(p.pricing_data),
145
+ }));
146
+ return { success: true, data: { products, count: products.length, query } };
147
+ }
148
+ // ======================== CATEGORIES ========================
149
+ case "categories": {
150
+ const { data, error } = await sb.from("categories")
151
+ .select("id, name, slug, description, icon, parent_id, display_order, product_count")
152
+ .eq("store_id", sid)
153
+ .eq("is_active", true)
154
+ .order("display_order", { ascending: true });
155
+ if (error)
156
+ return { success: false, error: error.message };
157
+ return { success: true, data: { categories: data || [] } };
158
+ }
159
+ // ======================== CART CREATE ========================
160
+ case "cart_create": {
161
+ // Resolve default location if not provided
162
+ let locationId = args.location_id;
163
+ if (!locationId) {
164
+ const { data: loc } = await sb.from("locations")
165
+ .select("id")
166
+ .eq("store_id", sid)
167
+ .eq("is_active", true)
168
+ .limit(1)
169
+ .single();
170
+ if (loc)
171
+ locationId = loc.id;
172
+ }
173
+ const expiresAt = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString();
174
+ const record = {
175
+ store_id: sid,
176
+ status: "active",
177
+ expires_at: expiresAt,
178
+ };
179
+ if (locationId)
180
+ record.location_id = locationId;
181
+ if (args.customer_id)
182
+ record.customer_id = args.customer_id;
183
+ if (args.customer_email)
184
+ record.customer_email = args.customer_email;
185
+ const { data, error } = await sb.from("carts").insert(record).select().single();
186
+ if (error)
187
+ return { success: false, error: error.message };
188
+ return { success: true, data: { cart: data, items: [] } };
189
+ }
190
+ // ======================== CART ADD ========================
191
+ case "cart_add": {
192
+ const cartId = args.cart_id;
193
+ const productId = args.product_id;
194
+ const quantity = args.quantity || 1;
195
+ if (!cartId)
196
+ return { success: false, error: "cart_id is required" };
197
+ if (!productId)
198
+ return { success: false, error: "product_id is required" };
199
+ if (quantity < 1)
200
+ return { success: false, error: "quantity must be at least 1" };
201
+ // Validate cart is active
202
+ const { data: cart, error: cartErr } = await sb.from("carts")
203
+ .select("id, status")
204
+ .eq("id", cartId)
205
+ .eq("store_id", sid)
206
+ .single();
207
+ if (cartErr || !cart)
208
+ return { success: false, error: "Cart not found" };
209
+ if (cart.status !== "active")
210
+ return { success: false, error: "Cart is not active" };
211
+ // Validate product is published
212
+ const { data: product, error: prodErr } = await sb.from("products")
213
+ .select("id, name, sku, status, pricing_data")
214
+ .eq("id", productId)
215
+ .eq("store_id", sid)
216
+ .single();
217
+ if (prodErr || !product)
218
+ return { success: false, error: "Product not found" };
219
+ if (product.status !== "published" && product.status !== "active") {
220
+ return { success: false, error: "Product is not available for purchase" };
221
+ }
222
+ // Resolve price from pricing_data
223
+ let unitPrice = args.unit_price;
224
+ if (unitPrice === undefined) {
225
+ if (product.pricing_data) {
226
+ const pd = product.pricing_data;
227
+ if (args.tier && pd[args.tier]) {
228
+ unitPrice = pd[args.tier].default_price ?? pd[args.tier].price ?? 0;
229
+ }
230
+ else {
231
+ unitPrice = resolvePrimaryPrice(pd);
232
+ }
233
+ }
234
+ else {
235
+ unitPrice = 0;
236
+ }
237
+ }
238
+ const itemRecord = {
239
+ cart_id: cartId,
240
+ product_id: productId,
241
+ product_name: product.name,
242
+ sku: product.sku,
243
+ quantity,
244
+ unit_price: unitPrice,
245
+ };
246
+ if (args.tier)
247
+ itemRecord.tier_label = args.tier;
248
+ const { data: item, error: insertErr } = await sb.from("cart_items")
249
+ .insert(itemRecord).select().single();
250
+ if (insertErr)
251
+ return { success: false, error: insertErr.message };
252
+ return { success: true, data: { item, line_total: Math.round(quantity * (unitPrice || 0) * 100) / 100 } };
253
+ }
254
+ // ======================== CART VIEW ========================
255
+ case "cart_view": {
256
+ const cartId = args.cart_id;
257
+ if (!cartId)
258
+ return { success: false, error: "cart_id is required" };
259
+ const { data: cart, error: cartErr } = await sb.from("carts")
260
+ .select("*")
261
+ .eq("id", cartId)
262
+ .eq("store_id", sid)
263
+ .single();
264
+ if (cartErr || !cart)
265
+ return { success: false, error: "Cart not found" };
266
+ const { data: items } = await sb.from("cart_items")
267
+ .select("*")
268
+ .eq("cart_id", cartId)
269
+ .order("created_at", { ascending: true });
270
+ const cartItems = items || [];
271
+ const subtotal = cartItems.reduce((sum, i) => sum + (i.quantity ?? 0) * (i.unit_price ?? 0), 0);
272
+ return {
273
+ success: true,
274
+ data: {
275
+ cart: { id: cart.id, status: cart.status, expires_at: cart.expires_at },
276
+ items: cartItems.map((i) => ({
277
+ id: i.id,
278
+ product_id: i.product_id,
279
+ product_name: i.product_name,
280
+ sku: i.sku,
281
+ quantity: i.quantity,
282
+ unit_price: i.unit_price,
283
+ line_total: Math.round((i.quantity ?? 0) * (i.unit_price ?? 0) * 100) / 100,
284
+ tier_label: i.tier_label,
285
+ })),
286
+ subtotal: Math.round(subtotal * 100) / 100,
287
+ item_count: cartItems.length,
288
+ },
289
+ };
290
+ }
291
+ // ======================== CART UPDATE ========================
292
+ case "cart_update": {
293
+ const cartId = args.cart_id;
294
+ const itemId = args.item_id;
295
+ const quantity = args.quantity;
296
+ if (!cartId)
297
+ return { success: false, error: "cart_id is required" };
298
+ if (!itemId)
299
+ return { success: false, error: "item_id is required" };
300
+ if (!quantity || quantity < 1)
301
+ return { success: false, error: "quantity must be a positive integer" };
302
+ // Validate cart is active
303
+ const { data: cart } = await sb.from("carts")
304
+ .select("id, status")
305
+ .eq("id", cartId)
306
+ .eq("store_id", sid)
307
+ .single();
308
+ if (!cart)
309
+ return { success: false, error: "Cart not found" };
310
+ if (cart.status !== "active")
311
+ return { success: false, error: "Cart is not active" };
312
+ const { data, error } = await sb.from("cart_items")
313
+ .update({ quantity, updated_at: new Date().toISOString() })
314
+ .eq("id", itemId)
315
+ .eq("cart_id", cartId)
316
+ .select()
317
+ .single();
318
+ if (error || !data)
319
+ return { success: false, error: error?.message || "Cart item not found" };
320
+ return {
321
+ success: true,
322
+ data: {
323
+ item: data,
324
+ line_total: Math.round(data.quantity * (data.unit_price ?? 0) * 100) / 100,
325
+ },
326
+ };
327
+ }
328
+ // ======================== CART REMOVE ========================
329
+ case "cart_remove": {
330
+ const cartId = args.cart_id;
331
+ const itemId = args.item_id;
332
+ if (!cartId)
333
+ return { success: false, error: "cart_id is required" };
334
+ if (!itemId)
335
+ return { success: false, error: "item_id is required" };
336
+ // Validate cart is active
337
+ const { data: cart } = await sb.from("carts")
338
+ .select("id, status")
339
+ .eq("id", cartId)
340
+ .eq("store_id", sid)
341
+ .single();
342
+ if (!cart)
343
+ return { success: false, error: "Cart not found" };
344
+ if (cart.status !== "active")
345
+ return { success: false, error: "Cart is not active" };
346
+ const { error } = await sb.from("cart_items")
347
+ .delete()
348
+ .eq("id", itemId)
349
+ .eq("cart_id", cartId);
350
+ if (error)
351
+ return { success: false, error: error.message };
352
+ return { success: true, data: { removed: true, item_id: itemId } };
353
+ }
354
+ // ======================== CHECKOUT ========================
355
+ case "checkout": {
356
+ const cartId = args.cart_id;
357
+ if (!cartId)
358
+ return { success: false, error: "cart_id is required" };
359
+ // 1. Validate cart
360
+ const { data: cart, error: cartErr } = await sb.from("carts")
361
+ .select("*")
362
+ .eq("id", cartId)
363
+ .eq("store_id", sid)
364
+ .single();
365
+ if (cartErr || !cart)
366
+ return { success: false, error: "Cart not found" };
367
+ if (cart.status !== "active")
368
+ return { success: false, error: "Cart is not active" };
369
+ // 2. Fetch cart items
370
+ const { data: cartItems, error: itemsErr } = await sb.from("cart_items")
371
+ .select("*")
372
+ .eq("cart_id", cartId)
373
+ .order("created_at", { ascending: true });
374
+ if (itemsErr)
375
+ return { success: false, error: itemsErr.message };
376
+ if (!cartItems || cartItems.length === 0)
377
+ return { success: false, error: "Cart has no items" };
378
+ // 3. Compute totals
379
+ const subtotal = cartItems.reduce((sum, i) => sum + (i.quantity ?? 0) * (i.unit_price ?? 0), 0);
380
+ const taxAmount = cart.tax_amount ?? 0;
381
+ const total = Math.round((subtotal + taxAmount) * 100) / 100;
382
+ const roundedSubtotal = Math.round(subtotal * 100) / 100;
383
+ // 4. Generate sequential order number
384
+ const { data: maxOrder } = await sb.from("orders")
385
+ .select("order_number")
386
+ .eq("store_id", sid)
387
+ .order("created_at", { ascending: false })
388
+ .limit(1)
389
+ .single();
390
+ let nextNum = 10001;
391
+ if (maxOrder?.order_number) {
392
+ const parsed = parseInt(maxOrder.order_number.replace("#", ""), 10);
393
+ if (!isNaN(parsed))
394
+ nextNum = parsed + 1;
395
+ }
396
+ const orderNumber = `#${String(nextNum).padStart(5, "0")}`;
397
+ // 5. Insert order
398
+ const orderRecord = {
399
+ store_id: sid,
400
+ location_id: cart.location_id ?? null,
401
+ customer_id: cart.customer_id ?? null,
402
+ order_number: orderNumber,
403
+ channel: "online",
404
+ status: "pending",
405
+ subtotal: roundedSubtotal,
406
+ tax_amount: Math.round(taxAmount * 100) / 100,
407
+ total_amount: total,
408
+ payment_method: "pending",
409
+ payment_status: "pending",
410
+ };
411
+ const { data: order, error: orderErr } = await sb.from("orders")
412
+ .insert(orderRecord).select().single();
413
+ if (orderErr)
414
+ return { success: false, error: orderErr.message };
415
+ // 6. Bulk insert order_items
416
+ const orderItems = cartItems.map((ci) => ({
417
+ order_id: order.id,
418
+ product_id: ci.product_id,
419
+ product_name: ci.product_name,
420
+ quantity: ci.quantity,
421
+ unit_price: ci.unit_price,
422
+ line_total: Math.round((ci.quantity ?? 0) * (ci.unit_price ?? 0) * 100) / 100,
423
+ variant_id: ci.variant_id ?? null,
424
+ variant_name: ci.variant_name ?? null,
425
+ tier_label: ci.tier_label ?? null,
426
+ }));
427
+ await sb.from("order_items").insert(orderItems);
428
+ // 7. Deduct inventory per item (best-effort)
429
+ const locationId = cart.location_id;
430
+ if (locationId) {
431
+ for (const ci of cartItems) {
432
+ try {
433
+ await sb.rpc("atomic_inventory_adjust", {
434
+ p_store_id: sid,
435
+ p_product_id: ci.product_id,
436
+ p_location_id: locationId,
437
+ p_adjustment: -(ci.quantity ?? 0),
438
+ p_reason: "sale",
439
+ });
440
+ }
441
+ catch {
442
+ // Inventory deduction non-critical — order still stands
443
+ }
444
+ }
445
+ }
446
+ // 8. Mark cart as completed
447
+ await sb.from("carts")
448
+ .update({ status: "completed", updated_at: new Date().toISOString() })
449
+ .eq("id", cartId)
450
+ .eq("store_id", sid);
451
+ // 9. Return order summary
452
+ return {
453
+ success: true,
454
+ data: {
455
+ order_id: order.id,
456
+ order_number: orderNumber,
457
+ status: "pending",
458
+ subtotal: roundedSubtotal,
459
+ tax_amount: Math.round(taxAmount * 100) / 100,
460
+ total,
461
+ items: orderItems.map((oi) => ({
462
+ product_id: oi.product_id,
463
+ product_name: oi.product_name,
464
+ quantity: oi.quantity,
465
+ unit_price: oi.unit_price,
466
+ line_total: oi.line_total,
467
+ })),
468
+ },
469
+ };
470
+ }
471
+ default:
472
+ return {
473
+ success: false,
474
+ error: `Unknown storefront action: ${args.action}. Valid: browse, product_detail, search, categories, cart_create, cart_add, cart_view, cart_update, cart_remove, checkout`,
475
+ };
476
+ }
477
+ }
@@ -140,14 +140,32 @@ export async function handlePurchaseOrders(sb, args, storeId) {
140
140
  if (poErr || !po)
141
141
  return { success: false, error: poErr?.message || "PO not found" };
142
142
  // If caller provides specific items use those, otherwise receive all remaining
143
- const receiveItems = args.items
144
- ? args.items
145
- : (po.items || [])
143
+ const poItems = po.items || [];
144
+ let receiveItems;
145
+ if (args.items) {
146
+ // Normalize caller-provided items: accept item_id, po_item_id, or product_id
147
+ const callerItems = args.items;
148
+ receiveItems = callerItems.map((ci) => {
149
+ let itemId = ci.item_id || ci.po_item_id || ci.line_item_id;
150
+ // If caller used product_id instead, resolve to PO line item ID
151
+ if (!itemId && ci.product_id) {
152
+ const match = poItems.find((pi) => pi.id === ci.product_id || pi.product_id === ci.product_id);
153
+ if (match)
154
+ itemId = match.id;
155
+ }
156
+ return { item_id: itemId || null, quantity: Number(ci.quantity) || 0 };
157
+ }).filter((i) => i.item_id && i.quantity > 0);
158
+ if (!receiveItems.length)
159
+ return { success: false, error: "No valid items to receive — provide item_id or product_id matching a PO line item" };
160
+ }
161
+ else {
162
+ receiveItems = poItems
146
163
  .map((item) => ({
147
164
  item_id: item.id,
148
165
  quantity: Math.max((item.quantity || 0) - (item.received_quantity || 0), 0),
149
166
  }))
150
167
  .filter((item) => item.quantity > 0);
168
+ }
151
169
  if (!receiveItems.length)
152
170
  return { success: false, error: "No items to receive (all fully received)" };
153
171
  const locationId = args.location_id || po.location_id;