@datasynx/agentic-crm 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -243,10 +243,17 @@ right customer by sender/recipient domain** (or to one customer with a slug). On
243
243
  mailbox connection, all customers populated, same attachment + search pipeline.
244
244
 
245
245
  ```bash
246
- DXCRM_IMAP_HOST=outlook.office365.com DXCRM_IMAP_USER=me@org.com DXCRM_IMAP_TOKEN=ey... \
247
- dxcrm mailbox sync # auto-route the whole mailbox to customers by domain
246
+ # One-time OAuth (Gmail & Microsoft 365 require it for IMAP in 2026):
247
+ dxcrm mailbox login gmail --user you@gmail.com
248
+ dxcrm mailbox login microsoft --user you@org.com
249
+
250
+ # Then auto-route the whole mailbox to customers by domain:
251
+ dxcrm mailbox sync --account gmail:you@gmail.com
248
252
  ```
249
253
 
254
+ Tokens are stored locally and auto-refreshed. A password-based IMAP server works too
255
+ (`DXCRM_IMAP_HOST` / `DXCRM_IMAP_USER` / `DXCRM_IMAP_PASS`).
256
+
250
257
  <br/>
251
258
 
252
259
  ## What it's not
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { i as writeJsonFile } from "./json-store-WWsFzXub.js";
5
5
  import { a as warning, i as success, n as error, r as info, t as bold } from "./colors-BG07TZQz.js";
6
6
  import { n as getSession } from "./session-store-CEa39Dxs.js";
7
7
  import { i as sessionCommand, r as readAllSessions } from "./session-BgGDyP2C.js";
8
- import { a as searchKbSimple, i as listKbArticles, n as getKbArticle, o as writeKbArticle, s as CAPABILITIES_TEXT, t as deleteKbArticle } from "./knowledge-base-Bx2PKQR2.js";
8
+ import { a as searchKbSimple, i as listKbArticles, n as getKbArticle, o as writeKbArticle, s as CAPABILITIES_TEXT, t as deleteKbArticle } from "./knowledge-base-Cc0niBFf.js";
9
9
  import { a as restoreCommand, t as backupCommand } from "./backup-CTlIxUdO.js";
10
10
  import { n as readSyncState } from "./sync-state-DMZgzpez.js";
11
11
  import { n as readUnmatched } from "./unmatched-transcripts-DC-VQ9YS.js";
@@ -32,6 +32,7 @@ import { createHash } from "crypto";
32
32
  import Table from "cli-table3";
33
33
  import os from "os";
34
34
  import { execSync, spawn } from "child_process";
35
+ import readline from "readline";
35
36
  //#region src/commands/create.ts
36
37
  async function createCustomer(opts) {
37
38
  const id = slugify(opts.name, { lower: true });
@@ -240,14 +241,14 @@ mcpCommand.command("start").description("Start MCP server (stdio by default)").o
240
241
  if (opts.http) {
241
242
  const port = parseInt(opts.port, 10);
242
243
  console.error(info(`Starting MCP server in HTTP mode on port ${port}...`));
243
- const { startHttp } = await import("./server-uqXUhF4H.js");
244
+ const { startHttp } = await import("./server-BbInMUgp.js");
244
245
  await startHttp(port);
245
246
  } else {
246
- const { startStdio } = await import("./server-uqXUhF4H.js");
247
+ const { startStdio } = await import("./server-BbInMUgp.js");
247
248
  await startStdio();
248
249
  }
249
250
  });
250
- const TOOL_COUNT = 58;
251
+ const TOOL_COUNT = 59;
251
252
  /** Claude Code: CLAUDE.md in CRM dataDir */
252
253
  function buildClaudeMd(dataDir) {
253
254
  return `# DatasynxOpenCRM v2 — Agent Instructions (${TOOL_COUNT} MCP Tools)
@@ -375,6 +376,7 @@ It combines graph, health, revenue simulation, playbook, and org intelligence in
375
376
  - \`get_audit_log({ slug?, actor?, limit? })\` — read append-only audit log of all write operations
376
377
  - \`get_logs({ level?, component?, since?, contains?, limit?, summary? })\` — query/aggregate the structured application log
377
378
  - \`get_diagnostics({ fix? })\` — self-diagnostic health check (data integrity, temp files, log errors, backups)
379
+ - \`get_pipeline_changes({ since?, days? })\` — pipeline time-travel: what changed (won/lost/moved/value) since a date
378
380
 
379
381
  ### Custom Objects (Platform / metadata)
380
382
  - \`define_custom_object({ name, label?, fields })\` — define a runtime entity type with typed fields (no migration), admin
@@ -636,7 +638,7 @@ create_ticket · update_ticket · list_tickets · close_ticket ·
636
638
  send_nps_survey · get_survey_results ·
637
639
  search_knowledge_base · create_kb_article ·
638
640
  backup_now · list_backups ·
639
- trigger_sync · get_audit_log · get_logs · get_diagnostics ·
641
+ trigger_sync · get_audit_log · get_logs · get_diagnostics · get_pipeline_changes ·
640
642
  define_custom_object · create_record · list_records · list_custom_objects
641
643
 
642
644
  ## Data: ${dataDir}`.trim();
@@ -2901,14 +2903,14 @@ function buildQuery(opts) {
2901
2903
  limit: opts.limit ?? 50
2902
2904
  };
2903
2905
  }
2904
- function dataDir$18() {
2906
+ function dataDir$19() {
2905
2907
  return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2906
2908
  }
2907
2909
  const logsCommand = new Command("logs").description("View and analyze the structured application log").option("--level <level>", "Minimum level: debug | info | warn | error").option("--component <name>", "Filter by component (e.g. gmail-sync, lancedb)").option("--since <iso>", "Only entries at or after this ISO timestamp").option("--contains <text>", "Filter by message substring").option("--limit <n>", "Max entries to show (default: 50)", parseInt).option("--summary", "Show aggregated counts (by level + component) instead of entries").action(async (opts) => {
2908
2910
  const query = buildQuery(opts);
2909
2911
  if (opts.summary) {
2910
2912
  const { summarizeLogs } = await import("./logger-vKQS34w9.js");
2911
- const s = summarizeLogs(dataDir$18(), query);
2913
+ const s = summarizeLogs(dataDir$19(), query);
2912
2914
  console.log(bold(`Logs — ${s.total} entr${s.total === 1 ? "y" : "ies"}`));
2913
2915
  if (s.firstTs) console.log(info(` ${s.firstTs} → ${s.lastTs}`));
2914
2916
  console.log(" By level:");
@@ -2925,7 +2927,7 @@ const logsCommand = new Command("logs").description("View and analyze the struct
2925
2927
  return;
2926
2928
  }
2927
2929
  const { queryLogs } = await import("./logger-vKQS34w9.js");
2928
- const entries = queryLogs(dataDir$18(), query);
2930
+ const entries = queryLogs(dataDir$19(), query);
2929
2931
  if (entries.length === 0) {
2930
2932
  console.log(success("No matching log entries."));
2931
2933
  return;
@@ -2937,7 +2939,7 @@ const logsCommand = new Command("logs").description("View and analyze the struct
2937
2939
  });
2938
2940
  //#endregion
2939
2941
  //#region src/commands/doctor.ts
2940
- function dataDir$17() {
2942
+ function dataDir$18() {
2941
2943
  return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2942
2944
  }
2943
2945
  function icon(status) {
@@ -2948,10 +2950,10 @@ function icon(status) {
2948
2950
  const doctorCommand = new Command("doctor").description("Run self-diagnostics: data integrity, temp files, log errors, backup freshness").option("--fix", "Clean up safely-fixable issues (orphaned temp files)").action(async (opts) => {
2949
2951
  const { runDiagnostics, cleanupTempFiles } = await import("./doctor-CYDaNmFn.js");
2950
2952
  if (opts.fix) {
2951
- const removed = cleanupTempFiles(dataDir$17());
2953
+ const removed = cleanupTempFiles(dataDir$18());
2952
2954
  console.log(removed.length > 0 ? success(`Removed ${removed.length} orphaned temp file(s).`) : warning("Nothing to fix."));
2953
2955
  }
2954
- const report = await runDiagnostics(dataDir$17());
2956
+ const report = await runDiagnostics(dataDir$18());
2955
2957
  console.log(bold("dxcrm doctor"));
2956
2958
  for (const c of report.checks) console.log(` ${icon(c.status)} ${c.name.padEnd(16)} ${c.detail}`);
2957
2959
  if (report.ok) {
@@ -2963,6 +2965,59 @@ const doctorCommand = new Command("doctor").description("Run self-diagnostics: d
2963
2965
  }
2964
2966
  });
2965
2967
  //#endregion
2968
+ //#region src/commands/pipeline.ts
2969
+ function dataDir$17() {
2970
+ return process.env["DXCRM_DATA_DIR"] ?? process.cwd();
2971
+ }
2972
+ function daysAgoIso(days) {
2973
+ return (/* @__PURE__ */ new Date(Date.now() - days * 864e5)).toISOString().slice(0, 10);
2974
+ }
2975
+ const pipelineCommand = new Command("pipeline").description("Pipeline time-travel: daily snapshots and 'what changed?' diffs");
2976
+ pipelineCommand.command("snapshot").description("Capture a snapshot of the current pipeline across all customers").action(async () => {
2977
+ const { takeSnapshot } = await import("./snapshots-B6aOhoXs.js");
2978
+ const snap = takeSnapshot(dataDir$17());
2979
+ console.log(success(`Snapshot ${snap.id} taken — ${snap.deals.length} deal(s).`));
2980
+ });
2981
+ pipelineCommand.command("list").description("List available pipeline snapshots").action(async () => {
2982
+ const { listSnapshots } = await import("./snapshots-B6aOhoXs.js");
2983
+ const snaps = listSnapshots(dataDir$17());
2984
+ if (snaps.length === 0) {
2985
+ console.log(info("No snapshots yet. Run 'dxcrm pipeline snapshot' (or let the daemon take daily ones)."));
2986
+ return;
2987
+ }
2988
+ for (const s of snaps) console.log(`${s.id} ${String(s.dealCount).padStart(4)} deals open €${s.openValue.toLocaleString()}`);
2989
+ });
2990
+ pipelineCommand.command("changes").description("Show what changed in the pipeline since a date (default: 7 days ago)").option("--since <YYYY-MM-DD>", "Baseline date (default: 7 days ago)").action(async (opts) => {
2991
+ const since = opts.since ?? daysAgoIso(7);
2992
+ const { diffAgainstNow } = await import("./snapshots-B6aOhoXs.js");
2993
+ const diff = diffAgainstNow(dataDir$17(), since);
2994
+ if (!diff) {
2995
+ console.log(warning(`No snapshot at or before ${since}. Take snapshots first (or wait for the daemon).`));
2996
+ return;
2997
+ }
2998
+ console.log(bold(`Pipeline changes since ${diff.fromId}`));
2999
+ const line = (label, n) => ` ${label.padEnd(16)} ${n}`;
3000
+ console.log(success(line("Won", diff.won.length)));
3001
+ console.log(error(line("Lost", diff.lost.length)));
3002
+ console.log(line("New deals", diff.added.length));
3003
+ console.log(line("Removed", diff.removed.length));
3004
+ console.log(line("Stage moves", diff.advanced.length));
3005
+ console.log(line("Value changes", diff.valueChanged.length));
3006
+ const delta = diff.openValueDelta;
3007
+ const deltaStr = `${delta >= 0 ? "+" : ""}€${delta.toLocaleString()}`;
3008
+ console.log(` ${"Open value".padEnd(16)} €${diff.openValueAfter.toLocaleString()} (${delta >= 0 ? success(deltaStr) : error(deltaStr)})`);
3009
+ if (diff.won.length) console.log(success(`\nWon: ${diff.won.map((d) => d.name).join(", ")}`));
3010
+ if (diff.lost.length) console.log(error(`Lost: ${diff.lost.map((d) => d.name).join(", ")}`));
3011
+ if (diff.advanced.length) {
3012
+ console.log(info("\nStage moves:"));
3013
+ for (const m of diff.advanced) console.log(` ${m.slug}/${m.name}: ${m.from} → ${m.to}`);
3014
+ }
3015
+ if (diff.added.length) {
3016
+ console.log(info("\nNew deals:"));
3017
+ for (const d of diff.added) console.log(` ${d.slug}/${d.name}`);
3018
+ }
3019
+ });
3020
+ //#endregion
2966
3021
  //#region src/commands/rbac.ts
2967
3022
  const ROLES = [
2968
3023
  "admin",
@@ -4427,6 +4482,17 @@ complianceCommand.command("status", { isDefault: true }).description("Show the a
4427
4482
  });
4428
4483
  //#endregion
4429
4484
  //#region src/commands/mailbox.ts
4485
+ /** Default IMAP endpoints for OAuth providers. */
4486
+ const PROVIDER_IMAP_HOST = {
4487
+ gmail: {
4488
+ host: "imap.gmail.com",
4489
+ port: 993
4490
+ },
4491
+ microsoft: {
4492
+ host: "outlook.office365.com",
4493
+ port: 993
4494
+ }
4495
+ };
4430
4496
  /** Read IMAP mailbox connection settings from the environment. */
4431
4497
  function imapConfigFromEnv(env = process.env) {
4432
4498
  const host = env["DXCRM_IMAP_HOST"];
@@ -4448,15 +4514,54 @@ function imapConfigFromEnv(env = process.env) {
4448
4514
  }
4449
4515
  };
4450
4516
  }
4517
+ /** Parse a "provider:user" account string. */
4518
+ function parseAccount(account) {
4519
+ const idx = account.indexOf(":");
4520
+ if (idx < 0) return null;
4521
+ const provider = account.slice(0, idx);
4522
+ const user = account.slice(idx + 1);
4523
+ if (provider !== "gmail" && provider !== "microsoft" || !user) return null;
4524
+ return {
4525
+ provider,
4526
+ user
4527
+ };
4528
+ }
4529
+ /** Build an IMAP config for a stored OAuth account, refreshing the token if needed. */
4530
+ async function resolveAccountConfig(dataDir, account, env = process.env, mailbox) {
4531
+ const parsed = parseAccount(account);
4532
+ if (!parsed) throw new Error(`Invalid --account '${account}'. Use 'gmail:you@gmail.com' or 'microsoft:you@org.com'.`);
4533
+ const { getFreshAccessToken } = await import("./token-resolver-BRLOmRvF.js");
4534
+ const accessToken = await getFreshAccessToken(dataDir, parsed.provider, parsed.user, { env });
4535
+ const { host, port } = PROVIDER_IMAP_HOST[parsed.provider];
4536
+ return {
4537
+ host,
4538
+ port,
4539
+ secure: true,
4540
+ mailbox: mailbox ?? env["DXCRM_IMAP_MAILBOX"] ?? "INBOX",
4541
+ auth: {
4542
+ user: parsed.user,
4543
+ accessToken
4544
+ }
4545
+ };
4546
+ }
4451
4547
  /**
4452
4548
  * Sync an IMAP mailbox (any provider). With a slug, all mail goes to that one
4453
4549
  * customer; without, mail is auto-routed to customers by sender/recipient
4454
- * domain and unmatched mail is reported as unrouted.
4550
+ * domain and unmatched mail is reported as unrouted. Use `account` to sync a
4551
+ * stored OAuth mailbox (Gmail/Outlook) instead of env-configured IMAP.
4455
4552
  */
4456
4553
  async function runMailboxSync(opts) {
4457
- const config = imapConfigFromEnv(opts.env ?? process.env);
4554
+ const env = opts.env ?? process.env;
4555
+ let config;
4556
+ try {
4557
+ config = opts.account ? await resolveAccountConfig(opts.dataDir, opts.account, env) : imapConfigFromEnv(env);
4558
+ } catch (err) {
4559
+ const msg = err.message;
4560
+ console.error(error(msg));
4561
+ return { error: msg };
4562
+ }
4458
4563
  if (!config) {
4459
- const msg = "IMAP not configured. Set DXCRM_IMAP_HOST, DXCRM_IMAP_USER and DXCRM_IMAP_PASS (or DXCRM_IMAP_TOKEN).";
4564
+ const msg = "IMAP not configured. Set DXCRM_IMAP_HOST, DXCRM_IMAP_USER and DXCRM_IMAP_PASS (or DXCRM_IMAP_TOKEN), or use --account.";
4460
4565
  console.error(error(msg));
4461
4566
  return { error: msg };
4462
4567
  }
@@ -4473,11 +4578,67 @@ async function runMailboxSync(opts) {
4473
4578
  if (!opts.slug && result.unrouted > 0) console.log(info(` ${result.unrouted} message(s) matched no customer. Add their domains via 'dxcrm create <slug> --domain <domain>'.`));
4474
4579
  return result;
4475
4580
  }
4581
+ function ask(question) {
4582
+ const rl = readline.createInterface({
4583
+ input: process.stdin,
4584
+ output: process.stdout
4585
+ });
4586
+ return new Promise((resolve) => rl.question(question, (a) => (rl.close(), resolve(a))));
4587
+ }
4588
+ /** Interactive `dxcrm mailbox login <provider>` flow. */
4589
+ async function runLogin(provider, user) {
4590
+ const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
4591
+ const print = (l) => console.log(l);
4592
+ if (provider === "gmail") {
4593
+ const clientId = process.env["DXCRM_GOOGLE_CLIENT_ID"];
4594
+ const clientSecret = process.env["DXCRM_GOOGLE_CLIENT_SECRET"];
4595
+ if (!clientId || !clientSecret) {
4596
+ console.error(error("Set DXCRM_GOOGLE_CLIENT_ID and DXCRM_GOOGLE_CLIENT_SECRET (OAuth desktop app)."));
4597
+ return;
4598
+ }
4599
+ const account = user ?? (await ask("Gmail address: ")).trim();
4600
+ const { runGmailLogin } = await import("./login-CYgla6-A.js");
4601
+ await runGmailLogin({
4602
+ dataDir,
4603
+ clientId,
4604
+ clientSecret,
4605
+ user: account,
4606
+ prompt: ask,
4607
+ print
4608
+ });
4609
+ console.log(success(`✓ Gmail authorized for ${bold(account)}. Sync: dxcrm mailbox sync --account gmail:${account}`));
4610
+ return;
4611
+ }
4612
+ if (provider === "microsoft") {
4613
+ const clientId = process.env["DXCRM_MS_CLIENT_ID"];
4614
+ if (!clientId) {
4615
+ console.error(error("Set DXCRM_MS_CLIENT_ID (Azure app registration, public client)."));
4616
+ return;
4617
+ }
4618
+ const tenant = process.env["DXCRM_MS_TENANT"] ?? "common";
4619
+ const account = user ?? (await ask("Outlook/Microsoft address: ")).trim();
4620
+ const { runMicrosoftLogin } = await import("./login-CYgla6-A.js");
4621
+ await runMicrosoftLogin({
4622
+ dataDir,
4623
+ clientId,
4624
+ user: account,
4625
+ tenant,
4626
+ print
4627
+ });
4628
+ console.log(success(`✓ Microsoft authorized for ${bold(account)}. Sync: dxcrm mailbox sync --account microsoft:${account}`));
4629
+ return;
4630
+ }
4631
+ console.error(error(`Unknown provider '${provider}'. Use 'gmail' or 'microsoft'.`));
4632
+ }
4476
4633
  const mailboxCommand = new Command("mailbox").description("Sync any IMAP mailbox (Gmail, Outlook, custom) into the CRM");
4477
- mailboxCommand.command("sync").description("Sync an IMAP mailbox; auto-routes to customers by domain unless a slug is given").argument("[slug]", "Route all mail to this customer (omit to auto-route by domain)").option("--since <date>", "Only sync messages after this date (YYYY-MM-DD)").option("--no-attachments", "Skip downloading/converting/indexing attachments").action(async (slug, options) => {
4634
+ mailboxCommand.command("login").description("Authorize a Gmail or Microsoft mailbox via OAuth (stores tokens locally)").argument("<provider>", "gmail | microsoft").option("--user <email>", "Mailbox address (otherwise prompted)").action(async (provider, options) => {
4635
+ await runLogin(provider, options.user);
4636
+ });
4637
+ mailboxCommand.command("sync").description("Sync an IMAP mailbox; auto-routes to customers by domain unless a slug is given").argument("[slug]", "Route all mail to this customer (omit to auto-route by domain)").option("--account <provider:user>", "Use a stored OAuth mailbox (e.g. gmail:you@gmail.com)").option("--since <date>", "Only sync messages after this date (YYYY-MM-DD)").option("--no-attachments", "Skip downloading/converting/indexing attachments").action(async (slug, options) => {
4478
4638
  await runMailboxSync({
4479
4639
  dataDir: process.env["DXCRM_DATA_DIR"] ?? process.cwd(),
4480
4640
  slug,
4641
+ ...options.account ? { account: options.account } : {},
4481
4642
  ...options.since ? { since: new Date(options.since) } : {},
4482
4643
  includeAttachments: options.attachments !== false
4483
4644
  });
@@ -4505,6 +4666,7 @@ const ALL_COMMANDS = [
4505
4666
  auditCommand,
4506
4667
  logsCommand,
4507
4668
  doctorCommand,
4669
+ pipelineCommand,
4508
4670
  rbacCommand,
4509
4671
  gdprCommand,
4510
4672
  securityReportCommand,