@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,229 @@
|
|
|
1
|
+
import { u as runScheduledBackupIfDue } from "./backup-CeMk9z86.js";
|
|
2
|
+
import { t as readPipeline } from "./pipeline-writer-BvVquKIe.js";
|
|
3
|
+
import { n as readHealth, t as computeCustomerHealth } from "./relationship-health-odxEoQdJ.js";
|
|
4
|
+
import { n as enqueueTask, t as buildDailyBriefing } from "./proactive-agent-BgQXw3ac.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import https from "https";
|
|
8
|
+
//#region src/sync/external-signals.ts
|
|
9
|
+
function signalsDir(dataDir, slug) {
|
|
10
|
+
return path.join(dataDir, "customers", slug, "signals");
|
|
11
|
+
}
|
|
12
|
+
function signalsFilePath(dataDir, slug, date) {
|
|
13
|
+
return path.join(signalsDir(dataDir, slug), `${date}.json`);
|
|
14
|
+
}
|
|
15
|
+
function writeSignals(dataDir, slug, date, signals) {
|
|
16
|
+
const dir = signalsDir(dataDir, slug);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
fs.writeFileSync(signalsFilePath(dataDir, slug, date), JSON.stringify(signals, null, 2), "utf-8");
|
|
19
|
+
}
|
|
20
|
+
async function fetchJson(url, headers = {}) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const u = new URL(url);
|
|
23
|
+
const options = {
|
|
24
|
+
hostname: u.hostname,
|
|
25
|
+
path: u.pathname + u.search,
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: {
|
|
28
|
+
"User-Agent": "datasynx-opencrm/2.0",
|
|
29
|
+
...headers
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const req = https.request(options, (res) => {
|
|
33
|
+
let data = "";
|
|
34
|
+
res.on("data", (chunk) => {
|
|
35
|
+
data += chunk.toString();
|
|
36
|
+
});
|
|
37
|
+
res.on("end", () => {
|
|
38
|
+
try {
|
|
39
|
+
resolve(JSON.parse(data));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
reject(e);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
req.on("error", reject);
|
|
46
|
+
req.setTimeout(5e3, () => {
|
|
47
|
+
req.destroy();
|
|
48
|
+
reject(/* @__PURE__ */ new Error("timeout"));
|
|
49
|
+
});
|
|
50
|
+
req.end();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function checkCompanyNews(domain, companyName) {
|
|
54
|
+
const signals = [];
|
|
55
|
+
try {
|
|
56
|
+
const query = encodeURIComponent(companyName.split(" ")[0] ?? domain.split(".")[0] ?? "");
|
|
57
|
+
const result = await fetchJson(`https://hn.algolia.com/api/v1/search?query=${query}&tags=story&numericFilters=created_at_i>${Math.floor(Date.now() / 1e3) - 30 * 86400}&hitsPerPage=5`);
|
|
58
|
+
for (const hit of result.hits ?? []) {
|
|
59
|
+
const title = hit.title ?? hit.story_title ?? "";
|
|
60
|
+
if (!title.toLowerCase().includes(query.toLowerCase())) continue;
|
|
61
|
+
const titleLc = title.toLowerCase();
|
|
62
|
+
const type = titleLc.includes("fund") ? "funding_round" : titleLc.includes("acqui") ? "acquisition" : titleLc.includes("lay") || titleLc.includes("reduc") ? "layoffs" : "news_mention";
|
|
63
|
+
const impact = type === "funding_round" || type === "acquisition" ? "positive" : type === "layoffs" ? "negative" : "neutral";
|
|
64
|
+
signals.push({
|
|
65
|
+
id: `hn_${hit.objectID}`,
|
|
66
|
+
slug: "",
|
|
67
|
+
source: "hacker_news",
|
|
68
|
+
type,
|
|
69
|
+
summary: title,
|
|
70
|
+
url: hit.url ?? `https://news.ycombinator.com/item?id=${hit.objectID}`,
|
|
71
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
72
|
+
impact
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch {}
|
|
76
|
+
return signals;
|
|
77
|
+
}
|
|
78
|
+
async function checkFundingEvents(domain) {
|
|
79
|
+
const apiKey = process.env["CRUNCHBASE_API_KEY"];
|
|
80
|
+
if (!apiKey) return [];
|
|
81
|
+
const signals = [];
|
|
82
|
+
try {
|
|
83
|
+
const props = (await fetchJson(`https://api.crunchbase.com/api/v4/entities/organizations/${domain.split(".")[0] ?? domain}?field_ids=short_description,funding_total,last_funding_type&user_key=${apiKey}`)).data?.properties;
|
|
84
|
+
if (props?.last_funding_type && props?.funding_total?.value_usd) {
|
|
85
|
+
const millions = (props.funding_total.value_usd / 1e6).toFixed(1);
|
|
86
|
+
signals.push({
|
|
87
|
+
id: `cb_${domain}_${Date.now()}`,
|
|
88
|
+
slug: "",
|
|
89
|
+
source: "crunchbase",
|
|
90
|
+
type: "funding_round",
|
|
91
|
+
summary: `${domain} raised funding (${props.last_funding_type}, $${millions}M total)`,
|
|
92
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
93
|
+
impact: "positive"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
return signals;
|
|
98
|
+
}
|
|
99
|
+
async function fetchSignalsForCustomer(dataDir, slug, domain, companyName, today) {
|
|
100
|
+
const [newsSignals, fundingSignals] = await Promise.all([checkCompanyNews(domain, companyName), checkFundingEvents(domain)]);
|
|
101
|
+
const signals = [...newsSignals.map((s) => ({
|
|
102
|
+
...s,
|
|
103
|
+
slug
|
|
104
|
+
})), ...fundingSignals.map((s) => ({
|
|
105
|
+
...s,
|
|
106
|
+
slug
|
|
107
|
+
}))];
|
|
108
|
+
if (signals.length > 0) writeSignals(dataDir, slug, today, signals);
|
|
109
|
+
return signals;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/daemon/proactive-worker.ts
|
|
113
|
+
const MAX_CUSTOMERS_PER_CYCLE = 50;
|
|
114
|
+
function defaultChannel() {
|
|
115
|
+
if (process.env["TELEGRAM_BOT_TOKEN"] && process.env["TELEGRAM_CHAT_ID"]) return "telegram";
|
|
116
|
+
if (process.env["SLACK_WEBHOOK_URL"]) return "slack";
|
|
117
|
+
return "mcp_tool_response";
|
|
118
|
+
}
|
|
119
|
+
async function runDailyProactiveChecks(dataDir, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
120
|
+
const result = {
|
|
121
|
+
today,
|
|
122
|
+
customersChecked: 0,
|
|
123
|
+
tasksEnqueued: 0,
|
|
124
|
+
errors: []
|
|
125
|
+
};
|
|
126
|
+
const channel = defaultChannel();
|
|
127
|
+
const customersDir = path.join(dataDir, "customers");
|
|
128
|
+
const slugs = fs.existsSync(customersDir) ? fs.readdirSync(customersDir).filter((s) => {
|
|
129
|
+
try {
|
|
130
|
+
return fs.statSync(path.join(customersDir, s)).isDirectory();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}).slice(0, MAX_CUSTOMERS_PER_CYCLE) : [];
|
|
135
|
+
const todayMs = (/* @__PURE__ */ new Date(`${today}T00:00:00Z`)).getTime();
|
|
136
|
+
await Promise.all(slugs.map(async (slug) => {
|
|
137
|
+
try {
|
|
138
|
+
const health = readHealth(dataDir, slug) ?? computeCustomerHealth(dataDir, slug, today);
|
|
139
|
+
for (const contact of health.contacts) if (contact.riskFlags.includes("NO_CONTACT_30D") || contact.grade === "F") {
|
|
140
|
+
await enqueueTask(dataDir, {
|
|
141
|
+
type: "relationship_decay_alert",
|
|
142
|
+
slug,
|
|
143
|
+
priority: contact.grade === "F" ? "urgent" : "high",
|
|
144
|
+
payload: {
|
|
145
|
+
contactId: contact.contactId,
|
|
146
|
+
name: contact.name,
|
|
147
|
+
email: contact.email,
|
|
148
|
+
daysSinceContact: contact.daysSinceContact,
|
|
149
|
+
grade: contact.grade,
|
|
150
|
+
riskFlags: contact.riskFlags
|
|
151
|
+
},
|
|
152
|
+
scheduledFor: (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
+
channel
|
|
154
|
+
});
|
|
155
|
+
result.tasksEnqueued++;
|
|
156
|
+
}
|
|
157
|
+
const deals = await readPipeline(dataDir, slug).catch(() => []);
|
|
158
|
+
for (const deal of deals) {
|
|
159
|
+
if (deal.stage === "won" || deal.stage === "lost") continue;
|
|
160
|
+
if (!deal.close_date?.trim()) continue;
|
|
161
|
+
const daysToClose = Math.floor((new Date(deal.close_date).getTime() - todayMs) / 864e5);
|
|
162
|
+
if (daysToClose <= 7) {
|
|
163
|
+
await enqueueTask(dataDir, {
|
|
164
|
+
type: "deal_risk_alert",
|
|
165
|
+
slug,
|
|
166
|
+
priority: daysToClose < 0 ? "urgent" : "high",
|
|
167
|
+
payload: {
|
|
168
|
+
dealName: deal.name,
|
|
169
|
+
stage: deal.stage,
|
|
170
|
+
value: deal.value,
|
|
171
|
+
closeDate: deal.close_date,
|
|
172
|
+
daysToClose,
|
|
173
|
+
overdue: daysToClose < 0
|
|
174
|
+
},
|
|
175
|
+
scheduledFor: (/* @__PURE__ */ new Date()).toISOString(),
|
|
176
|
+
channel
|
|
177
|
+
});
|
|
178
|
+
result.tasksEnqueued++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const factsPath = path.join(dataDir, "customers", slug, "main_facts.md");
|
|
183
|
+
if (fs.existsSync(factsPath)) {
|
|
184
|
+
const raw = fs.readFileSync(factsPath, "utf-8");
|
|
185
|
+
const domainMatch = raw.match(/^domain:\s*(.+)$/im);
|
|
186
|
+
const nameMatch = raw.match(/^name:\s*(.+)$/im);
|
|
187
|
+
const domain = domainMatch?.[1]?.trim();
|
|
188
|
+
const companyName = nameMatch?.[1]?.trim() ?? slug;
|
|
189
|
+
if (domain) {
|
|
190
|
+
const signals = await fetchSignalsForCustomer(dataDir, slug, domain, companyName, today);
|
|
191
|
+
for (const signal of signals) {
|
|
192
|
+
if (signal.impact === "neutral") continue;
|
|
193
|
+
await enqueueTask(dataDir, {
|
|
194
|
+
type: "external_signal_alert",
|
|
195
|
+
slug,
|
|
196
|
+
priority: signal.impact === "negative" ? "urgent" : "high",
|
|
197
|
+
payload: signal,
|
|
198
|
+
scheduledFor: (/* @__PURE__ */ new Date()).toISOString(),
|
|
199
|
+
channel
|
|
200
|
+
});
|
|
201
|
+
result.tasksEnqueued++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
result.customersChecked++;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
result.errors.push(`${slug}: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
211
|
+
try {
|
|
212
|
+
await enqueueTask(dataDir, {
|
|
213
|
+
type: "daily_briefing",
|
|
214
|
+
priority: "normal",
|
|
215
|
+
payload: await buildDailyBriefing(dataDir, today),
|
|
216
|
+
scheduledFor: (/* @__PURE__ */ new Date()).toISOString(),
|
|
217
|
+
channel
|
|
218
|
+
});
|
|
219
|
+
result.tasksEnqueued++;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
result.errors.push(`daily_briefing: ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
runScheduledBackupIfDue(dataDir).catch(() => {});
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { runDailyProactiveChecks };
|
|
228
|
+
|
|
229
|
+
//# sourceMappingURL=proactive-worker-BrLHNhjH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proactive-worker-BrLHNhjH.js","names":[],"sources":["../src/sync/external-signals.ts","../src/daemon/proactive-worker.ts"],"sourcesContent":["// src/sync/external-signals.ts\n// External signal detection: Hacker News (free), Crunchbase (key optional),\n// Clearbit (key optional). Non-fatal on network errors — CRM works offline.\nimport https from \"https\";\nimport fs from \"fs\";\nimport path from \"path\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type SignalType =\n | \"funding_round\"\n | \"leadership_change\"\n | \"layoffs\"\n | \"acquisition\"\n | \"expansion\"\n | \"product_launch\"\n | \"news_mention\";\n\nexport type SignalImpact = \"positive\" | \"negative\" | \"neutral\";\n\nexport interface ExternalSignal {\n id: string;\n slug: string;\n source: \"hacker_news\" | \"crunchbase\" | \"clearbit\" | \"rss\";\n type: SignalType;\n summary: string;\n url?: string;\n detectedAt: string;\n impact: SignalImpact;\n}\n\n// ─── File I/O ─────────────────────────────────────────────────────────────────\n\nexport function signalsDir(dataDir: string, slug: string): string {\n return path.join(dataDir, \"customers\", slug, \"signals\");\n}\n\nexport function signalsFilePath(dataDir: string, slug: string, date: string): string {\n return path.join(signalsDir(dataDir, slug), `${date}.json`);\n}\n\nexport function readSignals(dataDir: string, slug: string, date: string): ExternalSignal[] {\n const p = signalsFilePath(dataDir, slug, date);\n if (!fs.existsSync(p)) return [];\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as ExternalSignal[];\n } catch {\n return [];\n }\n}\n\nexport function writeSignals(\n dataDir: string,\n slug: string,\n date: string,\n signals: ExternalSignal[]\n): void {\n const dir = signalsDir(dataDir, slug);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(signalsFilePath(dataDir, slug, date), JSON.stringify(signals, null, 2), \"utf-8\");\n}\n\n// ─── HTTP helper ──────────────────────────────────────────────────────────────\n\nexport async function fetchJson<T>(url: string, headers: Record<string, string> = {}): Promise<T> {\n return new Promise((resolve, reject) => {\n const u = new URL(url);\n const options = {\n hostname: u.hostname,\n path: u.pathname + u.search,\n method: \"GET\",\n headers: { \"User-Agent\": \"datasynx-opencrm/2.0\", ...headers },\n };\n const req = https.request(options, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk: Buffer) => {\n data += chunk.toString();\n });\n res.on(\"end\", () => {\n try {\n resolve(JSON.parse(data) as T);\n } catch (e) {\n reject(e);\n }\n });\n });\n req.on(\"error\", reject);\n req.setTimeout(5000, () => {\n req.destroy();\n reject(new Error(\"timeout\"));\n });\n req.end();\n });\n}\n\n// ─── Hacker News / Algolia API (free, no key needed) ─────────────────────────\n\ninterface HNHit {\n objectID: string;\n title?: string;\n story_title?: string;\n url?: string;\n created_at: string;\n}\n\ninterface HNSearchResult {\n hits: HNHit[];\n}\n\nexport async function checkCompanyNews(\n domain: string,\n companyName: string\n): Promise<ExternalSignal[]> {\n const signals: ExternalSignal[] = [];\n try {\n const query = encodeURIComponent(companyName.split(\" \")[0] ?? domain.split(\".\")[0] ?? \"\");\n const since = Math.floor(Date.now() / 1000) - 30 * 86400;\n const url = `https://hn.algolia.com/api/v1/search?query=${query}&tags=story&numericFilters=created_at_i>${since}&hitsPerPage=5`;\n const result = await fetchJson<HNSearchResult>(url);\n\n for (const hit of result.hits ?? []) {\n const title = hit.title ?? hit.story_title ?? \"\";\n if (!title.toLowerCase().includes(query.toLowerCase())) continue;\n\n const titleLc = title.toLowerCase();\n const type: SignalType = titleLc.includes(\"fund\")\n ? \"funding_round\"\n : titleLc.includes(\"acqui\")\n ? \"acquisition\"\n : titleLc.includes(\"lay\") || titleLc.includes(\"reduc\")\n ? \"layoffs\"\n : \"news_mention\";\n\n const impact: SignalImpact =\n type === \"funding_round\" || type === \"acquisition\"\n ? \"positive\"\n : type === \"layoffs\"\n ? \"negative\"\n : \"neutral\";\n\n signals.push({\n id: `hn_${hit.objectID}`,\n slug: \"\",\n source: \"hacker_news\",\n type,\n summary: title,\n url: hit.url ?? `https://news.ycombinator.com/item?id=${hit.objectID}`,\n detectedAt: new Date().toISOString(),\n impact,\n });\n }\n } catch {\n // Network errors are non-fatal\n }\n return signals;\n}\n\n// ─── Crunchbase Basic API (CRUNCHBASE_API_KEY optional) ──────────────────────\n\ninterface CrunchbaseOrg {\n properties?: {\n short_description?: string;\n funding_total?: { value_usd?: number };\n last_funding_type?: string;\n };\n}\n\nexport async function checkFundingEvents(domain: string): Promise<ExternalSignal[]> {\n const apiKey = process.env[\"CRUNCHBASE_API_KEY\"];\n if (!apiKey) return [];\n\n const signals: ExternalSignal[] = [];\n try {\n const orgName = domain.split(\".\")[0] ?? domain;\n const url = `https://api.crunchbase.com/api/v4/entities/organizations/${orgName}?field_ids=short_description,funding_total,last_funding_type&user_key=${apiKey}`;\n const result = await fetchJson<{ data?: CrunchbaseOrg }>(url);\n const props = result.data?.properties;\n\n if (props?.last_funding_type && props?.funding_total?.value_usd) {\n const millions = (props.funding_total.value_usd / 1_000_000).toFixed(1);\n signals.push({\n id: `cb_${domain}_${Date.now()}`,\n slug: \"\",\n source: \"crunchbase\",\n type: \"funding_round\",\n summary: `${domain} raised funding (${props.last_funding_type}, $${millions}M total)`,\n detectedAt: new Date().toISOString(),\n impact: \"positive\",\n });\n }\n } catch {\n // Non-fatal\n }\n return signals;\n}\n\n// ─── Clearbit enrichment (CLEARBIT_API_KEY optional) ─────────────────────────\n\ninterface ClearbitPerson {\n name?: { fullName?: string };\n employment?: { title?: string; name?: string };\n}\n\nexport async function enrichContact(\n email: string\n): Promise<{ name?: string; title?: string; company?: string } | null> {\n const apiKey = process.env[\"CLEARBIT_API_KEY\"];\n if (!apiKey) return null;\n\n try {\n const url = `https://person.clearbit.com/v2/combined/find?email=${encodeURIComponent(email)}`;\n const result = await fetchJson<ClearbitPerson>(url, {\n Authorization: `Bearer ${apiKey}`,\n });\n return {\n ...(result.name?.fullName ? { name: result.name.fullName } : {}),\n ...(result.employment?.title ? { title: result.employment.title } : {}),\n ...(result.employment?.name ? { company: result.employment.name } : {}),\n };\n } catch {\n return null;\n }\n}\n\n// ─── Main entry point ─────────────────────────────────────────────────────────\n\nexport async function fetchSignalsForCustomer(\n dataDir: string,\n slug: string,\n domain: string,\n companyName: string,\n today: string\n): Promise<ExternalSignal[]> {\n const [newsSignals, fundingSignals] = await Promise.all([\n checkCompanyNews(domain, companyName),\n checkFundingEvents(domain),\n ]);\n\n const signals: ExternalSignal[] = [\n ...newsSignals.map((s) => ({ ...s, slug })),\n ...fundingSignals.map((s) => ({ ...s, slug })),\n ];\n\n if (signals.length > 0) {\n writeSignals(dataDir, slug, today, signals);\n }\n\n return signals;\n}\n","// src/daemon/proactive-worker.ts\n// Daily proactive checks: relationship decay + deal risk + daily briefing.\n// Called from worker.ts CronJob at 07:00. Enqueues tasks to agent-queue.json.\n// Queue draining (Telegram/Slack dispatch) is handled by G3 / notification-dispatcher.\nimport fs from \"fs\";\nimport path from \"path\";\nimport { computeCustomerHealth, readHealth } from \"../core/relationship-health.js\";\nimport { readPipeline } from \"../fs/pipeline-writer.js\";\nimport {\n buildDailyBriefing,\n enqueueTask,\n type NotificationChannel,\n} from \"../core/proactive-agent.js\";\nimport { fetchSignalsForCustomer } from \"../sync/external-signals.js\";\nimport { runScheduledBackupIfDue } from \"../commands/backup.js\";\n\nconst MAX_CUSTOMERS_PER_CYCLE = 50;\n\nfunction defaultChannel(): NotificationChannel {\n if (process.env[\"TELEGRAM_BOT_TOKEN\"] && process.env[\"TELEGRAM_CHAT_ID\"]) return \"telegram\";\n if (process.env[\"SLACK_WEBHOOK_URL\"]) return \"slack\";\n return \"mcp_tool_response\";\n}\n\nexport interface ProactiveCheckResult {\n today: string;\n customersChecked: number;\n tasksEnqueued: number;\n errors: string[];\n}\n\nexport async function runDailyProactiveChecks(\n dataDir: string,\n today: string = new Date().toISOString().slice(0, 10)\n): Promise<ProactiveCheckResult> {\n const result: ProactiveCheckResult = { today, customersChecked: 0, tasksEnqueued: 0, errors: [] };\n const channel = defaultChannel();\n\n const customersDir = path.join(dataDir, \"customers\");\n const slugs = fs.existsSync(customersDir)\n ? fs\n .readdirSync(customersDir)\n .filter((s) => {\n try {\n return fs.statSync(path.join(customersDir, s)).isDirectory();\n } catch {\n return false;\n }\n })\n .slice(0, MAX_CUSTOMERS_PER_CYCLE)\n : [];\n\n const todayMs = new Date(`${today}T00:00:00Z`).getTime();\n\n await Promise.all(\n slugs.map(async (slug) => {\n try {\n // Relationship health — use cached snapshot if fresh, compute otherwise\n const health = readHealth(dataDir, slug) ?? computeCustomerHealth(dataDir, slug, today);\n\n for (const contact of health.contacts) {\n const isDecayed = contact.riskFlags.includes(\"NO_CONTACT_30D\") || contact.grade === \"F\";\n\n if (isDecayed) {\n await enqueueTask(dataDir, {\n type: \"relationship_decay_alert\",\n slug,\n priority: contact.grade === \"F\" ? \"urgent\" : \"high\",\n payload: {\n contactId: contact.contactId,\n name: contact.name,\n email: contact.email,\n daysSinceContact: contact.daysSinceContact,\n grade: contact.grade,\n riskFlags: contact.riskFlags,\n },\n scheduledFor: new Date().toISOString(),\n channel,\n });\n result.tasksEnqueued++;\n }\n }\n\n // Deal risk — close date within 7 days or already overdue\n const deals = await readPipeline(dataDir, slug).catch(() => []);\n for (const deal of deals) {\n if (deal.stage === \"won\" || deal.stage === \"lost\") continue;\n if (!deal.close_date?.trim()) continue;\n\n const daysToClose = Math.floor(\n (new Date(deal.close_date).getTime() - todayMs) / 86_400_000\n );\n\n if (daysToClose <= 7) {\n await enqueueTask(dataDir, {\n type: \"deal_risk_alert\",\n slug,\n priority: daysToClose < 0 ? \"urgent\" : \"high\",\n payload: {\n dealName: deal.name,\n stage: deal.stage,\n value: deal.value,\n closeDate: deal.close_date,\n daysToClose,\n overdue: daysToClose < 0,\n },\n scheduledFor: new Date().toISOString(),\n channel,\n });\n result.tasksEnqueued++;\n }\n }\n\n // External signals — read domain/name from main_facts if available\n try {\n const factsPath = path.join(dataDir, \"customers\", slug, \"main_facts.md\");\n if (fs.existsSync(factsPath)) {\n const raw = fs.readFileSync(factsPath, \"utf-8\");\n const domainMatch = raw.match(/^domain:\\s*(.+)$/im);\n const nameMatch = raw.match(/^name:\\s*(.+)$/im);\n const domain = domainMatch?.[1]?.trim();\n const companyName = nameMatch?.[1]?.trim() ?? slug;\n if (domain) {\n const signals = await fetchSignalsForCustomer(\n dataDir,\n slug,\n domain,\n companyName,\n today\n );\n for (const signal of signals) {\n if (signal.impact === \"neutral\") continue;\n await enqueueTask(dataDir, {\n type: \"external_signal_alert\",\n slug,\n priority: signal.impact === \"negative\" ? \"urgent\" : \"high\",\n payload: signal,\n scheduledFor: new Date().toISOString(),\n channel,\n });\n result.tasksEnqueued++;\n }\n }\n }\n } catch {\n // External signals are best-effort — never block the rest of the cycle\n }\n\n result.customersChecked++;\n } catch (err) {\n result.errors.push(`${slug}: ${(err as Error).message}`);\n }\n })\n );\n\n // Daily briefing — always enqueue, agents consume via get_proactive_briefing\n try {\n const briefing = await buildDailyBriefing(dataDir, today);\n await enqueueTask(dataDir, {\n type: \"daily_briefing\",\n priority: \"normal\",\n payload: briefing,\n scheduledFor: new Date().toISOString(),\n channel,\n });\n result.tasksEnqueued++;\n } catch (err) {\n result.errors.push(`daily_briefing: ${(err as Error).message}`);\n }\n\n // Scheduled backup — fire-and-forget, non-blocking\n runScheduledBackupIfDue(dataDir).catch(() => {\n // non-critical — proactive cycle must not fail due to backup errors\n });\n\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
//#region src/sync/push-manager.ts
|
|
4
|
+
function makePushSubId() {
|
|
5
|
+
return `psub_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
6
|
+
}
|
|
7
|
+
function subscriptionsPath(dataDir) {
|
|
8
|
+
return path.join(dataDir, ".agentic", "push-subscriptions.json");
|
|
9
|
+
}
|
|
10
|
+
async function readSubscriptions(dataDir) {
|
|
11
|
+
const filePath = subscriptionsPath(dataDir);
|
|
12
|
+
if (!fs.existsSync(filePath)) return [];
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
15
|
+
return JSON.parse(raw).subscriptions ?? [];
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function writeSubscriptions(dataDir, subs) {
|
|
21
|
+
const filePath = subscriptionsPath(dataDir);
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23
|
+
const file = {
|
|
24
|
+
subscriptions: subs,
|
|
25
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26
|
+
};
|
|
27
|
+
fs.writeFileSync(filePath, JSON.stringify(file, null, 2), "utf-8");
|
|
28
|
+
}
|
|
29
|
+
function expiresAtForProvider(provider) {
|
|
30
|
+
if (provider === "gmail") return new Date(Date.now() + 10080 * 60 * 1e3).toISOString();
|
|
31
|
+
if (provider === "microsoft-graph") return new Date(Date.now() + 4320 * 60 * 1e3).toISOString();
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async function register(dataDir, provider, slug, opts) {
|
|
35
|
+
const subs = await readSubscriptions(dataDir);
|
|
36
|
+
const sub = {
|
|
37
|
+
id: makePushSubId(),
|
|
38
|
+
provider,
|
|
39
|
+
slug,
|
|
40
|
+
webhookUrl: opts.webhookUrl,
|
|
41
|
+
expiresAt: expiresAtForProvider(provider),
|
|
42
|
+
renewedAt: null,
|
|
43
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44
|
+
providerData: opts.providerData ?? {},
|
|
45
|
+
status: "active",
|
|
46
|
+
lastEventAt: null,
|
|
47
|
+
eventsProcessed: 0
|
|
48
|
+
};
|
|
49
|
+
await writeSubscriptions(dataDir, [...subs, sub]);
|
|
50
|
+
return sub;
|
|
51
|
+
}
|
|
52
|
+
async function revoke(dataDir, id) {
|
|
53
|
+
const subs = await readSubscriptions(dataDir);
|
|
54
|
+
const idx = subs.findIndex((s) => s.id === id);
|
|
55
|
+
if (idx === -1) throw new Error(`Subscription ${id} not found`);
|
|
56
|
+
subs[idx] = {
|
|
57
|
+
...subs[idx],
|
|
58
|
+
status: "revoked"
|
|
59
|
+
};
|
|
60
|
+
await writeSubscriptions(dataDir, subs);
|
|
61
|
+
}
|
|
62
|
+
async function renewExpiringSubscriptions(dataDir, renewFn, thresholdHours = 24) {
|
|
63
|
+
const subs = await readSubscriptions(dataDir);
|
|
64
|
+
const thresholdMs = thresholdHours * 60 * 60 * 1e3;
|
|
65
|
+
const cutoff = Date.now() + thresholdMs;
|
|
66
|
+
const renewed = [];
|
|
67
|
+
const errors = [];
|
|
68
|
+
const PERMANENT_FAILURE_THRESHOLD = 3;
|
|
69
|
+
for (let i = 0; i < subs.length; i++) {
|
|
70
|
+
const sub = subs[i];
|
|
71
|
+
if (sub.status !== "active" && sub.status !== "error") continue;
|
|
72
|
+
if (sub.expiresAt === null) continue;
|
|
73
|
+
if (new Date(sub.expiresAt).getTime() > cutoff) continue;
|
|
74
|
+
try {
|
|
75
|
+
const result = await renewFn(sub);
|
|
76
|
+
subs[i] = {
|
|
77
|
+
...sub,
|
|
78
|
+
status: "active",
|
|
79
|
+
expiresAt: result.expiresAt,
|
|
80
|
+
renewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
81
|
+
renewFailures: 0,
|
|
82
|
+
providerData: result.providerData ? {
|
|
83
|
+
...sub.providerData,
|
|
84
|
+
...result.providerData
|
|
85
|
+
} : sub.providerData
|
|
86
|
+
};
|
|
87
|
+
renewed.push(sub.id);
|
|
88
|
+
} catch {
|
|
89
|
+
const failures = (sub.renewFailures ?? 0) + 1;
|
|
90
|
+
const newStatus = failures >= PERMANENT_FAILURE_THRESHOLD ? "permanently_failed" : "error";
|
|
91
|
+
subs[i] = {
|
|
92
|
+
...sub,
|
|
93
|
+
status: newStatus,
|
|
94
|
+
renewFailures: failures
|
|
95
|
+
};
|
|
96
|
+
errors.push(sub.id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await writeSubscriptions(dataDir, subs);
|
|
100
|
+
return {
|
|
101
|
+
renewed,
|
|
102
|
+
errors
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { revoke as a, renewExpiringSubscriptions as i, readSubscriptions as n, subscriptionsPath as o, register as r, writeSubscriptions as s, makePushSubId as t };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=push-manager-CdqIIkuh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-manager-CdqIIkuh.js","names":[],"sources":["../src/sync/push-manager.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\nexport type PushProvider = \"gmail\" | \"microsoft-graph\" | \"slack\";\nexport type PushStatus = \"active\" | \"expired\" | \"revoked\" | \"error\" | \"permanently_failed\";\n\nexport interface PushSubscription {\n id: string;\n provider: PushProvider;\n slug: string;\n webhookUrl: string;\n expiresAt: string | null;\n renewedAt: string | null;\n createdAt: string;\n providerData: {\n gmailHistoryId?: string;\n gmailTopicName?: string;\n gmailLabelIds?: string[];\n gmailEmailAddress?: string;\n microsoftSubscriptionId?: string;\n microsoftResource?: string;\n microsoftClientState?: string;\n slackTeamId?: string;\n slackChannelId?: string;\n slackBotToken?: string;\n };\n status: PushStatus;\n lastEventAt: string | null;\n eventsProcessed: number;\n renewFailures?: number;\n}\n\ninterface PushSubscriptionsFile {\n subscriptions: PushSubscription[];\n updatedAt: string;\n}\n\nexport function makePushSubId(): string {\n return `psub_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;\n}\n\nexport function subscriptionsPath(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"push-subscriptions.json\");\n}\n\nexport async function readSubscriptions(dataDir: string): Promise<PushSubscription[]> {\n const filePath = subscriptionsPath(dataDir);\n if (!fs.existsSync(filePath)) return [];\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\") as string;\n const parsed = JSON.parse(raw) as PushSubscriptionsFile;\n return parsed.subscriptions ?? [];\n } catch {\n return [];\n }\n}\n\nexport async function writeSubscriptions(dataDir: string, subs: PushSubscription[]): Promise<void> {\n const filePath = subscriptionsPath(dataDir);\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const file: PushSubscriptionsFile = { subscriptions: subs, updatedAt: new Date().toISOString() };\n fs.writeFileSync(filePath, JSON.stringify(file, null, 2), \"utf-8\");\n}\n\nfunction expiresAtForProvider(provider: PushProvider): string | null {\n if (provider === \"gmail\") {\n return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();\n }\n if (provider === \"microsoft-graph\") {\n return new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();\n }\n return null; // slack: no expiry\n}\n\nexport async function register(\n dataDir: string,\n provider: PushProvider,\n slug: string,\n opts: { webhookUrl: string; providerData?: Partial<PushSubscription[\"providerData\"]> }\n): Promise<PushSubscription> {\n const subs = await readSubscriptions(dataDir);\n const sub: PushSubscription = {\n id: makePushSubId(),\n provider,\n slug,\n webhookUrl: opts.webhookUrl,\n expiresAt: expiresAtForProvider(provider),\n renewedAt: null,\n createdAt: new Date().toISOString(),\n providerData: opts.providerData ?? {},\n status: \"active\",\n lastEventAt: null,\n eventsProcessed: 0,\n };\n await writeSubscriptions(dataDir, [...subs, sub]);\n return sub;\n}\n\nexport async function revoke(dataDir: string, id: string): Promise<void> {\n const subs = await readSubscriptions(dataDir);\n const idx = subs.findIndex((s) => s.id === id);\n if (idx === -1) throw new Error(`Subscription ${id} not found`);\n subs[idx] = { ...subs[idx]!, status: \"revoked\" };\n await writeSubscriptions(dataDir, subs);\n}\n\nexport type RenewFn = (\n sub: PushSubscription\n) => Promise<{ expiresAt: string; providerData?: Partial<PushSubscription[\"providerData\"]> }>;\n\nexport async function renewExpiringSubscriptions(\n dataDir: string,\n renewFn: RenewFn,\n thresholdHours = 24\n): Promise<{ renewed: string[]; errors: string[] }> {\n const subs = await readSubscriptions(dataDir);\n const thresholdMs = thresholdHours * 60 * 60 * 1000;\n const cutoff = Date.now() + thresholdMs;\n\n const renewed: string[] = [];\n const errors: string[] = [];\n\n const PERMANENT_FAILURE_THRESHOLD = 3;\n\n for (let i = 0; i < subs.length; i++) {\n const sub = subs[i]!;\n if (sub.status !== \"active\" && sub.status !== \"error\") continue;\n if (sub.expiresAt === null) continue; // slack: no expiry\n if (new Date(sub.expiresAt).getTime() > cutoff) continue;\n\n try {\n const result = await renewFn(sub);\n subs[i] = {\n ...sub,\n status: \"active\",\n expiresAt: result.expiresAt,\n renewedAt: new Date().toISOString(),\n renewFailures: 0,\n providerData: result.providerData\n ? { ...sub.providerData, ...result.providerData }\n : sub.providerData,\n };\n renewed.push(sub.id);\n } catch {\n const failures = (sub.renewFailures ?? 0) + 1;\n const newStatus: PushStatus =\n failures >= PERMANENT_FAILURE_THRESHOLD ? \"permanently_failed\" : \"error\";\n subs[i] = { ...sub, status: newStatus, renewFailures: failures };\n errors.push(sub.id);\n }\n }\n\n await writeSubscriptions(dataDir, subs);\n return { renewed, errors };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
//#region src/core/quote-generator.ts
|
|
5
|
+
function quotesDir(dataDir) {
|
|
6
|
+
return path.join(dataDir, ".agentic", "quotes");
|
|
7
|
+
}
|
|
8
|
+
function loadQuoteConfig(dataDir) {
|
|
9
|
+
const p = path.join(dataDir, ".agentic", "quote-config.yaml");
|
|
10
|
+
if (!fs.existsSync(p)) return {};
|
|
11
|
+
try {
|
|
12
|
+
return yaml.load(fs.readFileSync(p, "utf-8")) ?? {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function nextQuoteNumber(dataDir) {
|
|
18
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
19
|
+
const dir = quotesDir(dataDir);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
return `Q-${year}-001`;
|
|
23
|
+
}
|
|
24
|
+
const existing = fs.readdirSync(dir).filter((f) => f.endsWith(".json") && f.startsWith(`Q-${year}-`)).map((f) => parseInt(f.replace(`Q-${year}-`, "").replace(".json", ""), 10)).filter((n) => !isNaN(n));
|
|
25
|
+
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
26
|
+
return `Q-${year}-${String(max + 1).padStart(3, "0")}`;
|
|
27
|
+
}
|
|
28
|
+
function addDaysToDate(isoDate, days) {
|
|
29
|
+
const [year, month, day] = isoDate.slice(0, 10).split("-").map(Number);
|
|
30
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
31
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
32
|
+
return d.toISOString().slice(0, 10);
|
|
33
|
+
}
|
|
34
|
+
function buildHtml(quote, config, customerName) {
|
|
35
|
+
const lineRows = quote.lineItems.map((item) => `<tr><td>${item.description}</td><td style="text-align:right">${item.quantity}</td><td style="text-align:right">${item.unitPrice.toFixed(2)} ${quote.currency}</td><td style="text-align:right">${item.total.toFixed(2)} ${quote.currency}</td></tr>`).join("\n");
|
|
36
|
+
return `<!DOCTYPE html>
|
|
37
|
+
<html lang="de">
|
|
38
|
+
<head><meta charset="UTF-8"><title>Angebot ${quote.quoteNumber}</title>
|
|
39
|
+
<style>body{font-family:Arial,sans-serif;max-width:800px;margin:40px auto;color:#222}table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border:1px solid #ddd}th{background:#f5f5f5}h1{color:#1a1a2e}.total{font-weight:bold;font-size:1.1em}</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<h1>Angebot ${quote.quoteNumber}</h1>
|
|
43
|
+
<p><strong>${config.companyName ?? ""}</strong><br>${config.companyAddress ?? ""}<br>${config.vatId ? `USt-IdNr.: ${config.vatId}` : ""}</p>
|
|
44
|
+
<hr>
|
|
45
|
+
<p><strong>An:</strong> ${customerName}</p>
|
|
46
|
+
<p><strong>Datum:</strong> ${quote.createdAt.slice(0, 10)} <strong>Gültig bis:</strong> ${quote.validUntil}</p>
|
|
47
|
+
<h2>Leistungen</h2>
|
|
48
|
+
<table>
|
|
49
|
+
<thead><tr><th>Beschreibung</th><th style="text-align:right">Menge</th><th style="text-align:right">Einzelpreis</th><th style="text-align:right">Gesamt</th></tr></thead>
|
|
50
|
+
<tbody>${lineRows}</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
<br>
|
|
53
|
+
<table style="width:300px;margin-left:auto">
|
|
54
|
+
<tr><td>Nettobetrag</td><td style="text-align:right">${quote.subtotal.toFixed(2)} ${quote.currency}</td></tr>
|
|
55
|
+
<tr><td>MwSt. (${quote.vatPercent}%)</td><td style="text-align:right">${quote.vat.toFixed(2)} ${quote.currency}</td></tr>
|
|
56
|
+
<tr class="total"><td><strong>Gesamtbetrag</strong></td><td style="text-align:right"><strong>${quote.total.toFixed(2)} ${quote.currency}</strong></td></tr>
|
|
57
|
+
</table>
|
|
58
|
+
<br><p>${config.paymentTerms ?? ""}</p>
|
|
59
|
+
<hr><small>${config.footerText ?? ""}</small>
|
|
60
|
+
</body></html>`;
|
|
61
|
+
}
|
|
62
|
+
function readCustomerName(dataDir, slug) {
|
|
63
|
+
const p = path.join(dataDir, "customers", slug, "main_facts.md");
|
|
64
|
+
if (!fs.existsSync(p)) return slug;
|
|
65
|
+
const content = fs.readFileSync(p, "utf-8");
|
|
66
|
+
return /^name:\s*(.+)$/m.exec(content)?.[1]?.trim() ?? slug;
|
|
67
|
+
}
|
|
68
|
+
function readQuote(dataDir, quoteNumber) {
|
|
69
|
+
const p = path.join(quotesDir(dataDir), `${quoteNumber}.json`);
|
|
70
|
+
if (!fs.existsSync(p)) return null;
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function listQuotes(dataDir, slug) {
|
|
78
|
+
const dir = quotesDir(dataDir);
|
|
79
|
+
if (!fs.existsSync(dir)) return [];
|
|
80
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
|
|
81
|
+
try {
|
|
82
|
+
const q = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
83
|
+
return slug === void 0 || q.slug === slug ? [q] : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function generateQuote(dataDir, input) {
|
|
90
|
+
const config = loadQuoteConfig(dataDir);
|
|
91
|
+
const vatPercent = input.vatPercent ?? 19;
|
|
92
|
+
const validUntilDays = input.validUntilDays ?? 30;
|
|
93
|
+
const currency = input.currency ?? config.currency ?? "EUR";
|
|
94
|
+
const items = input.lineItems.map((item) => ({
|
|
95
|
+
description: item.description,
|
|
96
|
+
quantity: item.quantity,
|
|
97
|
+
unitPrice: item.unitPrice,
|
|
98
|
+
total: item.quantity * item.unitPrice
|
|
99
|
+
}));
|
|
100
|
+
const subtotal = items.reduce((sum, i) => sum + i.total, 0);
|
|
101
|
+
const vat = Math.round(subtotal * (vatPercent / 100) * 100) / 100;
|
|
102
|
+
const total = Math.round((subtotal + vat) * 100) / 100;
|
|
103
|
+
const quoteNumber = nextQuoteNumber(dataDir);
|
|
104
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
+
const validUntil = addDaysToDate(now.slice(0, 10), validUntilDays);
|
|
106
|
+
const dir = quotesDir(dataDir);
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
+
const htmlPath = path.join(dir, `${quoteNumber}.html`);
|
|
109
|
+
const quote = {
|
|
110
|
+
quoteNumber,
|
|
111
|
+
slug: input.slug,
|
|
112
|
+
dealName: input.dealName,
|
|
113
|
+
lineItems: items,
|
|
114
|
+
subtotal,
|
|
115
|
+
vatPercent,
|
|
116
|
+
vat,
|
|
117
|
+
total,
|
|
118
|
+
currency,
|
|
119
|
+
createdAt: now,
|
|
120
|
+
validUntilDays,
|
|
121
|
+
validUntil,
|
|
122
|
+
status: "draft",
|
|
123
|
+
htmlPath
|
|
124
|
+
};
|
|
125
|
+
fs.writeFileSync(path.join(dir, `${quoteNumber}.json`), JSON.stringify(quote, null, 2), "utf-8");
|
|
126
|
+
const html = buildHtml(quote, config, readCustomerName(dataDir, input.slug));
|
|
127
|
+
fs.writeFileSync(htmlPath, html, "utf-8");
|
|
128
|
+
return quote;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { listQuotes as n, readQuote as r, generateQuote as t };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=quote-generator-BfwENXzg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-generator-BfwENXzg.js","names":[],"sources":["../src/core/quote-generator.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport yaml from \"js-yaml\";\nimport type { Quote, QuoteLineItem } from \"../schemas/quote.js\";\n\ninterface QuoteConfig {\n companyName?: string;\n companyAddress?: string;\n vatId?: string;\n currency?: string;\n paymentTerms?: string;\n footerText?: string;\n}\n\nexport interface GenerateQuoteInput {\n slug: string;\n dealName: string;\n lineItems: Array<{ description: string; quantity: number; unitPrice: number }>;\n vatPercent?: number;\n validUntilDays?: number;\n currency?: string;\n}\n\nfunction quotesDir(dataDir: string): string {\n return path.join(dataDir, \".agentic\", \"quotes\");\n}\n\nfunction loadQuoteConfig(dataDir: string): QuoteConfig {\n const p = path.join(dataDir, \".agentic\", \"quote-config.yaml\");\n if (!fs.existsSync(p)) return {};\n try {\n return (yaml.load(fs.readFileSync(p, \"utf-8\") as string) as QuoteConfig) ?? {};\n } catch {\n return {};\n }\n}\n\nfunction nextQuoteNumber(dataDir: string): string {\n const year = new Date().getFullYear();\n const dir = quotesDir(dataDir);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n return `Q-${year}-001`;\n }\n const existing = fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".json\") && f.startsWith(`Q-${year}-`))\n .map((f) => parseInt(f.replace(`Q-${year}-`, \"\").replace(\".json\", \"\"), 10))\n .filter((n) => !isNaN(n));\n const max = existing.length > 0 ? Math.max(...existing) : 0;\n return `Q-${year}-${String(max + 1).padStart(3, \"0\")}`;\n}\n\nfunction addDaysToDate(isoDate: string, days: number): string {\n const [year, month, day] = isoDate.slice(0, 10).split(\"-\").map(Number) as [\n number,\n number,\n number,\n ];\n const d = new Date(Date.UTC(year, month - 1, day));\n d.setUTCDate(d.getUTCDate() + days);\n return d.toISOString().slice(0, 10);\n}\n\nfunction buildHtml(quote: Quote, config: QuoteConfig, customerName: string): string {\n const lineRows = quote.lineItems\n .map(\n (item) =>\n `<tr><td>${item.description}</td><td style=\"text-align:right\">${item.quantity}</td><td style=\"text-align:right\">${item.unitPrice.toFixed(2)} ${quote.currency}</td><td style=\"text-align:right\">${item.total.toFixed(2)} ${quote.currency}</td></tr>`\n )\n .join(\"\\n\");\n\n return `<!DOCTYPE html>\n<html lang=\"de\">\n<head><meta charset=\"UTF-8\"><title>Angebot ${quote.quoteNumber}</title>\n<style>body{font-family:Arial,sans-serif;max-width:800px;margin:40px auto;color:#222}table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border:1px solid #ddd}th{background:#f5f5f5}h1{color:#1a1a2e}.total{font-weight:bold;font-size:1.1em}</style>\n</head>\n<body>\n<h1>Angebot ${quote.quoteNumber}</h1>\n<p><strong>${config.companyName ?? \"\"}</strong><br>${config.companyAddress ?? \"\"}<br>${config.vatId ? `USt-IdNr.: ${config.vatId}` : \"\"}</p>\n<hr>\n<p><strong>An:</strong> ${customerName}</p>\n<p><strong>Datum:</strong> ${quote.createdAt.slice(0, 10)} <strong>Gültig bis:</strong> ${quote.validUntil}</p>\n<h2>Leistungen</h2>\n<table>\n<thead><tr><th>Beschreibung</th><th style=\"text-align:right\">Menge</th><th style=\"text-align:right\">Einzelpreis</th><th style=\"text-align:right\">Gesamt</th></tr></thead>\n<tbody>${lineRows}</tbody>\n</table>\n<br>\n<table style=\"width:300px;margin-left:auto\">\n<tr><td>Nettobetrag</td><td style=\"text-align:right\">${quote.subtotal.toFixed(2)} ${quote.currency}</td></tr>\n<tr><td>MwSt. (${quote.vatPercent}%)</td><td style=\"text-align:right\">${quote.vat.toFixed(2)} ${quote.currency}</td></tr>\n<tr class=\"total\"><td><strong>Gesamtbetrag</strong></td><td style=\"text-align:right\"><strong>${quote.total.toFixed(2)} ${quote.currency}</strong></td></tr>\n</table>\n<br><p>${config.paymentTerms ?? \"\"}</p>\n<hr><small>${config.footerText ?? \"\"}</small>\n</body></html>`;\n}\n\nfunction readCustomerName(dataDir: string, slug: string): string {\n const p = path.join(dataDir, \"customers\", slug, \"main_facts.md\");\n if (!fs.existsSync(p)) return slug;\n const content = fs.readFileSync(p, \"utf-8\") as string;\n const match = /^name:\\s*(.+)$/m.exec(content);\n return match?.[1]?.trim() ?? slug;\n}\n\nexport function readQuote(dataDir: string, quoteNumber: string): Quote | null {\n const p = path.join(quotesDir(dataDir), `${quoteNumber}.json`);\n if (!fs.existsSync(p)) return null;\n try {\n return JSON.parse(fs.readFileSync(p, \"utf-8\") as string) as Quote;\n } catch {\n return null;\n }\n}\n\nexport function listQuotes(dataDir: string, slug?: string): Quote[] {\n const dir = quotesDir(dataDir);\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".json\"))\n .flatMap((f) => {\n try {\n const q = JSON.parse(fs.readFileSync(path.join(dir, f), \"utf-8\") as string) as Quote;\n return slug === undefined || q.slug === slug ? [q] : [];\n } catch {\n return [];\n }\n });\n}\n\nexport function updateQuoteStatus(\n dataDir: string,\n quoteNumber: string,\n status: Quote[\"status\"]\n): void {\n const q = readQuote(dataDir, quoteNumber);\n if (!q) return;\n const updated: Quote = { ...q, status };\n if (status === \"viewed\" && !q.viewedAt) updated.viewedAt = new Date().toISOString();\n if (status === \"accepted\" && !q.acceptedAt) updated.acceptedAt = new Date().toISOString();\n fs.writeFileSync(\n path.join(quotesDir(dataDir), `${quoteNumber}.json`),\n JSON.stringify(updated, null, 2),\n \"utf-8\"\n );\n}\n\nexport async function generateQuote(dataDir: string, input: GenerateQuoteInput): Promise<Quote> {\n const config = loadQuoteConfig(dataDir);\n const vatPercent = input.vatPercent ?? 19;\n const validUntilDays = input.validUntilDays ?? 30;\n const currency = input.currency ?? config.currency ?? \"EUR\";\n\n const items: QuoteLineItem[] = input.lineItems.map((item) => ({\n description: item.description,\n quantity: item.quantity,\n unitPrice: item.unitPrice,\n total: item.quantity * item.unitPrice,\n }));\n\n const subtotal = items.reduce((sum, i) => sum + i.total, 0);\n const vat = Math.round(subtotal * (vatPercent / 100) * 100) / 100;\n const total = Math.round((subtotal + vat) * 100) / 100;\n\n const quoteNumber = nextQuoteNumber(dataDir);\n const now = new Date().toISOString();\n const validUntil = addDaysToDate(now.slice(0, 10), validUntilDays);\n\n const dir = quotesDir(dataDir);\n fs.mkdirSync(dir, { recursive: true });\n\n const htmlPath = path.join(dir, `${quoteNumber}.html`);\n\n const quote: Quote = {\n quoteNumber,\n slug: input.slug,\n dealName: input.dealName,\n lineItems: items,\n subtotal,\n vatPercent,\n vat,\n total,\n currency,\n createdAt: now,\n validUntilDays,\n validUntil,\n status: \"draft\",\n htmlPath,\n };\n\n fs.writeFileSync(path.join(dir, `${quoteNumber}.json`), JSON.stringify(quote, null, 2), \"utf-8\");\n\n const customerName = readCustomerName(dataDir, input.slug);\n const html = buildHtml(quote, config, customerName);\n fs.writeFileSync(htmlPath, html, \"utf-8\");\n\n return quote;\n}\n"],"mappings":"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"}
|