@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
package/dist/cli.js ADDED
@@ -0,0 +1,4396 @@
1
+ #!/usr/bin/env node
2
+ import { a as writeMainFacts, i as readMainFacts, n as ensureCustomerDir, o as MainFactsSchema, r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
3
+ import { a as warning, i as success, n as error, r as info, t as bold } from "./colors-BG07TZQz.js";
4
+ import { n as getSession } from "./session-store-CEa39Dxs.js";
5
+ import { i as sessionCommand, r as readAllSessions } from "./session-B9AilxOE.js";
6
+ import { a as searchKbSimple, i as listKbArticles, n as getKbArticle, o as writeKbArticle, s as CAPABILITIES_TEXT, t as deleteKbArticle } from "./knowledge-base-D0Fh40kc.js";
7
+ import { a as restoreCommand, t as backupCommand } from "./backup-CeMk9z86.js";
8
+ import { n as readSyncState } from "./sync-state-ChaLbamC.js";
9
+ import { n as readUnmatched } from "./unmatched-transcripts-BsH5bhkU.js";
10
+ import { t as AgentConfigSchema } from "./agent-config-zPvcqu07.js";
11
+ import { t as appendInteraction } from "./interactions-writer-SLHnoEeE.js";
12
+ import { i as writeAuditEntry, n as getActor, r as readAuditLog, t as filterAuditLog } from "./audit-log-DNMY9mUZ.js";
13
+ import { i as canWrite, o as getRbacConfig, u as setActorRole } from "./rbac-CTIktZaC.js";
14
+ import { t as withJsonFile } from "./file-lock-B_zi7NQl.js";
15
+ import { d as deletePipelineStage, f as getPipelineStages, m as setPipelineStage, p as resetToDefaults } from "./revenue-simulation-Bqf2DLVB.js";
16
+ import { d as pursueGoal, h as updateGoalProgress, i as getActiveGoals, n as cancelGoal } from "./goal-engine-KpBftn4V.js";
17
+ import { a as revoke, i as renewExpiringSubscriptions, n as readSubscriptions, r as register } from "./push-manager-CdqIIkuh.js";
18
+ import { a as writeEnrollment, d as getTemplate, f as listTemplates, l as interpolate, n as listSequences, o as writeSequence, p as writeTemplate, r as readEnrollments, s as buildVariablesFromCustomer, t as getSequence, u as deleteTemplate } from "./sequence-store-DaaWr0Os.js";
19
+ import { r as runSequenceCycle } from "./sequence-engine-J1lTW_in.js";
20
+ import { n as listQuotes, r as readQuote, t as generateQuote } from "./quote-generator-BfwENXzg.js";
21
+ import { i as upsertTicket, n as nextTicketId, r as readTickets, t as listAllTickets } from "./ticket-writer-j2oX_Wal.js";
22
+ import { i as loadSlaRules, t as calcSlaDue } from "./sla-engine-BqX-7u-7.js";
23
+ import { a as listSurveys, d as writeSurvey, i as getSurvey, l as savePendingSurvey, n as calcNpsScore, o as loadSurveyResponses, r as generateSurveyToken } from "./survey-engine-DBjCYqCv.js";
24
+ import { Command } from "commander";
25
+ import path from "path";
26
+ import fs from "fs";
27
+ import slugify from "slug";
28
+ import matter from "gray-matter";
29
+ import Table from "cli-table3";
30
+ import os from "os";
31
+ import { execSync, spawn } from "child_process";
32
+ import { createHash } from "crypto";
33
+ //#region src/commands/create.ts
34
+ async function createCustomer(opts) {
35
+ const id = slugify(opts.name, { lower: true });
36
+ const dataDir = opts.dataDir ?? process.env["DXCRM_DATA_DIR"] ?? process.cwd();
37
+ await ensureCustomerDir(dataDir, id);
38
+ const dir = path.join(dataDir, "customers", id);
39
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
40
+ await writeMainFacts(dataDir, id, {
41
+ name: opts.name,
42
+ domain: opts.domain,
43
+ email: opts.email,
44
+ relationship_stage: "prospect",
45
+ tags: [],
46
+ currency: "EUR",
47
+ created: today,
48
+ updated: today
49
+ });
50
+ const interactionsPath = path.join(dir, "interactions.md");
51
+ if (!fs.existsSync(interactionsPath)) fs.writeFileSync(interactionsPath, `# Interactions — ${opts.name}\n\n`);
52
+ const pipelinePath = path.join(dir, "pipeline.md");
53
+ if (!fs.existsSync(pipelinePath)) fs.writeFileSync(pipelinePath, `# Pipeline — ${opts.name}\n\n| Deal | Stage | Value | Currency | Probability | Close Date | Updated | Notes |\n|---|---|---|---|---|---|---|---|\n`);
54
+ const sourcesPath = path.join(dir, "sources.json");
55
+ if (!fs.existsSync(sourcesPath)) {
56
+ const sources = {
57
+ gmail: {
58
+ type: "gmail",
59
+ query: opts.domain ? `from:${opts.domain} OR to:${opts.domain}` : opts.email ? `from:${opts.email} OR to:${opts.email}` : "",
60
+ enabled: true
61
+ },
62
+ version: 1,
63
+ created: (/* @__PURE__ */ new Date()).toISOString()
64
+ };
65
+ fs.writeFileSync(sourcesPath, JSON.stringify(sources, null, 2));
66
+ }
67
+ return {
68
+ id,
69
+ dir
70
+ };
71
+ }
72
+ const createCommand = new Command("create").argument("<name>", "Customer name").option("--domain <domain>", "Primary domain (for Gmail sync)").option("--email <email>", "Primary contact email").action(async (name, opts) => {
73
+ try {
74
+ const { id, dir } = await createCustomer({
75
+ name,
76
+ ...opts
77
+ });
78
+ console.log(success(`✓ Created customer: ${bold(id)}`));
79
+ console.log(` Dir: ${dir}`);
80
+ console.log(` Files: main_facts.md, interactions.md, pipeline.md, sources.json`);
81
+ } catch (err) {
82
+ console.error(error(`✗ ${err.message}`));
83
+ process.exit(1);
84
+ }
85
+ });
86
+ //#endregion
87
+ //#region src/ui/table.ts
88
+ function renderCustomerTable(customers) {
89
+ const table = new Table({
90
+ head: [
91
+ "Slug",
92
+ "Name",
93
+ "Stage",
94
+ "Industry",
95
+ "Tags",
96
+ "Updated"
97
+ ],
98
+ style: { head: ["cyan"] },
99
+ colWidths: [
100
+ 20,
101
+ 25,
102
+ 15,
103
+ 15,
104
+ 20,
105
+ 12
106
+ ],
107
+ wordWrap: true
108
+ });
109
+ for (const { slug, facts } of customers) table.push([
110
+ slug,
111
+ facts.name,
112
+ facts.relationship_stage,
113
+ facts.industry ?? "—",
114
+ facts.tags.join(", ") || "—",
115
+ facts.updated
116
+ ]);
117
+ return table.toString();
118
+ }
119
+ //#endregion
120
+ //#region src/commands/list.ts
121
+ const listCommand = new Command("list").option("--filter <query>", "Filter by name or slug").action(async (opts) => {
122
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
123
+ const slugs = listCustomerSlugs(dataDir);
124
+ if (slugs.length === 0) {
125
+ console.log("No customers yet. Run: dxcrm create \"Customer Name\"");
126
+ return;
127
+ }
128
+ const customers = [];
129
+ for (const slug of slugs) try {
130
+ const facts = await readMainFacts(dataDir, slug);
131
+ if (opts.filter) {
132
+ const q = opts.filter.toLowerCase();
133
+ if (!facts.name.toLowerCase().includes(q) && !slug.includes(q) && !(facts.relationship_stage ?? "").toLowerCase().includes(q)) continue;
134
+ }
135
+ customers.push({
136
+ slug,
137
+ facts
138
+ });
139
+ } catch {}
140
+ if (customers.length === 0) {
141
+ console.log(opts.filter ? `No customers matching "${opts.filter}"` : "No customers yet.");
142
+ return;
143
+ }
144
+ console.log(renderCustomerTable(customers));
145
+ });
146
+ //#endregion
147
+ //#region src/commands/validate.ts
148
+ const RECOVERABLE_DEFAULTS = {
149
+ tags: [],
150
+ currency: "EUR"
151
+ };
152
+ function applyFix(factsPath, content, data) {
153
+ const fixed = [];
154
+ const patched = { ...data };
155
+ for (const [field, defaultValue] of Object.entries(RECOVERABLE_DEFAULTS)) if (patched[field] === void 0 || patched[field] === null) {
156
+ patched[field] = defaultValue;
157
+ fixed.push(`${field} → ${JSON.stringify(defaultValue)}`);
158
+ }
159
+ if (patched["updated"] === void 0 && patched["created"]) {
160
+ patched["updated"] = patched["created"];
161
+ fixed.push(`updated → ${String(patched["created"])}`);
162
+ }
163
+ if (fixed.length === 0) return null;
164
+ const parsed = matter(content);
165
+ const newContent = matter.stringify(parsed.content, patched);
166
+ fs.writeFileSync(factsPath, newContent);
167
+ return {
168
+ fixed,
169
+ content: newContent
170
+ };
171
+ }
172
+ async function runValidate(opts, dataDir) {
173
+ const customersDir = path.join(dataDir, "customers");
174
+ if (!fs.existsSync(customersDir)) {
175
+ console.log(warning("⚠ No customers directory found."));
176
+ return;
177
+ }
178
+ const slugs = listCustomerSlugs(dataDir);
179
+ let errorCount = 0;
180
+ let fixedCount = 0;
181
+ for (const slug of slugs) {
182
+ const factsPath = path.join(customersDir, slug, "main_facts.md");
183
+ const interactionsPath = path.join(customersDir, slug, "interactions.md");
184
+ if (!fs.existsSync(factsPath)) {
185
+ console.log(error(`✗ ${slug}: missing main_facts.md`));
186
+ errorCount++;
187
+ continue;
188
+ }
189
+ try {
190
+ let content = fs.readFileSync(factsPath, "utf-8");
191
+ const { data } = matter(content);
192
+ if (opts.fix) {
193
+ const result = applyFix(factsPath, content, data);
194
+ if (result) {
195
+ content = result.content;
196
+ fixedCount++;
197
+ console.log(info(`⚙ ${slug}: fixed ${result.fixed.join(", ")}`));
198
+ }
199
+ }
200
+ const { data: refetchedData } = matter(content);
201
+ MainFactsSchema.parse(refetchedData);
202
+ if (!fs.existsSync(interactionsPath)) console.log(warning(`⚠ ${slug}: missing interactions.md`));
203
+ else console.log(success(`✓ ${slug}`));
204
+ } catch (err) {
205
+ console.log(error(`✗ ${slug}: ${err.message}`));
206
+ errorCount++;
207
+ }
208
+ }
209
+ if (opts.fix && fixedCount > 0) console.log(info(`\n⚙ Fixed ${fixedCount} customer(s).`));
210
+ if (errorCount > 0) {
211
+ console.error(error(`\n${errorCount} error(s) found.`));
212
+ process.exit(1);
213
+ } else console.log(success("\n✓ All customers valid."));
214
+ }
215
+ const validateCommand = new Command("validate").option("--fix", "Auto-fix recoverable issues").action(async (opts) => {
216
+ await runValidate(opts, process.env["DXCRM_DATA_DIR"] ?? process.cwd());
217
+ });
218
+ //#endregion
219
+ //#region src/commands/guide.ts
220
+ const guideCommand = new Command("guide").description("Full CRM documentation in terminal").action(() => {
221
+ console.log(CAPABILITIES_TEXT);
222
+ });
223
+ const mcpCommand = new Command("mcp");
224
+ mcpCommand.command("docs").action(() => {
225
+ console.log(CAPABILITIES_TEXT);
226
+ });
227
+ mcpCommand.command("token").description("Mint a bearer token for the HTTP MCP server (printed once)").requiredOption("--actor <actor>", "Actor/user the token authenticates as").option("--role <role>", "RBAC role: admin | manager | rep", "rep").option("--label <label>", "Optional label (e.g. device name)").action(async (opts) => {
228
+ const role = [
229
+ "admin",
230
+ "manager",
231
+ "rep"
232
+ ].includes(opts.role) ? opts.role : "rep";
233
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
234
+ const { createMcpToken } = await import("./auth-CyFuu9X_.js");
235
+ const token = createMcpToken(dataDir, opts.actor, role, opts.label);
236
+ console.log(info("MCP bearer token (store it now — it is not shown again):"));
237
+ console.log(token);
238
+ console.log(info(`actor=${opts.actor} role=${role} — HTTP /mcp now requires this token.`));
239
+ });
240
+ mcpCommand.command("start").description("Start MCP server (stdio by default)").option("--http", "Use HTTP transport instead of stdio").option("--port <port>", "HTTP port (default 3847)", "3847").action(async (opts) => {
241
+ if (opts.http) {
242
+ const port = parseInt(opts.port, 10);
243
+ console.error(info(`Starting MCP server in HTTP mode on port ${port}...`));
244
+ const { startHttp } = await import("./server-Dyva03K8.js");
245
+ await startHttp(port);
246
+ } else {
247
+ const { startStdio } = await import("./server-Dyva03K8.js");
248
+ await startStdio();
249
+ }
250
+ });
251
+ const TOOL_COUNT = 56;
252
+ /** Claude Code: CLAUDE.md in CRM dataDir */
253
+ function buildClaudeMd(dataDir) {
254
+ return `# DatasynxOpenCRM v2 — Agent Instructions (${TOOL_COUNT} MCP Tools)
255
+
256
+ ## Proactive — Act Without Being Asked
257
+ At the start of every session, before the user says anything:
258
+ 1. \`get_proactive_briefing()\` — today's urgent items, forecast, top action
259
+ 2. \`get_goal_status()\` — if active goals exist, show progress
260
+
261
+ ## Before Every Deal Conversation
262
+ Use \`open_deal_room({ slug, dealName })\` — not \`get_customer_context()\`.
263
+ It combines graph, health, revenue simulation, playbook, and org intelligence in one call (~3–5s).
264
+
265
+ ## Standard Workflow
266
+ | Trigger | Tool |
267
+ |---|---|
268
+ | Customer mentioned | \`get_customer_context(slug)\` or \`open_deal_room(slug, dealName)\` |
269
+ | After call/meeting/email | \`log_interaction(slug, type, summary, nextSteps)\` |
270
+ | Deal stage changes | \`update_deal(slug, dealName, { stage, probability, value })\` |
271
+ | Historical question | \`search_customer_knowledge(slug, query)\` |
272
+ | "What should I do today?" | \`get_proactive_briefing()\` |
273
+ | "Close €X this quarter" | \`pursue_goal(goal, deadline)\` |
274
+
275
+ ## Autonomy Patterns
276
+
277
+ **User says "Look at Acme Corp":**
278
+ 1. \`open_deal_room({ slug: "acme-corp", dealName: "<active deal>" })\`
279
+ 2. Summarize in 3 bullets, recommend 1 action
280
+
281
+ **User says "What do I need to do today?":**
282
+ 1. \`get_proactive_briefing()\`
283
+ 2. \`get_goal_status()\` if goals are active
284
+ 3. Reply with prioritized actions
285
+
286
+ **User says "Deal is stalled":**
287
+ 1. \`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
288
+ 2. Present the plan, ask for approval before acting
289
+
290
+ ## All ${TOOL_COUNT} MCP Tools
291
+
292
+ ### Foundation
293
+ - \`get_capabilities()\` — complete tool reference with schemas
294
+ - \`get_active_session()\` — which customer is currently open
295
+ - \`get_customer_context(slug?)\` — full briefing, triggers background Gmail sync
296
+ - \`search_customer_knowledge(slug, query)\` — semantic vector search across emails + transcripts
297
+ - \`list_customers(filter?)\` — all customers with health score and last touchpoint
298
+ - \`log_interaction(slug, type, summary, nextSteps?)\` — write to CRM; auto-updates graph + health
299
+ - \`update_deal(slug, dealName, fields)\` — pipeline stage, value, probability, close date
300
+ - \`export_customer(slug, format?)\` — export as JSON or Markdown ZIP
301
+ - \`update_customer_facts(slug, fields)\` — name, domain, email, primary_contact, tags
302
+ - \`get_deal_health(slug)\` — health score A–F with warnings per deal
303
+ - \`get_pipeline_forecast()\` — weighted pipeline total and deal list
304
+ - \`summarize_meeting(transcript)\` — LLM meeting analysis → structured notes
305
+ - \`get_pipeline_stages()\` — configured stages with default probabilities
306
+ - \`get_market_intelligence(slug)\` — competitor mentions and market context
307
+
308
+ ### Graph & Relationship (D11/D12)
309
+ - \`get_relationship_graph(slug)\` — stakeholder graph: champions, blockers, economic buyers, warm intro paths
310
+ - \`get_relationship_health(slug)\` — contact health scores A–F, decay detection, risk flags
311
+
312
+ ### Autonomous Deal Agent (D13)
313
+ - \`run_deal_agent({ slug, dealName, autonomyLevel })\` — AI deal analysis + action plan; autonomyLevel: "observe" | "suggest" | "act"
314
+ - \`approve_agent_action({ actionId, approved })\` — approve or reject a queued agent action
315
+
316
+ ### Revenue Intelligence (D14/D18)
317
+ - \`simulate_revenue({ horizon })\` — Monte Carlo P10/P50/P90 forecast over full pipeline
318
+ - \`get_org_intelligence({ slug, dealName })\` — stakeholder map, missing roles, external signals (funding, news)
319
+
320
+ ### Playbooks (D15)
321
+ - \`get_playbook({ slug, situation })\` — matching playbook for current deal situation
322
+ - \`create_playbook({ name, trigger, content })\` — save a new playbook
323
+ - \`list_playbooks()\` — all available playbooks
324
+ - \`distill_playbook({ slug, dealName, outcome })\` — learn from a won/lost deal
325
+
326
+ ### Goals (D16)
327
+ - \`pursue_goal({ goal, deadline, context? })\` — decompose a revenue goal into prioritized sub-actions
328
+ - \`get_goal_status()\` — progress of all active goals
329
+
330
+ ### Push (D17)
331
+ - \`register_push_subscription({ provider, webhookUrl })\` — gmail | microsoft-graph | slack real-time push
332
+ - \`get_push_status()\` — active subscriptions and expiry dates
333
+
334
+ ### Intelligence Synthesis (D19/D20)
335
+ - \`open_deal_room({ slug, dealName })\` — orchestrates 7 sub-tools; returns complete deal brief in one call
336
+ - \`get_proactive_briefing({ date? })\` — AI-generated daily briefing: urgent items, opportunities, forecast
337
+
338
+ ### Email Templates (H2)
339
+ - \`list_email_templates({ category? })\` — list templates; filter by category (outreach, followup, support)
340
+ - \`get_email_template({ id })\` — get full template with auto-detected variables
341
+ - \`draft_email({ slug, templateId, overrides? })\` — draft personalized email from template + customer facts
342
+
343
+ ### Email Sequences (H1)
344
+ - \`enroll_in_sequence({ slug, contactEmail, sequenceId })\` — enroll contact in automated email sequence
345
+ - \`list_sequence_enrollments({ slug?, status? })\` — list enrollments; filter by slug or status
346
+ - \`unenroll_from_sequence({ enrollmentId })\` — pause an active enrollment
347
+ - \`list_sequences()\` — all sequences with step count and enrollment count
348
+
349
+ ### Quotes & Invoices (H4)
350
+ - \`generate_quote({ slug, dealName, lineItems, vatPercent?, validUntilDays? })\` — create HTML quote with auto-numbering Q-YYYY-NNN
351
+ - \`get_quote_status({ quoteNumber?, slug? })\` — get quote or list all quotes for customer
352
+
353
+ ### Meeting Scheduler (H3)
354
+ - \`get_booking_link({ slug, eventType?, prefillName? })\` — get Calendly booking URL, optionally pre-filled with customer name/email
355
+
356
+ ### Ticket Management (H6)
357
+ - \`create_ticket({ slug, title, priority?, assignee? })\` — open support ticket with auto-SLA due date
358
+ - \`update_ticket({ slug, ticketId, status?, assignee? })\` — update ticket status or assignee
359
+ - \`list_tickets({ slug?, status?, priority?, assignee? })\` — list tickets sorted by priority
360
+ - \`close_ticket({ slug, ticketId, resolution? })\` — close ticket and optionally log resolution
361
+
362
+ ### NPS/CSAT Surveys (H7)
363
+ - \`send_nps_survey({ slug, contactEmail, surveyId, serverUrl? })\` — generate survey token and email body for NPS/CSAT survey
364
+ - \`get_survey_results({ surveyId, slug? })\` — NPS score, promoters/passives/detractors, all responses
365
+
366
+ ### Knowledge Base (H8)
367
+ - \`search_knowledge_base({ query, publicOnly? })\` — full-text search across KB articles
368
+ - \`create_kb_article({ id, title, body, category?, tags?, public?, sourceTicketId? })\` — create or update KB article
369
+
370
+ ### Backup (Enterprise)
371
+ - \`backup_now({ remote?, note? })\` — trigger immediate backup of customers/ + .agentic/ with manifest + integrity check
372
+ - \`list_backups({ limit? })\` — list available backups with date, size, verification status, customer count
373
+
374
+ ### Sync & Audit (Enterprise)
375
+ - \`trigger_sync({ slug?, since? })\` — force immediate Gmail sync for one or all customers (bypasses 30-min daemon cycle)
376
+ - \`get_audit_log({ slug?, actor?, limit? })\` — read append-only audit log of all write operations
377
+
378
+ ### Custom Objects (Platform / metadata)
379
+ - \`define_custom_object({ name, label?, fields })\` — define a runtime entity type with typed fields (no migration), admin
380
+ - \`create_record({ object, values })\` — create a record of a custom object, validated against its schema, rep+
381
+ - \`list_records({ object })\` — list records of a custom object
382
+ - \`list_custom_objects()\` — list all defined custom objects and their schemas
383
+
384
+ ## Rules
385
+ - Never discuss a customer without first loading their context
386
+ - Always log interactions — calls, emails, Slack, demos, proposals
387
+ - Never invent information — if uncertain, use search_customer_knowledge
388
+ - Use open_deal_room before any deal conversation, not get_customer_context
389
+
390
+ ## Data Directory
391
+ ${dataDir}`.trim();
392
+ }
393
+ /** OpenClaw / Hermes: SOUL.md */
394
+ function buildSoulMd(framework) {
395
+ return `# Identity
396
+ I am a CRM-integrated AI assistant powered by DatasynxOpenCRM v2 (${TOOL_COUNT} MCP tools).
397
+ My purpose is to help manage customer relationships proactively — acting before being asked.
398
+
399
+ # Core Behaviors
400
+ - **Proactive first.** At session start I call \`get_proactive_briefing()\` without being asked.
401
+ - **Context before action.** Before any customer discussion: \`open_deal_room()\` or \`get_customer_context()\`.
402
+ - **Log everything.** Every interaction — calls, emails, meetings, Slack — goes into the CRM via \`log_interaction()\`.
403
+ - **Cite sources.** All customer information is cited: gmail://, file://, transcript://.
404
+ - **Glass-box reasoning.** When using \`run_deal_agent()\`, I surface the trace so the user can inspect my reasoning.
405
+
406
+ # Tool Hierarchy
407
+ 1. \`open_deal_room()\` — for deal conversations (combines 7 sub-tools)
408
+ 2. \`get_proactive_briefing()\` — for morning / session start
409
+ 3. \`get_customer_context()\` — for general customer questions
410
+ 4. \`run_deal_agent()\` — when a deal needs AI-driven analysis
411
+
412
+ # Boundaries
413
+ - I do not invent customer data. I search or sync first.
414
+ - I do not skip logging — even quick Slack messages deserve a \`log_interaction()\`.
415
+ - I do not act autonomously beyond "suggest" level without explicit human approval via \`approve_agent_action()\`.
416
+ - I do not discuss a customer without context loaded.
417
+
418
+ # Communication
419
+ Direct. Action-oriented. Lead with the most important insight.
420
+ Bullet points for next steps. End every deal summary with: "What do you want to do first?"
421
+
422
+ # Framework
423
+ ${framework === "openclaw" ? "OpenClaw — tool prefix: datasynx_opencrm:" : "Hermes — skill: datasynx-crm"}`.trim();
424
+ }
425
+ /** Hermes SOUL.md is same as OpenClaw */
426
+ const buildHermesSoulMd = buildSoulMd;
427
+ /** Codex / OpenClaw / Antigravity: AGENTS.md in dataDir */
428
+ function buildAgentsMd(dataDir) {
429
+ return `# DatasynxOpenCRM v2 — Agent Instructions (${TOOL_COUNT} MCP Tools)
430
+
431
+ ## Role
432
+ You are a proactive CRM AI assistant. You act before being asked.
433
+ At every session start: call \`get_proactive_briefing()\`.
434
+
435
+ ## Priority Tool Order
436
+ 1. \`open_deal_room(slug, dealName)\` — before any deal conversation
437
+ 2. \`get_proactive_briefing()\` — at session start, or when asked "what should I do?"
438
+ 3. \`get_customer_context(slug)\` — for general customer questions
439
+ 4. \`run_deal_agent(slug, dealName, "suggest")\` — when a deal is stalled
440
+
441
+ ## Core Workflow
442
+ - Customer mentioned → \`get_customer_context(slug)\` immediately
443
+ - Deal conversation → \`open_deal_room(slug, dealName)\` first
444
+ - After interaction → \`log_interaction(slug, type, summary, nextSteps)\`
445
+ - Deal stage change → \`update_deal(slug, dealName, { stage, probability })\`
446
+ - Revenue goal → \`pursue_goal(goal, deadline)\`
447
+
448
+ ## Available Tools (${TOOL_COUNT})
449
+ **Foundation:** get_capabilities · get_active_session · get_customer_context ·
450
+ search_customer_knowledge · list_customers · log_interaction · update_deal ·
451
+ export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
452
+ summarize_meeting · get_pipeline_stages · get_market_intelligence
453
+
454
+ **Graph & Health (D11/D12):** get_relationship_graph · get_relationship_health
455
+
456
+ **Autonomous Agent (D13):** run_deal_agent · approve_agent_action
457
+
458
+ **Revenue (D14/D18):** simulate_revenue · get_org_intelligence
459
+
460
+ **Playbooks (D15):** get_playbook · create_playbook · list_playbooks · distill_playbook
461
+
462
+ **Goals (D16):** pursue_goal · get_goal_status
463
+
464
+ **Push (D17):** register_push_subscription · get_push_status
465
+
466
+ **Synthesis (D19/D20):** open_deal_room · get_proactive_briefing
467
+
468
+ **Email Templates (H2):** list_email_templates · get_email_template · draft_email
469
+
470
+ **Email Sequences (H1):** enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences
471
+
472
+ **Quotes (H4):** generate_quote · get_quote_status
473
+
474
+ **Calendly (H3):** get_booking_link
475
+
476
+ **Tickets (H6):** create_ticket · update_ticket · list_tickets · close_ticket
477
+
478
+ **NPS/CSAT (H7):** send_nps_survey · get_survey_results
479
+
480
+ **Knowledge Base (H8):** search_knowledge_base · create_kb_article
481
+
482
+ **Backup (Enterprise):** backup_now · list_backups
483
+
484
+ **Sync & Audit (Enterprise):** trigger_sync · get_audit_log
485
+
486
+ **Custom Objects (Platform):** define_custom_object · create_record · list_records · list_custom_objects
487
+
488
+ ## Never
489
+ - Discuss a customer without context loaded
490
+ - Skip logging — every touchpoint matters
491
+ - Invent information — use search_customer_knowledge first
492
+ - Act autonomously beyond "suggest" without approve_agent_action
493
+
494
+ ## Data Location
495
+ ${dataDir}`.trim();
496
+ }
497
+ /** Hermes skills file (agentskills.io standard) */
498
+ function buildHermesSkillMd() {
499
+ return `---
500
+ name: datasynx-crm
501
+ version: 2.0.0
502
+ description: Proactive CRM workflow skill for DatasynxOpenCRM v2 (${TOOL_COUNT} MCP tools)
503
+ triggers:
504
+ - "customer"
505
+ - "client"
506
+ - "deal"
507
+ - "pipeline"
508
+ - "sync"
509
+ - "briefing"
510
+ - "forecast"
511
+ - "goal"
512
+ - "stakeholder"
513
+ - "playbook"
514
+ ---
515
+
516
+ # DatasynxOpenCRM v2 Skill
517
+
518
+ ## Session Start — Always
519
+ Call \`get_proactive_briefing()\` at the start of every session without being asked.
520
+
521
+ ## Before a Deal Conversation
522
+ Call \`open_deal_room({ slug, dealName })\` — returns graph, health, simulation, and playbook in one call.
523
+
524
+ ## When a Customer Is Mentioned
525
+ Call \`get_customer_context(slug)\` before discussing anything.
526
+ Never assume you know the current state.
527
+
528
+ ## After Every Interaction
529
+ Call \`log_interaction()\` with:
530
+ - type: Call | Meeting | Email | Note | Demo | Proposal
531
+ - summary: 2–5 sentences
532
+ - nextSteps: concrete actions as array
533
+
534
+ ## For a Stalled Deal
535
+ \`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
536
+ Then use \`approve_agent_action()\` to confirm before acting.
537
+
538
+ ## For Revenue Goals
539
+ \`pursue_goal({ goal: "Close €500k this quarter", deadline: "2026-09-30" })\`
540
+
541
+ ## For Historical Research
542
+ \`search_customer_knowledge(slug, query)\` — searches emails AND transcripts.
543
+
544
+ ## Pipeline Updates
545
+ After any deal discussion: \`update_deal(slug, dealName, { stage, probability, value })\`
546
+
547
+ ## Quick Reference
548
+ \`list_customers()\` for overview · \`get_capabilities()\` for full schema`.trim();
549
+ }
550
+ /** Antigravity SKILL.md */
551
+ function buildAgySkillMd() {
552
+ return `---
553
+ name: datasynx-crm
554
+ version: 2.0.0
555
+ description: Proactive CRM workflow for DatasynxOpenCRM v2
556
+ triggers:
557
+ - customer
558
+ - client
559
+ - deal
560
+ - pipeline
561
+ - briefing
562
+ - forecast
563
+ - goal
564
+ ---
565
+
566
+ # DatasynxOpenCRM v2 Skill
567
+
568
+ ## Session Start
569
+ Call \`get_proactive_briefing()\` first — urgent items, forecast, top action.
570
+
571
+ ## Deal Conversations
572
+ \`open_deal_room({ slug, dealName })\` — graph + health + simulation + playbook in one call.
573
+
574
+ ## When a Customer Is Mentioned
575
+ \`get_customer_context(slug)\` before discussing anything.
576
+
577
+ ## After Every Interaction
578
+ \`log_interaction(slug, type, summary, nextSteps)\`
579
+
580
+ ## Stalled Deal
581
+ \`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
582
+
583
+ ## Revenue Goal
584
+ \`pursue_goal({ goal, deadline })\`
585
+
586
+ ## Historical Research
587
+ \`search_customer_knowledge(slug, query)\`
588
+
589
+ ## Pipeline
590
+ \`update_deal(slug, dealName, { stage, value, probability })\`
591
+
592
+ ## Overview
593
+ \`list_customers()\` for all customers · \`get_capabilities()\` for full reference`.trim();
594
+ }
595
+ /** Antigravity: global GEMINI.md (~/.gemini/GEMINI.md) — token budget: max 50 lines */
596
+ function buildAgyGeminiMd(dataDir) {
597
+ return `# DatasynxOpenCRM v2 — Agent Context (${TOOL_COUNT} Tools)
598
+
599
+ You have access to a local CRM via MCP tools (server: datasynx-opencrm).
600
+
601
+ ## Session Start — Always Do This First
602
+ \`get_proactive_briefing()\` — urgent items, opportunities, forecast
603
+
604
+ ## Priority Tool Order
605
+ 1. \`open_deal_room(slug, dealName)\` — before deal conversations
606
+ 2. \`get_proactive_briefing()\` — at session start or "what should I do?"
607
+ 3. \`get_customer_context(slug)\` — for general customer questions
608
+
609
+ ## Core Workflow
610
+ - Customer mentioned → \`get_customer_context(slug)\`
611
+ - After interaction → \`log_interaction(slug, type, summary)\`
612
+ - Deal change → \`update_deal(slug, dealName, fields)\`
613
+ - Historical → \`search_customer_knowledge(slug, query)\`
614
+ - Revenue goal → \`pursue_goal(goal, deadline)\`
615
+
616
+ ## Key v2 Tools
617
+ get_proactive_briefing · open_deal_room · get_relationship_graph ·
618
+ get_relationship_health · run_deal_agent · simulate_revenue ·
619
+ get_org_intelligence · pursue_goal · get_goal_status · get_playbook
620
+
621
+ ## All Tools
622
+ get_capabilities · get_active_session · get_customer_context ·
623
+ search_customer_knowledge · list_customers · log_interaction · update_deal ·
624
+ export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
625
+ summarize_meeting · get_pipeline_stages · get_market_intelligence ·
626
+ get_relationship_graph · get_relationship_health · run_deal_agent ·
627
+ approve_agent_action · simulate_revenue · get_org_intelligence ·
628
+ get_playbook · create_playbook · list_playbooks · distill_playbook ·
629
+ pursue_goal · get_goal_status · register_push_subscription · get_push_status ·
630
+ open_deal_room · get_proactive_briefing ·
631
+ list_email_templates · get_email_template · draft_email ·
632
+ enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences ·
633
+ generate_quote · get_quote_status · get_booking_link ·
634
+ create_ticket · update_ticket · list_tickets · close_ticket ·
635
+ send_nps_survey · get_survey_results ·
636
+ search_knowledge_base · create_kb_article ·
637
+ backup_now · list_backups ·
638
+ trigger_sync · get_audit_log ·
639
+ define_custom_object · create_record · list_records · list_custom_objects
640
+
641
+ ## Data: ${dataDir}`.trim();
642
+ }
643
+ /** Grok Build: .grok/settings.json — project-level MCP config (array format) */
644
+ function buildGrokSettingsJson(config) {
645
+ const entry = { mcpServers: [{
646
+ name: config.serverName,
647
+ transport: {
648
+ type: "stdio",
649
+ command: "node",
650
+ args: [config.mcpServerPath],
651
+ env: { DXCRM_DATA_DIR: config.dataDir }
652
+ }
653
+ }] };
654
+ return JSON.stringify(entry, null, 2);
655
+ }
656
+ /** Cursor: .cursor/rules/datasynx-crm.mdc (MDC format with frontmatter) */
657
+ function buildCursorRulesMdc(dataDir) {
658
+ return `---
659
+ description: DatasynxOpenCRM v2 — CRM workflow rules (${TOOL_COUNT} tools)
660
+ globs: ["**/*"]
661
+ alwaysApply: true
662
+ ---
663
+
664
+ # DatasynxOpenCRM v2 Rules
665
+
666
+ You have access to a local CRM via MCP tools (datasynx-opencrm, ${TOOL_COUNT} tools).
667
+
668
+ ## Session Start
669
+ Call \`get_proactive_briefing()\` at the start of every session.
670
+
671
+ ## Deal Conversations
672
+ Call \`open_deal_room({ slug, dealName })\` before any deal discussion.
673
+
674
+ ## Mandatory Workflow
675
+ - Customer mentioned → \`get_customer_context(slug)\` immediately
676
+ - After call/meeting/email → \`log_interaction(slug, type, summary)\`
677
+ - Historical question → \`search_customer_knowledge(slug, query)\`
678
+ - Deal discussed → \`update_deal(slug, dealName, fields)\`
679
+ - Revenue goal → \`pursue_goal({ goal, deadline })\`
680
+
681
+ ## Key v2 Tools
682
+ open_deal_room · get_proactive_briefing · get_relationship_graph ·
683
+ get_relationship_health · run_deal_agent · approve_agent_action ·
684
+ simulate_revenue · get_org_intelligence · get_playbook · pursue_goal
685
+
686
+ ## All ${TOOL_COUNT} Tools
687
+ get_capabilities · get_active_session · get_customer_context ·
688
+ search_customer_knowledge · list_customers · log_interaction · update_deal ·
689
+ export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
690
+ summarize_meeting · get_pipeline_stages · get_market_intelligence ·
691
+ get_relationship_graph · get_relationship_health · run_deal_agent ·
692
+ approve_agent_action · simulate_revenue · get_org_intelligence ·
693
+ get_playbook · create_playbook · list_playbooks · distill_playbook ·
694
+ pursue_goal · get_goal_status · register_push_subscription · get_push_status ·
695
+ open_deal_room · get_proactive_briefing ·
696
+ list_email_templates · get_email_template · draft_email ·
697
+ enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences ·
698
+ generate_quote · get_quote_status · get_booking_link ·
699
+ create_ticket · update_ticket · list_tickets · close_ticket ·
700
+ send_nps_survey · get_survey_results ·
701
+ search_knowledge_base · create_kb_article
702
+
703
+ ## Never
704
+ - Discuss a customer without loading context first
705
+ - Skip logging — every touchpoint matters
706
+ - Invent information — sync or search first
707
+
708
+ ## Data: ${dataDir}`.trim();
709
+ }
710
+ //#endregion
711
+ //#region src/setup/adapters/claude-code.ts
712
+ const HOME$7 = os.homedir();
713
+ const CLAUDE_JSON = path.join(HOME$7, ".claude.json");
714
+ const CLAUDE_DIR = path.join(HOME$7, ".claude");
715
+ var ClaudeCodeAdapter = class {
716
+ name = "Claude Code";
717
+ detect() {
718
+ try {
719
+ execSync("which claude", { stdio: "ignore" });
720
+ return true;
721
+ } catch {}
722
+ return fs.existsSync(CLAUDE_JSON) || fs.existsSync(CLAUDE_DIR);
723
+ }
724
+ isInstalled() {
725
+ if (!fs.existsSync(CLAUDE_JSON)) return false;
726
+ try {
727
+ return !!JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
728
+ } catch {
729
+ return false;
730
+ }
731
+ }
732
+ async install(config) {
733
+ const harnessFiles = [];
734
+ this.writeMcpConfig(config);
735
+ this.writeGlobalSettings();
736
+ const claudeMdPath = path.join(config.dataDir, "CLAUDE.md");
737
+ fs.writeFileSync(claudeMdPath, buildClaudeMd(config.dataDir));
738
+ harnessFiles.push(claudeMdPath);
739
+ const projectSettingsDir = path.join(config.dataDir, ".claude");
740
+ fs.mkdirSync(projectSettingsDir, { recursive: true });
741
+ const allow = [
742
+ "mcp__datasynx-opencrm__get_capabilities",
743
+ "mcp__datasynx-opencrm__get_active_session",
744
+ "mcp__datasynx-opencrm__get_customer_context",
745
+ "mcp__datasynx-opencrm__search_customer_knowledge",
746
+ "mcp__datasynx-opencrm__list_customers",
747
+ "mcp__datasynx-opencrm__log_interaction",
748
+ "mcp__datasynx-opencrm__update_deal",
749
+ "mcp__datasynx-opencrm__export_customer"
750
+ ];
751
+ const projectSettings = { permissions: { allow } };
752
+ const settingsPath = path.join(projectSettingsDir, "settings.json");
753
+ fs.writeFileSync(settingsPath, JSON.stringify(projectSettings, null, 2));
754
+ harnessFiles.push(settingsPath);
755
+ return {
756
+ framework: this.name,
757
+ success: true,
758
+ transport: "stdio",
759
+ configPath: CLAUDE_JSON,
760
+ harnessFiles,
761
+ notes: `${allow.length} of ${TOOL_COUNT} MCP tools pre-allowed (common read/write); CLAUDE.md written to CRM root.`
762
+ };
763
+ }
764
+ async uninstall() {
765
+ if (!fs.existsSync(CLAUDE_JSON)) return;
766
+ try {
767
+ const json = JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"));
768
+ const servers = json["mcpServers"];
769
+ if (servers) delete servers["datasynx-opencrm"];
770
+ fs.writeFileSync(CLAUDE_JSON, JSON.stringify(json, null, 2));
771
+ } catch {}
772
+ }
773
+ writeMcpConfig(config) {
774
+ let json = {};
775
+ if (fs.existsSync(CLAUDE_JSON)) try {
776
+ json = JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"));
777
+ } catch {}
778
+ if (!json["mcpServers"]) json["mcpServers"] = {};
779
+ json["mcpServers"][config.serverName] = {
780
+ type: "stdio",
781
+ command: process.execPath,
782
+ args: [config.mcpServerPath],
783
+ env: { DXCRM_DATA_DIR: config.dataDir }
784
+ };
785
+ fs.mkdirSync(path.dirname(CLAUDE_JSON), { recursive: true });
786
+ fs.writeFileSync(CLAUDE_JSON, JSON.stringify(json, null, 2));
787
+ }
788
+ writeGlobalSettings() {
789
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
790
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
791
+ let settings = {};
792
+ if (fs.existsSync(settingsPath)) try {
793
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
794
+ } catch {}
795
+ if (!settings["permissions"]) settings["permissions"] = {};
796
+ const perms = settings["permissions"];
797
+ if (!perms["allow"]) perms["allow"] = [];
798
+ const allow = perms["allow"];
799
+ const wildcard = "mcp__datasynx-opencrm__*";
800
+ if (!allow.includes(wildcard)) allow.push(wildcard);
801
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
802
+ }
803
+ };
804
+ //#endregion
805
+ //#region src/setup/adapters/claude-desktop.ts
806
+ function getDesktopConfigPath() {
807
+ switch (process.platform) {
808
+ case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
809
+ case "win32": return path.join(process.env["APPDATA"] ?? os.homedir(), "Claude", "claude_desktop_config.json");
810
+ default: return path.join(os.homedir(), ".config", "claude-desktop", "claude_desktop_config.json");
811
+ }
812
+ }
813
+ const DESKTOP_CONFIG = getDesktopConfigPath();
814
+ var ClaudeDesktopAdapter = class {
815
+ name = "Claude Desktop";
816
+ detect() {
817
+ return fs.existsSync(DESKTOP_CONFIG) || fs.existsSync(path.dirname(DESKTOP_CONFIG));
818
+ }
819
+ isInstalled() {
820
+ if (!fs.existsSync(DESKTOP_CONFIG)) return false;
821
+ try {
822
+ return !!JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
823
+ } catch {
824
+ return false;
825
+ }
826
+ }
827
+ async install(config) {
828
+ fs.mkdirSync(path.dirname(DESKTOP_CONFIG), { recursive: true });
829
+ let json = {};
830
+ if (fs.existsSync(DESKTOP_CONFIG)) try {
831
+ json = JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"));
832
+ } catch {}
833
+ if (!json["mcpServers"]) json["mcpServers"] = {};
834
+ json["mcpServers"][config.serverName] = {
835
+ command: process.execPath,
836
+ args: [config.mcpServerPath],
837
+ env: { DXCRM_DATA_DIR: config.dataDir }
838
+ };
839
+ fs.writeFileSync(DESKTOP_CONFIG, JSON.stringify(json, null, 2));
840
+ return {
841
+ framework: this.name,
842
+ success: true,
843
+ transport: "stdio",
844
+ configPath: DESKTOP_CONFIG,
845
+ harnessFiles: [],
846
+ notes: "Restart Claude Desktop to activate the MCP server. No harness files for Desktop app."
847
+ };
848
+ }
849
+ async uninstall() {
850
+ if (!fs.existsSync(DESKTOP_CONFIG)) return;
851
+ try {
852
+ const json = JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"));
853
+ const servers = json["mcpServers"];
854
+ if (servers) delete servers["datasynx-opencrm"];
855
+ fs.writeFileSync(DESKTOP_CONFIG, JSON.stringify(json, null, 2));
856
+ } catch {}
857
+ }
858
+ };
859
+ //#endregion
860
+ //#region src/setup/adapters/codex.ts
861
+ const CODEX_DIR = path.join(os.homedir(), ".codex");
862
+ const CODEX_CONFIG = path.join(CODEX_DIR, "config.toml");
863
+ var CodexAdapter = class {
864
+ name = "Codex";
865
+ detect() {
866
+ try {
867
+ execSync("which codex", { stdio: "ignore" });
868
+ return true;
869
+ } catch {}
870
+ return fs.existsSync(CODEX_DIR);
871
+ }
872
+ isInstalled() {
873
+ if (!fs.existsSync(CODEX_CONFIG)) return false;
874
+ return fs.readFileSync(CODEX_CONFIG, "utf-8").includes("[mcp_servers.datasynx-opencrm]");
875
+ }
876
+ async install(config) {
877
+ fs.mkdirSync(CODEX_DIR, { recursive: true });
878
+ const harnessFiles = [];
879
+ if (!this.isInstalled()) {
880
+ const block = [
881
+ ``,
882
+ `[mcp_servers.${config.serverName}]`,
883
+ `command = ${JSON.stringify(process.execPath)}`,
884
+ `args = [${JSON.stringify(config.mcpServerPath)}]`,
885
+ `env = { DXCRM_DATA_DIR = ${JSON.stringify(config.dataDir)} }`,
886
+ `startup_timeout_sec = 30`,
887
+ `tool_timeout_sec = 120`,
888
+ `enabled = true`,
889
+ ``
890
+ ].join("\n");
891
+ fs.appendFileSync(CODEX_CONFIG, block, "utf-8");
892
+ }
893
+ const agentsPath = path.join(config.dataDir, "AGENTS.md");
894
+ if (!fs.existsSync(agentsPath)) {
895
+ fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
896
+ harnessFiles.push(agentsPath);
897
+ } else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
898
+ fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
899
+ harnessFiles.push(agentsPath + " (appended)");
900
+ }
901
+ return {
902
+ framework: this.name,
903
+ success: true,
904
+ transport: "stdio",
905
+ configPath: CODEX_CONFIG,
906
+ harnessFiles,
907
+ notes: `startup_timeout_sec=30, tool_timeout_sec=120. AGENTS.md written to CRM root.`
908
+ };
909
+ }
910
+ async uninstall() {
911
+ if (!fs.existsSync(CODEX_CONFIG)) return;
912
+ const cleaned = fs.readFileSync(CODEX_CONFIG, "utf-8").replace(/\n?\[mcp_servers\.datasynx-opencrm\][^\[]*/, "");
913
+ fs.writeFileSync(CODEX_CONFIG, cleaned);
914
+ }
915
+ };
916
+ //#endregion
917
+ //#region src/setup/adapters/openclaw.ts
918
+ const HOME$6 = os.homedir();
919
+ const OPENCLAW_DIR = path.join(HOME$6, ".openclaw");
920
+ const OPENCLAW_JSON = path.join(OPENCLAW_DIR, "openclaw.json");
921
+ const OPENCLAW_WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
922
+ var OpenClawAdapter = class {
923
+ name = "OpenClaw";
924
+ detect() {
925
+ try {
926
+ execSync("which openclaw", { stdio: "ignore" });
927
+ return true;
928
+ } catch {}
929
+ return fs.existsSync(OPENCLAW_DIR) || fs.existsSync(OPENCLAW_JSON);
930
+ }
931
+ isInstalled() {
932
+ if (!fs.existsSync(OPENCLAW_JSON)) return false;
933
+ try {
934
+ return !!JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
935
+ } catch {
936
+ return false;
937
+ }
938
+ }
939
+ async install(config) {
940
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
941
+ fs.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
942
+ const harnessFiles = [];
943
+ let json = {};
944
+ if (fs.existsSync(OPENCLAW_JSON)) try {
945
+ json = JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"));
946
+ } catch {}
947
+ if (!json["mcpServers"]) json["mcpServers"] = {};
948
+ const servers = json["mcpServers"];
949
+ servers[config.serverName] = {
950
+ command: process.execPath,
951
+ args: [config.mcpServerPath],
952
+ transport: "stdio",
953
+ env: { DXCRM_DATA_DIR: config.dataDir }
954
+ };
955
+ servers[`${config.serverName}-http`] = {
956
+ url: `http://localhost:${config.httpPort}/mcp`,
957
+ transport: "streamable-http",
958
+ enabled: false
959
+ };
960
+ fs.writeFileSync(OPENCLAW_JSON, JSON.stringify(json, null, 2));
961
+ const soulPath = path.join(OPENCLAW_WORKSPACE, "SOUL.md");
962
+ if (!fs.existsSync(soulPath)) {
963
+ fs.writeFileSync(soulPath, buildSoulMd("openclaw"));
964
+ harnessFiles.push(soulPath);
965
+ } else if (!fs.readFileSync(soulPath, "utf-8").includes("DatasynxOpenCRM")) {
966
+ fs.appendFileSync(soulPath, "\n\n---\n\n" + buildCrmSoulAppend());
967
+ harnessFiles.push(soulPath + " (appended)");
968
+ }
969
+ const agentsPath = path.join(OPENCLAW_WORKSPACE, "AGENTS.md");
970
+ if (!fs.existsSync(agentsPath)) {
971
+ fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
972
+ harnessFiles.push(agentsPath);
973
+ } else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
974
+ fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
975
+ harnessFiles.push(agentsPath + " (appended)");
976
+ }
977
+ const toolsPath = path.join(OPENCLAW_WORKSPACE, "TOOLS.md");
978
+ const toolsContent = buildOpenClawToolsMd();
979
+ if (!fs.existsSync(toolsPath)) fs.writeFileSync(toolsPath, toolsContent);
980
+ else if (!fs.readFileSync(toolsPath, "utf-8").includes("datasynx-opencrm")) fs.appendFileSync(toolsPath, "\n\n" + toolsContent);
981
+ harnessFiles.push(toolsPath);
982
+ return {
983
+ framework: this.name,
984
+ success: true,
985
+ transport: "stdio",
986
+ configPath: OPENCLAW_JSON,
987
+ harnessFiles,
988
+ notes: "Config hot-reloaded by Gateway. SOUL.md + AGENTS.md + TOOLS.md written to workspace."
989
+ };
990
+ }
991
+ async uninstall() {
992
+ if (!fs.existsSync(OPENCLAW_JSON)) return;
993
+ try {
994
+ const json = JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"));
995
+ const servers = json["mcpServers"];
996
+ if (servers) {
997
+ delete servers["datasynx-opencrm"];
998
+ delete servers["datasynx-opencrm-http"];
999
+ }
1000
+ fs.writeFileSync(OPENCLAW_JSON, JSON.stringify(json, null, 2));
1001
+ } catch {}
1002
+ }
1003
+ };
1004
+ function buildCrmSoulAppend() {
1005
+ return `## CRM Integration
1006
+ I have access to DatasynxOpenCRM. I always load customer context before discussing customers.
1007
+ I log every interaction without being asked. I cite sources when referencing customer data.`;
1008
+ }
1009
+ function buildOpenClawToolsMd() {
1010
+ return `## datasynx-opencrm MCP Tools
1011
+ - get_customer_context(slug) — load full customer briefing
1012
+ - search_customer_knowledge(slug, query) — search emails + transcripts
1013
+ - list_customers() — pipeline overview
1014
+ - log_interaction(slug, type, summary) — write to CRM
1015
+ - update_deal(slug, dealName, fields) — pipeline update
1016
+ - get_capabilities() — full reference`;
1017
+ }
1018
+ //#endregion
1019
+ //#region src/setup/adapters/hermes.ts
1020
+ const HOME$5 = os.homedir();
1021
+ const HERMES_HOME = process.env["HERMES_HOME"] ?? path.join(HOME$5, ".hermes");
1022
+ const HERMES_CONFIG = path.join(HERMES_HOME, "config.yaml");
1023
+ const HERMES_SOUL = path.join(HERMES_HOME, "SOUL.md");
1024
+ const HERMES_SKILLS = path.join(HERMES_HOME, "skills");
1025
+ var HermesAdapter = class {
1026
+ name = "Hermes Agent";
1027
+ detect() {
1028
+ try {
1029
+ execSync("which hermes", { stdio: "ignore" });
1030
+ return true;
1031
+ } catch {}
1032
+ return fs.existsSync(HERMES_HOME) || fs.existsSync(HERMES_CONFIG);
1033
+ }
1034
+ isInstalled() {
1035
+ if (!fs.existsSync(HERMES_CONFIG)) return false;
1036
+ return fs.readFileSync(HERMES_CONFIG, "utf-8").includes("datasynx");
1037
+ }
1038
+ async install(config) {
1039
+ fs.mkdirSync(HERMES_HOME, { recursive: true });
1040
+ fs.mkdirSync(HERMES_SKILLS, { recursive: true });
1041
+ const harnessFiles = [];
1042
+ this.writeMcpConfig(config);
1043
+ if (!fs.existsSync(HERMES_SOUL)) {
1044
+ fs.writeFileSync(HERMES_SOUL, buildHermesSoulMd("hermes"));
1045
+ harnessFiles.push(HERMES_SOUL);
1046
+ } else if (!fs.readFileSync(HERMES_SOUL, "utf-8").includes("DatasynxOpenCRM")) {
1047
+ fs.appendFileSync(HERMES_SOUL, "\n\n---\n\n## CRM Integration\nI have access to DatasynxOpenCRM MCP tools.\nI always load customer context before discussing customers.\nI log every interaction automatically via log_interaction().");
1048
+ harnessFiles.push(HERMES_SOUL + " (appended)");
1049
+ }
1050
+ const skillPath = path.join(HERMES_SKILLS, "datasynx-crm.md");
1051
+ fs.writeFileSync(skillPath, buildHermesSkillMd());
1052
+ harnessFiles.push(skillPath);
1053
+ return {
1054
+ framework: this.name,
1055
+ success: true,
1056
+ transport: "stdio",
1057
+ configPath: HERMES_CONFIG,
1058
+ harnessFiles,
1059
+ notes: "SOUL.md updated (Slot #1 system prompt). Skill registered in ~/.hermes/skills/. Server name uses underscore: datasynx_opencrm."
1060
+ };
1061
+ }
1062
+ async uninstall() {
1063
+ if (!fs.existsSync(HERMES_CONFIG)) return;
1064
+ const cleaned = fs.readFileSync(HERMES_CONFIG, "utf-8").replace(/\n datasynx[_-]opencrm:[\s\S]*?(?=\n \w|\n[a-z]|$)/, "");
1065
+ fs.writeFileSync(HERMES_CONFIG, cleaned);
1066
+ const skillPath = path.join(HERMES_SKILLS, "datasynx-crm.md");
1067
+ if (fs.existsSync(skillPath)) fs.unlinkSync(skillPath);
1068
+ }
1069
+ writeMcpConfig(config) {
1070
+ let content = fs.existsSync(HERMES_CONFIG) ? fs.readFileSync(HERMES_CONFIG, "utf-8") : "";
1071
+ if (content.includes("datasynx")) return;
1072
+ if (content.includes("mcp_servers:")) {
1073
+ content = content.replace("mcp_servers:", [
1074
+ "mcp_servers:",
1075
+ " datasynx_opencrm:",
1076
+ ` command: ${JSON.stringify(process.execPath)}`,
1077
+ ` args: [${JSON.stringify(config.mcpServerPath)}]`,
1078
+ ` env:`,
1079
+ ` DXCRM_DATA_DIR: ${JSON.stringify(config.dataDir)}`,
1080
+ ` timeout: 120`,
1081
+ ` connect_timeout: 30`,
1082
+ ` enabled: true`,
1083
+ ` tools:`,
1084
+ ` include: [get_capabilities, get_active_session, get_customer_context, search_customer_knowledge, list_customers, log_interaction, update_deal, export_customer]`,
1085
+ ` prompts: false`,
1086
+ ` resources: false`
1087
+ ].join("\n"));
1088
+ fs.writeFileSync(HERMES_CONFIG, content);
1089
+ } else {
1090
+ const mcpBlock = [
1091
+ ``,
1092
+ `# DatasynxOpenCRM MCP Server (added by dxcrm init)`,
1093
+ `mcp_servers:`,
1094
+ ` datasynx_opencrm:`,
1095
+ ` command: ${JSON.stringify(process.execPath)}`,
1096
+ ` args: [${JSON.stringify(config.mcpServerPath)}]`,
1097
+ ` env:`,
1098
+ ` DXCRM_DATA_DIR: ${JSON.stringify(config.dataDir)}`,
1099
+ ` timeout: 120`,
1100
+ ` connect_timeout: 30`,
1101
+ ` enabled: true`,
1102
+ ` tools:`,
1103
+ ` include:`,
1104
+ ` - get_capabilities`,
1105
+ ` - get_active_session`,
1106
+ ` - get_customer_context`,
1107
+ ` - search_customer_knowledge`,
1108
+ ` - list_customers`,
1109
+ ` - log_interaction`,
1110
+ ` - update_deal`,
1111
+ ` - export_customer`,
1112
+ ` prompts: false`,
1113
+ ` resources: false`,
1114
+ ``
1115
+ ].join("\n");
1116
+ fs.appendFileSync(HERMES_CONFIG, mcpBlock);
1117
+ }
1118
+ }
1119
+ };
1120
+ //#endregion
1121
+ //#region src/setup/adapters/antigravity.ts
1122
+ const HOME$4 = os.homedir();
1123
+ const AGY_BIN_UNIX = path.join(HOME$4, ".local", "bin", "agy");
1124
+ const GEMINI_CONFIG_DIR = path.join(HOME$4, ".gemini", "config");
1125
+ const SHARED_MCP_CONFIG = path.join(GEMINI_CONFIG_DIR, "mcp_config.json");
1126
+ const AGY_DIR = path.join(HOME$4, ".gemini", "antigravity");
1127
+ const AGY_MCP_CONFIG = path.join(AGY_DIR, "mcp_config.json");
1128
+ const GEMINI_GLOBAL_MD = path.join(HOME$4, ".gemini", "GEMINI.md");
1129
+ const AGY_SKILLS_DIR = path.join(HOME$4, ".gemini", "antigravity-cli", "skills");
1130
+ var AntigravityAdapter = class {
1131
+ name = "Antigravity CLI";
1132
+ detect() {
1133
+ try {
1134
+ execSync("which agy", { stdio: "ignore" });
1135
+ return true;
1136
+ } catch {}
1137
+ if (fs.existsSync(AGY_BIN_UNIX)) return true;
1138
+ return fs.existsSync(path.join(HOME$4, ".gemini"));
1139
+ }
1140
+ isInstalled() {
1141
+ for (const configPath of [SHARED_MCP_CONFIG, AGY_MCP_CONFIG]) {
1142
+ if (!fs.existsSync(configPath)) continue;
1143
+ try {
1144
+ if (JSON.parse(fs.readFileSync(configPath, "utf-8"))?.mcpServers?.["datasynx-opencrm"]) return true;
1145
+ } catch {}
1146
+ }
1147
+ return false;
1148
+ }
1149
+ async install(config) {
1150
+ const harnessFiles = [];
1151
+ fs.mkdirSync(GEMINI_CONFIG_DIR, { recursive: true });
1152
+ this.writeMcpEntry(SHARED_MCP_CONFIG, config);
1153
+ if (!fs.existsSync(GEMINI_GLOBAL_MD)) {
1154
+ fs.mkdirSync(path.dirname(GEMINI_GLOBAL_MD), { recursive: true });
1155
+ fs.writeFileSync(GEMINI_GLOBAL_MD, buildAgyGeminiMd(config.dataDir));
1156
+ harnessFiles.push(GEMINI_GLOBAL_MD);
1157
+ } else if (!fs.readFileSync(GEMINI_GLOBAL_MD, "utf-8").includes("DatasynxOpenCRM")) {
1158
+ fs.appendFileSync(GEMINI_GLOBAL_MD, "\n\n---\n\n" + buildAgyGeminiMdAppend());
1159
+ harnessFiles.push(GEMINI_GLOBAL_MD + " (appended)");
1160
+ }
1161
+ const agentsPath = path.join(config.dataDir, "AGENTS.md");
1162
+ fs.mkdirSync(config.dataDir, { recursive: true });
1163
+ if (!fs.existsSync(agentsPath)) {
1164
+ fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
1165
+ harnessFiles.push(agentsPath);
1166
+ } else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
1167
+ fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
1168
+ harnessFiles.push(agentsPath + " (appended)");
1169
+ }
1170
+ const skillDir = path.join(AGY_SKILLS_DIR, "datasynx-crm");
1171
+ fs.mkdirSync(skillDir, { recursive: true });
1172
+ const skillPath = path.join(skillDir, "SKILL.md");
1173
+ fs.writeFileSync(skillPath, buildAgySkillMd());
1174
+ harnessFiles.push(skillPath);
1175
+ return {
1176
+ framework: this.name,
1177
+ success: true,
1178
+ transport: "stdio",
1179
+ configPath: SHARED_MCP_CONFIG,
1180
+ harnessFiles,
1181
+ notes: "Shared config (~/.gemini/config/mcp_config.json) covers both CLI and IDE. Skill registered. GEMINI.md updated."
1182
+ };
1183
+ }
1184
+ async uninstall() {
1185
+ for (const configPath of [SHARED_MCP_CONFIG, AGY_MCP_CONFIG]) {
1186
+ if (!fs.existsSync(configPath)) continue;
1187
+ try {
1188
+ const json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1189
+ if (json.mcpServers) {
1190
+ delete json.mcpServers["datasynx-opencrm"];
1191
+ delete json.mcpServers["datasynx-opencrm-http"];
1192
+ }
1193
+ fs.writeFileSync(configPath, JSON.stringify(json, null, 2));
1194
+ } catch {}
1195
+ }
1196
+ const skillDir = path.join(AGY_SKILLS_DIR, "datasynx-crm");
1197
+ if (fs.existsSync(skillDir)) fs.rmSync(skillDir, { recursive: true });
1198
+ }
1199
+ writeMcpEntry(configPath, config) {
1200
+ let json = { mcpServers: {} };
1201
+ if (fs.existsSync(configPath)) try {
1202
+ json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1203
+ if (!json.mcpServers) json.mcpServers = {};
1204
+ } catch {}
1205
+ json.mcpServers["datasynx-opencrm"] = {
1206
+ command: process.execPath,
1207
+ args: [config.mcpServerPath],
1208
+ env: { DXCRM_DATA_DIR: config.dataDir }
1209
+ };
1210
+ json.mcpServers["datasynx-opencrm-http"] = { serverUrl: `http://localhost:${config.httpPort}/mcp` };
1211
+ fs.writeFileSync(configPath, JSON.stringify(json, null, 2));
1212
+ }
1213
+ };
1214
+ function buildAgyGeminiMdAppend() {
1215
+ return `## DatasynxOpenCRM
1216
+ CRM MCP tools available: get_customer_context, search_customer_knowledge,
1217
+ list_customers, log_interaction, update_deal. Always load context first.`;
1218
+ }
1219
+ //#endregion
1220
+ //#region src/setup/adapters/cursor.ts
1221
+ const HOME$3 = os.homedir();
1222
+ const CURSOR_DIR = path.join(HOME$3, ".cursor");
1223
+ const CURSOR_GLOBAL_MCP = path.join(CURSOR_DIR, "mcp.json");
1224
+ var CursorAdapter = class {
1225
+ name = "Cursor";
1226
+ detect() {
1227
+ return fs.existsSync(CURSOR_DIR) || fs.existsSync(CURSOR_GLOBAL_MCP);
1228
+ }
1229
+ isInstalled() {
1230
+ if (!fs.existsSync(CURSOR_GLOBAL_MCP)) return false;
1231
+ try {
1232
+ return !!JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
1233
+ } catch {
1234
+ return false;
1235
+ }
1236
+ }
1237
+ async install(config) {
1238
+ fs.mkdirSync(CURSOR_DIR, { recursive: true });
1239
+ const harnessFiles = [];
1240
+ let json = { mcpServers: {} };
1241
+ if (fs.existsSync(CURSOR_GLOBAL_MCP)) try {
1242
+ json = JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"));
1243
+ if (!json.mcpServers) json.mcpServers = {};
1244
+ } catch {}
1245
+ json.mcpServers["datasynx-opencrm"] = {
1246
+ command: process.execPath,
1247
+ args: [config.mcpServerPath],
1248
+ env: { DXCRM_DATA_DIR: config.dataDir }
1249
+ };
1250
+ fs.writeFileSync(CURSOR_GLOBAL_MCP, JSON.stringify(json, null, 2));
1251
+ const rulesDir = path.join(config.dataDir, ".cursor", "rules");
1252
+ fs.mkdirSync(rulesDir, { recursive: true });
1253
+ const rulesPath = path.join(rulesDir, "datasynx-crm.mdc");
1254
+ if (!fs.existsSync(rulesPath)) {
1255
+ fs.writeFileSync(rulesPath, buildCursorRulesMdc(config.dataDir));
1256
+ harnessFiles.push(rulesPath);
1257
+ }
1258
+ return {
1259
+ framework: this.name,
1260
+ success: true,
1261
+ transport: "stdio",
1262
+ configPath: CURSOR_GLOBAL_MCP,
1263
+ harnessFiles,
1264
+ notes: "Global MCP registered. CRM rules written to .cursor/rules/. Restart Cursor to activate."
1265
+ };
1266
+ }
1267
+ async uninstall() {
1268
+ if (!fs.existsSync(CURSOR_GLOBAL_MCP)) return;
1269
+ try {
1270
+ const json = JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"));
1271
+ if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
1272
+ fs.writeFileSync(CURSOR_GLOBAL_MCP, JSON.stringify(json, null, 2));
1273
+ } catch {}
1274
+ }
1275
+ };
1276
+ //#endregion
1277
+ //#region src/setup/adapters/windsurf.ts
1278
+ const HOME$2 = os.homedir();
1279
+ const WINDSURF_DIR = path.join(HOME$2, ".codeium", "windsurf");
1280
+ const WINDSURF_CONFIG = path.join(WINDSURF_DIR, "mcp_config.json");
1281
+ var WindsurfAdapter = class {
1282
+ name = "Windsurf";
1283
+ detect() {
1284
+ return fs.existsSync(WINDSURF_DIR) || fs.existsSync(WINDSURF_CONFIG);
1285
+ }
1286
+ isInstalled() {
1287
+ if (!fs.existsSync(WINDSURF_CONFIG)) return false;
1288
+ try {
1289
+ return !!JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
1290
+ } catch {
1291
+ return false;
1292
+ }
1293
+ }
1294
+ async install(config) {
1295
+ fs.mkdirSync(WINDSURF_DIR, { recursive: true });
1296
+ let json = { mcpServers: {} };
1297
+ if (fs.existsSync(WINDSURF_CONFIG)) try {
1298
+ json = JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"));
1299
+ if (!json.mcpServers) json.mcpServers = {};
1300
+ } catch {}
1301
+ json.mcpServers["datasynx-opencrm"] = {
1302
+ command: process.execPath,
1303
+ args: [config.mcpServerPath],
1304
+ env: { DXCRM_DATA_DIR: config.dataDir }
1305
+ };
1306
+ fs.writeFileSync(WINDSURF_CONFIG, JSON.stringify(json, null, 2));
1307
+ return {
1308
+ framework: this.name,
1309
+ success: true,
1310
+ transport: "stdio",
1311
+ configPath: WINDSURF_CONFIG,
1312
+ harnessFiles: [],
1313
+ notes: "No harness files for IDE-based tools. Restart Windsurf to activate."
1314
+ };
1315
+ }
1316
+ async uninstall() {
1317
+ if (!fs.existsSync(WINDSURF_CONFIG)) return;
1318
+ try {
1319
+ const json = JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"));
1320
+ if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
1321
+ fs.writeFileSync(WINDSURF_CONFIG, JSON.stringify(json, null, 2));
1322
+ } catch {}
1323
+ }
1324
+ };
1325
+ //#endregion
1326
+ //#region src/setup/adapters/cline.ts
1327
+ const HOME$1 = os.homedir();
1328
+ const CLINE_DIR = path.join(HOME$1, ".cline");
1329
+ const CLINE_CONFIG = path.join(CLINE_DIR, "data", "settings", "cline_mcp_settings.json");
1330
+ var ClineAdapter = class {
1331
+ name = "Cline";
1332
+ detect() {
1333
+ return fs.existsSync(CLINE_DIR) || fs.existsSync(CLINE_CONFIG);
1334
+ }
1335
+ isInstalled() {
1336
+ if (!fs.existsSync(CLINE_CONFIG)) return false;
1337
+ try {
1338
+ return !!JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1343
+ async install(config) {
1344
+ fs.mkdirSync(path.dirname(CLINE_CONFIG), { recursive: true });
1345
+ let json = { mcpServers: {} };
1346
+ if (fs.existsSync(CLINE_CONFIG)) try {
1347
+ json = JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"));
1348
+ if (!json.mcpServers) json.mcpServers = {};
1349
+ } catch {}
1350
+ json.mcpServers["datasynx-opencrm"] = {
1351
+ command: process.execPath,
1352
+ args: [config.mcpServerPath],
1353
+ env: { DXCRM_DATA_DIR: config.dataDir }
1354
+ };
1355
+ fs.writeFileSync(CLINE_CONFIG, JSON.stringify(json, null, 2));
1356
+ return {
1357
+ framework: this.name,
1358
+ success: true,
1359
+ transport: "stdio",
1360
+ configPath: CLINE_CONFIG,
1361
+ harnessFiles: [],
1362
+ notes: "Cline requires absolute paths. No harness files for VSCode extensions."
1363
+ };
1364
+ }
1365
+ async uninstall() {
1366
+ if (!fs.existsSync(CLINE_CONFIG)) return;
1367
+ try {
1368
+ const json = JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"));
1369
+ if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
1370
+ fs.writeFileSync(CLINE_CONFIG, JSON.stringify(json, null, 2));
1371
+ } catch {}
1372
+ }
1373
+ };
1374
+ //#endregion
1375
+ //#region src/setup/adapters/grok.ts
1376
+ const HOME = os.homedir();
1377
+ const GROK_DIR = path.join(HOME, ".grok");
1378
+ const GROK_USER_SETTINGS = path.join(GROK_DIR, "user-settings.json");
1379
+ var GrokAdapter = class {
1380
+ name = "Grok Build";
1381
+ detect() {
1382
+ try {
1383
+ execSync("which grok", { stdio: "ignore" });
1384
+ return true;
1385
+ } catch {}
1386
+ return fs.existsSync(GROK_DIR);
1387
+ }
1388
+ isInstalled() {
1389
+ if (!fs.existsSync(GROK_USER_SETTINGS)) return false;
1390
+ try {
1391
+ return (JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8")).mcpServers ?? []).some((s) => s.name === "datasynx-opencrm");
1392
+ } catch {
1393
+ return false;
1394
+ }
1395
+ }
1396
+ async install(config) {
1397
+ fs.mkdirSync(GROK_DIR, { recursive: true });
1398
+ const harnessFiles = [];
1399
+ if (!this.isInstalled()) {
1400
+ let settings = {};
1401
+ if (fs.existsSync(GROK_USER_SETTINGS)) try {
1402
+ settings = JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8"));
1403
+ } catch {}
1404
+ if (!settings.mcpServers) settings.mcpServers = [];
1405
+ settings.mcpServers.push({
1406
+ name: config.serverName,
1407
+ transport: {
1408
+ type: "stdio",
1409
+ command: process.execPath,
1410
+ args: [config.mcpServerPath],
1411
+ env: { DXCRM_DATA_DIR: config.dataDir }
1412
+ }
1413
+ });
1414
+ fs.writeFileSync(GROK_USER_SETTINGS, JSON.stringify(settings, null, 2));
1415
+ }
1416
+ const grokProjectDir = path.join(config.dataDir, ".grok");
1417
+ fs.mkdirSync(grokProjectDir, { recursive: true });
1418
+ const projectSettings = path.join(grokProjectDir, "settings.json");
1419
+ fs.writeFileSync(projectSettings, buildGrokSettingsJson(config));
1420
+ harnessFiles.push(projectSettings);
1421
+ const agentsPath = path.join(config.dataDir, "AGENTS.md");
1422
+ if (!fs.existsSync(agentsPath)) {
1423
+ fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
1424
+ harnessFiles.push(agentsPath);
1425
+ } else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
1426
+ fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
1427
+ harnessFiles.push(agentsPath + " (appended)");
1428
+ }
1429
+ return {
1430
+ framework: this.name,
1431
+ success: true,
1432
+ transport: "stdio",
1433
+ configPath: GROK_USER_SETTINGS,
1434
+ harnessFiles,
1435
+ notes: "MCP registered in ~/.grok/user-settings.json (array format). Project config written to .grok/settings.json. AGENTS.md written — Grok Build reads it natively alongside CLAUDE.md."
1436
+ };
1437
+ }
1438
+ async uninstall() {
1439
+ if (!fs.existsSync(GROK_USER_SETTINGS)) return;
1440
+ try {
1441
+ const settings = JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8"));
1442
+ if (settings.mcpServers) settings.mcpServers = settings.mcpServers.filter((s) => s.name !== "datasynx-opencrm");
1443
+ fs.writeFileSync(GROK_USER_SETTINGS, JSON.stringify(settings, null, 2));
1444
+ } catch {}
1445
+ }
1446
+ };
1447
+ //#endregion
1448
+ //#region src/setup/framework-registry.ts
1449
+ const FRAMEWORK_ADAPTERS = [
1450
+ new ClaudeCodeAdapter(),
1451
+ new CodexAdapter(),
1452
+ new GrokAdapter(),
1453
+ new OpenClawAdapter(),
1454
+ new HermesAdapter(),
1455
+ new AntigravityAdapter(),
1456
+ new CursorAdapter(),
1457
+ new WindsurfAdapter(),
1458
+ new ClineAdapter(),
1459
+ new ClaudeDesktopAdapter()
1460
+ ];
1461
+ async function installAllDetected(config) {
1462
+ const results = [];
1463
+ for (const adapter of FRAMEWORK_ADAPTERS) {
1464
+ if (!adapter.detect()) continue;
1465
+ try {
1466
+ results.push(await adapter.install(config));
1467
+ } catch (err) {
1468
+ results.push({
1469
+ framework: adapter.name,
1470
+ success: false,
1471
+ transport: "stdio",
1472
+ configPath: "",
1473
+ harnessFiles: [],
1474
+ notes: err.message
1475
+ });
1476
+ }
1477
+ }
1478
+ return results;
1479
+ }
1480
+ //#endregion
1481
+ //#region src/commands/init.ts
1482
+ const initCommand = new Command("init").description("Initialize CRM and configure AI frameworks").option("--team <url>", "Team mode: configure frameworks to connect to shared HTTP server at this URL (e.g. http://vm-ip:3847/mcp)").action(async (opts) => {
1483
+ const dataDir = process.cwd();
1484
+ const agenticDir = path.join(dataDir, ".agentic");
1485
+ fs.mkdirSync(agenticDir, { recursive: true });
1486
+ const configPath = path.join(agenticDir, "config.json");
1487
+ if (!fs.existsSync(configPath)) fs.writeFileSync(configPath, JSON.stringify({
1488
+ version: 1,
1489
+ dataDir,
1490
+ created: (/* @__PURE__ */ new Date()).toISOString()
1491
+ }, null, 2));
1492
+ const home = os.homedir();
1493
+ const transcriptPaths = [
1494
+ path.join(home, "Downloads", "Fireflies"),
1495
+ path.join(home, "Downloads", "Otter"),
1496
+ path.join(home, "Documents", "Zoom"),
1497
+ path.join(home, "Downloads", "Zoom")
1498
+ ].filter((p) => fs.existsSync(p));
1499
+ const sourcesPath = path.join(agenticDir, "sources.json");
1500
+ if (!fs.existsSync(sourcesPath)) {
1501
+ const sources = {
1502
+ gmail: {
1503
+ type: "gmail",
1504
+ query: "",
1505
+ enabled: false
1506
+ },
1507
+ version: 1,
1508
+ created: (/* @__PURE__ */ new Date()).toISOString()
1509
+ };
1510
+ if (transcriptPaths.length > 0) sources.transcripts = {
1511
+ type: "transcript",
1512
+ paths: transcriptPaths,
1513
+ extensions: [".txt", ".vtt"],
1514
+ enabled: true
1515
+ };
1516
+ fs.writeFileSync(sourcesPath, JSON.stringify(sources, null, 2));
1517
+ }
1518
+ fs.mkdirSync(path.join(dataDir, "customers"), { recursive: true });
1519
+ const schemaPath = path.join(agenticDir, "schema.json");
1520
+ if (!fs.existsSync(schemaPath)) fs.writeFileSync(schemaPath, JSON.stringify({
1521
+ version: 1,
1522
+ description: "DatasynxOpenCRM validation schema for main_facts.md frontmatter",
1523
+ main_facts: {
1524
+ required: [
1525
+ "name",
1526
+ "relationship_stage",
1527
+ "created",
1528
+ "tags",
1529
+ "currency"
1530
+ ],
1531
+ properties: {
1532
+ name: { type: "string" },
1533
+ relationship_stage: {
1534
+ type: "string",
1535
+ enum: [
1536
+ "lead",
1537
+ "qualified",
1538
+ "discovery",
1539
+ "proposal",
1540
+ "negotiation",
1541
+ "active",
1542
+ "churned",
1543
+ "closed"
1544
+ ]
1545
+ },
1546
+ domain: { type: "string" },
1547
+ email: {
1548
+ type: "string",
1549
+ format: "email"
1550
+ },
1551
+ created: {
1552
+ type: "string",
1553
+ format: "date"
1554
+ },
1555
+ updated: {
1556
+ type: "string",
1557
+ format: "date"
1558
+ },
1559
+ tags: {
1560
+ type: "array",
1561
+ items: { type: "string" }
1562
+ },
1563
+ currency: {
1564
+ type: "string",
1565
+ enum: [
1566
+ "EUR",
1567
+ "USD",
1568
+ "GBP",
1569
+ "CHF"
1570
+ ]
1571
+ },
1572
+ deal_value: {
1573
+ type: "number",
1574
+ minimum: 0
1575
+ },
1576
+ last_touchpoint: {
1577
+ type: "string",
1578
+ format: "date"
1579
+ },
1580
+ primary_contact: { type: "string" }
1581
+ }
1582
+ }
1583
+ }, null, 2));
1584
+ console.log(info("Detecting AI frameworks..."));
1585
+ const mcpServerPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/mcp.js");
1586
+ if (opts.team) console.log(info(`Team mode: connecting frameworks to ${bold(opts.team)}`));
1587
+ const results = await installAllDetected({
1588
+ mcpServerPath,
1589
+ dataDir,
1590
+ httpPort: 3847,
1591
+ serverName: "datasynx-opencrm",
1592
+ ...opts.team ? { httpUrl: opts.team } : {}
1593
+ });
1594
+ if (results.length === 0) console.log(info(" No AI frameworks detected. Configure manually: dxcrm guide --framework <name>"));
1595
+ else for (const r of results) if (r.success) {
1596
+ console.log(success(` ✓ ${r.framework} — ${r.transport} transport`));
1597
+ if (r.notes) console.log(` ${r.notes}`);
1598
+ } else console.log(error(` ✗ ${r.framework} — ${r.notes ?? "failed"}`));
1599
+ console.log(success(`\n✓ DatasynxOpenCRM initialized in ${bold(dataDir)}`));
1600
+ if (opts.team) {
1601
+ console.log(info(` Team server: ${opts.team}`));
1602
+ console.log(info(` Set identity: export DXCRM_ACTOR=<your-name>`));
1603
+ } else console.log(info(` Next: dxcrm create "Your Customer Name"`));
1604
+ });
1605
+ //#endregion
1606
+ //#region src/commands/sync.ts
1607
+ const syncCommand = new Command("sync").argument("<slug>", "Customer slug to sync").description("Sync Gmail and transcripts for a customer").option("--since <date>", "Only sync emails/files after this date (YYYY-MM-DD)").option("--gmail", "Sync Gmail only").option("--transcripts", "Sync transcripts only").option("--provider <provider>", "Sync provider: gmail | microsoft | transcripts").action(async (slug, opts) => {
1608
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
1609
+ const customerDir = path.join(dataDir, "customers", slug);
1610
+ if (!fs.existsSync(customerDir)) {
1611
+ console.error(error(`✗ Customer '${slug}' not found. Run 'dxcrm list' to see available customers.`));
1612
+ process.exit(1);
1613
+ }
1614
+ const sourcesPath = path.join(customerDir, "sources.json");
1615
+ if (!fs.existsSync(sourcesPath)) {
1616
+ console.error(error(`✗ No sources.json found for '${slug}'.`));
1617
+ process.exit(1);
1618
+ }
1619
+ const sources = JSON.parse(fs.readFileSync(sourcesPath, "utf-8"));
1620
+ const since = opts.since ? new Date(opts.since) : /* @__PURE__ */ new Date(Date.now() - 720 * 60 * 60 * 1e3);
1621
+ const provider = opts.provider;
1622
+ const syncGmail = !opts.transcripts && provider !== "microsoft" && provider !== "transcripts" && provider !== "google-drive";
1623
+ const syncMicrosoft = provider === "microsoft";
1624
+ const syncTranscripts = !opts.gmail && provider !== "gmail" && provider !== "microsoft" && provider !== "google-drive";
1625
+ const syncGoogleDrive = provider === "google-drive";
1626
+ let totalSynced = 0;
1627
+ if (syncGmail && sources.gmail?.enabled && sources.gmail.query) {
1628
+ const tokenPath = path.join(dataDir, ".agentic", "gmail-token.json");
1629
+ const credPath = path.join(dataDir, ".agentic", "gmail-credentials.json");
1630
+ if (!fs.existsSync(tokenPath) || !fs.existsSync(credPath)) console.log(info(" Gmail: credentials not configured (run dxcrm sync --setup-gmail)"));
1631
+ else try {
1632
+ console.log(info(` Syncing Gmail for ${bold(slug)}...`));
1633
+ const { getGmailAuth } = await import("./gmail-auth-OComS92L.js");
1634
+ const { syncGmail: doGmailSync } = await import("./gmail-sync-DIaxInDT.js");
1635
+ const result = await doGmailSync({
1636
+ slug,
1637
+ dataDir,
1638
+ auth: await getGmailAuth(credPath, tokenPath),
1639
+ query: sources.gmail.query,
1640
+ since
1641
+ });
1642
+ totalSynced += result.synced;
1643
+ console.log(success(` ✓ Gmail: +${result.synced} synced, ${result.skipped} skipped`));
1644
+ } catch (err) {
1645
+ console.error(error(` ✗ Gmail sync failed: ${err.message}`));
1646
+ }
1647
+ } else if (syncGmail) console.log(info(" Gmail: not configured (add domain/email to sources.json)"));
1648
+ if (syncMicrosoft) try {
1649
+ console.log(info(` Syncing Microsoft Outlook for ${bold(slug)}...`));
1650
+ const { getMicrosoftToken } = await import("./microsoft-auth-B8_S45gh.js");
1651
+ const token = await getMicrosoftToken(dataDir);
1652
+ if (!token) console.log(info(" Microsoft: no token found (.agentic/microsoft-token.json)"));
1653
+ else {
1654
+ const { syncMicrosoft: doMsSync } = await import("./microsoft-sync-CpZVoSuq.js");
1655
+ const emailResult = await doMsSync({
1656
+ slug,
1657
+ dataDir,
1658
+ accessToken: token,
1659
+ since
1660
+ });
1661
+ totalSynced += emailResult.synced;
1662
+ console.log(success(` ✓ Microsoft Email: +${emailResult.synced} synced, ${emailResult.skipped} skipped`));
1663
+ const { syncMicrosoftCalendar } = await import("./microsoft-calendar-B6MMtUQK.js");
1664
+ const calResult = await syncMicrosoftCalendar({
1665
+ slug,
1666
+ dataDir,
1667
+ accessToken: token,
1668
+ since
1669
+ });
1670
+ totalSynced += calResult.synced;
1671
+ console.log(success(` ✓ Microsoft Calendar: +${calResult.synced} synced, ${calResult.skipped} skipped`));
1672
+ }
1673
+ } catch (err) {
1674
+ console.error(error(` ✗ Microsoft sync failed: ${err.message}`));
1675
+ }
1676
+ if (syncTranscripts) {
1677
+ const agenticSourcesPath = path.join(dataDir, ".agentic", "sources.json");
1678
+ if (fs.existsSync(agenticSourcesPath)) try {
1679
+ const agenticSources = JSON.parse(fs.readFileSync(agenticSourcesPath, "utf-8"));
1680
+ if (agenticSources.transcripts?.enabled && agenticSources.transcripts.paths?.length) {
1681
+ const { processTranscriptFile } = await import("./transcript-watcher-CL2QUygI.js");
1682
+ const exts = agenticSources.transcripts.extensions ?? [".txt", ".vtt"];
1683
+ let transcriptSynced = 0;
1684
+ for (const watchPath of agenticSources.transcripts.paths) {
1685
+ if (!fs.existsSync(watchPath)) continue;
1686
+ const files = fs.readdirSync(watchPath).filter((f) => exts.some((ext) => f.endsWith(ext))).map((f) => path.join(watchPath, f));
1687
+ for (const file of files) try {
1688
+ await processTranscriptFile(file, slug, dataDir);
1689
+ transcriptSynced++;
1690
+ } catch {}
1691
+ }
1692
+ if (transcriptSynced > 0) {
1693
+ totalSynced += transcriptSynced;
1694
+ console.log(success(` ✓ Transcripts: +${transcriptSynced} processed`));
1695
+ } else console.log(info(" Transcripts: no new files"));
1696
+ } else console.log(info(" Transcripts: not configured"));
1697
+ } catch (err) {
1698
+ console.error(error(` ✗ Transcript sync failed: ${err.message}`));
1699
+ }
1700
+ }
1701
+ if (syncGoogleDrive) {
1702
+ const tokenPath = path.join(dataDir, ".agentic", "google-token.json");
1703
+ if (!fs.existsSync(tokenPath)) console.log(info(" Google Drive: token not configured (.agentic/google-token.json)"));
1704
+ else try {
1705
+ const tokenData = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
1706
+ const accessToken = tokenData.accessToken ?? tokenData.access_token;
1707
+ if (!accessToken) console.log(info(" Google Drive: accessToken not found in token file"));
1708
+ else {
1709
+ console.log(info(` Syncing Google Drive for ${bold(slug)}...`));
1710
+ const { syncGoogleDriveFiles } = await import("./google-drive-sync-DEPcqFca.js");
1711
+ const result = await syncGoogleDriveFiles({
1712
+ slug,
1713
+ dataDir,
1714
+ accessToken,
1715
+ customerName: slug
1716
+ });
1717
+ totalSynced += result.synced;
1718
+ if (result.errors.length > 0) for (const err of result.errors) console.error(error(` ✗ Google Drive: ${err}`));
1719
+ console.log(success(` ✓ Google Drive: +${result.synced} synced, ${result.skipped} skipped`));
1720
+ }
1721
+ } catch (err) {
1722
+ console.error(error(` ✗ Google Drive sync failed: ${err.message}`));
1723
+ }
1724
+ }
1725
+ if (totalSynced > 0) console.log(success(`\n✓ Sync complete: ${bold(String(totalSynced))} new interactions for ${bold(slug)}`));
1726
+ else console.log(info(`\n✓ Sync complete: no new interactions for ${bold(slug)}`));
1727
+ });
1728
+ //#endregion
1729
+ //#region src/commands/daemon.ts
1730
+ function getPidFile$1() {
1731
+ return path.join(process.cwd(), ".agentic", "daemon.pid");
1732
+ }
1733
+ const daemonCommand = new Command("daemon");
1734
+ daemonCommand.command("start").action(async () => {
1735
+ const pidFile = getPidFile$1();
1736
+ if (fs.existsSync(pidFile)) {
1737
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
1738
+ try {
1739
+ process.kill(pid, 0);
1740
+ console.log(info(`Daemon already running (PID ${pid})`));
1741
+ return;
1742
+ } catch {}
1743
+ }
1744
+ const workerPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/daemon/worker.js");
1745
+ const child = spawn(process.execPath, [workerPath], {
1746
+ detached: true,
1747
+ stdio: "ignore"
1748
+ });
1749
+ child.unref();
1750
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
1751
+ fs.writeFileSync(pidFile, String(child.pid));
1752
+ console.log(success(`✓ Daemon started (PID ${child.pid})`));
1753
+ });
1754
+ daemonCommand.command("stop").action(() => {
1755
+ const pidFile = getPidFile$1();
1756
+ if (!fs.existsSync(pidFile)) {
1757
+ console.log(info("Daemon not running."));
1758
+ return;
1759
+ }
1760
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
1761
+ try {
1762
+ process.kill(pid, "SIGTERM");
1763
+ fs.unlinkSync(pidFile);
1764
+ console.log(success("✓ Daemon stopped."));
1765
+ } catch (err) {
1766
+ console.error(error(`✗ ${err.message}`));
1767
+ }
1768
+ });
1769
+ daemonCommand.command("status").action(() => {
1770
+ const pidFile = getPidFile$1();
1771
+ if (!fs.existsSync(pidFile)) {
1772
+ console.log(info("Daemon: not running."));
1773
+ return;
1774
+ }
1775
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
1776
+ try {
1777
+ process.kill(pid, 0);
1778
+ console.log(success(`Daemon: running (PID ${pid})`));
1779
+ } catch {
1780
+ console.log(info("Daemon: stopped (stale PID file)."));
1781
+ fs.unlinkSync(pidFile);
1782
+ }
1783
+ });
1784
+ //#endregion
1785
+ //#region src/commands/status.ts
1786
+ function formatAge(isoString) {
1787
+ const diff = Date.now() - new Date(isoString).getTime();
1788
+ const mins = Math.floor(diff / 6e4);
1789
+ if (mins < 60) return `${mins}m ago`;
1790
+ const hours = Math.floor(mins / 60);
1791
+ if (hours < 24) return `${hours}h ago`;
1792
+ return `${Math.floor(hours / 24)}d ago`;
1793
+ }
1794
+ function checkDaemon(dataDir) {
1795
+ const pidFile = path.join(dataDir, ".agentic", "daemon.pid");
1796
+ if (!fs.existsSync(pidFile)) return { running: false };
1797
+ try {
1798
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
1799
+ if (isNaN(pid)) return { running: false };
1800
+ process.kill(pid, 0);
1801
+ return {
1802
+ running: true,
1803
+ pid
1804
+ };
1805
+ } catch {
1806
+ return { running: false };
1807
+ }
1808
+ }
1809
+ async function fetchTeamSessions(serverUrl) {
1810
+ try {
1811
+ const res = await fetch(`${serverUrl.replace(/\/$/, "")}/sessions`, { signal: AbortSignal.timeout(3e3) });
1812
+ if (!res.ok) return null;
1813
+ return (await res.json()).sessions ?? null;
1814
+ } catch {
1815
+ return null;
1816
+ }
1817
+ }
1818
+ async function runStatus(opts, dataDir) {
1819
+ const dir = dataDir ?? process.cwd();
1820
+ if (opts.unmatched) {
1821
+ const unmatched = readUnmatched(dir);
1822
+ const sep = "─".repeat(37);
1823
+ console.log(sep);
1824
+ console.log(bold(" Unmatched Transcripts"));
1825
+ console.log(sep);
1826
+ if (unmatched.length === 0) console.log(info(" No unmatched transcripts."));
1827
+ else for (const entry of unmatched) console.log(` ${entry.filePath} ${entry.reason} ${entry.addedAt}`);
1828
+ console.log(sep);
1829
+ return;
1830
+ }
1831
+ const daemon = checkDaemon(dir);
1832
+ const slugs = listCustomerSlugs(dir);
1833
+ const syncState = readSyncState(dir);
1834
+ const unmatched = readUnmatched(dir);
1835
+ const sep = "─".repeat(37);
1836
+ console.log(sep);
1837
+ console.log(bold(" DatasynxOpenCRM Status"));
1838
+ console.log(sep);
1839
+ const daemonLine = daemon.running ? success(`running (PID ${daemon.pid})`) : error("not running");
1840
+ console.log(` Daemon: ${daemonLine}`);
1841
+ console.log(` Customers: ${slugs.length} active`);
1842
+ const session = getSession() ?? readAllSessions(dir)[0] ?? null;
1843
+ if (session) {
1844
+ const ownerPart = session.owner ? ` [${session.owner}]` : "";
1845
+ console.log(` Session: ${session.customerName} (${session.customerSlug})${ownerPart}`);
1846
+ } else console.log(` Session: none`);
1847
+ if (slugs.length > 0) {
1848
+ console.log(` Syncs:`);
1849
+ for (const slug of slugs) {
1850
+ const state = syncState[slug];
1851
+ const ageStr = state?.lastGmailSync ? `Gmail ${formatAge(state.lastGmailSync)}` : "no sync yet";
1852
+ console.log(` ${slug}: ${ageStr}`);
1853
+ }
1854
+ }
1855
+ const unmatchedCount = unmatched.length;
1856
+ if (unmatchedCount > 0) console.log(` Unmatched: ${unmatchedCount} Transcript${unmatchedCount !== 1 ? "s" : ""} (dxcrm status --unmatched)`);
1857
+ else console.log(` Unmatched: 0 Transcripts`);
1858
+ const serverUrl = opts.team ?? process.env["DXCRM_SERVER_URL"];
1859
+ if (serverUrl) {
1860
+ const teamSessions = await fetchTeamSessions(serverUrl);
1861
+ if (teamSessions && teamSessions.length > 0) {
1862
+ console.log(bold("\n Team overview:"));
1863
+ for (const s of teamSessions) {
1864
+ const ownerPart = s.owner ? `${s.owner}` : "anonymous";
1865
+ console.log(info(` ${ownerPart.padEnd(15)} → ${s.customerName} (${s.customerSlug})`));
1866
+ }
1867
+ } else if (teamSessions !== null) console.log(info(" Team: no active sessions"));
1868
+ else console.log(info(` Team: server unreachable (${serverUrl})`));
1869
+ }
1870
+ console.log(sep);
1871
+ }
1872
+ const statusCommand = new Command("status").description("Show CRM status: daemon, sync state, customer counts").option("--unmatched", "Show unmatched transcript queue").option("--team <url>", "Show team sessions from HTTP server (or set DXCRM_SERVER_URL)").action((opts) => runStatus(opts, process.env["DXCRM_DATA_DIR"] ?? process.cwd()));
1873
+ //#endregion
1874
+ //#region src/commands/agent.ts
1875
+ function agentsDir(dataDir) {
1876
+ return path.join(dataDir, ".agentic", "agents");
1877
+ }
1878
+ function agentConfigPath(dataDir, slug) {
1879
+ return path.join(agentsDir(dataDir), `${slug}.agent.json`);
1880
+ }
1881
+ async function runAgentSpawn(slug, opts, dataDir) {
1882
+ const dir = dataDir ?? process.cwd();
1883
+ const customerDir = path.join(dir, "customers", slug);
1884
+ if (!fs.existsSync(customerDir)) {
1885
+ console.error(error(`✗ Customer '${slug}' not found. Run 'dxcrm list' to see available customers.`));
1886
+ process.exit(1);
1887
+ }
1888
+ const channel = opts.channel ?? "telegram";
1889
+ const wakeOn = opts.wakeOnEmail ? ["email"] : ["email"];
1890
+ const config = AgentConfigSchema.parse({
1891
+ slug,
1892
+ channel,
1893
+ wakeOn,
1894
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1895
+ lastWake: null,
1896
+ ...opts.chatId !== void 0 ? { telegramChatId: opts.chatId } : {}
1897
+ });
1898
+ fs.mkdirSync(agentsDir(dir), { recursive: true });
1899
+ fs.writeFileSync(agentConfigPath(dir, slug), JSON.stringify(config, null, 2), "utf-8");
1900
+ console.log(success(`✓ Agent spawned: ${bold(slug)}`));
1901
+ console.log(info(` Channel: ${channel}`));
1902
+ console.log(info(` Wake on: ${wakeOn.join(", ")}`));
1903
+ console.log(info(` Config: .agentic/agents/${slug}.agent.json`));
1904
+ if (channel === "telegram" && !process.env["TELEGRAM_BOT_TOKEN"]) console.log(info(`\n Note: Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars to enable Telegram messages.`));
1905
+ }
1906
+ async function runAgentStatus(dataDir) {
1907
+ const dir2 = agentsDir(dataDir ?? process.cwd());
1908
+ if (!fs.existsSync(dir2)) {
1909
+ console.log(info("No agents configured. Run: dxcrm agent spawn <slug> --channel telegram"));
1910
+ return;
1911
+ }
1912
+ const files = fs.readdirSync(dir2).filter((f) => f.endsWith(".agent.json"));
1913
+ if (files.length === 0) {
1914
+ console.log(info("No agents configured."));
1915
+ return;
1916
+ }
1917
+ console.log(bold(`\n Agents (${files.length})\n`));
1918
+ for (const file of files) try {
1919
+ const raw = JSON.parse(fs.readFileSync(path.join(dir2, file), "utf-8"));
1920
+ const lastWake = raw.lastWake ? `last wake: ${new Date(raw.lastWake).toLocaleString()}` : "never woken";
1921
+ console.log(info(` ${bold(raw.slug)} — ${raw.channel} · ${raw.wakeOn.join("+")} · ${lastWake}`));
1922
+ } catch {
1923
+ console.log(info(` ${file} (malformed)`));
1924
+ }
1925
+ console.log("");
1926
+ }
1927
+ async function runAgentRemove(slug, dataDir) {
1928
+ const configPath = agentConfigPath(dataDir ?? process.cwd(), slug);
1929
+ if (!fs.existsSync(configPath)) {
1930
+ console.error(error(`✗ No agent config found for '${slug}'.`));
1931
+ process.exit(1);
1932
+ }
1933
+ fs.unlinkSync(configPath);
1934
+ console.log(success(`✓ Agent removed: ${slug}`));
1935
+ }
1936
+ const agentCommand = new Command("agent").description("Manage per-customer agents");
1937
+ agentCommand.command("spawn <slug>").description("Spawn a wake-triggered agent for a customer").option("--channel <channel>", "Notification channel (telegram)", "telegram").option("--wake-on-email", "Wake agent on new email (default: on)").option("--chat-id <chatId>", "Telegram chat ID override").action((slug, opts) => runAgentSpawn(slug, opts, process.env["DXCRM_DATA_DIR"]));
1938
+ agentCommand.command("status").description("Show all configured agents").action(() => runAgentStatus(process.env["DXCRM_DATA_DIR"]));
1939
+ agentCommand.command("remove <slug>").description("Remove agent config for a customer").action((slug) => runAgentRemove(slug, process.env["DXCRM_DATA_DIR"]));
1940
+ //#endregion
1941
+ //#region src/commands/import.ts
1942
+ /** Map a Salesforce StageName to opencrm's fixed pipeline stage enum. */
1943
+ function mapSalesforceStage(stageName) {
1944
+ const s = (stageName ?? "").toLowerCase();
1945
+ if (s.includes("won")) return "won";
1946
+ if (s.includes("lost")) return "lost";
1947
+ if (s.includes("negoti")) return "negotiation";
1948
+ if (s.includes("propos") || s.includes("quote")) return "proposal";
1949
+ if (s.includes("qualif")) return "qualified";
1950
+ return "lead";
1951
+ }
1952
+ /** Map a Salesforce Case Status to opencrm's ticket status enum. */
1953
+ function mapCaseStatus(status) {
1954
+ const s = (status ?? "").toLowerCase();
1955
+ if (s.includes("closed")) return "closed";
1956
+ if (s.includes("resolved")) return "resolved";
1957
+ if (s.includes("escalat") || s.includes("wait") || s.includes("hold")) return "waiting";
1958
+ if (s.includes("working") || s.includes("progress")) return "in-progress";
1959
+ return "open";
1960
+ }
1961
+ /** Map a Salesforce Case Priority to opencrm's ticket priority enum. */
1962
+ function mapCasePriority(priority) {
1963
+ const p = (priority ?? "").toLowerCase();
1964
+ if (p.includes("urgent") || p.includes("critical")) return "urgent";
1965
+ if (p.includes("high")) return "high";
1966
+ if (p.includes("low")) return "low";
1967
+ return "normal";
1968
+ }
1969
+ function hashRow(row) {
1970
+ return createHash("sha256").update(JSON.stringify(row)).digest("hex").slice(0, 16);
1971
+ }
1972
+ function slugify$1(name) {
1973
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1974
+ }
1975
+ function parseCSV(content) {
1976
+ const lines = content.trim().split("\n");
1977
+ if (lines.length < 2) return [];
1978
+ const headers = (lines[0] ?? "").split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
1979
+ return lines.slice(1).map((line) => {
1980
+ const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
1981
+ const row = {};
1982
+ headers.forEach((h, i) => {
1983
+ row[h] = values[i] ?? "";
1984
+ });
1985
+ return row;
1986
+ });
1987
+ }
1988
+ const IMPORT_TARGET_FIELDS = [
1989
+ "name",
1990
+ "email",
1991
+ "domain",
1992
+ "notes",
1993
+ "date",
1994
+ "activityType",
1995
+ "sourceId"
1996
+ ];
1997
+ function ensureCustomer(dataDir, name, domain, email, dryRun) {
1998
+ const slug = slugify$1(name || "unknown");
1999
+ const customerDir = path.join(dataDir, "customers", slug);
2000
+ const mainFactsPath = path.join(customerDir, "main_facts.md");
2001
+ if (fs.existsSync(mainFactsPath)) return {
2002
+ slug,
2003
+ created: false
2004
+ };
2005
+ if (dryRun) return {
2006
+ slug,
2007
+ created: true
2008
+ };
2009
+ fs.mkdirSync(customerDir, { recursive: true });
2010
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2011
+ const frontmatter = [
2012
+ "---",
2013
+ `name: ${name}`,
2014
+ domain ? `domain: ${domain}` : null,
2015
+ email ? `email: ${email}` : null,
2016
+ "relationship_stage: prospect",
2017
+ `created: ${today}`,
2018
+ `updated: ${today}`,
2019
+ `last_touchpoint: ${today}`,
2020
+ "tags: []",
2021
+ "---"
2022
+ ].filter(Boolean).join("\n");
2023
+ fs.writeFileSync(mainFactsPath, `${frontmatter}\n\n# Customer: ${name}\n`, "utf-8");
2024
+ fs.writeFileSync(path.join(customerDir, "interactions.md"), `# Interactions — ${name}\n\n`, "utf-8");
2025
+ fs.writeFileSync(path.join(customerDir, "pipeline.md"), `# Pipeline — ${name}\n\n`, "utf-8");
2026
+ fs.writeFileSync(path.join(customerDir, "sources.json"), JSON.stringify({
2027
+ gmail: {
2028
+ query: domain ? `from:${domain} OR to:${domain}` : email ? `from:${email} OR to:${email}` : "",
2029
+ enabled: true
2030
+ },
2031
+ transcripts: {
2032
+ paths: [],
2033
+ extensions: [".txt", ".vtt"],
2034
+ enabled: false
2035
+ }
2036
+ }, null, 2), "utf-8");
2037
+ return {
2038
+ slug,
2039
+ created: true
2040
+ };
2041
+ }
2042
+ function readCsvFromDirectory(dirPath, filename) {
2043
+ const variants = [
2044
+ filename,
2045
+ filename.toLowerCase(),
2046
+ filename.toUpperCase()
2047
+ ];
2048
+ for (const name of variants) {
2049
+ const p = path.join(dirPath, name);
2050
+ if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8");
2051
+ }
2052
+ const match = fs.readdirSync(dirPath).find((f) => f.toLowerCase() === filename.toLowerCase());
2053
+ if (match) return fs.readFileSync(path.join(dirPath, match), "utf-8");
2054
+ return null;
2055
+ }
2056
+ async function extractZip(zipPath) {
2057
+ const AdmZip = (await import("adm-zip")).default;
2058
+ const zip = new AdmZip(zipPath);
2059
+ const tmpDir = `${zipPath}.extracted`;
2060
+ fs.mkdirSync(tmpDir, { recursive: true });
2061
+ zip.extractAllTo(tmpDir, true);
2062
+ return tmpDir;
2063
+ }
2064
+ async function runSalesforceFileImport(sourcePath, opts, dir) {
2065
+ const result = {
2066
+ customersCreated: 0,
2067
+ interactionsImported: 0,
2068
+ skipped: 0,
2069
+ errors: []
2070
+ };
2071
+ let dataDir = sourcePath;
2072
+ let tmpDir = null;
2073
+ if (sourcePath.endsWith(".zip")) {
2074
+ tmpDir = await extractZip(sourcePath);
2075
+ dataDir = tmpDir;
2076
+ }
2077
+ if (!fs.statSync(dataDir).isDirectory()) {
2078
+ result.errors.push("Salesforce file import requires a directory or .zip file");
2079
+ return result;
2080
+ }
2081
+ const accountsCsv = readCsvFromDirectory(dataDir, "Accounts.csv") ?? readCsvFromDirectory(dataDir, "accounts.csv");
2082
+ const activitiesCsv = readCsvFromDirectory(dataDir, "Activities.csv") ?? readCsvFromDirectory(dataDir, "activities.csv") ?? readCsvFromDirectory(dataDir, "Tasks.csv") ?? readCsvFromDirectory(dataDir, "tasks.csv");
2083
+ if (!accountsCsv) {
2084
+ result.errors.push("Could not find Accounts.csv in Salesforce export");
2085
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2086
+ return result;
2087
+ }
2088
+ const accounts = parseCSV(accountsCsv);
2089
+ const activities = activitiesCsv ? parseCSV(activitiesCsv) : [];
2090
+ if (opts.dryRun) {
2091
+ console.log(info(`Dry run — ${accounts.length} accounts, ${activities.length} activities from Salesforce export`));
2092
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2093
+ return result;
2094
+ }
2095
+ const slugMap = /* @__PURE__ */ new Map();
2096
+ for (const row of accounts) {
2097
+ const name = (row["Name"] ?? row["Account Name"] ?? "").trim();
2098
+ if (!name) continue;
2099
+ const domain = (row["Website"] ?? "").replace(/^https?:\/\//, "");
2100
+ const email = row["Email"] ?? "";
2101
+ try {
2102
+ const { slug, created } = ensureCustomer(dir, name, domain, email, false);
2103
+ if (row["Id"]) slugMap.set(row["Id"], slug);
2104
+ slugMap.set(name.toLowerCase(), slug);
2105
+ if (created) result.customersCreated++;
2106
+ } catch (err) {
2107
+ result.errors.push(`Account '${name}': ${err.message}`);
2108
+ }
2109
+ }
2110
+ for (const row of activities) {
2111
+ const accountId = row["AccountId"] ?? row["WhatId"] ?? "";
2112
+ const slug = accountId ? slugMap.get(accountId) : void 0;
2113
+ if (!slug) continue;
2114
+ const id = row["Id"] ?? hashRow(row);
2115
+ const sourceRef = `salesforce://row/${id}`;
2116
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2117
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2118
+ result.skipped++;
2119
+ continue;
2120
+ }
2121
+ const date = row["ActivityDate"] ?? row["CreatedDate"] ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2122
+ const notes = (row["Description"] ?? row["Subject"] ?? "").slice(0, 500);
2123
+ const t = (row["Type"] ?? "").toLowerCase();
2124
+ const type = t.includes("call") ? "Call" : t.includes("email") ? "Email" : t.includes("meeting") ? "Meeting" : "Note";
2125
+ try {
2126
+ await appendInteraction(dir, slug, {
2127
+ date,
2128
+ type,
2129
+ with: slug,
2130
+ summary: notes,
2131
+ nextSteps: [],
2132
+ sourceRef,
2133
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2134
+ });
2135
+ result.interactionsImported++;
2136
+ } catch (err) {
2137
+ result.errors.push(`Activity ${id}: ${err.message}`);
2138
+ }
2139
+ }
2140
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2141
+ return result;
2142
+ }
2143
+ async function runPipedriveFileImport(sourcePath, opts, dir) {
2144
+ const result = {
2145
+ customersCreated: 0,
2146
+ interactionsImported: 0,
2147
+ skipped: 0,
2148
+ errors: []
2149
+ };
2150
+ let dataDir = sourcePath;
2151
+ let tmpDir = null;
2152
+ if (sourcePath.endsWith(".zip")) {
2153
+ tmpDir = await extractZip(sourcePath);
2154
+ dataDir = tmpDir;
2155
+ }
2156
+ if (!fs.statSync(dataDir).isDirectory()) {
2157
+ result.errors.push("Pipedrive file import requires a directory or .zip file");
2158
+ return result;
2159
+ }
2160
+ const orgsCsv = readCsvFromDirectory(dataDir, "organizations.csv") ?? readCsvFromDirectory(dataDir, "Organizations.csv");
2161
+ const activitiesCsv = readCsvFromDirectory(dataDir, "activities.csv") ?? readCsvFromDirectory(dataDir, "Activities.csv");
2162
+ if (!orgsCsv) {
2163
+ result.errors.push("Could not find organizations.csv in Pipedrive export");
2164
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2165
+ return result;
2166
+ }
2167
+ const orgs = parseCSV(orgsCsv);
2168
+ const activities = activitiesCsv ? parseCSV(activitiesCsv) : [];
2169
+ if (opts.dryRun) {
2170
+ console.log(info(`Dry run — ${orgs.length} organizations, ${activities.length} activities from Pipedrive export`));
2171
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2172
+ return result;
2173
+ }
2174
+ const slugMap = /* @__PURE__ */ new Map();
2175
+ for (const row of orgs) {
2176
+ const name = (row["name"] ?? row["Name"] ?? "").trim();
2177
+ if (!name) continue;
2178
+ const id = row["id"] ?? row["ID"] ?? "";
2179
+ try {
2180
+ const { slug, created } = ensureCustomer(dir, name, "", "", false);
2181
+ if (id) slugMap.set(id, slug);
2182
+ slugMap.set(name.toLowerCase(), slug);
2183
+ if (created) result.customersCreated++;
2184
+ } catch (err) {
2185
+ result.errors.push(`Organization '${name}': ${err.message}`);
2186
+ }
2187
+ }
2188
+ for (const row of activities) {
2189
+ const orgId = row["org_id"] ?? row["organization_id"] ?? "";
2190
+ const slug = orgId ? slugMap.get(orgId) : void 0;
2191
+ if (!slug) continue;
2192
+ const id = row["id"] ?? hashRow(row);
2193
+ const sourceRef = `pipedrive://row/${id}`;
2194
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2195
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2196
+ result.skipped++;
2197
+ continue;
2198
+ }
2199
+ const date = row["due_date"] ?? row["add_time"]?.slice(0, 10) ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2200
+ const notes = (row["note"] ?? row["subject"] ?? "").slice(0, 500);
2201
+ const t = (row["type"] ?? "").toLowerCase();
2202
+ const type = t === "call" ? "Call" : t === "email" ? "Email" : t === "meeting" ? "Meeting" : "Note";
2203
+ try {
2204
+ await appendInteraction(dir, slug, {
2205
+ date,
2206
+ type,
2207
+ with: slug,
2208
+ summary: notes,
2209
+ nextSteps: [],
2210
+ sourceRef,
2211
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2212
+ });
2213
+ result.interactionsImported++;
2214
+ } catch (err) {
2215
+ result.errors.push(`Activity ${id}: ${err.message}`);
2216
+ }
2217
+ }
2218
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
2219
+ return result;
2220
+ }
2221
+ async function runImport(sourcePath, opts, dataDir) {
2222
+ const dir = dataDir ?? process.cwd();
2223
+ const result = {
2224
+ customersCreated: 0,
2225
+ interactionsImported: 0,
2226
+ skipped: 0,
2227
+ errors: []
2228
+ };
2229
+ if (opts.from === "salesforce" && opts.mode === "api") return runSalesforceApiImport(opts, dir);
2230
+ if (opts.from === "pipedrive" && opts.mode === "api") return runPipedriveApiImport(opts, dir);
2231
+ if (opts.from === "hubspot" && sourcePath && fs.existsSync(sourcePath) && fs.statSync(sourcePath).isDirectory()) {
2232
+ const { runHubSpotCsvImport } = await import("./import-hubspot-BaK71U_K.js");
2233
+ const r = await runHubSpotCsvImport(sourcePath, dir, {
2234
+ ...opts.dryRun ? { dryRun: true } : {},
2235
+ ...opts.resume ? { resume: true } : {},
2236
+ ownerMap: opts.ownerMap ?? {}
2237
+ });
2238
+ if (r.customPropertiesSaved > 0) console.error(`[import] Custom properties saved: ${r.customPropertiesSaved}`);
2239
+ if (r.ownersResolved > 0) console.error(`[import] Owners resolved: ${r.ownersResolved}`);
2240
+ return {
2241
+ customersCreated: r.companiesProcessed,
2242
+ interactionsImported: r.engagementsImported + r.dealsImported + r.contactsImported,
2243
+ skipped: 0,
2244
+ errors: r.errors
2245
+ };
2246
+ }
2247
+ if (opts.from === "salesforce" && sourcePath) return runSalesforceFileImport(sourcePath, opts, dir);
2248
+ if (opts.from === "pipedrive" && sourcePath) return runPipedriveFileImport(sourcePath, opts, dir);
2249
+ if (!fs.existsSync(sourcePath)) {
2250
+ console.error(error(`✗ File not found: ${sourcePath}`));
2251
+ process.exit(1);
2252
+ }
2253
+ const rows = parseCSV(fs.readFileSync(sourcePath, "utf-8"));
2254
+ if (rows.length === 0) {
2255
+ console.log(info("No rows found in CSV."));
2256
+ return result;
2257
+ }
2258
+ const headers = Object.keys(rows[0]);
2259
+ const { mapCsvFields } = await import("./llm-DEjWcqmW.js");
2260
+ const mapping = await mapCsvFields(headers, [...IMPORT_TARGET_FIELDS]);
2261
+ if (opts.dryRun) {
2262
+ console.log(info(`Dry run — ${rows.length} rows, field mapping:`));
2263
+ Object.entries(mapping).forEach(([k, v]) => v && console.log(info(` ${k} ← "${v}"`)));
2264
+ console.log(info(`\nWould create up to ${rows.length} customers and interaction entries.`));
2265
+ return result;
2266
+ }
2267
+ const customerRows = rows.filter((r) => {
2268
+ return (r[mapping.name ?? ""] ?? "").trim().length > 0;
2269
+ });
2270
+ const slugMap = /* @__PURE__ */ new Map();
2271
+ for (const row of customerRows) {
2272
+ const name = (row[mapping.name ?? ""] ?? "").trim();
2273
+ if (!name) continue;
2274
+ const domain = (row[mapping.domain ?? ""] ?? "").trim();
2275
+ const email = (row[mapping.email ?? ""] ?? "").trim();
2276
+ try {
2277
+ const { slug, created } = ensureCustomer(dir, name, domain, email, opts.dryRun ?? false);
2278
+ slugMap.set(name.toLowerCase(), slug);
2279
+ if (created) result.customersCreated++;
2280
+ } catch (err) {
2281
+ result.errors.push(`Customer '${name}': ${err.message}`);
2282
+ }
2283
+ }
2284
+ for (const row of rows) {
2285
+ const activityType = (row[mapping["activityType"] ?? ""] ?? "").trim();
2286
+ const notes = (row[mapping["notes"] ?? ""] ?? "").trim();
2287
+ const activityDate = (row[mapping["date"] ?? ""] ?? "").trim();
2288
+ const sourceIdVal = (row[mapping["sourceId"] ?? ""] ?? "").trim();
2289
+ const name = (row[mapping["name"] ?? ""] ?? "").trim();
2290
+ if (!notes && !activityType) continue;
2291
+ const slug = slugMap.get(name.toLowerCase());
2292
+ if (!slug) continue;
2293
+ const rowHash = hashRow(row);
2294
+ const prefix = opts.from === "hubspot" ? "hubspot" : "csv";
2295
+ const sourceRef = sourceIdVal ? `${prefix}://activity/${sourceIdVal}` : `${prefix}://row/${rowHash}`;
2296
+ const date = activityDate ? (() => {
2297
+ try {
2298
+ return new Date(activityDate).toISOString().slice(0, 10);
2299
+ } catch {
2300
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2301
+ }
2302
+ })() : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2303
+ const type = (() => {
2304
+ const t = activityType.toLowerCase();
2305
+ if (t.includes("call")) return "Call";
2306
+ if (t.includes("meeting") || t.includes("demo")) return "Meeting";
2307
+ if (t.includes("email")) return "Email";
2308
+ if (t.includes("note")) return "Note";
2309
+ return "Note";
2310
+ })();
2311
+ try {
2312
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2313
+ if ((await readInteractions(dir, slug)).includes(sourceRef)) {
2314
+ result.skipped++;
2315
+ continue;
2316
+ }
2317
+ await appendInteraction(dir, slug, {
2318
+ date,
2319
+ type,
2320
+ with: name,
2321
+ summary: notes.slice(0, 500),
2322
+ nextSteps: [],
2323
+ sourceRef,
2324
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2325
+ });
2326
+ result.interactionsImported++;
2327
+ } catch (err) {
2328
+ result.errors.push(`Activity for '${name}': ${err.message}`);
2329
+ }
2330
+ }
2331
+ return result;
2332
+ }
2333
+ async function runSalesforceApiImport(opts, dir) {
2334
+ const result = {
2335
+ customersCreated: 0,
2336
+ interactionsImported: 0,
2337
+ skipped: 0,
2338
+ errors: []
2339
+ };
2340
+ const token = opts.token ?? process.env["SFDC_TOKEN"] ?? "";
2341
+ const instanceUrl = opts.url ?? process.env["SFDC_URL"] ?? "";
2342
+ if (!token || !instanceUrl) {
2343
+ console.error(error("✗ Salesforce API mode requires --token and --url (or SFDC_TOKEN + SFDC_URL env vars)"));
2344
+ process.exit(1);
2345
+ }
2346
+ const { fetchSalesforceContacts, fetchSalesforceTasks, fetchSalesforceOpportunities, fetchSalesforceLeads, fetchSalesforceEvents, fetchSalesforceCases, fetchSalesforceLineItems, fetchSalesforceNotes, fetchSalesforceCampaignMembers } = await import("./salesforce-client-rhZFa_p5.js");
2347
+ let contacts;
2348
+ let tasks;
2349
+ let opportunities;
2350
+ let leads;
2351
+ let events;
2352
+ let cases;
2353
+ let lineItems;
2354
+ let notes;
2355
+ let campaignMembers;
2356
+ try {
2357
+ contacts = await fetchSalesforceContacts(instanceUrl, token);
2358
+ tasks = await fetchSalesforceTasks(instanceUrl, token);
2359
+ opportunities = await fetchSalesforceOpportunities(instanceUrl, token) ?? [];
2360
+ leads = await fetchSalesforceLeads(instanceUrl, token) ?? [];
2361
+ events = await fetchSalesforceEvents(instanceUrl, token) ?? [];
2362
+ cases = await fetchSalesforceCases(instanceUrl, token) ?? [];
2363
+ lineItems = await fetchSalesforceLineItems(instanceUrl, token) ?? [];
2364
+ notes = await fetchSalesforceNotes(instanceUrl, token) ?? [];
2365
+ campaignMembers = await fetchSalesforceCampaignMembers(instanceUrl, token) ?? [];
2366
+ } catch (err) {
2367
+ result.errors.push(`Salesforce API: ${err.message}`);
2368
+ return result;
2369
+ }
2370
+ if (opts.dryRun) {
2371
+ console.log(info(`Dry run — ${contacts.length} contacts, ${tasks.length} tasks, ${opportunities.length} opportunities, ${leads.length} leads, ${events.length} events, ${cases.length} cases from Salesforce`));
2372
+ return result;
2373
+ }
2374
+ const slugMap = /* @__PURE__ */ new Map();
2375
+ for (const contact of contacts) {
2376
+ const name = contact.Name?.trim();
2377
+ if (!name) continue;
2378
+ const domain = contact.Account?.Website?.replace(/^https?:\/\//, "") ?? "";
2379
+ const email = contact.Email ?? "";
2380
+ try {
2381
+ const { slug, created } = ensureCustomer(dir, name, domain, email, false);
2382
+ slugMap.set(contact.Id, slug);
2383
+ slugMap.set(name.toLowerCase(), slug);
2384
+ if (created) result.customersCreated++;
2385
+ } catch (err) {
2386
+ result.errors.push(`Contact '${name}': ${err.message}`);
2387
+ }
2388
+ }
2389
+ for (const task of tasks) {
2390
+ const slug = task.WhoId ? slugMap.get(task.WhoId) : void 0;
2391
+ if (!slug) continue;
2392
+ const sourceRef = `salesforce://task/${task.Id}`;
2393
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2394
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2395
+ result.skipped++;
2396
+ continue;
2397
+ }
2398
+ const date = task.ActivityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2399
+ const notes = (task.Description ?? task.Subject ?? "").slice(0, 500);
2400
+ const t = (task.Type ?? "").toLowerCase();
2401
+ const type = t.includes("call") ? "Call" : t.includes("email") ? "Email" : t.includes("meeting") ? "Meeting" : "Note";
2402
+ try {
2403
+ await appendInteraction(dir, slug, {
2404
+ date,
2405
+ type,
2406
+ with: slug,
2407
+ summary: notes,
2408
+ nextSteps: [],
2409
+ sourceRef,
2410
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2411
+ });
2412
+ result.interactionsImported++;
2413
+ } catch (err) {
2414
+ result.errors.push(`Task ${task.Id}: ${err.message}`);
2415
+ }
2416
+ }
2417
+ const { upsertDeal } = await import("./pipeline-writer-BqBrYrQc.js");
2418
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2419
+ const oppSlugById = /* @__PURE__ */ new Map();
2420
+ for (const opp of opportunities) {
2421
+ const accountName = opp.Account?.Name?.trim();
2422
+ if (!opp.Name || !accountName) continue;
2423
+ let slug = slugMap.get(accountName.toLowerCase());
2424
+ if (!slug) {
2425
+ const domain = opp.Account?.Website?.replace(/^https?:\/\//, "") ?? "";
2426
+ try {
2427
+ const r = ensureCustomer(dir, accountName, domain, "", false);
2428
+ slug = r.slug;
2429
+ slugMap.set(accountName.toLowerCase(), slug);
2430
+ if (r.created) result.customersCreated++;
2431
+ } catch (err) {
2432
+ result.errors.push(`Opportunity '${opp.Name}': ${err.message}`);
2433
+ continue;
2434
+ }
2435
+ }
2436
+ oppSlugById.set(opp.Id, {
2437
+ slug,
2438
+ dealName: opp.Name
2439
+ });
2440
+ try {
2441
+ await upsertDeal(dir, slug, {
2442
+ name: opp.Name,
2443
+ stage: mapSalesforceStage(opp.StageName),
2444
+ currency: "EUR",
2445
+ updated: today,
2446
+ notes: `Imported from Salesforce (${opp.StageName ?? "unknown stage"})`,
2447
+ ...typeof opp.Amount === "number" ? { value: opp.Amount } : {},
2448
+ ...typeof opp.Probability === "number" ? { probability: opp.Probability } : {},
2449
+ ...opp.CloseDate ? { close_date: opp.CloseDate } : {}
2450
+ });
2451
+ result.dealsImported = (result.dealsImported ?? 0) + 1;
2452
+ } catch (err) {
2453
+ result.errors.push(`Opportunity '${opp.Name}': ${err.message}`);
2454
+ }
2455
+ }
2456
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2457
+ for (const lead of leads) {
2458
+ const name = lead.Company?.trim() || lead.Name?.trim();
2459
+ if (!name) continue;
2460
+ const domain = lead.Website?.replace(/^https?:\/\//, "") ?? lead.Email?.split("@")[1] ?? "";
2461
+ let slug;
2462
+ try {
2463
+ const r = ensureCustomer(dir, name, domain, lead.Email ?? "", false);
2464
+ slug = r.slug;
2465
+ slugMap.set(name.toLowerCase(), slug);
2466
+ if (r.created) result.customersCreated++;
2467
+ } catch (err) {
2468
+ result.errors.push(`Lead '${name}': ${err.message}`);
2469
+ continue;
2470
+ }
2471
+ const sourceRef = `salesforce://lead/${lead.Id}`;
2472
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2473
+ result.skipped++;
2474
+ continue;
2475
+ }
2476
+ const contactPart = lead.Title ? `${lead.Name}, ${lead.Title}` : lead.Name;
2477
+ try {
2478
+ await appendInteraction(dir, slug, {
2479
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2480
+ type: "Note",
2481
+ with: lead.Name,
2482
+ summary: `Salesforce Lead imported (status: ${lead.Status ?? "n/a"}; contact: ${contactPart})`,
2483
+ nextSteps: [],
2484
+ sourceRef,
2485
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2486
+ });
2487
+ result.leadsImported = (result.leadsImported ?? 0) + 1;
2488
+ } catch (err) {
2489
+ result.errors.push(`Lead ${lead.Id}: ${err.message}`);
2490
+ }
2491
+ }
2492
+ for (const event of events) {
2493
+ const slug = event.WhoId ? slugMap.get(event.WhoId) : event.WhatId ? slugMap.get(event.WhatId) : void 0;
2494
+ if (!slug) {
2495
+ result.skipped++;
2496
+ continue;
2497
+ }
2498
+ const sourceRef = `salesforce://event/${event.Id}`;
2499
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2500
+ result.skipped++;
2501
+ continue;
2502
+ }
2503
+ const date = (event.StartDateTime ?? event.ActivityDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
2504
+ try {
2505
+ await appendInteraction(dir, slug, {
2506
+ date,
2507
+ type: "Meeting",
2508
+ with: slug,
2509
+ subject: event.Subject ?? "Salesforce Event",
2510
+ summary: (event.Description ?? event.Subject ?? "").slice(0, 500),
2511
+ nextSteps: [],
2512
+ sourceRef,
2513
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2514
+ });
2515
+ result.eventsImported = (result.eventsImported ?? 0) + 1;
2516
+ } catch (err) {
2517
+ result.errors.push(`Event ${event.Id}: ${err.message}`);
2518
+ }
2519
+ }
2520
+ const { readTickets, upsertTicket, nextTicketId } = await import("./ticket-writer-CjqKeIRD.js");
2521
+ const { calcSlaDue, loadSlaRules } = await import("./sla-engine-5IhTsBUR.js");
2522
+ const slaRules = loadSlaRules(dir);
2523
+ for (const c of cases) {
2524
+ const accountName = c.Account?.Name?.trim();
2525
+ if (!accountName) {
2526
+ result.skipped++;
2527
+ continue;
2528
+ }
2529
+ let slug = slugMap.get(accountName.toLowerCase());
2530
+ if (!slug) try {
2531
+ const r = ensureCustomer(dir, accountName, "", "", false);
2532
+ slug = r.slug;
2533
+ slugMap.set(accountName.toLowerCase(), slug);
2534
+ if (r.created) result.customersCreated++;
2535
+ } catch (err) {
2536
+ result.errors.push(`Case '${c.Id}': ${err.message}`);
2537
+ continue;
2538
+ }
2539
+ if (c.AccountId) slugMap.set(c.AccountId, slug);
2540
+ if (c.ContactId) slugMap.set(c.ContactId, slug);
2541
+ const caseRef = `salesforce://case/${c.Id}`;
2542
+ const existingTickets = await readTickets(dir, slug);
2543
+ if (existingTickets.some((t) => (t.description ?? "").includes(caseRef))) {
2544
+ result.skipped++;
2545
+ continue;
2546
+ }
2547
+ const created = (c.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
2548
+ const status = mapCaseStatus(c.Status);
2549
+ const priority = mapCasePriority(c.Priority);
2550
+ const isDone = status === "closed" || status === "resolved";
2551
+ try {
2552
+ await upsertTicket(dir, slug, {
2553
+ id: nextTicketId(existingTickets),
2554
+ title: c.Subject ?? `Case ${c.CaseNumber ?? c.Id}`,
2555
+ status,
2556
+ priority,
2557
+ created,
2558
+ slaDue: calcSlaDue(created, priority, slaRules),
2559
+ description: `${c.Description ?? ""}\n\n[${caseRef}]`.trim(),
2560
+ ...isDone ? { resolved: (c.ClosedDate ?? created).slice(0, 10) } : {}
2561
+ });
2562
+ result.casesImported = (result.casesImported ?? 0) + 1;
2563
+ } catch (err) {
2564
+ result.errors.push(`Case ${c.Id}: ${err.message}`);
2565
+ }
2566
+ }
2567
+ if (lineItems.length > 0 && oppSlugById.size > 0) {
2568
+ const { generateQuote, listQuotes } = await import("./quote-generator-OhSFsi3x.js");
2569
+ const byOpp = /* @__PURE__ */ new Map();
2570
+ for (const li of lineItems) {
2571
+ if (!li.OpportunityId) continue;
2572
+ const arr = byOpp.get(li.OpportunityId) ?? [];
2573
+ arr.push(li);
2574
+ byOpp.set(li.OpportunityId, arr);
2575
+ }
2576
+ for (const [oppId, items] of byOpp) {
2577
+ const opp = oppSlugById.get(oppId);
2578
+ if (!opp) continue;
2579
+ if (listQuotes(dir, opp.slug).some((q) => q.dealName === opp.dealName)) {
2580
+ result.skipped++;
2581
+ continue;
2582
+ }
2583
+ const quoteLineItems = items.map((li) => {
2584
+ const quantity = typeof li.Quantity === "number" && li.Quantity > 0 ? li.Quantity : 1;
2585
+ const unitPrice = typeof li.UnitPrice === "number" ? li.UnitPrice : typeof li.TotalPrice === "number" ? li.TotalPrice / quantity : 0;
2586
+ return {
2587
+ description: li.Product2?.Name ?? li.Description ?? "Line item",
2588
+ quantity,
2589
+ unitPrice
2590
+ };
2591
+ });
2592
+ try {
2593
+ await generateQuote(dir, {
2594
+ slug: opp.slug,
2595
+ dealName: opp.dealName,
2596
+ lineItems: quoteLineItems
2597
+ });
2598
+ result.quotesImported = (result.quotesImported ?? 0) + 1;
2599
+ } catch (err) {
2600
+ result.errors.push(`LineItems for '${opp.dealName}': ${err.message}`);
2601
+ }
2602
+ }
2603
+ }
2604
+ for (const note of notes) {
2605
+ const slug = note.ParentId ? slugMap.get(note.ParentId) : void 0;
2606
+ if (!slug) {
2607
+ result.skipped++;
2608
+ continue;
2609
+ }
2610
+ const sourceRef = `salesforce://note/${note.Id}`;
2611
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2612
+ result.skipped++;
2613
+ continue;
2614
+ }
2615
+ const date = (note.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
2616
+ const title = note.Title ?? "Salesforce Note";
2617
+ try {
2618
+ await appendInteraction(dir, slug, {
2619
+ date,
2620
+ type: "Note",
2621
+ with: slug,
2622
+ subject: title,
2623
+ summary: `${title}${note.Body ? `: ${note.Body}` : ""}`.slice(0, 500),
2624
+ nextSteps: [],
2625
+ sourceRef,
2626
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2627
+ });
2628
+ result.notesImported = (result.notesImported ?? 0) + 1;
2629
+ } catch (err) {
2630
+ result.errors.push(`Note ${note.Id}: ${err.message}`);
2631
+ }
2632
+ }
2633
+ for (const cm of campaignMembers) {
2634
+ const slug = cm.ContactId ? slugMap.get(cm.ContactId) : cm.LeadId ? slugMap.get(cm.LeadId) : void 0;
2635
+ if (!slug) {
2636
+ result.skipped++;
2637
+ continue;
2638
+ }
2639
+ const sourceRef = `salesforce://campaignmember/${cm.Id}`;
2640
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2641
+ result.skipped++;
2642
+ continue;
2643
+ }
2644
+ const campaignName = cm.Campaign?.Name ?? cm.CampaignId ?? "Unknown campaign";
2645
+ try {
2646
+ await appendInteraction(dir, slug, {
2647
+ date: (cm.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10),
2648
+ type: "Note",
2649
+ with: slug,
2650
+ subject: `Campaign: ${campaignName}`,
2651
+ summary: `Salesforce Campaign: ${campaignName} (status: ${cm.Status ?? "n/a"})`,
2652
+ nextSteps: [],
2653
+ sourceRef,
2654
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2655
+ });
2656
+ result.campaignsImported = (result.campaignsImported ?? 0) + 1;
2657
+ } catch (err) {
2658
+ result.errors.push(`CampaignMember ${cm.Id}: ${err.message}`);
2659
+ }
2660
+ }
2661
+ return result;
2662
+ }
2663
+ async function runPipedriveApiImport(opts, dir = process.cwd()) {
2664
+ const result = {
2665
+ customersCreated: 0,
2666
+ interactionsImported: 0,
2667
+ skipped: 0,
2668
+ errors: []
2669
+ };
2670
+ const token = opts.token ?? process.env["PIPEDRIVE_TOKEN"] ?? "";
2671
+ const instanceUrl = opts.url ?? process.env["PIPEDRIVE_URL"] ?? "";
2672
+ if (!token || !instanceUrl) {
2673
+ result.errors.push("Pipedrive API mode requires --token and --url (or PIPEDRIVE_TOKEN + PIPEDRIVE_URL env vars)");
2674
+ return result;
2675
+ }
2676
+ const { fetchPipedrivePersons, fetchPipedriveActivities } = await import("./pipedrive-client-CdGKpH9b.js");
2677
+ let persons;
2678
+ let activities;
2679
+ try {
2680
+ [persons, activities] = await Promise.all([fetchPipedrivePersons(instanceUrl, token), fetchPipedriveActivities(instanceUrl, token)]);
2681
+ } catch (err) {
2682
+ result.errors.push(`Pipedrive API: ${err.message}`);
2683
+ return result;
2684
+ }
2685
+ if (opts.dryRun) {
2686
+ console.log(info(`Dry run — ${persons.length} persons, ${activities.length} activities from Pipedrive`));
2687
+ return result;
2688
+ }
2689
+ const slugByPersonId = /* @__PURE__ */ new Map();
2690
+ const slugByOrgId = /* @__PURE__ */ new Map();
2691
+ for (const person of persons) {
2692
+ const name = (person.org_name ?? person.name ?? "").trim();
2693
+ if (!name) continue;
2694
+ const email = person.primary_email ?? "";
2695
+ try {
2696
+ const { slug, created } = ensureCustomer(dir, name, "", email, false);
2697
+ if (person.id) slugByPersonId.set(person.id, slug);
2698
+ if (person.org_id?.value) slugByOrgId.set(person.org_id.value, slug);
2699
+ if (created) result.customersCreated++;
2700
+ } catch (err) {
2701
+ result.errors.push(`Person '${name}': ${err.message}`);
2702
+ }
2703
+ }
2704
+ for (const activity of activities) {
2705
+ const slug = (activity.person_id && slugByPersonId.get(activity.person_id)) ?? (activity.org_id && slugByOrgId.get(activity.org_id)) ?? void 0;
2706
+ if (!slug) continue;
2707
+ const sourceRef = `pipedrive://activity/${activity.id}`;
2708
+ const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
2709
+ if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
2710
+ result.skipped++;
2711
+ continue;
2712
+ }
2713
+ const date = activity.due_date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2714
+ const notes = (activity.note ?? activity.subject ?? "").slice(0, 500);
2715
+ const t = (activity.type ?? "").toLowerCase();
2716
+ const type = t === "call" ? "Call" : t === "email" ? "Email" : t === "meeting" ? "Meeting" : "Note";
2717
+ try {
2718
+ await appendInteraction(dir, slug, {
2719
+ date,
2720
+ type,
2721
+ with: slug,
2722
+ summary: `${activity.subject ?? type}: ${notes}`,
2723
+ nextSteps: [],
2724
+ sourceRef,
2725
+ synced: (/* @__PURE__ */ new Date()).toISOString()
2726
+ });
2727
+ result.interactionsImported++;
2728
+ } catch (err) {
2729
+ result.errors.push(`Activity ${activity.id}: ${err.message}`);
2730
+ }
2731
+ }
2732
+ return result;
2733
+ }
2734
+ const importCommand = new Command("import").description("Import customers and interactions from HubSpot, Salesforce, Pipedrive, or CSV").argument("[path]", "Path to export file or directory").option("--from <source>", "Source CRM: hubspot | csv | salesforce | pipedrive", "csv").option("--dry-run", "Preview what would be imported without writing").option("--mode <mode>", "Import mode: file | api").option("--token <token>", "API token (Salesforce, Pipedrive, HubSpot)").option("--url <url>", "Instance URL (e.g. https://myco.salesforce.com)").option("--analyze", "Analyze export and show what would be imported (no write)").option("--resume", "Resume a previously interrupted import").option("--owner-map <mapping>", "Map HubSpot owner emails to reps: \"alice@hs.com=alice,bob@hs.com=bob\"").action(async (sourcePath, opts) => {
2735
+ const dryRun = opts.dryRun ?? false;
2736
+ const ownerMap = {};
2737
+ if (opts.ownerMap) for (const pair of opts.ownerMap.split(",")) {
2738
+ const [hs, rep] = pair.split("=");
2739
+ if (hs && rep) ownerMap[hs.trim()] = rep.trim();
2740
+ }
2741
+ if (opts.analyze && opts.from === "hubspot" && sourcePath) {
2742
+ const { analyzeHubSpotExport } = await import("./import-hubspot-BaK71U_K.js");
2743
+ const analysis = await analyzeHubSpotExport(sourcePath);
2744
+ console.log(bold("\nDatasynxOpenCRM — HubSpot Import Analysis"));
2745
+ console.log("==========================================");
2746
+ console.log(info(`Companies: ${analysis.companiesFound}`));
2747
+ console.log(info(`Contacts: ${analysis.contactsFound} (${analysis.unmappedContacts} unmapped companies)`));
2748
+ console.log(info(`Deals: ${analysis.dealsFound}`));
2749
+ console.log(info(`Engagements: ${analysis.engagementsFound}`));
2750
+ if (analysis.customPropertiesDetected.length > 0) {
2751
+ console.log(info(`\nCustom Properties: ${analysis.customPropertiesDetected.length} detected`));
2752
+ console.log(` ${analysis.customPropertiesDetected.slice(0, 10).join(", ")}${analysis.customPropertiesDetected.length > 10 ? " ..." : ""}`);
2753
+ }
2754
+ if (analysis.ownersDetected.length > 0) {
2755
+ console.log(info(`\nOwners detected: ${analysis.ownersDetected.join(", ")}`));
2756
+ console.log(` Use --owner-map "${analysis.ownersDetected.map((o) => `${o}=<rep>`).join(",")}"`);
2757
+ }
2758
+ if (analysis.unknownStages.length > 0) console.log(info(`\nUnknown stages (→ "qualified"): ${analysis.unknownStages.join(", ")}`));
2759
+ console.log(info(`\nEstimated import time: ~${analysis.estimatedMinutes} min`));
2760
+ console.log(info(`\nRun without --analyze to start import.`));
2761
+ return;
2762
+ }
2763
+ if (!dryRun) console.log(info(`Importing from ${bold(opts.from)}: ${sourcePath}`));
2764
+ const result = await runImport(sourcePath, {
2765
+ from: opts.from,
2766
+ ...dryRun ? { dryRun: true } : {},
2767
+ ...opts.mode ? { mode: opts.mode } : {},
2768
+ ...opts.token ? { token: opts.token } : {},
2769
+ ...opts.url ? { url: opts.url } : {},
2770
+ ...opts.resume ? { resume: true } : {},
2771
+ ownerMap
2772
+ }, process.env["DXCRM_DATA_DIR"]);
2773
+ if (!dryRun) {
2774
+ console.log(success(`✓ Import complete:`));
2775
+ console.log(info(` Customers created: ${result.customersCreated}`));
2776
+ console.log(info(` Interactions imported: ${result.interactionsImported}`));
2777
+ console.log(info(` Skipped (duplicates): ${result.skipped}`));
2778
+ if (result.errors.length > 0) {
2779
+ console.log(error(` Errors (${result.errors.length}):`));
2780
+ result.errors.slice(0, 5).forEach((e) => console.log(error(` ${e}`)));
2781
+ }
2782
+ }
2783
+ });
2784
+ //#endregion
2785
+ //#region src/commands/server.ts
2786
+ function getPidFile(dataDir) {
2787
+ return path.join(dataDir, ".agentic", "server.pid");
2788
+ }
2789
+ async function runServerStart(opts, dataDir) {
2790
+ const port = parseInt(opts.port ?? "3847", 10);
2791
+ const dir = opts.data ?? dataDir ?? process.cwd();
2792
+ if (opts.data) process.env["DXCRM_DATA_DIR"] = opts.data;
2793
+ const pidFile = getPidFile(dir);
2794
+ if (fs.existsSync(pidFile)) {
2795
+ const existing = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
2796
+ if (!isNaN(existing)) try {
2797
+ process.kill(existing, 0);
2798
+ console.log(info(`Server already running (PID ${existing})`));
2799
+ return;
2800
+ } catch {}
2801
+ }
2802
+ const serverEntry = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/mcp/server.js");
2803
+ const env = {
2804
+ ...process.env,
2805
+ DXCRM_MCP_MODE: "http",
2806
+ DXCRM_MCP_PORT: String(port)
2807
+ };
2808
+ if (opts.data) env["DXCRM_DATA_DIR"] = opts.data;
2809
+ const child = spawn(process.execPath, [serverEntry], {
2810
+ detached: true,
2811
+ stdio: "ignore",
2812
+ env
2813
+ });
2814
+ child.unref();
2815
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
2816
+ fs.writeFileSync(pidFile, String(child.pid), "utf-8");
2817
+ const hostname = os.hostname();
2818
+ console.log(success(`DatasynxOpenCRM server running on http://0.0.0.0:${port}/mcp`));
2819
+ console.log(info(`Data dir: ${dir}`));
2820
+ console.log(info(`Add to your AI framework config: url: http://${hostname}:${port}/mcp`));
2821
+ }
2822
+ function runServerStatus(dataDir) {
2823
+ const pidFile = getPidFile(process.env["DXCRM_DATA_DIR"] ?? dataDir ?? process.cwd());
2824
+ if (!fs.existsSync(pidFile)) {
2825
+ console.log(info("Server: not running."));
2826
+ return;
2827
+ }
2828
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
2829
+ if (isNaN(pid)) {
2830
+ console.log(info("Server: not running (invalid PID file)."));
2831
+ return;
2832
+ }
2833
+ try {
2834
+ process.kill(pid, 0);
2835
+ console.log(success(`Server: running (PID ${pid})`));
2836
+ } catch {
2837
+ console.log(info("Server: not running (stale PID file)."));
2838
+ try {
2839
+ fs.unlinkSync(pidFile);
2840
+ } catch {}
2841
+ }
2842
+ }
2843
+ const serverCommand = new Command("server");
2844
+ serverCommand.command("start").description("Start the DatasynxOpenCRM HTTP MCP server").option("--port <port>", "HTTP port (default 3847)", "3847").option("--data <dir>", "Data directory (sets DXCRM_DATA_DIR)").action((opts) => runServerStart(opts));
2845
+ serverCommand.command("status").description("Check if the server is running").action(() => runServerStatus());
2846
+ //#endregion
2847
+ //#region src/commands/audit.ts
2848
+ const SEP = "─".repeat(70);
2849
+ async function runAudit(opts, dataDir) {
2850
+ const dir = dataDir ?? process.cwd();
2851
+ const limit = opts.limit ?? 20;
2852
+ const entries = filterAuditLog(readAuditLog(dir), {
2853
+ ...opts.slug !== void 0 ? { slug: opts.slug } : {},
2854
+ ...opts.actor !== void 0 ? { actor: opts.actor } : {},
2855
+ limit
2856
+ });
2857
+ console.log(SEP);
2858
+ console.log(" DatasynxOpenCRM — Audit Trail");
2859
+ if (opts.slug) console.log(` Customer: ${opts.slug}`);
2860
+ if (opts.actor) console.log(` Actor: ${opts.actor}`);
2861
+ console.log(SEP);
2862
+ if (entries.length === 0) {
2863
+ console.log(" No audit entries found.");
2864
+ console.log(SEP);
2865
+ return;
2866
+ }
2867
+ for (const entry of entries) console.log(` ${entry.timestamp} ${entry.actor.padEnd(12)} ${entry.tool.padEnd(20)} ${entry.slug.padEnd(20)} ${entry.summary}`);
2868
+ console.log(SEP);
2869
+ console.log(` ${entries.length} entr${entries.length === 1 ? "y" : "ies"} shown`);
2870
+ console.log(SEP);
2871
+ }
2872
+ const auditCommand = new Command("audit").description("Show CRM audit trail — who changed what and when").option("--slug <slug>", "Filter by customer slug").option("--actor <actor>", "Filter by actor").option("--limit <n>", "Number of entries to show (default: 20)", parseInt).option("--tail", "Show all new entries (simplified: shows current entries)").action((opts) => runAudit(opts, process.env["DXCRM_DATA_DIR"]));
2873
+ //#endregion
2874
+ //#region src/commands/rbac.ts
2875
+ const ROLES = [
2876
+ "admin",
2877
+ "manager",
2878
+ "rep"
2879
+ ];
2880
+ async function runRbacSet(actor, role, dataDir) {
2881
+ const dir = dataDir ?? process.cwd();
2882
+ if (!ROLES.includes(role)) {
2883
+ console.error(error(`✗ Invalid role '${role}'. Must be: ${ROLES.join(", ")}`));
2884
+ process.exit(1);
2885
+ }
2886
+ setActorRole(dir, actor, role);
2887
+ console.log(success(`✓ ${bold(actor)} → ${bold(role)}`));
2888
+ }
2889
+ async function runRbacShow(dataDir) {
2890
+ const config = getRbacConfig(dataDir ?? process.cwd());
2891
+ const entries = Object.entries(config.actors);
2892
+ if (entries.length === 0) {
2893
+ console.log(info("No RBAC roles configured. All actors default to 'rep'."));
2894
+ return;
2895
+ }
2896
+ console.log(bold(`\n RBAC Roles\n`));
2897
+ for (const [actor, role] of entries) console.log(info(` ${bold(actor).padEnd(20)} ${role}`));
2898
+ if (config.default) console.log(info(`\n Default: ${config.default}`));
2899
+ console.log("");
2900
+ }
2901
+ async function runRbacCheck(actor, tool, dataDir) {
2902
+ const dir = dataDir ?? process.cwd();
2903
+ const { getRole } = await import("./rbac-C7c8tcES.js");
2904
+ const role = getRole(dir, actor);
2905
+ if (canWrite(role, tool)) console.log(success(`✓ ${actor} (${role}) CAN use '${tool}'`));
2906
+ else console.log(error(`✗ ${actor} (${role}) CANNOT use '${tool}'`));
2907
+ }
2908
+ const rbacCommand = new Command("rbac").description("Manage role-based access control");
2909
+ rbacCommand.command("set <actor> <role>").description(`Assign role to actor (roles: ${ROLES.join(", ")})`).action((actor, role) => runRbacSet(actor, role, process.env["DXCRM_DATA_DIR"]));
2910
+ rbacCommand.command("show").alias("list").description("List all RBAC role assignments").action(() => runRbacShow(process.env["DXCRM_DATA_DIR"]));
2911
+ rbacCommand.command("check <actor> <tool>").description("Check if an actor can use a specific tool").action((actor, tool) => runRbacCheck(actor, tool, process.env["DXCRM_DATA_DIR"]));
2912
+ //#endregion
2913
+ //#region src/commands/gdpr.ts
2914
+ function erasuresPath(dataDir) {
2915
+ return path.join(dataDir, ".agentic", "gdpr-erasures.json");
2916
+ }
2917
+ function readErasures(dataDir) {
2918
+ const p = erasuresPath(dataDir);
2919
+ if (!fs.existsSync(p)) return [];
2920
+ try {
2921
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
2922
+ } catch {
2923
+ return [];
2924
+ }
2925
+ }
2926
+ function appendErasure(dataDir, record) {
2927
+ const p = erasuresPath(dataDir);
2928
+ fs.mkdirSync(path.dirname(p), { recursive: true });
2929
+ const existing = readErasures(dataDir);
2930
+ existing.push(record);
2931
+ fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
2932
+ }
2933
+ async function runGdprErase(slug, opts, dataDir) {
2934
+ const dir = dataDir ?? process.cwd();
2935
+ const customerDir = path.join(dir, "customers", slug);
2936
+ if (!opts.confirm) {
2937
+ console.log(info(`Dry run — would permanently erase: ${bold(slug)}`));
2938
+ console.log(info(` Directory: ${customerDir}`));
2939
+ console.log(info(` Audit log entry will be written to .agentic/audit.log`));
2940
+ console.log(info(` Erasure record will be added to .agentic/gdpr-erasures.json`));
2941
+ console.log(info(`\n To proceed: dxcrm gdpr erase ${slug} --confirm`));
2942
+ return;
2943
+ }
2944
+ if (!fs.existsSync(customerDir)) console.warn(info(` Customer '${slug}' directory not found — may already be erased.`));
2945
+ else {
2946
+ fs.rmSync(customerDir, {
2947
+ recursive: true,
2948
+ force: true
2949
+ });
2950
+ try {
2951
+ const { dropCustomerTable } = await import("./lancedb-CCBbpulq.js");
2952
+ await dropCustomerTable(dir, slug);
2953
+ } catch {}
2954
+ }
2955
+ const globalQueuePath = path.join(dir, ".agentic", "agent-queue.json");
2956
+ if (fs.existsSync(globalQueuePath)) await withJsonFile(globalQueuePath, (tasks) => (Array.isArray(tasks) ? tasks : []).filter((t) => t.slug !== slug));
2957
+ const goalsPath = path.join(dir, ".agentic", "goals.json");
2958
+ if (fs.existsSync(goalsPath)) {
2959
+ const { readGoals, writeGoals } = await import("./goal-engine-CUZSpERI.js");
2960
+ writeGoals(dir, readGoals(dir).map((g) => ({
2961
+ ...g,
2962
+ decomposition: {
2963
+ ...g.decomposition,
2964
+ subGoals: g.decomposition.subGoals.filter((sg) => sg.slug !== slug)
2965
+ }
2966
+ })));
2967
+ }
2968
+ const { readSubscriptions, writeSubscriptions } = await import("./push-manager-CowY-0IK.js");
2969
+ const subs = await readSubscriptions(dir);
2970
+ const remaining = subs.filter((s) => s.slug !== slug);
2971
+ if (remaining.length !== subs.length) await writeSubscriptions(dir, remaining);
2972
+ const actor = getActor();
2973
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2974
+ writeAuditEntry(dir, {
2975
+ timestamp: now,
2976
+ actor,
2977
+ tool: "gdpr_erase",
2978
+ slug,
2979
+ summary: "Customer data permanently erased"
2980
+ });
2981
+ appendErasure(dir, {
2982
+ slug,
2983
+ erasedAt: now,
2984
+ erasedBy: actor,
2985
+ reason: "GDPR Art. 17 request"
2986
+ });
2987
+ console.log(success(`✓ Customer '${bold(slug)}' erased.`));
2988
+ console.log(info(` Deletion logged to .agentic/audit.log`));
2989
+ console.log(info(` Record added to .agentic/gdpr-erasures.json`));
2990
+ }
2991
+ async function runGdprListErasures(dataDir) {
2992
+ const records = readErasures(dataDir ?? process.cwd());
2993
+ if (records.length === 0) {
2994
+ console.log(info("No erasures on record."));
2995
+ return;
2996
+ }
2997
+ console.log(bold(`\n GDPR Erasures (${records.length})\n`));
2998
+ for (const r of records) console.log(info(` ${r.erasedAt} ${r.erasedBy.padEnd(12)} ${r.slug} — ${r.reason}`));
2999
+ console.log("");
3000
+ }
3001
+ const gdprCommand = new Command("gdpr").description("GDPR compliance tools");
3002
+ gdprCommand.command("erase <slug>").description("Permanently erase all data for a customer (Art. 17 right to erasure)").option("--confirm", "Confirm permanent deletion (required)").action((slug, opts) => runGdprErase(slug, opts, process.env["DXCRM_DATA_DIR"]));
3003
+ gdprCommand.command("list-erasures").description("Show history of GDPR erasures").action(() => runGdprListErasures(process.env["DXCRM_DATA_DIR"]));
3004
+ //#endregion
3005
+ //#region src/commands/security-report.ts
3006
+ const REPORT = `# DatasynxOpenCRM — Security Report
3007
+
3008
+ Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
3009
+
3010
+ ## 1. Data Storage
3011
+
3012
+ - **Location**: Local filesystem only. All data lives in \`customers/\` and \`.agentic/\` directories on your infrastructure.
3013
+ - **External transmission**: None by default. Data is never sent to Datasynx servers.
3014
+ - **Cloud dependencies**: None for core functionality. Optional integrations (Gmail, Anthropic API) are explicitly configured.
3015
+
3016
+ ## 2. Authentication & Authorization
3017
+
3018
+ - **Phase 3**: No authentication on HTTP MCP server. Restrict access via firewall or VPN (port 3847 should be on private network only).
3019
+ - **Phase 4 (RBAC)**: Role-based access control via \`.agentic/rbac.json\`. Roles: admin, manager, rep. Enforced per MCP tool call.
3020
+ - **Actor identity**: \`DXCRM_ACTOR\` environment variable. No cryptographic identity in Phase 3.
3021
+
3022
+ ## 3. Encryption
3023
+
3024
+ - **At rest**: Not encrypted at application level. Use OS-level disk encryption (LUKS on Linux, FileVault on macOS).
3025
+ - **In transit**: HTTP (no TLS) in Phase 3. Use a reverse proxy (nginx + Let's Encrypt) or VPN for TLS.
3026
+ - **Recommendation**: Deploy behind Tailscale or WireGuard for team access.
3027
+
3028
+ ## 4. Audit Trail
3029
+
3030
+ - **File**: \`.agentic/audit.log\` — append-only, one line per entry.
3031
+ - **Format**: \`timestamp | actor | tool | customer | summary\`
3032
+ - **Coverage**: All write operations (\`log_interaction\`, \`update_deal\`, \`gdpr_erase\`).
3033
+ - **Tamper evidence**: Phase 3: none. Phase 4+: hash chaining planned.
3034
+ - **Retention**: Indefinite (append-only, never deleted by the application).
3035
+
3036
+ ## 5. Network Calls
3037
+
3038
+ The following external services are contacted when configured:
3039
+
3040
+ | Service | When | Data sent |
3041
+ |---|---|---|
3042
+ | Gmail API | Gmail sync enabled + credentials configured | Email headers + snippets |
3043
+ | Anthropic API | \`ANTHROPIC_API_KEY\` set | Email/transcript content for summarization |
3044
+ | Telegram Bot API | Agent notifications enabled + token set | Customer slug + context excerpt (≤800 chars) |
3045
+ | Microsoft Graph | Microsoft sync configured | Email headers + snippets |
3046
+
3047
+ **No telemetry, no analytics, no usage data is sent to Datasynx.**
3048
+
3049
+ ## 6. Data Residency
3050
+
3051
+ DatasynxOpenCRM runs entirely on customer-controlled infrastructure. Data never leaves the deployment environment without explicit integration configuration.
3052
+
3053
+ This makes EU data residency guarantees straightforward — a key differentiator vs cloud CRMs.
3054
+
3055
+ ## 7. GDPR Compliance
3056
+
3057
+ - **Right to erasure (Art. 17)**: \`dxcrm gdpr erase <slug> --confirm\` permanently deletes all customer data.
3058
+ - **Erasure log**: \`.agentic/gdpr-erasures.json\` records what was deleted, when, and by whom.
3059
+ - **Audit trail**: Every write operation is attributed to an actor.
3060
+ - **Data portability (Art. 20)**: \`dxcrm export <slug>\` exports all data as JSON or Markdown.
3061
+
3062
+ ## 8. SOC 2 Readiness
3063
+
3064
+ - **Audit log**: Available from Phase 3 (audit trail covers all write operations).
3065
+ - **SOC 2 Type 2**: Requires 6 months of consistent audit logs. Apply after Phase 4 completion.
3066
+ - **Security review questionnaire**: This document serves as the primary answer document.
3067
+
3068
+ ## 9. Dependencies (Key Packages)
3069
+
3070
+ | Package | Purpose | Cloud dependency? |
3071
+ |---|---|---|
3072
+ | \`@modelcontextprotocol/sdk\` | MCP server | No |
3073
+ | \`@googleapis/gmail\`, \`@googleapis/calendar\` | Gmail/Calendar sync | Optional — only if configured |
3074
+ | \`@anthropic-ai/sdk\` | LLM summarization | Optional — only if API key set |
3075
+ | \`lancedb\` | Local vector search | No — embedded DB |
3076
+ | \`gray-matter\` | Markdown frontmatter | No |
3077
+ | \`commander\` | CLI framework | No |
3078
+ | \`zod\` | Schema validation | No |
3079
+ | \`cron\` | Background sync | No |
3080
+
3081
+ ## 10. Incident Response
3082
+
3083
+ - **Data breach**: Filesystem-only — scope is limited to the deployment host.
3084
+ - **Revoke access**: Remove actor from \`.agentic/rbac.json\`, rotate \`DXCRM_ACTOR\` env var.
3085
+ - **Audit**: \`dxcrm audit --actor <actor>\` shows all actions by a specific user.
3086
+ `;
3087
+ async function runSecurityReport(opts, _dataDir) {
3088
+ const report = REPORT;
3089
+ if (opts.output) {
3090
+ const outputPath = path.resolve(opts.output);
3091
+ fs.writeFileSync(outputPath, report, "utf-8");
3092
+ console.log(success(`✓ Security report written to: ${outputPath}`));
3093
+ } else console.log(report);
3094
+ }
3095
+ const securityReportCommand = new Command("security-report").description("Generate security questionnaire answer document for enterprise reviews").option("--output <file>", "Write report to file instead of stdout").action((opts) => runSecurityReport(opts, process.env["DXCRM_DATA_DIR"]));
3096
+ //#endregion
3097
+ //#region src/commands/pipeline-stages.ts
3098
+ function printStagesTable(stages) {
3099
+ console.log(bold("\n Pipeline Stages\n"));
3100
+ console.log(info(` ${"ID".padEnd(20)} ${"Label".padEnd(20)} ${"Order".padEnd(8)} ${"Prob%".padEnd(8)} ${"Final".padEnd(6)} Color`));
3101
+ console.log(info(` ${"─".repeat(72)}`));
3102
+ for (const s of stages) {
3103
+ const prob = s.probability !== void 0 ? String(s.probability) : "-";
3104
+ const isFinal = s.isFinal ? "yes" : "no";
3105
+ const color = s.color ?? "-";
3106
+ console.log(info(` ${s.id.padEnd(20)} ${s.label.padEnd(20)} ${String(s.order).padEnd(8)} ${prob.padEnd(8)} ${isFinal.padEnd(6)} ${color}`));
3107
+ }
3108
+ console.log("");
3109
+ }
3110
+ const stagesCommand = new Command("stages").description("Manage custom pipeline stages");
3111
+ stagesCommand.command("list").description("List all configured pipeline stages").action(() => {
3112
+ printStagesTable(getPipelineStages(process.env["DXCRM_DATA_DIR"] ?? process.cwd()));
3113
+ });
3114
+ stagesCommand.command("set <id> <label>").description("Create or update a pipeline stage").option("--order <n>", "Sort order (number)", "1").option("--probability <n>", "Default win probability 0-100").option("--color <hex>", "Hex color code (e.g. #3B82F6)").option("--final", "Mark as final stage (won/lost)").action((id, label, opts) => {
3115
+ setPipelineStage(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
3116
+ id,
3117
+ label,
3118
+ order: parseInt(opts.order, 10),
3119
+ ...opts.probability !== void 0 ? { probability: parseInt(opts.probability, 10) } : {},
3120
+ ...opts.color ? { color: opts.color } : {},
3121
+ ...opts.final ? { isFinal: true } : {}
3122
+ });
3123
+ console.log(success(`✓ Stage '${id}' saved`));
3124
+ });
3125
+ stagesCommand.command("delete <id>").description("Delete a pipeline stage by ID").action((id) => {
3126
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3127
+ if (!getPipelineStages(dataDir).find((s) => s.id === id)) {
3128
+ console.error(error(`✗ Stage '${id}' not found`));
3129
+ process.exit(1);
3130
+ }
3131
+ deletePipelineStage(dataDir, id);
3132
+ console.log(success(`✓ Stage '${id}' deleted`));
3133
+ });
3134
+ stagesCommand.command("reset").description("Reset pipeline stages to defaults").action(() => {
3135
+ resetToDefaults(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
3136
+ console.log(success("✓ Pipeline stages reset to defaults"));
3137
+ });
3138
+ //#endregion
3139
+ //#region src/core/plugin-registry.ts
3140
+ const _plugins = /* @__PURE__ */ new Map();
3141
+ function getPlugin(name) {
3142
+ return _plugins.get(name);
3143
+ }
3144
+ function listPlugins() {
3145
+ return Array.from(_plugins.values());
3146
+ }
3147
+ //#endregion
3148
+ //#region src/commands/plugin.ts
3149
+ const pluginCommand = new Command("plugin").description("Manage dxcrm plugins");
3150
+ pluginCommand.command("list").description("List all registered plugins").action(() => {
3151
+ const plugins = listPlugins();
3152
+ if (plugins.length === 0) {
3153
+ console.log(info("No plugins registered."));
3154
+ return;
3155
+ }
3156
+ console.log(bold(`\n dxcrm Plugins (${plugins.length})\n`));
3157
+ for (const p of plugins) console.log(info(` ${p.name.padEnd(20)} v${p.version} ${p.description ?? ""}`));
3158
+ console.log("");
3159
+ });
3160
+ pluginCommand.command("info <name>").description("Show info about a registered plugin").action((name) => {
3161
+ const plugin = getPlugin(name);
3162
+ if (!plugin) {
3163
+ console.log(error(`Plugin '${name}' not found.`));
3164
+ process.exit(1);
3165
+ }
3166
+ console.log(bold(`\n Plugin: ${plugin.name}\n`));
3167
+ console.log(info(` Version: ${plugin.version}`));
3168
+ if (plugin.description) console.log(info(` Description: ${plugin.description}`));
3169
+ if (plugin.mcpTools?.length) console.log(info(` MCP Tools: ${plugin.mcpTools.join(", ")}`));
3170
+ console.log("");
3171
+ });
3172
+ //#endregion
3173
+ //#region src/commands/goal.ts
3174
+ async function runGoalSet(description, options) {
3175
+ const goal = await pursueGoal(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
3176
+ description,
3177
+ deadline: options.deadline
3178
+ });
3179
+ console.log(success(`✓ Goal created: ${bold(goal.id)}`));
3180
+ console.log(info(` Description : ${goal.description}`));
3181
+ console.log(info(` Target : €${goal.target.toLocaleString()}`));
3182
+ console.log(info(` Deadline : ${goal.deadline}`));
3183
+ console.log(info(` Pipeline P50: €${goal.decomposition.currentPipeline.toLocaleString()}`));
3184
+ console.log(info(` Gap : €${goal.decomposition.gap.toLocaleString()}`));
3185
+ if (goal.decomposition.subGoals.length > 0) {
3186
+ console.log(bold("\n Action Plan:"));
3187
+ for (const sg of goal.decomposition.subGoals) {
3188
+ console.log(info(` ${sg.priority}. ${sg.action}`));
3189
+ console.log(info(` → ${sg.nextStep}`));
3190
+ }
3191
+ }
3192
+ }
3193
+ async function runGoalStatus() {
3194
+ const goals = getActiveGoals(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
3195
+ if (goals.length === 0) {
3196
+ console.log(info("No active goals. Use `dxcrm goal set` to create one."));
3197
+ return;
3198
+ }
3199
+ console.log(bold(`\n Active Goals (${goals.length})\n`));
3200
+ for (const g of goals) {
3201
+ const bar = "█".repeat(Math.round(g.progress / 10)) + "░".repeat(10 - Math.round(g.progress / 10));
3202
+ const deadlineMs = new Date(g.deadline).getTime() - Date.now();
3203
+ const daysLeft = Math.max(0, Math.ceil(deadlineMs / 864e5));
3204
+ console.log(bold(` ${g.id}`));
3205
+ console.log(info(` ${g.description}`));
3206
+ console.log(info(` [${bar}] ${g.progress}% | €${g.target.toLocaleString()} by ${g.deadline} (${daysLeft}d left)`));
3207
+ console.log("");
3208
+ }
3209
+ }
3210
+ async function runGoalUpdate(goalId, options) {
3211
+ const dir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3212
+ const progress = parseInt(options.progress, 10);
3213
+ if (isNaN(progress) || progress < 0 || progress > 100) {
3214
+ console.error(error("✗ --progress must be a number 0–100"));
3215
+ process.exit(1);
3216
+ }
3217
+ if (!await updateGoalProgress(dir, goalId, progress)) {
3218
+ console.error(error(`✗ Goal '${goalId}' not found`));
3219
+ process.exit(1);
3220
+ }
3221
+ console.log(success(`✓ Goal ${bold(goalId)} progress updated to ${bold(String(progress))}%`));
3222
+ }
3223
+ async function runGoalCancel(goalId) {
3224
+ if (!await cancelGoal(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), goalId)) {
3225
+ console.error(error(`✗ Goal '${goalId}' not found`));
3226
+ process.exit(1);
3227
+ }
3228
+ console.log(success(`✓ Goal ${bold(goalId)} cancelled`));
3229
+ }
3230
+ const goalCommand = new Command("goal").description("Manage goals and action plans");
3231
+ goalCommand.command("set <description>").description("Set a new goal and get a decomposed action plan").requiredOption("--deadline <date>", "Target deadline (YYYY-MM-DD)").action((description, opts) => runGoalSet(description, opts));
3232
+ goalCommand.command("status").description("Show all active goals with progress").action(() => runGoalStatus());
3233
+ goalCommand.command("update <goalId>").description("Update goal progress percentage").requiredOption("--progress <n>", "Progress 0–100").action((goalId, opts) => runGoalUpdate(goalId, opts));
3234
+ goalCommand.command("cancel <goalId>").description("Cancel an active goal").action((goalId) => runGoalCancel(goalId));
3235
+ //#endregion
3236
+ //#region src/commands/push.ts
3237
+ async function runPushRegister(slug, options) {
3238
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3239
+ const providerData = {};
3240
+ if (options.topicName) providerData["gmailTopicName"] = options.topicName;
3241
+ if (options.clientState) providerData["microsoftClientState"] = options.clientState;
3242
+ if (options.resource) providerData["microsoftResource"] = options.resource;
3243
+ if (options.teamId) providerData["slackTeamId"] = options.teamId;
3244
+ if (options.channelId) providerData["slackChannelId"] = options.channelId;
3245
+ const sub = await register(dataDir, options.provider, slug, {
3246
+ webhookUrl: options.webhookUrl,
3247
+ providerData
3248
+ });
3249
+ console.log(success(`✓ Push subscription registered: ${bold(sub.id)}`));
3250
+ console.log(info(` Provider : ${sub.provider}`));
3251
+ console.log(info(` Slug : ${sub.slug}`));
3252
+ console.log(info(` Webhook : ${sub.webhookUrl}`));
3253
+ console.log(info(` Expires : ${sub.expiresAt ?? "never"}`));
3254
+ if (options.webhookUrl.includes("localhost")) {
3255
+ console.log(info(` ⚠ Warning: localhost URLs cannot be reached by external providers.`));
3256
+ console.log(info(` Use a tunnel: ngrok http 3847`));
3257
+ }
3258
+ }
3259
+ async function runPushStatus(options) {
3260
+ let subs = await readSubscriptions(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
3261
+ if (options.slug) subs = subs.filter((s) => s.slug === options.slug);
3262
+ if (options.provider) subs = subs.filter((s) => s.provider === options.provider);
3263
+ if (subs.length === 0) {
3264
+ console.log(info("No push subscriptions registered. Use `dxcrm push register` to add one."));
3265
+ return;
3266
+ }
3267
+ const now = Date.now();
3268
+ console.log(bold(`\n Push Subscriptions (${subs.length})\n`));
3269
+ for (const s of subs) {
3270
+ const expiresIn = s.expiresAt ? Math.round((new Date(s.expiresAt).getTime() - now) / (3600 * 1e3)) : null;
3271
+ const expiryStr = expiresIn !== null ? `${expiresIn}h remaining` : "no expiry";
3272
+ const needsRenewal = s.expiresAt !== null && new Date(s.expiresAt).getTime() - now < 1440 * 60 * 1e3;
3273
+ console.log(bold(` ${s.id}`));
3274
+ console.log(info(` ${s.provider} → ${s.slug} [${s.status}]`));
3275
+ console.log(info(` Events: ${s.eventsProcessed} | Expires: ${expiryStr}${needsRenewal ? " ⚠ RENEW SOON" : ""}`));
3276
+ console.log(info(` Last event: ${s.lastEventAt ?? "—"}`));
3277
+ console.log("");
3278
+ }
3279
+ }
3280
+ async function runPushRevoke(id) {
3281
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3282
+ try {
3283
+ await revoke(dataDir, id);
3284
+ console.log(success(`✓ Subscription ${bold(id)} revoked`));
3285
+ } catch {
3286
+ console.error(error(`✗ Subscription not found: ${id}`));
3287
+ process.exit(1);
3288
+ }
3289
+ }
3290
+ async function runPushRenew(options) {
3291
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3292
+ if (options.id) {
3293
+ const sub = (await readSubscriptions(dataDir)).find((s) => s.id === options.id);
3294
+ if (!sub) {
3295
+ console.error(error(`✗ Subscription not found: ${options.id}`));
3296
+ process.exit(1);
3297
+ }
3298
+ console.log(info(` Renewal for ${sub.id} requires provider-specific logic.`));
3299
+ console.log(info(` Use the daemon's automatic renewal (daily 06:00) or re-register.`));
3300
+ return;
3301
+ }
3302
+ const result = await renewExpiringSubscriptions(dataDir, async () => {
3303
+ throw new Error("No default renew function — use provider-specific tooling");
3304
+ }, 24);
3305
+ console.log(info(` Renewed: ${result.renewed.length} | Errors: ${result.errors.length}`));
3306
+ if (result.renewed.length > 0) console.log(success(`✓ Renewed: ${result.renewed.join(", ")}`));
3307
+ if (result.errors.length > 0) console.log(error(`✗ Errors: ${result.errors.join(", ")}`));
3308
+ }
3309
+ const pushCommand = new Command("push").description("Manage real-time push subscriptions (Gmail Pub/Sub, MS Graph, Slack Events)");
3310
+ pushCommand.command("register <slug>").description("Register a push subscription for a customer").requiredOption("--provider <provider>", "Provider: gmail | microsoft-graph | slack").requiredOption("--webhook-url <url>", "Public HTTPS URL for provider callbacks").option("--topic-name <topic>", "Gmail: Cloud Pub/Sub topic name").option("--client-state <secret>", "MS Graph: client state secret").option("--resource <path>", "MS Graph: resource path").option("--team-id <id>", "Slack: workspace team ID").option("--channel-id <id>", "Slack: optional channel ID").action(async (slug, opts) => {
3311
+ await runPushRegister(slug, opts);
3312
+ });
3313
+ pushCommand.command("status").description("Show all push subscriptions").option("--slug <slug>", "Filter by customer slug").option("--provider <provider>", "Filter by provider").action(async (opts) => {
3314
+ await runPushStatus(opts);
3315
+ });
3316
+ pushCommand.command("revoke <id>").description("Revoke a push subscription by ID").action(async (id) => {
3317
+ await runPushRevoke(id);
3318
+ });
3319
+ pushCommand.command("renew").description("Renew expiring push subscriptions").option("--all", "Renew all expiring subscriptions").option("--id <id>", "Renew a specific subscription by ID").action(async (opts) => {
3320
+ await runPushRenew(opts);
3321
+ });
3322
+ //#endregion
3323
+ //#region src/commands/attach.ts
3324
+ async function runAttach(slug, filePath, dataDir) {
3325
+ const dir = dataDir ?? process.cwd();
3326
+ const customerDir = path.join(dir, "customers", slug);
3327
+ if (!fs.existsSync(customerDir)) {
3328
+ const msg = `Customer '${slug}' not found.`;
3329
+ console.error(error(msg));
3330
+ return { error: msg };
3331
+ }
3332
+ if (!fs.existsSync(filePath)) {
3333
+ const msg = `File not found: ${filePath}`;
3334
+ console.error(error(msg));
3335
+ return { error: msg };
3336
+ }
3337
+ const attachmentsDir = path.join(customerDir, "attachments");
3338
+ fs.mkdirSync(attachmentsDir, { recursive: true });
3339
+ const filename = path.basename(filePath);
3340
+ const dest = path.join(attachmentsDir, filename);
3341
+ if (fs.existsSync(dest)) {
3342
+ const msg = `Attachment already exists: ${filename}`;
3343
+ console.log(info(msg));
3344
+ return { attached: dest };
3345
+ }
3346
+ fs.copyFileSync(filePath, dest);
3347
+ console.log(success(`Attached ${bold(filename)} to ${bold(slug)}`));
3348
+ return { attached: dest };
3349
+ }
3350
+ const attachCommand = new Command("attach").description("Attach a file to a customer (copies to customers/<slug>/attachments/)").argument("<slug>", "Customer slug").argument("<file>", "Path to the file to attach").action(async (slug, file) => {
3351
+ await runAttach(slug, file, process.env["DXCRM_DATA_DIR"]);
3352
+ });
3353
+ //#endregion
3354
+ //#region src/commands/template.ts
3355
+ const templateCommand = new Command("template").description("Manage email templates");
3356
+ templateCommand.command("list").option("--category <category>", "Filter by category").action((opts) => {
3357
+ const templates = listTemplates(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), opts.category ? { category: opts.category } : {});
3358
+ if (templates.length === 0) {
3359
+ console.log(info("No templates found."));
3360
+ return;
3361
+ }
3362
+ for (const t of templates) console.log(` ${bold(t.id)} [${t.category}] ${t.subject}`);
3363
+ });
3364
+ templateCommand.command("get <id>").action((id) => {
3365
+ const tmpl = getTemplate(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id);
3366
+ if (!tmpl) {
3367
+ console.error(error(`Template '${id}' not found`));
3368
+ process.exit(1);
3369
+ }
3370
+ console.log(bold(`Subject: ${tmpl.subject}`));
3371
+ console.log(`Category: ${tmpl.category} Language: ${tmpl.language}`);
3372
+ console.log(`Variables: ${tmpl.variables.join(", ") || "(none defined)"}`);
3373
+ console.log("\n" + tmpl.body);
3374
+ });
3375
+ templateCommand.command("preview <id>").option("--slug <slug>", "Customer slug to preview with").action(async (id, opts) => {
3376
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3377
+ const tmpl = getTemplate(dataDir, id);
3378
+ if (!tmpl) {
3379
+ console.error(error(`Template '${id}' not found`));
3380
+ process.exit(1);
3381
+ }
3382
+ const vars = opts.slug ? await buildVariablesFromCustomer(dataDir, opts.slug) : {};
3383
+ console.log(bold(`Subject: ${interpolate(tmpl.subject, vars)}`));
3384
+ console.log(interpolate(tmpl.body, vars));
3385
+ });
3386
+ templateCommand.command("create <id>").option("--category <category>", "Category", "general").option("--subject <subject>", "Subject line").action((id, opts) => {
3387
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3388
+ if (getTemplate(dataDir, id)) {
3389
+ console.error(error(`Template '${id}' already exists`));
3390
+ process.exit(1);
3391
+ }
3392
+ writeTemplate(dataDir, {
3393
+ id,
3394
+ subject: opts.subject ?? `Subject for ${id}`,
3395
+ category: opts.category,
3396
+ variables: [],
3397
+ language: "de",
3398
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3399
+ body: `Hi {{firstName}},\n\n[your message here]\n\nMit freundlichen Grüßen,\n{{senderName}}`
3400
+ });
3401
+ console.log(success(`✓ Template '${id}' created in category '${opts.category}'`));
3402
+ });
3403
+ templateCommand.command("delete <id>").action((id) => {
3404
+ if (deleteTemplate(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id)) console.log(success(`✓ Template '${id}' deleted`));
3405
+ else {
3406
+ console.error(error(`Template '${id}' not found`));
3407
+ process.exit(1);
3408
+ }
3409
+ });
3410
+ //#endregion
3411
+ //#region src/commands/sequence.ts
3412
+ const sequenceCommand = new Command("sequence").description("Manage email sequences");
3413
+ sequenceCommand.command("list").description("List all sequences").action(() => {
3414
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3415
+ const sequences = listSequences(dataDir);
3416
+ const enrollments = readEnrollments(dataDir);
3417
+ if (sequences.length === 0) {
3418
+ console.log(info("No sequences found."));
3419
+ return;
3420
+ }
3421
+ for (const seq of sequences) {
3422
+ const count = enrollments.filter((e) => e.sequenceId === seq.id).length;
3423
+ console.log(` ${bold(seq.id)} "${seq.name}" ${seq.steps.length} steps ${count} enrolled`);
3424
+ }
3425
+ });
3426
+ sequenceCommand.command("create <id>").description("Create a new sequence skeleton").option("--name <name>", "Sequence display name").action((id, opts) => {
3427
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3428
+ if (getSequence(dataDir, id)) {
3429
+ console.error(error(`Sequence '${id}' already exists`));
3430
+ process.exit(1);
3431
+ }
3432
+ const seq = {
3433
+ id,
3434
+ name: opts.name ?? id,
3435
+ steps: [
3436
+ {
3437
+ day: 0,
3438
+ templateId: "intro",
3439
+ skipIfReplied: true
3440
+ },
3441
+ {
3442
+ day: 3,
3443
+ templateId: "followup-1",
3444
+ skipIfReplied: true
3445
+ },
3446
+ {
3447
+ day: 7,
3448
+ templateId: "followup-2",
3449
+ skipIfReplied: true
3450
+ }
3451
+ ],
3452
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3453
+ };
3454
+ writeSequence(dataDir, seq);
3455
+ console.log(success(`✓ Sequence '${id}' created with ${seq.steps.length} steps`));
3456
+ console.log(info(`Edit .agentic/sequences/${id}.yaml to customize steps and templates`));
3457
+ });
3458
+ sequenceCommand.command("enroll <slug>").description("Enroll a customer contact in a sequence").requiredOption("--email <email>", "Contact email address").requiredOption("--sequence <id>", "Sequence ID").action(async (slug, opts) => {
3459
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3460
+ const seq = getSequence(dataDir, opts.sequence);
3461
+ if (!seq) {
3462
+ console.error(error(`Sequence '${opts.sequence}' not found`));
3463
+ process.exit(1);
3464
+ }
3465
+ const enrollmentId = `enroll_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
3466
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3467
+ await writeEnrollment(dataDir, {
3468
+ id: enrollmentId,
3469
+ sequenceId: opts.sequence,
3470
+ slug,
3471
+ contactEmail: opts.email,
3472
+ enrolledAt: now,
3473
+ status: "active",
3474
+ currentStep: 0,
3475
+ stepsCompleted: []
3476
+ });
3477
+ console.log(success(`✓ Enrolled ${opts.email} in sequence '${seq.name}'`));
3478
+ console.log(info(`Enrollment ID: ${enrollmentId}`));
3479
+ });
3480
+ sequenceCommand.command("status").description("Show enrollment status").option("--slug <slug>", "Filter by customer slug").action((opts) => {
3481
+ let enrollments = readEnrollments(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
3482
+ if (opts.slug) enrollments = enrollments.filter((e) => e.slug === opts.slug);
3483
+ if (enrollments.length === 0) {
3484
+ console.log(info("No enrollments found."));
3485
+ return;
3486
+ }
3487
+ for (const e of enrollments) {
3488
+ const stepInfo = `step ${e.currentStep}`;
3489
+ const lastSent = e.lastSentAt ? ` last sent ${e.lastSentAt.slice(0, 10)}` : "";
3490
+ console.log(` ${bold(e.id)} ${e.slug} <${e.contactEmail}> seq:${e.sequenceId} [${e.status}] ${stepInfo}${lastSent}`);
3491
+ }
3492
+ });
3493
+ sequenceCommand.command("run").description("Run the sequence cycle (send due emails)").option("--dry-run", "Show what would be sent without sending").action(async (opts) => {
3494
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3495
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3496
+ if (opts.dryRun) {
3497
+ const enrollments = readEnrollments(dataDir).filter((e) => e.status === "active");
3498
+ console.log(info(`Dry run — ${enrollments.length} active enrollments for ${today}`));
3499
+ for (const e of enrollments) console.log(` Would process: ${e.id} (${e.contactEmail}, step ${e.currentStep})`);
3500
+ return;
3501
+ }
3502
+ const result = await runSequenceCycle(dataDir, today);
3503
+ console.log(success(`✓ Cycle complete: ${result.sent} sent, ${result.completed} completed, ${result.errors.length} errors`));
3504
+ if (result.errors.length > 0) for (const e of result.errors) console.error(error(` Error: ${e}`));
3505
+ });
3506
+ //#endregion
3507
+ //#region src/commands/quote.ts
3508
+ const quoteCommand = new Command("quote").description("Manage customer quotes");
3509
+ quoteCommand.command("generate <slug>").description("Generate a quote for a customer").requiredOption("--deal <name>", "Deal name").option("--items <items>", "Line items: \"Description Qty Price,...\" (e.g. \"Consulting 1 5000,Support 12 500\")").option("--vat <percent>", "VAT percent", "19").option("--valid <days>", "Valid for N days", "30").action(async (slug, opts) => {
3510
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3511
+ const lineItems = (opts.items ?? "Service 1 1000").split(",").map((item) => item.trim()).filter(Boolean).map((item) => {
3512
+ const parts = item.split(/\s+/);
3513
+ const unitPrice = parseFloat(parts[parts.length - 1] ?? "0");
3514
+ const quantity = parseFloat(parts[parts.length - 2] ?? "1");
3515
+ return {
3516
+ description: parts.slice(0, -2).join(" ") || item,
3517
+ quantity,
3518
+ unitPrice
3519
+ };
3520
+ });
3521
+ try {
3522
+ const quote = await generateQuote(dataDir, {
3523
+ slug,
3524
+ dealName: opts.deal,
3525
+ lineItems,
3526
+ vatPercent: parseFloat(opts.vat),
3527
+ validUntilDays: parseInt(opts.valid, 10)
3528
+ });
3529
+ console.log(success(`✓ Quote ${bold(quote.quoteNumber)} generated`));
3530
+ console.log(info(` Total: ${quote.total.toFixed(2)} ${quote.currency}`));
3531
+ console.log(info(` Valid until: ${quote.validUntil}`));
3532
+ console.log(info(` HTML: ${quote.htmlPath}`));
3533
+ } catch (err) {
3534
+ console.error(error(`Failed to generate quote: ${err.message}`));
3535
+ process.exit(1);
3536
+ }
3537
+ });
3538
+ quoteCommand.command("list").description("List quotes").option("--slug <slug>", "Filter by customer slug").action((opts) => {
3539
+ const quotes = listQuotes(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), opts.slug);
3540
+ if (quotes.length === 0) {
3541
+ console.log(info("No quotes found."));
3542
+ return;
3543
+ }
3544
+ for (const q of quotes) console.log(` ${bold(q.quoteNumber)} ${q.slug} ${q.dealName} ${q.total.toFixed(2)} ${q.currency} [${q.status}] ${q.validUntil}`);
3545
+ });
3546
+ quoteCommand.command("get <quoteNumber>").description("Show quote details").action((quoteNumber) => {
3547
+ const quote = readQuote(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), quoteNumber);
3548
+ if (!quote) {
3549
+ console.error(error(`Quote '${quoteNumber}' not found`));
3550
+ process.exit(1);
3551
+ }
3552
+ console.log(bold(`Quote: ${quote.quoteNumber}`));
3553
+ console.log(`Customer: ${quote.slug} Deal: ${quote.dealName}`);
3554
+ console.log(`Status: ${quote.status} Valid until: ${quote.validUntil}`);
3555
+ console.log(`Subtotal: ${quote.subtotal.toFixed(2)} VAT: ${quote.vat.toFixed(2)} Total: ${quote.total.toFixed(2)} ${quote.currency}`);
3556
+ for (const item of quote.lineItems) console.log(` - ${item.description}: ${item.quantity} × ${item.unitPrice} = ${item.total}`);
3557
+ });
3558
+ //#endregion
3559
+ //#region src/commands/ticket.ts
3560
+ const ticketCommand = new Command("ticket").description("Manage support tickets");
3561
+ ticketCommand.command("list").description("List tickets").option("--slug <slug>", "Filter by customer slug").option("--status <status>", "Filter by status").option("--priority <priority>", "Filter by priority").action(async (opts) => {
3562
+ const tickets = await listAllTickets(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
3563
+ ...opts.slug ? { slug: opts.slug } : {},
3564
+ ...opts.status ? { status: opts.status } : {},
3565
+ ...opts.priority ? { priority: opts.priority } : {}
3566
+ });
3567
+ if (tickets.length === 0) {
3568
+ console.log(info("No tickets found."));
3569
+ return;
3570
+ }
3571
+ for (const { slug, ticket: t } of tickets) {
3572
+ const flag = t.slaDue && t.slaDue < (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) && t.status !== "resolved" && t.status !== "closed" ? " ⚠ SLA" : "";
3573
+ console.log(` ${bold(t.id)} [${slug}] ${t.title} [${t.status}/${t.priority}]${t.assignee ? ` @${t.assignee}` : ""}${flag}`);
3574
+ }
3575
+ });
3576
+ ticketCommand.command("create <slug>").description("Create a ticket for a customer").requiredOption("--title <title>", "Ticket title").option("--description <desc>", "Description").option("--priority <priority>", "Priority: urgent|high|normal|low", "normal").option("--assignee <name>", "Assignee").action(async (slug, opts) => {
3577
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3578
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3579
+ const rules = loadSlaRules(dataDir);
3580
+ const priority = opts.priority ?? "normal";
3581
+ const id = nextTicketId(await readTickets(dataDir, slug));
3582
+ const ticket = {
3583
+ id,
3584
+ title: opts.title,
3585
+ status: "open",
3586
+ priority,
3587
+ ...opts.assignee ? { assignee: opts.assignee } : {},
3588
+ created: today,
3589
+ slaDue: calcSlaDue(today, priority, rules),
3590
+ ...opts.description ? { description: opts.description } : {}
3591
+ };
3592
+ await upsertTicket(dataDir, slug, ticket);
3593
+ console.log(success(`✓ Ticket ${bold(id)} created for ${slug}`));
3594
+ console.log(info(` SLA due: ${ticket.slaDue}`));
3595
+ });
3596
+ ticketCommand.command("update <ticketId>").description("Update a ticket").requiredOption("--slug <slug>", "Customer slug").option("--status <status>", "New status").option("--assignee <name>", "New assignee").action(async (ticketId, opts) => {
3597
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3598
+ const ticket = (await readTickets(dataDir, opts.slug)).find((t) => t.id === ticketId);
3599
+ if (!ticket) {
3600
+ console.error(error(`Ticket '${ticketId}' not found`));
3601
+ process.exit(1);
3602
+ }
3603
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3604
+ const updated = {
3605
+ ...ticket,
3606
+ ...opts.status ? { status: opts.status } : {},
3607
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
3608
+ ...opts.status === "resolved" && !ticket.resolved ? { resolved: today } : {}
3609
+ };
3610
+ await upsertTicket(dataDir, opts.slug, updated);
3611
+ console.log(success(`✓ Ticket ${bold(ticketId)} updated`));
3612
+ });
3613
+ ticketCommand.command("close <ticketId>").description("Close a ticket").requiredOption("--slug <slug>", "Customer slug").option("--resolution <text>", "Resolution notes").action(async (ticketId, opts) => {
3614
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3615
+ const ticket = (await readTickets(dataDir, opts.slug)).find((t) => t.id === ticketId);
3616
+ if (!ticket) {
3617
+ console.error(error(`Ticket '${ticketId}' not found`));
3618
+ process.exit(1);
3619
+ }
3620
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3621
+ const updated = {
3622
+ ...ticket,
3623
+ status: "closed",
3624
+ resolved: ticket.resolved ?? today
3625
+ };
3626
+ await upsertTicket(dataDir, opts.slug, updated);
3627
+ console.log(success(`✓ Ticket ${bold(ticketId)} closed`));
3628
+ });
3629
+ //#endregion
3630
+ //#region src/commands/survey.ts
3631
+ const surveyCommand = new Command("survey").description("Manage NPS/CSAT surveys");
3632
+ surveyCommand.command("list").description("List all survey definitions").action(() => {
3633
+ const surveys = listSurveys(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
3634
+ if (surveys.length === 0) {
3635
+ console.log(info("No surveys found."));
3636
+ return;
3637
+ }
3638
+ for (const s of surveys) console.log(` ${bold(s.id)} [${s.type}] ${s.question.slice(0, 60)}`);
3639
+ });
3640
+ surveyCommand.command("create <id>").description("Create a new survey definition").option("--type <type>", "Survey type: nps|csat|ces", "nps").option("--question <q>", "Survey question").action((id, opts) => {
3641
+ writeSurvey(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
3642
+ id,
3643
+ type: opts.type ?? "nps",
3644
+ question: opts.question ?? "How likely are you to recommend us? (0–10)",
3645
+ scale: {
3646
+ min: 0,
3647
+ max: 10
3648
+ },
3649
+ includeComment: true,
3650
+ commentPrompt: "What's the main reason for your score?",
3651
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3652
+ });
3653
+ console.log(success(`✓ Survey '${id}' created`));
3654
+ });
3655
+ surveyCommand.command("send <surveyId>").description("Generate survey token for a contact").requiredOption("--slug <slug>", "Customer slug").requiredOption("--email <email>", "Contact email").option("--server <url>", "Server URL", process.env["DXCRM_SERVER_URL"] ?? "http://localhost:3456").action(async (surveyId, opts) => {
3656
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3657
+ if (!getSurvey(dataDir, surveyId)) {
3658
+ console.error(error(`Survey '${surveyId}' not found`));
3659
+ process.exit(1);
3660
+ }
3661
+ const token = generateSurveyToken(opts.slug, opts.email, surveyId);
3662
+ await savePendingSurvey(dataDir, surveyId, opts.slug, opts.email, token);
3663
+ console.log(success(`✓ Survey token generated`));
3664
+ console.log(info(` URL: ${opts.server}/survey/respond?token=${token}`));
3665
+ });
3666
+ surveyCommand.command("results <surveyId>").description("Show survey results and NPS score").option("--slug <slug>", "Filter by customer slug").action((surveyId, opts) => {
3667
+ const responses = loadSurveyResponses(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), surveyId, opts.slug);
3668
+ const nps = calcNpsScore(responses);
3669
+ const promoters = responses.filter((r) => r.score >= 9).length;
3670
+ const detractors = responses.filter((r) => r.score <= 6).length;
3671
+ console.log(bold(`Survey: ${surveyId}`));
3672
+ console.log(info(`Responses: ${responses.length} NPS: ${nps} Promoters: ${promoters} Detractors: ${detractors}`));
3673
+ for (const r of responses) console.log(` ${r.slug} <${r.contactEmail}> score=${r.score}${r.comment ? ` "${r.comment.slice(0, 80)}"` : ""}`);
3674
+ });
3675
+ //#endregion
3676
+ //#region src/commands/kb.ts
3677
+ const kbCommand = new Command("kb").description("Manage the knowledge base");
3678
+ kbCommand.command("list").description("List all KB articles").option("--category <cat>", "Filter by category").option("--public", "Show only public articles").action((opts) => {
3679
+ const articles = listKbArticles(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
3680
+ ...opts.category ? { category: opts.category } : {},
3681
+ ...opts.public ? { publicOnly: true } : {}
3682
+ });
3683
+ if (articles.length === 0) {
3684
+ console.log(info("No articles found."));
3685
+ return;
3686
+ }
3687
+ for (const a of articles) {
3688
+ const pub = a.public ? " [public]" : "";
3689
+ console.log(` ${bold(a.id)} [${a.category}] ${a.title}${pub}`);
3690
+ }
3691
+ });
3692
+ kbCommand.command("get <id>").description("Get a KB article").action((id) => {
3693
+ const article = getKbArticle(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id);
3694
+ if (!article) {
3695
+ console.error(error(`Article '${id}' not found`));
3696
+ process.exit(1);
3697
+ }
3698
+ console.log(bold(article.title));
3699
+ console.log(`Category: ${article.category} Tags: ${article.tags.join(", ") || "(none)"}`);
3700
+ console.log("\n" + article.body);
3701
+ });
3702
+ kbCommand.command("search <query>").description("Search KB articles").option("--public", "Search only public articles").action((query, opts) => {
3703
+ const results = searchKbSimple(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), query, opts.public ? { publicOnly: true } : {});
3704
+ if (results.length === 0) {
3705
+ console.log(info("No results."));
3706
+ return;
3707
+ }
3708
+ for (const a of results) {
3709
+ console.log(` ${bold(a.id)} ${a.title}`);
3710
+ console.log(` ${a.body.slice(0, 120).replace(/\n/g, " ")}...`);
3711
+ }
3712
+ });
3713
+ kbCommand.command("create <id>").description("Create a KB article (opens editor-ready template)").requiredOption("--title <title>", "Article title").option("--category <cat>", "Category", "general").option("--ticket <id>", "Source ticket ID").action((id, opts) => {
3714
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3715
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3716
+ writeKbArticle(dataDir, {
3717
+ id,
3718
+ title: opts.title,
3719
+ category: opts.category,
3720
+ tags: [],
3721
+ public: false,
3722
+ createdAt: now,
3723
+ updatedAt: now,
3724
+ ...opts.ticket ? { sourceTicketId: opts.ticket } : {},
3725
+ body: `## Problem\n\n[Describe the problem]\n\n## Solution\n\n[Describe the solution]`
3726
+ });
3727
+ console.log(success(`✓ Article '${id}' created in category '${opts.category}'`));
3728
+ console.log(info(`Edit: .agentic/knowledge-base/${opts.category}/${id}.md`));
3729
+ });
3730
+ kbCommand.command("delete <id>").description("Delete a KB article").action((id) => {
3731
+ if (deleteKbArticle(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id)) console.log(success(`✓ Article '${id}' deleted`));
3732
+ else {
3733
+ console.error(error(`Article '${id}' not found`));
3734
+ process.exit(1);
3735
+ }
3736
+ });
3737
+ //#endregion
3738
+ //#region src/commands/fields.ts
3739
+ const VALID_TYPES = [
3740
+ "text",
3741
+ "number",
3742
+ "boolean",
3743
+ "date",
3744
+ "select"
3745
+ ];
3746
+ function dataDir$16() {
3747
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3748
+ }
3749
+ function collect(value, prev) {
3750
+ return [...prev, value];
3751
+ }
3752
+ /** Parse a `--field name:type[:opt1|opt2]` spec into a FieldDefinition. */
3753
+ function parseFieldSpec(spec) {
3754
+ const [name, type, opts] = spec.split(":");
3755
+ if (!name || !type || !VALID_TYPES.includes(type)) return null;
3756
+ return {
3757
+ name,
3758
+ type,
3759
+ ...opts ? { options: opts.split("|").map((s) => s.trim()) } : {}
3760
+ };
3761
+ }
3762
+ const fieldsCommand = new Command("fields").description("Manage custom fields (metadata-driven extensibility)");
3763
+ fieldsCommand.command("list").description("List defined custom fields").action(async () => {
3764
+ const { loadFieldDefinitions } = await import("./custom-fields-CzNeD3_v.js");
3765
+ const defs = loadFieldDefinitions(dataDir$16());
3766
+ if (defs.length === 0) {
3767
+ console.log(info("No custom fields defined. Add one with: dxcrm fields add <name> <type>"));
3768
+ return;
3769
+ }
3770
+ for (const d of defs) {
3771
+ const opts = d.options ? ` [${d.options.join(", ")}]` : "";
3772
+ console.log(`${d.name} (${d.type})${opts}${d.label ? ` — ${d.label}` : ""}`);
3773
+ }
3774
+ });
3775
+ fieldsCommand.command("add <name> <type>").description("Define a custom field (type: text|number|boolean|date|select)").option("--label <label>", "Human-readable label").option("--options <csv>", "Comma-separated options (for select)").action(async (name, type, opts) => {
3776
+ if (!VALID_TYPES.includes(type)) {
3777
+ console.error(error(`Invalid type '${type}'. Use one of: ${VALID_TYPES.join(", ")}`));
3778
+ process.exitCode = 1;
3779
+ return;
3780
+ }
3781
+ const { defineCustomField } = await import("./custom-fields-CzNeD3_v.js");
3782
+ defineCustomField(dataDir$16(), {
3783
+ name,
3784
+ type,
3785
+ ...opts.label ? { label: opts.label } : {},
3786
+ ...opts.options ? { options: opts.options.split(",").map((s) => s.trim()) } : {}
3787
+ });
3788
+ console.log(success(`Custom field '${name}' (${type}) defined.`));
3789
+ });
3790
+ const objectCommand = new Command("object").description("Manage custom objects (runtime-defined entities, no-migration)");
3791
+ objectCommand.command("define <name>").description("Define a custom object with fields (--field name:type[:opt1|opt2])").option("--label <label>", "Human-readable label").option("--field <spec>", "Field spec, repeatable", collect, []).action(async (name, opts) => {
3792
+ const fields = [];
3793
+ for (const spec of opts.field) {
3794
+ const f = parseFieldSpec(spec);
3795
+ if (!f) {
3796
+ console.error(error(`Invalid --field spec '${spec}' (use name:type[:a|b])`));
3797
+ process.exitCode = 1;
3798
+ return;
3799
+ }
3800
+ fields.push(f);
3801
+ }
3802
+ const { defineCustomObject } = await import("./custom-objects-CIFrmQ2V.js");
3803
+ defineCustomObject(dataDir$16(), {
3804
+ name,
3805
+ ...opts.label ? { label: opts.label } : {},
3806
+ fields
3807
+ });
3808
+ console.log(success(`Custom object '${name}' defined with ${fields.length} field(s).`));
3809
+ });
3810
+ objectCommand.command("add <name>").description("Create a record (--set key=value, repeatable)").option("--set <kv>", "key=value, repeatable", collect, []).action(async (name, opts) => {
3811
+ const values = {};
3812
+ for (const kv of opts.set) {
3813
+ const eq = kv.indexOf("=");
3814
+ if (eq < 0) continue;
3815
+ values[kv.slice(0, eq).trim()] = kv.slice(eq + 1).trim();
3816
+ }
3817
+ const { createRecord } = await import("./custom-objects-CIFrmQ2V.js");
3818
+ const res = createRecord(dataDir$16(), name, values);
3819
+ if (!res.ok) {
3820
+ console.error(error(`Could not create record: ${(res.errors ?? []).join("; ")}`));
3821
+ process.exitCode = 1;
3822
+ return;
3823
+ }
3824
+ console.log(success(`Created ${name} record ${res.record.id}`));
3825
+ });
3826
+ objectCommand.command("list <name>").description("List records of a custom object").action(async (name) => {
3827
+ const { listRecords } = await import("./custom-objects-CIFrmQ2V.js");
3828
+ const records = listRecords(dataDir$16(), name);
3829
+ if (records.length === 0) {
3830
+ console.log(info(`No records for '${name}'.`));
3831
+ return;
3832
+ }
3833
+ for (const r of records) console.log(`${r.id} ${JSON.stringify(r.values)}`);
3834
+ });
3835
+ //#endregion
3836
+ //#region src/commands/webhook.ts
3837
+ function dataDir$15() {
3838
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3839
+ }
3840
+ const webhookCommand = new Command("webhook").description("Manage outbound webhooks (event-driven integrations)");
3841
+ webhookCommand.command("add <url>").description("Subscribe a URL to events (--events record.created,deal.updated or '*')").option("--events <csv>", "Comma-separated event patterns", "*").option("--secret <secret>", "HMAC secret for X-DXCRM-Signature").action(async (url, opts) => {
3842
+ const { addWebhook } = await import("./webhooks-7EpA05Qr.js");
3843
+ const events = opts.events.split(",").map((s) => s.trim()).filter(Boolean);
3844
+ const sub = addWebhook(dataDir$15(), url, events, opts.secret);
3845
+ console.log(success(`Webhook ${sub.id} → ${url} [${events.join(", ")}]`));
3846
+ });
3847
+ webhookCommand.command("list").description("List webhook subscriptions").action(async () => {
3848
+ const { loadWebhooks } = await import("./webhooks-7EpA05Qr.js");
3849
+ const subs = loadWebhooks(dataDir$15());
3850
+ if (subs.length === 0) {
3851
+ console.log(info("No webhooks configured."));
3852
+ return;
3853
+ }
3854
+ for (const s of subs) console.log(`${s.id} ${s.url} [${s.events.join(", ")}]`);
3855
+ });
3856
+ webhookCommand.command("remove <id>").description("Remove a webhook subscription").action(async (id) => {
3857
+ const { removeWebhook } = await import("./webhooks-7EpA05Qr.js");
3858
+ if (removeWebhook(dataDir$15(), id)) console.log(success(`Removed ${id}`));
3859
+ else {
3860
+ console.error(error(`Not found: ${id}`));
3861
+ process.exitCode = 1;
3862
+ }
3863
+ });
3864
+ webhookCommand.command("retry").description("Re-attempt failed webhook deliveries (replay store)").action(async () => {
3865
+ const { retryFailures } = await import("./webhooks-7EpA05Qr.js");
3866
+ const r = await retryFailures(dataDir$15());
3867
+ console.log(info(`Retried ${r.retried}, still failing ${r.stillFailing}.`));
3868
+ });
3869
+ //#endregion
3870
+ //#region src/commands/segment.ts
3871
+ function dataDir$14() {
3872
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3873
+ }
3874
+ const segmentCommand = new Command("segment").description("Manage customer segments (marketing lists)");
3875
+ segmentCommand.command("define <name>").description("Define a segment by criteria").option("--stage <stage>", "relationship_stage (prospect|active|churned|paused)").option("--tags <csv>", "Comma-separated tags (all must match)").option("--min-deal-value <n>", "Minimum deal value").option("--stale-days <n>", "Days since last update (staleness)").action(async (name, opts) => {
3876
+ const criteria = {
3877
+ ...opts.stage ? { stage: opts.stage } : {},
3878
+ ...opts.tags ? { tags: opts.tags.split(",").map((s) => s.trim()) } : {},
3879
+ ...opts.minDealValue ? { minDealValue: Number(opts.minDealValue) } : {},
3880
+ ...opts.staleDays ? { staleDays: Number(opts.staleDays) } : {}
3881
+ };
3882
+ const { defineSegment } = await import("./segments-BqcD5HIl.js");
3883
+ defineSegment(dataDir$14(), name, criteria);
3884
+ console.log(success(`Segment '${name}' defined: ${JSON.stringify(criteria)}`));
3885
+ });
3886
+ segmentCommand.command("list").description("List defined segments").action(async () => {
3887
+ const { loadSegments } = await import("./segments-BqcD5HIl.js");
3888
+ const segs = loadSegments(dataDir$14());
3889
+ if (segs.length === 0) {
3890
+ console.log(info("No segments defined."));
3891
+ return;
3892
+ }
3893
+ for (const s of segs) console.log(`${s.name} ${JSON.stringify(s.criteria)}`);
3894
+ });
3895
+ segmentCommand.command("members <name>").description("List customers matching a segment").action(async (name) => {
3896
+ const { loadSegments, evaluateSegment } = await import("./segments-BqcD5HIl.js");
3897
+ const seg = loadSegments(dataDir$14()).find((s) => s.name === name);
3898
+ if (!seg) {
3899
+ console.error(error(`Segment not found: ${name}`));
3900
+ process.exitCode = 1;
3901
+ return;
3902
+ }
3903
+ const members = await evaluateSegment(dataDir$14(), seg.criteria);
3904
+ console.log(info(`${members.length} member(s):`));
3905
+ for (const slug of members) console.log(slug);
3906
+ });
3907
+ //#endregion
3908
+ //#region src/commands/identity.ts
3909
+ function dataDir$13() {
3910
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3911
+ }
3912
+ const identityCommand = new Command("identity").description("Identity resolution / deduplication (CDP)");
3913
+ identityCommand.command("duplicates").description("Find clusters of likely-duplicate customers (by canonical domain)").action(async () => {
3914
+ const { findDuplicateClusters } = await import("./identity-gyfWdrcX.js");
3915
+ const clusters = await findDuplicateClusters(dataDir$13());
3916
+ if (clusters.length === 0) {
3917
+ console.log(info("No duplicate clusters found."));
3918
+ return;
3919
+ }
3920
+ console.log(info(`${clusters.length} duplicate cluster(s):`));
3921
+ for (const c of clusters) console.log(`${c.key}: ${c.slugs.join(", ")}`);
3922
+ });
3923
+ //#endregion
3924
+ //#region src/commands/metrics.ts
3925
+ function dataDir$12() {
3926
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3927
+ }
3928
+ const metricsCommand = new Command("metrics").description("Command-center metrics from the audit trail").action(async () => {
3929
+ const { computeAuditMetrics } = await import("./metrics-DH8wHvya.js");
3930
+ const m = computeAuditMetrics(dataDir$12());
3931
+ console.log(bold("Command Center"));
3932
+ console.log(`Total operations: ${m.totalOperations}`);
3933
+ console.log(`Customers touched: ${m.customersTouched}`);
3934
+ console.log(`Automation rate: ${(m.automationRate * 100).toFixed(0)}%`);
3935
+ console.log(info("By tool:"));
3936
+ for (const [tool, n] of Object.entries(m.byTool).sort((a, b) => b[1] - a[1])) console.log(` ${tool}: ${n}`);
3937
+ console.log(info("By actor:"));
3938
+ for (const [actor, n] of Object.entries(m.byActor).sort((a, b) => b[1] - a[1])) console.log(` ${actor}: ${n}`);
3939
+ });
3940
+ //#endregion
3941
+ //#region src/commands/usage.ts
3942
+ function dataDir$11() {
3943
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3944
+ }
3945
+ const usageCommand = new Command("usage").description("LLM token usage & cost (per customer)").option("--slug <slug>", "Filter by customer").action(async (opts) => {
3946
+ const { aggregateUsage } = await import("./usage-D0-TYJkw.js");
3947
+ const agg = aggregateUsage(dataDir$11(), opts.slug ? { slug: opts.slug } : {});
3948
+ console.log(bold("LLM Usage"));
3949
+ console.log(`Calls: ${agg.calls}`);
3950
+ console.log(`Input tokens: ${agg.totalInputTokens}`);
3951
+ console.log(`Output tokens: ${agg.totalOutputTokens}`);
3952
+ console.log(`Cost (USD): $${agg.totalCostUsd.toFixed(4)}`);
3953
+ if (!opts.slug) {
3954
+ console.log(info("By customer:"));
3955
+ for (const [slug, b] of Object.entries(agg.bySlug).sort((a, b) => b[1].costUsd - a[1].costUsd)) console.log(` ${slug}: $${b.costUsd.toFixed(4)} (${b.calls} calls)`);
3956
+ }
3957
+ });
3958
+ //#endregion
3959
+ //#region src/commands/approvals.ts
3960
+ function dataDir$10() {
3961
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
3962
+ }
3963
+ const approvalsCommand = new Command("approvals").description("Human-in-the-loop approval queue");
3964
+ approvalsCommand.command("list").description("List approvals (default: pending)").option("--status <status>", "pending | approved | rejected", "pending").action(async (opts) => {
3965
+ const { listApprovals } = await import("./approvals-DpjxGHFp.js");
3966
+ const list = listApprovals(dataDir$10(), opts.status);
3967
+ if (list.length === 0) {
3968
+ console.log(info(`No ${opts.status} approvals.`));
3969
+ return;
3970
+ }
3971
+ for (const a of list) console.log(`${a.id} ${a.tool} ${a.slug ?? "-"} ${a.requestedAt}`);
3972
+ });
3973
+ approvalsCommand.command("approve <id>").description("Approve a pending action").action(async (id) => {
3974
+ const { decideApproval } = await import("./approvals-DpjxGHFp.js");
3975
+ if (decideApproval(dataDir$10(), id, "approved")) console.log(success(`Approved ${id}`));
3976
+ else {
3977
+ console.error(error(`Not found: ${id}`));
3978
+ process.exitCode = 1;
3979
+ }
3980
+ });
3981
+ approvalsCommand.command("reject <id>").description("Reject a pending action").action(async (id) => {
3982
+ const { decideApproval } = await import("./approvals-DpjxGHFp.js");
3983
+ if (decideApproval(dataDir$10(), id, "rejected")) console.log(success(`Rejected ${id}`));
3984
+ else {
3985
+ console.error(error(`Not found: ${id}`));
3986
+ process.exitCode = 1;
3987
+ }
3988
+ });
3989
+ const policyCommand = new Command("policy").description("Autonomy policy per tool/customer (auto|approve|block)");
3990
+ policyCommand.command("set <tool> <policy>").description("Set autonomy policy for a tool (optionally per --slug)").option("--slug <slug>", "Customer slug (per-customer override)").action(async (tool, policy, opts) => {
3991
+ if (![
3992
+ "auto",
3993
+ "approve",
3994
+ "block"
3995
+ ].includes(policy)) {
3996
+ console.error(error("policy must be auto | approve | block"));
3997
+ process.exitCode = 1;
3998
+ return;
3999
+ }
4000
+ const { setPolicy } = await import("./approvals-DpjxGHFp.js");
4001
+ setPolicy(dataDir$10(), tool, policy, opts.slug);
4002
+ console.log(success(`Policy ${tool}${opts.slug ? `@${opts.slug}` : ""} = ${policy}`));
4003
+ });
4004
+ //#endregion
4005
+ //#region src/commands/hygiene.ts
4006
+ function dataDir$9() {
4007
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4008
+ }
4009
+ const hygieneCommand = new Command("hygiene").description("Data-quality scanning");
4010
+ hygieneCommand.command("scan").description("Scan customers for data-quality issues (missing/malformed/duplicate)").action(async () => {
4011
+ const { scanHygiene } = await import("./hygiene-DZqfYpFf.js");
4012
+ const issues = await scanHygiene(dataDir$9());
4013
+ if (issues.length === 0) {
4014
+ console.log(success("✓ No data-quality issues found."));
4015
+ return;
4016
+ }
4017
+ console.log(info(`${issues.length} issue(s):`));
4018
+ for (const i of issues) {
4019
+ const fix = i.suggestedFix ? ` → fix: ${i.suggestedFix}` : "";
4020
+ console.log(` [${i.type}] ${i.slug}: ${i.detail}${fix}`);
4021
+ }
4022
+ });
4023
+ //#endregion
4024
+ //#region src/commands/memory.ts
4025
+ function dataDir$8() {
4026
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4027
+ }
4028
+ const TYPES = [
4029
+ "fact",
4030
+ "preference",
4031
+ "learning",
4032
+ "instruction"
4033
+ ];
4034
+ const memoryCommand = new Command("memory").description("Agent memories (per customer + global)");
4035
+ memoryCommand.command("add <text>").description("Add a memory (global unless --slug given)").option("--type <type>", "fact | preference | learning | instruction", "fact").option("--slug <slug>", "Customer slug (omit for global)").action(async (text, opts) => {
4036
+ const type = TYPES.includes(opts.type) ? opts.type : "fact";
4037
+ const { addMemory } = await import("./memory-Cy6-Tbyl.js");
4038
+ const m = addMemory(dataDir$8(), {
4039
+ scope: opts.slug ? "customer" : "global",
4040
+ ...opts.slug ? { slug: opts.slug } : {},
4041
+ type,
4042
+ text
4043
+ });
4044
+ console.log(success(`Memory ${m.id} stored (${m.scope}${opts.slug ? `:${opts.slug}` : ""}).`));
4045
+ });
4046
+ memoryCommand.command("list").description("List memories (global + customer if --slug)").option("--slug <slug>", "Customer slug").action(async (opts) => {
4047
+ const { loadMemories } = await import("./memory-Cy6-Tbyl.js");
4048
+ const mems = loadMemories(dataDir$8(), opts.slug);
4049
+ if (mems.length === 0) {
4050
+ console.log(info("No memories."));
4051
+ return;
4052
+ }
4053
+ for (const m of mems) console.log(`[${m.scope}/${m.type}] ${m.text}`);
4054
+ });
4055
+ memoryCommand.command("search <query>").description("Search memories by relevance").option("--slug <slug>", "Customer slug").action(async (query, opts) => {
4056
+ const { searchMemory } = await import("./memory-Cy6-Tbyl.js");
4057
+ const hits = await searchMemory(dataDir$8(), query, opts.slug);
4058
+ if (hits.length === 0) {
4059
+ console.log(info("No matching memories."));
4060
+ return;
4061
+ }
4062
+ for (const m of hits) console.log(`[${m.scope}/${m.type}] ${m.text}`);
4063
+ });
4064
+ //#endregion
4065
+ //#region src/commands/sop.ts
4066
+ function dataDir$7() {
4067
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4068
+ }
4069
+ const sopCommand = new Command("sop").description("Standard Operating Procedures (global / per customer)");
4070
+ sopCommand.command("add <title>").description("Add an SOP (global unless --slug given)").option("--triggers <csv>", "Comma-separated trigger keywords", "").option("--body <text>", "SOP body / steps", "").option("--slug <slug>", "Customer slug (omit for global)").action(async (title, opts) => {
4071
+ const { addSop } = await import("./sop-DkhVChGy.js");
4072
+ const triggers = opts.triggers.split(",").map((s) => s.trim()).filter(Boolean);
4073
+ const s = addSop(dataDir$7(), {
4074
+ scope: opts.slug ? "customer" : "global",
4075
+ ...opts.slug ? { slug: opts.slug } : {},
4076
+ title,
4077
+ triggers,
4078
+ body: opts.body
4079
+ });
4080
+ console.log(success(`SOP ${s.id} added (${s.scope}${opts.slug ? `:${opts.slug}` : ""}).`));
4081
+ });
4082
+ sopCommand.command("list").description("List SOPs (global + customer if --slug)").option("--slug <slug>", "Customer slug").action(async (opts) => {
4083
+ const { loadSops } = await import("./sop-DkhVChGy.js");
4084
+ const sops = loadSops(dataDir$7(), opts.slug);
4085
+ if (sops.length === 0) {
4086
+ console.log(info("No SOPs."));
4087
+ return;
4088
+ }
4089
+ for (const s of sops) console.log(`[${s.scope}] ${s.title} (${s.triggers.join(", ")})`);
4090
+ });
4091
+ sopCommand.command("find <query>").description("Find SOPs relevant to a task").option("--slug <slug>", "Customer slug").action(async (query, opts) => {
4092
+ const { findSops } = await import("./sop-DkhVChGy.js");
4093
+ const hits = await findSops(dataDir$7(), query, opts.slug);
4094
+ if (hits.length === 0) {
4095
+ console.log(info("No matching SOPs."));
4096
+ return;
4097
+ }
4098
+ for (const s of hits) console.log(`[${s.scope}] ${s.title}`);
4099
+ });
4100
+ //#endregion
4101
+ //#region src/commands/tone.ts
4102
+ function dataDir$6() {
4103
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4104
+ }
4105
+ const toneCommand = new Command("tone").description("Customer tonality profiles (per customer + global)");
4106
+ toneCommand.command("set").description("Set tone profile (global unless --slug given)").option("--formality <v>", "formal | casual | friendly").option("--language <v>", "Language code, e.g. de | en").option("--do <csv>", "Comma-separated phrases to prefer").option("--dont <csv>", "Comma-separated phrases to avoid").option("--slug <slug>", "Customer slug (omit for global)").action(async (opts) => {
4107
+ const { setTone } = await import("./tone-Bdm5uaht.js");
4108
+ setTone(dataDir$6(), {
4109
+ ...opts.formality ? { formality: opts.formality } : {},
4110
+ ...opts.language ? { language: opts.language } : {},
4111
+ ...opts.do ? { dos: opts.do.split(",").map((s) => s.trim()) } : {},
4112
+ ...opts.dont ? { donts: opts.dont.split(",").map((s) => s.trim()) } : {}
4113
+ }, opts.slug);
4114
+ console.log(success(`Tone profile saved (${opts.slug ? `customer:${opts.slug}` : "global"}).`));
4115
+ });
4116
+ toneCommand.command("show").description("Show the effective tone profile").option("--slug <slug>", "Customer slug").action(async (opts) => {
4117
+ const { resolveTone, toneInstruction } = await import("./tone-Bdm5uaht.js");
4118
+ const profile = resolveTone(dataDir$6(), opts.slug);
4119
+ console.log(info(JSON.stringify(profile)));
4120
+ console.log(`instruction: ${toneInstruction(profile) || "(none)"}`);
4121
+ });
4122
+ //#endregion
4123
+ //#region src/commands/autofill.ts
4124
+ const autofillCommand = new Command("autofill").description("Extract structured CRM fields from a transcript file").argument("<file>", "Path to transcript file").option("--slug <slug>", "Customer slug (for usage attribution)").action(async (file, opts) => {
4125
+ if (!fs.existsSync(file)) {
4126
+ console.error(error(`File not found: ${file}`));
4127
+ process.exitCode = 1;
4128
+ return;
4129
+ }
4130
+ const transcript = fs.readFileSync(file, "utf-8");
4131
+ const { extractAutofill } = await import("./autofill-Di_-SP7t.js");
4132
+ const result = await extractAutofill(transcript, opts.slug ? { slug: opts.slug } : {});
4133
+ console.log(info("Extracted fields (review before applying):"));
4134
+ console.log(JSON.stringify(result, null, 2));
4135
+ });
4136
+ //#endregion
4137
+ //#region src/commands/ask.ts
4138
+ function dataDir$5() {
4139
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4140
+ }
4141
+ const askCommand = new Command("ask").description("Ask your CRM a natural-language question").argument("<question>", "The question").option("--slug <slug>", "Scope to a customer").action(async (question, opts) => {
4142
+ const { askCrm } = await import("./ask-CID3jnuL.js");
4143
+ const res = await askCrm(dataDir$5(), question, opts.slug);
4144
+ if (res.answer) {
4145
+ console.log(bold("Answer:"));
4146
+ console.log(res.answer);
4147
+ }
4148
+ if (res.sources.length === 0) {
4149
+ console.log(info("No relevant data found."));
4150
+ return;
4151
+ }
4152
+ console.log(info("Sources:"));
4153
+ res.sources.forEach((s, i) => console.log(` [${i + 1}] ${s.text.slice(0, 120)}`));
4154
+ });
4155
+ //#endregion
4156
+ //#region src/commands/nba.ts
4157
+ function dataDir$4() {
4158
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4159
+ }
4160
+ const nbaCommand = new Command("nba").description("Next-best-action recommendations for a customer").argument("<slug>", "Customer slug").action(async (slug) => {
4161
+ const { nextBestAction } = await import("./nba-3wanmJ0U.js");
4162
+ const actions = await nextBestAction(dataDir$4(), slug);
4163
+ if (actions.length === 0) {
4164
+ console.log(info("No recommendations."));
4165
+ return;
4166
+ }
4167
+ for (const a of actions) console.log(`[${a.priority}] ${a.action} — ${a.reason}`);
4168
+ });
4169
+ //#endregion
4170
+ //#region src/commands/vault.ts
4171
+ function dataDir$3() {
4172
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4173
+ }
4174
+ /** Master key from the environment only — never a flag (avoids shell history). */
4175
+ function masterKey() {
4176
+ const key = process.env["DXCRM_VAULT_KEY"];
4177
+ if (!key) {
4178
+ console.error(error("DXCRM_VAULT_KEY is not set. Export your vault master key first."));
4179
+ process.exit(1);
4180
+ }
4181
+ return key;
4182
+ }
4183
+ /** Run a vault action, turning decryption/IO failures into a clean exit. */
4184
+ async function guard(fn) {
4185
+ try {
4186
+ await fn();
4187
+ } catch (e) {
4188
+ console.error(error(e.message));
4189
+ process.exit(1);
4190
+ }
4191
+ }
4192
+ const vaultCommand = new Command("vault").description("Local encrypted credential vault (AES-256-GCM)");
4193
+ vaultCommand.command("set <name> <value>").description("Store (or overwrite) a secret").action((name, value) => guard(async () => {
4194
+ const { setSecret } = await import("./vault-C1D3zScD.js");
4195
+ setSecret(dataDir$3(), masterKey(), name, value);
4196
+ console.log(success(`Secret '${name}' stored.`));
4197
+ }));
4198
+ vaultCommand.command("get <name>").description("Retrieve a secret").action((name) => guard(async () => {
4199
+ const { getSecret } = await import("./vault-C1D3zScD.js");
4200
+ const value = getSecret(dataDir$3(), masterKey(), name);
4201
+ if (value === void 0) {
4202
+ console.log(info(`No secret named '${name}'.`));
4203
+ return;
4204
+ }
4205
+ console.log(value);
4206
+ }));
4207
+ vaultCommand.command("list").description("List secret names (values stay encrypted)").action(() => guard(async () => {
4208
+ const { listSecretKeys } = await import("./vault-C1D3zScD.js");
4209
+ const names = listSecretKeys(dataDir$3(), masterKey());
4210
+ if (names.length === 0) {
4211
+ console.log(info("Vault is empty."));
4212
+ return;
4213
+ }
4214
+ for (const n of names.sort()) console.log(n);
4215
+ }));
4216
+ vaultCommand.command("rm <name>").description("Remove a secret").action((name) => guard(async () => {
4217
+ const { removeSecret } = await import("./vault-C1D3zScD.js");
4218
+ const removed = removeSecret(dataDir$3(), masterKey(), name);
4219
+ console.log(removed ? success(`Secret '${name}' removed.`) : info(`No secret named '${name}'.`));
4220
+ }));
4221
+ //#endregion
4222
+ //#region src/commands/churn.ts
4223
+ function dataDir$2() {
4224
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4225
+ }
4226
+ function paint(level, text) {
4227
+ if (level === "high") return error(text);
4228
+ if (level === "medium") return warning(text);
4229
+ return success(text);
4230
+ }
4231
+ const churnCommand = new Command("churn").description("Churn early-warning (relationship-health based)");
4232
+ churnCommand.command("assess <slug>").description("Assess churn risk for one customer").action(async (slug) => {
4233
+ const { assessChurn } = await import("./churn-C28IgnAj.js");
4234
+ const r = assessChurn(dataDir$2(), slug);
4235
+ console.log(paint(r.level, `${slug}: ${r.level.toUpperCase()} risk (${r.riskScore}/100)`));
4236
+ for (const s of r.signals) console.log(` • ${s}`);
4237
+ });
4238
+ churnCommand.command("scan").description("Rank all customers by churn risk (highest first)").action(async () => {
4239
+ const { scanChurn } = await import("./churn-C28IgnAj.js");
4240
+ const ranked = scanChurn(dataDir$2());
4241
+ if (ranked.length === 0) {
4242
+ console.log(info("No customers to assess."));
4243
+ return;
4244
+ }
4245
+ for (const r of ranked) console.log(paint(r.level, `${r.riskScore.toString().padStart(3)} ${r.level.padEnd(6)} ${r.slug}`));
4246
+ });
4247
+ //#endregion
4248
+ //#region src/commands/leadscore.ts
4249
+ function dataDir$1() {
4250
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4251
+ }
4252
+ const leadscoreCommand = new Command("leadscore").description("Predictive lead scoring (logistic regression on won/lost history)");
4253
+ leadscoreCommand.command("train").description("Train the model from won/lost history and persist it").action(async () => {
4254
+ const { buildLeadModel, saveLeadModel } = await import("./lead-model-BCFzyktm.js");
4255
+ const model = buildLeadModel(dataDir$1());
4256
+ if (!model.sufficient) {
4257
+ console.log(warning(`Not enough closed history to train (${model.trainedOn} deals, need ≥4 with both outcomes).`));
4258
+ console.log(info("Predictions fall back to the deterministic heuristic until then."));
4259
+ return;
4260
+ }
4261
+ saveLeadModel(dataDir$1(), model);
4262
+ console.log(success(`Trained on ${model.trainedOn} closed deals. Model saved.`));
4263
+ });
4264
+ leadscoreCommand.command("predict <slug>").description("Score a customer's open deals with the trained model").action(async (slug) => {
4265
+ const { loadLeadModel, buildLeadModel, predictWin } = await import("./lead-model-BCFzyktm.js");
4266
+ const { readPipelineSync } = await import("./pipeline-writer-BqBrYrQc.js");
4267
+ const model = loadLeadModel(dataDir$1()) ?? buildLeadModel(dataDir$1());
4268
+ const open = readPipelineSync(dataDir$1(), slug).filter((d) => d.stage !== "won" && d.stage !== "lost");
4269
+ if (open.length === 0) {
4270
+ console.log(info(`No open deals for ${slug}.`));
4271
+ return;
4272
+ }
4273
+ const source = model.sufficient ? "model" : "heuristic";
4274
+ console.log(info(`Win-probability for ${slug} (${source}):`));
4275
+ for (const d of open) {
4276
+ const p = Math.round(predictWin(model, d) * 100);
4277
+ console.log(` ${String(p).padStart(3)}% ${d.name} (${d.stage})`);
4278
+ }
4279
+ });
4280
+ //#endregion
4281
+ //#region src/commands/enrich.ts
4282
+ function dataDir() {
4283
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4284
+ }
4285
+ const enrichCommand = new Command("enrich").description("Enrich a customer's facts (offline domain-from-email + plugins)").argument("<slug>", "Customer slug").option("--write", "Write newly-derived fields back to main_facts.md").action(async (slug, opts) => {
4286
+ const { enrichCustomer } = await import("./enrichment-3XvgGDfB.js");
4287
+ const res = await enrichCustomer(dataDir(), slug, { write: opts.write ?? false });
4288
+ const applied = Object.entries(res.applied);
4289
+ if (applied.length === 0) {
4290
+ console.log(info(`Nothing new to enrich for ${slug}.`));
4291
+ return;
4292
+ }
4293
+ for (const [k, v] of applied) console.log(` ${k}: ${String(v)}`);
4294
+ console.log(res.written ? success(`Applied ${applied.length} field(s) to ${slug}.`) : info(`Found ${applied.length} field(s) — re-run with --write to apply.`));
4295
+ });
4296
+ //#endregion
4297
+ //#region src/commands/coach.ts
4298
+ const coachCommand = new Command("coach").description("Conversation-intelligence: analyze a call transcript").argument("<file>", "Path to a speaker-labelled transcript (\"Rep: ...\" / \"Customer: ...\")").option("--rep <labels>", "Comma-separated speaker labels treated as the rep", "rep,sales,ae,me,agent").action(async (file, opts) => {
4299
+ if (!fs.existsSync(file)) {
4300
+ console.error(`Transcript not found: ${file}`);
4301
+ process.exit(1);
4302
+ }
4303
+ const transcript = fs.readFileSync(file, "utf-8");
4304
+ const { analyzeConversation } = await import("./conversation-intel-mm7Lhemh.js");
4305
+ const a = analyzeConversation(transcript, opts.rep.split(",").map((s) => s.trim()).filter(Boolean));
4306
+ console.log(success(`Conversation analysis (${a.turns} turns)`));
4307
+ console.log(` Talk ratio (rep): ${Math.round(a.talkRatio * 100)}%`);
4308
+ console.log(` Questions asked: ${a.questionsAsked}`);
4309
+ console.log(` Longest monologue: ${a.longestMonologue} words`);
4310
+ if (a.objections.length > 0) {
4311
+ console.log(info(" Objections:"));
4312
+ for (const o of a.objections) console.log(` • ${o}`);
4313
+ }
4314
+ console.log(info(" Coaching:"));
4315
+ for (const c of a.coaching) console.log(` → ${c}`);
4316
+ });
4317
+ //#endregion
4318
+ //#region src/commands/compliance.ts
4319
+ const complianceCommand = new Command("compliance").description("Privacy/compliance posture (AI-Act Art. 50, local-LLM, PII, guardrails)");
4320
+ complianceCommand.command("status", { isDefault: true }).description("Show the active compliance configuration").action(async () => {
4321
+ const { complianceConfig, aiDisclosure } = await import("./compliance-CujOqAKk.js");
4322
+ const cfg = complianceConfig();
4323
+ const onOff = (b) => b ? success("on") : warning("off");
4324
+ console.log(info("Compliance posture"));
4325
+ console.log(` LLM provider: ${cfg.provider}`);
4326
+ if (cfg.local) {
4327
+ console.log(` Local endpoint: ${cfg.local.baseUrl}`);
4328
+ console.log(` Local model: ${cfg.local.model}`);
4329
+ console.log(success(" → Customer data stays on-machine (data-residency moat)."));
4330
+ }
4331
+ console.log(` AI-Act Art.50 label: ${onOff(cfg.aiDisclosure)}`);
4332
+ console.log(` PII masking: ${onOff(cfg.piiMasking)}`);
4333
+ console.log(` Prompt guardrails: ${onOff(cfg.guardrails)}`);
4334
+ if (cfg.aiDisclosure) console.log(info(` Disclosure: "${aiDisclosure()}"`));
4335
+ });
4336
+ //#endregion
4337
+ //#region src/cli.ts
4338
+ const program = new Command();
4339
+ program.name("dxcrm").description("DatasynxOpenCRM — local-first, MCP-native CRM").version("0.1.0").exitOverride();
4340
+ program.addCommand(initCommand);
4341
+ program.addCommand(createCommand);
4342
+ program.addCommand(listCommand);
4343
+ program.addCommand(validateCommand);
4344
+ program.addCommand(sessionCommand);
4345
+ program.addCommand(guideCommand);
4346
+ program.addCommand(mcpCommand);
4347
+ program.addCommand(syncCommand);
4348
+ program.addCommand(backupCommand);
4349
+ program.addCommand(restoreCommand);
4350
+ program.addCommand(daemonCommand);
4351
+ program.addCommand(statusCommand);
4352
+ program.addCommand(agentCommand);
4353
+ program.addCommand(importCommand);
4354
+ program.addCommand(serverCommand);
4355
+ program.addCommand(auditCommand);
4356
+ program.addCommand(rbacCommand);
4357
+ program.addCommand(gdprCommand);
4358
+ program.addCommand(securityReportCommand);
4359
+ program.addCommand(stagesCommand);
4360
+ program.addCommand(pluginCommand);
4361
+ program.addCommand(goalCommand);
4362
+ program.addCommand(pushCommand);
4363
+ program.addCommand(attachCommand);
4364
+ program.addCommand(templateCommand);
4365
+ program.addCommand(sequenceCommand);
4366
+ program.addCommand(quoteCommand);
4367
+ program.addCommand(ticketCommand);
4368
+ program.addCommand(surveyCommand);
4369
+ program.addCommand(kbCommand);
4370
+ program.addCommand(fieldsCommand);
4371
+ program.addCommand(objectCommand);
4372
+ program.addCommand(webhookCommand);
4373
+ program.addCommand(segmentCommand);
4374
+ program.addCommand(identityCommand);
4375
+ program.addCommand(metricsCommand);
4376
+ program.addCommand(usageCommand);
4377
+ program.addCommand(approvalsCommand);
4378
+ program.addCommand(policyCommand);
4379
+ program.addCommand(hygieneCommand);
4380
+ program.addCommand(memoryCommand);
4381
+ program.addCommand(sopCommand);
4382
+ program.addCommand(toneCommand);
4383
+ program.addCommand(autofillCommand);
4384
+ program.addCommand(askCommand);
4385
+ program.addCommand(nbaCommand);
4386
+ program.addCommand(vaultCommand);
4387
+ program.addCommand(churnCommand);
4388
+ program.addCommand(leadscoreCommand);
4389
+ program.addCommand(enrichCommand);
4390
+ program.addCommand(coachCommand);
4391
+ program.addCommand(complianceCommand);
4392
+ await program.parseAsync(process.argv);
4393
+ //#endregion
4394
+ export {};
4395
+
4396
+ //# sourceMappingURL=cli.js.map