@datasynx/agentic-crm 0.1.0 → 1.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 (309) hide show
  1. package/README.md +270 -669
  2. package/dist/{approvals-DpjxGHFp.js → approvals-CmDT2eUg.js} +7 -24
  3. package/dist/approvals-CmDT2eUg.js.map +1 -0
  4. package/dist/{ask-CID3jnuL.js → ask-CDysGnRg.js} +6 -6
  5. package/dist/{ask-CID3jnuL.js.map → ask-CDysGnRg.js.map} +1 -1
  6. package/dist/atomic-write-8yjqqLtS.js +29 -0
  7. package/dist/atomic-write-8yjqqLtS.js.map +1 -0
  8. package/dist/atomic-write-BYmF-ThH.cjs +37 -0
  9. package/dist/atomic-write-BYmF-ThH.cjs.map +1 -0
  10. package/dist/attachments-CX2GAtsw.cjs +517 -0
  11. package/dist/attachments-CX2GAtsw.cjs.map +1 -0
  12. package/dist/attachments-D207gXfN.js +514 -0
  13. package/dist/attachments-D207gXfN.js.map +1 -0
  14. package/dist/attachments-rLa96rOK.js +514 -0
  15. package/dist/attachments-rLa96rOK.js.map +1 -0
  16. package/dist/auth-B5DcjJ_6.js +2 -0
  17. package/dist/{auth-DFWwWcYD.js → auth-DDXZTwS0.js} +4 -13
  18. package/dist/auth-DDXZTwS0.js.map +1 -0
  19. package/dist/{autofill-Di_-SP7t.js → autofill-B9VtlR2j.js} +2 -2
  20. package/dist/{autofill-Di_-SP7t.js.map → autofill-B9VtlR2j.js.map} +1 -1
  21. package/dist/{backup-CeMk9z86.js → backup-CTlIxUdO.js} +5 -7
  22. package/dist/backup-CTlIxUdO.js.map +1 -0
  23. package/dist/backup-LFnC09oV.js +2 -0
  24. package/dist/chunk-BfDYWZQ8.cjs +32 -0
  25. package/dist/chunk-BfDYWZQ8.cjs.map +1 -0
  26. package/dist/chunk-BhUZmQg5.js +32 -0
  27. package/dist/chunk-BhUZmQg5.js.map +1 -0
  28. package/dist/chunk-ChC83jai.js +2 -0
  29. package/dist/chunk-e_w8qqtP.js +32 -0
  30. package/dist/chunk-e_w8qqtP.js.map +1 -0
  31. package/dist/{churn-C28IgnAj.js → churn-DN9WDGNM.js} +3 -3
  32. package/dist/{churn-C28IgnAj.js.map → churn-DN9WDGNM.js.map} +1 -1
  33. package/dist/cli.js +285 -186
  34. package/dist/cli.js.map +1 -1
  35. package/dist/{compliance-CKSBoQUe.js → compliance-Bc12Hn9a.js} +3 -3
  36. package/dist/{compliance-CKSBoQUe.js.map → compliance-Bc12Hn9a.js.map} +1 -1
  37. package/dist/{compliance-CujOqAKk.js → compliance-TqYQXhBj.js} +1 -1
  38. package/dist/{compliance-B1kk5-YS.js → compliance-kq0xHRw3.js} +3 -3
  39. package/dist/{compliance-B1kk5-YS.js.map → compliance-kq0xHRw3.js.map} +1 -1
  40. package/dist/{compliance-B91zNvCR.cjs → compliance-pAj9FcGI.cjs} +3 -3
  41. package/dist/{compliance-B91zNvCR.cjs.map → compliance-pAj9FcGI.cjs.map} +1 -1
  42. package/dist/{context-builder-BzWAp3Zs.js → context-builder-7Uab5-G4.js} +3 -2
  43. package/dist/context-builder-7Uab5-G4.js.map +1 -0
  44. package/dist/context-builder-hmOPvgso.js +2 -0
  45. package/dist/{custom-fields-CzNeD3_v.js → custom-fields-BMyz5Ruh.js} +1 -1
  46. package/dist/{custom-fields-Pl2t9xzp.js → custom-fields-GzpOHW_2.js} +4 -13
  47. package/dist/custom-fields-GzpOHW_2.js.map +1 -0
  48. package/dist/{custom-objects-CIFrmQ2V.js → custom-objects-BNy-ayR-.js} +1 -1
  49. package/dist/{custom-objects-BHgn1GEX.js → custom-objects-CxW1gHwJ.js} +10 -25
  50. package/dist/custom-objects-CxW1gHwJ.js.map +1 -0
  51. package/dist/{customer-dir-DIylZ8Q6.js → customer-dir-CkMMXhb0.js} +9 -4
  52. package/dist/customer-dir-CkMMXhb0.js.map +1 -0
  53. package/dist/daemon/worker.js +66 -40
  54. package/dist/daemon/worker.js.map +1 -1
  55. package/dist/doctor-C14-vnJ1.js +103 -0
  56. package/dist/doctor-C14-vnJ1.js.map +1 -0
  57. package/dist/email-body-BFSRa0AW.cjs +42 -0
  58. package/dist/email-body-BFSRa0AW.cjs.map +1 -0
  59. package/dist/email-body-BOd7U-D2.js +42 -0
  60. package/dist/email-body-BOd7U-D2.js.map +1 -0
  61. package/dist/{enrichment-3XvgGDfB.js → enrichment-CDFdWmvD.js} +3 -3
  62. package/dist/{enrichment-3XvgGDfB.js.map → enrichment-CDFdWmvD.js.map} +1 -1
  63. package/dist/{file-lock-B_zi7NQl.js → file-lock-CcHotQkZ.js} +3 -4
  64. package/dist/file-lock-CcHotQkZ.js.map +1 -0
  65. package/dist/{gmail-sync-DIaxInDT.js → gmail-sync-B4Iu3AQb.js} +56 -22
  66. package/dist/gmail-sync-B4Iu3AQb.js.map +1 -0
  67. package/dist/{gmail-sync-hHm9gaWd.cjs → gmail-sync-BpSVESSe.cjs} +55 -21
  68. package/dist/gmail-sync-BpSVESSe.cjs.map +1 -0
  69. package/dist/{gmail-sync-rQaVqKWd.js → gmail-sync-DIbrPnTK.js} +55 -21
  70. package/dist/gmail-sync-DIbrPnTK.js.map +1 -0
  71. package/dist/{gmail-webhook-handler-e5Od25FX.js → gmail-webhook-handler-BzOFbvgh.js} +4 -4
  72. package/dist/{gmail-webhook-handler-e5Od25FX.js.map → gmail-webhook-handler-BzOFbvgh.js.map} +1 -1
  73. package/dist/{gmail-webhook-handler-DS7OlRPX.js → gmail-webhook-handler-CvSDW_Js.js} +2 -2
  74. package/dist/{goal-engine-KpBftn4V.js → goal-engine-BbroPhqm.js} +10 -11
  75. package/dist/goal-engine-BbroPhqm.js.map +1 -0
  76. package/dist/{goal-engine-CUZSpERI.js → goal-engine-CfDAJTFt.js} +1 -1
  77. package/dist/{google-drive-sync-DEPcqFca.js → google-drive-sync-B_I1d54Y.js} +3 -3
  78. package/dist/{google-drive-sync-DEPcqFca.js.map → google-drive-sync-B_I1d54Y.js.map} +1 -1
  79. package/dist/html-BaeOCZKE.js +36 -0
  80. package/dist/html-BaeOCZKE.js.map +1 -0
  81. package/dist/html-CmOku6jS.cjs +47 -0
  82. package/dist/html-CmOku6jS.cjs.map +1 -0
  83. package/dist/{hygiene-DZqfYpFf.js → hygiene-DzQPnc6P.js} +3 -3
  84. package/dist/{hygiene-DZqfYpFf.js.map → hygiene-DzQPnc6P.js.map} +1 -1
  85. package/dist/identity-CB7j-Zr1.js +2 -0
  86. package/dist/{identity-CI6olMNm.js → identity-_uZ3Lbr2.js} +2 -2
  87. package/dist/{identity-CI6olMNm.js.map → identity-_uZ3Lbr2.js.map} +1 -1
  88. package/dist/{import-hubspot-BaK71U_K.js → import-hubspot-CTId9IGV.js} +51 -45
  89. package/dist/import-hubspot-CTId9IGV.js.map +1 -0
  90. package/dist/{index-YqwMd6aQ.d.cts → index-BAutNcAT.d.cts} +20 -12
  91. package/dist/index-BAutNcAT.d.cts.map +1 -0
  92. package/dist/{index-V8BFaH-b.d.ts → index-FzDsNSSb.d.ts} +12 -4
  93. package/dist/index-FzDsNSSb.d.ts.map +1 -0
  94. package/dist/index.cjs +19 -21
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.d.cts +20 -12
  97. package/dist/index.d.cts.map +1 -1
  98. package/dist/index.d.ts +12 -4
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +19 -21
  101. package/dist/index.js.map +1 -1
  102. package/dist/interactions-writer-B2y-73lh.js +2 -0
  103. package/dist/{interactions-writer-SLHnoEeE.js → interactions-writer-B8XAzdqR.js} +34 -4
  104. package/dist/interactions-writer-B8XAzdqR.js.map +1 -0
  105. package/dist/{interactions-writer-CrPStUll.cjs → interactions-writer-BRJNrefF.cjs} +7 -3
  106. package/dist/interactions-writer-BRJNrefF.cjs.map +1 -0
  107. package/dist/{interactions-writer-DO3KcSR3.js → interactions-writer-ZQcpFOh9.js} +7 -3
  108. package/dist/interactions-writer-ZQcpFOh9.js.map +1 -0
  109. package/dist/json-store-WWsFzXub.js +43 -0
  110. package/dist/json-store-WWsFzXub.js.map +1 -0
  111. package/dist/{knowledge-base-D0Fh40kc.js → knowledge-base--063Kpa3.js} +51 -22
  112. package/dist/knowledge-base--063Kpa3.js.map +1 -0
  113. package/dist/{lancedb-CCBbpulq.js → lancedb-CswQEE5K.js} +1 -1
  114. package/dist/{lancedb-rlvWoPwl.js → lancedb-CuHKNsNZ.js} +4 -3
  115. package/dist/lancedb-CuHKNsNZ.js.map +1 -0
  116. package/dist/{lead-model-BCFzyktm.js → lead-model-CEmx7te7.js} +6 -14
  117. package/dist/lead-model-CEmx7te7.js.map +1 -0
  118. package/dist/{llm-Z8RIYkpF.js → llm-BnSUBisu.js} +2 -2
  119. package/dist/{llm-Z8RIYkpF.js.map → llm-BnSUBisu.js.map} +1 -1
  120. package/dist/{llm-iijeXmgq.cjs → llm-CXycmEl9.cjs} +2 -2
  121. package/dist/{llm-iijeXmgq.cjs.map → llm-CXycmEl9.cjs.map} +1 -1
  122. package/dist/{llm-DEjWcqmW.js → llm-DSX1-wFu.js} +1 -1
  123. package/dist/{llm-DvzZqva0.js → llm-PZzgPphl.js} +3 -3
  124. package/dist/{llm-DvzZqva0.js.map → llm-PZzgPphl.js.map} +1 -1
  125. package/dist/logger-BkInaGoV.cjs +167 -0
  126. package/dist/logger-BkInaGoV.cjs.map +1 -0
  127. package/dist/logger-Dyl4VcLO.js +147 -0
  128. package/dist/logger-Dyl4VcLO.js.map +1 -0
  129. package/dist/logger-UaF5p9d1.js +147 -0
  130. package/dist/logger-UaF5p9d1.js.map +1 -0
  131. package/dist/logger-vKQS34w9.js +2 -0
  132. package/dist/mcp-CdTJWTJf.d.cts.map +1 -1
  133. package/dist/mcp-CdTJWTJf.d.ts.map +1 -1
  134. package/dist/mcp.cjs +365 -319
  135. package/dist/mcp.cjs.map +1 -1
  136. package/dist/mcp.d.cts.map +1 -1
  137. package/dist/mcp.d.ts.map +1 -1
  138. package/dist/mcp.js +365 -319
  139. package/dist/mcp.js.map +1 -1
  140. package/dist/{memory-Cy6-Tbyl.js → memory-D8hmgD9d.js} +1 -1
  141. package/dist/{memory-Bb6ky3kb.js → memory-Dzr9dXSM.js} +4 -11
  142. package/dist/memory-Dzr9dXSM.js.map +1 -0
  143. package/dist/{microsoft-calendar-B6MMtUQK.js → microsoft-calendar-BgVR8GDv.js} +4 -4
  144. package/dist/{microsoft-calendar-B6MMtUQK.js.map → microsoft-calendar-BgVR8GDv.js.map} +1 -1
  145. package/dist/{microsoft-sync-CpZVoSuq.js → microsoft-sync-D30_XksI.js} +5 -5
  146. package/dist/{microsoft-sync-CpZVoSuq.js.map → microsoft-sync-D30_XksI.js.map} +1 -1
  147. package/dist/{nba-3wanmJ0U.js → nba-DwdfM93s.js} +3 -3
  148. package/dist/{nba-3wanmJ0U.js.map → nba-DwdfM93s.js.map} +1 -1
  149. package/dist/{notification-dispatcher-0vYNngWe.js → notification-dispatcher-inpKyuBz.js} +7 -3
  150. package/dist/notification-dispatcher-inpKyuBz.js.map +1 -0
  151. package/dist/{pipeline-writer-BqBrYrQc.js → pipeline-writer-0LJ6Qkat.js} +1 -1
  152. package/dist/{pipeline-writer-N2omexxp.cjs → pipeline-writer-B1tRAhuD.cjs} +11 -3
  153. package/dist/pipeline-writer-B1tRAhuD.cjs.map +1 -0
  154. package/dist/{pipeline-writer-BvVquKIe.js → pipeline-writer-CIllfnZl.js} +5 -3
  155. package/dist/pipeline-writer-CIllfnZl.js.map +1 -0
  156. package/dist/{pipeline-writer-eufx_0o1.js → pipeline-writer-rDj-ni6q.js} +6 -4
  157. package/dist/pipeline-writer-rDj-ni6q.js.map +1 -0
  158. package/dist/{proactive-agent-BgQXw3ac.js → proactive-agent-B7u3Bj_l.js} +6 -6
  159. package/dist/{proactive-agent-BgQXw3ac.js.map → proactive-agent-B7u3Bj_l.js.map} +1 -1
  160. package/dist/{proactive-worker-BrLHNhjH.js → proactive-worker-1zkm6aJD.js} +7 -8
  161. package/dist/proactive-worker-1zkm6aJD.js.map +1 -0
  162. package/dist/{push-manager-CowY-0IK.js → push-manager-BXM-IHfP.js} +1 -1
  163. package/dist/{push-manager-CdqIIkuh.js → push-manager-C0ECQgva.js} +4 -4
  164. package/dist/push-manager-C0ECQgva.js.map +1 -0
  165. package/dist/{quote-generator-OhSFsi3x.js → quote-generator-ByUyIYtw.js} +1 -1
  166. package/dist/{quote-generator-BfwENXzg.js → quote-generator-CTdR8eEI.js} +5 -5
  167. package/dist/quote-generator-CTdR8eEI.js.map +1 -0
  168. package/dist/rbac-DzbyFhVH.js +2 -0
  169. package/dist/{rbac-CTIktZaC.js → rbac-msmBc_tK.js} +19 -12
  170. package/dist/rbac-msmBc_tK.js.map +1 -0
  171. package/dist/regex-Jt5DatPi.js +13 -0
  172. package/dist/regex-Jt5DatPi.js.map +1 -0
  173. package/dist/{relationship-health-odxEoQdJ.js → relationship-health-ZZNXR1RZ.js} +8 -16
  174. package/dist/relationship-health-ZZNXR1RZ.js.map +1 -0
  175. package/dist/{revenue-simulation-Bqf2DLVB.js → revenue-simulation-D8f_YkUY.js} +9 -19
  176. package/dist/revenue-simulation-D8f_YkUY.js.map +1 -0
  177. package/dist/{revenue-simulation-BJdRTEHc.js → revenue-simulation-njJZlTqm.js} +1 -1
  178. package/dist/safe-path-mpp0dKtO.js +18 -0
  179. package/dist/safe-path-mpp0dKtO.js.map +1 -0
  180. package/dist/{segments-BqcD5HIl.js → segments-DI3LOQNe.js} +5 -14
  181. package/dist/segments-DI3LOQNe.js.map +1 -0
  182. package/dist/sequence-engine-C6nnewHX.js +2 -0
  183. package/dist/{sequence-engine-J1lTW_in.js → sequence-engine-DNTVLq7o.js} +15 -8
  184. package/dist/sequence-engine-DNTVLq7o.js.map +1 -0
  185. package/dist/{sequence-store-DaaWr0Os.js → sequence-store-CmYb6s0g.js} +6 -5
  186. package/dist/sequence-store-CmYb6s0g.js.map +1 -0
  187. package/dist/{server-Dyva03K8.js → server-DoRPPOeR.js} +308 -230
  188. package/dist/server-DoRPPOeR.js.map +1 -0
  189. package/dist/{session-D9ub6Wl1.js → session-B6XaP83h.js} +3 -3
  190. package/dist/session-B6XaP83h.js.map +1 -0
  191. package/dist/{session-B9AilxOE.js → session-BgGDyP2C.js} +3 -3
  192. package/dist/session-BgGDyP2C.js.map +1 -0
  193. package/dist/session-Bp4zTh4l.js +2 -0
  194. package/dist/{session-D0qFkBla.cjs → session-Mm7GQbSH.cjs} +3 -3
  195. package/dist/session-Mm7GQbSH.cjs.map +1 -0
  196. package/dist/{session-store-C8tEvMPw.js → session-store-DWxJ5Pof.js} +79 -17
  197. package/dist/session-store-DWxJ5Pof.js.map +1 -0
  198. package/dist/{session-store-B0QZE8Bx.cjs → session-store-yfwnj0OC.cjs} +126 -16
  199. package/dist/session-store-yfwnj0OC.cjs.map +1 -0
  200. package/dist/{sla-engine-5IhTsBUR.js → sla-engine-CP2KiKDS.js} +1 -1
  201. package/dist/{sla-engine-BqX-7u-7.js → sla-engine-O-A1ntu_.js} +2 -2
  202. package/dist/{sla-engine-BqX-7u-7.js.map → sla-engine-O-A1ntu_.js.map} +1 -1
  203. package/dist/{sop-Vp0UPWFW.js → sop-BV7ICAFR.js} +4 -11
  204. package/dist/sop-BV7ICAFR.js.map +1 -0
  205. package/dist/{sop-DkhVChGy.js → sop-D33qTHUb.js} +1 -1
  206. package/dist/survey-engine-DKctGcLQ.js +2 -0
  207. package/dist/{survey-engine-DBjCYqCv.js → survey-engine-DngXBv47.js} +5 -4
  208. package/dist/survey-engine-DngXBv47.js.map +1 -0
  209. package/dist/{sync-state-CwLSt_1m.js → sync-state-BaA8LbTI.js} +1 -1
  210. package/dist/{sync-state-ChaLbamC.js → sync-state-DMZgzpez.js} +4 -12
  211. package/dist/sync-state-DMZgzpez.js.map +1 -0
  212. package/dist/{ticket-writer-CjqKeIRD.js → ticket-writer-DsfpeLGZ.js} +1 -1
  213. package/dist/{ticket-writer-j2oX_Wal.js → ticket-writer-a9on36Wb.js} +12 -24
  214. package/dist/ticket-writer-a9on36Wb.js.map +1 -0
  215. package/dist/{tone-Bdm5uaht.js → tone-C7bqK69y.js} +5 -12
  216. package/dist/tone-C7bqK69y.js.map +1 -0
  217. package/dist/{tone-DRKlZgPr.cjs → tone-Cmc7O2Fx.cjs} +3 -9
  218. package/dist/tone-Cmc7O2Fx.cjs.map +1 -0
  219. package/dist/{tone-vNb2DAAD.js → tone-mXSftvTn.js} +3 -8
  220. package/dist/tone-mXSftvTn.js.map +1 -0
  221. package/dist/{transcript-watcher-CL2QUygI.js → transcript-watcher-BoClrJAz.js} +18 -11
  222. package/dist/transcript-watcher-BoClrJAz.js.map +1 -0
  223. package/dist/unmatched-transcripts-C92zAoM4.js +2 -0
  224. package/dist/unmatched-transcripts-DC-VQ9YS.js +16 -0
  225. package/dist/unmatched-transcripts-DC-VQ9YS.js.map +1 -0
  226. package/dist/update-deal-CWy1eLJI.js +2 -0
  227. package/dist/{update-deal-DKC79skb.js → update-deal-DSzr_Aau.js} +3 -3
  228. package/dist/{update-deal-DKC79skb.js.map → update-deal-DSzr_Aau.js.map} +1 -1
  229. package/dist/{usage-D0-TYJkw.js → usage-BVlFlKW_.js} +8 -6
  230. package/dist/usage-BVlFlKW_.js.map +1 -0
  231. package/dist/usage-CClTf5e6.cjs.map +1 -1
  232. package/dist/usage-D0u9a-lV.js.map +1 -1
  233. package/dist/{vault-DXCg29W-.js → vault-CfwZdNzC.js} +3 -4
  234. package/dist/vault-CfwZdNzC.js.map +1 -0
  235. package/dist/{vault-C1D3zScD.js → vault-DxKP4_R2.js} +1 -1
  236. package/dist/{webhooks-Xn6zO6kd.cjs → webhooks-CwW-3kvG.cjs} +5 -19
  237. package/dist/webhooks-CwW-3kvG.cjs.map +1 -0
  238. package/dist/{webhooks-7EpA05Qr.js → webhooks-DXr1IoKn.js} +8 -21
  239. package/dist/webhooks-DXr1IoKn.js.map +1 -0
  240. package/dist/{webhooks-BO2UAnmn.js → webhooks-sWZ8CJtR.js} +5 -18
  241. package/dist/webhooks-sWZ8CJtR.js.map +1 -0
  242. package/package.json +22 -2
  243. package/dist/approvals-DpjxGHFp.js.map +0 -1
  244. package/dist/auth-CyFuu9X_.js +0 -2
  245. package/dist/auth-DFWwWcYD.js.map +0 -1
  246. package/dist/backup-CeMk9z86.js.map +0 -1
  247. package/dist/backup-f_hC7rBV.js +0 -2
  248. package/dist/context-builder-BzWAp3Zs.js.map +0 -1
  249. package/dist/context-builder-DlrRcqmJ.js +0 -2
  250. package/dist/custom-fields-Pl2t9xzp.js.map +0 -1
  251. package/dist/custom-objects-BHgn1GEX.js.map +0 -1
  252. package/dist/customer-dir-DIylZ8Q6.js.map +0 -1
  253. package/dist/file-lock-B_zi7NQl.js.map +0 -1
  254. package/dist/gmail-sync-DIaxInDT.js.map +0 -1
  255. package/dist/gmail-sync-hHm9gaWd.cjs.map +0 -1
  256. package/dist/gmail-sync-rQaVqKWd.js.map +0 -1
  257. package/dist/goal-engine-KpBftn4V.js.map +0 -1
  258. package/dist/identity-gyfWdrcX.js +0 -2
  259. package/dist/import-hubspot-BaK71U_K.js.map +0 -1
  260. package/dist/index-V8BFaH-b.d.ts.map +0 -1
  261. package/dist/index-YqwMd6aQ.d.cts.map +0 -1
  262. package/dist/interactions-writer-CrPStUll.cjs.map +0 -1
  263. package/dist/interactions-writer-DO3KcSR3.js.map +0 -1
  264. package/dist/interactions-writer-SLHnoEeE.js.map +0 -1
  265. package/dist/interactions-writer-dSPy1XfO.js +0 -2
  266. package/dist/knowledge-base-D0Fh40kc.js.map +0 -1
  267. package/dist/lancedb-rlvWoPwl.js.map +0 -1
  268. package/dist/lead-model-BCFzyktm.js.map +0 -1
  269. package/dist/memory-Bb6ky3kb.js.map +0 -1
  270. package/dist/notification-dispatcher-0vYNngWe.js.map +0 -1
  271. package/dist/pipeline-writer-BvVquKIe.js.map +0 -1
  272. package/dist/pipeline-writer-N2omexxp.cjs.map +0 -1
  273. package/dist/pipeline-writer-eufx_0o1.js.map +0 -1
  274. package/dist/proactive-worker-BrLHNhjH.js.map +0 -1
  275. package/dist/push-manager-CdqIIkuh.js.map +0 -1
  276. package/dist/quote-generator-BfwENXzg.js.map +0 -1
  277. package/dist/rbac-C7c8tcES.js +0 -2
  278. package/dist/rbac-CTIktZaC.js.map +0 -1
  279. package/dist/relationship-health-odxEoQdJ.js.map +0 -1
  280. package/dist/revenue-simulation-Bqf2DLVB.js.map +0 -1
  281. package/dist/segments-BqcD5HIl.js.map +0 -1
  282. package/dist/sequence-engine-CCTHEBgi.js +0 -2
  283. package/dist/sequence-engine-J1lTW_in.js.map +0 -1
  284. package/dist/sequence-store-DaaWr0Os.js.map +0 -1
  285. package/dist/server-Dyva03K8.js.map +0 -1
  286. package/dist/session-B9AilxOE.js.map +0 -1
  287. package/dist/session-D0qFkBla.cjs.map +0 -1
  288. package/dist/session-D9ub6Wl1.js.map +0 -1
  289. package/dist/session-mWHA71Lw.js +0 -2
  290. package/dist/session-store-B0QZE8Bx.cjs.map +0 -1
  291. package/dist/session-store-C8tEvMPw.js.map +0 -1
  292. package/dist/sop-Vp0UPWFW.js.map +0 -1
  293. package/dist/survey-engine-C06hcQt3.js +0 -2
  294. package/dist/survey-engine-DBjCYqCv.js.map +0 -1
  295. package/dist/sync-state-ChaLbamC.js.map +0 -1
  296. package/dist/ticket-writer-j2oX_Wal.js.map +0 -1
  297. package/dist/tone-Bdm5uaht.js.map +0 -1
  298. package/dist/tone-DRKlZgPr.cjs.map +0 -1
  299. package/dist/tone-vNb2DAAD.js.map +0 -1
  300. package/dist/transcript-watcher-CL2QUygI.js.map +0 -1
  301. package/dist/unmatched-transcripts-BsH5bhkU.js +0 -26
  302. package/dist/unmatched-transcripts-BsH5bhkU.js.map +0 -1
  303. package/dist/unmatched-transcripts-D0PrJ9iz.js +0 -2
  304. package/dist/update-deal-BNwPGaTV.js +0 -2
  305. package/dist/usage-D0-TYJkw.js.map +0 -1
  306. package/dist/vault-DXCg29W-.js.map +0 -1
  307. package/dist/webhooks-7EpA05Qr.js.map +0 -1
  308. package/dist/webhooks-BO2UAnmn.js.map +0 -1
  309. package/dist/webhooks-Xn6zO6kd.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-manager-C0ECQgva.js","names":[],"sources":["../src/sync/push-manager.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { writeJsonFile } from \"../fs/json-store.js\";\n\nexport type PushProvider = \"gmail\" | \"microsoft-graph\" | \"slack\";\nexport type PushStatus = \"active\" | \"expired\" | \"revoked\" | \"error\" | \"permanently_failed\";\n\nexport interface PushSubscription {\n id: string;\n provider: PushProvider;\n slug: string;\n webhookUrl: string;\n expiresAt: string | null;\n renewedAt: string | null;\n createdAt: string;\n providerData: {\n gmailHistoryId?: string;\n gmailTopicName?: string;\n gmailLabelIds?: string[];\n gmailEmailAddress?: string;\n microsoftSubscriptionId?: string;\n microsoftResource?: string;\n microsoftClientState?: string;\n slackTeamId?: string;\n slackChannelId?: string;\n slackBotToken?: string;\n };\n status: PushStatus;\n lastEventAt: string | null;\n eventsProcessed: number;\n renewFailures?: number;\n}\n\ninterface PushSubscriptionsFile {\n subscriptions: PushSubscription[];\n updatedAt: string;\n}\n\nexport function makePushSubId(): string {\n return `psub_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;\n}\n\nexport function subscriptionsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"push-subscriptions.json\");\n}\n\nexport async function readSubscriptions(dataDir: string): Promise<PushSubscription[]> {\n const filePath = subscriptionsPath(dataDir);\n if (!fs.existsSync(filePath)) return [];\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\") as string;\n const parsed = JSON.parse(raw) as PushSubscriptionsFile;\n return parsed.subscriptions ?? [];\n } catch {\n return [];\n }\n}\n\nexport async function writeSubscriptions(dataDir: string, subs: PushSubscription[]): Promise<void> {\n const filePath = subscriptionsPath(dataDir);\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const file: PushSubscriptionsFile = { subscriptions: subs, updatedAt: new Date().toISOString() };\n writeJsonFile(filePath, file);\n}\n\nfunction expiresAtForProvider(provider: PushProvider): string | null {\n if (provider === \"gmail\") {\n return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();\n }\n if (provider === \"microsoft-graph\") {\n return new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();\n }\n return null; // slack: no expiry\n}\n\nexport async function register(\n dataDir: string,\n provider: PushProvider,\n slug: string,\n opts: { webhookUrl: string; providerData?: Partial<PushSubscription[\"providerData\"]> }\n): Promise<PushSubscription> {\n const subs = await readSubscriptions(dataDir);\n const sub: PushSubscription = {\n id: makePushSubId(),\n provider,\n slug,\n webhookUrl: opts.webhookUrl,\n expiresAt: expiresAtForProvider(provider),\n renewedAt: null,\n createdAt: new Date().toISOString(),\n providerData: opts.providerData ?? {},\n status: \"active\",\n lastEventAt: null,\n eventsProcessed: 0,\n };\n await writeSubscriptions(dataDir, [...subs, sub]);\n return sub;\n}\n\nexport async function revoke(dataDir: string, id: string): Promise<void> {\n const subs = await readSubscriptions(dataDir);\n const idx = subs.findIndex((s) => s.id === id);\n if (idx === -1) throw new Error(`Subscription ${id} not found`);\n subs[idx] = { ...subs[idx]!, status: \"revoked\" };\n await writeSubscriptions(dataDir, subs);\n}\n\nexport type RenewFn = (\n sub: PushSubscription\n) => Promise<{ expiresAt: string; providerData?: Partial<PushSubscription[\"providerData\"]> }>;\n\nexport async function renewExpiringSubscriptions(\n dataDir: string,\n renewFn: RenewFn,\n thresholdHours = 24\n): Promise<{ renewed: string[]; errors: string[] }> {\n const subs = await readSubscriptions(dataDir);\n const thresholdMs = thresholdHours * 60 * 60 * 1000;\n const cutoff = Date.now() + thresholdMs;\n\n const renewed: string[] = [];\n const errors: string[] = [];\n\n const PERMANENT_FAILURE_THRESHOLD = 3;\n\n for (let i = 0; i < subs.length; i++) {\n const sub = subs[i]!;\n if (sub.status !== \"active\" && sub.status !== \"error\") continue;\n if (sub.expiresAt === null) continue; // slack: no expiry\n if (new Date(sub.expiresAt).getTime() > cutoff) continue;\n\n try {\n const result = await renewFn(sub);\n subs[i] = {\n ...sub,\n status: \"active\",\n expiresAt: result.expiresAt,\n renewedAt: new Date().toISOString(),\n renewFailures: 0,\n providerData: result.providerData\n ? { ...sub.providerData, ...result.providerData }\n : sub.providerData,\n };\n renewed.push(sub.id);\n } catch {\n const failures = (sub.renewFailures ?? 0) + 1;\n const newStatus: PushStatus =\n failures >= PERMANENT_FAILURE_THRESHOLD ? \"permanently_failed\" : \"error\";\n subs[i] = { ...sub, status: newStatus, renewFailures: failures };\n errors.push(sub.id);\n }\n }\n\n await writeSubscriptions(dataDir, subs);\n return { renewed, errors };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { n as listQuotes, t as generateQuote } from "./quote-generator-BfwENXzg.js";
1
+ import { n as listQuotes, t as generateQuote } from "./quote-generator-CTdR8eEI.js";
2
2
  export { generateQuote, listQuotes };
@@ -1,3 +1,4 @@
1
+ import { t as writeFileAtomic } from "./atomic-write-8yjqqLtS.js";
1
2
  import path from "path";
2
3
  import fs from "fs";
3
4
  import yaml from "js-yaml";
@@ -43,7 +44,7 @@ function buildHtml(quote, config, customerName) {
43
44
  <p><strong>${config.companyName ?? ""}</strong><br>${config.companyAddress ?? ""}<br>${config.vatId ? `USt-IdNr.: ${config.vatId}` : ""}</p>
44
45
  <hr>
45
46
  <p><strong>An:</strong> ${customerName}</p>
46
- <p><strong>Datum:</strong> ${quote.createdAt.slice(0, 10)} &nbsp;&nbsp; <strong>Gültig bis:</strong> ${quote.validUntil}</p>
47
+ <p><strong>Date:</strong> ${quote.createdAt.slice(0, 10)} &nbsp;&nbsp; <strong>Valid until:</strong> ${quote.validUntil}</p>
47
48
  <h2>Leistungen</h2>
48
49
  <table>
49
50
  <thead><tr><th>Beschreibung</th><th style="text-align:right">Menge</th><th style="text-align:right">Einzelpreis</th><th style="text-align:right">Gesamt</th></tr></thead>
@@ -122,12 +123,11 @@ async function generateQuote(dataDir, input) {
122
123
  status: "draft",
123
124
  htmlPath
124
125
  };
125
- fs.writeFileSync(path.join(dir, `${quoteNumber}.json`), JSON.stringify(quote, null, 2), "utf-8");
126
- const html = buildHtml(quote, config, readCustomerName(dataDir, input.slug));
127
- fs.writeFileSync(htmlPath, html, "utf-8");
126
+ writeFileAtomic(path.join(dir, `${quoteNumber}.json`), JSON.stringify(quote, null, 2));
127
+ writeFileAtomic(htmlPath, buildHtml(quote, config, readCustomerName(dataDir, input.slug)));
128
128
  return quote;
129
129
  }
130
130
  //#endregion
131
131
  export { listQuotes as n, readQuote as r, generateQuote as t };
132
132
 
133
- //# sourceMappingURL=quote-generator-BfwENXzg.js.map
133
+ //# sourceMappingURL=quote-generator-CTdR8eEI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-generator-CTdR8eEI.js","names":[],"sources":["../src/core/quote-generator.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { writeFileAtomic } from \"../fs/atomic-write.js\";\nimport yaml from \"js-yaml\";\nimport type { Quote, QuoteLineItem } from \"../schemas/quote.js\";\n\ninterface QuoteConfig {\n companyName?: string;\n companyAddress?: string;\n vatId?: string;\n currency?: string;\n paymentTerms?: string;\n footerText?: string;\n}\n\nexport interface GenerateQuoteInput {\n slug: string;\n dealName: string;\n lineItems: Array<{ description: string; quantity: number; unitPrice: number }>;\n vatPercent?: number;\n validUntilDays?: number;\n currency?: string;\n}\n\nfunction quotesDir(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"quotes\");\n}\n\nfunction loadQuoteConfig(dataDir: string): QuoteConfig {\n const p = path.join(dataDir, \".agentic\", \"quote-config.yaml\");\n if (!fs.existsSync(p)) return {};\n try {\n return (yaml.load(fs.readFileSync(p, \"utf-8\") as string) as QuoteConfig) ?? {};\n } catch {\n return {};\n }\n}\n\nfunction nextQuoteNumber(dataDir: string): string {\n const year = new Date().getFullYear();\n const dir = quotesDir(dataDir);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n return `Q-${year}-001`;\n }\n const existing = fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".json\") && f.startsWith(`Q-${year}-`))\n .map((f) => parseInt(f.replace(`Q-${year}-`, \"\").replace(\".json\", \"\"), 10))\n .filter((n) => !isNaN(n));\n const max = existing.length > 0 ? Math.max(...existing) : 0;\n return `Q-${year}-${String(max + 1).padStart(3, \"0\")}`;\n}\n\nfunction addDaysToDate(isoDate: string, days: number): string {\n const [year, month, day] = isoDate.slice(0, 10).split(\"-\").map(Number) as [\n number,\n number,\n number,\n ];\n const d = new Date(Date.UTC(year, month - 1, day));\n d.setUTCDate(d.getUTCDate() + days);\n return d.toISOString().slice(0, 10);\n}\n\nfunction buildHtml(quote: Quote, config: QuoteConfig, customerName: string): string {\n const lineRows = quote.lineItems\n .map(\n (item) =>\n `<tr><td>${item.description}</td><td style=\"text-align:right\">${item.quantity}</td><td style=\"text-align:right\">${item.unitPrice.toFixed(2)} ${quote.currency}</td><td style=\"text-align:right\">${item.total.toFixed(2)} ${quote.currency}</td></tr>`\n )\n .join(\"\\n\");\n\n return `<!DOCTYPE html>\n<html lang=\"de\">\n<head><meta charset=\"UTF-8\"><title>Angebot ${quote.quoteNumber}</title>\n<style>body{font-family:Arial,sans-serif;max-width:800px;margin:40px auto;color:#222}table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border:1px solid #ddd}th{background:#f5f5f5}h1{color:#1a1a2e}.total{font-weight:bold;font-size:1.1em}</style>\n</head>\n<body>\n<h1>Angebot ${quote.quoteNumber}</h1>\n<p><strong>${config.companyName ?? \"\"}</strong><br>${config.companyAddress ?? \"\"}<br>${config.vatId ? `USt-IdNr.: ${config.vatId}` : \"\"}</p>\n<hr>\n<p><strong>An:</strong> ${customerName}</p>\n<p><strong>Date:</strong> ${quote.createdAt.slice(0, 10)} &nbsp;&nbsp; <strong>Valid until:</strong> ${quote.validUntil}</p>\n<h2>Leistungen</h2>\n<table>\n<thead><tr><th>Beschreibung</th><th style=\"text-align:right\">Menge</th><th style=\"text-align:right\">Einzelpreis</th><th style=\"text-align:right\">Gesamt</th></tr></thead>\n<tbody>${lineRows}</tbody>\n</table>\n<br>\n<table style=\"width:300px;margin-left:auto\">\n<tr><td>Nettobetrag</td><td style=\"text-align:right\">${quote.subtotal.toFixed(2)} ${quote.currency}</td></tr>\n<tr><td>MwSt. (${quote.vatPercent}%)</td><td style=\"text-align:right\">${quote.vat.toFixed(2)} ${quote.currency}</td></tr>\n<tr class=\"total\"><td><strong>Gesamtbetrag</strong></td><td style=\"text-align:right\"><strong>${quote.total.toFixed(2)} ${quote.currency}</strong></td></tr>\n</table>\n<br><p>${config.paymentTerms ?? \"\"}</p>\n<hr><small>${config.footerText ?? \"\"}</small>\n</body></html>`;\n}\n\nfunction readCustomerName(dataDir: string, slug: string): string {\n const p = path.join(dataDir, \"customers\", slug, \"main_facts.md\");\n if (!fs.existsSync(p)) return slug;\n const content = fs.readFileSync(p, \"utf-8\") as string;\n const match = /^name:\\s*(.+)$/m.exec(content);\n return match?.[1]?.trim() ?? slug;\n}\n\nexport function readQuote(dataDir: string, quoteNumber: string): Quote | null {\n const p = path.join(quotesDir(dataDir), `${quoteNumber}.json`);\n if (!fs.existsSync(p)) return null;\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as Quote;\n } catch {\n return null;\n }\n}\n\nexport function listQuotes(dataDir: string, slug?: string): Quote[] {\n const dir = quotesDir(dataDir);\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".json\"))\n .flatMap((f) => {\n try {\n const q = JSON.parse(fs.readFileSync(path.join(dir, f), \"utf-8\") as string) as Quote;\n return slug === undefined || q.slug === slug ? [q] : [];\n } catch {\n return [];\n }\n });\n}\n\nexport function updateQuoteStatus(\n dataDir: string,\n quoteNumber: string,\n status: Quote[\"status\"]\n): void {\n const q = readQuote(dataDir, quoteNumber);\n if (!q) return;\n const updated: Quote = { ...q, status };\n if (status === \"viewed\" && !q.viewedAt) updated.viewedAt = new Date().toISOString();\n if (status === \"accepted\" && !q.acceptedAt) updated.acceptedAt = new Date().toISOString();\n writeFileAtomic(\n path.join(quotesDir(dataDir), `${quoteNumber}.json`),\n JSON.stringify(updated, null, 2)\n );\n}\n\nexport async function generateQuote(dataDir: string, input: GenerateQuoteInput): Promise<Quote> {\n const config = loadQuoteConfig(dataDir);\n const vatPercent = input.vatPercent ?? 19;\n const validUntilDays = input.validUntilDays ?? 30;\n const currency = input.currency ?? config.currency ?? \"EUR\";\n\n const items: QuoteLineItem[] = input.lineItems.map((item) => ({\n description: item.description,\n quantity: item.quantity,\n unitPrice: item.unitPrice,\n total: item.quantity * item.unitPrice,\n }));\n\n const subtotal = items.reduce((sum, i) => sum + i.total, 0);\n const vat = Math.round(subtotal * (vatPercent / 100) * 100) / 100;\n const total = Math.round((subtotal + vat) * 100) / 100;\n\n const quoteNumber = nextQuoteNumber(dataDir);\n const now = new Date().toISOString();\n const validUntil = addDaysToDate(now.slice(0, 10), validUntilDays);\n\n const dir = quotesDir(dataDir);\n fs.mkdirSync(dir, { recursive: true });\n\n const htmlPath = path.join(dir, `${quoteNumber}.html`);\n\n const quote: Quote = {\n quoteNumber,\n slug: input.slug,\n dealName: input.dealName,\n lineItems: items,\n subtotal,\n vatPercent,\n vat,\n total,\n currency,\n createdAt: now,\n validUntilDays,\n validUntil,\n status: \"draft\",\n htmlPath,\n };\n\n writeFileAtomic(path.join(dir, `${quoteNumber}.json`), JSON.stringify(quote, null, 2));\n\n const customerName = readCustomerName(dataDir, input.slug);\n const html = buildHtml(quote, config, customerName);\n writeFileAtomic(htmlPath, html);\n\n return quote;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { c as getRole, l as loadFieldAcl, u as redactFields } from "./rbac-msmBc_tK.js";
2
+ export { getRole, loadFieldAcl, redactFields };
@@ -1,3 +1,4 @@
1
+ import { i as writeJsonFile, n as readJsonFile } from "./json-store-WWsFzXub.js";
1
2
  import path from "path";
2
3
  import fs from "fs";
3
4
  //#region src/core/rbac.ts
@@ -28,20 +29,12 @@ function rbacPath(dataDir) {
28
29
  return path.join(dataDir, ".agentic", "rbac.json");
29
30
  }
30
31
  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
- }
32
+ return readJsonFile(rbacPath(dataDir), { actors: {} });
38
33
  }
39
34
  function setActorRole(dataDir, actor, role) {
40
- const p = rbacPath(dataDir);
41
- fs.mkdirSync(path.dirname(p), { recursive: true });
42
35
  const config = getRbacConfig(dataDir);
43
36
  config.actors[actor] = role;
44
- fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
37
+ writeJsonFile(rbacPath(dataDir), config);
45
38
  }
46
39
  function getRole(dataDir, actor) {
47
40
  const config = getRbacConfig(dataDir);
@@ -69,6 +62,20 @@ function canSeeCustomer(dataDir, actor, slug) {
69
62
  if (!owned) return false;
70
63
  return (owned[actor] ?? []).includes(slug);
71
64
  }
65
+ /**
66
+ * Build a once-loaded predicate for which customers `actor` may see. Equivalent
67
+ * to calling canSeeCustomer per slug, but reads/parses rbac.json a single time
68
+ * (and uses O(1) Set membership) — for hot loops like list_customers.
69
+ */
70
+ function customerVisibility(dataDir, actor) {
71
+ if (!fs.existsSync(rbacPath(dataDir))) return () => true;
72
+ if (actor === "system") return () => true;
73
+ const config = getRbacConfig(dataDir);
74
+ const role = config.actors[actor] ?? config.default ?? "rep";
75
+ if (role === "admin" || role === "manager") return () => true;
76
+ const owned = new Set(config.owned_customers?.[actor] ?? []);
77
+ return (slug) => owned.has(slug);
78
+ }
72
79
  /** Load the field-level ACL (field → allowed roles) from rbac.json. */
73
80
  function loadFieldAcl(dataDir) {
74
81
  return getRbacConfig(dataDir).field_acl ?? {};
@@ -86,6 +93,6 @@ function redactFields(values, role, acl) {
86
93
  return out;
87
94
  }
88
95
  //#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 };
96
+ export { customerVisibility as a, getRole as c, setActorRole as d, canWrite as i, loadFieldAcl as l, canSeeCustomer as n, enforceRbac as o, canSeeField as r, getRbacConfig as s, assertCanWrite as t, redactFields as u };
90
97
 
91
- //# sourceMappingURL=rbac-CTIktZaC.js.map
98
+ //# sourceMappingURL=rbac-msmBc_tK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rbac-msmBc_tK.js","names":[],"sources":["../src/core/rbac.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { readJsonFile, writeJsonFile } from \"../fs/json-store.js\";\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 return readJsonFile<RbacConfig>(rbacPath(dataDir), { actors: {} });\n}\n\nexport function setActorRole(dataDir: string, actor: string, role: Role): void {\n const config = getRbacConfig(dataDir);\n config.actors[actor] = role;\n writeJsonFile(rbacPath(dataDir), config);\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/**\n * Build a once-loaded predicate for which customers `actor` may see. Equivalent\n * to calling canSeeCustomer per slug, but reads/parses rbac.json a single time\n * (and uses O(1) Set membership) — for hot loops like list_customers.\n */\nexport function customerVisibility(dataDir: string, actor: string): (slug: string) => boolean {\n if (!fs.existsSync(rbacPath(dataDir))) return () => true; // open access\n if (actor === \"system\") return () => true;\n const config = getRbacConfig(dataDir);\n const role = config.actors[actor] ?? config.default ?? \"rep\";\n if (role === \"admin\" || role === \"manager\") return () => true;\n const owned = new Set(config.owned_customers?.[actor] ?? []);\n return (slug: string) => owned.has(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":";;;;AAcA,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,OAAO,aAAyB,SAAS,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;AACnE;AAEA,SAAgB,aAAa,SAAiB,OAAe,MAAkB;CAC7E,MAAM,SAAS,cAAc,OAAO;CACpC,OAAO,OAAO,SAAS;CACvB,cAAc,SAAS,OAAO,GAAG,MAAM;AACzC;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;;;;;;AAOA,SAAgB,mBAAmB,SAAiB,OAA0C;CAC5F,IAAI,CAAC,GAAG,WAAW,SAAS,OAAO,CAAC,GAAG,aAAa;CACpD,IAAI,UAAU,UAAU,aAAa;CACrC,MAAM,SAAS,cAAc,OAAO;CACpC,MAAM,OAAO,OAAO,OAAO,UAAU,OAAO,WAAW;CACvD,IAAI,SAAS,WAAW,SAAS,WAAW,aAAa;CACzD,MAAM,QAAQ,IAAI,IAAI,OAAO,kBAAkB,UAAU,CAAC,CAAC;CAC3D,QAAQ,SAAiB,MAAM,IAAI,IAAI;AACzC;;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,13 @@
1
+ //#region src/core/regex.ts
2
+ /**
3
+ * Escape a string so it can be embedded safely as a literal inside a `RegExp`.
4
+ * Prevents both broken patterns and ReDoS/injection when interpolating
5
+ * field names, section headers, or other dynamic values into a regex.
6
+ */
7
+ function escapeRegExp(value) {
8
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ }
10
+ //#endregion
11
+ export { escapeRegExp as t };
12
+
13
+ //# sourceMappingURL=regex-Jt5DatPi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-Jt5DatPi.js","names":[],"sources":["../src/core/regex.ts"],"sourcesContent":["/**\n * Escape a string so it can be embedded safely as a literal inside a `RegExp`.\n * Prevents both broken patterns and ReDoS/injection when interpolating\n * field names, section headers, or other dynamic values into a regex.\n */\nexport function escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;AAKA,SAAgB,aAAa,OAAuB;CAClD,OAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD"}
@@ -1,4 +1,6 @@
1
- import { t as withJsonFile } from "./file-lock-B_zi7NQl.js";
1
+ import { i as writeJsonFile, n as readJsonFile } from "./json-store-WWsFzXub.js";
2
+ import { t as withJsonFile } from "./file-lock-CcHotQkZ.js";
3
+ import { n as logger } from "./logger-Dyl4VcLO.js";
2
4
  import path from "path";
3
5
  import fs from "fs";
4
6
  import matter from "gray-matter";
@@ -21,7 +23,7 @@ function readGraph(dataDir, slug) {
21
23
  try {
22
24
  return JSON.parse(fs.readFileSync(p, "utf-8"));
23
25
  } catch {
24
- process.stderr.write(`[graph] failed to parse ${p} — returning empty graph\n`);
26
+ logger.warn("graph", "failed to parse graph file — returning empty graph", { path: p });
25
27
  return emptyGraph(slug);
26
28
  }
27
29
  }
@@ -274,23 +276,13 @@ function healthPath(dataDir, slug) {
274
276
  return path.join(dataDir, "customers", slug, "health.json");
275
277
  }
276
278
  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
- }
279
+ return readJsonFile(healthPath(dataDir, slug), null);
284
280
  }
285
281
  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 = {
282
+ writeJsonFile(healthPath(dataDir, slug), {
290
283
  ...health,
291
284
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
292
- };
293
- fs.writeFileSync(p, JSON.stringify(updated, null, 2), "utf-8");
285
+ });
294
286
  }
295
287
  function parseContactInteractions(content) {
296
288
  const blocks = content.split(/(?=^## \d{4}-\d{2}-\d{2})/m).filter((b) => b.trim().length > 0);
@@ -451,4 +443,4 @@ async function updateHealthFromInteraction(dataDir, slug) {
451
443
  //#endregion
452
444
  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
445
 
454
- //# sourceMappingURL=relationship-health-odxEoQdJ.js.map
446
+ //# sourceMappingURL=relationship-health-ZZNXR1RZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationship-health-ZZNXR1RZ.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\";\nimport { logger } from \"./logger.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 logger.warn(\"graph\", \"failed to parse graph file — returning empty graph\", { path: p });\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\";\nimport { readJsonFile, writeJsonFile } from \"../fs/json-store.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 return readJsonFile<HealthSnapshot | null>(healthPath(dataDir, slug), null);\n}\n\nexport function writeHealth(dataDir: string, slug: string, health: HealthSnapshot): void {\n writeJsonFile(healthPath(dataDir, slug), { ...health, updatedAt: new Date().toISOString() });\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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"}
@@ -1,8 +1,8 @@
1
- import { r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
2
- import { t as readPipeline } from "./pipeline-writer-BvVquKIe.js";
3
- import { c as readGraph, n as readHealth, s as getStakeholders, t as computeCustomerHealth } from "./relationship-health-odxEoQdJ.js";
1
+ import { i as listCustomerSlugs } from "./customer-dir-CkMMXhb0.js";
2
+ import { i as writeJsonFile, n as readJsonFile } from "./json-store-WWsFzXub.js";
3
+ import { t as readPipeline } from "./pipeline-writer-CIllfnZl.js";
4
+ import { c as readGraph, n as readHealth, s as getStakeholders, t as computeCustomerHealth } from "./relationship-health-ZZNXR1RZ.js";
4
5
  import path from "path";
5
- import fs from "fs";
6
6
  //#region src/core/pipeline-stages.ts
7
7
  const DEFAULT_STAGES = [
8
8
  {
@@ -48,13 +48,7 @@ function stagesPath(dataDir) {
48
48
  return path.join(dataDir, ".agentic", "pipeline-stages.json");
49
49
  }
50
50
  function getPipelineStages(dataDir) {
51
- const p = stagesPath(dataDir);
52
- if (!fs.existsSync(p)) return DEFAULT_STAGES;
53
- try {
54
- return JSON.parse(fs.readFileSync(p, "utf-8"));
55
- } catch {
56
- return DEFAULT_STAGES;
57
- }
51
+ return readJsonFile(stagesPath(dataDir), DEFAULT_STAGES);
58
52
  }
59
53
  function setPipelineStage(dataDir, stage) {
60
54
  const stages = getPipelineStages(dataDir);
@@ -62,17 +56,13 @@ function setPipelineStage(dataDir, stage) {
62
56
  if (idx >= 0) stages[idx] = stage;
63
57
  else stages.push(stage);
64
58
  stages.sort((a, b) => a.order - b.order);
65
- fs.mkdirSync(path.dirname(stagesPath(dataDir)), { recursive: true });
66
- fs.writeFileSync(stagesPath(dataDir), JSON.stringify(stages, null, 2));
59
+ writeJsonFile(stagesPath(dataDir), stages);
67
60
  }
68
61
  function deletePipelineStage(dataDir, id) {
69
- const stages = getPipelineStages(dataDir).filter((s) => s.id !== id);
70
- fs.mkdirSync(path.dirname(stagesPath(dataDir)), { recursive: true });
71
- fs.writeFileSync(stagesPath(dataDir), JSON.stringify(stages, null, 2));
62
+ writeJsonFile(stagesPath(dataDir), getPipelineStages(dataDir).filter((s) => s.id !== id));
72
63
  }
73
64
  function resetToDefaults(dataDir) {
74
- fs.mkdirSync(path.dirname(stagesPath(dataDir)), { recursive: true });
75
- fs.writeFileSync(stagesPath(dataDir), JSON.stringify(DEFAULT_STAGES, null, 2));
65
+ writeJsonFile(stagesPath(dataDir), DEFAULT_STAGES);
76
66
  }
77
67
  //#endregion
78
68
  //#region src/core/revenue-simulation.ts
@@ -248,4 +238,4 @@ async function buildSimulationInput(dataDir, horizon, today, externalSignals = [
248
238
  //#endregion
249
239
  export { buildTopRisks as a, percentile as c, deletePipelineStage as d, getPipelineStages as f, buildSimulationInput as i, runSimulation as l, setPipelineStage as m, buildConfidenceMessage as n, closeVarianceFn as o, resetToDefaults as p, buildSensitivityMap as r, mean as s, adjustProbability as t, stdDevFn as u };
250
240
 
251
- //# sourceMappingURL=revenue-simulation-Bqf2DLVB.js.map
241
+ //# sourceMappingURL=revenue-simulation-D8f_YkUY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revenue-simulation-D8f_YkUY.js","names":[],"sources":["../src/core/pipeline-stages.ts","../src/core/revenue-simulation.ts"],"sourcesContent":["import path from \"path\";\nimport { readJsonFile, writeJsonFile } from \"../fs/json-store.js\";\n\nexport interface PipelineStage {\n id: string;\n label: string;\n color?: string;\n order: number;\n isFinal?: boolean;\n probability?: number;\n}\n\nexport const DEFAULT_STAGES: PipelineStage[] = [\n { id: \"lead\", label: \"Lead\", order: 1, probability: 10 },\n { id: \"qualified\", label: \"Qualified\", order: 2, probability: 30 },\n { id: \"proposal\", label: \"Proposal\", order: 3, probability: 50 },\n { id: \"negotiation\", label: \"Negotiation\", order: 4, probability: 75 },\n { id: \"won\", label: \"Won\", order: 5, isFinal: true, probability: 100 },\n { id: \"lost\", label: \"Lost\", order: 6, isFinal: true, probability: 0 },\n];\n\nfunction stagesPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"pipeline-stages.json\");\n}\n\nexport function getPipelineStages(dataDir: string): PipelineStage[] {\n return readJsonFile<PipelineStage[]>(stagesPath(dataDir), DEFAULT_STAGES);\n}\n\nexport function setPipelineStage(dataDir: string, stage: PipelineStage): void {\n const stages = getPipelineStages(dataDir);\n const idx = stages.findIndex((s) => s.id === stage.id);\n if (idx >= 0) stages[idx] = stage;\n else stages.push(stage);\n stages.sort((a, b) => a.order - b.order);\n writeJsonFile(stagesPath(dataDir), stages);\n}\n\nexport function deletePipelineStage(dataDir: string, id: string): void {\n writeJsonFile(\n stagesPath(dataDir),\n getPipelineStages(dataDir).filter((s) => s.id !== id)\n );\n}\n\nexport function resetToDefaults(dataDir: string): void {\n writeJsonFile(stagesPath(dataDir), DEFAULT_STAGES);\n}\n","import { readPipeline } from \"../fs/pipeline-writer.js\";\nimport { listCustomerSlugs } from \"../fs/customer-dir.js\";\nimport { readHealth, computeCustomerHealth } from \"./relationship-health.js\";\nimport { readGraph, getStakeholders } from \"./graph.js\";\nimport { getPipelineStages } from \"./pipeline-stages.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface ExternalSignal {\n slug: string;\n type: \"funding_round\" | \"leadership_change\" | \"news_positive\" | \"news_negative\";\n impact: \"positive\" | \"negative\" | \"neutral\";\n magnitude: number; // 0.0–1.0\n summary: string;\n}\n\nexport interface DealSnapshot {\n slug: string;\n name: string;\n stage: string;\n value: number;\n probability: number;\n closeDate?: string;\n healthScore: number;\n daysSinceContact: number;\n championPresent: boolean;\n}\n\nexport interface SimulationInput {\n deals: DealSnapshot[];\n externalSignals: ExternalSignal[];\n iterations: number;\n horizon: \"quarter\" | \"year\";\n today: string;\n}\n\nexport interface MonthForecast {\n p50: number;\n range: [number, number];\n}\n\nexport interface SimulationResult {\n p10: number;\n p50: number;\n p90: number;\n expected: number;\n stdDev: number;\n atRiskRevenue: number;\n byCloseMonth: Record<string, MonthForecast>;\n topRisks: string[];\n sensitivityMap: Record<string, number>;\n}\n\n// ─── Pure helpers ─────────────────────────────────────────────────────────────\n\nexport function percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.max(0, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[idx]!;\n}\n\nexport function mean(values: number[]): number {\n if (values.length === 0) return 0;\n return values.reduce((s, v) => s + v, 0) / values.length;\n}\n\nexport function stdDevFn(values: number[], m: number): number {\n if (values.length < 2) return 0;\n const variance = values.reduce((s, v) => s + (v - m) ** 2, 0) / values.length;\n return Math.sqrt(variance);\n}\n\nexport function adjustProbability(deal: DealSnapshot, signals: ExternalSignal[] = []): number {\n let prob = deal.probability / 100;\n\n // Health adjustment: health 60 = neutral, range -0.12 to +0.08\n const healthAdj = ((deal.healthScore - 60) / 100) * 0.2;\n prob += healthAdj;\n\n // Champion bonus\n if (deal.championPresent) prob += 0.05;\n\n // External signals (D18-ready)\n for (const signal of signals) {\n if (signal.slug === deal.slug) {\n if (signal.impact === \"positive\") prob += 0.05 * signal.magnitude;\n if (signal.impact === \"negative\") prob -= 0.1 * signal.magnitude;\n }\n }\n\n return Math.max(0.02, Math.min(0.98, prob));\n}\n\nexport function closeVarianceFn(\n deal: DealSnapshot,\n randFn: () => number,\n todayMs: number = Date.now()\n): number {\n const daysToClose = deal.closeDate\n ? Math.max(0, Math.floor((new Date(deal.closeDate).getTime() - todayMs) / 86_400_000))\n : 90;\n const variance = daysToClose < 30 ? 0.05 : 0.15;\n return 1 + (randFn() - 0.5) * 2 * variance;\n}\n\nexport function buildSensitivityMap(\n deals: DealSnapshot[],\n signals: ExternalSignal[]\n): Record<string, number> {\n const map: Record<string, number> = {};\n for (const deal of deals) {\n map[deal.name] = Math.round(deal.value * adjustProbability(deal, signals));\n }\n return map;\n}\n\nexport function buildTopRisks(\n deals: DealSnapshot[],\n signals: ExternalSignal[],\n sensitivityMap: Record<string, number>\n): string[] {\n const atRisk = deals.filter((d) => d.healthScore < 60 || d.daysSinceContact > 14);\n return atRisk\n .sort((a, b) => (sensitivityMap[b.name] ?? 0) - (sensitivityMap[a.name] ?? 0))\n .slice(0, 5)\n .map((d) => {\n const reasons: string[] = [];\n if (d.healthScore < 60) reasons.push(`health ${d.healthScore}`);\n if (d.daysSinceContact > 14) reasons.push(`${d.daysSinceContact}d no contact`);\n if (!d.championPresent) reasons.push(\"no champion\");\n return `${d.slug}/${d.name}: ${reasons.join(\", \")} — €${d.value} at risk`;\n });\n}\n\n// ─── Monte Carlo Core ─────────────────────────────────────────────────────────\n\nconst MAX_ITERATIONS = 50_000;\n\nexport function runSimulation(\n input: SimulationInput,\n randFn: () => number = Math.random\n): SimulationResult {\n const { deals, externalSignals } = input;\n const iterations = Math.min(input.iterations, MAX_ITERATIONS);\n\n if (deals.length === 0) {\n return {\n p10: 0,\n p50: 0,\n p90: 0,\n expected: 0,\n stdDev: 0,\n atRiskRevenue: 0,\n byCloseMonth: {},\n topRisks: [],\n sensitivityMap: {},\n };\n }\n\n const todayMs = new Date(input.today).getTime();\n const adjustedProbs = deals.map((d) => adjustProbability(d, externalSignals));\n const outcomes: number[] = [];\n const byMonthOutcomes: Record<string, number[]> = {};\n\n for (let i = 0; i < iterations; i++) {\n let total = 0;\n const monthTotals: Record<string, number> = {};\n\n for (let j = 0; j < deals.length; j++) {\n const deal = deals[j]!;\n const prob = adjustedProbs[j]!;\n if (randFn() < prob) {\n const closedValue = Math.round(deal.value * closeVarianceFn(deal, randFn, todayMs));\n total += closedValue;\n if (deal.closeDate) {\n const month = deal.closeDate.slice(0, 7);\n monthTotals[month] = (monthTotals[month] ?? 0) + closedValue;\n }\n }\n }\n\n outcomes.push(total);\n // Winning-only: only record months where at least one deal closed in this iteration\n for (const [month, val] of Object.entries(monthTotals)) {\n if (val > 0) {\n byMonthOutcomes[month] ??= [];\n byMonthOutcomes[month]!.push(val);\n }\n }\n }\n\n outcomes.sort((a, b) => a - b);\n const exp = Math.round(mean(outcomes));\n const sd = Math.round(stdDevFn(outcomes, exp));\n\n const byCloseMonth: Record<string, MonthForecast> = {};\n for (const [month, vals] of Object.entries(byMonthOutcomes)) {\n const sorted = [...vals].sort((a, b) => a - b);\n byCloseMonth[month] = {\n p50: Math.round(percentile(sorted, 50)),\n range: [Math.round(percentile(sorted, 10)), Math.round(percentile(sorted, 90))],\n };\n }\n\n const sensitivityMap = buildSensitivityMap(deals, externalSignals);\n const topRisks = buildTopRisks(deals, externalSignals, sensitivityMap);\n const atRiskRevenue = deals.filter((d) => d.healthScore < 60).reduce((s, d) => s + d.value, 0);\n\n return {\n p10: Math.round(percentile(outcomes, 10)),\n p50: Math.round(percentile(outcomes, 50)),\n p90: Math.round(percentile(outcomes, 90)),\n expected: exp,\n stdDev: sd,\n atRiskRevenue,\n byCloseMonth,\n topRisks,\n sensitivityMap,\n };\n}\n\n// ─── Confidence message ───────────────────────────────────────────────────────\n\nexport function buildConfidenceMessage(result: SimulationResult, dealCount: number): string {\n const range = result.p90 - result.p10;\n const atRiskPct =\n result.expected > 0 ? Math.round((result.atRiskRevenue / result.expected) * 100) : 0;\n return `P50 forecast: €${(result.p50 / 1000).toFixed(1)}k with ±€${(range / 2 / 1000).toFixed(1)}k uncertainty (P10–P90 range). ${atRiskPct}% of pipeline is at risk. ${dealCount} deals simulated.`;\n}\n\n// ─── Quarter helper ───────────────────────────────────────────────────────────\n\nfunction getQuarterEnd(date: Date): Date {\n const month = date.getMonth();\n const quarterEndMonth = Math.floor(month / 3) * 3 + 2;\n return new Date(date.getFullYear(), quarterEndMonth + 1, 0);\n}\n\n// ─── Data aggregation ─────────────────────────────────────────────────────────\n\nexport async function buildSimulationInput(\n dataDir: string,\n horizon: \"quarter\" | \"year\",\n today: string,\n externalSignals: ExternalSignal[] = []\n): Promise<SimulationInput> {\n const slugs = listCustomerSlugs(dataDir);\n if (slugs.length === 0) {\n return { deals: [], externalSignals, iterations: 10_000, horizon, today };\n }\n\n const stages = getPipelineStages(dataDir);\n const stageProb: Record<string, number> = {};\n for (const s of stages) {\n stageProb[s.id] = s.probability ?? 50;\n }\n\n const deals: DealSnapshot[] = [];\n const todayDate = new Date(today);\n const horizonEnd =\n horizon === \"quarter\" ? getQuarterEnd(todayDate) : new Date(todayDate.getFullYear(), 11, 31);\n\n for (const slug of slugs) {\n const pipelineDeals = await readPipeline(dataDir, slug).catch(() => []);\n if (pipelineDeals.length === 0) continue;\n\n const health = readHealth(dataDir, slug) ?? computeCustomerHealth(dataDir, slug, today);\n const healthScore = health.overallHealth;\n\n const graph = readGraph(dataDir, slug);\n const stakeholders = getStakeholders(graph);\n const championPresent = stakeholders.champions.length > 0;\n\n const lastContact = health.contacts\n .map((c) => c.lastContact)\n .filter((lc): lc is string => !!lc)\n .sort()\n .pop();\n const daysSinceContact = lastContact\n ? Math.floor((todayDate.getTime() - new Date(lastContact).getTime()) / 86_400_000)\n : 999;\n\n for (const deal of pipelineDeals) {\n if (deal.stage === \"won\" || deal.stage === \"lost\") continue;\n\n if (deal.close_date && deal.close_date.trim() !== \"\") {\n const closeDate = new Date(deal.close_date);\n if (closeDate > horizonEnd) continue;\n }\n\n const probability = deal.probability ?? stageProb[deal.stage] ?? 50;\n const snapshot: DealSnapshot = {\n slug,\n name: deal.name,\n stage: deal.stage,\n value: deal.value ?? 0,\n probability,\n healthScore,\n daysSinceContact,\n championPresent,\n };\n if (deal.close_date && deal.close_date.trim() !== \"\") {\n snapshot.closeDate = deal.close_date;\n }\n deals.push(snapshot);\n }\n }\n\n return { deals, externalSignals, iterations: 10_000, horizon, today };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { i as buildSimulationInput } from "./revenue-simulation-Bqf2DLVB.js";
1
+ import { i as buildSimulationInput } from "./revenue-simulation-D8f_YkUY.js";
2
2
  export { buildSimulationInput };
@@ -0,0 +1,18 @@
1
+ //#region src/fs/safe-path.ts
2
+ /**
3
+ * Filesystem path-segment safety. A segment (customer slug, custom-object name,
4
+ * KB article id/category, …) is safe iff it cannot escape its parent directory:
5
+ * no path separators, no `..`, no NUL, not "." or empty, and bounded in length.
6
+ * Enforced wherever an untrusted value (from an MCP tool, API, or import) is used
7
+ * to build a file path, to prevent path-traversal (arbitrary read/write).
8
+ */
9
+ function isSafePathSegment(segment) {
10
+ return typeof segment === "string" && segment.length > 0 && segment.length <= 128 && segment !== "." && !segment.includes("/") && !segment.includes("\\") && !segment.includes("\0") && !segment.includes("..");
11
+ }
12
+ function assertSafePathSegment(segment, kind = "path segment") {
13
+ if (!isSafePathSegment(segment)) throw new Error(`Invalid ${kind}: ${JSON.stringify(segment)}`);
14
+ }
15
+ //#endregion
16
+ export { isSafePathSegment as n, assertSafePathSegment as t };
17
+
18
+ //# sourceMappingURL=safe-path-mpp0dKtO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-path-mpp0dKtO.js","names":[],"sources":["../src/fs/safe-path.ts"],"sourcesContent":["/**\n * Filesystem path-segment safety. A segment (customer slug, custom-object name,\n * KB article id/category, …) is safe iff it cannot escape its parent directory:\n * no path separators, no `..`, no NUL, not \".\" or empty, and bounded in length.\n * Enforced wherever an untrusted value (from an MCP tool, API, or import) is used\n * to build a file path, to prevent path-traversal (arbitrary read/write).\n */\nexport function isSafePathSegment(segment: unknown): segment is string {\n return (\n typeof segment === \"string\" &&\n segment.length > 0 &&\n segment.length <= 128 &&\n segment !== \".\" &&\n !segment.includes(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"\\0\") &&\n !segment.includes(\"..\")\n );\n}\n\nexport function assertSafePathSegment(segment: string, kind = \"path segment\"): void {\n if (!isSafePathSegment(segment)) {\n throw new Error(`Invalid ${kind}: ${JSON.stringify(segment)}`);\n }\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,kBAAkB,SAAqC;CACrE,OACE,OAAO,YAAY,YACnB,QAAQ,SAAS,KACjB,QAAQ,UAAU,OAClB,YAAY,OACZ,CAAC,QAAQ,SAAS,GAAG,KACrB,CAAC,QAAQ,SAAS,IAAI,KACtB,CAAC,QAAQ,SAAS,IAAI,KACtB,CAAC,QAAQ,SAAS,IAAI;AAE1B;AAEA,SAAgB,sBAAsB,SAAiB,OAAO,gBAAsB;CAClF,IAAI,CAAC,kBAAkB,OAAO,GAC5B,MAAM,IAAI,MAAM,WAAW,KAAK,IAAI,KAAK,UAAU,OAAO,GAAG;AAEjE"}
@@ -1,19 +1,12 @@
1
- import { i as readMainFacts, r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
1
+ import { a as readMainFacts, i as listCustomerSlugs } from "./customer-dir-CkMMXhb0.js";
2
+ import { r as writeJsonArray, t as readJsonArray } from "./json-store-WWsFzXub.js";
2
3
  import path from "path";
3
- import fs from "fs";
4
4
  //#region src/core/segments.ts
5
5
  function segmentsPath(dataDir) {
6
6
  return path.join(dataDir, ".agentic", "segments.json");
7
7
  }
8
8
  function loadSegments(dataDir) {
9
- const p = segmentsPath(dataDir);
10
- if (!fs.existsSync(p)) return [];
11
- try {
12
- const data = JSON.parse(fs.readFileSync(p, "utf-8"));
13
- return Array.isArray(data.segments) ? data.segments : [];
14
- } catch {
15
- return [];
16
- }
9
+ return readJsonArray(segmentsPath(dataDir), "segments");
17
10
  }
18
11
  function defineSegment(dataDir, name, criteria) {
19
12
  const segs = loadSegments(dataDir);
@@ -24,9 +17,7 @@ function defineSegment(dataDir, name, criteria) {
24
17
  };
25
18
  if (idx >= 0) segs[idx] = def;
26
19
  else segs.push(def);
27
- const p = segmentsPath(dataDir);
28
- fs.mkdirSync(path.dirname(p), { recursive: true });
29
- fs.writeFileSync(p, JSON.stringify({ segments: segs }, null, 2), "utf-8");
20
+ writeJsonArray(segmentsPath(dataDir), "segments", segs);
30
21
  return segs;
31
22
  }
32
23
  function daysBetween(fromIso, toIso) {
@@ -58,4 +49,4 @@ async function evaluateSegment(dataDir, criteria, now = (/* @__PURE__ */ new Dat
58
49
  //#endregion
59
50
  export { defineSegment, evaluateSegment, loadSegments };
60
51
 
61
- //# sourceMappingURL=segments-BqcD5HIl.js.map
52
+ //# sourceMappingURL=segments-DI3LOQNe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segments-DI3LOQNe.js","names":[],"sources":["../src/core/segments.ts"],"sourcesContent":["import path from \"path\";\nimport { readMainFacts, listCustomerSlugs } from \"../fs/customer-dir.js\";\nimport { readJsonArray, writeJsonArray } from \"../fs/json-store.js\";\n\n/**\n * Customer segments (marketing lists, N4-1): named filter criteria over\n * customers, evaluated on demand. Definitions live in .agentic/segments.json.\n */\nexport interface SegmentCriteria {\n stage?: string;\n tags?: string[];\n minDealValue?: number;\n staleDays?: number;\n}\n\nexport interface SegmentDefinition {\n name: string;\n criteria: SegmentCriteria;\n}\n\nfunction segmentsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"segments.json\");\n}\n\nexport function loadSegments(dataDir: string): SegmentDefinition[] {\n return readJsonArray<SegmentDefinition>(segmentsPath(dataDir), \"segments\");\n}\n\nexport function defineSegment(\n dataDir: string,\n name: string,\n criteria: SegmentCriteria\n): SegmentDefinition[] {\n const segs = loadSegments(dataDir);\n const idx = segs.findIndex((s) => s.name === name);\n const def: SegmentDefinition = { name, criteria };\n if (idx >= 0) segs[idx] = def;\n else segs.push(def);\n writeJsonArray(segmentsPath(dataDir), \"segments\", segs);\n return segs;\n}\n\nexport function removeSegment(dataDir: string, name: string): boolean {\n const segs = loadSegments(dataDir);\n const next = segs.filter((s) => s.name !== name);\n if (next.length === segs.length) return false;\n writeJsonArray(segmentsPath(dataDir), \"segments\", next);\n return true;\n}\n\nfunction daysBetween(fromIso: string, toIso: string): number {\n const a = new Date(fromIso).getTime();\n const b = new Date(toIso).getTime();\n if (Number.isNaN(a) || Number.isNaN(b)) return 0;\n return Math.floor((b - a) / 86_400_000);\n}\n\n/** Return the customer slugs matching the criteria (now defaults to today). */\nexport async function evaluateSegment(\n dataDir: string,\n criteria: SegmentCriteria,\n now: string = new Date().toISOString().slice(0, 10)\n): Promise<string[]> {\n const matches: string[] = [];\n for (const slug of listCustomerSlugs(dataDir)) {\n const facts = await readMainFacts(dataDir, slug).catch(() => null);\n if (!facts) continue;\n\n if (criteria.stage && facts.relationship_stage !== criteria.stage) continue;\n if (\n criteria.minDealValue !== undefined &&\n !(typeof facts.deal_value === \"number\" && facts.deal_value >= criteria.minDealValue)\n ) {\n continue;\n }\n if (criteria.tags && criteria.tags.length > 0) {\n const tags = facts.tags ?? [];\n if (!criteria.tags.every((t) => tags.includes(t))) continue;\n }\n if (criteria.staleDays !== undefined) {\n // `updated` is the recency proxy (last record change).\n const lt = facts.updated;\n if (!lt || daysBetween(lt, now) < criteria.staleDays) continue;\n }\n matches.push(slug);\n }\n return matches;\n}\n"],"mappings":";;;;AAoBA,SAAS,aAAa,SAAyB;CAC7C,OAAO,KAAK,KAAK,SAAS,YAAY,eAAe;AACvD;AAEA,SAAgB,aAAa,SAAsC;CACjE,OAAO,cAAiC,aAAa,OAAO,GAAG,UAAU;AAC3E;AAEA,SAAgB,cACd,SACA,MACA,UACqB;CACrB,MAAM,OAAO,aAAa,OAAO;CACjC,MAAM,MAAM,KAAK,WAAW,MAAM,EAAE,SAAS,IAAI;CACjD,MAAM,MAAyB;EAAE;EAAM;CAAS;CAChD,IAAI,OAAO,GAAG,KAAK,OAAO;MACrB,KAAK,KAAK,GAAG;CAClB,eAAe,aAAa,OAAO,GAAG,YAAY,IAAI;CACtD,OAAO;AACT;AAUA,SAAS,YAAY,SAAiB,OAAuB;CAC3D,MAAM,IAAI,IAAI,KAAK,OAAO,EAAE,QAAQ;CACpC,MAAM,IAAI,IAAI,KAAK,KAAK,EAAE,QAAQ;CAClC,IAAI,OAAO,MAAM,CAAC,KAAK,OAAO,MAAM,CAAC,GAAG,OAAO;CAC/C,OAAO,KAAK,OAAO,IAAI,KAAK,KAAU;AACxC;;AAGA,eAAsB,gBACpB,SACA,UACA,uBAAc,IAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,GAC/B;CACnB,MAAM,UAAoB,CAAC;CAC3B,KAAK,MAAM,QAAQ,kBAAkB,OAAO,GAAG;EAC7C,MAAM,QAAQ,MAAM,cAAc,SAAS,IAAI,EAAE,YAAY,IAAI;EACjE,IAAI,CAAC,OAAO;EAEZ,IAAI,SAAS,SAAS,MAAM,uBAAuB,SAAS,OAAO;EACnE,IACE,SAAS,iBAAiB,KAAA,KAC1B,EAAE,OAAO,MAAM,eAAe,YAAY,MAAM,cAAc,SAAS,eAEvE;EAEF,IAAI,SAAS,QAAQ,SAAS,KAAK,SAAS,GAAG;GAC7C,MAAM,OAAO,MAAM,QAAQ,CAAC;GAC5B,IAAI,CAAC,SAAS,KAAK,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,GAAG;EACrD;EACA,IAAI,SAAS,cAAc,KAAA,GAAW;GAEpC,MAAM,KAAK,MAAM;GACjB,IAAI,CAAC,MAAM,YAAY,IAAI,GAAG,IAAI,SAAS,WAAW;EACxD;EACA,QAAQ,KAAK,IAAI;CACnB;CACA,OAAO;AACT"}
@@ -0,0 +1,2 @@
1
+ import { r as runSequenceCycle } from "./sequence-engine-DNTVLq7o.js";
2
+ export { runSequenceCycle };
@@ -1,4 +1,5 @@
1
- import { d as getTemplate, i as updateEnrollment, l as interpolate, r as readEnrollments, s as buildVariablesFromCustomer, t as getSequence } from "./sequence-store-DaaWr0Os.js";
1
+ import { n as logger } from "./logger-Dyl4VcLO.js";
2
+ import { d as getTemplate, i as updateEnrollment, l as interpolate, r as readEnrollments, s as buildVariablesFromCustomer, t as getSequence } from "./sequence-store-CmYb6s0g.js";
2
3
  import path from "path";
3
4
  import fs from "fs";
4
5
  //#region src/core/sequence-engine.ts
@@ -14,7 +15,7 @@ function addDays(isoDateStr, n) {
14
15
  async function processSequenceStep(dataDir, enrollment, today) {
15
16
  const sequence = getSequence(dataDir, enrollment.sequenceId);
16
17
  if (!sequence) {
17
- process.stderr.write(`[sequences] Sequence not found: ${enrollment.sequenceId}\n`);
18
+ logger.warn("sequences", "sequence not found", { sequenceId: enrollment.sequenceId });
18
19
  return "no_step_due";
19
20
  }
20
21
  if (enrollment.currentStep >= sequence.steps.length) {
@@ -29,7 +30,7 @@ async function processSequenceStep(dataDir, enrollment, today) {
29
30
  }
30
31
  const template = getTemplate(dataDir, step.templateId);
31
32
  if (!template) {
32
- process.stderr.write(`[sequences] Template not found: ${step.templateId}, skipping step\n`);
33
+ logger.warn("sequences", "template not found, skipping step", { templateId: step.templateId });
33
34
  await updateEnrollment(dataDir, enrollment.id, {
34
35
  currentStep: enrollment.currentStep + 1,
35
36
  lastSentAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -52,11 +53,14 @@ async function processSequenceStep(dataDir, enrollment, today) {
52
53
  body,
53
54
  isHtml: false
54
55
  });
55
- process.stderr.write(`[sequences] Sent step ${enrollment.currentStep} to ${enrollment.contactEmail}\n`);
56
+ logger.info("sequences", "sent step", {
57
+ step: enrollment.currentStep,
58
+ to: enrollment.contactEmail
59
+ });
56
60
  } catch (err) {
57
- process.stderr.write(`[sequences] Send failed: ${err.message}\n`);
61
+ logger.error("sequences", "send failed", { error: err.message });
58
62
  }
59
- else process.stderr.write(`[sequences] Gmail not configured, advancing step for ${enrollment.contactEmail}\n`);
63
+ else logger.warn("sequences", "gmail not configured, advancing step", { to: enrollment.contactEmail });
60
64
  await updateEnrollment(dataDir, enrollment.id, {
61
65
  currentStep: enrollment.currentStep + 1,
62
66
  lastSentAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -76,7 +80,10 @@ async function runSequenceCycle(dataDir, today) {
76
80
  } catch (err) {
77
81
  const msg = `${enrollment.id}: ${err.message}`;
78
82
  errors.push(msg);
79
- process.stderr.write(`[sequences] Error processing ${enrollment.id}: ${err.message}\n`);
83
+ logger.error("sequences", "error processing enrollment", {
84
+ enrollment: enrollment.id,
85
+ error: err.message
86
+ });
80
87
  }
81
88
  return {
82
89
  processed: active.length,
@@ -88,4 +95,4 @@ async function runSequenceCycle(dataDir, today) {
88
95
  //#endregion
89
96
  export { processSequenceStep as n, runSequenceCycle as r, addDays as t };
90
97
 
91
- //# sourceMappingURL=sequence-engine-J1lTW_in.js.map
98
+ //# sourceMappingURL=sequence-engine-DNTVLq7o.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence-engine-DNTVLq7o.js","names":[],"sources":["../src/core/sequence-engine.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { getSequence, readEnrollments, updateEnrollment } from \"../fs/sequence-store.js\";\nimport { getTemplate } from \"../fs/template-store.js\";\nimport { interpolate, buildVariablesFromCustomer } from \"./template-engine.js\";\nimport { logger } from \"./logger.js\";\nimport type { SequenceEnrollment } from \"../schemas/sequence.js\";\n\n/**\n * Add n days to an ISO date string (YYYY-MM-DD) and return YYYY-MM-DD.\n */\nexport function addDays(isoDateStr: string, n: number): string {\n // Parse the date parts to avoid timezone issues\n const [year, month, day] = isoDateStr.split(\"-\").map(Number) as [number, number, number];\n const date = new Date(Date.UTC(year, month - 1, day));\n date.setUTCDate(date.getUTCDate() + n);\n return date.toISOString().slice(0, 10);\n}\n\nexport async function processSequenceStep(\n dataDir: string,\n enrollment: SequenceEnrollment,\n today: string // YYYY-MM-DD\n): Promise<\"sent\" | \"skipped_replied\" | \"completed\" | \"no_step_due\"> {\n const sequence = getSequence(dataDir, enrollment.sequenceId);\n if (!sequence) {\n logger.warn(\"sequences\", \"sequence not found\", { sequenceId: enrollment.sequenceId });\n return \"no_step_due\";\n }\n\n // All steps completed\n if (enrollment.currentStep >= sequence.steps.length) {\n await updateEnrollment(dataDir, enrollment.id, { status: \"completed\" });\n return \"completed\";\n }\n\n const step = sequence.steps[enrollment.currentStep]!;\n\n // Calculate due date: enrolledAt date + step.day\n const enrolledDate = enrollment.enrolledAt.slice(0, 10); // YYYY-MM-DD\n const dueDate = addDays(enrolledDate, step.day);\n\n // Not yet due\n if (today < dueDate) {\n return \"no_step_due\";\n }\n\n // Skip if replied and skipIfReplied is true\n if (step.skipIfReplied && enrollment.lastRepliedAt) {\n await updateEnrollment(dataDir, enrollment.id, {\n currentStep: enrollment.currentStep + 1,\n });\n return \"skipped_replied\";\n }\n\n // Load template\n const template = getTemplate(dataDir, step.templateId);\n if (!template) {\n logger.warn(\"sequences\", \"template not found, skipping step\", { templateId: step.templateId });\n await updateEnrollment(dataDir, enrollment.id, {\n currentStep: enrollment.currentStep + 1,\n lastSentAt: new Date().toISOString(),\n });\n return \"no_step_due\";\n }\n\n // Build variables\n const vars = await buildVariablesFromCustomer(dataDir, enrollment.slug);\n vars[\"contactEmail\"] = enrollment.contactEmail;\n\n // Interpolate\n const subject = interpolate(template.subject, vars);\n const body = interpolate(template.body, vars);\n\n // Try Gmail send if credentials available\n const tokenPath = path.join(dataDir, \".agentic\", \"gmail-token.json\");\n const credPath = path.join(dataDir, \".agentic\", \"gmail-credentials.json\");\n\n if (fs.existsSync(tokenPath) && fs.existsSync(credPath)) {\n try {\n const { getGmailAuth } = await import(\"../sync/gmail-auth.js\");\n const { sendEmail } = await import(\"../sync/gmail-sender.js\");\n const auth = await getGmailAuth(credPath, tokenPath);\n await sendEmail({\n auth,\n to: enrollment.contactEmail,\n subject,\n body,\n isHtml: false,\n });\n logger.info(\"sequences\", \"sent step\", {\n step: enrollment.currentStep,\n to: enrollment.contactEmail,\n });\n } catch (err) {\n logger.error(\"sequences\", \"send failed\", { error: (err as Error).message });\n }\n } else {\n logger.warn(\"sequences\", \"gmail not configured, advancing step\", {\n to: enrollment.contactEmail,\n });\n }\n\n // Update enrollment\n await updateEnrollment(dataDir, enrollment.id, {\n currentStep: enrollment.currentStep + 1,\n lastSentAt: new Date().toISOString(),\n stepsCompleted: [...enrollment.stepsCompleted, enrollment.currentStep],\n });\n\n return \"sent\";\n}\n\nexport async function runSequenceCycle(\n dataDir: string,\n today: string\n): Promise<{ processed: number; sent: number; completed: number; errors: string[] }> {\n const enrollments = readEnrollments(dataDir);\n const active = enrollments.filter((e) => e.status === \"active\");\n\n let sent = 0;\n let completed = 0;\n const errors: string[] = [];\n\n for (const enrollment of active) {\n try {\n const result = await processSequenceStep(dataDir, enrollment, today);\n if (result === \"sent\") sent++;\n if (result === \"completed\") completed++;\n } catch (err) {\n const msg = `${enrollment.id}: ${(err as Error).message}`;\n errors.push(msg);\n logger.error(\"sequences\", \"error processing enrollment\", {\n enrollment: enrollment.id,\n error: (err as Error).message,\n });\n }\n }\n\n return { processed: active.length, sent, completed, errors };\n}\n"],"mappings":"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"}