@datasynx/agentic-ai-cartography 2.3.0 → 2.4.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/cli.js CHANGED
@@ -11,10 +11,10 @@ import {
11
11
  runDrift,
12
12
  runLocalDiscovery,
13
13
  startMcp
14
- } from "./chunk-B2AKONVW.js";
14
+ } from "./chunk-B4QWX7CP.js";
15
15
  import {
16
16
  startApi
17
- } from "./chunk-7VZH5PFV.js";
17
+ } from "./chunk-L4OSL7I6.js";
18
18
  import {
19
19
  CartographyDB,
20
20
  buildCartographyToolHandlers,
@@ -25,7 +25,7 @@ import {
25
25
  redactValue,
26
26
  stableStringify,
27
27
  stripSensitive
28
- } from "./chunk-7QEBFMN4.js";
28
+ } from "./chunk-X5JA2UDT.js";
29
29
  import {
30
30
  ConfigFileSchema,
31
31
  CostEntrySchema,
@@ -36,7 +36,7 @@ import {
36
36
  SharingLevelSchema,
37
37
  centralDbFromEnv,
38
38
  defaultConfig
39
- } from "./chunk-WCR47QA2.js";
39
+ } from "./chunk-QQOQBE2A.js";
40
40
  import {
41
41
  IS_MAC,
42
42
  IS_WIN,
@@ -4508,7 +4508,7 @@ ${infraSummary.substring(0, 12e3)}`;
4508
4508
  `);
4509
4509
  return;
4510
4510
  }
4511
- const { NODE_TYPES } = await import("./types-TJWXAQ2L.js");
4511
+ const { NODE_TYPES } = await import("./types-5L3AGZLG.js");
4512
4512
  if (!process.stdin.isTTY) {
4513
4513
  w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
4514
4514
  process.exitCode = 1;
package/dist/index.cjs CHANGED
@@ -46,14 +46,17 @@ __export(src_exports, {
46
46
  INGEST_SCHEMA_VERSION: () => INGEST_SCHEMA_VERSION,
47
47
  IngestEnvelopeSchema: () => IngestEnvelopeSchema,
48
48
  InvalidTenantError: () => InvalidTenantError,
49
+ JiraSink: () => JiraSink,
49
50
  LOOPBACK_HOSTS: () => LOOPBACK_HOSTS2,
50
51
  MCP_BIN: () => MCP_BIN,
51
52
  NotFoundError: () => NotFoundError,
52
53
  PACKAGE_NAME: () => PACKAGE_NAME,
54
+ PAGERDUTY_ENQUEUE_URL: () => PAGERDUTY_ENQUEUE_URL,
53
55
  PERSONAL: () => PERSONAL,
54
56
  PORT_MAP: () => PORT_MAP,
55
57
  PRIVATE_IP: () => PRIVATE_IP,
56
58
  PUSH_SCHEMA_VERSION: () => PUSH_SCHEMA_VERSION,
59
+ PagerDutySink: () => PagerDutySink,
57
60
  ProviderRegistry: () => ProviderRegistry,
58
61
  RELATION_TO_DIRECTION: () => RELATION_TO_DIRECTION,
59
62
  RuleCheckSchema: () => RuleCheckSchema,
@@ -65,6 +68,7 @@ __export(src_exports, {
65
68
  ScannerRegistry: () => ScannerRegistry,
66
69
  ScannerShape: () => ScannerShape,
67
70
  SharingLevelSchema: () => SharingLevelSchema,
71
+ SlackSink: () => SlackSink,
68
72
  SqliteQueryBackend: () => SqliteQueryBackend,
69
73
  SqliteStoreBackend: () => SqliteStoreBackend,
70
74
  StdoutSink: () => StdoutSink,
@@ -149,6 +153,9 @@ __export(src_exports, {
149
153
  filterBySeverity: () => filterBySeverity,
150
154
  findAnonViolations: () => findAnonViolations,
151
155
  formatComplianceText: () => formatComplianceText,
156
+ formatJira: () => formatJira,
157
+ formatPagerDuty: () => formatPagerDuty,
158
+ formatSlack: () => formatSlack,
152
159
  generateDependencyMermaid: () => generateDependencyMermaid,
153
160
  generateDiffMermaid: () => generateDiffMermaid,
154
161
  generateTopologyMermaid: () => generateTopologyMermaid,
@@ -171,6 +178,7 @@ __export(src_exports, {
171
178
  isPersonalHost: () => isPersonalHost,
172
179
  isReadOnlyCommand: () => isReadOnlyCommand,
173
180
  isRemembered: () => isRemembered,
181
+ isSecureWebhookUrl: () => isSecureWebhookUrl,
174
182
  k8sScanner: () => k8sScanner,
175
183
  keyMetaOf: () => keyMetaOf,
176
184
  layoutClusters: () => layoutClusters,
@@ -209,6 +217,7 @@ __export(src_exports, {
209
217
  pixelToHex: () => pixelToHex,
210
218
  planInstall: () => planInstall,
211
219
  portsScanner: () => portsScanner,
220
+ postJson: () => postJson,
212
221
  previewShare: () => previewShare,
213
222
  pseudonymize: () => pseudonymize,
214
223
  pseudonymizeFragment: () => pseudonymizeFragment,
@@ -417,15 +426,26 @@ var SECURITY_METADATA_KEYS = [
417
426
  var DriftConfigSchema = import_zod.z.object({
418
427
  minSeverity: import_zod.z.enum(SEVERITIES).default("info"),
419
428
  sinks: import_zod.z.array(import_zod.z.object({
420
- type: import_zod.z.enum(["stdout", "webhook"]),
429
+ type: import_zod.z.enum(["stdout", "webhook", "slack", "pagerduty", "jira"]),
421
430
  url: import_zod.z.string().url().optional(),
422
431
  token: import_zod.z.string().optional(),
423
- timeoutMs: import_zod.z.number().int().positive().optional()
432
+ timeoutMs: import_zod.z.number().int().positive().optional(),
433
+ routingKey: import_zod.z.string().optional(),
434
+ email: import_zod.z.string().optional(),
435
+ project: import_zod.z.string().optional(),
436
+ issueType: import_zod.z.string().optional()
424
437
  })).default([{ type: "stdout" }])
425
438
  }).superRefine((cfg, ctx) => {
426
439
  for (const [i, s] of cfg.sinks.entries()) {
427
- if (s.type === "webhook" && !s.url) {
428
- ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: "webhook sink requires a url" });
440
+ const requireUrl = (msg) => {
441
+ if (!s.url) ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: msg });
442
+ };
443
+ if (s.type === "webhook") requireUrl("webhook sink requires a url");
444
+ if (s.type === "slack") requireUrl("slack sink requires a webhook url");
445
+ if (s.type === "jira") {
446
+ requireUrl("jira sink requires a base url");
447
+ if (!s.email) ctx.addIssue({ code: "custom", path: ["sinks", i, "email"], message: "jira sink requires an email" });
448
+ if (!s.project) ctx.addIssue({ code: "custom", path: ["sinks", i, "project"], message: "jira sink requires a project key" });
429
449
  }
430
450
  }
431
451
  });
@@ -2039,8 +2059,17 @@ function stripSensitive(target) {
2039
2059
  const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
2040
2060
  return stripped || raw;
2041
2061
  } catch {
2042
- const stripped = raw.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
2043
- return stripped || raw;
2062
+ let s = raw;
2063
+ const slash = s.indexOf("/");
2064
+ if (slash >= 0) s = s.slice(0, slash);
2065
+ const q = s.indexOf("?");
2066
+ if (q >= 0) s = s.slice(0, q);
2067
+ const at = s.indexOf("@");
2068
+ if (at >= 0) {
2069
+ const colon = s.lastIndexOf(":");
2070
+ if (colon > at) s = s.slice(0, at) + ":" + s.slice(colon + 1);
2071
+ }
2072
+ return s || raw;
2044
2073
  }
2045
2074
  }
2046
2075
  var SCAN_ARG_PATTERNS = {
@@ -2058,7 +2087,7 @@ function assertSafeScanArg(kind, value) {
2058
2087
  return value;
2059
2088
  }
2060
2089
  function redactSecrets(value) {
2061
- return value.replace(/([a-z][a-z0-9+.-]*:\/\/[^:@/\s]+):[^@/\s]+@/gi, "$1:***@");
2090
+ return value.replace(/([a-z][a-z0-9+.-]{0,63}:\/\/[^:@/\s]{1,256}):[^@/\s]{1,256}@/gi, "$1:***@");
2062
2091
  }
2063
2092
  function redactValue(value) {
2064
2093
  if (typeof value === "string") return redactSecrets(value);
@@ -5156,6 +5185,41 @@ var StdoutSink = class {
5156
5185
 
5157
5186
  // src/sinks/webhook.ts
5158
5187
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
5188
+ async function postJson(opts) {
5189
+ const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
5190
+ if (!doFetch) {
5191
+ logWarn("sink unavailable: global fetch missing", { sink: opts.sinkName });
5192
+ return;
5193
+ }
5194
+ if (!opts.url) {
5195
+ logWarn("sink unavailable: no url configured", { sink: opts.sinkName });
5196
+ return;
5197
+ }
5198
+ if (!isSecureWebhookUrl(opts.url)) {
5199
+ logWarn("sink refused: insecure scheme (use https:// or a loopback host)", {
5200
+ sink: opts.sinkName,
5201
+ host: stripSensitive(opts.url)
5202
+ });
5203
+ return;
5204
+ }
5205
+ try {
5206
+ const res = await doFetch(opts.url, {
5207
+ method: "POST",
5208
+ headers: { "content-type": "application/json", ...opts.headers ?? {} },
5209
+ body: JSON.stringify(opts.body),
5210
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
5211
+ });
5212
+ if (!res.ok) {
5213
+ logError("sink delivery failed", { sink: opts.sinkName, host: stripSensitive(opts.url), status: res.status });
5214
+ }
5215
+ } catch (err) {
5216
+ logError("sink delivery failed", {
5217
+ sink: opts.sinkName,
5218
+ host: stripSensitive(opts.url),
5219
+ reason: err instanceof Error ? err.message : String(err)
5220
+ });
5221
+ }
5222
+ }
5159
5223
  function isSecureWebhookUrl(url, env = process.env) {
5160
5224
  if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
5161
5225
  let parsed;
@@ -5174,59 +5238,177 @@ var WebhookSink = class {
5174
5238
  }
5175
5239
  name = "webhook";
5176
5240
  async emit(alert) {
5177
- if (typeof fetch !== "function") {
5178
- logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
5179
- return;
5180
- }
5181
5241
  const { url, token, timeoutMs } = this.opts;
5182
- if (!url) {
5183
- logWarn("webhook sink unavailable: no url configured", { sink: this.name });
5184
- return;
5185
- }
5186
- if (!isSecureWebhookUrl(url)) {
5187
- logWarn("webhook sink refused: insecure scheme (use https:// or a loopback host)", {
5188
- sink: this.name,
5189
- host: stripSensitive(url)
5190
- });
5191
- return;
5192
- }
5193
- try {
5194
- const res = await fetch(url, {
5195
- method: "POST",
5196
- headers: {
5197
- "content-type": "application/json",
5198
- ...token ? { authorization: `Bearer ${token}` } : {}
5199
- },
5200
- body: JSON.stringify(redactValue(alert)),
5201
- signal: AbortSignal.timeout(timeoutMs ?? 1e4)
5202
- });
5203
- if (!res.ok) {
5204
- logError("webhook sink failed", { sink: this.name, host: stripSensitive(url), status: res.status });
5242
+ await postJson({
5243
+ url,
5244
+ body: redactValue(alert),
5245
+ ...token ? { headers: { authorization: `Bearer ${token}` } } : {},
5246
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
5247
+ sinkName: this.name
5248
+ });
5249
+ }
5250
+ };
5251
+
5252
+ // src/sinks/providers.ts
5253
+ var MAX_ITEMS2 = 20;
5254
+ var SEVERITY_EMOJI = { info: "\u{1F7E2}", warning: "\u{1F7E1}", critical: "\u{1F534}" };
5255
+ function headline(alert) {
5256
+ const s = alert.summary;
5257
+ return `${s.nodesAdded}+ / ${s.nodesRemoved}- / ${s.nodesChanged}~ nodes, ${s.edgesAdded}+ / ${s.edgesRemoved}- edges`;
5258
+ }
5259
+ function itemLine(it) {
5260
+ const sec = it.securityFields?.length ? ` [security: ${it.securityFields.join(", ")}]` : "";
5261
+ const fields = it.changedFields?.length ? ` (${it.changedFields.join(", ")})` : "";
5262
+ return `${it.severity.toUpperCase()} \xB7 ${it.kind} \xB7 ${it.label}${fields}${sec}`;
5263
+ }
5264
+ function bodyText(alert) {
5265
+ const lines = alert.items.slice(0, MAX_ITEMS2).map(itemLine);
5266
+ const more = alert.items.length > MAX_ITEMS2 ? [`\u2026and ${alert.items.length - MAX_ITEMS2} more`] : [];
5267
+ return [headline(alert), "", ...lines, ...more].join("\n");
5268
+ }
5269
+ function formatSlack(alert) {
5270
+ const title = `${SEVERITY_EMOJI[alert.severity]} Topology drift \u2014 ${alert.severity}`;
5271
+ return {
5272
+ text: `${title}: ${headline(alert)}`,
5273
+ blocks: [
5274
+ { type: "header", text: { type: "plain_text", text: title, emoji: true } },
5275
+ { type: "section", text: { type: "mrkdwn", text: "```" + bodyText(alert) + "```" } },
5276
+ { type: "context", elements: [{ type: "mrkdwn", text: `base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId} \xB7 ${alert.generatedAt}` }] }
5277
+ ]
5278
+ };
5279
+ }
5280
+ var PD_SEVERITY = {
5281
+ info: "info",
5282
+ warning: "warning",
5283
+ critical: "critical"
5284
+ };
5285
+ function formatPagerDuty(alert, routingKey) {
5286
+ return {
5287
+ routing_key: routingKey,
5288
+ event_action: "trigger",
5289
+ // Stable per base→current pair so repeated alerts for the same delta de-duplicate.
5290
+ dedup_key: `cartograph-drift:${alert.base.sessionId}:${alert.current.sessionId}`,
5291
+ payload: {
5292
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
5293
+ source: "cartograph",
5294
+ severity: PD_SEVERITY[alert.severity],
5295
+ timestamp: alert.generatedAt,
5296
+ custom_details: {
5297
+ summary: alert.summary,
5298
+ items: alert.items.slice(0, MAX_ITEMS2).map((it) => ({
5299
+ kind: it.kind,
5300
+ ref: it.ref,
5301
+ severity: it.severity,
5302
+ ...it.changedFields ? { changedFields: it.changedFields } : {},
5303
+ ...it.securityFields ? { securityFields: it.securityFields } : {}
5304
+ }))
5205
5305
  }
5206
- } catch (err) {
5207
- logError("webhook sink failed", {
5208
- sink: this.name,
5209
- host: stripSensitive(url),
5210
- reason: err instanceof Error ? err.message : String(err)
5211
- });
5212
5306
  }
5307
+ };
5308
+ }
5309
+ function formatJira(alert, opts) {
5310
+ return {
5311
+ fields: {
5312
+ project: { key: opts.project },
5313
+ issuetype: { name: opts.issueType ?? "Task" },
5314
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
5315
+ description: bodyText(alert) + `
5316
+
5317
+ base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId}
5318
+ generated ${alert.generatedAt}`
5319
+ }
5320
+ };
5321
+ }
5322
+
5323
+ // src/sinks/provider-sink.ts
5324
+ var PAGERDUTY_ENQUEUE_URL = "https://events.pagerduty.com/v2/enqueue";
5325
+ function deliver(name, url, body, opts, headers) {
5326
+ return postJson({
5327
+ url,
5328
+ body,
5329
+ sinkName: name,
5330
+ ...headers ? { headers } : {},
5331
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5332
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
5333
+ });
5334
+ }
5335
+ var SlackSink = class {
5336
+ constructor(opts) {
5337
+ this.opts = opts;
5338
+ }
5339
+ name = "slack";
5340
+ async emit(alert) {
5341
+ await deliver(this.name, this.opts.url, formatSlack(redactValue(alert)), this.opts);
5342
+ }
5343
+ };
5344
+ var PagerDutySink = class {
5345
+ constructor(opts) {
5346
+ this.opts = opts;
5347
+ }
5348
+ name = "pagerduty";
5349
+ async emit(alert) {
5350
+ const body = formatPagerDuty(redactValue(alert), this.opts.routingKey);
5351
+ await deliver(this.name, this.opts.url || PAGERDUTY_ENQUEUE_URL, body, this.opts);
5352
+ }
5353
+ };
5354
+ var JiraSink = class {
5355
+ constructor(opts) {
5356
+ this.opts = opts;
5357
+ }
5358
+ name = "jira";
5359
+ async emit(alert) {
5360
+ const body = formatJira(redactValue(alert), {
5361
+ project: this.opts.project,
5362
+ ...this.opts.issueType ? { issueType: this.opts.issueType } : {}
5363
+ });
5364
+ const auth = Buffer.from(`${this.opts.email}:${this.opts.token}`).toString("base64");
5365
+ const base = this.opts.url.replace(/\/+$/, "");
5366
+ await deliver(this.name, `${base}/rest/api/2/issue`, body, this.opts, { authorization: `Basic ${auth}` });
5213
5367
  }
5214
5368
  };
5215
5369
 
5216
5370
  // src/sinks/index.ts
5217
5371
  function buildSinks(drift) {
5218
5372
  const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
5373
+ const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
5219
5374
  const sinks = [];
5220
5375
  for (const s of configs) {
5221
- if (s.type === "webhook") {
5222
- if (!s.url) continue;
5223
- sinks.push(new WebhookSink({
5224
- url: s.url,
5225
- token: s.token ?? process.env.CARTOGRAPHY_DRIFT_TOKEN,
5226
- timeoutMs: s.timeoutMs
5227
- }));
5228
- } else {
5229
- sinks.push(new StdoutSink());
5376
+ const timeoutMs = s.timeoutMs;
5377
+ switch (s.type) {
5378
+ case "webhook":
5379
+ if (!s.url) {
5380
+ logWarn("drift sink skipped: webhook requires a url", { sink: s.type });
5381
+ break;
5382
+ }
5383
+ sinks.push(new WebhookSink({ url: s.url, token: s.token ?? envSecret, timeoutMs }));
5384
+ break;
5385
+ case "slack":
5386
+ if (!s.url) {
5387
+ logWarn("drift sink skipped: slack requires a webhook url", { sink: s.type });
5388
+ break;
5389
+ }
5390
+ sinks.push(new SlackSink({ url: s.url, timeoutMs }));
5391
+ break;
5392
+ case "pagerduty": {
5393
+ const routingKey = s.routingKey ?? s.token ?? envSecret;
5394
+ if (!routingKey) {
5395
+ logWarn("drift sink skipped: pagerduty requires a routingKey (or CARTOGRAPHY_DRIFT_TOKEN)", { sink: s.type });
5396
+ break;
5397
+ }
5398
+ sinks.push(new PagerDutySink({ url: s.url ?? PAGERDUTY_ENQUEUE_URL, routingKey, timeoutMs }));
5399
+ break;
5400
+ }
5401
+ case "jira": {
5402
+ const token = s.token ?? envSecret;
5403
+ if (!s.url || !s.email || !s.project || !token) {
5404
+ logWarn("drift sink skipped: jira requires url, email, project and a token", { sink: s.type });
5405
+ break;
5406
+ }
5407
+ sinks.push(new JiraSink({ url: s.url, email: s.email, token, project: s.project, issueType: s.issueType, timeoutMs }));
5408
+ break;
5409
+ }
5410
+ default:
5411
+ sinks.push(new StdoutSink());
5230
5412
  }
5231
5413
  }
5232
5414
  return sinks.length > 0 ? sinks : [new StdoutSink()];
@@ -5635,7 +5817,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5635
5817
 
5636
5818
  // src/mcp/server.ts
5637
5819
  var SERVER_NAME = "cartography";
5638
- var SERVER_VERSION = "2.3.0";
5820
+ var SERVER_VERSION = "2.4.0";
5639
5821
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5640
5822
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5641
5823
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -11393,14 +11575,17 @@ function checkClaudePrerequisites() {
11393
11575
  INGEST_SCHEMA_VERSION,
11394
11576
  IngestEnvelopeSchema,
11395
11577
  InvalidTenantError,
11578
+ JiraSink,
11396
11579
  LOOPBACK_HOSTS,
11397
11580
  MCP_BIN,
11398
11581
  NotFoundError,
11399
11582
  PACKAGE_NAME,
11583
+ PAGERDUTY_ENQUEUE_URL,
11400
11584
  PERSONAL,
11401
11585
  PORT_MAP,
11402
11586
  PRIVATE_IP,
11403
11587
  PUSH_SCHEMA_VERSION,
11588
+ PagerDutySink,
11404
11589
  ProviderRegistry,
11405
11590
  RELATION_TO_DIRECTION,
11406
11591
  RuleCheckSchema,
@@ -11412,6 +11597,7 @@ function checkClaudePrerequisites() {
11412
11597
  ScannerRegistry,
11413
11598
  ScannerShape,
11414
11599
  SharingLevelSchema,
11600
+ SlackSink,
11415
11601
  SqliteQueryBackend,
11416
11602
  SqliteStoreBackend,
11417
11603
  StdoutSink,
@@ -11496,6 +11682,9 @@ function checkClaudePrerequisites() {
11496
11682
  filterBySeverity,
11497
11683
  findAnonViolations,
11498
11684
  formatComplianceText,
11685
+ formatJira,
11686
+ formatPagerDuty,
11687
+ formatSlack,
11499
11688
  generateDependencyMermaid,
11500
11689
  generateDiffMermaid,
11501
11690
  generateTopologyMermaid,
@@ -11518,6 +11707,7 @@ function checkClaudePrerequisites() {
11518
11707
  isPersonalHost,
11519
11708
  isReadOnlyCommand,
11520
11709
  isRemembered,
11710
+ isSecureWebhookUrl,
11521
11711
  k8sScanner,
11522
11712
  keyMetaOf,
11523
11713
  layoutClusters,
@@ -11556,6 +11746,7 @@ function checkClaudePrerequisites() {
11556
11746
  pixelToHex,
11557
11747
  planInstall,
11558
11748
  portsScanner,
11749
+ postJson,
11559
11750
  previewShare,
11560
11751
  pseudonymize,
11561
11752
  pseudonymizeFragment,