whale-code 6.4.0 → 6.5.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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -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 +66 -2
  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 +15 -3
  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 +71 -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 +45 -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 +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  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 +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  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 +1 -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
@@ -1,21 +1,32 @@
1
1
  import { sanitizeFilterValue } from "../lib/utils.js";
2
+ import { handleEmail } from "./comms.js";
3
+ /** Strip internal infrastructure fields that stores should never see */
4
+ function stripInternal(row) {
5
+ if (!row)
6
+ return null;
7
+ const { platform_user_id, store_id, password_hash, ...clean } = row;
8
+ return clean;
9
+ }
10
+ function stripInternalArray(rows) {
11
+ return (rows || []).map(r => stripInternal(r));
12
+ }
2
13
  export async function handleCustomers(sb, args, storeId) {
3
14
  const sid = storeId;
4
15
  switch (args.action) {
5
- // ---- FIND: search customers via v_store_customers view ----
16
+ // ---- FIND: search customers via v_segment_customers view ----
6
17
  case "find": {
7
- let q = sb.from("v_store_customers")
8
- .select("id, platform_user_id, first_name, last_name, email, phone, loyalty_points, loyalty_tier, total_spent, total_orders, lifetime_value, is_active, created_at")
18
+ const sortField = args.sort_by || "created_at";
19
+ const sortAsc = args.sort_order === "asc";
20
+ let q = sb.from("v_segment_customers")
21
+ .select("id, first_name, last_name, email, phone, loyalty_points, loyalty_tier, total_spent, total_orders, lifetime_value, is_active, rfm_segment, is_vip_customer, is_at_risk, is_churned, reorder_due, ai_churn_risk, days_since_last_order, engagement_score, age_bracket, created_at")
9
22
  .eq("store_id", sid)
10
- .order("created_at", { ascending: false });
23
+ .order(sortField, { ascending: sortAsc });
11
24
  if (args.limit)
12
25
  q = q.limit(args.limit);
13
26
  if (args.query) {
14
27
  const raw = sanitizeFilterValue(String(args.query).trim());
15
- // Split multi-word queries so "Hannah Spivey" matches first_name=Hannah OR last_name=Spivey
16
28
  const words = raw.split(/\s+/).filter(Boolean);
17
29
  if (words.length > 1) {
18
- // Multi-word: each word matches any field
19
30
  const clauses = words.map(w => { const sw = sanitizeFilterValue(w); return `first_name.ilike.%${sw}%,last_name.ilike.%${sw}%`; }).join(",");
20
31
  q = q.or(`${clauses},email.ilike.%${raw}%,phone.ilike.%${raw}%`);
21
32
  }
@@ -30,79 +41,147 @@ export async function handleCustomers(sb, args, storeId) {
30
41
  q = q.eq("is_active", false);
31
42
  if (args.loyalty_tier)
32
43
  q = q.eq("loyalty_tier", args.loyalty_tier);
44
+ if (args.rfm_segment)
45
+ q = q.eq("rfm_segment", args.rfm_segment);
46
+ if (args.is_vip !== undefined)
47
+ q = q.eq("is_vip_customer", args.is_vip);
48
+ if (args.is_at_risk !== undefined)
49
+ q = q.eq("is_at_risk", args.is_at_risk);
50
+ if (args.is_churned !== undefined)
51
+ q = q.eq("is_churned", args.is_churned);
52
+ if (args.reorder_due !== undefined)
53
+ q = q.eq("reorder_due", args.reorder_due);
54
+ if (args.age_bracket)
55
+ q = q.eq("age_bracket", args.age_bracket);
56
+ if (args.min_orders !== undefined)
57
+ q = q.gte("total_orders", args.min_orders);
58
+ if (args.max_orders !== undefined)
59
+ q = q.lte("total_orders", args.max_orders);
60
+ if (args.min_spent !== undefined)
61
+ q = q.gte("total_spent", args.min_spent);
62
+ if (args.max_spent !== undefined)
63
+ q = q.lte("total_spent", args.max_spent);
33
64
  const { data, error } = await q;
34
65
  return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
35
66
  }
36
- // ---- GET: full customer detail with orders, activity, notes ----
67
+ // ---- GET: full 360° customer detail with orders, activity, notes, loyalty, segments ----
37
68
  case "get": {
38
69
  const custId = args.customer_id;
39
- const { data: customer, error: custErr } = await sb.from("v_store_customers")
70
+ const { data: customer, error: custErr } = await sb.from("v_segment_customers")
40
71
  .select("*").eq("id", custId).eq("store_id", sid).single();
41
72
  if (custErr)
42
73
  return { success: false, error: custErr.message };
43
- const { data: orders } = await sb.from("orders")
44
- .select("id, order_number, status, total_amount, payment_status, fulfillment_status, created_at")
45
- .eq("customer_id", custId).eq("store_id", sid)
46
- .order("created_at", { ascending: false })
47
- .limit(args.orders_limit || 10);
48
- const { data: notes } = await sb.from("customer_notes")
49
- .select("id, note, created_by, created_at")
50
- .eq("customer_id", custId)
51
- .order("created_at", { ascending: false }).limit(10);
52
- const { data: activity } = await sb.from("customer_activity")
53
- .select("id, activity_type, description, created_at")
54
- .eq("customer_id", custId)
55
- .order("created_at", { ascending: false }).limit(10);
56
- const { data: profile } = await sb.from("store_customer_profiles")
57
- .select("*").eq("relationship_id", custId).maybeSingle();
58
- // Flatten profile into customer object so all fields are visible
59
- // (formatter drops nested objects that aren't arrays)
74
+ const [{ data: orders }, { data: notes }, { data: activity }, { data: profile }, { data: loyaltyHistory }, { data: segmentRows }] = await Promise.all([
75
+ sb.from("orders")
76
+ .select("id, order_number, status, total_amount, payment_status, fulfillment_status, created_at")
77
+ .eq("customer_id", custId).eq("store_id", sid)
78
+ .order("created_at", { ascending: false })
79
+ .limit(args.orders_limit || 10),
80
+ sb.from("customer_notes")
81
+ .select("id, note, created_by, created_at")
82
+ .eq("customer_id", custId)
83
+ .order("created_at", { ascending: false }).limit(10),
84
+ sb.from("customer_activity")
85
+ .select("id, activity_type, description, created_at")
86
+ .eq("customer_id", custId)
87
+ .order("created_at", { ascending: false }).limit(10),
88
+ sb.from("store_customer_profiles")
89
+ .select("*").eq("relationship_id", custId).maybeSingle(),
90
+ sb.from("loyalty_transactions")
91
+ .select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
92
+ .eq("customer_id", custId)
93
+ .order("created_at", { ascending: false }).limit(20),
94
+ sb.from("customer_segment_memberships")
95
+ .select("added_at, segment:customer_segments(id, name, type)")
96
+ .eq("customer_id", custId),
97
+ ]);
98
+ // Flatten profile fields into customer object (skip sensitive + computed-only fields)
60
99
  const prof = profile;
61
- const profileFields = prof ? {
62
- street_address: prof.street_address || null,
63
- city: prof.city || null,
64
- state: prof.state || null,
65
- postal_code: prof.postal_code || null,
66
- drivers_license_number: prof.drivers_license_number || null,
67
- id_verified: prof.id_verified || false,
68
- medical_card_number: prof.medical_card_number || null,
69
- medical_card_expiry: prof.medical_card_expiry || null,
70
- is_wholesale_approved: prof.is_wholesale_approved || false,
71
- wholesale_tier: prof.wholesale_tier || null,
72
- } : {};
73
- return { success: true, data: { ...customer, ...profileFields, orders, notes, activity } };
100
+ const profileFields = {};
101
+ if (prof) {
102
+ const skip = new Set([
103
+ "id", "relationship_id", "created_at", "updated_at",
104
+ "password_hash", // never expose
105
+ // These are recomputed by refresh_metrics — read from the view instead
106
+ "total_spent", "total_orders", "lifetime_value", "average_order_value",
107
+ "first_order_at", "last_order_at",
108
+ ]);
109
+ for (const [k, v] of Object.entries(prof)) {
110
+ if (!skip.has(k))
111
+ profileFields[k] = v ?? null;
112
+ }
113
+ }
114
+ // Flatten segment memberships
115
+ const segments = (segmentRows || []).map((r) => ({
116
+ segment_id: r.segment?.id, name: r.segment?.name, type: r.segment?.type, added_at: r.added_at,
117
+ }));
118
+ return { success: true, data: stripInternal({ ...customer, ...profileFields, orders, notes, activity, loyalty_history: loyaltyHistory || [], segments }) };
74
119
  }
75
120
  // ---- CREATE: new customer (platform_user + relationship + profile) ----
121
+ // Matching priority: email → phone → drivers_license → name+DOB → create new
76
122
  case "create": {
77
123
  const email = args.email;
78
124
  const phone = args.phone;
79
125
  const firstName = args.first_name;
80
126
  const lastName = args.last_name;
127
+ const dob = args.date_of_birth;
128
+ const dl = args.drivers_license_number;
81
129
  if (!firstName && !lastName)
82
130
  return { success: false, error: "first_name or last_name is required" };
83
- // Check for existing platform_user by email or phone
131
+ // Multi-tier matching to find existing platform_user (mirrors POS verify flow)
84
132
  let platformUserId = null;
85
- if (email) {
133
+ let matchedVia = null;
134
+ // Tier 1: email
135
+ if (!platformUserId && email) {
86
136
  const { data: existing } = await sb.from("platform_users")
87
137
  .select("id").eq("email", email).maybeSingle();
88
- if (existing)
138
+ if (existing) {
89
139
  platformUserId = existing.id;
140
+ matchedVia = "email";
141
+ }
90
142
  }
143
+ // Tier 2: phone
91
144
  if (!platformUserId && phone) {
92
145
  const { data: existing } = await sb.from("platform_users")
93
146
  .select("id").eq("phone", phone).maybeSingle();
94
- if (existing)
147
+ if (existing) {
95
148
  platformUserId = existing.id;
149
+ matchedVia = "phone";
150
+ }
96
151
  }
97
- // Create platform_user if not found
152
+ // Tier 3: drivers license (look up via store_customer_profiles → relationship → platform_user)
153
+ if (!platformUserId && dl) {
154
+ const { data: profMatch } = await sb.from("store_customer_profiles")
155
+ .select("relationship_id, relationship:user_creation_relationships!relationship_id(user_id, store_id)")
156
+ .eq("drivers_license_number", dl).limit(1).maybeSingle();
157
+ if (profMatch) {
158
+ const rel = profMatch.relationship;
159
+ if (rel?.user_id) {
160
+ platformUserId = rel.user_id;
161
+ matchedVia = "drivers_license";
162
+ }
163
+ }
164
+ }
165
+ // Tier 4: exact name + date of birth (only when all 3 are provided)
166
+ if (!platformUserId && firstName && lastName && dob) {
167
+ const { data: nameMatch } = await sb.from("platform_users")
168
+ .select("id").ilike("first_name", firstName).ilike("last_name", lastName)
169
+ .eq("date_of_birth", dob).limit(1).maybeSingle();
170
+ if (nameMatch) {
171
+ platformUserId = nameMatch.id;
172
+ matchedVia = "name+dob";
173
+ }
174
+ }
175
+ // Create platform_user if no match found
176
+ const isNameOnly = !email && !phone;
98
177
  if (!platformUserId) {
99
178
  const puInsert = { first_name: firstName, last_name: lastName };
100
179
  if (email)
101
180
  puInsert.email = email;
102
181
  if (phone)
103
182
  puInsert.phone = phone;
104
- if (args.date_of_birth)
105
- puInsert.date_of_birth = args.date_of_birth;
183
+ if (dob)
184
+ puInsert.date_of_birth = dob;
106
185
  const { data: newPu, error: puErr } = await sb.from("platform_users")
107
186
  .insert(puInsert).select("id").single();
108
187
  if (puErr)
@@ -115,7 +194,7 @@ export async function handleCustomers(sb, args, storeId) {
115
194
  if (existingRel) {
116
195
  const { data: existing } = await sb.from("v_store_customers")
117
196
  .select("*").eq("id", existingRel.id).single();
118
- return { success: true, data: existing, note: "Customer already exists for this store" };
197
+ return { success: true, data: stripInternal(existing), note: `Customer already exists (matched via ${matchedVia || "existing record"})` };
119
198
  }
120
199
  // Create relationship
121
200
  const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
@@ -137,44 +216,63 @@ export async function handleCustomers(sb, args, storeId) {
137
216
  profileInsert.state = args.state;
138
217
  if (args.postal_code)
139
218
  profileInsert.postal_code = args.postal_code;
140
- if (args.drivers_license_number)
141
- profileInsert.drivers_license_number = args.drivers_license_number;
219
+ if (dl)
220
+ profileInsert.drivers_license_number = dl;
142
221
  if (args.medical_card_number)
143
222
  profileInsert.medical_card_number = args.medical_card_number;
144
223
  if (args.medical_card_expiry)
145
224
  profileInsert.medical_card_expiry = args.medical_card_expiry;
225
+ if (args.gender)
226
+ profileInsert.gender = args.gender;
227
+ if (args.eye_color)
228
+ profileInsert.eye_color = args.eye_color;
229
+ if (args.hair_color)
230
+ profileInsert.hair_color = args.hair_color;
231
+ if (args.height_raw)
232
+ profileInsert.height_raw = args.height_raw;
233
+ if (args.weight)
234
+ profileInsert.weight = args.weight;
235
+ if (args.suffix)
236
+ profileInsert.suffix = args.suffix;
237
+ if (args.is_staff !== undefined)
238
+ profileInsert.is_staff = args.is_staff;
146
239
  await sb.from("store_customer_profiles").insert(profileInsert);
147
240
  const { data: created } = await sb.from("v_store_customers")
148
241
  .select("*").eq("id", rel.id).single();
149
- return { success: true, data: created };
242
+ return {
243
+ success: true,
244
+ data: stripInternal(created),
245
+ ...(matchedVia ? { note: `Linked to existing customer record (matched via ${matchedVia})` } : {}),
246
+ ...(isNameOnly ? { warning: "Created with no email or phone — add contact info to prevent duplicates." } : {}),
247
+ };
150
248
  }
151
- // ---- UPDATE: modify customer identity or store profile ----
249
+ // ---- UPDATE: modify any customer fields ----
152
250
  case "update": {
153
251
  const custId = args.customer_id;
154
252
  const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
155
253
  .select("id, user_id").eq("id", custId).eq("store_id", sid).single();
156
254
  if (relErr)
157
255
  return { success: false, error: `Customer not found: ${relErr.message}` };
158
- // Update platform_users (identity fields)
159
- const puUpdates = {};
256
+ // Customer identity (name, email, phone, DOB)
257
+ const identityUpdates = {};
160
258
  if (args.first_name !== undefined)
161
- puUpdates.first_name = args.first_name;
259
+ identityUpdates.first_name = args.first_name;
162
260
  if (args.last_name !== undefined)
163
- puUpdates.last_name = args.last_name;
261
+ identityUpdates.last_name = args.last_name;
164
262
  if (args.email !== undefined)
165
- puUpdates.email = args.email;
263
+ identityUpdates.email = args.email;
166
264
  if (args.phone !== undefined)
167
- puUpdates.phone = args.phone;
265
+ identityUpdates.phone = args.phone;
168
266
  if (args.date_of_birth !== undefined)
169
- puUpdates.date_of_birth = args.date_of_birth;
170
- if (Object.keys(puUpdates).length > 0) {
171
- puUpdates.updated_at = new Date().toISOString();
267
+ identityUpdates.date_of_birth = args.date_of_birth;
268
+ if (Object.keys(identityUpdates).length > 0) {
269
+ identityUpdates.updated_at = new Date().toISOString();
172
270
  const { error: puErr } = await sb.from("platform_users")
173
- .update(puUpdates).eq("id", rel.user_id);
271
+ .update(identityUpdates).eq("id", rel.user_id);
174
272
  if (puErr)
175
- return { success: false, error: `Failed to update user: ${puErr.message}` };
273
+ return { success: false, error: `Failed to update customer: ${puErr.message}` };
176
274
  }
177
- // Update relationship (consent, status)
275
+ // Consent & status
178
276
  const relUpdates = {};
179
277
  if (args.status !== undefined)
180
278
  relUpdates.status = args.status;
@@ -188,7 +286,7 @@ export async function handleCustomers(sb, args, storeId) {
188
286
  relUpdates.updated_at = new Date().toISOString();
189
287
  await sb.from("user_creation_relationships").update(relUpdates).eq("id", custId);
190
288
  }
191
- // Update store profile (loyalty, address, ID, wholesale)
289
+ // Profile fields
192
290
  const profUpdates = {};
193
291
  if (args.loyalty_points !== undefined)
194
292
  profUpdates.loyalty_points = args.loyalty_points;
@@ -202,24 +300,65 @@ export async function handleCustomers(sb, args, storeId) {
202
300
  profUpdates.state = args.state;
203
301
  if (args.postal_code !== undefined)
204
302
  profUpdates.postal_code = args.postal_code;
303
+ if (args.billing_address !== undefined)
304
+ profUpdates.billing_address = args.billing_address;
305
+ if (args.shipping_addresses !== undefined)
306
+ profUpdates.shipping_addresses = args.shipping_addresses;
307
+ if (args.default_shipping_address_index !== undefined)
308
+ profUpdates.default_shipping_address_index = args.default_shipping_address_index;
205
309
  if (args.drivers_license_number !== undefined)
206
310
  profUpdates.drivers_license_number = args.drivers_license_number;
207
- if (args.id_verified !== undefined)
311
+ if (args.id_verified !== undefined) {
208
312
  profUpdates.id_verified = args.id_verified;
313
+ if (args.id_verified === true)
314
+ profUpdates.id_verified_at = new Date().toISOString();
315
+ }
316
+ if (args.license_expiration_date !== undefined)
317
+ profUpdates.license_expiration_date = args.license_expiration_date;
318
+ if (args.license_issue_date !== undefined)
319
+ profUpdates.license_issue_date = args.license_issue_date;
209
320
  if (args.medical_card_number !== undefined)
210
321
  profUpdates.medical_card_number = args.medical_card_number;
211
322
  if (args.medical_card_expiry !== undefined)
212
323
  profUpdates.medical_card_expiry = args.medical_card_expiry;
324
+ if (args.gender !== undefined)
325
+ profUpdates.gender = args.gender;
326
+ if (args.eye_color !== undefined)
327
+ profUpdates.eye_color = args.eye_color;
328
+ if (args.height_raw !== undefined)
329
+ profUpdates.height_raw = args.height_raw;
330
+ if (args.weight !== undefined)
331
+ profUpdates.weight = args.weight;
332
+ if (args.hair_color !== undefined)
333
+ profUpdates.hair_color = args.hair_color;
334
+ if (args.suffix !== undefined)
335
+ profUpdates.suffix = args.suffix;
336
+ if (args.is_staff !== undefined)
337
+ profUpdates.is_staff = args.is_staff;
338
+ if (args.has_wallet_pass !== undefined)
339
+ profUpdates.has_wallet_pass = args.has_wallet_pass;
213
340
  if (args.is_wholesale_approved !== undefined)
214
341
  profUpdates.is_wholesale_approved = args.is_wholesale_approved;
215
342
  if (args.wholesale_tier !== undefined)
216
343
  profUpdates.wholesale_tier = args.wholesale_tier;
217
344
  if (args.wholesale_business_name !== undefined)
218
345
  profUpdates.wholesale_business_name = args.wholesale_business_name;
346
+ if (args.wholesale_business_type !== undefined)
347
+ profUpdates.wholesale_business_type = args.wholesale_business_type;
219
348
  if (args.wholesale_license_number !== undefined)
220
349
  profUpdates.wholesale_license_number = args.wholesale_license_number;
221
350
  if (args.wholesale_tax_id !== undefined)
222
351
  profUpdates.wholesale_tax_id = args.wholesale_tax_id;
352
+ if (args.wholesale_discount_percent !== undefined)
353
+ profUpdates.wholesale_discount_percent = args.wholesale_discount_percent;
354
+ if (args.wholesale_payment_terms !== undefined)
355
+ profUpdates.wholesale_payment_terms = args.wholesale_payment_terms;
356
+ if (args.wholesale_credit_limit !== undefined)
357
+ profUpdates.wholesale_credit_limit = args.wholesale_credit_limit;
358
+ if (args.wholesale_resale_certificate !== undefined)
359
+ profUpdates.wholesale_resale_certificate = args.wholesale_resale_certificate;
360
+ if (args.wholesale_notes !== undefined)
361
+ profUpdates.wholesale_notes = args.wholesale_notes;
223
362
  if (Object.keys(profUpdates).length > 0) {
224
363
  profUpdates.updated_at = new Date().toISOString();
225
364
  const { error: profErr } = await sb.from("store_customer_profiles")
@@ -227,9 +366,9 @@ export async function handleCustomers(sb, args, storeId) {
227
366
  if (profErr)
228
367
  return { success: false, error: `Failed to update profile: ${profErr.message}` };
229
368
  }
230
- const { data: updated } = await sb.from("v_store_customers")
369
+ const { data: updated } = await sb.from("v_segment_customers")
231
370
  .select("*").eq("id", custId).eq("store_id", sid).single();
232
- return { success: true, data: updated };
371
+ return { success: true, data: stripInternal(updated) };
233
372
  }
234
373
  // ---- FIND_DUPLICATES: identify potential duplicate customer accounts ----
235
374
  case "find_duplicates": {
@@ -389,7 +528,7 @@ export async function handleCustomers(sb, args, storeId) {
389
528
  }
390
529
  }
391
530
  const { data: merged } = await sb.from("v_store_customers").select("*").eq("id", primaryId).single();
392
- return { success: true, data: { merged_customer: merged, reassign_results: reassignResults } };
531
+ return { success: true, data: { merged_customer: stripInternal(merged), reassign_results: reassignResults } };
393
532
  }
394
533
  // ---- ADD_NOTE ----
395
534
  case "add_note": {
@@ -469,8 +608,154 @@ export async function handleCustomers(sb, args, storeId) {
469
608
  .order("last_seen_at", { ascending: false });
470
609
  return error ? { success: false, error: error.message } : { success: true, data };
471
610
  }
611
+ // ---- INVITE: create auth account for customer and send branded invite email ----
612
+ case "invite": {
613
+ const custId = args.customer_id;
614
+ if (!custId)
615
+ return { success: false, error: "customer_id is required" };
616
+ // Verify customer belongs to this store
617
+ const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
618
+ .select("id, user_id, store_id")
619
+ .eq("id", custId).eq("store_id", sid).single();
620
+ if (relErr || !rel)
621
+ return { success: false, error: "Customer not found for this store" };
622
+ // Get platform user + store name in parallel
623
+ const [{ data: pu }, { data: store }] = await Promise.all([
624
+ sb.from("platform_users").select("id, auth_id, email, first_name, last_name").eq("id", rel.user_id).single(),
625
+ sb.from("stores").select("store_name").eq("id", sid).single(),
626
+ ]);
627
+ if (!pu)
628
+ return { success: false, error: "Platform user not found" };
629
+ if (!pu.email)
630
+ return { success: false, error: "Customer has no email address — add one first" };
631
+ const storeName = store?.store_name || "our platform";
632
+ const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ") || "there";
633
+ const portalUrl = args.portal_url || args.redirect_to || null;
634
+ // If already has an auth account, just resend the email with a new magic link
635
+ let authId = pu.auth_id;
636
+ if (!authId) {
637
+ // Create auth account silently (no Supabase built-in email)
638
+ const { data: newUser, error: createErr } = await sb.auth.admin.createUser({
639
+ email: pu.email,
640
+ email_confirm: true, // skip verification — store is vouching for this email
641
+ user_metadata: {
642
+ first_name: pu.first_name,
643
+ last_name: pu.last_name,
644
+ invited_by_store: sid,
645
+ },
646
+ });
647
+ if (createErr)
648
+ return { success: false, error: `Account creation failed: ${createErr.message}` };
649
+ authId = newUser.user.id;
650
+ // Link auth account to platform user
651
+ await sb.from("platform_users").update({ auth_id: authId }).eq("id", pu.id);
652
+ }
653
+ // Generate a magic link for the customer to set up / log in
654
+ const { data: linkData, error: linkErr } = await sb.auth.admin.generateLink({
655
+ type: "magiclink",
656
+ email: pu.email,
657
+ options: {
658
+ ...(portalUrl ? { redirectTo: portalUrl } : {}),
659
+ },
660
+ });
661
+ if (linkErr)
662
+ return { success: false, error: `Link generation failed: ${linkErr.message}` };
663
+ const magicLink = linkData?.properties?.action_link || "";
664
+ // Send branded invite email through our email system
665
+ const emailResult = await handleEmail(sb, {
666
+ action: "send_template",
667
+ to: pu.email,
668
+ template: "customer-portal-invite",
669
+ template_data: {
670
+ customer_name: customerName,
671
+ store_name: storeName,
672
+ magic_link: magicLink,
673
+ portal_url: portalUrl || magicLink,
674
+ },
675
+ }, sid);
676
+ return {
677
+ success: true,
678
+ data: {
679
+ email: pu.email,
680
+ name: customerName,
681
+ invited: true,
682
+ email_sent: emailResult.success,
683
+ ...(emailResult.success ? {} : { email_error: emailResult.error }),
684
+ },
685
+ };
686
+ }
687
+ // ---- LOYALTY_HISTORY: transaction history for a customer ----
688
+ case "loyalty_history": {
689
+ const custId = args.customer_id;
690
+ if (!custId)
691
+ return { success: false, error: "customer_id is required" };
692
+ let q = sb.from("loyalty_transactions")
693
+ .select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
694
+ .eq("customer_id", custId)
695
+ .order("created_at", { ascending: false });
696
+ if (args.transaction_type)
697
+ q = q.eq("transaction_type", args.transaction_type);
698
+ q = q.limit(args.limit || 50);
699
+ const { data, error } = await q;
700
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
701
+ }
702
+ // ---- ADJUST_LOYALTY: award or deduct loyalty points via RPC ----
703
+ case "adjust_loyalty": {
704
+ const custId = args.customer_id;
705
+ const points = args.points;
706
+ const reason = args.reason;
707
+ if (!custId || points === undefined || !reason)
708
+ return { success: false, error: "customer_id, points, and reason are required" };
709
+ const { data, error } = await sb.rpc("adjust_customer_loyalty_points", {
710
+ p_customer_id: custId,
711
+ p_points_change: points,
712
+ p_reason: reason,
713
+ });
714
+ if (error)
715
+ return { success: false, error: error.message };
716
+ const { data: updated } = await sb.from("store_customer_profiles")
717
+ .select("loyalty_points, loyalty_tier, lifetime_points_earned")
718
+ .eq("relationship_id", custId).maybeSingle();
719
+ return { success: true, data: { rpc_result: data, ...updated } };
720
+ }
721
+ // ---- SEGMENTS: list all customer segments for the store ----
722
+ case "segments": {
723
+ let q = sb.from("customer_segments")
724
+ .select("id, name, ai_description, type, segment_rules, filter_criteria, customer_count, is_active, color, icon, targeting_tips, created_at")
725
+ .eq("store_id", sid)
726
+ .order("customer_count", { ascending: false });
727
+ if (args.limit)
728
+ q = q.limit(args.limit);
729
+ const { data, error } = await q;
730
+ return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
731
+ }
732
+ // ---- SEGMENT_MEMBERS: list customers in a specific segment ----
733
+ case "segment_members": {
734
+ const segId = args.segment_id;
735
+ if (!segId)
736
+ return { success: false, error: "segment_id is required" };
737
+ const { data, error } = await sb.from("customer_segment_memberships")
738
+ .select("added_at, customer:v_segment_customers!customer_id(*)")
739
+ .eq("segment_id", segId)
740
+ .order("added_at", { ascending: false })
741
+ .limit(args.limit || 50);
742
+ if (error)
743
+ return { success: false, error: error.message };
744
+ const members = (data || []).map((r) => ({ added_at: r.added_at, ...r.customer }))
745
+ .filter((m) => m.store_id === sid);
746
+ return { success: true, count: members.length, data: stripInternalArray(members) };
747
+ }
748
+ // ---- REFRESH_METRICS: recalculate all customer metrics for the store ----
749
+ case "refresh_metrics": {
750
+ const start = Date.now();
751
+ const { data, error } = await sb.rpc("refresh_customer_metrics", { p_store_id: sid });
752
+ const elapsed = Date.now() - start;
753
+ if (error)
754
+ return { success: false, error: error.message };
755
+ return { success: true, data: { result: data, elapsed_ms: elapsed, message: `Customer metrics refreshed in ${elapsed}ms` } };
756
+ }
472
757
  default:
473
- return { success: false, error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, find_duplicates, merge, add_note, notes, activity, orders, link_channel_identity, get_channel_identities` };
758
+ return { success: false, error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, invite, find_duplicates, merge, add_note, notes, activity, orders, loyalty_history, adjust_loyalty, segments, segment_members, refresh_metrics, link_channel_identity, get_channel_identities` };
474
759
  }
475
760
  }
476
761
  export async function handleOrders(sb, args, storeId) {
@@ -45,8 +45,8 @@ function productToText(product) {
45
45
  parts.push(String(product.short_description));
46
46
  if (product.description)
47
47
  parts.push(String(product.description));
48
- if (product.field_values && typeof product.field_values === "object") {
49
- const fv = product.field_values;
48
+ if (product.custom_fields && typeof product.custom_fields === "object") {
49
+ const fv = product.custom_fields;
50
50
  for (const [key, val] of Object.entries(fv)) {
51
51
  if (val !== null && val !== undefined && val !== "") {
52
52
  parts.push(`${key}: ${String(val)}`);
@@ -117,7 +117,7 @@ export async function handleEmbeddings(sb, args, storeId) {
117
117
  case "index_products": {
118
118
  const { data: products, error: fetchErr } = await sb
119
119
  .from("products")
120
- .select("id, name, description, short_description, field_values")
120
+ .select("id, name, description, short_description, custom_fields")
121
121
  .eq("store_id", sid);
122
122
  if (fetchErr)
123
123
  return { success: false, error: fetchErr.message };