@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.js CHANGED
@@ -1157,6 +1157,65 @@ function resolveVendorIdForContactCheck(scope, bodyOrRow) {
1157
1157
  return Number.isFinite(n) ? n : null;
1158
1158
  }
1159
1159
 
1160
+ // src/api/crud.ts
1161
+ import crypto2 from "crypto";
1162
+
1163
+ // src/plugins/llm/llm-agent-scope.ts
1164
+ var LLM_AGENT_SCOPE_CHATBOT = "chatbot";
1165
+ var LLM_AGENT_SCOPE_BLOG_CREATION = "blog_creation";
1166
+ var LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST = "social_media_post";
1167
+ var LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT = "email_intent_chatbot";
1168
+ var LLM_AGENT_SCOPE_BLOG_METADATA = "blog_metadata";
1169
+ var LLM_AGENT_SCOPES = [
1170
+ LLM_AGENT_SCOPE_CHATBOT,
1171
+ LLM_AGENT_SCOPE_BLOG_CREATION,
1172
+ LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST,
1173
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
1174
+ LLM_AGENT_SCOPE_BLOG_METADATA
1175
+ ];
1176
+ var LLM_AGENT_DEFAULT_SLUG_BY_SCOPE = {
1177
+ [LLM_AGENT_SCOPE_CHATBOT]: "site-chat-assistant",
1178
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "blog-generator",
1179
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "blog-generator-social",
1180
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "email-intent-chatbot",
1181
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "blog-generator-metadata"
1182
+ };
1183
+ var LLM_AGENT_DEFAULT_NAME_BY_SCOPE = {
1184
+ [LLM_AGENT_SCOPE_CHATBOT]: "Site Chat Assistant",
1185
+ [LLM_AGENT_SCOPE_BLOG_CREATION]: "Blog Generator",
1186
+ [LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "Blog Generator (Social)",
1187
+ [LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "Chat Lead Intent Classifier",
1188
+ [LLM_AGENT_SCOPE_BLOG_METADATA]: "Blog Generator (Metadata)"
1189
+ };
1190
+ var SITE_CHAT_ASSISTANT_SLUG = LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
1191
+ var SITE_CHAT_ASSISTANT_NAME = LLM_AGENT_DEFAULT_NAME_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
1192
+ function isLlmAgentScope(value) {
1193
+ if (!value?.trim()) return false;
1194
+ return LLM_AGENT_SCOPES.includes(value.trim());
1195
+ }
1196
+ var BLOG_LLM_AGENT_SLUGS = [
1197
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_CREATION],
1198
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_METADATA],
1199
+ LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]
1200
+ ];
1201
+
1202
+ // src/plugins/llm/llm-agent-scope-crud.ts
1203
+ async function validateLlmAgentScopeForWrite(dataSource, entityMap, scope, excludeId) {
1204
+ if (scope == null || scope === "") return null;
1205
+ if (typeof scope !== "string" || !isLlmAgentScope(scope)) {
1206
+ return `Invalid agent scope. Allowed: chatbot, blog_creation, social_media_post, email_intent_chatbot, blog_metadata.`;
1207
+ }
1208
+ const entity = entityMap.llm_agents;
1209
+ if (!entity) return null;
1210
+ const repo = dataSource.getRepository(entity);
1211
+ const existing = await repo.findOne({
1212
+ where: { scope, deleted: false }
1213
+ });
1214
+ if (!existing) return null;
1215
+ if (excludeId != null && existing.id === excludeId) return null;
1216
+ return `An active agent already uses scope "${scope}".`;
1217
+ }
1218
+
1160
1219
  // src/api/crud.ts
1161
1220
  var CRUD_LOG = "[cms-crud]";
1162
1221
  function logCrudClientError(op, detail) {
@@ -1373,6 +1432,7 @@ function buildListFilterAndFromSearchParams(repo, searchParams) {
1373
1432
  if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
1374
1433
  if (!isListStringColumn(col)) continue;
1375
1434
  if (Object.prototype.hasOwnProperty.call(and, name)) continue;
1435
+ if (name === "scope") continue;
1376
1436
  const raw = searchParams.get(name)?.trim();
1377
1437
  if (!raw) continue;
1378
1438
  and[name] = ILike(`%${raw}%`);
@@ -1402,6 +1462,10 @@ function buildExactListParamWhere(repo, searchParams) {
1402
1462
  extraWhere[name] = raw === "true";
1403
1463
  }
1404
1464
  }
1465
+ const scopeParam = searchParams.get("scope")?.trim();
1466
+ if (scopeParam && columnNames.has("scope")) {
1467
+ extraWhere.scope = scopeParam;
1468
+ }
1405
1469
  return extraWhere;
1406
1470
  }
1407
1471
  function mergeDeletedFalseWhere(repo, where) {
@@ -1613,6 +1677,12 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
1613
1677
  case "minAmount":
1614
1678
  return discountCompare(cartTotal, rule.comparisonOperator, ruleValue);
1615
1679
  case "quantity": {
1680
+ if (rule.subType === "productId" && rule.value?.productId) {
1681
+ const targetProductId = Number(rule.value.productId);
1682
+ const requiredQty = Number(rule.value.v);
1683
+ const productQty = cartLines.filter((l) => l.productId === targetProductId).reduce((s, l) => s + l.quantity, 0);
1684
+ return discountCompare(productQty, rule.comparisonOperator, requiredQty);
1685
+ }
1616
1686
  const totalQty = cartLines.reduce((s, l) => s + l.quantity, 0);
1617
1687
  return discountCompare(totalQty, rule.comparisonOperator, ruleValue);
1618
1688
  }
@@ -1676,6 +1746,10 @@ function discountExtractBuyProductId(nodes) {
1676
1746
  const n = Number(v);
1677
1747
  if (Number.isFinite(n) && n > 0) return n;
1678
1748
  }
1749
+ if (node.conditionType === "rule" && node.type === "quantity" && node.subType === "productId") {
1750
+ const n = Number(node.value?.productId);
1751
+ if (Number.isFinite(n) && n > 0) return n;
1752
+ }
1679
1753
  if (node.children?.length) {
1680
1754
  const found = discountExtractBuyProductId(node.children);
1681
1755
  if (found !== null) return found;
@@ -2386,6 +2460,25 @@ function createCrudHandler(dataSource, entityMap, options) {
2386
2460
  if (pe) return pe;
2387
2461
  return null;
2388
2462
  }
2463
+ async function tryAssignSingleVendorOnAdminCreate(resource, scope, persistBody) {
2464
+ if (!resourceUsesVendorScope(resource) || scope.type !== "all") return;
2465
+ const currentVendorId = Number(persistBody.vendorId);
2466
+ if (Number.isFinite(currentVendorId) && currentVendorId > 0) return;
2467
+ if (!entityMap.vendors) return;
2468
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
2469
+ const where = mergeDeletedFalseWhere(vendorRepo, {});
2470
+ const rows = await vendorRepo.find({
2471
+ where,
2472
+ order: { id: "ASC" },
2473
+ take: 2
2474
+ });
2475
+ if (rows.length === 1) {
2476
+ const onlyVendorId = Number(rows[0].id);
2477
+ if (Number.isFinite(onlyVendorId) && onlyVendorId > 0) {
2478
+ persistBody.vendorId = onlyVendorId;
2479
+ }
2480
+ }
2481
+ }
2389
2482
  return {
2390
2483
  async GET(req, resource) {
2391
2484
  const authError = await authz(req, resource, "read");
@@ -2901,6 +2994,7 @@ function createCrudHandler(dataSource, entityMap, options) {
2901
2994
  const couponCode = String(body.couponCode).trim().toUpperCase();
2902
2995
  const persistBody2 = pickColumnUpdates(repo2, { ...body, couponCode });
2903
2996
  const scopeDiscount = await resolveScope();
2997
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeDiscount, persistBody2);
2904
2998
  const vendorIdCheck2 = requireVendorIdForScopedCreate(
2905
2999
  resource,
2906
3000
  persistBody2,
@@ -2978,6 +3072,10 @@ function createCrudHandler(dataSource, entityMap, options) {
2978
3072
  });
2979
3073
  return json({ error: "Invalid request payload" }, { status: 400 });
2980
3074
  }
3075
+ if (resource === "llm_agents" && "scope" in persistBody) {
3076
+ const scopeErr = await validateLlmAgentScopeForWrite(dataSource, entityMap, persistBody.scope);
3077
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
3078
+ }
2981
3079
  if (resource === "products") {
2982
3080
  if ("sku" in persistBody) {
2983
3081
  const skuNorm = normalizeProductSku(persistBody.sku);
@@ -3093,6 +3191,7 @@ function createCrudHandler(dataSource, entityMap, options) {
3093
3191
  );
3094
3192
  }
3095
3193
  const scopeCreate = await resolveScope();
3194
+ await tryAssignSingleVendorOnAdminCreate(resource, scopeCreate, persistBody);
3096
3195
  const vendorIdCheck = requireVendorIdForScopedCreate(resource, persistBody, scopeCreate, repo, body);
3097
3196
  if (!vendorIdCheck.ok) {
3098
3197
  return json({ error: vendorIdCheck.error }, { status: vendorIdCheck.status });
@@ -3135,6 +3234,7 @@ function createCrudHandler(dataSource, entityMap, options) {
3135
3234
  const randomPart = Math.floor(1e4 + Math.random() * 9e4);
3136
3235
  persistBody.contactId = contact.id;
3137
3236
  persistBody.orderNumber = `ORD00${randomPart}`;
3237
+ persistBody.qrToken = crypto2.randomBytes(16).toString("hex");
3138
3238
  }
3139
3239
  created = await repo.save(repo.create(persistBody));
3140
3240
  if (resource === "orders") {
@@ -3903,6 +4003,15 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
3903
4003
  const t = updatePayload.type;
3904
4004
  if (t === "" || t === "none" || t == null) updatePayload.type = null;
3905
4005
  }
4006
+ if (resource === "llm_agents" && "scope" in updatePayload) {
4007
+ const scopeErr = await validateLlmAgentScopeForWrite(
4008
+ dataSource,
4009
+ entityMap,
4010
+ updatePayload.scope,
4011
+ numericId
4012
+ );
4013
+ if (scopeErr) return json({ error: scopeErr }, { status: 400 });
4014
+ }
3906
4015
  if ((resource === "orders" || resource === "payments") && "contactId" in updatePayload && updatePayload.contactId != null && entityMap.vendor_customers) {
3907
4016
  const existingRow = await repo.findOne({
3908
4017
  where: { id: numericId }
@@ -4112,8 +4221,8 @@ function createForgotPasswordHandler(config) {
4112
4221
  const user = await userRepo.findOne({ where: { email }, select: ["email"] });
4113
4222
  const msg = "If an account exists with this email, you will receive a reset link shortly.";
4114
4223
  if (!user) return json({ message: msg }, { status: 200 });
4115
- const crypto2 = await import("crypto");
4116
- const token = crypto2.randomBytes(32).toString("hex");
4224
+ const crypto3 = await import("crypto");
4225
+ const token = crypto3.randomBytes(32).toString("hex");
4117
4226
  const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
4118
4227
  const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
4119
4228
  await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
@@ -4266,6 +4375,7 @@ var LlmAgent = class {
4266
4375
  id;
4267
4376
  name;
4268
4377
  slug;
4378
+ scope;
4269
4379
  systemInstruction;
4270
4380
  model;
4271
4381
  temperature;
@@ -4289,6 +4399,9 @@ __decorateClass([
4289
4399
  __decorateClass([
4290
4400
  Column("varchar")
4291
4401
  ], LlmAgent.prototype, "slug", 2);
4402
+ __decorateClass([
4403
+ Column("varchar", { nullable: true })
4404
+ ], LlmAgent.prototype, "scope", 2);
4292
4405
  __decorateClass([
4293
4406
  Column("text", { name: "system_instruction", default: "" })
4294
4407
  ], LlmAgent.prototype, "systemInstruction", 2);
@@ -4340,6 +4453,515 @@ function llmAgentToChatAgentOptions(agent) {
4340
4453
  };
4341
4454
  }
4342
4455
 
4456
+ // src/plugins/llm/chat-email-intent.ts
4457
+ var NONE_INTENT = "NONE";
4458
+ var CHAT_EMAIL_LOG = "[chat-email-tool]";
4459
+ function logChatEmail(step, data) {
4460
+ if (data && Object.keys(data).length > 0) {
4461
+ console.info(CHAT_EMAIL_LOG, step, data);
4462
+ } else {
4463
+ console.info(CHAT_EMAIL_LOG, step);
4464
+ }
4465
+ }
4466
+ function normalizeIntentKey(raw) {
4467
+ return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
4468
+ }
4469
+ function parseIntentsJson(raw) {
4470
+ if (!raw?.trim()) return null;
4471
+ try {
4472
+ const parsed = JSON.parse(raw);
4473
+ if (!Array.isArray(parsed)) return null;
4474
+ const out = [];
4475
+ for (const row of parsed) {
4476
+ if (!row || typeof row !== "object") continue;
4477
+ const o = row;
4478
+ const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
4479
+ const description = String(o.description ?? "").trim();
4480
+ const emailTo = String(o.emailTo ?? o.email ?? "").trim();
4481
+ if (!intent || !description || !emailTo) continue;
4482
+ out.push({ intent, description, emailTo });
4483
+ }
4484
+ return out.length > 0 ? out : null;
4485
+ } catch {
4486
+ return null;
4487
+ }
4488
+ }
4489
+ function dedupeIntents(intents) {
4490
+ const seen = /* @__PURE__ */ new Set();
4491
+ const out = [];
4492
+ for (const row of intents) {
4493
+ const key = normalizeIntentKey(row.intent);
4494
+ if (!key || seen.has(key)) continue;
4495
+ seen.add(key);
4496
+ out.push({
4497
+ intent: key,
4498
+ description: row.description.trim(),
4499
+ emailTo: row.emailTo.trim()
4500
+ });
4501
+ }
4502
+ return out;
4503
+ }
4504
+ function parseChatEmailToolSettings(map) {
4505
+ const fromJson = parseIntentsJson(map.emailIntents);
4506
+ const intents = dedupeIntents(fromJson ?? []);
4507
+ const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
4508
+ const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
4509
+ let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
4510
+ if (!classifierInstructions && (legacyPositive || legacyNegative)) {
4511
+ const parts = [];
4512
+ if (legacyPositive) parts.push(`Legacy positive signals:
4513
+ ${legacyPositive}`);
4514
+ if (legacyNegative) parts.push(`Do NOT assign an intent when:
4515
+ ${legacyNegative}`);
4516
+ classifierInstructions = parts.join("\n\n");
4517
+ }
4518
+ return {
4519
+ enabled: map.emailToolEnabled === "true",
4520
+ intents,
4521
+ classifierInstructions,
4522
+ toolPrompt: map.emailToolPrompt ?? ""
4523
+ };
4524
+ }
4525
+ var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
4526
+ function parseEmailToolObject(raw) {
4527
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4528
+ const o = raw;
4529
+ const intentsRaw = o.intents;
4530
+ let intents;
4531
+ if (Array.isArray(intentsRaw)) {
4532
+ const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
4533
+ if (parsed?.length) intents = parsed;
4534
+ }
4535
+ return {
4536
+ enabled: o.enabled === true,
4537
+ classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
4538
+ toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
4539
+ intents
4540
+ };
4541
+ }
4542
+ function parseEmailToolFromAgentValidationRules(validationRulesText) {
4543
+ const raw = validationRulesText?.trim();
4544
+ if (!raw) return null;
4545
+ try {
4546
+ const parsed = JSON.parse(raw);
4547
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4548
+ const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
4549
+ return parseEmailToolObject(emailTool);
4550
+ } catch {
4551
+ return null;
4552
+ }
4553
+ }
4554
+ function resolveChatEmailToolSettings(configMap, sources) {
4555
+ const fromConfig = parseChatEmailToolSettings(configMap);
4556
+ const resolved = typeof sources === "string" || sources == null ? { chatbotValidationRules: typeof sources === "string" ? sources : null } : sources;
4557
+ const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
4558
+ const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
4559
+ const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
4560
+ const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
4561
+ const classifierParts = [
4562
+ notifySystem,
4563
+ fromNotify?.classifierInstructions?.trim(),
4564
+ fromChatbot?.classifierInstructions?.trim(),
4565
+ fromConfig.classifierInstructions.trim()
4566
+ ].filter(Boolean);
4567
+ const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
4568
+ return {
4569
+ enabled: fromConfig.enabled,
4570
+ intents,
4571
+ classifierInstructions: classifierParts.join("\n\n"),
4572
+ toolPrompt
4573
+ };
4574
+ }
4575
+ function intentByKey(intents, key) {
4576
+ if (!key) return null;
4577
+ const norm2 = normalizeIntentKey(key);
4578
+ return intents.find((i) => i.intent === norm2) ?? null;
4579
+ }
4580
+ function buildIntentTableForPrompt(intents) {
4581
+ return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
4582
+ }
4583
+ function buildIntentClassifierSystem(config, agentClassifierInstructions) {
4584
+ const table = buildIntentTableForPrompt(config.intents);
4585
+ const extra = [agentClassifierInstructions?.trim(), config.classifierInstructions.trim()].filter(Boolean).join("\n\n");
4586
+ const intentKeys = config.intents.map((i) => i.intent).join(", ");
4587
+ return `You classify a chat visitor into exactly one lead intent for email routing.
4588
+
4589
+ Available intents (pick the best match):
4590
+ ${table}
4591
+
4592
+ 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}".
4593
+
4594
+ Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
4595
+
4596
+ ${extra ? `Additional instructions:
4597
+ ${extra}
4598
+ ` : ""}
4599
+ Reply with ONLY valid JSON, no markdown:
4600
+ {"intent":"INTENT_KEY","reason":"short explanation"}
4601
+ Use "${NONE_INTENT}" when no intent applies.`;
4602
+ }
4603
+ function buildChatEmailAgentContext(settings) {
4604
+ if (!settings.enabled) return "";
4605
+ const classifier = settings.classifierInstructions.trim();
4606
+ const extra = settings.toolPrompt?.trim() ?? "";
4607
+ const parts = [];
4608
+ if (settings.intents.length > 0) {
4609
+ parts.push(
4610
+ "## Lead email routing",
4611
+ "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."
4612
+ );
4613
+ if (classifier) parts.push(`### Routing rules
4614
+ ${classifier}`);
4615
+ parts.push(`### Intent catalog
4616
+ ${buildIntentTableForPrompt(settings.intents)}`);
4617
+ } else if (classifier) {
4618
+ parts.push(`## Lead email routing
4619
+ ${classifier}`);
4620
+ }
4621
+ if (extra) parts.push(`### Assistant behavior
4622
+ ${extra}`);
4623
+ return parts.join("\n\n");
4624
+ }
4625
+ function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
4626
+ let prompt = (baseSystemInstruction ?? "").trim();
4627
+ const guard = guardrailsForPrompt?.trim();
4628
+ if (guard) prompt = [prompt, guard].filter(Boolean).join("\n\n");
4629
+ if (emailSettings.enabled) {
4630
+ const emailCtx = buildChatEmailAgentContext(emailSettings);
4631
+ if (emailCtx) prompt = [prompt, emailCtx].filter(Boolean).join("\n\n");
4632
+ }
4633
+ return prompt;
4634
+ }
4635
+ function formatTranscript(history, latestMessage, maxChars = 4e3) {
4636
+ const lines = [];
4637
+ for (const m of history) {
4638
+ const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
4639
+ lines.push(`${role}: ${m.content}`);
4640
+ }
4641
+ const last = history[history.length - 1];
4642
+ if (!last || last.role !== "user" || last.content !== latestMessage) {
4643
+ lines.push(`Visitor: ${latestMessage}`);
4644
+ }
4645
+ let text = lines.join("\n");
4646
+ if (text.length > maxChars) text = text.slice(-maxChars);
4647
+ return text;
4648
+ }
4649
+ function parseIntentJson(raw, intents) {
4650
+ const trimmed = raw.trim();
4651
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
4652
+ const candidate = (fence?.[1] ?? trimmed).trim();
4653
+ const tryParse = (s) => {
4654
+ const o = JSON.parse(s);
4655
+ let intentRaw = null;
4656
+ if (typeof o.intent === "string") intentRaw = o.intent;
4657
+ else if (o.interested === false) intentRaw = NONE_INTENT;
4658
+ else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
4659
+ if (intentRaw == null) return null;
4660
+ const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
4661
+ return { intent: normalizeIntentKey(intentRaw), reason };
4662
+ };
4663
+ try {
4664
+ return tryParse(candidate);
4665
+ } catch {
4666
+ const m = candidate.match(/\{[\s\S]*\}/);
4667
+ if (!m) return null;
4668
+ try {
4669
+ return tryParse(m[0]);
4670
+ } catch {
4671
+ return null;
4672
+ }
4673
+ }
4674
+ }
4675
+ async function detectChatLeadIntent(llm, params) {
4676
+ if (!params.settings.intents.length) {
4677
+ logChatEmail("classify skipped", { reason: "no_intents_configured" });
4678
+ return {
4679
+ intent: null,
4680
+ intentLabel: "NONE",
4681
+ reason: "No lead intents configured",
4682
+ emailTo: null
4683
+ };
4684
+ }
4685
+ logChatEmail("classify start", {
4686
+ intentCount: params.settings.intents.length,
4687
+ intentKeys: params.settings.intents.map((i) => i.intent),
4688
+ model: params.model?.trim() || "(gateway default)",
4689
+ messageChars: params.message.length,
4690
+ historyTurns: params.history.length,
4691
+ hasClassifierInstructions: Boolean(
4692
+ params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim()
4693
+ )
4694
+ });
4695
+ const transcript = formatTranscript(params.history, params.message);
4696
+ const contactLines = [
4697
+ params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
4698
+ params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
4699
+ params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
4700
+ ].filter(Boolean).join("\n");
4701
+ const userPrompt = `Customer:
4702
+ ${contactLines || "(unknown)"}
4703
+
4704
+ Conversation:
4705
+ ${transcript}
4706
+
4707
+ Pick the single best intent for the visitor's latest message.`;
4708
+ const res = await llm.chatAgent({
4709
+ systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
4710
+ userPrompt,
4711
+ temperature: 0.1,
4712
+ max_tokens: 256,
4713
+ ...params.model?.trim() ? { model: params.model.trim() } : {}
4714
+ });
4715
+ const rawContent = res.content ?? "";
4716
+ logChatEmail("classify llm response", {
4717
+ responseChars: rawContent.length,
4718
+ responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
4719
+ });
4720
+ const parsed = parseIntentJson(rawContent, params.settings.intents);
4721
+ if (!parsed) {
4722
+ logChatEmail("classify result", {
4723
+ matched: false,
4724
+ outcome: "parse_failed",
4725
+ emailWillSend: false
4726
+ });
4727
+ return {
4728
+ intent: null,
4729
+ intentLabel: "Unclassified",
4730
+ reason: "Could not parse intent classifier response",
4731
+ emailTo: null
4732
+ };
4733
+ }
4734
+ if (parsed.intent === NONE_INTENT) {
4735
+ logChatEmail("classify result", {
4736
+ matched: false,
4737
+ outcome: NONE_INTENT,
4738
+ reason: parsed.reason,
4739
+ emailWillSend: false
4740
+ });
4741
+ return {
4742
+ intent: null,
4743
+ intentLabel: NONE_INTENT,
4744
+ reason: parsed.reason,
4745
+ emailTo: null
4746
+ };
4747
+ }
4748
+ const matched = intentByKey(params.settings.intents, parsed.intent);
4749
+ if (!matched) {
4750
+ logChatEmail("classify result", {
4751
+ matched: false,
4752
+ outcome: "unknown_intent",
4753
+ parsedIntent: parsed.intent,
4754
+ reason: parsed.reason,
4755
+ emailWillSend: false
4756
+ });
4757
+ return {
4758
+ intent: null,
4759
+ intentLabel: parsed.intent,
4760
+ reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
4761
+ emailTo: null
4762
+ };
4763
+ }
4764
+ logChatEmail("classify result", {
4765
+ matched: true,
4766
+ outcome: "intent_found",
4767
+ intent: matched.intent,
4768
+ emailTo: matched.emailTo,
4769
+ reason: parsed.reason,
4770
+ emailWillSend: true
4771
+ });
4772
+ return {
4773
+ intent: matched.intent,
4774
+ intentLabel: `${matched.intent} \u2014 ${matched.description}`,
4775
+ reason: parsed.reason,
4776
+ emailTo: matched.emailTo
4777
+ };
4778
+ }
4779
+ function buildTranscriptForLeadEmail(history, latestMessage) {
4780
+ return formatTranscript(history, latestMessage);
4781
+ }
4782
+ function parseIntentRecipientEmails(emailTo) {
4783
+ return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
4784
+ }
4785
+
4786
+ // src/plugins/llm/find-llm-agent-by-scope.ts
4787
+ async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
4788
+ const { enabledOnly = true } = options;
4789
+ const entity = entityMap.llm_agents;
4790
+ if (!entity) return null;
4791
+ const repo = dataSource.getRepository(entity);
4792
+ const where = { scope, deleted: false };
4793
+ if (enabledOnly) where.enabled = true;
4794
+ return repo.findOne({ where });
4795
+ }
4796
+
4797
+ // src/plugins/email/templates/types.ts
4798
+ function normalizeSocialLinkItem(o) {
4799
+ const url = String(o.url ?? "").trim();
4800
+ if (!url) return null;
4801
+ let iconUrl = String(o.iconUrl ?? o.icon_image ?? "").trim();
4802
+ let icon = String(o.icon ?? "").trim();
4803
+ if (!iconUrl && /^https?:\/\//i.test(icon)) {
4804
+ iconUrl = icon;
4805
+ icon = "";
4806
+ }
4807
+ const item = { url };
4808
+ if (iconUrl) item.iconUrl = iconUrl;
4809
+ if (icon) item.icon = icon;
4810
+ return item;
4811
+ }
4812
+ function parseSocialLinksJson(raw) {
4813
+ if (raw == null || raw.trim() === "") return void 0;
4814
+ try {
4815
+ const parsed = JSON.parse(raw);
4816
+ if (!Array.isArray(parsed)) return void 0;
4817
+ const out = [];
4818
+ for (const item of parsed) {
4819
+ if (item && typeof item === "object" && "url" in item) {
4820
+ const n = normalizeSocialLinkItem(item);
4821
+ if (n) out.push(n);
4822
+ }
4823
+ }
4824
+ return out.length ? out : void 0;
4825
+ } catch {
4826
+ return void 0;
4827
+ }
4828
+ }
4829
+ function mergeEmailLayoutCompanyDetails(branding, emailSettings) {
4830
+ const fromBranding = getCompanyDetailsFromSettings(branding);
4831
+ const pick = (emailVal, fallback) => {
4832
+ const t = emailVal?.trim();
4833
+ return t || fallback?.trim() || void 0;
4834
+ };
4835
+ const logoUrl = pick(emailSettings.logoUrl ?? emailSettings.emailLogoUrl, fromBranding.logoUrl);
4836
+ const companyName = pick(emailSettings.companyName ?? emailSettings.emailCompanyName, fromBranding.companyName);
4837
+ const supportEmail = pick(emailSettings.supportEmail ?? emailSettings.emailSupportEmail, fromBranding.supportEmail);
4838
+ const supportPhone = pick(emailSettings.supportPhone, void 0);
4839
+ const footerDisclaimer = pick(emailSettings.footerDisclaimer, void 0);
4840
+ const followUsTitle = pick(emailSettings.followUsTitle, "Follow Us") || "Follow Us";
4841
+ const socialFromEmail = parseSocialLinksJson(emailSettings.socialLinks);
4842
+ const socialLinks = socialFromEmail?.length ? socialFromEmail : fromBranding.socialLinks;
4843
+ return {
4844
+ logoUrl,
4845
+ companyName,
4846
+ supportEmail,
4847
+ supportPhone,
4848
+ socialLinks,
4849
+ footerDisclaimer,
4850
+ followUsTitle
4851
+ };
4852
+ }
4853
+ function getCompanyDetailsFromSettings(settingsGroup) {
4854
+ const logoUrl = settingsGroup.logo ?? settingsGroup.logoUrl ?? "";
4855
+ const companyName = settingsGroup.companyName ?? settingsGroup.company_name ?? "";
4856
+ const supportEmail = settingsGroup.supportEmail ?? settingsGroup.support_email ?? "";
4857
+ let socialLinks = [];
4858
+ const raw = settingsGroup.socialLinks ?? settingsGroup.social_links;
4859
+ if (typeof raw === "string") {
4860
+ try {
4861
+ const arr = JSON.parse(raw);
4862
+ if (Array.isArray(arr)) {
4863
+ for (const item of arr) {
4864
+ if (item && typeof item === "object") {
4865
+ const n = normalizeSocialLinkItem(item);
4866
+ if (n) socialLinks.push(n);
4867
+ }
4868
+ }
4869
+ }
4870
+ } catch {
4871
+ }
4872
+ }
4873
+ return { logoUrl: logoUrl || void 0, companyName: companyName || void 0, supportEmail: supportEmail || void 0, socialLinks: socialLinks.length ? socialLinks : void 0 };
4874
+ }
4875
+
4876
+ // src/plugins/email/chat-lead-email.ts
4877
+ init_email_queue();
4878
+
4879
+ // src/lib/email-recipients.ts
4880
+ function parseEmailRecipientsFromConfig(raw) {
4881
+ if (raw == null || raw === "") return [];
4882
+ const trimmed = raw.trim();
4883
+ if (trimmed.startsWith("[")) {
4884
+ try {
4885
+ const parsed = JSON.parse(trimmed);
4886
+ if (Array.isArray(parsed)) {
4887
+ return parsed.map((e) => String(e).trim()).filter(Boolean);
4888
+ }
4889
+ } catch {
4890
+ }
4891
+ }
4892
+ return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
4893
+ }
4894
+
4895
+ // src/plugins/email/chat-lead-email.ts
4896
+ function resolveLeadRecipients(emailPlugin, emailSettings) {
4897
+ const fromCrm = parseEmailRecipientsFromConfig(
4898
+ emailSettings.crmEmails ?? emailSettings.crmEmail ?? ""
4899
+ );
4900
+ if (fromCrm.length > 0) return fromCrm;
4901
+ const fallback = emailPlugin.getDefaultTo?.() ?? "";
4902
+ if (fallback.trim()) return [fallback.trim()];
4903
+ return [];
4904
+ }
4905
+ async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
4906
+ console.info(CHAT_EMAIL_LOG, "send start", {
4907
+ conversationId: input.conversationId,
4908
+ intentCode: input.intentCode ?? null,
4909
+ contactEmail: input.contactEmail,
4910
+ contactName: input.contactName,
4911
+ explicitRecipients: input.recipients?.length ?? 0
4912
+ });
4913
+ const email = cms.getPlugin("email");
4914
+ if (!email?.send || !email.renderTemplate) {
4915
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
4916
+ conversationId: input.conversationId,
4917
+ reason: "email_plugin_disabled"
4918
+ });
4919
+ return { sent: false, recipients: [], error: "Email plugin is not enabled" };
4920
+ }
4921
+ const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
4922
+ if (recipients.length === 0) {
4923
+ console.warn(CHAT_EMAIL_LOG, "send failed", {
4924
+ conversationId: input.conversationId,
4925
+ reason: "no_recipients"
4926
+ });
4927
+ return {
4928
+ sent: false,
4929
+ recipients: [],
4930
+ error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
4931
+ };
4932
+ }
4933
+ console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
4934
+ conversationId: input.conversationId,
4935
+ recipients,
4936
+ source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
4937
+ });
4938
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
4939
+ const ctx = {
4940
+ ...input,
4941
+ companyDetails: input.companyDetails ?? companyDetails
4942
+ };
4943
+ let anySent = false;
4944
+ for (const to of recipients) {
4945
+ await queueEmail(cms, {
4946
+ to,
4947
+ templateName: "chatLead",
4948
+ ctx
4949
+ });
4950
+ console.info(CHAT_EMAIL_LOG, "send queued", {
4951
+ conversationId: input.conversationId,
4952
+ to,
4953
+ template: "chatLead"
4954
+ });
4955
+ anySent = true;
4956
+ }
4957
+ console.info(CHAT_EMAIL_LOG, "send complete", {
4958
+ conversationId: input.conversationId,
4959
+ sent: anySent,
4960
+ recipientCount: recipients.length
4961
+ });
4962
+ return { sent: anySent, recipients };
4963
+ }
4964
+
4343
4965
  // src/lib/media-folder-path.ts
4344
4966
  function sanitizeMediaFolderPath(input) {
4345
4967
  if (input == null) return "";
@@ -5819,16 +6441,19 @@ function normalizeChatModeSetting(raw) {
5819
6441
  if (raw === "external" || raw === "llm") return raw;
5820
6442
  return "whatsapp";
5821
6443
  }
5822
- async function loadLlmSettingsMap(dataSource, entityMap) {
6444
+ async function loadSettingsGroupMap(dataSource, entityMap, group) {
5823
6445
  if (!entityMap.configs) return {};
5824
6446
  const repo = dataSource.getRepository(entityMap.configs);
5825
- const rows = await repo.find({ where: { settings: "llm", deleted: false } });
6447
+ const rows = await repo.find({ where: { settings: group, deleted: false } });
5826
6448
  const out = {};
5827
6449
  for (const row of rows) {
5828
6450
  out[row.key] = row.value;
5829
6451
  }
5830
6452
  return out;
5831
6453
  }
6454
+ async function loadLlmSettingsMap(dataSource, entityMap) {
6455
+ return loadSettingsGroupMap(dataSource, entityMap, "llm");
6456
+ }
5832
6457
  function createChatHandlers(config) {
5833
6458
  const { dataSource, entityMap, json, getCms } = config;
5834
6459
  const contactRepo = () => dataSource.getRepository(entityMap.contacts);
@@ -5840,10 +6465,13 @@ function createChatHandlers(config) {
5840
6465
  try {
5841
6466
  const map = await loadLlmSettingsMap(dataSource, entityMap);
5842
6467
  const mode = normalizeChatModeSetting(map.chatMode);
6468
+ const chatbotAgent = mode === "llm" && entityMap.llm_agents ? await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT, {
6469
+ enabledOnly: false
6470
+ }) : null;
5843
6471
  const body = {
5844
6472
  enabled: map.enabled !== "false",
5845
6473
  chatMode: mode,
5846
- agentSlug: mode === "llm" ? (map.attachedAgentSlug ?? "").trim() : "",
6474
+ agentSlug: mode === "llm" ? chatbotAgent?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT] : "",
5847
6475
  botName: map.botName ?? "",
5848
6476
  icon: map.icon ?? "",
5849
6477
  iconImageUrl: map.iconImageUrl ?? "",
@@ -5929,24 +6557,26 @@ function createChatHandlers(config) {
5929
6557
  if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
5930
6558
  const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
5931
6559
  const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
5932
- let effectiveSlug = (body?.agentSlug ?? "").trim();
5933
- if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
5934
- effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
5935
- }
6560
+ const bodyAgentSlug = (body?.agentSlug ?? "").trim();
5936
6561
  let agentRow = null;
5937
- if (effectiveSlug) {
5938
- if (!entityMap.llm_agents) {
5939
- return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
5940
- }
5941
- const agentRepo = dataSource.getRepository(
5942
- entityMap.llm_agents
5943
- );
5944
- agentRow = await agentRepo.findOne({
5945
- where: { slug: effectiveSlug, deleted: false, enabled: true }
5946
- });
5947
- if (!agentRow && (body?.agentSlug ?? "").trim()) {
5948
- return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
6562
+ let effectiveSlug = bodyAgentSlug;
6563
+ if (entityMap.llm_agents) {
6564
+ if (bodyAgentSlug) {
6565
+ const agentRepo = dataSource.getRepository(
6566
+ entityMap.llm_agents
6567
+ );
6568
+ agentRow = await agentRepo.findOne({
6569
+ where: { slug: bodyAgentSlug, deleted: false, enabled: true }
6570
+ });
6571
+ if (!agentRow) {
6572
+ return json({ error: "Agent not found or disabled", agentSlug: bodyAgentSlug }, { status: 404 });
6573
+ }
6574
+ } else if (supportMode === "llm") {
6575
+ agentRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT);
6576
+ effectiveSlug = agentRow?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
5949
6577
  }
6578
+ } else if (bodyAgentSlug) {
6579
+ return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
5950
6580
  }
5951
6581
  console.info(RAG_LOG, "step 1 | resolve agent", {
5952
6582
  agentSlug: effectiveSlug || "(none)",
@@ -6044,6 +6674,139 @@ function createChatHandlers(config) {
6044
6674
  }
6045
6675
  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 }));
6046
6676
  const history = historyBeforeCurrentUser(historyRaw, message);
6677
+ const notifyEmailAgent = await findLlmAgentByScope(
6678
+ dataSource,
6679
+ entityMap,
6680
+ LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
6681
+ { enabledOnly: false }
6682
+ );
6683
+ const emailTool = resolveChatEmailToolSettings(llmSettings, {
6684
+ chatbotValidationRules: agentRow?.validationRules ?? null,
6685
+ notifyAgent: notifyEmailAgent ? {
6686
+ systemInstruction: notifyEmailAgent.systemInstruction,
6687
+ validationRules: notifyEmailAgent.validationRules
6688
+ } : null
6689
+ });
6690
+ const emailPlugin = cms.getPlugin("email");
6691
+ const convLeadSent = conv.leadEmailSentAt;
6692
+ const chatAgentFn = llm.chatAgent?.bind(llm);
6693
+ console.info(CHAT_EMAIL_LOG, "pipeline check", {
6694
+ conversationId,
6695
+ enabled: emailTool.enabled,
6696
+ intentCount: emailTool.intents.length,
6697
+ chatbotAgentId: agentRow?.id ?? null,
6698
+ chatbotAgentSlug: agentRow?.slug ?? null,
6699
+ notifyEmailAgentId: notifyEmailAgent?.id ?? null,
6700
+ notifyEmailAgentSlug: notifyEmailAgent?.slug ?? null,
6701
+ emailPluginPresent: Boolean(emailPlugin),
6702
+ chatAgentFnPresent: Boolean(chatAgentFn),
6703
+ leadEmailAlreadySent: Boolean(convLeadSent),
6704
+ mergedPromptIncludesNotify: Boolean(notifyEmailAgent?.systemInstruction?.trim())
6705
+ });
6706
+ if (!emailTool.enabled) {
6707
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6708
+ conversationId,
6709
+ reason: "email_tool_disabled"
6710
+ });
6711
+ } else if (emailTool.intents.length === 0) {
6712
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6713
+ conversationId,
6714
+ reason: "no_intents_configured"
6715
+ });
6716
+ } else if (!emailPlugin) {
6717
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6718
+ conversationId,
6719
+ reason: "email_plugin_missing"
6720
+ });
6721
+ } else if (!chatAgentFn) {
6722
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6723
+ conversationId,
6724
+ reason: "llm_chat_agent_unavailable"
6725
+ });
6726
+ } else if (convLeadSent) {
6727
+ console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
6728
+ conversationId,
6729
+ reason: "lead_email_already_sent",
6730
+ leadEmailSentAt: convLeadSent
6731
+ });
6732
+ } else {
6733
+ const contactId = conv.contactId;
6734
+ const contactRow = await contactRepo().findOne({
6735
+ where: { id: contactId }
6736
+ });
6737
+ if (!contactRow) {
6738
+ console.warn(CHAT_EMAIL_LOG, "pipeline skipped", {
6739
+ conversationId,
6740
+ reason: "contact_not_found",
6741
+ contactId
6742
+ });
6743
+ } else {
6744
+ const c = contactRow;
6745
+ try {
6746
+ const intent = await detectChatLeadIntent({ chatAgent: chatAgentFn }, {
6747
+ settings: emailTool,
6748
+ message,
6749
+ history,
6750
+ contact: { name: c.name, email: c.email, phone: c.phone },
6751
+ model: agentRow?.model?.trim() || notifyEmailAgent?.model?.trim() || void 0
6752
+ });
6753
+ if (!intent.intent || !intent.emailTo) {
6754
+ console.info(CHAT_EMAIL_LOG, "no email sent", {
6755
+ conversationId,
6756
+ reason: intent.intent ? "missing_email_to" : "no_intent_match",
6757
+ intentLabel: intent.intentLabel,
6758
+ classifierReason: intent.reason
6759
+ });
6760
+ } else {
6761
+ const intentRecipients = parseIntentRecipientEmails(intent.emailTo);
6762
+ console.info(CHAT_EMAIL_LOG, "intent matched \u2014 sending", {
6763
+ conversationId,
6764
+ intent: intent.intent,
6765
+ emailTo: intent.emailTo,
6766
+ parsedRecipientCount: intentRecipients.length,
6767
+ recipients: intentRecipients
6768
+ });
6769
+ const emailSettings = await loadSettingsGroupMap(dataSource, entityMap, "email");
6770
+ const brandingSettings = await loadSettingsGroupMap(dataSource, entityMap, "branding");
6771
+ const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
6772
+ const leadResult = await sendChatLeadEmail(cms, emailSettings, brandingSettings, {
6773
+ contactName: String(c.name ?? "").trim() || "Visitor",
6774
+ contactEmail: String(c.email ?? "").trim(),
6775
+ contactPhone: c.phone ?? null,
6776
+ conversationId,
6777
+ latestMessage: message,
6778
+ intentCode: intent.intent,
6779
+ intentReason: intent.intentLabel || intent.reason,
6780
+ transcript: buildTranscriptForLeadEmail(history, message),
6781
+ companyDetails,
6782
+ recipients: intentRecipients.length > 0 ? intentRecipients : void 0
6783
+ });
6784
+ if (leadResult.sent) {
6785
+ await convRepo().update(conversationId, {
6786
+ leadEmailSentAt: /* @__PURE__ */ new Date()
6787
+ });
6788
+ console.info(CHAT_EMAIL_LOG, "lead email recorded", {
6789
+ conversationId,
6790
+ sent: true,
6791
+ recipients: leadResult.recipients
6792
+ });
6793
+ } else {
6794
+ console.warn(CHAT_EMAIL_LOG, "lead email not sent", {
6795
+ conversationId,
6796
+ sent: false,
6797
+ error: leadResult.error ?? "unknown",
6798
+ recipients: leadResult.recipients
6799
+ });
6800
+ }
6801
+ }
6802
+ } catch (intentErr) {
6803
+ console.warn(CHAT_EMAIL_LOG, "pipeline error", {
6804
+ conversationId,
6805
+ error: intentErr instanceof Error ? intentErr.message : String(intentErr)
6806
+ });
6807
+ }
6808
+ }
6809
+ }
6047
6810
  let content;
6048
6811
  const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
6049
6812
  console.info(RAG_LOG, "step 7 | final context", {
@@ -6054,9 +6817,10 @@ function createChatHandlers(config) {
6054
6817
  });
6055
6818
  if (agentRow && llm.chatAgent) {
6056
6819
  const fromAgent = llmAgentToChatAgentOptions(agentRow);
6057
- const systemPrompt = mergeGuardrailsIntoSystemPrompt(
6820
+ const systemPrompt = buildChatbotSystemPromptWithEmailTool(
6058
6821
  fromAgent.systemPrompt,
6059
- parsedValidation.guardrailsForPrompt
6822
+ parsedValidation.guardrailsForPrompt,
6823
+ emailTool
6060
6824
  );
6061
6825
  const res = await llm.chatAgent({
6062
6826
  ...fromAgent,
@@ -6074,11 +6838,12 @@ ${contextParts.join("\n\n")}` : "";
6074
6838
  const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
6075
6839
  let systemContent;
6076
6840
  if (agentRow) {
6077
- const base = agentRow.systemInstruction?.trim() || "";
6078
- systemContent = mergeGuardrailsIntoSystemPrompt(
6079
- [base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
6080
- parsedValidation.guardrailsForPrompt
6081
- );
6841
+ const mergedBase = [agentRow.systemInstruction?.trim(), ragSystem].filter(Boolean).join("\n\n");
6842
+ systemContent = buildChatbotSystemPromptWithEmailTool(
6843
+ mergedBase || defaultSystem,
6844
+ parsedValidation.guardrailsForPrompt,
6845
+ emailTool
6846
+ ) || defaultSystem;
6082
6847
  } else {
6083
6848
  systemContent = ragSystem || defaultSystem;
6084
6849
  }
@@ -8603,6 +9368,7 @@ var Order = class {
8603
9368
  id;
8604
9369
  vendorId;
8605
9370
  orderNumber;
9371
+ qrToken;
8606
9372
  orderKind;
8607
9373
  parentOrderId;
8608
9374
  contactId;
@@ -8640,6 +9406,9 @@ __decorateClass([
8640
9406
  __decorateClass([
8641
9407
  Column18("varchar")
8642
9408
  ], Order.prototype, "orderNumber", 2);
9409
+ __decorateClass([
9410
+ Column18("varchar", { unique: true, nullable: true })
9411
+ ], Order.prototype, "qrToken", 2);
8643
9412
  __decorateClass([
8644
9413
  Column18("varchar", { default: "sale" })
8645
9414
  ], Order.prototype, "orderKind", 2);
@@ -8868,6 +9637,7 @@ var ChatConversation = class {
8868
9637
  contactId;
8869
9638
  createdAt;
8870
9639
  updatedAt;
9640
+ leadEmailSentAt;
8871
9641
  contact;
8872
9642
  messages;
8873
9643
  };
@@ -8883,6 +9653,9 @@ __decorateClass([
8883
9653
  __decorateClass([
8884
9654
  Column21({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
8885
9655
  ], ChatConversation.prototype, "updatedAt", 2);
9656
+ __decorateClass([
9657
+ Column21({ type: "timestamp", nullable: true })
9658
+ ], ChatConversation.prototype, "leadEmailSentAt", 2);
8886
9659
  __decorateClass([
8887
9660
  ManyToOne12(() => Contact, (c) => c.chatConversations, { onDelete: "CASCADE" }),
8888
9661
  JoinColumn12({ name: "contactId" })
@@ -11123,7 +11896,6 @@ var CMS_ENTITY_MAP = {
11123
11896
  import Parser from "rss-parser";
11124
11897
 
11125
11898
  // src/plugins/blog-generator/blog-generator-agent-defaults.ts
11126
- var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
11127
11899
  var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
11128
11900
  var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
11129
11901
 
@@ -11203,7 +11975,6 @@ var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
11203
11975
  );
11204
11976
 
11205
11977
  // src/plugins/blog-generator/blog-generator-metadata-defaults.ts
11206
- var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
11207
11978
  var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11208
11979
  {
11209
11980
  maxUserChars: 5e5,
@@ -11214,7 +11985,6 @@ var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11214
11985
  );
11215
11986
 
11216
11987
  // src/plugins/blog-generator/blog-generator-social-defaults.ts
11217
- var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
11218
11988
  var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
11219
11989
  {
11220
11990
  maxUserChars: 5e5,
@@ -13341,28 +14111,24 @@ function createCmsApiHandler(config) {
13341
14111
  let socialLlmAgentResolution = null;
13342
14112
  let metadataRow = null;
13343
14113
  let socialRow = null;
14114
+ let blogCreationRow = null;
13344
14115
  if (entityMap.llm_agents) {
13345
- const agentRepo = dataSource.getRepository(entityMap.llm_agents);
13346
- const agentRow = await agentRepo.findOne({
13347
- where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
13348
- });
13349
- if (agentRow) {
13350
- const o = llmAgentToChatAgentOptions(agentRow);
14116
+ blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
14117
+ if (blogCreationRow) {
14118
+ const o = llmAgentToChatAgentOptions(blogCreationRow);
13351
14119
  llmAgentChatOptions = {
13352
14120
  model: o.model,
13353
14121
  temperature: o.temperature,
13354
14122
  max_tokens: o.max_tokens
13355
14123
  };
13356
14124
  llmAgentResolution = {
13357
- slug: BLOG_GENERATOR_LLM_AGENT_SLUG,
13358
- model: agentRow.model?.trim() || null,
13359
- temperature: agentRow.temperature ?? null,
13360
- maxTokens: agentRow.maxTokens ?? null
14125
+ slug: blogCreationRow.slug,
14126
+ model: blogCreationRow.model?.trim() || null,
14127
+ temperature: blogCreationRow.temperature ?? null,
14128
+ maxTokens: blogCreationRow.maxTokens ?? null
13361
14129
  };
13362
14130
  }
13363
- metadataRow = await agentRepo.findOne({
13364
- where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
13365
- });
14131
+ metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
13366
14132
  if (metadataRow) {
13367
14133
  const mo = llmAgentToChatAgentOptions(metadataRow);
13368
14134
  metadataLlmAgentChatOptions = {
@@ -13371,15 +14137,13 @@ function createCmsApiHandler(config) {
13371
14137
  max_tokens: mo.max_tokens
13372
14138
  };
13373
14139
  metadataLlmAgentResolution = {
13374
- slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG,
14140
+ slug: metadataRow.slug,
13375
14141
  model: metadataRow.model?.trim() || null,
13376
14142
  temperature: metadataRow.temperature ?? null,
13377
14143
  maxTokens: metadataRow.maxTokens ?? null
13378
14144
  };
13379
14145
  }
13380
- socialRow = await agentRepo.findOne({
13381
- where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
13382
- });
14146
+ socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
13383
14147
  if (socialRow) {
13384
14148
  const so = llmAgentToChatAgentOptions(socialRow);
13385
14149
  socialLlmAgentChatOptions = {
@@ -13388,7 +14152,7 @@ function createCmsApiHandler(config) {
13388
14152
  max_tokens: so.max_tokens
13389
14153
  };
13390
14154
  socialLlmAgentResolution = {
13391
- slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG,
14155
+ slug: socialRow.slug,
13392
14156
  model: socialRow.model?.trim() || null,
13393
14157
  temperature: socialRow.temperature ?? null,
13394
14158
  maxTokens: socialRow.maxTokens ?? null
@@ -13412,8 +14176,8 @@ function createCmsApiHandler(config) {
13412
14176
  const out = await svc.generateBlogMarkdownFromRss({
13413
14177
  llm,
13414
14178
  rssUrls,
13415
- systemInstruction,
13416
- validationRules,
14179
+ systemInstruction: systemInstruction ?? blogCreationRow?.systemInstruction?.trim() ?? void 0,
14180
+ validationRules: validationRules ?? blogCreationRow?.validationRules?.trim() ?? void 0,
13417
14181
  categoryNamesHint: categoryRows.map((c) => c.name),
13418
14182
  tagNamesHint: tagNames,
13419
14183
  llmAgentChatOptions,
@@ -13898,6 +14662,22 @@ function createCmsApiHandler(config) {
13898
14662
  return config.json({ error: message }, { status: 500 });
13899
14663
  }
13900
14664
  }
14665
+ if (path2[0] === "track" && path2.length === 2 && m === "GET") {
14666
+ const token = path2[1];
14667
+ if (!token) return config.json({ error: "Token required" }, { status: 400 });
14668
+ try {
14669
+ const orderRepo = dataSource.getRepository(entityMap.orders);
14670
+ const order = await orderRepo.findOne({
14671
+ where: { qrToken: token, deleted: false },
14672
+ relations: ["contact", "items", "items.product"]
14673
+ });
14674
+ if (!order) return config.json({ error: "Order not found" }, { status: 404 });
14675
+ return config.json(order);
14676
+ } catch (err) {
14677
+ const message = err instanceof Error ? err.message : String(err);
14678
+ return config.json({ error: message }, { status: 500 });
14679
+ }
14680
+ }
13901
14681
  if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
13902
14682
  const resource = resolveResource(path2[0]);
13903
14683
  if (!crudResources.includes(resource)) {
@@ -15099,8 +15879,8 @@ function createStorefrontApiHandler(config) {
15099
15879
  let emailVerificationSent = false;
15100
15880
  if (requireEmailVerification && getCms) {
15101
15881
  try {
15102
- const crypto2 = await import("crypto");
15103
- const rawToken = crypto2.randomBytes(32).toString("hex");
15882
+ const crypto3 = await import("crypto");
15883
+ const rawToken = crypto3.randomBytes(32).toString("hex");
15104
15884
  const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
15105
15885
  await tokenRepo().save(
15106
15886
  tokenRepo().create({ email, token: rawToken, expiresAt })