@datasynx/agentic-ai-cartography 2.7.0 → 2.8.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.
@@ -1539,7 +1539,7 @@ async function executeNlQuery(db, sessionId, search, intent, opts = {}) {
1539
1539
 
1540
1540
  // src/mcp/server.ts
1541
1541
  var SERVER_NAME = "cartography";
1542
- var SERVER_VERSION = "2.7.0";
1542
+ var SERVER_VERSION = "2.8.0";
1543
1543
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
1544
1544
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
1545
1545
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -1614,9 +1614,10 @@ function createMcpServer(opts = {}) {
1614
1614
  "graph-summary",
1615
1615
  "cartography://graph/summary",
1616
1616
  { title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
1617
- (uri) => {
1617
+ async (uri) => {
1618
1618
  if (org !== void 0) {
1619
- return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(db.getOrgSummary(org)) }] };
1619
+ const s = opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org);
1620
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(s) }] };
1620
1621
  }
1621
1622
  const sid = resolveSession();
1622
1623
  if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
@@ -1691,8 +1692,8 @@ function createMcpServer(opts = {}) {
1691
1692
  server.registerTool(
1692
1693
  "get_summary",
1693
1694
  { title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
1694
- () => {
1695
- if (org !== void 0) return json(db.getOrgSummary(org));
1695
+ async () => {
1696
+ if (org !== void 0) return json(opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org));
1696
1697
  const sid = resolveSession();
1697
1698
  if (!sid) return json({ error: "No discovery session found." });
1698
1699
  return json(db.getGraphSummary(sid));
@@ -2143,7 +2144,7 @@ async function runHttp(factory, opts = {}) {
2143
2144
  res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
2144
2145
  return;
2145
2146
  }
2146
- const out = onIngest(value);
2147
+ const out = await onIngest(value);
2147
2148
  res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
2148
2149
  return;
2149
2150
  }
@@ -2382,6 +2383,148 @@ var SqliteStoreBackend = class {
2382
2383
  }
2383
2384
  };
2384
2385
 
2386
+ // src/store/graph.ts
2387
+ function toNum(v) {
2388
+ if (typeof v === "number") return v;
2389
+ if (v && typeof v === "object" && "toNumber" in v && typeof v.toNumber === "function") {
2390
+ return v.toNumber();
2391
+ }
2392
+ return Number(v ?? 0);
2393
+ }
2394
+ var GraphStoreBackend = class {
2395
+ constructor(driver) {
2396
+ this.driver = driver;
2397
+ }
2398
+ async run(cypher, params) {
2399
+ const session = this.driver.session();
2400
+ try {
2401
+ return await session.run(cypher, params);
2402
+ } finally {
2403
+ await session.close();
2404
+ }
2405
+ }
2406
+ async upsertNode(org, node, identity, contributor) {
2407
+ const res = await this.run(
2408
+ `MERGE (n:Node {org: $org, globalId: $globalId})
2409
+ ON CREATE SET n._created = true
2410
+ ON MATCH SET n._created = false
2411
+ SET n.id = $id, n.contentHash = $contentHash, n.type = $type, n.name = $name,
2412
+ n.domain = $domain, n.owner = $owner,
2413
+ n.confidence = CASE WHEN n.confidence IS NULL OR $confidence > n.confidence THEN $confidence ELSE n.confidence END
2414
+ MERGE (c:Contributor {org: $org, globalId: $globalId, machineId: $machineId})
2415
+ SET c.hostname = $hostname, c.user = $user, c.at = $at,
2416
+ c.confidence = CASE WHEN c.confidence IS NULL OR $contribConfidence > c.confidence THEN $contribConfidence ELSE c.confidence END
2417
+ RETURN n._created AS created`,
2418
+ {
2419
+ org,
2420
+ globalId: identity.globalId,
2421
+ contentHash: identity.contentHash,
2422
+ id: node.id,
2423
+ type: node.type,
2424
+ name: node.name,
2425
+ domain: node.domain ?? null,
2426
+ owner: node.owner ?? null,
2427
+ confidence: node.confidence,
2428
+ machineId: contributor.machineId,
2429
+ hostname: contributor.hostname,
2430
+ user: contributor.user,
2431
+ at: contributor.at,
2432
+ contribConfidence: contributor.confidence
2433
+ }
2434
+ );
2435
+ return res.records[0]?.get("created") === true ? "created" : "merged";
2436
+ }
2437
+ async insertEdge(org, edge) {
2438
+ await this.run(
2439
+ `MATCH (s:Node {org: $org, id: $source})
2440
+ MATCH (t:Node {org: $org, id: $target})
2441
+ MERGE (s)-[r:DEPENDS {relationship: $rel}]->(t)
2442
+ SET r.evidence = $evidence, r.confidence = $confidence`,
2443
+ { org, source: edge.sourceId, target: edge.targetId, rel: edge.relationship, evidence: edge.evidence, confidence: edge.confidence }
2444
+ );
2445
+ }
2446
+ async getSummary(org) {
2447
+ const totals = await this.run(
2448
+ `MATCH (n:Node {org: $org})
2449
+ OPTIONAL MATCH (n)-[r:DEPENDS]->(:Node {org: $org})
2450
+ RETURN count(DISTINCT n) AS nodes, count(r) AS edges`,
2451
+ { org }
2452
+ );
2453
+ const byType = await this.run(`MATCH (n:Node {org: $org}) RETURN n.type AS k, count(*) AS c`, { org });
2454
+ const byDomain = await this.run(`MATCH (n:Node {org: $org}) RETURN coalesce(n.domain, '(none)') AS k, count(*) AS c`, { org });
2455
+ const byRel = await this.run(`MATCH (:Node {org: $org})-[r:DEPENDS]->(:Node {org: $org}) RETURN r.relationship AS k, count(*) AS c`, { org });
2456
+ const top = await this.run(
2457
+ `MATCH (n:Node {org: $org})
2458
+ OPTIONAL MATCH (n)-[r:DEPENDS]-(:Node {org: $org})
2459
+ RETURN n.id AS id, n.name AS name, n.type AS type, count(r) AS degree
2460
+ ORDER BY degree DESC, id ASC LIMIT 10`,
2461
+ { org }
2462
+ );
2463
+ const contrib = await this.run(`MATCH (c:Contributor {org: $org}) RETURN count(DISTINCT c.machineId) AS contributors`, { org });
2464
+ const counts = (r) => {
2465
+ const out = {};
2466
+ for (const rec of r.records) out[String(rec.get("k"))] = toNum(rec.get("c"));
2467
+ return out;
2468
+ };
2469
+ return {
2470
+ org,
2471
+ totals: { nodes: toNum(totals.records[0]?.get("nodes")), edges: toNum(totals.records[0]?.get("edges")) },
2472
+ nodesByType: counts(byType),
2473
+ nodesByDomain: counts(byDomain),
2474
+ edgesByRelationship: counts(byRel),
2475
+ topConnected: top.records.map((rec) => ({
2476
+ id: String(rec.get("id")),
2477
+ name: String(rec.get("name")),
2478
+ type: String(rec.get("type")),
2479
+ degree: toNum(rec.get("degree"))
2480
+ })),
2481
+ contributors: toNum(contrib.records[0]?.get("contributors"))
2482
+ };
2483
+ }
2484
+ async getContributors(globalId2) {
2485
+ const res = await this.run(
2486
+ `MATCH (c:Contributor {globalId: $globalId})
2487
+ RETURN c.machineId AS machineId, c.hostname AS hostname, c.user AS user, c.org AS org, c.at AS at, c.confidence AS confidence`,
2488
+ { globalId: globalId2 }
2489
+ );
2490
+ return res.records.map((rec) => ({
2491
+ machineId: String(rec.get("machineId")),
2492
+ hostname: String(rec.get("hostname")),
2493
+ user: String(rec.get("user")),
2494
+ organization: rec.get("org") != null ? String(rec.get("org")) : void 0,
2495
+ at: String(rec.get("at")),
2496
+ confidence: toNum(rec.get("confidence"))
2497
+ }));
2498
+ }
2499
+ async close() {
2500
+ await this.driver.close();
2501
+ }
2502
+ };
2503
+
2504
+ // src/store/index.ts
2505
+ async function defaultNeo4jDriver(url, user, password) {
2506
+ const mod = await import("neo4j-driver");
2507
+ return mod.default.driver(url, mod.default.auth.basic(user, password));
2508
+ }
2509
+ async function openStoreBackend(db, opts = {}) {
2510
+ if (opts.backend === "graph" && opts.graphUrl) {
2511
+ try {
2512
+ const make = opts.driverFactory ?? defaultNeo4jDriver;
2513
+ const driver = await make(opts.graphUrl, opts.graphUser ?? "neo4j", opts.graphPassword ?? "");
2514
+ if (driver.verifyConnectivity) await driver.verifyConnectivity();
2515
+ logInfo("central store: graph backend active", { host: stripSensitive(opts.graphUrl) });
2516
+ return new GraphStoreBackend(driver);
2517
+ } catch (err) {
2518
+ logWarn("central store: graph backend unavailable \u2014 falling back to SQLite", {
2519
+ host: stripSensitive(opts.graphUrl),
2520
+ reason: err instanceof Error ? err.message : String(err)
2521
+ });
2522
+ return new SqliteStoreBackend(db);
2523
+ }
2524
+ }
2525
+ return new SqliteStoreBackend(db);
2526
+ }
2527
+
2385
2528
  // src/central/ingest.ts
2386
2529
  import { z as z3 } from "zod";
2387
2530
 
@@ -2482,9 +2625,9 @@ var IngestEnvelopeSchema = z3.object({
2482
2625
  contributor: ContributorSchema.optional(),
2483
2626
  anonymizationLevel: z3.enum(["none", "anonymized", "full"]).optional()
2484
2627
  });
2485
- function ingestEnvelope(store, envelope, opts = {}) {
2628
+ async function ingestEnvelope(store, envelope, opts = {}) {
2486
2629
  const anonMode = opts.anonMode ?? "reject";
2487
- const org = envelope.org ?? opts.defaultOrg ?? "local";
2630
+ const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
2488
2631
  const level = envelope.anonymizationLevel ?? "anonymized";
2489
2632
  const at = (/* @__PURE__ */ new Date()).toISOString();
2490
2633
  const contributor = {
@@ -2535,7 +2678,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
2535
2678
  }
2536
2679
  const safe = check.node;
2537
2680
  const identity = computeIdentity(org, safe);
2538
- const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
2681
+ const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
2539
2682
  accepted += 1;
2540
2683
  if (outcome === "merged") merged += 1;
2541
2684
  acceptedNodeIds.add(safe.id);
@@ -2551,7 +2694,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
2551
2694
  if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
2552
2695
  continue;
2553
2696
  }
2554
- store.insertEdge(org, edge);
2697
+ await store.insertEdge(org, edge);
2555
2698
  edges += 1;
2556
2699
  }
2557
2700
  logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
@@ -2561,7 +2704,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
2561
2704
  // src/central/server.ts
2562
2705
  function createIngestHandler(store, opts = {}) {
2563
2706
  const quota = opts.quota;
2564
- return (body) => {
2707
+ return async (body) => {
2565
2708
  const parsed = IngestEnvelopeSchema.safeParse(body);
2566
2709
  if (!parsed.success) {
2567
2710
  const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
@@ -2577,7 +2720,7 @@ function createIngestHandler(store, opts = {}) {
2577
2720
  }
2578
2721
  }
2579
2722
  try {
2580
- const result = ingestEnvelope(store, parsed.data, opts);
2723
+ const result = await ingestEnvelope(store, parsed.data, opts);
2581
2724
  return { status: 200, body: result };
2582
2725
  } catch (err) {
2583
2726
  logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
@@ -2658,6 +2801,7 @@ async function startMcp(opts = {}) {
2658
2801
  const serverMode = opts.serverMode === true;
2659
2802
  const authStore = new SqliteCredentialStore(db);
2660
2803
  const rbacActive = authStore.count() > 0;
2804
+ let resolvedStore;
2661
2805
  const factory = (principal) => {
2662
2806
  const scopeTenant = rbacActive && principal ? principal.tenant : tenant;
2663
2807
  const orgArg = serverMode ? scopeTenant : void 0;
@@ -2668,7 +2812,8 @@ async function startMcp(opts = {}) {
2668
2812
  search,
2669
2813
  discovery,
2670
2814
  ...principal ? { principal } : {},
2671
- ...orgArg !== void 0 ? { org: orgArg } : {}
2815
+ ...orgArg !== void 0 ? { org: orgArg } : {},
2816
+ ...resolvedStore && orgArg !== void 0 ? { orgSummary: (o) => resolvedStore.getSummary(o) } : {}
2672
2817
  });
2673
2818
  };
2674
2819
  const transport = serverMode ? "http" : opts.transport;
@@ -2678,7 +2823,13 @@ async function startMcp(opts = {}) {
2678
2823
  const token = opts.token ?? process.env["CARTOGRAPHY_HTTP_TOKEN"] ?? process.env["CARTOGRAPHY_CENTRAL_TOKEN"];
2679
2824
  let onIngest;
2680
2825
  if (serverMode) {
2681
- const store = new SqliteStoreBackend(db);
2826
+ const store = await openStoreBackend(db, {
2827
+ backend: opts.storeBackend ?? (process.env["CARTOGRAPHY_GRAPH_URL"] ? "graph" : "sqlite"),
2828
+ ...opts.graphUrl ?? process.env["CARTOGRAPHY_GRAPH_URL"] ? { graphUrl: opts.graphUrl ?? process.env["CARTOGRAPHY_GRAPH_URL"] } : {},
2829
+ ...opts.graphUser ?? process.env["CARTOGRAPHY_GRAPH_USER"] ? { graphUser: opts.graphUser ?? process.env["CARTOGRAPHY_GRAPH_USER"] } : {},
2830
+ ...opts.graphPassword ?? process.env["CARTOGRAPHY_GRAPH_PASSWORD"] ? { graphPassword: opts.graphPassword ?? process.env["CARTOGRAPHY_GRAPH_PASSWORD"] } : {}
2831
+ });
2832
+ resolvedStore = store;
2682
2833
  const anonMode = opts.anonMode ?? "reject";
2683
2834
  const quota = new RateLimiter(opts.ingestQuota ?? DEFAULT_INGEST_QUOTA);
2684
2835
  onIngest = createIngestHandler(store, { anonMode, defaultOrg: tenant, quota });
@@ -2701,6 +2852,14 @@ async function startMcp(opts = {}) {
2701
2852
  ...token ? { token } : {},
2702
2853
  ...onIngest ? { onIngest } : {}
2703
2854
  });
2855
+ if (resolvedStore) {
2856
+ const closeStore = () => {
2857
+ void Promise.resolve(resolvedStore?.close()).catch(() => {
2858
+ });
2859
+ };
2860
+ process.once("SIGINT", closeStore);
2861
+ process.once("SIGTERM", closeStore);
2862
+ }
2704
2863
  const modeNote = serverMode ? ` [central collector: POST /ingest enabled, anon-mode=${opts.anonMode ?? "reject"}]` : "";
2705
2864
  log(`Cartography MCP server (Streamable HTTP) on http://${host}:${port}/mcp${token ? " (auth: bearer token required)" : ""} (tenant: ${tenant})${modeNote}`);
2706
2865
  } else {
@@ -2723,4 +2882,4 @@ export {
2723
2882
  parseMcpArgs,
2724
2883
  startMcp
2725
2884
  };
2726
- //# sourceMappingURL=chunk-HLWNO3RF.js.map
2885
+ //# sourceMappingURL=chunk-5D5ZZEZM.js.map