@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,4287 @@
1
+ import { a as writeMainFacts, i as readMainFacts, n as ensureCustomerDir, r as listCustomerSlugs, t as customerExists } from "./customer-dir-DIylZ8Q6.js";
2
+ import { n as getSession } from "./session-store-CEa39Dxs.js";
3
+ import { a as searchKbSimple, n as getKbArticle, o as writeKbArticle, r as getKbMetaForExport, s as CAPABILITIES_TEXT } from "./knowledge-base-D0Fh40kc.js";
4
+ import { i as readBackupLog, n as listBackupsInDir, o as runBackup } from "./backup-CeMk9z86.js";
5
+ import { r as updateSlugSyncState, t as getLastGmailSync } from "./sync-state-ChaLbamC.js";
6
+ import { t as withFileQueue } from "./write-queue-IbsAjUnh.js";
7
+ import { n as formatInteractionEntry, t as appendInteraction } from "./interactions-writer-SLHnoEeE.js";
8
+ import { i as writeAuditEntry, n as getActor, r as readAuditLog, t as filterAuditLog } from "./audit-log-DNMY9mUZ.js";
9
+ import { a as enforceRbac, n as canSeeCustomer } from "./rbac-CTIktZaC.js";
10
+ import { f as getPipelineStages, i as buildSimulationInput, l as runSimulation, n as buildConfidenceMessage } from "./revenue-simulation-Bqf2DLVB.js";
11
+ import { t as readPipeline } from "./pipeline-writer-BvVquKIe.js";
12
+ import { a as updateGraphFromInteraction, c as readGraph, i as writeHealth, n as readHealth, o as findPath, r as updateHealthFromInteraction, s as getStakeholders, t as computeCustomerHealth } from "./relationship-health-odxEoQdJ.js";
13
+ import { t as callLlm } from "./llm-DvzZqva0.js";
14
+ import { d as pursueGoal, i as getActiveGoals, p as readGoals } from "./goal-engine-KpBftn4V.js";
15
+ import { n as readSubscriptions, r as register, s as writeSubscriptions } from "./push-manager-CdqIIkuh.js";
16
+ import { a as writeEnrollment, c as extractVariables, d as getTemplate, f as listTemplates, i as updateEnrollment, l as interpolate, n as listSequences, r as readEnrollments, s as buildVariablesFromCustomer, t as getSequence } from "./sequence-store-DaaWr0Os.js";
17
+ import { n as listQuotes, r as readQuote, t as generateQuote } from "./quote-generator-BfwENXzg.js";
18
+ import { i as upsertTicket, n as nextTicketId, r as readTickets, t as listAllTickets } from "./ticket-writer-j2oX_Wal.js";
19
+ import { i as loadSlaRules, t as calcSlaDue } from "./sla-engine-BqX-7u-7.js";
20
+ import { i as getSurvey, l as savePendingSurvey, n as calcNpsScore, o as loadSurveyResponses, r as generateSurveyToken, t as buildSurveyEmail } from "./survey-engine-DBjCYqCv.js";
21
+ import { t as buildContext } from "./context-builder-BzWAp3Zs.js";
22
+ import { a as loadCustomObjects, i as listRecords, n as defineCustomObject, t as createRecord } from "./custom-objects-BHgn1GEX.js";
23
+ import { r as searchKnowledge } from "./lancedb-rlvWoPwl.js";
24
+ import { t as buildDailyBriefing } from "./proactive-agent-BgQXw3ac.js";
25
+ import { a as protectedResourceMetadata, o as verifyBearer, r as isAuthRequired, s as wwwAuthenticateHeader } from "./auth-DFWwWcYD.js";
26
+ import { i as verifyGmailPubSubSignature, n as decodeGmailPubSubPayload, r as handleGmailPushEvent } from "./gmail-webhook-handler-e5Od25FX.js";
27
+ import { n as registerUpdateDeal } from "./update-deal-DKC79skb.js";
28
+ import path from "path";
29
+ import fs from "fs";
30
+ import matter from "gray-matter";
31
+ import { z } from "zod";
32
+ import crypto from "crypto";
33
+ import yaml from "js-yaml";
34
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
35
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
36
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
37
+ //#region src/core/oauth-store.ts
38
+ let _auth = null;
39
+ async function initOAuthFromDisk(dataDir) {
40
+ const credPath = path.join(dataDir, ".agentic", "gmail-credentials.json");
41
+ const tokenPath = path.join(dataDir, ".agentic", "gmail-token.json");
42
+ if (!fs.existsSync(credPath) || !fs.existsSync(tokenPath)) return false;
43
+ try {
44
+ const { getGmailAuth: loadAuth } = await import("./gmail-auth-OComS92L.js");
45
+ _auth = await loadAuth(credPath, tokenPath);
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ function getGmailAuth() {
52
+ return _auth;
53
+ }
54
+ //#endregion
55
+ //#region src/sync/microsoft-webhook-handler.ts
56
+ function verifyMicrosoftGraphSignature(body, expectedClientState) {
57
+ const notifications = body.value ?? [];
58
+ if (notifications.length === 0) return expectedClientState === "";
59
+ return notifications.every((n) => n.clientState === expectedClientState);
60
+ }
61
+ function handleMicrosoftValidationRequest(queryParams) {
62
+ const token = queryParams["validationToken"];
63
+ if (token) return {
64
+ isValidation: true,
65
+ token
66
+ };
67
+ return { isValidation: false };
68
+ }
69
+ function findSubscriptionByMsId(subs, subscriptionId) {
70
+ return subs.find((s) => s.provider === "microsoft-graph" && s.status === "active" && s.providerData.microsoftSubscriptionId === subscriptionId) ?? null;
71
+ }
72
+ async function handleMicrosoftPushEvent(dataDir, notifications, accessToken, options = {}) {
73
+ const subs = await readSubscriptions(dataDir);
74
+ const { fetchMessageFn, appendInteractionFn = appendInteraction } = options;
75
+ let processed = 0;
76
+ let skipped = 0;
77
+ let anyProcessed = false;
78
+ for (const notification of notifications) {
79
+ const sub = findSubscriptionByMsId(subs, notification.subscriptionId);
80
+ if (!sub) {
81
+ skipped++;
82
+ continue;
83
+ }
84
+ const messageId = notification.resourceData?.id;
85
+ if (!messageId || !fetchMessageFn) {
86
+ skipped++;
87
+ continue;
88
+ }
89
+ try {
90
+ const message = await fetchMessageFn(accessToken, messageId);
91
+ if (!message) {
92
+ skipped++;
93
+ continue;
94
+ }
95
+ const from = message.from?.emailAddress?.address ?? "unknown";
96
+ const sourceRef = `msgraph://message/${message.id}`;
97
+ await appendInteractionFn(dataDir, sub.slug, {
98
+ date: message.receivedDateTime ? new Date(message.receivedDateTime).toISOString().slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
99
+ type: "Email",
100
+ direction: "inbound",
101
+ with: from,
102
+ subject: message.subject ?? "(no subject)",
103
+ summary: message.bodyPreview ?? "(no preview)",
104
+ nextSteps: [],
105
+ sourceRef,
106
+ synced: (/* @__PURE__ */ new Date()).toISOString()
107
+ });
108
+ processed++;
109
+ anyProcessed = true;
110
+ const idx = subs.findIndex((s) => s.id === sub.id);
111
+ if (idx !== -1) subs[idx] = {
112
+ ...subs[idx],
113
+ eventsProcessed: subs[idx].eventsProcessed + 1,
114
+ lastEventAt: (/* @__PURE__ */ new Date()).toISOString()
115
+ };
116
+ } catch {
117
+ skipped++;
118
+ }
119
+ }
120
+ if (anyProcessed) await writeSubscriptions(dataDir, subs);
121
+ return {
122
+ processed,
123
+ skipped
124
+ };
125
+ }
126
+ //#endregion
127
+ //#region src/sync/slack-webhook-handler.ts
128
+ function verifySlackSignature(body, headers, signingSecret) {
129
+ const sig = headers["x-slack-signature"];
130
+ const ts = headers["x-slack-request-timestamp"];
131
+ if (!sig || !ts) return false;
132
+ const tsNum = Number(ts);
133
+ if (Math.abs(Date.now() / 1e3 - tsNum) > 300) return false;
134
+ const sigBase = `v0:${ts}:${body}`;
135
+ const expected = "v0=" + crypto.createHmac("sha256", signingSecret).update(sigBase).digest("hex");
136
+ try {
137
+ const sigBuf = Buffer.from(sig);
138
+ const expBuf = Buffer.from(expected);
139
+ if (sigBuf.length !== expBuf.length) return false;
140
+ return crypto.timingSafeEqual(sigBuf, expBuf);
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+ function handleSlackUrlVerification(body) {
146
+ if (body.type === "url_verification") return {
147
+ isVerification: true,
148
+ challenge: body.challenge ?? ""
149
+ };
150
+ return { isVerification: false };
151
+ }
152
+ function findSubscriptionByTeam(subs, teamId) {
153
+ return subs.find((s) => s.provider === "slack" && s.status === "active" && (!teamId || s.providerData.slackTeamId === teamId)) ?? null;
154
+ }
155
+ async function handleSlackPushEvent(dataDir, event, botToken, options = {}) {
156
+ if (event.type !== "message") return {
157
+ processed: 0,
158
+ skipped: 1
159
+ };
160
+ if (event.bot_id) return {
161
+ processed: 0,
162
+ skipped: 1
163
+ };
164
+ if (!event.text?.trim()) return {
165
+ processed: 0,
166
+ skipped: 1
167
+ };
168
+ const subs = await readSubscriptions(dataDir);
169
+ const sub = findSubscriptionByTeam(subs, options.teamId);
170
+ if (!sub) return {
171
+ processed: 0,
172
+ skipped: 1
173
+ };
174
+ const { appendInteractionFn = appendInteraction, fetchUserInfoFn } = options;
175
+ let senderName = event.user ?? "unknown";
176
+ if (fetchUserInfoFn && event.user) try {
177
+ const info = await fetchUserInfoFn(botToken, event.user);
178
+ senderName = info.name ?? info.email ?? event.user;
179
+ } catch {}
180
+ const ts = event.ts ? (/* @__PURE__ */ new Date(Number(event.ts) * 1e3)).toISOString().slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
181
+ const sourceRef = `slack://channel/${event.channel ?? "dm"}/ts/${event.ts ?? Date.now()}`;
182
+ try {
183
+ await appendInteractionFn(dataDir, sub.slug, {
184
+ date: ts,
185
+ type: "Meeting",
186
+ direction: "inbound",
187
+ with: senderName,
188
+ subject: `Slack message in ${event.channel ?? "DM"}`,
189
+ summary: event.text.slice(0, 300),
190
+ nextSteps: [],
191
+ sourceRef,
192
+ synced: (/* @__PURE__ */ new Date()).toISOString()
193
+ });
194
+ const idx = subs.findIndex((s) => s.id === sub.id);
195
+ if (idx !== -1) {
196
+ subs[idx] = {
197
+ ...subs[idx],
198
+ eventsProcessed: subs[idx].eventsProcessed + 1,
199
+ lastEventAt: (/* @__PURE__ */ new Date()).toISOString()
200
+ };
201
+ await writeSubscriptions(dataDir, subs);
202
+ }
203
+ return {
204
+ processed: 1,
205
+ skipped: 0
206
+ };
207
+ } catch {
208
+ return {
209
+ processed: 0,
210
+ skipped: 1
211
+ };
212
+ }
213
+ }
214
+ //#endregion
215
+ //#region src/mcp/tools/get-capabilities.ts
216
+ async function handleGetCapabilities() {
217
+ return { content: [{
218
+ type: "text",
219
+ text: CAPABILITIES_TEXT
220
+ }] };
221
+ }
222
+ function registerGetCapabilities(server) {
223
+ server.registerTool("get_capabilities", {
224
+ title: "Get Capabilities",
225
+ description: "Returns all available MCP tools, their inputs, and the CRM workflow guide. Call this first to understand what DatasynxOpenCRM can do.",
226
+ inputSchema: z.object({})
227
+ }, async () => handleGetCapabilities());
228
+ }
229
+ //#endregion
230
+ //#region src/mcp/tools/get-active-session.ts
231
+ async function handleGetActiveSession() {
232
+ const session = getSession();
233
+ const result = session ? {
234
+ hasSession: true,
235
+ customerSlug: session.customerSlug,
236
+ customerName: session.customerName,
237
+ startedAt: session.startedAt,
238
+ ...session.owner !== void 0 ? { owner: session.owner } : {}
239
+ } : { hasSession: false };
240
+ return { content: [{
241
+ type: "text",
242
+ text: JSON.stringify(result, null, 2)
243
+ }] };
244
+ }
245
+ function registerGetActiveSession(server) {
246
+ server.registerTool("get_active_session", {
247
+ title: "Get Active Session",
248
+ description: "Check which customer is currently active in the session store. Returns session info if a customer session is open, otherwise returns hasSession: false.",
249
+ inputSchema: z.object({})
250
+ }, async () => handleGetActiveSession());
251
+ }
252
+ //#endregion
253
+ //#region src/mcp/tools/get-customer-context.ts
254
+ const DATA_DIR$50 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
255
+ function triggerOnQuerySync(dataDir, slug) {
256
+ const auth = getGmailAuth();
257
+ if (!auth) return;
258
+ const lastSync = getLastGmailSync(dataDir, slug);
259
+ const thirtyMinAgo = /* @__PURE__ */ new Date(Date.now() - 1800 * 1e3);
260
+ if (lastSync && lastSync >= thirtyMinAgo) return;
261
+ const sourcesPath = path.join(dataDir, "customers", slug, "sources.json");
262
+ if (!fs.existsSync(sourcesPath)) return;
263
+ try {
264
+ const sources = JSON.parse(fs.readFileSync(sourcesPath, "utf-8"));
265
+ if (!sources.gmail?.enabled || !sources.gmail.query) return;
266
+ const query = sources.gmail.query;
267
+ import("./gmail-sync-DIaxInDT.js").then(({ syncGmail }) => syncGmail({
268
+ slug,
269
+ dataDir,
270
+ auth,
271
+ query
272
+ }).then(() => updateSlugSyncState(dataDir, slug, { lastGmailSync: (/* @__PURE__ */ new Date()).toISOString() })).catch(() => {})).catch(() => {});
273
+ } catch {}
274
+ }
275
+ async function handleGetCustomerContext(input, dataDir = DATA_DIR$50) {
276
+ const targetSlug = input.slug ?? getSession()?.customerSlug;
277
+ if (!targetSlug) return {
278
+ content: [{
279
+ type: "text",
280
+ text: "No customer specified and no active session. Use: get_customer_context({ slug: 'acme-corp' })"
281
+ }],
282
+ isError: true
283
+ };
284
+ const actor = process.env["DXCRM_ACTOR"] ?? "system";
285
+ if (!canSeeCustomer(dataDir, actor, targetSlug)) return {
286
+ content: [{
287
+ type: "text",
288
+ text: `Access denied: '${actor}' cannot view customer '${targetSlug}'.`
289
+ }],
290
+ isError: true
291
+ };
292
+ triggerOnQuerySync(dataDir, targetSlug);
293
+ try {
294
+ return { content: [{
295
+ type: "text",
296
+ text: await buildContext(dataDir, targetSlug)
297
+ }] };
298
+ } catch (err) {
299
+ return {
300
+ content: [{
301
+ type: "text",
302
+ text: `Error: ${err.message}`
303
+ }],
304
+ isError: true
305
+ };
306
+ }
307
+ }
308
+ function registerGetCustomerContext(server) {
309
+ server.registerTool("get_customer_context", {
310
+ title: "Get Customer Context",
311
+ description: `Returns a complete, LLM-ready context block for a customer.
312
+ Use this before any customer-related conversation or action.
313
+ Automatically triggers a background Gmail sync if last sync was >30 minutes ago.
314
+
315
+ Args:
316
+ slug: Customer ID (e.g. "acme-corp"). Leave empty to use active session customer.
317
+
318
+ Returns: Structured markdown with Quick Reference, Contacts, Critical Context,
319
+ Recent Activity (last 10 interactions), Pipeline, and Open Questions.
320
+
321
+ Performance: <3 seconds. Token budget: <3000.`,
322
+ inputSchema: z.object({ slug: z.string().optional().describe("Customer slug (e.g. 'acme-corp'). Leave empty for active session customer.") })
323
+ }, async ({ slug }) => handleGetCustomerContext({ ...slug !== void 0 ? { slug } : {} }));
324
+ }
325
+ //#endregion
326
+ //#region src/mcp/tools/search-customer-knowledge.ts
327
+ const DATA_DIR$49 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
328
+ async function handleSearchCustomerKnowledge(input, dataDir = DATA_DIR$49) {
329
+ const limit = input.limit ?? 5;
330
+ try {
331
+ const results = await searchKnowledge(dataDir, input.slug, input.query, limit);
332
+ const response = results.length === 0 ? {
333
+ results: [],
334
+ message: `No results found for "${input.query}" in customer "${input.slug}". The customer may not have been synced yet. Run dxcrm sync to index emails and transcripts.`
335
+ } : { results };
336
+ return { content: [{
337
+ type: "text",
338
+ text: JSON.stringify(response, null, 2)
339
+ }] };
340
+ } catch {
341
+ return { content: [{
342
+ type: "text",
343
+ text: JSON.stringify({
344
+ results: [],
345
+ message: `Search unavailable for customer "${input.slug}". LanceDB may not be initialized.`
346
+ })
347
+ }] };
348
+ }
349
+ }
350
+ function registerSearchCustomerKnowledge(server) {
351
+ server.registerTool("search_customer_knowledge", {
352
+ title: "Search Customer Knowledge",
353
+ description: `Hybrid vector + full-text search across all emails and transcripts for a customer.
354
+ Use when you need to find specific information from past communications.
355
+
356
+ Args:
357
+ slug: Customer ID (e.g. "acme-corp")
358
+ query: Natural language search query (e.g. "pricing discussion", "GDPR concerns")
359
+ limit: Max results to return (default 5)
360
+
361
+ Returns: { results: Array<{ content, score, source }> }
362
+ If no results: returns empty array with a helpful sync suggestion.`,
363
+ inputSchema: z.object({
364
+ slug: z.string().describe("Customer slug (e.g. 'acme-corp')"),
365
+ query: z.string().describe("Search query (natural language or keywords)"),
366
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 5)")
367
+ })
368
+ }, async ({ slug, query, limit }) => handleSearchCustomerKnowledge({
369
+ slug,
370
+ query,
371
+ ...limit !== void 0 ? { limit } : {}
372
+ }));
373
+ }
374
+ //#endregion
375
+ //#region src/mcp/tools/list-customers.ts
376
+ const DATA_DIR$48 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
377
+ function extractLastInteractionDate(interactionsPath) {
378
+ if (!fs.existsSync(interactionsPath)) return void 0;
379
+ const content = fs.readFileSync(interactionsPath, "utf-8");
380
+ const match = /^## (\d{4}-\d{2}-\d{2})/m.exec(content);
381
+ return match ? match[1] : void 0;
382
+ }
383
+ async function handleListCustomers(input, dataDir = DATA_DIR$48) {
384
+ const customersDir = path.join(dataDir, "customers");
385
+ const customers = [];
386
+ if (!fs.existsSync(customersDir)) return { content: [{
387
+ type: "text",
388
+ text: JSON.stringify([], null, 2)
389
+ }] };
390
+ const entries = fs.readdirSync(customersDir);
391
+ for (const entry of entries) {
392
+ const customerDir = path.join(customersDir, entry);
393
+ try {
394
+ if (!fs.statSync(customerDir).isDirectory()) continue;
395
+ } catch {
396
+ continue;
397
+ }
398
+ const mainFactsPath = path.join(customerDir, "main_facts.md");
399
+ if (!fs.existsSync(mainFactsPath)) continue;
400
+ try {
401
+ const data = matter(fs.readFileSync(mainFactsPath, "utf-8")).data;
402
+ const name = typeof data["name"] === "string" ? data["name"] : entry;
403
+ const stage = typeof data["relationship_stage"] === "string" ? data["relationship_stage"] : "unknown";
404
+ const dealValue = typeof data["deal_value"] === "number" ? data["deal_value"] : void 0;
405
+ const lastInteraction = extractLastInteractionDate(path.join(customerDir, "interactions.md"));
406
+ const summary = {
407
+ slug: entry,
408
+ name,
409
+ stage,
410
+ ...lastInteraction !== void 0 ? { lastInteraction } : {},
411
+ ...dealValue !== void 0 ? { dealValue } : {}
412
+ };
413
+ if (input.filter) {
414
+ const filterLower = input.filter.toLowerCase();
415
+ if (!(name.toLowerCase().includes(filterLower) || entry.toLowerCase().includes(filterLower) || stage.toLowerCase().includes(filterLower))) continue;
416
+ }
417
+ if (!canSeeCustomer(dataDir, process.env["DXCRM_ACTOR"] ?? "system", entry)) continue;
418
+ customers.push(summary);
419
+ } catch {
420
+ continue;
421
+ }
422
+ }
423
+ return { content: [{
424
+ type: "text",
425
+ text: JSON.stringify(customers, null, 2)
426
+ }] };
427
+ }
428
+ function registerListCustomers(server) {
429
+ server.registerTool("list_customers", {
430
+ title: "List Customers",
431
+ description: `List all customers with their pipeline stage, last interaction date, and deal value.
432
+ Useful for morning briefings and pipeline overviews.
433
+
434
+ Args:
435
+ filter: Optional substring to filter by name or slug (case-insensitive)
436
+
437
+ Returns: Array of { slug, name, stage, lastInteraction?, dealValue? }`,
438
+ inputSchema: z.object({ filter: z.string().optional().describe("Substring filter on customer name or slug (case-insensitive)") })
439
+ }, async ({ filter }) => handleListCustomers({ ...filter !== void 0 ? { filter } : {} }));
440
+ }
441
+ //#endregion
442
+ //#region src/mcp/tools/log-interaction.ts
443
+ const DATA_DIR$47 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
444
+ async function handleLogInteraction(input, dataDir = DATA_DIR$47) {
445
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
446
+ const interactionDate = input.date ?? today;
447
+ const sourceRef = input.source ?? `agent://log/${Date.now()}`;
448
+ const entry = {
449
+ date: interactionDate,
450
+ type: input.type,
451
+ with: input.with,
452
+ summary: input.summary,
453
+ nextSteps: input.nextSteps ?? [],
454
+ sourceRef,
455
+ synced: (/* @__PURE__ */ new Date()).toISOString(),
456
+ ...input.direction !== void 0 ? { direction: input.direction } : {}
457
+ };
458
+ const interactionsPath = path.join(dataDir, "customers", input.slug, "interactions.md");
459
+ const entryText = formatInteractionEntry(entry);
460
+ try {
461
+ enforceRbac(dataDir, "log_interaction");
462
+ await appendInteraction(dataDir, input.slug, entry);
463
+ updateGraphFromInteraction(dataDir, input.slug, {
464
+ withStr: input.with,
465
+ interactionDate
466
+ }).catch(() => {});
467
+ updateHealthFromInteraction(dataDir, input.slug).catch(() => {});
468
+ const mainFactsPath = path.join(dataDir, "customers", input.slug, "main_facts.md");
469
+ if (fs.existsSync(mainFactsPath)) try {
470
+ const raw = matter(fs.readFileSync(mainFactsPath, "utf-8"));
471
+ raw.data.last_touchpoint = interactionDate;
472
+ let serialized = matter.stringify(raw.content, raw.data);
473
+ serialized = serialized.replace(/^(last_touchpoint:\s*)['"](\d{4}-\d{2}-\d{2})['"]/m, "$1$2");
474
+ fs.writeFileSync(mainFactsPath, serialized, "utf-8");
475
+ } catch {}
476
+ writeAuditEntry(dataDir, {
477
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
478
+ actor: getActor(),
479
+ tool: "log_interaction",
480
+ slug: input.slug,
481
+ summary: input.summary
482
+ });
483
+ return { content: [{
484
+ type: "text",
485
+ text: JSON.stringify({
486
+ success: true,
487
+ path: interactionsPath,
488
+ entry: entryText
489
+ }, null, 2)
490
+ }] };
491
+ } catch (err) {
492
+ return { content: [{
493
+ type: "text",
494
+ text: JSON.stringify({
495
+ success: false,
496
+ error: err.message
497
+ }, null, 2)
498
+ }] };
499
+ }
500
+ }
501
+ function registerLogInteraction(server) {
502
+ server.registerTool("log_interaction", {
503
+ title: "Log Interaction",
504
+ description: `Write a new interaction entry to the CRM. Use after every call, meeting, or email.
505
+ Format matches auto-synced entries exactly — immediately searchable.
506
+
507
+ Args:
508
+ slug: Customer ID
509
+ type: Interaction type ("Email" | "Call" | "Meeting" | "Note" | "Demo" | "Proposal" | "Contract" | "Other")
510
+ summary: 2-5 sentences describing what happened (min 1 char)
511
+ with: Who was involved (name or email address)
512
+ nextSteps: Array of action items (optional)
513
+ direction: "inbound" or "outbound" (optional)
514
+ source: Source reference string (optional, auto-generated if omitted)
515
+
516
+ Returns: { success: boolean, path: string, entry: string }`,
517
+ inputSchema: z.object({
518
+ slug: z.string().describe("Customer slug (e.g. 'acme-corp')"),
519
+ type: z.enum([
520
+ "Email",
521
+ "Call",
522
+ "Meeting",
523
+ "Note",
524
+ "Demo",
525
+ "Proposal",
526
+ "Contract",
527
+ "Other"
528
+ ]).describe("Type of interaction"),
529
+ summary: z.string().min(1).describe("2-5 sentence summary of what happened"),
530
+ with: z.string().describe("Who was involved (name or email)"),
531
+ nextSteps: z.array(z.string()).optional().describe("Action items for follow-up"),
532
+ direction: z.enum(["inbound", "outbound"]).optional().describe("Direction of communication"),
533
+ source: z.string().optional().describe("Source reference (e.g. gmail://thread/123). Auto-generated if omitted."),
534
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Date of interaction (YYYY-MM-DD). Defaults to today.")
535
+ })
536
+ }, async ({ slug, type, summary, with: withStr, nextSteps, direction, source, date }) => handleLogInteraction({
537
+ slug,
538
+ type,
539
+ summary,
540
+ with: withStr,
541
+ ...nextSteps !== void 0 ? { nextSteps } : {},
542
+ ...direction !== void 0 ? { direction } : {},
543
+ ...source !== void 0 ? { source } : {},
544
+ ...date !== void 0 ? { date } : {}
545
+ }));
546
+ }
547
+ //#endregion
548
+ //#region src/mcp/tools/export-customer.ts
549
+ const DATA_DIR$46 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
550
+ function countInteractions(content) {
551
+ const matches = content.match(/^## \d{4}-\d{2}-\d{2}/gm);
552
+ return matches ? matches.length : 0;
553
+ }
554
+ async function handleExportCustomer(input, dataDir = DATA_DIR$46) {
555
+ enforceRbac(dataDir, "export_customer");
556
+ const customerDir = path.join(dataDir, "customers", input.slug);
557
+ if (!fs.existsSync(customerDir)) return {
558
+ content: [{
559
+ type: "text",
560
+ text: `Error: Customer '${input.slug}' not found. Check 'list_customers()' for available customers.`
561
+ }],
562
+ isError: true
563
+ };
564
+ const format = input.format ?? "json";
565
+ const mainFactsPath = path.join(customerDir, "main_facts.md");
566
+ let mainFacts = {};
567
+ let mainFactsContent = "";
568
+ if (fs.existsSync(mainFactsPath)) {
569
+ const raw = matter(fs.readFileSync(mainFactsPath, "utf-8"));
570
+ mainFacts = raw.data;
571
+ mainFactsContent = raw.content ?? "";
572
+ }
573
+ const interactionsPath = path.join(customerDir, "interactions.md");
574
+ let interactionsContent = "";
575
+ let interactionsCount = 0;
576
+ if (fs.existsSync(interactionsPath)) {
577
+ interactionsContent = fs.readFileSync(interactionsPath, "utf-8");
578
+ interactionsCount = countInteractions(interactionsContent);
579
+ }
580
+ const pipeline = await readPipeline(dataDir, input.slug);
581
+ const attachmentsDir = path.join(customerDir, "attachments");
582
+ const attachments = [];
583
+ if (fs.existsSync(attachmentsDir)) try {
584
+ const files = fs.readdirSync(attachmentsDir);
585
+ for (const f of files) try {
586
+ if (fs.statSync(path.join(attachmentsDir, f)).isFile()) attachments.push(f);
587
+ } catch {}
588
+ } catch {}
589
+ if (format === "markdown") return { content: [{
590
+ type: "text",
591
+ text: [
592
+ `# Export: ${input.slug}`,
593
+ "",
594
+ "## Main Facts",
595
+ mainFactsContent.trim() || "(no content)",
596
+ "",
597
+ "## Metadata",
598
+ Object.entries(mainFacts).map(([k, v]) => `- **${k}**: ${JSON.stringify(v)}`).join("\n") || "(no metadata)",
599
+ "",
600
+ `## Interactions (${interactionsCount} total)`,
601
+ interactionsContent.trim() || "(no interactions)",
602
+ "",
603
+ "## Pipeline",
604
+ pipeline.length > 0 ? pipeline.map((d) => `- **${d.name}** · ${d.stage}${d.value !== void 0 ? ` · €${d.value}` : ""}${d.close_date ? ` · close: ${d.close_date}` : ""}`).join("\n") : "(no deals)",
605
+ "",
606
+ `## Attachments (${attachments.length})`,
607
+ attachments.length > 0 ? attachments.map((f) => `- ${f}`).join("\n") : "(none)"
608
+ ].join("\n")
609
+ }] };
610
+ const exported = {
611
+ slug: input.slug,
612
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
613
+ mainFacts,
614
+ interactionsCount,
615
+ pipeline,
616
+ attachments
617
+ };
618
+ return { content: [{
619
+ type: "text",
620
+ text: JSON.stringify(exported, null, 2)
621
+ }] };
622
+ }
623
+ function registerExportCustomer(server) {
624
+ server.registerTool("export_customer", {
625
+ title: "Export Customer",
626
+ description: `Export all customer data (main_facts + interactions count + pipeline deals).
627
+ Useful for reporting, audits, or creating backups.
628
+
629
+ Args:
630
+ slug: Customer ID (e.g. "acme-corp")
631
+ format: Output format — "json" (default) or "markdown"
632
+
633
+ Returns:
634
+ JSON: { slug, exportedAt, mainFacts, interactionsCount, pipeline }
635
+ Markdown: Formatted document with all sections`,
636
+ inputSchema: z.object({
637
+ slug: z.string().describe("Customer slug (e.g. 'acme-corp')"),
638
+ format: z.enum(["json", "markdown"]).optional().describe("Output format: 'json' (default) or 'markdown'")
639
+ })
640
+ }, async ({ slug, format }) => handleExportCustomer({
641
+ slug,
642
+ ...format !== void 0 ? { format } : {}
643
+ }));
644
+ }
645
+ //#endregion
646
+ //#region src/mcp/tools/update-customer-facts.ts
647
+ const DATA_DIR$45 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
648
+ async function handleUpdateCustomerFacts(input, dataDir = DATA_DIR$45) {
649
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
650
+ try {
651
+ enforceRbac(dataDir, "update_customer_facts");
652
+ let existing;
653
+ let created = false;
654
+ if (!customerExists(dataDir, input.slug)) {
655
+ await ensureCustomerDir(dataDir, input.slug);
656
+ existing = {
657
+ name: input.name ?? input.slug,
658
+ relationship_stage: "prospect",
659
+ currency: "EUR",
660
+ tags: [],
661
+ created: today,
662
+ updated: today
663
+ };
664
+ created = true;
665
+ } else existing = await readMainFacts(dataDir, input.slug);
666
+ const updated = {
667
+ ...existing,
668
+ ...input.name !== void 0 ? { name: input.name } : {},
669
+ ...input.domain !== void 0 ? { domain: input.domain } : {},
670
+ ...input.email !== void 0 ? { email: input.email } : {},
671
+ ...input.phone !== void 0 ? { phone: input.phone } : {},
672
+ ...input.industry !== void 0 ? { industry: input.industry } : {},
673
+ ...input.relationshipStage !== void 0 ? { relationship_stage: input.relationshipStage } : {},
674
+ ...input.dealValue !== void 0 ? { deal_value: input.dealValue } : {},
675
+ ...input.primaryContact !== void 0 ? { primary_contact: input.primaryContact } : {},
676
+ ...input.timezone !== void 0 ? { timezone: input.timezone } : {},
677
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
678
+ updated: today
679
+ };
680
+ await writeMainFacts(dataDir, input.slug, updated);
681
+ writeAuditEntry(dataDir, {
682
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
683
+ actor: getActor(),
684
+ tool: "update_customer_facts",
685
+ slug: input.slug,
686
+ summary: Object.keys(input).filter((k) => k !== "slug").join(", ")
687
+ });
688
+ return { content: [{
689
+ type: "text",
690
+ text: JSON.stringify({
691
+ success: true,
692
+ created,
693
+ facts: updated
694
+ }, null, 2)
695
+ }] };
696
+ } catch (err) {
697
+ return { content: [{
698
+ type: "text",
699
+ text: JSON.stringify({
700
+ success: false,
701
+ error: err.message
702
+ }, null, 2)
703
+ }] };
704
+ }
705
+ }
706
+ function registerUpdateCustomerFacts(server) {
707
+ server.registerTool("update_customer_facts", {
708
+ title: "Update Customer Facts",
709
+ description: `Create or update a customer's main_facts.md profile.
710
+ If the customer slug does not exist yet, creates the customer directory and initial profile.
711
+ If it exists, merges the provided fields into existing data.
712
+
713
+ Use to add a new customer ("create acme-corp") or update existing info.
714
+
715
+ Args:
716
+ slug: Customer ID / slug — e.g. "acme-corp" (kebab-case, no spaces)
717
+ name: Company name (used as display name)
718
+ domain: Primary domain (e.g. "acme.com")
719
+ email: Primary contact email
720
+ phone: Phone number
721
+ industry: Industry vertical
722
+ relationshipStage: "prospect" | "active" | "churned" | "paused"
723
+ dealValue: Expected deal value in EUR
724
+ primaryContact: Primary contact person name
725
+ timezone: Timezone (e.g. "Europe/Berlin")
726
+ tags: Array of tags (replaces existing tags)
727
+
728
+ Returns: { success: boolean, created: boolean, facts: object }`,
729
+ inputSchema: z.object({
730
+ slug: z.string().describe("Customer slug (e.g. 'acme-corp')"),
731
+ name: z.string().optional().describe("Company name"),
732
+ domain: z.string().optional().describe("Primary domain"),
733
+ email: z.string().optional().describe("Primary contact email"),
734
+ phone: z.string().optional().describe("Phone number"),
735
+ industry: z.string().optional().describe("Industry vertical"),
736
+ relationshipStage: z.enum([
737
+ "prospect",
738
+ "active",
739
+ "churned",
740
+ "paused"
741
+ ]).optional().describe("Relationship stage"),
742
+ dealValue: z.number().optional().describe("Expected deal value in EUR"),
743
+ primaryContact: z.string().optional().describe("Primary contact person name"),
744
+ timezone: z.string().optional().describe("Timezone (e.g. Europe/Berlin)"),
745
+ tags: z.array(z.string()).optional().describe("Tags (replaces existing)")
746
+ })
747
+ }, async (input) => handleUpdateCustomerFacts(input));
748
+ }
749
+ //#endregion
750
+ //#region src/core/deal-health.ts
751
+ function grade(score) {
752
+ if (score >= 80) return "A";
753
+ if (score >= 65) return "B";
754
+ if (score >= 50) return "C";
755
+ if (score >= 35) return "D";
756
+ return "F";
757
+ }
758
+ function scoreDeal(deal, signals) {
759
+ let score = 100;
760
+ const warnings = [];
761
+ if (signals.daysSinceLastActivity > 60) {
762
+ score -= 40;
763
+ warnings.push(`No activity in ${signals.daysSinceLastActivity} days`);
764
+ } else if (signals.daysSinceLastActivity > 30) {
765
+ score -= 25;
766
+ warnings.push(`Low activity — last touch ${signals.daysSinceLastActivity} days ago`);
767
+ } else if (signals.daysSinceLastActivity > 14) score -= 10;
768
+ if (signals.daysInCurrentStage > 90) {
769
+ score -= 25;
770
+ warnings.push(`Stuck in "${deal.stage}" for ${signals.daysInCurrentStage} days`);
771
+ } else if (signals.daysInCurrentStage > 45) {
772
+ score -= 12;
773
+ warnings.push(`Slow progress in "${deal.stage}"`);
774
+ }
775
+ if (signals.daysToClose !== void 0) {
776
+ if (signals.daysToClose < 0) {
777
+ score -= 20;
778
+ warnings.push("Close date passed");
779
+ } else if (signals.daysToClose < 7) {
780
+ score -= 10;
781
+ warnings.push("Close date in less than 7 days");
782
+ }
783
+ }
784
+ if (signals.probability !== void 0) {
785
+ if (signals.probability < 20 && deal.stage !== "lead") {
786
+ score -= 15;
787
+ warnings.push(`Low probability (${signals.probability}%) for stage "${deal.stage}"`);
788
+ }
789
+ }
790
+ score = Math.max(0, score);
791
+ return {
792
+ score,
793
+ grade: grade(score),
794
+ signals,
795
+ warnings
796
+ };
797
+ }
798
+ //#endregion
799
+ //#region src/mcp/tools/get-deal-health.ts
800
+ const DATA_DIR$44 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
801
+ async function handleGetDealHealth(input, dataDir = DATA_DIR$44) {
802
+ try {
803
+ const deals = await readPipeline(dataDir, input.slug);
804
+ const today = /* @__PURE__ */ new Date();
805
+ const results = [];
806
+ for (const deal of deals) {
807
+ const updatedDate = deal.updated ? new Date(deal.updated) : today;
808
+ const daysSinceLastActivity = Math.floor((today.getTime() - updatedDate.getTime()) / (1e3 * 60 * 60 * 24));
809
+ const daysToClose = deal.close_date ? Math.floor((new Date(deal.close_date).getTime() - today.getTime()) / (1e3 * 60 * 60 * 24)) : void 0;
810
+ const health = scoreDeal(deal, {
811
+ daysSinceLastActivity,
812
+ daysInCurrentStage: daysSinceLastActivity,
813
+ ...daysToClose !== void 0 ? { daysToClose } : {},
814
+ ...deal.probability !== void 0 ? { probability: deal.probability } : {}
815
+ });
816
+ results.push({
817
+ deal: deal.name,
818
+ stage: deal.stage,
819
+ ...health
820
+ });
821
+ }
822
+ return { content: [{
823
+ type: "text",
824
+ text: JSON.stringify({
825
+ slug: input.slug,
826
+ deals: results
827
+ }, null, 2)
828
+ }] };
829
+ } catch (err) {
830
+ return { content: [{
831
+ type: "text",
832
+ text: JSON.stringify({
833
+ success: false,
834
+ error: err.message
835
+ }, null, 2)
836
+ }] };
837
+ }
838
+ }
839
+ function registerGetDealHealth(server) {
840
+ server.registerTool("get_deal_health", {
841
+ title: "Get Deal Health",
842
+ description: `Score the health of all deals for a customer (0–100). Grade A–F based on activity recency, stage velocity, close date proximity, and probability.
843
+
844
+ Returns: { slug, deals: [{ deal, stage, score, grade, signals, warnings }] }`,
845
+ inputSchema: z.object({ slug: z.string().describe("Customer slug") })
846
+ }, async ({ slug }) => handleGetDealHealth({ slug }));
847
+ }
848
+ //#endregion
849
+ //#region src/mcp/tools/get-pipeline-forecast.ts
850
+ const DATA_DIR$43 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
851
+ async function handleGetPipelineForecast(input, dataDir = DATA_DIR$43) {
852
+ try {
853
+ const customersDir = path.join(dataDir, "customers");
854
+ if (!fs.existsSync(customersDir)) return { content: [{
855
+ type: "text",
856
+ text: JSON.stringify({
857
+ deals: [],
858
+ totalWeightedValue: 0,
859
+ byStage: {}
860
+ }, null, 2)
861
+ }] };
862
+ const slugs = fs.readdirSync(customersDir).filter((d) => {
863
+ if (input.filter && !d.includes(input.filter)) return false;
864
+ return fs.statSync(path.join(customersDir, d)).isDirectory();
865
+ });
866
+ const allDeals = [];
867
+ for (const slug of slugs) {
868
+ const pipelinePath = path.join(customersDir, slug, "pipeline.md");
869
+ if (!fs.existsSync(pipelinePath)) continue;
870
+ const { readPipeline } = await import("./pipeline-writer-BqBrYrQc.js");
871
+ const deals = await readPipeline(dataDir, slug).catch(() => []);
872
+ for (const deal of deals) {
873
+ if (deal.stage === "won" || deal.stage === "lost") continue;
874
+ const prob = deal.probability ?? 50;
875
+ const value = deal.value ?? 0;
876
+ const forecastDeal = {
877
+ slug,
878
+ dealName: deal.name,
879
+ stage: deal.stage,
880
+ value,
881
+ probability: prob,
882
+ weightedValue: Math.round(value * prob / 100)
883
+ };
884
+ if (deal.close_date !== void 0) forecastDeal.closeDate = deal.close_date;
885
+ allDeals.push(forecastDeal);
886
+ }
887
+ }
888
+ const totalWeightedValue = allDeals.reduce((sum, d) => sum + d.weightedValue, 0);
889
+ const byStage = allDeals.reduce((acc, d) => {
890
+ if (!acc[d.stage]) acc[d.stage] = {
891
+ count: 0,
892
+ weightedValue: 0
893
+ };
894
+ acc[d.stage].count++;
895
+ acc[d.stage].weightedValue += d.weightedValue;
896
+ return acc;
897
+ }, {});
898
+ return { content: [{
899
+ type: "text",
900
+ text: JSON.stringify({
901
+ deals: allDeals,
902
+ totalWeightedValue,
903
+ byStage
904
+ }, null, 2)
905
+ }] };
906
+ } catch (err) {
907
+ return { content: [{
908
+ type: "text",
909
+ text: JSON.stringify({
910
+ success: false,
911
+ error: err.message
912
+ }, null, 2)
913
+ }] };
914
+ }
915
+ }
916
+ function registerGetPipelineForecast(server) {
917
+ server.registerTool("get_pipeline_forecast", {
918
+ title: "Get Pipeline Forecast",
919
+ description: `Aggregate weighted pipeline revenue across all customers. Groups open deals by stage, computes probability-weighted expected revenue.
920
+
921
+ Returns: { deals: [...], totalWeightedValue: number, byStage: { stage: { count, weightedValue } } }`,
922
+ inputSchema: z.object({ filter: z.string().optional().describe("Filter by customer slug substring") })
923
+ }, async ({ filter }) => handleGetPipelineForecast({ ...filter !== void 0 ? { filter } : {} }));
924
+ }
925
+ //#endregion
926
+ //#region src/mcp/tools/summarize-meeting.ts
927
+ const DATA_DIR$42 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
928
+ async function handleSummarizeMeeting(input, dataDir = DATA_DIR$42) {
929
+ try {
930
+ let summary = input.transcript.slice(0, 400);
931
+ let nextSteps = [];
932
+ try {
933
+ const { callLlm } = await import("./llm-DEjWcqmW.js");
934
+ const response = await callLlm(`Summarize this meeting transcript in 3-5 sentences and extract action items.\n\nTranscript:\n${input.transcript.slice(0, 3e3)}\n\nRespond as JSON: { "summary": "...", "nextSteps": ["..."] }`);
935
+ const parsed = JSON.parse(response);
936
+ summary = parsed.summary ?? summary;
937
+ nextSteps = parsed.nextSteps ?? [];
938
+ } catch {}
939
+ const date = input.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
940
+ const sourceRef = `agent://meeting/${Date.now()}`;
941
+ await appendInteraction(dataDir, input.slug, {
942
+ date,
943
+ type: "Meeting",
944
+ with: input.with ?? "Meeting Participant",
945
+ summary,
946
+ nextSteps,
947
+ sourceRef,
948
+ synced: (/* @__PURE__ */ new Date()).toISOString()
949
+ });
950
+ writeAuditEntry(dataDir, {
951
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
952
+ actor: getActor(),
953
+ tool: "summarize_meeting",
954
+ slug: input.slug,
955
+ summary: summary.slice(0, 100)
956
+ });
957
+ return { content: [{
958
+ type: "text",
959
+ text: JSON.stringify({
960
+ success: true,
961
+ summary,
962
+ nextSteps,
963
+ sourceRef
964
+ }, null, 2)
965
+ }] };
966
+ } catch (err) {
967
+ return { content: [{
968
+ type: "text",
969
+ text: JSON.stringify({
970
+ success: false,
971
+ error: err.message
972
+ }, null, 2)
973
+ }] };
974
+ }
975
+ }
976
+ function registerSummarizeMeeting(server) {
977
+ server.registerTool("summarize_meeting", {
978
+ title: "Summarize Meeting",
979
+ description: `Summarize a meeting transcript and log it as an interaction. Uses LLM to extract key points and action items (falls back to raw text slice if LLM unavailable).
980
+
981
+ Args:
982
+ slug: Customer ID
983
+ transcript: Full meeting transcript text
984
+ with: Participant name(s) (optional)
985
+ date: Meeting date YYYY-MM-DD (optional, defaults to today)
986
+
987
+ Returns: { success, summary, nextSteps, sourceRef }`,
988
+ inputSchema: z.object({
989
+ slug: z.string().describe("Customer slug"),
990
+ transcript: z.string().min(1).describe("Full meeting transcript"),
991
+ with: z.string().optional().describe("Participant names"),
992
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Meeting date")
993
+ })
994
+ }, async ({ slug, transcript, with: withStr, date }) => handleSummarizeMeeting({
995
+ slug,
996
+ transcript,
997
+ ...withStr !== void 0 ? { with: withStr } : {},
998
+ ...date !== void 0 ? { date } : {}
999
+ }));
1000
+ }
1001
+ //#endregion
1002
+ //#region src/mcp/tools/get-pipeline-stages.ts
1003
+ const DATA_DIR$41 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1004
+ async function handleGetPipelineStages(_input, dataDir = DATA_DIR$41) {
1005
+ const stages = getPipelineStages(dataDir);
1006
+ return { content: [{
1007
+ type: "text",
1008
+ text: JSON.stringify({ stages }, null, 2)
1009
+ }] };
1010
+ }
1011
+ function registerGetPipelineStages(server) {
1012
+ server.registerTool("get_pipeline_stages", {
1013
+ title: "Get Pipeline Stages",
1014
+ description: "Returns all configured pipeline stages. Falls back to default stages (lead, qualified, proposal, negotiation, won, lost) if no custom stages are configured.",
1015
+ inputSchema: z.object({})
1016
+ }, async () => handleGetPipelineStages({}));
1017
+ }
1018
+ //#endregion
1019
+ //#region src/core/cross-customer.ts
1020
+ async function searchAcrossCustomers(dataDir, query, limit = 5, excludeSlug) {
1021
+ const slugs = listCustomerSlugs(dataDir).filter((d) => d !== excludeSlug);
1022
+ const allResults = [];
1023
+ for (const slug of slugs) {
1024
+ const results = await searchKnowledge(dataDir, slug, query, 2);
1025
+ for (const r of results) allResults.push({
1026
+ slug,
1027
+ relevantContent: r.content.slice(0, 200),
1028
+ score: r.score
1029
+ });
1030
+ }
1031
+ return allResults.sort((a, b) => b.score - a.score).slice(0, limit);
1032
+ }
1033
+ //#endregion
1034
+ //#region src/mcp/tools/get-market-intelligence.ts
1035
+ const DATA_DIR$40 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1036
+ async function handleGetMarketIntelligence(input, dataDir = DATA_DIR$40) {
1037
+ const excludeSlug = input.excludeCurrentCustomer ? input.slug : void 0;
1038
+ const all = listCustomerSlugs(dataDir);
1039
+ const totalCustomersSearched = excludeSlug ? all.filter((s) => s !== excludeSlug).length : all.length;
1040
+ const results = await searchAcrossCustomers(dataDir, input.query, 10, excludeSlug);
1041
+ return { content: [{
1042
+ type: "text",
1043
+ text: JSON.stringify({
1044
+ query: input.query,
1045
+ results,
1046
+ totalCustomersSearched
1047
+ }, null, 2)
1048
+ }] };
1049
+ }
1050
+ function registerGetMarketIntelligence(server) {
1051
+ server.registerTool("get_market_intelligence", {
1052
+ title: "Get Market Intelligence",
1053
+ description: "Search across all customers to find patterns, common topics, or similar issues. Uses semantic search (LanceDB) across all customer knowledge bases. Results use slug (not real names) for privacy.",
1054
+ inputSchema: z.object({
1055
+ query: z.string().describe("What to search for across all customers"),
1056
+ excludeCurrentCustomer: z.boolean().optional().describe("Exclude the current customer from results"),
1057
+ slug: z.string().optional().describe("Current customer slug (used with excludeCurrentCustomer)")
1058
+ })
1059
+ }, async ({ query, excludeCurrentCustomer, slug }) => handleGetMarketIntelligence({
1060
+ query,
1061
+ ...excludeCurrentCustomer !== void 0 ? { excludeCurrentCustomer } : {},
1062
+ ...slug !== void 0 ? { slug } : {}
1063
+ }));
1064
+ }
1065
+ //#endregion
1066
+ //#region src/mcp/tools/get-relationship-graph.ts
1067
+ const DATA_DIR$39 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1068
+ function summarizeNode(n) {
1069
+ return {
1070
+ id: n.id,
1071
+ name: n.label,
1072
+ email: n.properties["email"]
1073
+ };
1074
+ }
1075
+ async function handleGetRelationshipGraph(input, dataDir = DATA_DIR$39) {
1076
+ try {
1077
+ const graph = readGraph(dataDir, input.slug);
1078
+ const stakeholders = getStakeholders(graph);
1079
+ const ownerContactIds = graph.nodes.filter((n) => n.type === "person" && n.properties["isOwnerContact"] === true).map((n) => n.id);
1080
+ const economicBuyerIds = stakeholders.economicBuyers.map((n) => n.id);
1081
+ const warmIntroPaths = [];
1082
+ for (const ebId of economicBuyerIds) for (const ownerId of ownerContactIds) {
1083
+ const p = findPath(graph, ownerId, ebId);
1084
+ if (p.length > 1) {
1085
+ warmIntroPaths.push({
1086
+ target: ebId,
1087
+ path: p
1088
+ });
1089
+ break;
1090
+ }
1091
+ }
1092
+ return { content: [{
1093
+ type: "text",
1094
+ text: JSON.stringify({
1095
+ slug: input.slug,
1096
+ nodeCount: graph.nodes.length,
1097
+ edgeCount: graph.edges.length,
1098
+ updatedAt: graph.updatedAt,
1099
+ stakeholders: {
1100
+ champions: stakeholders.champions.map(summarizeNode),
1101
+ blockers: stakeholders.blockers.map(summarizeNode),
1102
+ economicBuyers: stakeholders.economicBuyers.map(summarizeNode),
1103
+ allContacts: stakeholders.allContacts.map(summarizeNode),
1104
+ missingRoles: stakeholders.missingRoles
1105
+ },
1106
+ warmIntroPaths,
1107
+ nodes: graph.nodes,
1108
+ edges: graph.edges
1109
+ }, null, 2)
1110
+ }] };
1111
+ } catch (err) {
1112
+ return { content: [{
1113
+ type: "text",
1114
+ text: JSON.stringify({
1115
+ success: false,
1116
+ error: err.message
1117
+ }, null, 2)
1118
+ }] };
1119
+ }
1120
+ }
1121
+ function registerGetRelationshipGraph(server) {
1122
+ server.registerTool("get_relationship_graph", {
1123
+ title: "Get Relationship Graph",
1124
+ description: `Returns the knowledge graph for a customer: all known contacts, companies,
1125
+ and the relationships between them (KNOWS, WORKS_AT, IS_CHAMPION, IS_BLOCKER, IS_ECONOMIC_BUYER).
1126
+
1127
+ The graph auto-populates from every log_interaction call.
1128
+ Use this before a complex deal conversation to understand the stakeholder map.
1129
+
1130
+ Args:
1131
+ slug: Customer slug
1132
+
1133
+ Returns: {
1134
+ stakeholders: { champions[], blockers[], economicBuyers[], allContacts[], missingRoles[] },
1135
+ nodes: GraphNode[],
1136
+ edges: GraphEdge[]
1137
+ }`,
1138
+ inputSchema: z.object({ slug: z.string().describe("Customer slug (e.g. 'acme-corp')") })
1139
+ }, async ({ slug }) => handleGetRelationshipGraph({ slug }));
1140
+ }
1141
+ //#endregion
1142
+ //#region src/mcp/tools/get-relationship-health.ts
1143
+ const DATA_DIR$38 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1144
+ const MAX_HEALTH_AGE_MS = 3600 * 1e3;
1145
+ async function handleGetRelationshipHealth(input, dataDir = DATA_DIR$38) {
1146
+ try {
1147
+ let health = readHealth(dataDir, input.slug);
1148
+ if (health === null || Date.now() - new Date(health.updatedAt).getTime() > MAX_HEALTH_AGE_MS) {
1149
+ health = computeCustomerHealth(dataDir, input.slug);
1150
+ writeHealth(dataDir, input.slug, health);
1151
+ }
1152
+ return { content: [{
1153
+ type: "text",
1154
+ text: JSON.stringify({
1155
+ slug: input.slug,
1156
+ overallHealth: health.overallHealth,
1157
+ updatedAt: health.updatedAt,
1158
+ atRiskContacts: health.contacts.filter((c) => c.riskFlags.length > 0).map((c) => c.email ?? c.contactId),
1159
+ coldContacts: health.contacts.filter((c) => c.trend === "cold").map((c) => c.email ?? c.contactId),
1160
+ contacts: health.contacts
1161
+ }, null, 2)
1162
+ }] };
1163
+ } catch (err) {
1164
+ return { content: [{
1165
+ type: "text",
1166
+ text: JSON.stringify({
1167
+ success: false,
1168
+ error: err.message
1169
+ }, null, 2)
1170
+ }] };
1171
+ }
1172
+ }
1173
+ function registerGetRelationshipHealth(server) {
1174
+ server.registerTool("get_relationship_health", {
1175
+ title: "Get Relationship Health",
1176
+ description: `Returns health scores for all contacts of a customer.
1177
+ Scores decay automatically when communication cadence breaks — without any manual input.
1178
+
1179
+ Each contact gets:
1180
+ - score (0–100), grade (A–F), trend (rising|stable|declining|cold)
1181
+ - riskFlags: NO_CONTACT_14D, NO_CONTACT_30D, CHAMPION_SILENT
1182
+ - recommendation: concrete next action
1183
+
1184
+ overallHealth is the average across all contacts.
1185
+ atRiskContacts + coldContacts are pre-filtered for quick triage.
1186
+ Health auto-updates after every log_interaction call. Recomputes if stale (>1h).
1187
+
1188
+ Args:
1189
+ slug: Customer slug
1190
+
1191
+ Returns: {
1192
+ overallHealth: number,
1193
+ atRiskContacts: string[],
1194
+ coldContacts: string[],
1195
+ contacts: ContactHealth[]
1196
+ }`,
1197
+ inputSchema: z.object({ slug: z.string().describe("Customer slug (e.g. 'acme-corp')") })
1198
+ }, async ({ slug }) => handleGetRelationshipHealth({ slug }));
1199
+ }
1200
+ //#endregion
1201
+ //#region src/core/playbooks.ts
1202
+ function playbooksDir(dataDir, slug) {
1203
+ return path.join(dataDir, "customers", slug, "playbooks");
1204
+ }
1205
+ function listPlaybooks(dataDir, slug) {
1206
+ const dir = playbooksDir(dataDir, slug);
1207
+ if (!fs.existsSync(dir)) return [];
1208
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => {
1209
+ const filePath = path.join(dir, f);
1210
+ const parsed = matter(fs.readFileSync(filePath, "utf-8"));
1211
+ return {
1212
+ slug,
1213
+ name: f.replace(/\.md$/, ""),
1214
+ frontmatter: parsed.data,
1215
+ content: parsed.content.trim(),
1216
+ path: filePath
1217
+ };
1218
+ });
1219
+ }
1220
+ async function writePlaybook(dataDir, slug, playbook) {
1221
+ const dir = playbooksDir(dataDir, slug);
1222
+ const filePath = path.join(dir, `${playbook.name}.md`);
1223
+ await withFileQueue(filePath, async () => {
1224
+ fs.mkdirSync(dir, { recursive: true });
1225
+ const raw = matter.stringify(playbook.content, playbook.frontmatter);
1226
+ fs.writeFileSync(filePath, raw, "utf-8");
1227
+ });
1228
+ }
1229
+ function toKebabCase(name) {
1230
+ return name.replace(/[^a-z0-9-]/gi, "-").toLowerCase().replace(/-+/g, "-").replace(/^-|-$/g, "");
1231
+ }
1232
+ function parseTokens(tokens) {
1233
+ return tokens.flatMap((token) => {
1234
+ if (token.startsWith("deal_stage_")) return [{
1235
+ type: "stage",
1236
+ stage: token.slice(11)
1237
+ }];
1238
+ const valueGt = token.match(/^value\s*>\s*(\d+)$/);
1239
+ if (valueGt) return [{
1240
+ type: "value_gt",
1241
+ value: Number(valueGt[1])
1242
+ }];
1243
+ const valueLt = token.match(/^value\s*<\s*(\d+)$/);
1244
+ if (valueLt) return [{
1245
+ type: "value_lt",
1246
+ value: Number(valueLt[1])
1247
+ }];
1248
+ const stalledGt = token.match(/^days_stalled\s*>\s*(\d+)$/);
1249
+ if (stalledGt) return [{
1250
+ type: "days_stalled_gt",
1251
+ value: Number(stalledGt[1])
1252
+ }];
1253
+ const stalledLt = token.match(/^days_stalled\s*<\s*(\d+)$/);
1254
+ if (stalledLt) return [{
1255
+ type: "days_stalled_lt",
1256
+ value: Number(stalledLt[1])
1257
+ }];
1258
+ const healthLt = token.match(/^health\s*<\s*(\d+)$/);
1259
+ if (healthLt) return [{
1260
+ type: "health_lt",
1261
+ value: Number(healthLt[1])
1262
+ }];
1263
+ const healthGt = token.match(/^health\s*>\s*(\d+)$/);
1264
+ if (healthGt) return [{
1265
+ type: "health_gt",
1266
+ value: Number(healthGt[1])
1267
+ }];
1268
+ if (token === "no_champion") return [{ type: "no_champion" }];
1269
+ if (token === "has_champion") return [{ type: "has_champion" }];
1270
+ return [];
1271
+ });
1272
+ }
1273
+ function parseTriggerFull(triggerStr) {
1274
+ if (!triggerStr?.trim()) return {
1275
+ conditions: [],
1276
+ operator: "AND"
1277
+ };
1278
+ const orTokens = triggerStr.split(/\s+OR\s+/).map((t) => t.trim()).filter(Boolean);
1279
+ if (orTokens.length > 1) return {
1280
+ conditions: parseTokens(orTokens),
1281
+ operator: "OR"
1282
+ };
1283
+ return {
1284
+ conditions: parseTokens(triggerStr.split(/\s+AND\s+/).map((t) => t.trim()).filter(Boolean)),
1285
+ operator: "AND"
1286
+ };
1287
+ }
1288
+ function evaluateCondition(cond, deal, daysSinceContact) {
1289
+ switch (cond.type) {
1290
+ case "stage": return deal.stage === cond.stage;
1291
+ case "value_gt": return deal.value > (cond.value ?? 0);
1292
+ case "value_lt": return deal.value < (cond.value ?? Infinity);
1293
+ case "days_stalled_gt": return daysSinceContact > (cond.value ?? 0);
1294
+ case "days_stalled_lt": return daysSinceContact < (cond.value ?? Infinity);
1295
+ case "health_lt": return deal.healthScore < (cond.value ?? 100);
1296
+ case "health_gt": return deal.healthScore > (cond.value ?? 0);
1297
+ case "no_champion": return !deal.championPresent;
1298
+ case "has_champion": return deal.championPresent;
1299
+ default: return false;
1300
+ }
1301
+ }
1302
+ function matchPlaybooks(playbooks, deal, daysSinceContact = 0) {
1303
+ const results = [];
1304
+ for (const pb of playbooks) {
1305
+ const { conditions, operator } = parseTriggerFull(pb.frontmatter.trigger);
1306
+ if (conditions.length === 0) continue;
1307
+ const matched = conditions.filter((c) => evaluateCondition(c, deal, daysSinceContact));
1308
+ if (operator === "OR" ? matched.length > 0 : matched.length === conditions.length) results.push({
1309
+ playbook: pb,
1310
+ score: 1,
1311
+ matchedConditions: matched,
1312
+ totalConditions: conditions.length
1313
+ });
1314
+ }
1315
+ return results.sort((a, b) => {
1316
+ const rateDiff = (b.playbook.frontmatter.successRate ?? 0) - (a.playbook.frontmatter.successRate ?? 0);
1317
+ return rateDiff !== 0 ? rateDiff : (b.playbook.frontmatter.usedCount ?? 0) - (a.playbook.frontmatter.usedCount ?? 0);
1318
+ });
1319
+ }
1320
+ function buildDistillPrompt(slug, dealName, outcome, interactions) {
1321
+ return `You are analyzing a sales deal to extract a reusable playbook.
1322
+
1323
+ Customer: ${slug}
1324
+ Deal: ${dealName}
1325
+ Outcome: ${outcome}
1326
+ Interactions (chronological):
1327
+ ${interactions.slice(0, 4e3)}
1328
+
1329
+ Extract a reusable playbook from this deal's journey.
1330
+
1331
+ Allowed trigger tokens (combine with " AND "):
1332
+ - deal_stage_<stage> (e.g. deal_stage_negotiation)
1333
+ - value > <n> (e.g. value > 50000)
1334
+ - value < <n>
1335
+ - days_stalled > <n> (e.g. days_stalled > 7)
1336
+ - days_stalled < <n>
1337
+ - health < <n> (e.g. health < 60)
1338
+ - health > <n>
1339
+ - no_champion
1340
+ - has_champion
1341
+
1342
+ Return JSON only (no markdown wrapper):
1343
+ {
1344
+ "name": "<kebab-case-playbook-name>",
1345
+ "trigger": "<DSL string using allowed tokens>",
1346
+ "content": "<markdown with ## Situation, ## Steps, ## Warnings sections>",
1347
+ "successRate": <0.0-1.0>,
1348
+ "reasoning": "<why these trigger conditions>"
1349
+ }`;
1350
+ }
1351
+ function parseLlmDistillation(response, outcomeFallback = .5) {
1352
+ try {
1353
+ const match = response.match(/\{[\s\S]*\}/);
1354
+ if (!match) return null;
1355
+ const parsed = JSON.parse(match[0]);
1356
+ if (!parsed.name || !parsed.trigger || !parsed.content) return null;
1357
+ return {
1358
+ name: parsed.name,
1359
+ trigger: parsed.trigger,
1360
+ content: parsed.content,
1361
+ successRate: typeof parsed.successRate === "number" ? parsed.successRate : outcomeFallback,
1362
+ reasoning: parsed.reasoning ?? ""
1363
+ };
1364
+ } catch {
1365
+ return null;
1366
+ }
1367
+ }
1368
+ async function distillPlaybook(dataDir, slug, dealName, outcome, llmFn = callLlm) {
1369
+ const interactionsPath = path.join(dataDir, "customers", slug, "interactions.md");
1370
+ if (!fs.existsSync(interactionsPath)) return {
1371
+ ok: false,
1372
+ errorKind: "no_interactions"
1373
+ };
1374
+ const distillation = parseLlmDistillation(await llmFn(buildDistillPrompt(slug, dealName, outcome, fs.readFileSync(interactionsPath, "utf-8"))), outcome === "won" ? 1 : 0);
1375
+ if (!distillation) return {
1376
+ ok: false,
1377
+ errorKind: "parse_failed"
1378
+ };
1379
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1380
+ const name = toKebabCase(distillation.name);
1381
+ const playbook = {
1382
+ slug,
1383
+ name,
1384
+ frontmatter: {
1385
+ trigger: distillation.trigger,
1386
+ successRate: distillation.successRate,
1387
+ usedCount: 1,
1388
+ lastUpdated: today
1389
+ },
1390
+ content: distillation.content,
1391
+ path: path.join(playbooksDir(dataDir, slug), `${name}.md`)
1392
+ };
1393
+ await writePlaybook(dataDir, slug, playbook);
1394
+ return {
1395
+ ok: true,
1396
+ playbook,
1397
+ reasoning: distillation.reasoning
1398
+ };
1399
+ }
1400
+ //#endregion
1401
+ //#region src/agents/deal-agent.ts
1402
+ function agentQueuePath(dataDir, slug) {
1403
+ return path.join(dataDir, "customers", slug, "agent-queue.json");
1404
+ }
1405
+ function readAgentQueue(dataDir, slug) {
1406
+ const p = agentQueuePath(dataDir, slug);
1407
+ if (!fs.existsSync(p)) return {
1408
+ schemaVersion: "1",
1409
+ slug,
1410
+ pendingActions: [],
1411
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1412
+ };
1413
+ try {
1414
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
1415
+ } catch {
1416
+ return {
1417
+ schemaVersion: "1",
1418
+ slug,
1419
+ pendingActions: [],
1420
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1421
+ };
1422
+ }
1423
+ }
1424
+ function writeAgentQueue(dataDir, slug, queue) {
1425
+ const p = agentQueuePath(dataDir, slug);
1426
+ const dir = path.dirname(p);
1427
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1428
+ const updated = {
1429
+ ...queue,
1430
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1431
+ };
1432
+ fs.writeFileSync(p, JSON.stringify(updated, null, 2), "utf-8");
1433
+ }
1434
+ function makeActionId() {
1435
+ return `da_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1436
+ }
1437
+ function buildRecentInteractionsSummary(interactionsPath) {
1438
+ if (!fs.existsSync(interactionsPath)) return "(no interactions)";
1439
+ return fs.readFileSync(interactionsPath, "utf-8").split(/(?=^## \d{4}-\d{2}-\d{2})/m).filter((b) => b.trim().length > 0).slice(0, 3).map((b) => {
1440
+ const dateMatch = b.match(/^## (\d{4}-\d{2}-\d{2}) · (\w+)/m);
1441
+ const summaryMatch = b.match(/^\*\*Summary:\*\*\s*(.+)$/m);
1442
+ if (!dateMatch || !summaryMatch) return "";
1443
+ return `[${dateMatch[1]}/${dateMatch[2]}] ${summaryMatch[1].trim()}`;
1444
+ }).filter(Boolean).join("\n");
1445
+ }
1446
+ function buildContextSummary(data) {
1447
+ return [
1448
+ `Deal: ${data.deal.name} | Stage: ${data.deal.stage} | Value: €${data.deal.value ?? "?"} | Close: ${data.deal.close_date ?? "not set"}`,
1449
+ `Days since activity: ${data.daysSinceLastActivity} | Days to close: ${data.daysToClose ?? "?"}`,
1450
+ `Deal health: grade ${data.dealHealthScore.grade} (score ${data.dealHealthScore.score})`,
1451
+ `Warnings: ${data.dealHealthScore.warnings.join("; ") || "none"}`,
1452
+ ``,
1453
+ `Relationship health: ${data.health.overallHealth}/100`,
1454
+ `At-risk contacts: ${data.atRiskContacts.join(", ") || "none"}`,
1455
+ `Cold contacts: ${data.coldContacts.join(", ") || "none"}`,
1456
+ `Missing stakeholder roles: ${data.missingRoles.map((r) => r.role).join(", ") || "none"}`,
1457
+ `Champions identified: ${data.championCount}`,
1458
+ ``,
1459
+ `Recent interactions:`,
1460
+ data.recentInteractionsSummary || "(none)"
1461
+ ].join("\n");
1462
+ }
1463
+ async function observeDeal(dataDir, slug, dealName, today) {
1464
+ const deal = (await readPipeline(dataDir, slug).catch(() => [])).find((d) => d.name.toLowerCase() === dealName.toLowerCase());
1465
+ if (!deal) return null;
1466
+ const todayDate = new Date(today);
1467
+ const updatedDate = deal.updated ? new Date(deal.updated) : todayDate;
1468
+ const daysSinceLastActivity = Math.floor((todayDate.getTime() - updatedDate.getTime()) / 864e5);
1469
+ const daysInCurrentStage = daysSinceLastActivity;
1470
+ const daysToClose = deal.close_date && deal.close_date.trim() !== "" ? Math.floor((new Date(deal.close_date).getTime() - todayDate.getTime()) / 864e5) : void 0;
1471
+ const dealHealthScore = scoreDeal(deal, {
1472
+ daysSinceLastActivity,
1473
+ daysInCurrentStage,
1474
+ ...daysToClose !== void 0 ? { daysToClose } : {},
1475
+ ...deal.probability !== void 0 ? { probability: deal.probability } : {}
1476
+ });
1477
+ const health = computeCustomerHealth(dataDir, slug, today);
1478
+ const atRiskContacts = health.contacts.filter((c) => c.riskFlags.length > 0).map((c) => c.email ?? c.contactId);
1479
+ const coldContacts = health.contacts.filter((c) => c.trend === "cold").map((c) => c.email ?? c.contactId);
1480
+ const stakeholders = getStakeholders(readGraph(dataDir, slug));
1481
+ const missingRoles = stakeholders.missingRoles.map((r) => ({
1482
+ role: r.role,
1483
+ urgency: r.urgency
1484
+ }));
1485
+ const championCount = stakeholders.champions.length;
1486
+ const recentInteractionsSummary = buildRecentInteractionsSummary(path.join(dataDir, "customers", slug, "interactions.md"));
1487
+ const contextSummary = buildContextSummary({
1488
+ deal,
1489
+ daysSinceLastActivity,
1490
+ daysInCurrentStage,
1491
+ daysToClose,
1492
+ dealHealthScore,
1493
+ health,
1494
+ atRiskContacts,
1495
+ coldContacts,
1496
+ missingRoles,
1497
+ championCount,
1498
+ recentInteractionsSummary
1499
+ });
1500
+ const obs = {
1501
+ deal,
1502
+ daysSinceLastActivity,
1503
+ daysInCurrentStage,
1504
+ dealHealthScore,
1505
+ overallRelationshipHealth: health.overallHealth,
1506
+ atRiskContacts,
1507
+ coldContacts,
1508
+ missingRoles,
1509
+ championCount,
1510
+ recentInteractionsSummary,
1511
+ contextSummary
1512
+ };
1513
+ if (daysToClose !== void 0) obs.daysToClose = daysToClose;
1514
+ const dealSnap = {
1515
+ slug,
1516
+ name: deal.name,
1517
+ stage: deal.stage,
1518
+ value: deal.value ?? 0,
1519
+ probability: deal.probability ?? 50,
1520
+ healthScore: health.overallHealth,
1521
+ daysSinceContact: daysSinceLastActivity,
1522
+ championPresent: championCount > 0
1523
+ };
1524
+ const matchingPlaybooks = matchPlaybooks(listPlaybooks(dataDir, slug), dealSnap, daysSinceLastActivity);
1525
+ if (matchingPlaybooks.length > 0) obs.matchingPlaybooks = matchingPlaybooks;
1526
+ return obs;
1527
+ }
1528
+ function buildLlmPrompt(obs, config) {
1529
+ const instruction = config.instruction ?? "Analyze this deal and recommend next actions.";
1530
+ const playbookSection = obs.matchingPlaybooks && obs.matchingPlaybooks.length > 0 ? `\n## Matching Playbooks (${obs.matchingPlaybooks.length} found — apply these proven tactics)\n` + obs.matchingPlaybooks.slice(0, 2).map((m) => `### ${m.playbook.name} (${Math.round(m.playbook.frontmatter.successRate * 100)}% success rate, used ${m.playbook.frontmatter.usedCount}x)\n${m.playbook.content.slice(0, 500)}`).join("\n\n") : "";
1531
+ return `You are a CRM deal agent. Analyze the deal situation and return an action plan.
1532
+ Return ONLY valid JSON — no markdown, no explanation.
1533
+
1534
+ ${obs.contextSummary}${playbookSection}
1535
+
1536
+ Instruction: ${instruction}
1537
+
1538
+ Respond with JSON matching exactly:
1539
+ {
1540
+ "assessment": "<2-3 sentence situation assessment>",
1541
+ "riskLevel": "low" | "medium" | "high" | "critical",
1542
+ "plan": [
1543
+ { "step": 1, "action": "<what to do>", "priority": "urgent" | "high" | "medium" | "low", "reason": "<why>" }
1544
+ ],
1545
+ "actions": [
1546
+ {
1547
+ "type": "log_interaction" | "update_deal" | "alert" | "schedule_meeting",
1548
+ "payload": { /* tool-specific fields */ },
1549
+ "confidence": 0.0-1.0,
1550
+ "reasoning": "<why this action>"
1551
+ }
1552
+ ]
1553
+ }
1554
+
1555
+ Payload schema per type:
1556
+ - log_interaction: { slug, type: "Note"|"Call"|"Meeting", summary, with }
1557
+ - update_deal: { slug, dealName, stage?, probability?, closeDate?, notes? }
1558
+ - alert: { slug, message, urgency: "critical"|"high"|"medium" }
1559
+ - schedule_meeting: { slug, with, notes }`;
1560
+ }
1561
+ function parseLlmResponse(response) {
1562
+ try {
1563
+ const cleaned = response.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
1564
+ const parsed = JSON.parse(cleaned);
1565
+ if (!parsed.assessment || !parsed.riskLevel || !Array.isArray(parsed.plan)) return null;
1566
+ return {
1567
+ assessment: String(parsed.assessment),
1568
+ riskLevel: parsed.riskLevel,
1569
+ plan: Array.isArray(parsed.plan) ? parsed.plan : [],
1570
+ actions: Array.isArray(parsed.actions) ? parsed.actions : []
1571
+ };
1572
+ } catch {
1573
+ return null;
1574
+ }
1575
+ }
1576
+ function buildRuleBasedAnalysis(obs, config) {
1577
+ const plan = [];
1578
+ const actions = [];
1579
+ let riskLevel = "low";
1580
+ if (obs.dealHealthScore.grade === "F" || obs.coldContacts.length > 0) riskLevel = "critical";
1581
+ else if (obs.dealHealthScore.grade === "D" || obs.atRiskContacts.length > 0) riskLevel = "high";
1582
+ else if (obs.dealHealthScore.grade === "C") riskLevel = "medium";
1583
+ let step = 1;
1584
+ if (obs.matchingPlaybooks && obs.matchingPlaybooks.length > 0) for (const match of obs.matchingPlaybooks.slice(0, 2)) {
1585
+ plan.push({
1586
+ step: step++,
1587
+ action: `Apply playbook: "${match.playbook.name}"`,
1588
+ priority: "high",
1589
+ reason: `Proven tactic (${Math.round(match.playbook.frontmatter.successRate * 100)}% success, used ${match.playbook.frontmatter.usedCount}x) — trigger: ${match.playbook.frontmatter.trigger}`
1590
+ });
1591
+ actions.push({
1592
+ type: "alert",
1593
+ payload: {
1594
+ slug: config.slug,
1595
+ message: `Playbook available: "${match.playbook.name}" (${Math.round(match.playbook.frontmatter.successRate * 100)}% success rate)`,
1596
+ playbookContent: match.playbook.content.slice(0, 1e3),
1597
+ urgency: "high"
1598
+ },
1599
+ confidence: match.playbook.frontmatter.successRate,
1600
+ reasoning: `Trigger matched: ${match.playbook.frontmatter.trigger}`
1601
+ });
1602
+ }
1603
+ if (obs.coldContacts.length > 0) {
1604
+ plan.push({
1605
+ step: step++,
1606
+ action: `Re-engage cold contacts: ${obs.coldContacts.join(", ")}`,
1607
+ priority: "urgent",
1608
+ reason: "No contact in 30+ days"
1609
+ });
1610
+ actions.push({
1611
+ type: "alert",
1612
+ payload: {
1613
+ slug: config.slug,
1614
+ message: `Cold contacts: ${obs.coldContacts.join(", ")}`,
1615
+ urgency: "critical"
1616
+ },
1617
+ confidence: .95,
1618
+ reasoning: "No contact in 30+ days"
1619
+ });
1620
+ }
1621
+ if (obs.atRiskContacts.length > 0) {
1622
+ plan.push({
1623
+ step: step++,
1624
+ action: `Schedule call with at-risk contacts`,
1625
+ priority: "high",
1626
+ reason: "14+ days without contact"
1627
+ });
1628
+ actions.push({
1629
+ type: "schedule_meeting",
1630
+ payload: {
1631
+ slug: config.slug,
1632
+ with: obs.atRiskContacts[0] ?? "",
1633
+ notes: "Scheduled by deal agent — relationship at risk"
1634
+ },
1635
+ confidence: .8,
1636
+ reasoning: "At-risk contact identified"
1637
+ });
1638
+ }
1639
+ if (obs.missingRoles.some((r) => r.role === "economic_buyer")) plan.push({
1640
+ step: step++,
1641
+ action: "Identify economic buyer",
1642
+ priority: "high",
1643
+ reason: "No budget owner identified"
1644
+ });
1645
+ if (obs.daysToClose !== void 0 && obs.daysToClose < 14 && obs.dealHealthScore.grade !== "A") {
1646
+ plan.push({
1647
+ step: step++,
1648
+ action: "Update deal close date or probability",
1649
+ priority: "urgent",
1650
+ reason: `Close date in ${obs.daysToClose} days, deal at grade ${obs.dealHealthScore.grade}`
1651
+ });
1652
+ actions.push({
1653
+ type: "update_deal",
1654
+ payload: {
1655
+ slug: config.slug,
1656
+ dealName: config.dealName,
1657
+ notes: `Reviewed by deal agent — ${obs.daysToClose}d to close`
1658
+ },
1659
+ confidence: .75,
1660
+ reasoning: "Close date imminent"
1661
+ });
1662
+ }
1663
+ if (plan.length === 0) plan.push({
1664
+ step: 1,
1665
+ action: "Maintain current cadence",
1666
+ priority: "low",
1667
+ reason: "Deal healthy"
1668
+ });
1669
+ return {
1670
+ assessment: `Deal "${config.dealName}" in stage "${obs.deal.stage}" — health grade ${obs.dealHealthScore.grade} (${obs.dealHealthScore.score}/100). Risk: ${riskLevel}.`,
1671
+ riskLevel,
1672
+ plan,
1673
+ actions
1674
+ };
1675
+ }
1676
+ function selectActions(analysis, obs, config) {
1677
+ return analysis.actions.map((a) => {
1678
+ const dealValue = obs.deal.value ?? 0;
1679
+ const autoExecutable = config.autonomyLevel === "act" && a.confidence >= .7 && dealValue < config.valueThreshold;
1680
+ return {
1681
+ actionId: makeActionId(),
1682
+ type: a.type,
1683
+ payload: a.payload,
1684
+ confidence: a.confidence,
1685
+ reasoning: a.reasoning,
1686
+ requiresHumanApproval: !autoExecutable,
1687
+ status: "pending",
1688
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1689
+ };
1690
+ });
1691
+ }
1692
+ const VALID_STAGES = [
1693
+ "lead",
1694
+ "qualified",
1695
+ "proposal",
1696
+ "negotiation",
1697
+ "won",
1698
+ "lost"
1699
+ ];
1700
+ async function executeAction(action, dataDir) {
1701
+ const slug = action.payload["slug"];
1702
+ if (!slug) return "skipped";
1703
+ switch (action.type) {
1704
+ case "log_interaction": {
1705
+ const { appendInteraction } = await import("./interactions-writer-dSPy1XfO.js");
1706
+ await appendInteraction(dataDir, slug, {
1707
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1708
+ type: action.payload["type"] ?? "Note",
1709
+ with: String(action.payload["with"] ?? "agent"),
1710
+ summary: String(action.payload["summary"] ?? ""),
1711
+ nextSteps: [],
1712
+ sourceRef: `agent://deal-agent/${action.actionId}`,
1713
+ synced: (/* @__PURE__ */ new Date()).toISOString()
1714
+ });
1715
+ return "executed";
1716
+ }
1717
+ case "schedule_meeting": {
1718
+ const { appendInteraction } = await import("./interactions-writer-dSPy1XfO.js");
1719
+ await appendInteraction(dataDir, slug, {
1720
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1721
+ type: "Note",
1722
+ with: String(action.payload["with"] ?? ""),
1723
+ summary: `[Agent scheduled] ${String(action.payload["notes"] ?? "Meeting scheduled by deal agent")}`,
1724
+ nextSteps: [`Schedule meeting with ${String(action.payload["with"] ?? "contact")}`],
1725
+ sourceRef: `agent://deal-agent/${action.actionId}`,
1726
+ synced: (/* @__PURE__ */ new Date()).toISOString()
1727
+ });
1728
+ return "executed";
1729
+ }
1730
+ case "update_deal": {
1731
+ const { handleUpdateDeal } = await import("./update-deal-BNwPGaTV.js");
1732
+ const payload = action.payload;
1733
+ const validStage = VALID_STAGES.find((s) => s === payload.stage);
1734
+ await handleUpdateDeal({
1735
+ slug: payload.slug,
1736
+ dealName: payload.dealName,
1737
+ ...validStage !== void 0 ? { stage: validStage } : {},
1738
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1739
+ ...payload.probability !== void 0 ? { probability: payload.probability } : {},
1740
+ ...payload.closeDate !== void 0 ? { closeDate: payload.closeDate } : {},
1741
+ ...payload.notes !== void 0 ? { notes: payload.notes } : {}
1742
+ }, dataDir);
1743
+ return "executed";
1744
+ }
1745
+ case "alert": {
1746
+ const queue = readAgentQueue(dataDir, slug);
1747
+ const alertAction = {
1748
+ ...action,
1749
+ status: "pending"
1750
+ };
1751
+ if (!queue.pendingActions.find((a) => a.actionId === action.actionId)) {
1752
+ queue.pendingActions.push(alertAction);
1753
+ writeAgentQueue(dataDir, slug, queue);
1754
+ }
1755
+ return "executed";
1756
+ }
1757
+ }
1758
+ }
1759
+ async function runDealAgent(config, dataDir, llmFn = callLlm) {
1760
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1761
+ const obs = await observeDeal(dataDir, config.slug, config.dealName, config.today);
1762
+ if (!obs) throw new Error(`Deal "${config.dealName}" not found for customer "${config.slug}"`);
1763
+ let analysis;
1764
+ try {
1765
+ analysis = parseLlmResponse(await llmFn(buildLlmPrompt(obs, config))) ?? buildRuleBasedAnalysis(obs, config);
1766
+ } catch {
1767
+ analysis = buildRuleBasedAnalysis(obs, config);
1768
+ }
1769
+ const allActions = selectActions(analysis, obs, config);
1770
+ const actionsQueued = [];
1771
+ const actionsExecuted = [];
1772
+ if (config.autonomyLevel === "observe") {} else if (config.autonomyLevel === "suggest") {
1773
+ if (allActions.length > 0) {
1774
+ const queue = readAgentQueue(dataDir, config.slug);
1775
+ for (const action of allActions) {
1776
+ queue.pendingActions.push({
1777
+ ...action,
1778
+ requiresHumanApproval: true
1779
+ });
1780
+ actionsQueued.push(action);
1781
+ }
1782
+ writeAgentQueue(dataDir, config.slug, queue);
1783
+ }
1784
+ } else {
1785
+ const queue = readAgentQueue(dataDir, config.slug);
1786
+ let queueDirty = false;
1787
+ for (const action of allActions) if (!action.requiresHumanApproval) {
1788
+ const outcome = await executeAction(action, dataDir).catch(() => "skipped");
1789
+ actionsExecuted.push({
1790
+ ...action,
1791
+ status: outcome === "executed" ? "executed" : "skipped"
1792
+ });
1793
+ } else {
1794
+ queue.pendingActions.push(action);
1795
+ actionsQueued.push(action);
1796
+ queueDirty = true;
1797
+ }
1798
+ if (queueDirty) writeAgentQueue(dataDir, config.slug, queue);
1799
+ }
1800
+ const trace = {
1801
+ timestamp,
1802
+ slug: config.slug,
1803
+ dealName: config.dealName,
1804
+ autonomyLevel: config.autonomyLevel,
1805
+ observation: obs.contextSummary,
1806
+ plan: analysis.plan.map((s) => `${s.step}. ${s.action} [${s.priority}]`),
1807
+ actionsConsidered: allActions,
1808
+ actionTaken: actionsExecuted[0] ?? null,
1809
+ outcome: actionsExecuted.length > 0 ? "executed" : actionsQueued.length > 0 ? "queued" : "observed"
1810
+ };
1811
+ return {
1812
+ slug: config.slug,
1813
+ dealName: config.dealName,
1814
+ assessment: analysis.assessment,
1815
+ riskLevel: analysis.riskLevel,
1816
+ plan: analysis.plan,
1817
+ actionsQueued,
1818
+ actionsExecuted,
1819
+ trace
1820
+ };
1821
+ }
1822
+ //#endregion
1823
+ //#region src/mcp/tools/run-deal-agent.ts
1824
+ const DATA_DIR$37 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1825
+ async function handleRunDealAgent(input, dataDir = DATA_DIR$37) {
1826
+ try {
1827
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1828
+ const result = await runDealAgent({
1829
+ slug: input.slug,
1830
+ dealName: input.dealName,
1831
+ autonomyLevel: input.autonomyLevel ?? "suggest",
1832
+ valueThreshold: input.valueThreshold ?? 5e4,
1833
+ today,
1834
+ ...input.instruction !== void 0 ? { instruction: input.instruction } : {}
1835
+ }, dataDir);
1836
+ return { content: [{
1837
+ type: "text",
1838
+ text: JSON.stringify(result, null, 2)
1839
+ }] };
1840
+ } catch (err) {
1841
+ return { content: [{
1842
+ type: "text",
1843
+ text: JSON.stringify({
1844
+ success: false,
1845
+ error: err.message
1846
+ }, null, 2)
1847
+ }] };
1848
+ }
1849
+ }
1850
+ function registerRunDealAgent(server) {
1851
+ server.registerTool("run_deal_agent", {
1852
+ title: "Run Deal Agent",
1853
+ description: `Analyzes a specific deal and generates a prioritized action plan.
1854
+
1855
+ Three autonomy levels:
1856
+ - observe: analyze and return plan, no side effects
1857
+ - suggest (default): queue actions for human review in agent-queue.json
1858
+ - act: auto-execute actions with confidence ≥ 0.7 and value < valueThreshold
1859
+
1860
+ Each action includes confidence score and reasoning (glass-box).
1861
+ Returns full trace for inspection.
1862
+
1863
+ Args:
1864
+ slug: Customer slug
1865
+ dealName: Exact deal name
1866
+ autonomyLevel: "observe" | "suggest" | "act" (default: "suggest")
1867
+ instruction: Optional context/question for the agent
1868
+ valueThreshold: EUR value above which no auto-execution (default: 50000)
1869
+
1870
+ Returns: { assessment, riskLevel, plan[], actionsQueued[], actionsExecuted[], trace }`,
1871
+ inputSchema: z.object({
1872
+ slug: z.string().describe("Customer slug"),
1873
+ dealName: z.string().describe("Exact deal name"),
1874
+ autonomyLevel: z.enum([
1875
+ "observe",
1876
+ "suggest",
1877
+ "act"
1878
+ ]).optional().describe("Autonomy level (default: suggest)"),
1879
+ instruction: z.string().optional().describe("Optional instruction for the agent"),
1880
+ valueThreshold: z.number().optional().describe("EUR value above which no auto-execution (default: 50000)")
1881
+ })
1882
+ }, async ({ slug, dealName, autonomyLevel, instruction, valueThreshold }) => handleRunDealAgent({
1883
+ slug,
1884
+ dealName,
1885
+ ...autonomyLevel !== void 0 ? { autonomyLevel } : {},
1886
+ ...instruction !== void 0 ? { instruction } : {},
1887
+ ...valueThreshold !== void 0 ? { valueThreshold } : {}
1888
+ }));
1889
+ }
1890
+ //#endregion
1891
+ //#region src/mcp/tools/approve-agent-action.ts
1892
+ const DATA_DIR$36 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1893
+ async function handleApproveAgentAction(input, dataDir = DATA_DIR$36) {
1894
+ try {
1895
+ const queue = readAgentQueue(dataDir, input.slug);
1896
+ const idx = queue.pendingActions.findIndex((a) => a.actionId === input.actionId);
1897
+ if (idx === -1) return { content: [{
1898
+ type: "text",
1899
+ text: JSON.stringify({
1900
+ success: false,
1901
+ error: `Action ${input.actionId} not found in queue`
1902
+ }, null, 2)
1903
+ }] };
1904
+ const action = queue.pendingActions[idx];
1905
+ if (!input.approved) {
1906
+ queue.pendingActions[idx] = {
1907
+ ...action,
1908
+ status: "rejected"
1909
+ };
1910
+ writeAgentQueue(dataDir, input.slug, queue);
1911
+ return { content: [{
1912
+ type: "text",
1913
+ text: JSON.stringify({
1914
+ success: true,
1915
+ actionId: input.actionId,
1916
+ status: "rejected"
1917
+ }, null, 2)
1918
+ }] };
1919
+ }
1920
+ const outcome = await executeAction(action, dataDir);
1921
+ queue.pendingActions[idx] = {
1922
+ ...action,
1923
+ status: outcome === "executed" ? "executed" : "skipped"
1924
+ };
1925
+ writeAgentQueue(dataDir, input.slug, queue);
1926
+ return { content: [{
1927
+ type: "text",
1928
+ text: JSON.stringify({
1929
+ success: true,
1930
+ actionId: input.actionId,
1931
+ status: queue.pendingActions[idx].status
1932
+ }, null, 2)
1933
+ }] };
1934
+ } catch (err) {
1935
+ return { content: [{
1936
+ type: "text",
1937
+ text: JSON.stringify({
1938
+ success: false,
1939
+ error: err.message
1940
+ }, null, 2)
1941
+ }] };
1942
+ }
1943
+ }
1944
+ function registerApproveAgentAction(server) {
1945
+ server.registerTool("approve_agent_action", {
1946
+ title: "Approve Agent Action",
1947
+ description: `Approve or reject a pending action from the deal agent queue.
1948
+
1949
+ Find actionId in the actionsQueued array returned by run_deal_agent.
1950
+
1951
+ Args:
1952
+ slug: Customer slug
1953
+ actionId: Action ID from the agent queue
1954
+ approved: true to execute, false to reject
1955
+
1956
+ Returns: { success, actionId, status }`,
1957
+ inputSchema: z.object({
1958
+ slug: z.string(),
1959
+ actionId: z.string(),
1960
+ approved: z.boolean()
1961
+ })
1962
+ }, async ({ slug, actionId, approved }) => handleApproveAgentAction({
1963
+ slug,
1964
+ actionId,
1965
+ approved
1966
+ }));
1967
+ }
1968
+ //#endregion
1969
+ //#region src/mcp/tools/simulate-revenue.ts
1970
+ const DATA_DIR$35 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1971
+ async function handleSimulateRevenue(input, dataDir = DATA_DIR$35) {
1972
+ try {
1973
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1974
+ const horizon = input.horizon ?? "quarter";
1975
+ const simInput = await buildSimulationInput(dataDir, horizon, today);
1976
+ if (input.iterations !== void 0) simInput.iterations = input.iterations;
1977
+ const result = runSimulation(simInput);
1978
+ const confidence = buildConfidenceMessage(result, simInput.deals.length);
1979
+ return { content: [{
1980
+ type: "text",
1981
+ text: JSON.stringify({
1982
+ forecast: result,
1983
+ confidence,
1984
+ dealCount: simInput.deals.length,
1985
+ horizon,
1986
+ simulatedAt: (/* @__PURE__ */ new Date()).toISOString()
1987
+ }, null, 2)
1988
+ }] };
1989
+ } catch (err) {
1990
+ return { content: [{
1991
+ type: "text",
1992
+ text: JSON.stringify({
1993
+ success: false,
1994
+ error: err.message
1995
+ }, null, 2)
1996
+ }] };
1997
+ }
1998
+ }
1999
+ function registerSimulateRevenue(server) {
2000
+ server.registerTool("simulate_revenue", {
2001
+ title: "Simulate Revenue",
2002
+ description: `Monte Carlo pipeline revenue simulation with P10/P50/P90 confidence intervals.
2003
+
2004
+ Adjusts deal win probabilities using relationship health scores (D12) and
2005
+ champion presence (D11). Returns the range of possible quarterly/annual outcomes.
2006
+
2007
+ Use this instead of (or alongside) get_pipeline_forecast when you need:
2008
+ - Uncertainty quantification (not just expected value)
2009
+ - At-risk revenue identification
2010
+ - Deal sensitivity analysis ("which deal matters most")
2011
+ - Month-by-month close distribution
2012
+
2013
+ Args:
2014
+ horizon: "quarter" (default) | "year"
2015
+ iterations: simulation iterations (default: 10000)
2016
+
2017
+ Returns: { forecast: { p10, p50, p90, expected, stdDev, atRiskRevenue, byCloseMonth, topRisks, sensitivityMap }, confidence, dealCount, horizon }`,
2018
+ inputSchema: z.object({
2019
+ horizon: z.enum(["quarter", "year"]).optional().describe("Forecast horizon (default: \"quarter\")"),
2020
+ iterations: z.number().optional().describe("Monte Carlo iterations (default: 10000)")
2021
+ })
2022
+ }, async ({ horizon, iterations }) => handleSimulateRevenue({
2023
+ ...horizon !== void 0 ? { horizon } : {},
2024
+ ...iterations !== void 0 ? { iterations } : {}
2025
+ }));
2026
+ }
2027
+ //#endregion
2028
+ //#region src/mcp/tools/get-playbook.ts
2029
+ const DATA_DIR$34 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2030
+ async function handleGetPlaybook(input, dataDir = DATA_DIR$34) {
2031
+ try {
2032
+ const playbooks = listPlaybooks(dataDir, input.slug);
2033
+ if (!(input.stage !== void 0 || input.value !== void 0 || input.healthScore !== void 0)) return { content: [{
2034
+ type: "text",
2035
+ text: JSON.stringify({
2036
+ matches: playbooks.map((pb) => ({
2037
+ name: pb.name,
2038
+ trigger: pb.frontmatter.trigger,
2039
+ successRate: pb.frontmatter.successRate,
2040
+ usedCount: pb.frontmatter.usedCount,
2041
+ lastUpdated: pb.frontmatter.lastUpdated,
2042
+ content: pb.content
2043
+ })),
2044
+ totalPlaybooks: playbooks.length,
2045
+ slug: input.slug
2046
+ }, null, 2)
2047
+ }] };
2048
+ const matches = matchPlaybooks(playbooks, {
2049
+ slug: input.slug,
2050
+ name: "",
2051
+ stage: input.stage ?? "lead",
2052
+ value: input.value ?? 0,
2053
+ probability: 50,
2054
+ healthScore: input.healthScore ?? 60,
2055
+ daysSinceContact: input.daysSinceContact ?? 0,
2056
+ championPresent: input.championPresent ?? false
2057
+ }, input.daysSinceContact ?? 0);
2058
+ return { content: [{
2059
+ type: "text",
2060
+ text: JSON.stringify({
2061
+ matches: matches.map((m) => ({
2062
+ name: m.playbook.name,
2063
+ score: m.score,
2064
+ matchedConditions: m.matchedConditions,
2065
+ trigger: m.playbook.frontmatter.trigger,
2066
+ successRate: m.playbook.frontmatter.successRate,
2067
+ usedCount: m.playbook.frontmatter.usedCount,
2068
+ content: m.playbook.content
2069
+ })),
2070
+ totalPlaybooks: playbooks.length,
2071
+ slug: input.slug
2072
+ }, null, 2)
2073
+ }] };
2074
+ } catch (err) {
2075
+ return { content: [{
2076
+ type: "text",
2077
+ text: JSON.stringify({
2078
+ success: false,
2079
+ error: err.message
2080
+ }, null, 2)
2081
+ }] };
2082
+ }
2083
+ }
2084
+ function registerGetPlaybook(server) {
2085
+ server.registerTool("get_playbook", {
2086
+ title: "Get Playbook",
2087
+ description: `Retrieve playbooks for a customer. With deal context, returns only matching playbooks sorted by success rate. Without deal context, returns all playbooks.
2088
+
2089
+ Use after run_deal_agent or before a sales call to get proven guidance for the current situation.
2090
+
2091
+ Args:
2092
+ slug: Customer ID (required)
2093
+ stage: Deal stage (optional — enables trigger matching)
2094
+ value: Deal value in euros (optional)
2095
+ healthScore: Relationship health score 0–100 (optional)
2096
+ daysSinceContact: Days since last contact / days_stalled proxy (optional)
2097
+ championPresent: Whether a champion is identified (optional)
2098
+
2099
+ Returns: { matches: [{ name, score, trigger, successRate, usedCount, content }], totalPlaybooks, slug }`,
2100
+ inputSchema: z.object({
2101
+ slug: z.string().describe("Customer ID"),
2102
+ stage: z.string().optional().describe("Deal stage"),
2103
+ value: z.number().optional().describe("Deal value in euros"),
2104
+ healthScore: z.number().optional().describe("Health score 0–100"),
2105
+ daysSinceContact: z.number().optional().describe("Days since last contact"),
2106
+ championPresent: z.boolean().optional().describe("Champion identified")
2107
+ })
2108
+ }, async ({ slug, stage, value, healthScore, daysSinceContact, championPresent }) => handleGetPlaybook({
2109
+ slug,
2110
+ ...stage !== void 0 ? { stage } : {},
2111
+ ...value !== void 0 ? { value } : {},
2112
+ ...healthScore !== void 0 ? { healthScore } : {},
2113
+ ...daysSinceContact !== void 0 ? { daysSinceContact } : {},
2114
+ ...championPresent !== void 0 ? { championPresent } : {}
2115
+ }, DATA_DIR$34));
2116
+ }
2117
+ //#endregion
2118
+ //#region src/mcp/tools/create-playbook.ts
2119
+ const DATA_DIR$33 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2120
+ async function handleCreatePlaybook(input, dataDir = DATA_DIR$33) {
2121
+ try {
2122
+ const name = toKebabCase(input.name);
2123
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2124
+ const filePath = path.join(playbooksDir(dataDir, input.slug), `${name}.md`);
2125
+ const playbook = {
2126
+ slug: input.slug,
2127
+ name,
2128
+ frontmatter: {
2129
+ trigger: input.trigger,
2130
+ successRate: input.successRate ?? .5,
2131
+ usedCount: 0,
2132
+ lastUpdated: today
2133
+ },
2134
+ content: input.content,
2135
+ path: filePath
2136
+ };
2137
+ await writePlaybook(dataDir, input.slug, playbook);
2138
+ return { content: [{
2139
+ type: "text",
2140
+ text: JSON.stringify({
2141
+ success: true,
2142
+ playbook: {
2143
+ name,
2144
+ trigger: input.trigger,
2145
+ successRate: playbook.frontmatter.successRate,
2146
+ usedCount: 0,
2147
+ lastUpdated: today,
2148
+ path: filePath
2149
+ }
2150
+ }, null, 2)
2151
+ }] };
2152
+ } catch (err) {
2153
+ return { content: [{
2154
+ type: "text",
2155
+ text: JSON.stringify({
2156
+ success: false,
2157
+ error: err.message
2158
+ }, null, 2)
2159
+ }] };
2160
+ }
2161
+ }
2162
+ function registerCreatePlaybook(server) {
2163
+ server.registerTool("create_playbook", {
2164
+ title: "Create Playbook",
2165
+ description: `Create or update a playbook for a customer. Playbooks encode proven tactics for specific deal situations.
2166
+
2167
+ Trigger DSL uses AND-only conditions:
2168
+ deal_stage_<stage> | value > N | value < N | days_stalled > N | health < N | health > N | no_champion | has_champion
2169
+
2170
+ Example: "deal_stage_negotiation AND value > 50000 AND days_stalled > 7"
2171
+
2172
+ Args:
2173
+ slug: Customer ID
2174
+ name: Playbook name (auto-converted to kebab-case)
2175
+ trigger: Trigger DSL string (conditions separated by AND)
2176
+ content: Playbook markdown body (## Situation, ## Steps, ## Warnings, ## Templates)
2177
+ successRate: Historical win rate 0.0–1.0 (default: 0.5)
2178
+
2179
+ Returns: { success: true, playbook: { name, trigger, successRate, path } }`,
2180
+ inputSchema: z.object({
2181
+ slug: z.string().describe("Customer ID"),
2182
+ name: z.string().describe("Playbook name"),
2183
+ trigger: z.string().describe("Trigger DSL string"),
2184
+ content: z.string().describe("Playbook markdown body"),
2185
+ successRate: z.number().min(0).max(1).optional().describe("Historical win rate 0.0–1.0")
2186
+ })
2187
+ }, async ({ slug, name, trigger, content, successRate }) => handleCreatePlaybook({
2188
+ slug,
2189
+ name,
2190
+ trigger,
2191
+ content,
2192
+ ...successRate !== void 0 ? { successRate } : {}
2193
+ }, DATA_DIR$33));
2194
+ }
2195
+ //#endregion
2196
+ //#region src/mcp/tools/list-playbooks.ts
2197
+ const DATA_DIR$32 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2198
+ async function handleListPlaybooks(input, dataDir = DATA_DIR$32) {
2199
+ try {
2200
+ const playbooks = listPlaybooks(dataDir, input.slug);
2201
+ return { content: [{
2202
+ type: "text",
2203
+ text: JSON.stringify({
2204
+ playbooks: playbooks.map((pb) => ({
2205
+ name: pb.name,
2206
+ trigger: pb.frontmatter.trigger,
2207
+ successRate: pb.frontmatter.successRate,
2208
+ usedCount: pb.frontmatter.usedCount,
2209
+ lastUpdated: pb.frontmatter.lastUpdated
2210
+ })),
2211
+ count: playbooks.length,
2212
+ slug: input.slug
2213
+ }, null, 2)
2214
+ }] };
2215
+ } catch (err) {
2216
+ return { content: [{
2217
+ type: "text",
2218
+ text: JSON.stringify({
2219
+ success: false,
2220
+ error: err.message
2221
+ }, null, 2)
2222
+ }] };
2223
+ }
2224
+ }
2225
+ function registerListPlaybooks(server) {
2226
+ server.registerTool("list_playbooks", {
2227
+ title: "List Playbooks",
2228
+ description: `List all playbooks for a customer (metadata only, no body content).
2229
+
2230
+ Use to discover available playbooks before calling get_playbook with deal context.
2231
+
2232
+ Args:
2233
+ slug: Customer ID
2234
+
2235
+ Returns: { playbooks: [{ name, trigger, successRate, usedCount, lastUpdated }], count, slug }`,
2236
+ inputSchema: z.object({ slug: z.string().describe("Customer ID") })
2237
+ }, async ({ slug }) => handleListPlaybooks({ slug }, DATA_DIR$32));
2238
+ }
2239
+ //#endregion
2240
+ //#region src/mcp/tools/distill-playbook.ts
2241
+ const DATA_DIR$31 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2242
+ async function handleDistillPlaybook(input, dataDir = DATA_DIR$31, llmFn = callLlm) {
2243
+ try {
2244
+ const result = await distillPlaybook(dataDir, input.slug, input.dealName, input.outcome, llmFn);
2245
+ if (!result.ok) {
2246
+ const error = result.errorKind === "no_interactions" ? `No interactions.md found for ${input.slug}` : "LLM response could not be parsed as playbook";
2247
+ return { content: [{
2248
+ type: "text",
2249
+ text: JSON.stringify({
2250
+ success: false,
2251
+ error
2252
+ }, null, 2)
2253
+ }] };
2254
+ }
2255
+ return { content: [{
2256
+ type: "text",
2257
+ text: JSON.stringify({
2258
+ success: true,
2259
+ playbook: {
2260
+ name: result.playbook.name,
2261
+ trigger: result.playbook.frontmatter.trigger,
2262
+ successRate: result.playbook.frontmatter.successRate,
2263
+ usedCount: result.playbook.frontmatter.usedCount,
2264
+ path: result.playbook.path
2265
+ },
2266
+ reasoning: result.reasoning
2267
+ }, null, 2)
2268
+ }] };
2269
+ } catch (err) {
2270
+ return { content: [{
2271
+ type: "text",
2272
+ text: JSON.stringify({
2273
+ success: false,
2274
+ error: err.message
2275
+ }, null, 2)
2276
+ }] };
2277
+ }
2278
+ }
2279
+ function registerDistillPlaybook(server) {
2280
+ server.registerTool("distill_playbook", {
2281
+ title: "Distill Playbook",
2282
+ description: `Use LLM to extract a reusable playbook from a won or lost deal's interaction history. Analyzes the customer's interactions.md and identifies the winning/losing pattern as a structured playbook.
2283
+
2284
+ Run after every won or lost deal to build your procedural memory library.
2285
+
2286
+ Args:
2287
+ slug: Customer ID
2288
+ dealName: Name of the deal to analyze
2289
+ outcome: "won" or "lost"
2290
+
2291
+ Returns: { success: true, playbook: { name, trigger, successRate, path }, reasoning }`,
2292
+ inputSchema: z.object({
2293
+ slug: z.string().describe("Customer ID"),
2294
+ dealName: z.string().describe("Deal name to analyze"),
2295
+ outcome: z.enum(["won", "lost"]).describe("Deal outcome")
2296
+ })
2297
+ }, async ({ slug, dealName, outcome }) => handleDistillPlaybook({
2298
+ slug,
2299
+ dealName,
2300
+ outcome
2301
+ }, DATA_DIR$31));
2302
+ }
2303
+ //#endregion
2304
+ //#region src/mcp/tools/pursue-goal.ts
2305
+ const DATA_DIR$30 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2306
+ async function handlePursueGoal(input, dataDir = DATA_DIR$30, options = {}) {
2307
+ try {
2308
+ enforceRbac(dataDir, "pursue_goal");
2309
+ const goal = await pursueGoal(dataDir, {
2310
+ description: input.goal,
2311
+ deadline: input.deadline,
2312
+ ...input.context ? { context: input.context } : {}
2313
+ }, {
2314
+ actor: getActor(),
2315
+ ...options.buildInputFn ? { buildInputFn: options.buildInputFn } : {},
2316
+ ...options.llmFn ? { llmFn: options.llmFn } : {}
2317
+ });
2318
+ return { content: [{
2319
+ type: "text",
2320
+ text: JSON.stringify({
2321
+ goalId: goal.id,
2322
+ description: goal.description,
2323
+ target: goal.target,
2324
+ deadline: goal.deadline,
2325
+ type: goal.type,
2326
+ decomposition: {
2327
+ analysis: goal.decomposition.analysis,
2328
+ currentPipeline: goal.decomposition.currentPipeline,
2329
+ gap: goal.decomposition.gap,
2330
+ subGoals: goal.decomposition.subGoals,
2331
+ probabilisticOutcome: goal.decomposition.probabilisticOutcome
2332
+ }
2333
+ }, null, 2)
2334
+ }] };
2335
+ } catch (err) {
2336
+ return { content: [{
2337
+ type: "text",
2338
+ text: JSON.stringify({
2339
+ success: false,
2340
+ error: err.message
2341
+ }, null, 2)
2342
+ }] };
2343
+ }
2344
+ }
2345
+ function registerPursueGoal(server) {
2346
+ server.registerTool("pursue_goal", {
2347
+ title: "Pursue Goal",
2348
+ description: `Set a revenue or pipeline goal and get an AI-decomposed action plan.
2349
+
2350
+ Analyzes current pipeline (P50 forecast) and decomposes the gap into prioritized sub-goals per deal. Persists the goal in .agentic/goals.json for tracking.
2351
+
2352
+ RBAC: manager+
2353
+
2354
+ Args:
2355
+ goal: Natural language goal description (e.g. "Close €500k ARR this quarter")
2356
+ deadline: Target deadline (YYYY-MM-DD)
2357
+ context: Optional constraints (e.g. "Focus on existing pipeline only")
2358
+
2359
+ Returns: { goalId, description, target, deadline, decomposition: { analysis, currentPipeline, gap, subGoals, probabilisticOutcome } }`,
2360
+ inputSchema: z.object({
2361
+ goal: z.string().describe("Natural language goal (e.g. 'Close €500k ARR this quarter')"),
2362
+ deadline: z.string().describe("Target deadline YYYY-MM-DD"),
2363
+ context: z.string().optional().describe("Optional constraints or focus areas")
2364
+ })
2365
+ }, async ({ goal, deadline, context }) => handlePursueGoal({
2366
+ goal,
2367
+ deadline,
2368
+ ...context !== void 0 ? { context } : {}
2369
+ }, DATA_DIR$30));
2370
+ }
2371
+ //#endregion
2372
+ //#region src/mcp/tools/get-goal-status.ts
2373
+ const DATA_DIR$29 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2374
+ async function handleGetGoalStatus(input, dataDir = DATA_DIR$29) {
2375
+ try {
2376
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2377
+ const allGoals = input.goalId ? readGoals(dataDir).filter((g) => g.id === input.goalId) : getActiveGoals(dataDir);
2378
+ if (input.goalId && allGoals.length === 0) return { content: [{
2379
+ type: "text",
2380
+ text: JSON.stringify({
2381
+ success: false,
2382
+ error: `Goal '${input.goalId}' not found`
2383
+ }, null, 2)
2384
+ }] };
2385
+ const goals = allGoals.map((g) => {
2386
+ const deadlineMs = new Date(g.deadline).getTime();
2387
+ const todayMs = new Date(today).getTime();
2388
+ const daysRemaining = Math.max(0, Math.ceil((deadlineMs - todayMs) / 864e5));
2389
+ return {
2390
+ id: g.id,
2391
+ description: g.description,
2392
+ target: g.target,
2393
+ progress: g.progress,
2394
+ status: g.status,
2395
+ deadline: g.deadline,
2396
+ daysRemaining,
2397
+ subGoals: g.decomposition.subGoals.slice(0, 3),
2398
+ createdAt: g.createdAt
2399
+ };
2400
+ });
2401
+ const active = allGoals.filter((g) => g.status === "active");
2402
+ const completed = allGoals.filter((g) => g.status === "completed");
2403
+ return { content: [{
2404
+ type: "text",
2405
+ text: JSON.stringify({
2406
+ goals,
2407
+ activeCount: active.length,
2408
+ completedCount: completed.length
2409
+ }, null, 2)
2410
+ }] };
2411
+ } catch (err) {
2412
+ return { content: [{
2413
+ type: "text",
2414
+ text: JSON.stringify({
2415
+ success: false,
2416
+ error: err.message
2417
+ }, null, 2)
2418
+ }] };
2419
+ }
2420
+ }
2421
+ function registerGetGoalStatus(server) {
2422
+ server.registerTool("get_goal_status", {
2423
+ title: "Get Goal Status",
2424
+ description: `Get the status of active goals. Without goalId, returns all active goals. With goalId, returns that specific goal.
2425
+
2426
+ Returns progress, days remaining, and top sub-goals for each goal.
2427
+
2428
+ Args:
2429
+ goalId: (optional) Specific goal ID — if omitted, returns all active goals
2430
+
2431
+ Returns: { goals: [{ id, description, target, progress, status, deadline, daysRemaining, subGoals }], activeCount, completedCount }`,
2432
+ inputSchema: z.object({ goalId: z.string().optional().describe("Specific goal ID (omit for all active goals)") })
2433
+ }, async ({ goalId }) => handleGetGoalStatus({ ...goalId !== void 0 ? { goalId } : {} }, DATA_DIR$29));
2434
+ }
2435
+ //#endregion
2436
+ //#region src/mcp/tools/register-push-subscription.ts
2437
+ const DATA_DIR$28 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2438
+ const VALID_PROVIDERS = [
2439
+ "gmail",
2440
+ "microsoft-graph",
2441
+ "slack"
2442
+ ];
2443
+ async function handleRegisterPushSubscription(input, dataDir = DATA_DIR$28) {
2444
+ try {
2445
+ if (!VALID_PROVIDERS.includes(input.provider)) return { content: [{
2446
+ type: "text",
2447
+ text: JSON.stringify({
2448
+ success: false,
2449
+ error: `Unknown provider: ${input.provider}`
2450
+ }, null, 2)
2451
+ }] };
2452
+ enforceRbac(dataDir, "register_push_subscription");
2453
+ const providerData = {};
2454
+ if (input.gmailTopicName) providerData["gmailTopicName"] = input.gmailTopicName;
2455
+ if (input.microsoftClientState) providerData["microsoftClientState"] = input.microsoftClientState;
2456
+ if (input.microsoftResource) providerData["microsoftResource"] = input.microsoftResource;
2457
+ if (input.slackTeamId) providerData["slackTeamId"] = input.slackTeamId;
2458
+ if (input.slackChannelId) providerData["slackChannelId"] = input.slackChannelId;
2459
+ const sub = await register(dataDir, input.provider, input.slug, {
2460
+ webhookUrl: input.webhookUrl,
2461
+ providerData
2462
+ });
2463
+ const warning = input.webhookUrl.includes("localhost") ? "Warning: webhookUrl contains 'localhost' — providers cannot reach local endpoints. Use a tunnel (e.g. ngrok http 3847) for development." : void 0;
2464
+ return { content: [{
2465
+ type: "text",
2466
+ text: JSON.stringify({
2467
+ subscriptionId: sub.id,
2468
+ provider: sub.provider,
2469
+ slug: sub.slug,
2470
+ status: sub.status,
2471
+ expiresAt: sub.expiresAt,
2472
+ createdAt: sub.createdAt,
2473
+ ...warning ? { warning } : {}
2474
+ }, null, 2)
2475
+ }] };
2476
+ } catch (err) {
2477
+ return { content: [{
2478
+ type: "text",
2479
+ text: JSON.stringify({
2480
+ success: false,
2481
+ error: err.message
2482
+ }, null, 2)
2483
+ }] };
2484
+ }
2485
+ }
2486
+ function registerRegisterPushSubscription(server) {
2487
+ server.registerTool("register_push_subscription", {
2488
+ title: "Register Push Subscription",
2489
+ description: `Register a real-time push subscription for a customer (Gmail Pub/Sub, MS Graph webhook, or Slack Events).
2490
+
2491
+ Instead of polling every 30 minutes, the provider will push new events to dxcrm within seconds.
2492
+
2493
+ RBAC: admin only
2494
+
2495
+ Args:
2496
+ provider: "gmail" | "microsoft-graph" | "slack"
2497
+ slug: Customer slug to receive events for
2498
+ webhookUrl: Public HTTPS URL where the provider will POST events (e.g. https://yourserver.com/webhooks/gmail)
2499
+ gmailTopicName: (Gmail only) Cloud Pub/Sub topic name (e.g. projects/my-project/topics/gmail-push)
2500
+ microsoftClientState: (MS Graph only) Secret for HMAC verification
2501
+ microsoftResource: (MS Graph only) Resource path (e.g. /me/mailFolders/Inbox/messages)
2502
+ slackTeamId: (Slack only) Workspace/team ID (e.g. T12345)
2503
+ slackChannelId: (Slack only) Optional specific channel to monitor
2504
+
2505
+ Returns: { subscriptionId, provider, slug, status, expiresAt, createdAt, warning? }`,
2506
+ inputSchema: z.object({
2507
+ provider: z.enum([
2508
+ "gmail",
2509
+ "microsoft-graph",
2510
+ "slack"
2511
+ ]).describe("Push provider"),
2512
+ slug: z.string().describe("Customer slug"),
2513
+ webhookUrl: z.string().describe("Public HTTPS URL for provider callbacks"),
2514
+ gmailTopicName: z.string().optional().describe("Gmail: Cloud Pub/Sub topic name"),
2515
+ microsoftClientState: z.string().optional().describe("MS Graph: secret for verification"),
2516
+ microsoftResource: z.string().optional().describe("MS Graph: resource path"),
2517
+ slackTeamId: z.string().optional().describe("Slack: workspace team ID"),
2518
+ slackChannelId: z.string().optional().describe("Slack: optional channel ID")
2519
+ })
2520
+ }, async ({ provider, slug, webhookUrl, gmailTopicName, microsoftClientState, microsoftResource, slackTeamId, slackChannelId }) => handleRegisterPushSubscription({
2521
+ provider,
2522
+ slug,
2523
+ webhookUrl,
2524
+ ...gmailTopicName !== void 0 ? { gmailTopicName } : {},
2525
+ ...microsoftClientState !== void 0 ? { microsoftClientState } : {},
2526
+ ...microsoftResource !== void 0 ? { microsoftResource } : {},
2527
+ ...slackTeamId !== void 0 ? { slackTeamId } : {},
2528
+ ...slackChannelId !== void 0 ? { slackChannelId } : {}
2529
+ }, DATA_DIR$28));
2530
+ }
2531
+ //#endregion
2532
+ //#region src/mcp/tools/get-push-status.ts
2533
+ const DATA_DIR$27 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2534
+ async function handleGetPushStatus(input, dataDir = DATA_DIR$27) {
2535
+ try {
2536
+ let subs = await readSubscriptions(dataDir);
2537
+ if (input.slug) subs = subs.filter((s) => s.slug === input.slug);
2538
+ if (input.provider) subs = subs.filter((s) => s.provider === input.provider);
2539
+ const now = Date.now();
2540
+ const RENEWAL_THRESHOLD_MS = 1440 * 60 * 1e3;
2541
+ const subscriptions = subs.map((s) => {
2542
+ const expiresInHours = s.expiresAt ? Math.round((new Date(s.expiresAt).getTime() - now) / (3600 * 1e3)) : null;
2543
+ const needsRenewal = s.expiresAt !== null && new Date(s.expiresAt).getTime() - now < RENEWAL_THRESHOLD_MS;
2544
+ return {
2545
+ id: s.id,
2546
+ provider: s.provider,
2547
+ slug: s.slug,
2548
+ status: s.status,
2549
+ expiresAt: s.expiresAt,
2550
+ expiresInHours,
2551
+ needsRenewal,
2552
+ lastEventAt: s.lastEventAt,
2553
+ eventsProcessed: s.eventsProcessed,
2554
+ webhookUrl: s.webhookUrl
2555
+ };
2556
+ });
2557
+ const countByStatus = (status) => subs.filter((s) => s.status === status).length;
2558
+ const summary = {
2559
+ total: subs.length,
2560
+ active: countByStatus("active"),
2561
+ expiringSoon: subscriptions.filter((s) => s.needsRenewal && s.status === "active").length,
2562
+ expired: countByStatus("expired")
2563
+ };
2564
+ return { content: [{
2565
+ type: "text",
2566
+ text: JSON.stringify({
2567
+ subscriptions,
2568
+ summary
2569
+ }, null, 2)
2570
+ }] };
2571
+ } catch (err) {
2572
+ return { content: [{
2573
+ type: "text",
2574
+ text: JSON.stringify({
2575
+ success: false,
2576
+ error: err.message
2577
+ }, null, 2)
2578
+ }] };
2579
+ }
2580
+ }
2581
+ function registerGetPushStatus(server) {
2582
+ server.registerTool("get_push_status", {
2583
+ title: "Get Push Status",
2584
+ description: `Get the status of all active push subscriptions (Gmail Pub/Sub, MS Graph, Slack Events).
2585
+
2586
+ Shows which customers have real-time push enabled, when subscriptions expire, and how many events have been processed.
2587
+
2588
+ RBAC: any
2589
+
2590
+ Args:
2591
+ slug: (optional) Filter by customer slug
2592
+ provider: (optional) Filter by provider — "gmail" | "microsoft-graph" | "slack"
2593
+
2594
+ Returns: { subscriptions: [{ id, provider, slug, status, expiresAt, expiresInHours, needsRenewal, lastEventAt, eventsProcessed }], summary: { total, active, expiringSoon, expired } }`,
2595
+ inputSchema: z.object({
2596
+ slug: z.string().optional().describe("Filter by customer slug"),
2597
+ provider: z.enum([
2598
+ "gmail",
2599
+ "microsoft-graph",
2600
+ "slack"
2601
+ ]).optional().describe("Filter by provider")
2602
+ })
2603
+ }, async ({ slug, provider }) => handleGetPushStatus({
2604
+ ...slug !== void 0 ? { slug } : {},
2605
+ ...provider !== void 0 ? { provider } : {}
2606
+ }, DATA_DIR$27));
2607
+ }
2608
+ //#endregion
2609
+ //#region src/core/org-intelligence.ts
2610
+ function buildStakeholderMap(dataDir, slug, today, dealName) {
2611
+ const graph = readGraph(dataDir, slug);
2612
+ const stakeholders = getStakeholders(graph);
2613
+ const health = readHealth(dataDir, slug);
2614
+ const champIds = new Set(stakeholders.champions.map((n) => n.id));
2615
+ const buyerIds = new Set(stakeholders.economicBuyers.map((n) => n.id));
2616
+ const blockerIds = new Set(stakeholders.blockers.map((n) => n.id));
2617
+ const healthByContactId = new Map((health?.contacts ?? []).map((c) => [c.contactId, c]));
2618
+ const people = graph.nodes.filter((n) => n.type === "person").map((node) => {
2619
+ const contactHealth = healthByContactId.get(node.id);
2620
+ let role = "unknown";
2621
+ if (champIds.has(node.id)) role = "champion";
2622
+ else if (buyerIds.has(node.id)) role = "economic_buyer";
2623
+ else if (blockerIds.has(node.id)) role = "blocker";
2624
+ const edges = graph.edges.filter((e) => e.from === node.id);
2625
+ const contactStrength = edges.length > 0 ? Math.round(Math.max(...edges.map((e) => e.weight)) * 100) / 100 : .5;
2626
+ const profile = {
2627
+ name: node.label,
2628
+ role,
2629
+ healthScore: contactHealth?.score ?? 50,
2630
+ daysSinceContact: contactHealth?.daysSinceContact ?? 999,
2631
+ contactStrength,
2632
+ riskFlags: contactHealth?.riskFlags ?? []
2633
+ };
2634
+ if (node.properties.email) profile.email = node.properties.email;
2635
+ return profile;
2636
+ });
2637
+ const riskAssessment = buildRiskAssessment(people, stakeholders.missingRoles, []);
2638
+ const recommendation = deriveRecommendation(people, stakeholders.missingRoles);
2639
+ return {
2640
+ slug,
2641
+ ...dealName ? { dealName } : {},
2642
+ updatedAt: (/* @__PURE__ */ new Date(`${today}T00:00:00Z`)).toISOString(),
2643
+ people,
2644
+ missingRoles: stakeholders.missingRoles,
2645
+ riskAssessment,
2646
+ recommendation
2647
+ };
2648
+ }
2649
+ function buildRiskAssessment(people, missingRoles, signals = []) {
2650
+ const risks = [];
2651
+ if (missingRoles.some((r) => r.role === "champion")) risks.push("No champion identified — deal lacks an internal advocate.");
2652
+ if (missingRoles.some((r) => r.role === "economic_buyer")) risks.push("Economic buyer unknown — decision authority not confirmed.");
2653
+ const coldPeople = people.filter((p) => p.riskFlags.includes("NO_CONTACT_30D"));
2654
+ if (coldPeople.length > 0) risks.push(`Cold contacts (30d+ silence): ${coldPeople.map((p) => p.name).join(", ")}.`);
2655
+ const lowHealth = people.filter((p) => p.healthScore < 40);
2656
+ if (lowHealth.length > 0) risks.push(`Low health score (<40): ${lowHealth.map((p) => p.name).join(", ")}.`);
2657
+ const negativeSignals = signals.filter((s) => s.impact === "negative");
2658
+ for (const sig of negativeSignals.slice(0, 2)) risks.push(`External signal: ${sig.summary}`);
2659
+ return risks.length > 0 ? risks.join(" ") : "No critical risks detected.";
2660
+ }
2661
+ function deriveRecommendation(people, missingRoles) {
2662
+ const critical = missingRoles.find((r) => r.urgency === "critical");
2663
+ if (critical) return critical.suggestion;
2664
+ const coldPeople = people.filter((p) => p.riskFlags.includes("NO_CONTACT_30D"));
2665
+ if (coldPeople.length > 0) return `Re-engage ${coldPeople.map((p) => p.name).join(", ")} — no contact in 30+ days.`;
2666
+ const important = missingRoles.find((r) => r.urgency === "important");
2667
+ if (important) return important.suggestion;
2668
+ return `Relationship health avg ${people.length > 0 ? Math.round(people.reduce((s, p) => s + p.healthScore, 0) / people.length) : 0}/100. Maintain regular contact cadence.`;
2669
+ }
2670
+ //#endregion
2671
+ //#region src/mcp/tools/get-org-intelligence.ts
2672
+ const DATA_DIR$26 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2673
+ async function handleGetOrgIntelligence(input, dataDir = DATA_DIR$26) {
2674
+ try {
2675
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2676
+ const map = buildStakeholderMap(dataDir, input.slug, today, input.dealName);
2677
+ return { content: [{
2678
+ type: "text",
2679
+ text: JSON.stringify(map, null, 2)
2680
+ }] };
2681
+ } catch (err) {
2682
+ return { content: [{
2683
+ type: "text",
2684
+ text: JSON.stringify({
2685
+ success: false,
2686
+ error: err.message
2687
+ }, null, 2)
2688
+ }] };
2689
+ }
2690
+ }
2691
+ function registerGetOrgIntelligence(server) {
2692
+ server.registerTool("get_org_intelligence", {
2693
+ title: "Get Org Intelligence",
2694
+ description: `Build a stakeholder map for a customer: champions, economic buyers, blockers, health scores, risk flags, and a prioritised recommendation.
2695
+
2696
+ Returns: { slug, updatedAt, people: [{ name, email, role, healthScore, daysSinceContact, contactStrength, riskFlags }], missingRoles, riskAssessment, recommendation }`,
2697
+ inputSchema: z.object({
2698
+ slug: z.string().describe("Customer slug"),
2699
+ dealName: z.string().optional().describe("Optional deal name to scope the analysis")
2700
+ })
2701
+ }, async ({ slug, dealName }) => handleGetOrgIntelligence(dealName ? {
2702
+ slug,
2703
+ dealName
2704
+ } : { slug }));
2705
+ }
2706
+ //#endregion
2707
+ //#region src/agents/deal-room.ts
2708
+ async function buildDealRoom(dataDir, slug, dealName, today) {
2709
+ const [pipelineDeals, simInput] = await Promise.all([readPipeline(dataDir, slug).catch(() => []), buildSimulationInput(dataDir, "quarter", today).catch(() => ({
2710
+ deals: [],
2711
+ externalSignals: [],
2712
+ iterations: 1e3,
2713
+ horizon: "quarter",
2714
+ today
2715
+ }))]);
2716
+ const stakeholders = buildStakeholderMap(dataDir, slug, today, dealName);
2717
+ const health = readHealth(dataDir, slug);
2718
+ const simResult = runSimulation({
2719
+ ...simInput,
2720
+ iterations: 1e3
2721
+ });
2722
+ const todayDate = new Date(today);
2723
+ const dealHealth = pipelineDeals.filter((d) => d.stage !== "won" && d.stage !== "lost").map((deal) => {
2724
+ const updatedDate = deal.updated ? new Date(deal.updated) : todayDate;
2725
+ const daysSinceLastActivity = Math.floor((todayDate.getTime() - updatedDate.getTime()) / 864e5);
2726
+ const daysToClose = deal.close_date ? Math.floor((new Date(deal.close_date).getTime() - todayDate.getTime()) / 864e5) : void 0;
2727
+ const scored = scoreDeal(deal, {
2728
+ daysSinceLastActivity,
2729
+ daysInCurrentStage: daysSinceLastActivity,
2730
+ ...daysToClose !== void 0 ? { daysToClose } : {},
2731
+ ...deal.probability !== void 0 ? { probability: deal.probability } : {}
2732
+ });
2733
+ return {
2734
+ deal: deal.name,
2735
+ stage: deal.stage,
2736
+ score: scored.score,
2737
+ grade: scored.grade,
2738
+ warnings: scored.warnings
2739
+ };
2740
+ });
2741
+ const firstActiveDeal = pipelineDeals.find((d) => d.stage !== "won" && d.stage !== "lost");
2742
+ let recommendedPlaybook = null;
2743
+ if (firstActiveDeal) {
2744
+ const championEmail = stakeholders.people.find((p) => p.role === "champion")?.email;
2745
+ const daysSinceContact = ((championEmail ? health?.contacts.find((c) => c.email === championEmail || c.contactId === `person:${championEmail}`) : void 0) ?? health?.contacts?.[0])?.daysSinceContact ?? 999;
2746
+ const dealSnapshot = {
2747
+ slug,
2748
+ name: firstActiveDeal.name,
2749
+ stage: firstActiveDeal.stage,
2750
+ value: firstActiveDeal.value ?? 0,
2751
+ probability: firstActiveDeal.probability ?? 50,
2752
+ healthScore: health?.overallHealth ?? 50,
2753
+ daysSinceContact,
2754
+ championPresent: stakeholders.people.some((p) => p.role === "champion")
2755
+ };
2756
+ recommendedPlaybook = matchPlaybooks(listPlaybooks(dataDir, slug), dealSnapshot, daysSinceContact)[0] ?? null;
2757
+ }
2758
+ const riskScore = computeRiskScore(stakeholders.missingRoles, dealHealth, health?.overallHealth ?? 100);
2759
+ const topPriorities = buildTopPriorities(stakeholders, dealHealth);
2760
+ const executiveSummary = buildExecutiveSummary(slug, dealName, stakeholders, health?.overallHealth ?? 100, simResult, riskScore);
2761
+ return {
2762
+ slug,
2763
+ dealName,
2764
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2765
+ stakeholders,
2766
+ relationshipHealth: health?.contacts ?? [],
2767
+ dealHealth,
2768
+ revenueSimulation: {
2769
+ p50: simResult.p50,
2770
+ p10: simResult.p10,
2771
+ p90: simResult.p90,
2772
+ expected: simResult.expected,
2773
+ atRiskRevenue: simResult.atRiskRevenue
2774
+ },
2775
+ recommendedPlaybook,
2776
+ executiveSummary,
2777
+ topPriorities,
2778
+ riskScore
2779
+ };
2780
+ }
2781
+ function computeRiskScore(missingRoles, dealHealth, overallHealth) {
2782
+ let risk = 0;
2783
+ for (const mr of missingRoles) risk += mr.urgency === "critical" ? 25 : 10;
2784
+ const avgDealScore = dealHealth.length > 0 ? dealHealth.reduce((s, d) => s + d.score, 0) / dealHealth.length : 100;
2785
+ risk += Math.round((100 - avgDealScore) * .3);
2786
+ risk += Math.round((100 - overallHealth) * .2);
2787
+ return Math.min(100, Math.max(0, risk));
2788
+ }
2789
+ function buildTopPriorities(stakeholders, dealHealth) {
2790
+ const priorities = [];
2791
+ for (const mr of stakeholders.missingRoles) if (mr.urgency === "critical") priorities.push(mr.suggestion);
2792
+ const coldPeople = stakeholders.people.filter((p) => p.riskFlags.includes("NO_CONTACT_30D"));
2793
+ if (coldPeople.length > 0) priorities.push(`Re-engage ${coldPeople.map((p) => p.name).join(", ")} — silent 30+ days.`);
2794
+ const atRiskDeals = dealHealth.filter((d) => d.score < 50);
2795
+ const showCount = Math.min(3, atRiskDeals.length);
2796
+ for (const d of atRiskDeals.slice(0, showCount)) priorities.push(`Rescue deal "${d.deal}" (health ${d.score}/100): ${d.warnings[0] ?? "at risk"}`);
2797
+ if (atRiskDeals.length > showCount) priorities.push(`+${atRiskDeals.length - showCount} more at-risk deal(s) need attention.`);
2798
+ for (const mr of stakeholders.missingRoles) if (mr.urgency === "important") priorities.push(mr.suggestion);
2799
+ if (priorities.length === 0) priorities.push("Maintain current momentum — schedule next check-in.");
2800
+ return priorities;
2801
+ }
2802
+ function buildExecutiveSummary(slug, dealName, stakeholders, overallHealth, sim, riskScore) {
2803
+ const champCount = stakeholders.people.filter((p) => p.role === "champion").length;
2804
+ const missingCritical = stakeholders.missingRoles.filter((r) => r.urgency === "critical").length;
2805
+ const parts = [];
2806
+ parts.push(`${slug}/${dealName}: relationship health ${overallHealth}/100, ${champCount} champion(s) identified.`);
2807
+ if (sim.p50 > 0) parts.push(`Pipeline P50 forecast: €${(sim.p50 / 1e3).toFixed(1)}k.`);
2808
+ if (missingCritical > 0) parts.push(`Critical gaps: ${missingCritical} key role(s) unidentified — risk score ${riskScore}/100.`);
2809
+ else parts.push(`Overall risk score: ${riskScore}/100.`);
2810
+ return parts.join(" ");
2811
+ }
2812
+ //#endregion
2813
+ //#region src/mcp/tools/open-deal-room.ts
2814
+ const DATA_DIR$25 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2815
+ async function handleOpenDealRoom(input, dataDir = DATA_DIR$25) {
2816
+ try {
2817
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2818
+ const brief = await buildDealRoom(dataDir, input.slug, input.dealName, today);
2819
+ return { content: [{
2820
+ type: "text",
2821
+ text: JSON.stringify(brief, null, 2)
2822
+ }] };
2823
+ } catch (err) {
2824
+ return { content: [{
2825
+ type: "text",
2826
+ text: JSON.stringify({
2827
+ success: false,
2828
+ error: err.message
2829
+ }, null, 2)
2830
+ }] };
2831
+ }
2832
+ }
2833
+ function registerOpenDealRoom(server) {
2834
+ server.registerTool("open_deal_room", {
2835
+ title: "Open Deal Room",
2836
+ description: `Multi-agent deal brief: orchestrates relationship graph, health scores, deal health, revenue simulation, and playbook matching into a unified deal-room brief with executive summary, top priorities, and risk score (0–100).
2837
+
2838
+ Returns: { slug, dealName, generatedAt, stakeholders, relationshipHealth, dealHealth, revenueSimulation, recommendedPlaybook, executiveSummary, topPriorities, riskScore }`,
2839
+ inputSchema: z.object({
2840
+ slug: z.string().describe("Customer slug"),
2841
+ dealName: z.string().describe("Name of the deal to analyse")
2842
+ })
2843
+ }, async ({ slug, dealName }) => handleOpenDealRoom({
2844
+ slug,
2845
+ dealName
2846
+ }));
2847
+ }
2848
+ //#endregion
2849
+ //#region src/mcp/tools/get-proactive-briefing.ts
2850
+ const DATA_DIR$24 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2851
+ async function handleGetProactiveBriefing(input, dataDir = DATA_DIR$24) {
2852
+ try {
2853
+ const briefing = await buildDailyBriefing(dataDir, input.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
2854
+ return { content: [{
2855
+ type: "text",
2856
+ text: JSON.stringify(briefing, null, 2)
2857
+ }] };
2858
+ } catch (err) {
2859
+ return { content: [{
2860
+ type: "text",
2861
+ text: JSON.stringify({
2862
+ success: false,
2863
+ error: err.message
2864
+ }, null, 2)
2865
+ }] };
2866
+ }
2867
+ }
2868
+ function registerGetProactiveBriefing(server) {
2869
+ server.registerTool("get_proactive_briefing", {
2870
+ title: "Get Proactive Briefing",
2871
+ description: `Generate a proactive daily briefing: urgent alerts (relationship decay, deal risk, overdue close dates), pipeline forecast (P50/P90), and a single top-action recommendation.
2872
+
2873
+ Returns: { date, generatedAt, urgent: string[], opportunities: string[], forecast: string, topAction: string }`,
2874
+ inputSchema: z.object({ date: z.string().optional().describe("ISO date (YYYY-MM-DD). Defaults to today.") })
2875
+ }, async ({ date }) => handleGetProactiveBriefing(date ? { date } : {}));
2876
+ }
2877
+ //#endregion
2878
+ //#region src/mcp/tools/list-email-templates.ts
2879
+ const DATA_DIR$23 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2880
+ async function handleListEmailTemplates(input, dataDir = DATA_DIR$23) {
2881
+ const summary = listTemplates(dataDir, input.category ? { category: input.category } : {}).map(({ body: _body, ...meta }) => meta);
2882
+ return { content: [{
2883
+ type: "text",
2884
+ text: JSON.stringify(summary, null, 2)
2885
+ }] };
2886
+ }
2887
+ function registerListEmailTemplates(server, dataDir = DATA_DIR$23) {
2888
+ server.registerTool("list_email_templates", {
2889
+ description: "List available email templates. Optionally filter by category (e.g. 'outreach', 'followup', 'support').",
2890
+ inputSchema: z.object({ category: z.string().optional().describe("Filter by category") })
2891
+ }, ({ category }) => handleListEmailTemplates(category ? { category } : {}, dataDir));
2892
+ }
2893
+ //#endregion
2894
+ //#region src/mcp/tools/get-email-template.ts
2895
+ const DATA_DIR$22 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2896
+ async function handleGetEmailTemplate(input, dataDir = DATA_DIR$22) {
2897
+ const tmpl = getTemplate(dataDir, input.id);
2898
+ if (!tmpl) return { content: [{
2899
+ type: "text",
2900
+ text: JSON.stringify({ error: `Template '${input.id}' not found` })
2901
+ }] };
2902
+ const allVars = extractVariables(`${tmpl.subject}\n${tmpl.body}`);
2903
+ const unique = [...new Set(allVars)];
2904
+ return { content: [{
2905
+ type: "text",
2906
+ text: JSON.stringify({
2907
+ ...tmpl,
2908
+ detectedVariables: unique
2909
+ }, null, 2)
2910
+ }] };
2911
+ }
2912
+ function registerGetEmailTemplate(server, dataDir = DATA_DIR$22) {
2913
+ server.registerTool("get_email_template", {
2914
+ description: "Get a specific email template by ID, including its body and detected variables.",
2915
+ inputSchema: z.object({ id: z.string().describe("Template ID (e.g. 'enterprise-intro')") })
2916
+ }, ({ id }) => handleGetEmailTemplate({ id }, dataDir));
2917
+ }
2918
+ //#endregion
2919
+ //#region src/mcp/tools/draft-email.ts
2920
+ const DATA_DIR$21 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2921
+ async function handleDraftEmail(input, dataDir = DATA_DIR$21) {
2922
+ const tmpl = getTemplate(dataDir, input.templateId);
2923
+ if (!tmpl) return { content: [{
2924
+ type: "text",
2925
+ text: JSON.stringify({ error: `Template '${input.templateId}' not found` })
2926
+ }] };
2927
+ const vars = {
2928
+ ...await buildVariablesFromCustomer(dataDir, input.slug),
2929
+ ...input.overrides ?? {}
2930
+ };
2931
+ const subject = interpolate(tmpl.subject, vars);
2932
+ const interpolatedBody = interpolate(tmpl.body, vars);
2933
+ let effectiveTone = input.tone;
2934
+ if (!effectiveTone) {
2935
+ const { resolveTone, toneInstruction } = await import("./tone-Bdm5uaht.js");
2936
+ const instr = toneInstruction(resolveTone(dataDir, input.slug));
2937
+ if (instr) effectiveTone = instr;
2938
+ }
2939
+ let body = interpolatedBody;
2940
+ let polished = false;
2941
+ if (effectiveTone) try {
2942
+ const { callLlm } = await import("./llm-DEjWcqmW.js");
2943
+ const refined = await callLlm(`Rewrite the following email in a ${effectiveTone} tone. Keep the same language, preserve all names and facts, and do not invent details. Return ONLY the rewritten email body, no preamble.\n\n---\n${interpolatedBody}`);
2944
+ if (refined && refined.trim()) {
2945
+ const { labelAiContent } = await import("./compliance-CujOqAKk.js");
2946
+ body = labelAiContent(refined.trim());
2947
+ polished = true;
2948
+ }
2949
+ } catch {}
2950
+ const to = (await readMainFacts(dataDir, input.slug).catch(() => null))?.email ?? "";
2951
+ return { content: [{
2952
+ type: "text",
2953
+ text: JSON.stringify({
2954
+ subject,
2955
+ body,
2956
+ to,
2957
+ slug: input.slug,
2958
+ templateId: input.templateId,
2959
+ tone: effectiveTone ?? null,
2960
+ polished,
2961
+ resolvedVariables: vars
2962
+ }, null, 2)
2963
+ }] };
2964
+ }
2965
+ function registerDraftEmail(server, dataDir = DATA_DIR$21) {
2966
+ server.registerTool("draft_email", {
2967
+ description: `Draft a personalized email for a customer using a stored template.
2968
+ Variables are auto-filled from the customer's main_facts.md. Override any variable manually.
2969
+ Optionally pass a tone (e.g. "formal", "friendly", "concise") to LLM-polish the body —
2970
+ falls back to plain template-fill when no ANTHROPIC_API_KEY is configured.
2971
+ Returns: { subject, body, to, tone, polished, resolvedVariables } — does NOT send automatically.`,
2972
+ inputSchema: z.object({
2973
+ slug: z.string().describe("Customer slug"),
2974
+ templateId: z.string().describe("Template ID to use"),
2975
+ overrides: z.record(z.string()).optional().describe("Override any template variable (e.g. {firstName: 'Alice'})"),
2976
+ tone: z.string().optional().describe("Optional tone to LLM-polish the body (e.g. 'formal', 'friendly', 'concise')")
2977
+ })
2978
+ }, ({ slug, templateId, overrides, tone }) => handleDraftEmail({
2979
+ slug,
2980
+ templateId,
2981
+ ...overrides !== void 0 ? { overrides } : {},
2982
+ ...tone !== void 0 ? { tone } : {}
2983
+ }, dataDir));
2984
+ }
2985
+ //#endregion
2986
+ //#region src/mcp/tools/enroll-in-sequence.ts
2987
+ const DATA_DIR$20 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2988
+ async function handleEnrollInSequence(input, dataDir = DATA_DIR$20) {
2989
+ const sequence = getSequence(dataDir, input.sequenceId);
2990
+ if (!sequence) return { content: [{
2991
+ type: "text",
2992
+ text: JSON.stringify({ error: `Sequence '${input.sequenceId}' not found` })
2993
+ }] };
2994
+ const firstStep = sequence.steps[0];
2995
+ if (!getTemplate(dataDir, firstStep.templateId)) return { content: [{
2996
+ type: "text",
2997
+ text: JSON.stringify({ error: `Template '${firstStep.templateId}' for step 0 not found` })
2998
+ }] };
2999
+ const enrollmentId = `enroll_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
3000
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3001
+ await writeEnrollment(dataDir, {
3002
+ id: enrollmentId,
3003
+ sequenceId: input.sequenceId,
3004
+ slug: input.slug,
3005
+ contactEmail: input.contactEmail,
3006
+ enrolledAt: now,
3007
+ status: "active",
3008
+ currentStep: 0,
3009
+ stepsCompleted: []
3010
+ });
3011
+ return { content: [{
3012
+ type: "text",
3013
+ text: JSON.stringify({
3014
+ enrollmentId,
3015
+ sequenceName: sequence.name,
3016
+ totalSteps: sequence.steps.length
3017
+ })
3018
+ }] };
3019
+ }
3020
+ function registerEnrollInSequence(server, dataDir = DATA_DIR$20) {
3021
+ server.registerTool("enroll_in_sequence", {
3022
+ description: `Enroll a contact in an email sequence. Validates that the sequence and its first template exist.
3023
+ Returns: { enrollmentId, sequenceName, totalSteps }`,
3024
+ inputSchema: z.object({
3025
+ slug: z.string().describe("Customer slug"),
3026
+ contactEmail: z.string().email().describe("Email address of the contact to enroll"),
3027
+ sequenceId: z.string().describe("ID of the sequence to enroll in")
3028
+ })
3029
+ }, ({ slug, contactEmail, sequenceId }) => handleEnrollInSequence({
3030
+ slug,
3031
+ contactEmail,
3032
+ sequenceId
3033
+ }, dataDir));
3034
+ }
3035
+ //#endregion
3036
+ //#region src/mcp/tools/list-sequence-enrollments.ts
3037
+ const DATA_DIR$19 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3038
+ async function handleListSequenceEnrollments(input, dataDir = DATA_DIR$19) {
3039
+ let enrollments = readEnrollments(dataDir);
3040
+ if (input.slug !== void 0) enrollments = enrollments.filter((e) => e.slug === input.slug);
3041
+ if (input.status !== void 0) enrollments = enrollments.filter((e) => e.status === input.status);
3042
+ return { content: [{
3043
+ type: "text",
3044
+ text: JSON.stringify({ enrollments }, null, 2)
3045
+ }] };
3046
+ }
3047
+ function registerListSequenceEnrollments(server, dataDir = DATA_DIR$19) {
3048
+ server.registerTool("list_sequence_enrollments", {
3049
+ description: `List email sequence enrollments. Filter by customer slug or status.
3050
+ Returns: { enrollments: SequenceEnrollment[] }`,
3051
+ inputSchema: z.object({
3052
+ slug: z.string().optional().describe("Filter by customer slug"),
3053
+ status: z.enum([
3054
+ "active",
3055
+ "paused",
3056
+ "completed"
3057
+ ]).optional().describe("Filter by enrollment status")
3058
+ })
3059
+ }, ({ slug, status }) => handleListSequenceEnrollments({
3060
+ ...slug !== void 0 ? { slug } : {},
3061
+ ...status !== void 0 ? { status } : {}
3062
+ }, dataDir));
3063
+ }
3064
+ //#endregion
3065
+ //#region src/mcp/tools/unenroll-from-sequence.ts
3066
+ const DATA_DIR$18 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3067
+ async function handleUnenrollFromSequence(input, dataDir = DATA_DIR$18) {
3068
+ if (!await updateEnrollment(dataDir, input.enrollmentId, { status: "paused" })) return { content: [{
3069
+ type: "text",
3070
+ text: JSON.stringify({
3071
+ success: false,
3072
+ error: `Enrollment '${input.enrollmentId}' not found`
3073
+ })
3074
+ }] };
3075
+ return { content: [{
3076
+ type: "text",
3077
+ text: JSON.stringify({ success: true })
3078
+ }] };
3079
+ }
3080
+ function registerUnenrollFromSequence(server, dataDir = DATA_DIR$18) {
3081
+ server.registerTool("unenroll_from_sequence", {
3082
+ description: `Unenroll (pause) a contact from an email sequence. Sets status to "paused" (soft delete).
3083
+ Returns: { success: boolean }`,
3084
+ inputSchema: z.object({ enrollmentId: z.string().describe("ID of the enrollment to pause") })
3085
+ }, ({ enrollmentId }) => handleUnenrollFromSequence({ enrollmentId }, dataDir));
3086
+ }
3087
+ //#endregion
3088
+ //#region src/mcp/tools/list-sequences.ts
3089
+ const DATA_DIR$17 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3090
+ async function handleListSequences(_input, dataDir = DATA_DIR$17) {
3091
+ const sequences = listSequences(dataDir);
3092
+ const enrollments = readEnrollments(dataDir);
3093
+ const result = sequences.map((seq) => ({
3094
+ id: seq.id,
3095
+ name: seq.name,
3096
+ stepCount: seq.steps.length,
3097
+ enrollmentCount: enrollments.filter((e) => e.sequenceId === seq.id).length
3098
+ }));
3099
+ return { content: [{
3100
+ type: "text",
3101
+ text: JSON.stringify({ sequences: result }, null, 2)
3102
+ }] };
3103
+ }
3104
+ function registerListSequences(server, dataDir = DATA_DIR$17) {
3105
+ server.registerTool("list_sequences", {
3106
+ description: `List all email sequences with step count and enrollment count.
3107
+ Returns: { sequences: Array<{ id, name, stepCount, enrollmentCount }> }`,
3108
+ inputSchema: z.object({})
3109
+ }, () => handleListSequences({}, dataDir));
3110
+ }
3111
+ //#endregion
3112
+ //#region src/mcp/tools/generate-quote.ts
3113
+ const DATA_DIR$16 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3114
+ async function handleGenerateQuote(input, dataDir = DATA_DIR$16) {
3115
+ try {
3116
+ const quote = await generateQuote(dataDir, input);
3117
+ return { content: [{
3118
+ type: "text",
3119
+ text: JSON.stringify({
3120
+ quoteNumber: quote.quoteNumber,
3121
+ htmlPath: quote.htmlPath,
3122
+ total: quote.total,
3123
+ subtotal: quote.subtotal,
3124
+ vat: quote.vat,
3125
+ vatPercent: quote.vatPercent,
3126
+ currency: quote.currency,
3127
+ validUntil: quote.validUntil,
3128
+ status: quote.status
3129
+ }, null, 2)
3130
+ }] };
3131
+ } catch (err) {
3132
+ return { content: [{
3133
+ type: "text",
3134
+ text: JSON.stringify({ error: err.message })
3135
+ }] };
3136
+ }
3137
+ }
3138
+ function registerGenerateQuote(server, dataDir = DATA_DIR$16) {
3139
+ server.registerTool("generate_quote", {
3140
+ description: `Generate a professional HTML quote/offer for a customer deal.
3141
+ Calculates subtotal, VAT, and total. Saves JSON + HTML to .agentic/quotes/.
3142
+ Returns: { quoteNumber, htmlPath, total, currency, validUntil }`,
3143
+ inputSchema: z.object({
3144
+ slug: z.string().describe("Customer slug"),
3145
+ dealName: z.string().describe("Name of the deal this quote is for"),
3146
+ lineItems: z.array(z.object({
3147
+ description: z.string(),
3148
+ quantity: z.number().positive(),
3149
+ unitPrice: z.number().min(0)
3150
+ })).min(1).describe("Line items for the quote"),
3151
+ vatPercent: z.number().min(0).max(100).optional().describe("VAT percentage (default 19)"),
3152
+ validUntilDays: z.number().int().positive().optional().describe("Quote validity in days (default 30)"),
3153
+ currency: z.string().optional().describe("Currency code (default EUR)")
3154
+ })
3155
+ }, ({ slug, dealName, lineItems, vatPercent, validUntilDays, currency }) => handleGenerateQuote({
3156
+ slug,
3157
+ dealName,
3158
+ lineItems,
3159
+ ...vatPercent !== void 0 ? { vatPercent } : {},
3160
+ ...validUntilDays !== void 0 ? { validUntilDays } : {},
3161
+ ...currency !== void 0 ? { currency } : {}
3162
+ }, dataDir));
3163
+ }
3164
+ //#endregion
3165
+ //#region src/mcp/tools/get-quote-status.ts
3166
+ const DATA_DIR$15 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3167
+ async function handleGetQuoteStatus(input, dataDir = DATA_DIR$15) {
3168
+ if (input.quoteNumber) {
3169
+ const quote = readQuote(dataDir, input.quoteNumber);
3170
+ if (!quote) return { content: [{
3171
+ type: "text",
3172
+ text: JSON.stringify({ error: `Quote '${input.quoteNumber}' not found` })
3173
+ }] };
3174
+ return { content: [{
3175
+ type: "text",
3176
+ text: JSON.stringify(quote, null, 2)
3177
+ }] };
3178
+ }
3179
+ const quotes = listQuotes(dataDir, input.slug);
3180
+ return { content: [{
3181
+ type: "text",
3182
+ text: JSON.stringify({ quotes }, null, 2)
3183
+ }] };
3184
+ }
3185
+ function registerGetQuoteStatus(server, dataDir = DATA_DIR$15) {
3186
+ server.registerTool("get_quote_status", {
3187
+ description: `Get quote status and details. Filter by quoteNumber (single quote) or slug (all quotes for a customer).
3188
+ Returns quote with status: draft | sent | viewed | accepted | declined`,
3189
+ inputSchema: z.object({
3190
+ quoteNumber: z.string().optional().describe("Specific quote number (e.g. Q-2026-001)"),
3191
+ slug: z.string().optional().describe("Customer slug to list all quotes for")
3192
+ })
3193
+ }, ({ quoteNumber, slug }) => handleGetQuoteStatus({
3194
+ ...quoteNumber !== void 0 ? { quoteNumber } : {},
3195
+ ...slug !== void 0 ? { slug } : {}
3196
+ }, dataDir));
3197
+ }
3198
+ //#endregion
3199
+ //#region src/mcp/tools/get-booking-link.ts
3200
+ const DATA_DIR$14 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3201
+ function loadCalendlyConfig(dataDir) {
3202
+ const p = path.join(dataDir, ".agentic", "integrations", "calendly.yaml");
3203
+ if (!fs.existsSync(p)) return {};
3204
+ try {
3205
+ return yaml.load(fs.readFileSync(p, "utf-8")) ?? {};
3206
+ } catch {
3207
+ return {};
3208
+ }
3209
+ }
3210
+ function readCustomerFacts(dataDir, slug) {
3211
+ const p = path.join(dataDir, "customers", slug, "main_facts.md");
3212
+ if (!fs.existsSync(p)) return {};
3213
+ const content = fs.readFileSync(p, "utf-8");
3214
+ const nameMatch = /^name:\s*(.+)$/m.exec(content);
3215
+ const emailMatch = /^email:\s*(.+)$/m.exec(content);
3216
+ const name = nameMatch?.[1]?.trim();
3217
+ const email = emailMatch?.[1]?.trim();
3218
+ return {
3219
+ ...name ? { name } : {},
3220
+ ...email ? { email } : {}
3221
+ };
3222
+ }
3223
+ async function handleGetBookingLink(input, dataDir = DATA_DIR$14) {
3224
+ const config = loadCalendlyConfig(dataDir);
3225
+ const apiKey = config.apiKey ?? process.env["CALENDLY_API_KEY"] ?? "";
3226
+ if (!apiKey) return { content: [{
3227
+ type: "text",
3228
+ text: JSON.stringify({ error: "Calendly API key not configured. Set CALENDLY_API_KEY env var or configure .agentic/integrations/calendly.yaml" })
3229
+ }] };
3230
+ const eventTypeSlug = input.eventType ?? config.defaultEventType ?? "30min";
3231
+ try {
3232
+ const { getSchedulingLink, listEventTypes } = await import("./calendly-Bft_wwji.js");
3233
+ const bookingUrl = await getSchedulingLink(apiKey, eventTypeSlug, input.prefillName ? readCustomerFacts(dataDir, input.slug) : void 0);
3234
+ const eventType = (await listEventTypes(apiKey)).find((et) => et.slug === eventTypeSlug || et.name.toLowerCase().includes(eventTypeSlug.toLowerCase()));
3235
+ return { content: [{
3236
+ type: "text",
3237
+ text: JSON.stringify({
3238
+ bookingUrl,
3239
+ eventType: eventType?.name ?? eventTypeSlug,
3240
+ duration: eventType?.duration ?? 30,
3241
+ slug: input.slug
3242
+ }, null, 2)
3243
+ }] };
3244
+ } catch (err) {
3245
+ return { content: [{
3246
+ type: "text",
3247
+ text: JSON.stringify({ error: err.message })
3248
+ }] };
3249
+ }
3250
+ }
3251
+ function registerGetBookingLink(server, dataDir = DATA_DIR$14) {
3252
+ server.registerTool("get_booking_link", {
3253
+ description: `Get a Calendly booking link for a customer. Optionally pre-fills the customer's name/email.
3254
+ Requires CALENDLY_API_KEY env var or .agentic/integrations/calendly.yaml config.
3255
+ Returns: { bookingUrl, eventType, duration }`,
3256
+ inputSchema: z.object({
3257
+ slug: z.string().describe("Customer slug"),
3258
+ eventType: z.string().optional().describe("Calendly event type slug (e.g. '30min', '60min'). Uses default if not specified."),
3259
+ prefillName: z.boolean().optional().describe("Pre-fill customer name and email in the booking link")
3260
+ })
3261
+ }, ({ slug, eventType, prefillName }) => handleGetBookingLink({
3262
+ slug,
3263
+ ...eventType !== void 0 ? { eventType } : {},
3264
+ ...prefillName !== void 0 ? { prefillName } : {}
3265
+ }, dataDir));
3266
+ }
3267
+ //#endregion
3268
+ //#region src/mcp/tools/create-ticket.ts
3269
+ const DATA_DIR$13 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3270
+ async function handleCreateTicket(input, dataDir = DATA_DIR$13) {
3271
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3272
+ const rules = loadSlaRules(dataDir);
3273
+ const priority = input.priority ?? "normal";
3274
+ const id = nextTicketId(await readTickets(dataDir, input.slug));
3275
+ const slaDue = calcSlaDue(today, priority, rules);
3276
+ const ticket = {
3277
+ id,
3278
+ title: input.title,
3279
+ status: "open",
3280
+ priority,
3281
+ ...input.assignee ? { assignee: input.assignee } : {},
3282
+ created: today,
3283
+ slaDue,
3284
+ ...input.description ? { description: input.description } : {}
3285
+ };
3286
+ await upsertTicket(dataDir, input.slug, ticket);
3287
+ return { content: [{
3288
+ type: "text",
3289
+ text: JSON.stringify({ ticket }, null, 2)
3290
+ }] };
3291
+ }
3292
+ function registerCreateTicket(server, dataDir = DATA_DIR$13) {
3293
+ server.registerTool("create_ticket", {
3294
+ description: `Create a support ticket for a customer. Auto-calculates SLA due date based on priority.
3295
+ Returns: { ticket } with id T-NNN, status=open, slaDue`,
3296
+ inputSchema: z.object({
3297
+ slug: z.string().describe("Customer slug"),
3298
+ title: z.string().min(1).describe("Ticket title"),
3299
+ description: z.string().optional().describe("Detailed description"),
3300
+ priority: z.enum([
3301
+ "urgent",
3302
+ "high",
3303
+ "normal",
3304
+ "low"
3305
+ ]).optional().describe("Priority (default: normal)"),
3306
+ assignee: z.string().optional().describe("Assignee name or email")
3307
+ })
3308
+ }, ({ slug, title, description, priority, assignee }) => handleCreateTicket({
3309
+ slug,
3310
+ title,
3311
+ ...description !== void 0 ? { description } : {},
3312
+ ...priority !== void 0 ? { priority } : {},
3313
+ ...assignee !== void 0 ? { assignee } : {}
3314
+ }, dataDir));
3315
+ }
3316
+ //#endregion
3317
+ //#region src/mcp/tools/update-ticket.ts
3318
+ const DATA_DIR$12 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3319
+ async function handleUpdateTicket(input, dataDir = DATA_DIR$12) {
3320
+ const ticket = (await readTickets(dataDir, input.slug)).find((t) => t.id === input.ticketId);
3321
+ if (!ticket) return { content: [{
3322
+ type: "text",
3323
+ text: JSON.stringify({ error: `Ticket '${input.ticketId}' not found for customer '${input.slug}'` })
3324
+ }] };
3325
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3326
+ const updated = {
3327
+ ...ticket,
3328
+ ...input.status ? { status: input.status } : {},
3329
+ ...input.assignee !== void 0 ? { assignee: input.assignee } : {},
3330
+ ...input.status === "resolved" && !ticket.resolved ? { resolved: today } : {}
3331
+ };
3332
+ await upsertTicket(dataDir, input.slug, updated);
3333
+ return { content: [{
3334
+ type: "text",
3335
+ text: JSON.stringify({ ticket: updated }, null, 2)
3336
+ }] };
3337
+ }
3338
+ function registerUpdateTicket(server, dataDir = DATA_DIR$12) {
3339
+ server.registerTool("update_ticket", {
3340
+ description: `Update a ticket's status or assignee. Setting status=resolved auto-sets resolved date.
3341
+ Returns: { ticket }`,
3342
+ inputSchema: z.object({
3343
+ slug: z.string().describe("Customer slug"),
3344
+ ticketId: z.string().describe("Ticket ID (e.g. T-001)"),
3345
+ status: z.enum([
3346
+ "open",
3347
+ "in-progress",
3348
+ "waiting",
3349
+ "resolved",
3350
+ "closed"
3351
+ ]).optional(),
3352
+ assignee: z.string().optional().describe("New assignee")
3353
+ })
3354
+ }, ({ slug, ticketId, status, assignee }) => handleUpdateTicket({
3355
+ slug,
3356
+ ticketId,
3357
+ ...status !== void 0 ? { status } : {},
3358
+ ...assignee !== void 0 ? { assignee } : {}
3359
+ }, dataDir));
3360
+ }
3361
+ //#endregion
3362
+ //#region src/mcp/tools/list-tickets.ts
3363
+ const DATA_DIR$11 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3364
+ async function handleListTickets(input, dataDir = DATA_DIR$11) {
3365
+ const results = await listAllTickets(dataDir, {
3366
+ ...input.slug !== void 0 ? { slug: input.slug } : {},
3367
+ ...input.status !== void 0 ? { status: input.status } : {},
3368
+ ...input.priority !== void 0 ? { priority: input.priority } : {},
3369
+ ...input.assignee !== void 0 ? { assignee: input.assignee } : {}
3370
+ });
3371
+ return { content: [{
3372
+ type: "text",
3373
+ text: JSON.stringify({ tickets: results }, null, 2)
3374
+ }] };
3375
+ }
3376
+ function registerListTickets(server, dataDir = DATA_DIR$11) {
3377
+ server.registerTool("list_tickets", {
3378
+ description: `List support tickets. Filter by customer, status, priority, or assignee. Sorted by priority then date.
3379
+ Returns: { tickets: Array<{ slug, ticket }> }`,
3380
+ inputSchema: z.object({
3381
+ slug: z.string().optional().describe("Filter by customer slug"),
3382
+ status: z.enum([
3383
+ "open",
3384
+ "in-progress",
3385
+ "waiting",
3386
+ "resolved",
3387
+ "closed"
3388
+ ]).optional(),
3389
+ priority: z.enum([
3390
+ "urgent",
3391
+ "high",
3392
+ "normal",
3393
+ "low"
3394
+ ]).optional(),
3395
+ assignee: z.string().optional().describe("Filter by assignee")
3396
+ })
3397
+ }, ({ slug, status, priority, assignee }) => handleListTickets({
3398
+ ...slug !== void 0 ? { slug } : {},
3399
+ ...status !== void 0 ? { status } : {},
3400
+ ...priority !== void 0 ? { priority } : {},
3401
+ ...assignee !== void 0 ? { assignee } : {}
3402
+ }, dataDir));
3403
+ }
3404
+ //#endregion
3405
+ //#region src/mcp/tools/close-ticket.ts
3406
+ const DATA_DIR$10 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3407
+ async function handleCloseTicket(input, dataDir = DATA_DIR$10) {
3408
+ const ticket = (await readTickets(dataDir, input.slug)).find((t) => t.id === input.ticketId);
3409
+ if (!ticket) return { content: [{
3410
+ type: "text",
3411
+ text: JSON.stringify({ error: `Ticket '${input.ticketId}' not found for '${input.slug}'` })
3412
+ }] };
3413
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3414
+ const updated = {
3415
+ ...ticket,
3416
+ status: "closed",
3417
+ resolved: ticket.resolved ?? today
3418
+ };
3419
+ await upsertTicket(dataDir, input.slug, updated);
3420
+ if (input.resolution) await appendInteraction(dataDir, input.slug, {
3421
+ date: today,
3422
+ type: "Note",
3423
+ with: input.slug,
3424
+ summary: `Ticket ${input.ticketId} closed: ${input.resolution}`,
3425
+ nextSteps: [],
3426
+ sourceRef: `ticket://${input.ticketId}/close`,
3427
+ synced: (/* @__PURE__ */ new Date()).toISOString()
3428
+ });
3429
+ return { content: [{
3430
+ type: "text",
3431
+ text: JSON.stringify({ ticket: updated }, null, 2)
3432
+ }] };
3433
+ }
3434
+ function registerCloseTicket(server, dataDir = DATA_DIR$10) {
3435
+ server.registerTool("close_ticket", {
3436
+ description: `Close a support ticket. Optionally logs the resolution as an interaction.
3437
+ Returns: { ticket } with status=closed`,
3438
+ inputSchema: z.object({
3439
+ slug: z.string().describe("Customer slug"),
3440
+ ticketId: z.string().describe("Ticket ID (e.g. T-001)"),
3441
+ resolution: z.string().optional().describe("Resolution notes (logged as interaction)")
3442
+ })
3443
+ }, ({ slug, ticketId, resolution }) => handleCloseTicket({
3444
+ slug,
3445
+ ticketId,
3446
+ ...resolution !== void 0 ? { resolution } : {}
3447
+ }, dataDir));
3448
+ }
3449
+ //#endregion
3450
+ //#region src/mcp/tools/send-nps-survey.ts
3451
+ const DATA_DIR$9 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3452
+ async function handleSendNpsSurvey(input, dataDir = DATA_DIR$9) {
3453
+ const survey = getSurvey(dataDir, input.surveyId);
3454
+ if (!survey) return { content: [{
3455
+ type: "text",
3456
+ text: JSON.stringify({ error: `Survey '${input.surveyId}' not found` })
3457
+ }] };
3458
+ const serverUrl = input.serverUrl ?? process.env["DXCRM_SERVER_URL"] ?? "http://localhost:3456";
3459
+ const token = generateSurveyToken(input.slug, input.contactEmail, input.surveyId);
3460
+ const email = buildSurveyEmail(survey, serverUrl, token);
3461
+ await savePendingSurvey(dataDir, input.surveyId, input.slug, input.contactEmail, token);
3462
+ return { content: [{
3463
+ type: "text",
3464
+ text: JSON.stringify({
3465
+ token,
3466
+ subject: email.subject,
3467
+ body: email.body,
3468
+ surveyUrl: `${serverUrl}/survey/respond?token=${token}`,
3469
+ note: "Email draft ready. Use draft_email or Gmail to send."
3470
+ }, null, 2)
3471
+ }] };
3472
+ }
3473
+ function registerSendNpsSurvey(server, dataDir = DATA_DIR$9) {
3474
+ server.registerTool("send_nps_survey", {
3475
+ description: `Generate an NPS/CSAT survey email for a customer contact. Returns subject, HTML body, and a token-based response URL.
3476
+ Does NOT send automatically — returns draft for review.
3477
+ Returns: { token, subject, body, surveyUrl }`,
3478
+ inputSchema: z.object({
3479
+ slug: z.string().describe("Customer slug"),
3480
+ contactEmail: z.string().email().describe("Contact email to send survey to"),
3481
+ surveyId: z.string().describe("Survey definition ID from .agentic/surveys/"),
3482
+ serverUrl: z.string().optional().describe("Server URL for response links (default: DXCRM_SERVER_URL env var or localhost:3456)")
3483
+ })
3484
+ }, ({ slug, contactEmail, surveyId, serverUrl }) => handleSendNpsSurvey({
3485
+ slug,
3486
+ contactEmail,
3487
+ surveyId,
3488
+ ...serverUrl !== void 0 ? { serverUrl } : {}
3489
+ }, dataDir));
3490
+ }
3491
+ //#endregion
3492
+ //#region src/mcp/tools/get-survey-results.ts
3493
+ const DATA_DIR$8 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3494
+ async function handleGetSurveyResults(input, dataDir = DATA_DIR$8) {
3495
+ const responses = loadSurveyResponses(dataDir, input.surveyId, input.slug);
3496
+ const nps = calcNpsScore(responses);
3497
+ const promoters = responses.filter((r) => r.score >= 9).length;
3498
+ const passives = responses.filter((r) => r.score >= 7 && r.score <= 8).length;
3499
+ const detractors = responses.filter((r) => r.score <= 6).length;
3500
+ return { content: [{
3501
+ type: "text",
3502
+ text: JSON.stringify({
3503
+ surveyId: input.surveyId,
3504
+ ...input.slug ? { slug: input.slug } : {},
3505
+ totalResponses: responses.length,
3506
+ npsScore: nps,
3507
+ promoters,
3508
+ passives,
3509
+ detractors,
3510
+ responses: responses.map((r) => ({
3511
+ slug: r.slug,
3512
+ email: r.contactEmail,
3513
+ score: r.score,
3514
+ ...r.comment ? { comment: r.comment } : {},
3515
+ respondedAt: r.respondedAt
3516
+ }))
3517
+ }, null, 2)
3518
+ }] };
3519
+ }
3520
+ function registerGetSurveyResults(server, dataDir = DATA_DIR$8) {
3521
+ server.registerTool("get_survey_results", {
3522
+ description: `Get NPS/CSAT survey results with score breakdown. Calculates Net Promoter Score.
3523
+ Returns: { npsScore, totalResponses, promoters, passives, detractors, responses[] }`,
3524
+ inputSchema: z.object({
3525
+ surveyId: z.string().describe("Survey ID"),
3526
+ slug: z.string().optional().describe("Filter results to a specific customer")
3527
+ })
3528
+ }, ({ surveyId, slug }) => handleGetSurveyResults({
3529
+ surveyId,
3530
+ ...slug !== void 0 ? { slug } : {}
3531
+ }, dataDir));
3532
+ }
3533
+ //#endregion
3534
+ //#region src/mcp/tools/search-knowledge-base.ts
3535
+ const DATA_DIR$7 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3536
+ async function handleSearchKnowledgeBase(input, dataDir = DATA_DIR$7) {
3537
+ const results = searchKbSimple(dataDir, input.query, { ...input.publicOnly ? { publicOnly: true } : {} });
3538
+ const limited = (input.category ? results.filter((a) => a.category === input.category) : results).slice(0, input.limit ?? 10);
3539
+ return { content: [{
3540
+ type: "text",
3541
+ text: JSON.stringify({
3542
+ query: input.query,
3543
+ count: limited.length,
3544
+ articles: limited.map((a) => ({
3545
+ ...getKbMetaForExport(a),
3546
+ excerpt: a.body.slice(0, 300).trim()
3547
+ }))
3548
+ }, null, 2)
3549
+ }] };
3550
+ }
3551
+ function registerSearchKnowledgeBase(server, dataDir = DATA_DIR$7) {
3552
+ server.registerTool("search_knowledge_base", {
3553
+ description: `Search the knowledge base for articles. Text search on title, body, and tags.
3554
+ Returns: { count, articles[] } with excerpts`,
3555
+ inputSchema: z.object({
3556
+ query: z.string().describe("Search query"),
3557
+ category: z.string().optional().describe("Filter by category (e.g. 'troubleshooting', 'howto')"),
3558
+ publicOnly: z.boolean().optional().describe("Only return public articles"),
3559
+ limit: z.number().int().positive().optional().describe("Max results (default 10)")
3560
+ })
3561
+ }, ({ query, category, publicOnly, limit }) => handleSearchKnowledgeBase({
3562
+ query,
3563
+ ...category !== void 0 ? { category } : {},
3564
+ ...publicOnly !== void 0 ? { publicOnly } : {},
3565
+ ...limit !== void 0 ? { limit } : {}
3566
+ }, dataDir));
3567
+ }
3568
+ //#endregion
3569
+ //#region src/mcp/tools/create-kb-article.ts
3570
+ const DATA_DIR$6 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3571
+ async function handleCreateKbArticle(input, dataDir = DATA_DIR$6) {
3572
+ if (getKbArticle(dataDir, input.id)) return { content: [{
3573
+ type: "text",
3574
+ text: JSON.stringify({ error: `Article '${input.id}' already exists` })
3575
+ }] };
3576
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3577
+ const article = {
3578
+ id: input.id,
3579
+ title: input.title,
3580
+ body: input.body,
3581
+ category: input.category ?? "general",
3582
+ tags: input.tags ?? [],
3583
+ public: input.public ?? false,
3584
+ createdAt: now,
3585
+ updatedAt: now,
3586
+ ...input.sourceTicketId ? { sourceTicketId: input.sourceTicketId } : {}
3587
+ };
3588
+ writeKbArticle(dataDir, article);
3589
+ return { content: [{
3590
+ type: "text",
3591
+ text: JSON.stringify({
3592
+ id: article.id,
3593
+ title: article.title,
3594
+ category: article.category,
3595
+ path: `.agentic/knowledge-base/${article.category}/${article.id}.md`
3596
+ }, null, 2)
3597
+ }] };
3598
+ }
3599
+ function registerCreateKbArticle(server, dataDir = DATA_DIR$6) {
3600
+ server.registerTool("create_kb_article", {
3601
+ description: `Create a new knowledge base article. Articles are stored as Markdown files in .agentic/knowledge-base/.
3602
+ Returns: { id, title, category, path }`,
3603
+ inputSchema: z.object({
3604
+ id: z.string().min(1).describe("Article ID (slug, e.g. 'troubleshoot-api-timeout')"),
3605
+ title: z.string().min(1).describe("Article title"),
3606
+ body: z.string().min(1).describe("Article body in Markdown"),
3607
+ category: z.string().optional().describe("Category (default: 'general')"),
3608
+ tags: z.array(z.string()).optional().describe("Tags for search"),
3609
+ public: z.boolean().optional().describe("Make article publicly accessible (default: false)"),
3610
+ sourceTicketId: z.string().optional().describe("Ticket ID this article was created from")
3611
+ })
3612
+ }, ({ id, title, body, category, tags, public: pub, sourceTicketId }) => handleCreateKbArticle({
3613
+ id,
3614
+ title,
3615
+ body,
3616
+ ...category !== void 0 ? { category } : {},
3617
+ ...tags !== void 0 ? { tags } : {},
3618
+ ...pub !== void 0 ? { public: pub } : {},
3619
+ ...sourceTicketId !== void 0 ? { sourceTicketId } : {}
3620
+ }, dataDir));
3621
+ }
3622
+ //#endregion
3623
+ //#region src/mcp/tools/backup-now.ts
3624
+ const DATA_DIR$5 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3625
+ async function handleBackupNow(input, dataDir = DATA_DIR$5) {
3626
+ const zipPath = path.join(dataDir, `dxcrm-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}.zip`);
3627
+ const manifest = await runBackup(zipPath, dataDir, { ...input.remote ? { remote: input.remote } : {} }).catch(() => null);
3628
+ if (!manifest) return { content: [{
3629
+ type: "text",
3630
+ text: "Backup failed. Check disk space and permissions."
3631
+ }] };
3632
+ const sizeMb = fs.existsSync(zipPath) ? (fs.statSync(zipPath).size / 1024 / 1024).toFixed(1) : "?";
3633
+ return { content: [{
3634
+ type: "text",
3635
+ text: JSON.stringify({
3636
+ path: zipPath,
3637
+ createdAt: manifest.createdAt,
3638
+ customerCount: manifest.customerCount,
3639
+ fileCount: manifest.fileCount,
3640
+ sizeMb: `${sizeMb} MB`,
3641
+ directories: manifest.directories,
3642
+ verified: true,
3643
+ ...input.remote ? { uploadedTo: input.remote } : {},
3644
+ ...input.note ? { note: input.note } : {}
3645
+ }, null, 2)
3646
+ }] };
3647
+ }
3648
+ function registerBackupNow(server) {
3649
+ server.registerTool("backup_now", {
3650
+ description: "Trigger an immediate backup of all CRM data (customers/ + .agentic/). Returns backup path, size, and integrity status. Use before risky operations or on user request.",
3651
+ inputSchema: z.object({
3652
+ remote: z.string().optional().describe("Upload destination: s3://bucket/path/, rsync://user@host:/path/, or local directory"),
3653
+ note: z.string().optional().describe("Optional note to tag this backup")
3654
+ })
3655
+ }, ({ remote, note }) => handleBackupNow({
3656
+ ...remote !== void 0 ? { remote } : {},
3657
+ ...note !== void 0 ? { note } : {}
3658
+ }));
3659
+ }
3660
+ //#endregion
3661
+ //#region src/mcp/tools/list-backups.ts
3662
+ const DATA_DIR$4 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3663
+ async function handleListBackups(input, dataDir = DATA_DIR$4) {
3664
+ const logEntries = readBackupLog(dataDir);
3665
+ const fileEntries = listBackupsInDir(dataDir);
3666
+ const entries = logEntries.length > 0 ? logEntries : fileEntries;
3667
+ const limited = entries.slice(0, input.limit);
3668
+ if (limited.length === 0) return { content: [{
3669
+ type: "text",
3670
+ text: "No backups found. Run backup_now to create one."
3671
+ }] };
3672
+ return { content: [{
3673
+ type: "text",
3674
+ text: JSON.stringify({
3675
+ count: limited.length,
3676
+ totalAvailable: entries.length,
3677
+ backups: limited.map((e) => ({
3678
+ filename: e.filename,
3679
+ createdAt: e.createdAt,
3680
+ sizeMb: e.sizeBytes > 0 ? `${(e.sizeBytes / 1024 / 1024).toFixed(1)} MB` : "unknown",
3681
+ verified: e.verified,
3682
+ encrypted: e.encrypted,
3683
+ customerCount: e.customerCount,
3684
+ fileCount: e.fileCount
3685
+ }))
3686
+ }, null, 2)
3687
+ }] };
3688
+ }
3689
+ function registerListBackups(server) {
3690
+ server.registerTool("list_backups", {
3691
+ description: "List available CRM backups with metadata (date, size, verification status, customer count). Shows log-tracked backups first, falls back to directory scan.",
3692
+ inputSchema: z.object({ limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of backups to return") })
3693
+ }, (input) => handleListBackups(input));
3694
+ }
3695
+ //#endregion
3696
+ //#region src/mcp/tools/trigger-sync.ts
3697
+ const DATA_DIR$3 = process.cwd();
3698
+ async function handleTriggerSync(input, dataDir = DATA_DIR$3) {
3699
+ const auth = getGmailAuth();
3700
+ if (!auth) return { content: [{
3701
+ type: "text",
3702
+ text: JSON.stringify({
3703
+ success: false,
3704
+ error: "Gmail auth not configured. Run `dxcrm sync gmail --init` first."
3705
+ })
3706
+ }] };
3707
+ const customersDir = path.join(dataDir, "customers");
3708
+ if (!fs.existsSync(customersDir)) return { content: [{
3709
+ type: "text",
3710
+ text: JSON.stringify({
3711
+ success: true,
3712
+ synced: 0,
3713
+ skipped: 0,
3714
+ customers: []
3715
+ })
3716
+ }] };
3717
+ const slugs = input.slug ? [input.slug] : fs.readdirSync(customersDir).filter((s) => {
3718
+ try {
3719
+ return fs.statSync(path.join(customersDir, s)).isDirectory();
3720
+ } catch {
3721
+ return false;
3722
+ }
3723
+ });
3724
+ const sinceDate = input.since ? new Date(input.since) : /* @__PURE__ */ new Date(Date.now() - 1440 * 60 * 1e3);
3725
+ const results = [];
3726
+ const errors = [];
3727
+ for (const slug of slugs) {
3728
+ const sourcesPath = path.join(customersDir, slug, "sources.json");
3729
+ if (!fs.existsSync(sourcesPath)) continue;
3730
+ try {
3731
+ const sources = JSON.parse(fs.readFileSync(sourcesPath, "utf-8"));
3732
+ if (!sources.gmail?.enabled || !sources.gmail.query) continue;
3733
+ const { syncGmail } = await import("./gmail-sync-DIaxInDT.js");
3734
+ const result = await syncGmail({
3735
+ slug,
3736
+ dataDir,
3737
+ auth,
3738
+ query: sources.gmail.query,
3739
+ since: sinceDate
3740
+ });
3741
+ updateSlugSyncState(dataDir, slug, { lastGmailSync: (/* @__PURE__ */ new Date()).toISOString() });
3742
+ results.push({
3743
+ slug,
3744
+ ...result
3745
+ });
3746
+ } catch (err) {
3747
+ errors.push(`${slug}: ${err.message}`);
3748
+ }
3749
+ }
3750
+ const total = results.reduce((acc, r) => ({
3751
+ synced: acc.synced + r.synced,
3752
+ skipped: acc.skipped + r.skipped
3753
+ }), {
3754
+ synced: 0,
3755
+ skipped: 0
3756
+ });
3757
+ return { content: [{
3758
+ type: "text",
3759
+ text: JSON.stringify({
3760
+ success: true,
3761
+ ...total,
3762
+ customers: results,
3763
+ errors
3764
+ }, null, 2)
3765
+ }] };
3766
+ }
3767
+ function registerTriggerSync(server) {
3768
+ server.registerTool("trigger_sync", {
3769
+ title: "Trigger Sync",
3770
+ description: `Immediately trigger a Gmail sync for one or all customers.
3771
+ Use when you need fresh email data before answering a question.
3772
+ The background daemon syncs every 30 minutes automatically — this forces an immediate sync.
3773
+
3774
+ Args:
3775
+ slug: Customer slug to sync (leave empty to sync all customers)
3776
+ since: ISO date string — only fetch emails since this date (default: last 24 hours)
3777
+
3778
+ Returns: { success: boolean, synced: number, skipped: number, customers: [...], errors: [...] }`,
3779
+ inputSchema: z.object({
3780
+ slug: z.string().optional().describe("Customer slug to sync (empty = all customers)"),
3781
+ since: z.string().optional().describe("Sync emails since this ISO date (default: last 24h)")
3782
+ })
3783
+ }, async ({ slug, since }) => {
3784
+ const input = {};
3785
+ if (slug !== void 0) input.slug = slug;
3786
+ if (since !== void 0) input.since = since;
3787
+ return handleTriggerSync(input);
3788
+ });
3789
+ }
3790
+ //#endregion
3791
+ //#region src/mcp/tools/get-audit-log.ts
3792
+ const DATA_DIR$2 = process.cwd();
3793
+ async function handleGetAuditLog(input, dataDir = DATA_DIR$2) {
3794
+ const entries = readAuditLog(dataDir);
3795
+ const filterOpts = { limit: input.limit ?? 50 };
3796
+ if (input.slug !== void 0) filterOpts.slug = input.slug;
3797
+ if (input.actor !== void 0) filterOpts.actor = input.actor;
3798
+ const filtered = filterAuditLog(entries, filterOpts);
3799
+ return { content: [{
3800
+ type: "text",
3801
+ text: JSON.stringify({
3802
+ total: entries.length,
3803
+ returned: filtered.length,
3804
+ entries: filtered
3805
+ }, null, 2)
3806
+ }] };
3807
+ }
3808
+ function registerGetAuditLog(server) {
3809
+ server.registerTool("get_audit_log", {
3810
+ title: "Get Audit Log",
3811
+ description: `Read the CRM audit log — all write operations with timestamp, actor, tool, and customer.
3812
+ Use to answer "what changed recently?", "what did alice do?", or "show me all actions for acme-corp".
3813
+
3814
+ Args:
3815
+ slug: Filter by customer slug (optional)
3816
+ actor: Filter by actor name (optional)
3817
+ limit: Max entries to return (default: 50, most recent)
3818
+
3819
+ Returns: { total: number, returned: number, entries: [{timestamp, actor, tool, slug, summary}] }`,
3820
+ inputSchema: z.object({
3821
+ slug: z.string().optional().describe("Filter by customer slug"),
3822
+ actor: z.string().optional().describe("Filter by actor (user or system)"),
3823
+ limit: z.number().int().min(1).max(500).optional().describe("Max entries (default 50)")
3824
+ })
3825
+ }, async ({ slug, actor, limit }) => {
3826
+ const input = {};
3827
+ if (slug !== void 0) input.slug = slug;
3828
+ if (actor !== void 0) input.actor = actor;
3829
+ if (limit !== void 0) input.limit = limit;
3830
+ return handleGetAuditLog(input);
3831
+ });
3832
+ }
3833
+ //#endregion
3834
+ //#region src/mcp/prompts.ts
3835
+ /**
3836
+ * CRM playbook prompts exposed via MCP `prompts/list` + `prompts/get`.
3837
+ * Each renders an actionable, tool-referencing instruction for the host LLM —
3838
+ * the agent-native equivalent of a Salesforce "playbook".
3839
+ */
3840
+ const CRM_PROMPTS = [
3841
+ {
3842
+ name: "deal_risk_review",
3843
+ title: "Assess deal risk",
3844
+ description: "Evaluate the health and risk of a customer's open deals and recommend next steps.",
3845
+ build: ({ slug }) => `Assess the deal risk for customer "${slug}".\n1. Call open_deal_room({ slug: "${slug}" }) for a consolidated brief, or get_customer_context + get_deal_health.\n2. Identify stalled deals, approaching close dates, and silent champions (get_relationship_health).\n3. Summarise the top risks and recommend concrete next actions. Do not invent data — cite what you read.`
3846
+ },
3847
+ {
3848
+ name: "draft_follow_up",
3849
+ title: "Draft a follow-up email",
3850
+ description: "Draft a personalized follow-up email for a customer based on recent interactions.",
3851
+ build: ({ slug }) => `Draft a follow-up email for customer "${slug}".\n1. Read recent context with get_customer_context({ slug: "${slug}" }).\n2. Use draft_email({ slug: "${slug}", templateId, tone: "friendly" }) with an appropriate template.\n3. Reference the latest interaction concretely; keep it concise. Return the draft for review — do not send.`
3852
+ },
3853
+ {
3854
+ name: "account_brief",
3855
+ title: "Create an account brief",
3856
+ description: "Produce a concise executive brief for a customer account.",
3857
+ build: ({ slug }) => `Create an executive account brief for "${slug}".\n1. get_customer_context({ slug: "${slug}" }) and get_org_intelligence({ slug: "${slug}" }).\n2. Summarise: who the stakeholders are (champions/buyers/blockers), open pipeline, health, and risks.\n3. End with the single most important next action.`
3858
+ },
3859
+ {
3860
+ name: "pipeline_summary",
3861
+ title: "Summarize the pipeline",
3862
+ description: "Summarize pipeline and forecast, optionally focused on one customer.",
3863
+ build: ({ slug }) => `Summarize the sales pipeline (focus customer: "${slug}").\n1. get_pipeline_forecast() for the weighted total and per-stage breakdown.\n2. simulate_revenue() for P10/P50/P90 if a probabilistic view helps.\n3. Highlight at-risk revenue and the deals that most move the forecast.`
3864
+ }
3865
+ ];
3866
+ function registerPrompts(server) {
3867
+ for (const prompt of CRM_PROMPTS) server.registerPrompt(prompt.name, {
3868
+ title: prompt.title,
3869
+ description: prompt.description,
3870
+ argsSchema: { slug: z.string().describe("Customer slug") }
3871
+ }, ({ slug }) => ({ messages: [{
3872
+ role: "user",
3873
+ content: {
3874
+ type: "text",
3875
+ text: prompt.build({ slug })
3876
+ }
3877
+ }] }));
3878
+ }
3879
+ //#endregion
3880
+ //#region src/mcp/resources.ts
3881
+ const DATA_DIR$1 = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3882
+ /**
3883
+ * Read-only MCP Resources for CRM entities. Complements the action Tools:
3884
+ * agents can `resources/read` a customer briefing, pipeline or timeline by URI
3885
+ * instead of calling a tool.
3886
+ */
3887
+ function registerResources(server, dataDir = DATA_DIR$1) {
3888
+ server.registerResource("customers", "crm://customers", {
3889
+ title: "Customers",
3890
+ description: "List of all customer slugs",
3891
+ mimeType: "application/json"
3892
+ }, (uri) => ({ contents: [{
3893
+ uri: uri.href,
3894
+ mimeType: "application/json",
3895
+ text: JSON.stringify(listCustomerSlugs(dataDir), null, 2)
3896
+ }] }));
3897
+ server.registerResource("customer", new ResourceTemplate("crm://customer/{slug}", { list: void 0 }), {
3898
+ title: "Customer context",
3899
+ description: "LLM-ready briefing (main facts, recent interactions, pipeline) for a customer",
3900
+ mimeType: "text/markdown"
3901
+ }, async (uri, variables) => {
3902
+ const { buildContext } = await import("./context-builder-DlrRcqmJ.js");
3903
+ const text = await buildContext(dataDir, String(variables["slug"]));
3904
+ return { contents: [{
3905
+ uri: uri.href,
3906
+ mimeType: "text/markdown",
3907
+ text
3908
+ }] };
3909
+ });
3910
+ server.registerResource("pipeline", new ResourceTemplate("crm://pipeline/{slug}", { list: void 0 }), {
3911
+ title: "Pipeline",
3912
+ description: "Open and closed deals for a customer",
3913
+ mimeType: "application/json"
3914
+ }, async (uri, variables) => {
3915
+ const { readPipeline } = await import("./pipeline-writer-BqBrYrQc.js");
3916
+ const deals = await readPipeline(dataDir, String(variables["slug"]));
3917
+ return { contents: [{
3918
+ uri: uri.href,
3919
+ mimeType: "application/json",
3920
+ text: JSON.stringify(deals, null, 2)
3921
+ }] };
3922
+ });
3923
+ server.registerResource("timeline", new ResourceTemplate("crm://timeline/{slug}", { list: void 0 }), {
3924
+ title: "Interaction timeline",
3925
+ description: "Newest-first interaction history for a customer",
3926
+ mimeType: "text/markdown"
3927
+ }, async (uri, variables) => {
3928
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
3929
+ const text = await readInteractions(dataDir, String(variables["slug"]));
3930
+ return { contents: [{
3931
+ uri: uri.href,
3932
+ mimeType: "text/markdown",
3933
+ text
3934
+ }] };
3935
+ });
3936
+ }
3937
+ //#endregion
3938
+ //#region src/mcp/tools/custom-objects.ts
3939
+ const DATA_DIR = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3940
+ const FIELD_TYPES = [
3941
+ "text",
3942
+ "number",
3943
+ "boolean",
3944
+ "date",
3945
+ "select"
3946
+ ];
3947
+ function json(data) {
3948
+ return { content: [{
3949
+ type: "text",
3950
+ text: JSON.stringify(data, null, 2)
3951
+ }] };
3952
+ }
3953
+ function handleDefineCustomObject(input, dataDir = DATA_DIR) {
3954
+ enforceRbac(dataDir, "define_custom_object");
3955
+ const objects = defineCustomObject(dataDir, {
3956
+ name: input.name,
3957
+ ...input.label ? { label: input.label } : {},
3958
+ fields: input.fields
3959
+ });
3960
+ return json({
3961
+ defined: input.name,
3962
+ objectCount: objects.length
3963
+ });
3964
+ }
3965
+ function handleCreateRecord(input, dataDir = DATA_DIR) {
3966
+ enforceRbac(dataDir, "create_record");
3967
+ const res = createRecord(dataDir, input.object, input.values);
3968
+ if (!res.ok) return json({ error: (res.errors ?? []).join("; ") });
3969
+ import("./webhooks-7EpA05Qr.js").then(({ emitEvent }) => emitEvent(dataDir, "record.created", {
3970
+ object: input.object,
3971
+ record: res.record
3972
+ }));
3973
+ return json({ record: res.record });
3974
+ }
3975
+ function handleListRecords(input, dataDir = DATA_DIR) {
3976
+ return json({
3977
+ object: input.object,
3978
+ records: listRecords(dataDir, input.object)
3979
+ });
3980
+ }
3981
+ function handleListCustomObjects(dataDir = DATA_DIR) {
3982
+ return json({ objects: loadCustomObjects(dataDir) });
3983
+ }
3984
+ function registerCustomObjectTools(server, dataDir = DATA_DIR) {
3985
+ server.registerTool("define_custom_object", {
3986
+ description: "Define a custom object (runtime entity type) with typed fields — no code migration. admin only.",
3987
+ inputSchema: z.object({
3988
+ name: z.string().describe("Object name (e.g. contract)"),
3989
+ label: z.string().optional(),
3990
+ fields: z.array(z.object({
3991
+ name: z.string(),
3992
+ type: z.enum(FIELD_TYPES),
3993
+ label: z.string().optional(),
3994
+ options: z.array(z.string()).optional()
3995
+ })).describe("Field definitions")
3996
+ })
3997
+ }, ({ name, label, fields }) => handleDefineCustomObject({
3998
+ name,
3999
+ ...label ? { label } : {},
4000
+ fields
4001
+ }, dataDir));
4002
+ server.registerTool("create_record", {
4003
+ description: "Create a record of a custom object. Values are validated against the schema. rep+.",
4004
+ inputSchema: z.object({
4005
+ object: z.string().describe("Custom object name"),
4006
+ values: z.record(z.string()).describe("Field values (key=value)")
4007
+ })
4008
+ }, ({ object, values }) => handleCreateRecord({
4009
+ object,
4010
+ values
4011
+ }, dataDir));
4012
+ server.registerTool("list_records", {
4013
+ description: "List records of a custom object.",
4014
+ inputSchema: z.object({ object: z.string().describe("Custom object name") })
4015
+ }, ({ object }) => handleListRecords({ object }, dataDir));
4016
+ server.registerTool("list_custom_objects", {
4017
+ description: "List all defined custom objects and their field schemas.",
4018
+ inputSchema: z.object({})
4019
+ }, () => handleListCustomObjects(dataDir));
4020
+ }
4021
+ //#endregion
4022
+ //#region src/mcp/server.ts
4023
+ function surveyThankYouPage(score, comment) {
4024
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Thank you</title>
4025
+ <style>body{font-family:sans-serif;max-width:480px;margin:80px auto;text-align:center;padding:0 20px}
4026
+ h1{font-size:2.5em;margin-bottom:.3em}p{color:#555;font-size:1.1em}</style></head>
4027
+ <body><h1>${score >= 9 ? "🎉" : score >= 7 ? "🙂" : "🙏"}</h1><h2>Thank you for your feedback!</h2>
4028
+ <p>You rated us <strong>${score}/10</strong>.${comment ? `<br>Your comment: <em>"${String(comment).slice(0, 200).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}"</em>` : ""}</p>
4029
+ <p style="margin-top:40px;color:#aaa;font-size:.85em">Powered by DatasynxOpenCRM</p>
4030
+ </body></html>`;
4031
+ }
4032
+ function createMcpServer() {
4033
+ const server = new McpServer({
4034
+ name: "datasynx-opencrm",
4035
+ version: "0.1.0"
4036
+ });
4037
+ registerGetCapabilities(server);
4038
+ registerGetActiveSession(server);
4039
+ registerGetCustomerContext(server);
4040
+ registerSearchCustomerKnowledge(server);
4041
+ registerListCustomers(server);
4042
+ registerLogInteraction(server);
4043
+ registerUpdateDeal(server);
4044
+ registerExportCustomer(server);
4045
+ registerUpdateCustomerFacts(server);
4046
+ registerGetDealHealth(server);
4047
+ registerGetPipelineForecast(server);
4048
+ registerSummarizeMeeting(server);
4049
+ registerGetPipelineStages(server);
4050
+ registerGetMarketIntelligence(server);
4051
+ registerGetRelationshipGraph(server);
4052
+ registerGetRelationshipHealth(server);
4053
+ registerRunDealAgent(server);
4054
+ registerApproveAgentAction(server);
4055
+ registerSimulateRevenue(server);
4056
+ registerGetPlaybook(server);
4057
+ registerCreatePlaybook(server);
4058
+ registerListPlaybooks(server);
4059
+ registerDistillPlaybook(server);
4060
+ registerPursueGoal(server);
4061
+ registerGetGoalStatus(server);
4062
+ registerRegisterPushSubscription(server);
4063
+ registerGetPushStatus(server);
4064
+ registerGetOrgIntelligence(server);
4065
+ registerOpenDealRoom(server);
4066
+ registerGetProactiveBriefing(server);
4067
+ registerListEmailTemplates(server);
4068
+ registerGetEmailTemplate(server);
4069
+ registerDraftEmail(server);
4070
+ registerEnrollInSequence(server);
4071
+ registerListSequenceEnrollments(server);
4072
+ registerUnenrollFromSequence(server);
4073
+ registerListSequences(server);
4074
+ registerGenerateQuote(server);
4075
+ registerGetQuoteStatus(server);
4076
+ registerGetBookingLink(server);
4077
+ registerCreateTicket(server);
4078
+ registerUpdateTicket(server);
4079
+ registerListTickets(server);
4080
+ registerCloseTicket(server);
4081
+ registerSendNpsSurvey(server);
4082
+ registerGetSurveyResults(server);
4083
+ registerSearchKnowledgeBase(server);
4084
+ registerCreateKbArticle(server);
4085
+ registerBackupNow(server);
4086
+ registerListBackups(server);
4087
+ registerTriggerSync(server);
4088
+ registerGetAuditLog(server);
4089
+ registerCustomObjectTools(server);
4090
+ registerPrompts(server);
4091
+ registerResources(server);
4092
+ return server;
4093
+ }
4094
+ async function startStdio() {
4095
+ await initOAuthFromDisk(process.cwd());
4096
+ const server = createMcpServer();
4097
+ const transport = new StdioServerTransport();
4098
+ await server.connect(transport);
4099
+ console.error("DatasynxOpenCRM MCP Server running via stdio");
4100
+ }
4101
+ async function startHttp(port = 3847) {
4102
+ await initOAuthFromDisk(process.cwd());
4103
+ const { default: express } = await import("express");
4104
+ const app = express();
4105
+ app.use(express.json());
4106
+ const server = createMcpServer();
4107
+ const dataDir = process.cwd();
4108
+ app.get("/.well-known/oauth-protected-resource", (req, res) => {
4109
+ const base = `${req.protocol}://${req.get("host") ?? "localhost"}`;
4110
+ res.json(protectedResourceMetadata(`${base}/mcp`));
4111
+ });
4112
+ app.post("/mcp", async (req, res) => {
4113
+ if (isAuthRequired(dataDir)) {
4114
+ const auth = verifyBearer(req.headers["authorization"], dataDir);
4115
+ if (!auth.ok) {
4116
+ const base = `${req.protocol}://${req.get("host") ?? "localhost"}`;
4117
+ res.status(401).set("WWW-Authenticate", wwwAuthenticateHeader(`${base}/.well-known/oauth-protected-resource`)).json({ error: "unauthorized" });
4118
+ return;
4119
+ }
4120
+ if (auth.actor) process.env["DXCRM_ACTOR"] = auth.actor;
4121
+ }
4122
+ const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
4123
+ transport.onclose = () => {};
4124
+ res.on("close", () => {
4125
+ transport.close();
4126
+ });
4127
+ await server.connect(transport);
4128
+ await transport.handleRequest(req, res, req.body);
4129
+ });
4130
+ app.get("/health", (_req, res) => {
4131
+ res.json({
4132
+ status: "ok",
4133
+ server: "datasynx-opencrm",
4134
+ version: "0.1.0"
4135
+ });
4136
+ });
4137
+ app.get("/sessions", async (_req, res) => {
4138
+ try {
4139
+ const { readAllSessions } = await import("./session-mWHA71Lw.js");
4140
+ const sessions = readAllSessions(dataDir);
4141
+ res.json({ sessions });
4142
+ } catch {
4143
+ res.json({ sessions: [] });
4144
+ }
4145
+ });
4146
+ app.post("/webhooks/gmail", async (req, res) => {
4147
+ const token = process.env["GMAIL_PUBSUB_TOKEN"] ?? "";
4148
+ if (!verifyGmailPubSubSignature(req.headers["authorization"], token)) {
4149
+ res.status(401).json({ error: "unauthorized" });
4150
+ return;
4151
+ }
4152
+ const payload = decodeGmailPubSubPayload(req.body);
4153
+ if (!payload) {
4154
+ res.status(400).json({ error: "invalid_payload" });
4155
+ return;
4156
+ }
4157
+ const result = await handleGmailPushEvent(dataDir, payload, "").catch(() => ({
4158
+ processed: 0,
4159
+ slug: null
4160
+ }));
4161
+ res.json({
4162
+ ok: true,
4163
+ processed: result.processed
4164
+ });
4165
+ });
4166
+ app.all("/webhooks/microsoft", async (req, res) => {
4167
+ const validation = handleMicrosoftValidationRequest(req.query);
4168
+ if (validation.isValidation) {
4169
+ res.setHeader("content-type", "text/plain");
4170
+ res.status(200).send(validation.token);
4171
+ return;
4172
+ }
4173
+ const clientState = process.env["MS_GRAPH_CLIENT_STATE"] ?? "";
4174
+ const body = req.body;
4175
+ if (!verifyMicrosoftGraphSignature(body, clientState)) {
4176
+ res.status(401).json({ error: "unauthorized" });
4177
+ return;
4178
+ }
4179
+ const result = await handleMicrosoftPushEvent(dataDir, body.value ?? [], "").catch(() => ({
4180
+ processed: 0,
4181
+ skipped: 0
4182
+ }));
4183
+ res.json({
4184
+ ok: true,
4185
+ ...result
4186
+ });
4187
+ });
4188
+ app.post("/webhooks/slack", express.text({ type: "*/*" }), async (req, res) => {
4189
+ const rawBody = req.body;
4190
+ const signingSecret = process.env["SLACK_SIGNING_SECRET"] ?? "";
4191
+ if (!verifySlackSignature(rawBody, req.headers, signingSecret)) {
4192
+ res.status(401).json({ error: "unauthorized" });
4193
+ return;
4194
+ }
4195
+ let parsed;
4196
+ try {
4197
+ parsed = JSON.parse(rawBody);
4198
+ } catch {
4199
+ res.status(400).json({ error: "invalid_json" });
4200
+ return;
4201
+ }
4202
+ const verification = handleSlackUrlVerification(parsed);
4203
+ if (verification.isVerification) {
4204
+ res.json({ challenge: verification.challenge });
4205
+ return;
4206
+ }
4207
+ if (!parsed.event) {
4208
+ res.json({
4209
+ ok: true,
4210
+ processed: 0
4211
+ });
4212
+ return;
4213
+ }
4214
+ const botToken = process.env["SLACK_BOT_TOKEN"] ?? "";
4215
+ const result = await handleSlackPushEvent(dataDir, parsed.event, botToken, { ...parsed.team_id !== void 0 ? { teamId: parsed.team_id } : {} }).catch(() => ({
4216
+ processed: 0,
4217
+ skipped: 1
4218
+ }));
4219
+ res.json({
4220
+ ok: true,
4221
+ ...result
4222
+ });
4223
+ });
4224
+ app.get("/survey/respond", async (req, res) => {
4225
+ const { token, score, comment } = req.query;
4226
+ if (!token) {
4227
+ res.status(400).send("<h2>Invalid survey link.</h2>");
4228
+ return;
4229
+ }
4230
+ if (comment === "true") {
4231
+ res.setHeader("content-type", "text/html");
4232
+ res.send(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Survey Comment</title>
4233
+ <style>body{font-family:sans-serif;max-width:520px;margin:60px auto;padding:0 20px}
4234
+ textarea{width:100%;padding:10px;font-size:1em;border:1px solid #ccc;border-radius:4px}
4235
+ input[type=number]{width:80px;padding:8px;font-size:1em}
4236
+ button{margin-top:12px;padding:12px 28px;background:#1a1a2e;color:#fff;border:none;border-radius:4px;font-size:1em;cursor:pointer}</style></head>
4237
+ <body><h2>Leave a comment</h2>
4238
+ <form method="POST" action="/survey/respond">
4239
+ <input type="hidden" name="token" value="${String(token).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;")}">
4240
+ <label>Your score (0–10):<br><input type="number" name="score" min="0" max="10" required></label><br><br>
4241
+ <label>Comment (optional):<br><textarea name="comment" rows="5" placeholder="What can we improve?"></textarea></label><br>
4242
+ <button type="submit">Submit</button>
4243
+ </form></body></html>`);
4244
+ return;
4245
+ }
4246
+ const numScore = score !== void 0 ? parseInt(score, 10) : NaN;
4247
+ if (isNaN(numScore) || numScore < 0 || numScore > 10) {
4248
+ res.status(400).send("<h2>Invalid score. Please use the link from your email.</h2>");
4249
+ return;
4250
+ }
4251
+ const { recordSurveyResponse } = await import("./survey-engine-C06hcQt3.js");
4252
+ await recordSurveyResponse(dataDir, token, numScore).catch(() => null);
4253
+ res.setHeader("content-type", "text/html");
4254
+ res.send(surveyThankYouPage(numScore));
4255
+ });
4256
+ app.post("/survey/respond", express.urlencoded({ extended: false }), async (req, res) => {
4257
+ const { token, score, comment: commentText } = req.body;
4258
+ if (!token) {
4259
+ res.status(400).send("<h2>Invalid survey link.</h2>");
4260
+ return;
4261
+ }
4262
+ const numScore = score !== void 0 ? parseInt(score, 10) : NaN;
4263
+ if (isNaN(numScore) || numScore < 0 || numScore > 10) {
4264
+ res.status(400).send("<h2>Invalid score. Please go back and enter a number between 0 and 10.</h2>");
4265
+ return;
4266
+ }
4267
+ const { recordSurveyResponse } = await import("./survey-engine-C06hcQt3.js");
4268
+ await recordSurveyResponse(dataDir, token, numScore, commentText || void 0).catch(() => null);
4269
+ res.setHeader("content-type", "text/html");
4270
+ res.send(surveyThankYouPage(numScore, commentText));
4271
+ });
4272
+ app.listen(port, () => {
4273
+ console.error(`DatasynxOpenCRM MCP Server running on http://0.0.0.0:${port}/mcp`);
4274
+ });
4275
+ }
4276
+ if ((process.env["DXCRM_MCP_MODE"] ?? "stdio") === "http") startHttp(parseInt(process.env["DXCRM_MCP_PORT"] ?? "3847", 10)).catch((err) => {
4277
+ console.error("MCP Server fatal error:", err.message);
4278
+ process.exit(1);
4279
+ });
4280
+ else startStdio().catch((err) => {
4281
+ console.error("MCP Server fatal error:", err.message);
4282
+ process.exit(1);
4283
+ });
4284
+ //#endregion
4285
+ export { startHttp, startStdio };
4286
+
4287
+ //# sourceMappingURL=server-Dyva03K8.js.map