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,6 +1,9 @@
1
1
  import { sanitizeFilterValue, escapeCSV, fillTemplate, groupBy } from "../lib/utils.js";
2
- import { generatePdfFromHtml } from "./browser.js";
3
2
  import { validateUrl } from "../lib/ssrf-guard.js";
3
+ import { applyGenerationRules, generateCannabinoidData, applyCalculations, runValidation, generateFullPanelData, } from "../lib/pdf-renderer.js";
4
+ import { renderLayoutToPdf, renderHtmlToPdf, renderLabelToPdf } from "../lib/react-pdf-layout.js";
5
+ import { renderCOAToPdf } from "../lib/coa-renderer.js";
6
+ import QRCode from "qrcode";
4
7
  const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
5
8
  const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
6
9
  const MAX_ATTACHMENT_COUNT = 10;
@@ -301,12 +304,12 @@ export async function handleDocuments(sb, args, storeId) {
301
304
  const storagePath = `${sid}/${fileName}`;
302
305
  let uploadBuffer;
303
306
  if (docType === "pdf") {
304
- // PDF generation from HTML content via Playwright
307
+ // PDF generation from HTML content via React-PDF
305
308
  const htmlContent = args.html || args.content;
306
309
  if (!htmlContent)
307
310
  return { success: false, error: "html or content (HTML string) is required for PDF documents" };
308
311
  try {
309
- const pdfBuffer = await generatePdfFromHtml(htmlContent, {
312
+ const pdfBuffer = await renderHtmlToPdf(htmlContent, {
310
313
  format: args.format || "A4",
311
314
  landscape: args.landscape || false,
312
315
  });
@@ -530,10 +533,10 @@ export async function handleDocuments(sb, args, storeId) {
530
533
  return { success: true, data: { count: data?.length || 0, stores: data } };
531
534
  }
532
535
  case "list_profiles": {
533
- const { data, error } = await sb.from("document_templates")
534
- .select("id, name, description, document_type, headers, schema, created_at")
536
+ const { data, error } = await sb.from("document_profiles")
537
+ .select("id, name, category, sample_type, config, template_id, is_active, created_at")
535
538
  .eq("is_active", true)
536
- .or(`store_id.eq.${sid},store_id.is.null`)
539
+ .eq("owner_store_id", sid)
537
540
  .order("name");
538
541
  if (error)
539
542
  return { success: false, error: error.message };
@@ -541,10 +544,10 @@ export async function handleDocuments(sb, args, storeId) {
541
544
  success: true,
542
545
  data: {
543
546
  count: data?.length || 0,
544
- profiles: (data || []).map(t => ({
545
- id: t.id, name: t.name, description: t.description,
546
- type: t.document_type, headers: t.headers,
547
- fields: t.schema?.length || 0,
547
+ profiles: (data || []).map(p => ({
548
+ id: p.id, name: p.name, category: p.category,
549
+ sample_type: p.sample_type, template_id: p.template_id,
550
+ has_config: Object.keys(p.config || {}).length > 0,
548
551
  })),
549
552
  },
550
553
  };
@@ -582,7 +585,437 @@ export async function handleDocuments(sb, args, storeId) {
582
585
  const succeeded = results.filter(r => !r.error).length;
583
586
  return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
584
587
  }
588
+ // ================================================================
589
+ // PDF TEMPLATE SYSTEM — full layout rendering, calculations, profiles
590
+ // ================================================================
591
+ case "generate_pdf": {
592
+ const templateId = args.template_id;
593
+ const templateSlug = args.template_slug;
594
+ if (!templateId && !templateSlug)
595
+ return { success: false, error: "template_id or template_slug required" };
596
+ // 1. Load pdf_template
597
+ let tplQuery = sb.from("pdf_templates").select("*").eq("is_active", true);
598
+ if (templateId)
599
+ tplQuery = tplQuery.eq("id", templateId);
600
+ else
601
+ tplQuery = tplQuery.eq("slug", templateSlug);
602
+ tplQuery = tplQuery.or(`store_id.eq.${sid},store_id.is.null`);
603
+ const { data: tpl, error: tplErr } = await tplQuery.limit(1).single();
604
+ if (tplErr || !tpl)
605
+ return { success: false, error: `Template not found: ${templateId || templateSlug}` };
606
+ // 2. Optionally load document_profile + its client store
607
+ let profileConfig = {};
608
+ let profileConstants = {};
609
+ let profileClientData = {};
610
+ let resolvedClientStoreId = null;
611
+ if (args.profile_id) {
612
+ const { data: profile } = await sb.from("document_profiles")
613
+ .select("*").eq("id", args.profile_id).eq("is_active", true).single();
614
+ if (profile) {
615
+ const cfg = (profile.config || {});
616
+ profileConfig = cfg;
617
+ profileConstants = (cfg.constants || {});
618
+ resolvedClientStoreId = profile.client_store_id || null;
619
+ // Load client info from the profile's linked store
620
+ if (profile.client_store_id) {
621
+ const { data: clientStore } = await sb.from("stores")
622
+ .select("store_name, legal_name, address, city, state, zip, distributor_license_number, phone, email")
623
+ .eq("id", profile.client_store_id).single();
624
+ if (clientStore) {
625
+ profileClientData = {
626
+ clientName: clientStore.legal_name || clientStore.store_name,
627
+ clientAddress: [clientStore.address, clientStore.city, clientStore.state, clientStore.zip].filter(Boolean).join(", ") || null,
628
+ licenseNumber: clientStore.distributor_license_number || null,
629
+ clientPhone: clientStore.phone || null,
630
+ clientEmail: clientStore.email || null,
631
+ };
632
+ }
633
+ }
634
+ // Use profile name as sampleName if not already set
635
+ if (profile.name && !profileConstants.sampleName) {
636
+ profileClientData.sampleName = profile.name;
637
+ }
638
+ if (profile.sample_type) {
639
+ profileClientData.sampleType = profile.sample_type;
640
+ }
641
+ if (profile.default_size) {
642
+ profileClientData.sampleSize = profile.default_size;
643
+ }
644
+ }
645
+ }
646
+ // 3. Optionally load customer (via store_customer_profiles + platform_users) — overrides profile client
647
+ let clientData = {};
648
+ if (args.customer_id) {
649
+ const { data: rel } = await sb.from("user_creation_relationships")
650
+ .select("id, store_id, platform_users!inner(first_name, last_name, email, phone)")
651
+ .eq("id", args.customer_id).single();
652
+ if (rel) {
653
+ const pu = rel.platform_users;
654
+ const { data: prof } = await sb.from("store_customer_profiles")
655
+ .select("street_address, city, state, postal_code, drivers_license_number, medical_card_number")
656
+ .eq("relationship_id", rel.id).maybeSingle();
657
+ clientData = {
658
+ clientName: [pu.first_name, pu.last_name].filter(Boolean).join(" "),
659
+ clientEmail: pu.email,
660
+ clientPhone: pu.phone,
661
+ clientAddress: prof ? [prof.street_address, prof.city, prof.state, prof.postal_code].filter(Boolean).join(", ") : null,
662
+ clientLicense: prof?.drivers_license_number || null,
663
+ clientMedicalCard: prof?.medical_card_number || null,
664
+ };
665
+ }
666
+ }
667
+ // 4. Merge data: constants ← profile config ← profile client ← explicit client ← user data
668
+ const tplConstants = (tpl.constants || {});
669
+ const mergedConstants = { ...tplConstants, ...profileConstants };
670
+ const cannabinoidCfg = (profileConfig.cannabinoids ?? undefined);
671
+ const { cannabinoids: _cfgRanges, ...profileDataOnly } = profileConfig;
672
+ let mergedData = {
673
+ ...mergedConstants,
674
+ ...profileDataOnly,
675
+ ...profileClientData,
676
+ ...clientData,
677
+ ...(args.data || {}),
678
+ date: new Date().toISOString().slice(0, 10),
679
+ approvalDate: new Date().toISOString().slice(0, 10),
680
+ };
681
+ // 5. Apply generation rules — auto-fill sampleId, dates, etc.
682
+ mergedData = applyGenerationRules(tpl.generation_rules, mergedData);
683
+ // 6. Generate cannabinoid data if profile has cannabinoid config
684
+ if (cannabinoidCfg && !mergedData.cannabinoids) {
685
+ mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
686
+ }
687
+ // 7. Apply calculations
688
+ mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
689
+ // Also run per-row calculations if cannabinoids exist
690
+ if (Array.isArray(mergedData.cannabinoids)) {
691
+ const rows = mergedData.cannabinoids;
692
+ for (const row of rows) {
693
+ const rowCtx = { ...mergedConstants, ...row };
694
+ const rowCalcs = (tpl.row_calculations || tpl.calculations);
695
+ if (rowCalcs) {
696
+ const computed = applyCalculations(rowCalcs, mergedConstants, rowCtx);
697
+ Object.assign(row, computed);
698
+ }
699
+ }
700
+ }
701
+ // 7b. Generate full panel data if requested (safety tests, pesticides, solvents)
702
+ if (mergedData.fullPanel && !mergedData.microbialResults) {
703
+ const tplConstants = (tpl.constants || {});
704
+ const fullPanel = generateFullPanelData(tplConstants);
705
+ mergedData.microbialResults = fullPanel.microbialResults;
706
+ mergedData.heavyMetalsResults = fullPanel.heavyMetalsResults;
707
+ mergedData.mycotoxinResults = fullPanel.mycotoxinResults;
708
+ mergedData.pesticidesCat1 = fullPanel.pesticidesCat1;
709
+ mergedData.pesticidesCat2 = fullPanel.pesticidesCat2;
710
+ mergedData.testsResidualSolvents = fullPanel.residualSolventsResults;
711
+ mergedData.totalPages = 5;
712
+ mergedData.complianceResults = mergedData.complianceResults || {
713
+ heavyMetals: { status: "PASS" }, microbial: { status: "PASS" },
714
+ pesticides: { status: "PASS" }, mycotoxins: { status: "PASS" },
715
+ residualSolvents: { status: "PASS" },
716
+ };
717
+ }
718
+ // 7c. Generate QR code for COA templates — links to Quantix COA landing page
719
+ if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
720
+ const productSlug = (mergedData.sampleName || "certificate")
721
+ .toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
722
+ const coaUrl = `https://quantixanalytics.com/coa/${sid}/${productSlug}`;
723
+ try {
724
+ mergedData.qrCodeDataUrl = await QRCode.toDataURL(coaUrl, { width: 120, margin: 1 });
725
+ }
726
+ catch { /* skip QR if generation fails */ }
727
+ }
728
+ // 8. Validate
729
+ const validationResults = runValidation(tpl.validation_rules, mergedData);
730
+ const errors = validationResults.filter(v => v.severity === "error" && !v.passed);
731
+ const warnings = validationResults.filter(v => v.severity === "warning" && !v.passed);
732
+ if (errors.length > 0 && !args.force) {
733
+ return {
734
+ success: false,
735
+ error: `Validation failed: ${errors.map(e => e.message).join("; ")}`,
736
+ data: { errors, warnings },
737
+ };
738
+ }
739
+ // 9-10. Render PDF — all templates use React-PDF
740
+ let pdfBuffer;
741
+ try {
742
+ if (tpl.slug?.startsWith("cannabis-coa")) {
743
+ // COA templates use dedicated renderer
744
+ pdfBuffer = await renderCOAToPdf(mergedData);
745
+ }
746
+ else {
747
+ // Detect label layout vs generic layout
748
+ const layout = tpl.layout;
749
+ const isLabel = layout?.version === 1 && Array.isArray(layout?.elements) && layout.elements[0]?.field;
750
+ if (isLabel) {
751
+ const items = Array.isArray(mergedData.items)
752
+ ? mergedData.items
753
+ : [mergedData];
754
+ pdfBuffer = await renderLabelToPdf(layout, tpl.page_config, items);
755
+ }
756
+ else {
757
+ pdfBuffer = await renderLayoutToPdf(tpl.layout, tpl.page_config, tpl.styles, mergedData);
758
+ }
759
+ }
760
+ }
761
+ catch (err) {
762
+ return { success: false, error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}` };
763
+ }
764
+ // 11. Upload to Supabase storage
765
+ const sampleName = mergedData.sampleName || mergedData.productName || "";
766
+ const docName = args.name || (sampleName ? `${sampleName} COA` : fillTemplate(tpl.name || "Document", mergedData));
767
+ const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
768
+ const fileName = `${safeName}_${Date.now()}.pdf`;
769
+ const storagePath = `${sid}/${fileName}`;
770
+ const bucketName = "store-documents";
771
+ const { error: uploadErr } = await sb.storage
772
+ .from(bucketName)
773
+ .upload(storagePath, new Uint8Array(pdfBuffer), { contentType: "application/pdf", upsert: true });
774
+ if (uploadErr)
775
+ return { success: false, error: `Upload failed: ${uploadErr.message}` };
776
+ const { data: urlData } = sb.storage.from(bucketName).getPublicUrl(storagePath);
777
+ // 12. Build COA metadata for the Quantix landing page
778
+ const coaMetadata = {
779
+ size_bytes: pdfBuffer.length,
780
+ from_pdf_template: true,
781
+ template_version: tpl.version,
782
+ };
783
+ if (tpl.slug?.startsWith("cannabis-coa")) {
784
+ coaMetadata.sample_name = sampleName;
785
+ coaMetadata.sample_id = mergedData.sampleId || null;
786
+ coaMetadata.sample_type = mergedData.sampleType || mergedData.strain || null;
787
+ coaMetadata.sample_size = mergedData.sampleSize || null;
788
+ coaMetadata.batch_number = mergedData.batchId || null;
789
+ coaMetadata.lab_name = mergedData.labName || null;
790
+ coaMetadata.lab_contact = mergedData.labContact || null;
791
+ coaMetadata.lab_website = mergedData.labWebsite || null;
792
+ coaMetadata.lab_director = mergedData.labDirector || null;
793
+ coaMetadata.director_title = mergedData.directorTitle || null;
794
+ coaMetadata.logo_url = mergedData.logoUrl || null;
795
+ coaMetadata.signature_url = mergedData.signatureUrl || null;
796
+ coaMetadata.client_name = mergedData.clientName || null;
797
+ coaMetadata.client_address = mergedData.clientAddress || null;
798
+ coaMetadata.date_collected = mergedData.dateCollected || null;
799
+ coaMetadata.date_received = mergedData.dateReceived || null;
800
+ coaMetadata.date_tested = mergedData.dateTested || null;
801
+ coaMetadata.date_reported = mergedData.dateReported || null;
802
+ coaMetadata.status = "Pass";
803
+ coaMetadata.thc_total = mergedData.totalTHC || null;
804
+ coaMetadata.cbd_total = mergedData.totalCBD || null;
805
+ coaMetadata.cannabinoids_total = mergedData.totalCannabinoids || null;
806
+ coaMetadata.moisture = mergedData.moisture || null;
807
+ // Detailed cannabinoid data for the interactive landing page
808
+ const cannabinoids = mergedData.cannabinoids;
809
+ if (Array.isArray(cannabinoids)) {
810
+ coaMetadata.cannabinoids = Object.fromEntries(cannabinoids.filter((c) => typeof c.percentWeight === "number" && c.percentWeight > 0)
811
+ .map((c) => [c.name, c.percentWeight]));
812
+ coaMetadata.cannabinoids_detailed = cannabinoids.map((c) => ({
813
+ name: c.name, percent: c.percentWeight, mg_per_g: c.mgPerG, lod: c.lod, loq: c.loq, result: String(c.result ?? c.percentWeight),
814
+ }));
815
+ }
816
+ // Safety test panels
817
+ if (mergedData.fullPanel) {
818
+ coaMetadata.test_panels = {
819
+ cannabinoids: true, microbial: true, heavy_metals: true,
820
+ mycotoxins: true, pesticides: true, residual_solvents: true,
821
+ };
822
+ coaMetadata.safety_tests = {
823
+ microbial: "Pass", heavy_metals: "Pass", mycotoxins: "Pass",
824
+ pesticides: "Pass", residual_solvents: "Pass",
825
+ };
826
+ }
827
+ }
828
+ // 13. Insert store_documents record
829
+ const { data: record, error: insertErr } = await sb.from("store_documents").insert({
830
+ store_id: sid,
831
+ document_type: tpl.document_type || "pdf",
832
+ file_name: fileName,
833
+ file_url: urlData.publicUrl,
834
+ file_size: pdfBuffer.length,
835
+ file_type: "application/pdf",
836
+ document_name: docName,
837
+ source_name: "PDF Template Engine",
838
+ document_date: new Date().toISOString().split("T")[0],
839
+ customer_id: args.customer_id || null,
840
+ client_store_id: resolvedClientStoreId,
841
+ data: { template_id: tpl.id, template_slug: tpl.slug, profile_id: args.profile_id || null },
842
+ metadata: coaMetadata,
843
+ }).select("id, document_name, file_url, created_at").single();
844
+ if (insertErr)
845
+ return { success: false, error: insertErr.message };
846
+ return {
847
+ success: true,
848
+ data: {
849
+ id: record.id,
850
+ name: record.document_name,
851
+ url: record.file_url,
852
+ template: tpl.name,
853
+ size: pdfBuffer.length,
854
+ warnings: warnings.map(w => w.message),
855
+ },
856
+ };
857
+ }
858
+ case "list_pdf_templates": {
859
+ let query = sb.from("pdf_templates")
860
+ .select("id, slug, name, description, document_type, version, created_at")
861
+ .eq("is_active", true)
862
+ .or(`store_id.eq.${sid},store_id.is.null`)
863
+ .order("name");
864
+ if (args.document_type)
865
+ query = query.eq("document_type", args.document_type);
866
+ if (args.limit)
867
+ query = query.limit(args.limit);
868
+ const { data, error } = await query;
869
+ if (error)
870
+ return { success: false, error: error.message };
871
+ return {
872
+ success: true,
873
+ data: {
874
+ count: data?.length || 0,
875
+ templates: (data || []).map(t => ({
876
+ id: t.id, slug: t.slug, name: t.name,
877
+ description: t.description, type: t.document_type, version: t.version,
878
+ })),
879
+ },
880
+ };
881
+ }
882
+ case "list_document_profiles": {
883
+ let query = sb.from("document_profiles")
884
+ .select("id, name, category, sample_type, default_size, client_store_id, template_id, created_at")
885
+ .eq("is_active", true)
886
+ .eq("owner_store_id", sid)
887
+ .order("name");
888
+ if (args.template_id)
889
+ query = query.eq("template_id", args.template_id);
890
+ if (args.category)
891
+ query = query.eq("category", args.category);
892
+ if (args.limit)
893
+ query = query.limit(args.limit);
894
+ const { data, error } = await query;
895
+ if (error)
896
+ return { success: false, error: error.message };
897
+ return {
898
+ success: true,
899
+ data: {
900
+ count: data?.length || 0,
901
+ profiles: (data || []).map(p => ({
902
+ id: p.id, name: p.name, category: p.category,
903
+ sample_type: p.sample_type, default_size: p.default_size,
904
+ client_store_id: p.client_store_id,
905
+ })),
906
+ },
907
+ };
908
+ }
909
+ case "list_clients":
910
+ case "list_customers": {
911
+ let query = sb.from("v_store_customers")
912
+ .select("id, first_name, last_name, email, phone, loyalty_tier, total_orders, is_active, created_at")
913
+ .eq("store_id", sid)
914
+ .order("created_at", { ascending: false });
915
+ if (args.query) {
916
+ const term = `%${sanitizeFilterValue(String(args.query).trim())}%`;
917
+ query = query.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term}`);
918
+ }
919
+ if (args.limit)
920
+ query = query.limit(args.limit);
921
+ const { data, error } = await query;
922
+ if (error)
923
+ return { success: false, error: error.message };
924
+ return {
925
+ success: true,
926
+ data: {
927
+ count: data?.length || 0,
928
+ customers: (data || []).map(c => ({
929
+ id: c.id, name: [c.first_name, c.last_name].filter(Boolean).join(" "),
930
+ email: c.email, phone: c.phone, loyalty_tier: c.loyalty_tier,
931
+ total_orders: c.total_orders, is_active: c.is_active,
932
+ })),
933
+ },
934
+ };
935
+ }
936
+ case "deliver_documents": {
937
+ // Batch-assign documents to a customer and optionally send notification email
938
+ const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
939
+ const customerId = args.customer_id;
940
+ if (!docIds.length)
941
+ return { success: false, error: "document_ids (array) or document_id (string) is required" };
942
+ if (!customerId)
943
+ return { success: false, error: "customer_id is required" };
944
+ // Verify customer belongs to this store
945
+ const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
946
+ .select("id, store_id, platform_users!inner(first_name, last_name, email)")
947
+ .eq("id", customerId).eq("store_id", sid).single();
948
+ if (relErr || !rel)
949
+ return { success: false, error: "Customer not found for this store" };
950
+ const pu = rel.platform_users;
951
+ const customerEmail = pu.email;
952
+ const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ");
953
+ // Assign all documents to the customer
954
+ const { error: updateErr } = await sb.from("store_documents")
955
+ .update({ customer_id: customerId })
956
+ .in("id", docIds)
957
+ .eq("store_id", sid);
958
+ if (updateErr)
959
+ return { success: false, error: `Failed to assign documents: ${updateErr.message}` };
960
+ // Get document names for the email
961
+ const { data: docs } = await sb.from("store_documents")
962
+ .select("id, document_name, file_url")
963
+ .in("id", docIds);
964
+ let emailSent = false;
965
+ if (!args.skip_email && customerEmail) {
966
+ const portalUrl = args.portal_url || null;
967
+ const docList = (docs || []).map(d => `<li><a href="${d.file_url}">${d.document_name || "Document"}</a></li>`).join("");
968
+ // Get store name for template
969
+ const { data: store } = await sb.from("stores").select("store_name").eq("id", sid).single();
970
+ const emailResult = await handleEmail(sb, {
971
+ action: "send_template",
972
+ to: customerEmail,
973
+ template: "documents-delivered",
974
+ template_data: {
975
+ customer_name: customerName || "there",
976
+ store_name: store?.store_name || "",
977
+ document_count: String(docIds.length),
978
+ document_list: docList,
979
+ portal_url: portalUrl || "",
980
+ },
981
+ }, storeId);
982
+ emailSent = emailResult.success;
983
+ }
984
+ return {
985
+ success: true,
986
+ data: {
987
+ document_ids: docIds,
988
+ customer_id: customerId,
989
+ customer_email: customerEmail,
990
+ email_sent: emailSent,
991
+ count: docIds.length,
992
+ },
993
+ };
994
+ }
995
+ case "list_customer_documents": {
996
+ const customerId = args.customer_id;
997
+ if (!customerId)
998
+ return { success: false, error: "customer_id is required" };
999
+ let query = sb.from("store_documents")
1000
+ .select("id, document_name, file_url, file_type, created_at, thumbnail_url, metadata, document_type")
1001
+ .eq("store_id", sid)
1002
+ .eq("customer_id", customerId)
1003
+ .eq("is_active", true)
1004
+ .order("created_at", { ascending: false });
1005
+ if (args.limit)
1006
+ query = query.limit(args.limit);
1007
+ const { data, error } = await query;
1008
+ if (error)
1009
+ return { success: false, error: error.message };
1010
+ return {
1011
+ success: true,
1012
+ data: {
1013
+ count: data?.length || 0,
1014
+ documents: data || [],
1015
+ },
1016
+ };
1017
+ }
585
1018
  default:
586
- return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate` };
1019
+ return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, list_customers, deliver_documents, list_customer_documents` };
587
1020
  }
588
1021
  }
@@ -55,7 +55,7 @@ const { products, store, categories } = useStore();
55
55
  Product shape:
56
56
  { id, name, sku, description, short_description, status, type,
57
57
  cost_price, wholesale_price, stock_quantity, featured,
58
- field_values: { thca_percentage, strain_type, terpenes, effects, cbd_total, ... },
58
+ custom_fields: { thca_percentage, strain_type, terpenes, effects, cbd_total, ... },
59
59
  pricing_data: { mode, tiers: [{ label, quantity, unit, price }] },
60
60
  category: { id, name } }
61
61
 
@@ -4,14 +4,12 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
4
4
  error: string;
5
5
  count?: undefined;
6
6
  data?: undefined;
7
- note?: undefined;
8
7
  linked?: undefined;
9
8
  } | {
10
9
  success: boolean;
11
10
  count: number;
12
11
  data: {
13
12
  id: any;
14
- platform_user_id: any;
15
13
  first_name: any;
16
14
  last_name: any;
17
15
  email: any;
@@ -22,22 +20,30 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
22
20
  total_orders: any;
23
21
  lifetime_value: any;
24
22
  is_active: any;
23
+ rfm_segment: any;
24
+ is_vip_customer: any;
25
+ is_at_risk: any;
26
+ is_churned: any;
27
+ reorder_due: any;
28
+ ai_churn_risk: any;
29
+ days_since_last_order: any;
30
+ engagement_score: any;
31
+ age_bracket: any;
25
32
  created_at: any;
26
33
  }[];
27
34
  error?: undefined;
28
- note?: undefined;
29
35
  linked?: undefined;
30
36
  } | {
37
+ warning?: string | undefined;
38
+ note?: string | undefined;
31
39
  success: boolean;
32
- data: any;
40
+ data: Record<string, unknown> | null;
33
41
  error?: undefined;
34
42
  count?: undefined;
35
- note?: undefined;
36
43
  linked?: undefined;
37
44
  } | {
38
45
  success: boolean;
39
46
  data: any;
40
- note: string;
41
47
  error?: undefined;
42
48
  count?: undefined;
43
49
  linked?: undefined;
@@ -57,7 +63,6 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
57
63
  created_at: any;
58
64
  }[];
59
65
  error?: undefined;
60
- note?: undefined;
61
66
  linked?: undefined;
62
67
  } | {
63
68
  success: boolean;
@@ -69,7 +74,48 @@ export declare function handleCustomers(sb: SupabaseClient, args: Record<string,
69
74
  error?: undefined;
70
75
  count?: undefined;
71
76
  data?: undefined;
72
- note?: undefined;
77
+ } | {
78
+ success: boolean;
79
+ count: number;
80
+ data: {
81
+ id: any;
82
+ points: any;
83
+ transaction_type: any;
84
+ reference_type: any;
85
+ reference_id: any;
86
+ description: any;
87
+ balance_before: any;
88
+ balance_after: any;
89
+ expires_at: any;
90
+ created_at: any;
91
+ }[];
92
+ error?: undefined;
93
+ linked?: undefined;
94
+ } | {
95
+ success: boolean;
96
+ count: number;
97
+ data: {
98
+ id: any;
99
+ name: any;
100
+ ai_description: any;
101
+ type: any;
102
+ segment_rules: any;
103
+ filter_criteria: any;
104
+ customer_count: any;
105
+ is_active: any;
106
+ color: any;
107
+ icon: any;
108
+ targeting_tips: any;
109
+ created_at: any;
110
+ }[];
111
+ error?: undefined;
112
+ linked?: undefined;
113
+ } | {
114
+ success: boolean;
115
+ count: number;
116
+ data: Record<string, unknown>[];
117
+ error?: undefined;
118
+ linked?: undefined;
73
119
  }>;
74
120
  export declare function handleOrders(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
75
121
  success: boolean;