@datasynx/agentic-crm 0.1.0 → 1.0.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.
- package/README.md +264 -670
- package/dist/{approvals-DpjxGHFp.js → approvals-CmDT2eUg.js} +7 -24
- package/dist/approvals-CmDT2eUg.js.map +1 -0
- package/dist/{ask-CID3jnuL.js → ask-D8iYqDAr.js} +6 -6
- package/dist/{ask-CID3jnuL.js.map → ask-D8iYqDAr.js.map} +1 -1
- package/dist/atomic-write-8yjqqLtS.js +29 -0
- package/dist/atomic-write-8yjqqLtS.js.map +1 -0
- package/dist/atomic-write-BYmF-ThH.cjs +37 -0
- package/dist/atomic-write-BYmF-ThH.cjs.map +1 -0
- package/dist/auth-B5DcjJ_6.js +2 -0
- package/dist/{auth-DFWwWcYD.js → auth-DDXZTwS0.js} +4 -13
- package/dist/auth-DDXZTwS0.js.map +1 -0
- package/dist/{autofill-Di_-SP7t.js → autofill-B9VtlR2j.js} +2 -2
- package/dist/{autofill-Di_-SP7t.js.map → autofill-B9VtlR2j.js.map} +1 -1
- package/dist/{backup-CeMk9z86.js → backup-CTlIxUdO.js} +5 -7
- package/dist/backup-CTlIxUdO.js.map +1 -0
- package/dist/backup-LFnC09oV.js +2 -0
- package/dist/{churn-C28IgnAj.js → churn-DN9WDGNM.js} +3 -3
- package/dist/{churn-C28IgnAj.js.map → churn-DN9WDGNM.js.map} +1 -1
- package/dist/cli.js +282 -184
- package/dist/cli.js.map +1 -1
- package/dist/{compliance-CKSBoQUe.js → compliance-Bc12Hn9a.js} +3 -3
- package/dist/{compliance-CKSBoQUe.js.map → compliance-Bc12Hn9a.js.map} +1 -1
- package/dist/{compliance-CujOqAKk.js → compliance-TqYQXhBj.js} +1 -1
- package/dist/{compliance-B1kk5-YS.js → compliance-kq0xHRw3.js} +3 -3
- package/dist/{compliance-B1kk5-YS.js.map → compliance-kq0xHRw3.js.map} +1 -1
- package/dist/{compliance-B91zNvCR.cjs → compliance-pAj9FcGI.cjs} +3 -3
- package/dist/{compliance-B91zNvCR.cjs.map → compliance-pAj9FcGI.cjs.map} +1 -1
- package/dist/{context-builder-BzWAp3Zs.js → context-builder-7Uab5-G4.js} +3 -2
- package/dist/context-builder-7Uab5-G4.js.map +1 -0
- package/dist/context-builder-hmOPvgso.js +2 -0
- package/dist/{custom-fields-CzNeD3_v.js → custom-fields-BMyz5Ruh.js} +1 -1
- package/dist/{custom-fields-Pl2t9xzp.js → custom-fields-GzpOHW_2.js} +4 -13
- package/dist/custom-fields-GzpOHW_2.js.map +1 -0
- package/dist/{custom-objects-CIFrmQ2V.js → custom-objects-BNy-ayR-.js} +1 -1
- package/dist/{custom-objects-BHgn1GEX.js → custom-objects-CxW1gHwJ.js} +10 -25
- package/dist/custom-objects-CxW1gHwJ.js.map +1 -0
- package/dist/{customer-dir-DIylZ8Q6.js → customer-dir-CkMMXhb0.js} +9 -4
- package/dist/customer-dir-CkMMXhb0.js.map +1 -0
- package/dist/daemon/worker.js +66 -40
- package/dist/daemon/worker.js.map +1 -1
- package/dist/doctor-C14-vnJ1.js +103 -0
- package/dist/doctor-C14-vnJ1.js.map +1 -0
- package/dist/{enrichment-3XvgGDfB.js → enrichment-CDFdWmvD.js} +3 -3
- package/dist/{enrichment-3XvgGDfB.js.map → enrichment-CDFdWmvD.js.map} +1 -1
- package/dist/{file-lock-B_zi7NQl.js → file-lock-CcHotQkZ.js} +3 -4
- package/dist/file-lock-CcHotQkZ.js.map +1 -0
- package/dist/{gmail-sync-rQaVqKWd.js → gmail-sync-C-NmibzS.js} +13 -9
- package/dist/gmail-sync-C-NmibzS.js.map +1 -0
- package/dist/{gmail-sync-DIaxInDT.js → gmail-sync-DueE6tl5.js} +14 -10
- package/dist/gmail-sync-DueE6tl5.js.map +1 -0
- package/dist/{gmail-sync-hHm9gaWd.cjs → gmail-sync-GEy3oVvw.cjs} +13 -9
- package/dist/gmail-sync-GEy3oVvw.cjs.map +1 -0
- package/dist/{gmail-webhook-handler-DS7OlRPX.js → gmail-webhook-handler-B26COilD.js} +2 -2
- package/dist/{gmail-webhook-handler-e5Od25FX.js → gmail-webhook-handler-kGKpbY9h.js} +4 -4
- package/dist/{gmail-webhook-handler-e5Od25FX.js.map → gmail-webhook-handler-kGKpbY9h.js.map} +1 -1
- package/dist/{goal-engine-KpBftn4V.js → goal-engine-BbroPhqm.js} +10 -11
- package/dist/goal-engine-BbroPhqm.js.map +1 -0
- package/dist/{goal-engine-CUZSpERI.js → goal-engine-CfDAJTFt.js} +1 -1
- package/dist/{google-drive-sync-DEPcqFca.js → google-drive-sync-D1n7WKZn.js} +3 -3
- package/dist/{google-drive-sync-DEPcqFca.js.map → google-drive-sync-D1n7WKZn.js.map} +1 -1
- package/dist/{hygiene-DZqfYpFf.js → hygiene-DzQPnc6P.js} +3 -3
- package/dist/{hygiene-DZqfYpFf.js.map → hygiene-DzQPnc6P.js.map} +1 -1
- package/dist/identity-CB7j-Zr1.js +2 -0
- package/dist/{identity-CI6olMNm.js → identity-_uZ3Lbr2.js} +2 -2
- package/dist/{identity-CI6olMNm.js.map → identity-_uZ3Lbr2.js.map} +1 -1
- package/dist/{import-hubspot-BaK71U_K.js → import-hubspot-DB4n89jy.js} +51 -45
- package/dist/import-hubspot-DB4n89jy.js.map +1 -0
- package/dist/{index-V8BFaH-b.d.ts → index-B0IMMrp_.d.ts} +8 -4
- package/dist/{index-V8BFaH-b.d.ts.map → index-B0IMMrp_.d.ts.map} +1 -1
- package/dist/{index-YqwMd6aQ.d.cts → index-pY7tYXwH.d.cts} +8 -4
- package/dist/{index-YqwMd6aQ.d.cts.map → index-pY7tYXwH.d.cts.map} +1 -1
- package/dist/index.cjs +19 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -21
- package/dist/index.js.map +1 -1
- package/dist/{interactions-writer-DO3KcSR3.js → interactions-writer-BZzUIgJd.js} +5 -2
- package/dist/interactions-writer-BZzUIgJd.js.map +1 -0
- package/dist/{interactions-writer-SLHnoEeE.js → interactions-writer-DbSyI2rt.js} +32 -3
- package/dist/interactions-writer-DbSyI2rt.js.map +1 -0
- package/dist/interactions-writer-RJB8SWf2.js +2 -0
- package/dist/{interactions-writer-CrPStUll.cjs → interactions-writer-a2yzBd7T.cjs} +5 -2
- package/dist/interactions-writer-a2yzBd7T.cjs.map +1 -0
- package/dist/json-store-WWsFzXub.js +43 -0
- package/dist/json-store-WWsFzXub.js.map +1 -0
- package/dist/{knowledge-base-D0Fh40kc.js → knowledge-base-DHNc4hVj.js} +43 -16
- package/dist/knowledge-base-DHNc4hVj.js.map +1 -0
- package/dist/{lancedb-CCBbpulq.js → lancedb-CswQEE5K.js} +1 -1
- package/dist/{lancedb-rlvWoPwl.js → lancedb-CuHKNsNZ.js} +4 -3
- package/dist/lancedb-CuHKNsNZ.js.map +1 -0
- package/dist/{lead-model-BCFzyktm.js → lead-model-CEmx7te7.js} +6 -14
- package/dist/lead-model-CEmx7te7.js.map +1 -0
- package/dist/{llm-Z8RIYkpF.js → llm-BnSUBisu.js} +2 -2
- package/dist/{llm-Z8RIYkpF.js.map → llm-BnSUBisu.js.map} +1 -1
- package/dist/{llm-iijeXmgq.cjs → llm-CXycmEl9.cjs} +2 -2
- package/dist/{llm-iijeXmgq.cjs.map → llm-CXycmEl9.cjs.map} +1 -1
- package/dist/{llm-DEjWcqmW.js → llm-DSX1-wFu.js} +1 -1
- package/dist/{llm-DvzZqva0.js → llm-PZzgPphl.js} +3 -3
- package/dist/{llm-DvzZqva0.js.map → llm-PZzgPphl.js.map} +1 -1
- package/dist/logger-BkInaGoV.cjs +167 -0
- package/dist/logger-BkInaGoV.cjs.map +1 -0
- package/dist/logger-Dyl4VcLO.js +147 -0
- package/dist/logger-Dyl4VcLO.js.map +1 -0
- package/dist/logger-UaF5p9d1.js +147 -0
- package/dist/logger-UaF5p9d1.js.map +1 -0
- package/dist/logger-vKQS34w9.js +2 -0
- package/dist/mcp-CdTJWTJf.d.cts.map +1 -1
- package/dist/mcp-CdTJWTJf.d.ts.map +1 -1
- package/dist/mcp.cjs +327 -303
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +327 -303
- package/dist/mcp.js.map +1 -1
- package/dist/{memory-Cy6-Tbyl.js → memory-D8hmgD9d.js} +1 -1
- package/dist/{memory-Bb6ky3kb.js → memory-Dzr9dXSM.js} +4 -11
- package/dist/memory-Dzr9dXSM.js.map +1 -0
- package/dist/{microsoft-calendar-B6MMtUQK.js → microsoft-calendar-jIu9K5zX.js} +4 -4
- package/dist/{microsoft-calendar-B6MMtUQK.js.map → microsoft-calendar-jIu9K5zX.js.map} +1 -1
- package/dist/{microsoft-sync-CpZVoSuq.js → microsoft-sync-R_r8HL-B.js} +5 -5
- package/dist/{microsoft-sync-CpZVoSuq.js.map → microsoft-sync-R_r8HL-B.js.map} +1 -1
- package/dist/{nba-3wanmJ0U.js → nba-mTJ4yEqD.js} +3 -3
- package/dist/{nba-3wanmJ0U.js.map → nba-mTJ4yEqD.js.map} +1 -1
- package/dist/{notification-dispatcher-0vYNngWe.js → notification-dispatcher-inpKyuBz.js} +7 -3
- package/dist/notification-dispatcher-inpKyuBz.js.map +1 -0
- package/dist/{pipeline-writer-BqBrYrQc.js → pipeline-writer-0LJ6Qkat.js} +1 -1
- package/dist/{pipeline-writer-N2omexxp.cjs → pipeline-writer-B1tRAhuD.cjs} +11 -3
- package/dist/pipeline-writer-B1tRAhuD.cjs.map +1 -0
- package/dist/{pipeline-writer-BvVquKIe.js → pipeline-writer-CIllfnZl.js} +5 -3
- package/dist/pipeline-writer-CIllfnZl.js.map +1 -0
- package/dist/{pipeline-writer-eufx_0o1.js → pipeline-writer-rDj-ni6q.js} +6 -4
- package/dist/pipeline-writer-rDj-ni6q.js.map +1 -0
- package/dist/{proactive-agent-BgQXw3ac.js → proactive-agent-B7u3Bj_l.js} +6 -6
- package/dist/{proactive-agent-BgQXw3ac.js.map → proactive-agent-B7u3Bj_l.js.map} +1 -1
- package/dist/{proactive-worker-BrLHNhjH.js → proactive-worker-1zkm6aJD.js} +7 -8
- package/dist/proactive-worker-1zkm6aJD.js.map +1 -0
- package/dist/{push-manager-CowY-0IK.js → push-manager-BXM-IHfP.js} +1 -1
- package/dist/{push-manager-CdqIIkuh.js → push-manager-C0ECQgva.js} +4 -4
- package/dist/push-manager-C0ECQgva.js.map +1 -0
- package/dist/{quote-generator-OhSFsi3x.js → quote-generator-ByUyIYtw.js} +1 -1
- package/dist/{quote-generator-BfwENXzg.js → quote-generator-CTdR8eEI.js} +5 -5
- package/dist/quote-generator-CTdR8eEI.js.map +1 -0
- package/dist/rbac-DzbyFhVH.js +2 -0
- package/dist/{rbac-CTIktZaC.js → rbac-msmBc_tK.js} +19 -12
- package/dist/rbac-msmBc_tK.js.map +1 -0
- package/dist/regex-Jt5DatPi.js +13 -0
- package/dist/regex-Jt5DatPi.js.map +1 -0
- package/dist/{relationship-health-odxEoQdJ.js → relationship-health-ZZNXR1RZ.js} +8 -16
- package/dist/relationship-health-ZZNXR1RZ.js.map +1 -0
- package/dist/{revenue-simulation-Bqf2DLVB.js → revenue-simulation-D8f_YkUY.js} +9 -19
- package/dist/revenue-simulation-D8f_YkUY.js.map +1 -0
- package/dist/{revenue-simulation-BJdRTEHc.js → revenue-simulation-njJZlTqm.js} +1 -1
- package/dist/safe-path-mpp0dKtO.js +18 -0
- package/dist/safe-path-mpp0dKtO.js.map +1 -0
- package/dist/{segments-BqcD5HIl.js → segments-DI3LOQNe.js} +5 -14
- package/dist/segments-DI3LOQNe.js.map +1 -0
- package/dist/sequence-engine-C6nnewHX.js +2 -0
- package/dist/{sequence-engine-J1lTW_in.js → sequence-engine-DNTVLq7o.js} +15 -8
- package/dist/sequence-engine-DNTVLq7o.js.map +1 -0
- package/dist/{sequence-store-DaaWr0Os.js → sequence-store-CmYb6s0g.js} +6 -5
- package/dist/sequence-store-CmYb6s0g.js.map +1 -0
- package/dist/{server-Dyva03K8.js → server-DqSMYhSA.js} +278 -220
- package/dist/server-DqSMYhSA.js.map +1 -0
- package/dist/{session-D9ub6Wl1.js → session-B6XaP83h.js} +3 -3
- package/dist/session-B6XaP83h.js.map +1 -0
- package/dist/{session-B9AilxOE.js → session-BgGDyP2C.js} +3 -3
- package/dist/session-BgGDyP2C.js.map +1 -0
- package/dist/session-Bp4zTh4l.js +2 -0
- package/dist/{session-D0qFkBla.cjs → session-Mm7GQbSH.cjs} +3 -3
- package/dist/session-Mm7GQbSH.cjs.map +1 -0
- package/dist/{session-store-C8tEvMPw.js → session-store-DWxJ5Pof.js} +79 -17
- package/dist/session-store-DWxJ5Pof.js.map +1 -0
- package/dist/{session-store-B0QZE8Bx.cjs → session-store-yfwnj0OC.cjs} +126 -16
- package/dist/session-store-yfwnj0OC.cjs.map +1 -0
- package/dist/{sla-engine-5IhTsBUR.js → sla-engine-CP2KiKDS.js} +1 -1
- package/dist/{sla-engine-BqX-7u-7.js → sla-engine-O-A1ntu_.js} +2 -2
- package/dist/{sla-engine-BqX-7u-7.js.map → sla-engine-O-A1ntu_.js.map} +1 -1
- package/dist/{sop-Vp0UPWFW.js → sop-BV7ICAFR.js} +4 -11
- package/dist/sop-BV7ICAFR.js.map +1 -0
- package/dist/{sop-DkhVChGy.js → sop-D33qTHUb.js} +1 -1
- package/dist/survey-engine-DKctGcLQ.js +2 -0
- package/dist/{survey-engine-DBjCYqCv.js → survey-engine-DngXBv47.js} +5 -4
- package/dist/survey-engine-DngXBv47.js.map +1 -0
- package/dist/{sync-state-CwLSt_1m.js → sync-state-BaA8LbTI.js} +1 -1
- package/dist/{sync-state-ChaLbamC.js → sync-state-DMZgzpez.js} +4 -12
- package/dist/sync-state-DMZgzpez.js.map +1 -0
- package/dist/{ticket-writer-CjqKeIRD.js → ticket-writer-DsfpeLGZ.js} +1 -1
- package/dist/{ticket-writer-j2oX_Wal.js → ticket-writer-a9on36Wb.js} +12 -24
- package/dist/ticket-writer-a9on36Wb.js.map +1 -0
- package/dist/{tone-Bdm5uaht.js → tone-C7bqK69y.js} +5 -12
- package/dist/tone-C7bqK69y.js.map +1 -0
- package/dist/{tone-DRKlZgPr.cjs → tone-Cmc7O2Fx.cjs} +3 -9
- package/dist/tone-Cmc7O2Fx.cjs.map +1 -0
- package/dist/{tone-vNb2DAAD.js → tone-mXSftvTn.js} +3 -8
- package/dist/tone-mXSftvTn.js.map +1 -0
- package/dist/{transcript-watcher-CL2QUygI.js → transcript-watcher-0mh2ZhmH.js} +18 -11
- package/dist/transcript-watcher-0mh2ZhmH.js.map +1 -0
- package/dist/unmatched-transcripts-C92zAoM4.js +2 -0
- package/dist/unmatched-transcripts-DC-VQ9YS.js +16 -0
- package/dist/unmatched-transcripts-DC-VQ9YS.js.map +1 -0
- package/dist/update-deal-CWy1eLJI.js +2 -0
- package/dist/{update-deal-DKC79skb.js → update-deal-DSzr_Aau.js} +3 -3
- package/dist/{update-deal-DKC79skb.js.map → update-deal-DSzr_Aau.js.map} +1 -1
- package/dist/{usage-D0-TYJkw.js → usage-BVlFlKW_.js} +8 -6
- package/dist/usage-BVlFlKW_.js.map +1 -0
- package/dist/usage-CClTf5e6.cjs.map +1 -1
- package/dist/usage-D0u9a-lV.js.map +1 -1
- package/dist/{vault-DXCg29W-.js → vault-CfwZdNzC.js} +3 -4
- package/dist/vault-CfwZdNzC.js.map +1 -0
- package/dist/{vault-C1D3zScD.js → vault-DxKP4_R2.js} +1 -1
- package/dist/{webhooks-Xn6zO6kd.cjs → webhooks-CwW-3kvG.cjs} +5 -19
- package/dist/webhooks-CwW-3kvG.cjs.map +1 -0
- package/dist/{webhooks-7EpA05Qr.js → webhooks-DXr1IoKn.js} +8 -21
- package/dist/webhooks-DXr1IoKn.js.map +1 -0
- package/dist/{webhooks-BO2UAnmn.js → webhooks-sWZ8CJtR.js} +5 -18
- package/dist/webhooks-sWZ8CJtR.js.map +1 -0
- package/package.json +11 -2
- package/dist/approvals-DpjxGHFp.js.map +0 -1
- package/dist/auth-CyFuu9X_.js +0 -2
- package/dist/auth-DFWwWcYD.js.map +0 -1
- package/dist/backup-CeMk9z86.js.map +0 -1
- package/dist/backup-f_hC7rBV.js +0 -2
- package/dist/context-builder-BzWAp3Zs.js.map +0 -1
- package/dist/context-builder-DlrRcqmJ.js +0 -2
- package/dist/custom-fields-Pl2t9xzp.js.map +0 -1
- package/dist/custom-objects-BHgn1GEX.js.map +0 -1
- package/dist/customer-dir-DIylZ8Q6.js.map +0 -1
- package/dist/file-lock-B_zi7NQl.js.map +0 -1
- package/dist/gmail-sync-DIaxInDT.js.map +0 -1
- package/dist/gmail-sync-hHm9gaWd.cjs.map +0 -1
- package/dist/gmail-sync-rQaVqKWd.js.map +0 -1
- package/dist/goal-engine-KpBftn4V.js.map +0 -1
- package/dist/identity-gyfWdrcX.js +0 -2
- package/dist/import-hubspot-BaK71U_K.js.map +0 -1
- package/dist/interactions-writer-CrPStUll.cjs.map +0 -1
- package/dist/interactions-writer-DO3KcSR3.js.map +0 -1
- package/dist/interactions-writer-SLHnoEeE.js.map +0 -1
- package/dist/interactions-writer-dSPy1XfO.js +0 -2
- package/dist/knowledge-base-D0Fh40kc.js.map +0 -1
- package/dist/lancedb-rlvWoPwl.js.map +0 -1
- package/dist/lead-model-BCFzyktm.js.map +0 -1
- package/dist/memory-Bb6ky3kb.js.map +0 -1
- package/dist/notification-dispatcher-0vYNngWe.js.map +0 -1
- package/dist/pipeline-writer-BvVquKIe.js.map +0 -1
- package/dist/pipeline-writer-N2omexxp.cjs.map +0 -1
- package/dist/pipeline-writer-eufx_0o1.js.map +0 -1
- package/dist/proactive-worker-BrLHNhjH.js.map +0 -1
- package/dist/push-manager-CdqIIkuh.js.map +0 -1
- package/dist/quote-generator-BfwENXzg.js.map +0 -1
- package/dist/rbac-C7c8tcES.js +0 -2
- package/dist/rbac-CTIktZaC.js.map +0 -1
- package/dist/relationship-health-odxEoQdJ.js.map +0 -1
- package/dist/revenue-simulation-Bqf2DLVB.js.map +0 -1
- package/dist/segments-BqcD5HIl.js.map +0 -1
- package/dist/sequence-engine-CCTHEBgi.js +0 -2
- package/dist/sequence-engine-J1lTW_in.js.map +0 -1
- package/dist/sequence-store-DaaWr0Os.js.map +0 -1
- package/dist/server-Dyva03K8.js.map +0 -1
- package/dist/session-B9AilxOE.js.map +0 -1
- package/dist/session-D0qFkBla.cjs.map +0 -1
- package/dist/session-D9ub6Wl1.js.map +0 -1
- package/dist/session-mWHA71Lw.js +0 -2
- package/dist/session-store-B0QZE8Bx.cjs.map +0 -1
- package/dist/session-store-C8tEvMPw.js.map +0 -1
- package/dist/sop-Vp0UPWFW.js.map +0 -1
- package/dist/survey-engine-C06hcQt3.js +0 -2
- package/dist/survey-engine-DBjCYqCv.js.map +0 -1
- package/dist/sync-state-ChaLbamC.js.map +0 -1
- package/dist/ticket-writer-j2oX_Wal.js.map +0 -1
- package/dist/tone-Bdm5uaht.js.map +0 -1
- package/dist/tone-DRKlZgPr.cjs.map +0 -1
- package/dist/tone-vNb2DAAD.js.map +0 -1
- package/dist/transcript-watcher-CL2QUygI.js.map +0 -1
- package/dist/unmatched-transcripts-BsH5bhkU.js +0 -26
- package/dist/unmatched-transcripts-BsH5bhkU.js.map +0 -1
- package/dist/unmatched-transcripts-D0PrJ9iz.js +0 -2
- package/dist/update-deal-BNwPGaTV.js +0 -2
- package/dist/usage-D0-TYJkw.js.map +0 -1
- package/dist/vault-DXCg29W-.js.map +0 -1
- package/dist/webhooks-7EpA05Qr.js.map +0 -1
- package/dist/webhooks-BO2UAnmn.js.map +0 -1
- package/dist/webhooks-Xn6zO6kd.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as llmProvider, c as neutralizeUntrusted, l as maskPii, o as localLlmConfig, s as guardrailsEnabled, u as piiMaskingEnabled } from "./compliance-
|
|
1
|
+
import { a as llmProvider, c as neutralizeUntrusted, l as maskPii, o as localLlmConfig, s as guardrailsEnabled, u as piiMaskingEnabled } from "./compliance-Bc12Hn9a.js";
|
|
2
2
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
//#region src/core/resilience.ts
|
|
4
4
|
var CircuitBreaker = class {
|
|
@@ -146,7 +146,7 @@ async function recognizeCustomer(transcriptContent, candidates) {
|
|
|
146
146
|
}
|
|
147
147
|
function recordCall(model, inputTokens, outputTokens, ctx) {
|
|
148
148
|
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
149
|
-
import("./usage-
|
|
149
|
+
import("./usage-BVlFlKW_.js").then(({ recordUsage }) => recordUsage(dataDir, {
|
|
150
150
|
...ctx?.slug ? { slug: ctx.slug } : {},
|
|
151
151
|
...ctx?.tool ? { tool: ctx.tool } : {},
|
|
152
152
|
model,
|
|
@@ -369,4 +369,4 @@ Rules:
|
|
|
369
369
|
//#endregion
|
|
370
370
|
export { summarizeEmail as a, recognizeCustomer as i, mapCsvFields as n, guardIsoDate as o, mapCsvFieldsHeuristic as r, callLlm as t };
|
|
371
371
|
|
|
372
|
-
//# sourceMappingURL=llm-
|
|
372
|
+
//# sourceMappingURL=llm-PZzgPphl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-DvzZqva0.js","names":[],"sources":["../src/core/resilience.ts","../src/core/input-guard.ts","../src/core/llm.ts"],"sourcesContent":["export interface RetryOptions {\n attempts: number;\n backoffMs: number;\n maxBackoffMs?: number;\n shouldRetry?: (err: Error) => boolean;\n}\n\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const { attempts, backoffMs, maxBackoffMs, shouldRetry } = opts;\n let lastError!: Error;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err as Error;\n if (shouldRetry && !shouldRetry(lastError)) throw lastError;\n if (attempt < attempts - 1) {\n const delay = maxBackoffMs\n ? Math.min(backoffMs * 2 ** attempt, maxBackoffMs)\n : backoffMs * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n throw lastError;\n}\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerOptions {\n threshold: number;\n timeoutMs: number;\n halfOpenAfter: number;\n}\n\nexport class CircuitBreaker {\n private failures = 0;\n private openedAt: number | null = null;\n private _state: CircuitState = \"closed\";\n\n constructor(private readonly opts: CircuitBreakerOptions) {}\n\n get state(): CircuitState {\n return this._state;\n }\n\n async call<T>(fn: () => Promise<T>): Promise<T> {\n if (this._state === \"open\") {\n const elapsed = Date.now() - (this.openedAt ?? 0);\n if (elapsed >= this.opts.halfOpenAfter) {\n this._state = \"half-open\";\n } else {\n throw new Error(\"Circuit open\");\n }\n }\n\n try {\n const result = await fn();\n // Success — reset\n this.failures = 0;\n this._state = \"closed\";\n this.openedAt = null;\n return result;\n } catch (err) {\n this.failures++;\n if (this._state === \"half-open\" || this.failures >= this.opts.threshold) {\n this._state = \"open\";\n this.openedAt = Date.now();\n this.failures = 0;\n }\n throw err;\n }\n }\n}\n","export interface StringGuardOptions {\n maxLen?: number;\n pattern?: RegExp;\n trim?: boolean;\n}\n\nexport function guardString(val: unknown, field: string, opts: StringGuardOptions = {}): string {\n if (typeof val !== \"string\") throw new Error(`${field}: expected string, got ${typeof val}`);\n const trimmed = opts.trim !== false ? val.trim() : val;\n if (opts.maxLen !== undefined && trimmed.length > opts.maxLen) {\n throw new Error(`${field}: exceeds max length ${opts.maxLen}`);\n }\n if (opts.pattern && !opts.pattern.test(trimmed)) {\n throw new Error(`${field}: invalid format`);\n }\n return trimmed;\n}\n\nexport interface NumberGuardOptions {\n min?: number;\n max?: number;\n}\n\nexport function guardNumber(val: unknown, field: string, opts: NumberGuardOptions = {}): number {\n if (typeof val !== \"number\" || !isFinite(val)) {\n throw new Error(\n `${field}: expected number, got ${typeof val === \"number\" ? \"NaN/Infinity\" : typeof val}`\n );\n }\n if (opts.min !== undefined && val < opts.min) {\n throw new Error(`${field}: must be >= ${opts.min}`);\n }\n if (opts.max !== undefined && val > opts.max) {\n throw new Error(`${field}: must be <= ${opts.max}`);\n }\n return val;\n}\n\nexport function guardPositiveInt(val: unknown, field: string): number {\n const n = guardNumber(val, field);\n if (!Number.isInteger(n)) throw new Error(`${field}: must be integer`);\n if (n < 1) throw new Error(`${field}: must be >= 1`);\n return n;\n}\n\nexport function guardIsoDate(val: unknown, field: string): string {\n if (typeof val !== \"string\" || !val) throw new Error(`${field}: invalid date`);\n const d = new Date(val);\n if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);\n // Reject clearly invalid month/day combinations (e.g., 2026-13-01)\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) {\n const [year, month, day] = val.slice(0, 10).split(\"-\").map(Number) as [number, number, number];\n if (month < 1 || month > 12 || day < 1 || day > 31) {\n throw new Error(`${field}: invalid date`);\n }\n // Cross-check with Date parsing\n const reparse = new Date(\n `${year}-${String(month).padStart(2, \"0\")}-${String(day).padStart(2, \"0\")}`\n );\n if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) {\n throw new Error(`${field}: invalid date`);\n }\n }\n return val;\n}\n\nconst DEFAULT_LLM_MAX_BYTES = 512 * 1024; // 512 KB\n\nexport function guardLlmResponse(\n response: unknown,\n maxBytes: number = DEFAULT_LLM_MAX_BYTES\n): string {\n if (typeof response !== \"string\") {\n throw new Error(\"LLM response: expected string\");\n }\n const byteLen = Buffer.byteLength(response, \"utf-8\");\n if (byteLen > maxBytes) {\n throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);\n }\n return response;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { CircuitBreaker } from \"./resilience.js\";\nimport { guardLlmResponse } from \"./input-guard.js\";\nimport { maskPii, piiMaskingEnabled } from \"./pii.js\";\nimport { neutralizeUntrusted, guardrailsEnabled } from \"./guardrails.js\";\nimport { llmProvider, localLlmConfig } from \"./compliance.js\";\n\nconst MODEL = \"claude-haiku-4-5-20251001\";\n\nlet _client: Anthropic | null = null;\nlet llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n\nexport function resetLlmCircuit(): void {\n llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n}\n\nfunction getClient(): Anthropic | null {\n if (!process.env[\"ANTHROPIC_API_KEY\"]) return null;\n if (!_client) _client = new Anthropic();\n return _client;\n}\n\nexport interface EmailSummary {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n}\n\nexport interface CustomerMatch {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\nfunction emailFallback(snippet: string): EmailSummary {\n return {\n summary: snippet.slice(0, 300),\n sentiment: \"neutral\",\n nextSteps: [],\n };\n}\n\nexport async function summarizeEmail(\n subject: string,\n snippet: string,\n from: string\n): Promise<EmailSummary> {\n const client = getClient();\n if (!client) return emailFallback(snippet);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 200,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Extract structured information from email metadata.\\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Subject: ${subject}\\nFrom: ${from}\\nContent: ${snippet}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return emailFallback(snippet);\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n };\n return parsed;\n } catch {\n return emailFallback(snippet);\n }\n } catch {\n return emailFallback(snippet);\n }\n}\n\nexport async function recognizeCustomer(\n transcriptContent: string,\n candidates: Array<{ slug: string; name: string }>\n): Promise<CustomerMatch> {\n if (candidates.length === 0) return { slug: null, confidence: \"low\" };\n\n const client = getClient();\n if (!client) return { slug: null, confidence: \"low\" };\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 100,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Match a meeting transcript to the most likely customer.\\nReturn ONLY valid JSON: { \"slug\": string|null, \"confidence\": \"high\"|\"medium\"|\"low\" }\\nslug must be one of the provided candidates or null if no match.',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Available customers: ${candidates.map((c) => `${c.slug} (${c.name})`).join(\", \")}\\nTranscript (first 1000 chars): ${transcriptContent.slice(0, 1000)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return { slug: null, confidence: \"low\" };\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n };\n return parsed;\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n}\n\nexport function resetLlmClient(): void {\n _client = null;\n}\n\nfunction recordCall(\n model: string,\n inputTokens: number,\n outputTokens: number,\n ctx?: { slug?: string; tool?: string }\n): void {\n const dataDir = process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n void import(\"./usage.js\").then(({ recordUsage }) =>\n recordUsage(dataDir, {\n ...(ctx?.slug ? { slug: ctx.slug } : {}),\n ...(ctx?.tool ? { tool: ctx.tool } : {}),\n model,\n inputTokens,\n outputTokens,\n })\n );\n}\n\n/**\n * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via\n * fetch — no extra dependency — so customer data can stay on-machine. Usage is\n * still recorded for cost/observability parity with the Anthropic path.\n */\nasync function callLocalLlm(\n masked: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const { baseUrl, model } = localLlmConfig();\n const res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n }),\n });\n if (!res.ok) throw new Error(`Local LLM error ${res.status}`);\n const data = (await res.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n };\n const text = data.choices?.[0]?.message?.content;\n if (!text) throw new Error(\"No text response from local LLM\");\n if (data.usage)\n recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);\n return text;\n}\n\nexport async function callLlm(\n prompt: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const provider = llmProvider();\n const client = provider === \"anthropic\" ? getClient() : null;\n if (provider === \"anthropic\" && !client) throw new Error(\"ANTHROPIC_API_KEY not set\");\n\n // Opt-in guardrails (neutralize prompt-injection) + PII masking, then restore.\n const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;\n const { masked, unmask } = piiMaskingEnabled()\n ? maskPii(guarded)\n : { masked: guarded, unmask: (t: string) => t };\n\n if (provider !== \"anthropic\") {\n return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));\n }\n\n return llmCircuit.call(async () => {\n const response = await client!.messages.create({\n model: MODEL,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n });\n\n // Token-cost observability (D3): record usage per customer/tool.\n const usage = response.usage;\n if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") throw new Error(\"No text response from LLM\");\n return unmask(guardLlmResponse(textBlock.text));\n });\n}\n\nexport type FieldMapping = Record<string, string | null>;\n\n// Alias table: CRM field name → list of CSV column patterns (lowercased substrings)\nconst FIELD_ALIASES: Record<string, string[]> = {\n name: [\n \"company name\",\n \"company\",\n \"organization\",\n \"organisation\",\n \"account name\",\n \"name\",\n \"firma\",\n ],\n email: [\"email address\", \"e-mail\", \"email\", \"e-mail address\", \"mail\"],\n domain: [\"company domain\", \"website\", \"domain\", \"url\", \"web\", \"homepage\"],\n phone: [\"phone number\", \"phone\", \"tel\", \"telephone\", \"mobile\", \"cell\"],\n industry: [\"industry\", \"sector\", \"branche\", \"vertical\"],\n primary_contact: [\"contact name\", \"contact person\", \"contact\", \"ansprechpartner\", \"kontakt\"],\n timezone: [\"timezone\", \"time zone\", \"tz\"],\n // Import-specific fields\n notes: [\n \"notes\",\n \"description\",\n \"body\",\n \"comment\",\n \"details\",\n \"note\",\n \"inhalt\",\n \"subject\",\n \"summary\",\n ],\n date: [\"activity date\", \"activity_date\", \"due date\", \"date\", \"created_at\", \"timestamp\", \"time\"],\n activityType: [\"activity type\", \"activity_type\", \"activitytype\", \"type\", \"category\", \"art\"],\n sourceId: [\n \"record id\",\n \"record_id\",\n \"source id\",\n \"source_id\",\n \"external id\",\n \"external_id\",\n \"activity id\",\n ],\n};\n\nexport function mapCsvFieldsHeuristic(headers: string[], targetFields: string[]): FieldMapping {\n const result: FieldMapping = {};\n const usedHeaders = new Set<string>();\n\n for (const field of targetFields) {\n const aliases = FIELD_ALIASES[field] ?? [field];\n let matched: string | null = null;\n\n for (const header of headers) {\n if (usedHeaders.has(header)) continue;\n const lower = header.toLowerCase();\n if (aliases.some((alias) => lower === alias || lower.includes(alias))) {\n matched = header;\n break;\n }\n }\n\n result[field] = matched;\n if (matched) usedHeaders.add(matched);\n }\n\n return result;\n}\n\nconst FIELD_SEMANTICS = `CRM field semantics:\n- name: Company or organization name (required)\n- email: Contact email address\n- domain: Company website or domain (e.g. \"acme.com\")\n- notes: Interaction notes, description, or subject text\n- date: Date of activity/interaction (ISO 8601 or YYYY-MM-DD)\n- activityType: Type of interaction — Call, Email, Meeting, Note\n- sourceId: Unique ID from the source system used for deduplication`;\n\nexport async function mapCsvFields(\n headers: string[],\n targetFields: string[]\n): Promise<FieldMapping> {\n const client = getClient();\n if (!client) return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 300,\n system: [\n {\n type: \"text\",\n text: `You are a CRM data-import assistant. Map CSV column headers to internal CRM field names.\n\n${FIELD_SEMANTICS}\n\nRules:\n1. Return ONLY valid JSON: { \"<crmField>\": \"<csvColumn>\" | null, ... }\n2. Every requested CRM field must appear as a key in the response.\n3. Use null when no column is a reasonable match.\n4. Each CSV column may only be assigned to one CRM field.\n5. Only use column names that appear exactly in the provided CSV columns list.`,\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `CSV columns: ${JSON.stringify(headers)}\\nMap to CRM fields: ${JSON.stringify(targetFields)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\")\n return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const raw = JSON.parse(\n textBlock.text\n .replace(/^```(?:json)?\\n?/, \"\")\n .replace(/\\n?```$/, \"\")\n .trim()\n ) as Record<string, string | null>;\n const validated: FieldMapping = {};\n const headerSet = new Set(headers);\n for (const field of targetFields) {\n const col = raw[field] ?? null;\n validated[field] = col !== null && headerSet.has(col) ? col : null;\n }\n // Require at least 'name' to be mapped; fall back otherwise\n if (!validated[\"name\"]) return mapCsvFieldsHeuristic(headers, targetFields);\n return validated;\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"llm-PZzgPphl.js","names":[],"sources":["../src/core/resilience.ts","../src/core/input-guard.ts","../src/core/llm.ts"],"sourcesContent":["export interface RetryOptions {\n attempts: number;\n backoffMs: number;\n maxBackoffMs?: number;\n shouldRetry?: (err: Error) => boolean;\n}\n\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const { attempts, backoffMs, maxBackoffMs, shouldRetry } = opts;\n let lastError!: Error;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err as Error;\n if (shouldRetry && !shouldRetry(lastError)) throw lastError;\n if (attempt < attempts - 1) {\n const delay = maxBackoffMs\n ? Math.min(backoffMs * 2 ** attempt, maxBackoffMs)\n : backoffMs * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n throw lastError;\n}\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerOptions {\n threshold: number;\n timeoutMs: number;\n halfOpenAfter: number;\n}\n\nexport class CircuitBreaker {\n private failures = 0;\n private openedAt: number | null = null;\n private _state: CircuitState = \"closed\";\n\n constructor(private readonly opts: CircuitBreakerOptions) {}\n\n get state(): CircuitState {\n return this._state;\n }\n\n async call<T>(fn: () => Promise<T>): Promise<T> {\n if (this._state === \"open\") {\n const elapsed = Date.now() - (this.openedAt ?? 0);\n if (elapsed >= this.opts.halfOpenAfter) {\n this._state = \"half-open\";\n } else {\n throw new Error(\"Circuit open\");\n }\n }\n\n try {\n const result = await fn();\n // Success — reset\n this.failures = 0;\n this._state = \"closed\";\n this.openedAt = null;\n return result;\n } catch (err) {\n this.failures++;\n if (this._state === \"half-open\" || this.failures >= this.opts.threshold) {\n this._state = \"open\";\n this.openedAt = Date.now();\n this.failures = 0;\n }\n throw err;\n }\n }\n}\n","export interface StringGuardOptions {\n maxLen?: number;\n pattern?: RegExp;\n trim?: boolean;\n}\n\nexport function guardString(val: unknown, field: string, opts: StringGuardOptions = {}): string {\n if (typeof val !== \"string\") throw new Error(`${field}: expected string, got ${typeof val}`);\n const trimmed = opts.trim !== false ? val.trim() : val;\n if (opts.maxLen !== undefined && trimmed.length > opts.maxLen) {\n throw new Error(`${field}: exceeds max length ${opts.maxLen}`);\n }\n if (opts.pattern && !opts.pattern.test(trimmed)) {\n throw new Error(`${field}: invalid format`);\n }\n return trimmed;\n}\n\nexport interface NumberGuardOptions {\n min?: number;\n max?: number;\n}\n\nexport function guardNumber(val: unknown, field: string, opts: NumberGuardOptions = {}): number {\n if (typeof val !== \"number\" || !isFinite(val)) {\n throw new Error(\n `${field}: expected number, got ${typeof val === \"number\" ? \"NaN/Infinity\" : typeof val}`\n );\n }\n if (opts.min !== undefined && val < opts.min) {\n throw new Error(`${field}: must be >= ${opts.min}`);\n }\n if (opts.max !== undefined && val > opts.max) {\n throw new Error(`${field}: must be <= ${opts.max}`);\n }\n return val;\n}\n\nexport function guardPositiveInt(val: unknown, field: string): number {\n const n = guardNumber(val, field);\n if (!Number.isInteger(n)) throw new Error(`${field}: must be integer`);\n if (n < 1) throw new Error(`${field}: must be >= 1`);\n return n;\n}\n\nexport function guardIsoDate(val: unknown, field: string): string {\n if (typeof val !== \"string\" || !val) throw new Error(`${field}: invalid date`);\n const d = new Date(val);\n if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);\n // Reject clearly invalid month/day combinations (e.g., 2026-13-01)\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) {\n const [year, month, day] = val.slice(0, 10).split(\"-\").map(Number) as [number, number, number];\n if (month < 1 || month > 12 || day < 1 || day > 31) {\n throw new Error(`${field}: invalid date`);\n }\n // Cross-check with Date parsing\n const reparse = new Date(\n `${year}-${String(month).padStart(2, \"0\")}-${String(day).padStart(2, \"0\")}`\n );\n if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) {\n throw new Error(`${field}: invalid date`);\n }\n }\n return val;\n}\n\nconst DEFAULT_LLM_MAX_BYTES = 512 * 1024; // 512 KB\n\nexport function guardLlmResponse(\n response: unknown,\n maxBytes: number = DEFAULT_LLM_MAX_BYTES\n): string {\n if (typeof response !== \"string\") {\n throw new Error(\"LLM response: expected string\");\n }\n const byteLen = Buffer.byteLength(response, \"utf-8\");\n if (byteLen > maxBytes) {\n throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);\n }\n return response;\n}\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport { CircuitBreaker } from \"./resilience.js\";\nimport { guardLlmResponse } from \"./input-guard.js\";\nimport { maskPii, piiMaskingEnabled } from \"./pii.js\";\nimport { neutralizeUntrusted, guardrailsEnabled } from \"./guardrails.js\";\nimport { llmProvider, localLlmConfig } from \"./compliance.js\";\n\nconst MODEL = \"claude-haiku-4-5-20251001\";\n\nlet _client: Anthropic | null = null;\nlet llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n\nexport function resetLlmCircuit(): void {\n llmCircuit = new CircuitBreaker({ threshold: 3, timeoutMs: 30_000, halfOpenAfter: 30_000 });\n}\n\nfunction getClient(): Anthropic | null {\n if (!process.env[\"ANTHROPIC_API_KEY\"]) return null;\n if (!_client) _client = new Anthropic();\n return _client;\n}\n\nexport interface EmailSummary {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n}\n\nexport interface CustomerMatch {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n}\n\nfunction emailFallback(snippet: string): EmailSummary {\n return {\n summary: snippet.slice(0, 300),\n sentiment: \"neutral\",\n nextSteps: [],\n };\n}\n\nexport async function summarizeEmail(\n subject: string,\n snippet: string,\n from: string\n): Promise<EmailSummary> {\n const client = getClient();\n if (!client) return emailFallback(snippet);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 200,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Extract structured information from email metadata.\\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Subject: ${subject}\\nFrom: ${from}\\nContent: ${snippet}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return emailFallback(snippet);\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n summary: string;\n sentiment: \"positive\" | \"neutral\" | \"negative\" | \"urgent\";\n nextSteps: string[];\n };\n return parsed;\n } catch {\n return emailFallback(snippet);\n }\n } catch {\n return emailFallback(snippet);\n }\n}\n\nexport async function recognizeCustomer(\n transcriptContent: string,\n candidates: Array<{ slug: string; name: string }>\n): Promise<CustomerMatch> {\n if (candidates.length === 0) return { slug: null, confidence: \"low\" };\n\n const client = getClient();\n if (!client) return { slug: null, confidence: \"low\" };\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 100,\n system: [\n {\n type: \"text\",\n text: 'You are a CRM assistant. Match a meeting transcript to the most likely customer.\\nReturn ONLY valid JSON: { \"slug\": string|null, \"confidence\": \"high\"|\"medium\"|\"low\" }\\nslug must be one of the provided candidates or null if no match.',\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `Available customers: ${candidates.map((c) => `${c.slug} (${c.name})`).join(\", \")}\\nTranscript (first 1000 chars): ${transcriptContent.slice(0, 1000)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") return { slug: null, confidence: \"low\" };\n\n try {\n const parsed = JSON.parse(textBlock.text) as {\n slug: string | null;\n confidence: \"high\" | \"medium\" | \"low\";\n };\n return parsed;\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n } catch {\n return { slug: null, confidence: \"low\" };\n }\n}\n\nexport function resetLlmClient(): void {\n _client = null;\n}\n\nfunction recordCall(\n model: string,\n inputTokens: number,\n outputTokens: number,\n ctx?: { slug?: string; tool?: string }\n): void {\n const dataDir = process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n void import(\"./usage.js\").then(({ recordUsage }) =>\n recordUsage(dataDir, {\n ...(ctx?.slug ? { slug: ctx.slug } : {}),\n ...(ctx?.tool ? { tool: ctx.tool } : {}),\n model,\n inputTokens,\n outputTokens,\n })\n );\n}\n\n/**\n * Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via\n * fetch — no extra dependency — so customer data can stay on-machine. Usage is\n * still recorded for cost/observability parity with the Anthropic path.\n */\nasync function callLocalLlm(\n masked: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const { baseUrl, model } = localLlmConfig();\n const res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n }),\n });\n if (!res.ok) throw new Error(`Local LLM error ${res.status}`);\n const data = (await res.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n };\n const text = data.choices?.[0]?.message?.content;\n if (!text) throw new Error(\"No text response from local LLM\");\n if (data.usage)\n recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);\n return text;\n}\n\nexport async function callLlm(\n prompt: string,\n ctx?: { slug?: string; tool?: string }\n): Promise<string> {\n const provider = llmProvider();\n const client = provider === \"anthropic\" ? getClient() : null;\n if (provider === \"anthropic\" && !client) throw new Error(\"ANTHROPIC_API_KEY not set\");\n\n // Opt-in guardrails (neutralize prompt-injection) + PII masking, then restore.\n const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;\n const { masked, unmask } = piiMaskingEnabled()\n ? maskPii(guarded)\n : { masked: guarded, unmask: (t: string) => t };\n\n if (provider !== \"anthropic\") {\n return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));\n }\n\n return llmCircuit.call(async () => {\n const response = await client!.messages.create({\n model: MODEL,\n max_tokens: 500,\n messages: [{ role: \"user\", content: masked }],\n });\n\n // Token-cost observability (D3): record usage per customer/tool.\n const usage = response.usage;\n if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\") throw new Error(\"No text response from LLM\");\n return unmask(guardLlmResponse(textBlock.text));\n });\n}\n\nexport type FieldMapping = Record<string, string | null>;\n\n// Alias table: CRM field name → list of CSV column patterns (lowercased substrings)\nconst FIELD_ALIASES: Record<string, string[]> = {\n name: [\n \"company name\",\n \"company\",\n \"organization\",\n \"organisation\",\n \"account name\",\n \"name\",\n \"firma\",\n ],\n email: [\"email address\", \"e-mail\", \"email\", \"e-mail address\", \"mail\"],\n domain: [\"company domain\", \"website\", \"domain\", \"url\", \"web\", \"homepage\"],\n phone: [\"phone number\", \"phone\", \"tel\", \"telephone\", \"mobile\", \"cell\"],\n industry: [\"industry\", \"sector\", \"branche\", \"vertical\"],\n primary_contact: [\"contact name\", \"contact person\", \"contact\", \"ansprechpartner\", \"kontakt\"],\n timezone: [\"timezone\", \"time zone\", \"tz\"],\n // Import-specific fields\n notes: [\n \"notes\",\n \"description\",\n \"body\",\n \"comment\",\n \"details\",\n \"note\",\n \"inhalt\",\n \"subject\",\n \"summary\",\n ],\n date: [\"activity date\", \"activity_date\", \"due date\", \"date\", \"created_at\", \"timestamp\", \"time\"],\n activityType: [\"activity type\", \"activity_type\", \"activitytype\", \"type\", \"category\", \"art\"],\n sourceId: [\n \"record id\",\n \"record_id\",\n \"source id\",\n \"source_id\",\n \"external id\",\n \"external_id\",\n \"activity id\",\n ],\n};\n\nexport function mapCsvFieldsHeuristic(headers: string[], targetFields: string[]): FieldMapping {\n const result: FieldMapping = {};\n const usedHeaders = new Set<string>();\n\n for (const field of targetFields) {\n const aliases = FIELD_ALIASES[field] ?? [field];\n let matched: string | null = null;\n\n for (const header of headers) {\n if (usedHeaders.has(header)) continue;\n const lower = header.toLowerCase();\n if (aliases.some((alias) => lower === alias || lower.includes(alias))) {\n matched = header;\n break;\n }\n }\n\n result[field] = matched;\n if (matched) usedHeaders.add(matched);\n }\n\n return result;\n}\n\nconst FIELD_SEMANTICS = `CRM field semantics:\n- name: Company or organization name (required)\n- email: Contact email address\n- domain: Company website or domain (e.g. \"acme.com\")\n- notes: Interaction notes, description, or subject text\n- date: Date of activity/interaction (ISO 8601 or YYYY-MM-DD)\n- activityType: Type of interaction — Call, Email, Meeting, Note\n- sourceId: Unique ID from the source system used for deduplication`;\n\nexport async function mapCsvFields(\n headers: string[],\n targetFields: string[]\n): Promise<FieldMapping> {\n const client = getClient();\n if (!client) return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const response = await client.messages.create({\n model: MODEL,\n max_tokens: 300,\n system: [\n {\n type: \"text\",\n text: `You are a CRM data-import assistant. Map CSV column headers to internal CRM field names.\n\n${FIELD_SEMANTICS}\n\nRules:\n1. Return ONLY valid JSON: { \"<crmField>\": \"<csvColumn>\" | null, ... }\n2. Every requested CRM field must appear as a key in the response.\n3. Use null when no column is a reasonable match.\n4. Each CSV column may only be assigned to one CRM field.\n5. Only use column names that appear exactly in the provided CSV columns list.`,\n cache_control: { type: \"ephemeral\" },\n },\n ],\n messages: [\n {\n role: \"user\",\n content: `CSV columns: ${JSON.stringify(headers)}\\nMap to CRM fields: ${JSON.stringify(targetFields)}`,\n },\n ],\n });\n\n const textBlock = response.content.find((b) => b.type === \"text\");\n if (!textBlock || textBlock.type !== \"text\")\n return mapCsvFieldsHeuristic(headers, targetFields);\n\n try {\n const raw = JSON.parse(\n textBlock.text\n .replace(/^```(?:json)?\\n?/, \"\")\n .replace(/\\n?```$/, \"\")\n .trim()\n ) as Record<string, string | null>;\n const validated: FieldMapping = {};\n const headerSet = new Set(headers);\n for (const field of targetFields) {\n const col = raw[field] ?? null;\n validated[field] = col !== null && headerSet.has(col) ? col : null;\n }\n // Require at least 'name' to be mapped; fall back otherwise\n if (!validated[\"name\"]) return mapCsvFieldsHeuristic(headers, targetFields);\n return validated;\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n } catch {\n return mapCsvFieldsHeuristic(headers, targetFields);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-DakpK96I.cjs");
|
|
2
|
+
let path = require("path");
|
|
3
|
+
path = require_chunk.__toESM(path, 1);
|
|
4
|
+
let fs = require("fs");
|
|
5
|
+
fs = require_chunk.__toESM(fs, 1);
|
|
6
|
+
//#region src/core/logger.ts
|
|
7
|
+
const LEVELS = {
|
|
8
|
+
debug: 10,
|
|
9
|
+
info: 20,
|
|
10
|
+
warn: 30,
|
|
11
|
+
error: 40
|
|
12
|
+
};
|
|
13
|
+
function resolveDataDir(dataDir) {
|
|
14
|
+
return dataDir ?? process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
15
|
+
}
|
|
16
|
+
function logsPath(dataDir) {
|
|
17
|
+
return path.default.join(dataDir, ".agentic", "logs.ndjson");
|
|
18
|
+
}
|
|
19
|
+
function minLevel() {
|
|
20
|
+
return LEVELS[(process.env["DXCRM_LOG_LEVEL"] ?? "info").toLowerCase()] ?? LEVELS.info;
|
|
21
|
+
}
|
|
22
|
+
/** Max bytes for the active ledger before it rotates (default 5 MB). */
|
|
23
|
+
function maxBytes() {
|
|
24
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_BYTES"] ?? "", 10);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? n : 5e6;
|
|
26
|
+
}
|
|
27
|
+
/** Number of rotated archives to keep (default 5; 0 = truncate, no archives). */
|
|
28
|
+
function maxFiles() {
|
|
29
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_FILES"] ?? "", 10);
|
|
30
|
+
return Number.isFinite(n) && n >= 0 ? n : 5;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Size-based log rotation. Before an append would push the active ledger past
|
|
34
|
+
* the byte budget, the archives shift (logs.ndjson.1 → .2 → … dropping the
|
|
35
|
+
* oldest) and the current ledger becomes logs.ndjson.1, leaving a fresh active
|
|
36
|
+
* file. Best-effort: any fs error here is swallowed so logging never breaks.
|
|
37
|
+
*/
|
|
38
|
+
function rotateIfNeeded(p, incomingBytes) {
|
|
39
|
+
let size = 0;
|
|
40
|
+
try {
|
|
41
|
+
size = fs.default.statSync(p).size;
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (size + incomingBytes <= maxBytes()) return;
|
|
46
|
+
const keep = maxFiles();
|
|
47
|
+
try {
|
|
48
|
+
if (keep <= 0) {
|
|
49
|
+
fs.default.rmSync(p, { force: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
fs.default.rmSync(`${p}.${keep}`, { force: true });
|
|
53
|
+
for (let i = keep - 1; i >= 1; i--) if (fs.default.existsSync(`${p}.${i}`)) fs.default.renameSync(`${p}.${i}`, `${p}.${i + 1}`);
|
|
54
|
+
fs.default.renameSync(p, `${p}.1`);
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
/** Core log primitive. Honors DXCRM_LOG_LEVEL; mirrors to stderr unless off. */
|
|
58
|
+
function log(level, component, message, context, opts = {}) {
|
|
59
|
+
if (LEVELS[level] < minLevel()) return;
|
|
60
|
+
const entry = {
|
|
61
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
62
|
+
level,
|
|
63
|
+
component,
|
|
64
|
+
message,
|
|
65
|
+
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
const p = logsPath(resolveDataDir(opts.dataDir));
|
|
69
|
+
fs.default.mkdirSync(path.default.dirname(p), { recursive: true });
|
|
70
|
+
const line = JSON.stringify(entry) + "\n";
|
|
71
|
+
rotateIfNeeded(p, Buffer.byteLength(line));
|
|
72
|
+
fs.default.appendFileSync(p, line, "utf-8");
|
|
73
|
+
} catch {}
|
|
74
|
+
if (process.env["DXCRM_LOG_STDERR"] !== "off") {
|
|
75
|
+
const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : "";
|
|
76
|
+
try {
|
|
77
|
+
process.stderr.write(`[${component}] ${message}${ctx}\n`);
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const logger = {
|
|
82
|
+
debug: (component, message, context) => log("debug", component, message, context),
|
|
83
|
+
info: (component, message, context) => log("info", component, message, context),
|
|
84
|
+
warn: (component, message, context) => log("warn", component, message, context),
|
|
85
|
+
error: (component, message, context) => log("error", component, message, context)
|
|
86
|
+
};
|
|
87
|
+
/** Read the active ledger plus rotated archives, oldest first. */
|
|
88
|
+
function readLedgerText(p) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
for (let i = maxFiles(); i >= 1; i--) {
|
|
91
|
+
const archive = `${p}.${i}`;
|
|
92
|
+
if (fs.default.existsSync(archive)) try {
|
|
93
|
+
parts.push(fs.default.readFileSync(archive, "utf-8"));
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
if (fs.default.existsSync(p)) try {
|
|
97
|
+
parts.push(fs.default.readFileSync(p, "utf-8"));
|
|
98
|
+
} catch {}
|
|
99
|
+
return parts.join("");
|
|
100
|
+
}
|
|
101
|
+
/** Read the log ledger (incl. rotated archives), skipping malformed lines, and filter. */
|
|
102
|
+
function queryLogs(dataDir, query = {}) {
|
|
103
|
+
const entries = readLedgerText(logsPath(dataDir)).split("\n").filter(Boolean).flatMap((line) => {
|
|
104
|
+
try {
|
|
105
|
+
return [JSON.parse(line)];
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const minRank = query.level ? LEVELS[query.level] : 0;
|
|
111
|
+
const contains = query.contains?.toLowerCase();
|
|
112
|
+
const filtered = entries.filter((e) => {
|
|
113
|
+
if (LEVELS[e.level] < minRank) return false;
|
|
114
|
+
if (query.component && e.component !== query.component) return false;
|
|
115
|
+
if (query.since && e.ts < query.since) return false;
|
|
116
|
+
if (contains && !e.message.toLowerCase().includes(contains)) return false;
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
return query.limit && query.limit > 0 ? filtered.slice(-query.limit) : filtered;
|
|
120
|
+
}
|
|
121
|
+
/** Aggregate the (optionally filtered) log ledger for at-a-glance analysis. */
|
|
122
|
+
function summarizeLogs(dataDir, query = {}) {
|
|
123
|
+
const entries = queryLogs(dataDir, query);
|
|
124
|
+
const byLevel = {
|
|
125
|
+
debug: 0,
|
|
126
|
+
info: 0,
|
|
127
|
+
warn: 0,
|
|
128
|
+
error: 0
|
|
129
|
+
};
|
|
130
|
+
const byComponent = {};
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;
|
|
133
|
+
byComponent[e.component] = (byComponent[e.component] ?? 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
const summary = {
|
|
136
|
+
total: entries.length,
|
|
137
|
+
byLevel,
|
|
138
|
+
byComponent,
|
|
139
|
+
recentErrors: entries.filter((e) => e.level === "error").slice(-5)
|
|
140
|
+
};
|
|
141
|
+
if (entries.length > 0) {
|
|
142
|
+
summary.firstTs = entries[0].ts;
|
|
143
|
+
summary.lastTs = entries[entries.length - 1].ts;
|
|
144
|
+
}
|
|
145
|
+
return summary;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
Object.defineProperty(exports, "logger", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get: function() {
|
|
151
|
+
return logger;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
Object.defineProperty(exports, "queryLogs", {
|
|
155
|
+
enumerable: true,
|
|
156
|
+
get: function() {
|
|
157
|
+
return queryLogs;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
Object.defineProperty(exports, "summarizeLogs", {
|
|
161
|
+
enumerable: true,
|
|
162
|
+
get: function() {
|
|
163
|
+
return summarizeLogs;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=logger-BkInaGoV.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-BkInaGoV.cjs","names":[],"sources":["../src/core/logger.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Unified structured logging (enterprise observability). Every log entry is a\n * structured record appended as NDJSON to `.agentic/logs.ndjson`, so logs are\n * durable, queryable and aggregatable — over the CLI (`dxcrm logs`) and over\n * MCP (`get_logs`). A formatted copy is mirrored to stderr for live tailing.\n *\n * Logging must never break the caller: persistence failures are swallowed.\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LEVELS: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };\n\nexport interface LogEntry {\n ts: string; // ISO timestamp\n level: LogLevel;\n component: string;\n message: string;\n context?: Record<string, unknown>;\n}\n\nfunction resolveDataDir(dataDir?: string): string {\n return dataDir ?? process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n}\n\nexport function logsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"logs.ndjson\");\n}\n\nfunction minLevel(): number {\n const env = (process.env[\"DXCRM_LOG_LEVEL\"] ?? \"info\").toLowerCase();\n return LEVELS[env as LogLevel] ?? LEVELS.info;\n}\n\n/** Max bytes for the active ledger before it rotates (default 5 MB). */\nfunction maxBytes(): number {\n const n = parseInt(process.env[\"DXCRM_LOG_MAX_BYTES\"] ?? \"\", 10);\n return Number.isFinite(n) && n > 0 ? n : 5_000_000;\n}\n\n/** Number of rotated archives to keep (default 5; 0 = truncate, no archives). */\nfunction maxFiles(): number {\n const n = parseInt(process.env[\"DXCRM_LOG_MAX_FILES\"] ?? \"\", 10);\n return Number.isFinite(n) && n >= 0 ? n : 5;\n}\n\n/**\n * Size-based log rotation. Before an append would push the active ledger past\n * the byte budget, the archives shift (logs.ndjson.1 → .2 → … dropping the\n * oldest) and the current ledger becomes logs.ndjson.1, leaving a fresh active\n * file. Best-effort: any fs error here is swallowed so logging never breaks.\n */\nfunction rotateIfNeeded(p: string, incomingBytes: number): void {\n let size = 0;\n try {\n size = fs.statSync(p).size;\n } catch {\n return; // no active file yet\n }\n if (size + incomingBytes <= maxBytes()) return;\n\n const keep = maxFiles();\n try {\n if (keep <= 0) {\n fs.rmSync(p, { force: true });\n return;\n }\n fs.rmSync(`${p}.${keep}`, { force: true });\n for (let i = keep - 1; i >= 1; i--) {\n if (fs.existsSync(`${p}.${i}`)) fs.renameSync(`${p}.${i}`, `${p}.${i + 1}`);\n }\n fs.renameSync(p, `${p}.1`);\n } catch {\n /* rotation is best-effort */\n }\n}\n\n/** Core log primitive. Honors DXCRM_LOG_LEVEL; mirrors to stderr unless off. */\nexport function log(\n level: LogLevel,\n component: string,\n message: string,\n context?: Record<string, unknown>,\n opts: { dataDir?: string } = {}\n): void {\n if (LEVELS[level] < minLevel()) return;\n\n const entry: LogEntry = {\n ts: new Date().toISOString(),\n level,\n component,\n message,\n ...(context && Object.keys(context).length > 0 ? { context } : {}),\n };\n\n // Persist (never throw from logging).\n try {\n const p = logsPath(resolveDataDir(opts.dataDir));\n fs.mkdirSync(path.dirname(p), { recursive: true });\n const line = JSON.stringify(entry) + \"\\n\";\n rotateIfNeeded(p, Buffer.byteLength(line));\n fs.appendFileSync(p, line, \"utf-8\");\n } catch {\n /* logging must not break the caller */\n }\n\n // Mirror to stderr for live tailing (stdout is reserved for the MCP protocol).\n if (process.env[\"DXCRM_LOG_STDERR\"] !== \"off\") {\n const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : \"\";\n try {\n process.stderr.write(`[${component}] ${message}${ctx}\\n`);\n } catch {\n /* ignore */\n }\n }\n}\n\nexport const logger = {\n debug: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"debug\", component, message, context),\n info: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"info\", component, message, context),\n warn: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"warn\", component, message, context),\n error: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"error\", component, message, context),\n};\n\n/**\n * Start a timer; the returned function logs an `info` entry with `durationMs`\n * when called. Useful for timing syncs, LLM calls, and other operations.\n */\nexport function withTimer(\n component: string,\n message: string,\n context?: Record<string, unknown>\n): () => void {\n const t0 = Date.now();\n return () => log(\"info\", component, message, { ...context, durationMs: Date.now() - t0 });\n}\n\nexport interface LogQuery {\n level?: LogLevel; // minimum level to include\n component?: string;\n since?: string; // ISO timestamp; only entries at or after this\n contains?: string; // case-insensitive substring of the message\n limit?: number; // return only the last N matches\n}\n\n/** Read the active ledger plus rotated archives, oldest first. */\nfunction readLedgerText(p: string): string {\n const parts: string[] = [];\n for (let i = maxFiles(); i >= 1; i--) {\n const archive = `${p}.${i}`;\n if (fs.existsSync(archive)) {\n try {\n parts.push(fs.readFileSync(archive, \"utf-8\") as string);\n } catch {\n /* skip unreadable archive */\n }\n }\n }\n if (fs.existsSync(p)) {\n try {\n parts.push(fs.readFileSync(p, \"utf-8\") as string);\n } catch {\n /* skip */\n }\n }\n return parts.join(\"\");\n}\n\n/** Read the log ledger (incl. rotated archives), skipping malformed lines, and filter. */\nexport function queryLogs(dataDir: string, query: LogQuery = {}): LogEntry[] {\n const entries = readLedgerText(logsPath(dataDir))\n .split(\"\\n\")\n .filter(Boolean)\n .flatMap((line) => {\n try {\n return [JSON.parse(line) as LogEntry];\n } catch {\n return [];\n }\n });\n\n const minRank = query.level ? LEVELS[query.level] : 0;\n const contains = query.contains?.toLowerCase();\n\n const filtered = entries.filter((e) => {\n if (LEVELS[e.level] < minRank) return false;\n if (query.component && e.component !== query.component) return false;\n if (query.since && e.ts < query.since) return false;\n if (contains && !e.message.toLowerCase().includes(contains)) return false;\n return true;\n });\n\n return query.limit && query.limit > 0 ? filtered.slice(-query.limit) : filtered;\n}\n\nexport interface LogSummary {\n total: number;\n byLevel: Record<LogLevel, number>;\n byComponent: Record<string, number>;\n firstTs?: string;\n lastTs?: string;\n recentErrors: LogEntry[];\n}\n\n/** Aggregate the (optionally filtered) log ledger for at-a-glance analysis. */\nexport function summarizeLogs(dataDir: string, query: LogQuery = {}): LogSummary {\n const entries = queryLogs(dataDir, query);\n const byLevel: Record<LogLevel, number> = { debug: 0, info: 0, warn: 0, error: 0 };\n const byComponent: Record<string, number> = {};\n\n for (const e of entries) {\n byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;\n byComponent[e.component] = (byComponent[e.component] ?? 0) + 1;\n }\n\n const summary: LogSummary = {\n total: entries.length,\n byLevel,\n byComponent,\n recentErrors: entries.filter((e) => e.level === \"error\").slice(-5),\n };\n if (entries.length > 0) {\n summary.firstTs = entries[0]!.ts;\n summary.lastTs = entries[entries.length - 1]!.ts;\n }\n return summary;\n}\n"],"mappings":";;;;;;AAaA,MAAM,SAAmC;CAAE,OAAO;CAAI,MAAM;CAAI,MAAM;CAAI,OAAO;AAAG;AAUpF,SAAS,eAAe,SAA0B;CAChD,OAAO,WAAW,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;AACjE;AAEA,SAAgB,SAAS,SAAyB;CAChD,OAAO,KAAA,QAAK,KAAK,SAAS,YAAY,aAAa;AACrD;AAEA,SAAS,WAAmB;CAE1B,OAAO,QADM,QAAQ,IAAI,sBAAsB,QAAQ,YACvC,MAAkB,OAAO;AAC3C;;AAGA,SAAS,WAAmB;CAC1B,MAAM,IAAI,SAAS,QAAQ,IAAI,0BAA0B,IAAI,EAAE;CAC/D,OAAO,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,IAAI;AAC3C;;AAGA,SAAS,WAAmB;CAC1B,MAAM,IAAI,SAAS,QAAQ,IAAI,0BAA0B,IAAI,EAAE;CAC/D,OAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;;;;;;;AAQA,SAAS,eAAe,GAAW,eAA6B;CAC9D,IAAI,OAAO;CACX,IAAI;EACF,OAAO,GAAA,QAAG,SAAS,CAAC,EAAE;CACxB,QAAQ;EACN;CACF;CACA,IAAI,OAAO,iBAAiB,SAAS,GAAG;CAExC,MAAM,OAAO,SAAS;CACtB,IAAI;EACF,IAAI,QAAQ,GAAG;GACb,GAAA,QAAG,OAAO,GAAG,EAAE,OAAO,KAAK,CAAC;GAC5B;EACF;EACA,GAAA,QAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,OAAO,KAAK,CAAC;EACzC,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,GAAG,KAC7B,IAAI,GAAA,QAAG,WAAW,GAAG,EAAE,GAAG,GAAG,GAAG,GAAA,QAAG,WAAW,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,GAAG,IAAI,GAAG;EAE5E,GAAA,QAAG,WAAW,GAAG,GAAG,EAAE,GAAG;CAC3B,QAAQ,CAER;AACF;;AAGA,SAAgB,IACd,OACA,WACA,SACA,SACA,OAA6B,CAAC,GACxB;CACN,IAAI,OAAO,SAAS,SAAS,GAAG;CAEhC,MAAM,QAAkB;EACtB,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B;EACA;EACA;EACA,GAAI,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;CAClE;CAGA,IAAI;EACF,MAAM,IAAI,SAAS,eAAe,KAAK,OAAO,CAAC;EAC/C,GAAA,QAAG,UAAU,KAAA,QAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;EACjD,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI;EACrC,eAAe,GAAG,OAAO,WAAW,IAAI,CAAC;EACzC,GAAA,QAAG,eAAe,GAAG,MAAM,OAAO;CACpC,QAAQ,CAER;CAGA,IAAI,QAAQ,IAAI,wBAAwB,OAAO;EAC7C,MAAM,MAAM,MAAM,UAAU,IAAI,KAAK,UAAU,MAAM,OAAO,MAAM;EAClE,IAAI;GACF,QAAQ,OAAO,MAAM,IAAI,UAAU,IAAI,UAAU,IAAI,GAAG;EAC1D,QAAQ,CAER;CACF;AACF;AAEA,MAAa,SAAS;CACpB,QAAQ,WAAmB,SAAiB,YAC1C,IAAI,SAAS,WAAW,SAAS,OAAO;CAC1C,OAAO,WAAmB,SAAiB,YACzC,IAAI,QAAQ,WAAW,SAAS,OAAO;CACzC,OAAO,WAAmB,SAAiB,YACzC,IAAI,QAAQ,WAAW,SAAS,OAAO;CACzC,QAAQ,WAAmB,SAAiB,YAC1C,IAAI,SAAS,WAAW,SAAS,OAAO;AAC5C;;AAwBA,SAAS,eAAe,GAAmB;CACzC,MAAM,QAAkB,CAAC;CACzB,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK;EACpC,MAAM,UAAU,GAAG,EAAE,GAAG;EACxB,IAAI,GAAA,QAAG,WAAW,OAAO,GACvB,IAAI;GACF,MAAM,KAAK,GAAA,QAAG,aAAa,SAAS,OAAO,CAAW;EACxD,QAAQ,CAER;CAEJ;CACA,IAAI,GAAA,QAAG,WAAW,CAAC,GACjB,IAAI;EACF,MAAM,KAAK,GAAA,QAAG,aAAa,GAAG,OAAO,CAAW;CAClD,QAAQ,CAER;CAEF,OAAO,MAAM,KAAK,EAAE;AACtB;;AAGA,SAAgB,UAAU,SAAiB,QAAkB,CAAC,GAAe;CAC3E,MAAM,UAAU,eAAe,SAAS,OAAO,CAAC,EAC7C,MAAM,IAAI,EACV,OAAO,OAAO,EACd,SAAS,SAAS;EACjB,IAAI;GACF,OAAO,CAAC,KAAK,MAAM,IAAI,CAAa;EACtC,QAAQ;GACN,OAAO,CAAC;EACV;CACF,CAAC;CAEH,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,SAAS;CACpD,MAAM,WAAW,MAAM,UAAU,YAAY;CAE7C,MAAM,WAAW,QAAQ,QAAQ,MAAM;EACrC,IAAI,OAAO,EAAE,SAAS,SAAS,OAAO;EACtC,IAAI,MAAM,aAAa,EAAE,cAAc,MAAM,WAAW,OAAO;EAC/D,IAAI,MAAM,SAAS,EAAE,KAAK,MAAM,OAAO,OAAO;EAC9C,IAAI,YAAY,CAAC,EAAE,QAAQ,YAAY,EAAE,SAAS,QAAQ,GAAG,OAAO;EACpE,OAAO;CACT,CAAC;CAED,OAAO,MAAM,SAAS,MAAM,QAAQ,IAAI,SAAS,MAAM,CAAC,MAAM,KAAK,IAAI;AACzE;;AAYA,SAAgB,cAAc,SAAiB,QAAkB,CAAC,GAAe;CAC/E,MAAM,UAAU,UAAU,SAAS,KAAK;CACxC,MAAM,UAAoC;EAAE,OAAO;EAAG,MAAM;EAAG,MAAM;EAAG,OAAO;CAAE;CACjF,MAAM,cAAsC,CAAC;CAE7C,KAAK,MAAM,KAAK,SAAS;EACvB,QAAQ,EAAE,UAAU,QAAQ,EAAE,UAAU,KAAK;EAC7C,YAAY,EAAE,cAAc,YAAY,EAAE,cAAc,KAAK;CAC/D;CAEA,MAAM,UAAsB;EAC1B,OAAO,QAAQ;EACf;EACA;EACA,cAAc,QAAQ,QAAQ,MAAM,EAAE,UAAU,OAAO,EAAE,MAAM,EAAE;CACnE;CACA,IAAI,QAAQ,SAAS,GAAG;EACtB,QAAQ,UAAU,QAAQ,GAAI;EAC9B,QAAQ,SAAS,QAAQ,QAAQ,SAAS,GAAI;CAChD;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
//#region src/core/logger.ts
|
|
4
|
+
const LEVELS = {
|
|
5
|
+
debug: 10,
|
|
6
|
+
info: 20,
|
|
7
|
+
warn: 30,
|
|
8
|
+
error: 40
|
|
9
|
+
};
|
|
10
|
+
function resolveDataDir(dataDir) {
|
|
11
|
+
return dataDir ?? process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
12
|
+
}
|
|
13
|
+
function logsPath(dataDir) {
|
|
14
|
+
return path.join(dataDir, ".agentic", "logs.ndjson");
|
|
15
|
+
}
|
|
16
|
+
function minLevel() {
|
|
17
|
+
return LEVELS[(process.env["DXCRM_LOG_LEVEL"] ?? "info").toLowerCase()] ?? LEVELS.info;
|
|
18
|
+
}
|
|
19
|
+
/** Max bytes for the active ledger before it rotates (default 5 MB). */
|
|
20
|
+
function maxBytes() {
|
|
21
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_BYTES"] ?? "", 10);
|
|
22
|
+
return Number.isFinite(n) && n > 0 ? n : 5e6;
|
|
23
|
+
}
|
|
24
|
+
/** Number of rotated archives to keep (default 5; 0 = truncate, no archives). */
|
|
25
|
+
function maxFiles() {
|
|
26
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_FILES"] ?? "", 10);
|
|
27
|
+
return Number.isFinite(n) && n >= 0 ? n : 5;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Size-based log rotation. Before an append would push the active ledger past
|
|
31
|
+
* the byte budget, the archives shift (logs.ndjson.1 → .2 → … dropping the
|
|
32
|
+
* oldest) and the current ledger becomes logs.ndjson.1, leaving a fresh active
|
|
33
|
+
* file. Best-effort: any fs error here is swallowed so logging never breaks.
|
|
34
|
+
*/
|
|
35
|
+
function rotateIfNeeded(p, incomingBytes) {
|
|
36
|
+
let size = 0;
|
|
37
|
+
try {
|
|
38
|
+
size = fs.statSync(p).size;
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (size + incomingBytes <= maxBytes()) return;
|
|
43
|
+
const keep = maxFiles();
|
|
44
|
+
try {
|
|
45
|
+
if (keep <= 0) {
|
|
46
|
+
fs.rmSync(p, { force: true });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
fs.rmSync(`${p}.${keep}`, { force: true });
|
|
50
|
+
for (let i = keep - 1; i >= 1; i--) if (fs.existsSync(`${p}.${i}`)) fs.renameSync(`${p}.${i}`, `${p}.${i + 1}`);
|
|
51
|
+
fs.renameSync(p, `${p}.1`);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
/** Core log primitive. Honors DXCRM_LOG_LEVEL; mirrors to stderr unless off. */
|
|
55
|
+
function log(level, component, message, context, opts = {}) {
|
|
56
|
+
if (LEVELS[level] < minLevel()) return;
|
|
57
|
+
const entry = {
|
|
58
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59
|
+
level,
|
|
60
|
+
component,
|
|
61
|
+
message,
|
|
62
|
+
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const p = logsPath(resolveDataDir(opts.dataDir));
|
|
66
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
67
|
+
const line = JSON.stringify(entry) + "\n";
|
|
68
|
+
rotateIfNeeded(p, Buffer.byteLength(line));
|
|
69
|
+
fs.appendFileSync(p, line, "utf-8");
|
|
70
|
+
} catch {}
|
|
71
|
+
if (process.env["DXCRM_LOG_STDERR"] !== "off") {
|
|
72
|
+
const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : "";
|
|
73
|
+
try {
|
|
74
|
+
process.stderr.write(`[${component}] ${message}${ctx}\n`);
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const logger = {
|
|
79
|
+
debug: (component, message, context) => log("debug", component, message, context),
|
|
80
|
+
info: (component, message, context) => log("info", component, message, context),
|
|
81
|
+
warn: (component, message, context) => log("warn", component, message, context),
|
|
82
|
+
error: (component, message, context) => log("error", component, message, context)
|
|
83
|
+
};
|
|
84
|
+
/** Read the active ledger plus rotated archives, oldest first. */
|
|
85
|
+
function readLedgerText(p) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (let i = maxFiles(); i >= 1; i--) {
|
|
88
|
+
const archive = `${p}.${i}`;
|
|
89
|
+
if (fs.existsSync(archive)) try {
|
|
90
|
+
parts.push(fs.readFileSync(archive, "utf-8"));
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
if (fs.existsSync(p)) try {
|
|
94
|
+
parts.push(fs.readFileSync(p, "utf-8"));
|
|
95
|
+
} catch {}
|
|
96
|
+
return parts.join("");
|
|
97
|
+
}
|
|
98
|
+
/** Read the log ledger (incl. rotated archives), skipping malformed lines, and filter. */
|
|
99
|
+
function queryLogs(dataDir, query = {}) {
|
|
100
|
+
const entries = readLedgerText(logsPath(dataDir)).split("\n").filter(Boolean).flatMap((line) => {
|
|
101
|
+
try {
|
|
102
|
+
return [JSON.parse(line)];
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const minRank = query.level ? LEVELS[query.level] : 0;
|
|
108
|
+
const contains = query.contains?.toLowerCase();
|
|
109
|
+
const filtered = entries.filter((e) => {
|
|
110
|
+
if (LEVELS[e.level] < minRank) return false;
|
|
111
|
+
if (query.component && e.component !== query.component) return false;
|
|
112
|
+
if (query.since && e.ts < query.since) return false;
|
|
113
|
+
if (contains && !e.message.toLowerCase().includes(contains)) return false;
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
return query.limit && query.limit > 0 ? filtered.slice(-query.limit) : filtered;
|
|
117
|
+
}
|
|
118
|
+
/** Aggregate the (optionally filtered) log ledger for at-a-glance analysis. */
|
|
119
|
+
function summarizeLogs(dataDir, query = {}) {
|
|
120
|
+
const entries = queryLogs(dataDir, query);
|
|
121
|
+
const byLevel = {
|
|
122
|
+
debug: 0,
|
|
123
|
+
info: 0,
|
|
124
|
+
warn: 0,
|
|
125
|
+
error: 0
|
|
126
|
+
};
|
|
127
|
+
const byComponent = {};
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;
|
|
130
|
+
byComponent[e.component] = (byComponent[e.component] ?? 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
const summary = {
|
|
133
|
+
total: entries.length,
|
|
134
|
+
byLevel,
|
|
135
|
+
byComponent,
|
|
136
|
+
recentErrors: entries.filter((e) => e.level === "error").slice(-5)
|
|
137
|
+
};
|
|
138
|
+
if (entries.length > 0) {
|
|
139
|
+
summary.firstTs = entries[0].ts;
|
|
140
|
+
summary.lastTs = entries[entries.length - 1].ts;
|
|
141
|
+
}
|
|
142
|
+
return summary;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { summarizeLogs as a, queryLogs as i, logger as n, logsPath as r, log as t };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=logger-Dyl4VcLO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-Dyl4VcLO.js","names":[],"sources":["../src/core/logger.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Unified structured logging (enterprise observability). Every log entry is a\n * structured record appended as NDJSON to `.agentic/logs.ndjson`, so logs are\n * durable, queryable and aggregatable — over the CLI (`dxcrm logs`) and over\n * MCP (`get_logs`). A formatted copy is mirrored to stderr for live tailing.\n *\n * Logging must never break the caller: persistence failures are swallowed.\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LEVELS: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };\n\nexport interface LogEntry {\n ts: string; // ISO timestamp\n level: LogLevel;\n component: string;\n message: string;\n context?: Record<string, unknown>;\n}\n\nfunction resolveDataDir(dataDir?: string): string {\n return dataDir ?? process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n}\n\nexport function logsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"logs.ndjson\");\n}\n\nfunction minLevel(): number {\n const env = (process.env[\"DXCRM_LOG_LEVEL\"] ?? \"info\").toLowerCase();\n return LEVELS[env as LogLevel] ?? LEVELS.info;\n}\n\n/** Max bytes for the active ledger before it rotates (default 5 MB). */\nfunction maxBytes(): number {\n const n = parseInt(process.env[\"DXCRM_LOG_MAX_BYTES\"] ?? \"\", 10);\n return Number.isFinite(n) && n > 0 ? n : 5_000_000;\n}\n\n/** Number of rotated archives to keep (default 5; 0 = truncate, no archives). */\nfunction maxFiles(): number {\n const n = parseInt(process.env[\"DXCRM_LOG_MAX_FILES\"] ?? \"\", 10);\n return Number.isFinite(n) && n >= 0 ? n : 5;\n}\n\n/**\n * Size-based log rotation. Before an append would push the active ledger past\n * the byte budget, the archives shift (logs.ndjson.1 → .2 → … dropping the\n * oldest) and the current ledger becomes logs.ndjson.1, leaving a fresh active\n * file. Best-effort: any fs error here is swallowed so logging never breaks.\n */\nfunction rotateIfNeeded(p: string, incomingBytes: number): void {\n let size = 0;\n try {\n size = fs.statSync(p).size;\n } catch {\n return; // no active file yet\n }\n if (size + incomingBytes <= maxBytes()) return;\n\n const keep = maxFiles();\n try {\n if (keep <= 0) {\n fs.rmSync(p, { force: true });\n return;\n }\n fs.rmSync(`${p}.${keep}`, { force: true });\n for (let i = keep - 1; i >= 1; i--) {\n if (fs.existsSync(`${p}.${i}`)) fs.renameSync(`${p}.${i}`, `${p}.${i + 1}`);\n }\n fs.renameSync(p, `${p}.1`);\n } catch {\n /* rotation is best-effort */\n }\n}\n\n/** Core log primitive. Honors DXCRM_LOG_LEVEL; mirrors to stderr unless off. */\nexport function log(\n level: LogLevel,\n component: string,\n message: string,\n context?: Record<string, unknown>,\n opts: { dataDir?: string } = {}\n): void {\n if (LEVELS[level] < minLevel()) return;\n\n const entry: LogEntry = {\n ts: new Date().toISOString(),\n level,\n component,\n message,\n ...(context && Object.keys(context).length > 0 ? { context } : {}),\n };\n\n // Persist (never throw from logging).\n try {\n const p = logsPath(resolveDataDir(opts.dataDir));\n fs.mkdirSync(path.dirname(p), { recursive: true });\n const line = JSON.stringify(entry) + \"\\n\";\n rotateIfNeeded(p, Buffer.byteLength(line));\n fs.appendFileSync(p, line, \"utf-8\");\n } catch {\n /* logging must not break the caller */\n }\n\n // Mirror to stderr for live tailing (stdout is reserved for the MCP protocol).\n if (process.env[\"DXCRM_LOG_STDERR\"] !== \"off\") {\n const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : \"\";\n try {\n process.stderr.write(`[${component}] ${message}${ctx}\\n`);\n } catch {\n /* ignore */\n }\n }\n}\n\nexport const logger = {\n debug: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"debug\", component, message, context),\n info: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"info\", component, message, context),\n warn: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"warn\", component, message, context),\n error: (component: string, message: string, context?: Record<string, unknown>): void =>\n log(\"error\", component, message, context),\n};\n\n/**\n * Start a timer; the returned function logs an `info` entry with `durationMs`\n * when called. Useful for timing syncs, LLM calls, and other operations.\n */\nexport function withTimer(\n component: string,\n message: string,\n context?: Record<string, unknown>\n): () => void {\n const t0 = Date.now();\n return () => log(\"info\", component, message, { ...context, durationMs: Date.now() - t0 });\n}\n\nexport interface LogQuery {\n level?: LogLevel; // minimum level to include\n component?: string;\n since?: string; // ISO timestamp; only entries at or after this\n contains?: string; // case-insensitive substring of the message\n limit?: number; // return only the last N matches\n}\n\n/** Read the active ledger plus rotated archives, oldest first. */\nfunction readLedgerText(p: string): string {\n const parts: string[] = [];\n for (let i = maxFiles(); i >= 1; i--) {\n const archive = `${p}.${i}`;\n if (fs.existsSync(archive)) {\n try {\n parts.push(fs.readFileSync(archive, \"utf-8\") as string);\n } catch {\n /* skip unreadable archive */\n }\n }\n }\n if (fs.existsSync(p)) {\n try {\n parts.push(fs.readFileSync(p, \"utf-8\") as string);\n } catch {\n /* skip */\n }\n }\n return parts.join(\"\");\n}\n\n/** Read the log ledger (incl. rotated archives), skipping malformed lines, and filter. */\nexport function queryLogs(dataDir: string, query: LogQuery = {}): LogEntry[] {\n const entries = readLedgerText(logsPath(dataDir))\n .split(\"\\n\")\n .filter(Boolean)\n .flatMap((line) => {\n try {\n return [JSON.parse(line) as LogEntry];\n } catch {\n return [];\n }\n });\n\n const minRank = query.level ? LEVELS[query.level] : 0;\n const contains = query.contains?.toLowerCase();\n\n const filtered = entries.filter((e) => {\n if (LEVELS[e.level] < minRank) return false;\n if (query.component && e.component !== query.component) return false;\n if (query.since && e.ts < query.since) return false;\n if (contains && !e.message.toLowerCase().includes(contains)) return false;\n return true;\n });\n\n return query.limit && query.limit > 0 ? filtered.slice(-query.limit) : filtered;\n}\n\nexport interface LogSummary {\n total: number;\n byLevel: Record<LogLevel, number>;\n byComponent: Record<string, number>;\n firstTs?: string;\n lastTs?: string;\n recentErrors: LogEntry[];\n}\n\n/** Aggregate the (optionally filtered) log ledger for at-a-glance analysis. */\nexport function summarizeLogs(dataDir: string, query: LogQuery = {}): LogSummary {\n const entries = queryLogs(dataDir, query);\n const byLevel: Record<LogLevel, number> = { debug: 0, info: 0, warn: 0, error: 0 };\n const byComponent: Record<string, number> = {};\n\n for (const e of entries) {\n byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;\n byComponent[e.component] = (byComponent[e.component] ?? 0) + 1;\n }\n\n const summary: LogSummary = {\n total: entries.length,\n byLevel,\n byComponent,\n recentErrors: entries.filter((e) => e.level === \"error\").slice(-5),\n };\n if (entries.length > 0) {\n summary.firstTs = entries[0]!.ts;\n summary.lastTs = entries[entries.length - 1]!.ts;\n }\n return summary;\n}\n"],"mappings":";;;AAaA,MAAM,SAAmC;CAAE,OAAO;CAAI,MAAM;CAAI,MAAM;CAAI,OAAO;AAAG;AAUpF,SAAS,eAAe,SAA0B;CAChD,OAAO,WAAW,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;AACjE;AAEA,SAAgB,SAAS,SAAyB;CAChD,OAAO,KAAK,KAAK,SAAS,YAAY,aAAa;AACrD;AAEA,SAAS,WAAmB;CAE1B,OAAO,QADM,QAAQ,IAAI,sBAAsB,QAAQ,YACvC,MAAkB,OAAO;AAC3C;;AAGA,SAAS,WAAmB;CAC1B,MAAM,IAAI,SAAS,QAAQ,IAAI,0BAA0B,IAAI,EAAE;CAC/D,OAAO,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,IAAI;AAC3C;;AAGA,SAAS,WAAmB;CAC1B,MAAM,IAAI,SAAS,QAAQ,IAAI,0BAA0B,IAAI,EAAE;CAC/D,OAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;;;;;;;AAQA,SAAS,eAAe,GAAW,eAA6B;CAC9D,IAAI,OAAO;CACX,IAAI;EACF,OAAO,GAAG,SAAS,CAAC,EAAE;CACxB,QAAQ;EACN;CACF;CACA,IAAI,OAAO,iBAAiB,SAAS,GAAG;CAExC,MAAM,OAAO,SAAS;CACtB,IAAI;EACF,IAAI,QAAQ,GAAG;GACb,GAAG,OAAO,GAAG,EAAE,OAAO,KAAK,CAAC;GAC5B;EACF;EACA,GAAG,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,OAAO,KAAK,CAAC;EACzC,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,GAAG,KAC7B,IAAI,GAAG,WAAW,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,WAAW,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,GAAG,IAAI,GAAG;EAE5E,GAAG,WAAW,GAAG,GAAG,EAAE,GAAG;CAC3B,QAAQ,CAER;AACF;;AAGA,SAAgB,IACd,OACA,WACA,SACA,SACA,OAA6B,CAAC,GACxB;CACN,IAAI,OAAO,SAAS,SAAS,GAAG;CAEhC,MAAM,QAAkB;EACtB,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B;EACA;EACA;EACA,GAAI,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;CAClE;CAGA,IAAI;EACF,MAAM,IAAI,SAAS,eAAe,KAAK,OAAO,CAAC;EAC/C,GAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;EACjD,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI;EACrC,eAAe,GAAG,OAAO,WAAW,IAAI,CAAC;EACzC,GAAG,eAAe,GAAG,MAAM,OAAO;CACpC,QAAQ,CAER;CAGA,IAAI,QAAQ,IAAI,wBAAwB,OAAO;EAC7C,MAAM,MAAM,MAAM,UAAU,IAAI,KAAK,UAAU,MAAM,OAAO,MAAM;EAClE,IAAI;GACF,QAAQ,OAAO,MAAM,IAAI,UAAU,IAAI,UAAU,IAAI,GAAG;EAC1D,QAAQ,CAER;CACF;AACF;AAEA,MAAa,SAAS;CACpB,QAAQ,WAAmB,SAAiB,YAC1C,IAAI,SAAS,WAAW,SAAS,OAAO;CAC1C,OAAO,WAAmB,SAAiB,YACzC,IAAI,QAAQ,WAAW,SAAS,OAAO;CACzC,OAAO,WAAmB,SAAiB,YACzC,IAAI,QAAQ,WAAW,SAAS,OAAO;CACzC,QAAQ,WAAmB,SAAiB,YAC1C,IAAI,SAAS,WAAW,SAAS,OAAO;AAC5C;;AAwBA,SAAS,eAAe,GAAmB;CACzC,MAAM,QAAkB,CAAC;CACzB,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK;EACpC,MAAM,UAAU,GAAG,EAAE,GAAG;EACxB,IAAI,GAAG,WAAW,OAAO,GACvB,IAAI;GACF,MAAM,KAAK,GAAG,aAAa,SAAS,OAAO,CAAW;EACxD,QAAQ,CAER;CAEJ;CACA,IAAI,GAAG,WAAW,CAAC,GACjB,IAAI;EACF,MAAM,KAAK,GAAG,aAAa,GAAG,OAAO,CAAW;CAClD,QAAQ,CAER;CAEF,OAAO,MAAM,KAAK,EAAE;AACtB;;AAGA,SAAgB,UAAU,SAAiB,QAAkB,CAAC,GAAe;CAC3E,MAAM,UAAU,eAAe,SAAS,OAAO,CAAC,EAC7C,MAAM,IAAI,EACV,OAAO,OAAO,EACd,SAAS,SAAS;EACjB,IAAI;GACF,OAAO,CAAC,KAAK,MAAM,IAAI,CAAa;EACtC,QAAQ;GACN,OAAO,CAAC;EACV;CACF,CAAC;CAEH,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,SAAS;CACpD,MAAM,WAAW,MAAM,UAAU,YAAY;CAE7C,MAAM,WAAW,QAAQ,QAAQ,MAAM;EACrC,IAAI,OAAO,EAAE,SAAS,SAAS,OAAO;EACtC,IAAI,MAAM,aAAa,EAAE,cAAc,MAAM,WAAW,OAAO;EAC/D,IAAI,MAAM,SAAS,EAAE,KAAK,MAAM,OAAO,OAAO;EAC9C,IAAI,YAAY,CAAC,EAAE,QAAQ,YAAY,EAAE,SAAS,QAAQ,GAAG,OAAO;EACpE,OAAO;CACT,CAAC;CAED,OAAO,MAAM,SAAS,MAAM,QAAQ,IAAI,SAAS,MAAM,CAAC,MAAM,KAAK,IAAI;AACzE;;AAYA,SAAgB,cAAc,SAAiB,QAAkB,CAAC,GAAe;CAC/E,MAAM,UAAU,UAAU,SAAS,KAAK;CACxC,MAAM,UAAoC;EAAE,OAAO;EAAG,MAAM;EAAG,MAAM;EAAG,OAAO;CAAE;CACjF,MAAM,cAAsC,CAAC;CAE7C,KAAK,MAAM,KAAK,SAAS;EACvB,QAAQ,EAAE,UAAU,QAAQ,EAAE,UAAU,KAAK;EAC7C,YAAY,EAAE,cAAc,YAAY,EAAE,cAAc,KAAK;CAC/D;CAEA,MAAM,UAAsB;EAC1B,OAAO,QAAQ;EACf;EACA;EACA,cAAc,QAAQ,QAAQ,MAAM,EAAE,UAAU,OAAO,EAAE,MAAM,EAAE;CACnE;CACA,IAAI,QAAQ,SAAS,GAAG;EACtB,QAAQ,UAAU,QAAQ,GAAI;EAC9B,QAAQ,SAAS,QAAQ,QAAQ,SAAS,GAAI;CAChD;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
//#region src/core/logger.ts
|
|
4
|
+
const LEVELS = {
|
|
5
|
+
debug: 10,
|
|
6
|
+
info: 20,
|
|
7
|
+
warn: 30,
|
|
8
|
+
error: 40
|
|
9
|
+
};
|
|
10
|
+
function resolveDataDir(dataDir) {
|
|
11
|
+
return dataDir ?? process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
12
|
+
}
|
|
13
|
+
function logsPath(dataDir) {
|
|
14
|
+
return path.join(dataDir, ".agentic", "logs.ndjson");
|
|
15
|
+
}
|
|
16
|
+
function minLevel() {
|
|
17
|
+
return LEVELS[(process.env["DXCRM_LOG_LEVEL"] ?? "info").toLowerCase()] ?? LEVELS.info;
|
|
18
|
+
}
|
|
19
|
+
/** Max bytes for the active ledger before it rotates (default 5 MB). */
|
|
20
|
+
function maxBytes() {
|
|
21
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_BYTES"] ?? "", 10);
|
|
22
|
+
return Number.isFinite(n) && n > 0 ? n : 5e6;
|
|
23
|
+
}
|
|
24
|
+
/** Number of rotated archives to keep (default 5; 0 = truncate, no archives). */
|
|
25
|
+
function maxFiles() {
|
|
26
|
+
const n = parseInt(process.env["DXCRM_LOG_MAX_FILES"] ?? "", 10);
|
|
27
|
+
return Number.isFinite(n) && n >= 0 ? n : 5;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Size-based log rotation. Before an append would push the active ledger past
|
|
31
|
+
* the byte budget, the archives shift (logs.ndjson.1 → .2 → … dropping the
|
|
32
|
+
* oldest) and the current ledger becomes logs.ndjson.1, leaving a fresh active
|
|
33
|
+
* file. Best-effort: any fs error here is swallowed so logging never breaks.
|
|
34
|
+
*/
|
|
35
|
+
function rotateIfNeeded(p, incomingBytes) {
|
|
36
|
+
let size = 0;
|
|
37
|
+
try {
|
|
38
|
+
size = fs.statSync(p).size;
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (size + incomingBytes <= maxBytes()) return;
|
|
43
|
+
const keep = maxFiles();
|
|
44
|
+
try {
|
|
45
|
+
if (keep <= 0) {
|
|
46
|
+
fs.rmSync(p, { force: true });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
fs.rmSync(`${p}.${keep}`, { force: true });
|
|
50
|
+
for (let i = keep - 1; i >= 1; i--) if (fs.existsSync(`${p}.${i}`)) fs.renameSync(`${p}.${i}`, `${p}.${i + 1}`);
|
|
51
|
+
fs.renameSync(p, `${p}.1`);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
/** Core log primitive. Honors DXCRM_LOG_LEVEL; mirrors to stderr unless off. */
|
|
55
|
+
function log(level, component, message, context, opts = {}) {
|
|
56
|
+
if (LEVELS[level] < minLevel()) return;
|
|
57
|
+
const entry = {
|
|
58
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59
|
+
level,
|
|
60
|
+
component,
|
|
61
|
+
message,
|
|
62
|
+
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const p = logsPath(resolveDataDir(opts.dataDir));
|
|
66
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
67
|
+
const line = JSON.stringify(entry) + "\n";
|
|
68
|
+
rotateIfNeeded(p, Buffer.byteLength(line));
|
|
69
|
+
fs.appendFileSync(p, line, "utf-8");
|
|
70
|
+
} catch {}
|
|
71
|
+
if (process.env["DXCRM_LOG_STDERR"] !== "off") {
|
|
72
|
+
const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : "";
|
|
73
|
+
try {
|
|
74
|
+
process.stderr.write(`[${component}] ${message}${ctx}\n`);
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const logger = {
|
|
79
|
+
debug: (component, message, context) => log("debug", component, message, context),
|
|
80
|
+
info: (component, message, context) => log("info", component, message, context),
|
|
81
|
+
warn: (component, message, context) => log("warn", component, message, context),
|
|
82
|
+
error: (component, message, context) => log("error", component, message, context)
|
|
83
|
+
};
|
|
84
|
+
/** Read the active ledger plus rotated archives, oldest first. */
|
|
85
|
+
function readLedgerText(p) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (let i = maxFiles(); i >= 1; i--) {
|
|
88
|
+
const archive = `${p}.${i}`;
|
|
89
|
+
if (fs.existsSync(archive)) try {
|
|
90
|
+
parts.push(fs.readFileSync(archive, "utf-8"));
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
if (fs.existsSync(p)) try {
|
|
94
|
+
parts.push(fs.readFileSync(p, "utf-8"));
|
|
95
|
+
} catch {}
|
|
96
|
+
return parts.join("");
|
|
97
|
+
}
|
|
98
|
+
/** Read the log ledger (incl. rotated archives), skipping malformed lines, and filter. */
|
|
99
|
+
function queryLogs(dataDir, query = {}) {
|
|
100
|
+
const entries = readLedgerText(logsPath(dataDir)).split("\n").filter(Boolean).flatMap((line) => {
|
|
101
|
+
try {
|
|
102
|
+
return [JSON.parse(line)];
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const minRank = query.level ? LEVELS[query.level] : 0;
|
|
108
|
+
const contains = query.contains?.toLowerCase();
|
|
109
|
+
const filtered = entries.filter((e) => {
|
|
110
|
+
if (LEVELS[e.level] < minRank) return false;
|
|
111
|
+
if (query.component && e.component !== query.component) return false;
|
|
112
|
+
if (query.since && e.ts < query.since) return false;
|
|
113
|
+
if (contains && !e.message.toLowerCase().includes(contains)) return false;
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
return query.limit && query.limit > 0 ? filtered.slice(-query.limit) : filtered;
|
|
117
|
+
}
|
|
118
|
+
/** Aggregate the (optionally filtered) log ledger for at-a-glance analysis. */
|
|
119
|
+
function summarizeLogs(dataDir, query = {}) {
|
|
120
|
+
const entries = queryLogs(dataDir, query);
|
|
121
|
+
const byLevel = {
|
|
122
|
+
debug: 0,
|
|
123
|
+
info: 0,
|
|
124
|
+
warn: 0,
|
|
125
|
+
error: 0
|
|
126
|
+
};
|
|
127
|
+
const byComponent = {};
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;
|
|
130
|
+
byComponent[e.component] = (byComponent[e.component] ?? 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
const summary = {
|
|
133
|
+
total: entries.length,
|
|
134
|
+
byLevel,
|
|
135
|
+
byComponent,
|
|
136
|
+
recentErrors: entries.filter((e) => e.level === "error").slice(-5)
|
|
137
|
+
};
|
|
138
|
+
if (entries.length > 0) {
|
|
139
|
+
summary.firstTs = entries[0].ts;
|
|
140
|
+
summary.lastTs = entries[entries.length - 1].ts;
|
|
141
|
+
}
|
|
142
|
+
return summary;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { queryLogs as n, summarizeLogs as r, logger as t };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=logger-UaF5p9d1.js.map
|