@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,295 @@
|
|
|
1
|
+
import { r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
|
|
2
|
+
import { n as getActor } from "./audit-log-DNMY9mUZ.js";
|
|
3
|
+
import { t as withJsonFile } from "./file-lock-B_zi7NQl.js";
|
|
4
|
+
import { l as runSimulation } from "./revenue-simulation-Bqf2DLVB.js";
|
|
5
|
+
import { t as readPipeline } from "./pipeline-writer-BvVquKIe.js";
|
|
6
|
+
import { o as guardIsoDate, t as callLlm } from "./llm-DvzZqva0.js";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
//#region src/core/goal-engine.ts
|
|
10
|
+
function goalsPath(dataDir) {
|
|
11
|
+
return path.join(dataDir, ".agentic", "goals.json");
|
|
12
|
+
}
|
|
13
|
+
function readGoals(dataDir) {
|
|
14
|
+
const p = goalsPath(dataDir);
|
|
15
|
+
if (!fs.existsSync(p)) return [];
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
18
|
+
if (Array.isArray(raw)) return raw;
|
|
19
|
+
return raw.goals ?? [];
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeGoals(dataDir, goals) {
|
|
25
|
+
const p = goalsPath(dataDir);
|
|
26
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
27
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
28
|
+
goals,
|
|
29
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
30
|
+
}, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
function makeGoalId() {
|
|
33
|
+
return `goal_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
34
|
+
}
|
|
35
|
+
function parseTargetFromDescription(desc) {
|
|
36
|
+
const millionMatch = desc.match(/[\$€£]?\s*(\d+(?:\.\d+)?)\s*(?:M\b|million)/i);
|
|
37
|
+
if (millionMatch) return Math.round(parseFloat(millionMatch[1]) * 1e6);
|
|
38
|
+
const kMatch = desc.match(/[\$€£]?\s*(\d+(?:\.\d+)?)\s*k\b/i);
|
|
39
|
+
if (kMatch) return Math.round(parseFloat(kMatch[1]) * 1e3);
|
|
40
|
+
const rawMatch = desc.match(/[\$€£]\s*(\d{4,}(?:[,.\d]*\d)?)/);
|
|
41
|
+
if (rawMatch) return parseInt(rawMatch[1].replace(/[,. ]/g, ""), 10);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
function inferGoalType(desc) {
|
|
45
|
+
const lower = desc.toLowerCase();
|
|
46
|
+
if (/churn|retain|renewal|renew/.test(lower)) return "churn_prevention";
|
|
47
|
+
if (/meeting|call|book|relationship|contact/.test(lower)) return "relationship";
|
|
48
|
+
if (/pipeline|prospect|lead|qualify/.test(lower)) return "pipeline";
|
|
49
|
+
return "revenue";
|
|
50
|
+
}
|
|
51
|
+
function inferMetric(type) {
|
|
52
|
+
switch (type) {
|
|
53
|
+
case "pipeline": return "pipeline_created";
|
|
54
|
+
case "relationship": return "meetings_booked";
|
|
55
|
+
case "revenue": return "revenue";
|
|
56
|
+
case "churn_prevention": return "revenue";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function rankDealsByLeverage(deals) {
|
|
60
|
+
return deals.filter((d) => d.stage !== "won" && d.stage !== "lost").sort((a, b) => {
|
|
61
|
+
const leverageA = a.value * (a.probability / 100) * (a.healthScore / 100);
|
|
62
|
+
return b.value * (b.probability / 100) * (b.healthScore / 100) - leverageA;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function generateNextStep(deal) {
|
|
66
|
+
if (deal.healthScore < 40 && !deal.championPresent) return "Re-engage urgently and identify a champion";
|
|
67
|
+
if (deal.healthScore < 60) return "Schedule an urgent check-in call";
|
|
68
|
+
if (deal.daysSinceContact > 14) return "Reach out — contact is overdue";
|
|
69
|
+
if (!deal.championPresent) return "Identify a champion or economic buyer";
|
|
70
|
+
return "Push to next pipeline stage";
|
|
71
|
+
}
|
|
72
|
+
function decomposeGoalRuleBased(deals, target, currentP50, today, playbookLookup) {
|
|
73
|
+
const gap = Math.max(0, target - currentP50);
|
|
74
|
+
const decomposedAt = (/* @__PURE__ */ new Date(today + "T00:00:00Z")).toISOString();
|
|
75
|
+
if (gap === 0) return {
|
|
76
|
+
analysis: `Current pipeline (P50: €${currentP50.toLocaleString()}) already meets or exceeds the target of €${target.toLocaleString()}.`,
|
|
77
|
+
currentPipeline: currentP50,
|
|
78
|
+
gap: 0,
|
|
79
|
+
subGoals: [],
|
|
80
|
+
probabilisticOutcome: `Pipeline P50 (€${currentP50.toLocaleString()}) ≥ target (€${target.toLocaleString()}).`,
|
|
81
|
+
decomposedAt
|
|
82
|
+
};
|
|
83
|
+
const ranked = rankDealsByLeverage(deals);
|
|
84
|
+
if (ranked.length === 0) return {
|
|
85
|
+
analysis: `No active deals found. Gap to close: €${gap.toLocaleString()}. Focus on building pipeline.`,
|
|
86
|
+
currentPipeline: currentP50,
|
|
87
|
+
gap,
|
|
88
|
+
subGoals: [{
|
|
89
|
+
priority: 1,
|
|
90
|
+
action: "Build pipeline from scratch",
|
|
91
|
+
slug: "_all",
|
|
92
|
+
why: `No active deals. Need €${gap.toLocaleString()} to reach target.`,
|
|
93
|
+
nextStep: "Use list_customers() to find prospects and log_interaction to initiate outreach",
|
|
94
|
+
targetDelta: target
|
|
95
|
+
}],
|
|
96
|
+
probabilisticOutcome: `Insufficient pipeline. Need €${gap.toLocaleString()} in new deals.`,
|
|
97
|
+
decomposedAt
|
|
98
|
+
};
|
|
99
|
+
const subGoals = [];
|
|
100
|
+
let cumulative = 0;
|
|
101
|
+
for (const deal of ranked.slice(0, 5)) {
|
|
102
|
+
if (subGoals.length >= 5) break;
|
|
103
|
+
const playbookName = playbookLookup?.(deal.slug, deal);
|
|
104
|
+
const subGoal = {
|
|
105
|
+
priority: subGoals.length + 1,
|
|
106
|
+
action: `Accelerate ${deal.slug}/${deal.name}`,
|
|
107
|
+
slug: deal.slug,
|
|
108
|
+
...deal.name ? { dealName: deal.name } : {},
|
|
109
|
+
why: `€${deal.value.toLocaleString()} deal in ${deal.stage} — health ${deal.healthScore}/100`,
|
|
110
|
+
nextStep: generateNextStep(deal),
|
|
111
|
+
targetDelta: deal.value,
|
|
112
|
+
...playbookName ? { playbookName } : {}
|
|
113
|
+
};
|
|
114
|
+
subGoals.push(subGoal);
|
|
115
|
+
cumulative += deal.value;
|
|
116
|
+
if (cumulative >= gap) break;
|
|
117
|
+
}
|
|
118
|
+
const projectedTotal = currentP50 + cumulative;
|
|
119
|
+
return {
|
|
120
|
+
analysis: `Current pipeline P50: €${currentP50.toLocaleString()}. Gap to target: €${gap.toLocaleString()}. Top ${subGoals.length} deal(s) identified.`,
|
|
121
|
+
currentPipeline: currentP50,
|
|
122
|
+
gap,
|
|
123
|
+
subGoals,
|
|
124
|
+
probabilisticOutcome: `If all recommended deals close: ~€${projectedTotal.toLocaleString()} (target: €${target.toLocaleString()}).`,
|
|
125
|
+
decomposedAt
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildDecompositionPrompt(description, target, deadline, currentP50, deals, today) {
|
|
129
|
+
const gap = Math.max(0, target - currentP50);
|
|
130
|
+
const dealLines = deals.filter((d) => d.stage !== "won" && d.stage !== "lost").slice(0, 8).map((d, i) => `${i + 1}. ${d.slug}/${d.name} — €${d.value.toLocaleString()}, stage: ${d.stage}, health: ${d.healthScore}/100, probability: ${d.probability}%${d.championPresent ? ", champion ✓" : ""}`).join("\n");
|
|
131
|
+
return `You are a sales strategy AI helping decompose a revenue goal into actionable sub-goals.
|
|
132
|
+
|
|
133
|
+
Goal: ${description}
|
|
134
|
+
Target: €${target.toLocaleString()}
|
|
135
|
+
Deadline: ${deadline}
|
|
136
|
+
Current date: ${today}
|
|
137
|
+
Current weighted pipeline (P50): €${currentP50.toLocaleString()}
|
|
138
|
+
Gap to close: €${gap.toLocaleString()}
|
|
139
|
+
|
|
140
|
+
Active deals (sorted by weighted value):
|
|
141
|
+
${dealLines || "(no active deals)"}
|
|
142
|
+
|
|
143
|
+
Return JSON only (no markdown wrapper):
|
|
144
|
+
{
|
|
145
|
+
"analysis": "<1-2 sentence summary of the situation>",
|
|
146
|
+
"subGoals": [
|
|
147
|
+
{
|
|
148
|
+
"priority": 1,
|
|
149
|
+
"action": "<what to do>",
|
|
150
|
+
"slug": "<customer-slug>",
|
|
151
|
+
"dealName": "<deal name>",
|
|
152
|
+
"why": "<why this deal matters for the goal>",
|
|
153
|
+
"nextStep": "<concrete next action with deadline>",
|
|
154
|
+
"targetDelta": <expected revenue contribution in euros>
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"probabilisticOutcome": "<P50 forecast summary after actions>"
|
|
158
|
+
}`;
|
|
159
|
+
}
|
|
160
|
+
function parseLlmDecomposition(response, fallback) {
|
|
161
|
+
try {
|
|
162
|
+
const match = response.match(/\{[\s\S]*\}/);
|
|
163
|
+
if (!match) return fallback;
|
|
164
|
+
const parsed = JSON.parse(match[0]);
|
|
165
|
+
if (!parsed.analysis || !Array.isArray(parsed.subGoals)) return fallback;
|
|
166
|
+
return {
|
|
167
|
+
analysis: parsed.analysis,
|
|
168
|
+
currentPipeline: fallback.currentPipeline,
|
|
169
|
+
gap: fallback.gap,
|
|
170
|
+
subGoals: parsed.subGoals.map((s, i) => ({
|
|
171
|
+
priority: s.priority ?? i + 1,
|
|
172
|
+
action: s.action ?? "",
|
|
173
|
+
slug: s.slug ?? "_all",
|
|
174
|
+
...s.dealName ? { dealName: s.dealName } : {},
|
|
175
|
+
why: s.why ?? "",
|
|
176
|
+
nextStep: s.nextStep ?? "",
|
|
177
|
+
targetDelta: s.targetDelta ?? 0,
|
|
178
|
+
...s.playbookName ? { playbookName: s.playbookName } : {}
|
|
179
|
+
})),
|
|
180
|
+
probabilisticOutcome: parsed.probabilisticOutcome ?? fallback.probabilisticOutcome,
|
|
181
|
+
decomposedAt: fallback.decomposedAt
|
|
182
|
+
};
|
|
183
|
+
} catch {
|
|
184
|
+
return fallback;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function pursueGoal(dataDir, input, options = {}) {
|
|
188
|
+
guardIsoDate(input.deadline, "deadline");
|
|
189
|
+
const today = options.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
190
|
+
const actor = options.actor ?? getActor();
|
|
191
|
+
const simInput = await (options.buildInputFn ?? (async (dir, horizon, t) => {
|
|
192
|
+
const { buildSimulationInput } = await import("./revenue-simulation-BJdRTEHc.js");
|
|
193
|
+
return buildSimulationInput(dir, horizon, t);
|
|
194
|
+
}))(dataDir, "quarter", today);
|
|
195
|
+
const currentP50 = runSimulation(simInput).p50;
|
|
196
|
+
const deals = simInput.deals;
|
|
197
|
+
const target = parseTargetFromDescription(input.description);
|
|
198
|
+
const type = inferGoalType(input.description);
|
|
199
|
+
const metric = inferMetric(type);
|
|
200
|
+
const ruleBasedDecomp = decomposeGoalRuleBased(deals, target, currentP50, today);
|
|
201
|
+
let decomposition = ruleBasedDecomp;
|
|
202
|
+
const llmFn = options.llmFn ?? callLlm;
|
|
203
|
+
if (options.llmFn !== void 0) decomposition = parseLlmDecomposition(await llmFn(buildDecompositionPrompt(input.description, target, input.deadline, currentP50, deals, today)), ruleBasedDecomp);
|
|
204
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
205
|
+
const goal = {
|
|
206
|
+
id: makeGoalId(),
|
|
207
|
+
description: input.description,
|
|
208
|
+
type,
|
|
209
|
+
target,
|
|
210
|
+
metric,
|
|
211
|
+
deadline: input.deadline,
|
|
212
|
+
decomposition,
|
|
213
|
+
progress: 0,
|
|
214
|
+
status: "active",
|
|
215
|
+
createdAt: now,
|
|
216
|
+
updatedAt: now,
|
|
217
|
+
actor
|
|
218
|
+
};
|
|
219
|
+
await withJsonFile(goalsPath(dataDir), (current) => {
|
|
220
|
+
return {
|
|
221
|
+
goals: [...Array.isArray(current?.goals) ? current.goals : [], goal],
|
|
222
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
return goal;
|
|
226
|
+
}
|
|
227
|
+
function getActiveGoals(dataDir) {
|
|
228
|
+
return readGoals(dataDir).filter((g) => g.status === "active");
|
|
229
|
+
}
|
|
230
|
+
async function updateGoalProgress(dataDir, goalId, progress) {
|
|
231
|
+
let updated = null;
|
|
232
|
+
await withJsonFile(goalsPath(dataDir), (current) => {
|
|
233
|
+
const goals = Array.isArray(current?.goals) ? [...current.goals] : [];
|
|
234
|
+
const idx = goals.findIndex((g) => g.id === goalId);
|
|
235
|
+
if (idx >= 0) {
|
|
236
|
+
updated = {
|
|
237
|
+
...goals[idx],
|
|
238
|
+
progress,
|
|
239
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
240
|
+
};
|
|
241
|
+
goals[idx] = updated;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
goals,
|
|
245
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
return updated;
|
|
249
|
+
}
|
|
250
|
+
async function cancelGoal(dataDir, goalId) {
|
|
251
|
+
let cancelled = null;
|
|
252
|
+
await withJsonFile(goalsPath(dataDir), (current) => {
|
|
253
|
+
const goals = Array.isArray(current?.goals) ? [...current.goals] : [];
|
|
254
|
+
const idx = goals.findIndex((g) => g.id === goalId);
|
|
255
|
+
if (idx >= 0) {
|
|
256
|
+
cancelled = {
|
|
257
|
+
...goals[idx],
|
|
258
|
+
status: "cancelled",
|
|
259
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
260
|
+
};
|
|
261
|
+
goals[idx] = cancelled;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
goals,
|
|
265
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
return cancelled;
|
|
269
|
+
}
|
|
270
|
+
async function syncGoalProgressFromPipeline(dataDir, _today) {
|
|
271
|
+
const activeGoals = getActiveGoals(dataDir);
|
|
272
|
+
const revenueGoals = activeGoals.filter((g) => g.metric === "revenue" && g.target > 0);
|
|
273
|
+
if (revenueGoals.length === 0) return {
|
|
274
|
+
updated: [],
|
|
275
|
+
skipped: activeGoals.length
|
|
276
|
+
};
|
|
277
|
+
let totalWon = 0;
|
|
278
|
+
for (const slug of listCustomerSlugs(dataDir)) {
|
|
279
|
+
const deals = await readPipeline(dataDir, slug).catch(() => []);
|
|
280
|
+
for (const deal of deals) if (deal.stage === "won") totalWon += deal.value ?? 0;
|
|
281
|
+
}
|
|
282
|
+
const updated = [];
|
|
283
|
+
for (const goal of revenueGoals) {
|
|
284
|
+
const progress = Math.min(100, Math.round(totalWon / goal.target * 100));
|
|
285
|
+
if (await updateGoalProgress(dataDir, goal.id, progress)) updated.push(goal.id);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
updated,
|
|
289
|
+
skipped: activeGoals.length - revenueGoals.length
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
export { goalsPath as a, makeGoalId as c, pursueGoal as d, rankDealsByLeverage as f, writeGoals as g, updateGoalProgress as h, getActiveGoals as i, parseLlmDecomposition as l, syncGoalProgressFromPipeline as m, cancelGoal as n, inferGoalType as o, readGoals as p, decomposeGoalRuleBased as r, inferMetric as s, buildDecompositionPrompt as t, parseTargetFromDescription as u };
|
|
294
|
+
|
|
295
|
+
//# sourceMappingURL=goal-engine-KpBftn4V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goal-engine-KpBftn4V.js","names":[],"sources":["../src/core/goal-engine.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { runSimulation } from \"./revenue-simulation.js\";\nimport { callLlm } from \"./llm.js\";\nimport { getActor } from \"../fs/audit-log.js\";\nimport { withJsonFile } from \"./file-lock.js\";\nimport { guardIsoDate } from \"./input-guard.js\";\nimport type { DealSnapshot, SimulationInput } from \"./revenue-simulation.js\";\nimport { readPipeline } from \"../fs/pipeline-writer.js\";\nimport { listCustomerSlugs } from \"../fs/customer-dir.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type GoalMetric = \"revenue\" | \"deals_closed\" | \"meetings_booked\" | \"pipeline_created\";\nexport type GoalType = \"revenue\" | \"pipeline\" | \"relationship\" | \"churn_prevention\";\nexport type GoalStatus = \"active\" | \"completed\" | \"cancelled\" | \"blocked\";\n\nexport interface GoalSubGoal {\n priority: number;\n action: string;\n slug: string;\n dealName?: string;\n why: string;\n nextStep: string;\n targetDelta: number;\n playbookName?: string;\n}\n\nexport interface GoalDecomposition {\n analysis: string;\n currentPipeline: number;\n gap: number;\n subGoals: GoalSubGoal[];\n probabilisticOutcome: string;\n decomposedAt: string;\n}\n\nexport interface Goal {\n id: string;\n description: string;\n type: GoalType;\n target: number;\n metric: GoalMetric;\n deadline: string;\n decomposition: GoalDecomposition;\n progress: number;\n status: GoalStatus;\n createdAt: string;\n updatedAt: string;\n actor: string;\n}\n\nexport type BuildInputFn = (\n dataDir: string,\n horizon: \"quarter\" | \"year\",\n today: string\n) => Promise<SimulationInput>;\n\n// ─── Persistence ──────────────────────────────────────────────────────────────\n\nexport function goalsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"goals.json\");\n}\n\nexport function readGoals(dataDir: string): Goal[] {\n const p = goalsPath(dataDir);\n if (!fs.existsSync(p)) return [];\n try {\n const raw = JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as unknown;\n if (Array.isArray(raw)) return raw as Goal[];\n return (raw as { goals?: Goal[] }).goals ?? [];\n } catch {\n return [];\n }\n}\n\nexport function writeGoals(dataDir: string, goals: Goal[]): void {\n const p = goalsPath(dataDir);\n fs.mkdirSync(path.dirname(p), { recursive: true });\n fs.writeFileSync(\n p,\n JSON.stringify({ goals, updatedAt: new Date().toISOString() }, null, 2),\n \"utf-8\"\n );\n}\n\nexport function makeGoalId(): string {\n return `goal_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;\n}\n\n// ─── Parsing ──────────────────────────────────────────────────────────────────\n\nexport function parseTargetFromDescription(desc: string): number {\n // Try millions first: \"1.5M\", \"1.5 million\", \"$1.5M\"\n const millionMatch = desc.match(/[\\$€£]?\\s*(\\d+(?:\\.\\d+)?)\\s*(?:M\\b|million)/i);\n if (millionMatch) return Math.round(parseFloat(millionMatch[1]!) * 1_000_000);\n\n // Then thousands: \"500k\", \"€500k\"\n const kMatch = desc.match(/[\\$€£]?\\s*(\\d+(?:\\.\\d+)?)\\s*k\\b/i);\n if (kMatch) return Math.round(parseFloat(kMatch[1]!) * 1_000);\n\n // Then raw numbers with optional currency: \"€75000\"\n const rawMatch = desc.match(/[\\$€£]\\s*(\\d{4,}(?:[,.\\d]*\\d)?)/);\n if (rawMatch) return parseInt(rawMatch[1]!.replace(/[,. ]/g, \"\"), 10);\n\n return 0;\n}\n\nexport function inferGoalType(desc: string): GoalType {\n const lower = desc.toLowerCase();\n if (/churn|retain|renewal|renew/.test(lower)) return \"churn_prevention\";\n if (/meeting|call|book|relationship|contact/.test(lower)) return \"relationship\";\n if (/pipeline|prospect|lead|qualify/.test(lower)) return \"pipeline\";\n return \"revenue\";\n}\n\nexport function inferMetric(type: GoalType): GoalMetric {\n switch (type) {\n case \"pipeline\":\n return \"pipeline_created\";\n case \"relationship\":\n return \"meetings_booked\";\n case \"revenue\":\n return \"revenue\";\n case \"churn_prevention\":\n return \"revenue\";\n }\n}\n\n// ─── Rule-based decomposition ─────────────────────────────────────────────────\n\nexport function rankDealsByLeverage(deals: DealSnapshot[]): DealSnapshot[] {\n return deals\n .filter((d) => d.stage !== \"won\" && d.stage !== \"lost\")\n .sort((a, b) => {\n const leverageA = a.value * (a.probability / 100) * (a.healthScore / 100);\n const leverageB = b.value * (b.probability / 100) * (b.healthScore / 100);\n return leverageB - leverageA;\n });\n}\n\nfunction generateNextStep(deal: DealSnapshot): string {\n if (deal.healthScore < 40 && !deal.championPresent)\n return \"Re-engage urgently and identify a champion\";\n if (deal.healthScore < 60) return \"Schedule an urgent check-in call\";\n if (deal.daysSinceContact > 14) return \"Reach out — contact is overdue\";\n if (!deal.championPresent) return \"Identify a champion or economic buyer\";\n return \"Push to next pipeline stage\";\n}\n\nexport function decomposeGoalRuleBased(\n deals: DealSnapshot[],\n target: number,\n currentP50: number,\n today: string,\n playbookLookup?: (slug: string, deal: DealSnapshot) => string | undefined\n): GoalDecomposition {\n const gap = Math.max(0, target - currentP50);\n const decomposedAt = new Date(today + \"T00:00:00Z\").toISOString();\n\n if (gap === 0) {\n return {\n analysis: `Current pipeline (P50: €${currentP50.toLocaleString()}) already meets or exceeds the target of €${target.toLocaleString()}.`,\n currentPipeline: currentP50,\n gap: 0,\n subGoals: [],\n probabilisticOutcome: `Pipeline P50 (€${currentP50.toLocaleString()}) ≥ target (€${target.toLocaleString()}).`,\n decomposedAt,\n };\n }\n\n const ranked = rankDealsByLeverage(deals);\n\n if (ranked.length === 0) {\n return {\n analysis: `No active deals found. Gap to close: €${gap.toLocaleString()}. Focus on building pipeline.`,\n currentPipeline: currentP50,\n gap,\n subGoals: [\n {\n priority: 1,\n action: \"Build pipeline from scratch\",\n slug: \"_all\",\n why: `No active deals. Need €${gap.toLocaleString()} to reach target.`,\n nextStep:\n \"Use list_customers() to find prospects and log_interaction to initiate outreach\",\n targetDelta: target,\n },\n ],\n probabilisticOutcome: `Insufficient pipeline. Need €${gap.toLocaleString()} in new deals.`,\n decomposedAt,\n };\n }\n\n const subGoals: GoalSubGoal[] = [];\n let cumulative = 0;\n\n for (const deal of ranked.slice(0, 5)) {\n if (subGoals.length >= 5) break;\n const playbookName = playbookLookup?.(deal.slug, deal);\n const subGoal: GoalSubGoal = {\n priority: subGoals.length + 1,\n action: `Accelerate ${deal.slug}/${deal.name}`,\n slug: deal.slug,\n ...(deal.name ? { dealName: deal.name } : {}),\n why: `€${deal.value.toLocaleString()} deal in ${deal.stage} — health ${deal.healthScore}/100`,\n nextStep: generateNextStep(deal),\n targetDelta: deal.value,\n ...(playbookName ? { playbookName } : {}),\n };\n subGoals.push(subGoal);\n cumulative += deal.value;\n if (cumulative >= gap) break;\n }\n\n const projectedTotal = currentP50 + cumulative;\n return {\n analysis: `Current pipeline P50: €${currentP50.toLocaleString()}. Gap to target: €${gap.toLocaleString()}. Top ${subGoals.length} deal(s) identified.`,\n currentPipeline: currentP50,\n gap,\n subGoals,\n probabilisticOutcome: `If all recommended deals close: ~€${projectedTotal.toLocaleString()} (target: €${target.toLocaleString()}).`,\n decomposedAt,\n };\n}\n\n// ─── LLM path ─────────────────────────────────────────────────────────────────\n\nexport function buildDecompositionPrompt(\n description: string,\n target: number,\n deadline: string,\n currentP50: number,\n deals: DealSnapshot[],\n today: string\n): string {\n const gap = Math.max(0, target - currentP50);\n const dealLines = deals\n .filter((d) => d.stage !== \"won\" && d.stage !== \"lost\")\n .slice(0, 8)\n .map(\n (d, i) =>\n `${i + 1}. ${d.slug}/${d.name} — €${d.value.toLocaleString()}, stage: ${d.stage}, health: ${d.healthScore}/100, probability: ${d.probability}%${d.championPresent ? \", champion ✓\" : \"\"}`\n )\n .join(\"\\n\");\n\n return `You are a sales strategy AI helping decompose a revenue goal into actionable sub-goals.\n\nGoal: ${description}\nTarget: €${target.toLocaleString()}\nDeadline: ${deadline}\nCurrent date: ${today}\nCurrent weighted pipeline (P50): €${currentP50.toLocaleString()}\nGap to close: €${gap.toLocaleString()}\n\nActive deals (sorted by weighted value):\n${dealLines || \"(no active deals)\"}\n\nReturn JSON only (no markdown wrapper):\n{\n \"analysis\": \"<1-2 sentence summary of the situation>\",\n \"subGoals\": [\n {\n \"priority\": 1,\n \"action\": \"<what to do>\",\n \"slug\": \"<customer-slug>\",\n \"dealName\": \"<deal name>\",\n \"why\": \"<why this deal matters for the goal>\",\n \"nextStep\": \"<concrete next action with deadline>\",\n \"targetDelta\": <expected revenue contribution in euros>\n }\n ],\n \"probabilisticOutcome\": \"<P50 forecast summary after actions>\"\n}`;\n}\n\nexport function parseLlmDecomposition(\n response: string,\n fallback: GoalDecomposition\n): GoalDecomposition {\n try {\n const match = response.match(/\\{[\\s\\S]*\\}/);\n if (!match) return fallback;\n const parsed = JSON.parse(match[0]) as Partial<{\n analysis: string;\n subGoals: unknown[];\n probabilisticOutcome: string;\n }>;\n if (!parsed.analysis || !Array.isArray(parsed.subGoals)) return fallback;\n return {\n analysis: parsed.analysis,\n currentPipeline: fallback.currentPipeline,\n gap: fallback.gap,\n subGoals: (parsed.subGoals as Partial<GoalSubGoal>[]).map((s, i) => ({\n priority: s.priority ?? i + 1,\n action: s.action ?? \"\",\n slug: s.slug ?? \"_all\",\n ...(s.dealName ? { dealName: s.dealName } : {}),\n why: s.why ?? \"\",\n nextStep: s.nextStep ?? \"\",\n targetDelta: s.targetDelta ?? 0,\n ...(s.playbookName ? { playbookName: s.playbookName } : {}),\n })),\n probabilisticOutcome: parsed.probabilisticOutcome ?? fallback.probabilisticOutcome,\n decomposedAt: fallback.decomposedAt,\n };\n } catch {\n return fallback;\n }\n}\n\n// ─── pursueGoal ───────────────────────────────────────────────────────────────\n\nexport async function pursueGoal(\n dataDir: string,\n input: { description: string; deadline: string; context?: string },\n options: {\n llmFn?: (prompt: string) => Promise<string>;\n buildInputFn?: BuildInputFn;\n today?: string;\n actor?: string;\n } = {}\n): Promise<Goal> {\n guardIsoDate(input.deadline, \"deadline\");\n const today = options.today ?? new Date().toISOString().slice(0, 10);\n const actor = options.actor ?? getActor();\n\n const buildFn =\n options.buildInputFn ??\n ((async (dir, horizon, t) => {\n const { buildSimulationInput } = await import(\"./revenue-simulation.js\");\n return buildSimulationInput(dir, horizon, t);\n }) as BuildInputFn);\n\n const simInput = await buildFn(dataDir, \"quarter\", today);\n const simResult = runSimulation(simInput);\n const currentP50 = simResult.p50;\n const deals = simInput.deals;\n\n const target = parseTargetFromDescription(input.description);\n const type = inferGoalType(input.description);\n const metric = inferMetric(type);\n\n const ruleBasedDecomp = decomposeGoalRuleBased(deals, target, currentP50, today);\n\n let decomposition = ruleBasedDecomp;\n const llmFn = options.llmFn ?? callLlm;\n if (options.llmFn !== undefined) {\n const prompt = buildDecompositionPrompt(\n input.description,\n target,\n input.deadline,\n currentP50,\n deals,\n today\n );\n const response = await llmFn(prompt);\n decomposition = parseLlmDecomposition(response, ruleBasedDecomp);\n }\n\n const now = new Date().toISOString();\n const goal: Goal = {\n id: makeGoalId(),\n description: input.description,\n type,\n target,\n metric,\n deadline: input.deadline,\n decomposition,\n progress: 0,\n status: \"active\",\n createdAt: now,\n updatedAt: now,\n actor,\n };\n\n await withJsonFile<{ goals: Goal[]; updatedAt: string }>(goalsPath(dataDir), (current) => {\n const existing: Goal[] = Array.isArray(current?.goals) ? current.goals : [];\n return { goals: [...existing, goal], updatedAt: new Date().toISOString() };\n });\n return goal;\n}\n\n// ─── Goal management ──────────────────────────────────────────────────────────\n\nexport function getActiveGoals(dataDir: string): Goal[] {\n return readGoals(dataDir).filter((g) => g.status === \"active\");\n}\n\nexport async function updateGoalProgress(\n dataDir: string,\n goalId: string,\n progress: number\n): Promise<Goal | null> {\n let updated: Goal | null = null;\n await withJsonFile<{ goals: Goal[]; updatedAt: string }>(goalsPath(dataDir), (current) => {\n const goals: Goal[] = Array.isArray(current?.goals) ? [...current.goals] : [];\n const idx = goals.findIndex((g) => g.id === goalId);\n if (idx >= 0) {\n updated = { ...goals[idx]!, progress, updatedAt: new Date().toISOString() };\n goals[idx] = updated;\n }\n return { goals, updatedAt: new Date().toISOString() };\n });\n return updated;\n}\n\nexport async function cancelGoal(dataDir: string, goalId: string): Promise<Goal | null> {\n let cancelled: Goal | null = null;\n await withJsonFile<{ goals: Goal[]; updatedAt: string }>(goalsPath(dataDir), (current) => {\n const goals: Goal[] = Array.isArray(current?.goals) ? [...current.goals] : [];\n const idx = goals.findIndex((g) => g.id === goalId);\n if (idx >= 0) {\n cancelled = {\n ...goals[idx]!,\n status: \"cancelled\" as const,\n updatedAt: new Date().toISOString(),\n };\n goals[idx] = cancelled;\n }\n return { goals, updatedAt: new Date().toISOString() };\n });\n return cancelled;\n}\n\n// ─── Pipeline-driven progress sync ────────────────────────────────────────────\n\nexport interface SyncResult {\n updated: string[];\n skipped: number;\n}\n\nexport async function syncGoalProgressFromPipeline(\n dataDir: string,\n _today?: string\n): Promise<SyncResult> {\n const activeGoals = getActiveGoals(dataDir);\n const revenueGoals = activeGoals.filter((g) => g.metric === \"revenue\" && g.target > 0);\n\n if (revenueGoals.length === 0) return { updated: [], skipped: activeGoals.length };\n\n let totalWon = 0;\n for (const slug of listCustomerSlugs(dataDir)) {\n const deals = await readPipeline(dataDir, slug).catch(() => []);\n for (const deal of deals) {\n if (deal.stage === \"won\") totalWon += deal.value ?? 0;\n }\n }\n\n const updated: string[] = [];\n for (const goal of revenueGoals) {\n const progress = Math.min(100, Math.round((totalWon / goal.target) * 100));\n const result = await updateGoalProgress(dataDir, goal.id, progress);\n if (result) updated.push(goal.id);\n }\n\n return { updated, skipped: activeGoals.length - revenueGoals.length };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { r as readInteractions, t as appendInteraction } from "./interactions-writer-SLHnoEeE.js";
|
|
2
|
+
import { n as indexInLanceDB } from "./lancedb-rlvWoPwl.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
//#region src/sync/google-drive-sync.ts
|
|
6
|
+
const GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
|
|
7
|
+
const DRIVE_API_BASE = "https://www.googleapis.com/drive/v3";
|
|
8
|
+
async function syncGoogleDriveFiles(opts) {
|
|
9
|
+
const { slug, dataDir, accessToken } = opts;
|
|
10
|
+
const searchName = opts.customerName ?? slug;
|
|
11
|
+
const maxFiles = opts.maxFiles ?? 200;
|
|
12
|
+
const result = {
|
|
13
|
+
synced: 0,
|
|
14
|
+
skipped: 0,
|
|
15
|
+
errors: []
|
|
16
|
+
};
|
|
17
|
+
let existingInteractions = "";
|
|
18
|
+
try {
|
|
19
|
+
existingInteractions = await readInteractions(dataDir, slug);
|
|
20
|
+
} catch {
|
|
21
|
+
existingInteractions = "";
|
|
22
|
+
}
|
|
23
|
+
const encodedQuery = encodeURIComponent(`name contains "${searchName}" and mimeType!="application/vnd.google-apps.folder"`);
|
|
24
|
+
const fields = encodeURIComponent("files(id,name,mimeType,webViewLink,modifiedTime,size),nextPageToken");
|
|
25
|
+
let pageToken;
|
|
26
|
+
let totalFetched = 0;
|
|
27
|
+
do {
|
|
28
|
+
let url = `${DRIVE_API_BASE}/files?q=${encodedQuery}&fields=${fields}&pageSize=50`;
|
|
29
|
+
if (pageToken) url += `&pageToken=${encodeURIComponent(pageToken)}`;
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
result.errors.push(`Drive API request failed: ${err.message}`);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
result.errors.push(`Drive API error ${response.status}: ${await response.text().catch(() => "unknown")}`);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = await response.json();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
result.errors.push(`Failed to parse Drive API response: ${err.message}`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const files = data.files ?? [];
|
|
49
|
+
pageToken = data.nextPageToken;
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (totalFetched >= maxFiles) break;
|
|
52
|
+
totalFetched++;
|
|
53
|
+
const sourceRef = `google://drive/${file.id}`;
|
|
54
|
+
if (existingInteractions.includes(sourceRef)) {
|
|
55
|
+
result.skipped++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (file.mimeType === GOOGLE_DOC_MIME) {
|
|
60
|
+
const exportUrl = `${DRIVE_API_BASE}/files/${file.id}/export?mimeType=${encodeURIComponent("text/plain")}`;
|
|
61
|
+
const exportRes = await fetch(exportUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
62
|
+
if (!exportRes.ok) {
|
|
63
|
+
result.errors.push(`Failed to export doc '${file.name}': HTTP ${exportRes.status}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const text = await exportRes.text();
|
|
67
|
+
const attachmentsDir = path.join(dataDir, "customers", slug, "attachments");
|
|
68
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
69
|
+
const safeFilename = file.name.replace(/[/\\?%*:|"<>]/g, "-") + ".txt";
|
|
70
|
+
fs.writeFileSync(path.join(attachmentsDir, safeFilename), text, "utf-8");
|
|
71
|
+
await appendInteraction(dataDir, slug, {
|
|
72
|
+
date: file.modifiedTime ? file.modifiedTime.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
73
|
+
type: "Note",
|
|
74
|
+
with: "Google Drive",
|
|
75
|
+
summary: `Attachment: ${file.name}`,
|
|
76
|
+
nextSteps: [],
|
|
77
|
+
sourceRef,
|
|
78
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
79
|
+
});
|
|
80
|
+
const lanceOpts = { type: "attachment" };
|
|
81
|
+
if (file.modifiedTime) lanceOpts.date = file.modifiedTime.slice(0, 10);
|
|
82
|
+
await indexInLanceDB(dataDir, slug, text.slice(0, 2e3), sourceRef, lanceOpts);
|
|
83
|
+
} else await appendInteraction(dataDir, slug, {
|
|
84
|
+
date: file.modifiedTime ? file.modifiedTime.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
85
|
+
type: "Note",
|
|
86
|
+
with: "Google Drive",
|
|
87
|
+
summary: `Attachment: ${file.name}${file.webViewLink ? ` — ${file.webViewLink}` : ""}`,
|
|
88
|
+
nextSteps: [],
|
|
89
|
+
sourceRef,
|
|
90
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
91
|
+
});
|
|
92
|
+
result.synced++;
|
|
93
|
+
existingInteractions += sourceRef;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
result.errors.push(`Error processing '${file.name}': ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (totalFetched >= maxFiles) break;
|
|
99
|
+
} while (pageToken);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { syncGoogleDriveFiles };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=google-drive-sync-DEPcqFca.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-drive-sync-DEPcqFca.js","names":[],"sources":["../src/sync/google-drive-sync.ts"],"sourcesContent":["import { appendInteraction } from \"../fs/interactions-writer.js\";\nimport { indexInLanceDB } from \"../core/lancedb.js\";\nimport { readInteractions } from \"../fs/interactions-writer.js\";\nimport path from \"path\";\nimport fs from \"fs\";\n\nexport interface DriveFile {\n id: string;\n name: string;\n mimeType: string;\n webViewLink?: string;\n modifiedTime?: string;\n size?: string;\n}\n\nexport interface DriveFilesResponse {\n files: DriveFile[];\n nextPageToken?: string;\n}\n\nexport interface DriveSyncOptions {\n slug: string;\n dataDir: string;\n accessToken: string;\n customerName?: string; // If not provided, use slug\n maxFiles?: number;\n}\n\nexport interface DriveSyncResult {\n synced: number;\n skipped: number;\n errors: string[];\n}\n\nconst GOOGLE_DOC_MIME = \"application/vnd.google-apps.document\";\nconst DRIVE_API_BASE = \"https://www.googleapis.com/drive/v3\";\n\nexport async function syncGoogleDriveFiles(opts: DriveSyncOptions): Promise<DriveSyncResult> {\n const { slug, dataDir, accessToken } = opts;\n const searchName = opts.customerName ?? slug;\n const maxFiles = opts.maxFiles ?? 200;\n\n const result: DriveSyncResult = { synced: 0, skipped: 0, errors: [] };\n\n // Load existing interactions to detect already-synced files\n let existingInteractions = \"\";\n try {\n existingInteractions = await readInteractions(dataDir, slug);\n } catch {\n existingInteractions = \"\";\n }\n\n const encodedQuery = encodeURIComponent(\n `name contains \"${searchName}\" and mimeType!=\"application/vnd.google-apps.folder\"`\n );\n const fields = encodeURIComponent(\n \"files(id,name,mimeType,webViewLink,modifiedTime,size),nextPageToken\"\n );\n\n let pageToken: string | undefined;\n let totalFetched = 0;\n\n do {\n let url = `${DRIVE_API_BASE}/files?q=${encodedQuery}&fields=${fields}&pageSize=50`;\n if (pageToken) {\n url += `&pageToken=${encodeURIComponent(pageToken)}`;\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n } catch (err) {\n result.errors.push(`Drive API request failed: ${(err as Error).message}`);\n break;\n }\n\n if (!response.ok) {\n result.errors.push(\n `Drive API error ${response.status}: ${await response.text().catch(() => \"unknown\")}`\n );\n break;\n }\n\n let data: DriveFilesResponse;\n try {\n data = (await response.json()) as DriveFilesResponse;\n } catch (err) {\n result.errors.push(`Failed to parse Drive API response: ${(err as Error).message}`);\n break;\n }\n\n const files = data.files ?? [];\n pageToken = data.nextPageToken;\n\n for (const file of files) {\n if (totalFetched >= maxFiles) break;\n totalFetched++;\n\n const sourceRef = `google://drive/${file.id}`;\n\n // Skip already-synced files\n if (existingInteractions.includes(sourceRef)) {\n result.skipped++;\n continue;\n }\n\n try {\n if (file.mimeType === GOOGLE_DOC_MIME) {\n // Export Google Doc as plain text\n const exportUrl = `${DRIVE_API_BASE}/files/${file.id}/export?mimeType=${encodeURIComponent(\"text/plain\")}`;\n const exportRes = await fetch(exportUrl, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!exportRes.ok) {\n result.errors.push(`Failed to export doc '${file.name}': HTTP ${exportRes.status}`);\n continue;\n }\n\n const text = await exportRes.text();\n\n // Save to attachments directory\n const attachmentsDir = path.join(dataDir, \"customers\", slug, \"attachments\");\n fs.mkdirSync(attachmentsDir, { recursive: true });\n const safeFilename = file.name.replace(/[/\\\\?%*:|\"<>]/g, \"-\") + \".txt\";\n fs.writeFileSync(path.join(attachmentsDir, safeFilename), text, \"utf-8\");\n\n // Append interaction\n await appendInteraction(dataDir, slug, {\n date: file.modifiedTime\n ? file.modifiedTime.slice(0, 10)\n : new Date().toISOString().slice(0, 10),\n type: \"Note\",\n with: \"Google Drive\",\n summary: `Attachment: ${file.name}`,\n nextSteps: [],\n sourceRef,\n synced: new Date().toISOString(),\n });\n\n // Index in LanceDB\n const lanceOpts: { date?: string; type?: string } = { type: \"attachment\" };\n if (file.modifiedTime) lanceOpts.date = file.modifiedTime.slice(0, 10);\n await indexInLanceDB(dataDir, slug, text.slice(0, 2000), sourceRef, lanceOpts);\n } else {\n // Non-Doc file: record via appendInteraction (no binary download)\n await appendInteraction(dataDir, slug, {\n date: file.modifiedTime\n ? file.modifiedTime.slice(0, 10)\n : new Date().toISOString().slice(0, 10),\n type: \"Note\",\n with: \"Google Drive\",\n summary: `Attachment: ${file.name}${file.webViewLink ? ` — ${file.webViewLink}` : \"\"}`,\n nextSteps: [],\n sourceRef,\n synced: new Date().toISOString(),\n });\n }\n\n result.synced++;\n existingInteractions += sourceRef; // prevent double-sync within same run\n } catch (err) {\n result.errors.push(`Error processing '${file.name}': ${(err as Error).message}`);\n }\n }\n\n if (totalFetched >= maxFiles) break;\n } while (pageToken);\n\n return result;\n}\n"],"mappings":";;;;;AAkCA,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAEvB,eAAsB,qBAAqB,MAAkD;CAC3F,MAAM,EAAE,MAAM,SAAS,gBAAgB;CACvC,MAAM,aAAa,KAAK,gBAAgB;CACxC,MAAM,WAAW,KAAK,YAAY;CAElC,MAAM,SAA0B;EAAE,QAAQ;EAAG,SAAS;EAAG,QAAQ,CAAC;CAAE;CAGpE,IAAI,uBAAuB;CAC3B,IAAI;EACF,uBAAuB,MAAM,iBAAiB,SAAS,IAAI;CAC7D,QAAQ;EACN,uBAAuB;CACzB;CAEA,MAAM,eAAe,mBACnB,kBAAkB,WAAW,qDAC/B;CACA,MAAM,SAAS,mBACb,qEACF;CAEA,IAAI;CACJ,IAAI,eAAe;CAEnB,GAAG;EACD,IAAI,MAAM,GAAG,eAAe,WAAW,aAAa,UAAU,OAAO;EACrE,IAAI,WACF,OAAO,cAAc,mBAAmB,SAAS;EAGnD,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,MAAM,KAAK,EAC1B,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;EACH,SAAS,KAAK;GACZ,OAAO,OAAO,KAAK,6BAA8B,IAAc,SAAS;GACxE;EACF;EAEA,IAAI,CAAC,SAAS,IAAI;GAChB,OAAO,OAAO,KACZ,mBAAmB,SAAS,OAAO,IAAI,MAAM,SAAS,KAAK,EAAE,YAAY,SAAS,GACpF;GACA;EACF;EAEA,IAAI;EACJ,IAAI;GACF,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,KAAK;GACZ,OAAO,OAAO,KAAK,uCAAwC,IAAc,SAAS;GAClF;EACF;EAEA,MAAM,QAAQ,KAAK,SAAS,CAAC;EAC7B,YAAY,KAAK;EAEjB,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,gBAAgB,UAAU;GAC9B;GAEA,MAAM,YAAY,kBAAkB,KAAK;GAGzC,IAAI,qBAAqB,SAAS,SAAS,GAAG;IAC5C,OAAO;IACP;GACF;GAEA,IAAI;IACF,IAAI,KAAK,aAAa,iBAAiB;KAErC,MAAM,YAAY,GAAG,eAAe,SAAS,KAAK,GAAG,mBAAmB,mBAAmB,YAAY;KACvG,MAAM,YAAY,MAAM,MAAM,WAAW,EACvC,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;KAED,IAAI,CAAC,UAAU,IAAI;MACjB,OAAO,OAAO,KAAK,yBAAyB,KAAK,KAAK,UAAU,UAAU,QAAQ;MAClF;KACF;KAEA,MAAM,OAAO,MAAM,UAAU,KAAK;KAGlC,MAAM,iBAAiB,KAAK,KAAK,SAAS,aAAa,MAAM,aAAa;KAC1E,GAAG,UAAU,gBAAgB,EAAE,WAAW,KAAK,CAAC;KAChD,MAAM,eAAe,KAAK,KAAK,QAAQ,kBAAkB,GAAG,IAAI;KAChE,GAAG,cAAc,KAAK,KAAK,gBAAgB,YAAY,GAAG,MAAM,OAAO;KAGvE,MAAM,kBAAkB,SAAS,MAAM;MACrC,MAAM,KAAK,eACP,KAAK,aAAa,MAAM,GAAG,EAAE,qBAC7B,IAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;MACxC,MAAM;MACN,MAAM;MACN,SAAS,eAAe,KAAK;MAC7B,WAAW,CAAC;MACZ;MACA,yBAAQ,IAAI,KAAK,GAAE,YAAY;KACjC,CAAC;KAGD,MAAM,YAA8C,EAAE,MAAM,aAAa;KACzE,IAAI,KAAK,cAAc,UAAU,OAAO,KAAK,aAAa,MAAM,GAAG,EAAE;KACrE,MAAM,eAAe,SAAS,MAAM,KAAK,MAAM,GAAG,GAAI,GAAG,WAAW,SAAS;IAC/E,OAEE,MAAM,kBAAkB,SAAS,MAAM;KACrC,MAAM,KAAK,eACP,KAAK,aAAa,MAAM,GAAG,EAAE,qBAC7B,IAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;KACxC,MAAM;KACN,MAAM;KACN,SAAS,eAAe,KAAK,OAAO,KAAK,cAAc,MAAM,KAAK,gBAAgB;KAClF,WAAW,CAAC;KACZ;KACA,yBAAQ,IAAI,KAAK,GAAE,YAAY;IACjC,CAAC;IAGH,OAAO;IACP,wBAAwB;GAC1B,SAAS,KAAK;IACZ,OAAO,OAAO,KAAK,qBAAqB,KAAK,KAAK,KAAM,IAAc,SAAS;GACjF;EACF;EAEA,IAAI,gBAAgB,UAAU;CAChC,SAAS;CAET,OAAO;AACT"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/core/hybrid-search.ts
|
|
2
|
+
function tokenize(s) {
|
|
3
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
4
|
+
}
|
|
5
|
+
/** Rank doc ids by query-term overlap (term frequency), dropping zero-overlap docs. */
|
|
6
|
+
function keywordRank(query, docs) {
|
|
7
|
+
const qTokens = new Set(tokenize(query));
|
|
8
|
+
if (qTokens.size === 0) return [];
|
|
9
|
+
return docs.map((d) => {
|
|
10
|
+
const docTokens = tokenize(d.text);
|
|
11
|
+
let score = 0;
|
|
12
|
+
for (const t of docTokens) if (qTokens.has(t)) score++;
|
|
13
|
+
return {
|
|
14
|
+
id: d.id,
|
|
15
|
+
score
|
|
16
|
+
};
|
|
17
|
+
}).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s) => s.id);
|
|
18
|
+
}
|
|
19
|
+
/** Reciprocal Rank Fusion of multiple ranked id-lists. Higher score = better. */
|
|
20
|
+
function reciprocalRankFusion(rankings, k = 60) {
|
|
21
|
+
const scores = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const ranking of rankings) ranking.forEach((id, rank) => {
|
|
23
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank));
|
|
24
|
+
});
|
|
25
|
+
return [...scores.entries()].map(([id, score]) => ({
|
|
26
|
+
id,
|
|
27
|
+
score
|
|
28
|
+
})).sort((a, b) => b.score - a.score);
|
|
29
|
+
}
|
|
30
|
+
/** Hybrid search: fuse keyword ranking with an optional vector ranking. */
|
|
31
|
+
function hybridSearch(query, docs, opts = {}) {
|
|
32
|
+
const rankings = [keywordRank(query, docs)];
|
|
33
|
+
if (opts.vectorRanking && opts.vectorRanking.length > 0) rankings.push(opts.vectorRanking);
|
|
34
|
+
const fused = reciprocalRankFusion(rankings, opts.k ?? 60);
|
|
35
|
+
return opts.limit ? fused.slice(0, opts.limit) : fused;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { hybridSearch as t };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=hybrid-search-BmHttLrR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hybrid-search-BmHttLrR.js","names":[],"sources":["../src/core/hybrid-search.ts"],"sourcesContent":["/**\n * Hybrid search (domino D2 / F8): combines keyword ranking with an externally\n * supplied vector ranking (e.g. from LanceDB `searchKnowledge`) via Reciprocal\n * Rank Fusion (RRF). RRF needs no score normalization and is robust across\n * heterogeneous scorers. The shared retrieval foundation for memories (D6),\n * SOPs (D7), KB and \"Ask your CRM\" (D10).\n */\nexport interface HybridDoc {\n id: string;\n text: string;\n}\n\nfunction tokenize(s: string): string[] {\n return s\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 1);\n}\n\n/** Rank doc ids by query-term overlap (term frequency), dropping zero-overlap docs. */\nexport function keywordRank(query: string, docs: HybridDoc[]): string[] {\n const qTokens = new Set(tokenize(query));\n if (qTokens.size === 0) return [];\n const scored = docs\n .map((d) => {\n const docTokens = tokenize(d.text);\n let score = 0;\n for (const t of docTokens) if (qTokens.has(t)) score++;\n return { id: d.id, score };\n })\n .filter((s) => s.score > 0)\n .sort((a, b) => b.score - a.score);\n return scored.map((s) => s.id);\n}\n\n/** Reciprocal Rank Fusion of multiple ranked id-lists. Higher score = better. */\nexport function reciprocalRankFusion(\n rankings: string[][],\n k = 60\n): Array<{ id: string; score: number }> {\n const scores = new Map<string, number>();\n for (const ranking of rankings) {\n ranking.forEach((id, rank) => {\n scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank));\n });\n }\n return [...scores.entries()]\n .map(([id, score]) => ({ id, score }))\n .sort((a, b) => b.score - a.score);\n}\n\nexport interface HybridOptions {\n vectorRanking?: string[];\n limit?: number;\n k?: number;\n}\n\n/** Hybrid search: fuse keyword ranking with an optional vector ranking. */\nexport function hybridSearch(\n query: string,\n docs: HybridDoc[],\n opts: HybridOptions = {}\n): Array<{ id: string; score: number }> {\n const rankings: string[][] = [keywordRank(query, docs)];\n if (opts.vectorRanking && opts.vectorRanking.length > 0) rankings.push(opts.vectorRanking);\n const fused = reciprocalRankFusion(rankings, opts.k ?? 60);\n return opts.limit ? fused.slice(0, opts.limit) : fused;\n}\n"],"mappings":";AAYA,SAAS,SAAS,GAAqB;CACrC,OAAO,EACJ,YAAY,EACZ,MAAM,YAAY,EAClB,QAAQ,MAAM,EAAE,SAAS,CAAC;AAC/B;;AAGA,SAAgB,YAAY,OAAe,MAA6B;CACtE,MAAM,UAAU,IAAI,IAAI,SAAS,KAAK,CAAC;CACvC,IAAI,QAAQ,SAAS,GAAG,OAAO,CAAC;CAUhC,OATe,KACZ,KAAK,MAAM;EACV,MAAM,YAAY,SAAS,EAAE,IAAI;EACjC,IAAI,QAAQ;EACZ,KAAK,MAAM,KAAK,WAAW,IAAI,QAAQ,IAAI,CAAC,GAAG;EAC/C,OAAO;GAAE,IAAI,EAAE;GAAI;EAAM;CAC3B,CAAC,EACA,QAAQ,MAAM,EAAE,QAAQ,CAAC,EACzB,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAClB,EAAE,KAAK,MAAM,EAAE,EAAE;AAC/B;;AAGA,SAAgB,qBACd,UACA,IAAI,IACkC;CACtC,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,WAAW,UACpB,QAAQ,SAAS,IAAI,SAAS;EAC5B,OAAO,IAAI,KAAK,OAAO,IAAI,EAAE,KAAK,KAAK,KAAK,IAAI,KAAK;CACvD,CAAC;CAEH,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,EACxB,KAAK,CAAC,IAAI,YAAY;EAAE;EAAI;CAAM,EAAE,EACpC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACrC;;AASA,SAAgB,aACd,OACA,MACA,OAAsB,CAAC,GACe;CACtC,MAAM,WAAuB,CAAC,YAAY,OAAO,IAAI,CAAC;CACtD,IAAI,KAAK,iBAAiB,KAAK,cAAc,SAAS,GAAG,SAAS,KAAK,KAAK,aAAa;CACzF,MAAM,QAAQ,qBAAqB,UAAU,KAAK,KAAK,EAAE;CACzD,OAAO,KAAK,QAAQ,MAAM,MAAM,GAAG,KAAK,KAAK,IAAI;AACnD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { i as readMainFacts, r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
|
|
2
|
+
import { n as findDuplicateClusters, r as normalizeDomain } from "./identity-CI6olMNm.js";
|
|
3
|
+
//#region src/core/hygiene.ts
|
|
4
|
+
async function scanHygiene(dataDir) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
for (const slug of listCustomerSlugs(dataDir)) {
|
|
7
|
+
const facts = await readMainFacts(dataDir, slug).catch(() => null);
|
|
8
|
+
if (!facts) continue;
|
|
9
|
+
if (!facts.domain && !facts.email) issues.push({
|
|
10
|
+
type: "missing_contact",
|
|
11
|
+
slug,
|
|
12
|
+
detail: "No domain or email"
|
|
13
|
+
});
|
|
14
|
+
if (facts.domain && /^https?:\/\/|^www\./i.test(facts.domain)) issues.push({
|
|
15
|
+
type: "format_domain",
|
|
16
|
+
slug,
|
|
17
|
+
field: "domain",
|
|
18
|
+
detail: `Domain not normalized: ${facts.domain}`,
|
|
19
|
+
suggestedFix: normalizeDomain(facts.domain)
|
|
20
|
+
});
|
|
21
|
+
if (facts.email && !facts.email.includes("@")) issues.push({
|
|
22
|
+
type: "format_email",
|
|
23
|
+
slug,
|
|
24
|
+
field: "email",
|
|
25
|
+
detail: `Email missing '@': ${facts.email}`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const cluster of await findDuplicateClusters(dataDir)) for (const slug of cluster.slugs) issues.push({
|
|
29
|
+
type: "duplicate",
|
|
30
|
+
slug,
|
|
31
|
+
detail: `Shares canonical domain '${cluster.key}' with: ${cluster.slugs.filter((s) => s !== slug).join(", ")}`
|
|
32
|
+
});
|
|
33
|
+
return issues;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { scanHygiene };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=hygiene-DZqfYpFf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hygiene-DZqfYpFf.js","names":[],"sources":["../src/core/hygiene.ts"],"sourcesContent":["import { readMainFacts, listCustomerSlugs } from \"../fs/customer-dir.js\";\nimport { findDuplicateClusters, normalizeDomain } from \"./identity.js\";\n\n/**\n * Data-hygiene agent (domino D5 / C5): scans customers for quality issues\n * (missing contact info, malformed fields, duplicates) and suggests fixes.\n * Fixes are meant to be applied through the approval gate (D4) — clean data\n * lifts the quality of every downstream AI feature.\n */\nexport type HygieneIssueType = \"missing_contact\" | \"format_domain\" | \"format_email\" | \"duplicate\";\n\nexport interface HygieneIssue {\n type: HygieneIssueType;\n slug: string;\n field?: string;\n detail: string;\n suggestedFix?: string;\n}\n\nexport async function scanHygiene(dataDir: string): Promise<HygieneIssue[]> {\n const issues: HygieneIssue[] = [];\n\n for (const slug of listCustomerSlugs(dataDir)) {\n const facts = await readMainFacts(dataDir, slug).catch(() => null);\n if (!facts) continue;\n\n if (!facts.domain && !facts.email) {\n issues.push({ type: \"missing_contact\", slug, detail: \"No domain or email\" });\n }\n if (facts.domain && /^https?:\\/\\/|^www\\./i.test(facts.domain)) {\n issues.push({\n type: \"format_domain\",\n slug,\n field: \"domain\",\n detail: `Domain not normalized: ${facts.domain}`,\n suggestedFix: normalizeDomain(facts.domain),\n });\n }\n if (facts.email && !facts.email.includes(\"@\")) {\n issues.push({\n type: \"format_email\",\n slug,\n field: \"email\",\n detail: `Email missing '@': ${facts.email}`,\n });\n }\n }\n\n // Duplicate clusters (reuse identity resolution)\n for (const cluster of await findDuplicateClusters(dataDir)) {\n for (const slug of cluster.slugs) {\n issues.push({\n type: \"duplicate\",\n slug,\n detail: `Shares canonical domain '${cluster.key}' with: ${cluster.slugs\n .filter((s) => s !== slug)\n .join(\", \")}`,\n });\n }\n }\n\n return issues;\n}\n"],"mappings":";;;AAmBA,eAAsB,YAAY,SAA0C;CAC1E,MAAM,SAAyB,CAAC;CAEhC,KAAK,MAAM,QAAQ,kBAAkB,OAAO,GAAG;EAC7C,MAAM,QAAQ,MAAM,cAAc,SAAS,IAAI,EAAE,YAAY,IAAI;EACjE,IAAI,CAAC,OAAO;EAEZ,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,OAC1B,OAAO,KAAK;GAAE,MAAM;GAAmB;GAAM,QAAQ;EAAqB,CAAC;EAE7E,IAAI,MAAM,UAAU,uBAAuB,KAAK,MAAM,MAAM,GAC1D,OAAO,KAAK;GACV,MAAM;GACN;GACA,OAAO;GACP,QAAQ,0BAA0B,MAAM;GACxC,cAAc,gBAAgB,MAAM,MAAM;EAC5C,CAAC;EAEH,IAAI,MAAM,SAAS,CAAC,MAAM,MAAM,SAAS,GAAG,GAC1C,OAAO,KAAK;GACV,MAAM;GACN;GACA,OAAO;GACP,QAAQ,sBAAsB,MAAM;EACtC,CAAC;CAEL;CAGA,KAAK,MAAM,WAAW,MAAM,sBAAsB,OAAO,GACvD,KAAK,MAAM,QAAQ,QAAQ,OACzB,OAAO,KAAK;EACV,MAAM;EACN;EACA,QAAQ,4BAA4B,QAAQ,IAAI,UAAU,QAAQ,MAC/D,QAAQ,MAAM,MAAM,IAAI,EACxB,KAAK,IAAI;CACd,CAAC;CAIL,OAAO;AACT"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { i as readMainFacts, r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
|
|
2
|
+
//#region src/core/identity.ts
|
|
3
|
+
/**
|
|
4
|
+
* Identity resolution (CDP v1, N4-3): deterministic deduplication of customers
|
|
5
|
+
* by a canonical key (normalized domain, falling back to email domain). Reports
|
|
6
|
+
* clusters of likely-duplicate customers so they can be merged.
|
|
7
|
+
*/
|
|
8
|
+
function normalizeDomain(value) {
|
|
9
|
+
let v = value.trim().toLowerCase();
|
|
10
|
+
if (v.includes("@")) v = v.split("@").pop() ?? v;
|
|
11
|
+
v = v.replace(/^https?:\/\//, "").replace(/^www\./, "");
|
|
12
|
+
v = v.replace(/\/.*$/, "");
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
/** Canonical key for a customer: normalized domain, else email domain, else "". */
|
|
16
|
+
async function canonicalKey(dataDir, slug) {
|
|
17
|
+
const facts = await readMainFacts(dataDir, slug).catch(() => null);
|
|
18
|
+
if (!facts) return "";
|
|
19
|
+
if (facts.domain) return normalizeDomain(facts.domain);
|
|
20
|
+
if (facts.email) return normalizeDomain(facts.email);
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
/** Group customers by canonical key; clusters with ≥2 members are duplicates. */
|
|
24
|
+
async function findDuplicateClusters(dataDir) {
|
|
25
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const slug of listCustomerSlugs(dataDir)) {
|
|
27
|
+
const key = await canonicalKey(dataDir, slug);
|
|
28
|
+
if (!key) continue;
|
|
29
|
+
byKey.set(key, [...byKey.get(key) ?? [], slug]);
|
|
30
|
+
}
|
|
31
|
+
const clusters = [];
|
|
32
|
+
for (const [key, slugs] of byKey) if (slugs.length >= 2) clusters.push({
|
|
33
|
+
key,
|
|
34
|
+
slugs
|
|
35
|
+
});
|
|
36
|
+
return clusters;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { findDuplicateClusters as n, normalizeDomain as r, canonicalKey as t };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=identity-CI6olMNm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-CI6olMNm.js","names":[],"sources":["../src/core/identity.ts"],"sourcesContent":["import { readMainFacts, listCustomerSlugs } from \"../fs/customer-dir.js\";\n\n/**\n * Identity resolution (CDP v1, N4-3): deterministic deduplication of customers\n * by a canonical key (normalized domain, falling back to email domain). Reports\n * clusters of likely-duplicate customers so they can be merged.\n */\nexport function normalizeDomain(value: string): string {\n let v = value.trim().toLowerCase();\n if (v.includes(\"@\")) v = v.split(\"@\").pop() ?? v; // email -> domain\n v = v.replace(/^https?:\\/\\//, \"\").replace(/^www\\./, \"\");\n v = v.replace(/\\/.*$/, \"\"); // drop path\n return v;\n}\n\nexport interface DuplicateCluster {\n key: string;\n slugs: string[];\n}\n\n/** Canonical key for a customer: normalized domain, else email domain, else \"\". */\nexport async function canonicalKey(dataDir: string, slug: string): Promise<string> {\n const facts = await readMainFacts(dataDir, slug).catch(() => null);\n if (!facts) return \"\";\n if (facts.domain) return normalizeDomain(facts.domain);\n if (facts.email) return normalizeDomain(facts.email);\n return \"\";\n}\n\n/** Group customers by canonical key; clusters with ≥2 members are duplicates. */\nexport async function findDuplicateClusters(dataDir: string): Promise<DuplicateCluster[]> {\n const byKey = new Map<string, string[]>();\n for (const slug of listCustomerSlugs(dataDir)) {\n const key = await canonicalKey(dataDir, slug);\n if (!key) continue;\n byKey.set(key, [...(byKey.get(key) ?? []), slug]);\n }\n const clusters: DuplicateCluster[] = [];\n for (const [key, slugs] of byKey) {\n if (slugs.length >= 2) clusters.push({ key, slugs });\n }\n return clusters;\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,gBAAgB,OAAuB;CACrD,IAAI,IAAI,MAAM,KAAK,EAAE,YAAY;CACjC,IAAI,EAAE,SAAS,GAAG,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK;CAC/C,IAAI,EAAE,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,UAAU,EAAE;CACtD,IAAI,EAAE,QAAQ,SAAS,EAAE;CACzB,OAAO;AACT;;AAQA,eAAsB,aAAa,SAAiB,MAA+B;CACjF,MAAM,QAAQ,MAAM,cAAc,SAAS,IAAI,EAAE,YAAY,IAAI;CACjE,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI,MAAM,QAAQ,OAAO,gBAAgB,MAAM,MAAM;CACrD,IAAI,MAAM,OAAO,OAAO,gBAAgB,MAAM,KAAK;CACnD,OAAO;AACT;;AAGA,eAAsB,sBAAsB,SAA8C;CACxF,MAAM,wBAAQ,IAAI,IAAsB;CACxC,KAAK,MAAM,QAAQ,kBAAkB,OAAO,GAAG;EAC7C,MAAM,MAAM,MAAM,aAAa,SAAS,IAAI;EAC5C,IAAI,CAAC,KAAK;EACV,MAAM,IAAI,KAAK,CAAC,GAAI,MAAM,IAAI,GAAG,KAAK,CAAC,GAAI,IAAI,CAAC;CAClD;CACA,MAAM,WAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OACzB,IAAI,MAAM,UAAU,GAAG,SAAS,KAAK;EAAE;EAAK;CAAM,CAAC;CAErD,OAAO;AACT"}
|