@datasynx/agentic-ai-cartography 0.9.2 → 1.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/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  IS_MAC,
13
13
  IS_WIN,
14
14
  PLATFORM,
15
+ cleanupTempFiles,
15
16
  commandExists,
16
17
  dbScanDirs,
17
18
  findFiles,
@@ -20,7 +21,7 @@ import {
20
21
  scanAllHistory,
21
22
  scanWindowsDbServices,
22
23
  scanWindowsPrograms
23
- } from "./chunk-3NVQ3ND6.js";
24
+ } from "./chunk-QKNYI3SU.js";
24
25
 
25
26
  // src/cli.ts
26
27
  import { Command } from "commander";
@@ -66,6 +67,83 @@ function checkPrerequisites() {
66
67
  import Database from "better-sqlite3";
67
68
  import { mkdirSync } from "fs";
68
69
  import { dirname } from "path";
70
+ import { z } from "zod";
71
+ var SessionRowSchema = z.object({
72
+ id: z.string(),
73
+ mode: z.literal("discover"),
74
+ started_at: z.string(),
75
+ completed_at: z.string().nullable().optional(),
76
+ config: z.string()
77
+ });
78
+ var NodeRowSchema = z.object({
79
+ id: z.string(),
80
+ session_id: z.string(),
81
+ type: z.enum(NODE_TYPES),
82
+ name: z.string(),
83
+ discovered_via: z.string().nullable().optional(),
84
+ discovered_at: z.string(),
85
+ path_id: z.string().nullable().optional(),
86
+ depth: z.number().default(0),
87
+ confidence: z.number().default(0.5),
88
+ metadata: z.string().default("{}"),
89
+ tags: z.string().default("[]"),
90
+ domain: z.string().nullable().optional(),
91
+ sub_domain: z.string().nullable().optional(),
92
+ quality_score: z.number().nullable().optional()
93
+ });
94
+ var EdgeRowSchema = z.object({
95
+ id: z.string(),
96
+ session_id: z.string(),
97
+ source_id: z.string(),
98
+ target_id: z.string(),
99
+ relationship: z.enum(EDGE_RELATIONSHIPS),
100
+ evidence: z.string().nullable().optional(),
101
+ confidence: z.number().default(0.5),
102
+ discovered_at: z.string()
103
+ });
104
+ var EventRowSchema = z.object({
105
+ id: z.string(),
106
+ session_id: z.string(),
107
+ task_id: z.string().nullable().optional(),
108
+ timestamp: z.string(),
109
+ event_type: z.string(),
110
+ process: z.string(),
111
+ pid: z.number(),
112
+ target: z.string().nullable().optional(),
113
+ target_type: z.string().nullable().optional(),
114
+ port: z.number().nullable().optional(),
115
+ duration_ms: z.number().nullable().optional()
116
+ });
117
+ var TaskRowSchema = z.object({
118
+ id: z.string(),
119
+ session_id: z.string(),
120
+ description: z.string().nullable().optional(),
121
+ started_at: z.string(),
122
+ completed_at: z.string().nullable().optional(),
123
+ steps: z.string().default("[]"),
124
+ involved_services: z.string().default("[]"),
125
+ status: z.enum(["active", "completed", "cancelled"])
126
+ });
127
+ var WorkflowRowSchema = z.object({
128
+ id: z.string(),
129
+ session_id: z.string(),
130
+ name: z.string().nullable().optional(),
131
+ pattern: z.string(),
132
+ task_ids: z.string().default("[]"),
133
+ occurrences: z.number().default(1),
134
+ first_seen: z.string(),
135
+ last_seen: z.string(),
136
+ avg_duration_ms: z.number().nullable().optional(),
137
+ involved_services: z.string().default("[]")
138
+ });
139
+ var ConnectionRowSchema = z.object({
140
+ id: z.string(),
141
+ session_id: z.string(),
142
+ source_asset_id: z.string(),
143
+ target_asset_id: z.string(),
144
+ type: z.string().nullable().optional(),
145
+ created_at: z.string()
146
+ });
69
147
  var SCHEMA = `
70
148
  PRAGMA journal_mode = WAL;
71
149
  PRAGMA foreign_keys = ON;
@@ -230,12 +308,13 @@ var CartographyDB = class {
230
308
  return rows.map((r) => this.mapSession(r));
231
309
  }
232
310
  mapSession(r) {
311
+ const v = SessionRowSchema.parse(r);
233
312
  return {
234
- id: r["id"],
235
- mode: r["mode"],
236
- startedAt: r["started_at"],
237
- completedAt: r["completed_at"] ?? void 0,
238
- config: r["config"]
313
+ id: v.id,
314
+ mode: v.mode,
315
+ startedAt: v.started_at,
316
+ completedAt: v.completed_at ?? void 0,
317
+ config: v.config
239
318
  };
240
319
  }
241
320
  // ── Nodes ───────────────────────────────
@@ -266,21 +345,22 @@ var CartographyDB = class {
266
345
  return rows.map((r) => this.mapNode(r));
267
346
  }
268
347
  mapNode(r) {
348
+ const v = NodeRowSchema.parse(r);
269
349
  return {
270
- id: r["id"],
271
- sessionId: r["session_id"],
272
- type: r["type"],
273
- name: r["name"],
274
- discoveredVia: r["discovered_via"],
275
- discoveredAt: r["discovered_at"],
276
- depth: r["depth"],
277
- confidence: r["confidence"],
278
- metadata: JSON.parse(r["metadata"]),
279
- tags: JSON.parse(r["tags"]),
280
- pathId: r["path_id"],
281
- domain: r["domain"] ?? void 0,
282
- subDomain: r["sub_domain"] ?? void 0,
283
- qualityScore: r["quality_score"] ?? void 0
350
+ id: v.id,
351
+ sessionId: v.session_id,
352
+ type: v.type,
353
+ name: v.name,
354
+ discoveredVia: v.discovered_via ?? "",
355
+ discoveredAt: v.discovered_at,
356
+ depth: v.depth,
357
+ confidence: v.confidence,
358
+ metadata: JSON.parse(v.metadata),
359
+ tags: JSON.parse(v.tags),
360
+ pathId: v.path_id ?? void 0,
361
+ domain: v.domain ?? void 0,
362
+ subDomain: v.sub_domain ?? void 0,
363
+ qualityScore: v.quality_score ?? void 0
284
364
  };
285
365
  }
286
366
  deleteNode(sessionId, nodeId) {
@@ -309,16 +389,19 @@ var CartographyDB = class {
309
389
  }
310
390
  getEdges(sessionId) {
311
391
  const rows = this.db.prepare("SELECT * FROM edges WHERE session_id = ?").all(sessionId);
312
- return rows.map((r) => ({
313
- id: r["id"],
314
- sessionId: r["session_id"],
315
- sourceId: r["source_id"],
316
- targetId: r["target_id"],
317
- relationship: r["relationship"],
318
- evidence: r["evidence"],
319
- confidence: r["confidence"],
320
- discoveredAt: r["discovered_at"]
321
- }));
392
+ return rows.map((r) => {
393
+ const v = EdgeRowSchema.parse(r);
394
+ return {
395
+ id: v.id,
396
+ sessionId: v.session_id,
397
+ sourceId: v.source_id,
398
+ targetId: v.target_id,
399
+ relationship: v.relationship,
400
+ evidence: v.evidence ?? "",
401
+ confidence: v.confidence,
402
+ discoveredAt: v.discovered_at
403
+ };
404
+ });
322
405
  }
323
406
  // ── Events ──────────────────────────────
324
407
  insertEvent(sessionId, event, taskId) {
@@ -342,19 +425,22 @@ var CartographyDB = class {
342
425
  }
343
426
  getEvents(sessionId, since) {
344
427
  const rows = since ? this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp").all(sessionId, since) : this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? ORDER BY timestamp").all(sessionId);
345
- return rows.map((r) => ({
346
- id: r["id"],
347
- sessionId: r["session_id"],
348
- taskId: r["task_id"],
349
- timestamp: r["timestamp"],
350
- eventType: r["event_type"],
351
- process: r["process"],
352
- pid: r["pid"],
353
- target: r["target"],
354
- targetType: r["target_type"],
355
- port: r["port"],
356
- durationMs: r["duration_ms"]
357
- }));
428
+ return rows.map((r) => {
429
+ const v = EventRowSchema.parse(r);
430
+ return {
431
+ id: v.id,
432
+ sessionId: v.session_id,
433
+ taskId: v.task_id ?? void 0,
434
+ timestamp: v.timestamp,
435
+ eventType: v.event_type,
436
+ process: v.process,
437
+ pid: v.pid,
438
+ target: v.target ?? void 0,
439
+ targetType: v.target_type ?? void 0,
440
+ port: v.port ?? void 0,
441
+ durationMs: v.duration_ms ?? void 0
442
+ };
443
+ });
358
444
  }
359
445
  // ── Tasks ───────────────────────────────
360
446
  startTask(sessionId, description) {
@@ -388,15 +474,16 @@ var CartographyDB = class {
388
474
  return rows.map((r) => this.mapTask(r));
389
475
  }
390
476
  mapTask(r) {
477
+ const v = TaskRowSchema.parse(r);
391
478
  return {
392
- id: r["id"],
393
- sessionId: r["session_id"],
394
- description: r["description"],
395
- startedAt: r["started_at"],
396
- completedAt: r["completed_at"],
397
- steps: r["steps"],
398
- involvedServices: r["involved_services"],
399
- status: r["status"]
479
+ id: v.id,
480
+ sessionId: v.session_id,
481
+ description: v.description ?? void 0,
482
+ startedAt: v.started_at,
483
+ completedAt: v.completed_at ?? void 0,
484
+ steps: v.steps,
485
+ involvedServices: v.involved_services,
486
+ status: v.status
400
487
  };
401
488
  }
402
489
  // ── Workflows ───────────────────────────
@@ -422,18 +509,21 @@ var CartographyDB = class {
422
509
  }
423
510
  getWorkflows(sessionId) {
424
511
  const rows = this.db.prepare("SELECT * FROM workflows WHERE session_id = ?").all(sessionId);
425
- return rows.map((r) => ({
426
- id: r["id"],
427
- sessionId: r["session_id"],
428
- name: r["name"],
429
- pattern: r["pattern"],
430
- taskIds: r["task_ids"],
431
- occurrences: r["occurrences"],
432
- firstSeen: r["first_seen"],
433
- lastSeen: r["last_seen"],
434
- avgDurationMs: r["avg_duration_ms"],
435
- involvedServices: r["involved_services"]
436
- }));
512
+ return rows.map((r) => {
513
+ const v = WorkflowRowSchema.parse(r);
514
+ return {
515
+ id: v.id,
516
+ sessionId: v.session_id,
517
+ name: v.name ?? void 0,
518
+ pattern: v.pattern,
519
+ taskIds: v.task_ids,
520
+ occurrences: v.occurrences,
521
+ firstSeen: v.first_seen,
522
+ lastSeen: v.last_seen,
523
+ avgDurationMs: v.avg_duration_ms ?? 0,
524
+ involvedServices: v.involved_services
525
+ };
526
+ });
437
527
  }
438
528
  // ── Connections (user-created hex map links) ─────────────────────────────
439
529
  upsertConnection(sessionId, conn) {
@@ -450,14 +540,17 @@ var CartographyDB = class {
450
540
  }
451
541
  getConnections(sessionId) {
452
542
  const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
453
- return rows.map((r) => ({
454
- id: r["id"],
455
- sessionId: r["session_id"],
456
- sourceAssetId: r["source_asset_id"],
457
- targetAssetId: r["target_asset_id"],
458
- type: r["type"] ?? void 0,
459
- createdAt: r["created_at"]
460
- }));
543
+ return rows.map((r) => {
544
+ const v = ConnectionRowSchema.parse(r);
545
+ return {
546
+ id: v.id,
547
+ sessionId: v.session_id,
548
+ sourceAssetId: v.source_asset_id,
549
+ targetAssetId: v.target_asset_id,
550
+ type: v.type ?? void 0,
551
+ createdAt: v.created_at
552
+ };
553
+ });
461
554
  }
462
555
  deleteConnection(sessionId, connectionId) {
463
556
  this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
@@ -472,6 +565,31 @@ var CartographyDB = class {
472
565
  const row = this.db.prepare("SELECT action FROM node_approvals WHERE pattern = ?").get(pattern);
473
566
  return row?.action;
474
567
  }
568
+ // ── Pruning ──────────────────────────────
569
+ /**
570
+ * Delete a session and all its associated data (nodes, edges, events, tasks, workflows, connections).
571
+ */
572
+ deleteSession(sessionId) {
573
+ this.db.prepare("DELETE FROM connections WHERE session_id = ?").run(sessionId);
574
+ this.db.prepare("DELETE FROM workflows WHERE session_id = ?").run(sessionId);
575
+ this.db.prepare("DELETE FROM activity_events WHERE session_id = ?").run(sessionId);
576
+ this.db.prepare("DELETE FROM tasks WHERE session_id = ?").run(sessionId);
577
+ this.db.prepare("DELETE FROM edges WHERE session_id = ?").run(sessionId);
578
+ this.db.prepare("DELETE FROM nodes WHERE session_id = ?").run(sessionId);
579
+ this.db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
580
+ }
581
+ /**
582
+ * Prune sessions older than the given ISO date string. Returns count of deleted sessions.
583
+ */
584
+ pruneSessions(olderThan) {
585
+ const rows = this.db.prepare(
586
+ "SELECT id FROM sessions WHERE started_at < ?"
587
+ ).all(olderThan);
588
+ for (const row of rows) {
589
+ this.deleteSession(row.id);
590
+ }
591
+ return rows.length;
592
+ }
475
593
  // ── Stats ───────────────────────────────
476
594
  getStats(sessionId) {
477
595
  const nodes = this.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE session_id = ?").get(sessionId).c;
@@ -483,7 +601,23 @@ var CartographyDB = class {
483
601
  };
484
602
 
485
603
  // src/tools.ts
486
- import { z } from "zod";
604
+ import { z as z2 } from "zod";
605
+ function createScanRunner(runFn, opts = {}) {
606
+ const threshold = opts.threshold ?? 3;
607
+ let consecutiveFailures = 0;
608
+ let tripped = false;
609
+ return (cmd) => {
610
+ if (tripped) return "(skipped \u2014 circuit breaker: too many consecutive failures)";
611
+ const result = runFn(cmd, { timeout: opts.timeout ?? 2e4, env: opts.env });
612
+ if (!result) {
613
+ consecutiveFailures++;
614
+ if (consecutiveFailures >= threshold) tripped = true;
615
+ return "(error or not available)";
616
+ }
617
+ consecutiveFailures = 0;
618
+ return result;
619
+ };
620
+ }
487
621
  function stripSensitive(target) {
488
622
  try {
489
623
  const url = new URL(target.startsWith("http") ? target : `tcp://${target}`);
@@ -496,16 +630,16 @@ async function createCartographyTools(db, sessionId, opts = {}) {
496
630
  const { tool, createSdkMcpServer } = await import("@anthropic-ai/claude-agent-sdk");
497
631
  const tools = [
498
632
  tool("save_node", "Save an infrastructure node to the catalog", {
499
- id: z.string(),
500
- type: z.enum(NODE_TYPES),
501
- name: z.string(),
502
- discoveredVia: z.string(),
503
- confidence: z.number().min(0).max(1),
504
- metadata: z.record(z.string(), z.unknown()).optional(),
505
- tags: z.array(z.string()).optional(),
506
- domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
507
- subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
508
- qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
633
+ id: z2.string(),
634
+ type: z2.enum(NODE_TYPES),
635
+ name: z2.string(),
636
+ discoveredVia: z2.string(),
637
+ confidence: z2.number().min(0).max(1),
638
+ metadata: z2.record(z2.string(), z2.unknown()).optional(),
639
+ tags: z2.array(z2.string()).optional(),
640
+ domain: z2.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
641
+ subDomain: z2.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
642
+ qualityScore: z2.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
509
643
  }, async (args) => {
510
644
  const node = {
511
645
  id: stripSensitive(args["id"]),
@@ -523,11 +657,11 @@ async function createCartographyTools(db, sessionId, opts = {}) {
523
657
  return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
524
658
  }),
525
659
  tool("save_edge", "Save a relationship (edge) between two nodes \u2014 ALWAYS save edges when connections are clear", {
526
- sourceId: z.string(),
527
- targetId: z.string(),
528
- relationship: z.enum(EDGE_RELATIONSHIPS),
529
- evidence: z.string(),
530
- confidence: z.number().min(0).max(1)
660
+ sourceId: z2.string(),
661
+ targetId: z2.string(),
662
+ relationship: z2.enum(EDGE_RELATIONSHIPS),
663
+ evidence: z2.string(),
664
+ confidence: z2.number().min(0).max(1)
531
665
  }, async (args) => {
532
666
  db.insertEdge(sessionId, {
533
667
  sourceId: args["sourceId"],
@@ -539,7 +673,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
539
673
  return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
540
674
  }),
541
675
  tool("get_catalog", "Get the current catalog \u2014 use before save_node to avoid duplicates", {
542
- includeEdges: z.boolean().default(true)
676
+ includeEdges: z2.boolean().default(true)
543
677
  }, async (args) => {
544
678
  const nodes = db.getNodes(sessionId);
545
679
  const edges = args["includeEdges"] ? db.getEdges(sessionId) : [];
@@ -554,8 +688,8 @@ async function createCartographyTools(db, sessionId, opts = {}) {
554
688
  };
555
689
  }),
556
690
  tool("ask_user", "Ask the user a question \u2014 for clarifications, missing context, or consent (e.g. before scanning browser history)", {
557
- question: z.string().describe("The question for the user (clear and specific)"),
558
- context: z.string().optional().describe("Optional context explaining why this is relevant")
691
+ question: z2.string().describe("The question for the user (clear and specific)"),
692
+ context: z2.string().optional().describe("Optional context explaining why this is relevant")
559
693
  }, async (args) => {
560
694
  const question = args["question"];
561
695
  const context = args["context"];
@@ -568,7 +702,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
568
702
  };
569
703
  }),
570
704
  tool("scan_bookmarks", "Scan all browser bookmarks \u2014 hostnames only, no personal data (Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Firefox)", {
571
- minConfidence: z.number().min(0).max(1).default(0.5).optional()
705
+ minConfidence: z2.number().min(0).max(1).default(0.5).optional()
572
706
  }, async () => {
573
707
  const hosts = await scanAllBookmarks();
574
708
  return {
@@ -588,7 +722,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
588
722
  };
589
723
  }),
590
724
  tool("scan_browser_history", "Scan browser history \u2014 anonymized hostnames + visit frequency. ALWAYS call ask_user for consent before using this tool.", {
591
- minVisits: z.number().min(1).default(3).optional().describe("Minimum visit count to include a host (filters rarely-visited sites)")
725
+ minVisits: z2.number().min(1).default(3).optional().describe("Minimum visit count to include a host (filters rarely-visited sites)")
592
726
  }, async (args) => {
593
727
  const minVisits = args["minVisits"] ?? 3;
594
728
  const hosts = await scanAllHistory();
@@ -610,7 +744,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
610
744
  };
611
745
  }),
612
746
  tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
613
- deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
747
+ deep: z2.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
614
748
  }, async (args) => {
615
749
  const deep = args["deep"] ?? false;
616
750
  const results = {};
@@ -682,14 +816,11 @@ ${v}`).join("\n\n");
682
816
  return { content: [{ type: "text", text: out }] };
683
817
  }),
684
818
  tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
685
- namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
819
+ namespace: z2.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
686
820
  }, async (args) => {
687
821
  const ns = args["namespace"];
688
822
  const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
689
- const runK = (cmd) => {
690
- const r = run(cmd, { timeout: 15e3 });
691
- return r || `(error or not available)`;
692
- };
823
+ const runK = createScanRunner(run, { timeout: 15e3, threshold: 3 });
693
824
  const sections = IS_WIN ? [
694
825
  ["CONTEXT", "kubectl config current-context"],
695
826
  ["NODES", "kubectl get nodes -o wide"],
@@ -716,15 +847,15 @@ ${runK(c)}`).join("\n\n");
716
847
  return { content: [{ type: "text", text: out }] };
717
848
  }),
718
849
  tool("scan_aws_resources", "Scan AWS infrastructure via AWS CLI \u2014 100% readonly (describe, list)", {
719
- region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
720
- profile: z.string().optional().describe("AWS CLI profile")
850
+ region: z2.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
851
+ profile: z2.string().optional().describe("AWS CLI profile")
721
852
  }, async (args) => {
722
853
  const region = args["region"];
723
854
  const profile = args["profile"];
724
855
  const env = { ...process.env };
725
856
  if (region) env["AWS_DEFAULT_REGION"] = region;
726
857
  const pf = profile ? `--profile ${profile}` : "";
727
- const runAws = (cmd) => run(cmd, { timeout: 2e4, env }) || "(error or not available)";
858
+ const runAws = createScanRunner(run, { timeout: 2e4, env, threshold: 3 });
728
859
  const sections = [
729
860
  ["IDENTITY", `aws sts get-caller-identity ${pf} --output json`],
730
861
  ["EC2", `aws ec2 describe-instances ${pf} --query "Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name,PublicIpAddress,PrivateIpAddress]" --output table`],
@@ -740,11 +871,11 @@ ${runAws(c)}`).join("\n\n");
740
871
  return { content: [{ type: "text", text: out }] };
741
872
  }),
742
873
  tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
743
- project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
874
+ project: z2.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
744
875
  }, async (args) => {
745
876
  const project = args["project"];
746
877
  const pf = project ? `--project ${project}` : "";
747
- const runGcp = (cmd) => run(cmd, { timeout: 2e4 }) || "(error or not available)";
878
+ const runGcp = createScanRunner(run, { timeout: 2e4, threshold: 3 });
748
879
  const sections = [
749
880
  ["IDENTITY", `gcloud config list account --format="value(core.account)"`],
750
881
  ["COMPUTE_INSTANCES", `gcloud compute instances list ${pf}`],
@@ -761,14 +892,14 @@ ${runGcp(c)}`).join("\n\n");
761
892
  return { content: [{ type: "text", text: out }] };
762
893
  }),
763
894
  tool("scan_azure_resources", "Scan Azure infrastructure via az CLI \u2014 100% readonly (list, show)", {
764
- subscription: z.string().optional().describe("Azure Subscription ID"),
765
- resourceGroup: z.string().optional().describe("Filter by resource group")
895
+ subscription: z2.string().optional().describe("Azure Subscription ID"),
896
+ resourceGroup: z2.string().optional().describe("Filter by resource group")
766
897
  }, async (args) => {
767
898
  const sub = args["subscription"];
768
899
  const rg = args["resourceGroup"];
769
900
  const sf = sub ? `--subscription ${sub}` : "";
770
901
  const rf = rg ? `--resource-group ${rg}` : "";
771
- const runAz = (cmd) => run(cmd, { timeout: 2e4 }) || "(error or not available)";
902
+ const runAz = createScanRunner(run, { timeout: 2e4, threshold: 3 });
772
903
  const sections = [
773
904
  ["IDENTITY", `az account show --output json ${sf}`],
774
905
  ["VMS", `az vm list ${sf} ${rf} --output table`],
@@ -785,7 +916,7 @@ ${runAz(c)}`).join("\n\n");
785
916
  return { content: [{ type: "text", text: out }] };
786
917
  }),
787
918
  tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
788
- searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
919
+ searchHint: z2.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
789
920
  }, async (args) => {
790
921
  const hint = args["searchHint"];
791
922
  const results = {};
@@ -1109,68 +1240,81 @@ Then scan_local_databases() for database servers and SQLite files.
1109
1240
  Then systematically scan local services, then config files.
1110
1241
  Finally, map all edges (Step 8 \u2014 critical!) before finishing.
1111
1242
  Use ask_user when you need context from the user.`;
1243
+ const MAX_DISCOVERY_MS = 30 * 60 * 1e3;
1112
1244
  let turnCount = 0;
1113
- for await (const msg of query({
1114
- prompt: initialPrompt,
1115
- options: {
1116
- model: config.agentModel,
1117
- maxTurns: config.maxTurns,
1118
- systemPrompt,
1119
- mcpServers: { cartography: tools },
1120
- allowedTools: [
1121
- "Bash",
1122
- "mcp__cartograph__save_node",
1123
- "mcp__cartograph__save_edge",
1124
- "mcp__cartograph__get_catalog",
1125
- "mcp__cartograph__scan_bookmarks",
1126
- "mcp__cartograph__scan_browser_history",
1127
- "mcp__cartograph__scan_installed_apps",
1128
- "mcp__cartograph__scan_local_databases",
1129
- "mcp__cartograph__scan_k8s_resources",
1130
- "mcp__cartograph__scan_aws_resources",
1131
- "mcp__cartograph__scan_gcp_resources",
1132
- "mcp__cartograph__scan_azure_resources",
1133
- "mcp__cartograph__ask_user"
1134
- ],
1135
- hooks: {
1136
- PreToolUse: [{ matcher: "Bash", hooks: [safetyHook] }]
1137
- },
1138
- permissionMode: "bypassPermissions"
1139
- }
1140
- })) {
1141
- if (!onEvent) continue;
1142
- if (msg.type === "assistant") {
1143
- turnCount++;
1144
- onEvent({ kind: "turn", turn: turnCount });
1145
- for (const block of msg.message.content) {
1146
- if (block.type === "text") {
1147
- onEvent({ kind: "thinking", text: block.text });
1148
- }
1149
- if (block.type === "tool_use") {
1150
- onEvent({
1151
- kind: "tool_call",
1152
- tool: block.name,
1153
- input: block.input
1154
- });
1245
+ try {
1246
+ const startTime = Date.now();
1247
+ for await (const msg of query({
1248
+ prompt: initialPrompt,
1249
+ options: {
1250
+ model: config.agentModel,
1251
+ maxTurns: config.maxTurns,
1252
+ systemPrompt,
1253
+ mcpServers: { cartography: tools },
1254
+ allowedTools: [
1255
+ "Bash",
1256
+ "mcp__cartograph__save_node",
1257
+ "mcp__cartograph__save_edge",
1258
+ "mcp__cartograph__get_catalog",
1259
+ "mcp__cartograph__scan_bookmarks",
1260
+ "mcp__cartograph__scan_browser_history",
1261
+ "mcp__cartograph__scan_installed_apps",
1262
+ "mcp__cartograph__scan_local_databases",
1263
+ "mcp__cartograph__scan_k8s_resources",
1264
+ "mcp__cartograph__scan_aws_resources",
1265
+ "mcp__cartograph__scan_gcp_resources",
1266
+ "mcp__cartograph__scan_azure_resources",
1267
+ "mcp__cartograph__ask_user"
1268
+ ],
1269
+ hooks: {
1270
+ PreToolUse: [{ matcher: "Bash", hooks: [safetyHook] }]
1271
+ },
1272
+ permissionMode: "bypassPermissions"
1273
+ }
1274
+ })) {
1275
+ if (Date.now() - startTime > MAX_DISCOVERY_MS) {
1276
+ onEvent?.({ kind: "error", text: `Discovery timeout after ${MAX_DISCOVERY_MS / 6e4} minutes` });
1277
+ onEvent?.({ kind: "done" });
1278
+ return;
1279
+ }
1280
+ if (!onEvent) continue;
1281
+ if (msg.type === "assistant") {
1282
+ turnCount++;
1283
+ onEvent({ kind: "turn", turn: turnCount });
1284
+ for (const block of msg.message.content) {
1285
+ if (block.type === "text") {
1286
+ onEvent({ kind: "thinking", text: block.text });
1287
+ }
1288
+ if (block.type === "tool_use") {
1289
+ onEvent({
1290
+ kind: "tool_call",
1291
+ tool: block.name,
1292
+ input: block.input
1293
+ });
1294
+ }
1155
1295
  }
1156
1296
  }
1157
- }
1158
- if (msg.type === "user") {
1159
- const content = msg.message?.content;
1160
- if (Array.isArray(content)) {
1161
- for (const block of content) {
1162
- if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
1163
- const tb = block;
1164
- const text = typeof tb.content === "string" ? tb.content : "";
1165
- onEvent({ kind: "tool_result", tool: tb.tool_use_id ?? "", output: text });
1297
+ if (msg.type === "user") {
1298
+ const content = msg.message?.content;
1299
+ if (Array.isArray(content)) {
1300
+ for (const block of content) {
1301
+ if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
1302
+ const tb = block;
1303
+ const text = typeof tb.content === "string" ? tb.content : "";
1304
+ onEvent({ kind: "tool_result", tool: tb.tool_use_id ?? "", output: text });
1305
+ }
1166
1306
  }
1167
1307
  }
1168
1308
  }
1309
+ if (msg.type === "result") {
1310
+ onEvent({ kind: "done" });
1311
+ return;
1312
+ }
1169
1313
  }
1170
- if (msg.type === "result") {
1171
- onEvent({ kind: "done" });
1172
- return;
1173
- }
1314
+ } catch (err) {
1315
+ const message = err instanceof Error ? err.message : String(err);
1316
+ onEvent?.({ kind: "error", text: `Discovery error: ${message}` });
1317
+ throw err;
1174
1318
  }
1175
1319
  }
1176
1320
 
@@ -2704,6 +2848,33 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
2704
2848
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2705
2849
  import { resolve } from "path";
2706
2850
  import { createInterface } from "readline";
2851
+
2852
+ // src/logger.ts
2853
+ var verboseMode = false;
2854
+ function setVerbose(v) {
2855
+ verboseMode = v;
2856
+ }
2857
+ function log(level, message, context) {
2858
+ if (level === "DEBUG" && !verboseMode) return;
2859
+ const entry = {
2860
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2861
+ level,
2862
+ message,
2863
+ ...context && Object.keys(context).length > 0 ? { context } : {}
2864
+ };
2865
+ process.stderr.write(JSON.stringify(entry) + "\n");
2866
+ }
2867
+ function logInfo(message, context) {
2868
+ log("INFO", message, context);
2869
+ }
2870
+ function logWarn(message, context) {
2871
+ log("WARN", message, context);
2872
+ }
2873
+ function logError(message, context) {
2874
+ log("ERROR", message, context);
2875
+ }
2876
+
2877
+ // src/cli.ts
2707
2878
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
2708
2879
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2709
2880
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -2713,23 +2884,60 @@ var magenta = (s) => `\x1B[35m${s}\x1B[0m`;
2713
2884
  var red = (s) => `\x1B[31m${s}\x1B[0m`;
2714
2885
  main();
2715
2886
  function main() {
2887
+ let activeDb = null;
2888
+ const shutdown = (signal) => {
2889
+ logWarn(`Received ${signal}, shutting down gracefully\u2026`);
2890
+ if (activeDb) {
2891
+ try {
2892
+ activeDb.close();
2893
+ } catch {
2894
+ }
2895
+ activeDb = null;
2896
+ }
2897
+ process.exit(signal === "SIGINT" ? 130 : 0);
2898
+ };
2899
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2900
+ process.on("SIGINT", () => shutdown("SIGINT"));
2901
+ cleanupTempFiles();
2716
2902
  const program = new Command();
2717
2903
  const CMD = "datasynx-cartography";
2718
- const VERSION = "0.7.0";
2904
+ const VERSION = "1.0.1";
2719
2905
  program.name(CMD).description("AI-powered Infrastructure Discovery & Agentic AI Cartography").version(VERSION);
2720
2906
  program.command("discover").description("Scan and map your infrastructure").option("--entry <hosts...>", "Entry points", ["localhost"]).option("--depth <n>", "Max crawl depth", "8").option("--max-turns <n>", "Max agent turns", "50").option("--model <m>", "Agent model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organization name (for Backstage)").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--db <path>", "DB path").option("-v, --verbose", "Show agent reasoning", false).action(async (opts) => {
2721
2907
  checkPrerequisites();
2908
+ const parsedDepth = parseInt(opts.depth, 10);
2909
+ const parsedMaxTurns = parseInt(opts.maxTurns, 10);
2910
+ if (Number.isNaN(parsedDepth) || parsedDepth < 1 || parsedDepth > 50) {
2911
+ process.stderr.write(`\u274C Invalid --depth: "${opts.depth}" (must be 1\u201350)
2912
+ `);
2913
+ process.exitCode = 2;
2914
+ return;
2915
+ }
2916
+ if (Number.isNaN(parsedMaxTurns) || parsedMaxTurns < 1 || parsedMaxTurns > 500) {
2917
+ process.stderr.write(`\u274C Invalid --max-turns: "${opts.maxTurns}" (must be 1\u2013500)
2918
+ `);
2919
+ process.exitCode = 2;
2920
+ return;
2921
+ }
2922
+ setVerbose(opts.verbose);
2722
2923
  const config = defaultConfig({
2723
2924
  entryPoints: opts.entry,
2724
- maxDepth: parseInt(opts.depth, 10),
2725
- maxTurns: parseInt(opts.maxTurns, 10),
2925
+ maxDepth: parsedDepth,
2926
+ maxTurns: parsedMaxTurns,
2726
2927
  agentModel: opts.model,
2727
2928
  organization: opts.org,
2728
2929
  outputDir: opts.output,
2729
2930
  ...opts.db ? { dbPath: opts.db } : {},
2730
2931
  verbose: opts.verbose
2731
2932
  });
2933
+ logInfo("Discovery started", {
2934
+ entryPoints: config.entryPoints,
2935
+ model: config.agentModel,
2936
+ maxTurns: config.maxTurns,
2937
+ maxDepth: config.maxDepth
2938
+ });
2732
2939
  const db = new CartographyDB(config.dbPath);
2940
+ activeDb = db;
2733
2941
  const sessionId = db.createSession("discover", config);
2734
2942
  const w = process.stderr.write.bind(process.stderr);
2735
2943
  const SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -2857,10 +3065,13 @@ function main() {
2857
3065
  await runDiscovery(config, db, sessionId, handleEvent, onAskUser, void 0);
2858
3066
  } catch (err) {
2859
3067
  stopSpinner();
3068
+ const errMsg = err instanceof Error ? err.message : String(err);
3069
+ logError("Discovery failed", { sessionId, error: errMsg });
2860
3070
  w(`
2861
- ${bold("\x1B[31m\u2717\x1B[0m")} Discovery failed: ${err}
3071
+ ${bold("\x1B[31m\u2717\x1B[0m")} Discovery failed: ${errMsg}
2862
3072
  `);
2863
3073
  db.close();
3074
+ activeDb = null;
2864
3075
  process.exitCode = 1;
2865
3076
  return;
2866
3077
  }
@@ -2868,6 +3079,12 @@ function main() {
2868
3079
  db.endSession(sessionId);
2869
3080
  const stats = db.getStats(sessionId);
2870
3081
  const totalSec = ((Date.now() - startTime) / 1e3).toFixed(1);
3082
+ logInfo("Discovery completed", {
3083
+ sessionId,
3084
+ nodes: stats.nodes,
3085
+ edges: stats.edges,
3086
+ durationSec: parseFloat(totalSec)
3087
+ });
2871
3088
  w("\n");
2872
3089
  w(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
2873
3090
  w(` ${green(bold("DONE"))} ${bold(String(stats.nodes))} nodes, ${bold(String(stats.edges))} edges ${dim("in " + totalSec + "s")}
@@ -3324,7 +3541,7 @@ ${infraSummary.substring(0, 12e3)}`;
3324
3541
  out("\n");
3325
3542
  });
3326
3543
  program.command("bookmarks").description("View all browser bookmarks (Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Firefox)").action(async () => {
3327
- const { scanAllBookmarks: scanAllBookmarks2 } = await import("./bookmarks-72CDYAHD.js");
3544
+ const { scanAllBookmarks: scanAllBookmarks2 } = await import("./bookmarks-BWNVQGPG.js");
3328
3545
  const out = (s) => process.stdout.write(s);
3329
3546
  process.stderr.write(" Scanning bookmarks...\n\n");
3330
3547
  const hosts = await scanAllBookmarks2();
@@ -3583,6 +3800,40 @@ ${infraSummary.substring(0, 12e3)}`;
3583
3800
  process.exitCode = 1;
3584
3801
  }
3585
3802
  });
3803
+ program.command("prune").description("Delete old sessions and their data").option("--older-than <days>", "Delete sessions older than N days", "30").option("--db <path>", "DB path").option("--dry-run", "Show what would be deleted without actually deleting", false).action((opts) => {
3804
+ const days = parseInt(opts.olderThan, 10);
3805
+ if (Number.isNaN(days) || days < 1) {
3806
+ process.stderr.write(`Invalid --older-than: "${opts.olderThan}" (must be >= 1)
3807
+ `);
3808
+ process.exitCode = 2;
3809
+ return;
3810
+ }
3811
+ const config = defaultConfig({ ...opts.db ? { dbPath: opts.db } : {} });
3812
+ const db = new CartographyDB(config.dbPath);
3813
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
3814
+ const sessions = db.getSessions().filter((s) => s.startedAt < cutoff);
3815
+ if (sessions.length === 0) {
3816
+ process.stderr.write(`No sessions older than ${days} days.
3817
+ `);
3818
+ db.close();
3819
+ return;
3820
+ }
3821
+ if (opts.dryRun) {
3822
+ process.stderr.write(`Would delete ${sessions.length} session(s) older than ${days} days:
3823
+ `);
3824
+ for (const s of sessions) {
3825
+ const stats = db.getStats(s.id);
3826
+ process.stderr.write(` ${s.id.substring(0, 8)} ${s.startedAt.substring(0, 19)} nodes:${stats.nodes} edges:${stats.edges}
3827
+ `);
3828
+ }
3829
+ } else {
3830
+ const deleted = db.pruneSessions(cutoff);
3831
+ logInfo("Sessions pruned", { deleted, olderThanDays: days });
3832
+ process.stderr.write(`Deleted ${deleted} session(s) older than ${days} days.
3833
+ `);
3834
+ }
3835
+ db.close();
3836
+ });
3586
3837
  const o = (s) => process.stderr.write(s);
3587
3838
  const _b = (s) => `\x1B[1m${s}\x1B[0m`;
3588
3839
  const _d = (s) => `\x1B[2m${s}\x1B[0m`;