@infuro/cms-core 1.0.31 → 1.0.33

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.
package/dist/api.cjs CHANGED
@@ -1215,6 +1215,65 @@ function resolveVendorIdForContactCheck(scope, bodyOrRow) {
1215
1215
  return Number.isFinite(n) ? n : null;
1216
1216
  }
1217
1217
 
1218
+ // src/api/crud.ts
1219
+ var import_crypto = __toESM(require("crypto"), 1);
1220
+
1221
+ // src/plugins/llm/llm-agent-scope.ts
1222
+ var LLM_AGENT_SCOPE_CHATBOT = "chatbot";
1223
+ var LLM_AGENT_SCOPE_BLOG_CREATION = "blog_creation";
1224
+ var LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST = "social_media_post";
1225
+ var LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT = "email_intent_chatbot";
1226
+ var LLM_AGENT_SCOPE_BLOG_METADATA = "blog_metadata";
1227
+ var LLM_AGENT_SCOPES = [
1228
+ LLM_AGENT_SCOPE_CHATBOT,
1229
+ LLM_AGENT_SCOPE_BLOG_CREATION,
1230
+ LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST,
1231
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
1232
+ LLM_AGENT_SCOPE_BLOG_METADATA
1233
+ ];
1234
+ var LLM_AGENT_DEFAULT_SLUG_BY_SCOPE = {
1235
+ [LLM_AGENT_SCOPE_CHATBOT]: "site-chat-assistant",
1236
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "blog-generator",
1237
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "blog-generator-social",
1238
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "email-intent-chatbot",
1239
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "blog-generator-metadata"
1240
+ };
1241
+ var LLM_AGENT_DEFAULT_NAME_BY_SCOPE = {
1242
+ [LLM_AGENT_SCOPE_CHATBOT]: "Site Chat Assistant",
1243
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "Blog Generator",
1244
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "Blog Generator (Social)",
1245
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "Chat Lead Intent Classifier",
1246
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "Blog Generator (Metadata)"
1247
+ };
1248
+ var SITE_CHAT_ASSISTANT_SLUG = LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
1249
+ var SITE_CHAT_ASSISTANT_NAME = LLM_AGENT_DEFAULT_NAME_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
1250
+ function isLlmAgentScope(value) {
1251
+ if (!value?.trim()) return false;
1252
+ return LLM_AGENT_SCOPES.includes(value.trim());
1253
+ }
1254
+ var BLOG_LLM_AGENT_SLUGS = [
1255
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_CREATION],
1256
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_METADATA],
1257
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]
1258
+ ];
1259
+
1260
+ // src/plugins/llm/llm-agent-scope-crud.ts
1261
+ async function validateLlmAgentScopeForWrite(dataSource, entityMap, scope, excludeId) {
1262
+ if (scope == null || scope === "") return null;
1263
+ if (typeof scope !== "string" || !isLlmAgentScope(scope)) {
1264
+ return `Invalid agent scope. Allowed: chatbot, blog_creation, social_media_post, email_intent_chatbot, blog_metadata.`;
1265
+ }
1266
+ const entity = entityMap.llm_agents;
1267
+ if (!entity) return null;
1268
+ const repo = dataSource.getRepository(entity);
1269
+ const existing = await repo.findOne({
1270
+ where: { scope, deleted: false }
1271
+ });
1272
+ if (!existing) return null;
1273
+ if (excludeId != null && existing.id === excludeId) return null;
1274
+ return `An active agent already uses scope "${scope}".`;
1275
+ }
1276
+
1218
1277
  // src/api/crud.ts
1219
1278
  var CRUD_LOG = "[cms-crud]";
1220
1279
  function logCrudClientError(op, detail) {
@@ -1431,6 +1490,7 @@ function buildListFilterAndFromSearchParams(repo, searchParams) {
1431
1490
  if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
1432
1491
  if (!isListStringColumn(col)) continue;
1433
1492
  if (Object.prototype.hasOwnProperty.call(and, name)) continue;
1493
+ if (name === "scope") continue;
1434
1494
  const raw = searchParams.get(name)?.trim();
1435
1495
  if (!raw) continue;
1436
1496
  and[name] = (0, import_typeorm2.ILike)(`%${raw}%`);
@@ -1460,6 +1520,10 @@ function buildExactListParamWhere(repo, searchParams) {
1460
1520
  extraWhere[name] = raw === "true";
1461
1521
  }
1462
1522
  }
1523
+ const scopeParam = searchParams.get("scope")?.trim();
1524
+ if (scopeParam && columnNames.has("scope")) {
1525
+ extraWhere.scope = scopeParam;
1526
+ }
1463
1527
  return extraWhere;
1464
1528
  }
1465
1529
  function mergeDeletedFalseWhere(repo, where) {
@@ -1671,6 +1735,12 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
1671
1735
  case "minAmount":
1672
1736
  return discountCompare(cartTotal, rule.comparisonOperator, ruleValue);
1673
1737
  case "quantity": {
1738
+ if (rule.subType === "productId" && rule.value?.productId) {
1739
+ const targetProductId = Number(rule.value.productId);
1740
+ const requiredQty = Number(rule.value.v);
1741
+ const productQty = cartLines.filter((l) => l.productId === targetProductId).reduce((s, l) => s + l.quantity, 0);
1742
+ return discountCompare(productQty, rule.comparisonOperator, requiredQty);
1743
+ }
1674
1744
  const totalQty = cartLines.reduce((s, l) => s + l.quantity, 0);
1675
1745
  return discountCompare(totalQty, rule.comparisonOperator, ruleValue);
1676
1746
  }
@@ -1734,6 +1804,10 @@ function discountExtractBuyProductId(nodes) {
1734
1804
  const n = Number(v);
1735
1805
  if (Number.isFinite(n) && n > 0) return n;
1736
1806
  }
1807
+ if (node.conditionType === "rule" && node.type === "quantity" && node.subType === "productId") {
1808
+ const n = Number(node.value?.productId);
1809
+ if (Number.isFinite(n) && n > 0) return n;
1810
+ }
1737
1811
  if (node.children?.length) {
1738
1812
  const found = discountExtractBuyProductId(node.children);
1739
1813
  if (found !== null) return found;
@@ -2444,6 +2518,25 @@ function createCrudHandler(dataSource, entityMap, options) {
2444
2518
  if (pe) return pe;
2445
2519
  return null;
2446
2520
  }
2521
+ async function tryAssignSingleVendorOnAdminCreate(resource, scope, persistBody) {
2522
+ if (!resourceUsesVendorScope(resource) || scope.type !== "all") return;
2523
+ const currentVendorId = Number(persistBody.vendorId);
2524
+ if (Number.isFinite(currentVendorId) && currentVendorId > 0) return;
2525
+ if (!entityMap.vendors) return;
2526
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
2527
+ const where = mergeDeletedFalseWhere(vendorRepo, {});
2528
+ const rows = await vendorRepo.find({
2529
+ where,
2530
+ order: { id: "ASC" },
2531
+ take: 2
2532
+ });
2533
+ if (rows.length === 1) {
2534
+ const onlyVendorId = Number(rows[0].id);
2535
+ if (Number.isFinite(onlyVendorId) && onlyVendorId > 0) {
2536
+ persistBody.vendorId = onlyVendorId;
2537
+ }
2538
+ }
2539
+ }
2447
2540
  return {
2448
2541
  async GET(req, resource) {
2449
2542
  const authError = await authz(req, resource, "read");
@@ -2959,6 +3052,7 @@ function createCrudHandler(dataSource, entityMap, options) {
2959
3052
  const couponCode = String(body.couponCode).trim().toUpperCase();
2960
3053
  const persistBody2 = pickColumnUpdates(repo2, { ...body, couponCode });
2961
3054
  const scopeDiscount = await resolveScope();
3055
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeDiscount, persistBody2);
2962
3056
  const vendorIdCheck2 = requireVendorIdForScopedCreate(
2963
3057
  resource,
2964
3058
  persistBody2,
@@ -3036,6 +3130,10 @@ function createCrudHandler(dataSource, entityMap, options) {
3036
3130
  });
3037
3131
  return json({ error: "Invalid request payload" }, { status: 400 });
3038
3132
  }
3133
+ if (resource === "llm_agents" && "scope" in persistBody) {
3134
+ const scopeErr = await validateLlmAgentScopeForWrite(dataSource, entityMap, persistBody.scope);
3135
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
3136
+ }
3039
3137
  if (resource === "products") {
3040
3138
  if ("sku" in persistBody) {
3041
3139
  const skuNorm = normalizeProductSku(persistBody.sku);
@@ -3151,6 +3249,7 @@ function createCrudHandler(dataSource, entityMap, options) {
3151
3249
  );
3152
3250
  }
3153
3251
  const scopeCreate = await resolveScope();
3252
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeCreate, persistBody);
3154
3253
  const vendorIdCheck = requireVendorIdForScopedCreate(resource, persistBody, scopeCreate, repo, body);
3155
3254
  if (!vendorIdCheck.ok) {
3156
3255
  return json({ error: vendorIdCheck.error }, { status: vendorIdCheck.status });
@@ -3193,6 +3292,7 @@ function createCrudHandler(dataSource, entityMap, options) {
3193
3292
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
3194
3293
  persistBody.contactId = contact.id;
3195
3294
  persistBody.orderNumber = `ORD00${randomPart}`;
3295
+ persistBody.qrToken = import_crypto.default.randomBytes(16).toString("hex");
3196
3296
  }
3197
3297
  created = await repo.save(repo.create(persistBody));
3198
3298
  if (resource === "orders") {
@@ -3961,6 +4061,15 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
3961
4061
  const t = updatePayload.type;
3962
4062
  if (t === "" || t === "none" || t == null) updatePayload.type = null;
3963
4063
  }
4064
+ if (resource === "llm_agents" && "scope" in updatePayload) {
4065
+ const scopeErr = await validateLlmAgentScopeForWrite(
4066
+ dataSource,
4067
+ entityMap,
4068
+ updatePayload.scope,
4069
+ numericId
4070
+ );
4071
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
4072
+ }
3964
4073
  if ((resource === "orders" || resource === "payments") && "contactId" in updatePayload && updatePayload.contactId != null && entityMap.vendor_customers) {
3965
4074
  const existingRow = await repo.findOne({
3966
4075
  where: { id: numericId }
@@ -4170,8 +4279,8 @@ function createForgotPasswordHandler(config) {
4170
4279
  const user = await userRepo.findOne({ where: { email }, select: ["email"] });
4171
4280
  const msg = "If an account exists with this email, you will receive a reset link shortly.";
4172
4281
  if (!user) return json({ message: msg }, { status: 200 });
4173
- const crypto2 = await import("crypto");
4174
- const token = crypto2.randomBytes(32).toString("hex");
4282
+ const crypto3 = await import("crypto");
4283
+ const token = crypto3.randomBytes(32).toString("hex");
4175
4284
  const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
4176
4285
  const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
4177
4286
  await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
@@ -4324,6 +4433,7 @@ var LlmAgent = class {
4324
4433
  id;
4325
4434
  name;
4326
4435
  slug;
4436
+ scope;
4327
4437
  systemInstruction;
4328
4438
  model;
4329
4439
  temperature;
@@ -4347,6 +4457,9 @@ __decorateClass([
4347
4457
  __decorateClass([
4348
4458
  (0, import_typeorm4.Column)("varchar")
4349
4459
  ], LlmAgent.prototype, "slug", 2);
4460
+ __decorateClass([
4461
+ (0, import_typeorm4.Column)("varchar", { nullable: true })
4462
+ ], LlmAgent.prototype, "scope", 2);
4350
4463
  __decorateClass([
4351
4464
  (0, import_typeorm4.Column)("text", { name: "system_instruction", default: "" })
4352
4465
  ], LlmAgent.prototype, "systemInstruction", 2);
@@ -4398,6 +4511,515 @@ function llmAgentToChatAgentOptions(agent) {
4398
4511
  };
4399
4512
  }
4400
4513
 
4514
+ // src/plugins/llm/chat-email-intent.ts
4515
+ var NONE_INTENT = "NONE";
4516
+ var CHAT_EMAIL_LOG = "[chat-email-tool]";
4517
+ function logChatEmail(step, data) {
4518
+ if (data && Object.keys(data).length > 0) {
4519
+ console.info(CHAT_EMAIL_LOG, step, data);
4520
+ } else {
4521
+ console.info(CHAT_EMAIL_LOG, step);
4522
+ }
4523
+ }
4524
+ function normalizeIntentKey(raw) {
4525
+ return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
4526
+ }
4527
+ function parseIntentsJson(raw) {
4528
+ if (!raw?.trim()) return null;
4529
+ try {
4530
+ const parsed = JSON.parse(raw);
4531
+ if (!Array.isArray(parsed)) return null;
4532
+ const out = [];
4533
+ for (const row of parsed) {
4534
+ if (!row || typeof row !== "object") continue;
4535
+ const o = row;
4536
+ const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
4537
+ const description = String(o.description ?? "").trim();
4538
+ const emailTo = String(o.emailTo ?? o.email ?? "").trim();
4539
+ if (!intent || !description || !emailTo) continue;
4540
+ out.push({ intent, description, emailTo });
4541
+ }
4542
+ return out.length > 0 ? out : null;
4543
+ } catch {
4544
+ return null;
4545
+ }
4546
+ }
4547
+ function dedupeIntents(intents) {
4548
+ const seen = /* @__PURE__ */ new Set();
4549
+ const out = [];
4550
+ for (const row of intents) {
4551
+ const key = normalizeIntentKey(row.intent);
4552
+ if (!key || seen.has(key)) continue;
4553
+ seen.add(key);
4554
+ out.push({
4555
+ intent: key,
4556
+ description: row.description.trim(),
4557
+ emailTo: row.emailTo.trim()
4558
+ });
4559
+ }
4560
+ return out;
4561
+ }
4562
+ function parseChatEmailToolSettings(map) {
4563
+ const fromJson = parseIntentsJson(map.emailIntents);
4564
+ const intents = dedupeIntents(fromJson ?? []);
4565
+ const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
4566
+ const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
4567
+ let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
4568
+ if (!classifierInstructions && (legacyPositive || legacyNegative)) {
4569
+ const parts = [];
4570
+ if (legacyPositive) parts.push(`Legacy positive signals:
4571
+ ${legacyPositive}`);
4572
+ if (legacyNegative) parts.push(`Do NOT assign an intent when:
4573
+ ${legacyNegative}`);
4574
+ classifierInstructions = parts.join("\n\n");
4575
+ }
4576
+ return {
4577
+ enabled: map.emailToolEnabled === "true",
4578
+ intents,
4579
+ classifierInstructions,
4580
+ toolPrompt: map.emailToolPrompt ?? ""
4581
+ };
4582
+ }
4583
+ var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
4584
+ function parseEmailToolObject(raw) {
4585
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4586
+ const o = raw;
4587
+ const intentsRaw = o.intents;
4588
+ let intents;
4589
+ if (Array.isArray(intentsRaw)) {
4590
+ const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
4591
+ if (parsed?.length) intents = parsed;
4592
+ }
4593
+ return {
4594
+ enabled: o.enabled === true,
4595
+ classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
4596
+ toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
4597
+ intents
4598
+ };
4599
+ }
4600
+ function parseEmailToolFromAgentValidationRules(validationRulesText) {
4601
+ const raw = validationRulesText?.trim();
4602
+ if (!raw) return null;
4603
+ try {
4604
+ const parsed = JSON.parse(raw);
4605
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4606
+ const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
4607
+ return parseEmailToolObject(emailTool);
4608
+ } catch {
4609
+ return null;
4610
+ }
4611
+ }
4612
+ function resolveChatEmailToolSettings(configMap, sources) {
4613
+ const fromConfig = parseChatEmailToolSettings(configMap);
4614
+ const resolved = typeof sources === "string" || sources == null ? { chatbotValidationRules: typeof sources === "string" ? sources : null } : sources;
4615
+ const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
4616
+ const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
4617
+ const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
4618
+ const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
4619
+ const classifierParts = [
4620
+ notifySystem,
4621
+ fromNotify?.classifierInstructions?.trim(),
4622
+ fromChatbot?.classifierInstructions?.trim(),
4623
+ fromConfig.classifierInstructions.trim()
4624
+ ].filter(Boolean);
4625
+ const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
4626
+ return {
4627
+ enabled: fromConfig.enabled,
4628
+ intents,
4629
+ classifierInstructions: classifierParts.join("\n\n"),
4630
+ toolPrompt
4631
+ };
4632
+ }
4633
+ function intentByKey(intents, key) {
4634
+ if (!key) return null;
4635
+ const norm2 = normalizeIntentKey(key);
4636
+ return intents.find((i) => i.intent === norm2) ?? null;
4637
+ }
4638
+ function buildIntentTableForPrompt(intents) {
4639
+ return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
4640
+ }
4641
+ function buildIntentClassifierSystem(config, agentClassifierInstructions) {
4642
+ const table = buildIntentTableForPrompt(config.intents);
4643
+ const extra = [agentClassifierInstructions?.trim(), config.classifierInstructions.trim()].filter(Boolean).join("\n\n");
4644
+ const intentKeys = config.intents.map((i) => i.intent).join(", ");
4645
+ return `You classify a chat visitor into exactly one lead intent for email routing.
4646
+
4647
+ Available intents (pick the best match):
4648
+ ${table}
4649
+
4650
+ If the visitor is not ready for a lead email (general FAQ, greetings only, spam, off-topic, no clear business need), use intent "${NONE_INTENT}".
4651
+
4652
+ Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
4653
+
4654
+ ${extra ? `Additional instructions:
4655
+ ${extra}
4656
+ ` : ""}
4657
+ Reply with ONLY valid JSON, no markdown:
4658
+ {"intent":"INTENT_KEY","reason":"short explanation"}
4659
+ Use "${NONE_INTENT}" when no intent applies.`;
4660
+ }
4661
+ function buildChatEmailAgentContext(settings) {
4662
+ if (!settings.enabled) return "";
4663
+ const classifier = settings.classifierInstructions.trim();
4664
+ const extra = settings.toolPrompt?.trim() ?? "";
4665
+ const parts = [];
4666
+ if (settings.intents.length > 0) {
4667
+ parts.push(
4668
+ "## Lead email routing",
4669
+ "When a visitor message clearly matches an intent below, the system sends one team notification email (per conversation). Continue the conversation normally in all cases."
4670
+ );
4671
+ if (classifier) parts.push(`### Routing rules
4672
+ ${classifier}`);
4673
+ parts.push(`### Intent catalog
4674
+ ${buildIntentTableForPrompt(settings.intents)}`);
4675
+ } else if (classifier) {
4676
+ parts.push(`## Lead email routing
4677
+ ${classifier}`);
4678
+ }
4679
+ if (extra) parts.push(`### Assistant behavior
4680
+ ${extra}`);
4681
+ return parts.join("\n\n");
4682
+ }
4683
+ function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
4684
+ let prompt = (baseSystemInstruction ?? "").trim();
4685
+ const guard = guardrailsForPrompt?.trim();
4686
+ if (guard) prompt = [prompt, guard].filter(Boolean).join("\n\n");
4687
+ if (emailSettings.enabled) {
4688
+ const emailCtx = buildChatEmailAgentContext(emailSettings);
4689
+ if (emailCtx) prompt = [prompt, emailCtx].filter(Boolean).join("\n\n");
4690
+ }
4691
+ return prompt;
4692
+ }
4693
+ function formatTranscript(history, latestMessage, maxChars = 4e3) {
4694
+ const lines = [];
4695
+ for (const m of history) {
4696
+ const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
4697
+ lines.push(`${role}: ${m.content}`);
4698
+ }
4699
+ const last = history[history.length - 1];
4700
+ if (!last || last.role !== "user" || last.content !== latestMessage) {
4701
+ lines.push(`Visitor: ${latestMessage}`);
4702
+ }
4703
+ let text = lines.join("\n");
4704
+ if (text.length > maxChars) text = text.slice(-maxChars);
4705
+ return text;
4706
+ }
4707
+ function parseIntentJson(raw, intents) {
4708
+ const trimmed = raw.trim();
4709
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
4710
+ const candidate = (fence?.[1] ?? trimmed).trim();
4711
+ const tryParse = (s) => {
4712
+ const o = JSON.parse(s);
4713
+ let intentRaw = null;
4714
+ if (typeof o.intent === "string") intentRaw = o.intent;
4715
+ else if (o.interested === false) intentRaw = NONE_INTENT;
4716
+ else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
4717
+ if (intentRaw == null) return null;
4718
+ const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
4719
+ return { intent: normalizeIntentKey(intentRaw), reason };
4720
+ };
4721
+ try {
4722
+ return tryParse(candidate);
4723
+ } catch {
4724
+ const m = candidate.match(/\{[\s\S]*\}/);
4725
+ if (!m) return null;
4726
+ try {
4727
+ return tryParse(m[0]);
4728
+ } catch {
4729
+ return null;
4730
+ }
4731
+ }
4732
+ }
4733
+ async function detectChatLeadIntent(llm, params) {
4734
+ if (!params.settings.intents.length) {
4735
+ logChatEmail("classify skipped", { reason: "no_intents_configured" });
4736
+ return {
4737
+ intent: null,
4738
+ intentLabel: "NONE",
4739
+ reason: "No lead intents configured",
4740
+ emailTo: null
4741
+ };
4742
+ }
4743
+ logChatEmail("classify start", {
4744
+ intentCount: params.settings.intents.length,
4745
+ intentKeys: params.settings.intents.map((i) => i.intent),
4746
+ model: params.model?.trim() || "(gateway default)",
4747
+ messageChars: params.message.length,
4748
+ historyTurns: params.history.length,
4749
+ hasClassifierInstructions: Boolean(
4750
+ params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim()
4751
+ )
4752
+ });
4753
+ const transcript = formatTranscript(params.history, params.message);
4754
+ const contactLines = [
4755
+ params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
4756
+ params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
4757
+ params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
4758
+ ].filter(Boolean).join("\n");
4759
+ const userPrompt = `Customer:
4760
+ ${contactLines || "(unknown)"}
4761
+
4762
+ Conversation:
4763
+ ${transcript}
4764
+
4765
+ Pick the single best intent for the visitor's latest message.`;
4766
+ const res = await llm.chatAgent({
4767
+ systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
4768
+ userPrompt,
4769
+ temperature: 0.1,
4770
+ max_tokens: 256,
4771
+ ...params.model?.trim() ? { model: params.model.trim() } : {}
4772
+ });
4773
+ const rawContent = res.content ?? "";
4774
+ logChatEmail("classify llm response", {
4775
+ responseChars: rawContent.length,
4776
+ responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
4777
+ });
4778
+ const parsed = parseIntentJson(rawContent, params.settings.intents);
4779
+ if (!parsed) {
4780
+ logChatEmail("classify result", {
4781
+ matched: false,
4782
+ outcome: "parse_failed",
4783
+ emailWillSend: false
4784
+ });
4785
+ return {
4786
+ intent: null,
4787
+ intentLabel: "Unclassified",
4788
+ reason: "Could not parse intent classifier response",
4789
+ emailTo: null
4790
+ };
4791
+ }
4792
+ if (parsed.intent === NONE_INTENT) {
4793
+ logChatEmail("classify result", {
4794
+ matched: false,
4795
+ outcome: NONE_INTENT,
4796
+ reason: parsed.reason,
4797
+ emailWillSend: false
4798
+ });
4799
+ return {
4800
+ intent: null,
4801
+ intentLabel: NONE_INTENT,
4802
+ reason: parsed.reason,
4803
+ emailTo: null
4804
+ };
4805
+ }
4806
+ const matched = intentByKey(params.settings.intents, parsed.intent);
4807
+ if (!matched) {
4808
+ logChatEmail("classify result", {
4809
+ matched: false,
4810
+ outcome: "unknown_intent",
4811
+ parsedIntent: parsed.intent,
4812
+ reason: parsed.reason,
4813
+ emailWillSend: false
4814
+ });
4815
+ return {
4816
+ intent: null,
4817
+ intentLabel: parsed.intent,
4818
+ reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
4819
+ emailTo: null
4820
+ };
4821
+ }
4822
+ logChatEmail("classify result", {
4823
+ matched: true,
4824
+ outcome: "intent_found",
4825
+ intent: matched.intent,
4826
+ emailTo: matched.emailTo,
4827
+ reason: parsed.reason,
4828
+ emailWillSend: true
4829
+ });
4830
+ return {
4831
+ intent: matched.intent,
4832
+ intentLabel: `${matched.intent} \u2014 ${matched.description}`,
4833
+ reason: parsed.reason,
4834
+ emailTo: matched.emailTo
4835
+ };
4836
+ }
4837
+ function buildTranscriptForLeadEmail(history, latestMessage) {
4838
+ return formatTranscript(history, latestMessage);
4839
+ }
4840
+ function parseIntentRecipientEmails(emailTo) {
4841
+ return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
4842
+ }
4843
+
4844
+ // src/plugins/llm/find-llm-agent-by-scope.ts
4845
+ async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
4846
+ const { enabledOnly = true } = options;
4847
+ const entity = entityMap.llm_agents;
4848
+ if (!entity) return null;
4849
+ const repo = dataSource.getRepository(entity);
4850
+ const where = { scope, deleted: false };
4851
+ if (enabledOnly) where.enabled = true;
4852
+ return repo.findOne({ where });
4853
+ }
4854
+
4855
+ // src/plugins/email/templates/types.ts
4856
+ function normalizeSocialLinkItem(o) {
4857
+ const url = String(o.url ?? "").trim();
4858
+ if (!url) return null;
4859
+ let iconUrl = String(o.iconUrl ?? o.icon_image ?? "").trim();
4860
+ let icon = String(o.icon ?? "").trim();
4861
+ if (!iconUrl && /^https?:\/\//i.test(icon)) {
4862
+ iconUrl = icon;
4863
+ icon = "";
4864
+ }
4865
+ const item = { url };
4866
+ if (iconUrl) item.iconUrl = iconUrl;
4867
+ if (icon) item.icon = icon;
4868
+ return item;
4869
+ }
4870
+ function parseSocialLinksJson(raw) {
4871
+ if (raw == null || raw.trim() === "") return void 0;
4872
+ try {
4873
+ const parsed = JSON.parse(raw);
4874
+ if (!Array.isArray(parsed)) return void 0;
4875
+ const out = [];
4876
+ for (const item of parsed) {
4877
+ if (item && typeof item === "object" && "url" in item) {
4878
+ const n = normalizeSocialLinkItem(item);
4879
+ if (n) out.push(n);
4880
+ }
4881
+ }
4882
+ return out.length ? out : void 0;
4883
+ } catch {
4884
+ return void 0;
4885
+ }
4886
+ }
4887
+ function mergeEmailLayoutCompanyDetails(branding, emailSettings) {
4888
+ const fromBranding = getCompanyDetailsFromSettings(branding);
4889
+ const pick = (emailVal, fallback) => {
4890
+ const t = emailVal?.trim();
4891
+ return t || fallback?.trim() || void 0;
4892
+ };
4893
+ const logoUrl = pick(emailSettings.logoUrl ?? emailSettings.emailLogoUrl, fromBranding.logoUrl);
4894
+ const companyName = pick(emailSettings.companyName ?? emailSettings.emailCompanyName, fromBranding.companyName);
4895
+ const supportEmail = pick(emailSettings.supportEmail ?? emailSettings.emailSupportEmail, fromBranding.supportEmail);
4896
+ const supportPhone = pick(emailSettings.supportPhone, void 0);
4897
+ const footerDisclaimer = pick(emailSettings.footerDisclaimer, void 0);
4898
+ const followUsTitle = pick(emailSettings.followUsTitle, "Follow Us") || "Follow Us";
4899
+ const socialFromEmail = parseSocialLinksJson(emailSettings.socialLinks);
4900
+ const socialLinks = socialFromEmail?.length ? socialFromEmail : fromBranding.socialLinks;
4901
+ return {
4902
+ logoUrl,
4903
+ companyName,
4904
+ supportEmail,
4905
+ supportPhone,
4906
+ socialLinks,
4907
+ footerDisclaimer,
4908
+ followUsTitle
4909
+ };
4910
+ }
4911
+ function getCompanyDetailsFromSettings(settingsGroup) {
4912
+ const logoUrl = settingsGroup.logo ?? settingsGroup.logoUrl ?? "";
4913
+ const companyName = settingsGroup.companyName ?? settingsGroup.company_name ?? "";
4914
+ const supportEmail = settingsGroup.supportEmail ?? settingsGroup.support_email ?? "";
4915
+ let socialLinks = [];
4916
+ const raw = settingsGroup.socialLinks ?? settingsGroup.social_links;
4917
+ if (typeof raw === "string") {
4918
+ try {
4919
+ const arr = JSON.parse(raw);
4920
+ if (Array.isArray(arr)) {
4921
+ for (const item of arr) {
4922
+ if (item && typeof item === "object") {
4923
+ const n = normalizeSocialLinkItem(item);
4924
+ if (n) socialLinks.push(n);
4925
+ }
4926
+ }
4927
+ }
4928
+ } catch {
4929
+ }
4930
+ }
4931
+ return { logoUrl: logoUrl || void 0, companyName: companyName || void 0, supportEmail: supportEmail || void 0, socialLinks: socialLinks.length ? socialLinks : void 0 };
4932
+ }
4933
+
4934
+ // src/plugins/email/chat-lead-email.ts
4935
+ init_email_queue();
4936
+
4937
+ // src/lib/email-recipients.ts
4938
+ function parseEmailRecipientsFromConfig(raw) {
4939
+ if (raw == null || raw === "") return [];
4940
+ const trimmed = raw.trim();
4941
+ if (trimmed.startsWith("[")) {
4942
+ try {
4943
+ const parsed = JSON.parse(trimmed);
4944
+ if (Array.isArray(parsed)) {
4945
+ return parsed.map((e) => String(e).trim()).filter(Boolean);
4946
+ }
4947
+ } catch {
4948
+ }
4949
+ }
4950
+ return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
4951
+ }
4952
+
4953
+ // src/plugins/email/chat-lead-email.ts
4954
+ function resolveLeadRecipients(emailPlugin, emailSettings) {
4955
+ const fromCrm = parseEmailRecipientsFromConfig(
4956
+ emailSettings.crmEmails ?? emailSettings.crmEmail ?? ""
4957
+ );
4958
+ if (fromCrm.length > 0) return fromCrm;
4959
+ const fallback = emailPlugin.getDefaultTo?.() ?? "";
4960
+ if (fallback.trim()) return [fallback.trim()];
4961
+ return [];
4962
+ }
4963
+ async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
4964
+ console.info(CHAT_EMAIL_LOG, "send start", {
4965
+ conversationId: input.conversationId,
4966
+ intentCode: input.intentCode ?? null,
4967
+ contactEmail: input.contactEmail,
4968
+ contactName: input.contactName,
4969
+ explicitRecipients: input.recipients?.length ?? 0
4970
+ });
4971
+ const email = cms.getPlugin("email");
4972
+ if (!email?.send || !email.renderTemplate) {
4973
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
4974
+ conversationId: input.conversationId,
4975
+ reason: "email_plugin_disabled"
4976
+ });
4977
+ return { sent: false, recipients: [], error: "Email plugin is not enabled" };
4978
+ }
4979
+ const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
4980
+ if (recipients.length === 0) {
4981
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
4982
+ conversationId: input.conversationId,
4983
+ reason: "no_recipients"
4984
+ });
4985
+ return {
4986
+ sent: false,
4987
+ recipients: [],
4988
+ error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
4989
+ };
4990
+ }
4991
+ console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
4992
+ conversationId: input.conversationId,
4993
+ recipients,
4994
+ source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
4995
+ });
4996
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
4997
+ const ctx = {
4998
+ ...input,
4999
+ companyDetails: input.companyDetails ?? companyDetails
5000
+ };
5001
+ let anySent = false;
5002
+ for (const to of recipients) {
5003
+ await queueEmail(cms, {
5004
+ to,
5005
+ templateName: "chatLead",
5006
+ ctx
5007
+ });
5008
+ console.info(CHAT_EMAIL_LOG, "send queued", {
5009
+ conversationId: input.conversationId,
5010
+ to,
5011
+ template: "chatLead"
5012
+ });
5013
+ anySent = true;
5014
+ }
5015
+ console.info(CHAT_EMAIL_LOG, "send complete", {
5016
+ conversationId: input.conversationId,
5017
+ sent: anySent,
5018
+ recipientCount: recipients.length
5019
+ });
5020
+ return { sent: anySent, recipients };
5021
+ }
5022
+
4401
5023
  // src/lib/media-folder-path.ts
4402
5024
  function sanitizeMediaFolderPath(input) {
4403
5025
  if (input == null) return "";
@@ -5877,16 +6499,19 @@ function normalizeChatModeSetting(raw) {
5877
6499
  if (raw === "external" || raw === "llm") return raw;
5878
6500
  return "whatsapp";
5879
6501
  }
5880
- async function loadLlmSettingsMap(dataSource, entityMap) {
6502
+ async function loadSettingsGroupMap(dataSource, entityMap, group) {
5881
6503
  if (!entityMap.configs) return {};
5882
6504
  const repo = dataSource.getRepository(entityMap.configs);
5883
- const rows = await repo.find({ where: { settings: "llm", deleted: false } });
6505
+ const rows = await repo.find({ where: { settings: group, deleted: false } });
5884
6506
  const out = {};
5885
6507
  for (const row of rows) {
5886
6508
  out[row.key] = row.value;
5887
6509
  }
5888
6510
  return out;
5889
6511
  }
6512
+ async function loadLlmSettingsMap(dataSource, entityMap) {
6513
+ return loadSettingsGroupMap(dataSource, entityMap, "llm");
6514
+ }
5890
6515
  function createChatHandlers(config) {
5891
6516
  const { dataSource, entityMap, json, getCms } = config;
5892
6517
  const contactRepo = () => dataSource.getRepository(entityMap.contacts);
@@ -5898,10 +6523,13 @@ function createChatHandlers(config) {
5898
6523
  try {
5899
6524
  const map = await loadLlmSettingsMap(dataSource, entityMap);
5900
6525
  const mode = normalizeChatModeSetting(map.chatMode);
6526
+ const chatbotAgent = mode === "llm" && entityMap.llm_agents ? await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT, {
6527
+ enabledOnly: false
6528
+ }) : null;
5901
6529
  const body = {
5902
6530
  enabled: map.enabled !== "false",
5903
6531
  chatMode: mode,
5904
- agentSlug: mode === "llm" ? (map.attachedAgentSlug ?? "").trim() : "",
6532
+ agentSlug: mode === "llm" ? chatbotAgent?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT] : "",
5905
6533
  botName: map.botName ?? "",
5906
6534
  icon: map.icon ?? "",
5907
6535
  iconImageUrl: map.iconImageUrl ?? "",
@@ -5987,24 +6615,26 @@ function createChatHandlers(config) {
5987
6615
  if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
5988
6616
  const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
5989
6617
  const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
5990
- let effectiveSlug = (body?.agentSlug ?? "").trim();
5991
- if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
5992
- effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
5993
- }
6618
+ const bodyAgentSlug = (body?.agentSlug ?? "").trim();
5994
6619
  let agentRow = null;
5995
- if (effectiveSlug) {
5996
- if (!entityMap.llm_agents) {
5997
- return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
5998
- }
5999
- const agentRepo = dataSource.getRepository(
6000
- entityMap.llm_agents
6001
- );
6002
- agentRow = await agentRepo.findOne({
6003
- where: { slug: effectiveSlug, deleted: false, enabled: true }
6004
- });
6005
- if (!agentRow && (body?.agentSlug ?? "").trim()) {
6006
- return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
6620
+ let effectiveSlug = bodyAgentSlug;
6621
+ if (entityMap.llm_agents) {
6622
+ if (bodyAgentSlug) {
6623
+ const agentRepo = dataSource.getRepository(
6624
+ entityMap.llm_agents
6625
+ );
6626
+ agentRow = await agentRepo.findOne({
6627
+ where: { slug: bodyAgentSlug, deleted: false, enabled: true }
6628
+ });
6629
+ if (!agentRow) {
6630
+ return json({ error: "Agent not found or disabled", agentSlug: bodyAgentSlug }, { status: 404 });
6631
+ }
6632
+ } else if (supportMode === "llm") {
6633
+ agentRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT);
6634
+ effectiveSlug = agentRow?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
6007
6635
  }
6636
+ } else if (bodyAgentSlug) {
6637
+ return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
6008
6638
  }
6009
6639
  console.info(RAG_LOG, "step 1 | resolve agent", {
6010
6640
  agentSlug: effectiveSlug || "(none)",
@@ -6102,6 +6732,139 @@ function createChatHandlers(config) {
6102
6732
  }
6103
6733
  const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
6104
6734
  const history = historyBeforeCurrentUser(historyRaw, message);
6735
+ const notifyEmailAgent = await findLlmAgentByScope(
6736
+ dataSource,
6737
+ entityMap,
6738
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
6739
+ { enabledOnly: false }
6740
+ );
6741
+ const emailTool = resolveChatEmailToolSettings(llmSettings, {
6742
+ chatbotValidationRules: agentRow?.validationRules ?? null,
6743
+ notifyAgent: notifyEmailAgent ? {
6744
+ systemInstruction: notifyEmailAgent.systemInstruction,
6745
+ validationRules: notifyEmailAgent.validationRules
6746
+ } : null
6747
+ });
6748
+ const emailPlugin = cms.getPlugin("email");
6749
+ const convLeadSent = conv.leadEmailSentAt;
6750
+ const chatAgentFn = llm.chatAgent?.bind(llm);
6751
+ console.info(CHAT_EMAIL_LOG, "pipeline check", {
6752
+ conversationId,
6753
+ enabled: emailTool.enabled,
6754
+ intentCount: emailTool.intents.length,
6755
+ chatbotAgentId: agentRow?.id ?? null,
6756
+ chatbotAgentSlug: agentRow?.slug ?? null,
6757
+ notifyEmailAgentId: notifyEmailAgent?.id ?? null,
6758
+ notifyEmailAgentSlug: notifyEmailAgent?.slug ?? null,
6759
+ emailPluginPresent: Boolean(emailPlugin),
6760
+ chatAgentFnPresent: Boolean(chatAgentFn),
6761
+ leadEmailAlreadySent: Boolean(convLeadSent),
6762
+ mergedPromptIncludesNotify: Boolean(notifyEmailAgent?.systemInstruction?.trim())
6763
+ });
6764
+ if (!emailTool.enabled) {
6765
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6766
+ conversationId,
6767
+ reason: "email_tool_disabled"
6768
+ });
6769
+ } else if (emailTool.intents.length === 0) {
6770
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6771
+ conversationId,
6772
+ reason: "no_intents_configured"
6773
+ });
6774
+ } else if (!emailPlugin) {
6775
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6776
+ conversationId,
6777
+ reason: "email_plugin_missing"
6778
+ });
6779
+ } else if (!chatAgentFn) {
6780
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6781
+ conversationId,
6782
+ reason: "llm_chat_agent_unavailable"
6783
+ });
6784
+ } else if (convLeadSent) {
6785
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6786
+ conversationId,
6787
+ reason: "lead_email_already_sent",
6788
+ leadEmailSentAt: convLeadSent
6789
+ });
6790
+ } else {
6791
+ const contactId = conv.contactId;
6792
+ const contactRow = await contactRepo().findOne({
6793
+ where: { id: contactId }
6794
+ });
6795
+ if (!contactRow) {
6796
+ console.warn(CHAT_EMAIL_LOG, "pipeline skipped", {
6797
+ conversationId,
6798
+ reason: "contact_not_found",
6799
+ contactId
6800
+ });
6801
+ } else {
6802
+ const c = contactRow;
6803
+ try {
6804
+ const intent = await detectChatLeadIntent({ chatAgent: chatAgentFn }, {
6805
+ settings: emailTool,
6806
+ message,
6807
+ history,
6808
+ contact: { name: c.name, email: c.email, phone: c.phone },
6809
+ model: agentRow?.model?.trim() || notifyEmailAgent?.model?.trim() || void 0
6810
+ });
6811
+ if (!intent.intent || !intent.emailTo) {
6812
+ console.info(CHAT_EMAIL_LOG, "no email sent", {
6813
+ conversationId,
6814
+ reason: intent.intent ? "missing_email_to" : "no_intent_match",
6815
+ intentLabel: intent.intentLabel,
6816
+ classifierReason: intent.reason
6817
+ });
6818
+ } else {
6819
+ const intentRecipients = parseIntentRecipientEmails(intent.emailTo);
6820
+ console.info(CHAT_EMAIL_LOG, "intent matched \u2014 sending", {
6821
+ conversationId,
6822
+ intent: intent.intent,
6823
+ emailTo: intent.emailTo,
6824
+ parsedRecipientCount: intentRecipients.length,
6825
+ recipients: intentRecipients
6826
+ });
6827
+ const emailSettings = await loadSettingsGroupMap(dataSource, entityMap, "email");
6828
+ const brandingSettings = await loadSettingsGroupMap(dataSource, entityMap, "branding");
6829
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
6830
+ const leadResult = await sendChatLeadEmail(cms, emailSettings, brandingSettings, {
6831
+ contactName: String(c.name ?? "").trim() || "Visitor",
6832
+ contactEmail: String(c.email ?? "").trim(),
6833
+ contactPhone: c.phone ?? null,
6834
+ conversationId,
6835
+ latestMessage: message,
6836
+ intentCode: intent.intent,
6837
+ intentReason: intent.intentLabel || intent.reason,
6838
+ transcript: buildTranscriptForLeadEmail(history, message),
6839
+ companyDetails,
6840
+ recipients: intentRecipients.length > 0 ? intentRecipients : void 0
6841
+ });
6842
+ if (leadResult.sent) {
6843
+ await convRepo().update(conversationId, {
6844
+ leadEmailSentAt: /* @__PURE__ */ new Date()
6845
+ });
6846
+ console.info(CHAT_EMAIL_LOG, "lead email recorded", {
6847
+ conversationId,
6848
+ sent: true,
6849
+ recipients: leadResult.recipients
6850
+ });
6851
+ } else {
6852
+ console.warn(CHAT_EMAIL_LOG, "lead email not sent", {
6853
+ conversationId,
6854
+ sent: false,
6855
+ error: leadResult.error ?? "unknown",
6856
+ recipients: leadResult.recipients
6857
+ });
6858
+ }
6859
+ }
6860
+ } catch (intentErr) {
6861
+ console.warn(CHAT_EMAIL_LOG, "pipeline error", {
6862
+ conversationId,
6863
+ error: intentErr instanceof Error ? intentErr.message : String(intentErr)
6864
+ });
6865
+ }
6866
+ }
6867
+ }
6105
6868
  let content;
6106
6869
  const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
6107
6870
  console.info(RAG_LOG, "step 7 | final context", {
@@ -6112,9 +6875,10 @@ function createChatHandlers(config) {
6112
6875
  });
6113
6876
  if (agentRow && llm.chatAgent) {
6114
6877
  const fromAgent = llmAgentToChatAgentOptions(agentRow);
6115
- const systemPrompt = mergeGuardrailsIntoSystemPrompt(
6878
+ const systemPrompt = buildChatbotSystemPromptWithEmailTool(
6116
6879
  fromAgent.systemPrompt,
6117
- parsedValidation.guardrailsForPrompt
6880
+ parsedValidation.guardrailsForPrompt,
6881
+ emailTool
6118
6882
  );
6119
6883
  const res = await llm.chatAgent({
6120
6884
  ...fromAgent,
@@ -6132,11 +6896,12 @@ ${contextParts.join("\n\n")}` : "";
6132
6896
  const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
6133
6897
  let systemContent;
6134
6898
  if (agentRow) {
6135
- const base = agentRow.systemInstruction?.trim() || "";
6136
- systemContent = mergeGuardrailsIntoSystemPrompt(
6137
- [base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
6138
- parsedValidation.guardrailsForPrompt
6139
- );
6899
+ const mergedBase = [agentRow.systemInstruction?.trim(), ragSystem].filter(Boolean).join("\n\n");
6900
+ systemContent = buildChatbotSystemPromptWithEmailTool(
6901
+ mergedBase || defaultSystem,
6902
+ parsedValidation.guardrailsForPrompt,
6903
+ emailTool
6904
+ ) || defaultSystem;
6140
6905
  } else {
6141
6906
  systemContent = ragSystem || defaultSystem;
6142
6907
  }
@@ -8652,6 +9417,7 @@ var Order = class {
8652
9417
  id;
8653
9418
  vendorId;
8654
9419
  orderNumber;
9420
+ qrToken;
8655
9421
  orderKind;
8656
9422
  parentOrderId;
8657
9423
  contactId;
@@ -8689,6 +9455,9 @@ __decorateClass([
8689
9455
  __decorateClass([
8690
9456
  (0, import_typeorm25.Column)("varchar")
8691
9457
  ], Order.prototype, "orderNumber", 2);
9458
+ __decorateClass([
9459
+ (0, import_typeorm25.Column)("varchar", { unique: true, nullable: true })
9460
+ ], Order.prototype, "qrToken", 2);
8692
9461
  __decorateClass([
8693
9462
  (0, import_typeorm25.Column)("varchar", { default: "sale" })
8694
9463
  ], Order.prototype, "orderKind", 2);
@@ -8917,6 +9686,7 @@ var ChatConversation = class {
8917
9686
  contactId;
8918
9687
  createdAt;
8919
9688
  updatedAt;
9689
+ leadEmailSentAt;
8920
9690
  contact;
8921
9691
  messages;
8922
9692
  };
@@ -8932,6 +9702,9 @@ __decorateClass([
8932
9702
  __decorateClass([
8933
9703
  (0, import_typeorm28.Column)({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
8934
9704
  ], ChatConversation.prototype, "updatedAt", 2);
9705
+ __decorateClass([
9706
+ (0, import_typeorm28.Column)({ type: "timestamp", nullable: true })
9707
+ ], ChatConversation.prototype, "leadEmailSentAt", 2);
8935
9708
  __decorateClass([
8936
9709
  (0, import_typeorm28.ManyToOne)(() => Contact, (c) => c.chatConversations, { onDelete: "CASCADE" }),
8937
9710
  (0, import_typeorm28.JoinColumn)({ name: "contactId" })
@@ -11135,7 +11908,6 @@ var CMS_ENTITY_MAP = {
11135
11908
  var import_rss_parser = __toESM(require("rss-parser"), 1);
11136
11909
 
11137
11910
  // src/plugins/blog-generator/blog-generator-agent-defaults.ts
11138
- var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
11139
11911
  var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
11140
11912
  var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
11141
11913
 
@@ -11215,7 +11987,6 @@ var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
11215
11987
  );
11216
11988
 
11217
11989
  // src/plugins/blog-generator/blog-generator-metadata-defaults.ts
11218
- var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
11219
11990
  var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11220
11991
  {
11221
11992
  maxUserChars: 5e5,
@@ -11226,7 +11997,6 @@ var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11226
11997
  );
11227
11998
 
11228
11999
  // src/plugins/blog-generator/blog-generator-social-defaults.ts
11229
- var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
11230
12000
  var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11231
12001
  {
11232
12002
  maxUserChars: 5e5,
@@ -12463,7 +13233,7 @@ ${excerpt}`).trim().slice(0, 2200);
12463
13233
  }
12464
13234
 
12465
13235
  // src/api/job-schedule-handlers.ts
12466
- var import_crypto = require("crypto");
13236
+ var import_crypto2 = require("crypto");
12467
13237
 
12468
13238
  // src/plugins/jobs/schedule-cron.ts
12469
13239
  function pgBossScheduleNameForId(scheduleId) {
@@ -12698,7 +13468,7 @@ function createJobScheduleHandlers(apiConfig) {
12698
13468
  const repo = dataSource.getRepository(ScheduleEntity);
12699
13469
  let row = repo.create({
12700
13470
  ...patch,
12701
- pgBossScheduleName: `pending-${(0, import_crypto.randomUUID)()}`
13471
+ pgBossScheduleName: `pending-${(0, import_crypto2.randomUUID)()}`
12702
13472
  });
12703
13473
  row = await repo.save(row);
12704
13474
  row.pgBossScheduleName = pgBossScheduleNameForId(row.id);
@@ -13353,28 +14123,24 @@ function createCmsApiHandler(config) {
13353
14123
  let socialLlmAgentResolution = null;
13354
14124
  let metadataRow = null;
13355
14125
  let socialRow = null;
14126
+ let blogCreationRow = null;
13356
14127
  if (entityMap.llm_agents) {
13357
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
13358
- const agentRow = await agentRepo.findOne({
13359
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
13360
- });
13361
- if (agentRow) {
13362
- const o = llmAgentToChatAgentOptions(agentRow);
14128
+ blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
14129
+ if (blogCreationRow) {
14130
+ const o = llmAgentToChatAgentOptions(blogCreationRow);
13363
14131
  llmAgentChatOptions = {
13364
14132
  model: o.model,
13365
14133
  temperature: o.temperature,
13366
14134
  max_tokens: o.max_tokens
13367
14135
  };
13368
14136
  llmAgentResolution = {
13369
- slug: BLOG_GENERATOR_LLM_AGENT_SLUG,
13370
- model: agentRow.model?.trim() || null,
13371
- temperature: agentRow.temperature ?? null,
13372
- maxTokens: agentRow.maxTokens ?? null
14137
+ slug: blogCreationRow.slug,
14138
+ model: blogCreationRow.model?.trim() || null,
14139
+ temperature: blogCreationRow.temperature ?? null,
14140
+ maxTokens: blogCreationRow.maxTokens ?? null
13373
14141
  };
13374
14142
  }
13375
- metadataRow = await agentRepo.findOne({
13376
- where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
13377
- });
14143
+ metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
13378
14144
  if (metadataRow) {
13379
14145
  const mo = llmAgentToChatAgentOptions(metadataRow);
13380
14146
  metadataLlmAgentChatOptions = {
@@ -13383,15 +14149,13 @@ function createCmsApiHandler(config) {
13383
14149
  max_tokens: mo.max_tokens
13384
14150
  };
13385
14151
  metadataLlmAgentResolution = {
13386
- slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG,
14152
+ slug: metadataRow.slug,
13387
14153
  model: metadataRow.model?.trim() || null,
13388
14154
  temperature: metadataRow.temperature ?? null,
13389
14155
  maxTokens: metadataRow.maxTokens ?? null
13390
14156
  };
13391
14157
  }
13392
- socialRow = await agentRepo.findOne({
13393
- where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
13394
- });
14158
+ socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
13395
14159
  if (socialRow) {
13396
14160
  const so = llmAgentToChatAgentOptions(socialRow);
13397
14161
  socialLlmAgentChatOptions = {
@@ -13400,7 +14164,7 @@ function createCmsApiHandler(config) {
13400
14164
  max_tokens: so.max_tokens
13401
14165
  };
13402
14166
  socialLlmAgentResolution = {
13403
- slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG,
14167
+ slug: socialRow.slug,
13404
14168
  model: socialRow.model?.trim() || null,
13405
14169
  temperature: socialRow.temperature ?? null,
13406
14170
  maxTokens: socialRow.maxTokens ?? null
@@ -13424,8 +14188,8 @@ function createCmsApiHandler(config) {
13424
14188
  const out = await svc.generateBlogMarkdownFromRss({
13425
14189
  llm,
13426
14190
  rssUrls,
13427
- systemInstruction,
13428
- validationRules,
14191
+ systemInstruction: systemInstruction ?? blogCreationRow?.systemInstruction?.trim() ?? void 0,
14192
+ validationRules: validationRules ?? blogCreationRow?.validationRules?.trim() ?? void 0,
13429
14193
  categoryNamesHint: categoryRows.map((c) => c.name),
13430
14194
  tagNamesHint: tagNames,
13431
14195
  llmAgentChatOptions,
@@ -13910,6 +14674,22 @@ function createCmsApiHandler(config) {
13910
14674
  return config.json({ error: message }, { status: 500 });
13911
14675
  }
13912
14676
  }
14677
+ if (path2[0] === "track" && path2.length === 2 && m === "GET") {
14678
+ const token = path2[1];
14679
+ if (!token) return config.json({ error: "Token required" }, { status: 400 });
14680
+ try {
14681
+ const orderRepo = dataSource.getRepository(entityMap.orders);
14682
+ const order = await orderRepo.findOne({
14683
+ where: { qrToken: token, deleted: false },
14684
+ relations: ["contact", "items", "items.product"]
14685
+ });
14686
+ if (!order) return config.json({ error: "Order not found" }, { status: 404 });
14687
+ return config.json(order);
14688
+ } catch (err) {
14689
+ const message = err instanceof Error ? err.message : String(err);
14690
+ return config.json({ error: message }, { status: 500 });
14691
+ }
14692
+ }
13913
14693
  if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
13914
14694
  const resource = resolveResource(path2[0]);
13915
14695
  if (!crudResources.includes(resource)) {
@@ -14213,7 +14993,7 @@ async function queueSms(cms, payload) {
14213
14993
  }
14214
14994
 
14215
14995
  // src/lib/otp-challenge.ts
14216
- var import_crypto2 = require("crypto");
14996
+ var import_crypto3 = require("crypto");
14217
14997
  var import_typeorm62 = require("typeorm");
14218
14998
  var OTP_TTL_MS = 10 * 60 * 1e3;
14219
14999
  var MAX_SENDS_PER_HOUR = 5;
@@ -14222,19 +15002,19 @@ function getPepper(explicit) {
14222
15002
  return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
14223
15003
  }
14224
15004
  function hashOtpCode(code, purpose, identifier, pepper) {
14225
- return (0, import_crypto2.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
15005
+ return (0, import_crypto3.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
14226
15006
  }
14227
15007
  function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
14228
15008
  const h = hashOtpCode(code, purpose, identifier, pepper);
14229
15009
  try {
14230
- return (0, import_crypto2.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
15010
+ return (0, import_crypto3.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
14231
15011
  } catch {
14232
15012
  return false;
14233
15013
  }
14234
15014
  }
14235
15015
  function generateNumericOtp(length = 6) {
14236
15016
  const max = 10 ** length;
14237
- return (0, import_crypto2.randomInt)(0, max).toString().padStart(length, "0");
15017
+ return (0, import_crypto3.randomInt)(0, max).toString().padStart(length, "0");
14238
15018
  }
14239
15019
  function normalizePhoneE164(raw, defaultCountryCode) {
14240
15020
  const t = raw.trim();
@@ -15111,8 +15891,8 @@ function createStorefrontApiHandler(config) {
15111
15891
  let emailVerificationSent = false;
15112
15892
  if (requireEmailVerification && getCms) {
15113
15893
  try {
15114
- const crypto2 = await import("crypto");
15115
- const rawToken = crypto2.randomBytes(32).toString("hex");
15894
+ const crypto3 = await import("crypto");
15895
+ const rawToken = crypto3.randomBytes(32).toString("hex");
15116
15896
  const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
15117
15897
  await tokenRepo().save(
15118
15898
  tokenRepo().create({ email, token: rawToken, expiresAt })