@datasynx/agentic-crm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +767 -0
  3. package/dist/agent-config-zPvcqu07.js +14 -0
  4. package/dist/agent-config-zPvcqu07.js.map +1 -0
  5. package/dist/approvals-DpjxGHFp.js +67 -0
  6. package/dist/approvals-DpjxGHFp.js.map +1 -0
  7. package/dist/ask-CID3jnuL.js +52 -0
  8. package/dist/ask-CID3jnuL.js.map +1 -0
  9. package/dist/audit-log-DNMY9mUZ.js +49 -0
  10. package/dist/audit-log-DNMY9mUZ.js.map +1 -0
  11. package/dist/auth-CyFuu9X_.js +2 -0
  12. package/dist/auth-DFWwWcYD.js +93 -0
  13. package/dist/auth-DFWwWcYD.js.map +1 -0
  14. package/dist/autofill-Di_-SP7t.js +51 -0
  15. package/dist/autofill-Di_-SP7t.js.map +1 -0
  16. package/dist/backup-CeMk9z86.js +417 -0
  17. package/dist/backup-CeMk9z86.js.map +1 -0
  18. package/dist/backup-f_hC7rBV.js +2 -0
  19. package/dist/calendly-Bft_wwji.js +52 -0
  20. package/dist/calendly-Bft_wwji.js.map +1 -0
  21. package/dist/calendly-D3coO92o.cjs +53 -0
  22. package/dist/calendly-D3coO92o.cjs.map +1 -0
  23. package/dist/chunk-DakpK96I.cjs +43 -0
  24. package/dist/churn-C28IgnAj.js +54 -0
  25. package/dist/churn-C28IgnAj.js.map +1 -0
  26. package/dist/cli.js +4396 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/colors-BG07TZQz.js +11 -0
  29. package/dist/colors-BG07TZQz.js.map +1 -0
  30. package/dist/compliance-B1kk5-YS.js +115 -0
  31. package/dist/compliance-B1kk5-YS.js.map +1 -0
  32. package/dist/compliance-B91zNvCR.cjs +156 -0
  33. package/dist/compliance-B91zNvCR.cjs.map +1 -0
  34. package/dist/compliance-CKSBoQUe.js +118 -0
  35. package/dist/compliance-CKSBoQUe.js.map +1 -0
  36. package/dist/compliance-CujOqAKk.js +2 -0
  37. package/dist/context-builder-BzWAp3Zs.js +96 -0
  38. package/dist/context-builder-BzWAp3Zs.js.map +1 -0
  39. package/dist/context-builder-DlrRcqmJ.js +2 -0
  40. package/dist/conversation-intel-mm7Lhemh.js +72 -0
  41. package/dist/conversation-intel-mm7Lhemh.js.map +1 -0
  42. package/dist/custom-fields-CzNeD3_v.js +2 -0
  43. package/dist/custom-fields-Pl2t9xzp.js +73 -0
  44. package/dist/custom-fields-Pl2t9xzp.js.map +1 -0
  45. package/dist/custom-objects-BHgn1GEX.js +78 -0
  46. package/dist/custom-objects-BHgn1GEX.js.map +1 -0
  47. package/dist/custom-objects-CIFrmQ2V.js +2 -0
  48. package/dist/customer-dir-DIylZ8Q6.js +75 -0
  49. package/dist/customer-dir-DIylZ8Q6.js.map +1 -0
  50. package/dist/daemon/worker.js +207 -0
  51. package/dist/daemon/worker.js.map +1 -0
  52. package/dist/enrichment-3XvgGDfB.js +103 -0
  53. package/dist/enrichment-3XvgGDfB.js.map +1 -0
  54. package/dist/file-lock-B_zi7NQl.js +22 -0
  55. package/dist/file-lock-B_zi7NQl.js.map +1 -0
  56. package/dist/gmail-auth-BP6cJwfw.js +40 -0
  57. package/dist/gmail-auth-BP6cJwfw.js.map +1 -0
  58. package/dist/gmail-auth-DxakCtGm.cjs +44 -0
  59. package/dist/gmail-auth-DxakCtGm.cjs.map +1 -0
  60. package/dist/gmail-auth-OComS92L.js +40 -0
  61. package/dist/gmail-auth-OComS92L.js.map +1 -0
  62. package/dist/gmail-push-watch-DELQFMPk.js +20 -0
  63. package/dist/gmail-push-watch-DELQFMPk.js.map +1 -0
  64. package/dist/gmail-sender-StTpJ9Ub.js +32 -0
  65. package/dist/gmail-sender-StTpJ9Ub.js.map +1 -0
  66. package/dist/gmail-sync-DIaxInDT.js +204 -0
  67. package/dist/gmail-sync-DIaxInDT.js.map +1 -0
  68. package/dist/gmail-sync-hHm9gaWd.cjs +218 -0
  69. package/dist/gmail-sync-hHm9gaWd.cjs.map +1 -0
  70. package/dist/gmail-sync-rQaVqKWd.js +214 -0
  71. package/dist/gmail-sync-rQaVqKWd.js.map +1 -0
  72. package/dist/gmail-webhook-handler-DS7OlRPX.js +3 -0
  73. package/dist/gmail-webhook-handler-e5Od25FX.js +97 -0
  74. package/dist/gmail-webhook-handler-e5Od25FX.js.map +1 -0
  75. package/dist/goal-engine-CUZSpERI.js +2 -0
  76. package/dist/goal-engine-KpBftn4V.js +295 -0
  77. package/dist/goal-engine-KpBftn4V.js.map +1 -0
  78. package/dist/google-drive-sync-DEPcqFca.js +105 -0
  79. package/dist/google-drive-sync-DEPcqFca.js.map +1 -0
  80. package/dist/hybrid-search-BmHttLrR.js +40 -0
  81. package/dist/hybrid-search-BmHttLrR.js.map +1 -0
  82. package/dist/hygiene-DZqfYpFf.js +38 -0
  83. package/dist/hygiene-DZqfYpFf.js.map +1 -0
  84. package/dist/identity-CI6olMNm.js +41 -0
  85. package/dist/identity-CI6olMNm.js.map +1 -0
  86. package/dist/identity-gyfWdrcX.js +2 -0
  87. package/dist/import-hubspot-BaK71U_K.js +588 -0
  88. package/dist/import-hubspot-BaK71U_K.js.map +1 -0
  89. package/dist/index-V8BFaH-b.d.ts +539 -0
  90. package/dist/index-V8BFaH-b.d.ts.map +1 -0
  91. package/dist/index-YqwMd6aQ.d.cts +538 -0
  92. package/dist/index-YqwMd6aQ.d.cts.map +1 -0
  93. package/dist/index.cjs +185 -0
  94. package/dist/index.cjs.map +1 -0
  95. package/dist/index.d.cts +538 -0
  96. package/dist/index.d.cts.map +1 -0
  97. package/dist/index.d.ts +539 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +165 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/interactions-writer-CrPStUll.cjs +77 -0
  102. package/dist/interactions-writer-CrPStUll.cjs.map +1 -0
  103. package/dist/interactions-writer-DO3KcSR3.js +52 -0
  104. package/dist/interactions-writer-DO3KcSR3.js.map +1 -0
  105. package/dist/interactions-writer-SLHnoEeE.js +46 -0
  106. package/dist/interactions-writer-SLHnoEeE.js.map +1 -0
  107. package/dist/interactions-writer-dSPy1XfO.js +2 -0
  108. package/dist/knowledge-base-D0Fh40kc.js +1013 -0
  109. package/dist/knowledge-base-D0Fh40kc.js.map +1 -0
  110. package/dist/lancedb-CCBbpulq.js +2 -0
  111. package/dist/lancedb-rlvWoPwl.js +98 -0
  112. package/dist/lancedb-rlvWoPwl.js.map +1 -0
  113. package/dist/lead-model-BCFzyktm.js +109 -0
  114. package/dist/lead-model-BCFzyktm.js.map +1 -0
  115. package/dist/llm-DEjWcqmW.js +2 -0
  116. package/dist/llm-DvzZqva0.js +372 -0
  117. package/dist/llm-DvzZqva0.js.map +1 -0
  118. package/dist/llm-Z8RIYkpF.js +174 -0
  119. package/dist/llm-Z8RIYkpF.js.map +1 -0
  120. package/dist/llm-iijeXmgq.cjs +198 -0
  121. package/dist/llm-iijeXmgq.cjs.map +1 -0
  122. package/dist/mcp-CdTJWTJf.d.cts +12 -0
  123. package/dist/mcp-CdTJWTJf.d.cts.map +1 -0
  124. package/dist/mcp-CdTJWTJf.d.ts +12 -0
  125. package/dist/mcp-CdTJWTJf.d.ts.map +1 -0
  126. package/dist/mcp.cjs +7464 -0
  127. package/dist/mcp.cjs.map +1 -0
  128. package/dist/mcp.d.cts +12 -0
  129. package/dist/mcp.d.cts.map +1 -0
  130. package/dist/mcp.d.ts +12 -0
  131. package/dist/mcp.d.ts.map +1 -0
  132. package/dist/mcp.js +7448 -0
  133. package/dist/mcp.js.map +1 -0
  134. package/dist/memory-Bb6ky3kb.js +58 -0
  135. package/dist/memory-Bb6ky3kb.js.map +1 -0
  136. package/dist/memory-Cy6-Tbyl.js +2 -0
  137. package/dist/metrics-DH8wHvya.js +26 -0
  138. package/dist/metrics-DH8wHvya.js.map +1 -0
  139. package/dist/microsoft-auth-B8_S45gh.js +17 -0
  140. package/dist/microsoft-auth-B8_S45gh.js.map +1 -0
  141. package/dist/microsoft-calendar-B6MMtUQK.js +67 -0
  142. package/dist/microsoft-calendar-B6MMtUQK.js.map +1 -0
  143. package/dist/microsoft-sync-CpZVoSuq.js +68 -0
  144. package/dist/microsoft-sync-CpZVoSuq.js.map +1 -0
  145. package/dist/nba-3wanmJ0U.js +48 -0
  146. package/dist/nba-3wanmJ0U.js.map +1 -0
  147. package/dist/notification-dispatcher-0vYNngWe.js +97 -0
  148. package/dist/notification-dispatcher-0vYNngWe.js.map +1 -0
  149. package/dist/opportunity-score-BTMOQSTV.js +47 -0
  150. package/dist/opportunity-score-BTMOQSTV.js.map +1 -0
  151. package/dist/pipedrive-client-CdGKpH9b.js +17 -0
  152. package/dist/pipedrive-client-CdGKpH9b.js.map +1 -0
  153. package/dist/pipeline-writer-BqBrYrQc.js +2 -0
  154. package/dist/pipeline-writer-BvVquKIe.js +96 -0
  155. package/dist/pipeline-writer-BvVquKIe.js.map +1 -0
  156. package/dist/pipeline-writer-N2omexxp.cjs +121 -0
  157. package/dist/pipeline-writer-N2omexxp.cjs.map +1 -0
  158. package/dist/pipeline-writer-eufx_0o1.js +102 -0
  159. package/dist/pipeline-writer-eufx_0o1.js.map +1 -0
  160. package/dist/proactive-agent-BgQXw3ac.js +96 -0
  161. package/dist/proactive-agent-BgQXw3ac.js.map +1 -0
  162. package/dist/proactive-worker-BrLHNhjH.js +229 -0
  163. package/dist/proactive-worker-BrLHNhjH.js.map +1 -0
  164. package/dist/push-manager-CdqIIkuh.js +108 -0
  165. package/dist/push-manager-CdqIIkuh.js.map +1 -0
  166. package/dist/push-manager-CowY-0IK.js +2 -0
  167. package/dist/quote-generator-BfwENXzg.js +133 -0
  168. package/dist/quote-generator-BfwENXzg.js.map +1 -0
  169. package/dist/quote-generator-OhSFsi3x.js +2 -0
  170. package/dist/rbac-C7c8tcES.js +2 -0
  171. package/dist/rbac-CTIktZaC.js +91 -0
  172. package/dist/rbac-CTIktZaC.js.map +1 -0
  173. package/dist/relationship-health-odxEoQdJ.js +454 -0
  174. package/dist/relationship-health-odxEoQdJ.js.map +1 -0
  175. package/dist/revenue-simulation-BJdRTEHc.js +2 -0
  176. package/dist/revenue-simulation-Bqf2DLVB.js +251 -0
  177. package/dist/revenue-simulation-Bqf2DLVB.js.map +1 -0
  178. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  179. package/dist/salesforce-client-rhZFa_p5.js +51 -0
  180. package/dist/salesforce-client-rhZFa_p5.js.map +1 -0
  181. package/dist/segments-BqcD5HIl.js +61 -0
  182. package/dist/segments-BqcD5HIl.js.map +1 -0
  183. package/dist/sequence-engine-CCTHEBgi.js +2 -0
  184. package/dist/sequence-engine-J1lTW_in.js +91 -0
  185. package/dist/sequence-engine-J1lTW_in.js.map +1 -0
  186. package/dist/sequence-store-DaaWr0Os.js +221 -0
  187. package/dist/sequence-store-DaaWr0Os.js.map +1 -0
  188. package/dist/server-Dyva03K8.js +4287 -0
  189. package/dist/server-Dyva03K8.js.map +1 -0
  190. package/dist/session-B9AilxOE.js +81 -0
  191. package/dist/session-B9AilxOE.js.map +1 -0
  192. package/dist/session-D0qFkBla.cjs +82 -0
  193. package/dist/session-D0qFkBla.cjs.map +1 -0
  194. package/dist/session-D9ub6Wl1.js +79 -0
  195. package/dist/session-D9ub6Wl1.js.map +1 -0
  196. package/dist/session-mWHA71Lw.js +2 -0
  197. package/dist/session-store-B0QZE8Bx.cjs +697 -0
  198. package/dist/session-store-B0QZE8Bx.cjs.map +1 -0
  199. package/dist/session-store-C8tEvMPw.js +543 -0
  200. package/dist/session-store-C8tEvMPw.js.map +1 -0
  201. package/dist/session-store-CEa39Dxs.js +15 -0
  202. package/dist/session-store-CEa39Dxs.js.map +1 -0
  203. package/dist/sla-engine-5IhTsBUR.js +2 -0
  204. package/dist/sla-engine-BqX-7u-7.js +53 -0
  205. package/dist/sla-engine-BqX-7u-7.js.map +1 -0
  206. package/dist/sop-DkhVChGy.js +2 -0
  207. package/dist/sop-Vp0UPWFW.js +70 -0
  208. package/dist/sop-Vp0UPWFW.js.map +1 -0
  209. package/dist/survey-engine-C06hcQt3.js +2 -0
  210. package/dist/survey-engine-DBjCYqCv.js +147 -0
  211. package/dist/survey-engine-DBjCYqCv.js.map +1 -0
  212. package/dist/sync-state-ChaLbamC.js +33 -0
  213. package/dist/sync-state-ChaLbamC.js.map +1 -0
  214. package/dist/sync-state-CwLSt_1m.js +2 -0
  215. package/dist/ticket-writer-CjqKeIRD.js +2 -0
  216. package/dist/ticket-writer-j2oX_Wal.js +134 -0
  217. package/dist/ticket-writer-j2oX_Wal.js.map +1 -0
  218. package/dist/tone-Bdm5uaht.js +48 -0
  219. package/dist/tone-Bdm5uaht.js.map +1 -0
  220. package/dist/tone-DRKlZgPr.cjs +43 -0
  221. package/dist/tone-DRKlZgPr.cjs.map +1 -0
  222. package/dist/tone-vNb2DAAD.js +39 -0
  223. package/dist/tone-vNb2DAAD.js.map +1 -0
  224. package/dist/transcript-watcher-CL2QUygI.js +132 -0
  225. package/dist/transcript-watcher-CL2QUygI.js.map +1 -0
  226. package/dist/unmatched-transcripts-BsH5bhkU.js +26 -0
  227. package/dist/unmatched-transcripts-BsH5bhkU.js.map +1 -0
  228. package/dist/unmatched-transcripts-D0PrJ9iz.js +2 -0
  229. package/dist/update-deal-BNwPGaTV.js +2 -0
  230. package/dist/update-deal-DKC79skb.js +91 -0
  231. package/dist/update-deal-DKC79skb.js.map +1 -0
  232. package/dist/usage-CClTf5e6.cjs +57 -0
  233. package/dist/usage-CClTf5e6.cjs.map +1 -0
  234. package/dist/usage-D0-TYJkw.js +93 -0
  235. package/dist/usage-D0-TYJkw.js.map +1 -0
  236. package/dist/usage-D0u9a-lV.js +54 -0
  237. package/dist/usage-D0u9a-lV.js.map +1 -0
  238. package/dist/vault-C1D3zScD.js +2 -0
  239. package/dist/vault-DXCg29W-.js +86 -0
  240. package/dist/vault-DXCg29W-.js.map +1 -0
  241. package/dist/webhooks-7EpA05Qr.js +138 -0
  242. package/dist/webhooks-7EpA05Qr.js.map +1 -0
  243. package/dist/webhooks-BO2UAnmn.js +94 -0
  244. package/dist/webhooks-BO2UAnmn.js.map +1 -0
  245. package/dist/webhooks-Xn6zO6kd.cjs +97 -0
  246. package/dist/webhooks-Xn6zO6kd.cjs.map +1 -0
  247. package/dist/write-queue-BDolUxfs.cjs +26 -0
  248. package/dist/write-queue-BDolUxfs.cjs.map +1 -0
  249. package/dist/write-queue-IbsAjUnh.js +21 -0
  250. package/dist/write-queue-IbsAjUnh.js.map +1 -0
  251. package/package.json +142 -0
@@ -0,0 +1,91 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ //#region src/core/rbac.ts
4
+ const ALLOWED_TOOLS = {
5
+ admin: [
6
+ "log_interaction",
7
+ "update_deal",
8
+ "update_customer_facts",
9
+ "export_customer",
10
+ "pursue_goal",
11
+ "register_push_subscription",
12
+ "define_custom_object",
13
+ "create_record"
14
+ ],
15
+ manager: [
16
+ "log_interaction",
17
+ "update_deal",
18
+ "pursue_goal",
19
+ "create_record"
20
+ ],
21
+ rep: [
22
+ "log_interaction",
23
+ "update_deal",
24
+ "create_record"
25
+ ]
26
+ };
27
+ function rbacPath(dataDir) {
28
+ return path.join(dataDir, ".agentic", "rbac.json");
29
+ }
30
+ function getRbacConfig(dataDir) {
31
+ const p = rbacPath(dataDir);
32
+ if (!fs.existsSync(p)) return { actors: {} };
33
+ try {
34
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
35
+ } catch {
36
+ return { actors: {} };
37
+ }
38
+ }
39
+ function setActorRole(dataDir, actor, role) {
40
+ const p = rbacPath(dataDir);
41
+ fs.mkdirSync(path.dirname(p), { recursive: true });
42
+ const config = getRbacConfig(dataDir);
43
+ config.actors[actor] = role;
44
+ fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
45
+ }
46
+ function getRole(dataDir, actor) {
47
+ const config = getRbacConfig(dataDir);
48
+ return config.actors[actor] ?? config.default ?? "rep";
49
+ }
50
+ function canWrite(role, tool) {
51
+ return ALLOWED_TOOLS[role]?.includes(tool) ?? false;
52
+ }
53
+ function assertCanWrite(role, tool, actor) {
54
+ if (!canWrite(role, tool)) throw new Error(`Access denied: '${actor}' (role: ${role}) cannot use tool '${tool}'`);
55
+ }
56
+ function enforceRbac(dataDir, tool) {
57
+ if (!fs.existsSync(rbacPath(dataDir))) return;
58
+ const actor = process.env["DXCRM_ACTOR"] ?? "system";
59
+ if (actor === "system") return;
60
+ assertCanWrite(getRole(dataDir, actor), tool, actor);
61
+ }
62
+ function canSeeCustomer(dataDir, actor, slug) {
63
+ if (!fs.existsSync(rbacPath(dataDir))) return true;
64
+ if (actor === "system") return true;
65
+ const config = getRbacConfig(dataDir);
66
+ const role = config.actors[actor] ?? config.default ?? "rep";
67
+ if (role === "admin" || role === "manager") return true;
68
+ const owned = config.owned_customers;
69
+ if (!owned) return false;
70
+ return (owned[actor] ?? []).includes(slug);
71
+ }
72
+ /** Load the field-level ACL (field → allowed roles) from rbac.json. */
73
+ function loadFieldAcl(dataDir) {
74
+ return getRbacConfig(dataDir).field_acl ?? {};
75
+ }
76
+ /** Whether a role may see a field given the ACL (fields not in the ACL are public). */
77
+ function canSeeField(field, role, acl) {
78
+ const allowed = acl[field];
79
+ if (!allowed) return true;
80
+ return allowed.includes(role);
81
+ }
82
+ /** Return a copy of `values` with fields the role may not see removed. */
83
+ function redactFields(values, role, acl) {
84
+ const out = {};
85
+ for (const [k, v] of Object.entries(values)) if (canSeeField(k, role, acl)) out[k] = v;
86
+ return out;
87
+ }
88
+ //#endregion
89
+ export { enforceRbac as a, loadFieldAcl as c, canWrite as i, redactFields as l, canSeeCustomer as n, getRbacConfig as o, canSeeField as r, getRole as s, assertCanWrite as t, setActorRole as u };
90
+
91
+ //# sourceMappingURL=rbac-CTIktZaC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rbac-CTIktZaC.js","names":[],"sources":["../src/core/rbac.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\nexport type Role = \"admin\" | \"manager\" | \"rep\";\n\nexport interface RbacConfig {\n actors: Record<string, Role>;\n default?: Role;\n owned_customers?: Record<string, string[]>;\n /** Field-level ACL: field name → roles allowed to see it. Others get it redacted. */\n field_acl?: Record<string, Role[]>;\n}\n\nconst ALLOWED_TOOLS: Record<Role, string[]> = {\n admin: [\n \"log_interaction\",\n \"update_deal\",\n \"update_customer_facts\",\n \"export_customer\",\n \"pursue_goal\",\n \"register_push_subscription\",\n \"define_custom_object\",\n \"create_record\",\n ],\n manager: [\"log_interaction\", \"update_deal\", \"pursue_goal\", \"create_record\"],\n rep: [\"log_interaction\", \"update_deal\", \"create_record\"],\n};\n\nfunction rbacPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"rbac.json\");\n}\n\nexport function getRbacConfig(dataDir: string): RbacConfig {\n const p = rbacPath(dataDir);\n if (!fs.existsSync(p)) return { actors: {} };\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as RbacConfig;\n } catch {\n return { actors: {} };\n }\n}\n\nexport function setActorRole(dataDir: string, actor: string, role: Role): void {\n const p = rbacPath(dataDir);\n fs.mkdirSync(path.dirname(p), { recursive: true });\n const config = getRbacConfig(dataDir);\n config.actors[actor] = role;\n fs.writeFileSync(p, JSON.stringify(config, null, 2), \"utf-8\");\n}\n\nexport function getRole(dataDir: string, actor: string): Role {\n const config = getRbacConfig(dataDir);\n return config.actors[actor] ?? config.default ?? \"rep\";\n}\n\nexport function canWrite(role: Role, tool: string): boolean {\n return ALLOWED_TOOLS[role]?.includes(tool) ?? false;\n}\n\nexport function assertCanWrite(role: Role, tool: string, actor: string): void {\n if (!canWrite(role, tool)) {\n throw new Error(`Access denied: '${actor}' (role: ${role}) cannot use tool '${tool}'`);\n }\n}\n\nexport function enforceRbac(dataDir: string, tool: string): void {\n if (!fs.existsSync(rbacPath(dataDir))) return; // no rbac.json = open access\n const actor = process.env[\"DXCRM_ACTOR\"] ?? \"system\";\n if (actor === \"system\") return; // internal system actor bypasses RBAC\n const role = getRole(dataDir, actor);\n assertCanWrite(role, tool, actor);\n}\n\nexport function canSeeCustomer(dataDir: string, actor: string, slug: string): boolean {\n if (!fs.existsSync(rbacPath(dataDir))) return true; // open access\n if (actor === \"system\") return true; // internal system actor always has full access\n const config = getRbacConfig(dataDir);\n const role = config.actors[actor] ?? config.default ?? \"rep\";\n if (role === \"admin\" || role === \"manager\") return true;\n // rep: only sees customers listed in owned_customers[actor]\n const owned = config.owned_customers;\n if (!owned) return false;\n return (owned[actor] ?? []).includes(slug);\n}\n\n/** Load the field-level ACL (field → allowed roles) from rbac.json. */\nexport function loadFieldAcl(dataDir: string): Record<string, Role[]> {\n return getRbacConfig(dataDir).field_acl ?? {};\n}\n\n/** Whether a role may see a field given the ACL (fields not in the ACL are public). */\nexport function canSeeField(field: string, role: Role, acl: Record<string, Role[]>): boolean {\n const allowed = acl[field];\n if (!allowed) return true;\n return allowed.includes(role);\n}\n\n/** Return a copy of `values` with fields the role may not see removed. */\nexport function redactFields<T extends Record<string, unknown>>(\n values: T,\n role: Role,\n acl: Record<string, Role[]>\n): Partial<T> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(values)) {\n if (canSeeField(k, role, acl)) out[k] = v;\n }\n return out as Partial<T>;\n}\n"],"mappings":";;;AAaA,MAAM,gBAAwC;CAC5C,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,SAAS;EAAC;EAAmB;EAAe;EAAe;CAAe;CAC1E,KAAK;EAAC;EAAmB;EAAe;CAAe;AACzD;AAEA,SAAS,SAAS,SAAyB;CACzC,OAAO,KAAK,KAAK,SAAS,YAAY,WAAW;AACnD;AAEA,SAAgB,cAAc,SAA6B;CACzD,MAAM,IAAI,SAAS,OAAO;CAC1B,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,OAAO,EAAE,QAAQ,CAAC,EAAE;CAC3C,IAAI;EACF,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,OAAO,CAAW;CACzD,QAAQ;EACN,OAAO,EAAE,QAAQ,CAAC,EAAE;CACtB;AACF;AAEA,SAAgB,aAAa,SAAiB,OAAe,MAAkB;CAC7E,MAAM,IAAI,SAAS,OAAO;CAC1B,GAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;CACjD,MAAM,SAAS,cAAc,OAAO;CACpC,OAAO,OAAO,SAAS;CACvB,GAAG,cAAc,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AAC9D;AAEA,SAAgB,QAAQ,SAAiB,OAAqB;CAC5D,MAAM,SAAS,cAAc,OAAO;CACpC,OAAO,OAAO,OAAO,UAAU,OAAO,WAAW;AACnD;AAEA,SAAgB,SAAS,MAAY,MAAuB;CAC1D,OAAO,cAAc,OAAO,SAAS,IAAI,KAAK;AAChD;AAEA,SAAgB,eAAe,MAAY,MAAc,OAAqB;CAC5E,IAAI,CAAC,SAAS,MAAM,IAAI,GACtB,MAAM,IAAI,MAAM,mBAAmB,MAAM,WAAW,KAAK,qBAAqB,KAAK,EAAE;AAEzF;AAEA,SAAgB,YAAY,SAAiB,MAAoB;CAC/D,IAAI,CAAC,GAAG,WAAW,SAAS,OAAO,CAAC,GAAG;CACvC,MAAM,QAAQ,QAAQ,IAAI,kBAAkB;CAC5C,IAAI,UAAU,UAAU;CAExB,eADa,QAAQ,SAAS,KACZ,GAAG,MAAM,KAAK;AAClC;AAEA,SAAgB,eAAe,SAAiB,OAAe,MAAuB;CACpF,IAAI,CAAC,GAAG,WAAW,SAAS,OAAO,CAAC,GAAG,OAAO;CAC9C,IAAI,UAAU,UAAU,OAAO;CAC/B,MAAM,SAAS,cAAc,OAAO;CACpC,MAAM,OAAO,OAAO,OAAO,UAAU,OAAO,WAAW;CACvD,IAAI,SAAS,WAAW,SAAS,WAAW,OAAO;CAEnD,MAAM,QAAQ,OAAO;CACrB,IAAI,CAAC,OAAO,OAAO;CACnB,QAAQ,MAAM,UAAU,CAAC,GAAG,SAAS,IAAI;AAC3C;;AAGA,SAAgB,aAAa,SAAyC;CACpE,OAAO,cAAc,OAAO,EAAE,aAAa,CAAC;AAC9C;;AAGA,SAAgB,YAAY,OAAe,MAAY,KAAsC;CAC3F,MAAM,UAAU,IAAI;CACpB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO,QAAQ,SAAS,IAAI;AAC9B;;AAGA,SAAgB,aACd,QACA,MACA,KACY;CACZ,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,GACxC,IAAI,YAAY,GAAG,MAAM,GAAG,GAAG,IAAI,KAAK;CAE1C,OAAO;AACT"}
@@ -0,0 +1,454 @@
1
+ import { t as withJsonFile } from "./file-lock-B_zi7NQl.js";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import matter from "gray-matter";
5
+ //#region src/core/graph.ts
6
+ function graphPath(dataDir, slug) {
7
+ return path.join(dataDir, "customers", slug, "graph.json");
8
+ }
9
+ function emptyGraph(slug) {
10
+ return {
11
+ schemaVersion: "1",
12
+ slug,
13
+ nodes: [],
14
+ edges: [],
15
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16
+ };
17
+ }
18
+ function readGraph(dataDir, slug) {
19
+ const p = graphPath(dataDir, slug);
20
+ if (!fs.existsSync(p)) return emptyGraph(slug);
21
+ try {
22
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
23
+ } catch {
24
+ process.stderr.write(`[graph] failed to parse ${p} — returning empty graph\n`);
25
+ return emptyGraph(slug);
26
+ }
27
+ }
28
+ async function writeGraph(dataDir, slug, updater) {
29
+ return withJsonFile(graphPath(dataDir, slug), (current) => {
30
+ return {
31
+ ...updater(current),
32
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33
+ };
34
+ });
35
+ }
36
+ function upsertNode(graph, node) {
37
+ const now = (/* @__PURE__ */ new Date()).toISOString();
38
+ const idx = graph.nodes.findIndex((n) => n.id === node.id);
39
+ if (idx !== -1) {
40
+ const existing = graph.nodes[idx];
41
+ const updated = {
42
+ ...existing,
43
+ label: node.label || existing.label,
44
+ properties: {
45
+ ...existing.properties,
46
+ ...node.properties
47
+ },
48
+ updatedAt: now
49
+ };
50
+ const nodes = [...graph.nodes];
51
+ nodes[idx] = updated;
52
+ return {
53
+ ...graph,
54
+ nodes
55
+ };
56
+ }
57
+ const newNode = {
58
+ ...node,
59
+ createdAt: now,
60
+ updatedAt: now
61
+ };
62
+ return {
63
+ ...graph,
64
+ nodes: [...graph.nodes, newNode]
65
+ };
66
+ }
67
+ function findNode(graph, id) {
68
+ return graph.nodes.find((n) => n.id === id);
69
+ }
70
+ function findNodesByType(graph, type) {
71
+ return graph.nodes.filter((n) => n.type === type);
72
+ }
73
+ function makeEdgeId(type, fromId, toId) {
74
+ return `${type}:${fromId}__${toId}`;
75
+ }
76
+ function upsertEdge(graph, edge) {
77
+ const id = edge.id ?? makeEdgeId(edge.type, edge.from, edge.to);
78
+ const idx = graph.edges.findIndex((e) => e.id === id);
79
+ if (idx !== -1) {
80
+ const existing = graph.edges[idx];
81
+ const updated = {
82
+ ...existing,
83
+ weight: Math.min(1, existing.weight + .05),
84
+ contactCount: existing.contactCount + 1,
85
+ lastContact: edge.lastContact,
86
+ properties: {
87
+ ...existing.properties,
88
+ ...edge.properties
89
+ }
90
+ };
91
+ const edges = [...graph.edges];
92
+ edges[idx] = updated;
93
+ return {
94
+ ...graph,
95
+ edges
96
+ };
97
+ }
98
+ const now = (/* @__PURE__ */ new Date()).toISOString();
99
+ const newEdge = {
100
+ ...edge,
101
+ id,
102
+ recordedAt: edge.recordedAt ?? now,
103
+ validFrom: edge.validFrom ?? edge.lastContact ?? now
104
+ };
105
+ return {
106
+ ...graph,
107
+ edges: [...graph.edges, newEdge]
108
+ };
109
+ }
110
+ function getStakeholders(graph) {
111
+ const dedup = (nodes) => {
112
+ const seen = /* @__PURE__ */ new Set();
113
+ return nodes.filter((n) => seen.has(n.id) ? false : (seen.add(n.id), true));
114
+ };
115
+ const resolve = (edges) => dedup(edges.map((e) => findNode(graph, e.from)).filter((n) => n !== void 0));
116
+ const champions = resolve(graph.edges.filter((e) => e.type === "IS_CHAMPION"));
117
+ const blockers = resolve(graph.edges.filter((e) => e.type === "IS_BLOCKER"));
118
+ const economicBuyers = resolve(graph.edges.filter((e) => e.type === "IS_ECONOMIC_BUYER"));
119
+ const allContacts = findNodesByType(graph, "person");
120
+ const missingRoles = [];
121
+ if (allContacts.length > 0) {
122
+ if (champions.length === 0) missingRoles.push({
123
+ role: "champion",
124
+ urgency: "important",
125
+ suggestion: "Identify who is driving this deal internally."
126
+ });
127
+ if (economicBuyers.length === 0) missingRoles.push({
128
+ role: "economic_buyer",
129
+ urgency: "critical",
130
+ suggestion: "Find out who signs the contract. Ask your champion directly."
131
+ });
132
+ }
133
+ return {
134
+ champions,
135
+ blockers,
136
+ economicBuyers,
137
+ allContacts,
138
+ missingRoles
139
+ };
140
+ }
141
+ /** BFS shortest path between two nodes. Returns [] when no path exists. */
142
+ function findPath(graph, fromId, toId) {
143
+ if (fromId === toId) return [fromId];
144
+ const visited = new Set([fromId]);
145
+ const queue = [{
146
+ nodeId: fromId,
147
+ path: [fromId]
148
+ }];
149
+ while (queue.length > 0) {
150
+ const current = queue.shift();
151
+ const neighbors = graph.edges.filter((e) => e.from === current.nodeId || e.to === current.nodeId).map((e) => e.from === current.nodeId ? e.to : e.from).filter((id) => !visited.has(id));
152
+ for (const neighborId of neighbors) {
153
+ const newPath = [...current.path, neighborId];
154
+ if (neighborId === toId) return newPath;
155
+ visited.add(neighborId);
156
+ queue.push({
157
+ nodeId: neighborId,
158
+ path: newPath
159
+ });
160
+ }
161
+ }
162
+ return [];
163
+ }
164
+ //#endregion
165
+ //#region src/core/email-normalizer.ts
166
+ function normalizeEmail(raw) {
167
+ if (!raw) return "";
168
+ const trimmed = raw.trim();
169
+ const angleMatch = trimmed.match(/<([^>]+)>/);
170
+ return (angleMatch ? angleMatch[1] : trimmed).toLowerCase().trim();
171
+ }
172
+ //#endregion
173
+ //#region src/core/graph-extractor.ts
174
+ function extractEmail(withStr) {
175
+ const angleMatch = withStr.match(/<([^>]+@[^>]+)>/);
176
+ if (angleMatch?.[1]) return angleMatch[1].toLowerCase();
177
+ const bareMatch = withStr.match(/^([^\s]+@[^\s]+)$/);
178
+ if (bareMatch?.[1]) return bareMatch[1].toLowerCase();
179
+ }
180
+ function extractDisplayName(withStr) {
181
+ const match = withStr.match(/^(.+?)\s*<[^>]+>$/);
182
+ if (match?.[1]) return match[1].trim();
183
+ return withStr.trim();
184
+ }
185
+ function makePersonId(withStr, slug) {
186
+ const email = normalizeEmail(withStr);
187
+ if (email.includes("@")) return `person:${email}`;
188
+ return `person:${slug}:${extractDisplayName(withStr).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
189
+ }
190
+ function makeCompanyId(domain, slug, companyName) {
191
+ if (domain) return `company:${domain.toLowerCase()}`;
192
+ if (slug) return `company:${slug}`;
193
+ if (companyName) return `company:${companyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
194
+ return `company:unknown`;
195
+ }
196
+ function extractNodes(input) {
197
+ const email = extractEmail(input.withStr);
198
+ const label = extractDisplayName(input.withStr);
199
+ const personId = makePersonId(input.withStr, input.slug);
200
+ const personProps = {};
201
+ if (email !== void 0) personProps["email"] = email;
202
+ if (input.companyName !== void 0) personProps["company"] = input.companyName;
203
+ if (input.domain !== void 0) personProps["domain"] = input.domain;
204
+ const nodes = [{
205
+ id: personId,
206
+ type: "person",
207
+ label,
208
+ properties: personProps
209
+ }];
210
+ if (input.domain !== void 0 || input.companyName !== void 0) {
211
+ const companyId = makeCompanyId(input.domain, input.slug, input.companyName);
212
+ const companyProps = {};
213
+ if (input.domain !== void 0) companyProps["domain"] = input.domain;
214
+ const companyNode = {
215
+ id: companyId,
216
+ type: "company",
217
+ label: input.companyName ?? input.domain ?? input.slug,
218
+ properties: companyProps
219
+ };
220
+ nodes.push(companyNode);
221
+ }
222
+ return nodes;
223
+ }
224
+ function extractEdges(personId, companyId, interactionDate) {
225
+ if (!companyId) return [];
226
+ return [{
227
+ id: `WORKS_AT:${personId}__${companyId}`,
228
+ from: personId,
229
+ to: companyId,
230
+ type: "WORKS_AT",
231
+ weight: .5,
232
+ sentiment: 0,
233
+ lastContact: interactionDate,
234
+ contactCount: 1,
235
+ properties: {}
236
+ }];
237
+ }
238
+ async function updateGraphFromInteraction(dataDir, slug, input) {
239
+ if (!input.withStr.trim()) return;
240
+ let domain;
241
+ let companyName;
242
+ const factsPath = path.join(dataDir, "customers", slug, "main_facts.md");
243
+ if (fs.existsSync(factsPath)) try {
244
+ const parsed = matter(fs.readFileSync(factsPath, "utf-8"));
245
+ domain = parsed.data["domain"];
246
+ companyName = parsed.data["name"];
247
+ } catch {}
248
+ const extractionInput = {
249
+ slug,
250
+ withStr: input.withStr,
251
+ interactionDate: input.interactionDate
252
+ };
253
+ if (domain !== void 0) extractionInput.domain = domain;
254
+ if (companyName !== void 0) extractionInput.companyName = companyName;
255
+ const nodes = extractNodes(extractionInput);
256
+ const edges = extractEdges(makePersonId(input.withStr, slug), domain !== void 0 || companyName !== void 0 ? makeCompanyId(domain, slug, companyName) : void 0, input.interactionDate);
257
+ await writeGraph(dataDir, slug, (current) => {
258
+ const empty = {
259
+ schemaVersion: "1",
260
+ slug,
261
+ nodes: [],
262
+ edges: [],
263
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
264
+ };
265
+ let graph = current ?? empty;
266
+ for (const node of nodes) graph = upsertNode(graph, node);
267
+ for (const edge of edges) graph = upsertEdge(graph, edge);
268
+ return graph;
269
+ });
270
+ }
271
+ //#endregion
272
+ //#region src/core/relationship-health.ts
273
+ function healthPath(dataDir, slug) {
274
+ return path.join(dataDir, "customers", slug, "health.json");
275
+ }
276
+ function readHealth(dataDir, slug) {
277
+ const p = healthPath(dataDir, slug);
278
+ if (!fs.existsSync(p)) return null;
279
+ try {
280
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ function writeHealth(dataDir, slug, health) {
286
+ const p = healthPath(dataDir, slug);
287
+ const dir = path.dirname(p);
288
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
289
+ const updated = {
290
+ ...health,
291
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
292
+ };
293
+ fs.writeFileSync(p, JSON.stringify(updated, null, 2), "utf-8");
294
+ }
295
+ function parseContactInteractions(content) {
296
+ const blocks = content.split(/(?=^## \d{4}-\d{2}-\d{2})/m).filter((b) => b.trim().length > 0);
297
+ const result = [];
298
+ for (const block of blocks) {
299
+ const headingMatch = block.match(/^## (\d{4}-\d{2}-\d{2}) · (\w+)/m);
300
+ if (!headingMatch) continue;
301
+ const date = headingMatch[1];
302
+ const type = headingMatch[2];
303
+ const withMatch = block.match(/^\*\*(?:With|Subject):\*\*\s*(.+)$/m);
304
+ if (!withMatch) continue;
305
+ const withStr = withMatch[1].trim();
306
+ result.push({
307
+ date,
308
+ type,
309
+ withStr
310
+ });
311
+ }
312
+ return result;
313
+ }
314
+ function calcRecencyScore(daysSince) {
315
+ if (daysSince <= 0) return 100;
316
+ if (daysSince >= 30) return 0;
317
+ return Math.round(100 * (1 - daysSince / 30));
318
+ }
319
+ function calcCadenceScore(daysSince, avgCadenceDays) {
320
+ if (avgCadenceDays <= 0) return 50;
321
+ const ratio = daysSince / avgCadenceDays;
322
+ if (ratio <= 1) return 100;
323
+ if (ratio >= 3) return 0;
324
+ return Math.round(100 * (1 - (ratio - 1) / 2));
325
+ }
326
+ function calcMomentumScore(last30d, prev30d) {
327
+ if (last30d === 0 && prev30d === 0) return 50;
328
+ if (prev30d === 0) return 80;
329
+ const ratio = last30d / prev30d;
330
+ if (ratio >= 1.5) return 100;
331
+ if (ratio >= 1) return 75;
332
+ if (ratio >= .5) return 50;
333
+ if (ratio >= .25) return 25;
334
+ return 0;
335
+ }
336
+ function gradeFromScore(score) {
337
+ if (score >= 80) return "A";
338
+ if (score >= 60) return "B";
339
+ if (score >= 40) return "C";
340
+ if (score >= 20) return "D";
341
+ return "F";
342
+ }
343
+ function trendFromState(score, daysSince, avgCadenceDays, momentumScore) {
344
+ if (score < 20 || daysSince >= 30) return "cold";
345
+ if (momentumScore > 70 && score > 60) return "rising";
346
+ if (momentumScore < 30 || daysSince > avgCadenceDays * 1.5 && score < 60) return "declining";
347
+ return "stable";
348
+ }
349
+ function calcRiskFlags(_contactId, daysSince, score, isChampion) {
350
+ const flags = [];
351
+ if (daysSince >= 30) flags.push("NO_CONTACT_30D");
352
+ if (daysSince >= 14) flags.push("NO_CONTACT_14D");
353
+ if (isChampion && score < 50) flags.push("CHAMPION_SILENT");
354
+ return flags;
355
+ }
356
+ function generateRecommendation(name, grade, trend, riskFlags, daysSince, avgCadenceDays) {
357
+ if (riskFlags.includes("NO_CONTACT_30D")) return `Re-engage ${name} urgently — no contact in ${daysSince} days.`;
358
+ if (riskFlags.includes("CHAMPION_SILENT")) return `Champion ${name} has gone quiet — critical to re-engage before deal stalls.`;
359
+ if (riskFlags.includes("NO_CONTACT_14D")) return `Schedule contact with ${name} — ${daysSince} days since last touchpoint.`;
360
+ if (trend === "declining") return `${name} relationship declining — increase touchpoint frequency.`;
361
+ if (grade === "A") return `${name} — strong relationship. Keep current cadence.`;
362
+ const daysUntilDue = Math.max(0, avgCadenceDays - daysSince);
363
+ return `${name} — grade ${grade}. Next contact due in ~${daysUntilDue} day${daysUntilDue === 1 ? "" : "s"}.`;
364
+ }
365
+ function dateUtcMs(d) {
366
+ return (/* @__PURE__ */ new Date(`${d}T00:00:00Z`)).getTime();
367
+ }
368
+ function calcAvgCadence(interactions) {
369
+ if (interactions.length < 2) return 0;
370
+ const sorted = [...interactions].sort((a, b) => b.date.localeCompare(a.date));
371
+ let totalDays = 0;
372
+ for (let i = 0; i < sorted.length - 1; i++) {
373
+ const gap = Math.round((dateUtcMs(sorted[i].date) - dateUtcMs(sorted[i + 1].date)) / 864e5);
374
+ totalDays += gap;
375
+ }
376
+ return Math.round(totalDays / (sorted.length - 1));
377
+ }
378
+ function groupInteractionsByContact(interactions, slug) {
379
+ const map = /* @__PURE__ */ new Map();
380
+ for (const ix of interactions) {
381
+ const email = extractEmail(ix.withStr);
382
+ const name = extractDisplayName(ix.withStr);
383
+ const contactId = makePersonId(ix.withStr, slug);
384
+ if (!map.has(contactId)) {
385
+ const entry = {
386
+ contactId,
387
+ name,
388
+ interactions: []
389
+ };
390
+ if (email !== void 0) entry.email = email;
391
+ map.set(contactId, entry);
392
+ }
393
+ map.get(contactId).interactions.push(ix);
394
+ }
395
+ return Array.from(map.values());
396
+ }
397
+ function computeContactHealth(group, today, isChampion) {
398
+ const lastContact = [...group.interactions].sort((a, b) => b.date.localeCompare(a.date))[0]?.date ?? "";
399
+ const daysSince = lastContact ? Math.round((dateUtcMs(today) - dateUtcMs(lastContact)) / 864e5) : 999;
400
+ const avgCadenceDays = calcAvgCadence(group.interactions);
401
+ const todayMs = dateUtcMs(today);
402
+ const d30 = todayMs - 30 * 864e5;
403
+ const d60 = todayMs - 60 * 864e5;
404
+ const last30d = group.interactions.filter((i) => dateUtcMs(i.date) >= d30).length;
405
+ const prev30d = group.interactions.filter((i) => dateUtcMs(i.date) >= d60 && dateUtcMs(i.date) < d30).length;
406
+ const recency = calcRecencyScore(daysSince);
407
+ const cadence = calcCadenceScore(daysSince, avgCadenceDays);
408
+ const sentiment = 50;
409
+ const response = 50;
410
+ const momentum = calcMomentumScore(last30d, prev30d);
411
+ const score = Math.round(recency * .35 + cadence * .25 + sentiment * .2 + response * .1 + momentum * .1);
412
+ const grade = gradeFromScore(score);
413
+ const trend = trendFromState(score, daysSince, avgCadenceDays, momentum);
414
+ const riskFlags = calcRiskFlags(group.contactId, daysSince, score, isChampion);
415
+ const recommendation = generateRecommendation(group.name, grade, trend, riskFlags, daysSince, avgCadenceDays);
416
+ const health = {
417
+ contactId: group.contactId,
418
+ name: group.name,
419
+ score,
420
+ grade,
421
+ trend,
422
+ daysSinceContact: daysSince,
423
+ avgCadenceDays,
424
+ sentimentTrend: 0,
425
+ riskFlags,
426
+ lastContact,
427
+ interactionCount30d: last30d,
428
+ recommendation,
429
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
430
+ };
431
+ if (group.email !== void 0) health.email = group.email;
432
+ return health;
433
+ }
434
+ function computeCustomerHealth(dataDir, slug, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
435
+ const interactionsPath = path.join(dataDir, "customers", slug, "interactions.md");
436
+ const groups = groupInteractionsByContact(parseContactInteractions(fs.existsSync(interactionsPath) ? fs.readFileSync(interactionsPath, "utf-8") : ""), slug);
437
+ const graph = readGraph(dataDir, slug);
438
+ const championIds = new Set(graph.edges.filter((e) => e.type === "IS_CHAMPION").map((e) => e.from));
439
+ const contacts = groups.map((group) => computeContactHealth(group, today, championIds.has(group.contactId)));
440
+ return {
441
+ schemaVersion: "1",
442
+ slug,
443
+ contacts,
444
+ overallHealth: contacts.length === 0 ? 100 : Math.round(contacts.reduce((sum, c) => sum + c.score, 0) / contacts.length),
445
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
446
+ };
447
+ }
448
+ async function updateHealthFromInteraction(dataDir, slug) {
449
+ writeHealth(dataDir, slug, computeCustomerHealth(dataDir, slug));
450
+ }
451
+ //#endregion
452
+ export { updateGraphFromInteraction as a, readGraph as c, writeHealth as i, readHealth as n, findPath as o, updateHealthFromInteraction as r, getStakeholders as s, computeCustomerHealth as t };
453
+
454
+ //# sourceMappingURL=relationship-health-odxEoQdJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationship-health-odxEoQdJ.js","names":[],"sources":["../src/core/graph.ts","../src/core/email-normalizer.ts","../src/core/graph-extractor.ts","../src/core/relationship-health.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { withJsonFile } from \"./file-lock.js\";\n\nexport type NodeType = \"person\" | \"company\" | \"deal\" | \"product\" | \"event\";\n\nexport type EdgeType =\n | \"KNOWS\"\n | \"WORKS_AT\"\n | \"IS_CHAMPION\"\n | \"IS_BLOCKER\"\n | \"IS_ECONOMIC_BUYER\"\n | \"INTRODUCED_BY\"\n | \"OWNS_DEAL\"\n | \"COMPETES_WITH\";\n\nexport interface GraphNode {\n id: string;\n type: NodeType;\n label: string;\n status?: \"active\" | \"inactive\";\n properties: {\n email?: string;\n title?: string;\n company?: string;\n domain?: string;\n [key: string]: unknown;\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface GraphEdge {\n id: string;\n from: string;\n to: string;\n type: EdgeType;\n weight: number;\n sentiment: number;\n lastContact: string;\n contactCount: number;\n properties: Record<string, unknown>;\n // Bi-temporal fields (Graphiti-style), optional for backward compatibility:\n // valid time = when the fact held in the world; transaction time = when the\n // system recorded/retracted it. Edges are invalidated, never deleted.\n validFrom?: string;\n validTo?: string;\n recordedAt?: string;\n invalidatedAt?: string;\n}\n\nexport interface CustomerGraph {\n schemaVersion: \"1\";\n slug: string;\n nodes: GraphNode[];\n edges: GraphEdge[];\n updatedAt: string;\n}\n\nexport type StakeholderRole = \"champion\" | \"blocker\" | \"economic_buyer\" | \"user\";\n\nexport interface MissingRole {\n role: \"champion\" | \"economic_buyer\";\n urgency: \"critical\" | \"important\";\n suggestion: string;\n}\n\nexport interface StakeholderSummary {\n champions: GraphNode[];\n blockers: GraphNode[];\n economicBuyers: GraphNode[];\n allContacts: GraphNode[];\n missingRoles: MissingRole[];\n}\n\n// ─── File path ────────────────────────────────────────────────────────────────\n\nexport function graphPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \"customers\", slug, \"graph.json\");\n}\n\nexport function emptyGraph(slug: string): CustomerGraph {\n return {\n schemaVersion: \"1\",\n slug,\n nodes: [],\n edges: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\n// ─── Read / Write ─────────────────────────────────────────────────────────────\n\nexport function readGraph(dataDir: string, slug: string): CustomerGraph {\n const p = graphPath(dataDir, slug);\n if (!fs.existsSync(p)) return emptyGraph(slug);\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\")) as CustomerGraph;\n } catch {\n process.stderr.write(`[graph] failed to parse ${p} — returning empty graph\\n`);\n return emptyGraph(slug);\n }\n}\n\nexport async function writeGraph(\n dataDir: string,\n slug: string,\n updater: (current: CustomerGraph | null) => CustomerGraph\n): Promise<CustomerGraph> {\n return withJsonFile<CustomerGraph>(graphPath(dataDir, slug), (current) => {\n const g = updater(current);\n return { ...g, updatedAt: new Date().toISOString() };\n });\n}\n\n// ─── Node operations ──────────────────────────────────────────────────────────\n\nexport function upsertNode(\n graph: CustomerGraph,\n node: Omit<GraphNode, \"createdAt\" | \"updatedAt\">\n): CustomerGraph {\n const now = new Date().toISOString();\n const idx = graph.nodes.findIndex((n) => n.id === node.id);\n if (idx !== -1) {\n const existing = graph.nodes[idx]!;\n const updated: GraphNode = {\n ...existing,\n label: node.label || existing.label,\n properties: { ...existing.properties, ...node.properties },\n updatedAt: now,\n };\n const nodes = [...graph.nodes];\n nodes[idx] = updated;\n return { ...graph, nodes };\n }\n const newNode: GraphNode = { ...node, createdAt: now, updatedAt: now };\n return { ...graph, nodes: [...graph.nodes, newNode] };\n}\n\nexport function findNode(graph: CustomerGraph, id: string): GraphNode | undefined {\n return graph.nodes.find((n) => n.id === id);\n}\n\nexport function findNodesByType(graph: CustomerGraph, type: NodeType): GraphNode[] {\n return graph.nodes.filter((n) => n.type === type);\n}\n\n// ─── Edge operations ──────────────────────────────────────────────────────────\n\nexport function makeEdgeId(type: EdgeType, fromId: string, toId: string): string {\n return `${type}:${fromId}__${toId}`;\n}\n\nexport function upsertEdge(\n graph: CustomerGraph,\n edge: Omit<GraphEdge, \"id\"> & { id?: string }\n): CustomerGraph {\n const id = edge.id ?? makeEdgeId(edge.type, edge.from, edge.to);\n const idx = graph.edges.findIndex((e) => e.id === id);\n if (idx !== -1) {\n const existing = graph.edges[idx]!;\n const updated: GraphEdge = {\n ...existing,\n weight: Math.min(1.0, existing.weight + 0.05),\n contactCount: existing.contactCount + 1,\n lastContact: edge.lastContact,\n properties: { ...existing.properties, ...edge.properties },\n };\n const edges = [...graph.edges];\n edges[idx] = updated;\n return { ...graph, edges };\n }\n const now = new Date().toISOString();\n const newEdge: GraphEdge = {\n ...edge,\n id,\n recordedAt: edge.recordedAt ?? now,\n validFrom: edge.validFrom ?? edge.lastContact ?? now,\n };\n return { ...graph, edges: [...graph.edges, newEdge] };\n}\n\n/**\n * Invalidate an edge instead of deleting it: records when the fact stopped\n * being true (validTo) and when the system retracted it (invalidatedAt),\n * preserving full temporal auditability.\n */\nexport function invalidateEdge(graph: CustomerGraph, edgeId: string, at?: string): CustomerGraph {\n const stamp = at ?? new Date().toISOString();\n const edges = graph.edges.map((e) =>\n e.id === edgeId ? { ...e, validTo: stamp, invalidatedAt: new Date().toISOString() } : e\n );\n return { ...graph, edges };\n}\n\n/**\n * Edges considered active at a point in time (default: now): not invalidated,\n * already valid (validFrom <= at), and not yet expired (at < validTo).\n * Edges without temporal fields (legacy) are treated as active.\n */\nexport function activeEdges(graph: CustomerGraph, at?: string): GraphEdge[] {\n const t = at ?? new Date().toISOString();\n return graph.edges.filter((e) => {\n if (e.invalidatedAt) return false;\n if (e.validFrom && e.validFrom > t) return false;\n if (e.validTo && t >= e.validTo) return false;\n return true;\n });\n}\n\nexport function findEdges(graph: CustomerGraph, fromId: string, type?: EdgeType): GraphEdge[] {\n return graph.edges.filter((e) => e.from === fromId && (type === undefined || e.type === type));\n}\n\nexport function findEdgesTo(graph: CustomerGraph, toId: string, type?: EdgeType): GraphEdge[] {\n return graph.edges.filter((e) => e.to === toId && (type === undefined || e.type === type));\n}\n\n// ─── Role assignment ──────────────────────────────────────────────────────────\n\nconst ROLE_EDGE_MAP: Record<Exclude<StakeholderRole, \"user\">, EdgeType> = {\n champion: \"IS_CHAMPION\",\n blocker: \"IS_BLOCKER\",\n economic_buyer: \"IS_ECONOMIC_BUYER\",\n};\n\nexport function setNodeRole(\n graph: CustomerGraph,\n nodeId: string,\n targetId: string,\n role: StakeholderRole\n): CustomerGraph {\n if (role === \"user\") return graph;\n const edgeType = ROLE_EDGE_MAP[role];\n const today = new Date().toISOString().slice(0, 10);\n return upsertEdge(graph, {\n from: nodeId,\n to: targetId,\n type: edgeType,\n weight: 0.8,\n sentiment: 0,\n lastContact: today,\n contactCount: 1,\n properties: {},\n });\n}\n\n// ─── Stakeholder query ────────────────────────────────────────────────────────\n\nexport function getStakeholders(graph: CustomerGraph): StakeholderSummary {\n const dedup = (nodes: GraphNode[]): GraphNode[] => {\n const seen = new Set<string>();\n return nodes.filter((n) => (seen.has(n.id) ? false : (seen.add(n.id), true)));\n };\n\n const resolve = (edges: GraphEdge[]): GraphNode[] =>\n dedup(edges.map((e) => findNode(graph, e.from)).filter((n): n is GraphNode => n !== undefined));\n\n const champions = resolve(graph.edges.filter((e) => e.type === \"IS_CHAMPION\"));\n const blockers = resolve(graph.edges.filter((e) => e.type === \"IS_BLOCKER\"));\n const economicBuyers = resolve(graph.edges.filter((e) => e.type === \"IS_ECONOMIC_BUYER\"));\n const allContacts = findNodesByType(graph, \"person\");\n\n const missingRoles: MissingRole[] = [];\n if (allContacts.length > 0) {\n if (champions.length === 0) {\n missingRoles.push({\n role: \"champion\",\n urgency: \"important\",\n suggestion: \"Identify who is driving this deal internally.\",\n });\n }\n if (economicBuyers.length === 0) {\n missingRoles.push({\n role: \"economic_buyer\",\n urgency: \"critical\",\n suggestion: \"Find out who signs the contract. Ask your champion directly.\",\n });\n }\n }\n\n return { champions, blockers, economicBuyers, allContacts, missingRoles };\n}\n\n// ─── Pruning ──────────────────────────────────────────────────────────────────\n\nexport function pruneStaleNodes(\n graph: CustomerGraph,\n maxAgeDays = 365,\n today?: string\n): CustomerGraph {\n const todayMs = today ? new Date(`${today}T00:00:00Z`).getTime() : Date.now();\n const threshold = maxAgeDays * 86_400_000;\n return {\n ...graph,\n nodes: graph.nodes.map((node) => {\n const age = todayMs - new Date(node.updatedAt).getTime();\n if (age > threshold && node.status !== \"inactive\") {\n return { ...node, status: \"inactive\" as const };\n }\n return node;\n }),\n };\n}\n\n// ─── Path finding ─────────────────────────────────────────────────────────────\n\n/** BFS shortest path between two nodes. Returns [] when no path exists. */\nexport function findPath(graph: CustomerGraph, fromId: string, toId: string): string[] {\n if (fromId === toId) return [fromId];\n\n const visited = new Set<string>([fromId]);\n const queue: Array<{ nodeId: string; path: string[] }> = [{ nodeId: fromId, path: [fromId] }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n const neighbors = graph.edges\n .filter((e) => e.from === current.nodeId || e.to === current.nodeId)\n .map((e) => (e.from === current.nodeId ? e.to : e.from))\n .filter((id) => !visited.has(id));\n\n for (const neighborId of neighbors) {\n const newPath = [...current.path, neighborId];\n if (neighborId === toId) return newPath;\n visited.add(neighborId);\n queue.push({ nodeId: neighborId, path: newPath });\n }\n }\n\n return [];\n}\n","export function normalizeEmail(raw: string): string {\n if (!raw) return \"\";\n const trimmed = raw.trim();\n // Extract email from \"Display Name <email@example.com>\" format\n const angleMatch = trimmed.match(/<([^>]+)>/);\n const address = angleMatch ? angleMatch[1]! : trimmed;\n return address.toLowerCase().trim();\n}\n\nexport function isSameContact(a: string, b: string): boolean {\n return normalizeEmail(a) === normalizeEmail(b);\n}\n\nexport function normalizeContactId(raw: string): string {\n const email = normalizeEmail(raw);\n // Replace @ with _at_ so it can be used as an object key safely\n return email.replace(\"@\", \"_at_\");\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport type { GraphNode, GraphEdge, EdgeType, CustomerGraph } from \"./graph.js\";\nimport { writeGraph, upsertNode, upsertEdge } from \"./graph.js\";\nimport { normalizeEmail } from \"./email-normalizer.js\";\n\nexport interface ExtractionInput {\n slug: string;\n withStr: string;\n interactionDate: string;\n domain?: string;\n companyName?: string;\n}\n\nexport function extractEmail(withStr: string): string | undefined {\n const angleMatch = withStr.match(/<([^>]+@[^>]+)>/);\n if (angleMatch?.[1]) return angleMatch[1].toLowerCase();\n const bareMatch = withStr.match(/^([^\\s]+@[^\\s]+)$/);\n if (bareMatch?.[1]) return bareMatch[1].toLowerCase();\n return undefined;\n}\n\nexport function extractDisplayName(withStr: string): string {\n const match = withStr.match(/^(.+?)\\s*<[^>]+>$/);\n if (match?.[1]) return match[1].trim();\n return withStr.trim();\n}\n\nexport function makePersonId(withStr: string, slug: string): string {\n const email = normalizeEmail(withStr);\n if (email.includes(\"@\")) return `person:${email}`;\n const name = extractDisplayName(withStr);\n const nameSlug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n return `person:${slug}:${nameSlug}`;\n}\n\nexport function makeCompanyId(domain?: string, slug?: string, companyName?: string): string {\n if (domain) return `company:${domain.toLowerCase()}`;\n if (slug) return `company:${slug}`;\n if (companyName) {\n const s = companyName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n return `company:${s}`;\n }\n return `company:unknown`;\n}\n\nexport function extractNodes(input: ExtractionInput): Omit<GraphNode, \"createdAt\" | \"updatedAt\">[] {\n const email = extractEmail(input.withStr);\n const label = extractDisplayName(input.withStr);\n const personId = makePersonId(input.withStr, input.slug);\n\n const personProps: GraphNode[\"properties\"] = {};\n if (email !== undefined) personProps[\"email\"] = email;\n if (input.companyName !== undefined) personProps[\"company\"] = input.companyName;\n if (input.domain !== undefined) personProps[\"domain\"] = input.domain;\n\n const personNode: Omit<GraphNode, \"createdAt\" | \"updatedAt\"> = {\n id: personId,\n type: \"person\",\n label,\n properties: personProps,\n };\n\n const nodes: Omit<GraphNode, \"createdAt\" | \"updatedAt\">[] = [personNode];\n\n if (input.domain !== undefined || input.companyName !== undefined) {\n const companyId = makeCompanyId(input.domain, input.slug, input.companyName);\n const companyProps: GraphNode[\"properties\"] = {};\n if (input.domain !== undefined) companyProps[\"domain\"] = input.domain;\n\n const companyNode: Omit<GraphNode, \"createdAt\" | \"updatedAt\"> = {\n id: companyId,\n type: \"company\",\n label: input.companyName ?? input.domain ?? input.slug,\n properties: companyProps,\n };\n nodes.push(companyNode);\n }\n\n return nodes;\n}\n\nexport function extractEdges(\n personId: string,\n companyId: string | undefined,\n interactionDate: string\n): GraphEdge[] {\n if (!companyId) return [];\n return [\n {\n id: `WORKS_AT:${personId}__${companyId}`,\n from: personId,\n to: companyId,\n type: \"WORKS_AT\" as EdgeType,\n weight: 0.5,\n sentiment: 0,\n lastContact: interactionDate,\n contactCount: 1,\n properties: {},\n },\n ];\n}\n\nexport async function updateGraphFromInteraction(\n dataDir: string,\n slug: string,\n input: { withStr: string; interactionDate: string }\n): Promise<void> {\n if (!input.withStr.trim()) return;\n\n let domain: string | undefined;\n let companyName: string | undefined;\n const factsPath = path.join(dataDir, \"customers\", slug, \"main_facts.md\");\n if (fs.existsSync(factsPath)) {\n try {\n const parsed = matter(fs.readFileSync(factsPath, \"utf-8\"));\n domain = parsed.data[\"domain\"] as string | undefined;\n companyName = parsed.data[\"name\"] as string | undefined;\n } catch {\n // non-critical\n }\n }\n\n const extractionInput: ExtractionInput = {\n slug,\n withStr: input.withStr,\n interactionDate: input.interactionDate,\n };\n if (domain !== undefined) extractionInput.domain = domain;\n if (companyName !== undefined) extractionInput.companyName = companyName;\n const nodes = extractNodes(extractionInput);\n\n const personId = makePersonId(input.withStr, slug);\n const companyId =\n domain !== undefined || companyName !== undefined\n ? makeCompanyId(domain, slug, companyName)\n : undefined;\n const edges = extractEdges(personId, companyId, input.interactionDate);\n\n await writeGraph(dataDir, slug, (current) => {\n const empty: CustomerGraph = {\n schemaVersion: \"1\",\n slug,\n nodes: [],\n edges: [],\n updatedAt: new Date().toISOString(),\n };\n let graph = current ?? empty;\n for (const node of nodes) graph = upsertNode(graph, node);\n for (const edge of edges) graph = upsertEdge(graph, edge);\n return graph;\n });\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { readGraph } from \"./graph.js\";\nimport { extractEmail, extractDisplayName, makePersonId } from \"./graph-extractor.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type HealthGrade = \"A\" | \"B\" | \"C\" | \"D\" | \"F\";\n\nexport type HealthTrend = \"rising\" | \"stable\" | \"declining\" | \"cold\";\n\nexport type RiskFlag =\n | \"NO_CONTACT_14D\"\n | \"NO_CONTACT_30D\"\n | \"SENTIMENT_DECLINING\"\n | \"CHAMPION_SILENT\"\n | \"DEAL_STALLED\"\n | \"CLOSE_DATE_PASSED\"\n | \"CONTACT_LEFT_COMPANY\"\n | \"RESPONSE_LATENCY_INCREASING\";\n\nexport interface ContactHealth {\n contactId: string;\n name: string;\n email?: string;\n score: number;\n grade: HealthGrade;\n trend: HealthTrend;\n daysSinceContact: number;\n avgCadenceDays: number;\n sentimentTrend: number;\n riskFlags: RiskFlag[];\n lastContact: string;\n interactionCount30d: number;\n recommendation: string;\n updatedAt: string;\n}\n\nexport interface HealthSnapshot {\n schemaVersion: \"1\";\n slug: string;\n contacts: ContactHealth[];\n overallHealth: number;\n updatedAt: string;\n}\n\n// ─── Parsed interaction (from interactions.md) ────────────────────────────────\n\nexport interface ParsedInteraction {\n date: string;\n type: string;\n withStr: string;\n}\n\nexport interface ContactInteractionGroup {\n contactId: string;\n name: string;\n email?: string;\n interactions: ParsedInteraction[];\n}\n\n// ─── File path ────────────────────────────────────────────────────────────────\n\nexport function healthPath(dataDir: string, slug: string): string {\n return path.join(dataDir, \"customers\", slug, \"health.json\");\n}\n\n// ─── Read / Write ─────────────────────────────────────────────────────────────\n\nexport function readHealth(dataDir: string, slug: string): HealthSnapshot | null {\n const p = healthPath(dataDir, slug);\n if (!fs.existsSync(p)) return null;\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\")) as HealthSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function writeHealth(dataDir: string, slug: string, health: HealthSnapshot): void {\n const p = healthPath(dataDir, slug);\n const dir = path.dirname(p);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const updated: HealthSnapshot = { ...health, updatedAt: new Date().toISOString() };\n fs.writeFileSync(p, JSON.stringify(updated, null, 2), \"utf-8\");\n}\n\n// ─── Parsing ──────────────────────────────────────────────────────────────────\n\nexport function parseContactInteractions(content: string): ParsedInteraction[] {\n const blocks = content.split(/(?=^## \\d{4}-\\d{2}-\\d{2})/m).filter((b) => b.trim().length > 0);\n\n const result: ParsedInteraction[] = [];\n for (const block of blocks) {\n const headingMatch = block.match(/^## (\\d{4}-\\d{2}-\\d{2}) · (\\w+)/m);\n if (!headingMatch) continue;\n const date = headingMatch[1]!;\n const type = headingMatch[2]!;\n\n const withMatch = block.match(/^\\*\\*(?:With|Subject):\\*\\*\\s*(.+)$/m);\n if (!withMatch) continue;\n const withStr = withMatch[1]!.trim();\n\n result.push({ date, type, withStr });\n }\n return result;\n}\n\n// ─── Score functions (pure) ───────────────────────────────────────────────────\n\nexport function calcRecencyScore(daysSince: number): number {\n if (daysSince <= 0) return 100;\n if (daysSince >= 30) return 0;\n return Math.round(100 * (1 - daysSince / 30));\n}\n\nexport function calcCadenceScore(daysSince: number, avgCadenceDays: number): number {\n if (avgCadenceDays <= 0) return 50;\n const ratio = daysSince / avgCadenceDays;\n if (ratio <= 1.0) return 100;\n if (ratio >= 3.0) return 0;\n return Math.round(100 * (1 - (ratio - 1.0) / 2.0));\n}\n\nexport function calcMomentumScore(last30d: number, prev30d: number): number {\n if (last30d === 0 && prev30d === 0) return 50;\n if (prev30d === 0) return 80;\n const ratio = last30d / prev30d;\n if (ratio >= 1.5) return 100;\n if (ratio >= 1.0) return 75;\n if (ratio >= 0.5) return 50;\n if (ratio >= 0.25) return 25;\n return 0;\n}\n\nexport function gradeFromScore(score: number): HealthGrade {\n if (score >= 80) return \"A\";\n if (score >= 60) return \"B\";\n if (score >= 40) return \"C\";\n if (score >= 20) return \"D\";\n return \"F\";\n}\n\nexport function trendFromState(\n score: number,\n daysSince: number,\n avgCadenceDays: number,\n momentumScore: number\n): HealthTrend {\n if (score < 20 || daysSince >= 30) return \"cold\";\n if (momentumScore > 70 && score > 60) return \"rising\";\n if (momentumScore < 30 || (daysSince > avgCadenceDays * 1.5 && score < 60)) return \"declining\";\n return \"stable\";\n}\n\nexport function calcRiskFlags(\n _contactId: string,\n daysSince: number,\n score: number,\n isChampion: boolean\n): RiskFlag[] {\n const flags: RiskFlag[] = [];\n if (daysSince >= 30) flags.push(\"NO_CONTACT_30D\");\n if (daysSince >= 14) flags.push(\"NO_CONTACT_14D\");\n if (isChampion && score < 50) flags.push(\"CHAMPION_SILENT\");\n return flags;\n}\n\nexport function generateRecommendation(\n name: string,\n grade: HealthGrade,\n trend: HealthTrend,\n riskFlags: RiskFlag[],\n daysSince: number,\n avgCadenceDays: number\n): string {\n if (riskFlags.includes(\"NO_CONTACT_30D\")) {\n return `Re-engage ${name} urgently — no contact in ${daysSince} days.`;\n }\n if (riskFlags.includes(\"CHAMPION_SILENT\")) {\n return `Champion ${name} has gone quiet — critical to re-engage before deal stalls.`;\n }\n if (riskFlags.includes(\"NO_CONTACT_14D\")) {\n return `Schedule contact with ${name} — ${daysSince} days since last touchpoint.`;\n }\n if (trend === \"declining\") {\n return `${name} relationship declining — increase touchpoint frequency.`;\n }\n if (grade === \"A\") {\n return `${name} — strong relationship. Keep current cadence.`;\n }\n const daysUntilDue = Math.max(0, avgCadenceDays - daysSince);\n return `${name} — grade ${grade}. Next contact due in ~${daysUntilDue} day${daysUntilDue === 1 ? \"\" : \"s\"}.`;\n}\n\n// ─── Average cadence ──────────────────────────────────────────────────────────\n\nfunction dateUtcMs(d: string): number {\n return new Date(`${d}T00:00:00Z`).getTime();\n}\n\nexport function calcAvgCadence(interactions: ParsedInteraction[]): number {\n if (interactions.length < 2) return 0;\n const sorted = [...interactions].sort((a, b) => b.date.localeCompare(a.date));\n let totalDays = 0;\n for (let i = 0; i < sorted.length - 1; i++) {\n const gap = Math.round(\n (dateUtcMs(sorted[i]!.date) - dateUtcMs(sorted[i + 1]!.date)) / 86_400_000\n );\n totalDays += gap;\n }\n return Math.round(totalDays / (sorted.length - 1));\n}\n\n// ─── Group interactions by contact ───────────────────────────────────────────\n\nexport function groupInteractionsByContact(\n interactions: ParsedInteraction[],\n slug: string\n): ContactInteractionGroup[] {\n const map = new Map<\n string,\n { contactId: string; name: string; email?: string; interactions: ParsedInteraction[] }\n >();\n\n for (const ix of interactions) {\n const email = extractEmail(ix.withStr);\n const name = extractDisplayName(ix.withStr);\n const contactId = makePersonId(ix.withStr, slug);\n\n if (!map.has(contactId)) {\n const entry: {\n contactId: string;\n name: string;\n email?: string;\n interactions: ParsedInteraction[];\n } = {\n contactId,\n name,\n interactions: [],\n };\n if (email !== undefined) entry.email = email;\n map.set(contactId, entry);\n }\n map.get(contactId)!.interactions.push(ix);\n }\n\n return Array.from(map.values());\n}\n\n// ─── Per-contact health ───────────────────────────────────────────────────────\n\nexport function computeContactHealth(\n group: ContactInteractionGroup,\n today: string,\n isChampion: boolean\n): ContactHealth {\n const sorted = [...group.interactions].sort((a, b) => b.date.localeCompare(a.date));\n const lastContact = sorted[0]?.date ?? \"\";\n\n const daysSince = lastContact\n ? Math.round((dateUtcMs(today) - dateUtcMs(lastContact)) / 86_400_000)\n : 999;\n\n const avgCadenceDays = calcAvgCadence(group.interactions);\n\n const todayMs = dateUtcMs(today);\n const d30 = todayMs - 30 * 86_400_000;\n const d60 = todayMs - 60 * 86_400_000;\n const last30d = group.interactions.filter((i) => dateUtcMs(i.date) >= d30).length;\n const prev30d = group.interactions.filter(\n (i) => dateUtcMs(i.date) >= d60 && dateUtcMs(i.date) < d30\n ).length;\n\n const recency = calcRecencyScore(daysSince);\n const cadence = calcCadenceScore(daysSince, avgCadenceDays);\n const sentiment = 50;\n const response = 50;\n const momentum = calcMomentumScore(last30d, prev30d);\n\n const score = Math.round(\n recency * 0.35 + cadence * 0.25 + sentiment * 0.2 + response * 0.1 + momentum * 0.1\n );\n\n const grade = gradeFromScore(score);\n const trend = trendFromState(score, daysSince, avgCadenceDays, momentum);\n const riskFlags = calcRiskFlags(group.contactId, daysSince, score, isChampion);\n const recommendation = generateRecommendation(\n group.name,\n grade,\n trend,\n riskFlags,\n daysSince,\n avgCadenceDays\n );\n\n const health: ContactHealth = {\n contactId: group.contactId,\n name: group.name,\n score,\n grade,\n trend,\n daysSinceContact: daysSince,\n avgCadenceDays,\n sentimentTrend: 0,\n riskFlags,\n lastContact,\n interactionCount30d: last30d,\n recommendation,\n updatedAt: new Date().toISOString(),\n };\n if (group.email !== undefined) health.email = group.email;\n return health;\n}\n\n// ─── Full customer health ─────────────────────────────────────────────────────\n\nexport function computeCustomerHealth(\n dataDir: string,\n slug: string,\n today: string = new Date().toISOString().slice(0, 10)\n): HealthSnapshot {\n const interactionsPath = path.join(dataDir, \"customers\", slug, \"interactions.md\");\n const content = fs.existsSync(interactionsPath)\n ? (fs.readFileSync(interactionsPath, \"utf-8\") as string)\n : \"\";\n\n const parsed = parseContactInteractions(content);\n const groups = groupInteractionsByContact(parsed, slug);\n\n const graph = readGraph(dataDir, slug);\n const championIds = new Set(\n graph.edges.filter((e) => e.type === \"IS_CHAMPION\").map((e) => e.from)\n );\n\n const contacts = groups.map((group) =>\n computeContactHealth(group, today, championIds.has(group.contactId))\n );\n\n const overallHealth =\n contacts.length === 0\n ? 100\n : Math.round(contacts.reduce((sum, c) => sum + c.score, 0) / contacts.length);\n\n return {\n schemaVersion: \"1\",\n slug,\n contacts,\n overallHealth,\n updatedAt: new Date().toISOString(),\n };\n}\n\n// ─── Fire-and-forget update ───────────────────────────────────────────────────\n\nexport async function updateHealthFromInteraction(dataDir: string, slug: string): Promise<void> {\n const health = computeCustomerHealth(dataDir, slug);\n writeHealth(dataDir, slug, health);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { i as buildSimulationInput } from "./revenue-simulation-Bqf2DLVB.js";
2
+ export { buildSimulationInput };