@datasynx/agentic-ai-cartography 2.6.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.
package/dist/api-bin.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  parseApiArgs,
4
4
  startApi
5
- } from "./chunk-PQ7Q6MI5.js";
6
- import "./chunk-GA4427LB.js";
5
+ } from "./chunk-TBPGFEMQ.js";
6
+ import "./chunk-YVV6NIT2.js";
7
7
  import "./chunk-QQOQBE2A.js";
8
8
  import "./chunk-2SZ5QHGH.js";
9
9
 
@@ -3,6 +3,7 @@ import {
3
3
  AuthorizationError,
4
4
  CartographyDB,
5
5
  RulesetSchema,
6
+ SCHEMA_VERSION,
6
7
  SqliteCredentialStore,
7
8
  assertSafeBind,
8
9
  authorize,
@@ -24,7 +25,7 @@ import {
24
25
  sanitizeUntrusted,
25
26
  stableStringify,
26
27
  stripSensitive
27
- } from "./chunk-GA4427LB.js";
28
+ } from "./chunk-YVV6NIT2.js";
28
29
  import {
29
30
  EdgeSchema,
30
31
  NODE_TYPES,
@@ -1538,7 +1539,7 @@ async function executeNlQuery(db, sessionId, search, intent, opts = {}) {
1538
1539
 
1539
1540
  // src/mcp/server.ts
1540
1541
  var SERVER_NAME = "cartography";
1541
- var SERVER_VERSION = "2.6.0";
1542
+ var SERVER_VERSION = "2.8.0";
1542
1543
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
1543
1544
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
1544
1545
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -1613,9 +1614,10 @@ function createMcpServer(opts = {}) {
1613
1614
  "graph-summary",
1614
1615
  "cartography://graph/summary",
1615
1616
  { title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
1616
- (uri) => {
1617
+ async (uri) => {
1617
1618
  if (org !== void 0) {
1618
- 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) }] };
1619
1621
  }
1620
1622
  const sid = resolveSession();
1621
1623
  if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
@@ -1690,8 +1692,8 @@ function createMcpServer(opts = {}) {
1690
1692
  server.registerTool(
1691
1693
  "get_summary",
1692
1694
  { title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
1693
- () => {
1694
- 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));
1695
1697
  const sid = resolveSession();
1696
1698
  if (!sid) return json({ error: "No discovery session found." });
1697
1699
  return json(db.getGraphSummary(sid));
@@ -2106,6 +2108,16 @@ async function runHttp(factory, opts = {}) {
2106
2108
  const httpServer = http.createServer(async (req, res) => {
2107
2109
  try {
2108
2110
  const url = req.url ?? "";
2111
+ const probePath = new URL(url || "/", "http://probe").pathname;
2112
+ if (probePath === "/healthz") {
2113
+ res.writeHead(200, { "content-type": "application/json" }).end('{"status":"ok"}');
2114
+ return;
2115
+ }
2116
+ if (probePath === "/readyz") {
2117
+ const r = opts.readiness ? opts.readiness() : { ready: true };
2118
+ res.writeHead(r.ready ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ status: r.ready ? "ready" : "unready" }));
2119
+ return;
2120
+ }
2109
2121
  const isIngest = url.startsWith("/ingest") && opts.onIngest !== void 0;
2110
2122
  if (!url.startsWith("/mcp") && !isIngest) {
2111
2123
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
@@ -2132,8 +2144,8 @@ async function runHttp(factory, opts = {}) {
2132
2144
  res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
2133
2145
  return;
2134
2146
  }
2135
- const out = onIngest(value);
2136
- res.writeHead(out.status, { "content-type": "application/json" }).end(JSON.stringify(out.body));
2147
+ const out = await onIngest(value);
2148
+ res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
2137
2149
  return;
2138
2150
  }
2139
2151
  const sessionId = req.headers["mcp-session-id"];
@@ -2371,6 +2383,148 @@ var SqliteStoreBackend = class {
2371
2383
  }
2372
2384
  };
2373
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
+
2374
2528
  // src/central/ingest.ts
2375
2529
  import { z as z3 } from "zod";
2376
2530
 
@@ -2461,7 +2615,7 @@ var ContributorSchema = z3.object({
2461
2615
  });
2462
2616
  var IngestEnvelopeSchema = z3.object({
2463
2617
  schemaVersion: z3.literal(INGEST_SCHEMA_VERSION),
2464
- org: z3.string().min(1).optional(),
2618
+ org: z3.string().min(1).max(128).optional(),
2465
2619
  items: z3.array(z3.object({
2466
2620
  contentHash: z3.string(),
2467
2621
  kind: z3.enum(["node", "edge"]),
@@ -2471,9 +2625,9 @@ var IngestEnvelopeSchema = z3.object({
2471
2625
  contributor: ContributorSchema.optional(),
2472
2626
  anonymizationLevel: z3.enum(["none", "anonymized", "full"]).optional()
2473
2627
  });
2474
- function ingestEnvelope(store, envelope, opts = {}) {
2628
+ async function ingestEnvelope(store, envelope, opts = {}) {
2475
2629
  const anonMode = opts.anonMode ?? "reject";
2476
- const org = envelope.org ?? opts.defaultOrg ?? "local";
2630
+ const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
2477
2631
  const level = envelope.anonymizationLevel ?? "anonymized";
2478
2632
  const at = (/* @__PURE__ */ new Date()).toISOString();
2479
2633
  const contributor = {
@@ -2524,7 +2678,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
2524
2678
  }
2525
2679
  const safe = check.node;
2526
2680
  const identity = computeIdentity(org, safe);
2527
- const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
2681
+ const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
2528
2682
  accepted += 1;
2529
2683
  if (outcome === "merged") merged += 1;
2530
2684
  acceptedNodeIds.add(safe.id);
@@ -2540,7 +2694,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
2540
2694
  if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
2541
2695
  continue;
2542
2696
  }
2543
- store.insertEdge(org, edge);
2697
+ await store.insertEdge(org, edge);
2544
2698
  edges += 1;
2545
2699
  }
2546
2700
  logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
@@ -2549,15 +2703,24 @@ function ingestEnvelope(store, envelope, opts = {}) {
2549
2703
 
2550
2704
  // src/central/server.ts
2551
2705
  function createIngestHandler(store, opts = {}) {
2552
- return (body) => {
2706
+ const quota = opts.quota;
2707
+ return async (body) => {
2553
2708
  const parsed = IngestEnvelopeSchema.safeParse(body);
2554
2709
  if (!parsed.success) {
2555
2710
  const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
2556
2711
  logWarn("ingest: rejected invalid envelope", { issues });
2557
2712
  return { status: 400, body: { error: "invalid envelope", issues } };
2558
2713
  }
2714
+ if (quota) {
2715
+ const org = normalizeTenant(parsed.data.org ?? opts.defaultOrg);
2716
+ const decision = quota.take(org);
2717
+ if (!decision.allowed) {
2718
+ logWarn("ingest: rate limited", { org, retryAfterSec: decision.retryAfterSec });
2719
+ return { status: 429, body: { error: "too many requests" }, headers: { "retry-after": String(decision.retryAfterSec) } };
2720
+ }
2721
+ }
2559
2722
  try {
2560
- const result = ingestEnvelope(store, parsed.data, opts);
2723
+ const result = await ingestEnvelope(store, parsed.data, opts);
2561
2724
  return { status: 200, body: result };
2562
2725
  } catch (err) {
2563
2726
  logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
@@ -2566,7 +2729,41 @@ function createIngestHandler(store, opts = {}) {
2566
2729
  };
2567
2730
  }
2568
2731
 
2732
+ // src/central/quota.ts
2733
+ var DEFAULT_INGEST_QUOTA = { capacity: 120, refillMs: 6e4 };
2734
+ var MAX_KEYS = 1e4;
2735
+ var RateLimiter = class {
2736
+ constructor(cfg = DEFAULT_INGEST_QUOTA, now = () => Date.now()) {
2737
+ this.cfg = cfg;
2738
+ this.now = now;
2739
+ }
2740
+ buckets = /* @__PURE__ */ new Map();
2741
+ /** Consume one token for `key`. Returns whether the request is allowed (+ Retry-After when not). */
2742
+ take(key) {
2743
+ const t = this.now();
2744
+ const ratePerMs = this.cfg.capacity / this.cfg.refillMs;
2745
+ let b = this.buckets.get(key);
2746
+ if (!b) {
2747
+ if (this.buckets.size >= MAX_KEYS) {
2748
+ const oldest = this.buckets.keys().next().value;
2749
+ if (oldest !== void 0) this.buckets.delete(oldest);
2750
+ }
2751
+ b = { tokens: this.cfg.capacity, last: t };
2752
+ this.buckets.set(key, b);
2753
+ }
2754
+ b.tokens = Math.min(this.cfg.capacity, b.tokens + Math.max(0, t - b.last) * ratePerMs);
2755
+ b.last = t;
2756
+ if (b.tokens >= 1) {
2757
+ b.tokens -= 1;
2758
+ return { allowed: true, retryAfterSec: 0 };
2759
+ }
2760
+ const waitMs = (1 - b.tokens) / ratePerMs;
2761
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil(waitMs / 1e3)) };
2762
+ }
2763
+ };
2764
+
2569
2765
  // src/mcp/start.ts
2766
+ var EXPECTED_USER_VERSION = SCHEMA_VERSION;
2570
2767
  function parseMcpArgs(argv) {
2571
2768
  const opts = {};
2572
2769
  for (let i = 0; i < argv.length; i++) {
@@ -2604,6 +2801,7 @@ async function startMcp(opts = {}) {
2604
2801
  const serverMode = opts.serverMode === true;
2605
2802
  const authStore = new SqliteCredentialStore(db);
2606
2803
  const rbacActive = authStore.count() > 0;
2804
+ let resolvedStore;
2607
2805
  const factory = (principal) => {
2608
2806
  const scopeTenant = rbacActive && principal ? principal.tenant : tenant;
2609
2807
  const orgArg = serverMode ? scopeTenant : void 0;
@@ -2614,7 +2812,8 @@ async function startMcp(opts = {}) {
2614
2812
  search,
2615
2813
  discovery,
2616
2814
  ...principal ? { principal } : {},
2617
- ...orgArg !== void 0 ? { org: orgArg } : {}
2815
+ ...orgArg !== void 0 ? { org: orgArg } : {},
2816
+ ...resolvedStore && orgArg !== void 0 ? { orgSummary: (o) => resolvedStore.getSummary(o) } : {}
2618
2817
  });
2619
2818
  };
2620
2819
  const transport = serverMode ? "http" : opts.transport;
@@ -2624,19 +2823,43 @@ async function startMcp(opts = {}) {
2624
2823
  const token = opts.token ?? process.env["CARTOGRAPHY_HTTP_TOKEN"] ?? process.env["CARTOGRAPHY_CENTRAL_TOKEN"];
2625
2824
  let onIngest;
2626
2825
  if (serverMode) {
2627
- 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;
2628
2833
  const anonMode = opts.anonMode ?? "reject";
2629
- onIngest = createIngestHandler(store, { anonMode, defaultOrg: tenant });
2834
+ const quota = new RateLimiter(opts.ingestQuota ?? DEFAULT_INGEST_QUOTA);
2835
+ onIngest = createIngestHandler(store, { anonMode, defaultOrg: tenant, quota });
2630
2836
  }
2837
+ const readiness = () => {
2838
+ try {
2839
+ const v = db.rawConnection().pragma("user_version", { simple: true });
2840
+ return { ready: v === EXPECTED_USER_VERSION, detail: { schema: v } };
2841
+ } catch (err) {
2842
+ return { ready: false, detail: { error: err instanceof Error ? err.message : String(err) } };
2843
+ }
2844
+ };
2631
2845
  await runHttp(factory, {
2632
2846
  port,
2633
2847
  host,
2848
+ readiness,
2634
2849
  auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
2635
2850
  defaultTenant: tenant,
2636
2851
  ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
2637
2852
  ...token ? { token } : {},
2638
2853
  ...onIngest ? { onIngest } : {}
2639
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
+ }
2640
2863
  const modeNote = serverMode ? ` [central collector: POST /ingest enabled, anon-mode=${opts.anonMode ?? "reject"}]` : "";
2641
2864
  log(`Cartography MCP server (Streamable HTTP) on http://${host}:${port}/mcp${token ? " (auth: bearer token required)" : ""} (tenant: ${tenant})${modeNote}`);
2642
2865
  } else {
@@ -2659,4 +2882,4 @@ export {
2659
2882
  parseMcpArgs,
2660
2883
  startMcp
2661
2884
  };
2662
- //# sourceMappingURL=chunk-X3UWUX3G.js.map
2885
+ //# sourceMappingURL=chunk-5D5ZZEZM.js.map