@datasynx/agentic-crm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +767 -0
- package/dist/agent-config-zPvcqu07.js +14 -0
- package/dist/agent-config-zPvcqu07.js.map +1 -0
- package/dist/approvals-DpjxGHFp.js +67 -0
- package/dist/approvals-DpjxGHFp.js.map +1 -0
- package/dist/ask-CID3jnuL.js +52 -0
- package/dist/ask-CID3jnuL.js.map +1 -0
- package/dist/audit-log-DNMY9mUZ.js +49 -0
- package/dist/audit-log-DNMY9mUZ.js.map +1 -0
- package/dist/auth-CyFuu9X_.js +2 -0
- package/dist/auth-DFWwWcYD.js +93 -0
- package/dist/auth-DFWwWcYD.js.map +1 -0
- package/dist/autofill-Di_-SP7t.js +51 -0
- package/dist/autofill-Di_-SP7t.js.map +1 -0
- package/dist/backup-CeMk9z86.js +417 -0
- package/dist/backup-CeMk9z86.js.map +1 -0
- package/dist/backup-f_hC7rBV.js +2 -0
- package/dist/calendly-Bft_wwji.js +52 -0
- package/dist/calendly-Bft_wwji.js.map +1 -0
- package/dist/calendly-D3coO92o.cjs +53 -0
- package/dist/calendly-D3coO92o.cjs.map +1 -0
- package/dist/chunk-DakpK96I.cjs +43 -0
- package/dist/churn-C28IgnAj.js +54 -0
- package/dist/churn-C28IgnAj.js.map +1 -0
- package/dist/cli.js +4396 -0
- package/dist/cli.js.map +1 -0
- package/dist/colors-BG07TZQz.js +11 -0
- package/dist/colors-BG07TZQz.js.map +1 -0
- package/dist/compliance-B1kk5-YS.js +115 -0
- package/dist/compliance-B1kk5-YS.js.map +1 -0
- package/dist/compliance-B91zNvCR.cjs +156 -0
- package/dist/compliance-B91zNvCR.cjs.map +1 -0
- package/dist/compliance-CKSBoQUe.js +118 -0
- package/dist/compliance-CKSBoQUe.js.map +1 -0
- package/dist/compliance-CujOqAKk.js +2 -0
- package/dist/context-builder-BzWAp3Zs.js +96 -0
- package/dist/context-builder-BzWAp3Zs.js.map +1 -0
- package/dist/context-builder-DlrRcqmJ.js +2 -0
- package/dist/conversation-intel-mm7Lhemh.js +72 -0
- package/dist/conversation-intel-mm7Lhemh.js.map +1 -0
- package/dist/custom-fields-CzNeD3_v.js +2 -0
- package/dist/custom-fields-Pl2t9xzp.js +73 -0
- package/dist/custom-fields-Pl2t9xzp.js.map +1 -0
- package/dist/custom-objects-BHgn1GEX.js +78 -0
- package/dist/custom-objects-BHgn1GEX.js.map +1 -0
- package/dist/custom-objects-CIFrmQ2V.js +2 -0
- package/dist/customer-dir-DIylZ8Q6.js +75 -0
- package/dist/customer-dir-DIylZ8Q6.js.map +1 -0
- package/dist/daemon/worker.js +207 -0
- package/dist/daemon/worker.js.map +1 -0
- package/dist/enrichment-3XvgGDfB.js +103 -0
- package/dist/enrichment-3XvgGDfB.js.map +1 -0
- package/dist/file-lock-B_zi7NQl.js +22 -0
- package/dist/file-lock-B_zi7NQl.js.map +1 -0
- package/dist/gmail-auth-BP6cJwfw.js +40 -0
- package/dist/gmail-auth-BP6cJwfw.js.map +1 -0
- package/dist/gmail-auth-DxakCtGm.cjs +44 -0
- package/dist/gmail-auth-DxakCtGm.cjs.map +1 -0
- package/dist/gmail-auth-OComS92L.js +40 -0
- package/dist/gmail-auth-OComS92L.js.map +1 -0
- package/dist/gmail-push-watch-DELQFMPk.js +20 -0
- package/dist/gmail-push-watch-DELQFMPk.js.map +1 -0
- package/dist/gmail-sender-StTpJ9Ub.js +32 -0
- package/dist/gmail-sender-StTpJ9Ub.js.map +1 -0
- package/dist/gmail-sync-DIaxInDT.js +204 -0
- package/dist/gmail-sync-DIaxInDT.js.map +1 -0
- package/dist/gmail-sync-hHm9gaWd.cjs +218 -0
- package/dist/gmail-sync-hHm9gaWd.cjs.map +1 -0
- package/dist/gmail-sync-rQaVqKWd.js +214 -0
- package/dist/gmail-sync-rQaVqKWd.js.map +1 -0
- package/dist/gmail-webhook-handler-DS7OlRPX.js +3 -0
- package/dist/gmail-webhook-handler-e5Od25FX.js +97 -0
- package/dist/gmail-webhook-handler-e5Od25FX.js.map +1 -0
- package/dist/goal-engine-CUZSpERI.js +2 -0
- package/dist/goal-engine-KpBftn4V.js +295 -0
- package/dist/goal-engine-KpBftn4V.js.map +1 -0
- package/dist/google-drive-sync-DEPcqFca.js +105 -0
- package/dist/google-drive-sync-DEPcqFca.js.map +1 -0
- package/dist/hybrid-search-BmHttLrR.js +40 -0
- package/dist/hybrid-search-BmHttLrR.js.map +1 -0
- package/dist/hygiene-DZqfYpFf.js +38 -0
- package/dist/hygiene-DZqfYpFf.js.map +1 -0
- package/dist/identity-CI6olMNm.js +41 -0
- package/dist/identity-CI6olMNm.js.map +1 -0
- package/dist/identity-gyfWdrcX.js +2 -0
- package/dist/import-hubspot-BaK71U_K.js +588 -0
- package/dist/import-hubspot-BaK71U_K.js.map +1 -0
- package/dist/index-V8BFaH-b.d.ts +539 -0
- package/dist/index-V8BFaH-b.d.ts.map +1 -0
- package/dist/index-YqwMd6aQ.d.cts +538 -0
- package/dist/index-YqwMd6aQ.d.cts.map +1 -0
- package/dist/index.cjs +185 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +538 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +539 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions-writer-CrPStUll.cjs +77 -0
- package/dist/interactions-writer-CrPStUll.cjs.map +1 -0
- package/dist/interactions-writer-DO3KcSR3.js +52 -0
- package/dist/interactions-writer-DO3KcSR3.js.map +1 -0
- package/dist/interactions-writer-SLHnoEeE.js +46 -0
- package/dist/interactions-writer-SLHnoEeE.js.map +1 -0
- package/dist/interactions-writer-dSPy1XfO.js +2 -0
- package/dist/knowledge-base-D0Fh40kc.js +1013 -0
- package/dist/knowledge-base-D0Fh40kc.js.map +1 -0
- package/dist/lancedb-CCBbpulq.js +2 -0
- package/dist/lancedb-rlvWoPwl.js +98 -0
- package/dist/lancedb-rlvWoPwl.js.map +1 -0
- package/dist/lead-model-BCFzyktm.js +109 -0
- package/dist/lead-model-BCFzyktm.js.map +1 -0
- package/dist/llm-DEjWcqmW.js +2 -0
- package/dist/llm-DvzZqva0.js +372 -0
- package/dist/llm-DvzZqva0.js.map +1 -0
- package/dist/llm-Z8RIYkpF.js +174 -0
- package/dist/llm-Z8RIYkpF.js.map +1 -0
- package/dist/llm-iijeXmgq.cjs +198 -0
- package/dist/llm-iijeXmgq.cjs.map +1 -0
- package/dist/mcp-CdTJWTJf.d.cts +12 -0
- package/dist/mcp-CdTJWTJf.d.cts.map +1 -0
- package/dist/mcp-CdTJWTJf.d.ts +12 -0
- package/dist/mcp-CdTJWTJf.d.ts.map +1 -0
- package/dist/mcp.cjs +7464 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +12 -0
- package/dist/mcp.d.cts.map +1 -0
- package/dist/mcp.d.ts +12 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +7448 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory-Bb6ky3kb.js +58 -0
- package/dist/memory-Bb6ky3kb.js.map +1 -0
- package/dist/memory-Cy6-Tbyl.js +2 -0
- package/dist/metrics-DH8wHvya.js +26 -0
- package/dist/metrics-DH8wHvya.js.map +1 -0
- package/dist/microsoft-auth-B8_S45gh.js +17 -0
- package/dist/microsoft-auth-B8_S45gh.js.map +1 -0
- package/dist/microsoft-calendar-B6MMtUQK.js +67 -0
- package/dist/microsoft-calendar-B6MMtUQK.js.map +1 -0
- package/dist/microsoft-sync-CpZVoSuq.js +68 -0
- package/dist/microsoft-sync-CpZVoSuq.js.map +1 -0
- package/dist/nba-3wanmJ0U.js +48 -0
- package/dist/nba-3wanmJ0U.js.map +1 -0
- package/dist/notification-dispatcher-0vYNngWe.js +97 -0
- package/dist/notification-dispatcher-0vYNngWe.js.map +1 -0
- package/dist/opportunity-score-BTMOQSTV.js +47 -0
- package/dist/opportunity-score-BTMOQSTV.js.map +1 -0
- package/dist/pipedrive-client-CdGKpH9b.js +17 -0
- package/dist/pipedrive-client-CdGKpH9b.js.map +1 -0
- package/dist/pipeline-writer-BqBrYrQc.js +2 -0
- package/dist/pipeline-writer-BvVquKIe.js +96 -0
- package/dist/pipeline-writer-BvVquKIe.js.map +1 -0
- package/dist/pipeline-writer-N2omexxp.cjs +121 -0
- package/dist/pipeline-writer-N2omexxp.cjs.map +1 -0
- package/dist/pipeline-writer-eufx_0o1.js +102 -0
- package/dist/pipeline-writer-eufx_0o1.js.map +1 -0
- package/dist/proactive-agent-BgQXw3ac.js +96 -0
- package/dist/proactive-agent-BgQXw3ac.js.map +1 -0
- package/dist/proactive-worker-BrLHNhjH.js +229 -0
- package/dist/proactive-worker-BrLHNhjH.js.map +1 -0
- package/dist/push-manager-CdqIIkuh.js +108 -0
- package/dist/push-manager-CdqIIkuh.js.map +1 -0
- package/dist/push-manager-CowY-0IK.js +2 -0
- package/dist/quote-generator-BfwENXzg.js +133 -0
- package/dist/quote-generator-BfwENXzg.js.map +1 -0
- package/dist/quote-generator-OhSFsi3x.js +2 -0
- package/dist/rbac-C7c8tcES.js +2 -0
- package/dist/rbac-CTIktZaC.js +91 -0
- package/dist/rbac-CTIktZaC.js.map +1 -0
- package/dist/relationship-health-odxEoQdJ.js +454 -0
- package/dist/relationship-health-odxEoQdJ.js.map +1 -0
- package/dist/revenue-simulation-BJdRTEHc.js +2 -0
- package/dist/revenue-simulation-Bqf2DLVB.js +251 -0
- package/dist/revenue-simulation-Bqf2DLVB.js.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/salesforce-client-rhZFa_p5.js +51 -0
- package/dist/salesforce-client-rhZFa_p5.js.map +1 -0
- package/dist/segments-BqcD5HIl.js +61 -0
- package/dist/segments-BqcD5HIl.js.map +1 -0
- package/dist/sequence-engine-CCTHEBgi.js +2 -0
- package/dist/sequence-engine-J1lTW_in.js +91 -0
- package/dist/sequence-engine-J1lTW_in.js.map +1 -0
- package/dist/sequence-store-DaaWr0Os.js +221 -0
- package/dist/sequence-store-DaaWr0Os.js.map +1 -0
- package/dist/server-Dyva03K8.js +4287 -0
- package/dist/server-Dyva03K8.js.map +1 -0
- package/dist/session-B9AilxOE.js +81 -0
- package/dist/session-B9AilxOE.js.map +1 -0
- package/dist/session-D0qFkBla.cjs +82 -0
- package/dist/session-D0qFkBla.cjs.map +1 -0
- package/dist/session-D9ub6Wl1.js +79 -0
- package/dist/session-D9ub6Wl1.js.map +1 -0
- package/dist/session-mWHA71Lw.js +2 -0
- package/dist/session-store-B0QZE8Bx.cjs +697 -0
- package/dist/session-store-B0QZE8Bx.cjs.map +1 -0
- package/dist/session-store-C8tEvMPw.js +543 -0
- package/dist/session-store-C8tEvMPw.js.map +1 -0
- package/dist/session-store-CEa39Dxs.js +15 -0
- package/dist/session-store-CEa39Dxs.js.map +1 -0
- package/dist/sla-engine-5IhTsBUR.js +2 -0
- package/dist/sla-engine-BqX-7u-7.js +53 -0
- package/dist/sla-engine-BqX-7u-7.js.map +1 -0
- package/dist/sop-DkhVChGy.js +2 -0
- package/dist/sop-Vp0UPWFW.js +70 -0
- package/dist/sop-Vp0UPWFW.js.map +1 -0
- package/dist/survey-engine-C06hcQt3.js +2 -0
- package/dist/survey-engine-DBjCYqCv.js +147 -0
- package/dist/survey-engine-DBjCYqCv.js.map +1 -0
- package/dist/sync-state-ChaLbamC.js +33 -0
- package/dist/sync-state-ChaLbamC.js.map +1 -0
- package/dist/sync-state-CwLSt_1m.js +2 -0
- package/dist/ticket-writer-CjqKeIRD.js +2 -0
- package/dist/ticket-writer-j2oX_Wal.js +134 -0
- package/dist/ticket-writer-j2oX_Wal.js.map +1 -0
- package/dist/tone-Bdm5uaht.js +48 -0
- package/dist/tone-Bdm5uaht.js.map +1 -0
- package/dist/tone-DRKlZgPr.cjs +43 -0
- package/dist/tone-DRKlZgPr.cjs.map +1 -0
- package/dist/tone-vNb2DAAD.js +39 -0
- package/dist/tone-vNb2DAAD.js.map +1 -0
- package/dist/transcript-watcher-CL2QUygI.js +132 -0
- package/dist/transcript-watcher-CL2QUygI.js.map +1 -0
- package/dist/unmatched-transcripts-BsH5bhkU.js +26 -0
- package/dist/unmatched-transcripts-BsH5bhkU.js.map +1 -0
- package/dist/unmatched-transcripts-D0PrJ9iz.js +2 -0
- package/dist/update-deal-BNwPGaTV.js +2 -0
- package/dist/update-deal-DKC79skb.js +91 -0
- package/dist/update-deal-DKC79skb.js.map +1 -0
- package/dist/usage-CClTf5e6.cjs +57 -0
- package/dist/usage-CClTf5e6.cjs.map +1 -0
- package/dist/usage-D0-TYJkw.js +93 -0
- package/dist/usage-D0-TYJkw.js.map +1 -0
- package/dist/usage-D0u9a-lV.js +54 -0
- package/dist/usage-D0u9a-lV.js.map +1 -0
- package/dist/vault-C1D3zScD.js +2 -0
- package/dist/vault-DXCg29W-.js +86 -0
- package/dist/vault-DXCg29W-.js.map +1 -0
- package/dist/webhooks-7EpA05Qr.js +138 -0
- package/dist/webhooks-7EpA05Qr.js.map +1 -0
- package/dist/webhooks-BO2UAnmn.js +94 -0
- package/dist/webhooks-BO2UAnmn.js.map +1 -0
- package/dist/webhooks-Xn6zO6kd.cjs +97 -0
- package/dist/webhooks-Xn6zO6kd.cjs.map +1 -0
- package/dist/write-queue-BDolUxfs.cjs +26 -0
- package/dist/write-queue-BDolUxfs.cjs.map +1 -0
- package/dist/write-queue-IbsAjUnh.js +21 -0
- package/dist/write-queue-IbsAjUnh.js.map +1 -0
- package/package.json +142 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/core/conversation-intel.ts
|
|
2
|
+
/**
|
|
3
|
+
* Conversation-Intelligence-Lite (domino D16 / C7): deterministic, offline call
|
|
4
|
+
* analytics over a speaker-labelled transcript — talk-ratio, discovery-question
|
|
5
|
+
* count, objection detection and longest monologue — plus rule-based coaching
|
|
6
|
+
* tips. Reuses the D9 objection heuristic. The heavy lifting (ASR, diarization,
|
|
7
|
+
* realtime) lives in the agent/voice framework, not in this package; here we
|
|
8
|
+
* turn an existing transcript into structured, actionable signal.
|
|
9
|
+
*/
|
|
10
|
+
const OBJECTION_RE = /\b(concern|worried|too expensive|expensive|hesitant|however|push back|budget)\b/i;
|
|
11
|
+
const DEFAULT_REP_LABELS = [
|
|
12
|
+
"rep",
|
|
13
|
+
"sales",
|
|
14
|
+
"ae",
|
|
15
|
+
"me",
|
|
16
|
+
"agent"
|
|
17
|
+
];
|
|
18
|
+
const TURN_RE = /^([A-Za-z][\w .'-]{0,40}):\s*(.*)$/;
|
|
19
|
+
/** Parse "Speaker: text" lines into turns; lines without a label are ignored. */
|
|
20
|
+
function parseTurns(transcript, repLabels = DEFAULT_REP_LABELS) {
|
|
21
|
+
const reps = repLabels.map((r) => r.toLowerCase());
|
|
22
|
+
const turns = [];
|
|
23
|
+
for (const raw of transcript.split("\n")) {
|
|
24
|
+
const line = raw.trim();
|
|
25
|
+
if (!line) continue;
|
|
26
|
+
const m = line.match(TURN_RE);
|
|
27
|
+
if (!m) continue;
|
|
28
|
+
const speaker = m[1].trim();
|
|
29
|
+
const text = m[2].trim();
|
|
30
|
+
const isRep = reps.some((r) => speaker.toLowerCase().includes(r));
|
|
31
|
+
turns.push({
|
|
32
|
+
speaker,
|
|
33
|
+
text,
|
|
34
|
+
words: text.split(/\s+/).filter(Boolean).length,
|
|
35
|
+
isRep
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return turns;
|
|
39
|
+
}
|
|
40
|
+
function countQuestions(text) {
|
|
41
|
+
return (text.match(/\?/g) ?? []).length;
|
|
42
|
+
}
|
|
43
|
+
function analyzeConversation(transcript, repLabels = DEFAULT_REP_LABELS) {
|
|
44
|
+
const turns = parseTurns(transcript, repLabels);
|
|
45
|
+
const repWords = turns.filter((t) => t.isRep).reduce((s, t) => s + t.words, 0);
|
|
46
|
+
const totalWords = turns.reduce((s, t) => s + t.words, 0);
|
|
47
|
+
const talkRatio = totalWords > 0 ? repWords / totalWords : 0;
|
|
48
|
+
const questionsAsked = turns.filter((t) => t.isRep).reduce((s, t) => s + countQuestions(t.text), 0);
|
|
49
|
+
const longestMonologue = turns.filter((t) => t.isRep).reduce((m, t) => Math.max(m, t.words), 0);
|
|
50
|
+
const objections = transcript.split(/[\n.]/).map((s) => s.trim()).filter((s) => s && OBJECTION_RE.test(s)).slice(0, 10);
|
|
51
|
+
const coaching = [];
|
|
52
|
+
if (turns.length > 0) {
|
|
53
|
+
if (talkRatio > .65) coaching.push("Talk ratio is high — listen more and ask open questions to draw the customer out.");
|
|
54
|
+
if (talkRatio < .35 && repWords > 0) coaching.push("You spoke very little — make sure you're steering the conversation and confirming next steps.");
|
|
55
|
+
if (questionsAsked < 2) coaching.push("Few discovery questions — ask more to uncover needs and budget.");
|
|
56
|
+
if (longestMonologue > 40) coaching.push("Long monologue detected — break value pitches into shorter, interactive chunks.");
|
|
57
|
+
}
|
|
58
|
+
if (objections.length > 0) coaching.push(`Address ${objections.length} surfaced objection(s) explicitly before closing.`);
|
|
59
|
+
if (coaching.length === 0) coaching.push("Balanced conversation — keep confirming next steps.");
|
|
60
|
+
return {
|
|
61
|
+
turns: turns.length,
|
|
62
|
+
talkRatio: Math.round(talkRatio * 100) / 100,
|
|
63
|
+
questionsAsked,
|
|
64
|
+
longestMonologue,
|
|
65
|
+
objections,
|
|
66
|
+
coaching
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { analyzeConversation };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=conversation-intel-mm7Lhemh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-intel-mm7Lhemh.js","names":[],"sources":["../src/core/conversation-intel.ts"],"sourcesContent":["/**\n * Conversation-Intelligence-Lite (domino D16 / C7): deterministic, offline call\n * analytics over a speaker-labelled transcript — talk-ratio, discovery-question\n * count, objection detection and longest monologue — plus rule-based coaching\n * tips. Reuses the D9 objection heuristic. The heavy lifting (ASR, diarization,\n * realtime) lives in the agent/voice framework, not in this package; here we\n * turn an existing transcript into structured, actionable signal.\n */\nconst OBJECTION_RE =\n /\\b(concern|worried|too expensive|expensive|hesitant|however|push back|budget)\\b/i;\nconst DEFAULT_REP_LABELS = [\"rep\", \"sales\", \"ae\", \"me\", \"agent\"];\n\nexport interface Turn {\n speaker: string;\n text: string;\n words: number;\n isRep: boolean;\n}\n\nexport interface ConversationAnalysis {\n turns: number;\n talkRatio: number; // rep words / total words (0–1)\n questionsAsked: number; // questions asked by the rep\n longestMonologue: number; // words in the rep's longest single turn\n objections: string[];\n coaching: string[];\n}\n\nconst TURN_RE = /^([A-Za-z][\\w .'-]{0,40}):\\s*(.*)$/;\n\n/** Parse \"Speaker: text\" lines into turns; lines without a label are ignored. */\nexport function parseTurns(transcript: string, repLabels: string[] = DEFAULT_REP_LABELS): Turn[] {\n const reps = repLabels.map((r) => r.toLowerCase());\n const turns: Turn[] = [];\n for (const raw of transcript.split(\"\\n\")) {\n const line = raw.trim();\n if (!line) continue;\n const m = line.match(TURN_RE);\n if (!m) continue;\n const speaker = m[1]!.trim();\n const text = m[2]!.trim();\n const isRep = reps.some((r) => speaker.toLowerCase().includes(r));\n turns.push({ speaker, text, words: text.split(/\\s+/).filter(Boolean).length, isRep });\n }\n return turns;\n}\n\nfunction countQuestions(text: string): number {\n return (text.match(/\\?/g) ?? []).length;\n}\n\nexport function analyzeConversation(\n transcript: string,\n repLabels: string[] = DEFAULT_REP_LABELS\n): ConversationAnalysis {\n const turns = parseTurns(transcript, repLabels);\n\n const repWords = turns.filter((t) => t.isRep).reduce((s, t) => s + t.words, 0);\n const totalWords = turns.reduce((s, t) => s + t.words, 0);\n const talkRatio = totalWords > 0 ? repWords / totalWords : 0;\n const questionsAsked = turns\n .filter((t) => t.isRep)\n .reduce((s, t) => s + countQuestions(t.text), 0);\n const longestMonologue = turns.filter((t) => t.isRep).reduce((m, t) => Math.max(m, t.words), 0);\n\n // Objections: scan the whole transcript so unlabeled text still yields signal.\n const objections = transcript\n .split(/[\\n.]/)\n .map((s) => s.trim())\n .filter((s) => s && OBJECTION_RE.test(s))\n .slice(0, 10);\n\n const coaching: string[] = [];\n if (turns.length > 0) {\n if (talkRatio > 0.65)\n coaching.push(\n \"Talk ratio is high — listen more and ask open questions to draw the customer out.\"\n );\n if (talkRatio < 0.35 && repWords > 0)\n coaching.push(\n \"You spoke very little — make sure you're steering the conversation and confirming next steps.\"\n );\n if (questionsAsked < 2)\n coaching.push(\"Few discovery questions — ask more to uncover needs and budget.\");\n if (longestMonologue > 40)\n coaching.push(\n \"Long monologue detected — break value pitches into shorter, interactive chunks.\"\n );\n }\n if (objections.length > 0)\n coaching.push(`Address ${objections.length} surfaced objection(s) explicitly before closing.`);\n if (coaching.length === 0) coaching.push(\"Balanced conversation — keep confirming next steps.\");\n\n return {\n turns: turns.length,\n talkRatio: Math.round(talkRatio * 100) / 100,\n questionsAsked,\n longestMonologue,\n objections,\n coaching,\n };\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,eACJ;AACF,MAAM,qBAAqB;CAAC;CAAO;CAAS;CAAM;CAAM;AAAO;AAkB/D,MAAM,UAAU;;AAGhB,SAAgB,WAAW,YAAoB,YAAsB,oBAA4B;CAC/F,MAAM,OAAO,UAAU,KAAK,MAAM,EAAE,YAAY,CAAC;CACjD,MAAM,QAAgB,CAAC;CACvB,KAAK,MAAM,OAAO,WAAW,MAAM,IAAI,GAAG;EACxC,MAAM,OAAO,IAAI,KAAK;EACtB,IAAI,CAAC,MAAM;EACX,MAAM,IAAI,KAAK,MAAM,OAAO;EAC5B,IAAI,CAAC,GAAG;EACR,MAAM,UAAU,EAAE,GAAI,KAAK;EAC3B,MAAM,OAAO,EAAE,GAAI,KAAK;EACxB,MAAM,QAAQ,KAAK,MAAM,MAAM,QAAQ,YAAY,EAAE,SAAS,CAAC,CAAC;EAChE,MAAM,KAAK;GAAE;GAAS;GAAM,OAAO,KAAK,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE;GAAQ;EAAM,CAAC;CACtF;CACA,OAAO;AACT;AAEA,SAAS,eAAe,MAAsB;CAC5C,QAAQ,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG;AACnC;AAEA,SAAgB,oBACd,YACA,YAAsB,oBACA;CACtB,MAAM,QAAQ,WAAW,YAAY,SAAS;CAE9C,MAAM,WAAW,MAAM,QAAQ,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;CAC7E,MAAM,aAAa,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;CACxD,MAAM,YAAY,aAAa,IAAI,WAAW,aAAa;CAC3D,MAAM,iBAAiB,MACpB,QAAQ,MAAM,EAAE,KAAK,EACrB,QAAQ,GAAG,MAAM,IAAI,eAAe,EAAE,IAAI,GAAG,CAAC;CACjD,MAAM,mBAAmB,MAAM,QAAQ,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,KAAK,GAAG,CAAC;CAG9F,MAAM,aAAa,WAChB,MAAM,OAAO,EACb,KAAK,MAAM,EAAE,KAAK,CAAC,EACnB,QAAQ,MAAM,KAAK,aAAa,KAAK,CAAC,CAAC,EACvC,MAAM,GAAG,EAAE;CAEd,MAAM,WAAqB,CAAC;CAC5B,IAAI,MAAM,SAAS,GAAG;EACpB,IAAI,YAAY,KACd,SAAS,KACP,mFACF;EACF,IAAI,YAAY,OAAQ,WAAW,GACjC,SAAS,KACP,+FACF;EACF,IAAI,iBAAiB,GACnB,SAAS,KAAK,iEAAiE;EACjF,IAAI,mBAAmB,IACrB,SAAS,KACP,iFACF;CACJ;CACA,IAAI,WAAW,SAAS,GACtB,SAAS,KAAK,WAAW,WAAW,OAAO,kDAAkD;CAC/F,IAAI,SAAS,WAAW,GAAG,SAAS,KAAK,qDAAqD;CAE9F,OAAO;EACL,OAAO,MAAM;EACb,WAAW,KAAK,MAAM,YAAY,GAAG,IAAI;EACzC;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
//#region src/core/custom-fields.ts
|
|
4
|
+
function schemaPath(dataDir) {
|
|
5
|
+
return path.join(dataDir, ".agentic", "schema", "custom-fields.json");
|
|
6
|
+
}
|
|
7
|
+
function loadFieldDefinitions(dataDir) {
|
|
8
|
+
const p = schemaPath(dataDir);
|
|
9
|
+
if (!fs.existsSync(p)) return [];
|
|
10
|
+
try {
|
|
11
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
12
|
+
return Array.isArray(data.fields) ? data.fields : [];
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Add or update (by name) a custom field definition. */
|
|
18
|
+
function defineCustomField(dataDir, def) {
|
|
19
|
+
const defs = loadFieldDefinitions(dataDir);
|
|
20
|
+
const idx = defs.findIndex((d) => d.name === def.name);
|
|
21
|
+
if (idx >= 0) defs[idx] = def;
|
|
22
|
+
else defs.push(def);
|
|
23
|
+
const p = schemaPath(dataDir);
|
|
24
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
25
|
+
fs.writeFileSync(p, JSON.stringify({ fields: defs }, null, 2), "utf-8");
|
|
26
|
+
return defs;
|
|
27
|
+
}
|
|
28
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
29
|
+
/** Validate + coerce a record of raw values against custom field definitions. */
|
|
30
|
+
function validateCustomFields(input, defs) {
|
|
31
|
+
const byName = new Map(defs.map((d) => [d.name, d]));
|
|
32
|
+
const values = {};
|
|
33
|
+
const errors = [];
|
|
34
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
35
|
+
const def = byName.get(key);
|
|
36
|
+
if (!def) {
|
|
37
|
+
errors.push(`Unknown custom field: ${key}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const str = String(raw).trim();
|
|
41
|
+
switch (def.type) {
|
|
42
|
+
case "number": {
|
|
43
|
+
const n = Number(str);
|
|
44
|
+
if (!Number.isFinite(n)) errors.push(`${key}: not a number`);
|
|
45
|
+
else values[key] = n;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "boolean":
|
|
49
|
+
if (/^(true|yes|1)$/i.test(str)) values[key] = true;
|
|
50
|
+
else if (/^(false|no|0)$/i.test(str)) values[key] = false;
|
|
51
|
+
else errors.push(`${key}: not a boolean`);
|
|
52
|
+
break;
|
|
53
|
+
case "date":
|
|
54
|
+
if (DATE_RE.test(str)) values[key] = str;
|
|
55
|
+
else errors.push(`${key}: expected YYYY-MM-DD`);
|
|
56
|
+
break;
|
|
57
|
+
case "select":
|
|
58
|
+
if (def.options && def.options.includes(str)) values[key] = str;
|
|
59
|
+
else errors.push(`${key}: must be one of ${(def.options ?? []).join(", ")}`);
|
|
60
|
+
break;
|
|
61
|
+
default: values[key] = str;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
valid: errors.length === 0,
|
|
66
|
+
values,
|
|
67
|
+
errors
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { loadFieldDefinitions as n, validateCustomFields as r, defineCustomField as t };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=custom-fields-Pl2t9xzp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-fields-Pl2t9xzp.js","names":[],"sources":["../src/core/custom-fields.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Metadata-driven custom fields — the first increment of the metadata model\n * (next-plan N1-7). Definitions live in .agentic/schema/custom-fields.json and\n * extend customers without code changes. Core schemas stay strict; custom\n * fields are validated separately against this registry.\n */\nexport type CustomFieldType = \"text\" | \"number\" | \"boolean\" | \"date\" | \"select\";\n\nexport interface FieldDefinition {\n name: string;\n type: CustomFieldType;\n label?: string;\n options?: string[];\n}\n\nfunction schemaPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"schema\", \"custom-fields.json\");\n}\n\nexport function loadFieldDefinitions(dataDir: string): FieldDefinition[] {\n const p = schemaPath(dataDir);\n if (!fs.existsSync(p)) return [];\n try {\n const data = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as {\n fields?: FieldDefinition[];\n };\n return Array.isArray(data.fields) ? data.fields : [];\n } catch {\n return [];\n }\n}\n\n/** Add or update (by name) a custom field definition. */\nexport function defineCustomField(dataDir: string, def: FieldDefinition): FieldDefinition[] {\n const defs = loadFieldDefinitions(dataDir);\n const idx = defs.findIndex((d) => d.name === def.name);\n if (idx >= 0) defs[idx] = def;\n else defs.push(def);\n const p = schemaPath(dataDir);\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(p, JSON.stringify({ fields: defs }, null, 2), \"utf-8\");\n return defs;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n values: Record<string, string | number | boolean>;\n errors: string[];\n}\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/** Validate + coerce a record of raw values against custom field definitions. */\nexport function validateCustomFields(\n input: Record<string, unknown>,\n defs: FieldDefinition[]\n): ValidationResult {\n const byName = new Map(defs.map((d) => [d.name, d]));\n const values: Record<string, string | number | boolean> = {};\n const errors: string[] = [];\n\n for (const [key, raw] of Object.entries(input)) {\n const def = byName.get(key);\n if (!def) {\n errors.push(`Unknown custom field: ${key}`);\n continue;\n }\n const str = String(raw).trim();\n switch (def.type) {\n case \"number\": {\n const n = Number(str);\n if (!Number.isFinite(n)) errors.push(`${key}: not a number`);\n else values[key] = n;\n break;\n }\n case \"boolean\": {\n if (/^(true|yes|1)$/i.test(str)) values[key] = true;\n else if (/^(false|no|0)$/i.test(str)) values[key] = false;\n else errors.push(`${key}: not a boolean`);\n break;\n }\n case \"date\": {\n if (DATE_RE.test(str)) values[key] = str;\n else errors.push(`${key}: expected YYYY-MM-DD`);\n break;\n }\n case \"select\": {\n if (def.options && def.options.includes(str)) values[key] = str;\n else errors.push(`${key}: must be one of ${(def.options ?? []).join(\", \")}`);\n break;\n }\n default:\n values[key] = str;\n }\n }\n\n return { valid: errors.length === 0, values, errors };\n}\n"],"mappings":";;;AAkBA,SAAS,WAAW,SAAyB;CAC3C,OAAO,KAAK,KAAK,SAAS,YAAY,UAAU,oBAAoB;AACtE;AAEA,SAAgB,qBAAqB,SAAoC;CACvE,MAAM,IAAI,WAAW,OAAO;CAC5B,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC;CAC/B,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,OAAO,CAAW;EAG7D,OAAO,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;CACrD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,SAAgB,kBAAkB,SAAiB,KAAyC;CAC1F,MAAM,OAAO,qBAAqB,OAAO;CACzC,MAAM,MAAM,KAAK,WAAW,MAAM,EAAE,SAAS,IAAI,IAAI;CACrD,IAAI,OAAO,GAAG,KAAK,OAAO;MACrB,KAAK,KAAK,GAAG;CAClB,MAAM,IAAI,WAAW,OAAO;CAC5B,GAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;CACjD,GAAG,cAAc,GAAG,KAAK,UAAU,EAAE,QAAQ,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO;CACtE,OAAO;AACT;AAQA,MAAM,UAAU;;AAGhB,SAAgB,qBACd,OACA,MACkB;CAClB,MAAM,SAAS,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD,MAAM,SAAoD,CAAC;CAC3D,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,KAAK,GAAG;EAC9C,MAAM,MAAM,OAAO,IAAI,GAAG;EAC1B,IAAI,CAAC,KAAK;GACR,OAAO,KAAK,yBAAyB,KAAK;GAC1C;EACF;EACA,MAAM,MAAM,OAAO,GAAG,EAAE,KAAK;EAC7B,QAAQ,IAAI,MAAZ;GACE,KAAK,UAAU;IACb,MAAM,IAAI,OAAO,GAAG;IACpB,IAAI,CAAC,OAAO,SAAS,CAAC,GAAG,OAAO,KAAK,GAAG,IAAI,eAAe;SACtD,OAAO,OAAO;IACnB;GACF;GACA,KAAK;IACH,IAAI,kBAAkB,KAAK,GAAG,GAAG,OAAO,OAAO;SAC1C,IAAI,kBAAkB,KAAK,GAAG,GAAG,OAAO,OAAO;SAC/C,OAAO,KAAK,GAAG,IAAI,gBAAgB;IACxC;GAEF,KAAK;IACH,IAAI,QAAQ,KAAK,GAAG,GAAG,OAAO,OAAO;SAChC,OAAO,KAAK,GAAG,IAAI,sBAAsB;IAC9C;GAEF,KAAK;IACH,IAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,GAAG,GAAG,OAAO,OAAO;SACvD,OAAO,KAAK,GAAG,IAAI,oBAAoB,IAAI,WAAW,CAAC,GAAG,KAAK,IAAI,GAAG;IAC3E;GAEF,SACE,OAAO,OAAO;EAClB;CACF;CAEA,OAAO;EAAE,OAAO,OAAO,WAAW;EAAG;EAAQ;CAAO;AACtD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { r as validateCustomFields } from "./custom-fields-Pl2t9xzp.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
//#region src/core/custom-objects.ts
|
|
6
|
+
function objectsSchemaPath(dataDir) {
|
|
7
|
+
return path.join(dataDir, ".agentic", "schema", "custom-objects.json");
|
|
8
|
+
}
|
|
9
|
+
function recordsPath(dataDir, name) {
|
|
10
|
+
return path.join(dataDir, ".agentic", "objects", `${name}.json`);
|
|
11
|
+
}
|
|
12
|
+
function loadCustomObjects(dataDir) {
|
|
13
|
+
const p = objectsSchemaPath(dataDir);
|
|
14
|
+
if (!fs.existsSync(p)) return [];
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
17
|
+
return Array.isArray(data.objects) ? data.objects : [];
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function getObjectDefinition(dataDir, name) {
|
|
23
|
+
return loadCustomObjects(dataDir).find((o) => o.name === name);
|
|
24
|
+
}
|
|
25
|
+
/** Add or update (by name) a custom object definition. */
|
|
26
|
+
function defineCustomObject(dataDir, def) {
|
|
27
|
+
const objs = loadCustomObjects(dataDir);
|
|
28
|
+
const idx = objs.findIndex((o) => o.name === def.name);
|
|
29
|
+
if (idx >= 0) objs[idx] = def;
|
|
30
|
+
else objs.push(def);
|
|
31
|
+
const p = objectsSchemaPath(dataDir);
|
|
32
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
33
|
+
fs.writeFileSync(p, JSON.stringify({ objects: objs }, null, 2), "utf-8");
|
|
34
|
+
return objs;
|
|
35
|
+
}
|
|
36
|
+
function listRecords(dataDir, name) {
|
|
37
|
+
const p = recordsPath(dataDir, name);
|
|
38
|
+
if (!fs.existsSync(p)) return [];
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
41
|
+
return Array.isArray(data.records) ? data.records : [];
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writeRecords(dataDir, name, records) {
|
|
47
|
+
const p = recordsPath(dataDir, name);
|
|
48
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
49
|
+
fs.writeFileSync(p, JSON.stringify({ records }, null, 2), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
function createRecord(dataDir, name, values) {
|
|
52
|
+
const def = getObjectDefinition(dataDir, name);
|
|
53
|
+
if (!def) return {
|
|
54
|
+
ok: false,
|
|
55
|
+
errors: [`Unknown object: ${name}`]
|
|
56
|
+
};
|
|
57
|
+
const validation = validateCustomFields(values, def.fields);
|
|
58
|
+
if (!validation.valid) return {
|
|
59
|
+
ok: false,
|
|
60
|
+
errors: validation.errors
|
|
61
|
+
};
|
|
62
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
63
|
+
const record = {
|
|
64
|
+
id: `${name}_${randomBytes(6).toString("hex")}`,
|
|
65
|
+
createdAt: now,
|
|
66
|
+
updatedAt: now,
|
|
67
|
+
values: validation.values
|
|
68
|
+
};
|
|
69
|
+
writeRecords(dataDir, name, [...listRecords(dataDir, name), record]);
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
record
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { loadCustomObjects as a, listRecords as i, defineCustomObject as n, getObjectDefinition as r, createRecord as t };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=custom-objects-BHgn1GEX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-objects-BHgn1GEX.js","names":[],"sources":["../src/core/custom-objects.ts"],"sourcesContent":["import { randomBytes } from \"crypto\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { validateCustomFields, type FieldDefinition } from \"./custom-fields.js\";\n\n/**\n * Custom objects — runtime-defined entity types with their own fields, stored\n * as JSON without code migrations (Twenty-style \"no-migration\" model, the\n * N5-1 increment of the metadata layer). Definitions live in\n * .agentic/schema/custom-objects.json; records in .agentic/objects/<name>.json.\n */\nexport interface ObjectDefinition {\n name: string;\n label?: string;\n fields: FieldDefinition[];\n}\n\nexport interface ObjectRecord {\n id: string;\n createdAt: string;\n updatedAt: string;\n values: Record<string, string | number | boolean>;\n}\n\nexport interface RecordResult {\n ok: boolean;\n record?: ObjectRecord;\n errors?: string[];\n}\n\nfunction objectsSchemaPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"schema\", \"custom-objects.json\");\n}\nfunction recordsPath(dataDir: string, name: string): string {\n return path.join(dataDir, \".agentic\", \"objects\", `${name}.json`);\n}\n\nexport function loadCustomObjects(dataDir: string): ObjectDefinition[] {\n const p = objectsSchemaPath(dataDir);\n if (!fs.existsSync(p)) return [];\n try {\n const data = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as {\n objects?: ObjectDefinition[];\n };\n return Array.isArray(data.objects) ? data.objects : [];\n } catch {\n return [];\n }\n}\n\nexport function getObjectDefinition(dataDir: string, name: string): ObjectDefinition | undefined {\n return loadCustomObjects(dataDir).find((o) => o.name === name);\n}\n\n/** Add or update (by name) a custom object definition. */\nexport function defineCustomObject(dataDir: string, def: ObjectDefinition): ObjectDefinition[] {\n const objs = loadCustomObjects(dataDir);\n const idx = objs.findIndex((o) => o.name === def.name);\n if (idx >= 0) objs[idx] = def;\n else objs.push(def);\n const p = objectsSchemaPath(dataDir);\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(p, JSON.stringify({ objects: objs }, null, 2), \"utf-8\");\n return objs;\n}\n\nexport function listRecords(dataDir: string, name: string): ObjectRecord[] {\n const p = recordsPath(dataDir, name);\n if (!fs.existsSync(p)) return [];\n try {\n const data = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as { records?: ObjectRecord[] };\n return Array.isArray(data.records) ? data.records : [];\n } catch {\n return [];\n }\n}\n\nfunction writeRecords(dataDir: string, name: string, records: ObjectRecord[]): void {\n const p = recordsPath(dataDir, name);\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(p, JSON.stringify({ records }, null, 2), \"utf-8\");\n}\n\nexport function getRecord(dataDir: string, name: string, id: string): ObjectRecord | undefined {\n return listRecords(dataDir, name).find((r) => r.id === id);\n}\n\nexport function createRecord(\n dataDir: string,\n name: string,\n values: Record<string, unknown>\n): RecordResult {\n const def = getObjectDefinition(dataDir, name);\n if (!def) return { ok: false, errors: [`Unknown object: ${name}`] };\n\n const validation = validateCustomFields(values, def.fields);\n if (!validation.valid) return { ok: false, errors: validation.errors };\n\n const now = new Date().toISOString();\n const record: ObjectRecord = {\n id: `${name}_${randomBytes(6).toString(\"hex\")}`,\n createdAt: now,\n updatedAt: now,\n values: validation.values,\n };\n writeRecords(dataDir, name, [...listRecords(dataDir, name), record]);\n return { ok: true, record };\n}\n\nexport function updateRecord(\n dataDir: string,\n name: string,\n id: string,\n values: Record<string, unknown>\n): RecordResult {\n const def = getObjectDefinition(dataDir, name);\n if (!def) return { ok: false, errors: [`Unknown object: ${name}`] };\n\n const records = listRecords(dataDir, name);\n const idx = records.findIndex((r) => r.id === id);\n if (idx < 0) return { ok: false, errors: [`Record not found: ${id}`] };\n\n const validation = validateCustomFields(values, def.fields);\n if (!validation.valid) return { ok: false, errors: validation.errors };\n\n const updated: ObjectRecord = {\n ...records[idx]!,\n updatedAt: new Date().toISOString(),\n values: { ...records[idx]!.values, ...validation.values },\n };\n records[idx] = updated;\n writeRecords(dataDir, name, records);\n return { ok: true, record: updated };\n}\n\nexport function deleteRecord(dataDir: string, name: string, id: string): boolean {\n const records = listRecords(dataDir, name);\n const next = records.filter((r) => r.id !== id);\n if (next.length === records.length) return false;\n writeRecords(dataDir, name, next);\n return true;\n}\n"],"mappings":";;;;;AA8BA,SAAS,kBAAkB,SAAyB;CAClD,OAAO,KAAK,KAAK,SAAS,YAAY,UAAU,qBAAqB;AACvE;AACA,SAAS,YAAY,SAAiB,MAAsB;CAC1D,OAAO,KAAK,KAAK,SAAS,YAAY,WAAW,GAAG,KAAK,MAAM;AACjE;AAEA,SAAgB,kBAAkB,SAAqC;CACrE,MAAM,IAAI,kBAAkB,OAAO;CACnC,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC;CAC/B,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,OAAO,CAAW;EAG7D,OAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;CACvD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAgB,oBAAoB,SAAiB,MAA4C;CAC/F,OAAO,kBAAkB,OAAO,EAAE,MAAM,MAAM,EAAE,SAAS,IAAI;AAC/D;;AAGA,SAAgB,mBAAmB,SAAiB,KAA2C;CAC7F,MAAM,OAAO,kBAAkB,OAAO;CACtC,MAAM,MAAM,KAAK,WAAW,MAAM,EAAE,SAAS,IAAI,IAAI;CACrD,IAAI,OAAO,GAAG,KAAK,OAAO;MACrB,KAAK,KAAK,GAAG;CAClB,MAAM,IAAI,kBAAkB,OAAO;CACnC,GAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;CACjD,GAAG,cAAc,GAAG,KAAK,UAAU,EAAE,SAAS,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO;CACvE,OAAO;AACT;AAEA,SAAgB,YAAY,SAAiB,MAA8B;CACzE,MAAM,IAAI,YAAY,SAAS,IAAI;CACnC,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC;CAC/B,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,OAAO,CAAW;EAC7D,OAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;CACvD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,aAAa,SAAiB,MAAc,SAA+B;CAClF,MAAM,IAAI,YAAY,SAAS,IAAI;CACnC,GAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;CACjD,GAAG,cAAc,GAAG,KAAK,UAAU,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG,OAAO;AACnE;AAMA,SAAgB,aACd,SACA,MACA,QACc;CACd,MAAM,MAAM,oBAAoB,SAAS,IAAI;CAC7C,IAAI,CAAC,KAAK,OAAO;EAAE,IAAI;EAAO,QAAQ,CAAC,mBAAmB,MAAM;CAAE;CAElE,MAAM,aAAa,qBAAqB,QAAQ,IAAI,MAAM;CAC1D,IAAI,CAAC,WAAW,OAAO,OAAO;EAAE,IAAI;EAAO,QAAQ,WAAW;CAAO;CAErE,MAAM,uBAAM,IAAI,KAAK,GAAE,YAAY;CACnC,MAAM,SAAuB;EAC3B,IAAI,GAAG,KAAK,GAAG,YAAY,CAAC,EAAE,SAAS,KAAK;EAC5C,WAAW;EACX,WAAW;EACX,QAAQ,WAAW;CACrB;CACA,aAAa,SAAS,MAAM,CAAC,GAAG,YAAY,SAAS,IAAI,GAAG,MAAM,CAAC;CACnE,OAAO;EAAE,IAAI;EAAM;CAAO;AAC5B"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { fromZodError } from "zod-validation-error";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/schemas/main-facts.ts
|
|
7
|
+
const MainFactsSchema = z.object({
|
|
8
|
+
name: z.string().min(1),
|
|
9
|
+
domain: z.string().optional(),
|
|
10
|
+
email: z.string().optional(),
|
|
11
|
+
phone: z.string().optional(),
|
|
12
|
+
industry: z.string().optional(),
|
|
13
|
+
relationship_stage: z.enum([
|
|
14
|
+
"prospect",
|
|
15
|
+
"active",
|
|
16
|
+
"churned",
|
|
17
|
+
"paused"
|
|
18
|
+
]),
|
|
19
|
+
deal_value: z.number().optional(),
|
|
20
|
+
currency: z.string().default("EUR"),
|
|
21
|
+
primary_contact: z.string().optional(),
|
|
22
|
+
timezone: z.string().optional(),
|
|
23
|
+
tags: z.array(z.string()).default([]),
|
|
24
|
+
created: z.preprocess((v) => v instanceof Date ? v.toISOString().slice(0, 10) : v, z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD required")),
|
|
25
|
+
updated: z.preprocess((v) => v instanceof Date ? v.toISOString().slice(0, 10) : v, z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD required"))
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/fs/customer-dir.ts
|
|
29
|
+
function getCustomerDir(dataDir, slug) {
|
|
30
|
+
return path.join(dataDir, "customers", slug);
|
|
31
|
+
}
|
|
32
|
+
function customerExists(dataDir, slug) {
|
|
33
|
+
return fs.existsSync(getCustomerDir(dataDir, slug));
|
|
34
|
+
}
|
|
35
|
+
/** List all customer slugs (immediate subdirectories of customers/). */
|
|
36
|
+
function listCustomerSlugs(dataDir) {
|
|
37
|
+
const dir = path.join(dataDir, "customers");
|
|
38
|
+
if (!fs.existsSync(dir)) return [];
|
|
39
|
+
return fs.readdirSync(dir).filter((s) => {
|
|
40
|
+
try {
|
|
41
|
+
return fs.statSync(path.join(dir, s)).isDirectory();
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function ensureCustomerDir(dataDir, slug) {
|
|
48
|
+
const customerDir = getCustomerDir(dataDir, slug);
|
|
49
|
+
fs.mkdirSync(customerDir, { recursive: true });
|
|
50
|
+
fs.mkdirSync(path.join(customerDir, "attachments"), { recursive: true });
|
|
51
|
+
fs.mkdirSync(path.join(customerDir, "transcripts"), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
async function writeMainFacts(dataDir, slug, facts) {
|
|
54
|
+
const filePath = path.join(getCustomerDir(dataDir, slug), "main_facts.md");
|
|
55
|
+
const clean = Object.fromEntries(Object.entries(facts).filter(([, v]) => v !== void 0));
|
|
56
|
+
const content = matter.stringify("", clean);
|
|
57
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
async function readMainFacts(dataDir, slug) {
|
|
60
|
+
const filePath = path.join(getCustomerDir(dataDir, slug), "main_facts.md");
|
|
61
|
+
if (!fs.existsSync(filePath)) throw new Error(`main_facts.md not found for customer '${slug}'`);
|
|
62
|
+
const data = matter(fs.readFileSync(filePath, "utf-8")).data;
|
|
63
|
+
for (const key of ["created", "updated"]) if (data[key] instanceof Date) data[key] = data[key].toISOString().slice(0, 10);
|
|
64
|
+
const result = MainFactsSchema.safeParse(data);
|
|
65
|
+
if (!result.success) throw new Error(fromZodError(result.error, {
|
|
66
|
+
prefix: `Schema error in ${filePath}`,
|
|
67
|
+
prefixSeparator: ":\n - ",
|
|
68
|
+
issueSeparator: "\n - "
|
|
69
|
+
}).message);
|
|
70
|
+
return result.data;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { writeMainFacts as a, readMainFacts as i, ensureCustomerDir as n, MainFactsSchema as o, listCustomerSlugs as r, customerExists as t };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=customer-dir-DIylZ8Q6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-dir-DIylZ8Q6.js","names":[],"sources":["../src/schemas/main-facts.ts","../src/fs/customer-dir.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const MainFactsSchema = z.object({\n name: z.string().min(1),\n domain: z.string().optional(),\n email: z.string().optional(),\n phone: z.string().optional(),\n industry: z.string().optional(),\n relationship_stage: z.enum([\"prospect\", \"active\", \"churned\", \"paused\"]),\n deal_value: z.number().optional(),\n currency: z.string().default(\"EUR\"),\n primary_contact: z.string().optional(),\n timezone: z.string().optional(),\n tags: z.array(z.string()).default([]),\n created: z.preprocess(\n (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v),\n z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, \"YYYY-MM-DD required\")\n ),\n updated: z.preprocess(\n (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v),\n z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, \"YYYY-MM-DD required\")\n ),\n});\n\nexport type MainFacts = z.infer<typeof MainFactsSchema>;\n","import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport { fromZodError } from \"zod-validation-error\";\nimport { MainFactsSchema, type MainFacts } from \"../schemas/main-facts.js\";\n\nexport function getCustomerDir(dataDir: string, slug: string): string {\n return path.join(dataDir, \"customers\", slug);\n}\n\nexport function customerExists(dataDir: string, slug: string): boolean {\n return fs.existsSync(getCustomerDir(dataDir, slug));\n}\n\n/** List all customer slugs (immediate subdirectories of customers/). */\nexport function listCustomerSlugs(dataDir: string): string[] {\n const dir = path.join(dataDir, \"customers\");\n if (!fs.existsSync(dir)) return [];\n return fs.readdirSync(dir).filter((s) => {\n try {\n return fs.statSync(path.join(dir, s)).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\nexport async function ensureCustomerDir(dataDir: string, slug: string): Promise<void> {\n const customerDir = getCustomerDir(dataDir, slug);\n fs.mkdirSync(customerDir, { recursive: true });\n fs.mkdirSync(path.join(customerDir, \"attachments\"), { recursive: true });\n fs.mkdirSync(path.join(customerDir, \"transcripts\"), { recursive: true });\n}\n\nexport async function writeMainFacts(\n dataDir: string,\n slug: string,\n facts: MainFacts\n): Promise<void> {\n const filePath = path.join(getCustomerDir(dataDir, slug), \"main_facts.md\");\n // Strip undefined values — gray-matter YAML serializer rejects them\n const clean = Object.fromEntries(\n Object.entries(facts as Record<string, unknown>).filter(([, v]) => v !== undefined)\n );\n const content = matter.stringify(\"\", clean);\n fs.writeFileSync(filePath, content, \"utf-8\");\n}\n\nexport async function readMainFacts(dataDir: string, slug: string): Promise<MainFacts> {\n const filePath = path.join(getCustomerDir(dataDir, slug), \"main_facts.md\");\n if (!fs.existsSync(filePath)) {\n throw new Error(`main_facts.md not found for customer '${slug}'`);\n }\n // Use fs.readFileSync so the memfs mock is respected in tests,\n // then parse the string with matter.\n const content = fs.readFileSync(filePath, \"utf-8\") as string;\n const raw = matter(content);\n // gray-matter parses YYYY-MM-DD as Date objects; coerce back to strings for Zod\n const data = raw.data as Record<string, unknown>;\n for (const key of [\"created\", \"updated\"] as const) {\n if (data[key] instanceof Date) {\n data[key] = (data[key] as Date).toISOString().slice(0, 10);\n }\n }\n const result = MainFactsSchema.safeParse(data);\n if (!result.success) {\n throw new Error(\n fromZodError(result.error, {\n prefix: `Schema error in ${filePath}`,\n prefixSeparator: \":\\n - \",\n issueSeparator: \"\\n - \",\n }).message\n );\n }\n return result.data;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { CronJob } from "cron";
|
|
4
|
+
//#region src/daemon/worker.ts
|
|
5
|
+
const DATA_DIR = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
6
|
+
const MAX_CUSTOMERS_PER_CYCLE = 50;
|
|
7
|
+
async function syncWithBackoff(fn, maxRetries = 3) {
|
|
8
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) try {
|
|
9
|
+
await fn();
|
|
10
|
+
return;
|
|
11
|
+
} catch (err) {
|
|
12
|
+
const msg = err.message;
|
|
13
|
+
if (msg.includes("429") || msg.includes("rateLimitExceeded")) {
|
|
14
|
+
const delay = Math.pow(2, attempt) * 2e3;
|
|
15
|
+
process.stderr.write(`[daemon] Rate limit, retrying in ${delay}ms\n`);
|
|
16
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
17
|
+
} else throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function syncAllCustomers() {
|
|
21
|
+
const customersDir = path.join(DATA_DIR, "customers");
|
|
22
|
+
if (!fs.existsSync(customersDir)) return;
|
|
23
|
+
const slugsToSync = fs.readdirSync(customersDir).filter((s) => {
|
|
24
|
+
try {
|
|
25
|
+
return fs.statSync(path.join(customersDir, s)).isDirectory();
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}).slice(0, MAX_CUSTOMERS_PER_CYCLE);
|
|
30
|
+
for (const slug of slugsToSync) {
|
|
31
|
+
const sourcesPath = path.join(customersDir, slug, "sources.json");
|
|
32
|
+
if (!fs.existsSync(sourcesPath)) continue;
|
|
33
|
+
try {
|
|
34
|
+
const sources = JSON.parse(fs.readFileSync(sourcesPath, "utf-8"));
|
|
35
|
+
if (sources.gmail?.enabled && sources.gmail.query) {
|
|
36
|
+
const tokenPath = path.join(DATA_DIR, ".agentic", "gmail-token.json");
|
|
37
|
+
const credPath = path.join(DATA_DIR, ".agentic", "gmail-credentials.json");
|
|
38
|
+
if (fs.existsSync(tokenPath) && fs.existsSync(credPath)) {
|
|
39
|
+
const { getGmailAuth } = await import("../gmail-auth-OComS92L.js");
|
|
40
|
+
const { syncGmail } = await import("../gmail-sync-DIaxInDT.js");
|
|
41
|
+
const auth = await getGmailAuth(credPath, tokenPath);
|
|
42
|
+
await syncWithBackoff(async () => {
|
|
43
|
+
const result = await syncGmail({
|
|
44
|
+
slug,
|
|
45
|
+
dataDir: DATA_DIR,
|
|
46
|
+
auth,
|
|
47
|
+
query: sources.gmail.query,
|
|
48
|
+
since: /* @__PURE__ */ new Date(Date.now() - 1800 * 1e3)
|
|
49
|
+
});
|
|
50
|
+
if (result.synced > 0) process.stderr.write(`[daemon] ${slug}: synced ${result.synced} emails\n`);
|
|
51
|
+
const { updateSlugSyncState } = await import("../sync-state-CwLSt_1m.js");
|
|
52
|
+
updateSlugSyncState(DATA_DIR, slug, { lastGmailSync: (/* @__PURE__ */ new Date()).toISOString() });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
process.stderr.write(`[daemon] Error syncing ${slug}: ${err.message}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function startWatcher() {
|
|
62
|
+
const agenticSourcesPath = path.join(DATA_DIR, ".agentic", "sources.json");
|
|
63
|
+
if (!fs.existsSync(agenticSourcesPath)) return;
|
|
64
|
+
try {
|
|
65
|
+
const sources = JSON.parse(fs.readFileSync(agenticSourcesPath, "utf-8"));
|
|
66
|
+
if (sources.transcripts?.enabled && sources.transcripts.paths?.length) {
|
|
67
|
+
const { watchTranscripts, processTranscriptFileAutoMatch } = await import("../transcript-watcher-CL2QUygI.js");
|
|
68
|
+
watchTranscripts({
|
|
69
|
+
paths: sources.transcripts.paths,
|
|
70
|
+
extensions: sources.transcripts.extensions ?? [".txt", ".vtt"],
|
|
71
|
+
dataDir: DATA_DIR,
|
|
72
|
+
onFile: (filePath) => processTranscriptFileAutoMatch(filePath, DATA_DIR)
|
|
73
|
+
});
|
|
74
|
+
process.stderr.write(`[daemon] Watching transcripts (LLM auto-match)\n`);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
process.stderr.write(`[daemon] Watcher error: ${err.message}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function checkAgentWakeTriggers() {
|
|
81
|
+
const agentsDir = path.join(DATA_DIR, ".agentic", "agents");
|
|
82
|
+
if (!fs.existsSync(agentsDir)) return;
|
|
83
|
+
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".agent.json"));
|
|
84
|
+
for (const file of files) try {
|
|
85
|
+
const config = JSON.parse(fs.readFileSync(path.join(agentsDir, file), "utf-8"));
|
|
86
|
+
if (!config.wakeOn.includes("email")) continue;
|
|
87
|
+
const { getLastGmailSync } = await import("../sync-state-CwLSt_1m.js");
|
|
88
|
+
const lastSync = getLastGmailSync(DATA_DIR, config.slug);
|
|
89
|
+
const lastWake = config.lastWake ? new Date(config.lastWake) : null;
|
|
90
|
+
if (!lastSync) continue;
|
|
91
|
+
if (lastWake && lastSync <= lastWake) continue;
|
|
92
|
+
process.stderr.write(`[daemon] Wake trigger: ${config.slug}\n`);
|
|
93
|
+
const { buildContext } = await import("../context-builder-DlrRcqmJ.js");
|
|
94
|
+
const context = await buildContext(DATA_DIR, config.slug).catch(() => null);
|
|
95
|
+
if (!context) continue;
|
|
96
|
+
if (config.channel === "telegram" && process.env["TELEGRAM_BOT_TOKEN"] && (config.telegramChatId ?? process.env["TELEGRAM_CHAT_ID"])) {
|
|
97
|
+
const chatId = config.telegramChatId ?? process.env["TELEGRAM_CHAT_ID"];
|
|
98
|
+
const token = process.env["TELEGRAM_BOT_TOKEN"];
|
|
99
|
+
const message = `📬 New activity: *${config.slug}*\n\n${context.slice(0, 800)}`;
|
|
100
|
+
try {
|
|
101
|
+
const { default: https } = await import("https");
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
chat_id: chatId,
|
|
104
|
+
text: message,
|
|
105
|
+
parse_mode: "Markdown"
|
|
106
|
+
});
|
|
107
|
+
await new Promise((resolve, reject) => {
|
|
108
|
+
const req = https.request(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
"Content-Length": Buffer.byteLength(body)
|
|
113
|
+
}
|
|
114
|
+
}, (res) => {
|
|
115
|
+
res.resume();
|
|
116
|
+
resolve();
|
|
117
|
+
});
|
|
118
|
+
req.on("error", reject);
|
|
119
|
+
req.write(body);
|
|
120
|
+
req.end();
|
|
121
|
+
});
|
|
122
|
+
process.stderr.write(`[daemon] Telegram sent for ${config.slug}\n`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
process.stderr.write(`[daemon] Telegram failed: ${err.message}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
config.lastWake = (/* @__PURE__ */ new Date()).toISOString();
|
|
128
|
+
fs.writeFileSync(path.join(agentsDir, file), JSON.stringify(config, null, 2), "utf-8");
|
|
129
|
+
} catch (err) {
|
|
130
|
+
process.stderr.write(`[daemon] Agent check error ${file}: ${err.message}\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
new CronJob(`*/${Math.max(1, parseInt(process.env["DXCRM_DAEMON_INTERVAL"] ?? "30", 10) || 30)} * * * *`, async () => {
|
|
134
|
+
await syncAllCustomers();
|
|
135
|
+
await checkAgentWakeTriggers().catch((err) => {
|
|
136
|
+
process.stderr.write(`[daemon] Wake trigger check failed: ${err.message}\n`);
|
|
137
|
+
});
|
|
138
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
139
|
+
new CronJob("*/60 * * * *", async () => {
|
|
140
|
+
try {
|
|
141
|
+
const { runScheduledBackupIfDue } = await import("../backup-f_hC7rBV.js");
|
|
142
|
+
await runScheduledBackupIfDue(DATA_DIR);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
process.stderr.write(`[daemon] Backup check error: ${err.message}\n`);
|
|
145
|
+
}
|
|
146
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
147
|
+
new CronJob("0 6 * * *", async () => {
|
|
148
|
+
try {
|
|
149
|
+
const { renewExpiringSubscriptions } = await import("../push-manager-CowY-0IK.js");
|
|
150
|
+
const { buildGmailRenewFn } = await import("../gmail-webhook-handler-DS7OlRPX.js");
|
|
151
|
+
const tokenPath = path.join(DATA_DIR, ".agentic", "gmail-token.json");
|
|
152
|
+
const credPath = path.join(DATA_DIR, ".agentic", "gmail-credentials.json");
|
|
153
|
+
const { readSubscriptions } = await import("../push-manager-CowY-0IK.js");
|
|
154
|
+
if ((await readSubscriptions(DATA_DIR)).filter((s) => s.provider === "gmail" && s.status === "active").length === 0) return;
|
|
155
|
+
if (!fs.existsSync(tokenPath) || !fs.existsSync(credPath)) return;
|
|
156
|
+
const { getGmailAuth } = await import("../gmail-auth-OComS92L.js");
|
|
157
|
+
const result = await renewExpiringSubscriptions(DATA_DIR, buildGmailRenewFn((await getGmailAuth(credPath, tokenPath)).credentials?.access_token ?? "", ""), 24);
|
|
158
|
+
if (result.renewed.length > 0) process.stderr.write(`[push] Renewed ${result.renewed.length} subscription(s)\n`);
|
|
159
|
+
if (result.errors.length > 0) process.stderr.write(`[push] Renewal errors: ${result.errors.join(", ")}\n`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
process.stderr.write(`[push] Renewal failed: ${err.message}\n`);
|
|
162
|
+
}
|
|
163
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
164
|
+
new CronJob("0 7 * * *", async () => {
|
|
165
|
+
try {
|
|
166
|
+
const { runDailyProactiveChecks } = await import("../proactive-worker-BrLHNhjH.js");
|
|
167
|
+
const result = await runDailyProactiveChecks(DATA_DIR);
|
|
168
|
+
process.stderr.write(`[proactive] Daily check: ${result.customersChecked} customers, ${result.tasksEnqueued} tasks enqueued\n`);
|
|
169
|
+
if (result.errors.length > 0) process.stderr.write(`[proactive] Errors: ${result.errors.join(", ")}\n`);
|
|
170
|
+
const { drainProactiveQueue } = await import("../notification-dispatcher-0vYNngWe.js");
|
|
171
|
+
const drain = await drainProactiveQueue(DATA_DIR);
|
|
172
|
+
process.stderr.write(`[proactive] Dispatched ${drain.sent} task(s), ${drain.failed} failed\n`);
|
|
173
|
+
const { syncGoalProgressFromPipeline } = await import("../goal-engine-CUZSpERI.js");
|
|
174
|
+
const goalSync = await syncGoalProgressFromPipeline(DATA_DIR);
|
|
175
|
+
if (goalSync.updated.length > 0) process.stderr.write(`[goals] Progress synced: ${goalSync.updated.join(", ")}\n`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
process.stderr.write(`[proactive] Daily check failed: ${err.message}\n`);
|
|
178
|
+
}
|
|
179
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
180
|
+
new CronJob("0 8 * * *", async () => {
|
|
181
|
+
try {
|
|
182
|
+
const { checkSlaBreaches } = await import("../sla-engine-5IhTsBUR.js");
|
|
183
|
+
const breaches = await checkSlaBreaches(DATA_DIR, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
184
|
+
if (breaches.length > 0) {
|
|
185
|
+
process.stderr.write(`[tickets] ${breaches.length} SLA breach(es) found\n`);
|
|
186
|
+
for (const { slug, ticket } of breaches) process.stderr.write(`[tickets] SLA breach: ${slug}/${ticket.id} "${ticket.title}" due ${ticket.slaDue}\n`);
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
process.stderr.write(`[tickets] SLA check failed: ${err.message}\n`);
|
|
190
|
+
}
|
|
191
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
192
|
+
new CronJob("0 */6 * * *", async () => {
|
|
193
|
+
try {
|
|
194
|
+
const { runSequenceCycle } = await import("../sequence-engine-CCTHEBgi.js");
|
|
195
|
+
const result = await runSequenceCycle(DATA_DIR, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
196
|
+
process.stderr.write(`[sequences] ${result.sent} sent, ${result.completed} completed, ${result.errors.length} errors\n`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
process.stderr.write(`[sequences] cycle failed: ${err.message}\n`);
|
|
199
|
+
}
|
|
200
|
+
}, null, true, void 0, null, false, void 0, false, true);
|
|
201
|
+
await startWatcher();
|
|
202
|
+
if (process.send) process.send("ready");
|
|
203
|
+
process.stderr.write("[daemon] DatasynxOpenCRM daemon started\n");
|
|
204
|
+
//#endregion
|
|
205
|
+
export {};
|
|
206
|
+
|
|
207
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","names":[],"sources":["../../src/daemon/worker.ts"],"sourcesContent":["// src/daemon/worker.ts\n// Standalone detached process — started by `dxcrm daemon start`\n// Handles background Gmail sync + transcript watching via cron\nimport { CronJob } from \"cron\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nconst DATA_DIR = process.env[\"DXCRM_DATA_DIR\"] ?? process.cwd();\n\nconst MAX_CUSTOMERS_PER_CYCLE = 50;\n\nasync function syncWithBackoff(fn: () => Promise<void>, maxRetries = 3): Promise<void> {\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n await fn();\n return;\n } catch (err) {\n const msg = (err as Error).message;\n if (msg.includes(\"429\") || msg.includes(\"rateLimitExceeded\")) {\n const delay = Math.pow(2, attempt) * 2000; // 2s, 4s, 8s\n process.stderr.write(`[daemon] Rate limit, retrying in ${delay}ms\\n`);\n await new Promise((r) => setTimeout(r, delay));\n } else {\n throw err;\n }\n }\n }\n}\n\nasync function syncAllCustomers(): Promise<void> {\n const customersDir = path.join(DATA_DIR, \"customers\");\n if (!fs.existsSync(customersDir)) return;\n\n const slugs = fs.readdirSync(customersDir).filter((s) => {\n try {\n return fs.statSync(path.join(customersDir, s)).isDirectory();\n } catch {\n return false;\n }\n });\n\n const slugsToSync = slugs.slice(0, MAX_CUSTOMERS_PER_CYCLE);\n\n for (const slug of slugsToSync) {\n const sourcesPath = path.join(customersDir, slug, \"sources.json\");\n if (!fs.existsSync(sourcesPath)) continue;\n\n try {\n const sources = JSON.parse(fs.readFileSync(sourcesPath, \"utf-8\")) as {\n gmail?: { query?: string; enabled?: boolean };\n };\n\n if (sources.gmail?.enabled && sources.gmail.query) {\n // Gmail sync requires auth — skip if token not configured\n const tokenPath = path.join(DATA_DIR, \".agentic\", \"gmail-token.json\");\n const credPath = path.join(DATA_DIR, \".agentic\", \"gmail-credentials.json\");\n if (fs.existsSync(tokenPath) && fs.existsSync(credPath)) {\n const { getGmailAuth } = await import(\"../sync/gmail-auth.js\");\n const { syncGmail } = await import(\"../sync/gmail-sync.js\");\n const auth = await getGmailAuth(credPath, tokenPath);\n await syncWithBackoff(async () => {\n const result = await syncGmail({\n slug,\n dataDir: DATA_DIR,\n auth,\n query: sources.gmail!.query!,\n since: new Date(Date.now() - 30 * 60 * 1000), // last 30 min\n });\n if (result.synced > 0) {\n process.stderr.write(`[daemon] ${slug}: synced ${result.synced} emails\\n`);\n }\n // Update sync state after each successful customer sync\n const { updateSlugSyncState } = await import(\"../fs/sync-state.js\");\n updateSlugSyncState(DATA_DIR, slug, { lastGmailSync: new Date().toISOString() });\n });\n }\n }\n } catch (err) {\n process.stderr.write(`[daemon] Error syncing ${slug}: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// Start transcript watcher\nasync function startWatcher(): Promise<void> {\n const agenticSourcesPath = path.join(DATA_DIR, \".agentic\", \"sources.json\");\n if (!fs.existsSync(agenticSourcesPath)) return;\n\n try {\n const sources = JSON.parse(fs.readFileSync(agenticSourcesPath, \"utf-8\")) as {\n transcripts?: { paths?: string[]; extensions?: string[]; enabled?: boolean };\n };\n\n if (sources.transcripts?.enabled && sources.transcripts.paths?.length) {\n const { watchTranscripts, processTranscriptFileAutoMatch } =\n await import(\"../sync/transcript-watcher.js\");\n watchTranscripts({\n paths: sources.transcripts.paths,\n extensions: sources.transcripts.extensions ?? [\".txt\", \".vtt\"],\n dataDir: DATA_DIR,\n onFile: (filePath) => processTranscriptFileAutoMatch(filePath, DATA_DIR),\n });\n process.stderr.write(`[daemon] Watching transcripts (LLM auto-match)\\n`);\n }\n } catch (err) {\n process.stderr.write(`[daemon] Watcher error: ${(err as Error).message}\\n`);\n }\n}\n\nasync function checkAgentWakeTriggers(): Promise<void> {\n const agentsDir = path.join(DATA_DIR, \".agentic\", \"agents\");\n if (!fs.existsSync(agentsDir)) return;\n\n const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(\".agent.json\"));\n\n for (const file of files) {\n try {\n const config = JSON.parse(fs.readFileSync(path.join(agentsDir, file), \"utf-8\") as string) as {\n slug: string;\n channel: string;\n wakeOn: string[];\n lastWake: string | null;\n telegramChatId?: string;\n };\n\n if (!config.wakeOn.includes(\"email\")) continue;\n\n const { getLastGmailSync } = await import(\"../fs/sync-state.js\");\n const lastSync = getLastGmailSync(DATA_DIR, config.slug);\n const lastWake = config.lastWake ? new Date(config.lastWake) : null;\n\n if (!lastSync) continue;\n if (lastWake && lastSync <= lastWake) continue;\n\n // New email since last wake — build context and send notification\n process.stderr.write(`[daemon] Wake trigger: ${config.slug}\\n`);\n\n const { buildContext } = await import(\"../core/context-builder.js\");\n const context = await buildContext(DATA_DIR, config.slug).catch(() => null);\n if (!context) continue;\n\n if (\n config.channel === \"telegram\" &&\n process.env[\"TELEGRAM_BOT_TOKEN\"] &&\n (config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"])\n ) {\n const chatId = config.telegramChatId ?? process.env[\"TELEGRAM_CHAT_ID\"]!;\n const token = process.env[\"TELEGRAM_BOT_TOKEN\"];\n const message = `📬 New activity: *${config.slug}*\\n\\n${context.slice(0, 800)}`;\n\n try {\n const { default: https } = await import(\"https\");\n const body = JSON.stringify({ chat_id: chatId, text: message, parse_mode: \"Markdown\" });\n await new Promise<void>((resolve, reject) => {\n const req = https.request(\n `https://api.telegram.org/bot${token}/sendMessage`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n res.resume();\n resolve();\n }\n );\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n process.stderr.write(`[daemon] Telegram sent for ${config.slug}\\n`);\n } catch (err) {\n process.stderr.write(`[daemon] Telegram failed: ${(err as Error).message}\\n`);\n }\n }\n\n // Update lastWake\n config.lastWake = new Date().toISOString();\n fs.writeFileSync(path.join(agentsDir, file), JSON.stringify(config, null, 2), \"utf-8\");\n } catch (err) {\n process.stderr.write(`[daemon] Agent check error ${file}: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// Gmail sync — interval configurable via DXCRM_DAEMON_INTERVAL (minutes, default 30)\nconst daemonIntervalMin = Math.max(\n 1,\n parseInt(process.env[\"DXCRM_DAEMON_INTERVAL\"] ?? \"30\", 10) || 30\n);\nnew CronJob(\n `*/${daemonIntervalMin} * * * *`,\n async () => {\n await syncAllCustomers();\n await checkAgentWakeTriggers().catch((err: unknown) => {\n process.stderr.write(`[daemon] Wake trigger check failed: ${(err as Error).message}\\n`);\n });\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\n// Scheduled backup check — hourly, runs backup if >1 day since last\nnew CronJob(\n \"*/60 * * * *\",\n async () => {\n try {\n const { runScheduledBackupIfDue } = await import(\"../commands/backup.js\");\n await runScheduledBackupIfDue(DATA_DIR);\n } catch (err) {\n process.stderr.write(`[daemon] Backup check error: ${(err as Error).message}\\n`);\n }\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\n// Daily push subscription renewal at 06:00\nnew CronJob(\n \"0 6 * * *\",\n async () => {\n try {\n const { renewExpiringSubscriptions } = await import(\"../sync/push-manager.js\");\n const { buildGmailRenewFn } = await import(\"../sync/gmail-webhook-handler.js\");\n const tokenPath = path.join(DATA_DIR, \".agentic\", \"gmail-token.json\");\n const credPath = path.join(DATA_DIR, \".agentic\", \"gmail-credentials.json\");\n const { readSubscriptions } = await import(\"../sync/push-manager.js\");\n const subs = await readSubscriptions(DATA_DIR);\n const gmailSubs = subs.filter((s) => s.provider === \"gmail\" && s.status === \"active\");\n if (gmailSubs.length === 0) return;\n if (!fs.existsSync(tokenPath) || !fs.existsSync(credPath)) return;\n const { getGmailAuth } = await import(\"../sync/gmail-auth.js\");\n const auth = await getGmailAuth(credPath, tokenPath);\n const token = (auth.credentials?.access_token as string | undefined) ?? \"\";\n const result = await renewExpiringSubscriptions(DATA_DIR, buildGmailRenewFn(token, \"\"), 24);\n if (result.renewed.length > 0) {\n process.stderr.write(`[push] Renewed ${result.renewed.length} subscription(s)\\n`);\n }\n if (result.errors.length > 0) {\n process.stderr.write(`[push] Renewal errors: ${result.errors.join(\", \")}\\n`);\n }\n } catch (err) {\n process.stderr.write(`[push] Renewal failed: ${(err as Error).message}\\n`);\n }\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\n// Daily proactive checks at 07:00 — relationship decay, deal risk, daily briefing\nnew CronJob(\n \"0 7 * * *\",\n async () => {\n try {\n const { runDailyProactiveChecks } = await import(\"../daemon/proactive-worker.js\");\n const result = await runDailyProactiveChecks(DATA_DIR);\n process.stderr.write(\n `[proactive] Daily check: ${result.customersChecked} customers, ${result.tasksEnqueued} tasks enqueued\\n`\n );\n if (result.errors.length > 0) {\n process.stderr.write(`[proactive] Errors: ${result.errors.join(\", \")}\\n`);\n }\n const { drainProactiveQueue } = await import(\"../core/notification-dispatcher.js\");\n const drain = await drainProactiveQueue(DATA_DIR);\n process.stderr.write(\n `[proactive] Dispatched ${drain.sent} task(s), ${drain.failed} failed\\n`\n );\n const { syncGoalProgressFromPipeline } = await import(\"../core/goal-engine.js\");\n const goalSync = await syncGoalProgressFromPipeline(DATA_DIR);\n if (goalSync.updated.length > 0) {\n process.stderr.write(`[goals] Progress synced: ${goalSync.updated.join(\", \")}\\n`);\n }\n } catch (err) {\n process.stderr.write(`[proactive] Daily check failed: ${(err as Error).message}\\n`);\n }\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\n// SLA breach check — daily at 08:00\nnew CronJob(\n \"0 8 * * *\",\n async () => {\n try {\n const { checkSlaBreaches } = await import(\"../core/sla-engine.js\");\n const today = new Date().toISOString().slice(0, 10);\n const breaches = await checkSlaBreaches(DATA_DIR, today);\n if (breaches.length > 0) {\n process.stderr.write(`[tickets] ${breaches.length} SLA breach(es) found\\n`);\n for (const { slug, ticket } of breaches) {\n process.stderr.write(\n `[tickets] SLA breach: ${slug}/${ticket.id} \"${ticket.title}\" due ${ticket.slaDue}\\n`\n );\n }\n }\n } catch (err) {\n process.stderr.write(`[tickets] SLA check failed: ${(err as Error).message}\\n`);\n }\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\n// Email sequence cycle — every 6 hours\nnew CronJob(\n \"0 */6 * * *\",\n async () => {\n try {\n const { runSequenceCycle } = await import(\"../core/sequence-engine.js\");\n const today = new Date().toISOString().slice(0, 10);\n const result = await runSequenceCycle(DATA_DIR, today);\n process.stderr.write(\n `[sequences] ${result.sent} sent, ${result.completed} completed, ${result.errors.length} errors\\n`\n );\n } catch (err) {\n process.stderr.write(`[sequences] cycle failed: ${(err as Error).message}\\n`);\n }\n },\n null,\n true,\n undefined,\n null,\n false,\n undefined,\n false, // unrefTimeout — keep event loop alive\n true // waitForCompletion\n);\n\nawait startWatcher();\n\n// Signal ready\nif (process.send) process.send(\"ready\");\nprocess.stderr.write(\"[daemon] DatasynxOpenCRM daemon started\\n\");\n"],"mappings":"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"}
|