@datasynx/agentic-crm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +767 -0
  3. package/dist/agent-config-zPvcqu07.js +14 -0
  4. package/dist/agent-config-zPvcqu07.js.map +1 -0
  5. package/dist/approvals-DpjxGHFp.js +67 -0
  6. package/dist/approvals-DpjxGHFp.js.map +1 -0
  7. package/dist/ask-CID3jnuL.js +52 -0
  8. package/dist/ask-CID3jnuL.js.map +1 -0
  9. package/dist/audit-log-DNMY9mUZ.js +49 -0
  10. package/dist/audit-log-DNMY9mUZ.js.map +1 -0
  11. package/dist/auth-CyFuu9X_.js +2 -0
  12. package/dist/auth-DFWwWcYD.js +93 -0
  13. package/dist/auth-DFWwWcYD.js.map +1 -0
  14. package/dist/autofill-Di_-SP7t.js +51 -0
  15. package/dist/autofill-Di_-SP7t.js.map +1 -0
  16. package/dist/backup-CeMk9z86.js +417 -0
  17. package/dist/backup-CeMk9z86.js.map +1 -0
  18. package/dist/backup-f_hC7rBV.js +2 -0
  19. package/dist/calendly-Bft_wwji.js +52 -0
  20. package/dist/calendly-Bft_wwji.js.map +1 -0
  21. package/dist/calendly-D3coO92o.cjs +53 -0
  22. package/dist/calendly-D3coO92o.cjs.map +1 -0
  23. package/dist/chunk-DakpK96I.cjs +43 -0
  24. package/dist/churn-C28IgnAj.js +54 -0
  25. package/dist/churn-C28IgnAj.js.map +1 -0
  26. package/dist/cli.js +4396 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/colors-BG07TZQz.js +11 -0
  29. package/dist/colors-BG07TZQz.js.map +1 -0
  30. package/dist/compliance-B1kk5-YS.js +115 -0
  31. package/dist/compliance-B1kk5-YS.js.map +1 -0
  32. package/dist/compliance-B91zNvCR.cjs +156 -0
  33. package/dist/compliance-B91zNvCR.cjs.map +1 -0
  34. package/dist/compliance-CKSBoQUe.js +118 -0
  35. package/dist/compliance-CKSBoQUe.js.map +1 -0
  36. package/dist/compliance-CujOqAKk.js +2 -0
  37. package/dist/context-builder-BzWAp3Zs.js +96 -0
  38. package/dist/context-builder-BzWAp3Zs.js.map +1 -0
  39. package/dist/context-builder-DlrRcqmJ.js +2 -0
  40. package/dist/conversation-intel-mm7Lhemh.js +72 -0
  41. package/dist/conversation-intel-mm7Lhemh.js.map +1 -0
  42. package/dist/custom-fields-CzNeD3_v.js +2 -0
  43. package/dist/custom-fields-Pl2t9xzp.js +73 -0
  44. package/dist/custom-fields-Pl2t9xzp.js.map +1 -0
  45. package/dist/custom-objects-BHgn1GEX.js +78 -0
  46. package/dist/custom-objects-BHgn1GEX.js.map +1 -0
  47. package/dist/custom-objects-CIFrmQ2V.js +2 -0
  48. package/dist/customer-dir-DIylZ8Q6.js +75 -0
  49. package/dist/customer-dir-DIylZ8Q6.js.map +1 -0
  50. package/dist/daemon/worker.js +207 -0
  51. package/dist/daemon/worker.js.map +1 -0
  52. package/dist/enrichment-3XvgGDfB.js +103 -0
  53. package/dist/enrichment-3XvgGDfB.js.map +1 -0
  54. package/dist/file-lock-B_zi7NQl.js +22 -0
  55. package/dist/file-lock-B_zi7NQl.js.map +1 -0
  56. package/dist/gmail-auth-BP6cJwfw.js +40 -0
  57. package/dist/gmail-auth-BP6cJwfw.js.map +1 -0
  58. package/dist/gmail-auth-DxakCtGm.cjs +44 -0
  59. package/dist/gmail-auth-DxakCtGm.cjs.map +1 -0
  60. package/dist/gmail-auth-OComS92L.js +40 -0
  61. package/dist/gmail-auth-OComS92L.js.map +1 -0
  62. package/dist/gmail-push-watch-DELQFMPk.js +20 -0
  63. package/dist/gmail-push-watch-DELQFMPk.js.map +1 -0
  64. package/dist/gmail-sender-StTpJ9Ub.js +32 -0
  65. package/dist/gmail-sender-StTpJ9Ub.js.map +1 -0
  66. package/dist/gmail-sync-DIaxInDT.js +204 -0
  67. package/dist/gmail-sync-DIaxInDT.js.map +1 -0
  68. package/dist/gmail-sync-hHm9gaWd.cjs +218 -0
  69. package/dist/gmail-sync-hHm9gaWd.cjs.map +1 -0
  70. package/dist/gmail-sync-rQaVqKWd.js +214 -0
  71. package/dist/gmail-sync-rQaVqKWd.js.map +1 -0
  72. package/dist/gmail-webhook-handler-DS7OlRPX.js +3 -0
  73. package/dist/gmail-webhook-handler-e5Od25FX.js +97 -0
  74. package/dist/gmail-webhook-handler-e5Od25FX.js.map +1 -0
  75. package/dist/goal-engine-CUZSpERI.js +2 -0
  76. package/dist/goal-engine-KpBftn4V.js +295 -0
  77. package/dist/goal-engine-KpBftn4V.js.map +1 -0
  78. package/dist/google-drive-sync-DEPcqFca.js +105 -0
  79. package/dist/google-drive-sync-DEPcqFca.js.map +1 -0
  80. package/dist/hybrid-search-BmHttLrR.js +40 -0
  81. package/dist/hybrid-search-BmHttLrR.js.map +1 -0
  82. package/dist/hygiene-DZqfYpFf.js +38 -0
  83. package/dist/hygiene-DZqfYpFf.js.map +1 -0
  84. package/dist/identity-CI6olMNm.js +41 -0
  85. package/dist/identity-CI6olMNm.js.map +1 -0
  86. package/dist/identity-gyfWdrcX.js +2 -0
  87. package/dist/import-hubspot-BaK71U_K.js +588 -0
  88. package/dist/import-hubspot-BaK71U_K.js.map +1 -0
  89. package/dist/index-V8BFaH-b.d.ts +539 -0
  90. package/dist/index-V8BFaH-b.d.ts.map +1 -0
  91. package/dist/index-YqwMd6aQ.d.cts +538 -0
  92. package/dist/index-YqwMd6aQ.d.cts.map +1 -0
  93. package/dist/index.cjs +185 -0
  94. package/dist/index.cjs.map +1 -0
  95. package/dist/index.d.cts +538 -0
  96. package/dist/index.d.cts.map +1 -0
  97. package/dist/index.d.ts +539 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +165 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/interactions-writer-CrPStUll.cjs +77 -0
  102. package/dist/interactions-writer-CrPStUll.cjs.map +1 -0
  103. package/dist/interactions-writer-DO3KcSR3.js +52 -0
  104. package/dist/interactions-writer-DO3KcSR3.js.map +1 -0
  105. package/dist/interactions-writer-SLHnoEeE.js +46 -0
  106. package/dist/interactions-writer-SLHnoEeE.js.map +1 -0
  107. package/dist/interactions-writer-dSPy1XfO.js +2 -0
  108. package/dist/knowledge-base-D0Fh40kc.js +1013 -0
  109. package/dist/knowledge-base-D0Fh40kc.js.map +1 -0
  110. package/dist/lancedb-CCBbpulq.js +2 -0
  111. package/dist/lancedb-rlvWoPwl.js +98 -0
  112. package/dist/lancedb-rlvWoPwl.js.map +1 -0
  113. package/dist/lead-model-BCFzyktm.js +109 -0
  114. package/dist/lead-model-BCFzyktm.js.map +1 -0
  115. package/dist/llm-DEjWcqmW.js +2 -0
  116. package/dist/llm-DvzZqva0.js +372 -0
  117. package/dist/llm-DvzZqva0.js.map +1 -0
  118. package/dist/llm-Z8RIYkpF.js +174 -0
  119. package/dist/llm-Z8RIYkpF.js.map +1 -0
  120. package/dist/llm-iijeXmgq.cjs +198 -0
  121. package/dist/llm-iijeXmgq.cjs.map +1 -0
  122. package/dist/mcp-CdTJWTJf.d.cts +12 -0
  123. package/dist/mcp-CdTJWTJf.d.cts.map +1 -0
  124. package/dist/mcp-CdTJWTJf.d.ts +12 -0
  125. package/dist/mcp-CdTJWTJf.d.ts.map +1 -0
  126. package/dist/mcp.cjs +7464 -0
  127. package/dist/mcp.cjs.map +1 -0
  128. package/dist/mcp.d.cts +12 -0
  129. package/dist/mcp.d.cts.map +1 -0
  130. package/dist/mcp.d.ts +12 -0
  131. package/dist/mcp.d.ts.map +1 -0
  132. package/dist/mcp.js +7448 -0
  133. package/dist/mcp.js.map +1 -0
  134. package/dist/memory-Bb6ky3kb.js +58 -0
  135. package/dist/memory-Bb6ky3kb.js.map +1 -0
  136. package/dist/memory-Cy6-Tbyl.js +2 -0
  137. package/dist/metrics-DH8wHvya.js +26 -0
  138. package/dist/metrics-DH8wHvya.js.map +1 -0
  139. package/dist/microsoft-auth-B8_S45gh.js +17 -0
  140. package/dist/microsoft-auth-B8_S45gh.js.map +1 -0
  141. package/dist/microsoft-calendar-B6MMtUQK.js +67 -0
  142. package/dist/microsoft-calendar-B6MMtUQK.js.map +1 -0
  143. package/dist/microsoft-sync-CpZVoSuq.js +68 -0
  144. package/dist/microsoft-sync-CpZVoSuq.js.map +1 -0
  145. package/dist/nba-3wanmJ0U.js +48 -0
  146. package/dist/nba-3wanmJ0U.js.map +1 -0
  147. package/dist/notification-dispatcher-0vYNngWe.js +97 -0
  148. package/dist/notification-dispatcher-0vYNngWe.js.map +1 -0
  149. package/dist/opportunity-score-BTMOQSTV.js +47 -0
  150. package/dist/opportunity-score-BTMOQSTV.js.map +1 -0
  151. package/dist/pipedrive-client-CdGKpH9b.js +17 -0
  152. package/dist/pipedrive-client-CdGKpH9b.js.map +1 -0
  153. package/dist/pipeline-writer-BqBrYrQc.js +2 -0
  154. package/dist/pipeline-writer-BvVquKIe.js +96 -0
  155. package/dist/pipeline-writer-BvVquKIe.js.map +1 -0
  156. package/dist/pipeline-writer-N2omexxp.cjs +121 -0
  157. package/dist/pipeline-writer-N2omexxp.cjs.map +1 -0
  158. package/dist/pipeline-writer-eufx_0o1.js +102 -0
  159. package/dist/pipeline-writer-eufx_0o1.js.map +1 -0
  160. package/dist/proactive-agent-BgQXw3ac.js +96 -0
  161. package/dist/proactive-agent-BgQXw3ac.js.map +1 -0
  162. package/dist/proactive-worker-BrLHNhjH.js +229 -0
  163. package/dist/proactive-worker-BrLHNhjH.js.map +1 -0
  164. package/dist/push-manager-CdqIIkuh.js +108 -0
  165. package/dist/push-manager-CdqIIkuh.js.map +1 -0
  166. package/dist/push-manager-CowY-0IK.js +2 -0
  167. package/dist/quote-generator-BfwENXzg.js +133 -0
  168. package/dist/quote-generator-BfwENXzg.js.map +1 -0
  169. package/dist/quote-generator-OhSFsi3x.js +2 -0
  170. package/dist/rbac-C7c8tcES.js +2 -0
  171. package/dist/rbac-CTIktZaC.js +91 -0
  172. package/dist/rbac-CTIktZaC.js.map +1 -0
  173. package/dist/relationship-health-odxEoQdJ.js +454 -0
  174. package/dist/relationship-health-odxEoQdJ.js.map +1 -0
  175. package/dist/revenue-simulation-BJdRTEHc.js +2 -0
  176. package/dist/revenue-simulation-Bqf2DLVB.js +251 -0
  177. package/dist/revenue-simulation-Bqf2DLVB.js.map +1 -0
  178. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  179. package/dist/salesforce-client-rhZFa_p5.js +51 -0
  180. package/dist/salesforce-client-rhZFa_p5.js.map +1 -0
  181. package/dist/segments-BqcD5HIl.js +61 -0
  182. package/dist/segments-BqcD5HIl.js.map +1 -0
  183. package/dist/sequence-engine-CCTHEBgi.js +2 -0
  184. package/dist/sequence-engine-J1lTW_in.js +91 -0
  185. package/dist/sequence-engine-J1lTW_in.js.map +1 -0
  186. package/dist/sequence-store-DaaWr0Os.js +221 -0
  187. package/dist/sequence-store-DaaWr0Os.js.map +1 -0
  188. package/dist/server-Dyva03K8.js +4287 -0
  189. package/dist/server-Dyva03K8.js.map +1 -0
  190. package/dist/session-B9AilxOE.js +81 -0
  191. package/dist/session-B9AilxOE.js.map +1 -0
  192. package/dist/session-D0qFkBla.cjs +82 -0
  193. package/dist/session-D0qFkBla.cjs.map +1 -0
  194. package/dist/session-D9ub6Wl1.js +79 -0
  195. package/dist/session-D9ub6Wl1.js.map +1 -0
  196. package/dist/session-mWHA71Lw.js +2 -0
  197. package/dist/session-store-B0QZE8Bx.cjs +697 -0
  198. package/dist/session-store-B0QZE8Bx.cjs.map +1 -0
  199. package/dist/session-store-C8tEvMPw.js +543 -0
  200. package/dist/session-store-C8tEvMPw.js.map +1 -0
  201. package/dist/session-store-CEa39Dxs.js +15 -0
  202. package/dist/session-store-CEa39Dxs.js.map +1 -0
  203. package/dist/sla-engine-5IhTsBUR.js +2 -0
  204. package/dist/sla-engine-BqX-7u-7.js +53 -0
  205. package/dist/sla-engine-BqX-7u-7.js.map +1 -0
  206. package/dist/sop-DkhVChGy.js +2 -0
  207. package/dist/sop-Vp0UPWFW.js +70 -0
  208. package/dist/sop-Vp0UPWFW.js.map +1 -0
  209. package/dist/survey-engine-C06hcQt3.js +2 -0
  210. package/dist/survey-engine-DBjCYqCv.js +147 -0
  211. package/dist/survey-engine-DBjCYqCv.js.map +1 -0
  212. package/dist/sync-state-ChaLbamC.js +33 -0
  213. package/dist/sync-state-ChaLbamC.js.map +1 -0
  214. package/dist/sync-state-CwLSt_1m.js +2 -0
  215. package/dist/ticket-writer-CjqKeIRD.js +2 -0
  216. package/dist/ticket-writer-j2oX_Wal.js +134 -0
  217. package/dist/ticket-writer-j2oX_Wal.js.map +1 -0
  218. package/dist/tone-Bdm5uaht.js +48 -0
  219. package/dist/tone-Bdm5uaht.js.map +1 -0
  220. package/dist/tone-DRKlZgPr.cjs +43 -0
  221. package/dist/tone-DRKlZgPr.cjs.map +1 -0
  222. package/dist/tone-vNb2DAAD.js +39 -0
  223. package/dist/tone-vNb2DAAD.js.map +1 -0
  224. package/dist/transcript-watcher-CL2QUygI.js +132 -0
  225. package/dist/transcript-watcher-CL2QUygI.js.map +1 -0
  226. package/dist/unmatched-transcripts-BsH5bhkU.js +26 -0
  227. package/dist/unmatched-transcripts-BsH5bhkU.js.map +1 -0
  228. package/dist/unmatched-transcripts-D0PrJ9iz.js +2 -0
  229. package/dist/update-deal-BNwPGaTV.js +2 -0
  230. package/dist/update-deal-DKC79skb.js +91 -0
  231. package/dist/update-deal-DKC79skb.js.map +1 -0
  232. package/dist/usage-CClTf5e6.cjs +57 -0
  233. package/dist/usage-CClTf5e6.cjs.map +1 -0
  234. package/dist/usage-D0-TYJkw.js +93 -0
  235. package/dist/usage-D0-TYJkw.js.map +1 -0
  236. package/dist/usage-D0u9a-lV.js +54 -0
  237. package/dist/usage-D0u9a-lV.js.map +1 -0
  238. package/dist/vault-C1D3zScD.js +2 -0
  239. package/dist/vault-DXCg29W-.js +86 -0
  240. package/dist/vault-DXCg29W-.js.map +1 -0
  241. package/dist/webhooks-7EpA05Qr.js +138 -0
  242. package/dist/webhooks-7EpA05Qr.js.map +1 -0
  243. package/dist/webhooks-BO2UAnmn.js +94 -0
  244. package/dist/webhooks-BO2UAnmn.js.map +1 -0
  245. package/dist/webhooks-Xn6zO6kd.cjs +97 -0
  246. package/dist/webhooks-Xn6zO6kd.cjs.map +1 -0
  247. package/dist/write-queue-BDolUxfs.cjs +26 -0
  248. package/dist/write-queue-BDolUxfs.cjs.map +1 -0
  249. package/dist/write-queue-IbsAjUnh.js +21 -0
  250. package/dist/write-queue-IbsAjUnh.js.map +1 -0
  251. package/package.json +142 -0
@@ -0,0 +1,372 @@
1
+ import { a as llmProvider, c as neutralizeUntrusted, l as maskPii, o as localLlmConfig, s as guardrailsEnabled, u as piiMaskingEnabled } from "./compliance-CKSBoQUe.js";
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ //#region src/core/resilience.ts
4
+ var CircuitBreaker = class {
5
+ opts;
6
+ failures = 0;
7
+ openedAt = null;
8
+ _state = "closed";
9
+ constructor(opts) {
10
+ this.opts = opts;
11
+ }
12
+ get state() {
13
+ return this._state;
14
+ }
15
+ async call(fn) {
16
+ if (this._state === "open") if (Date.now() - (this.openedAt ?? 0) >= this.opts.halfOpenAfter) this._state = "half-open";
17
+ else throw new Error("Circuit open");
18
+ try {
19
+ const result = await fn();
20
+ this.failures = 0;
21
+ this._state = "closed";
22
+ this.openedAt = null;
23
+ return result;
24
+ } catch (err) {
25
+ this.failures++;
26
+ if (this._state === "half-open" || this.failures >= this.opts.threshold) {
27
+ this._state = "open";
28
+ this.openedAt = Date.now();
29
+ this.failures = 0;
30
+ }
31
+ throw err;
32
+ }
33
+ }
34
+ };
35
+ //#endregion
36
+ //#region src/core/input-guard.ts
37
+ function guardIsoDate(val, field) {
38
+ if (typeof val !== "string" || !val) throw new Error(`${field}: invalid date`);
39
+ const d = new Date(val);
40
+ if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);
41
+ if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
42
+ const [year, month, day] = val.slice(0, 10).split("-").map(Number);
43
+ if (month < 1 || month > 12 || day < 1 || day > 31) throw new Error(`${field}: invalid date`);
44
+ const reparse = /* @__PURE__ */ new Date(`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`);
45
+ if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) throw new Error(`${field}: invalid date`);
46
+ }
47
+ return val;
48
+ }
49
+ const DEFAULT_LLM_MAX_BYTES = 512 * 1024;
50
+ function guardLlmResponse(response, maxBytes = DEFAULT_LLM_MAX_BYTES) {
51
+ if (typeof response !== "string") throw new Error("LLM response: expected string");
52
+ const byteLen = Buffer.byteLength(response, "utf-8");
53
+ if (byteLen > maxBytes) throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);
54
+ return response;
55
+ }
56
+ //#endregion
57
+ //#region src/core/llm.ts
58
+ const MODEL = "claude-haiku-4-5-20251001";
59
+ let _client = null;
60
+ let llmCircuit = new CircuitBreaker({
61
+ threshold: 3,
62
+ timeoutMs: 3e4,
63
+ halfOpenAfter: 3e4
64
+ });
65
+ function getClient() {
66
+ if (!process.env["ANTHROPIC_API_KEY"]) return null;
67
+ if (!_client) _client = new Anthropic();
68
+ return _client;
69
+ }
70
+ function emailFallback(snippet) {
71
+ return {
72
+ summary: snippet.slice(0, 300),
73
+ sentiment: "neutral",
74
+ nextSteps: []
75
+ };
76
+ }
77
+ async function summarizeEmail(subject, snippet, from) {
78
+ const client = getClient();
79
+ if (!client) return emailFallback(snippet);
80
+ try {
81
+ const textBlock = (await client.messages.create({
82
+ model: MODEL,
83
+ max_tokens: 200,
84
+ system: [{
85
+ type: "text",
86
+ text: "You are a CRM assistant. Extract structured information from email metadata.\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }",
87
+ cache_control: { type: "ephemeral" }
88
+ }],
89
+ messages: [{
90
+ role: "user",
91
+ content: `Subject: ${subject}\nFrom: ${from}\nContent: ${snippet}`
92
+ }]
93
+ })).content.find((b) => b.type === "text");
94
+ if (!textBlock || textBlock.type !== "text") return emailFallback(snippet);
95
+ try {
96
+ return JSON.parse(textBlock.text);
97
+ } catch {
98
+ return emailFallback(snippet);
99
+ }
100
+ } catch {
101
+ return emailFallback(snippet);
102
+ }
103
+ }
104
+ async function recognizeCustomer(transcriptContent, candidates) {
105
+ if (candidates.length === 0) return {
106
+ slug: null,
107
+ confidence: "low"
108
+ };
109
+ const client = getClient();
110
+ if (!client) return {
111
+ slug: null,
112
+ confidence: "low"
113
+ };
114
+ try {
115
+ const textBlock = (await client.messages.create({
116
+ model: MODEL,
117
+ max_tokens: 100,
118
+ system: [{
119
+ type: "text",
120
+ text: "You are a CRM assistant. Match a meeting transcript to the most likely customer.\nReturn ONLY valid JSON: { \"slug\": string|null, \"confidence\": \"high\"|\"medium\"|\"low\" }\nslug must be one of the provided candidates or null if no match.",
121
+ cache_control: { type: "ephemeral" }
122
+ }],
123
+ messages: [{
124
+ role: "user",
125
+ content: `Available customers: ${candidates.map((c) => `${c.slug} (${c.name})`).join(", ")}\nTranscript (first 1000 chars): ${transcriptContent.slice(0, 1e3)}`
126
+ }]
127
+ })).content.find((b) => b.type === "text");
128
+ if (!textBlock || textBlock.type !== "text") return {
129
+ slug: null,
130
+ confidence: "low"
131
+ };
132
+ try {
133
+ return JSON.parse(textBlock.text);
134
+ } catch {
135
+ return {
136
+ slug: null,
137
+ confidence: "low"
138
+ };
139
+ }
140
+ } catch {
141
+ return {
142
+ slug: null,
143
+ confidence: "low"
144
+ };
145
+ }
146
+ }
147
+ function recordCall(model, inputTokens, outputTokens, ctx) {
148
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
149
+ import("./usage-D0-TYJkw.js").then(({ recordUsage }) => recordUsage(dataDir, {
150
+ ...ctx?.slug ? { slug: ctx.slug } : {},
151
+ ...ctx?.tool ? { tool: ctx.tool } : {},
152
+ model,
153
+ inputTokens,
154
+ outputTokens
155
+ }));
156
+ }
157
+ /**
158
+ * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via
159
+ * fetch — no extra dependency — so customer data can stay on-machine. Usage is
160
+ * still recorded for cost/observability parity with the Anthropic path.
161
+ */
162
+ async function callLocalLlm(masked, ctx) {
163
+ const { baseUrl, model } = localLlmConfig();
164
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({
168
+ model,
169
+ max_tokens: 500,
170
+ messages: [{
171
+ role: "user",
172
+ content: masked
173
+ }]
174
+ })
175
+ });
176
+ if (!res.ok) throw new Error(`Local LLM error ${res.status}`);
177
+ const data = await res.json();
178
+ const text = data.choices?.[0]?.message?.content;
179
+ if (!text) throw new Error("No text response from local LLM");
180
+ if (data.usage) recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);
181
+ return text;
182
+ }
183
+ async function callLlm(prompt, ctx) {
184
+ const provider = llmProvider();
185
+ const client = provider === "anthropic" ? getClient() : null;
186
+ if (provider === "anthropic" && !client) throw new Error("ANTHROPIC_API_KEY not set");
187
+ const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;
188
+ const { masked, unmask } = piiMaskingEnabled() ? maskPii(guarded) : {
189
+ masked: guarded,
190
+ unmask: (t) => t
191
+ };
192
+ if (provider !== "anthropic") return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));
193
+ return llmCircuit.call(async () => {
194
+ const response = await client.messages.create({
195
+ model: MODEL,
196
+ max_tokens: 500,
197
+ messages: [{
198
+ role: "user",
199
+ content: masked
200
+ }]
201
+ });
202
+ const usage = response.usage;
203
+ if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);
204
+ const textBlock = response.content.find((b) => b.type === "text");
205
+ if (!textBlock || textBlock.type !== "text") throw new Error("No text response from LLM");
206
+ return unmask(guardLlmResponse(textBlock.text));
207
+ });
208
+ }
209
+ const FIELD_ALIASES = {
210
+ name: [
211
+ "company name",
212
+ "company",
213
+ "organization",
214
+ "organisation",
215
+ "account name",
216
+ "name",
217
+ "firma"
218
+ ],
219
+ email: [
220
+ "email address",
221
+ "e-mail",
222
+ "email",
223
+ "e-mail address",
224
+ "mail"
225
+ ],
226
+ domain: [
227
+ "company domain",
228
+ "website",
229
+ "domain",
230
+ "url",
231
+ "web",
232
+ "homepage"
233
+ ],
234
+ phone: [
235
+ "phone number",
236
+ "phone",
237
+ "tel",
238
+ "telephone",
239
+ "mobile",
240
+ "cell"
241
+ ],
242
+ industry: [
243
+ "industry",
244
+ "sector",
245
+ "branche",
246
+ "vertical"
247
+ ],
248
+ primary_contact: [
249
+ "contact name",
250
+ "contact person",
251
+ "contact",
252
+ "ansprechpartner",
253
+ "kontakt"
254
+ ],
255
+ timezone: [
256
+ "timezone",
257
+ "time zone",
258
+ "tz"
259
+ ],
260
+ notes: [
261
+ "notes",
262
+ "description",
263
+ "body",
264
+ "comment",
265
+ "details",
266
+ "note",
267
+ "inhalt",
268
+ "subject",
269
+ "summary"
270
+ ],
271
+ date: [
272
+ "activity date",
273
+ "activity_date",
274
+ "due date",
275
+ "date",
276
+ "created_at",
277
+ "timestamp",
278
+ "time"
279
+ ],
280
+ activityType: [
281
+ "activity type",
282
+ "activity_type",
283
+ "activitytype",
284
+ "type",
285
+ "category",
286
+ "art"
287
+ ],
288
+ sourceId: [
289
+ "record id",
290
+ "record_id",
291
+ "source id",
292
+ "source_id",
293
+ "external id",
294
+ "external_id",
295
+ "activity id"
296
+ ]
297
+ };
298
+ function mapCsvFieldsHeuristic(headers, targetFields) {
299
+ const result = {};
300
+ const usedHeaders = /* @__PURE__ */ new Set();
301
+ for (const field of targetFields) {
302
+ const aliases = FIELD_ALIASES[field] ?? [field];
303
+ let matched = null;
304
+ for (const header of headers) {
305
+ if (usedHeaders.has(header)) continue;
306
+ const lower = header.toLowerCase();
307
+ if (aliases.some((alias) => lower === alias || lower.includes(alias))) {
308
+ matched = header;
309
+ break;
310
+ }
311
+ }
312
+ result[field] = matched;
313
+ if (matched) usedHeaders.add(matched);
314
+ }
315
+ return result;
316
+ }
317
+ const FIELD_SEMANTICS = `CRM field semantics:
318
+ - name: Company or organization name (required)
319
+ - email: Contact email address
320
+ - domain: Company website or domain (e.g. "acme.com")
321
+ - notes: Interaction notes, description, or subject text
322
+ - date: Date of activity/interaction (ISO 8601 or YYYY-MM-DD)
323
+ - activityType: Type of interaction — Call, Email, Meeting, Note
324
+ - sourceId: Unique ID from the source system used for deduplication`;
325
+ async function mapCsvFields(headers, targetFields) {
326
+ const client = getClient();
327
+ if (!client) return mapCsvFieldsHeuristic(headers, targetFields);
328
+ try {
329
+ const textBlock = (await client.messages.create({
330
+ model: MODEL,
331
+ max_tokens: 300,
332
+ system: [{
333
+ type: "text",
334
+ text: `You are a CRM data-import assistant. Map CSV column headers to internal CRM field names.
335
+
336
+ ${FIELD_SEMANTICS}
337
+
338
+ Rules:
339
+ 1. Return ONLY valid JSON: { "<crmField>": "<csvColumn>" | null, ... }
340
+ 2. Every requested CRM field must appear as a key in the response.
341
+ 3. Use null when no column is a reasonable match.
342
+ 4. Each CSV column may only be assigned to one CRM field.
343
+ 5. Only use column names that appear exactly in the provided CSV columns list.`,
344
+ cache_control: { type: "ephemeral" }
345
+ }],
346
+ messages: [{
347
+ role: "user",
348
+ content: `CSV columns: ${JSON.stringify(headers)}\nMap to CRM fields: ${JSON.stringify(targetFields)}`
349
+ }]
350
+ })).content.find((b) => b.type === "text");
351
+ if (!textBlock || textBlock.type !== "text") return mapCsvFieldsHeuristic(headers, targetFields);
352
+ try {
353
+ const raw = JSON.parse(textBlock.text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
354
+ const validated = {};
355
+ const headerSet = new Set(headers);
356
+ for (const field of targetFields) {
357
+ const col = raw[field] ?? null;
358
+ validated[field] = col !== null && headerSet.has(col) ? col : null;
359
+ }
360
+ if (!validated["name"]) return mapCsvFieldsHeuristic(headers, targetFields);
361
+ return validated;
362
+ } catch {
363
+ return mapCsvFieldsHeuristic(headers, targetFields);
364
+ }
365
+ } catch {
366
+ return mapCsvFieldsHeuristic(headers, targetFields);
367
+ }
368
+ }
369
+ //#endregion
370
+ export { summarizeEmail as a, recognizeCustomer as i, mapCsvFields as n, guardIsoDate as o, mapCsvFieldsHeuristic as r, callLlm as t };
371
+
372
+ //# sourceMappingURL=llm-DvzZqva0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-DvzZqva0.js","names":[],"sources":["../src/core/resilience.ts","../src/core/input-guard.ts","../src/core/llm.ts"],"sourcesContent":["export interface RetryOptions {\n attempts: number;\n backoffMs: number;\n maxBackoffMs?: number;\n shouldRetry?: (err: Error) => boolean;\n}\n\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const { attempts, backoffMs, maxBackoffMs, shouldRetry } = opts;\n let lastError!: Error;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err as Error;\n if (shouldRetry && !shouldRetry(lastError)) throw lastError;\n if (attempt < attempts - 1) {\n const delay = maxBackoffMs\n ? Math.min(backoffMs * 2 ** attempt, maxBackoffMs)\n : backoffMs * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n throw lastError;\n}\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerOptions {\n threshold: number;\n timeoutMs: number;\n halfOpenAfter: number;\n}\n\nexport class CircuitBreaker {\n private failures = 0;\n private openedAt: number | null = null;\n private _state: CircuitState = \"closed\";\n\n constructor(private readonly opts: CircuitBreakerOptions) {}\n\n get state(): CircuitState {\n return this._state;\n }\n\n async call<T>(fn: () => Promise<T>): Promise<T> {\n if (this._state === \"open\") {\n const elapsed = Date.now() - (this.openedAt ?? 0);\n if (elapsed >= this.opts.halfOpenAfter) {\n this._state = \"half-open\";\n } else {\n throw new Error(\"Circuit open\");\n }\n }\n\n try {\n const result = await fn();\n // Success — reset\n this.failures = 0;\n this._state = \"closed\";\n this.openedAt = null;\n return result;\n } catch (err) {\n this.failures++;\n if (this._state === \"half-open\" || this.failures >= this.opts.threshold) {\n this._state = \"open\";\n this.openedAt = Date.now();\n this.failures = 0;\n }\n throw err;\n }\n }\n}\n","export interface StringGuardOptions {\n maxLen?: number;\n pattern?: RegExp;\n trim?: boolean;\n}\n\nexport function guardString(val: unknown, field: string, opts: StringGuardOptions = {}): string {\n if (typeof val !== \"string\") throw new Error(`${field}: expected string, got ${typeof val}`);\n const trimmed = opts.trim !== false ? val.trim() : val;\n if (opts.maxLen !== undefined && trimmed.length > opts.maxLen) {\n throw new Error(`${field}: exceeds max length ${opts.maxLen}`);\n }\n if (opts.pattern && !opts.pattern.test(trimmed)) {\n throw new Error(`${field}: invalid format`);\n }\n return trimmed;\n}\n\nexport interface NumberGuardOptions {\n min?: number;\n max?: number;\n}\n\nexport function guardNumber(val: unknown, field: string, opts: NumberGuardOptions = {}): number {\n if (typeof val !== \"number\" || !isFinite(val)) {\n throw new Error(\n `${field}: expected number, got ${typeof val === \"number\" ? \"NaN/Infinity\" : typeof val}`\n );\n }\n if (opts.min !== undefined && val < opts.min) {\n throw new Error(`${field}: must be >= ${opts.min}`);\n }\n if (opts.max !== undefined && val > opts.max) {\n throw new Error(`${field}: must be <= ${opts.max}`);\n }\n return val;\n}\n\nexport function guardPositiveInt(val: unknown, field: string): number {\n const n = guardNumber(val, field);\n if (!Number.isInteger(n)) throw new Error(`${field}: must be integer`);\n if (n < 1) throw new Error(`${field}: must be >= 1`);\n return n;\n}\n\nexport function guardIsoDate(val: unknown, field: string): string {\n if (typeof val !== \"string\" || !val) throw new Error(`${field}: invalid date`);\n const d = new Date(val);\n if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);\n // Reject clearly invalid month/day combinations (e.g., 2026-13-01)\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) {\n const [year, month, day] = val.slice(0, 10).split(\"-\").map(Number) as [number, number, number];\n if (month < 1 || month > 12 || day < 1 || day > 31) {\n throw new Error(`${field}: invalid date`);\n }\n // Cross-check with Date parsing\n const reparse = new Date(\n `${year}-${String(month).padStart(2, \"0\")}-${String(day).padStart(2, \"0\")}`\n );\n if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) {\n throw new Error(`${field}: invalid date`);\n }\n }\n return val;\n}\n\nconst DEFAULT_LLM_MAX_BYTES = 512 * 1024; // 512 KB\n\nexport function guardLlmResponse(\n response: unknown,\n maxBytes: number = DEFAULT_LLM_MAX_BYTES\n): string {\n if (typeof response !== \"string\") {\n throw new Error(\"LLM response: expected string\");\n }\n const byteLen = Buffer.byteLength(response, \"utf-8\");\n if (byteLen > maxBytes) {\n throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);\n }\n return response;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { CircuitBreaker } from \"./resilience.js\";\nimport { guardLlmResponse } from \"./input-guard.js\";\nimport { maskPii, piiMaskingEnabled } from \"./pii.js\";\nimport { neutralizeUntrusted, guardrailsEnabled } from \"./guardrails.js\";\nimport { llmProvider, localLlmConfig } from \"./compliance.js\";\n\nconst MODEL = \"claude-haiku-4-5-20251001\";\n\nlet _client: Anthropic | null = null;\nlet llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n\nexport function resetLlmCircuit(): void {\n llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n}\n\nfunction getClient(): Anthropic | null {\n if (!process.env[\"ANTHROPIC_API_KEY\"]) return null;\n if (!_client) _client = new Anthropic();\n return _client;\n}\n\nexport interface EmailSummary {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n}\n\nexport interface CustomerMatch {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\nfunction emailFallback(snippet: string): EmailSummary {\n return {\n summary: snippet.slice(0, 300),\n sentiment: \"neutral\",\n nextSteps: [],\n };\n}\n\nexport async function summarizeEmail(\n subject: string,\n snippet: string,\n from: string\n): Promise<EmailSummary> {\n const client = getClient();\n if (!client) return emailFallback(snippet);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 200,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Extract structured information from email metadata.\\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Subject: ${subject}\\nFrom: ${from}\\nContent: ${snippet}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return emailFallback(snippet);\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n };\n return parsed;\n } catch {\n return emailFallback(snippet);\n }\n } catch {\n return emailFallback(snippet);\n }\n}\n\nexport async function recognizeCustomer(\n transcriptContent: string,\n candidates: Array<{ slug: string; name: string }>\n): Promise<CustomerMatch> {\n if (candidates.length === 0) return { slug: null, confidence: \"low\" };\n\n const client = getClient();\n if (!client) return { slug: null, confidence: \"low\" };\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 100,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Match a meeting transcript to the most likely customer.\\nReturn ONLY valid JSON: { \"slug\": string|null, \"confidence\": \"high\"|\"medium\"|\"low\" }\\nslug must be one of the provided candidates or null if no match.',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Available customers: ${candidates.map((c) => `${c.slug} (${c.name})`).join(\", \")}\\nTranscript (first 1000 chars): ${transcriptContent.slice(0, 1000)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return { slug: null, confidence: \"low\" };\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n };\n return parsed;\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n}\n\nexport function resetLlmClient(): void {\n _client = null;\n}\n\nfunction recordCall(\n model: string,\n inputTokens: number,\n outputTokens: number,\n ctx?: { slug?: string; tool?: string }\n): void {\n const dataDir = process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n void import(\"./usage.js\").then(({ recordUsage }) =>\n recordUsage(dataDir, {\n ...(ctx?.slug ? { slug: ctx.slug } : {}),\n ...(ctx?.tool ? { tool: ctx.tool } : {}),\n model,\n inputTokens,\n outputTokens,\n })\n );\n}\n\n/**\n * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via\n * fetch — no extra dependency — so customer data can stay on-machine. Usage is\n * still recorded for cost/observability parity with the Anthropic path.\n */\nasync function callLocalLlm(\n masked: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const { baseUrl, model } = localLlmConfig();\n const res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n }),\n });\n if (!res.ok) throw new Error(`Local LLM error ${res.status}`);\n const data = (await res.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n };\n const text = data.choices?.[0]?.message?.content;\n if (!text) throw new Error(\"No text response from local LLM\");\n if (data.usage)\n recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);\n return text;\n}\n\nexport async function callLlm(\n prompt: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const provider = llmProvider();\n const client = provider === \"anthropic\" ? getClient() : null;\n if (provider === \"anthropic\" && !client) throw new Error(\"ANTHROPIC_API_KEY not set\");\n\n // Opt-in guardrails (neutralize prompt-injection) + PII masking, then restore.\n const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;\n const { masked, unmask } = piiMaskingEnabled()\n ? maskPii(guarded)\n : { masked: guarded, unmask: (t: string) => t };\n\n if (provider !== \"anthropic\") {\n return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));\n }\n\n return llmCircuit.call(async () => {\n const response = await client!.messages.create({\n model: MODEL,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n });\n\n // Token-cost observability (D3): record usage per customer/tool.\n const usage = response.usage;\n if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") throw new Error(\"No text response from LLM\");\n return unmask(guardLlmResponse(textBlock.text));\n });\n}\n\nexport type FieldMapping = Record<string, string | null>;\n\n// Alias table: CRM field name → list of CSV column patterns (lowercased substrings)\nconst FIELD_ALIASES: Record<string, string[]> = {\n name: [\n \"company name\",\n \"company\",\n \"organization\",\n \"organisation\",\n \"account name\",\n \"name\",\n \"firma\",\n ],\n email: [\"email address\", \"e-mail\", \"email\", \"e-mail address\", \"mail\"],\n domain: [\"company domain\", \"website\", \"domain\", \"url\", \"web\", \"homepage\"],\n phone: [\"phone number\", \"phone\", \"tel\", \"telephone\", \"mobile\", \"cell\"],\n industry: [\"industry\", \"sector\", \"branche\", \"vertical\"],\n primary_contact: [\"contact name\", \"contact person\", \"contact\", \"ansprechpartner\", \"kontakt\"],\n timezone: [\"timezone\", \"time zone\", \"tz\"],\n // Import-specific fields\n notes: [\n \"notes\",\n \"description\",\n \"body\",\n \"comment\",\n \"details\",\n \"note\",\n \"inhalt\",\n \"subject\",\n \"summary\",\n ],\n date: [\"activity date\", \"activity_date\", \"due date\", \"date\", \"created_at\", \"timestamp\", \"time\"],\n activityType: [\"activity type\", \"activity_type\", \"activitytype\", \"type\", \"category\", \"art\"],\n sourceId: [\n \"record id\",\n \"record_id\",\n \"source id\",\n \"source_id\",\n \"external id\",\n \"external_id\",\n \"activity id\",\n ],\n};\n\nexport function mapCsvFieldsHeuristic(headers: string[], targetFields: string[]): FieldMapping {\n const result: FieldMapping = {};\n const usedHeaders = new Set<string>();\n\n for (const field of targetFields) {\n const aliases = FIELD_ALIASES[field] ?? [field];\n let matched: string | null = null;\n\n for (const header of headers) {\n if (usedHeaders.has(header)) continue;\n const lower = header.toLowerCase();\n if (aliases.some((alias) => lower === alias || lower.includes(alias))) {\n matched = header;\n break;\n }\n }\n\n result[field] = matched;\n if (matched) usedHeaders.add(matched);\n }\n\n return result;\n}\n\nconst FIELD_SEMANTICS = `CRM field semantics:\n- name: Company or organization name (required)\n- email: Contact email address\n- domain: Company website or domain (e.g. \"acme.com\")\n- notes: Interaction notes, description, or subject text\n- date: Date of activity/interaction (ISO 8601 or YYYY-MM-DD)\n- activityType: Type of interaction — Call, Email, Meeting, Note\n- sourceId: Unique ID from the source system used for deduplication`;\n\nexport async function mapCsvFields(\n headers: string[],\n targetFields: string[]\n): Promise<FieldMapping> {\n const client = getClient();\n if (!client) return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 300,\n system: [\n {\n type: \"text\",\n text: `You are a CRM data-import assistant. Map CSV column headers to internal CRM field names.\n\n${FIELD_SEMANTICS}\n\nRules:\n1. Return ONLY valid JSON: { \"<crmField>\": \"<csvColumn>\" | null, ... }\n2. Every requested CRM field must appear as a key in the response.\n3. Use null when no column is a reasonable match.\n4. Each CSV column may only be assigned to one CRM field.\n5. Only use column names that appear exactly in the provided CSV columns list.`,\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `CSV columns: ${JSON.stringify(headers)}\\nMap to CRM fields: ${JSON.stringify(targetFields)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\")\n return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const raw = JSON.parse(\n textBlock.text\n .replace(/^```(?:json)?\\n?/, \"\")\n .replace(/\\n?```$/, \"\")\n .trim()\n ) as Record<string, string | null>;\n const validated: FieldMapping = {};\n const headerSet = new Set(headers);\n for (const field of targetFields) {\n const col = raw[field] ?? null;\n validated[field] = col !== null && headerSet.has(col) ? col : null;\n }\n // Require at least 'name' to be mapped; fall back otherwise\n if (!validated[\"name\"]) return mapCsvFieldsHeuristic(headers, targetFields);\n return validated;\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n}\n"],"mappings":";;;AAoCA,IAAa,iBAAb,MAA4B;CAKG;CAJ7B,WAAmB;CACnB,WAAkC;CAClC,SAA+B;CAE/B,YAAY,MAA8C;EAA7B,KAAA,OAAA;CAA8B;CAE3D,IAAI,QAAsB;EACxB,OAAO,KAAK;CACd;CAEA,MAAM,KAAQ,IAAkC;EAC9C,IAAI,KAAK,WAAW,QAElB,IADgB,KAAK,IAAI,KAAK,KAAK,YAAY,MAChC,KAAK,KAAK,eACvB,KAAK,SAAS;OAEd,MAAM,IAAI,MAAM,cAAc;EAIlC,IAAI;GACF,MAAM,SAAS,MAAM,GAAG;GAExB,KAAK,WAAW;GAChB,KAAK,SAAS;GACd,KAAK,WAAW;GAChB,OAAO;EACT,SAAS,KAAK;GACZ,KAAK;GACL,IAAI,KAAK,WAAW,eAAe,KAAK,YAAY,KAAK,KAAK,WAAW;IACvE,KAAK,SAAS;IACd,KAAK,WAAW,KAAK,IAAI;IACzB,KAAK,WAAW;GAClB;GACA,MAAM;EACR;CACF;AACF;;;AC7BA,SAAgB,aAAa,KAAc,OAAuB;CAChE,IAAI,OAAO,QAAQ,YAAY,CAAC,KAAK,MAAM,IAAI,MAAM,GAAG,MAAM,eAAe;CAC7E,MAAM,IAAI,IAAI,KAAK,GAAG;CACtB,IAAI,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,eAAe;CAEhE,IAAI,qBAAqB,KAAK,GAAG,GAAG;EAClC,MAAM,CAAC,MAAM,OAAO,OAAO,IAAI,MAAM,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;EACjE,IAAI,QAAQ,KAAK,QAAQ,MAAM,MAAM,KAAK,MAAM,IAC9C,MAAM,IAAI,MAAM,GAAG,MAAM,eAAe;EAG1C,MAAM,0BAAU,IAAI,KAClB,GAAG,KAAK,GAAG,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,GAAG,EAAE,SAAS,GAAG,GAAG,GAC1E;EACA,IAAI,MAAM,QAAQ,QAAQ,CAAC,KAAK,QAAQ,SAAS,IAAI,MAAM,OACzD,MAAM,IAAI,MAAM,GAAG,MAAM,eAAe;CAE5C;CACA,OAAO;AACT;AAEA,MAAM,wBAAwB,MAAM;AAEpC,SAAgB,iBACd,UACA,WAAmB,uBACX;CACR,IAAI,OAAO,aAAa,UACtB,MAAM,IAAI,MAAM,+BAA+B;CAEjD,MAAM,UAAU,OAAO,WAAW,UAAU,OAAO;CACnD,IAAI,UAAU,UACZ,MAAM,IAAI,MAAM,wBAAwB,SAAS,cAAc,QAAQ,EAAE;CAE3E,OAAO;AACT;;;ACzEA,MAAM,QAAQ;AAEd,IAAI,UAA4B;AAChC,IAAI,aAAa,IAAI,eAAe;CAAE,WAAW;CAAG,WAAW;CAAQ,eAAe;AAAO,CAAC;AAM9F,SAAS,YAA8B;CACrC,IAAI,CAAC,QAAQ,IAAI,sBAAsB,OAAO;CAC9C,IAAI,CAAC,SAAS,UAAU,IAAI,UAAU;CACtC,OAAO;AACT;AAaA,SAAS,cAAc,SAA+B;CACpD,OAAO;EACL,SAAS,QAAQ,MAAM,GAAG,GAAG;EAC7B,WAAW;EACX,WAAW,CAAC;CACd;AACF;AAEA,eAAsB,eACpB,SACA,SACA,MACuB;CACvB,MAAM,SAAS,UAAU;CACzB,IAAI,CAAC,QAAQ,OAAO,cAAc,OAAO;CAEzC,IAAI;EAmBF,MAAM,aAAY,MAlBK,OAAO,SAAS,OAAO;GAC5C,OAAO;GACP,YAAY;GACZ,QAAQ,CACN;IACE,MAAM;IACN,MAAM;IACN,eAAe,EAAE,MAAM,YAAY;GACrC,CACF;GACA,UAAU,CACR;IACE,MAAM;IACN,SAAS,YAAY,QAAQ,UAAU,KAAK,aAAa;GAC3D,CACF;EACF,CAAC,GAE0B,QAAQ,MAAM,MAAM,EAAE,SAAS,MAAM;EAChE,IAAI,CAAC,aAAa,UAAU,SAAS,QAAQ,OAAO,cAAc,OAAO;EAEzE,IAAI;GAMF,OALe,KAAK,MAAM,UAAU,IAKxB;EACd,QAAQ;GACN,OAAO,cAAc,OAAO;EAC9B;CACF,QAAQ;EACN,OAAO,cAAc,OAAO;CAC9B;AACF;AAEA,eAAsB,kBACpB,mBACA,YACwB;CACxB,IAAI,WAAW,WAAW,GAAG,OAAO;EAAE,MAAM;EAAM,YAAY;CAAM;CAEpE,MAAM,SAAS,UAAU;CACzB,IAAI,CAAC,QAAQ,OAAO;EAAE,MAAM;EAAM,YAAY;CAAM;CAEpD,IAAI;EAmBF,MAAM,aAAY,MAlBK,OAAO,SAAS,OAAO;GAC5C,OAAO;GACP,YAAY;GACZ,QAAQ,CACN;IACE,MAAM;IACN,MAAM;IACN,eAAe,EAAE,MAAM,YAAY;GACrC,CACF;GACA,UAAU,CACR;IACE,MAAM;IACN,SAAS,wBAAwB,WAAW,KAAK,MAAM,GAAG,EAAE,KAAK,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE,mCAAmC,kBAAkB,MAAM,GAAG,GAAI;GAC/J,CACF;EACF,CAAC,GAE0B,QAAQ,MAAM,MAAM,EAAE,SAAS,MAAM;EAChE,IAAI,CAAC,aAAa,UAAU,SAAS,QAAQ,OAAO;GAAE,MAAM;GAAM,YAAY;EAAM;EAEpF,IAAI;GAKF,OAJe,KAAK,MAAM,UAAU,IAIxB;EACd,QAAQ;GACN,OAAO;IAAE,MAAM;IAAM,YAAY;GAAM;EACzC;CACF,QAAQ;EACN,OAAO;GAAE,MAAM;GAAM,YAAY;EAAM;CACzC;AACF;AAMA,SAAS,WACP,OACA,aACA,cACA,KACM;CACN,MAAM,UAAU,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;CAC7D,OAAY,uBAAc,MAAM,EAAE,kBAChC,YAAY,SAAS;EACnB,GAAI,KAAK,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;EACtC,GAAI,KAAK,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;EACtC;EACA;EACA;CACF,CAAC,CACH;AACF;;;;;;AAOA,eAAe,aACb,QACA,KACiB;CACjB,MAAM,EAAE,SAAS,UAAU,eAAe;CAC1C,MAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,EAAE,oBAAoB;EACxE,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,KAAK,UAAU;GACnB;GACA,YAAY;GACZ,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS;GAAO,CAAC;EAC9C,CAAC;CACH,CAAC;CACD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,mBAAmB,IAAI,QAAQ;CAC5D,MAAM,OAAQ,MAAM,IAAI,KAAK;CAI7B,MAAM,OAAO,KAAK,UAAU,IAAI,SAAS;CACzC,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,iCAAiC;CAC5D,IAAI,KAAK,OACP,WAAW,OAAO,KAAK,MAAM,iBAAiB,GAAG,KAAK,MAAM,qBAAqB,GAAG,GAAG;CACzF,OAAO;AACT;AAEA,eAAsB,QACpB,QACA,KACiB;CACjB,MAAM,WAAW,YAAY;CAC7B,MAAM,SAAS,aAAa,cAAc,UAAU,IAAI;CACxD,IAAI,aAAa,eAAe,CAAC,QAAQ,MAAM,IAAI,MAAM,2BAA2B;CAGpF,MAAM,UAAU,kBAAkB,IAAI,oBAAoB,MAAM,IAAI;CACpE,MAAM,EAAE,QAAQ,WAAW,kBAAkB,IACzC,QAAQ,OAAO,IACf;EAAE,QAAQ;EAAS,SAAS,MAAc;CAAE;CAEhD,IAAI,aAAa,aACf,OAAO,WAAW,KAAK,YAAY,OAAO,iBAAiB,MAAM,aAAa,QAAQ,GAAG,CAAC,CAAC,CAAC;CAG9F,OAAO,WAAW,KAAK,YAAY;EACjC,MAAM,WAAW,MAAM,OAAQ,SAAS,OAAO;GAC7C,OAAO;GACP,YAAY;GACZ,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS;GAAO,CAAC;EAC9C,CAAC;EAGD,MAAM,QAAQ,SAAS;EACvB,IAAI,OAAO,WAAW,OAAO,MAAM,cAAc,MAAM,eAAe,GAAG;EAEzE,MAAM,YAAY,SAAS,QAAQ,MAAM,MAAM,EAAE,SAAS,MAAM;EAChE,IAAI,CAAC,aAAa,UAAU,SAAS,QAAQ,MAAM,IAAI,MAAM,2BAA2B;EACxF,OAAO,OAAO,iBAAiB,UAAU,IAAI,CAAC;CAChD,CAAC;AACH;AAKA,MAAM,gBAA0C;CAC9C,MAAM;EACJ;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EAAC;EAAiB;EAAU;EAAS;EAAkB;CAAM;CACpE,QAAQ;EAAC;EAAkB;EAAW;EAAU;EAAO;EAAO;CAAU;CACxE,OAAO;EAAC;EAAgB;EAAS;EAAO;EAAa;EAAU;CAAM;CACrE,UAAU;EAAC;EAAY;EAAU;EAAW;CAAU;CACtD,iBAAiB;EAAC;EAAgB;EAAkB;EAAW;EAAmB;CAAS;CAC3F,UAAU;EAAC;EAAY;EAAa;CAAI;CAExC,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EAAC;EAAiB;EAAiB;EAAY;EAAQ;EAAc;EAAa;CAAM;CAC9F,cAAc;EAAC;EAAiB;EAAiB;EAAgB;EAAQ;EAAY;CAAK;CAC1F,UAAU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;AAEA,SAAgB,sBAAsB,SAAmB,cAAsC;CAC7F,MAAM,SAAuB,CAAC;CAC9B,MAAM,8BAAc,IAAI,IAAY;CAEpC,KAAK,MAAM,SAAS,cAAc;EAChC,MAAM,UAAU,cAAc,UAAU,CAAC,KAAK;EAC9C,IAAI,UAAyB;EAE7B,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,YAAY,IAAI,MAAM,GAAG;GAC7B,MAAM,QAAQ,OAAO,YAAY;GACjC,IAAI,QAAQ,MAAM,UAAU,UAAU,SAAS,MAAM,SAAS,KAAK,CAAC,GAAG;IACrE,UAAU;IACV;GACF;EACF;EAEA,OAAO,SAAS;EAChB,IAAI,SAAS,YAAY,IAAI,OAAO;CACtC;CAEA,OAAO;AACT;AAEA,MAAM,kBAAkB;;;;;;;;AASxB,eAAsB,aACpB,SACA,cACuB;CACvB,MAAM,SAAS,UAAU;CACzB,IAAI,CAAC,QAAQ,OAAO,sBAAsB,SAAS,YAAY;CAE/D,IAAI;EA4BF,MAAM,aAAY,MA3BK,OAAO,SAAS,OAAO;GAC5C,OAAO;GACP,YAAY;GACZ,QAAQ,CACN;IACE,MAAM;IACN,MAAM;;EAEd,gBAAgB;;;;;;;;IAQR,eAAe,EAAE,MAAM,YAAY;GACrC,CACF;GACA,UAAU,CACR;IACE,MAAM;IACN,SAAS,gBAAgB,KAAK,UAAU,OAAO,EAAE,uBAAuB,KAAK,UAAU,YAAY;GACrG,CACF;EACF,CAAC,GAE0B,QAAQ,MAAM,MAAM,EAAE,SAAS,MAAM;EAChE,IAAI,CAAC,aAAa,UAAU,SAAS,QACnC,OAAO,sBAAsB,SAAS,YAAY;EAEpD,IAAI;GACF,MAAM,MAAM,KAAK,MACf,UAAU,KACP,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,WAAW,EAAE,EACrB,KAAK,CACV;GACA,MAAM,YAA0B,CAAC;GACjC,MAAM,YAAY,IAAI,IAAI,OAAO;GACjC,KAAK,MAAM,SAAS,cAAc;IAChC,MAAM,MAAM,IAAI,UAAU;IAC1B,UAAU,SAAS,QAAQ,QAAQ,UAAU,IAAI,GAAG,IAAI,MAAM;GAChE;GAEA,IAAI,CAAC,UAAU,SAAS,OAAO,sBAAsB,SAAS,YAAY;GAC1E,OAAO;EACT,QAAQ;GACN,OAAO,sBAAsB,SAAS,YAAY;EACpD;CACF,QAAQ;EACN,OAAO,sBAAsB,SAAS,YAAY;CACpD;AACF"}
@@ -0,0 +1,174 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { a as neutralizeUntrusted, i as guardrailsEnabled, n as llmProvider, o as maskPii, r as localLlmConfig, s as piiMaskingEnabled } from "./compliance-B1kk5-YS.js";
3
+ import Anthropic from "@anthropic-ai/sdk";
4
+ //#region src/core/resilience.ts
5
+ var CircuitBreaker = class {
6
+ opts;
7
+ failures = 0;
8
+ openedAt = null;
9
+ _state = "closed";
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ }
13
+ get state() {
14
+ return this._state;
15
+ }
16
+ async call(fn) {
17
+ if (this._state === "open") if (Date.now() - (this.openedAt ?? 0) >= this.opts.halfOpenAfter) this._state = "half-open";
18
+ else throw new Error("Circuit open");
19
+ try {
20
+ const result = await fn();
21
+ this.failures = 0;
22
+ this._state = "closed";
23
+ this.openedAt = null;
24
+ return result;
25
+ } catch (err) {
26
+ this.failures++;
27
+ if (this._state === "half-open" || this.failures >= this.opts.threshold) {
28
+ this._state = "open";
29
+ this.openedAt = Date.now();
30
+ this.failures = 0;
31
+ }
32
+ throw err;
33
+ }
34
+ }
35
+ };
36
+ //#endregion
37
+ //#region src/core/input-guard.ts
38
+ function guardIsoDate(val, field) {
39
+ if (typeof val !== "string" || !val) throw new Error(`${field}: invalid date`);
40
+ const d = new Date(val);
41
+ if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);
42
+ if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
43
+ const [year, month, day] = val.slice(0, 10).split("-").map(Number);
44
+ if (month < 1 || month > 12 || day < 1 || day > 31) throw new Error(`${field}: invalid date`);
45
+ const reparse = /* @__PURE__ */ new Date(`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`);
46
+ if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) throw new Error(`${field}: invalid date`);
47
+ }
48
+ return val;
49
+ }
50
+ const DEFAULT_LLM_MAX_BYTES = 512 * 1024;
51
+ function guardLlmResponse(response, maxBytes = DEFAULT_LLM_MAX_BYTES) {
52
+ if (typeof response !== "string") throw new Error("LLM response: expected string");
53
+ const byteLen = Buffer.byteLength(response, "utf-8");
54
+ if (byteLen > maxBytes) throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);
55
+ return response;
56
+ }
57
+ //#endregion
58
+ //#region src/core/llm.ts
59
+ var llm_exports = /* @__PURE__ */ __exportAll({
60
+ callLlm: () => callLlm,
61
+ summarizeEmail: () => summarizeEmail
62
+ });
63
+ const MODEL = "claude-haiku-4-5-20251001";
64
+ let _client = null;
65
+ let llmCircuit = new CircuitBreaker({
66
+ threshold: 3,
67
+ timeoutMs: 3e4,
68
+ halfOpenAfter: 3e4
69
+ });
70
+ function getClient() {
71
+ if (!process.env["ANTHROPIC_API_KEY"]) return null;
72
+ if (!_client) _client = new Anthropic();
73
+ return _client;
74
+ }
75
+ function emailFallback(snippet) {
76
+ return {
77
+ summary: snippet.slice(0, 300),
78
+ sentiment: "neutral",
79
+ nextSteps: []
80
+ };
81
+ }
82
+ async function summarizeEmail(subject, snippet, from) {
83
+ const client = getClient();
84
+ if (!client) return emailFallback(snippet);
85
+ try {
86
+ const textBlock = (await client.messages.create({
87
+ model: MODEL,
88
+ max_tokens: 200,
89
+ system: [{
90
+ type: "text",
91
+ text: "You are a CRM assistant. Extract structured information from email metadata.\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }",
92
+ cache_control: { type: "ephemeral" }
93
+ }],
94
+ messages: [{
95
+ role: "user",
96
+ content: `Subject: ${subject}\nFrom: ${from}\nContent: ${snippet}`
97
+ }]
98
+ })).content.find((b) => b.type === "text");
99
+ if (!textBlock || textBlock.type !== "text") return emailFallback(snippet);
100
+ try {
101
+ return JSON.parse(textBlock.text);
102
+ } catch {
103
+ return emailFallback(snippet);
104
+ }
105
+ } catch {
106
+ return emailFallback(snippet);
107
+ }
108
+ }
109
+ function recordCall(model, inputTokens, outputTokens, ctx) {
110
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
111
+ import("./usage-D0u9a-lV.js").then(({ recordUsage }) => recordUsage(dataDir, {
112
+ ...ctx?.slug ? { slug: ctx.slug } : {},
113
+ ...ctx?.tool ? { tool: ctx.tool } : {},
114
+ model,
115
+ inputTokens,
116
+ outputTokens
117
+ }));
118
+ }
119
+ /**
120
+ * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via
121
+ * fetch — no extra dependency — so customer data can stay on-machine. Usage is
122
+ * still recorded for cost/observability parity with the Anthropic path.
123
+ */
124
+ async function callLocalLlm(masked, ctx) {
125
+ const { baseUrl, model } = localLlmConfig();
126
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({
130
+ model,
131
+ max_tokens: 500,
132
+ messages: [{
133
+ role: "user",
134
+ content: masked
135
+ }]
136
+ })
137
+ });
138
+ if (!res.ok) throw new Error(`Local LLM error ${res.status}`);
139
+ const data = await res.json();
140
+ const text = data.choices?.[0]?.message?.content;
141
+ if (!text) throw new Error("No text response from local LLM");
142
+ if (data.usage) recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);
143
+ return text;
144
+ }
145
+ async function callLlm(prompt, ctx) {
146
+ const provider = llmProvider();
147
+ const client = provider === "anthropic" ? getClient() : null;
148
+ if (provider === "anthropic" && !client) throw new Error("ANTHROPIC_API_KEY not set");
149
+ const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;
150
+ const { masked, unmask } = piiMaskingEnabled() ? maskPii(guarded) : {
151
+ masked: guarded,
152
+ unmask: (t) => t
153
+ };
154
+ if (provider !== "anthropic") return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));
155
+ return llmCircuit.call(async () => {
156
+ const response = await client.messages.create({
157
+ model: MODEL,
158
+ max_tokens: 500,
159
+ messages: [{
160
+ role: "user",
161
+ content: masked
162
+ }]
163
+ });
164
+ const usage = response.usage;
165
+ if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);
166
+ const textBlock = response.content.find((b) => b.type === "text");
167
+ if (!textBlock || textBlock.type !== "text") throw new Error("No text response from LLM");
168
+ return unmask(guardLlmResponse(textBlock.text));
169
+ });
170
+ }
171
+ //#endregion
172
+ export { guardIsoDate as i, llm_exports as n, summarizeEmail as r, callLlm as t };
173
+
174
+ //# sourceMappingURL=llm-Z8RIYkpF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-Z8RIYkpF.js","names":[],"sources":["../src/core/resilience.ts","../src/core/input-guard.ts","../src/core/llm.ts"],"sourcesContent":["export interface RetryOptions {\n attempts: number;\n backoffMs: number;\n maxBackoffMs?: number;\n shouldRetry?: (err: Error) => boolean;\n}\n\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const { attempts, backoffMs, maxBackoffMs, shouldRetry } = opts;\n let lastError!: Error;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err as Error;\n if (shouldRetry && !shouldRetry(lastError)) throw lastError;\n if (attempt < attempts - 1) {\n const delay = maxBackoffMs\n ? Math.min(backoffMs * 2 ** attempt, maxBackoffMs)\n : backoffMs * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n throw lastError;\n}\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerOptions {\n threshold: number;\n timeoutMs: number;\n halfOpenAfter: number;\n}\n\nexport class CircuitBreaker {\n private failures = 0;\n private openedAt: number | null = null;\n private _state: CircuitState = \"closed\";\n\n constructor(private readonly opts: CircuitBreakerOptions) {}\n\n get state(): CircuitState {\n return this._state;\n }\n\n async call<T>(fn: () => Promise<T>): Promise<T> {\n if (this._state === \"open\") {\n const elapsed = Date.now() - (this.openedAt ?? 0);\n if (elapsed >= this.opts.halfOpenAfter) {\n this._state = \"half-open\";\n } else {\n throw new Error(\"Circuit open\");\n }\n }\n\n try {\n const result = await fn();\n // Success — reset\n this.failures = 0;\n this._state = \"closed\";\n this.openedAt = null;\n return result;\n } catch (err) {\n this.failures++;\n if (this._state === \"half-open\" || this.failures >= this.opts.threshold) {\n this._state = \"open\";\n this.openedAt = Date.now();\n this.failures = 0;\n }\n throw err;\n }\n }\n}\n","export interface StringGuardOptions {\n maxLen?: number;\n pattern?: RegExp;\n trim?: boolean;\n}\n\nexport function guardString(val: unknown, field: string, opts: StringGuardOptions = {}): string {\n if (typeof val !== \"string\") throw new Error(`${field}: expected string, got ${typeof val}`);\n const trimmed = opts.trim !== false ? val.trim() : val;\n if (opts.maxLen !== undefined && trimmed.length > opts.maxLen) {\n throw new Error(`${field}: exceeds max length ${opts.maxLen}`);\n }\n if (opts.pattern && !opts.pattern.test(trimmed)) {\n throw new Error(`${field}: invalid format`);\n }\n return trimmed;\n}\n\nexport interface NumberGuardOptions {\n min?: number;\n max?: number;\n}\n\nexport function guardNumber(val: unknown, field: string, opts: NumberGuardOptions = {}): number {\n if (typeof val !== \"number\" || !isFinite(val)) {\n throw new Error(\n `${field}: expected number, got ${typeof val === \"number\" ? \"NaN/Infinity\" : typeof val}`\n );\n }\n if (opts.min !== undefined && val < opts.min) {\n throw new Error(`${field}: must be >= ${opts.min}`);\n }\n if (opts.max !== undefined && val > opts.max) {\n throw new Error(`${field}: must be <= ${opts.max}`);\n }\n return val;\n}\n\nexport function guardPositiveInt(val: unknown, field: string): number {\n const n = guardNumber(val, field);\n if (!Number.isInteger(n)) throw new Error(`${field}: must be integer`);\n if (n < 1) throw new Error(`${field}: must be >= 1`);\n return n;\n}\n\nexport function guardIsoDate(val: unknown, field: string): string {\n if (typeof val !== \"string\" || !val) throw new Error(`${field}: invalid date`);\n const d = new Date(val);\n if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);\n // Reject clearly invalid month/day combinations (e.g., 2026-13-01)\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) {\n const [year, month, day] = val.slice(0, 10).split(\"-\").map(Number) as [number, number, number];\n if (month < 1 || month > 12 || day < 1 || day > 31) {\n throw new Error(`${field}: invalid date`);\n }\n // Cross-check with Date parsing\n const reparse = new Date(\n `${year}-${String(month).padStart(2, \"0\")}-${String(day).padStart(2, \"0\")}`\n );\n if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) {\n throw new Error(`${field}: invalid date`);\n }\n }\n return val;\n}\n\nconst DEFAULT_LLM_MAX_BYTES = 512 * 1024; // 512 KB\n\nexport function guardLlmResponse(\n response: unknown,\n maxBytes: number = DEFAULT_LLM_MAX_BYTES\n): string {\n if (typeof response !== \"string\") {\n throw new Error(\"LLM response: expected string\");\n }\n const byteLen = Buffer.byteLength(response, \"utf-8\");\n if (byteLen > maxBytes) {\n throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);\n }\n return response;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { CircuitBreaker } from \"./resilience.js\";\nimport { guardLlmResponse } from \"./input-guard.js\";\nimport { maskPii, piiMaskingEnabled } from \"./pii.js\";\nimport { neutralizeUntrusted, guardrailsEnabled } from \"./guardrails.js\";\nimport { llmProvider, localLlmConfig } from \"./compliance.js\";\n\nconst MODEL = \"claude-haiku-4-5-20251001\";\n\nlet _client: Anthropic | null = null;\nlet llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n\nexport function resetLlmCircuit(): void {\n llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n}\n\nfunction getClient(): Anthropic | null {\n if (!process.env[\"ANTHROPIC_API_KEY\"]) return null;\n if (!_client) _client = new Anthropic();\n return _client;\n}\n\nexport interface EmailSummary {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n}\n\nexport interface CustomerMatch {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\nfunction emailFallback(snippet: string): EmailSummary {\n return {\n summary: snippet.slice(0, 300),\n sentiment: \"neutral\",\n nextSteps: [],\n };\n}\n\nexport async function summarizeEmail(\n subject: string,\n snippet: string,\n from: string\n): Promise<EmailSummary> {\n const client = getClient();\n if (!client) return emailFallback(snippet);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 200,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Extract structured information from email metadata.\\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Subject: ${subject}\\nFrom: ${from}\\nContent: ${snippet}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return emailFallback(snippet);\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n };\n return parsed;\n } catch {\n return emailFallback(snippet);\n }\n } catch {\n return emailFallback(snippet);\n }\n}\n\nexport async function recognizeCustomer(\n transcriptContent: string,\n candidates: Array<{ slug: string; name: string }>\n): Promise<CustomerMatch> {\n if (candidates.length === 0) return { slug: null, confidence: \"low\" };\n\n const client = getClient();\n if (!client) return { slug: null, confidence: \"low\" };\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 100,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Match a meeting transcript to the most likely customer.\\nReturn ONLY valid JSON: { \"slug\": string|null, \"confidence\": \"high\"|\"medium\"|\"low\" }\\nslug must be one of the provided candidates or null if no match.',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Available customers: ${candidates.map((c) => `${c.slug} (${c.name})`).join(\", \")}\\nTranscript (first 1000 chars): ${transcriptContent.slice(0, 1000)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return { slug: null, confidence: \"low\" };\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n };\n return parsed;\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n}\n\nexport function resetLlmClient(): void {\n _client = null;\n}\n\nfunction recordCall(\n model: string,\n inputTokens: number,\n outputTokens: number,\n ctx?: { slug?: string; tool?: string }\n): void {\n const dataDir = process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n void import(\"./usage.js\").then(({ recordUsage }) =>\n recordUsage(dataDir, {\n ...(ctx?.slug ? { slug: ctx.slug } : {}),\n ...(ctx?.tool ? { tool: ctx.tool } : {}),\n model,\n inputTokens,\n outputTokens,\n })\n );\n}\n\n/**\n * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via\n * fetch — no extra dependency — so customer data can stay on-machine. Usage is\n * still recorded for cost/observability parity with the Anthropic path.\n */\nasync function callLocalLlm(\n masked: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const { baseUrl, model } = localLlmConfig();\n const res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n }),\n });\n if (!res.ok) throw new Error(`Local LLM error ${res.status}`);\n const data = (await res.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n };\n const text = data.choices?.[0]?.message?.content;\n if (!text) throw new Error(\"No text response from local LLM\");\n if (data.usage)\n recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);\n return text;\n}\n\nexport async function callLlm(\n prompt: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const provider = llmProvider();\n const client = provider === \"anthropic\" ? getClient() : null;\n if (provider === \"anthropic\" && !client) throw new Error(\"ANTHROPIC_API_KEY not set\");\n\n // Opt-in guardrails (neutralize prompt-injection) + PII masking, then restore.\n const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;\n const { masked, unmask } = piiMaskingEnabled()\n ? maskPii(guarded)\n : { masked: guarded, unmask: (t: string) => t };\n\n if (provider !== \"anthropic\") {\n return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));\n }\n\n return llmCircuit.call(async () => {\n const response = await client!.messages.create({\n model: MODEL,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n });\n\n // Token-cost observability (D3): record usage per customer/tool.\n const usage = response.usage;\n if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") throw new Error(\"No text response from LLM\");\n return unmask(guardLlmResponse(textBlock.text));\n });\n}\n\nexport type FieldMapping = Record<string, string | null>;\n\n// Alias table: CRM field name → list of CSV column patterns (lowercased substrings)\nconst FIELD_ALIASES: Record<string, string[]> = {\n name: [\n \"company name\",\n \"company\",\n \"organization\",\n \"organisation\",\n \"account name\",\n \"name\",\n \"firma\",\n ],\n email: [\"email address\", \"e-mail\", \"email\", \"e-mail address\", \"mail\"],\n domain: [\"company domain\", \"website\", \"domain\", \"url\", \"web\", \"homepage\"],\n phone: [\"phone number\", \"phone\", \"tel\", \"telephone\", \"mobile\", \"cell\"],\n industry: [\"industry\", \"sector\", \"branche\", \"vertical\"],\n primary_contact: [\"contact name\", \"contact person\", \"contact\", \"ansprechpartner\", \"kontakt\"],\n timezone: [\"timezone\", \"time zone\", \"tz\"],\n // Import-specific fields\n notes: [\n \"notes\",\n \"description\",\n \"body\",\n \"comment\",\n \"details\",\n \"note\",\n \"inhalt\",\n \"subject\",\n \"summary\",\n ],\n date: [\"activity date\", \"activity_date\", \"due date\", \"date\", \"created_at\", \"timestamp\", \"time\"],\n activityType: [\"activity type\", \"activity_type\", \"activitytype\", \"type\", \"category\", \"art\"],\n sourceId: [\n \"record id\",\n \"record_id\",\n \"source id\",\n \"source_id\",\n \"external id\",\n \"external_id\",\n \"activity id\",\n ],\n};\n\nexport function mapCsvFieldsHeuristic(headers: string[], targetFields: string[]): FieldMapping {\n const result: FieldMapping = {};\n const usedHeaders = new Set<string>();\n\n for (const field of targetFields) {\n const aliases = FIELD_ALIASES[field] ?? [field];\n let matched: string | null = null;\n\n for (const header of headers) {\n if (usedHeaders.has(header)) continue;\n const lower = header.toLowerCase();\n if (aliases.some((alias) => lower === alias || lower.includes(alias))) {\n matched = header;\n break;\n }\n }\n\n result[field] = matched;\n if (matched) usedHeaders.add(matched);\n }\n\n return result;\n}\n\nconst FIELD_SEMANTICS = `CRM field semantics:\n- name: Company or organization name (required)\n- email: Contact email address\n- domain: Company website or domain (e.g. \"acme.com\")\n- notes: Interaction notes, description, or subject text\n- date: Date of activity/interaction (ISO 8601 or YYYY-MM-DD)\n- activityType: Type of interaction — Call, Email, Meeting, Note\n- sourceId: Unique ID from the source system used for deduplication`;\n\nexport async function mapCsvFields(\n headers: string[],\n targetFields: string[]\n): Promise<FieldMapping> {\n const client = getClient();\n if (!client) return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 300,\n system: [\n {\n type: \"text\",\n text: `You are a CRM data-import assistant. Map CSV column headers to internal CRM field names.\n\n${FIELD_SEMANTICS}\n\nRules:\n1. Return ONLY valid JSON: { \"<crmField>\": \"<csvColumn>\" | null, ... }\n2. Every requested CRM field must appear as a key in the response.\n3. Use null when no column is a reasonable match.\n4. Each CSV column may only be assigned to one CRM field.\n5. Only use column names that appear exactly in the provided CSV columns list.`,\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `CSV columns: ${JSON.stringify(headers)}\\nMap to CRM fields: ${JSON.stringify(targetFields)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\")\n return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const raw = JSON.parse(\n textBlock.text\n .replace(/^```(?:json)?\\n?/, \"\")\n .replace(/\\n?```$/, \"\")\n .trim()\n ) as Record<string, string | null>;\n const validated: FieldMapping = {};\n const headerSet = new Set(headers);\n for (const field of targetFields) {\n const col = raw[field] ?? null;\n validated[field] = col !== null && headerSet.has(col) ? col : null;\n }\n // Require at least 'name' to be mapped; fall back otherwise\n if (!validated[\"name\"]) return mapCsvFieldsHeuristic(headers, targetFields);\n return validated;\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n}\n"],"mappings":"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"}