@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as writeMainFacts, i as readMainFacts, n as ensureCustomerDir, o as MainFactsSchema, r as listCustomerSlugs } from "./customer-dir-DIylZ8Q6.js";
|
|
3
|
+
import { a as warning, i as success, n as error, r as info, t as bold } from "./colors-BG07TZQz.js";
|
|
4
|
+
import { n as getSession } from "./session-store-CEa39Dxs.js";
|
|
5
|
+
import { i as sessionCommand, r as readAllSessions } from "./session-B9AilxOE.js";
|
|
6
|
+
import { a as searchKbSimple, i as listKbArticles, n as getKbArticle, o as writeKbArticle, s as CAPABILITIES_TEXT, t as deleteKbArticle } from "./knowledge-base-D0Fh40kc.js";
|
|
7
|
+
import { a as restoreCommand, t as backupCommand } from "./backup-CeMk9z86.js";
|
|
8
|
+
import { n as readSyncState } from "./sync-state-ChaLbamC.js";
|
|
9
|
+
import { n as readUnmatched } from "./unmatched-transcripts-BsH5bhkU.js";
|
|
10
|
+
import { t as AgentConfigSchema } from "./agent-config-zPvcqu07.js";
|
|
11
|
+
import { t as appendInteraction } from "./interactions-writer-SLHnoEeE.js";
|
|
12
|
+
import { i as writeAuditEntry, n as getActor, r as readAuditLog, t as filterAuditLog } from "./audit-log-DNMY9mUZ.js";
|
|
13
|
+
import { i as canWrite, o as getRbacConfig, u as setActorRole } from "./rbac-CTIktZaC.js";
|
|
14
|
+
import { t as withJsonFile } from "./file-lock-B_zi7NQl.js";
|
|
15
|
+
import { d as deletePipelineStage, f as getPipelineStages, m as setPipelineStage, p as resetToDefaults } from "./revenue-simulation-Bqf2DLVB.js";
|
|
16
|
+
import { d as pursueGoal, h as updateGoalProgress, i as getActiveGoals, n as cancelGoal } from "./goal-engine-KpBftn4V.js";
|
|
17
|
+
import { a as revoke, i as renewExpiringSubscriptions, n as readSubscriptions, r as register } from "./push-manager-CdqIIkuh.js";
|
|
18
|
+
import { a as writeEnrollment, d as getTemplate, f as listTemplates, l as interpolate, n as listSequences, o as writeSequence, p as writeTemplate, r as readEnrollments, s as buildVariablesFromCustomer, t as getSequence, u as deleteTemplate } from "./sequence-store-DaaWr0Os.js";
|
|
19
|
+
import { r as runSequenceCycle } from "./sequence-engine-J1lTW_in.js";
|
|
20
|
+
import { n as listQuotes, r as readQuote, t as generateQuote } from "./quote-generator-BfwENXzg.js";
|
|
21
|
+
import { i as upsertTicket, n as nextTicketId, r as readTickets, t as listAllTickets } from "./ticket-writer-j2oX_Wal.js";
|
|
22
|
+
import { i as loadSlaRules, t as calcSlaDue } from "./sla-engine-BqX-7u-7.js";
|
|
23
|
+
import { a as listSurveys, d as writeSurvey, i as getSurvey, l as savePendingSurvey, n as calcNpsScore, o as loadSurveyResponses, r as generateSurveyToken } from "./survey-engine-DBjCYqCv.js";
|
|
24
|
+
import { Command } from "commander";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
import slugify from "slug";
|
|
28
|
+
import matter from "gray-matter";
|
|
29
|
+
import Table from "cli-table3";
|
|
30
|
+
import os from "os";
|
|
31
|
+
import { execSync, spawn } from "child_process";
|
|
32
|
+
import { createHash } from "crypto";
|
|
33
|
+
//#region src/commands/create.ts
|
|
34
|
+
async function createCustomer(opts) {
|
|
35
|
+
const id = slugify(opts.name, { lower: true });
|
|
36
|
+
const dataDir = opts.dataDir ?? process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
37
|
+
await ensureCustomerDir(dataDir, id);
|
|
38
|
+
const dir = path.join(dataDir, "customers", id);
|
|
39
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
40
|
+
await writeMainFacts(dataDir, id, {
|
|
41
|
+
name: opts.name,
|
|
42
|
+
domain: opts.domain,
|
|
43
|
+
email: opts.email,
|
|
44
|
+
relationship_stage: "prospect",
|
|
45
|
+
tags: [],
|
|
46
|
+
currency: "EUR",
|
|
47
|
+
created: today,
|
|
48
|
+
updated: today
|
|
49
|
+
});
|
|
50
|
+
const interactionsPath = path.join(dir, "interactions.md");
|
|
51
|
+
if (!fs.existsSync(interactionsPath)) fs.writeFileSync(interactionsPath, `# Interactions — ${opts.name}\n\n`);
|
|
52
|
+
const pipelinePath = path.join(dir, "pipeline.md");
|
|
53
|
+
if (!fs.existsSync(pipelinePath)) fs.writeFileSync(pipelinePath, `# Pipeline — ${opts.name}\n\n| Deal | Stage | Value | Currency | Probability | Close Date | Updated | Notes |\n|---|---|---|---|---|---|---|---|\n`);
|
|
54
|
+
const sourcesPath = path.join(dir, "sources.json");
|
|
55
|
+
if (!fs.existsSync(sourcesPath)) {
|
|
56
|
+
const sources = {
|
|
57
|
+
gmail: {
|
|
58
|
+
type: "gmail",
|
|
59
|
+
query: opts.domain ? `from:${opts.domain} OR to:${opts.domain}` : opts.email ? `from:${opts.email} OR to:${opts.email}` : "",
|
|
60
|
+
enabled: true
|
|
61
|
+
},
|
|
62
|
+
version: 1,
|
|
63
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
64
|
+
};
|
|
65
|
+
fs.writeFileSync(sourcesPath, JSON.stringify(sources, null, 2));
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
dir
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const createCommand = new Command("create").argument("<name>", "Customer name").option("--domain <domain>", "Primary domain (for Gmail sync)").option("--email <email>", "Primary contact email").action(async (name, opts) => {
|
|
73
|
+
try {
|
|
74
|
+
const { id, dir } = await createCustomer({
|
|
75
|
+
name,
|
|
76
|
+
...opts
|
|
77
|
+
});
|
|
78
|
+
console.log(success(`✓ Created customer: ${bold(id)}`));
|
|
79
|
+
console.log(` Dir: ${dir}`);
|
|
80
|
+
console.log(` Files: main_facts.md, interactions.md, pipeline.md, sources.json`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(error(`✗ ${err.message}`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/ui/table.ts
|
|
88
|
+
function renderCustomerTable(customers) {
|
|
89
|
+
const table = new Table({
|
|
90
|
+
head: [
|
|
91
|
+
"Slug",
|
|
92
|
+
"Name",
|
|
93
|
+
"Stage",
|
|
94
|
+
"Industry",
|
|
95
|
+
"Tags",
|
|
96
|
+
"Updated"
|
|
97
|
+
],
|
|
98
|
+
style: { head: ["cyan"] },
|
|
99
|
+
colWidths: [
|
|
100
|
+
20,
|
|
101
|
+
25,
|
|
102
|
+
15,
|
|
103
|
+
15,
|
|
104
|
+
20,
|
|
105
|
+
12
|
|
106
|
+
],
|
|
107
|
+
wordWrap: true
|
|
108
|
+
});
|
|
109
|
+
for (const { slug, facts } of customers) table.push([
|
|
110
|
+
slug,
|
|
111
|
+
facts.name,
|
|
112
|
+
facts.relationship_stage,
|
|
113
|
+
facts.industry ?? "—",
|
|
114
|
+
facts.tags.join(", ") || "—",
|
|
115
|
+
facts.updated
|
|
116
|
+
]);
|
|
117
|
+
return table.toString();
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/commands/list.ts
|
|
121
|
+
const listCommand = new Command("list").option("--filter <query>", "Filter by name or slug").action(async (opts) => {
|
|
122
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
123
|
+
const slugs = listCustomerSlugs(dataDir);
|
|
124
|
+
if (slugs.length === 0) {
|
|
125
|
+
console.log("No customers yet. Run: dxcrm create \"Customer Name\"");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const customers = [];
|
|
129
|
+
for (const slug of slugs) try {
|
|
130
|
+
const facts = await readMainFacts(dataDir, slug);
|
|
131
|
+
if (opts.filter) {
|
|
132
|
+
const q = opts.filter.toLowerCase();
|
|
133
|
+
if (!facts.name.toLowerCase().includes(q) && !slug.includes(q) && !(facts.relationship_stage ?? "").toLowerCase().includes(q)) continue;
|
|
134
|
+
}
|
|
135
|
+
customers.push({
|
|
136
|
+
slug,
|
|
137
|
+
facts
|
|
138
|
+
});
|
|
139
|
+
} catch {}
|
|
140
|
+
if (customers.length === 0) {
|
|
141
|
+
console.log(opts.filter ? `No customers matching "${opts.filter}"` : "No customers yet.");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log(renderCustomerTable(customers));
|
|
145
|
+
});
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/commands/validate.ts
|
|
148
|
+
const RECOVERABLE_DEFAULTS = {
|
|
149
|
+
tags: [],
|
|
150
|
+
currency: "EUR"
|
|
151
|
+
};
|
|
152
|
+
function applyFix(factsPath, content, data) {
|
|
153
|
+
const fixed = [];
|
|
154
|
+
const patched = { ...data };
|
|
155
|
+
for (const [field, defaultValue] of Object.entries(RECOVERABLE_DEFAULTS)) if (patched[field] === void 0 || patched[field] === null) {
|
|
156
|
+
patched[field] = defaultValue;
|
|
157
|
+
fixed.push(`${field} → ${JSON.stringify(defaultValue)}`);
|
|
158
|
+
}
|
|
159
|
+
if (patched["updated"] === void 0 && patched["created"]) {
|
|
160
|
+
patched["updated"] = patched["created"];
|
|
161
|
+
fixed.push(`updated → ${String(patched["created"])}`);
|
|
162
|
+
}
|
|
163
|
+
if (fixed.length === 0) return null;
|
|
164
|
+
const parsed = matter(content);
|
|
165
|
+
const newContent = matter.stringify(parsed.content, patched);
|
|
166
|
+
fs.writeFileSync(factsPath, newContent);
|
|
167
|
+
return {
|
|
168
|
+
fixed,
|
|
169
|
+
content: newContent
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function runValidate(opts, dataDir) {
|
|
173
|
+
const customersDir = path.join(dataDir, "customers");
|
|
174
|
+
if (!fs.existsSync(customersDir)) {
|
|
175
|
+
console.log(warning("⚠ No customers directory found."));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const slugs = listCustomerSlugs(dataDir);
|
|
179
|
+
let errorCount = 0;
|
|
180
|
+
let fixedCount = 0;
|
|
181
|
+
for (const slug of slugs) {
|
|
182
|
+
const factsPath = path.join(customersDir, slug, "main_facts.md");
|
|
183
|
+
const interactionsPath = path.join(customersDir, slug, "interactions.md");
|
|
184
|
+
if (!fs.existsSync(factsPath)) {
|
|
185
|
+
console.log(error(`✗ ${slug}: missing main_facts.md`));
|
|
186
|
+
errorCount++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
let content = fs.readFileSync(factsPath, "utf-8");
|
|
191
|
+
const { data } = matter(content);
|
|
192
|
+
if (opts.fix) {
|
|
193
|
+
const result = applyFix(factsPath, content, data);
|
|
194
|
+
if (result) {
|
|
195
|
+
content = result.content;
|
|
196
|
+
fixedCount++;
|
|
197
|
+
console.log(info(`⚙ ${slug}: fixed ${result.fixed.join(", ")}`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const { data: refetchedData } = matter(content);
|
|
201
|
+
MainFactsSchema.parse(refetchedData);
|
|
202
|
+
if (!fs.existsSync(interactionsPath)) console.log(warning(`⚠ ${slug}: missing interactions.md`));
|
|
203
|
+
else console.log(success(`✓ ${slug}`));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.log(error(`✗ ${slug}: ${err.message}`));
|
|
206
|
+
errorCount++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (opts.fix && fixedCount > 0) console.log(info(`\n⚙ Fixed ${fixedCount} customer(s).`));
|
|
210
|
+
if (errorCount > 0) {
|
|
211
|
+
console.error(error(`\n${errorCount} error(s) found.`));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
} else console.log(success("\n✓ All customers valid."));
|
|
214
|
+
}
|
|
215
|
+
const validateCommand = new Command("validate").option("--fix", "Auto-fix recoverable issues").action(async (opts) => {
|
|
216
|
+
await runValidate(opts, process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
217
|
+
});
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/commands/guide.ts
|
|
220
|
+
const guideCommand = new Command("guide").description("Full CRM documentation in terminal").action(() => {
|
|
221
|
+
console.log(CAPABILITIES_TEXT);
|
|
222
|
+
});
|
|
223
|
+
const mcpCommand = new Command("mcp");
|
|
224
|
+
mcpCommand.command("docs").action(() => {
|
|
225
|
+
console.log(CAPABILITIES_TEXT);
|
|
226
|
+
});
|
|
227
|
+
mcpCommand.command("token").description("Mint a bearer token for the HTTP MCP server (printed once)").requiredOption("--actor <actor>", "Actor/user the token authenticates as").option("--role <role>", "RBAC role: admin | manager | rep", "rep").option("--label <label>", "Optional label (e.g. device name)").action(async (opts) => {
|
|
228
|
+
const role = [
|
|
229
|
+
"admin",
|
|
230
|
+
"manager",
|
|
231
|
+
"rep"
|
|
232
|
+
].includes(opts.role) ? opts.role : "rep";
|
|
233
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
234
|
+
const { createMcpToken } = await import("./auth-CyFuu9X_.js");
|
|
235
|
+
const token = createMcpToken(dataDir, opts.actor, role, opts.label);
|
|
236
|
+
console.log(info("MCP bearer token (store it now — it is not shown again):"));
|
|
237
|
+
console.log(token);
|
|
238
|
+
console.log(info(`actor=${opts.actor} role=${role} — HTTP /mcp now requires this token.`));
|
|
239
|
+
});
|
|
240
|
+
mcpCommand.command("start").description("Start MCP server (stdio by default)").option("--http", "Use HTTP transport instead of stdio").option("--port <port>", "HTTP port (default 3847)", "3847").action(async (opts) => {
|
|
241
|
+
if (opts.http) {
|
|
242
|
+
const port = parseInt(opts.port, 10);
|
|
243
|
+
console.error(info(`Starting MCP server in HTTP mode on port ${port}...`));
|
|
244
|
+
const { startHttp } = await import("./server-Dyva03K8.js");
|
|
245
|
+
await startHttp(port);
|
|
246
|
+
} else {
|
|
247
|
+
const { startStdio } = await import("./server-Dyva03K8.js");
|
|
248
|
+
await startStdio();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const TOOL_COUNT = 56;
|
|
252
|
+
/** Claude Code: CLAUDE.md in CRM dataDir */
|
|
253
|
+
function buildClaudeMd(dataDir) {
|
|
254
|
+
return `# DatasynxOpenCRM v2 — Agent Instructions (${TOOL_COUNT} MCP Tools)
|
|
255
|
+
|
|
256
|
+
## Proactive — Act Without Being Asked
|
|
257
|
+
At the start of every session, before the user says anything:
|
|
258
|
+
1. \`get_proactive_briefing()\` — today's urgent items, forecast, top action
|
|
259
|
+
2. \`get_goal_status()\` — if active goals exist, show progress
|
|
260
|
+
|
|
261
|
+
## Before Every Deal Conversation
|
|
262
|
+
Use \`open_deal_room({ slug, dealName })\` — not \`get_customer_context()\`.
|
|
263
|
+
It combines graph, health, revenue simulation, playbook, and org intelligence in one call (~3–5s).
|
|
264
|
+
|
|
265
|
+
## Standard Workflow
|
|
266
|
+
| Trigger | Tool |
|
|
267
|
+
|---|---|
|
|
268
|
+
| Customer mentioned | \`get_customer_context(slug)\` or \`open_deal_room(slug, dealName)\` |
|
|
269
|
+
| After call/meeting/email | \`log_interaction(slug, type, summary, nextSteps)\` |
|
|
270
|
+
| Deal stage changes | \`update_deal(slug, dealName, { stage, probability, value })\` |
|
|
271
|
+
| Historical question | \`search_customer_knowledge(slug, query)\` |
|
|
272
|
+
| "What should I do today?" | \`get_proactive_briefing()\` |
|
|
273
|
+
| "Close €X this quarter" | \`pursue_goal(goal, deadline)\` |
|
|
274
|
+
|
|
275
|
+
## Autonomy Patterns
|
|
276
|
+
|
|
277
|
+
**User says "Look at Acme Corp":**
|
|
278
|
+
1. \`open_deal_room({ slug: "acme-corp", dealName: "<active deal>" })\`
|
|
279
|
+
2. Summarize in 3 bullets, recommend 1 action
|
|
280
|
+
|
|
281
|
+
**User says "What do I need to do today?":**
|
|
282
|
+
1. \`get_proactive_briefing()\`
|
|
283
|
+
2. \`get_goal_status()\` if goals are active
|
|
284
|
+
3. Reply with prioritized actions
|
|
285
|
+
|
|
286
|
+
**User says "Deal is stalled":**
|
|
287
|
+
1. \`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
|
|
288
|
+
2. Present the plan, ask for approval before acting
|
|
289
|
+
|
|
290
|
+
## All ${TOOL_COUNT} MCP Tools
|
|
291
|
+
|
|
292
|
+
### Foundation
|
|
293
|
+
- \`get_capabilities()\` — complete tool reference with schemas
|
|
294
|
+
- \`get_active_session()\` — which customer is currently open
|
|
295
|
+
- \`get_customer_context(slug?)\` — full briefing, triggers background Gmail sync
|
|
296
|
+
- \`search_customer_knowledge(slug, query)\` — semantic vector search across emails + transcripts
|
|
297
|
+
- \`list_customers(filter?)\` — all customers with health score and last touchpoint
|
|
298
|
+
- \`log_interaction(slug, type, summary, nextSteps?)\` — write to CRM; auto-updates graph + health
|
|
299
|
+
- \`update_deal(slug, dealName, fields)\` — pipeline stage, value, probability, close date
|
|
300
|
+
- \`export_customer(slug, format?)\` — export as JSON or Markdown ZIP
|
|
301
|
+
- \`update_customer_facts(slug, fields)\` — name, domain, email, primary_contact, tags
|
|
302
|
+
- \`get_deal_health(slug)\` — health score A–F with warnings per deal
|
|
303
|
+
- \`get_pipeline_forecast()\` — weighted pipeline total and deal list
|
|
304
|
+
- \`summarize_meeting(transcript)\` — LLM meeting analysis → structured notes
|
|
305
|
+
- \`get_pipeline_stages()\` — configured stages with default probabilities
|
|
306
|
+
- \`get_market_intelligence(slug)\` — competitor mentions and market context
|
|
307
|
+
|
|
308
|
+
### Graph & Relationship (D11/D12)
|
|
309
|
+
- \`get_relationship_graph(slug)\` — stakeholder graph: champions, blockers, economic buyers, warm intro paths
|
|
310
|
+
- \`get_relationship_health(slug)\` — contact health scores A–F, decay detection, risk flags
|
|
311
|
+
|
|
312
|
+
### Autonomous Deal Agent (D13)
|
|
313
|
+
- \`run_deal_agent({ slug, dealName, autonomyLevel })\` — AI deal analysis + action plan; autonomyLevel: "observe" | "suggest" | "act"
|
|
314
|
+
- \`approve_agent_action({ actionId, approved })\` — approve or reject a queued agent action
|
|
315
|
+
|
|
316
|
+
### Revenue Intelligence (D14/D18)
|
|
317
|
+
- \`simulate_revenue({ horizon })\` — Monte Carlo P10/P50/P90 forecast over full pipeline
|
|
318
|
+
- \`get_org_intelligence({ slug, dealName })\` — stakeholder map, missing roles, external signals (funding, news)
|
|
319
|
+
|
|
320
|
+
### Playbooks (D15)
|
|
321
|
+
- \`get_playbook({ slug, situation })\` — matching playbook for current deal situation
|
|
322
|
+
- \`create_playbook({ name, trigger, content })\` — save a new playbook
|
|
323
|
+
- \`list_playbooks()\` — all available playbooks
|
|
324
|
+
- \`distill_playbook({ slug, dealName, outcome })\` — learn from a won/lost deal
|
|
325
|
+
|
|
326
|
+
### Goals (D16)
|
|
327
|
+
- \`pursue_goal({ goal, deadline, context? })\` — decompose a revenue goal into prioritized sub-actions
|
|
328
|
+
- \`get_goal_status()\` — progress of all active goals
|
|
329
|
+
|
|
330
|
+
### Push (D17)
|
|
331
|
+
- \`register_push_subscription({ provider, webhookUrl })\` — gmail | microsoft-graph | slack real-time push
|
|
332
|
+
- \`get_push_status()\` — active subscriptions and expiry dates
|
|
333
|
+
|
|
334
|
+
### Intelligence Synthesis (D19/D20)
|
|
335
|
+
- \`open_deal_room({ slug, dealName })\` — orchestrates 7 sub-tools; returns complete deal brief in one call
|
|
336
|
+
- \`get_proactive_briefing({ date? })\` — AI-generated daily briefing: urgent items, opportunities, forecast
|
|
337
|
+
|
|
338
|
+
### Email Templates (H2)
|
|
339
|
+
- \`list_email_templates({ category? })\` — list templates; filter by category (outreach, followup, support)
|
|
340
|
+
- \`get_email_template({ id })\` — get full template with auto-detected variables
|
|
341
|
+
- \`draft_email({ slug, templateId, overrides? })\` — draft personalized email from template + customer facts
|
|
342
|
+
|
|
343
|
+
### Email Sequences (H1)
|
|
344
|
+
- \`enroll_in_sequence({ slug, contactEmail, sequenceId })\` — enroll contact in automated email sequence
|
|
345
|
+
- \`list_sequence_enrollments({ slug?, status? })\` — list enrollments; filter by slug or status
|
|
346
|
+
- \`unenroll_from_sequence({ enrollmentId })\` — pause an active enrollment
|
|
347
|
+
- \`list_sequences()\` — all sequences with step count and enrollment count
|
|
348
|
+
|
|
349
|
+
### Quotes & Invoices (H4)
|
|
350
|
+
- \`generate_quote({ slug, dealName, lineItems, vatPercent?, validUntilDays? })\` — create HTML quote with auto-numbering Q-YYYY-NNN
|
|
351
|
+
- \`get_quote_status({ quoteNumber?, slug? })\` — get quote or list all quotes for customer
|
|
352
|
+
|
|
353
|
+
### Meeting Scheduler (H3)
|
|
354
|
+
- \`get_booking_link({ slug, eventType?, prefillName? })\` — get Calendly booking URL, optionally pre-filled with customer name/email
|
|
355
|
+
|
|
356
|
+
### Ticket Management (H6)
|
|
357
|
+
- \`create_ticket({ slug, title, priority?, assignee? })\` — open support ticket with auto-SLA due date
|
|
358
|
+
- \`update_ticket({ slug, ticketId, status?, assignee? })\` — update ticket status or assignee
|
|
359
|
+
- \`list_tickets({ slug?, status?, priority?, assignee? })\` — list tickets sorted by priority
|
|
360
|
+
- \`close_ticket({ slug, ticketId, resolution? })\` — close ticket and optionally log resolution
|
|
361
|
+
|
|
362
|
+
### NPS/CSAT Surveys (H7)
|
|
363
|
+
- \`send_nps_survey({ slug, contactEmail, surveyId, serverUrl? })\` — generate survey token and email body for NPS/CSAT survey
|
|
364
|
+
- \`get_survey_results({ surveyId, slug? })\` — NPS score, promoters/passives/detractors, all responses
|
|
365
|
+
|
|
366
|
+
### Knowledge Base (H8)
|
|
367
|
+
- \`search_knowledge_base({ query, publicOnly? })\` — full-text search across KB articles
|
|
368
|
+
- \`create_kb_article({ id, title, body, category?, tags?, public?, sourceTicketId? })\` — create or update KB article
|
|
369
|
+
|
|
370
|
+
### Backup (Enterprise)
|
|
371
|
+
- \`backup_now({ remote?, note? })\` — trigger immediate backup of customers/ + .agentic/ with manifest + integrity check
|
|
372
|
+
- \`list_backups({ limit? })\` — list available backups with date, size, verification status, customer count
|
|
373
|
+
|
|
374
|
+
### Sync & Audit (Enterprise)
|
|
375
|
+
- \`trigger_sync({ slug?, since? })\` — force immediate Gmail sync for one or all customers (bypasses 30-min daemon cycle)
|
|
376
|
+
- \`get_audit_log({ slug?, actor?, limit? })\` — read append-only audit log of all write operations
|
|
377
|
+
|
|
378
|
+
### Custom Objects (Platform / metadata)
|
|
379
|
+
- \`define_custom_object({ name, label?, fields })\` — define a runtime entity type with typed fields (no migration), admin
|
|
380
|
+
- \`create_record({ object, values })\` — create a record of a custom object, validated against its schema, rep+
|
|
381
|
+
- \`list_records({ object })\` — list records of a custom object
|
|
382
|
+
- \`list_custom_objects()\` — list all defined custom objects and their schemas
|
|
383
|
+
|
|
384
|
+
## Rules
|
|
385
|
+
- Never discuss a customer without first loading their context
|
|
386
|
+
- Always log interactions — calls, emails, Slack, demos, proposals
|
|
387
|
+
- Never invent information — if uncertain, use search_customer_knowledge
|
|
388
|
+
- Use open_deal_room before any deal conversation, not get_customer_context
|
|
389
|
+
|
|
390
|
+
## Data Directory
|
|
391
|
+
${dataDir}`.trim();
|
|
392
|
+
}
|
|
393
|
+
/** OpenClaw / Hermes: SOUL.md */
|
|
394
|
+
function buildSoulMd(framework) {
|
|
395
|
+
return `# Identity
|
|
396
|
+
I am a CRM-integrated AI assistant powered by DatasynxOpenCRM v2 (${TOOL_COUNT} MCP tools).
|
|
397
|
+
My purpose is to help manage customer relationships proactively — acting before being asked.
|
|
398
|
+
|
|
399
|
+
# Core Behaviors
|
|
400
|
+
- **Proactive first.** At session start I call \`get_proactive_briefing()\` without being asked.
|
|
401
|
+
- **Context before action.** Before any customer discussion: \`open_deal_room()\` or \`get_customer_context()\`.
|
|
402
|
+
- **Log everything.** Every interaction — calls, emails, meetings, Slack — goes into the CRM via \`log_interaction()\`.
|
|
403
|
+
- **Cite sources.** All customer information is cited: gmail://, file://, transcript://.
|
|
404
|
+
- **Glass-box reasoning.** When using \`run_deal_agent()\`, I surface the trace so the user can inspect my reasoning.
|
|
405
|
+
|
|
406
|
+
# Tool Hierarchy
|
|
407
|
+
1. \`open_deal_room()\` — for deal conversations (combines 7 sub-tools)
|
|
408
|
+
2. \`get_proactive_briefing()\` — for morning / session start
|
|
409
|
+
3. \`get_customer_context()\` — for general customer questions
|
|
410
|
+
4. \`run_deal_agent()\` — when a deal needs AI-driven analysis
|
|
411
|
+
|
|
412
|
+
# Boundaries
|
|
413
|
+
- I do not invent customer data. I search or sync first.
|
|
414
|
+
- I do not skip logging — even quick Slack messages deserve a \`log_interaction()\`.
|
|
415
|
+
- I do not act autonomously beyond "suggest" level without explicit human approval via \`approve_agent_action()\`.
|
|
416
|
+
- I do not discuss a customer without context loaded.
|
|
417
|
+
|
|
418
|
+
# Communication
|
|
419
|
+
Direct. Action-oriented. Lead with the most important insight.
|
|
420
|
+
Bullet points for next steps. End every deal summary with: "What do you want to do first?"
|
|
421
|
+
|
|
422
|
+
# Framework
|
|
423
|
+
${framework === "openclaw" ? "OpenClaw — tool prefix: datasynx_opencrm:" : "Hermes — skill: datasynx-crm"}`.trim();
|
|
424
|
+
}
|
|
425
|
+
/** Hermes SOUL.md is same as OpenClaw */
|
|
426
|
+
const buildHermesSoulMd = buildSoulMd;
|
|
427
|
+
/** Codex / OpenClaw / Antigravity: AGENTS.md in dataDir */
|
|
428
|
+
function buildAgentsMd(dataDir) {
|
|
429
|
+
return `# DatasynxOpenCRM v2 — Agent Instructions (${TOOL_COUNT} MCP Tools)
|
|
430
|
+
|
|
431
|
+
## Role
|
|
432
|
+
You are a proactive CRM AI assistant. You act before being asked.
|
|
433
|
+
At every session start: call \`get_proactive_briefing()\`.
|
|
434
|
+
|
|
435
|
+
## Priority Tool Order
|
|
436
|
+
1. \`open_deal_room(slug, dealName)\` — before any deal conversation
|
|
437
|
+
2. \`get_proactive_briefing()\` — at session start, or when asked "what should I do?"
|
|
438
|
+
3. \`get_customer_context(slug)\` — for general customer questions
|
|
439
|
+
4. \`run_deal_agent(slug, dealName, "suggest")\` — when a deal is stalled
|
|
440
|
+
|
|
441
|
+
## Core Workflow
|
|
442
|
+
- Customer mentioned → \`get_customer_context(slug)\` immediately
|
|
443
|
+
- Deal conversation → \`open_deal_room(slug, dealName)\` first
|
|
444
|
+
- After interaction → \`log_interaction(slug, type, summary, nextSteps)\`
|
|
445
|
+
- Deal stage change → \`update_deal(slug, dealName, { stage, probability })\`
|
|
446
|
+
- Revenue goal → \`pursue_goal(goal, deadline)\`
|
|
447
|
+
|
|
448
|
+
## Available Tools (${TOOL_COUNT})
|
|
449
|
+
**Foundation:** get_capabilities · get_active_session · get_customer_context ·
|
|
450
|
+
search_customer_knowledge · list_customers · log_interaction · update_deal ·
|
|
451
|
+
export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
|
|
452
|
+
summarize_meeting · get_pipeline_stages · get_market_intelligence
|
|
453
|
+
|
|
454
|
+
**Graph & Health (D11/D12):** get_relationship_graph · get_relationship_health
|
|
455
|
+
|
|
456
|
+
**Autonomous Agent (D13):** run_deal_agent · approve_agent_action
|
|
457
|
+
|
|
458
|
+
**Revenue (D14/D18):** simulate_revenue · get_org_intelligence
|
|
459
|
+
|
|
460
|
+
**Playbooks (D15):** get_playbook · create_playbook · list_playbooks · distill_playbook
|
|
461
|
+
|
|
462
|
+
**Goals (D16):** pursue_goal · get_goal_status
|
|
463
|
+
|
|
464
|
+
**Push (D17):** register_push_subscription · get_push_status
|
|
465
|
+
|
|
466
|
+
**Synthesis (D19/D20):** open_deal_room · get_proactive_briefing
|
|
467
|
+
|
|
468
|
+
**Email Templates (H2):** list_email_templates · get_email_template · draft_email
|
|
469
|
+
|
|
470
|
+
**Email Sequences (H1):** enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences
|
|
471
|
+
|
|
472
|
+
**Quotes (H4):** generate_quote · get_quote_status
|
|
473
|
+
|
|
474
|
+
**Calendly (H3):** get_booking_link
|
|
475
|
+
|
|
476
|
+
**Tickets (H6):** create_ticket · update_ticket · list_tickets · close_ticket
|
|
477
|
+
|
|
478
|
+
**NPS/CSAT (H7):** send_nps_survey · get_survey_results
|
|
479
|
+
|
|
480
|
+
**Knowledge Base (H8):** search_knowledge_base · create_kb_article
|
|
481
|
+
|
|
482
|
+
**Backup (Enterprise):** backup_now · list_backups
|
|
483
|
+
|
|
484
|
+
**Sync & Audit (Enterprise):** trigger_sync · get_audit_log
|
|
485
|
+
|
|
486
|
+
**Custom Objects (Platform):** define_custom_object · create_record · list_records · list_custom_objects
|
|
487
|
+
|
|
488
|
+
## Never
|
|
489
|
+
- Discuss a customer without context loaded
|
|
490
|
+
- Skip logging — every touchpoint matters
|
|
491
|
+
- Invent information — use search_customer_knowledge first
|
|
492
|
+
- Act autonomously beyond "suggest" without approve_agent_action
|
|
493
|
+
|
|
494
|
+
## Data Location
|
|
495
|
+
${dataDir}`.trim();
|
|
496
|
+
}
|
|
497
|
+
/** Hermes skills file (agentskills.io standard) */
|
|
498
|
+
function buildHermesSkillMd() {
|
|
499
|
+
return `---
|
|
500
|
+
name: datasynx-crm
|
|
501
|
+
version: 2.0.0
|
|
502
|
+
description: Proactive CRM workflow skill for DatasynxOpenCRM v2 (${TOOL_COUNT} MCP tools)
|
|
503
|
+
triggers:
|
|
504
|
+
- "customer"
|
|
505
|
+
- "client"
|
|
506
|
+
- "deal"
|
|
507
|
+
- "pipeline"
|
|
508
|
+
- "sync"
|
|
509
|
+
- "briefing"
|
|
510
|
+
- "forecast"
|
|
511
|
+
- "goal"
|
|
512
|
+
- "stakeholder"
|
|
513
|
+
- "playbook"
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
# DatasynxOpenCRM v2 Skill
|
|
517
|
+
|
|
518
|
+
## Session Start — Always
|
|
519
|
+
Call \`get_proactive_briefing()\` at the start of every session without being asked.
|
|
520
|
+
|
|
521
|
+
## Before a Deal Conversation
|
|
522
|
+
Call \`open_deal_room({ slug, dealName })\` — returns graph, health, simulation, and playbook in one call.
|
|
523
|
+
|
|
524
|
+
## When a Customer Is Mentioned
|
|
525
|
+
Call \`get_customer_context(slug)\` before discussing anything.
|
|
526
|
+
Never assume you know the current state.
|
|
527
|
+
|
|
528
|
+
## After Every Interaction
|
|
529
|
+
Call \`log_interaction()\` with:
|
|
530
|
+
- type: Call | Meeting | Email | Note | Demo | Proposal
|
|
531
|
+
- summary: 2–5 sentences
|
|
532
|
+
- nextSteps: concrete actions as array
|
|
533
|
+
|
|
534
|
+
## For a Stalled Deal
|
|
535
|
+
\`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
|
|
536
|
+
Then use \`approve_agent_action()\` to confirm before acting.
|
|
537
|
+
|
|
538
|
+
## For Revenue Goals
|
|
539
|
+
\`pursue_goal({ goal: "Close €500k this quarter", deadline: "2026-09-30" })\`
|
|
540
|
+
|
|
541
|
+
## For Historical Research
|
|
542
|
+
\`search_customer_knowledge(slug, query)\` — searches emails AND transcripts.
|
|
543
|
+
|
|
544
|
+
## Pipeline Updates
|
|
545
|
+
After any deal discussion: \`update_deal(slug, dealName, { stage, probability, value })\`
|
|
546
|
+
|
|
547
|
+
## Quick Reference
|
|
548
|
+
\`list_customers()\` for overview · \`get_capabilities()\` for full schema`.trim();
|
|
549
|
+
}
|
|
550
|
+
/** Antigravity SKILL.md */
|
|
551
|
+
function buildAgySkillMd() {
|
|
552
|
+
return `---
|
|
553
|
+
name: datasynx-crm
|
|
554
|
+
version: 2.0.0
|
|
555
|
+
description: Proactive CRM workflow for DatasynxOpenCRM v2
|
|
556
|
+
triggers:
|
|
557
|
+
- customer
|
|
558
|
+
- client
|
|
559
|
+
- deal
|
|
560
|
+
- pipeline
|
|
561
|
+
- briefing
|
|
562
|
+
- forecast
|
|
563
|
+
- goal
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
# DatasynxOpenCRM v2 Skill
|
|
567
|
+
|
|
568
|
+
## Session Start
|
|
569
|
+
Call \`get_proactive_briefing()\` first — urgent items, forecast, top action.
|
|
570
|
+
|
|
571
|
+
## Deal Conversations
|
|
572
|
+
\`open_deal_room({ slug, dealName })\` — graph + health + simulation + playbook in one call.
|
|
573
|
+
|
|
574
|
+
## When a Customer Is Mentioned
|
|
575
|
+
\`get_customer_context(slug)\` before discussing anything.
|
|
576
|
+
|
|
577
|
+
## After Every Interaction
|
|
578
|
+
\`log_interaction(slug, type, summary, nextSteps)\`
|
|
579
|
+
|
|
580
|
+
## Stalled Deal
|
|
581
|
+
\`run_deal_agent({ slug, dealName, autonomyLevel: "suggest" })\`
|
|
582
|
+
|
|
583
|
+
## Revenue Goal
|
|
584
|
+
\`pursue_goal({ goal, deadline })\`
|
|
585
|
+
|
|
586
|
+
## Historical Research
|
|
587
|
+
\`search_customer_knowledge(slug, query)\`
|
|
588
|
+
|
|
589
|
+
## Pipeline
|
|
590
|
+
\`update_deal(slug, dealName, { stage, value, probability })\`
|
|
591
|
+
|
|
592
|
+
## Overview
|
|
593
|
+
\`list_customers()\` for all customers · \`get_capabilities()\` for full reference`.trim();
|
|
594
|
+
}
|
|
595
|
+
/** Antigravity: global GEMINI.md (~/.gemini/GEMINI.md) — token budget: max 50 lines */
|
|
596
|
+
function buildAgyGeminiMd(dataDir) {
|
|
597
|
+
return `# DatasynxOpenCRM v2 — Agent Context (${TOOL_COUNT} Tools)
|
|
598
|
+
|
|
599
|
+
You have access to a local CRM via MCP tools (server: datasynx-opencrm).
|
|
600
|
+
|
|
601
|
+
## Session Start — Always Do This First
|
|
602
|
+
\`get_proactive_briefing()\` — urgent items, opportunities, forecast
|
|
603
|
+
|
|
604
|
+
## Priority Tool Order
|
|
605
|
+
1. \`open_deal_room(slug, dealName)\` — before deal conversations
|
|
606
|
+
2. \`get_proactive_briefing()\` — at session start or "what should I do?"
|
|
607
|
+
3. \`get_customer_context(slug)\` — for general customer questions
|
|
608
|
+
|
|
609
|
+
## Core Workflow
|
|
610
|
+
- Customer mentioned → \`get_customer_context(slug)\`
|
|
611
|
+
- After interaction → \`log_interaction(slug, type, summary)\`
|
|
612
|
+
- Deal change → \`update_deal(slug, dealName, fields)\`
|
|
613
|
+
- Historical → \`search_customer_knowledge(slug, query)\`
|
|
614
|
+
- Revenue goal → \`pursue_goal(goal, deadline)\`
|
|
615
|
+
|
|
616
|
+
## Key v2 Tools
|
|
617
|
+
get_proactive_briefing · open_deal_room · get_relationship_graph ·
|
|
618
|
+
get_relationship_health · run_deal_agent · simulate_revenue ·
|
|
619
|
+
get_org_intelligence · pursue_goal · get_goal_status · get_playbook
|
|
620
|
+
|
|
621
|
+
## All Tools
|
|
622
|
+
get_capabilities · get_active_session · get_customer_context ·
|
|
623
|
+
search_customer_knowledge · list_customers · log_interaction · update_deal ·
|
|
624
|
+
export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
|
|
625
|
+
summarize_meeting · get_pipeline_stages · get_market_intelligence ·
|
|
626
|
+
get_relationship_graph · get_relationship_health · run_deal_agent ·
|
|
627
|
+
approve_agent_action · simulate_revenue · get_org_intelligence ·
|
|
628
|
+
get_playbook · create_playbook · list_playbooks · distill_playbook ·
|
|
629
|
+
pursue_goal · get_goal_status · register_push_subscription · get_push_status ·
|
|
630
|
+
open_deal_room · get_proactive_briefing ·
|
|
631
|
+
list_email_templates · get_email_template · draft_email ·
|
|
632
|
+
enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences ·
|
|
633
|
+
generate_quote · get_quote_status · get_booking_link ·
|
|
634
|
+
create_ticket · update_ticket · list_tickets · close_ticket ·
|
|
635
|
+
send_nps_survey · get_survey_results ·
|
|
636
|
+
search_knowledge_base · create_kb_article ·
|
|
637
|
+
backup_now · list_backups ·
|
|
638
|
+
trigger_sync · get_audit_log ·
|
|
639
|
+
define_custom_object · create_record · list_records · list_custom_objects
|
|
640
|
+
|
|
641
|
+
## Data: ${dataDir}`.trim();
|
|
642
|
+
}
|
|
643
|
+
/** Grok Build: .grok/settings.json — project-level MCP config (array format) */
|
|
644
|
+
function buildGrokSettingsJson(config) {
|
|
645
|
+
const entry = { mcpServers: [{
|
|
646
|
+
name: config.serverName,
|
|
647
|
+
transport: {
|
|
648
|
+
type: "stdio",
|
|
649
|
+
command: "node",
|
|
650
|
+
args: [config.mcpServerPath],
|
|
651
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
652
|
+
}
|
|
653
|
+
}] };
|
|
654
|
+
return JSON.stringify(entry, null, 2);
|
|
655
|
+
}
|
|
656
|
+
/** Cursor: .cursor/rules/datasynx-crm.mdc (MDC format with frontmatter) */
|
|
657
|
+
function buildCursorRulesMdc(dataDir) {
|
|
658
|
+
return `---
|
|
659
|
+
description: DatasynxOpenCRM v2 — CRM workflow rules (${TOOL_COUNT} tools)
|
|
660
|
+
globs: ["**/*"]
|
|
661
|
+
alwaysApply: true
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
# DatasynxOpenCRM v2 Rules
|
|
665
|
+
|
|
666
|
+
You have access to a local CRM via MCP tools (datasynx-opencrm, ${TOOL_COUNT} tools).
|
|
667
|
+
|
|
668
|
+
## Session Start
|
|
669
|
+
Call \`get_proactive_briefing()\` at the start of every session.
|
|
670
|
+
|
|
671
|
+
## Deal Conversations
|
|
672
|
+
Call \`open_deal_room({ slug, dealName })\` before any deal discussion.
|
|
673
|
+
|
|
674
|
+
## Mandatory Workflow
|
|
675
|
+
- Customer mentioned → \`get_customer_context(slug)\` immediately
|
|
676
|
+
- After call/meeting/email → \`log_interaction(slug, type, summary)\`
|
|
677
|
+
- Historical question → \`search_customer_knowledge(slug, query)\`
|
|
678
|
+
- Deal discussed → \`update_deal(slug, dealName, fields)\`
|
|
679
|
+
- Revenue goal → \`pursue_goal({ goal, deadline })\`
|
|
680
|
+
|
|
681
|
+
## Key v2 Tools
|
|
682
|
+
open_deal_room · get_proactive_briefing · get_relationship_graph ·
|
|
683
|
+
get_relationship_health · run_deal_agent · approve_agent_action ·
|
|
684
|
+
simulate_revenue · get_org_intelligence · get_playbook · pursue_goal
|
|
685
|
+
|
|
686
|
+
## All ${TOOL_COUNT} Tools
|
|
687
|
+
get_capabilities · get_active_session · get_customer_context ·
|
|
688
|
+
search_customer_knowledge · list_customers · log_interaction · update_deal ·
|
|
689
|
+
export_customer · update_customer_facts · get_deal_health · get_pipeline_forecast ·
|
|
690
|
+
summarize_meeting · get_pipeline_stages · get_market_intelligence ·
|
|
691
|
+
get_relationship_graph · get_relationship_health · run_deal_agent ·
|
|
692
|
+
approve_agent_action · simulate_revenue · get_org_intelligence ·
|
|
693
|
+
get_playbook · create_playbook · list_playbooks · distill_playbook ·
|
|
694
|
+
pursue_goal · get_goal_status · register_push_subscription · get_push_status ·
|
|
695
|
+
open_deal_room · get_proactive_briefing ·
|
|
696
|
+
list_email_templates · get_email_template · draft_email ·
|
|
697
|
+
enroll_in_sequence · list_sequence_enrollments · unenroll_from_sequence · list_sequences ·
|
|
698
|
+
generate_quote · get_quote_status · get_booking_link ·
|
|
699
|
+
create_ticket · update_ticket · list_tickets · close_ticket ·
|
|
700
|
+
send_nps_survey · get_survey_results ·
|
|
701
|
+
search_knowledge_base · create_kb_article
|
|
702
|
+
|
|
703
|
+
## Never
|
|
704
|
+
- Discuss a customer without loading context first
|
|
705
|
+
- Skip logging — every touchpoint matters
|
|
706
|
+
- Invent information — sync or search first
|
|
707
|
+
|
|
708
|
+
## Data: ${dataDir}`.trim();
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/setup/adapters/claude-code.ts
|
|
712
|
+
const HOME$7 = os.homedir();
|
|
713
|
+
const CLAUDE_JSON = path.join(HOME$7, ".claude.json");
|
|
714
|
+
const CLAUDE_DIR = path.join(HOME$7, ".claude");
|
|
715
|
+
var ClaudeCodeAdapter = class {
|
|
716
|
+
name = "Claude Code";
|
|
717
|
+
detect() {
|
|
718
|
+
try {
|
|
719
|
+
execSync("which claude", { stdio: "ignore" });
|
|
720
|
+
return true;
|
|
721
|
+
} catch {}
|
|
722
|
+
return fs.existsSync(CLAUDE_JSON) || fs.existsSync(CLAUDE_DIR);
|
|
723
|
+
}
|
|
724
|
+
isInstalled() {
|
|
725
|
+
if (!fs.existsSync(CLAUDE_JSON)) return false;
|
|
726
|
+
try {
|
|
727
|
+
return !!JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
|
|
728
|
+
} catch {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async install(config) {
|
|
733
|
+
const harnessFiles = [];
|
|
734
|
+
this.writeMcpConfig(config);
|
|
735
|
+
this.writeGlobalSettings();
|
|
736
|
+
const claudeMdPath = path.join(config.dataDir, "CLAUDE.md");
|
|
737
|
+
fs.writeFileSync(claudeMdPath, buildClaudeMd(config.dataDir));
|
|
738
|
+
harnessFiles.push(claudeMdPath);
|
|
739
|
+
const projectSettingsDir = path.join(config.dataDir, ".claude");
|
|
740
|
+
fs.mkdirSync(projectSettingsDir, { recursive: true });
|
|
741
|
+
const allow = [
|
|
742
|
+
"mcp__datasynx-opencrm__get_capabilities",
|
|
743
|
+
"mcp__datasynx-opencrm__get_active_session",
|
|
744
|
+
"mcp__datasynx-opencrm__get_customer_context",
|
|
745
|
+
"mcp__datasynx-opencrm__search_customer_knowledge",
|
|
746
|
+
"mcp__datasynx-opencrm__list_customers",
|
|
747
|
+
"mcp__datasynx-opencrm__log_interaction",
|
|
748
|
+
"mcp__datasynx-opencrm__update_deal",
|
|
749
|
+
"mcp__datasynx-opencrm__export_customer"
|
|
750
|
+
];
|
|
751
|
+
const projectSettings = { permissions: { allow } };
|
|
752
|
+
const settingsPath = path.join(projectSettingsDir, "settings.json");
|
|
753
|
+
fs.writeFileSync(settingsPath, JSON.stringify(projectSettings, null, 2));
|
|
754
|
+
harnessFiles.push(settingsPath);
|
|
755
|
+
return {
|
|
756
|
+
framework: this.name,
|
|
757
|
+
success: true,
|
|
758
|
+
transport: "stdio",
|
|
759
|
+
configPath: CLAUDE_JSON,
|
|
760
|
+
harnessFiles,
|
|
761
|
+
notes: `${allow.length} of ${TOOL_COUNT} MCP tools pre-allowed (common read/write); CLAUDE.md written to CRM root.`
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async uninstall() {
|
|
765
|
+
if (!fs.existsSync(CLAUDE_JSON)) return;
|
|
766
|
+
try {
|
|
767
|
+
const json = JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"));
|
|
768
|
+
const servers = json["mcpServers"];
|
|
769
|
+
if (servers) delete servers["datasynx-opencrm"];
|
|
770
|
+
fs.writeFileSync(CLAUDE_JSON, JSON.stringify(json, null, 2));
|
|
771
|
+
} catch {}
|
|
772
|
+
}
|
|
773
|
+
writeMcpConfig(config) {
|
|
774
|
+
let json = {};
|
|
775
|
+
if (fs.existsSync(CLAUDE_JSON)) try {
|
|
776
|
+
json = JSON.parse(fs.readFileSync(CLAUDE_JSON, "utf-8"));
|
|
777
|
+
} catch {}
|
|
778
|
+
if (!json["mcpServers"]) json["mcpServers"] = {};
|
|
779
|
+
json["mcpServers"][config.serverName] = {
|
|
780
|
+
type: "stdio",
|
|
781
|
+
command: process.execPath,
|
|
782
|
+
args: [config.mcpServerPath],
|
|
783
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
784
|
+
};
|
|
785
|
+
fs.mkdirSync(path.dirname(CLAUDE_JSON), { recursive: true });
|
|
786
|
+
fs.writeFileSync(CLAUDE_JSON, JSON.stringify(json, null, 2));
|
|
787
|
+
}
|
|
788
|
+
writeGlobalSettings() {
|
|
789
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
790
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
791
|
+
let settings = {};
|
|
792
|
+
if (fs.existsSync(settingsPath)) try {
|
|
793
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
794
|
+
} catch {}
|
|
795
|
+
if (!settings["permissions"]) settings["permissions"] = {};
|
|
796
|
+
const perms = settings["permissions"];
|
|
797
|
+
if (!perms["allow"]) perms["allow"] = [];
|
|
798
|
+
const allow = perms["allow"];
|
|
799
|
+
const wildcard = "mcp__datasynx-opencrm__*";
|
|
800
|
+
if (!allow.includes(wildcard)) allow.push(wildcard);
|
|
801
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region src/setup/adapters/claude-desktop.ts
|
|
806
|
+
function getDesktopConfigPath() {
|
|
807
|
+
switch (process.platform) {
|
|
808
|
+
case "darwin": return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
809
|
+
case "win32": return path.join(process.env["APPDATA"] ?? os.homedir(), "Claude", "claude_desktop_config.json");
|
|
810
|
+
default: return path.join(os.homedir(), ".config", "claude-desktop", "claude_desktop_config.json");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const DESKTOP_CONFIG = getDesktopConfigPath();
|
|
814
|
+
var ClaudeDesktopAdapter = class {
|
|
815
|
+
name = "Claude Desktop";
|
|
816
|
+
detect() {
|
|
817
|
+
return fs.existsSync(DESKTOP_CONFIG) || fs.existsSync(path.dirname(DESKTOP_CONFIG));
|
|
818
|
+
}
|
|
819
|
+
isInstalled() {
|
|
820
|
+
if (!fs.existsSync(DESKTOP_CONFIG)) return false;
|
|
821
|
+
try {
|
|
822
|
+
return !!JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
|
|
823
|
+
} catch {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async install(config) {
|
|
828
|
+
fs.mkdirSync(path.dirname(DESKTOP_CONFIG), { recursive: true });
|
|
829
|
+
let json = {};
|
|
830
|
+
if (fs.existsSync(DESKTOP_CONFIG)) try {
|
|
831
|
+
json = JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"));
|
|
832
|
+
} catch {}
|
|
833
|
+
if (!json["mcpServers"]) json["mcpServers"] = {};
|
|
834
|
+
json["mcpServers"][config.serverName] = {
|
|
835
|
+
command: process.execPath,
|
|
836
|
+
args: [config.mcpServerPath],
|
|
837
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
838
|
+
};
|
|
839
|
+
fs.writeFileSync(DESKTOP_CONFIG, JSON.stringify(json, null, 2));
|
|
840
|
+
return {
|
|
841
|
+
framework: this.name,
|
|
842
|
+
success: true,
|
|
843
|
+
transport: "stdio",
|
|
844
|
+
configPath: DESKTOP_CONFIG,
|
|
845
|
+
harnessFiles: [],
|
|
846
|
+
notes: "Restart Claude Desktop to activate the MCP server. No harness files for Desktop app."
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
async uninstall() {
|
|
850
|
+
if (!fs.existsSync(DESKTOP_CONFIG)) return;
|
|
851
|
+
try {
|
|
852
|
+
const json = JSON.parse(fs.readFileSync(DESKTOP_CONFIG, "utf-8"));
|
|
853
|
+
const servers = json["mcpServers"];
|
|
854
|
+
if (servers) delete servers["datasynx-opencrm"];
|
|
855
|
+
fs.writeFileSync(DESKTOP_CONFIG, JSON.stringify(json, null, 2));
|
|
856
|
+
} catch {}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/setup/adapters/codex.ts
|
|
861
|
+
const CODEX_DIR = path.join(os.homedir(), ".codex");
|
|
862
|
+
const CODEX_CONFIG = path.join(CODEX_DIR, "config.toml");
|
|
863
|
+
var CodexAdapter = class {
|
|
864
|
+
name = "Codex";
|
|
865
|
+
detect() {
|
|
866
|
+
try {
|
|
867
|
+
execSync("which codex", { stdio: "ignore" });
|
|
868
|
+
return true;
|
|
869
|
+
} catch {}
|
|
870
|
+
return fs.existsSync(CODEX_DIR);
|
|
871
|
+
}
|
|
872
|
+
isInstalled() {
|
|
873
|
+
if (!fs.existsSync(CODEX_CONFIG)) return false;
|
|
874
|
+
return fs.readFileSync(CODEX_CONFIG, "utf-8").includes("[mcp_servers.datasynx-opencrm]");
|
|
875
|
+
}
|
|
876
|
+
async install(config) {
|
|
877
|
+
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
878
|
+
const harnessFiles = [];
|
|
879
|
+
if (!this.isInstalled()) {
|
|
880
|
+
const block = [
|
|
881
|
+
``,
|
|
882
|
+
`[mcp_servers.${config.serverName}]`,
|
|
883
|
+
`command = ${JSON.stringify(process.execPath)}`,
|
|
884
|
+
`args = [${JSON.stringify(config.mcpServerPath)}]`,
|
|
885
|
+
`env = { DXCRM_DATA_DIR = ${JSON.stringify(config.dataDir)} }`,
|
|
886
|
+
`startup_timeout_sec = 30`,
|
|
887
|
+
`tool_timeout_sec = 120`,
|
|
888
|
+
`enabled = true`,
|
|
889
|
+
``
|
|
890
|
+
].join("\n");
|
|
891
|
+
fs.appendFileSync(CODEX_CONFIG, block, "utf-8");
|
|
892
|
+
}
|
|
893
|
+
const agentsPath = path.join(config.dataDir, "AGENTS.md");
|
|
894
|
+
if (!fs.existsSync(agentsPath)) {
|
|
895
|
+
fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
|
|
896
|
+
harnessFiles.push(agentsPath);
|
|
897
|
+
} else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
|
|
898
|
+
fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
|
|
899
|
+
harnessFiles.push(agentsPath + " (appended)");
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
framework: this.name,
|
|
903
|
+
success: true,
|
|
904
|
+
transport: "stdio",
|
|
905
|
+
configPath: CODEX_CONFIG,
|
|
906
|
+
harnessFiles,
|
|
907
|
+
notes: `startup_timeout_sec=30, tool_timeout_sec=120. AGENTS.md written to CRM root.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async uninstall() {
|
|
911
|
+
if (!fs.existsSync(CODEX_CONFIG)) return;
|
|
912
|
+
const cleaned = fs.readFileSync(CODEX_CONFIG, "utf-8").replace(/\n?\[mcp_servers\.datasynx-opencrm\][^\[]*/, "");
|
|
913
|
+
fs.writeFileSync(CODEX_CONFIG, cleaned);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/setup/adapters/openclaw.ts
|
|
918
|
+
const HOME$6 = os.homedir();
|
|
919
|
+
const OPENCLAW_DIR = path.join(HOME$6, ".openclaw");
|
|
920
|
+
const OPENCLAW_JSON = path.join(OPENCLAW_DIR, "openclaw.json");
|
|
921
|
+
const OPENCLAW_WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
|
|
922
|
+
var OpenClawAdapter = class {
|
|
923
|
+
name = "OpenClaw";
|
|
924
|
+
detect() {
|
|
925
|
+
try {
|
|
926
|
+
execSync("which openclaw", { stdio: "ignore" });
|
|
927
|
+
return true;
|
|
928
|
+
} catch {}
|
|
929
|
+
return fs.existsSync(OPENCLAW_DIR) || fs.existsSync(OPENCLAW_JSON);
|
|
930
|
+
}
|
|
931
|
+
isInstalled() {
|
|
932
|
+
if (!fs.existsSync(OPENCLAW_JSON)) return false;
|
|
933
|
+
try {
|
|
934
|
+
return !!JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"))["mcpServers"]?.["datasynx-opencrm"];
|
|
935
|
+
} catch {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async install(config) {
|
|
940
|
+
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
941
|
+
fs.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
|
|
942
|
+
const harnessFiles = [];
|
|
943
|
+
let json = {};
|
|
944
|
+
if (fs.existsSync(OPENCLAW_JSON)) try {
|
|
945
|
+
json = JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"));
|
|
946
|
+
} catch {}
|
|
947
|
+
if (!json["mcpServers"]) json["mcpServers"] = {};
|
|
948
|
+
const servers = json["mcpServers"];
|
|
949
|
+
servers[config.serverName] = {
|
|
950
|
+
command: process.execPath,
|
|
951
|
+
args: [config.mcpServerPath],
|
|
952
|
+
transport: "stdio",
|
|
953
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
954
|
+
};
|
|
955
|
+
servers[`${config.serverName}-http`] = {
|
|
956
|
+
url: `http://localhost:${config.httpPort}/mcp`,
|
|
957
|
+
transport: "streamable-http",
|
|
958
|
+
enabled: false
|
|
959
|
+
};
|
|
960
|
+
fs.writeFileSync(OPENCLAW_JSON, JSON.stringify(json, null, 2));
|
|
961
|
+
const soulPath = path.join(OPENCLAW_WORKSPACE, "SOUL.md");
|
|
962
|
+
if (!fs.existsSync(soulPath)) {
|
|
963
|
+
fs.writeFileSync(soulPath, buildSoulMd("openclaw"));
|
|
964
|
+
harnessFiles.push(soulPath);
|
|
965
|
+
} else if (!fs.readFileSync(soulPath, "utf-8").includes("DatasynxOpenCRM")) {
|
|
966
|
+
fs.appendFileSync(soulPath, "\n\n---\n\n" + buildCrmSoulAppend());
|
|
967
|
+
harnessFiles.push(soulPath + " (appended)");
|
|
968
|
+
}
|
|
969
|
+
const agentsPath = path.join(OPENCLAW_WORKSPACE, "AGENTS.md");
|
|
970
|
+
if (!fs.existsSync(agentsPath)) {
|
|
971
|
+
fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
|
|
972
|
+
harnessFiles.push(agentsPath);
|
|
973
|
+
} else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
|
|
974
|
+
fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
|
|
975
|
+
harnessFiles.push(agentsPath + " (appended)");
|
|
976
|
+
}
|
|
977
|
+
const toolsPath = path.join(OPENCLAW_WORKSPACE, "TOOLS.md");
|
|
978
|
+
const toolsContent = buildOpenClawToolsMd();
|
|
979
|
+
if (!fs.existsSync(toolsPath)) fs.writeFileSync(toolsPath, toolsContent);
|
|
980
|
+
else if (!fs.readFileSync(toolsPath, "utf-8").includes("datasynx-opencrm")) fs.appendFileSync(toolsPath, "\n\n" + toolsContent);
|
|
981
|
+
harnessFiles.push(toolsPath);
|
|
982
|
+
return {
|
|
983
|
+
framework: this.name,
|
|
984
|
+
success: true,
|
|
985
|
+
transport: "stdio",
|
|
986
|
+
configPath: OPENCLAW_JSON,
|
|
987
|
+
harnessFiles,
|
|
988
|
+
notes: "Config hot-reloaded by Gateway. SOUL.md + AGENTS.md + TOOLS.md written to workspace."
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
async uninstall() {
|
|
992
|
+
if (!fs.existsSync(OPENCLAW_JSON)) return;
|
|
993
|
+
try {
|
|
994
|
+
const json = JSON.parse(fs.readFileSync(OPENCLAW_JSON, "utf-8"));
|
|
995
|
+
const servers = json["mcpServers"];
|
|
996
|
+
if (servers) {
|
|
997
|
+
delete servers["datasynx-opencrm"];
|
|
998
|
+
delete servers["datasynx-opencrm-http"];
|
|
999
|
+
}
|
|
1000
|
+
fs.writeFileSync(OPENCLAW_JSON, JSON.stringify(json, null, 2));
|
|
1001
|
+
} catch {}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
function buildCrmSoulAppend() {
|
|
1005
|
+
return `## CRM Integration
|
|
1006
|
+
I have access to DatasynxOpenCRM. I always load customer context before discussing customers.
|
|
1007
|
+
I log every interaction without being asked. I cite sources when referencing customer data.`;
|
|
1008
|
+
}
|
|
1009
|
+
function buildOpenClawToolsMd() {
|
|
1010
|
+
return `## datasynx-opencrm MCP Tools
|
|
1011
|
+
- get_customer_context(slug) — load full customer briefing
|
|
1012
|
+
- search_customer_knowledge(slug, query) — search emails + transcripts
|
|
1013
|
+
- list_customers() — pipeline overview
|
|
1014
|
+
- log_interaction(slug, type, summary) — write to CRM
|
|
1015
|
+
- update_deal(slug, dealName, fields) — pipeline update
|
|
1016
|
+
- get_capabilities() — full reference`;
|
|
1017
|
+
}
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/setup/adapters/hermes.ts
|
|
1020
|
+
const HOME$5 = os.homedir();
|
|
1021
|
+
const HERMES_HOME = process.env["HERMES_HOME"] ?? path.join(HOME$5, ".hermes");
|
|
1022
|
+
const HERMES_CONFIG = path.join(HERMES_HOME, "config.yaml");
|
|
1023
|
+
const HERMES_SOUL = path.join(HERMES_HOME, "SOUL.md");
|
|
1024
|
+
const HERMES_SKILLS = path.join(HERMES_HOME, "skills");
|
|
1025
|
+
var HermesAdapter = class {
|
|
1026
|
+
name = "Hermes Agent";
|
|
1027
|
+
detect() {
|
|
1028
|
+
try {
|
|
1029
|
+
execSync("which hermes", { stdio: "ignore" });
|
|
1030
|
+
return true;
|
|
1031
|
+
} catch {}
|
|
1032
|
+
return fs.existsSync(HERMES_HOME) || fs.existsSync(HERMES_CONFIG);
|
|
1033
|
+
}
|
|
1034
|
+
isInstalled() {
|
|
1035
|
+
if (!fs.existsSync(HERMES_CONFIG)) return false;
|
|
1036
|
+
return fs.readFileSync(HERMES_CONFIG, "utf-8").includes("datasynx");
|
|
1037
|
+
}
|
|
1038
|
+
async install(config) {
|
|
1039
|
+
fs.mkdirSync(HERMES_HOME, { recursive: true });
|
|
1040
|
+
fs.mkdirSync(HERMES_SKILLS, { recursive: true });
|
|
1041
|
+
const harnessFiles = [];
|
|
1042
|
+
this.writeMcpConfig(config);
|
|
1043
|
+
if (!fs.existsSync(HERMES_SOUL)) {
|
|
1044
|
+
fs.writeFileSync(HERMES_SOUL, buildHermesSoulMd("hermes"));
|
|
1045
|
+
harnessFiles.push(HERMES_SOUL);
|
|
1046
|
+
} else if (!fs.readFileSync(HERMES_SOUL, "utf-8").includes("DatasynxOpenCRM")) {
|
|
1047
|
+
fs.appendFileSync(HERMES_SOUL, "\n\n---\n\n## CRM Integration\nI have access to DatasynxOpenCRM MCP tools.\nI always load customer context before discussing customers.\nI log every interaction automatically via log_interaction().");
|
|
1048
|
+
harnessFiles.push(HERMES_SOUL + " (appended)");
|
|
1049
|
+
}
|
|
1050
|
+
const skillPath = path.join(HERMES_SKILLS, "datasynx-crm.md");
|
|
1051
|
+
fs.writeFileSync(skillPath, buildHermesSkillMd());
|
|
1052
|
+
harnessFiles.push(skillPath);
|
|
1053
|
+
return {
|
|
1054
|
+
framework: this.name,
|
|
1055
|
+
success: true,
|
|
1056
|
+
transport: "stdio",
|
|
1057
|
+
configPath: HERMES_CONFIG,
|
|
1058
|
+
harnessFiles,
|
|
1059
|
+
notes: "SOUL.md updated (Slot #1 system prompt). Skill registered in ~/.hermes/skills/. Server name uses underscore: datasynx_opencrm."
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
async uninstall() {
|
|
1063
|
+
if (!fs.existsSync(HERMES_CONFIG)) return;
|
|
1064
|
+
const cleaned = fs.readFileSync(HERMES_CONFIG, "utf-8").replace(/\n datasynx[_-]opencrm:[\s\S]*?(?=\n \w|\n[a-z]|$)/, "");
|
|
1065
|
+
fs.writeFileSync(HERMES_CONFIG, cleaned);
|
|
1066
|
+
const skillPath = path.join(HERMES_SKILLS, "datasynx-crm.md");
|
|
1067
|
+
if (fs.existsSync(skillPath)) fs.unlinkSync(skillPath);
|
|
1068
|
+
}
|
|
1069
|
+
writeMcpConfig(config) {
|
|
1070
|
+
let content = fs.existsSync(HERMES_CONFIG) ? fs.readFileSync(HERMES_CONFIG, "utf-8") : "";
|
|
1071
|
+
if (content.includes("datasynx")) return;
|
|
1072
|
+
if (content.includes("mcp_servers:")) {
|
|
1073
|
+
content = content.replace("mcp_servers:", [
|
|
1074
|
+
"mcp_servers:",
|
|
1075
|
+
" datasynx_opencrm:",
|
|
1076
|
+
` command: ${JSON.stringify(process.execPath)}`,
|
|
1077
|
+
` args: [${JSON.stringify(config.mcpServerPath)}]`,
|
|
1078
|
+
` env:`,
|
|
1079
|
+
` DXCRM_DATA_DIR: ${JSON.stringify(config.dataDir)}`,
|
|
1080
|
+
` timeout: 120`,
|
|
1081
|
+
` connect_timeout: 30`,
|
|
1082
|
+
` enabled: true`,
|
|
1083
|
+
` tools:`,
|
|
1084
|
+
` include: [get_capabilities, get_active_session, get_customer_context, search_customer_knowledge, list_customers, log_interaction, update_deal, export_customer]`,
|
|
1085
|
+
` prompts: false`,
|
|
1086
|
+
` resources: false`
|
|
1087
|
+
].join("\n"));
|
|
1088
|
+
fs.writeFileSync(HERMES_CONFIG, content);
|
|
1089
|
+
} else {
|
|
1090
|
+
const mcpBlock = [
|
|
1091
|
+
``,
|
|
1092
|
+
`# DatasynxOpenCRM MCP Server (added by dxcrm init)`,
|
|
1093
|
+
`mcp_servers:`,
|
|
1094
|
+
` datasynx_opencrm:`,
|
|
1095
|
+
` command: ${JSON.stringify(process.execPath)}`,
|
|
1096
|
+
` args: [${JSON.stringify(config.mcpServerPath)}]`,
|
|
1097
|
+
` env:`,
|
|
1098
|
+
` DXCRM_DATA_DIR: ${JSON.stringify(config.dataDir)}`,
|
|
1099
|
+
` timeout: 120`,
|
|
1100
|
+
` connect_timeout: 30`,
|
|
1101
|
+
` enabled: true`,
|
|
1102
|
+
` tools:`,
|
|
1103
|
+
` include:`,
|
|
1104
|
+
` - get_capabilities`,
|
|
1105
|
+
` - get_active_session`,
|
|
1106
|
+
` - get_customer_context`,
|
|
1107
|
+
` - search_customer_knowledge`,
|
|
1108
|
+
` - list_customers`,
|
|
1109
|
+
` - log_interaction`,
|
|
1110
|
+
` - update_deal`,
|
|
1111
|
+
` - export_customer`,
|
|
1112
|
+
` prompts: false`,
|
|
1113
|
+
` resources: false`,
|
|
1114
|
+
``
|
|
1115
|
+
].join("\n");
|
|
1116
|
+
fs.appendFileSync(HERMES_CONFIG, mcpBlock);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/setup/adapters/antigravity.ts
|
|
1122
|
+
const HOME$4 = os.homedir();
|
|
1123
|
+
const AGY_BIN_UNIX = path.join(HOME$4, ".local", "bin", "agy");
|
|
1124
|
+
const GEMINI_CONFIG_DIR = path.join(HOME$4, ".gemini", "config");
|
|
1125
|
+
const SHARED_MCP_CONFIG = path.join(GEMINI_CONFIG_DIR, "mcp_config.json");
|
|
1126
|
+
const AGY_DIR = path.join(HOME$4, ".gemini", "antigravity");
|
|
1127
|
+
const AGY_MCP_CONFIG = path.join(AGY_DIR, "mcp_config.json");
|
|
1128
|
+
const GEMINI_GLOBAL_MD = path.join(HOME$4, ".gemini", "GEMINI.md");
|
|
1129
|
+
const AGY_SKILLS_DIR = path.join(HOME$4, ".gemini", "antigravity-cli", "skills");
|
|
1130
|
+
var AntigravityAdapter = class {
|
|
1131
|
+
name = "Antigravity CLI";
|
|
1132
|
+
detect() {
|
|
1133
|
+
try {
|
|
1134
|
+
execSync("which agy", { stdio: "ignore" });
|
|
1135
|
+
return true;
|
|
1136
|
+
} catch {}
|
|
1137
|
+
if (fs.existsSync(AGY_BIN_UNIX)) return true;
|
|
1138
|
+
return fs.existsSync(path.join(HOME$4, ".gemini"));
|
|
1139
|
+
}
|
|
1140
|
+
isInstalled() {
|
|
1141
|
+
for (const configPath of [SHARED_MCP_CONFIG, AGY_MCP_CONFIG]) {
|
|
1142
|
+
if (!fs.existsSync(configPath)) continue;
|
|
1143
|
+
try {
|
|
1144
|
+
if (JSON.parse(fs.readFileSync(configPath, "utf-8"))?.mcpServers?.["datasynx-opencrm"]) return true;
|
|
1145
|
+
} catch {}
|
|
1146
|
+
}
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
async install(config) {
|
|
1150
|
+
const harnessFiles = [];
|
|
1151
|
+
fs.mkdirSync(GEMINI_CONFIG_DIR, { recursive: true });
|
|
1152
|
+
this.writeMcpEntry(SHARED_MCP_CONFIG, config);
|
|
1153
|
+
if (!fs.existsSync(GEMINI_GLOBAL_MD)) {
|
|
1154
|
+
fs.mkdirSync(path.dirname(GEMINI_GLOBAL_MD), { recursive: true });
|
|
1155
|
+
fs.writeFileSync(GEMINI_GLOBAL_MD, buildAgyGeminiMd(config.dataDir));
|
|
1156
|
+
harnessFiles.push(GEMINI_GLOBAL_MD);
|
|
1157
|
+
} else if (!fs.readFileSync(GEMINI_GLOBAL_MD, "utf-8").includes("DatasynxOpenCRM")) {
|
|
1158
|
+
fs.appendFileSync(GEMINI_GLOBAL_MD, "\n\n---\n\n" + buildAgyGeminiMdAppend());
|
|
1159
|
+
harnessFiles.push(GEMINI_GLOBAL_MD + " (appended)");
|
|
1160
|
+
}
|
|
1161
|
+
const agentsPath = path.join(config.dataDir, "AGENTS.md");
|
|
1162
|
+
fs.mkdirSync(config.dataDir, { recursive: true });
|
|
1163
|
+
if (!fs.existsSync(agentsPath)) {
|
|
1164
|
+
fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
|
|
1165
|
+
harnessFiles.push(agentsPath);
|
|
1166
|
+
} else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
|
|
1167
|
+
fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
|
|
1168
|
+
harnessFiles.push(agentsPath + " (appended)");
|
|
1169
|
+
}
|
|
1170
|
+
const skillDir = path.join(AGY_SKILLS_DIR, "datasynx-crm");
|
|
1171
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1172
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
1173
|
+
fs.writeFileSync(skillPath, buildAgySkillMd());
|
|
1174
|
+
harnessFiles.push(skillPath);
|
|
1175
|
+
return {
|
|
1176
|
+
framework: this.name,
|
|
1177
|
+
success: true,
|
|
1178
|
+
transport: "stdio",
|
|
1179
|
+
configPath: SHARED_MCP_CONFIG,
|
|
1180
|
+
harnessFiles,
|
|
1181
|
+
notes: "Shared config (~/.gemini/config/mcp_config.json) covers both CLI and IDE. Skill registered. GEMINI.md updated."
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
async uninstall() {
|
|
1185
|
+
for (const configPath of [SHARED_MCP_CONFIG, AGY_MCP_CONFIG]) {
|
|
1186
|
+
if (!fs.existsSync(configPath)) continue;
|
|
1187
|
+
try {
|
|
1188
|
+
const json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1189
|
+
if (json.mcpServers) {
|
|
1190
|
+
delete json.mcpServers["datasynx-opencrm"];
|
|
1191
|
+
delete json.mcpServers["datasynx-opencrm-http"];
|
|
1192
|
+
}
|
|
1193
|
+
fs.writeFileSync(configPath, JSON.stringify(json, null, 2));
|
|
1194
|
+
} catch {}
|
|
1195
|
+
}
|
|
1196
|
+
const skillDir = path.join(AGY_SKILLS_DIR, "datasynx-crm");
|
|
1197
|
+
if (fs.existsSync(skillDir)) fs.rmSync(skillDir, { recursive: true });
|
|
1198
|
+
}
|
|
1199
|
+
writeMcpEntry(configPath, config) {
|
|
1200
|
+
let json = { mcpServers: {} };
|
|
1201
|
+
if (fs.existsSync(configPath)) try {
|
|
1202
|
+
json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1203
|
+
if (!json.mcpServers) json.mcpServers = {};
|
|
1204
|
+
} catch {}
|
|
1205
|
+
json.mcpServers["datasynx-opencrm"] = {
|
|
1206
|
+
command: process.execPath,
|
|
1207
|
+
args: [config.mcpServerPath],
|
|
1208
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
1209
|
+
};
|
|
1210
|
+
json.mcpServers["datasynx-opencrm-http"] = { serverUrl: `http://localhost:${config.httpPort}/mcp` };
|
|
1211
|
+
fs.writeFileSync(configPath, JSON.stringify(json, null, 2));
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
function buildAgyGeminiMdAppend() {
|
|
1215
|
+
return `## DatasynxOpenCRM
|
|
1216
|
+
CRM MCP tools available: get_customer_context, search_customer_knowledge,
|
|
1217
|
+
list_customers, log_interaction, update_deal. Always load context first.`;
|
|
1218
|
+
}
|
|
1219
|
+
//#endregion
|
|
1220
|
+
//#region src/setup/adapters/cursor.ts
|
|
1221
|
+
const HOME$3 = os.homedir();
|
|
1222
|
+
const CURSOR_DIR = path.join(HOME$3, ".cursor");
|
|
1223
|
+
const CURSOR_GLOBAL_MCP = path.join(CURSOR_DIR, "mcp.json");
|
|
1224
|
+
var CursorAdapter = class {
|
|
1225
|
+
name = "Cursor";
|
|
1226
|
+
detect() {
|
|
1227
|
+
return fs.existsSync(CURSOR_DIR) || fs.existsSync(CURSOR_GLOBAL_MCP);
|
|
1228
|
+
}
|
|
1229
|
+
isInstalled() {
|
|
1230
|
+
if (!fs.existsSync(CURSOR_GLOBAL_MCP)) return false;
|
|
1231
|
+
try {
|
|
1232
|
+
return !!JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
|
|
1233
|
+
} catch {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
async install(config) {
|
|
1238
|
+
fs.mkdirSync(CURSOR_DIR, { recursive: true });
|
|
1239
|
+
const harnessFiles = [];
|
|
1240
|
+
let json = { mcpServers: {} };
|
|
1241
|
+
if (fs.existsSync(CURSOR_GLOBAL_MCP)) try {
|
|
1242
|
+
json = JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"));
|
|
1243
|
+
if (!json.mcpServers) json.mcpServers = {};
|
|
1244
|
+
} catch {}
|
|
1245
|
+
json.mcpServers["datasynx-opencrm"] = {
|
|
1246
|
+
command: process.execPath,
|
|
1247
|
+
args: [config.mcpServerPath],
|
|
1248
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
1249
|
+
};
|
|
1250
|
+
fs.writeFileSync(CURSOR_GLOBAL_MCP, JSON.stringify(json, null, 2));
|
|
1251
|
+
const rulesDir = path.join(config.dataDir, ".cursor", "rules");
|
|
1252
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
1253
|
+
const rulesPath = path.join(rulesDir, "datasynx-crm.mdc");
|
|
1254
|
+
if (!fs.existsSync(rulesPath)) {
|
|
1255
|
+
fs.writeFileSync(rulesPath, buildCursorRulesMdc(config.dataDir));
|
|
1256
|
+
harnessFiles.push(rulesPath);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
framework: this.name,
|
|
1260
|
+
success: true,
|
|
1261
|
+
transport: "stdio",
|
|
1262
|
+
configPath: CURSOR_GLOBAL_MCP,
|
|
1263
|
+
harnessFiles,
|
|
1264
|
+
notes: "Global MCP registered. CRM rules written to .cursor/rules/. Restart Cursor to activate."
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async uninstall() {
|
|
1268
|
+
if (!fs.existsSync(CURSOR_GLOBAL_MCP)) return;
|
|
1269
|
+
try {
|
|
1270
|
+
const json = JSON.parse(fs.readFileSync(CURSOR_GLOBAL_MCP, "utf-8"));
|
|
1271
|
+
if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
|
|
1272
|
+
fs.writeFileSync(CURSOR_GLOBAL_MCP, JSON.stringify(json, null, 2));
|
|
1273
|
+
} catch {}
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/setup/adapters/windsurf.ts
|
|
1278
|
+
const HOME$2 = os.homedir();
|
|
1279
|
+
const WINDSURF_DIR = path.join(HOME$2, ".codeium", "windsurf");
|
|
1280
|
+
const WINDSURF_CONFIG = path.join(WINDSURF_DIR, "mcp_config.json");
|
|
1281
|
+
var WindsurfAdapter = class {
|
|
1282
|
+
name = "Windsurf";
|
|
1283
|
+
detect() {
|
|
1284
|
+
return fs.existsSync(WINDSURF_DIR) || fs.existsSync(WINDSURF_CONFIG);
|
|
1285
|
+
}
|
|
1286
|
+
isInstalled() {
|
|
1287
|
+
if (!fs.existsSync(WINDSURF_CONFIG)) return false;
|
|
1288
|
+
try {
|
|
1289
|
+
return !!JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async install(config) {
|
|
1295
|
+
fs.mkdirSync(WINDSURF_DIR, { recursive: true });
|
|
1296
|
+
let json = { mcpServers: {} };
|
|
1297
|
+
if (fs.existsSync(WINDSURF_CONFIG)) try {
|
|
1298
|
+
json = JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"));
|
|
1299
|
+
if (!json.mcpServers) json.mcpServers = {};
|
|
1300
|
+
} catch {}
|
|
1301
|
+
json.mcpServers["datasynx-opencrm"] = {
|
|
1302
|
+
command: process.execPath,
|
|
1303
|
+
args: [config.mcpServerPath],
|
|
1304
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
1305
|
+
};
|
|
1306
|
+
fs.writeFileSync(WINDSURF_CONFIG, JSON.stringify(json, null, 2));
|
|
1307
|
+
return {
|
|
1308
|
+
framework: this.name,
|
|
1309
|
+
success: true,
|
|
1310
|
+
transport: "stdio",
|
|
1311
|
+
configPath: WINDSURF_CONFIG,
|
|
1312
|
+
harnessFiles: [],
|
|
1313
|
+
notes: "No harness files for IDE-based tools. Restart Windsurf to activate."
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
async uninstall() {
|
|
1317
|
+
if (!fs.existsSync(WINDSURF_CONFIG)) return;
|
|
1318
|
+
try {
|
|
1319
|
+
const json = JSON.parse(fs.readFileSync(WINDSURF_CONFIG, "utf-8"));
|
|
1320
|
+
if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
|
|
1321
|
+
fs.writeFileSync(WINDSURF_CONFIG, JSON.stringify(json, null, 2));
|
|
1322
|
+
} catch {}
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/setup/adapters/cline.ts
|
|
1327
|
+
const HOME$1 = os.homedir();
|
|
1328
|
+
const CLINE_DIR = path.join(HOME$1, ".cline");
|
|
1329
|
+
const CLINE_CONFIG = path.join(CLINE_DIR, "data", "settings", "cline_mcp_settings.json");
|
|
1330
|
+
var ClineAdapter = class {
|
|
1331
|
+
name = "Cline";
|
|
1332
|
+
detect() {
|
|
1333
|
+
return fs.existsSync(CLINE_DIR) || fs.existsSync(CLINE_CONFIG);
|
|
1334
|
+
}
|
|
1335
|
+
isInstalled() {
|
|
1336
|
+
if (!fs.existsSync(CLINE_CONFIG)) return false;
|
|
1337
|
+
try {
|
|
1338
|
+
return !!JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"))?.mcpServers?.["datasynx-opencrm"];
|
|
1339
|
+
} catch {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async install(config) {
|
|
1344
|
+
fs.mkdirSync(path.dirname(CLINE_CONFIG), { recursive: true });
|
|
1345
|
+
let json = { mcpServers: {} };
|
|
1346
|
+
if (fs.existsSync(CLINE_CONFIG)) try {
|
|
1347
|
+
json = JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"));
|
|
1348
|
+
if (!json.mcpServers) json.mcpServers = {};
|
|
1349
|
+
} catch {}
|
|
1350
|
+
json.mcpServers["datasynx-opencrm"] = {
|
|
1351
|
+
command: process.execPath,
|
|
1352
|
+
args: [config.mcpServerPath],
|
|
1353
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
1354
|
+
};
|
|
1355
|
+
fs.writeFileSync(CLINE_CONFIG, JSON.stringify(json, null, 2));
|
|
1356
|
+
return {
|
|
1357
|
+
framework: this.name,
|
|
1358
|
+
success: true,
|
|
1359
|
+
transport: "stdio",
|
|
1360
|
+
configPath: CLINE_CONFIG,
|
|
1361
|
+
harnessFiles: [],
|
|
1362
|
+
notes: "Cline requires absolute paths. No harness files for VSCode extensions."
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
async uninstall() {
|
|
1366
|
+
if (!fs.existsSync(CLINE_CONFIG)) return;
|
|
1367
|
+
try {
|
|
1368
|
+
const json = JSON.parse(fs.readFileSync(CLINE_CONFIG, "utf-8"));
|
|
1369
|
+
if (json.mcpServers) delete json.mcpServers["datasynx-opencrm"];
|
|
1370
|
+
fs.writeFileSync(CLINE_CONFIG, JSON.stringify(json, null, 2));
|
|
1371
|
+
} catch {}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/setup/adapters/grok.ts
|
|
1376
|
+
const HOME = os.homedir();
|
|
1377
|
+
const GROK_DIR = path.join(HOME, ".grok");
|
|
1378
|
+
const GROK_USER_SETTINGS = path.join(GROK_DIR, "user-settings.json");
|
|
1379
|
+
var GrokAdapter = class {
|
|
1380
|
+
name = "Grok Build";
|
|
1381
|
+
detect() {
|
|
1382
|
+
try {
|
|
1383
|
+
execSync("which grok", { stdio: "ignore" });
|
|
1384
|
+
return true;
|
|
1385
|
+
} catch {}
|
|
1386
|
+
return fs.existsSync(GROK_DIR);
|
|
1387
|
+
}
|
|
1388
|
+
isInstalled() {
|
|
1389
|
+
if (!fs.existsSync(GROK_USER_SETTINGS)) return false;
|
|
1390
|
+
try {
|
|
1391
|
+
return (JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8")).mcpServers ?? []).some((s) => s.name === "datasynx-opencrm");
|
|
1392
|
+
} catch {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
async install(config) {
|
|
1397
|
+
fs.mkdirSync(GROK_DIR, { recursive: true });
|
|
1398
|
+
const harnessFiles = [];
|
|
1399
|
+
if (!this.isInstalled()) {
|
|
1400
|
+
let settings = {};
|
|
1401
|
+
if (fs.existsSync(GROK_USER_SETTINGS)) try {
|
|
1402
|
+
settings = JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8"));
|
|
1403
|
+
} catch {}
|
|
1404
|
+
if (!settings.mcpServers) settings.mcpServers = [];
|
|
1405
|
+
settings.mcpServers.push({
|
|
1406
|
+
name: config.serverName,
|
|
1407
|
+
transport: {
|
|
1408
|
+
type: "stdio",
|
|
1409
|
+
command: process.execPath,
|
|
1410
|
+
args: [config.mcpServerPath],
|
|
1411
|
+
env: { DXCRM_DATA_DIR: config.dataDir }
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
fs.writeFileSync(GROK_USER_SETTINGS, JSON.stringify(settings, null, 2));
|
|
1415
|
+
}
|
|
1416
|
+
const grokProjectDir = path.join(config.dataDir, ".grok");
|
|
1417
|
+
fs.mkdirSync(grokProjectDir, { recursive: true });
|
|
1418
|
+
const projectSettings = path.join(grokProjectDir, "settings.json");
|
|
1419
|
+
fs.writeFileSync(projectSettings, buildGrokSettingsJson(config));
|
|
1420
|
+
harnessFiles.push(projectSettings);
|
|
1421
|
+
const agentsPath = path.join(config.dataDir, "AGENTS.md");
|
|
1422
|
+
if (!fs.existsSync(agentsPath)) {
|
|
1423
|
+
fs.writeFileSync(agentsPath, buildAgentsMd(config.dataDir));
|
|
1424
|
+
harnessFiles.push(agentsPath);
|
|
1425
|
+
} else if (!fs.readFileSync(agentsPath, "utf-8").includes("DatasynxOpenCRM")) {
|
|
1426
|
+
fs.appendFileSync(agentsPath, "\n\n---\n\n" + buildAgentsMd(config.dataDir));
|
|
1427
|
+
harnessFiles.push(agentsPath + " (appended)");
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
framework: this.name,
|
|
1431
|
+
success: true,
|
|
1432
|
+
transport: "stdio",
|
|
1433
|
+
configPath: GROK_USER_SETTINGS,
|
|
1434
|
+
harnessFiles,
|
|
1435
|
+
notes: "MCP registered in ~/.grok/user-settings.json (array format). Project config written to .grok/settings.json. AGENTS.md written — Grok Build reads it natively alongside CLAUDE.md."
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
async uninstall() {
|
|
1439
|
+
if (!fs.existsSync(GROK_USER_SETTINGS)) return;
|
|
1440
|
+
try {
|
|
1441
|
+
const settings = JSON.parse(fs.readFileSync(GROK_USER_SETTINGS, "utf-8"));
|
|
1442
|
+
if (settings.mcpServers) settings.mcpServers = settings.mcpServers.filter((s) => s.name !== "datasynx-opencrm");
|
|
1443
|
+
fs.writeFileSync(GROK_USER_SETTINGS, JSON.stringify(settings, null, 2));
|
|
1444
|
+
} catch {}
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
//#endregion
|
|
1448
|
+
//#region src/setup/framework-registry.ts
|
|
1449
|
+
const FRAMEWORK_ADAPTERS = [
|
|
1450
|
+
new ClaudeCodeAdapter(),
|
|
1451
|
+
new CodexAdapter(),
|
|
1452
|
+
new GrokAdapter(),
|
|
1453
|
+
new OpenClawAdapter(),
|
|
1454
|
+
new HermesAdapter(),
|
|
1455
|
+
new AntigravityAdapter(),
|
|
1456
|
+
new CursorAdapter(),
|
|
1457
|
+
new WindsurfAdapter(),
|
|
1458
|
+
new ClineAdapter(),
|
|
1459
|
+
new ClaudeDesktopAdapter()
|
|
1460
|
+
];
|
|
1461
|
+
async function installAllDetected(config) {
|
|
1462
|
+
const results = [];
|
|
1463
|
+
for (const adapter of FRAMEWORK_ADAPTERS) {
|
|
1464
|
+
if (!adapter.detect()) continue;
|
|
1465
|
+
try {
|
|
1466
|
+
results.push(await adapter.install(config));
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
results.push({
|
|
1469
|
+
framework: adapter.name,
|
|
1470
|
+
success: false,
|
|
1471
|
+
transport: "stdio",
|
|
1472
|
+
configPath: "",
|
|
1473
|
+
harnessFiles: [],
|
|
1474
|
+
notes: err.message
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return results;
|
|
1479
|
+
}
|
|
1480
|
+
//#endregion
|
|
1481
|
+
//#region src/commands/init.ts
|
|
1482
|
+
const initCommand = new Command("init").description("Initialize CRM and configure AI frameworks").option("--team <url>", "Team mode: configure frameworks to connect to shared HTTP server at this URL (e.g. http://vm-ip:3847/mcp)").action(async (opts) => {
|
|
1483
|
+
const dataDir = process.cwd();
|
|
1484
|
+
const agenticDir = path.join(dataDir, ".agentic");
|
|
1485
|
+
fs.mkdirSync(agenticDir, { recursive: true });
|
|
1486
|
+
const configPath = path.join(agenticDir, "config.json");
|
|
1487
|
+
if (!fs.existsSync(configPath)) fs.writeFileSync(configPath, JSON.stringify({
|
|
1488
|
+
version: 1,
|
|
1489
|
+
dataDir,
|
|
1490
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1491
|
+
}, null, 2));
|
|
1492
|
+
const home = os.homedir();
|
|
1493
|
+
const transcriptPaths = [
|
|
1494
|
+
path.join(home, "Downloads", "Fireflies"),
|
|
1495
|
+
path.join(home, "Downloads", "Otter"),
|
|
1496
|
+
path.join(home, "Documents", "Zoom"),
|
|
1497
|
+
path.join(home, "Downloads", "Zoom")
|
|
1498
|
+
].filter((p) => fs.existsSync(p));
|
|
1499
|
+
const sourcesPath = path.join(agenticDir, "sources.json");
|
|
1500
|
+
if (!fs.existsSync(sourcesPath)) {
|
|
1501
|
+
const sources = {
|
|
1502
|
+
gmail: {
|
|
1503
|
+
type: "gmail",
|
|
1504
|
+
query: "",
|
|
1505
|
+
enabled: false
|
|
1506
|
+
},
|
|
1507
|
+
version: 1,
|
|
1508
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1509
|
+
};
|
|
1510
|
+
if (transcriptPaths.length > 0) sources.transcripts = {
|
|
1511
|
+
type: "transcript",
|
|
1512
|
+
paths: transcriptPaths,
|
|
1513
|
+
extensions: [".txt", ".vtt"],
|
|
1514
|
+
enabled: true
|
|
1515
|
+
};
|
|
1516
|
+
fs.writeFileSync(sourcesPath, JSON.stringify(sources, null, 2));
|
|
1517
|
+
}
|
|
1518
|
+
fs.mkdirSync(path.join(dataDir, "customers"), { recursive: true });
|
|
1519
|
+
const schemaPath = path.join(agenticDir, "schema.json");
|
|
1520
|
+
if (!fs.existsSync(schemaPath)) fs.writeFileSync(schemaPath, JSON.stringify({
|
|
1521
|
+
version: 1,
|
|
1522
|
+
description: "DatasynxOpenCRM validation schema for main_facts.md frontmatter",
|
|
1523
|
+
main_facts: {
|
|
1524
|
+
required: [
|
|
1525
|
+
"name",
|
|
1526
|
+
"relationship_stage",
|
|
1527
|
+
"created",
|
|
1528
|
+
"tags",
|
|
1529
|
+
"currency"
|
|
1530
|
+
],
|
|
1531
|
+
properties: {
|
|
1532
|
+
name: { type: "string" },
|
|
1533
|
+
relationship_stage: {
|
|
1534
|
+
type: "string",
|
|
1535
|
+
enum: [
|
|
1536
|
+
"lead",
|
|
1537
|
+
"qualified",
|
|
1538
|
+
"discovery",
|
|
1539
|
+
"proposal",
|
|
1540
|
+
"negotiation",
|
|
1541
|
+
"active",
|
|
1542
|
+
"churned",
|
|
1543
|
+
"closed"
|
|
1544
|
+
]
|
|
1545
|
+
},
|
|
1546
|
+
domain: { type: "string" },
|
|
1547
|
+
email: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
format: "email"
|
|
1550
|
+
},
|
|
1551
|
+
created: {
|
|
1552
|
+
type: "string",
|
|
1553
|
+
format: "date"
|
|
1554
|
+
},
|
|
1555
|
+
updated: {
|
|
1556
|
+
type: "string",
|
|
1557
|
+
format: "date"
|
|
1558
|
+
},
|
|
1559
|
+
tags: {
|
|
1560
|
+
type: "array",
|
|
1561
|
+
items: { type: "string" }
|
|
1562
|
+
},
|
|
1563
|
+
currency: {
|
|
1564
|
+
type: "string",
|
|
1565
|
+
enum: [
|
|
1566
|
+
"EUR",
|
|
1567
|
+
"USD",
|
|
1568
|
+
"GBP",
|
|
1569
|
+
"CHF"
|
|
1570
|
+
]
|
|
1571
|
+
},
|
|
1572
|
+
deal_value: {
|
|
1573
|
+
type: "number",
|
|
1574
|
+
minimum: 0
|
|
1575
|
+
},
|
|
1576
|
+
last_touchpoint: {
|
|
1577
|
+
type: "string",
|
|
1578
|
+
format: "date"
|
|
1579
|
+
},
|
|
1580
|
+
primary_contact: { type: "string" }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}, null, 2));
|
|
1584
|
+
console.log(info("Detecting AI frameworks..."));
|
|
1585
|
+
const mcpServerPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/mcp.js");
|
|
1586
|
+
if (opts.team) console.log(info(`Team mode: connecting frameworks to ${bold(opts.team)}`));
|
|
1587
|
+
const results = await installAllDetected({
|
|
1588
|
+
mcpServerPath,
|
|
1589
|
+
dataDir,
|
|
1590
|
+
httpPort: 3847,
|
|
1591
|
+
serverName: "datasynx-opencrm",
|
|
1592
|
+
...opts.team ? { httpUrl: opts.team } : {}
|
|
1593
|
+
});
|
|
1594
|
+
if (results.length === 0) console.log(info(" No AI frameworks detected. Configure manually: dxcrm guide --framework <name>"));
|
|
1595
|
+
else for (const r of results) if (r.success) {
|
|
1596
|
+
console.log(success(` ✓ ${r.framework} — ${r.transport} transport`));
|
|
1597
|
+
if (r.notes) console.log(` ${r.notes}`);
|
|
1598
|
+
} else console.log(error(` ✗ ${r.framework} — ${r.notes ?? "failed"}`));
|
|
1599
|
+
console.log(success(`\n✓ DatasynxOpenCRM initialized in ${bold(dataDir)}`));
|
|
1600
|
+
if (opts.team) {
|
|
1601
|
+
console.log(info(` Team server: ${opts.team}`));
|
|
1602
|
+
console.log(info(` Set identity: export DXCRM_ACTOR=<your-name>`));
|
|
1603
|
+
} else console.log(info(` Next: dxcrm create "Your Customer Name"`));
|
|
1604
|
+
});
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/commands/sync.ts
|
|
1607
|
+
const syncCommand = new Command("sync").argument("<slug>", "Customer slug to sync").description("Sync Gmail and transcripts for a customer").option("--since <date>", "Only sync emails/files after this date (YYYY-MM-DD)").option("--gmail", "Sync Gmail only").option("--transcripts", "Sync transcripts only").option("--provider <provider>", "Sync provider: gmail | microsoft | transcripts").action(async (slug, opts) => {
|
|
1608
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
1609
|
+
const customerDir = path.join(dataDir, "customers", slug);
|
|
1610
|
+
if (!fs.existsSync(customerDir)) {
|
|
1611
|
+
console.error(error(`✗ Customer '${slug}' not found. Run 'dxcrm list' to see available customers.`));
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
const sourcesPath = path.join(customerDir, "sources.json");
|
|
1615
|
+
if (!fs.existsSync(sourcesPath)) {
|
|
1616
|
+
console.error(error(`✗ No sources.json found for '${slug}'.`));
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
const sources = JSON.parse(fs.readFileSync(sourcesPath, "utf-8"));
|
|
1620
|
+
const since = opts.since ? new Date(opts.since) : /* @__PURE__ */ new Date(Date.now() - 720 * 60 * 60 * 1e3);
|
|
1621
|
+
const provider = opts.provider;
|
|
1622
|
+
const syncGmail = !opts.transcripts && provider !== "microsoft" && provider !== "transcripts" && provider !== "google-drive";
|
|
1623
|
+
const syncMicrosoft = provider === "microsoft";
|
|
1624
|
+
const syncTranscripts = !opts.gmail && provider !== "gmail" && provider !== "microsoft" && provider !== "google-drive";
|
|
1625
|
+
const syncGoogleDrive = provider === "google-drive";
|
|
1626
|
+
let totalSynced = 0;
|
|
1627
|
+
if (syncGmail && sources.gmail?.enabled && sources.gmail.query) {
|
|
1628
|
+
const tokenPath = path.join(dataDir, ".agentic", "gmail-token.json");
|
|
1629
|
+
const credPath = path.join(dataDir, ".agentic", "gmail-credentials.json");
|
|
1630
|
+
if (!fs.existsSync(tokenPath) || !fs.existsSync(credPath)) console.log(info(" Gmail: credentials not configured (run dxcrm sync --setup-gmail)"));
|
|
1631
|
+
else try {
|
|
1632
|
+
console.log(info(` Syncing Gmail for ${bold(slug)}...`));
|
|
1633
|
+
const { getGmailAuth } = await import("./gmail-auth-OComS92L.js");
|
|
1634
|
+
const { syncGmail: doGmailSync } = await import("./gmail-sync-DIaxInDT.js");
|
|
1635
|
+
const result = await doGmailSync({
|
|
1636
|
+
slug,
|
|
1637
|
+
dataDir,
|
|
1638
|
+
auth: await getGmailAuth(credPath, tokenPath),
|
|
1639
|
+
query: sources.gmail.query,
|
|
1640
|
+
since
|
|
1641
|
+
});
|
|
1642
|
+
totalSynced += result.synced;
|
|
1643
|
+
console.log(success(` ✓ Gmail: +${result.synced} synced, ${result.skipped} skipped`));
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
console.error(error(` ✗ Gmail sync failed: ${err.message}`));
|
|
1646
|
+
}
|
|
1647
|
+
} else if (syncGmail) console.log(info(" Gmail: not configured (add domain/email to sources.json)"));
|
|
1648
|
+
if (syncMicrosoft) try {
|
|
1649
|
+
console.log(info(` Syncing Microsoft Outlook for ${bold(slug)}...`));
|
|
1650
|
+
const { getMicrosoftToken } = await import("./microsoft-auth-B8_S45gh.js");
|
|
1651
|
+
const token = await getMicrosoftToken(dataDir);
|
|
1652
|
+
if (!token) console.log(info(" Microsoft: no token found (.agentic/microsoft-token.json)"));
|
|
1653
|
+
else {
|
|
1654
|
+
const { syncMicrosoft: doMsSync } = await import("./microsoft-sync-CpZVoSuq.js");
|
|
1655
|
+
const emailResult = await doMsSync({
|
|
1656
|
+
slug,
|
|
1657
|
+
dataDir,
|
|
1658
|
+
accessToken: token,
|
|
1659
|
+
since
|
|
1660
|
+
});
|
|
1661
|
+
totalSynced += emailResult.synced;
|
|
1662
|
+
console.log(success(` ✓ Microsoft Email: +${emailResult.synced} synced, ${emailResult.skipped} skipped`));
|
|
1663
|
+
const { syncMicrosoftCalendar } = await import("./microsoft-calendar-B6MMtUQK.js");
|
|
1664
|
+
const calResult = await syncMicrosoftCalendar({
|
|
1665
|
+
slug,
|
|
1666
|
+
dataDir,
|
|
1667
|
+
accessToken: token,
|
|
1668
|
+
since
|
|
1669
|
+
});
|
|
1670
|
+
totalSynced += calResult.synced;
|
|
1671
|
+
console.log(success(` ✓ Microsoft Calendar: +${calResult.synced} synced, ${calResult.skipped} skipped`));
|
|
1672
|
+
}
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
console.error(error(` ✗ Microsoft sync failed: ${err.message}`));
|
|
1675
|
+
}
|
|
1676
|
+
if (syncTranscripts) {
|
|
1677
|
+
const agenticSourcesPath = path.join(dataDir, ".agentic", "sources.json");
|
|
1678
|
+
if (fs.existsSync(agenticSourcesPath)) try {
|
|
1679
|
+
const agenticSources = JSON.parse(fs.readFileSync(agenticSourcesPath, "utf-8"));
|
|
1680
|
+
if (agenticSources.transcripts?.enabled && agenticSources.transcripts.paths?.length) {
|
|
1681
|
+
const { processTranscriptFile } = await import("./transcript-watcher-CL2QUygI.js");
|
|
1682
|
+
const exts = agenticSources.transcripts.extensions ?? [".txt", ".vtt"];
|
|
1683
|
+
let transcriptSynced = 0;
|
|
1684
|
+
for (const watchPath of agenticSources.transcripts.paths) {
|
|
1685
|
+
if (!fs.existsSync(watchPath)) continue;
|
|
1686
|
+
const files = fs.readdirSync(watchPath).filter((f) => exts.some((ext) => f.endsWith(ext))).map((f) => path.join(watchPath, f));
|
|
1687
|
+
for (const file of files) try {
|
|
1688
|
+
await processTranscriptFile(file, slug, dataDir);
|
|
1689
|
+
transcriptSynced++;
|
|
1690
|
+
} catch {}
|
|
1691
|
+
}
|
|
1692
|
+
if (transcriptSynced > 0) {
|
|
1693
|
+
totalSynced += transcriptSynced;
|
|
1694
|
+
console.log(success(` ✓ Transcripts: +${transcriptSynced} processed`));
|
|
1695
|
+
} else console.log(info(" Transcripts: no new files"));
|
|
1696
|
+
} else console.log(info(" Transcripts: not configured"));
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
console.error(error(` ✗ Transcript sync failed: ${err.message}`));
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (syncGoogleDrive) {
|
|
1702
|
+
const tokenPath = path.join(dataDir, ".agentic", "google-token.json");
|
|
1703
|
+
if (!fs.existsSync(tokenPath)) console.log(info(" Google Drive: token not configured (.agentic/google-token.json)"));
|
|
1704
|
+
else try {
|
|
1705
|
+
const tokenData = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
|
|
1706
|
+
const accessToken = tokenData.accessToken ?? tokenData.access_token;
|
|
1707
|
+
if (!accessToken) console.log(info(" Google Drive: accessToken not found in token file"));
|
|
1708
|
+
else {
|
|
1709
|
+
console.log(info(` Syncing Google Drive for ${bold(slug)}...`));
|
|
1710
|
+
const { syncGoogleDriveFiles } = await import("./google-drive-sync-DEPcqFca.js");
|
|
1711
|
+
const result = await syncGoogleDriveFiles({
|
|
1712
|
+
slug,
|
|
1713
|
+
dataDir,
|
|
1714
|
+
accessToken,
|
|
1715
|
+
customerName: slug
|
|
1716
|
+
});
|
|
1717
|
+
totalSynced += result.synced;
|
|
1718
|
+
if (result.errors.length > 0) for (const err of result.errors) console.error(error(` ✗ Google Drive: ${err}`));
|
|
1719
|
+
console.log(success(` ✓ Google Drive: +${result.synced} synced, ${result.skipped} skipped`));
|
|
1720
|
+
}
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
console.error(error(` ✗ Google Drive sync failed: ${err.message}`));
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (totalSynced > 0) console.log(success(`\n✓ Sync complete: ${bold(String(totalSynced))} new interactions for ${bold(slug)}`));
|
|
1726
|
+
else console.log(info(`\n✓ Sync complete: no new interactions for ${bold(slug)}`));
|
|
1727
|
+
});
|
|
1728
|
+
//#endregion
|
|
1729
|
+
//#region src/commands/daemon.ts
|
|
1730
|
+
function getPidFile$1() {
|
|
1731
|
+
return path.join(process.cwd(), ".agentic", "daemon.pid");
|
|
1732
|
+
}
|
|
1733
|
+
const daemonCommand = new Command("daemon");
|
|
1734
|
+
daemonCommand.command("start").action(async () => {
|
|
1735
|
+
const pidFile = getPidFile$1();
|
|
1736
|
+
if (fs.existsSync(pidFile)) {
|
|
1737
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
|
|
1738
|
+
try {
|
|
1739
|
+
process.kill(pid, 0);
|
|
1740
|
+
console.log(info(`Daemon already running (PID ${pid})`));
|
|
1741
|
+
return;
|
|
1742
|
+
} catch {}
|
|
1743
|
+
}
|
|
1744
|
+
const workerPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/daemon/worker.js");
|
|
1745
|
+
const child = spawn(process.execPath, [workerPath], {
|
|
1746
|
+
detached: true,
|
|
1747
|
+
stdio: "ignore"
|
|
1748
|
+
});
|
|
1749
|
+
child.unref();
|
|
1750
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
1751
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
1752
|
+
console.log(success(`✓ Daemon started (PID ${child.pid})`));
|
|
1753
|
+
});
|
|
1754
|
+
daemonCommand.command("stop").action(() => {
|
|
1755
|
+
const pidFile = getPidFile$1();
|
|
1756
|
+
if (!fs.existsSync(pidFile)) {
|
|
1757
|
+
console.log(info("Daemon not running."));
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
|
|
1761
|
+
try {
|
|
1762
|
+
process.kill(pid, "SIGTERM");
|
|
1763
|
+
fs.unlinkSync(pidFile);
|
|
1764
|
+
console.log(success("✓ Daemon stopped."));
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
console.error(error(`✗ ${err.message}`));
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
daemonCommand.command("status").action(() => {
|
|
1770
|
+
const pidFile = getPidFile$1();
|
|
1771
|
+
if (!fs.existsSync(pidFile)) {
|
|
1772
|
+
console.log(info("Daemon: not running."));
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10);
|
|
1776
|
+
try {
|
|
1777
|
+
process.kill(pid, 0);
|
|
1778
|
+
console.log(success(`Daemon: running (PID ${pid})`));
|
|
1779
|
+
} catch {
|
|
1780
|
+
console.log(info("Daemon: stopped (stale PID file)."));
|
|
1781
|
+
fs.unlinkSync(pidFile);
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
//#endregion
|
|
1785
|
+
//#region src/commands/status.ts
|
|
1786
|
+
function formatAge(isoString) {
|
|
1787
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
1788
|
+
const mins = Math.floor(diff / 6e4);
|
|
1789
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1790
|
+
const hours = Math.floor(mins / 60);
|
|
1791
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1792
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
1793
|
+
}
|
|
1794
|
+
function checkDaemon(dataDir) {
|
|
1795
|
+
const pidFile = path.join(dataDir, ".agentic", "daemon.pid");
|
|
1796
|
+
if (!fs.existsSync(pidFile)) return { running: false };
|
|
1797
|
+
try {
|
|
1798
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
1799
|
+
if (isNaN(pid)) return { running: false };
|
|
1800
|
+
process.kill(pid, 0);
|
|
1801
|
+
return {
|
|
1802
|
+
running: true,
|
|
1803
|
+
pid
|
|
1804
|
+
};
|
|
1805
|
+
} catch {
|
|
1806
|
+
return { running: false };
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async function fetchTeamSessions(serverUrl) {
|
|
1810
|
+
try {
|
|
1811
|
+
const res = await fetch(`${serverUrl.replace(/\/$/, "")}/sessions`, { signal: AbortSignal.timeout(3e3) });
|
|
1812
|
+
if (!res.ok) return null;
|
|
1813
|
+
return (await res.json()).sessions ?? null;
|
|
1814
|
+
} catch {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function runStatus(opts, dataDir) {
|
|
1819
|
+
const dir = dataDir ?? process.cwd();
|
|
1820
|
+
if (opts.unmatched) {
|
|
1821
|
+
const unmatched = readUnmatched(dir);
|
|
1822
|
+
const sep = "─".repeat(37);
|
|
1823
|
+
console.log(sep);
|
|
1824
|
+
console.log(bold(" Unmatched Transcripts"));
|
|
1825
|
+
console.log(sep);
|
|
1826
|
+
if (unmatched.length === 0) console.log(info(" No unmatched transcripts."));
|
|
1827
|
+
else for (const entry of unmatched) console.log(` ${entry.filePath} ${entry.reason} ${entry.addedAt}`);
|
|
1828
|
+
console.log(sep);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const daemon = checkDaemon(dir);
|
|
1832
|
+
const slugs = listCustomerSlugs(dir);
|
|
1833
|
+
const syncState = readSyncState(dir);
|
|
1834
|
+
const unmatched = readUnmatched(dir);
|
|
1835
|
+
const sep = "─".repeat(37);
|
|
1836
|
+
console.log(sep);
|
|
1837
|
+
console.log(bold(" DatasynxOpenCRM Status"));
|
|
1838
|
+
console.log(sep);
|
|
1839
|
+
const daemonLine = daemon.running ? success(`running (PID ${daemon.pid})`) : error("not running");
|
|
1840
|
+
console.log(` Daemon: ${daemonLine}`);
|
|
1841
|
+
console.log(` Customers: ${slugs.length} active`);
|
|
1842
|
+
const session = getSession() ?? readAllSessions(dir)[0] ?? null;
|
|
1843
|
+
if (session) {
|
|
1844
|
+
const ownerPart = session.owner ? ` [${session.owner}]` : "";
|
|
1845
|
+
console.log(` Session: ${session.customerName} (${session.customerSlug})${ownerPart}`);
|
|
1846
|
+
} else console.log(` Session: none`);
|
|
1847
|
+
if (slugs.length > 0) {
|
|
1848
|
+
console.log(` Syncs:`);
|
|
1849
|
+
for (const slug of slugs) {
|
|
1850
|
+
const state = syncState[slug];
|
|
1851
|
+
const ageStr = state?.lastGmailSync ? `Gmail ${formatAge(state.lastGmailSync)}` : "no sync yet";
|
|
1852
|
+
console.log(` ${slug}: ${ageStr}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const unmatchedCount = unmatched.length;
|
|
1856
|
+
if (unmatchedCount > 0) console.log(` Unmatched: ${unmatchedCount} Transcript${unmatchedCount !== 1 ? "s" : ""} (dxcrm status --unmatched)`);
|
|
1857
|
+
else console.log(` Unmatched: 0 Transcripts`);
|
|
1858
|
+
const serverUrl = opts.team ?? process.env["DXCRM_SERVER_URL"];
|
|
1859
|
+
if (serverUrl) {
|
|
1860
|
+
const teamSessions = await fetchTeamSessions(serverUrl);
|
|
1861
|
+
if (teamSessions && teamSessions.length > 0) {
|
|
1862
|
+
console.log(bold("\n Team overview:"));
|
|
1863
|
+
for (const s of teamSessions) {
|
|
1864
|
+
const ownerPart = s.owner ? `${s.owner}` : "anonymous";
|
|
1865
|
+
console.log(info(` ${ownerPart.padEnd(15)} → ${s.customerName} (${s.customerSlug})`));
|
|
1866
|
+
}
|
|
1867
|
+
} else if (teamSessions !== null) console.log(info(" Team: no active sessions"));
|
|
1868
|
+
else console.log(info(` Team: server unreachable (${serverUrl})`));
|
|
1869
|
+
}
|
|
1870
|
+
console.log(sep);
|
|
1871
|
+
}
|
|
1872
|
+
const statusCommand = new Command("status").description("Show CRM status: daemon, sync state, customer counts").option("--unmatched", "Show unmatched transcript queue").option("--team <url>", "Show team sessions from HTTP server (or set DXCRM_SERVER_URL)").action((opts) => runStatus(opts, process.env["DXCRM_DATA_DIR"] ?? process.cwd()));
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/commands/agent.ts
|
|
1875
|
+
function agentsDir(dataDir) {
|
|
1876
|
+
return path.join(dataDir, ".agentic", "agents");
|
|
1877
|
+
}
|
|
1878
|
+
function agentConfigPath(dataDir, slug) {
|
|
1879
|
+
return path.join(agentsDir(dataDir), `${slug}.agent.json`);
|
|
1880
|
+
}
|
|
1881
|
+
async function runAgentSpawn(slug, opts, dataDir) {
|
|
1882
|
+
const dir = dataDir ?? process.cwd();
|
|
1883
|
+
const customerDir = path.join(dir, "customers", slug);
|
|
1884
|
+
if (!fs.existsSync(customerDir)) {
|
|
1885
|
+
console.error(error(`✗ Customer '${slug}' not found. Run 'dxcrm list' to see available customers.`));
|
|
1886
|
+
process.exit(1);
|
|
1887
|
+
}
|
|
1888
|
+
const channel = opts.channel ?? "telegram";
|
|
1889
|
+
const wakeOn = opts.wakeOnEmail ? ["email"] : ["email"];
|
|
1890
|
+
const config = AgentConfigSchema.parse({
|
|
1891
|
+
slug,
|
|
1892
|
+
channel,
|
|
1893
|
+
wakeOn,
|
|
1894
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1895
|
+
lastWake: null,
|
|
1896
|
+
...opts.chatId !== void 0 ? { telegramChatId: opts.chatId } : {}
|
|
1897
|
+
});
|
|
1898
|
+
fs.mkdirSync(agentsDir(dir), { recursive: true });
|
|
1899
|
+
fs.writeFileSync(agentConfigPath(dir, slug), JSON.stringify(config, null, 2), "utf-8");
|
|
1900
|
+
console.log(success(`✓ Agent spawned: ${bold(slug)}`));
|
|
1901
|
+
console.log(info(` Channel: ${channel}`));
|
|
1902
|
+
console.log(info(` Wake on: ${wakeOn.join(", ")}`));
|
|
1903
|
+
console.log(info(` Config: .agentic/agents/${slug}.agent.json`));
|
|
1904
|
+
if (channel === "telegram" && !process.env["TELEGRAM_BOT_TOKEN"]) console.log(info(`\n Note: Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars to enable Telegram messages.`));
|
|
1905
|
+
}
|
|
1906
|
+
async function runAgentStatus(dataDir) {
|
|
1907
|
+
const dir2 = agentsDir(dataDir ?? process.cwd());
|
|
1908
|
+
if (!fs.existsSync(dir2)) {
|
|
1909
|
+
console.log(info("No agents configured. Run: dxcrm agent spawn <slug> --channel telegram"));
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const files = fs.readdirSync(dir2).filter((f) => f.endsWith(".agent.json"));
|
|
1913
|
+
if (files.length === 0) {
|
|
1914
|
+
console.log(info("No agents configured."));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
console.log(bold(`\n Agents (${files.length})\n`));
|
|
1918
|
+
for (const file of files) try {
|
|
1919
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir2, file), "utf-8"));
|
|
1920
|
+
const lastWake = raw.lastWake ? `last wake: ${new Date(raw.lastWake).toLocaleString()}` : "never woken";
|
|
1921
|
+
console.log(info(` ${bold(raw.slug)} — ${raw.channel} · ${raw.wakeOn.join("+")} · ${lastWake}`));
|
|
1922
|
+
} catch {
|
|
1923
|
+
console.log(info(` ${file} (malformed)`));
|
|
1924
|
+
}
|
|
1925
|
+
console.log("");
|
|
1926
|
+
}
|
|
1927
|
+
async function runAgentRemove(slug, dataDir) {
|
|
1928
|
+
const configPath = agentConfigPath(dataDir ?? process.cwd(), slug);
|
|
1929
|
+
if (!fs.existsSync(configPath)) {
|
|
1930
|
+
console.error(error(`✗ No agent config found for '${slug}'.`));
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
fs.unlinkSync(configPath);
|
|
1934
|
+
console.log(success(`✓ Agent removed: ${slug}`));
|
|
1935
|
+
}
|
|
1936
|
+
const agentCommand = new Command("agent").description("Manage per-customer agents");
|
|
1937
|
+
agentCommand.command("spawn <slug>").description("Spawn a wake-triggered agent for a customer").option("--channel <channel>", "Notification channel (telegram)", "telegram").option("--wake-on-email", "Wake agent on new email (default: on)").option("--chat-id <chatId>", "Telegram chat ID override").action((slug, opts) => runAgentSpawn(slug, opts, process.env["DXCRM_DATA_DIR"]));
|
|
1938
|
+
agentCommand.command("status").description("Show all configured agents").action(() => runAgentStatus(process.env["DXCRM_DATA_DIR"]));
|
|
1939
|
+
agentCommand.command("remove <slug>").description("Remove agent config for a customer").action((slug) => runAgentRemove(slug, process.env["DXCRM_DATA_DIR"]));
|
|
1940
|
+
//#endregion
|
|
1941
|
+
//#region src/commands/import.ts
|
|
1942
|
+
/** Map a Salesforce StageName to opencrm's fixed pipeline stage enum. */
|
|
1943
|
+
function mapSalesforceStage(stageName) {
|
|
1944
|
+
const s = (stageName ?? "").toLowerCase();
|
|
1945
|
+
if (s.includes("won")) return "won";
|
|
1946
|
+
if (s.includes("lost")) return "lost";
|
|
1947
|
+
if (s.includes("negoti")) return "negotiation";
|
|
1948
|
+
if (s.includes("propos") || s.includes("quote")) return "proposal";
|
|
1949
|
+
if (s.includes("qualif")) return "qualified";
|
|
1950
|
+
return "lead";
|
|
1951
|
+
}
|
|
1952
|
+
/** Map a Salesforce Case Status to opencrm's ticket status enum. */
|
|
1953
|
+
function mapCaseStatus(status) {
|
|
1954
|
+
const s = (status ?? "").toLowerCase();
|
|
1955
|
+
if (s.includes("closed")) return "closed";
|
|
1956
|
+
if (s.includes("resolved")) return "resolved";
|
|
1957
|
+
if (s.includes("escalat") || s.includes("wait") || s.includes("hold")) return "waiting";
|
|
1958
|
+
if (s.includes("working") || s.includes("progress")) return "in-progress";
|
|
1959
|
+
return "open";
|
|
1960
|
+
}
|
|
1961
|
+
/** Map a Salesforce Case Priority to opencrm's ticket priority enum. */
|
|
1962
|
+
function mapCasePriority(priority) {
|
|
1963
|
+
const p = (priority ?? "").toLowerCase();
|
|
1964
|
+
if (p.includes("urgent") || p.includes("critical")) return "urgent";
|
|
1965
|
+
if (p.includes("high")) return "high";
|
|
1966
|
+
if (p.includes("low")) return "low";
|
|
1967
|
+
return "normal";
|
|
1968
|
+
}
|
|
1969
|
+
function hashRow(row) {
|
|
1970
|
+
return createHash("sha256").update(JSON.stringify(row)).digest("hex").slice(0, 16);
|
|
1971
|
+
}
|
|
1972
|
+
function slugify$1(name) {
|
|
1973
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1974
|
+
}
|
|
1975
|
+
function parseCSV(content) {
|
|
1976
|
+
const lines = content.trim().split("\n");
|
|
1977
|
+
if (lines.length < 2) return [];
|
|
1978
|
+
const headers = (lines[0] ?? "").split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
|
|
1979
|
+
return lines.slice(1).map((line) => {
|
|
1980
|
+
const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
|
|
1981
|
+
const row = {};
|
|
1982
|
+
headers.forEach((h, i) => {
|
|
1983
|
+
row[h] = values[i] ?? "";
|
|
1984
|
+
});
|
|
1985
|
+
return row;
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
const IMPORT_TARGET_FIELDS = [
|
|
1989
|
+
"name",
|
|
1990
|
+
"email",
|
|
1991
|
+
"domain",
|
|
1992
|
+
"notes",
|
|
1993
|
+
"date",
|
|
1994
|
+
"activityType",
|
|
1995
|
+
"sourceId"
|
|
1996
|
+
];
|
|
1997
|
+
function ensureCustomer(dataDir, name, domain, email, dryRun) {
|
|
1998
|
+
const slug = slugify$1(name || "unknown");
|
|
1999
|
+
const customerDir = path.join(dataDir, "customers", slug);
|
|
2000
|
+
const mainFactsPath = path.join(customerDir, "main_facts.md");
|
|
2001
|
+
if (fs.existsSync(mainFactsPath)) return {
|
|
2002
|
+
slug,
|
|
2003
|
+
created: false
|
|
2004
|
+
};
|
|
2005
|
+
if (dryRun) return {
|
|
2006
|
+
slug,
|
|
2007
|
+
created: true
|
|
2008
|
+
};
|
|
2009
|
+
fs.mkdirSync(customerDir, { recursive: true });
|
|
2010
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2011
|
+
const frontmatter = [
|
|
2012
|
+
"---",
|
|
2013
|
+
`name: ${name}`,
|
|
2014
|
+
domain ? `domain: ${domain}` : null,
|
|
2015
|
+
email ? `email: ${email}` : null,
|
|
2016
|
+
"relationship_stage: prospect",
|
|
2017
|
+
`created: ${today}`,
|
|
2018
|
+
`updated: ${today}`,
|
|
2019
|
+
`last_touchpoint: ${today}`,
|
|
2020
|
+
"tags: []",
|
|
2021
|
+
"---"
|
|
2022
|
+
].filter(Boolean).join("\n");
|
|
2023
|
+
fs.writeFileSync(mainFactsPath, `${frontmatter}\n\n# Customer: ${name}\n`, "utf-8");
|
|
2024
|
+
fs.writeFileSync(path.join(customerDir, "interactions.md"), `# Interactions — ${name}\n\n`, "utf-8");
|
|
2025
|
+
fs.writeFileSync(path.join(customerDir, "pipeline.md"), `# Pipeline — ${name}\n\n`, "utf-8");
|
|
2026
|
+
fs.writeFileSync(path.join(customerDir, "sources.json"), JSON.stringify({
|
|
2027
|
+
gmail: {
|
|
2028
|
+
query: domain ? `from:${domain} OR to:${domain}` : email ? `from:${email} OR to:${email}` : "",
|
|
2029
|
+
enabled: true
|
|
2030
|
+
},
|
|
2031
|
+
transcripts: {
|
|
2032
|
+
paths: [],
|
|
2033
|
+
extensions: [".txt", ".vtt"],
|
|
2034
|
+
enabled: false
|
|
2035
|
+
}
|
|
2036
|
+
}, null, 2), "utf-8");
|
|
2037
|
+
return {
|
|
2038
|
+
slug,
|
|
2039
|
+
created: true
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
function readCsvFromDirectory(dirPath, filename) {
|
|
2043
|
+
const variants = [
|
|
2044
|
+
filename,
|
|
2045
|
+
filename.toLowerCase(),
|
|
2046
|
+
filename.toUpperCase()
|
|
2047
|
+
];
|
|
2048
|
+
for (const name of variants) {
|
|
2049
|
+
const p = path.join(dirPath, name);
|
|
2050
|
+
if (fs.existsSync(p)) return fs.readFileSync(p, "utf-8");
|
|
2051
|
+
}
|
|
2052
|
+
const match = fs.readdirSync(dirPath).find((f) => f.toLowerCase() === filename.toLowerCase());
|
|
2053
|
+
if (match) return fs.readFileSync(path.join(dirPath, match), "utf-8");
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
async function extractZip(zipPath) {
|
|
2057
|
+
const AdmZip = (await import("adm-zip")).default;
|
|
2058
|
+
const zip = new AdmZip(zipPath);
|
|
2059
|
+
const tmpDir = `${zipPath}.extracted`;
|
|
2060
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2061
|
+
zip.extractAllTo(tmpDir, true);
|
|
2062
|
+
return tmpDir;
|
|
2063
|
+
}
|
|
2064
|
+
async function runSalesforceFileImport(sourcePath, opts, dir) {
|
|
2065
|
+
const result = {
|
|
2066
|
+
customersCreated: 0,
|
|
2067
|
+
interactionsImported: 0,
|
|
2068
|
+
skipped: 0,
|
|
2069
|
+
errors: []
|
|
2070
|
+
};
|
|
2071
|
+
let dataDir = sourcePath;
|
|
2072
|
+
let tmpDir = null;
|
|
2073
|
+
if (sourcePath.endsWith(".zip")) {
|
|
2074
|
+
tmpDir = await extractZip(sourcePath);
|
|
2075
|
+
dataDir = tmpDir;
|
|
2076
|
+
}
|
|
2077
|
+
if (!fs.statSync(dataDir).isDirectory()) {
|
|
2078
|
+
result.errors.push("Salesforce file import requires a directory or .zip file");
|
|
2079
|
+
return result;
|
|
2080
|
+
}
|
|
2081
|
+
const accountsCsv = readCsvFromDirectory(dataDir, "Accounts.csv") ?? readCsvFromDirectory(dataDir, "accounts.csv");
|
|
2082
|
+
const activitiesCsv = readCsvFromDirectory(dataDir, "Activities.csv") ?? readCsvFromDirectory(dataDir, "activities.csv") ?? readCsvFromDirectory(dataDir, "Tasks.csv") ?? readCsvFromDirectory(dataDir, "tasks.csv");
|
|
2083
|
+
if (!accountsCsv) {
|
|
2084
|
+
result.errors.push("Could not find Accounts.csv in Salesforce export");
|
|
2085
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2086
|
+
return result;
|
|
2087
|
+
}
|
|
2088
|
+
const accounts = parseCSV(accountsCsv);
|
|
2089
|
+
const activities = activitiesCsv ? parseCSV(activitiesCsv) : [];
|
|
2090
|
+
if (opts.dryRun) {
|
|
2091
|
+
console.log(info(`Dry run — ${accounts.length} accounts, ${activities.length} activities from Salesforce export`));
|
|
2092
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2093
|
+
return result;
|
|
2094
|
+
}
|
|
2095
|
+
const slugMap = /* @__PURE__ */ new Map();
|
|
2096
|
+
for (const row of accounts) {
|
|
2097
|
+
const name = (row["Name"] ?? row["Account Name"] ?? "").trim();
|
|
2098
|
+
if (!name) continue;
|
|
2099
|
+
const domain = (row["Website"] ?? "").replace(/^https?:\/\//, "");
|
|
2100
|
+
const email = row["Email"] ?? "";
|
|
2101
|
+
try {
|
|
2102
|
+
const { slug, created } = ensureCustomer(dir, name, domain, email, false);
|
|
2103
|
+
if (row["Id"]) slugMap.set(row["Id"], slug);
|
|
2104
|
+
slugMap.set(name.toLowerCase(), slug);
|
|
2105
|
+
if (created) result.customersCreated++;
|
|
2106
|
+
} catch (err) {
|
|
2107
|
+
result.errors.push(`Account '${name}': ${err.message}`);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
for (const row of activities) {
|
|
2111
|
+
const accountId = row["AccountId"] ?? row["WhatId"] ?? "";
|
|
2112
|
+
const slug = accountId ? slugMap.get(accountId) : void 0;
|
|
2113
|
+
if (!slug) continue;
|
|
2114
|
+
const id = row["Id"] ?? hashRow(row);
|
|
2115
|
+
const sourceRef = `salesforce://row/${id}`;
|
|
2116
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2117
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2118
|
+
result.skipped++;
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const date = row["ActivityDate"] ?? row["CreatedDate"] ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2122
|
+
const notes = (row["Description"] ?? row["Subject"] ?? "").slice(0, 500);
|
|
2123
|
+
const t = (row["Type"] ?? "").toLowerCase();
|
|
2124
|
+
const type = t.includes("call") ? "Call" : t.includes("email") ? "Email" : t.includes("meeting") ? "Meeting" : "Note";
|
|
2125
|
+
try {
|
|
2126
|
+
await appendInteraction(dir, slug, {
|
|
2127
|
+
date,
|
|
2128
|
+
type,
|
|
2129
|
+
with: slug,
|
|
2130
|
+
summary: notes,
|
|
2131
|
+
nextSteps: [],
|
|
2132
|
+
sourceRef,
|
|
2133
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2134
|
+
});
|
|
2135
|
+
result.interactionsImported++;
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
result.errors.push(`Activity ${id}: ${err.message}`);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2141
|
+
return result;
|
|
2142
|
+
}
|
|
2143
|
+
async function runPipedriveFileImport(sourcePath, opts, dir) {
|
|
2144
|
+
const result = {
|
|
2145
|
+
customersCreated: 0,
|
|
2146
|
+
interactionsImported: 0,
|
|
2147
|
+
skipped: 0,
|
|
2148
|
+
errors: []
|
|
2149
|
+
};
|
|
2150
|
+
let dataDir = sourcePath;
|
|
2151
|
+
let tmpDir = null;
|
|
2152
|
+
if (sourcePath.endsWith(".zip")) {
|
|
2153
|
+
tmpDir = await extractZip(sourcePath);
|
|
2154
|
+
dataDir = tmpDir;
|
|
2155
|
+
}
|
|
2156
|
+
if (!fs.statSync(dataDir).isDirectory()) {
|
|
2157
|
+
result.errors.push("Pipedrive file import requires a directory or .zip file");
|
|
2158
|
+
return result;
|
|
2159
|
+
}
|
|
2160
|
+
const orgsCsv = readCsvFromDirectory(dataDir, "organizations.csv") ?? readCsvFromDirectory(dataDir, "Organizations.csv");
|
|
2161
|
+
const activitiesCsv = readCsvFromDirectory(dataDir, "activities.csv") ?? readCsvFromDirectory(dataDir, "Activities.csv");
|
|
2162
|
+
if (!orgsCsv) {
|
|
2163
|
+
result.errors.push("Could not find organizations.csv in Pipedrive export");
|
|
2164
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2165
|
+
return result;
|
|
2166
|
+
}
|
|
2167
|
+
const orgs = parseCSV(orgsCsv);
|
|
2168
|
+
const activities = activitiesCsv ? parseCSV(activitiesCsv) : [];
|
|
2169
|
+
if (opts.dryRun) {
|
|
2170
|
+
console.log(info(`Dry run — ${orgs.length} organizations, ${activities.length} activities from Pipedrive export`));
|
|
2171
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2172
|
+
return result;
|
|
2173
|
+
}
|
|
2174
|
+
const slugMap = /* @__PURE__ */ new Map();
|
|
2175
|
+
for (const row of orgs) {
|
|
2176
|
+
const name = (row["name"] ?? row["Name"] ?? "").trim();
|
|
2177
|
+
if (!name) continue;
|
|
2178
|
+
const id = row["id"] ?? row["ID"] ?? "";
|
|
2179
|
+
try {
|
|
2180
|
+
const { slug, created } = ensureCustomer(dir, name, "", "", false);
|
|
2181
|
+
if (id) slugMap.set(id, slug);
|
|
2182
|
+
slugMap.set(name.toLowerCase(), slug);
|
|
2183
|
+
if (created) result.customersCreated++;
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
result.errors.push(`Organization '${name}': ${err.message}`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
for (const row of activities) {
|
|
2189
|
+
const orgId = row["org_id"] ?? row["organization_id"] ?? "";
|
|
2190
|
+
const slug = orgId ? slugMap.get(orgId) : void 0;
|
|
2191
|
+
if (!slug) continue;
|
|
2192
|
+
const id = row["id"] ?? hashRow(row);
|
|
2193
|
+
const sourceRef = `pipedrive://row/${id}`;
|
|
2194
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2195
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2196
|
+
result.skipped++;
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
const date = row["due_date"] ?? row["add_time"]?.slice(0, 10) ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2200
|
+
const notes = (row["note"] ?? row["subject"] ?? "").slice(0, 500);
|
|
2201
|
+
const t = (row["type"] ?? "").toLowerCase();
|
|
2202
|
+
const type = t === "call" ? "Call" : t === "email" ? "Email" : t === "meeting" ? "Meeting" : "Note";
|
|
2203
|
+
try {
|
|
2204
|
+
await appendInteraction(dir, slug, {
|
|
2205
|
+
date,
|
|
2206
|
+
type,
|
|
2207
|
+
with: slug,
|
|
2208
|
+
summary: notes,
|
|
2209
|
+
nextSteps: [],
|
|
2210
|
+
sourceRef,
|
|
2211
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2212
|
+
});
|
|
2213
|
+
result.interactionsImported++;
|
|
2214
|
+
} catch (err) {
|
|
2215
|
+
result.errors.push(`Activity ${id}: ${err.message}`);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true });
|
|
2219
|
+
return result;
|
|
2220
|
+
}
|
|
2221
|
+
async function runImport(sourcePath, opts, dataDir) {
|
|
2222
|
+
const dir = dataDir ?? process.cwd();
|
|
2223
|
+
const result = {
|
|
2224
|
+
customersCreated: 0,
|
|
2225
|
+
interactionsImported: 0,
|
|
2226
|
+
skipped: 0,
|
|
2227
|
+
errors: []
|
|
2228
|
+
};
|
|
2229
|
+
if (opts.from === "salesforce" && opts.mode === "api") return runSalesforceApiImport(opts, dir);
|
|
2230
|
+
if (opts.from === "pipedrive" && opts.mode === "api") return runPipedriveApiImport(opts, dir);
|
|
2231
|
+
if (opts.from === "hubspot" && sourcePath && fs.existsSync(sourcePath) && fs.statSync(sourcePath).isDirectory()) {
|
|
2232
|
+
const { runHubSpotCsvImport } = await import("./import-hubspot-BaK71U_K.js");
|
|
2233
|
+
const r = await runHubSpotCsvImport(sourcePath, dir, {
|
|
2234
|
+
...opts.dryRun ? { dryRun: true } : {},
|
|
2235
|
+
...opts.resume ? { resume: true } : {},
|
|
2236
|
+
ownerMap: opts.ownerMap ?? {}
|
|
2237
|
+
});
|
|
2238
|
+
if (r.customPropertiesSaved > 0) console.error(`[import] Custom properties saved: ${r.customPropertiesSaved}`);
|
|
2239
|
+
if (r.ownersResolved > 0) console.error(`[import] Owners resolved: ${r.ownersResolved}`);
|
|
2240
|
+
return {
|
|
2241
|
+
customersCreated: r.companiesProcessed,
|
|
2242
|
+
interactionsImported: r.engagementsImported + r.dealsImported + r.contactsImported,
|
|
2243
|
+
skipped: 0,
|
|
2244
|
+
errors: r.errors
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
if (opts.from === "salesforce" && sourcePath) return runSalesforceFileImport(sourcePath, opts, dir);
|
|
2248
|
+
if (opts.from === "pipedrive" && sourcePath) return runPipedriveFileImport(sourcePath, opts, dir);
|
|
2249
|
+
if (!fs.existsSync(sourcePath)) {
|
|
2250
|
+
console.error(error(`✗ File not found: ${sourcePath}`));
|
|
2251
|
+
process.exit(1);
|
|
2252
|
+
}
|
|
2253
|
+
const rows = parseCSV(fs.readFileSync(sourcePath, "utf-8"));
|
|
2254
|
+
if (rows.length === 0) {
|
|
2255
|
+
console.log(info("No rows found in CSV."));
|
|
2256
|
+
return result;
|
|
2257
|
+
}
|
|
2258
|
+
const headers = Object.keys(rows[0]);
|
|
2259
|
+
const { mapCsvFields } = await import("./llm-DEjWcqmW.js");
|
|
2260
|
+
const mapping = await mapCsvFields(headers, [...IMPORT_TARGET_FIELDS]);
|
|
2261
|
+
if (opts.dryRun) {
|
|
2262
|
+
console.log(info(`Dry run — ${rows.length} rows, field mapping:`));
|
|
2263
|
+
Object.entries(mapping).forEach(([k, v]) => v && console.log(info(` ${k} ← "${v}"`)));
|
|
2264
|
+
console.log(info(`\nWould create up to ${rows.length} customers and interaction entries.`));
|
|
2265
|
+
return result;
|
|
2266
|
+
}
|
|
2267
|
+
const customerRows = rows.filter((r) => {
|
|
2268
|
+
return (r[mapping.name ?? ""] ?? "").trim().length > 0;
|
|
2269
|
+
});
|
|
2270
|
+
const slugMap = /* @__PURE__ */ new Map();
|
|
2271
|
+
for (const row of customerRows) {
|
|
2272
|
+
const name = (row[mapping.name ?? ""] ?? "").trim();
|
|
2273
|
+
if (!name) continue;
|
|
2274
|
+
const domain = (row[mapping.domain ?? ""] ?? "").trim();
|
|
2275
|
+
const email = (row[mapping.email ?? ""] ?? "").trim();
|
|
2276
|
+
try {
|
|
2277
|
+
const { slug, created } = ensureCustomer(dir, name, domain, email, opts.dryRun ?? false);
|
|
2278
|
+
slugMap.set(name.toLowerCase(), slug);
|
|
2279
|
+
if (created) result.customersCreated++;
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
result.errors.push(`Customer '${name}': ${err.message}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
for (const row of rows) {
|
|
2285
|
+
const activityType = (row[mapping["activityType"] ?? ""] ?? "").trim();
|
|
2286
|
+
const notes = (row[mapping["notes"] ?? ""] ?? "").trim();
|
|
2287
|
+
const activityDate = (row[mapping["date"] ?? ""] ?? "").trim();
|
|
2288
|
+
const sourceIdVal = (row[mapping["sourceId"] ?? ""] ?? "").trim();
|
|
2289
|
+
const name = (row[mapping["name"] ?? ""] ?? "").trim();
|
|
2290
|
+
if (!notes && !activityType) continue;
|
|
2291
|
+
const slug = slugMap.get(name.toLowerCase());
|
|
2292
|
+
if (!slug) continue;
|
|
2293
|
+
const rowHash = hashRow(row);
|
|
2294
|
+
const prefix = opts.from === "hubspot" ? "hubspot" : "csv";
|
|
2295
|
+
const sourceRef = sourceIdVal ? `${prefix}://activity/${sourceIdVal}` : `${prefix}://row/${rowHash}`;
|
|
2296
|
+
const date = activityDate ? (() => {
|
|
2297
|
+
try {
|
|
2298
|
+
return new Date(activityDate).toISOString().slice(0, 10);
|
|
2299
|
+
} catch {
|
|
2300
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2301
|
+
}
|
|
2302
|
+
})() : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2303
|
+
const type = (() => {
|
|
2304
|
+
const t = activityType.toLowerCase();
|
|
2305
|
+
if (t.includes("call")) return "Call";
|
|
2306
|
+
if (t.includes("meeting") || t.includes("demo")) return "Meeting";
|
|
2307
|
+
if (t.includes("email")) return "Email";
|
|
2308
|
+
if (t.includes("note")) return "Note";
|
|
2309
|
+
return "Note";
|
|
2310
|
+
})();
|
|
2311
|
+
try {
|
|
2312
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2313
|
+
if ((await readInteractions(dir, slug)).includes(sourceRef)) {
|
|
2314
|
+
result.skipped++;
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
await appendInteraction(dir, slug, {
|
|
2318
|
+
date,
|
|
2319
|
+
type,
|
|
2320
|
+
with: name,
|
|
2321
|
+
summary: notes.slice(0, 500),
|
|
2322
|
+
nextSteps: [],
|
|
2323
|
+
sourceRef,
|
|
2324
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2325
|
+
});
|
|
2326
|
+
result.interactionsImported++;
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
result.errors.push(`Activity for '${name}': ${err.message}`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return result;
|
|
2332
|
+
}
|
|
2333
|
+
async function runSalesforceApiImport(opts, dir) {
|
|
2334
|
+
const result = {
|
|
2335
|
+
customersCreated: 0,
|
|
2336
|
+
interactionsImported: 0,
|
|
2337
|
+
skipped: 0,
|
|
2338
|
+
errors: []
|
|
2339
|
+
};
|
|
2340
|
+
const token = opts.token ?? process.env["SFDC_TOKEN"] ?? "";
|
|
2341
|
+
const instanceUrl = opts.url ?? process.env["SFDC_URL"] ?? "";
|
|
2342
|
+
if (!token || !instanceUrl) {
|
|
2343
|
+
console.error(error("✗ Salesforce API mode requires --token and --url (or SFDC_TOKEN + SFDC_URL env vars)"));
|
|
2344
|
+
process.exit(1);
|
|
2345
|
+
}
|
|
2346
|
+
const { fetchSalesforceContacts, fetchSalesforceTasks, fetchSalesforceOpportunities, fetchSalesforceLeads, fetchSalesforceEvents, fetchSalesforceCases, fetchSalesforceLineItems, fetchSalesforceNotes, fetchSalesforceCampaignMembers } = await import("./salesforce-client-rhZFa_p5.js");
|
|
2347
|
+
let contacts;
|
|
2348
|
+
let tasks;
|
|
2349
|
+
let opportunities;
|
|
2350
|
+
let leads;
|
|
2351
|
+
let events;
|
|
2352
|
+
let cases;
|
|
2353
|
+
let lineItems;
|
|
2354
|
+
let notes;
|
|
2355
|
+
let campaignMembers;
|
|
2356
|
+
try {
|
|
2357
|
+
contacts = await fetchSalesforceContacts(instanceUrl, token);
|
|
2358
|
+
tasks = await fetchSalesforceTasks(instanceUrl, token);
|
|
2359
|
+
opportunities = await fetchSalesforceOpportunities(instanceUrl, token) ?? [];
|
|
2360
|
+
leads = await fetchSalesforceLeads(instanceUrl, token) ?? [];
|
|
2361
|
+
events = await fetchSalesforceEvents(instanceUrl, token) ?? [];
|
|
2362
|
+
cases = await fetchSalesforceCases(instanceUrl, token) ?? [];
|
|
2363
|
+
lineItems = await fetchSalesforceLineItems(instanceUrl, token) ?? [];
|
|
2364
|
+
notes = await fetchSalesforceNotes(instanceUrl, token) ?? [];
|
|
2365
|
+
campaignMembers = await fetchSalesforceCampaignMembers(instanceUrl, token) ?? [];
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
result.errors.push(`Salesforce API: ${err.message}`);
|
|
2368
|
+
return result;
|
|
2369
|
+
}
|
|
2370
|
+
if (opts.dryRun) {
|
|
2371
|
+
console.log(info(`Dry run — ${contacts.length} contacts, ${tasks.length} tasks, ${opportunities.length} opportunities, ${leads.length} leads, ${events.length} events, ${cases.length} cases from Salesforce`));
|
|
2372
|
+
return result;
|
|
2373
|
+
}
|
|
2374
|
+
const slugMap = /* @__PURE__ */ new Map();
|
|
2375
|
+
for (const contact of contacts) {
|
|
2376
|
+
const name = contact.Name?.trim();
|
|
2377
|
+
if (!name) continue;
|
|
2378
|
+
const domain = contact.Account?.Website?.replace(/^https?:\/\//, "") ?? "";
|
|
2379
|
+
const email = contact.Email ?? "";
|
|
2380
|
+
try {
|
|
2381
|
+
const { slug, created } = ensureCustomer(dir, name, domain, email, false);
|
|
2382
|
+
slugMap.set(contact.Id, slug);
|
|
2383
|
+
slugMap.set(name.toLowerCase(), slug);
|
|
2384
|
+
if (created) result.customersCreated++;
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
result.errors.push(`Contact '${name}': ${err.message}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
for (const task of tasks) {
|
|
2390
|
+
const slug = task.WhoId ? slugMap.get(task.WhoId) : void 0;
|
|
2391
|
+
if (!slug) continue;
|
|
2392
|
+
const sourceRef = `salesforce://task/${task.Id}`;
|
|
2393
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2394
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2395
|
+
result.skipped++;
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
const date = task.ActivityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2399
|
+
const notes = (task.Description ?? task.Subject ?? "").slice(0, 500);
|
|
2400
|
+
const t = (task.Type ?? "").toLowerCase();
|
|
2401
|
+
const type = t.includes("call") ? "Call" : t.includes("email") ? "Email" : t.includes("meeting") ? "Meeting" : "Note";
|
|
2402
|
+
try {
|
|
2403
|
+
await appendInteraction(dir, slug, {
|
|
2404
|
+
date,
|
|
2405
|
+
type,
|
|
2406
|
+
with: slug,
|
|
2407
|
+
summary: notes,
|
|
2408
|
+
nextSteps: [],
|
|
2409
|
+
sourceRef,
|
|
2410
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2411
|
+
});
|
|
2412
|
+
result.interactionsImported++;
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
result.errors.push(`Task ${task.Id}: ${err.message}`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const { upsertDeal } = await import("./pipeline-writer-BqBrYrQc.js");
|
|
2418
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2419
|
+
const oppSlugById = /* @__PURE__ */ new Map();
|
|
2420
|
+
for (const opp of opportunities) {
|
|
2421
|
+
const accountName = opp.Account?.Name?.trim();
|
|
2422
|
+
if (!opp.Name || !accountName) continue;
|
|
2423
|
+
let slug = slugMap.get(accountName.toLowerCase());
|
|
2424
|
+
if (!slug) {
|
|
2425
|
+
const domain = opp.Account?.Website?.replace(/^https?:\/\//, "") ?? "";
|
|
2426
|
+
try {
|
|
2427
|
+
const r = ensureCustomer(dir, accountName, domain, "", false);
|
|
2428
|
+
slug = r.slug;
|
|
2429
|
+
slugMap.set(accountName.toLowerCase(), slug);
|
|
2430
|
+
if (r.created) result.customersCreated++;
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
result.errors.push(`Opportunity '${opp.Name}': ${err.message}`);
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
oppSlugById.set(opp.Id, {
|
|
2437
|
+
slug,
|
|
2438
|
+
dealName: opp.Name
|
|
2439
|
+
});
|
|
2440
|
+
try {
|
|
2441
|
+
await upsertDeal(dir, slug, {
|
|
2442
|
+
name: opp.Name,
|
|
2443
|
+
stage: mapSalesforceStage(opp.StageName),
|
|
2444
|
+
currency: "EUR",
|
|
2445
|
+
updated: today,
|
|
2446
|
+
notes: `Imported from Salesforce (${opp.StageName ?? "unknown stage"})`,
|
|
2447
|
+
...typeof opp.Amount === "number" ? { value: opp.Amount } : {},
|
|
2448
|
+
...typeof opp.Probability === "number" ? { probability: opp.Probability } : {},
|
|
2449
|
+
...opp.CloseDate ? { close_date: opp.CloseDate } : {}
|
|
2450
|
+
});
|
|
2451
|
+
result.dealsImported = (result.dealsImported ?? 0) + 1;
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
result.errors.push(`Opportunity '${opp.Name}': ${err.message}`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2457
|
+
for (const lead of leads) {
|
|
2458
|
+
const name = lead.Company?.trim() || lead.Name?.trim();
|
|
2459
|
+
if (!name) continue;
|
|
2460
|
+
const domain = lead.Website?.replace(/^https?:\/\//, "") ?? lead.Email?.split("@")[1] ?? "";
|
|
2461
|
+
let slug;
|
|
2462
|
+
try {
|
|
2463
|
+
const r = ensureCustomer(dir, name, domain, lead.Email ?? "", false);
|
|
2464
|
+
slug = r.slug;
|
|
2465
|
+
slugMap.set(name.toLowerCase(), slug);
|
|
2466
|
+
if (r.created) result.customersCreated++;
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
result.errors.push(`Lead '${name}': ${err.message}`);
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
const sourceRef = `salesforce://lead/${lead.Id}`;
|
|
2472
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2473
|
+
result.skipped++;
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
const contactPart = lead.Title ? `${lead.Name}, ${lead.Title}` : lead.Name;
|
|
2477
|
+
try {
|
|
2478
|
+
await appendInteraction(dir, slug, {
|
|
2479
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2480
|
+
type: "Note",
|
|
2481
|
+
with: lead.Name,
|
|
2482
|
+
summary: `Salesforce Lead imported (status: ${lead.Status ?? "n/a"}; contact: ${contactPart})`,
|
|
2483
|
+
nextSteps: [],
|
|
2484
|
+
sourceRef,
|
|
2485
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2486
|
+
});
|
|
2487
|
+
result.leadsImported = (result.leadsImported ?? 0) + 1;
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
result.errors.push(`Lead ${lead.Id}: ${err.message}`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
for (const event of events) {
|
|
2493
|
+
const slug = event.WhoId ? slugMap.get(event.WhoId) : event.WhatId ? slugMap.get(event.WhatId) : void 0;
|
|
2494
|
+
if (!slug) {
|
|
2495
|
+
result.skipped++;
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
const sourceRef = `salesforce://event/${event.Id}`;
|
|
2499
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2500
|
+
result.skipped++;
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
const date = (event.StartDateTime ?? event.ActivityDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
|
|
2504
|
+
try {
|
|
2505
|
+
await appendInteraction(dir, slug, {
|
|
2506
|
+
date,
|
|
2507
|
+
type: "Meeting",
|
|
2508
|
+
with: slug,
|
|
2509
|
+
subject: event.Subject ?? "Salesforce Event",
|
|
2510
|
+
summary: (event.Description ?? event.Subject ?? "").slice(0, 500),
|
|
2511
|
+
nextSteps: [],
|
|
2512
|
+
sourceRef,
|
|
2513
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2514
|
+
});
|
|
2515
|
+
result.eventsImported = (result.eventsImported ?? 0) + 1;
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
result.errors.push(`Event ${event.Id}: ${err.message}`);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
const { readTickets, upsertTicket, nextTicketId } = await import("./ticket-writer-CjqKeIRD.js");
|
|
2521
|
+
const { calcSlaDue, loadSlaRules } = await import("./sla-engine-5IhTsBUR.js");
|
|
2522
|
+
const slaRules = loadSlaRules(dir);
|
|
2523
|
+
for (const c of cases) {
|
|
2524
|
+
const accountName = c.Account?.Name?.trim();
|
|
2525
|
+
if (!accountName) {
|
|
2526
|
+
result.skipped++;
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
let slug = slugMap.get(accountName.toLowerCase());
|
|
2530
|
+
if (!slug) try {
|
|
2531
|
+
const r = ensureCustomer(dir, accountName, "", "", false);
|
|
2532
|
+
slug = r.slug;
|
|
2533
|
+
slugMap.set(accountName.toLowerCase(), slug);
|
|
2534
|
+
if (r.created) result.customersCreated++;
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
result.errors.push(`Case '${c.Id}': ${err.message}`);
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (c.AccountId) slugMap.set(c.AccountId, slug);
|
|
2540
|
+
if (c.ContactId) slugMap.set(c.ContactId, slug);
|
|
2541
|
+
const caseRef = `salesforce://case/${c.Id}`;
|
|
2542
|
+
const existingTickets = await readTickets(dir, slug);
|
|
2543
|
+
if (existingTickets.some((t) => (t.description ?? "").includes(caseRef))) {
|
|
2544
|
+
result.skipped++;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const created = (c.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
|
|
2548
|
+
const status = mapCaseStatus(c.Status);
|
|
2549
|
+
const priority = mapCasePriority(c.Priority);
|
|
2550
|
+
const isDone = status === "closed" || status === "resolved";
|
|
2551
|
+
try {
|
|
2552
|
+
await upsertTicket(dir, slug, {
|
|
2553
|
+
id: nextTicketId(existingTickets),
|
|
2554
|
+
title: c.Subject ?? `Case ${c.CaseNumber ?? c.Id}`,
|
|
2555
|
+
status,
|
|
2556
|
+
priority,
|
|
2557
|
+
created,
|
|
2558
|
+
slaDue: calcSlaDue(created, priority, slaRules),
|
|
2559
|
+
description: `${c.Description ?? ""}\n\n[${caseRef}]`.trim(),
|
|
2560
|
+
...isDone ? { resolved: (c.ClosedDate ?? created).slice(0, 10) } : {}
|
|
2561
|
+
});
|
|
2562
|
+
result.casesImported = (result.casesImported ?? 0) + 1;
|
|
2563
|
+
} catch (err) {
|
|
2564
|
+
result.errors.push(`Case ${c.Id}: ${err.message}`);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
if (lineItems.length > 0 && oppSlugById.size > 0) {
|
|
2568
|
+
const { generateQuote, listQuotes } = await import("./quote-generator-OhSFsi3x.js");
|
|
2569
|
+
const byOpp = /* @__PURE__ */ new Map();
|
|
2570
|
+
for (const li of lineItems) {
|
|
2571
|
+
if (!li.OpportunityId) continue;
|
|
2572
|
+
const arr = byOpp.get(li.OpportunityId) ?? [];
|
|
2573
|
+
arr.push(li);
|
|
2574
|
+
byOpp.set(li.OpportunityId, arr);
|
|
2575
|
+
}
|
|
2576
|
+
for (const [oppId, items] of byOpp) {
|
|
2577
|
+
const opp = oppSlugById.get(oppId);
|
|
2578
|
+
if (!opp) continue;
|
|
2579
|
+
if (listQuotes(dir, opp.slug).some((q) => q.dealName === opp.dealName)) {
|
|
2580
|
+
result.skipped++;
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
const quoteLineItems = items.map((li) => {
|
|
2584
|
+
const quantity = typeof li.Quantity === "number" && li.Quantity > 0 ? li.Quantity : 1;
|
|
2585
|
+
const unitPrice = typeof li.UnitPrice === "number" ? li.UnitPrice : typeof li.TotalPrice === "number" ? li.TotalPrice / quantity : 0;
|
|
2586
|
+
return {
|
|
2587
|
+
description: li.Product2?.Name ?? li.Description ?? "Line item",
|
|
2588
|
+
quantity,
|
|
2589
|
+
unitPrice
|
|
2590
|
+
};
|
|
2591
|
+
});
|
|
2592
|
+
try {
|
|
2593
|
+
await generateQuote(dir, {
|
|
2594
|
+
slug: opp.slug,
|
|
2595
|
+
dealName: opp.dealName,
|
|
2596
|
+
lineItems: quoteLineItems
|
|
2597
|
+
});
|
|
2598
|
+
result.quotesImported = (result.quotesImported ?? 0) + 1;
|
|
2599
|
+
} catch (err) {
|
|
2600
|
+
result.errors.push(`LineItems for '${opp.dealName}': ${err.message}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
for (const note of notes) {
|
|
2605
|
+
const slug = note.ParentId ? slugMap.get(note.ParentId) : void 0;
|
|
2606
|
+
if (!slug) {
|
|
2607
|
+
result.skipped++;
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
const sourceRef = `salesforce://note/${note.Id}`;
|
|
2611
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2612
|
+
result.skipped++;
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
const date = (note.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10);
|
|
2616
|
+
const title = note.Title ?? "Salesforce Note";
|
|
2617
|
+
try {
|
|
2618
|
+
await appendInteraction(dir, slug, {
|
|
2619
|
+
date,
|
|
2620
|
+
type: "Note",
|
|
2621
|
+
with: slug,
|
|
2622
|
+
subject: title,
|
|
2623
|
+
summary: `${title}${note.Body ? `: ${note.Body}` : ""}`.slice(0, 500),
|
|
2624
|
+
nextSteps: [],
|
|
2625
|
+
sourceRef,
|
|
2626
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2627
|
+
});
|
|
2628
|
+
result.notesImported = (result.notesImported ?? 0) + 1;
|
|
2629
|
+
} catch (err) {
|
|
2630
|
+
result.errors.push(`Note ${note.Id}: ${err.message}`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
for (const cm of campaignMembers) {
|
|
2634
|
+
const slug = cm.ContactId ? slugMap.get(cm.ContactId) : cm.LeadId ? slugMap.get(cm.LeadId) : void 0;
|
|
2635
|
+
if (!slug) {
|
|
2636
|
+
result.skipped++;
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const sourceRef = `salesforce://campaignmember/${cm.Id}`;
|
|
2640
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2641
|
+
result.skipped++;
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
const campaignName = cm.Campaign?.Name ?? cm.CampaignId ?? "Unknown campaign";
|
|
2645
|
+
try {
|
|
2646
|
+
await appendInteraction(dir, slug, {
|
|
2647
|
+
date: (cm.CreatedDate ?? (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10),
|
|
2648
|
+
type: "Note",
|
|
2649
|
+
with: slug,
|
|
2650
|
+
subject: `Campaign: ${campaignName}`,
|
|
2651
|
+
summary: `Salesforce Campaign: ${campaignName} (status: ${cm.Status ?? "n/a"})`,
|
|
2652
|
+
nextSteps: [],
|
|
2653
|
+
sourceRef,
|
|
2654
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2655
|
+
});
|
|
2656
|
+
result.campaignsImported = (result.campaignsImported ?? 0) + 1;
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
result.errors.push(`CampaignMember ${cm.Id}: ${err.message}`);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
async function runPipedriveApiImport(opts, dir = process.cwd()) {
|
|
2664
|
+
const result = {
|
|
2665
|
+
customersCreated: 0,
|
|
2666
|
+
interactionsImported: 0,
|
|
2667
|
+
skipped: 0,
|
|
2668
|
+
errors: []
|
|
2669
|
+
};
|
|
2670
|
+
const token = opts.token ?? process.env["PIPEDRIVE_TOKEN"] ?? "";
|
|
2671
|
+
const instanceUrl = opts.url ?? process.env["PIPEDRIVE_URL"] ?? "";
|
|
2672
|
+
if (!token || !instanceUrl) {
|
|
2673
|
+
result.errors.push("Pipedrive API mode requires --token and --url (or PIPEDRIVE_TOKEN + PIPEDRIVE_URL env vars)");
|
|
2674
|
+
return result;
|
|
2675
|
+
}
|
|
2676
|
+
const { fetchPipedrivePersons, fetchPipedriveActivities } = await import("./pipedrive-client-CdGKpH9b.js");
|
|
2677
|
+
let persons;
|
|
2678
|
+
let activities;
|
|
2679
|
+
try {
|
|
2680
|
+
[persons, activities] = await Promise.all([fetchPipedrivePersons(instanceUrl, token), fetchPipedriveActivities(instanceUrl, token)]);
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
result.errors.push(`Pipedrive API: ${err.message}`);
|
|
2683
|
+
return result;
|
|
2684
|
+
}
|
|
2685
|
+
if (opts.dryRun) {
|
|
2686
|
+
console.log(info(`Dry run — ${persons.length} persons, ${activities.length} activities from Pipedrive`));
|
|
2687
|
+
return result;
|
|
2688
|
+
}
|
|
2689
|
+
const slugByPersonId = /* @__PURE__ */ new Map();
|
|
2690
|
+
const slugByOrgId = /* @__PURE__ */ new Map();
|
|
2691
|
+
for (const person of persons) {
|
|
2692
|
+
const name = (person.org_name ?? person.name ?? "").trim();
|
|
2693
|
+
if (!name) continue;
|
|
2694
|
+
const email = person.primary_email ?? "";
|
|
2695
|
+
try {
|
|
2696
|
+
const { slug, created } = ensureCustomer(dir, name, "", email, false);
|
|
2697
|
+
if (person.id) slugByPersonId.set(person.id, slug);
|
|
2698
|
+
if (person.org_id?.value) slugByOrgId.set(person.org_id.value, slug);
|
|
2699
|
+
if (created) result.customersCreated++;
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
result.errors.push(`Person '${name}': ${err.message}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
for (const activity of activities) {
|
|
2705
|
+
const slug = (activity.person_id && slugByPersonId.get(activity.person_id)) ?? (activity.org_id && slugByOrgId.get(activity.org_id)) ?? void 0;
|
|
2706
|
+
if (!slug) continue;
|
|
2707
|
+
const sourceRef = `pipedrive://activity/${activity.id}`;
|
|
2708
|
+
const { readInteractions } = await import("./interactions-writer-dSPy1XfO.js");
|
|
2709
|
+
if ((await readInteractions(dir, slug).catch(() => "")).includes(sourceRef)) {
|
|
2710
|
+
result.skipped++;
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const date = activity.due_date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2714
|
+
const notes = (activity.note ?? activity.subject ?? "").slice(0, 500);
|
|
2715
|
+
const t = (activity.type ?? "").toLowerCase();
|
|
2716
|
+
const type = t === "call" ? "Call" : t === "email" ? "Email" : t === "meeting" ? "Meeting" : "Note";
|
|
2717
|
+
try {
|
|
2718
|
+
await appendInteraction(dir, slug, {
|
|
2719
|
+
date,
|
|
2720
|
+
type,
|
|
2721
|
+
with: slug,
|
|
2722
|
+
summary: `${activity.subject ?? type}: ${notes}`,
|
|
2723
|
+
nextSteps: [],
|
|
2724
|
+
sourceRef,
|
|
2725
|
+
synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2726
|
+
});
|
|
2727
|
+
result.interactionsImported++;
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
result.errors.push(`Activity ${activity.id}: ${err.message}`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
return result;
|
|
2733
|
+
}
|
|
2734
|
+
const importCommand = new Command("import").description("Import customers and interactions from HubSpot, Salesforce, Pipedrive, or CSV").argument("[path]", "Path to export file or directory").option("--from <source>", "Source CRM: hubspot | csv | salesforce | pipedrive", "csv").option("--dry-run", "Preview what would be imported without writing").option("--mode <mode>", "Import mode: file | api").option("--token <token>", "API token (Salesforce, Pipedrive, HubSpot)").option("--url <url>", "Instance URL (e.g. https://myco.salesforce.com)").option("--analyze", "Analyze export and show what would be imported (no write)").option("--resume", "Resume a previously interrupted import").option("--owner-map <mapping>", "Map HubSpot owner emails to reps: \"alice@hs.com=alice,bob@hs.com=bob\"").action(async (sourcePath, opts) => {
|
|
2735
|
+
const dryRun = opts.dryRun ?? false;
|
|
2736
|
+
const ownerMap = {};
|
|
2737
|
+
if (opts.ownerMap) for (const pair of opts.ownerMap.split(",")) {
|
|
2738
|
+
const [hs, rep] = pair.split("=");
|
|
2739
|
+
if (hs && rep) ownerMap[hs.trim()] = rep.trim();
|
|
2740
|
+
}
|
|
2741
|
+
if (opts.analyze && opts.from === "hubspot" && sourcePath) {
|
|
2742
|
+
const { analyzeHubSpotExport } = await import("./import-hubspot-BaK71U_K.js");
|
|
2743
|
+
const analysis = await analyzeHubSpotExport(sourcePath);
|
|
2744
|
+
console.log(bold("\nDatasynxOpenCRM — HubSpot Import Analysis"));
|
|
2745
|
+
console.log("==========================================");
|
|
2746
|
+
console.log(info(`Companies: ${analysis.companiesFound}`));
|
|
2747
|
+
console.log(info(`Contacts: ${analysis.contactsFound} (${analysis.unmappedContacts} unmapped companies)`));
|
|
2748
|
+
console.log(info(`Deals: ${analysis.dealsFound}`));
|
|
2749
|
+
console.log(info(`Engagements: ${analysis.engagementsFound}`));
|
|
2750
|
+
if (analysis.customPropertiesDetected.length > 0) {
|
|
2751
|
+
console.log(info(`\nCustom Properties: ${analysis.customPropertiesDetected.length} detected`));
|
|
2752
|
+
console.log(` ${analysis.customPropertiesDetected.slice(0, 10).join(", ")}${analysis.customPropertiesDetected.length > 10 ? " ..." : ""}`);
|
|
2753
|
+
}
|
|
2754
|
+
if (analysis.ownersDetected.length > 0) {
|
|
2755
|
+
console.log(info(`\nOwners detected: ${analysis.ownersDetected.join(", ")}`));
|
|
2756
|
+
console.log(` Use --owner-map "${analysis.ownersDetected.map((o) => `${o}=<rep>`).join(",")}"`);
|
|
2757
|
+
}
|
|
2758
|
+
if (analysis.unknownStages.length > 0) console.log(info(`\nUnknown stages (→ "qualified"): ${analysis.unknownStages.join(", ")}`));
|
|
2759
|
+
console.log(info(`\nEstimated import time: ~${analysis.estimatedMinutes} min`));
|
|
2760
|
+
console.log(info(`\nRun without --analyze to start import.`));
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (!dryRun) console.log(info(`Importing from ${bold(opts.from)}: ${sourcePath}`));
|
|
2764
|
+
const result = await runImport(sourcePath, {
|
|
2765
|
+
from: opts.from,
|
|
2766
|
+
...dryRun ? { dryRun: true } : {},
|
|
2767
|
+
...opts.mode ? { mode: opts.mode } : {},
|
|
2768
|
+
...opts.token ? { token: opts.token } : {},
|
|
2769
|
+
...opts.url ? { url: opts.url } : {},
|
|
2770
|
+
...opts.resume ? { resume: true } : {},
|
|
2771
|
+
ownerMap
|
|
2772
|
+
}, process.env["DXCRM_DATA_DIR"]);
|
|
2773
|
+
if (!dryRun) {
|
|
2774
|
+
console.log(success(`✓ Import complete:`));
|
|
2775
|
+
console.log(info(` Customers created: ${result.customersCreated}`));
|
|
2776
|
+
console.log(info(` Interactions imported: ${result.interactionsImported}`));
|
|
2777
|
+
console.log(info(` Skipped (duplicates): ${result.skipped}`));
|
|
2778
|
+
if (result.errors.length > 0) {
|
|
2779
|
+
console.log(error(` Errors (${result.errors.length}):`));
|
|
2780
|
+
result.errors.slice(0, 5).forEach((e) => console.log(error(` ${e}`)));
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/commands/server.ts
|
|
2786
|
+
function getPidFile(dataDir) {
|
|
2787
|
+
return path.join(dataDir, ".agentic", "server.pid");
|
|
2788
|
+
}
|
|
2789
|
+
async function runServerStart(opts, dataDir) {
|
|
2790
|
+
const port = parseInt(opts.port ?? "3847", 10);
|
|
2791
|
+
const dir = opts.data ?? dataDir ?? process.cwd();
|
|
2792
|
+
if (opts.data) process.env["DXCRM_DATA_DIR"] = opts.data;
|
|
2793
|
+
const pidFile = getPidFile(dir);
|
|
2794
|
+
if (fs.existsSync(pidFile)) {
|
|
2795
|
+
const existing = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
2796
|
+
if (!isNaN(existing)) try {
|
|
2797
|
+
process.kill(existing, 0);
|
|
2798
|
+
console.log(info(`Server already running (PID ${existing})`));
|
|
2799
|
+
return;
|
|
2800
|
+
} catch {}
|
|
2801
|
+
}
|
|
2802
|
+
const serverEntry = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../dist/mcp/server.js");
|
|
2803
|
+
const env = {
|
|
2804
|
+
...process.env,
|
|
2805
|
+
DXCRM_MCP_MODE: "http",
|
|
2806
|
+
DXCRM_MCP_PORT: String(port)
|
|
2807
|
+
};
|
|
2808
|
+
if (opts.data) env["DXCRM_DATA_DIR"] = opts.data;
|
|
2809
|
+
const child = spawn(process.execPath, [serverEntry], {
|
|
2810
|
+
detached: true,
|
|
2811
|
+
stdio: "ignore",
|
|
2812
|
+
env
|
|
2813
|
+
});
|
|
2814
|
+
child.unref();
|
|
2815
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
2816
|
+
fs.writeFileSync(pidFile, String(child.pid), "utf-8");
|
|
2817
|
+
const hostname = os.hostname();
|
|
2818
|
+
console.log(success(`DatasynxOpenCRM server running on http://0.0.0.0:${port}/mcp`));
|
|
2819
|
+
console.log(info(`Data dir: ${dir}`));
|
|
2820
|
+
console.log(info(`Add to your AI framework config: url: http://${hostname}:${port}/mcp`));
|
|
2821
|
+
}
|
|
2822
|
+
function runServerStatus(dataDir) {
|
|
2823
|
+
const pidFile = getPidFile(process.env["DXCRM_DATA_DIR"] ?? dataDir ?? process.cwd());
|
|
2824
|
+
if (!fs.existsSync(pidFile)) {
|
|
2825
|
+
console.log(info("Server: not running."));
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
2829
|
+
if (isNaN(pid)) {
|
|
2830
|
+
console.log(info("Server: not running (invalid PID file)."));
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
try {
|
|
2834
|
+
process.kill(pid, 0);
|
|
2835
|
+
console.log(success(`Server: running (PID ${pid})`));
|
|
2836
|
+
} catch {
|
|
2837
|
+
console.log(info("Server: not running (stale PID file)."));
|
|
2838
|
+
try {
|
|
2839
|
+
fs.unlinkSync(pidFile);
|
|
2840
|
+
} catch {}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const serverCommand = new Command("server");
|
|
2844
|
+
serverCommand.command("start").description("Start the DatasynxOpenCRM HTTP MCP server").option("--port <port>", "HTTP port (default 3847)", "3847").option("--data <dir>", "Data directory (sets DXCRM_DATA_DIR)").action((opts) => runServerStart(opts));
|
|
2845
|
+
serverCommand.command("status").description("Check if the server is running").action(() => runServerStatus());
|
|
2846
|
+
//#endregion
|
|
2847
|
+
//#region src/commands/audit.ts
|
|
2848
|
+
const SEP = "─".repeat(70);
|
|
2849
|
+
async function runAudit(opts, dataDir) {
|
|
2850
|
+
const dir = dataDir ?? process.cwd();
|
|
2851
|
+
const limit = opts.limit ?? 20;
|
|
2852
|
+
const entries = filterAuditLog(readAuditLog(dir), {
|
|
2853
|
+
...opts.slug !== void 0 ? { slug: opts.slug } : {},
|
|
2854
|
+
...opts.actor !== void 0 ? { actor: opts.actor } : {},
|
|
2855
|
+
limit
|
|
2856
|
+
});
|
|
2857
|
+
console.log(SEP);
|
|
2858
|
+
console.log(" DatasynxOpenCRM — Audit Trail");
|
|
2859
|
+
if (opts.slug) console.log(` Customer: ${opts.slug}`);
|
|
2860
|
+
if (opts.actor) console.log(` Actor: ${opts.actor}`);
|
|
2861
|
+
console.log(SEP);
|
|
2862
|
+
if (entries.length === 0) {
|
|
2863
|
+
console.log(" No audit entries found.");
|
|
2864
|
+
console.log(SEP);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
for (const entry of entries) console.log(` ${entry.timestamp} ${entry.actor.padEnd(12)} ${entry.tool.padEnd(20)} ${entry.slug.padEnd(20)} ${entry.summary}`);
|
|
2868
|
+
console.log(SEP);
|
|
2869
|
+
console.log(` ${entries.length} entr${entries.length === 1 ? "y" : "ies"} shown`);
|
|
2870
|
+
console.log(SEP);
|
|
2871
|
+
}
|
|
2872
|
+
const auditCommand = new Command("audit").description("Show CRM audit trail — who changed what and when").option("--slug <slug>", "Filter by customer slug").option("--actor <actor>", "Filter by actor").option("--limit <n>", "Number of entries to show (default: 20)", parseInt).option("--tail", "Show all new entries (simplified: shows current entries)").action((opts) => runAudit(opts, process.env["DXCRM_DATA_DIR"]));
|
|
2873
|
+
//#endregion
|
|
2874
|
+
//#region src/commands/rbac.ts
|
|
2875
|
+
const ROLES = [
|
|
2876
|
+
"admin",
|
|
2877
|
+
"manager",
|
|
2878
|
+
"rep"
|
|
2879
|
+
];
|
|
2880
|
+
async function runRbacSet(actor, role, dataDir) {
|
|
2881
|
+
const dir = dataDir ?? process.cwd();
|
|
2882
|
+
if (!ROLES.includes(role)) {
|
|
2883
|
+
console.error(error(`✗ Invalid role '${role}'. Must be: ${ROLES.join(", ")}`));
|
|
2884
|
+
process.exit(1);
|
|
2885
|
+
}
|
|
2886
|
+
setActorRole(dir, actor, role);
|
|
2887
|
+
console.log(success(`✓ ${bold(actor)} → ${bold(role)}`));
|
|
2888
|
+
}
|
|
2889
|
+
async function runRbacShow(dataDir) {
|
|
2890
|
+
const config = getRbacConfig(dataDir ?? process.cwd());
|
|
2891
|
+
const entries = Object.entries(config.actors);
|
|
2892
|
+
if (entries.length === 0) {
|
|
2893
|
+
console.log(info("No RBAC roles configured. All actors default to 'rep'."));
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
console.log(bold(`\n RBAC Roles\n`));
|
|
2897
|
+
for (const [actor, role] of entries) console.log(info(` ${bold(actor).padEnd(20)} ${role}`));
|
|
2898
|
+
if (config.default) console.log(info(`\n Default: ${config.default}`));
|
|
2899
|
+
console.log("");
|
|
2900
|
+
}
|
|
2901
|
+
async function runRbacCheck(actor, tool, dataDir) {
|
|
2902
|
+
const dir = dataDir ?? process.cwd();
|
|
2903
|
+
const { getRole } = await import("./rbac-C7c8tcES.js");
|
|
2904
|
+
const role = getRole(dir, actor);
|
|
2905
|
+
if (canWrite(role, tool)) console.log(success(`✓ ${actor} (${role}) CAN use '${tool}'`));
|
|
2906
|
+
else console.log(error(`✗ ${actor} (${role}) CANNOT use '${tool}'`));
|
|
2907
|
+
}
|
|
2908
|
+
const rbacCommand = new Command("rbac").description("Manage role-based access control");
|
|
2909
|
+
rbacCommand.command("set <actor> <role>").description(`Assign role to actor (roles: ${ROLES.join(", ")})`).action((actor, role) => runRbacSet(actor, role, process.env["DXCRM_DATA_DIR"]));
|
|
2910
|
+
rbacCommand.command("show").alias("list").description("List all RBAC role assignments").action(() => runRbacShow(process.env["DXCRM_DATA_DIR"]));
|
|
2911
|
+
rbacCommand.command("check <actor> <tool>").description("Check if an actor can use a specific tool").action((actor, tool) => runRbacCheck(actor, tool, process.env["DXCRM_DATA_DIR"]));
|
|
2912
|
+
//#endregion
|
|
2913
|
+
//#region src/commands/gdpr.ts
|
|
2914
|
+
function erasuresPath(dataDir) {
|
|
2915
|
+
return path.join(dataDir, ".agentic", "gdpr-erasures.json");
|
|
2916
|
+
}
|
|
2917
|
+
function readErasures(dataDir) {
|
|
2918
|
+
const p = erasuresPath(dataDir);
|
|
2919
|
+
if (!fs.existsSync(p)) return [];
|
|
2920
|
+
try {
|
|
2921
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
2922
|
+
} catch {
|
|
2923
|
+
return [];
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function appendErasure(dataDir, record) {
|
|
2927
|
+
const p = erasuresPath(dataDir);
|
|
2928
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
2929
|
+
const existing = readErasures(dataDir);
|
|
2930
|
+
existing.push(record);
|
|
2931
|
+
fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
|
|
2932
|
+
}
|
|
2933
|
+
async function runGdprErase(slug, opts, dataDir) {
|
|
2934
|
+
const dir = dataDir ?? process.cwd();
|
|
2935
|
+
const customerDir = path.join(dir, "customers", slug);
|
|
2936
|
+
if (!opts.confirm) {
|
|
2937
|
+
console.log(info(`Dry run — would permanently erase: ${bold(slug)}`));
|
|
2938
|
+
console.log(info(` Directory: ${customerDir}`));
|
|
2939
|
+
console.log(info(` Audit log entry will be written to .agentic/audit.log`));
|
|
2940
|
+
console.log(info(` Erasure record will be added to .agentic/gdpr-erasures.json`));
|
|
2941
|
+
console.log(info(`\n To proceed: dxcrm gdpr erase ${slug} --confirm`));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
if (!fs.existsSync(customerDir)) console.warn(info(` Customer '${slug}' directory not found — may already be erased.`));
|
|
2945
|
+
else {
|
|
2946
|
+
fs.rmSync(customerDir, {
|
|
2947
|
+
recursive: true,
|
|
2948
|
+
force: true
|
|
2949
|
+
});
|
|
2950
|
+
try {
|
|
2951
|
+
const { dropCustomerTable } = await import("./lancedb-CCBbpulq.js");
|
|
2952
|
+
await dropCustomerTable(dir, slug);
|
|
2953
|
+
} catch {}
|
|
2954
|
+
}
|
|
2955
|
+
const globalQueuePath = path.join(dir, ".agentic", "agent-queue.json");
|
|
2956
|
+
if (fs.existsSync(globalQueuePath)) await withJsonFile(globalQueuePath, (tasks) => (Array.isArray(tasks) ? tasks : []).filter((t) => t.slug !== slug));
|
|
2957
|
+
const goalsPath = path.join(dir, ".agentic", "goals.json");
|
|
2958
|
+
if (fs.existsSync(goalsPath)) {
|
|
2959
|
+
const { readGoals, writeGoals } = await import("./goal-engine-CUZSpERI.js");
|
|
2960
|
+
writeGoals(dir, readGoals(dir).map((g) => ({
|
|
2961
|
+
...g,
|
|
2962
|
+
decomposition: {
|
|
2963
|
+
...g.decomposition,
|
|
2964
|
+
subGoals: g.decomposition.subGoals.filter((sg) => sg.slug !== slug)
|
|
2965
|
+
}
|
|
2966
|
+
})));
|
|
2967
|
+
}
|
|
2968
|
+
const { readSubscriptions, writeSubscriptions } = await import("./push-manager-CowY-0IK.js");
|
|
2969
|
+
const subs = await readSubscriptions(dir);
|
|
2970
|
+
const remaining = subs.filter((s) => s.slug !== slug);
|
|
2971
|
+
if (remaining.length !== subs.length) await writeSubscriptions(dir, remaining);
|
|
2972
|
+
const actor = getActor();
|
|
2973
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2974
|
+
writeAuditEntry(dir, {
|
|
2975
|
+
timestamp: now,
|
|
2976
|
+
actor,
|
|
2977
|
+
tool: "gdpr_erase",
|
|
2978
|
+
slug,
|
|
2979
|
+
summary: "Customer data permanently erased"
|
|
2980
|
+
});
|
|
2981
|
+
appendErasure(dir, {
|
|
2982
|
+
slug,
|
|
2983
|
+
erasedAt: now,
|
|
2984
|
+
erasedBy: actor,
|
|
2985
|
+
reason: "GDPR Art. 17 request"
|
|
2986
|
+
});
|
|
2987
|
+
console.log(success(`✓ Customer '${bold(slug)}' erased.`));
|
|
2988
|
+
console.log(info(` Deletion logged to .agentic/audit.log`));
|
|
2989
|
+
console.log(info(` Record added to .agentic/gdpr-erasures.json`));
|
|
2990
|
+
}
|
|
2991
|
+
async function runGdprListErasures(dataDir) {
|
|
2992
|
+
const records = readErasures(dataDir ?? process.cwd());
|
|
2993
|
+
if (records.length === 0) {
|
|
2994
|
+
console.log(info("No erasures on record."));
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
console.log(bold(`\n GDPR Erasures (${records.length})\n`));
|
|
2998
|
+
for (const r of records) console.log(info(` ${r.erasedAt} ${r.erasedBy.padEnd(12)} ${r.slug} — ${r.reason}`));
|
|
2999
|
+
console.log("");
|
|
3000
|
+
}
|
|
3001
|
+
const gdprCommand = new Command("gdpr").description("GDPR compliance tools");
|
|
3002
|
+
gdprCommand.command("erase <slug>").description("Permanently erase all data for a customer (Art. 17 right to erasure)").option("--confirm", "Confirm permanent deletion (required)").action((slug, opts) => runGdprErase(slug, opts, process.env["DXCRM_DATA_DIR"]));
|
|
3003
|
+
gdprCommand.command("list-erasures").description("Show history of GDPR erasures").action(() => runGdprListErasures(process.env["DXCRM_DATA_DIR"]));
|
|
3004
|
+
//#endregion
|
|
3005
|
+
//#region src/commands/security-report.ts
|
|
3006
|
+
const REPORT = `# DatasynxOpenCRM — Security Report
|
|
3007
|
+
|
|
3008
|
+
Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
3009
|
+
|
|
3010
|
+
## 1. Data Storage
|
|
3011
|
+
|
|
3012
|
+
- **Location**: Local filesystem only. All data lives in \`customers/\` and \`.agentic/\` directories on your infrastructure.
|
|
3013
|
+
- **External transmission**: None by default. Data is never sent to Datasynx servers.
|
|
3014
|
+
- **Cloud dependencies**: None for core functionality. Optional integrations (Gmail, Anthropic API) are explicitly configured.
|
|
3015
|
+
|
|
3016
|
+
## 2. Authentication & Authorization
|
|
3017
|
+
|
|
3018
|
+
- **Phase 3**: No authentication on HTTP MCP server. Restrict access via firewall or VPN (port 3847 should be on private network only).
|
|
3019
|
+
- **Phase 4 (RBAC)**: Role-based access control via \`.agentic/rbac.json\`. Roles: admin, manager, rep. Enforced per MCP tool call.
|
|
3020
|
+
- **Actor identity**: \`DXCRM_ACTOR\` environment variable. No cryptographic identity in Phase 3.
|
|
3021
|
+
|
|
3022
|
+
## 3. Encryption
|
|
3023
|
+
|
|
3024
|
+
- **At rest**: Not encrypted at application level. Use OS-level disk encryption (LUKS on Linux, FileVault on macOS).
|
|
3025
|
+
- **In transit**: HTTP (no TLS) in Phase 3. Use a reverse proxy (nginx + Let's Encrypt) or VPN for TLS.
|
|
3026
|
+
- **Recommendation**: Deploy behind Tailscale or WireGuard for team access.
|
|
3027
|
+
|
|
3028
|
+
## 4. Audit Trail
|
|
3029
|
+
|
|
3030
|
+
- **File**: \`.agentic/audit.log\` — append-only, one line per entry.
|
|
3031
|
+
- **Format**: \`timestamp | actor | tool | customer | summary\`
|
|
3032
|
+
- **Coverage**: All write operations (\`log_interaction\`, \`update_deal\`, \`gdpr_erase\`).
|
|
3033
|
+
- **Tamper evidence**: Phase 3: none. Phase 4+: hash chaining planned.
|
|
3034
|
+
- **Retention**: Indefinite (append-only, never deleted by the application).
|
|
3035
|
+
|
|
3036
|
+
## 5. Network Calls
|
|
3037
|
+
|
|
3038
|
+
The following external services are contacted when configured:
|
|
3039
|
+
|
|
3040
|
+
| Service | When | Data sent |
|
|
3041
|
+
|---|---|---|
|
|
3042
|
+
| Gmail API | Gmail sync enabled + credentials configured | Email headers + snippets |
|
|
3043
|
+
| Anthropic API | \`ANTHROPIC_API_KEY\` set | Email/transcript content for summarization |
|
|
3044
|
+
| Telegram Bot API | Agent notifications enabled + token set | Customer slug + context excerpt (≤800 chars) |
|
|
3045
|
+
| Microsoft Graph | Microsoft sync configured | Email headers + snippets |
|
|
3046
|
+
|
|
3047
|
+
**No telemetry, no analytics, no usage data is sent to Datasynx.**
|
|
3048
|
+
|
|
3049
|
+
## 6. Data Residency
|
|
3050
|
+
|
|
3051
|
+
DatasynxOpenCRM runs entirely on customer-controlled infrastructure. Data never leaves the deployment environment without explicit integration configuration.
|
|
3052
|
+
|
|
3053
|
+
This makes EU data residency guarantees straightforward — a key differentiator vs cloud CRMs.
|
|
3054
|
+
|
|
3055
|
+
## 7. GDPR Compliance
|
|
3056
|
+
|
|
3057
|
+
- **Right to erasure (Art. 17)**: \`dxcrm gdpr erase <slug> --confirm\` permanently deletes all customer data.
|
|
3058
|
+
- **Erasure log**: \`.agentic/gdpr-erasures.json\` records what was deleted, when, and by whom.
|
|
3059
|
+
- **Audit trail**: Every write operation is attributed to an actor.
|
|
3060
|
+
- **Data portability (Art. 20)**: \`dxcrm export <slug>\` exports all data as JSON or Markdown.
|
|
3061
|
+
|
|
3062
|
+
## 8. SOC 2 Readiness
|
|
3063
|
+
|
|
3064
|
+
- **Audit log**: Available from Phase 3 (audit trail covers all write operations).
|
|
3065
|
+
- **SOC 2 Type 2**: Requires 6 months of consistent audit logs. Apply after Phase 4 completion.
|
|
3066
|
+
- **Security review questionnaire**: This document serves as the primary answer document.
|
|
3067
|
+
|
|
3068
|
+
## 9. Dependencies (Key Packages)
|
|
3069
|
+
|
|
3070
|
+
| Package | Purpose | Cloud dependency? |
|
|
3071
|
+
|---|---|---|
|
|
3072
|
+
| \`@modelcontextprotocol/sdk\` | MCP server | No |
|
|
3073
|
+
| \`@googleapis/gmail\`, \`@googleapis/calendar\` | Gmail/Calendar sync | Optional — only if configured |
|
|
3074
|
+
| \`@anthropic-ai/sdk\` | LLM summarization | Optional — only if API key set |
|
|
3075
|
+
| \`lancedb\` | Local vector search | No — embedded DB |
|
|
3076
|
+
| \`gray-matter\` | Markdown frontmatter | No |
|
|
3077
|
+
| \`commander\` | CLI framework | No |
|
|
3078
|
+
| \`zod\` | Schema validation | No |
|
|
3079
|
+
| \`cron\` | Background sync | No |
|
|
3080
|
+
|
|
3081
|
+
## 10. Incident Response
|
|
3082
|
+
|
|
3083
|
+
- **Data breach**: Filesystem-only — scope is limited to the deployment host.
|
|
3084
|
+
- **Revoke access**: Remove actor from \`.agentic/rbac.json\`, rotate \`DXCRM_ACTOR\` env var.
|
|
3085
|
+
- **Audit**: \`dxcrm audit --actor <actor>\` shows all actions by a specific user.
|
|
3086
|
+
`;
|
|
3087
|
+
async function runSecurityReport(opts, _dataDir) {
|
|
3088
|
+
const report = REPORT;
|
|
3089
|
+
if (opts.output) {
|
|
3090
|
+
const outputPath = path.resolve(opts.output);
|
|
3091
|
+
fs.writeFileSync(outputPath, report, "utf-8");
|
|
3092
|
+
console.log(success(`✓ Security report written to: ${outputPath}`));
|
|
3093
|
+
} else console.log(report);
|
|
3094
|
+
}
|
|
3095
|
+
const securityReportCommand = new Command("security-report").description("Generate security questionnaire answer document for enterprise reviews").option("--output <file>", "Write report to file instead of stdout").action((opts) => runSecurityReport(opts, process.env["DXCRM_DATA_DIR"]));
|
|
3096
|
+
//#endregion
|
|
3097
|
+
//#region src/commands/pipeline-stages.ts
|
|
3098
|
+
function printStagesTable(stages) {
|
|
3099
|
+
console.log(bold("\n Pipeline Stages\n"));
|
|
3100
|
+
console.log(info(` ${"ID".padEnd(20)} ${"Label".padEnd(20)} ${"Order".padEnd(8)} ${"Prob%".padEnd(8)} ${"Final".padEnd(6)} Color`));
|
|
3101
|
+
console.log(info(` ${"─".repeat(72)}`));
|
|
3102
|
+
for (const s of stages) {
|
|
3103
|
+
const prob = s.probability !== void 0 ? String(s.probability) : "-";
|
|
3104
|
+
const isFinal = s.isFinal ? "yes" : "no";
|
|
3105
|
+
const color = s.color ?? "-";
|
|
3106
|
+
console.log(info(` ${s.id.padEnd(20)} ${s.label.padEnd(20)} ${String(s.order).padEnd(8)} ${prob.padEnd(8)} ${isFinal.padEnd(6)} ${color}`));
|
|
3107
|
+
}
|
|
3108
|
+
console.log("");
|
|
3109
|
+
}
|
|
3110
|
+
const stagesCommand = new Command("stages").description("Manage custom pipeline stages");
|
|
3111
|
+
stagesCommand.command("list").description("List all configured pipeline stages").action(() => {
|
|
3112
|
+
printStagesTable(getPipelineStages(process.env["DXCRM_DATA_DIR"] ?? process.cwd()));
|
|
3113
|
+
});
|
|
3114
|
+
stagesCommand.command("set <id> <label>").description("Create or update a pipeline stage").option("--order <n>", "Sort order (number)", "1").option("--probability <n>", "Default win probability 0-100").option("--color <hex>", "Hex color code (e.g. #3B82F6)").option("--final", "Mark as final stage (won/lost)").action((id, label, opts) => {
|
|
3115
|
+
setPipelineStage(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
|
|
3116
|
+
id,
|
|
3117
|
+
label,
|
|
3118
|
+
order: parseInt(opts.order, 10),
|
|
3119
|
+
...opts.probability !== void 0 ? { probability: parseInt(opts.probability, 10) } : {},
|
|
3120
|
+
...opts.color ? { color: opts.color } : {},
|
|
3121
|
+
...opts.final ? { isFinal: true } : {}
|
|
3122
|
+
});
|
|
3123
|
+
console.log(success(`✓ Stage '${id}' saved`));
|
|
3124
|
+
});
|
|
3125
|
+
stagesCommand.command("delete <id>").description("Delete a pipeline stage by ID").action((id) => {
|
|
3126
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3127
|
+
if (!getPipelineStages(dataDir).find((s) => s.id === id)) {
|
|
3128
|
+
console.error(error(`✗ Stage '${id}' not found`));
|
|
3129
|
+
process.exit(1);
|
|
3130
|
+
}
|
|
3131
|
+
deletePipelineStage(dataDir, id);
|
|
3132
|
+
console.log(success(`✓ Stage '${id}' deleted`));
|
|
3133
|
+
});
|
|
3134
|
+
stagesCommand.command("reset").description("Reset pipeline stages to defaults").action(() => {
|
|
3135
|
+
resetToDefaults(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
3136
|
+
console.log(success("✓ Pipeline stages reset to defaults"));
|
|
3137
|
+
});
|
|
3138
|
+
//#endregion
|
|
3139
|
+
//#region src/core/plugin-registry.ts
|
|
3140
|
+
const _plugins = /* @__PURE__ */ new Map();
|
|
3141
|
+
function getPlugin(name) {
|
|
3142
|
+
return _plugins.get(name);
|
|
3143
|
+
}
|
|
3144
|
+
function listPlugins() {
|
|
3145
|
+
return Array.from(_plugins.values());
|
|
3146
|
+
}
|
|
3147
|
+
//#endregion
|
|
3148
|
+
//#region src/commands/plugin.ts
|
|
3149
|
+
const pluginCommand = new Command("plugin").description("Manage dxcrm plugins");
|
|
3150
|
+
pluginCommand.command("list").description("List all registered plugins").action(() => {
|
|
3151
|
+
const plugins = listPlugins();
|
|
3152
|
+
if (plugins.length === 0) {
|
|
3153
|
+
console.log(info("No plugins registered."));
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
console.log(bold(`\n dxcrm Plugins (${plugins.length})\n`));
|
|
3157
|
+
for (const p of plugins) console.log(info(` ${p.name.padEnd(20)} v${p.version} ${p.description ?? ""}`));
|
|
3158
|
+
console.log("");
|
|
3159
|
+
});
|
|
3160
|
+
pluginCommand.command("info <name>").description("Show info about a registered plugin").action((name) => {
|
|
3161
|
+
const plugin = getPlugin(name);
|
|
3162
|
+
if (!plugin) {
|
|
3163
|
+
console.log(error(`Plugin '${name}' not found.`));
|
|
3164
|
+
process.exit(1);
|
|
3165
|
+
}
|
|
3166
|
+
console.log(bold(`\n Plugin: ${plugin.name}\n`));
|
|
3167
|
+
console.log(info(` Version: ${plugin.version}`));
|
|
3168
|
+
if (plugin.description) console.log(info(` Description: ${plugin.description}`));
|
|
3169
|
+
if (plugin.mcpTools?.length) console.log(info(` MCP Tools: ${plugin.mcpTools.join(", ")}`));
|
|
3170
|
+
console.log("");
|
|
3171
|
+
});
|
|
3172
|
+
//#endregion
|
|
3173
|
+
//#region src/commands/goal.ts
|
|
3174
|
+
async function runGoalSet(description, options) {
|
|
3175
|
+
const goal = await pursueGoal(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
|
|
3176
|
+
description,
|
|
3177
|
+
deadline: options.deadline
|
|
3178
|
+
});
|
|
3179
|
+
console.log(success(`✓ Goal created: ${bold(goal.id)}`));
|
|
3180
|
+
console.log(info(` Description : ${goal.description}`));
|
|
3181
|
+
console.log(info(` Target : €${goal.target.toLocaleString()}`));
|
|
3182
|
+
console.log(info(` Deadline : ${goal.deadline}`));
|
|
3183
|
+
console.log(info(` Pipeline P50: €${goal.decomposition.currentPipeline.toLocaleString()}`));
|
|
3184
|
+
console.log(info(` Gap : €${goal.decomposition.gap.toLocaleString()}`));
|
|
3185
|
+
if (goal.decomposition.subGoals.length > 0) {
|
|
3186
|
+
console.log(bold("\n Action Plan:"));
|
|
3187
|
+
for (const sg of goal.decomposition.subGoals) {
|
|
3188
|
+
console.log(info(` ${sg.priority}. ${sg.action}`));
|
|
3189
|
+
console.log(info(` → ${sg.nextStep}`));
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
async function runGoalStatus() {
|
|
3194
|
+
const goals = getActiveGoals(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
3195
|
+
if (goals.length === 0) {
|
|
3196
|
+
console.log(info("No active goals. Use `dxcrm goal set` to create one."));
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
console.log(bold(`\n Active Goals (${goals.length})\n`));
|
|
3200
|
+
for (const g of goals) {
|
|
3201
|
+
const bar = "█".repeat(Math.round(g.progress / 10)) + "░".repeat(10 - Math.round(g.progress / 10));
|
|
3202
|
+
const deadlineMs = new Date(g.deadline).getTime() - Date.now();
|
|
3203
|
+
const daysLeft = Math.max(0, Math.ceil(deadlineMs / 864e5));
|
|
3204
|
+
console.log(bold(` ${g.id}`));
|
|
3205
|
+
console.log(info(` ${g.description}`));
|
|
3206
|
+
console.log(info(` [${bar}] ${g.progress}% | €${g.target.toLocaleString()} by ${g.deadline} (${daysLeft}d left)`));
|
|
3207
|
+
console.log("");
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async function runGoalUpdate(goalId, options) {
|
|
3211
|
+
const dir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3212
|
+
const progress = parseInt(options.progress, 10);
|
|
3213
|
+
if (isNaN(progress) || progress < 0 || progress > 100) {
|
|
3214
|
+
console.error(error("✗ --progress must be a number 0–100"));
|
|
3215
|
+
process.exit(1);
|
|
3216
|
+
}
|
|
3217
|
+
if (!await updateGoalProgress(dir, goalId, progress)) {
|
|
3218
|
+
console.error(error(`✗ Goal '${goalId}' not found`));
|
|
3219
|
+
process.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
console.log(success(`✓ Goal ${bold(goalId)} progress updated to ${bold(String(progress))}%`));
|
|
3222
|
+
}
|
|
3223
|
+
async function runGoalCancel(goalId) {
|
|
3224
|
+
if (!await cancelGoal(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), goalId)) {
|
|
3225
|
+
console.error(error(`✗ Goal '${goalId}' not found`));
|
|
3226
|
+
process.exit(1);
|
|
3227
|
+
}
|
|
3228
|
+
console.log(success(`✓ Goal ${bold(goalId)} cancelled`));
|
|
3229
|
+
}
|
|
3230
|
+
const goalCommand = new Command("goal").description("Manage goals and action plans");
|
|
3231
|
+
goalCommand.command("set <description>").description("Set a new goal and get a decomposed action plan").requiredOption("--deadline <date>", "Target deadline (YYYY-MM-DD)").action((description, opts) => runGoalSet(description, opts));
|
|
3232
|
+
goalCommand.command("status").description("Show all active goals with progress").action(() => runGoalStatus());
|
|
3233
|
+
goalCommand.command("update <goalId>").description("Update goal progress percentage").requiredOption("--progress <n>", "Progress 0–100").action((goalId, opts) => runGoalUpdate(goalId, opts));
|
|
3234
|
+
goalCommand.command("cancel <goalId>").description("Cancel an active goal").action((goalId) => runGoalCancel(goalId));
|
|
3235
|
+
//#endregion
|
|
3236
|
+
//#region src/commands/push.ts
|
|
3237
|
+
async function runPushRegister(slug, options) {
|
|
3238
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3239
|
+
const providerData = {};
|
|
3240
|
+
if (options.topicName) providerData["gmailTopicName"] = options.topicName;
|
|
3241
|
+
if (options.clientState) providerData["microsoftClientState"] = options.clientState;
|
|
3242
|
+
if (options.resource) providerData["microsoftResource"] = options.resource;
|
|
3243
|
+
if (options.teamId) providerData["slackTeamId"] = options.teamId;
|
|
3244
|
+
if (options.channelId) providerData["slackChannelId"] = options.channelId;
|
|
3245
|
+
const sub = await register(dataDir, options.provider, slug, {
|
|
3246
|
+
webhookUrl: options.webhookUrl,
|
|
3247
|
+
providerData
|
|
3248
|
+
});
|
|
3249
|
+
console.log(success(`✓ Push subscription registered: ${bold(sub.id)}`));
|
|
3250
|
+
console.log(info(` Provider : ${sub.provider}`));
|
|
3251
|
+
console.log(info(` Slug : ${sub.slug}`));
|
|
3252
|
+
console.log(info(` Webhook : ${sub.webhookUrl}`));
|
|
3253
|
+
console.log(info(` Expires : ${sub.expiresAt ?? "never"}`));
|
|
3254
|
+
if (options.webhookUrl.includes("localhost")) {
|
|
3255
|
+
console.log(info(` ⚠ Warning: localhost URLs cannot be reached by external providers.`));
|
|
3256
|
+
console.log(info(` Use a tunnel: ngrok http 3847`));
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
async function runPushStatus(options) {
|
|
3260
|
+
let subs = await readSubscriptions(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
3261
|
+
if (options.slug) subs = subs.filter((s) => s.slug === options.slug);
|
|
3262
|
+
if (options.provider) subs = subs.filter((s) => s.provider === options.provider);
|
|
3263
|
+
if (subs.length === 0) {
|
|
3264
|
+
console.log(info("No push subscriptions registered. Use `dxcrm push register` to add one."));
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
const now = Date.now();
|
|
3268
|
+
console.log(bold(`\n Push Subscriptions (${subs.length})\n`));
|
|
3269
|
+
for (const s of subs) {
|
|
3270
|
+
const expiresIn = s.expiresAt ? Math.round((new Date(s.expiresAt).getTime() - now) / (3600 * 1e3)) : null;
|
|
3271
|
+
const expiryStr = expiresIn !== null ? `${expiresIn}h remaining` : "no expiry";
|
|
3272
|
+
const needsRenewal = s.expiresAt !== null && new Date(s.expiresAt).getTime() - now < 1440 * 60 * 1e3;
|
|
3273
|
+
console.log(bold(` ${s.id}`));
|
|
3274
|
+
console.log(info(` ${s.provider} → ${s.slug} [${s.status}]`));
|
|
3275
|
+
console.log(info(` Events: ${s.eventsProcessed} | Expires: ${expiryStr}${needsRenewal ? " ⚠ RENEW SOON" : ""}`));
|
|
3276
|
+
console.log(info(` Last event: ${s.lastEventAt ?? "—"}`));
|
|
3277
|
+
console.log("");
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
async function runPushRevoke(id) {
|
|
3281
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3282
|
+
try {
|
|
3283
|
+
await revoke(dataDir, id);
|
|
3284
|
+
console.log(success(`✓ Subscription ${bold(id)} revoked`));
|
|
3285
|
+
} catch {
|
|
3286
|
+
console.error(error(`✗ Subscription not found: ${id}`));
|
|
3287
|
+
process.exit(1);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
async function runPushRenew(options) {
|
|
3291
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3292
|
+
if (options.id) {
|
|
3293
|
+
const sub = (await readSubscriptions(dataDir)).find((s) => s.id === options.id);
|
|
3294
|
+
if (!sub) {
|
|
3295
|
+
console.error(error(`✗ Subscription not found: ${options.id}`));
|
|
3296
|
+
process.exit(1);
|
|
3297
|
+
}
|
|
3298
|
+
console.log(info(` Renewal for ${sub.id} requires provider-specific logic.`));
|
|
3299
|
+
console.log(info(` Use the daemon's automatic renewal (daily 06:00) or re-register.`));
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
const result = await renewExpiringSubscriptions(dataDir, async () => {
|
|
3303
|
+
throw new Error("No default renew function — use provider-specific tooling");
|
|
3304
|
+
}, 24);
|
|
3305
|
+
console.log(info(` Renewed: ${result.renewed.length} | Errors: ${result.errors.length}`));
|
|
3306
|
+
if (result.renewed.length > 0) console.log(success(`✓ Renewed: ${result.renewed.join(", ")}`));
|
|
3307
|
+
if (result.errors.length > 0) console.log(error(`✗ Errors: ${result.errors.join(", ")}`));
|
|
3308
|
+
}
|
|
3309
|
+
const pushCommand = new Command("push").description("Manage real-time push subscriptions (Gmail Pub/Sub, MS Graph, Slack Events)");
|
|
3310
|
+
pushCommand.command("register <slug>").description("Register a push subscription for a customer").requiredOption("--provider <provider>", "Provider: gmail | microsoft-graph | slack").requiredOption("--webhook-url <url>", "Public HTTPS URL for provider callbacks").option("--topic-name <topic>", "Gmail: Cloud Pub/Sub topic name").option("--client-state <secret>", "MS Graph: client state secret").option("--resource <path>", "MS Graph: resource path").option("--team-id <id>", "Slack: workspace team ID").option("--channel-id <id>", "Slack: optional channel ID").action(async (slug, opts) => {
|
|
3311
|
+
await runPushRegister(slug, opts);
|
|
3312
|
+
});
|
|
3313
|
+
pushCommand.command("status").description("Show all push subscriptions").option("--slug <slug>", "Filter by customer slug").option("--provider <provider>", "Filter by provider").action(async (opts) => {
|
|
3314
|
+
await runPushStatus(opts);
|
|
3315
|
+
});
|
|
3316
|
+
pushCommand.command("revoke <id>").description("Revoke a push subscription by ID").action(async (id) => {
|
|
3317
|
+
await runPushRevoke(id);
|
|
3318
|
+
});
|
|
3319
|
+
pushCommand.command("renew").description("Renew expiring push subscriptions").option("--all", "Renew all expiring subscriptions").option("--id <id>", "Renew a specific subscription by ID").action(async (opts) => {
|
|
3320
|
+
await runPushRenew(opts);
|
|
3321
|
+
});
|
|
3322
|
+
//#endregion
|
|
3323
|
+
//#region src/commands/attach.ts
|
|
3324
|
+
async function runAttach(slug, filePath, dataDir) {
|
|
3325
|
+
const dir = dataDir ?? process.cwd();
|
|
3326
|
+
const customerDir = path.join(dir, "customers", slug);
|
|
3327
|
+
if (!fs.existsSync(customerDir)) {
|
|
3328
|
+
const msg = `Customer '${slug}' not found.`;
|
|
3329
|
+
console.error(error(msg));
|
|
3330
|
+
return { error: msg };
|
|
3331
|
+
}
|
|
3332
|
+
if (!fs.existsSync(filePath)) {
|
|
3333
|
+
const msg = `File not found: ${filePath}`;
|
|
3334
|
+
console.error(error(msg));
|
|
3335
|
+
return { error: msg };
|
|
3336
|
+
}
|
|
3337
|
+
const attachmentsDir = path.join(customerDir, "attachments");
|
|
3338
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
3339
|
+
const filename = path.basename(filePath);
|
|
3340
|
+
const dest = path.join(attachmentsDir, filename);
|
|
3341
|
+
if (fs.existsSync(dest)) {
|
|
3342
|
+
const msg = `Attachment already exists: ${filename}`;
|
|
3343
|
+
console.log(info(msg));
|
|
3344
|
+
return { attached: dest };
|
|
3345
|
+
}
|
|
3346
|
+
fs.copyFileSync(filePath, dest);
|
|
3347
|
+
console.log(success(`Attached ${bold(filename)} to ${bold(slug)}`));
|
|
3348
|
+
return { attached: dest };
|
|
3349
|
+
}
|
|
3350
|
+
const attachCommand = new Command("attach").description("Attach a file to a customer (copies to customers/<slug>/attachments/)").argument("<slug>", "Customer slug").argument("<file>", "Path to the file to attach").action(async (slug, file) => {
|
|
3351
|
+
await runAttach(slug, file, process.env["DXCRM_DATA_DIR"]);
|
|
3352
|
+
});
|
|
3353
|
+
//#endregion
|
|
3354
|
+
//#region src/commands/template.ts
|
|
3355
|
+
const templateCommand = new Command("template").description("Manage email templates");
|
|
3356
|
+
templateCommand.command("list").option("--category <category>", "Filter by category").action((opts) => {
|
|
3357
|
+
const templates = listTemplates(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), opts.category ? { category: opts.category } : {});
|
|
3358
|
+
if (templates.length === 0) {
|
|
3359
|
+
console.log(info("No templates found."));
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
for (const t of templates) console.log(` ${bold(t.id)} [${t.category}] ${t.subject}`);
|
|
3363
|
+
});
|
|
3364
|
+
templateCommand.command("get <id>").action((id) => {
|
|
3365
|
+
const tmpl = getTemplate(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id);
|
|
3366
|
+
if (!tmpl) {
|
|
3367
|
+
console.error(error(`Template '${id}' not found`));
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
console.log(bold(`Subject: ${tmpl.subject}`));
|
|
3371
|
+
console.log(`Category: ${tmpl.category} Language: ${tmpl.language}`);
|
|
3372
|
+
console.log(`Variables: ${tmpl.variables.join(", ") || "(none defined)"}`);
|
|
3373
|
+
console.log("\n" + tmpl.body);
|
|
3374
|
+
});
|
|
3375
|
+
templateCommand.command("preview <id>").option("--slug <slug>", "Customer slug to preview with").action(async (id, opts) => {
|
|
3376
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3377
|
+
const tmpl = getTemplate(dataDir, id);
|
|
3378
|
+
if (!tmpl) {
|
|
3379
|
+
console.error(error(`Template '${id}' not found`));
|
|
3380
|
+
process.exit(1);
|
|
3381
|
+
}
|
|
3382
|
+
const vars = opts.slug ? await buildVariablesFromCustomer(dataDir, opts.slug) : {};
|
|
3383
|
+
console.log(bold(`Subject: ${interpolate(tmpl.subject, vars)}`));
|
|
3384
|
+
console.log(interpolate(tmpl.body, vars));
|
|
3385
|
+
});
|
|
3386
|
+
templateCommand.command("create <id>").option("--category <category>", "Category", "general").option("--subject <subject>", "Subject line").action((id, opts) => {
|
|
3387
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3388
|
+
if (getTemplate(dataDir, id)) {
|
|
3389
|
+
console.error(error(`Template '${id}' already exists`));
|
|
3390
|
+
process.exit(1);
|
|
3391
|
+
}
|
|
3392
|
+
writeTemplate(dataDir, {
|
|
3393
|
+
id,
|
|
3394
|
+
subject: opts.subject ?? `Subject for ${id}`,
|
|
3395
|
+
category: opts.category,
|
|
3396
|
+
variables: [],
|
|
3397
|
+
language: "de",
|
|
3398
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3399
|
+
body: `Hi {{firstName}},\n\n[your message here]\n\nMit freundlichen Grüßen,\n{{senderName}}`
|
|
3400
|
+
});
|
|
3401
|
+
console.log(success(`✓ Template '${id}' created in category '${opts.category}'`));
|
|
3402
|
+
});
|
|
3403
|
+
templateCommand.command("delete <id>").action((id) => {
|
|
3404
|
+
if (deleteTemplate(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id)) console.log(success(`✓ Template '${id}' deleted`));
|
|
3405
|
+
else {
|
|
3406
|
+
console.error(error(`Template '${id}' not found`));
|
|
3407
|
+
process.exit(1);
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
//#endregion
|
|
3411
|
+
//#region src/commands/sequence.ts
|
|
3412
|
+
const sequenceCommand = new Command("sequence").description("Manage email sequences");
|
|
3413
|
+
sequenceCommand.command("list").description("List all sequences").action(() => {
|
|
3414
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3415
|
+
const sequences = listSequences(dataDir);
|
|
3416
|
+
const enrollments = readEnrollments(dataDir);
|
|
3417
|
+
if (sequences.length === 0) {
|
|
3418
|
+
console.log(info("No sequences found."));
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
for (const seq of sequences) {
|
|
3422
|
+
const count = enrollments.filter((e) => e.sequenceId === seq.id).length;
|
|
3423
|
+
console.log(` ${bold(seq.id)} "${seq.name}" ${seq.steps.length} steps ${count} enrolled`);
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
sequenceCommand.command("create <id>").description("Create a new sequence skeleton").option("--name <name>", "Sequence display name").action((id, opts) => {
|
|
3427
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3428
|
+
if (getSequence(dataDir, id)) {
|
|
3429
|
+
console.error(error(`Sequence '${id}' already exists`));
|
|
3430
|
+
process.exit(1);
|
|
3431
|
+
}
|
|
3432
|
+
const seq = {
|
|
3433
|
+
id,
|
|
3434
|
+
name: opts.name ?? id,
|
|
3435
|
+
steps: [
|
|
3436
|
+
{
|
|
3437
|
+
day: 0,
|
|
3438
|
+
templateId: "intro",
|
|
3439
|
+
skipIfReplied: true
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
day: 3,
|
|
3443
|
+
templateId: "followup-1",
|
|
3444
|
+
skipIfReplied: true
|
|
3445
|
+
},
|
|
3446
|
+
{
|
|
3447
|
+
day: 7,
|
|
3448
|
+
templateId: "followup-2",
|
|
3449
|
+
skipIfReplied: true
|
|
3450
|
+
}
|
|
3451
|
+
],
|
|
3452
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3453
|
+
};
|
|
3454
|
+
writeSequence(dataDir, seq);
|
|
3455
|
+
console.log(success(`✓ Sequence '${id}' created with ${seq.steps.length} steps`));
|
|
3456
|
+
console.log(info(`Edit .agentic/sequences/${id}.yaml to customize steps and templates`));
|
|
3457
|
+
});
|
|
3458
|
+
sequenceCommand.command("enroll <slug>").description("Enroll a customer contact in a sequence").requiredOption("--email <email>", "Contact email address").requiredOption("--sequence <id>", "Sequence ID").action(async (slug, opts) => {
|
|
3459
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3460
|
+
const seq = getSequence(dataDir, opts.sequence);
|
|
3461
|
+
if (!seq) {
|
|
3462
|
+
console.error(error(`Sequence '${opts.sequence}' not found`));
|
|
3463
|
+
process.exit(1);
|
|
3464
|
+
}
|
|
3465
|
+
const enrollmentId = `enroll_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
3466
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3467
|
+
await writeEnrollment(dataDir, {
|
|
3468
|
+
id: enrollmentId,
|
|
3469
|
+
sequenceId: opts.sequence,
|
|
3470
|
+
slug,
|
|
3471
|
+
contactEmail: opts.email,
|
|
3472
|
+
enrolledAt: now,
|
|
3473
|
+
status: "active",
|
|
3474
|
+
currentStep: 0,
|
|
3475
|
+
stepsCompleted: []
|
|
3476
|
+
});
|
|
3477
|
+
console.log(success(`✓ Enrolled ${opts.email} in sequence '${seq.name}'`));
|
|
3478
|
+
console.log(info(`Enrollment ID: ${enrollmentId}`));
|
|
3479
|
+
});
|
|
3480
|
+
sequenceCommand.command("status").description("Show enrollment status").option("--slug <slug>", "Filter by customer slug").action((opts) => {
|
|
3481
|
+
let enrollments = readEnrollments(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
3482
|
+
if (opts.slug) enrollments = enrollments.filter((e) => e.slug === opts.slug);
|
|
3483
|
+
if (enrollments.length === 0) {
|
|
3484
|
+
console.log(info("No enrollments found."));
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
for (const e of enrollments) {
|
|
3488
|
+
const stepInfo = `step ${e.currentStep}`;
|
|
3489
|
+
const lastSent = e.lastSentAt ? ` last sent ${e.lastSentAt.slice(0, 10)}` : "";
|
|
3490
|
+
console.log(` ${bold(e.id)} ${e.slug} <${e.contactEmail}> seq:${e.sequenceId} [${e.status}] ${stepInfo}${lastSent}`);
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
sequenceCommand.command("run").description("Run the sequence cycle (send due emails)").option("--dry-run", "Show what would be sent without sending").action(async (opts) => {
|
|
3494
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3495
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3496
|
+
if (opts.dryRun) {
|
|
3497
|
+
const enrollments = readEnrollments(dataDir).filter((e) => e.status === "active");
|
|
3498
|
+
console.log(info(`Dry run — ${enrollments.length} active enrollments for ${today}`));
|
|
3499
|
+
for (const e of enrollments) console.log(` Would process: ${e.id} (${e.contactEmail}, step ${e.currentStep})`);
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
const result = await runSequenceCycle(dataDir, today);
|
|
3503
|
+
console.log(success(`✓ Cycle complete: ${result.sent} sent, ${result.completed} completed, ${result.errors.length} errors`));
|
|
3504
|
+
if (result.errors.length > 0) for (const e of result.errors) console.error(error(` Error: ${e}`));
|
|
3505
|
+
});
|
|
3506
|
+
//#endregion
|
|
3507
|
+
//#region src/commands/quote.ts
|
|
3508
|
+
const quoteCommand = new Command("quote").description("Manage customer quotes");
|
|
3509
|
+
quoteCommand.command("generate <slug>").description("Generate a quote for a customer").requiredOption("--deal <name>", "Deal name").option("--items <items>", "Line items: \"Description Qty Price,...\" (e.g. \"Consulting 1 5000,Support 12 500\")").option("--vat <percent>", "VAT percent", "19").option("--valid <days>", "Valid for N days", "30").action(async (slug, opts) => {
|
|
3510
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3511
|
+
const lineItems = (opts.items ?? "Service 1 1000").split(",").map((item) => item.trim()).filter(Boolean).map((item) => {
|
|
3512
|
+
const parts = item.split(/\s+/);
|
|
3513
|
+
const unitPrice = parseFloat(parts[parts.length - 1] ?? "0");
|
|
3514
|
+
const quantity = parseFloat(parts[parts.length - 2] ?? "1");
|
|
3515
|
+
return {
|
|
3516
|
+
description: parts.slice(0, -2).join(" ") || item,
|
|
3517
|
+
quantity,
|
|
3518
|
+
unitPrice
|
|
3519
|
+
};
|
|
3520
|
+
});
|
|
3521
|
+
try {
|
|
3522
|
+
const quote = await generateQuote(dataDir, {
|
|
3523
|
+
slug,
|
|
3524
|
+
dealName: opts.deal,
|
|
3525
|
+
lineItems,
|
|
3526
|
+
vatPercent: parseFloat(opts.vat),
|
|
3527
|
+
validUntilDays: parseInt(opts.valid, 10)
|
|
3528
|
+
});
|
|
3529
|
+
console.log(success(`✓ Quote ${bold(quote.quoteNumber)} generated`));
|
|
3530
|
+
console.log(info(` Total: ${quote.total.toFixed(2)} ${quote.currency}`));
|
|
3531
|
+
console.log(info(` Valid until: ${quote.validUntil}`));
|
|
3532
|
+
console.log(info(` HTML: ${quote.htmlPath}`));
|
|
3533
|
+
} catch (err) {
|
|
3534
|
+
console.error(error(`Failed to generate quote: ${err.message}`));
|
|
3535
|
+
process.exit(1);
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3538
|
+
quoteCommand.command("list").description("List quotes").option("--slug <slug>", "Filter by customer slug").action((opts) => {
|
|
3539
|
+
const quotes = listQuotes(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), opts.slug);
|
|
3540
|
+
if (quotes.length === 0) {
|
|
3541
|
+
console.log(info("No quotes found."));
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
for (const q of quotes) console.log(` ${bold(q.quoteNumber)} ${q.slug} ${q.dealName} ${q.total.toFixed(2)} ${q.currency} [${q.status}] ${q.validUntil}`);
|
|
3545
|
+
});
|
|
3546
|
+
quoteCommand.command("get <quoteNumber>").description("Show quote details").action((quoteNumber) => {
|
|
3547
|
+
const quote = readQuote(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), quoteNumber);
|
|
3548
|
+
if (!quote) {
|
|
3549
|
+
console.error(error(`Quote '${quoteNumber}' not found`));
|
|
3550
|
+
process.exit(1);
|
|
3551
|
+
}
|
|
3552
|
+
console.log(bold(`Quote: ${quote.quoteNumber}`));
|
|
3553
|
+
console.log(`Customer: ${quote.slug} Deal: ${quote.dealName}`);
|
|
3554
|
+
console.log(`Status: ${quote.status} Valid until: ${quote.validUntil}`);
|
|
3555
|
+
console.log(`Subtotal: ${quote.subtotal.toFixed(2)} VAT: ${quote.vat.toFixed(2)} Total: ${quote.total.toFixed(2)} ${quote.currency}`);
|
|
3556
|
+
for (const item of quote.lineItems) console.log(` - ${item.description}: ${item.quantity} × ${item.unitPrice} = ${item.total}`);
|
|
3557
|
+
});
|
|
3558
|
+
//#endregion
|
|
3559
|
+
//#region src/commands/ticket.ts
|
|
3560
|
+
const ticketCommand = new Command("ticket").description("Manage support tickets");
|
|
3561
|
+
ticketCommand.command("list").description("List tickets").option("--slug <slug>", "Filter by customer slug").option("--status <status>", "Filter by status").option("--priority <priority>", "Filter by priority").action(async (opts) => {
|
|
3562
|
+
const tickets = await listAllTickets(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
|
|
3563
|
+
...opts.slug ? { slug: opts.slug } : {},
|
|
3564
|
+
...opts.status ? { status: opts.status } : {},
|
|
3565
|
+
...opts.priority ? { priority: opts.priority } : {}
|
|
3566
|
+
});
|
|
3567
|
+
if (tickets.length === 0) {
|
|
3568
|
+
console.log(info("No tickets found."));
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
for (const { slug, ticket: t } of tickets) {
|
|
3572
|
+
const flag = t.slaDue && t.slaDue < (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) && t.status !== "resolved" && t.status !== "closed" ? " ⚠ SLA" : "";
|
|
3573
|
+
console.log(` ${bold(t.id)} [${slug}] ${t.title} [${t.status}/${t.priority}]${t.assignee ? ` @${t.assignee}` : ""}${flag}`);
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
ticketCommand.command("create <slug>").description("Create a ticket for a customer").requiredOption("--title <title>", "Ticket title").option("--description <desc>", "Description").option("--priority <priority>", "Priority: urgent|high|normal|low", "normal").option("--assignee <name>", "Assignee").action(async (slug, opts) => {
|
|
3577
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3578
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3579
|
+
const rules = loadSlaRules(dataDir);
|
|
3580
|
+
const priority = opts.priority ?? "normal";
|
|
3581
|
+
const id = nextTicketId(await readTickets(dataDir, slug));
|
|
3582
|
+
const ticket = {
|
|
3583
|
+
id,
|
|
3584
|
+
title: opts.title,
|
|
3585
|
+
status: "open",
|
|
3586
|
+
priority,
|
|
3587
|
+
...opts.assignee ? { assignee: opts.assignee } : {},
|
|
3588
|
+
created: today,
|
|
3589
|
+
slaDue: calcSlaDue(today, priority, rules),
|
|
3590
|
+
...opts.description ? { description: opts.description } : {}
|
|
3591
|
+
};
|
|
3592
|
+
await upsertTicket(dataDir, slug, ticket);
|
|
3593
|
+
console.log(success(`✓ Ticket ${bold(id)} created for ${slug}`));
|
|
3594
|
+
console.log(info(` SLA due: ${ticket.slaDue}`));
|
|
3595
|
+
});
|
|
3596
|
+
ticketCommand.command("update <ticketId>").description("Update a ticket").requiredOption("--slug <slug>", "Customer slug").option("--status <status>", "New status").option("--assignee <name>", "New assignee").action(async (ticketId, opts) => {
|
|
3597
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3598
|
+
const ticket = (await readTickets(dataDir, opts.slug)).find((t) => t.id === ticketId);
|
|
3599
|
+
if (!ticket) {
|
|
3600
|
+
console.error(error(`Ticket '${ticketId}' not found`));
|
|
3601
|
+
process.exit(1);
|
|
3602
|
+
}
|
|
3603
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3604
|
+
const updated = {
|
|
3605
|
+
...ticket,
|
|
3606
|
+
...opts.status ? { status: opts.status } : {},
|
|
3607
|
+
...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
|
|
3608
|
+
...opts.status === "resolved" && !ticket.resolved ? { resolved: today } : {}
|
|
3609
|
+
};
|
|
3610
|
+
await upsertTicket(dataDir, opts.slug, updated);
|
|
3611
|
+
console.log(success(`✓ Ticket ${bold(ticketId)} updated`));
|
|
3612
|
+
});
|
|
3613
|
+
ticketCommand.command("close <ticketId>").description("Close a ticket").requiredOption("--slug <slug>", "Customer slug").option("--resolution <text>", "Resolution notes").action(async (ticketId, opts) => {
|
|
3614
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3615
|
+
const ticket = (await readTickets(dataDir, opts.slug)).find((t) => t.id === ticketId);
|
|
3616
|
+
if (!ticket) {
|
|
3617
|
+
console.error(error(`Ticket '${ticketId}' not found`));
|
|
3618
|
+
process.exit(1);
|
|
3619
|
+
}
|
|
3620
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3621
|
+
const updated = {
|
|
3622
|
+
...ticket,
|
|
3623
|
+
status: "closed",
|
|
3624
|
+
resolved: ticket.resolved ?? today
|
|
3625
|
+
};
|
|
3626
|
+
await upsertTicket(dataDir, opts.slug, updated);
|
|
3627
|
+
console.log(success(`✓ Ticket ${bold(ticketId)} closed`));
|
|
3628
|
+
});
|
|
3629
|
+
//#endregion
|
|
3630
|
+
//#region src/commands/survey.ts
|
|
3631
|
+
const surveyCommand = new Command("survey").description("Manage NPS/CSAT surveys");
|
|
3632
|
+
surveyCommand.command("list").description("List all survey definitions").action(() => {
|
|
3633
|
+
const surveys = listSurveys(process.env["DXCRM_DATA_DIR"] ?? process.cwd());
|
|
3634
|
+
if (surveys.length === 0) {
|
|
3635
|
+
console.log(info("No surveys found."));
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
for (const s of surveys) console.log(` ${bold(s.id)} [${s.type}] ${s.question.slice(0, 60)}`);
|
|
3639
|
+
});
|
|
3640
|
+
surveyCommand.command("create <id>").description("Create a new survey definition").option("--type <type>", "Survey type: nps|csat|ces", "nps").option("--question <q>", "Survey question").action((id, opts) => {
|
|
3641
|
+
writeSurvey(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
|
|
3642
|
+
id,
|
|
3643
|
+
type: opts.type ?? "nps",
|
|
3644
|
+
question: opts.question ?? "How likely are you to recommend us? (0–10)",
|
|
3645
|
+
scale: {
|
|
3646
|
+
min: 0,
|
|
3647
|
+
max: 10
|
|
3648
|
+
},
|
|
3649
|
+
includeComment: true,
|
|
3650
|
+
commentPrompt: "What's the main reason for your score?",
|
|
3651
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3652
|
+
});
|
|
3653
|
+
console.log(success(`✓ Survey '${id}' created`));
|
|
3654
|
+
});
|
|
3655
|
+
surveyCommand.command("send <surveyId>").description("Generate survey token for a contact").requiredOption("--slug <slug>", "Customer slug").requiredOption("--email <email>", "Contact email").option("--server <url>", "Server URL", process.env["DXCRM_SERVER_URL"] ?? "http://localhost:3456").action(async (surveyId, opts) => {
|
|
3656
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3657
|
+
if (!getSurvey(dataDir, surveyId)) {
|
|
3658
|
+
console.error(error(`Survey '${surveyId}' not found`));
|
|
3659
|
+
process.exit(1);
|
|
3660
|
+
}
|
|
3661
|
+
const token = generateSurveyToken(opts.slug, opts.email, surveyId);
|
|
3662
|
+
await savePendingSurvey(dataDir, surveyId, opts.slug, opts.email, token);
|
|
3663
|
+
console.log(success(`✓ Survey token generated`));
|
|
3664
|
+
console.log(info(` URL: ${opts.server}/survey/respond?token=${token}`));
|
|
3665
|
+
});
|
|
3666
|
+
surveyCommand.command("results <surveyId>").description("Show survey results and NPS score").option("--slug <slug>", "Filter by customer slug").action((surveyId, opts) => {
|
|
3667
|
+
const responses = loadSurveyResponses(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), surveyId, opts.slug);
|
|
3668
|
+
const nps = calcNpsScore(responses);
|
|
3669
|
+
const promoters = responses.filter((r) => r.score >= 9).length;
|
|
3670
|
+
const detractors = responses.filter((r) => r.score <= 6).length;
|
|
3671
|
+
console.log(bold(`Survey: ${surveyId}`));
|
|
3672
|
+
console.log(info(`Responses: ${responses.length} NPS: ${nps} Promoters: ${promoters} Detractors: ${detractors}`));
|
|
3673
|
+
for (const r of responses) console.log(` ${r.slug} <${r.contactEmail}> score=${r.score}${r.comment ? ` "${r.comment.slice(0, 80)}"` : ""}`);
|
|
3674
|
+
});
|
|
3675
|
+
//#endregion
|
|
3676
|
+
//#region src/commands/kb.ts
|
|
3677
|
+
const kbCommand = new Command("kb").description("Manage the knowledge base");
|
|
3678
|
+
kbCommand.command("list").description("List all KB articles").option("--category <cat>", "Filter by category").option("--public", "Show only public articles").action((opts) => {
|
|
3679
|
+
const articles = listKbArticles(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), {
|
|
3680
|
+
...opts.category ? { category: opts.category } : {},
|
|
3681
|
+
...opts.public ? { publicOnly: true } : {}
|
|
3682
|
+
});
|
|
3683
|
+
if (articles.length === 0) {
|
|
3684
|
+
console.log(info("No articles found."));
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
for (const a of articles) {
|
|
3688
|
+
const pub = a.public ? " [public]" : "";
|
|
3689
|
+
console.log(` ${bold(a.id)} [${a.category}] ${a.title}${pub}`);
|
|
3690
|
+
}
|
|
3691
|
+
});
|
|
3692
|
+
kbCommand.command("get <id>").description("Get a KB article").action((id) => {
|
|
3693
|
+
const article = getKbArticle(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id);
|
|
3694
|
+
if (!article) {
|
|
3695
|
+
console.error(error(`Article '${id}' not found`));
|
|
3696
|
+
process.exit(1);
|
|
3697
|
+
}
|
|
3698
|
+
console.log(bold(article.title));
|
|
3699
|
+
console.log(`Category: ${article.category} Tags: ${article.tags.join(", ") || "(none)"}`);
|
|
3700
|
+
console.log("\n" + article.body);
|
|
3701
|
+
});
|
|
3702
|
+
kbCommand.command("search <query>").description("Search KB articles").option("--public", "Search only public articles").action((query, opts) => {
|
|
3703
|
+
const results = searchKbSimple(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), query, opts.public ? { publicOnly: true } : {});
|
|
3704
|
+
if (results.length === 0) {
|
|
3705
|
+
console.log(info("No results."));
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
for (const a of results) {
|
|
3709
|
+
console.log(` ${bold(a.id)} ${a.title}`);
|
|
3710
|
+
console.log(` ${a.body.slice(0, 120).replace(/\n/g, " ")}...`);
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
kbCommand.command("create <id>").description("Create a KB article (opens editor-ready template)").requiredOption("--title <title>", "Article title").option("--category <cat>", "Category", "general").option("--ticket <id>", "Source ticket ID").action((id, opts) => {
|
|
3714
|
+
const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3715
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3716
|
+
writeKbArticle(dataDir, {
|
|
3717
|
+
id,
|
|
3718
|
+
title: opts.title,
|
|
3719
|
+
category: opts.category,
|
|
3720
|
+
tags: [],
|
|
3721
|
+
public: false,
|
|
3722
|
+
createdAt: now,
|
|
3723
|
+
updatedAt: now,
|
|
3724
|
+
...opts.ticket ? { sourceTicketId: opts.ticket } : {},
|
|
3725
|
+
body: `## Problem\n\n[Describe the problem]\n\n## Solution\n\n[Describe the solution]`
|
|
3726
|
+
});
|
|
3727
|
+
console.log(success(`✓ Article '${id}' created in category '${opts.category}'`));
|
|
3728
|
+
console.log(info(`Edit: .agentic/knowledge-base/${opts.category}/${id}.md`));
|
|
3729
|
+
});
|
|
3730
|
+
kbCommand.command("delete <id>").description("Delete a KB article").action((id) => {
|
|
3731
|
+
if (deleteKbArticle(process.env["DXCRM_DATA_DIR"] ?? process.cwd(), id)) console.log(success(`✓ Article '${id}' deleted`));
|
|
3732
|
+
else {
|
|
3733
|
+
console.error(error(`Article '${id}' not found`));
|
|
3734
|
+
process.exit(1);
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
3737
|
+
//#endregion
|
|
3738
|
+
//#region src/commands/fields.ts
|
|
3739
|
+
const VALID_TYPES = [
|
|
3740
|
+
"text",
|
|
3741
|
+
"number",
|
|
3742
|
+
"boolean",
|
|
3743
|
+
"date",
|
|
3744
|
+
"select"
|
|
3745
|
+
];
|
|
3746
|
+
function dataDir$16() {
|
|
3747
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3748
|
+
}
|
|
3749
|
+
function collect(value, prev) {
|
|
3750
|
+
return [...prev, value];
|
|
3751
|
+
}
|
|
3752
|
+
/** Parse a `--field name:type[:opt1|opt2]` spec into a FieldDefinition. */
|
|
3753
|
+
function parseFieldSpec(spec) {
|
|
3754
|
+
const [name, type, opts] = spec.split(":");
|
|
3755
|
+
if (!name || !type || !VALID_TYPES.includes(type)) return null;
|
|
3756
|
+
return {
|
|
3757
|
+
name,
|
|
3758
|
+
type,
|
|
3759
|
+
...opts ? { options: opts.split("|").map((s) => s.trim()) } : {}
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
const fieldsCommand = new Command("fields").description("Manage custom fields (metadata-driven extensibility)");
|
|
3763
|
+
fieldsCommand.command("list").description("List defined custom fields").action(async () => {
|
|
3764
|
+
const { loadFieldDefinitions } = await import("./custom-fields-CzNeD3_v.js");
|
|
3765
|
+
const defs = loadFieldDefinitions(dataDir$16());
|
|
3766
|
+
if (defs.length === 0) {
|
|
3767
|
+
console.log(info("No custom fields defined. Add one with: dxcrm fields add <name> <type>"));
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
for (const d of defs) {
|
|
3771
|
+
const opts = d.options ? ` [${d.options.join(", ")}]` : "";
|
|
3772
|
+
console.log(`${d.name} (${d.type})${opts}${d.label ? ` — ${d.label}` : ""}`);
|
|
3773
|
+
}
|
|
3774
|
+
});
|
|
3775
|
+
fieldsCommand.command("add <name> <type>").description("Define a custom field (type: text|number|boolean|date|select)").option("--label <label>", "Human-readable label").option("--options <csv>", "Comma-separated options (for select)").action(async (name, type, opts) => {
|
|
3776
|
+
if (!VALID_TYPES.includes(type)) {
|
|
3777
|
+
console.error(error(`Invalid type '${type}'. Use one of: ${VALID_TYPES.join(", ")}`));
|
|
3778
|
+
process.exitCode = 1;
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
const { defineCustomField } = await import("./custom-fields-CzNeD3_v.js");
|
|
3782
|
+
defineCustomField(dataDir$16(), {
|
|
3783
|
+
name,
|
|
3784
|
+
type,
|
|
3785
|
+
...opts.label ? { label: opts.label } : {},
|
|
3786
|
+
...opts.options ? { options: opts.options.split(",").map((s) => s.trim()) } : {}
|
|
3787
|
+
});
|
|
3788
|
+
console.log(success(`Custom field '${name}' (${type}) defined.`));
|
|
3789
|
+
});
|
|
3790
|
+
const objectCommand = new Command("object").description("Manage custom objects (runtime-defined entities, no-migration)");
|
|
3791
|
+
objectCommand.command("define <name>").description("Define a custom object with fields (--field name:type[:opt1|opt2])").option("--label <label>", "Human-readable label").option("--field <spec>", "Field spec, repeatable", collect, []).action(async (name, opts) => {
|
|
3792
|
+
const fields = [];
|
|
3793
|
+
for (const spec of opts.field) {
|
|
3794
|
+
const f = parseFieldSpec(spec);
|
|
3795
|
+
if (!f) {
|
|
3796
|
+
console.error(error(`Invalid --field spec '${spec}' (use name:type[:a|b])`));
|
|
3797
|
+
process.exitCode = 1;
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
fields.push(f);
|
|
3801
|
+
}
|
|
3802
|
+
const { defineCustomObject } = await import("./custom-objects-CIFrmQ2V.js");
|
|
3803
|
+
defineCustomObject(dataDir$16(), {
|
|
3804
|
+
name,
|
|
3805
|
+
...opts.label ? { label: opts.label } : {},
|
|
3806
|
+
fields
|
|
3807
|
+
});
|
|
3808
|
+
console.log(success(`Custom object '${name}' defined with ${fields.length} field(s).`));
|
|
3809
|
+
});
|
|
3810
|
+
objectCommand.command("add <name>").description("Create a record (--set key=value, repeatable)").option("--set <kv>", "key=value, repeatable", collect, []).action(async (name, opts) => {
|
|
3811
|
+
const values = {};
|
|
3812
|
+
for (const kv of opts.set) {
|
|
3813
|
+
const eq = kv.indexOf("=");
|
|
3814
|
+
if (eq < 0) continue;
|
|
3815
|
+
values[kv.slice(0, eq).trim()] = kv.slice(eq + 1).trim();
|
|
3816
|
+
}
|
|
3817
|
+
const { createRecord } = await import("./custom-objects-CIFrmQ2V.js");
|
|
3818
|
+
const res = createRecord(dataDir$16(), name, values);
|
|
3819
|
+
if (!res.ok) {
|
|
3820
|
+
console.error(error(`Could not create record: ${(res.errors ?? []).join("; ")}`));
|
|
3821
|
+
process.exitCode = 1;
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
console.log(success(`Created ${name} record ${res.record.id}`));
|
|
3825
|
+
});
|
|
3826
|
+
objectCommand.command("list <name>").description("List records of a custom object").action(async (name) => {
|
|
3827
|
+
const { listRecords } = await import("./custom-objects-CIFrmQ2V.js");
|
|
3828
|
+
const records = listRecords(dataDir$16(), name);
|
|
3829
|
+
if (records.length === 0) {
|
|
3830
|
+
console.log(info(`No records for '${name}'.`));
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
for (const r of records) console.log(`${r.id} ${JSON.stringify(r.values)}`);
|
|
3834
|
+
});
|
|
3835
|
+
//#endregion
|
|
3836
|
+
//#region src/commands/webhook.ts
|
|
3837
|
+
function dataDir$15() {
|
|
3838
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3839
|
+
}
|
|
3840
|
+
const webhookCommand = new Command("webhook").description("Manage outbound webhooks (event-driven integrations)");
|
|
3841
|
+
webhookCommand.command("add <url>").description("Subscribe a URL to events (--events record.created,deal.updated or '*')").option("--events <csv>", "Comma-separated event patterns", "*").option("--secret <secret>", "HMAC secret for X-DXCRM-Signature").action(async (url, opts) => {
|
|
3842
|
+
const { addWebhook } = await import("./webhooks-7EpA05Qr.js");
|
|
3843
|
+
const events = opts.events.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3844
|
+
const sub = addWebhook(dataDir$15(), url, events, opts.secret);
|
|
3845
|
+
console.log(success(`Webhook ${sub.id} → ${url} [${events.join(", ")}]`));
|
|
3846
|
+
});
|
|
3847
|
+
webhookCommand.command("list").description("List webhook subscriptions").action(async () => {
|
|
3848
|
+
const { loadWebhooks } = await import("./webhooks-7EpA05Qr.js");
|
|
3849
|
+
const subs = loadWebhooks(dataDir$15());
|
|
3850
|
+
if (subs.length === 0) {
|
|
3851
|
+
console.log(info("No webhooks configured."));
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
for (const s of subs) console.log(`${s.id} ${s.url} [${s.events.join(", ")}]`);
|
|
3855
|
+
});
|
|
3856
|
+
webhookCommand.command("remove <id>").description("Remove a webhook subscription").action(async (id) => {
|
|
3857
|
+
const { removeWebhook } = await import("./webhooks-7EpA05Qr.js");
|
|
3858
|
+
if (removeWebhook(dataDir$15(), id)) console.log(success(`Removed ${id}`));
|
|
3859
|
+
else {
|
|
3860
|
+
console.error(error(`Not found: ${id}`));
|
|
3861
|
+
process.exitCode = 1;
|
|
3862
|
+
}
|
|
3863
|
+
});
|
|
3864
|
+
webhookCommand.command("retry").description("Re-attempt failed webhook deliveries (replay store)").action(async () => {
|
|
3865
|
+
const { retryFailures } = await import("./webhooks-7EpA05Qr.js");
|
|
3866
|
+
const r = await retryFailures(dataDir$15());
|
|
3867
|
+
console.log(info(`Retried ${r.retried}, still failing ${r.stillFailing}.`));
|
|
3868
|
+
});
|
|
3869
|
+
//#endregion
|
|
3870
|
+
//#region src/commands/segment.ts
|
|
3871
|
+
function dataDir$14() {
|
|
3872
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3873
|
+
}
|
|
3874
|
+
const segmentCommand = new Command("segment").description("Manage customer segments (marketing lists)");
|
|
3875
|
+
segmentCommand.command("define <name>").description("Define a segment by criteria").option("--stage <stage>", "relationship_stage (prospect|active|churned|paused)").option("--tags <csv>", "Comma-separated tags (all must match)").option("--min-deal-value <n>", "Minimum deal value").option("--stale-days <n>", "Days since last update (staleness)").action(async (name, opts) => {
|
|
3876
|
+
const criteria = {
|
|
3877
|
+
...opts.stage ? { stage: opts.stage } : {},
|
|
3878
|
+
...opts.tags ? { tags: opts.tags.split(",").map((s) => s.trim()) } : {},
|
|
3879
|
+
...opts.minDealValue ? { minDealValue: Number(opts.minDealValue) } : {},
|
|
3880
|
+
...opts.staleDays ? { staleDays: Number(opts.staleDays) } : {}
|
|
3881
|
+
};
|
|
3882
|
+
const { defineSegment } = await import("./segments-BqcD5HIl.js");
|
|
3883
|
+
defineSegment(dataDir$14(), name, criteria);
|
|
3884
|
+
console.log(success(`Segment '${name}' defined: ${JSON.stringify(criteria)}`));
|
|
3885
|
+
});
|
|
3886
|
+
segmentCommand.command("list").description("List defined segments").action(async () => {
|
|
3887
|
+
const { loadSegments } = await import("./segments-BqcD5HIl.js");
|
|
3888
|
+
const segs = loadSegments(dataDir$14());
|
|
3889
|
+
if (segs.length === 0) {
|
|
3890
|
+
console.log(info("No segments defined."));
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
for (const s of segs) console.log(`${s.name} ${JSON.stringify(s.criteria)}`);
|
|
3894
|
+
});
|
|
3895
|
+
segmentCommand.command("members <name>").description("List customers matching a segment").action(async (name) => {
|
|
3896
|
+
const { loadSegments, evaluateSegment } = await import("./segments-BqcD5HIl.js");
|
|
3897
|
+
const seg = loadSegments(dataDir$14()).find((s) => s.name === name);
|
|
3898
|
+
if (!seg) {
|
|
3899
|
+
console.error(error(`Segment not found: ${name}`));
|
|
3900
|
+
process.exitCode = 1;
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
const members = await evaluateSegment(dataDir$14(), seg.criteria);
|
|
3904
|
+
console.log(info(`${members.length} member(s):`));
|
|
3905
|
+
for (const slug of members) console.log(slug);
|
|
3906
|
+
});
|
|
3907
|
+
//#endregion
|
|
3908
|
+
//#region src/commands/identity.ts
|
|
3909
|
+
function dataDir$13() {
|
|
3910
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3911
|
+
}
|
|
3912
|
+
const identityCommand = new Command("identity").description("Identity resolution / deduplication (CDP)");
|
|
3913
|
+
identityCommand.command("duplicates").description("Find clusters of likely-duplicate customers (by canonical domain)").action(async () => {
|
|
3914
|
+
const { findDuplicateClusters } = await import("./identity-gyfWdrcX.js");
|
|
3915
|
+
const clusters = await findDuplicateClusters(dataDir$13());
|
|
3916
|
+
if (clusters.length === 0) {
|
|
3917
|
+
console.log(info("No duplicate clusters found."));
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
console.log(info(`${clusters.length} duplicate cluster(s):`));
|
|
3921
|
+
for (const c of clusters) console.log(`${c.key}: ${c.slugs.join(", ")}`);
|
|
3922
|
+
});
|
|
3923
|
+
//#endregion
|
|
3924
|
+
//#region src/commands/metrics.ts
|
|
3925
|
+
function dataDir$12() {
|
|
3926
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3927
|
+
}
|
|
3928
|
+
const metricsCommand = new Command("metrics").description("Command-center metrics from the audit trail").action(async () => {
|
|
3929
|
+
const { computeAuditMetrics } = await import("./metrics-DH8wHvya.js");
|
|
3930
|
+
const m = computeAuditMetrics(dataDir$12());
|
|
3931
|
+
console.log(bold("Command Center"));
|
|
3932
|
+
console.log(`Total operations: ${m.totalOperations}`);
|
|
3933
|
+
console.log(`Customers touched: ${m.customersTouched}`);
|
|
3934
|
+
console.log(`Automation rate: ${(m.automationRate * 100).toFixed(0)}%`);
|
|
3935
|
+
console.log(info("By tool:"));
|
|
3936
|
+
for (const [tool, n] of Object.entries(m.byTool).sort((a, b) => b[1] - a[1])) console.log(` ${tool}: ${n}`);
|
|
3937
|
+
console.log(info("By actor:"));
|
|
3938
|
+
for (const [actor, n] of Object.entries(m.byActor).sort((a, b) => b[1] - a[1])) console.log(` ${actor}: ${n}`);
|
|
3939
|
+
});
|
|
3940
|
+
//#endregion
|
|
3941
|
+
//#region src/commands/usage.ts
|
|
3942
|
+
function dataDir$11() {
|
|
3943
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3944
|
+
}
|
|
3945
|
+
const usageCommand = new Command("usage").description("LLM token usage & cost (per customer)").option("--slug <slug>", "Filter by customer").action(async (opts) => {
|
|
3946
|
+
const { aggregateUsage } = await import("./usage-D0-TYJkw.js");
|
|
3947
|
+
const agg = aggregateUsage(dataDir$11(), opts.slug ? { slug: opts.slug } : {});
|
|
3948
|
+
console.log(bold("LLM Usage"));
|
|
3949
|
+
console.log(`Calls: ${agg.calls}`);
|
|
3950
|
+
console.log(`Input tokens: ${agg.totalInputTokens}`);
|
|
3951
|
+
console.log(`Output tokens: ${agg.totalOutputTokens}`);
|
|
3952
|
+
console.log(`Cost (USD): $${agg.totalCostUsd.toFixed(4)}`);
|
|
3953
|
+
if (!opts.slug) {
|
|
3954
|
+
console.log(info("By customer:"));
|
|
3955
|
+
for (const [slug, b] of Object.entries(agg.bySlug).sort((a, b) => b[1].costUsd - a[1].costUsd)) console.log(` ${slug}: $${b.costUsd.toFixed(4)} (${b.calls} calls)`);
|
|
3956
|
+
}
|
|
3957
|
+
});
|
|
3958
|
+
//#endregion
|
|
3959
|
+
//#region src/commands/approvals.ts
|
|
3960
|
+
function dataDir$10() {
|
|
3961
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
3962
|
+
}
|
|
3963
|
+
const approvalsCommand = new Command("approvals").description("Human-in-the-loop approval queue");
|
|
3964
|
+
approvalsCommand.command("list").description("List approvals (default: pending)").option("--status <status>", "pending | approved | rejected", "pending").action(async (opts) => {
|
|
3965
|
+
const { listApprovals } = await import("./approvals-DpjxGHFp.js");
|
|
3966
|
+
const list = listApprovals(dataDir$10(), opts.status);
|
|
3967
|
+
if (list.length === 0) {
|
|
3968
|
+
console.log(info(`No ${opts.status} approvals.`));
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
for (const a of list) console.log(`${a.id} ${a.tool} ${a.slug ?? "-"} ${a.requestedAt}`);
|
|
3972
|
+
});
|
|
3973
|
+
approvalsCommand.command("approve <id>").description("Approve a pending action").action(async (id) => {
|
|
3974
|
+
const { decideApproval } = await import("./approvals-DpjxGHFp.js");
|
|
3975
|
+
if (decideApproval(dataDir$10(), id, "approved")) console.log(success(`Approved ${id}`));
|
|
3976
|
+
else {
|
|
3977
|
+
console.error(error(`Not found: ${id}`));
|
|
3978
|
+
process.exitCode = 1;
|
|
3979
|
+
}
|
|
3980
|
+
});
|
|
3981
|
+
approvalsCommand.command("reject <id>").description("Reject a pending action").action(async (id) => {
|
|
3982
|
+
const { decideApproval } = await import("./approvals-DpjxGHFp.js");
|
|
3983
|
+
if (decideApproval(dataDir$10(), id, "rejected")) console.log(success(`Rejected ${id}`));
|
|
3984
|
+
else {
|
|
3985
|
+
console.error(error(`Not found: ${id}`));
|
|
3986
|
+
process.exitCode = 1;
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
const policyCommand = new Command("policy").description("Autonomy policy per tool/customer (auto|approve|block)");
|
|
3990
|
+
policyCommand.command("set <tool> <policy>").description("Set autonomy policy for a tool (optionally per --slug)").option("--slug <slug>", "Customer slug (per-customer override)").action(async (tool, policy, opts) => {
|
|
3991
|
+
if (![
|
|
3992
|
+
"auto",
|
|
3993
|
+
"approve",
|
|
3994
|
+
"block"
|
|
3995
|
+
].includes(policy)) {
|
|
3996
|
+
console.error(error("policy must be auto | approve | block"));
|
|
3997
|
+
process.exitCode = 1;
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
const { setPolicy } = await import("./approvals-DpjxGHFp.js");
|
|
4001
|
+
setPolicy(dataDir$10(), tool, policy, opts.slug);
|
|
4002
|
+
console.log(success(`Policy ${tool}${opts.slug ? `@${opts.slug}` : ""} = ${policy}`));
|
|
4003
|
+
});
|
|
4004
|
+
//#endregion
|
|
4005
|
+
//#region src/commands/hygiene.ts
|
|
4006
|
+
function dataDir$9() {
|
|
4007
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4008
|
+
}
|
|
4009
|
+
const hygieneCommand = new Command("hygiene").description("Data-quality scanning");
|
|
4010
|
+
hygieneCommand.command("scan").description("Scan customers for data-quality issues (missing/malformed/duplicate)").action(async () => {
|
|
4011
|
+
const { scanHygiene } = await import("./hygiene-DZqfYpFf.js");
|
|
4012
|
+
const issues = await scanHygiene(dataDir$9());
|
|
4013
|
+
if (issues.length === 0) {
|
|
4014
|
+
console.log(success("✓ No data-quality issues found."));
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
console.log(info(`${issues.length} issue(s):`));
|
|
4018
|
+
for (const i of issues) {
|
|
4019
|
+
const fix = i.suggestedFix ? ` → fix: ${i.suggestedFix}` : "";
|
|
4020
|
+
console.log(` [${i.type}] ${i.slug}: ${i.detail}${fix}`);
|
|
4021
|
+
}
|
|
4022
|
+
});
|
|
4023
|
+
//#endregion
|
|
4024
|
+
//#region src/commands/memory.ts
|
|
4025
|
+
function dataDir$8() {
|
|
4026
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4027
|
+
}
|
|
4028
|
+
const TYPES = [
|
|
4029
|
+
"fact",
|
|
4030
|
+
"preference",
|
|
4031
|
+
"learning",
|
|
4032
|
+
"instruction"
|
|
4033
|
+
];
|
|
4034
|
+
const memoryCommand = new Command("memory").description("Agent memories (per customer + global)");
|
|
4035
|
+
memoryCommand.command("add <text>").description("Add a memory (global unless --slug given)").option("--type <type>", "fact | preference | learning | instruction", "fact").option("--slug <slug>", "Customer slug (omit for global)").action(async (text, opts) => {
|
|
4036
|
+
const type = TYPES.includes(opts.type) ? opts.type : "fact";
|
|
4037
|
+
const { addMemory } = await import("./memory-Cy6-Tbyl.js");
|
|
4038
|
+
const m = addMemory(dataDir$8(), {
|
|
4039
|
+
scope: opts.slug ? "customer" : "global",
|
|
4040
|
+
...opts.slug ? { slug: opts.slug } : {},
|
|
4041
|
+
type,
|
|
4042
|
+
text
|
|
4043
|
+
});
|
|
4044
|
+
console.log(success(`Memory ${m.id} stored (${m.scope}${opts.slug ? `:${opts.slug}` : ""}).`));
|
|
4045
|
+
});
|
|
4046
|
+
memoryCommand.command("list").description("List memories (global + customer if --slug)").option("--slug <slug>", "Customer slug").action(async (opts) => {
|
|
4047
|
+
const { loadMemories } = await import("./memory-Cy6-Tbyl.js");
|
|
4048
|
+
const mems = loadMemories(dataDir$8(), opts.slug);
|
|
4049
|
+
if (mems.length === 0) {
|
|
4050
|
+
console.log(info("No memories."));
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
for (const m of mems) console.log(`[${m.scope}/${m.type}] ${m.text}`);
|
|
4054
|
+
});
|
|
4055
|
+
memoryCommand.command("search <query>").description("Search memories by relevance").option("--slug <slug>", "Customer slug").action(async (query, opts) => {
|
|
4056
|
+
const { searchMemory } = await import("./memory-Cy6-Tbyl.js");
|
|
4057
|
+
const hits = await searchMemory(dataDir$8(), query, opts.slug);
|
|
4058
|
+
if (hits.length === 0) {
|
|
4059
|
+
console.log(info("No matching memories."));
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
for (const m of hits) console.log(`[${m.scope}/${m.type}] ${m.text}`);
|
|
4063
|
+
});
|
|
4064
|
+
//#endregion
|
|
4065
|
+
//#region src/commands/sop.ts
|
|
4066
|
+
function dataDir$7() {
|
|
4067
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4068
|
+
}
|
|
4069
|
+
const sopCommand = new Command("sop").description("Standard Operating Procedures (global / per customer)");
|
|
4070
|
+
sopCommand.command("add <title>").description("Add an SOP (global unless --slug given)").option("--triggers <csv>", "Comma-separated trigger keywords", "").option("--body <text>", "SOP body / steps", "").option("--slug <slug>", "Customer slug (omit for global)").action(async (title, opts) => {
|
|
4071
|
+
const { addSop } = await import("./sop-DkhVChGy.js");
|
|
4072
|
+
const triggers = opts.triggers.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4073
|
+
const s = addSop(dataDir$7(), {
|
|
4074
|
+
scope: opts.slug ? "customer" : "global",
|
|
4075
|
+
...opts.slug ? { slug: opts.slug } : {},
|
|
4076
|
+
title,
|
|
4077
|
+
triggers,
|
|
4078
|
+
body: opts.body
|
|
4079
|
+
});
|
|
4080
|
+
console.log(success(`SOP ${s.id} added (${s.scope}${opts.slug ? `:${opts.slug}` : ""}).`));
|
|
4081
|
+
});
|
|
4082
|
+
sopCommand.command("list").description("List SOPs (global + customer if --slug)").option("--slug <slug>", "Customer slug").action(async (opts) => {
|
|
4083
|
+
const { loadSops } = await import("./sop-DkhVChGy.js");
|
|
4084
|
+
const sops = loadSops(dataDir$7(), opts.slug);
|
|
4085
|
+
if (sops.length === 0) {
|
|
4086
|
+
console.log(info("No SOPs."));
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
for (const s of sops) console.log(`[${s.scope}] ${s.title} (${s.triggers.join(", ")})`);
|
|
4090
|
+
});
|
|
4091
|
+
sopCommand.command("find <query>").description("Find SOPs relevant to a task").option("--slug <slug>", "Customer slug").action(async (query, opts) => {
|
|
4092
|
+
const { findSops } = await import("./sop-DkhVChGy.js");
|
|
4093
|
+
const hits = await findSops(dataDir$7(), query, opts.slug);
|
|
4094
|
+
if (hits.length === 0) {
|
|
4095
|
+
console.log(info("No matching SOPs."));
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
for (const s of hits) console.log(`[${s.scope}] ${s.title}`);
|
|
4099
|
+
});
|
|
4100
|
+
//#endregion
|
|
4101
|
+
//#region src/commands/tone.ts
|
|
4102
|
+
function dataDir$6() {
|
|
4103
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4104
|
+
}
|
|
4105
|
+
const toneCommand = new Command("tone").description("Customer tonality profiles (per customer + global)");
|
|
4106
|
+
toneCommand.command("set").description("Set tone profile (global unless --slug given)").option("--formality <v>", "formal | casual | friendly").option("--language <v>", "Language code, e.g. de | en").option("--do <csv>", "Comma-separated phrases to prefer").option("--dont <csv>", "Comma-separated phrases to avoid").option("--slug <slug>", "Customer slug (omit for global)").action(async (opts) => {
|
|
4107
|
+
const { setTone } = await import("./tone-Bdm5uaht.js");
|
|
4108
|
+
setTone(dataDir$6(), {
|
|
4109
|
+
...opts.formality ? { formality: opts.formality } : {},
|
|
4110
|
+
...opts.language ? { language: opts.language } : {},
|
|
4111
|
+
...opts.do ? { dos: opts.do.split(",").map((s) => s.trim()) } : {},
|
|
4112
|
+
...opts.dont ? { donts: opts.dont.split(",").map((s) => s.trim()) } : {}
|
|
4113
|
+
}, opts.slug);
|
|
4114
|
+
console.log(success(`Tone profile saved (${opts.slug ? `customer:${opts.slug}` : "global"}).`));
|
|
4115
|
+
});
|
|
4116
|
+
toneCommand.command("show").description("Show the effective tone profile").option("--slug <slug>", "Customer slug").action(async (opts) => {
|
|
4117
|
+
const { resolveTone, toneInstruction } = await import("./tone-Bdm5uaht.js");
|
|
4118
|
+
const profile = resolveTone(dataDir$6(), opts.slug);
|
|
4119
|
+
console.log(info(JSON.stringify(profile)));
|
|
4120
|
+
console.log(`instruction: ${toneInstruction(profile) || "(none)"}`);
|
|
4121
|
+
});
|
|
4122
|
+
//#endregion
|
|
4123
|
+
//#region src/commands/autofill.ts
|
|
4124
|
+
const autofillCommand = new Command("autofill").description("Extract structured CRM fields from a transcript file").argument("<file>", "Path to transcript file").option("--slug <slug>", "Customer slug (for usage attribution)").action(async (file, opts) => {
|
|
4125
|
+
if (!fs.existsSync(file)) {
|
|
4126
|
+
console.error(error(`File not found: ${file}`));
|
|
4127
|
+
process.exitCode = 1;
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const transcript = fs.readFileSync(file, "utf-8");
|
|
4131
|
+
const { extractAutofill } = await import("./autofill-Di_-SP7t.js");
|
|
4132
|
+
const result = await extractAutofill(transcript, opts.slug ? { slug: opts.slug } : {});
|
|
4133
|
+
console.log(info("Extracted fields (review before applying):"));
|
|
4134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4135
|
+
});
|
|
4136
|
+
//#endregion
|
|
4137
|
+
//#region src/commands/ask.ts
|
|
4138
|
+
function dataDir$5() {
|
|
4139
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4140
|
+
}
|
|
4141
|
+
const askCommand = new Command("ask").description("Ask your CRM a natural-language question").argument("<question>", "The question").option("--slug <slug>", "Scope to a customer").action(async (question, opts) => {
|
|
4142
|
+
const { askCrm } = await import("./ask-CID3jnuL.js");
|
|
4143
|
+
const res = await askCrm(dataDir$5(), question, opts.slug);
|
|
4144
|
+
if (res.answer) {
|
|
4145
|
+
console.log(bold("Answer:"));
|
|
4146
|
+
console.log(res.answer);
|
|
4147
|
+
}
|
|
4148
|
+
if (res.sources.length === 0) {
|
|
4149
|
+
console.log(info("No relevant data found."));
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
console.log(info("Sources:"));
|
|
4153
|
+
res.sources.forEach((s, i) => console.log(` [${i + 1}] ${s.text.slice(0, 120)}`));
|
|
4154
|
+
});
|
|
4155
|
+
//#endregion
|
|
4156
|
+
//#region src/commands/nba.ts
|
|
4157
|
+
function dataDir$4() {
|
|
4158
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4159
|
+
}
|
|
4160
|
+
const nbaCommand = new Command("nba").description("Next-best-action recommendations for a customer").argument("<slug>", "Customer slug").action(async (slug) => {
|
|
4161
|
+
const { nextBestAction } = await import("./nba-3wanmJ0U.js");
|
|
4162
|
+
const actions = await nextBestAction(dataDir$4(), slug);
|
|
4163
|
+
if (actions.length === 0) {
|
|
4164
|
+
console.log(info("No recommendations."));
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
for (const a of actions) console.log(`[${a.priority}] ${a.action} — ${a.reason}`);
|
|
4168
|
+
});
|
|
4169
|
+
//#endregion
|
|
4170
|
+
//#region src/commands/vault.ts
|
|
4171
|
+
function dataDir$3() {
|
|
4172
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4173
|
+
}
|
|
4174
|
+
/** Master key from the environment only — never a flag (avoids shell history). */
|
|
4175
|
+
function masterKey() {
|
|
4176
|
+
const key = process.env["DXCRM_VAULT_KEY"];
|
|
4177
|
+
if (!key) {
|
|
4178
|
+
console.error(error("DXCRM_VAULT_KEY is not set. Export your vault master key first."));
|
|
4179
|
+
process.exit(1);
|
|
4180
|
+
}
|
|
4181
|
+
return key;
|
|
4182
|
+
}
|
|
4183
|
+
/** Run a vault action, turning decryption/IO failures into a clean exit. */
|
|
4184
|
+
async function guard(fn) {
|
|
4185
|
+
try {
|
|
4186
|
+
await fn();
|
|
4187
|
+
} catch (e) {
|
|
4188
|
+
console.error(error(e.message));
|
|
4189
|
+
process.exit(1);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
const vaultCommand = new Command("vault").description("Local encrypted credential vault (AES-256-GCM)");
|
|
4193
|
+
vaultCommand.command("set <name> <value>").description("Store (or overwrite) a secret").action((name, value) => guard(async () => {
|
|
4194
|
+
const { setSecret } = await import("./vault-C1D3zScD.js");
|
|
4195
|
+
setSecret(dataDir$3(), masterKey(), name, value);
|
|
4196
|
+
console.log(success(`Secret '${name}' stored.`));
|
|
4197
|
+
}));
|
|
4198
|
+
vaultCommand.command("get <name>").description("Retrieve a secret").action((name) => guard(async () => {
|
|
4199
|
+
const { getSecret } = await import("./vault-C1D3zScD.js");
|
|
4200
|
+
const value = getSecret(dataDir$3(), masterKey(), name);
|
|
4201
|
+
if (value === void 0) {
|
|
4202
|
+
console.log(info(`No secret named '${name}'.`));
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
console.log(value);
|
|
4206
|
+
}));
|
|
4207
|
+
vaultCommand.command("list").description("List secret names (values stay encrypted)").action(() => guard(async () => {
|
|
4208
|
+
const { listSecretKeys } = await import("./vault-C1D3zScD.js");
|
|
4209
|
+
const names = listSecretKeys(dataDir$3(), masterKey());
|
|
4210
|
+
if (names.length === 0) {
|
|
4211
|
+
console.log(info("Vault is empty."));
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
for (const n of names.sort()) console.log(n);
|
|
4215
|
+
}));
|
|
4216
|
+
vaultCommand.command("rm <name>").description("Remove a secret").action((name) => guard(async () => {
|
|
4217
|
+
const { removeSecret } = await import("./vault-C1D3zScD.js");
|
|
4218
|
+
const removed = removeSecret(dataDir$3(), masterKey(), name);
|
|
4219
|
+
console.log(removed ? success(`Secret '${name}' removed.`) : info(`No secret named '${name}'.`));
|
|
4220
|
+
}));
|
|
4221
|
+
//#endregion
|
|
4222
|
+
//#region src/commands/churn.ts
|
|
4223
|
+
function dataDir$2() {
|
|
4224
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4225
|
+
}
|
|
4226
|
+
function paint(level, text) {
|
|
4227
|
+
if (level === "high") return error(text);
|
|
4228
|
+
if (level === "medium") return warning(text);
|
|
4229
|
+
return success(text);
|
|
4230
|
+
}
|
|
4231
|
+
const churnCommand = new Command("churn").description("Churn early-warning (relationship-health based)");
|
|
4232
|
+
churnCommand.command("assess <slug>").description("Assess churn risk for one customer").action(async (slug) => {
|
|
4233
|
+
const { assessChurn } = await import("./churn-C28IgnAj.js");
|
|
4234
|
+
const r = assessChurn(dataDir$2(), slug);
|
|
4235
|
+
console.log(paint(r.level, `${slug}: ${r.level.toUpperCase()} risk (${r.riskScore}/100)`));
|
|
4236
|
+
for (const s of r.signals) console.log(` • ${s}`);
|
|
4237
|
+
});
|
|
4238
|
+
churnCommand.command("scan").description("Rank all customers by churn risk (highest first)").action(async () => {
|
|
4239
|
+
const { scanChurn } = await import("./churn-C28IgnAj.js");
|
|
4240
|
+
const ranked = scanChurn(dataDir$2());
|
|
4241
|
+
if (ranked.length === 0) {
|
|
4242
|
+
console.log(info("No customers to assess."));
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
for (const r of ranked) console.log(paint(r.level, `${r.riskScore.toString().padStart(3)} ${r.level.padEnd(6)} ${r.slug}`));
|
|
4246
|
+
});
|
|
4247
|
+
//#endregion
|
|
4248
|
+
//#region src/commands/leadscore.ts
|
|
4249
|
+
function dataDir$1() {
|
|
4250
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4251
|
+
}
|
|
4252
|
+
const leadscoreCommand = new Command("leadscore").description("Predictive lead scoring (logistic regression on won/lost history)");
|
|
4253
|
+
leadscoreCommand.command("train").description("Train the model from won/lost history and persist it").action(async () => {
|
|
4254
|
+
const { buildLeadModel, saveLeadModel } = await import("./lead-model-BCFzyktm.js");
|
|
4255
|
+
const model = buildLeadModel(dataDir$1());
|
|
4256
|
+
if (!model.sufficient) {
|
|
4257
|
+
console.log(warning(`Not enough closed history to train (${model.trainedOn} deals, need ≥4 with both outcomes).`));
|
|
4258
|
+
console.log(info("Predictions fall back to the deterministic heuristic until then."));
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
saveLeadModel(dataDir$1(), model);
|
|
4262
|
+
console.log(success(`Trained on ${model.trainedOn} closed deals. Model saved.`));
|
|
4263
|
+
});
|
|
4264
|
+
leadscoreCommand.command("predict <slug>").description("Score a customer's open deals with the trained model").action(async (slug) => {
|
|
4265
|
+
const { loadLeadModel, buildLeadModel, predictWin } = await import("./lead-model-BCFzyktm.js");
|
|
4266
|
+
const { readPipelineSync } = await import("./pipeline-writer-BqBrYrQc.js");
|
|
4267
|
+
const model = loadLeadModel(dataDir$1()) ?? buildLeadModel(dataDir$1());
|
|
4268
|
+
const open = readPipelineSync(dataDir$1(), slug).filter((d) => d.stage !== "won" && d.stage !== "lost");
|
|
4269
|
+
if (open.length === 0) {
|
|
4270
|
+
console.log(info(`No open deals for ${slug}.`));
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
const source = model.sufficient ? "model" : "heuristic";
|
|
4274
|
+
console.log(info(`Win-probability for ${slug} (${source}):`));
|
|
4275
|
+
for (const d of open) {
|
|
4276
|
+
const p = Math.round(predictWin(model, d) * 100);
|
|
4277
|
+
console.log(` ${String(p).padStart(3)}% ${d.name} (${d.stage})`);
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
//#endregion
|
|
4281
|
+
//#region src/commands/enrich.ts
|
|
4282
|
+
function dataDir() {
|
|
4283
|
+
return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
|
|
4284
|
+
}
|
|
4285
|
+
const enrichCommand = new Command("enrich").description("Enrich a customer's facts (offline domain-from-email + plugins)").argument("<slug>", "Customer slug").option("--write", "Write newly-derived fields back to main_facts.md").action(async (slug, opts) => {
|
|
4286
|
+
const { enrichCustomer } = await import("./enrichment-3XvgGDfB.js");
|
|
4287
|
+
const res = await enrichCustomer(dataDir(), slug, { write: opts.write ?? false });
|
|
4288
|
+
const applied = Object.entries(res.applied);
|
|
4289
|
+
if (applied.length === 0) {
|
|
4290
|
+
console.log(info(`Nothing new to enrich for ${slug}.`));
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
for (const [k, v] of applied) console.log(` ${k}: ${String(v)}`);
|
|
4294
|
+
console.log(res.written ? success(`Applied ${applied.length} field(s) to ${slug}.`) : info(`Found ${applied.length} field(s) — re-run with --write to apply.`));
|
|
4295
|
+
});
|
|
4296
|
+
//#endregion
|
|
4297
|
+
//#region src/commands/coach.ts
|
|
4298
|
+
const coachCommand = new Command("coach").description("Conversation-intelligence: analyze a call transcript").argument("<file>", "Path to a speaker-labelled transcript (\"Rep: ...\" / \"Customer: ...\")").option("--rep <labels>", "Comma-separated speaker labels treated as the rep", "rep,sales,ae,me,agent").action(async (file, opts) => {
|
|
4299
|
+
if (!fs.existsSync(file)) {
|
|
4300
|
+
console.error(`Transcript not found: ${file}`);
|
|
4301
|
+
process.exit(1);
|
|
4302
|
+
}
|
|
4303
|
+
const transcript = fs.readFileSync(file, "utf-8");
|
|
4304
|
+
const { analyzeConversation } = await import("./conversation-intel-mm7Lhemh.js");
|
|
4305
|
+
const a = analyzeConversation(transcript, opts.rep.split(",").map((s) => s.trim()).filter(Boolean));
|
|
4306
|
+
console.log(success(`Conversation analysis (${a.turns} turns)`));
|
|
4307
|
+
console.log(` Talk ratio (rep): ${Math.round(a.talkRatio * 100)}%`);
|
|
4308
|
+
console.log(` Questions asked: ${a.questionsAsked}`);
|
|
4309
|
+
console.log(` Longest monologue: ${a.longestMonologue} words`);
|
|
4310
|
+
if (a.objections.length > 0) {
|
|
4311
|
+
console.log(info(" Objections:"));
|
|
4312
|
+
for (const o of a.objections) console.log(` • ${o}`);
|
|
4313
|
+
}
|
|
4314
|
+
console.log(info(" Coaching:"));
|
|
4315
|
+
for (const c of a.coaching) console.log(` → ${c}`);
|
|
4316
|
+
});
|
|
4317
|
+
//#endregion
|
|
4318
|
+
//#region src/commands/compliance.ts
|
|
4319
|
+
const complianceCommand = new Command("compliance").description("Privacy/compliance posture (AI-Act Art. 50, local-LLM, PII, guardrails)");
|
|
4320
|
+
complianceCommand.command("status", { isDefault: true }).description("Show the active compliance configuration").action(async () => {
|
|
4321
|
+
const { complianceConfig, aiDisclosure } = await import("./compliance-CujOqAKk.js");
|
|
4322
|
+
const cfg = complianceConfig();
|
|
4323
|
+
const onOff = (b) => b ? success("on") : warning("off");
|
|
4324
|
+
console.log(info("Compliance posture"));
|
|
4325
|
+
console.log(` LLM provider: ${cfg.provider}`);
|
|
4326
|
+
if (cfg.local) {
|
|
4327
|
+
console.log(` Local endpoint: ${cfg.local.baseUrl}`);
|
|
4328
|
+
console.log(` Local model: ${cfg.local.model}`);
|
|
4329
|
+
console.log(success(" → Customer data stays on-machine (data-residency moat)."));
|
|
4330
|
+
}
|
|
4331
|
+
console.log(` AI-Act Art.50 label: ${onOff(cfg.aiDisclosure)}`);
|
|
4332
|
+
console.log(` PII masking: ${onOff(cfg.piiMasking)}`);
|
|
4333
|
+
console.log(` Prompt guardrails: ${onOff(cfg.guardrails)}`);
|
|
4334
|
+
if (cfg.aiDisclosure) console.log(info(` Disclosure: "${aiDisclosure()}"`));
|
|
4335
|
+
});
|
|
4336
|
+
//#endregion
|
|
4337
|
+
//#region src/cli.ts
|
|
4338
|
+
const program = new Command();
|
|
4339
|
+
program.name("dxcrm").description("DatasynxOpenCRM — local-first, MCP-native CRM").version("0.1.0").exitOverride();
|
|
4340
|
+
program.addCommand(initCommand);
|
|
4341
|
+
program.addCommand(createCommand);
|
|
4342
|
+
program.addCommand(listCommand);
|
|
4343
|
+
program.addCommand(validateCommand);
|
|
4344
|
+
program.addCommand(sessionCommand);
|
|
4345
|
+
program.addCommand(guideCommand);
|
|
4346
|
+
program.addCommand(mcpCommand);
|
|
4347
|
+
program.addCommand(syncCommand);
|
|
4348
|
+
program.addCommand(backupCommand);
|
|
4349
|
+
program.addCommand(restoreCommand);
|
|
4350
|
+
program.addCommand(daemonCommand);
|
|
4351
|
+
program.addCommand(statusCommand);
|
|
4352
|
+
program.addCommand(agentCommand);
|
|
4353
|
+
program.addCommand(importCommand);
|
|
4354
|
+
program.addCommand(serverCommand);
|
|
4355
|
+
program.addCommand(auditCommand);
|
|
4356
|
+
program.addCommand(rbacCommand);
|
|
4357
|
+
program.addCommand(gdprCommand);
|
|
4358
|
+
program.addCommand(securityReportCommand);
|
|
4359
|
+
program.addCommand(stagesCommand);
|
|
4360
|
+
program.addCommand(pluginCommand);
|
|
4361
|
+
program.addCommand(goalCommand);
|
|
4362
|
+
program.addCommand(pushCommand);
|
|
4363
|
+
program.addCommand(attachCommand);
|
|
4364
|
+
program.addCommand(templateCommand);
|
|
4365
|
+
program.addCommand(sequenceCommand);
|
|
4366
|
+
program.addCommand(quoteCommand);
|
|
4367
|
+
program.addCommand(ticketCommand);
|
|
4368
|
+
program.addCommand(surveyCommand);
|
|
4369
|
+
program.addCommand(kbCommand);
|
|
4370
|
+
program.addCommand(fieldsCommand);
|
|
4371
|
+
program.addCommand(objectCommand);
|
|
4372
|
+
program.addCommand(webhookCommand);
|
|
4373
|
+
program.addCommand(segmentCommand);
|
|
4374
|
+
program.addCommand(identityCommand);
|
|
4375
|
+
program.addCommand(metricsCommand);
|
|
4376
|
+
program.addCommand(usageCommand);
|
|
4377
|
+
program.addCommand(approvalsCommand);
|
|
4378
|
+
program.addCommand(policyCommand);
|
|
4379
|
+
program.addCommand(hygieneCommand);
|
|
4380
|
+
program.addCommand(memoryCommand);
|
|
4381
|
+
program.addCommand(sopCommand);
|
|
4382
|
+
program.addCommand(toneCommand);
|
|
4383
|
+
program.addCommand(autofillCommand);
|
|
4384
|
+
program.addCommand(askCommand);
|
|
4385
|
+
program.addCommand(nbaCommand);
|
|
4386
|
+
program.addCommand(vaultCommand);
|
|
4387
|
+
program.addCommand(churnCommand);
|
|
4388
|
+
program.addCommand(leadscoreCommand);
|
|
4389
|
+
program.addCommand(enrichCommand);
|
|
4390
|
+
program.addCommand(coachCommand);
|
|
4391
|
+
program.addCommand(complianceCommand);
|
|
4392
|
+
await program.parseAsync(process.argv);
|
|
4393
|
+
//#endregion
|
|
4394
|
+
export {};
|
|
4395
|
+
|
|
4396
|
+
//# sourceMappingURL=cli.js.map
|