@datasynx/agentic-ai-cartography 0.1.8 → 0.2.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
@@ -1,4 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ EDGE_RELATIONSHIPS,
4
+ EVENT_TYPES,
5
+ MIN_POLL_INTERVAL_MS,
6
+ NODE_TYPES,
7
+ SOPStepSchema,
8
+ defaultConfig
9
+ } from "./chunk-EVJP2FWQ.js";
10
+ import {
11
+ scanAllBookmarks
12
+ } from "./chunk-JAFRT2R6.js";
13
+ import {
14
+ exportAll
15
+ } from "./chunk-GUZXO6PM.js";
2
16
 
3
17
  // src/cli.ts
4
18
  import { Command } from "commander";
@@ -7,111 +21,6 @@ import { Command } from "commander";
7
21
  import { execSync } from "child_process";
8
22
  import { existsSync, readFileSync } from "fs";
9
23
  import { join } from "path";
10
-
11
- // src/types.ts
12
- import { z } from "zod";
13
- var NODE_TYPES = [
14
- "host",
15
- "database_server",
16
- "database",
17
- "table",
18
- "web_service",
19
- "api_endpoint",
20
- "cache_server",
21
- "message_broker",
22
- "queue",
23
- "topic",
24
- "container",
25
- "pod",
26
- "k8s_cluster",
27
- "config_file",
28
- "saas_tool",
29
- "unknown"
30
- ];
31
- var EDGE_RELATIONSHIPS = [
32
- "connects_to",
33
- "reads_from",
34
- "writes_to",
35
- "calls",
36
- "contains",
37
- "depends_on"
38
- ];
39
- var EVENT_TYPES = [
40
- "process_start",
41
- "process_end",
42
- "connection_open",
43
- "connection_close",
44
- "window_focus",
45
- "tool_switch"
46
- ];
47
- var NodeSchema = z.object({
48
- id: z.string().describe('Format: "{type}:{host}:{port}" oder "{type}:{name}"'),
49
- type: z.enum(NODE_TYPES),
50
- name: z.string(),
51
- discoveredVia: z.string(),
52
- confidence: z.number().min(0).max(1).default(0.5),
53
- metadata: z.record(z.unknown()).default({}),
54
- tags: z.array(z.string()).default([])
55
- });
56
- var EdgeSchema = z.object({
57
- sourceId: z.string(),
58
- targetId: z.string(),
59
- relationship: z.enum(EDGE_RELATIONSHIPS),
60
- evidence: z.string(),
61
- confidence: z.number().min(0).max(1).default(0.5)
62
- });
63
- var EventSchema = z.object({
64
- eventType: z.enum(EVENT_TYPES),
65
- process: z.string(),
66
- pid: z.number(),
67
- target: z.string().optional(),
68
- targetType: z.enum(NODE_TYPES).optional(),
69
- protocol: z.string().optional(),
70
- port: z.number().optional()
71
- });
72
- var SOPStepSchema = z.object({
73
- order: z.number(),
74
- instruction: z.string(),
75
- tool: z.string(),
76
- target: z.string().optional(),
77
- notes: z.string().optional()
78
- });
79
- var SOPSchema = z.object({
80
- title: z.string(),
81
- description: z.string(),
82
- steps: z.array(SOPStepSchema),
83
- involvedSystems: z.array(z.string()),
84
- estimatedDuration: z.string(),
85
- frequency: z.string(),
86
- confidence: z.number().min(0).max(1)
87
- });
88
- var MIN_POLL_INTERVAL_MS = 15e3;
89
- function defaultConfig(overrides = {}) {
90
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
91
- return {
92
- mode: "discover",
93
- maxDepth: 8,
94
- maxTurns: 50,
95
- entryPoints: ["localhost"],
96
- agentModel: "claude-sonnet-4-5-20250929",
97
- shadowMode: "daemon",
98
- pollIntervalMs: 3e4,
99
- inactivityTimeoutMs: 3e5,
100
- promptTimeoutMs: 6e4,
101
- trackWindowFocus: false,
102
- autoSaveNodes: false,
103
- enableNotifications: true,
104
- shadowModel: "claude-haiku-4-5-20251001",
105
- outputDir: "./cartography-output",
106
- dbPath: `${home}/.cartography/cartography.db`,
107
- socketPath: `${home}/.cartography/daemon.sock`,
108
- pidFile: `${home}/.cartography/daemon.pid`,
109
- verbose: false,
110
- ...overrides
111
- };
112
- }
113
-
114
- // src/preflight.ts
115
24
  function isOAuthLoggedIn() {
116
25
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
117
26
  const credFile = join(home, ".claude", ".credentials.json");
@@ -542,6 +451,24 @@ var CartographyDB = class {
542
451
  confidence: r["confidence"]
543
452
  }));
544
453
  }
454
+ markTaskAsSOPCandidate(taskId) {
455
+ this.db.prepare("UPDATE tasks SET is_sop_candidate = 1 WHERE id = ?").run(taskId);
456
+ }
457
+ getAllSOPs() {
458
+ const rows = this.db.prepare("SELECT * FROM sops ORDER BY generated_at DESC").all();
459
+ return rows.map((r) => ({
460
+ id: r["id"],
461
+ workflowId: r["workflow_id"],
462
+ title: r["title"],
463
+ description: r["description"],
464
+ steps: JSON.parse(r["steps"]),
465
+ involvedSystems: JSON.parse(r["involved_systems"]),
466
+ estimatedDuration: r["estimated_duration"],
467
+ frequency: r["frequency"],
468
+ confidence: r["confidence"],
469
+ generatedAt: r["generated_at"]
470
+ }));
471
+ }
545
472
  // ── Approvals ───────────────────────────
546
473
  setApproval(pattern, action) {
547
474
  this.db.prepare(`
@@ -563,100 +490,7 @@ var CartographyDB = class {
563
490
  };
564
491
 
565
492
  // src/tools.ts
566
- import { z as z2 } from "zod";
567
-
568
- // src/bookmarks.ts
569
- import { homedir, tmpdir } from "os";
570
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, copyFileSync } from "fs";
571
- import { join as join2 } from "path";
572
- function extractHost(rawUrl, source) {
573
- try {
574
- const u = new URL(rawUrl);
575
- if (u.protocol !== "http:" && u.protocol !== "https:") return null;
576
- const protocol = u.protocol === "https:" ? "https" : "http";
577
- const port = u.port ? parseInt(u.port, 10) : protocol === "https" ? 443 : 80;
578
- const hostname = u.hostname.toLowerCase();
579
- if (!hostname || hostname === "localhost" || hostname === "127.0.0.1") return null;
580
- return { hostname, port, protocol, source };
581
- } catch {
582
- return null;
583
- }
584
- }
585
- function walkChrome(node, source, out) {
586
- if (node.type === "url" && node.url) {
587
- const h = extractHost(node.url, source);
588
- if (h) out.push(h);
589
- }
590
- if (node.children) {
591
- for (const child of node.children) walkChrome(child, source, out);
592
- }
593
- }
594
- function readChromeLike(filePath, source) {
595
- if (!existsSync2(filePath)) return [];
596
- try {
597
- const raw = JSON.parse(readFileSync2(filePath, "utf8"));
598
- const out = [];
599
- for (const root of Object.values(raw.roots)) {
600
- if (root) walkChrome(root, source, out);
601
- }
602
- return out;
603
- } catch {
604
- return [];
605
- }
606
- }
607
- async function readFirefox(profileDir) {
608
- const src = join2(profileDir, "places.sqlite");
609
- if (!existsSync2(src)) return [];
610
- const tmp = join2(tmpdir(), `cartograph_ff_${Date.now()}.sqlite`);
611
- try {
612
- copyFileSync(src, tmp);
613
- const { default: Database2 } = await import("better-sqlite3");
614
- const db = new Database2(tmp, { readonly: true, fileMustExist: true });
615
- const rows = db.prepare(`
616
- SELECT DISTINCT p.url
617
- FROM moz_places p
618
- JOIN moz_bookmarks b ON b.fk = p.id
619
- WHERE b.type = 1 AND p.url NOT LIKE 'place:%'
620
- LIMIT 3000
621
- `).all();
622
- db.close();
623
- return rows.map((r) => extractHost(r.url, "firefox")).filter((h) => h !== null);
624
- } catch {
625
- return [];
626
- }
627
- }
628
- var HOME = homedir();
629
- var IS_MAC = process.platform === "darwin";
630
- var CHROME_PATHS = IS_MAC ? [`${HOME}/Library/Application Support/Google/Chrome/Default/Bookmarks`] : [`${HOME}/.config/google-chrome/Default/Bookmarks`];
631
- var EDGE_PATHS = IS_MAC ? [`${HOME}/Library/Application Support/Microsoft Edge/Default/Bookmarks`] : [`${HOME}/.config/microsoft-edge/Default/Bookmarks`];
632
- var BRAVE_PATHS = IS_MAC ? [`${HOME}/Library/Application Support/BraveSoftware/Brave-Browser/Default/Bookmarks`] : [`${HOME}/.config/BraveSoftware/Brave-Browser/Default/Bookmarks`];
633
- function firefoxProfileDirs() {
634
- const base = IS_MAC ? `${HOME}/Library/Application Support/Firefox/Profiles` : `${HOME}/.mozilla/firefox`;
635
- if (!existsSync2(base)) return [];
636
- try {
637
- return readdirSync(base).filter((d) => d.includes(".default") || d.includes("-release")).map((d) => join2(base, d));
638
- } catch {
639
- return [];
640
- }
641
- }
642
- async function scanAllBookmarks() {
643
- const all = [];
644
- for (const p of CHROME_PATHS) all.push(...readChromeLike(p, "chrome"));
645
- for (const p of EDGE_PATHS) all.push(...readChromeLike(p, "edge"));
646
- for (const p of BRAVE_PATHS) all.push(...readChromeLike(p, "brave"));
647
- for (const dir of firefoxProfileDirs()) {
648
- all.push(...await readFirefox(dir));
649
- }
650
- const seen = /* @__PURE__ */ new Set();
651
- return all.filter((h) => {
652
- const key = h.hostname;
653
- if (seen.has(key)) return false;
654
- seen.add(key);
655
- return true;
656
- });
657
- }
658
-
659
- // src/tools.ts
493
+ import { z } from "zod";
660
494
  function stripSensitive(target) {
661
495
  try {
662
496
  const url = new URL(target.startsWith("http") ? target : `tcp://${target}`);
@@ -670,13 +504,13 @@ async function createCartographyTools(db, sessionId, opts = {}) {
670
504
  const { tool, createSdkMcpServer } = sdk;
671
505
  const tools = [
672
506
  tool("save_node", "Infrastructure-Node speichern", {
673
- id: z2.string(),
674
- type: z2.enum(NODE_TYPES),
675
- name: z2.string(),
676
- discoveredVia: z2.string(),
677
- confidence: z2.number().min(0).max(1),
678
- metadata: z2.record(z2.unknown()).optional(),
679
- tags: z2.array(z2.string()).optional()
507
+ id: z.string(),
508
+ type: z.enum(NODE_TYPES),
509
+ name: z.string(),
510
+ discoveredVia: z.string(),
511
+ confidence: z.number().min(0).max(1),
512
+ metadata: z.record(z.unknown()).optional(),
513
+ tags: z.array(z.string()).optional()
680
514
  }, async (args) => {
681
515
  const node = {
682
516
  id: stripSensitive(args["id"]),
@@ -691,11 +525,11 @@ async function createCartographyTools(db, sessionId, opts = {}) {
691
525
  return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
692
526
  }),
693
527
  tool("save_edge", "Verbindung zwischen zwei Nodes speichern", {
694
- sourceId: z2.string(),
695
- targetId: z2.string(),
696
- relationship: z2.enum(EDGE_RELATIONSHIPS),
697
- evidence: z2.string(),
698
- confidence: z2.number().min(0).max(1)
528
+ sourceId: z.string(),
529
+ targetId: z.string(),
530
+ relationship: z.enum(EDGE_RELATIONSHIPS),
531
+ evidence: z.string(),
532
+ confidence: z.number().min(0).max(1)
699
533
  }, async (args) => {
700
534
  db.insertEdge(sessionId, {
701
535
  sourceId: args["sourceId"],
@@ -707,12 +541,12 @@ async function createCartographyTools(db, sessionId, opts = {}) {
707
541
  return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
708
542
  }),
709
543
  tool("save_event", "Activity-Event (Prozess/Verbindung) speichern", {
710
- eventType: z2.enum(EVENT_TYPES),
711
- process: z2.string(),
712
- pid: z2.number(),
713
- target: z2.string().optional(),
714
- targetType: z2.enum(NODE_TYPES).optional(),
715
- port: z2.number().optional()
544
+ eventType: z.enum(EVENT_TYPES),
545
+ process: z.string(),
546
+ pid: z.number(),
547
+ target: z.string().optional(),
548
+ targetType: z.enum(NODE_TYPES).optional(),
549
+ port: z.number().optional()
716
550
  }, async (args) => {
717
551
  db.insertEvent(sessionId, {
718
552
  eventType: args["eventType"],
@@ -725,7 +559,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
725
559
  return { content: [{ type: "text", text: `\u2713 ${args["eventType"]}` }] };
726
560
  }),
727
561
  tool("get_catalog", "Aktuellen Katalog abrufen (Duplikat-Check)", {
728
- includeEdges: z2.boolean().default(true)
562
+ includeEdges: z.boolean().default(true)
729
563
  }, async (args) => {
730
564
  const nodes = db.getNodes(sessionId);
731
565
  const edges = args["includeEdges"] ? db.getEdges(sessionId) : [];
@@ -740,8 +574,8 @@ async function createCartographyTools(db, sessionId, opts = {}) {
740
574
  };
741
575
  }),
742
576
  tool("manage_task", "Task starten, beenden oder beschreiben", {
743
- action: z2.enum(["start", "end", "describe"]),
744
- description: z2.string().optional()
577
+ action: z.enum(["start", "end", "describe"]),
578
+ description: z.string().optional()
745
579
  }, async (args) => {
746
580
  const action = args["action"];
747
581
  if (action === "start") {
@@ -756,8 +590,8 @@ async function createCartographyTools(db, sessionId, opts = {}) {
756
590
  return { content: [{ type: "text", text: "\u2713 Beschreibung aktualisiert" }] };
757
591
  }),
758
592
  tool("ask_user", "R\xFCckfrage an den User stellen \u2014 bei Unklarheiten, fehlenden Credentials-Hinweisen oder wenn Kontext fehlt", {
759
- question: z2.string().describe("Die Frage an den User (klar und konkret)"),
760
- context: z2.string().optional().describe("Optionaler Zusatzkontext warum die Frage relevant ist")
593
+ question: z.string().describe("Die Frage an den User (klar und konkret)"),
594
+ context: z.string().optional().describe("Optionaler Zusatzkontext warum die Frage relevant ist")
761
595
  }, async (args) => {
762
596
  const question = args["question"];
763
597
  const context = args["context"];
@@ -770,7 +604,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
770
604
  };
771
605
  }),
772
606
  tool("scan_bookmarks", "Alle Browser-Lesezeichen scannen \u2014 nur Hostnamen, keine pers\xF6nlichen Daten", {
773
- minConfidence: z2.number().min(0).max(1).default(0.5).optional()
607
+ minConfidence: z.number().min(0).max(1).default(0.5).optional()
774
608
  }, async () => {
775
609
  const hosts = await scanAllBookmarks();
776
610
  return {
@@ -789,15 +623,133 @@ async function createCartographyTools(db, sessionId, opts = {}) {
789
623
  }]
790
624
  };
791
625
  }),
626
+ tool("scan_k8s_resources", "Kubernetes-Cluster via kubectl scannen \u2014 100% readonly (get, describe)", {
627
+ namespace: z.string().optional().describe("Namespace filtern \u2014 leer = alle Namespaces")
628
+ }, async (args) => {
629
+ const { execSync: execSync3 } = await import("child_process");
630
+ const ns = args["namespace"];
631
+ const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
632
+ const run = (cmd) => {
633
+ try {
634
+ return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
635
+ } catch (e) {
636
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
637
+ }
638
+ };
639
+ const sections = [
640
+ ["CONTEXT", 'kubectl config current-context 2>/dev/null || echo "(kein Context gesetzt)"'],
641
+ ["NODES", "kubectl get nodes -o wide"],
642
+ ["NAMESPACES", "kubectl get namespaces"],
643
+ ["SERVICES", `kubectl get services ${nsFlag}`],
644
+ ["DEPLOYMENTS", `kubectl get deployments ${nsFlag}`],
645
+ ["STATEFULSETS", `kubectl get statefulsets ${nsFlag}`],
646
+ ["INGRESSES", `kubectl get ingress ${nsFlag} 2>/dev/null || echo "(keine)"`],
647
+ ["PODS_RUNNING", `kubectl get pods ${nsFlag} --field-selector=status.phase=Running 2>/dev/null | head -60`],
648
+ ["CONFIGMAPS_SYSTEM", "kubectl get configmaps -n kube-system 2>/dev/null | head -30"]
649
+ ];
650
+ const out = sections.map(([l, c]) => `=== ${l} ===
651
+ ${run(c)}`).join("\n\n");
652
+ return { content: [{ type: "text", text: out }] };
653
+ }),
654
+ tool("scan_aws_resources", "AWS-Infrastruktur via AWS CLI scannen \u2014 100% readonly (describe, list)", {
655
+ region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION oder Profil"),
656
+ profile: z.string().optional().describe("AWS CLI Profil")
657
+ }, async (args) => {
658
+ const { execSync: execSync3 } = await import("child_process");
659
+ const region = args["region"];
660
+ const profile = args["profile"];
661
+ const env = { ...process.env };
662
+ if (region) env["AWS_DEFAULT_REGION"] = region;
663
+ const pf = profile ? `--profile ${profile}` : "";
664
+ const run = (cmd) => {
665
+ try {
666
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
667
+ } catch (e) {
668
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
669
+ }
670
+ };
671
+ const sections = [
672
+ ["IDENTITY", `aws sts get-caller-identity ${pf} --output json`],
673
+ ["EC2", `aws ec2 describe-instances ${pf} --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name,PublicIpAddress,PrivateIpAddress,Tags[?Key==\`Name\`].Value|[0]]' --output table`],
674
+ ["RDS", `aws rds describe-db-instances ${pf} --query 'DBInstances[*].[DBInstanceIdentifier,Engine,DBInstanceStatus,Endpoint.Address,Endpoint.Port]' --output table`],
675
+ ["ELB_V2", `aws elbv2 describe-load-balancers ${pf} --query 'LoadBalancers[*].[LoadBalancerName,DNSName,Type,State.Code]' --output table`],
676
+ ["EKS", `aws eks list-clusters ${pf} --output json`],
677
+ ["ELASTICACHE", `aws elasticache describe-cache-clusters ${pf} --query 'CacheClusters[*].[CacheClusterId,Engine,CacheClusterStatus]' --output table 2>/dev/null || echo "(nicht verf\xFCgbar)"`],
678
+ ["S3", `aws s3 ls ${pf} 2>/dev/null || echo "(nicht verf\xFCgbar)"`],
679
+ ["VPC", `aws ec2 describe-vpcs ${pf} --query 'Vpcs[*].[VpcId,CidrBlock,IsDefault,Tags[?Key==\`Name\`].Value|[0]]' --output table`]
680
+ ];
681
+ const out = sections.map(([l, c]) => `=== ${l} ===
682
+ ${run(c)}`).join("\n\n");
683
+ return { content: [{ type: "text", text: out }] };
684
+ }),
685
+ tool("scan_gcp_resources", "Google Cloud Platform via gcloud CLI scannen \u2014 100% readonly (list, describe)", {
686
+ project: z.string().optional().describe("GCP Project ID \u2014 default: aktuelles gcloud-Projekt")
687
+ }, async (args) => {
688
+ const { execSync: execSync3 } = await import("child_process");
689
+ const project = args["project"];
690
+ const pf = project ? `--project ${project}` : "";
691
+ const run = (cmd) => {
692
+ try {
693
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
694
+ } catch (e) {
695
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
696
+ }
697
+ };
698
+ const sections = [
699
+ ["IDENTITY", `gcloud config list account --format='value(core.account)' 2>/dev/null; gcloud config get-value project 2>/dev/null`],
700
+ ["COMPUTE_INSTANCES", `gcloud compute instances list ${pf} 2>/dev/null || echo "(error)"`],
701
+ ["SQL_INSTANCES", `gcloud sql instances list ${pf} 2>/dev/null || echo "(error)"`],
702
+ ["GKE_CLUSTERS", `gcloud container clusters list ${pf} 2>/dev/null || echo "(error)"`],
703
+ ["CLOUD_RUN", `gcloud run services list ${pf} --platform managed 2>/dev/null || echo "(error)"`],
704
+ ["CLOUD_FUNCTIONS", `gcloud functions list ${pf} 2>/dev/null || echo "(error)"`],
705
+ ["REDIS", `gcloud redis instances list ${pf} --regions=- 2>/dev/null || echo "(error)"`],
706
+ ["PUBSUB", `gcloud pubsub topics list ${pf} 2>/dev/null || echo "(error)"`],
707
+ ["SPANNER", `gcloud spanner instances list ${pf} 2>/dev/null || echo "(error)"`]
708
+ ];
709
+ const out = sections.map(([l, c]) => `=== ${l} ===
710
+ ${run(c)}`).join("\n\n");
711
+ return { content: [{ type: "text", text: out }] };
712
+ }),
713
+ tool("scan_azure_resources", "Azure-Infrastruktur via az CLI scannen \u2014 100% readonly (list, show)", {
714
+ subscription: z.string().optional().describe("Azure Subscription ID"),
715
+ resourceGroup: z.string().optional().describe("Resource Group filtern")
716
+ }, async (args) => {
717
+ const { execSync: execSync3 } = await import("child_process");
718
+ const sub = args["subscription"];
719
+ const rg = args["resourceGroup"];
720
+ const sf = sub ? `--subscription ${sub}` : "";
721
+ const rf = rg ? `--resource-group ${rg}` : "";
722
+ const run = (cmd) => {
723
+ try {
724
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
725
+ } catch (e) {
726
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
727
+ }
728
+ };
729
+ const sections = [
730
+ ["IDENTITY", `az account show --output json ${sf} 2>/dev/null || echo "(nicht eingeloggt \u2014 az login)"`],
731
+ ["VMS", `az vm list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
732
+ ["AKS", `az aks list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
733
+ ["SQL_SERVERS", `az sql server list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
734
+ ["POSTGRES", `az postgres server list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
735
+ ["REDIS", `az redis list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
736
+ ["WEBAPPS", `az webapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
737
+ ["CONTAINER_APPS", `az containerapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
738
+ ["FUNCTIONS", `az functionapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`]
739
+ ];
740
+ const out = sections.map(([l, c]) => `=== ${l} ===
741
+ ${run(c)}`).join("\n\n");
742
+ return { content: [{ type: "text", text: out }] };
743
+ }),
792
744
  tool("save_sop", "Standard Operating Procedure speichern", {
793
- workflowId: z2.string(),
794
- title: z2.string(),
795
- description: z2.string(),
796
- steps: z2.array(SOPStepSchema),
797
- involvedSystems: z2.array(z2.string()),
798
- estimatedDuration: z2.string(),
799
- frequency: z2.string(),
800
- confidence: z2.number().min(0).max(1)
745
+ workflowId: z.string(),
746
+ title: z.string(),
747
+ description: z.string(),
748
+ steps: z.array(SOPStepSchema),
749
+ involvedSystems: z.array(z.string()),
750
+ estimatedDuration: z.string(),
751
+ frequency: z.string(),
752
+ confidence: z.number().min(0).max(1)
801
753
  }, async (args) => {
802
754
  db.insertSOP({
803
755
  workflowId: args["workflowId"],
@@ -860,15 +812,22 @@ SCHRITT 2 \u2014 Lokale Infrastruktur:
860
812
  ss -tlnp && ps aux \u2192 alle lauschenden Ports/Prozesse identifizieren
861
813
  Jeden Service vertiefen: DB\u2192Schemas, API\u2192Endpoints, Queue\u2192Topics
862
814
 
863
- SCHRITT 3 \u2014 Config-Files:
815
+ SCHRITT 3 \u2014 Cloud & Kubernetes (falls CLI vorhanden):
816
+ scan_k8s_resources() \u2192 Nodes, Services, Pods, Deployments, Ingresses
817
+ scan_aws_resources() \u2192 EC2, RDS, ELB, EKS, ElastiCache, S3 (falls AWS CLI + Credentials)
818
+ scan_gcp_resources() \u2192 Compute, SQL, GKE, Cloud Run, Functions (falls gcloud + Auth)
819
+ scan_azure_resources() \u2192 VMs, AKS, SQL, Redis, WebApps (falls az CLI + Login)
820
+ Fehler / "nicht verf\xFCgbar" \u2192 ignorieren, weiter mit n\xE4chstem Tool
821
+
822
+ SCHRITT 4 \u2014 Config-Files:
864
823
  .env, docker-compose.yml, application.yml, kubernetes/*.yml
865
824
  Nur Host:Port extrahieren \u2014 KEINE Credentials
866
825
 
867
- SCHRITT 4 \u2014 R\xFCckfragen bei Unklarheit:
826
+ SCHRITT 5 \u2014 R\xFCckfragen bei Unklarheit:
868
827
  ask_user() nutzen wenn: Dienst unklar ist, Kontext fehlt, oder User Input sinnvoll w\xE4re
869
828
  Beispiele: "Welche Umgebung ist das (dev/staging/prod)?", "Ist <host> ein internes Tool?"
870
829
 
871
- SCHRITT 5 \u2014 Fertig wenn alle Spuren ersch\xF6pft.
830
+ SCHRITT 6 \u2014 Fertig wenn alle Spuren ersch\xF6pft.
872
831
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
873
832
 
874
833
  PORT-MAPPING: 5432=postgres, 3306=mysql, 27017=mongodb, 6379=redis,
@@ -903,6 +862,10 @@ Nutze ask_user wenn du Kontext vom User brauchst.`;
903
862
  "mcp__cartograph__save_edge",
904
863
  "mcp__cartograph__get_catalog",
905
864
  "mcp__cartograph__scan_bookmarks",
865
+ "mcp__cartograph__scan_k8s_resources",
866
+ "mcp__cartograph__scan_aws_resources",
867
+ "mcp__cartograph__scan_gcp_resources",
868
+ "mcp__cartograph__scan_azure_resources",
906
869
  "mcp__cartograph__ask_user"
907
870
  ],
908
871
  hooks: {
@@ -1050,461 +1013,19 @@ function clusterTasks(tasks) {
1050
1013
  return clusters;
1051
1014
  }
1052
1015
 
1053
- // src/exporter.ts
1054
- import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
1055
- import { join as join3 } from "path";
1056
- function nodeLayer(type) {
1057
- if (type === "saas_tool") return "saas";
1058
- if (["web_service", "api_endpoint"].includes(type)) return "web";
1059
- if (["database_server", "database", "table", "cache_server"].includes(type)) return "data";
1060
- if (["message_broker", "queue", "topic"].includes(type)) return "messaging";
1061
- if (["host", "container", "pod", "k8s_cluster"].includes(type)) return "infra";
1062
- if (type === "config_file") return "config";
1063
- return "other";
1064
- }
1065
- var LAYER_LABELS = {
1066
- saas: "\u2601 SaaS Tools",
1067
- web: "\u{1F310} Web / API",
1068
- data: "\u{1F5C4} Data Layer",
1069
- messaging: "\u{1F4E8} Messaging",
1070
- infra: "\u{1F5A5} Infrastructure",
1071
- config: "\u{1F4C4} Config",
1072
- other: "\u2753 Sonstige"
1073
- };
1074
- var LAYER_ORDER = ["saas", "web", "data", "messaging", "infra", "config", "other"];
1075
- var MERMAID_ICONS = {
1076
- host: "\u{1F5A5}",
1077
- database_server: "\u{1F5C4}",
1078
- database: "\u{1F5C4}",
1079
- table: "\u{1F4CB}",
1080
- web_service: "\u{1F310}",
1081
- api_endpoint: "\u{1F50C}",
1082
- cache_server: "\u26A1",
1083
- message_broker: "\u{1F4E8}",
1084
- queue: "\u{1F4EC}",
1085
- topic: "\u{1F4E2}",
1086
- container: "\u{1F4E6}",
1087
- pod: "\u2638",
1088
- k8s_cluster: "\u2638",
1089
- config_file: "\u{1F4C4}",
1090
- saas_tool: "\u2601",
1091
- unknown: "\u2753"
1092
- };
1093
- var EDGE_LABELS = {
1094
- connects_to: "\u2192",
1095
- reads_from: "reads",
1096
- writes_to: "writes",
1097
- calls: "calls",
1098
- contains: "contains",
1099
- depends_on: "depends on"
1100
- };
1101
- var MERMAID_CLASSES = {
1102
- host: "fill:#1e3352,stroke:#4a82c4,color:#cce",
1103
- database_server: "fill:#1e3352,stroke:#4a82c4,color:#cce",
1104
- database: "fill:#163352,stroke:#3a8ad4,color:#bdf",
1105
- table: "fill:#0f2a40,stroke:#2a6090,color:#9bd",
1106
- web_service: "fill:#1a3a1a,stroke:#3a9a3a,color:#bfb",
1107
- api_endpoint: "fill:#0f2a0f,stroke:#2a7a2a,color:#9d9",
1108
- cache_server: "fill:#3a2a0a,stroke:#ca8a0a,color:#fda",
1109
- message_broker: "fill:#2a1a3a,stroke:#7a3aaa,color:#daf",
1110
- queue: "fill:#1f1030,stroke:#5a2a8a,color:#caf",
1111
- topic: "fill:#1f1030,stroke:#5a2a8a,color:#caf",
1112
- container: "fill:#1a2a3a,stroke:#3a6a9a,color:#acd",
1113
- pod: "fill:#0f1f2f,stroke:#2a5a8a,color:#8bc",
1114
- k8s_cluster: "fill:#0a1520,stroke:#1a4a7a,color:#7ab",
1115
- config_file: "fill:#2a2a1a,stroke:#7a7a2a,color:#ddc",
1116
- saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
1117
- unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
1118
- };
1119
- function sanitize(id) {
1120
- return id.replace(/[^a-zA-Z0-9_]/g, "_");
1121
- }
1122
- function nodeLabel(node) {
1123
- const icon = MERMAID_ICONS[node.type] ?? "?";
1124
- const parts = node.id.split(":");
1125
- const location = parts.length >= 3 ? `${parts[1]}:${parts[2]}` : parts[1] ?? "";
1126
- const conf = `${Math.round(node.confidence * 100)}%`;
1127
- const meta = node.metadata;
1128
- const extras = [];
1129
- for (const key of ["category", "version", "description"]) {
1130
- const v = meta[key];
1131
- if (typeof v === "string" && v.length > 0) {
1132
- extras.push(v.substring(0, 28));
1133
- break;
1134
- }
1135
- }
1136
- const locLine = location ? `<br/><small>${location}</small>` : "";
1137
- const extraLine = extras.length ? `<br/><small>${extras[0]}</small>` : "";
1138
- return `["${icon} <b>${node.name}</b>${locLine}${extraLine}<br/><small>${node.type} \xB7 ${conf}</small>"]`;
1139
- }
1140
- function generateTopologyMermaid(nodes, edges) {
1141
- if (nodes.length === 0) return 'graph TB\n empty["No nodes discovered yet"]';
1142
- const lines = ["graph TB"];
1143
- const usedTypes = new Set(nodes.map((n) => n.type));
1144
- for (const type of usedTypes) {
1145
- const style = MERMAID_CLASSES[type] ?? MERMAID_CLASSES["unknown"];
1146
- lines.push(` classDef ${type.replace(/_/g, "")} ${style}`);
1147
- }
1148
- lines.push("");
1149
- const layerMap = /* @__PURE__ */ new Map();
1150
- for (const node of nodes) {
1151
- const layer = nodeLayer(node.type);
1152
- if (!layerMap.has(layer)) layerMap.set(layer, []);
1153
- layerMap.get(layer).push(node);
1154
- }
1155
- for (const layerKey of LAYER_ORDER) {
1156
- const layerNodes = layerMap.get(layerKey);
1157
- if (!layerNodes || layerNodes.length === 0) continue;
1158
- const label = LAYER_LABELS[layerKey] ?? layerKey;
1159
- lines.push(` subgraph ${layerKey}["${label}"]`);
1160
- for (const node of layerNodes) {
1161
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
1162
- }
1163
- lines.push(" end");
1164
- lines.push("");
1165
- }
1166
- for (const edge of edges) {
1167
- const src = sanitize(edge.sourceId);
1168
- const tgt = sanitize(edge.targetId);
1169
- const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
1170
- const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
1171
- lines.push(` ${src} ${arrow} ${tgt}`);
1172
- }
1173
- return lines.join("\n");
1174
- }
1175
- function generateDependencyMermaid(nodes, edges) {
1176
- const depEdges = edges.filter(
1177
- (e) => ["calls", "reads_from", "writes_to", "depends_on"].includes(e.relationship)
1178
- );
1179
- if (depEdges.length === 0) return 'graph LR\n empty["No dependency edges found"]';
1180
- const lines = ["graph LR"];
1181
- const usedIds = /* @__PURE__ */ new Set();
1182
- for (const edge of depEdges) {
1183
- usedIds.add(edge.sourceId);
1184
- usedIds.add(edge.targetId);
1185
- }
1186
- const usedNodes = nodes.filter((n) => usedIds.has(n.id));
1187
- const usedTypes = new Set(usedNodes.map((n) => n.type));
1188
- for (const type of usedTypes) {
1189
- const style = MERMAID_CLASSES[type] ?? MERMAID_CLASSES["unknown"];
1190
- lines.push(` classDef ${type.replace(/_/g, "")} ${style}`);
1191
- }
1192
- lines.push("");
1193
- for (const node of usedNodes) {
1194
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
1195
- }
1196
- lines.push("");
1197
- for (const edge of depEdges) {
1198
- const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
1199
- lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
1200
- }
1201
- return lines.join("\n");
1202
- }
1203
- function generateWorkflowMermaid(sop) {
1204
- const lines = ["flowchart TD"];
1205
- for (const step of sop.steps) {
1206
- const nodeId = `S${step.order}`;
1207
- const label = `${step.order}. ${step.instruction.substring(0, 60)}`;
1208
- lines.push(` ${nodeId}["${label}"]`);
1209
- if (step.order > 1) {
1210
- lines.push(` S${step.order - 1} --> ${nodeId}`);
1211
- }
1212
- }
1213
- return lines.join("\n");
1214
- }
1215
- function exportBackstageYAML(nodes, edges, org) {
1216
- const owner = org ?? "unknown";
1217
- const docs = [];
1218
- for (const node of nodes) {
1219
- const isComponent = ["web_service", "container", "pod"].includes(node.type);
1220
- const isAPI = node.type === "api_endpoint";
1221
- const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
1222
- const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
1223
- const doc = [
1224
- `apiVersion: backstage.io/v1alpha1`,
1225
- `kind: ${kind}`,
1226
- `metadata:`,
1227
- ` name: ${sanitize(node.id)}`,
1228
- ` annotations:`,
1229
- ` cartography/discovered-at: "${node.discoveredAt}"`,
1230
- ` cartography/confidence: "${node.confidence}"`,
1231
- `spec:`,
1232
- ` type: ${node.type}`,
1233
- ` lifecycle: production`,
1234
- ` owner: ${owner}`,
1235
- ...deps.length > 0 ? [" dependsOn:", ...deps] : []
1236
- ].join("\n");
1237
- docs.push(doc);
1238
- }
1239
- return docs.join("\n---\n");
1240
- }
1241
- function exportJSON(db, sessionId) {
1242
- const nodes = db.getNodes(sessionId);
1243
- const edges = db.getEdges(sessionId);
1244
- const events = db.getEvents(sessionId);
1245
- const tasks = db.getTasks(sessionId);
1246
- const sops = db.getSOPs(sessionId);
1247
- const stats = db.getStats(sessionId);
1248
- return JSON.stringify({
1249
- sessionId,
1250
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1251
- stats,
1252
- nodes,
1253
- edges,
1254
- events,
1255
- tasks,
1256
- sops
1257
- }, null, 2);
1258
- }
1259
- function exportHTML(nodes, edges) {
1260
- const graphData = JSON.stringify({
1261
- nodes: nodes.map((n) => ({
1262
- id: n.id,
1263
- name: n.name,
1264
- type: n.type,
1265
- confidence: n.confidence,
1266
- discoveredVia: n.discoveredVia,
1267
- discoveredAt: n.discoveredAt,
1268
- tags: n.tags,
1269
- metadata: n.metadata
1270
- })),
1271
- links: edges.map((e) => ({
1272
- source: e.sourceId,
1273
- target: e.targetId,
1274
- relationship: e.relationship,
1275
- confidence: e.confidence,
1276
- evidence: e.evidence
1277
- }))
1278
- });
1279
- return `<!DOCTYPE html>
1280
- <html lang="de">
1281
- <head>
1282
- <meta charset="UTF-8">
1283
- <title>Cartography \u2014 Topology</title>
1284
- <script src="https://d3js.org/d3.v7.min.js"></script>
1285
- <style>
1286
- * { box-sizing: border-box; }
1287
- body { margin: 0; background: #0d1117; color: #e6edf3; font-family: 'SF Mono', 'Fira Code', monospace; display: flex; }
1288
- #graph { flex: 1; height: 100vh; }
1289
- svg { width: 100%; height: 100%; }
1290
- .link { stroke-opacity: 0.5; }
1291
- .link-label { font-size: 9px; fill: #8b949e; }
1292
- .node circle { stroke-width: 2px; cursor: pointer; transition: r 0.15s; }
1293
- .node circle:hover { r: 14; }
1294
- .node text { font-size: 11px; fill: #c9d1d9; pointer-events: none; }
1295
- /* \u2500\u2500 Sidebar \u2500\u2500 */
1296
- #sidebar {
1297
- width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
1298
- background: #161b22; border-left: 1px solid #30363d;
1299
- padding: 16px; font-size: 12px; line-height: 1.6;
1300
- }
1301
- #sidebar h2 { margin: 0 0 8px; font-size: 14px; color: #58a6ff; }
1302
- #sidebar .meta-table { width: 100%; border-collapse: collapse; }
1303
- #sidebar .meta-table td { padding: 3px 6px; border-bottom: 1px solid #21262d; vertical-align: top; }
1304
- #sidebar .meta-table td:first-child { color: #8b949e; white-space: nowrap; width: 90px; }
1305
- #sidebar .tag { display: inline-block; background: #21262d; border-radius: 3px; padding: 1px 5px; margin: 1px; }
1306
- #sidebar .conf-bar { height: 6px; border-radius: 3px; background: #21262d; margin-top: 3px; }
1307
- #sidebar .conf-fill { height: 100%; border-radius: 3px; }
1308
- #sidebar .edges-list { margin-top: 12px; }
1309
- #sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #21262d; color: #8b949e; }
1310
- #sidebar .edge-item span { color: #c9d1d9; }
1311
- .hint { color: #484f58; font-size: 11px; margin-top: 8px; }
1312
- #header { position: fixed; top: 10px; left: 10px; background: rgba(13,17,23,0.85);
1313
- padding: 8px 12px; border-radius: 6px; font-size: 12px; border: 1px solid #30363d; }
1314
- #header strong { color: #58a6ff; }
1315
- </style>
1316
- </head>
1317
- <body>
1318
- <div id="graph">
1319
- <div id="header">
1320
- <strong>Cartography</strong> &nbsp;
1321
- <span style="color:#8b949e">${nodes.length} Nodes \xB7 ${edges.length} Edges</span><br>
1322
- <span style="color:#484f58;font-size:10px">Scroll=zoom \xB7 Drag=pan \xB7 Click=details</span>
1323
- </div>
1324
- <svg></svg>
1325
- </div>
1326
- <div id="sidebar">
1327
- <h2>Infrastructure Map</h2>
1328
- <p class="hint">Klicke einen Node um Details anzuzeigen.</p>
1329
- </div>
1330
- <script>
1331
- const data = ${graphData};
1332
-
1333
- const TYPE_COLORS = {
1334
- host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
1335
- web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
1336
- message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
1337
- container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
1338
- config_file: '#adb5bd', saas_tool: '#da8bff', unknown: '#6c757d',
1339
- };
1340
-
1341
- const NODE_RADIUS = { saas_tool: 10, host: 11, database_server: 11, k8s_cluster: 13, default: 8 };
1342
- const radius = d => NODE_RADIUS[d.type] || NODE_RADIUS.default;
1343
-
1344
- const sidebar = document.getElementById('sidebar');
1345
-
1346
- function showNode(d) {
1347
- const c = TYPE_COLORS[d.type] || '#aaa';
1348
- const confPct = Math.round(d.confidence * 100);
1349
- const tags = (d.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
1350
- const metaRows = Object.entries(d.metadata || {})
1351
- .filter(([,v]) => v !== null && v !== undefined && String(v).length > 0)
1352
- .map(([k,v]) => \`<tr><td>\${k}</td><td>\${JSON.stringify(v)}</td></tr>\`)
1353
- .join('');
1354
- const related = data.links.filter(l =>
1355
- (l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id
1356
- );
1357
- const edgeItems = related.map(l => {
1358
- const isOut = (l.source.id||l.source) === d.id;
1359
- const other = isOut ? (l.target.id||l.target) : (l.source.id||l.source);
1360
- return \`<div class="edge-item">\${isOut ? '\u2192' : '\u2190'} <span>\${other}</span> <small>[\${l.relationship}]</small></div>\`;
1361
- }).join('');
1362
-
1363
- sidebar.innerHTML = \`
1364
- <h2>\${d.name}</h2>
1365
- <table class="meta-table">
1366
- <tr><td>ID</td><td style="font-size:10px;word-break:break-all">\${d.id}</td></tr>
1367
- <tr><td>Typ</td><td><span style="color:\${c}">\${d.type}</span></td></tr>
1368
- <tr><td>Confidence</td><td>
1369
- \${confPct}%
1370
- <div class="conf-bar"><div class="conf-fill" style="width:\${confPct}%;background:\${c}"></div></div>
1371
- </td></tr>
1372
- <tr><td>Entdeckt via</td><td>\${d.discoveredVia || '\u2014'}</td></tr>
1373
- <tr><td>Zeitpunkt</td><td>\${d.discoveredAt ? d.discoveredAt.substring(0,19).replace('T',' ') : '\u2014'}</td></tr>
1374
- \${tags ? '<tr><td>Tags</td><td>'+tags+'</td></tr>' : ''}
1375
- \${metaRows}
1376
- </table>
1377
- \${related.length > 0 ? '<div class="edges-list"><strong>Verbindungen:</strong>'+edgeItems+'</div>' : ''}
1378
- \`;
1379
- }
1380
-
1381
- const svgEl = d3.select('svg');
1382
- const graphDiv = document.getElementById('graph');
1383
- const width = () => graphDiv.clientWidth;
1384
- const height = () => graphDiv.clientHeight;
1385
- const g = svgEl.append('g');
1386
-
1387
- svgEl.call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform)));
1388
-
1389
- const sim = d3.forceSimulation(data.nodes)
1390
- .force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 60 : 120))
1391
- .force('charge', d3.forceManyBody().strength(-320))
1392
- .force('center', d3.forceCenter(width() / 2, height() / 2))
1393
- .force('collision', d3.forceCollide().radius(d => radius(d) + 20));
1394
-
1395
- const link = g.append('g')
1396
- .selectAll('line').data(data.links).join('line')
1397
- .attr('class', 'link')
1398
- .attr('stroke', d => d.confidence < 0.6 ? '#444' : '#555')
1399
- .attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
1400
- .attr('stroke-width', d => d.confidence < 0.6 ? 1 : 1.5);
1401
-
1402
- link.append('title').text(d => \`\${d.relationship} (conf:\${d.confidence})
1403
- \${d.evidence||''}\`);
1404
-
1405
- const node = g.append('g')
1406
- .selectAll('g').data(data.nodes).join('g').attr('class', 'node')
1407
- .call(d3.drag()
1408
- .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
1409
- .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
1410
- .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
1411
- )
1412
- .on('click', (e, d) => { e.stopPropagation(); showNode(d); });
1413
-
1414
- node.append('circle')
1415
- .attr('r', radius)
1416
- .attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
1417
- .attr('stroke', d => d3.color(TYPE_COLORS[d.type] || '#aaa').brighter(1).formatHex())
1418
- .append('title').text(d => \`\${d.id}
1419
- conf:\${d.confidence}\`);
1420
-
1421
- node.append('text').attr('dx', d => radius(d) + 4).attr('dy', '.35em').text(d => d.name);
1422
-
1423
- sim.on('tick', () => {
1424
- link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
1425
- .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
1426
- node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
1427
- });
1428
-
1429
- svgEl.on('click', () => {
1430
- sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Klicke einen Node um Details anzuzeigen.</p>';
1431
- });
1432
- </script>
1433
- </body>
1434
- </html>`;
1435
- }
1436
- function exportSOPMarkdown(sop) {
1437
- const lines = [
1438
- `# ${sop.title}`,
1439
- "",
1440
- `**Beschreibung:** ${sop.description}`,
1441
- `**Systeme:** ${sop.involvedSystems.join(", ")}`,
1442
- `**Dauer:** ${sop.estimatedDuration}`,
1443
- `**H\xE4ufigkeit:** ${sop.frequency}`,
1444
- `**Confidence:** ${sop.confidence.toFixed(2)}`,
1445
- "",
1446
- "## Schritte",
1447
- ""
1448
- ];
1449
- for (const step of sop.steps) {
1450
- lines.push(`${step.order}. **${step.tool}**${step.target ? ` \u2192 \`${step.target}\`` : ""}`);
1451
- lines.push(` ${step.instruction}`);
1452
- if (step.notes) lines.push(` _${step.notes}_`);
1453
- lines.push("");
1454
- }
1455
- return lines.join("\n");
1456
- }
1457
- function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
1458
- mkdirSync2(outputDir, { recursive: true });
1459
- mkdirSync2(join3(outputDir, "sops"), { recursive: true });
1460
- mkdirSync2(join3(outputDir, "workflows"), { recursive: true });
1461
- const nodes = db.getNodes(sessionId);
1462
- const edges = db.getEdges(sessionId);
1463
- if (formats.includes("mermaid")) {
1464
- writeFileSync(join3(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
1465
- writeFileSync(join3(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
1466
- process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
1467
- }
1468
- if (formats.includes("json")) {
1469
- writeFileSync(join3(outputDir, "catalog.json"), exportJSON(db, sessionId));
1470
- process.stderr.write("\u2713 catalog.json\n");
1471
- }
1472
- if (formats.includes("yaml")) {
1473
- writeFileSync(join3(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
1474
- process.stderr.write("\u2713 catalog-info.yaml\n");
1475
- }
1476
- if (formats.includes("html")) {
1477
- writeFileSync(join3(outputDir, "topology.html"), exportHTML(nodes, edges));
1478
- process.stderr.write("\u2713 topology.html\n");
1479
- }
1480
- if (formats.includes("sops")) {
1481
- const sops = db.getSOPs(sessionId);
1482
- for (const sop of sops) {
1483
- const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
1484
- writeFileSync(join3(outputDir, "sops", filename), exportSOPMarkdown(sop));
1485
- const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
1486
- writeFileSync(join3(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
1487
- }
1488
- if (sops.length > 0) {
1489
- process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
1490
- `);
1491
- }
1492
- }
1493
- }
1494
-
1495
1016
  // src/cli.ts
1496
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
1017
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
1497
1018
  import { resolve } from "path";
1498
1019
  import { createInterface } from "readline";
1499
1020
 
1500
1021
  // src/daemon.ts
1501
1022
  import { execSync as execSync2, spawn } from "child_process";
1502
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1023
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, unlinkSync as unlinkSync2 } from "fs";
1503
1024
 
1504
1025
  // src/ipc.ts
1505
1026
  import net from "net";
1506
1027
  import { EventEmitter } from "events";
1507
- import { chmodSync, existsSync as existsSync3, unlinkSync } from "fs";
1028
+ import { chmodSync, existsSync as existsSync2, unlinkSync } from "fs";
1508
1029
  var IPCServer = class extends EventEmitter {
1509
1030
  server = null;
1510
1031
  clients = /* @__PURE__ */ new Set();
@@ -1606,7 +1127,7 @@ var IPCClient = class extends EventEmitter {
1606
1127
  }
1607
1128
  };
1608
1129
  function cleanStaleSocket(socketPath) {
1609
- if (existsSync3(socketPath)) {
1130
+ if (existsSync2(socketPath)) {
1610
1131
  try {
1611
1132
  unlinkSync(socketPath);
1612
1133
  } catch {
@@ -1675,22 +1196,62 @@ var ShadowDaemon = class {
1675
1196
  this.notify = notify;
1676
1197
  }
1677
1198
  running = false;
1199
+ paused = false;
1678
1200
  prevSnapshot = "";
1679
1201
  cyclesRun = 0;
1680
1202
  cyclesSkipped = 0;
1203
+ lastTaskCount = 0;
1204
+ sessionId = "";
1681
1205
  async run() {
1682
1206
  this.running = true;
1683
- const sessionId = this.db.createSession("shadow", this.config);
1207
+ this.sessionId = this.db.createSession("shadow", this.config);
1684
1208
  process.on("SIGTERM", () => this.stop());
1685
1209
  process.on("SIGINT", () => this.stop());
1210
+ process.on("SIGUSR1", () => this.pause());
1211
+ process.on("SIGUSR2", () => this.resume());
1212
+ this.ipc.on("message", (msg) => {
1213
+ switch (msg.type) {
1214
+ case "command":
1215
+ if (msg.command === "pause") this.pause();
1216
+ else if (msg.command === "resume") this.resume();
1217
+ else if (msg.command === "stop") this.stop();
1218
+ else if (msg.command === "status") {
1219
+ this.ipc.broadcast({ type: "status", data: this.getStatus() });
1220
+ } else if (msg.command === "new-task") {
1221
+ this.db.startTask(this.sessionId);
1222
+ this.ipc.broadcast({ type: "info", message: "Task gestartet" });
1223
+ } else if (msg.command === "end-task") {
1224
+ this.db.endCurrentTask(this.sessionId);
1225
+ this.ipc.broadcast({ type: "info", message: "Task beendet" });
1226
+ }
1227
+ break;
1228
+ case "task-description":
1229
+ this.db.updateTaskDescription(this.sessionId, msg.description);
1230
+ break;
1231
+ case "prompt-response":
1232
+ if (msg.id.startsWith("sop-suggest:")) {
1233
+ const taskId = msg.id.replace("sop-suggest:", "");
1234
+ if (msg.answer === "ja" || msg.answer === "yes" || msg.answer === "Ja, als SOP speichern") {
1235
+ this.db.markTaskAsSOPCandidate(taskId);
1236
+ this.ipc.broadcast({ type: "info", message: `Task als SOP-Kandidat markiert` });
1237
+ }
1238
+ }
1239
+ break;
1240
+ }
1241
+ });
1686
1242
  while (this.running) {
1243
+ if (this.paused) {
1244
+ this.ipc.broadcast({ type: "status", data: this.getStatus() });
1245
+ await sleep(this.config.pollIntervalMs);
1246
+ continue;
1247
+ }
1687
1248
  const snapshot = takeSnapshot(this.config);
1688
1249
  if (snapshot !== this.prevSnapshot) {
1689
1250
  try {
1690
1251
  await runShadowCycle(
1691
1252
  this.config,
1692
1253
  this.db,
1693
- sessionId,
1254
+ this.sessionId,
1694
1255
  this.prevSnapshot,
1695
1256
  snapshot,
1696
1257
  (msg) => {
@@ -1705,38 +1266,84 @@ var ShadowDaemon = class {
1705
1266
  `);
1706
1267
  }
1707
1268
  this.prevSnapshot = snapshot;
1269
+ this.checkForCompletedTasks();
1708
1270
  } else {
1709
1271
  this.cyclesSkipped++;
1710
1272
  }
1711
- const status = this.getStatus(sessionId);
1712
- this.ipc.broadcast({ type: "status", data: status });
1273
+ this.ipc.broadcast({ type: "status", data: this.getStatus() });
1713
1274
  if (!this.ipc.hasClients()) {
1714
- const stats = this.db.getStats(sessionId);
1275
+ const stats = this.db.getStats(this.sessionId);
1715
1276
  if (stats.events > 0 && this.cyclesRun % 10 === 0) {
1716
1277
  this.notify.workflowDetected(stats.tasks, `${stats.events} events so far`);
1717
1278
  }
1718
1279
  }
1719
1280
  await sleep(this.config.pollIntervalMs);
1720
1281
  }
1721
- this.db.endSession(sessionId);
1282
+ this.db.endSession(this.sessionId);
1722
1283
  this.ipc.stop();
1723
1284
  cleanup(this.config);
1285
+ return this.sessionId;
1286
+ }
1287
+ pause() {
1288
+ if (!this.paused) {
1289
+ this.paused = true;
1290
+ this.ipc.broadcast({ type: "info", message: "\u23F8 Shadow-Daemon pausiert" });
1291
+ }
1292
+ }
1293
+ resume() {
1294
+ if (this.paused) {
1295
+ this.paused = false;
1296
+ this.ipc.broadcast({ type: "info", message: "\u25B6 Shadow-Daemon fortgesetzt" });
1297
+ }
1724
1298
  }
1725
1299
  stop() {
1726
1300
  this.running = false;
1727
1301
  }
1728
- getStatus(sessionId) {
1729
- const stats = this.db.getStats(sessionId);
1302
+ getSessionId() {
1303
+ return this.sessionId;
1304
+ }
1305
+ checkForCompletedTasks() {
1306
+ const tasks = this.db.getTasks(this.sessionId);
1307
+ const completedCount = tasks.filter((t) => t.status === "completed").length;
1308
+ if (completedCount > this.lastTaskCount) {
1309
+ const newlyCompleted = tasks.filter((t) => t.status === "completed" && !t.isSOPCandidate).slice(-1);
1310
+ for (const task of newlyCompleted) {
1311
+ const desc = task.description ?? `Task ${task.id.substring(0, 8)}`;
1312
+ if (this.ipc.hasClients()) {
1313
+ this.ipc.broadcast({
1314
+ type: "prompt",
1315
+ id: `sop-suggest:${task.id}`,
1316
+ prompt: {
1317
+ kind: "task-boundary",
1318
+ context: { taskId: task.id, description: desc },
1319
+ options: ["Ja, als SOP speichern", "Nein, \xFCberspringen"],
1320
+ defaultAnswer: "Ja, als SOP speichern",
1321
+ timeoutMs: 3e4,
1322
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1323
+ }
1324
+ });
1325
+ } else {
1326
+ this.db.markTaskAsSOPCandidate(task.id);
1327
+ }
1328
+ }
1329
+ this.lastTaskCount = completedCount;
1330
+ }
1331
+ }
1332
+ getStatus() {
1333
+ const stats = this.db.getStats(this.sessionId);
1334
+ const sops = this.db.getSOPs(this.sessionId);
1730
1335
  return {
1731
1336
  pid: process.pid,
1732
1337
  uptime: process.uptime(),
1733
1338
  nodeCount: stats.nodes,
1734
1339
  eventCount: stats.events,
1735
1340
  taskCount: stats.tasks,
1341
+ sopCount: sops.length,
1736
1342
  pendingPrompts: 0,
1737
1343
  autoSave: this.config.autoSaveNodes,
1738
1344
  mode: this.config.shadowMode,
1739
1345
  agentActive: false,
1346
+ paused: this.paused,
1740
1347
  cyclesRun: this.cyclesRun,
1741
1348
  cyclesSkipped: this.cyclesSkipped
1742
1349
  };
@@ -1759,13 +1366,13 @@ function forkDaemon(config) {
1759
1366
  child.unref();
1760
1367
  const pid = child.pid;
1761
1368
  if (!pid) throw new Error("Failed to fork daemon");
1762
- writeFileSync2(config.pidFile, String(pid), "utf8");
1369
+ writeFileSync(config.pidFile, String(pid), "utf8");
1763
1370
  return pid;
1764
1371
  }
1765
1372
  function isDaemonRunning(pidFile) {
1766
- if (!existsSync4(pidFile)) return { running: false };
1373
+ if (!existsSync3(pidFile)) return { running: false };
1767
1374
  try {
1768
- const pid = parseInt(readFileSync3(pidFile, "utf8").trim(), 10);
1375
+ const pid = parseInt(readFileSync2(pidFile, "utf8").trim(), 10);
1769
1376
  if (isNaN(pid)) return { running: false };
1770
1377
  process.kill(pid, 0);
1771
1378
  return { running: true, pid };
@@ -1791,6 +1398,26 @@ function stopDaemon(pidFile) {
1791
1398
  return false;
1792
1399
  }
1793
1400
  }
1401
+ function pauseDaemon(pidFile) {
1402
+ const { running, pid } = isDaemonRunning(pidFile);
1403
+ if (!running || !pid) return false;
1404
+ try {
1405
+ process.kill(pid, "SIGUSR1");
1406
+ return true;
1407
+ } catch {
1408
+ return false;
1409
+ }
1410
+ }
1411
+ function resumeDaemon(pidFile) {
1412
+ const { running, pid } = isDaemonRunning(pidFile);
1413
+ if (!running || !pid) return false;
1414
+ try {
1415
+ process.kill(pid, "SIGUSR2");
1416
+ return true;
1417
+ } catch {
1418
+ return false;
1419
+ }
1420
+ }
1794
1421
  function cleanup(config) {
1795
1422
  try {
1796
1423
  unlinkSync2(config.socketPath);
@@ -1826,6 +1453,7 @@ var ForegroundClient = class {
1826
1453
  }
1827
1454
  };
1828
1455
  var AttachClient = class {
1456
+ isPaused = false;
1829
1457
  async attach(socketPath) {
1830
1458
  const client = new IPCClient();
1831
1459
  try {
@@ -1838,7 +1466,7 @@ var AttachClient = class {
1838
1466
  return;
1839
1467
  }
1840
1468
  process.stderr.write("\u{1F4E1} Verbunden mit Shadow-Daemon\n");
1841
- process.stderr.write(" [T] Neuer Task [S] Status [D] Trennen [Q] Daemon stoppen\n\n");
1469
+ process.stderr.write(" [T] Neuer Task [S] Status [P] Pause/Resume [D] Trennen [Q] Stoppen\n\n");
1842
1470
  if (process.stdin.isTTY) {
1843
1471
  process.stdin.setRawMode(true);
1844
1472
  }
@@ -1861,6 +1489,17 @@ var AttachClient = class {
1861
1489
  client.send({ type: "command", command: "status" });
1862
1490
  return;
1863
1491
  }
1492
+ if (k === "p") {
1493
+ if (this.isPaused) {
1494
+ client.send({ type: "command", command: "resume" });
1495
+ process.stderr.write("\n\u25B6 Resume gesendet\n");
1496
+ } else {
1497
+ client.send({ type: "command", command: "pause" });
1498
+ process.stderr.write("\n\u23F8 Pause gesendet\n");
1499
+ }
1500
+ this.isPaused = !this.isPaused;
1501
+ return;
1502
+ }
1864
1503
  if (k === "d" || k === "") {
1865
1504
  process.stderr.write("\n\u{1F4E1} Getrennt. Daemon l\xE4uft weiter.\n");
1866
1505
  client.disconnect();
@@ -1882,6 +1521,7 @@ var AttachClient = class {
1882
1521
  client.on("message", (msg) => {
1883
1522
  switch (msg.type) {
1884
1523
  case "status":
1524
+ this.isPaused = msg.data.paused;
1885
1525
  renderStatus(msg.data);
1886
1526
  break;
1887
1527
  case "event":
@@ -1898,7 +1538,7 @@ var AttachClient = class {
1898
1538
  `);
1899
1539
  break;
1900
1540
  case "prompt":
1901
- renderPrompt(msg.prompt.kind, msg.prompt.options, (answer) => {
1541
+ renderPrompt(msg.id, msg.prompt.kind, msg.prompt.context, msg.prompt.options, (answer) => {
1902
1542
  client.send({ type: "prompt-response", id: msg.id, answer });
1903
1543
  });
1904
1544
  break;
@@ -1912,17 +1552,34 @@ var AttachClient = class {
1912
1552
  }
1913
1553
  };
1914
1554
  function renderStatus(status) {
1555
+ const state = status.paused ? "\x1B[33m\u23F8 PAUSED\x1B[0m" : "\x1B[32m\u25CF RUNNING\x1B[0m";
1915
1556
  process.stdout.write(
1916
1557
  `
1917
1558
  \u2500\u2500 Shadow Status \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
1918
- PID: ${status.pid} | Uptime: ${Math.round(status.uptime)}s
1919
- Nodes: ${status.nodeCount} | Events: ${status.eventCount} | Tasks: ${status.taskCount}
1559
+ ${state} PID: ${status.pid} | Uptime: ${Math.round(status.uptime)}s
1560
+ Nodes: ${status.nodeCount} | Events: ${status.eventCount} | Tasks: ${status.taskCount} | SOPs: ${status.sopCount}
1920
1561
  Cycles: ${status.cyclesRun} run, ${status.cyclesSkipped} skipped
1921
1562
  \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
1922
1563
  `
1923
1564
  );
1924
1565
  }
1925
- function renderPrompt(kind, options, callback) {
1566
+ function renderPrompt(id, kind, context, options, callback) {
1567
+ if (id.startsWith("sop-suggest:")) {
1568
+ const desc = context["description"] ?? "Unbenannter Task";
1569
+ process.stdout.write(`
1570
+ \u{1F4CB} Task abgeschlossen: "${desc}"
1571
+ `);
1572
+ process.stdout.write(` Als SOP speichern?
1573
+ `);
1574
+ options.forEach((opt, i) => process.stdout.write(` [${i + 1}] ${opt}
1575
+ `));
1576
+ process.stdout.write(" \u2192 ");
1577
+ process.stdin.once("data", (data) => {
1578
+ const idx = parseInt(data.trim(), 10) - 1;
1579
+ callback(options[idx] ?? options[0] ?? "");
1580
+ });
1581
+ return;
1582
+ }
1926
1583
  process.stdout.write(`
1927
1584
  \u2753 ${kind}
1928
1585
  `);
@@ -1936,6 +1593,13 @@ function renderPrompt(kind, options, callback) {
1936
1593
  }
1937
1594
 
1938
1595
  // src/cli.ts
1596
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
1597
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1598
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
1599
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
1600
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
1601
+ var magenta = (s) => `\x1B[35m${s}\x1B[0m`;
1602
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
1939
1603
  if (process.env.CARTOGRAPHYY_DAEMON === "1") {
1940
1604
  const config = JSON.parse(process.env.CARTOGRAPHYY_CONFIG ?? "{}");
1941
1605
  startDaemonProcess(config).catch((err) => {
@@ -1946,17 +1610,10 @@ if (process.env.CARTOGRAPHYY_DAEMON === "1") {
1946
1610
  } else {
1947
1611
  main();
1948
1612
  }
1949
- var bold = (s) => `\x1B[1m${s}\x1B[0m`;
1950
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1951
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
1952
- var green = (s) => `\x1B[32m${s}\x1B[0m`;
1953
- var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
1954
- var magenta = (s) => `\x1B[35m${s}\x1B[0m`;
1955
- var red = (s) => `\x1B[31m${s}\x1B[0m`;
1956
1613
  function main() {
1957
1614
  const program = new Command();
1958
1615
  const CMD = "datasynx-cartography";
1959
- const VERSION = "0.1.8";
1616
+ const VERSION = "0.2.0";
1960
1617
  program.name(CMD).description("AI-powered Infrastructure Cartography & SOP Generation").version(VERSION);
1961
1618
  program.command("discover").description("Infrastruktur scannen und kartographieren").option("--entry <hosts...>", "Startpunkte", ["localhost"]).option("--depth <n>", "Max Tiefe", "8").option("--max-turns <n>", "Max Agent-Turns", "50").option("--model <m>", "Agent-Model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organisation (f\xFCr Backstage)").option("-o, --output <dir>", "Output-Dir", "./datasynx-output").option("--db <path>", "DB-Pfad").option("-v, --verbose", "Agent-Reasoning anzeigen", false).action(async (opts) => {
1962
1619
  checkPrerequisites();
@@ -2150,13 +1807,13 @@ function main() {
2150
1807
  const htmlPath = resolve(config.outputDir, "topology.html");
2151
1808
  const topoPath = resolve(config.outputDir, "topology.mermaid");
2152
1809
  w("\n");
2153
- if (existsSync5(htmlPath)) {
1810
+ if (existsSync4(htmlPath)) {
2154
1811
  w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("topology.html \xF6ffnen"))}
2155
1812
  `);
2156
1813
  }
2157
- if (existsSync5(topoPath)) {
1814
+ if (existsSync4(topoPath)) {
2158
1815
  try {
2159
- const code = readFileSync4(topoPath, "utf8");
1816
+ const code = readFileSync3(topoPath, "utf8");
2160
1817
  const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
2161
1818
  w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("mermaid.live \xF6ffnen"))}
2162
1819
  `);
@@ -2200,11 +1857,99 @@ function main() {
2200
1857
  process.stderr.write(" datasynx-cartography shadow stop \u2014 stoppen\n\n");
2201
1858
  }
2202
1859
  });
2203
- shadow.command("stop").description("Shadow-Daemon stoppen").action(() => {
2204
- const config = defaultConfig();
1860
+ shadow.command("stop").description("Shadow-Daemon stoppen + SOP-Review").option("-o, --output <dir>", "Output-Dir f\xFCr SOPs + Dashboard", "./datasynx-output").option("--no-review", "SOP-Review \xFCberspringen").action(async (opts) => {
1861
+ const config = defaultConfig({ outputDir: opts.output });
2205
1862
  const stopped = stopDaemon(config.pidFile);
2206
- if (stopped) {
2207
- process.stderr.write("\u2713 Shadow-Daemon gestoppt\n");
1863
+ if (!stopped) {
1864
+ process.stderr.write("\u26A0 Kein laufender Shadow-Daemon gefunden\n");
1865
+ return;
1866
+ }
1867
+ process.stderr.write("\u2713 Shadow-Daemon gestoppt\n");
1868
+ if (opts.review === false) return;
1869
+ await new Promise((r) => setTimeout(r, 500));
1870
+ const db = new CartographyDB(config.dbPath);
1871
+ const session = db.getLatestSession("shadow");
1872
+ if (!session) {
1873
+ db.close();
1874
+ return;
1875
+ }
1876
+ const stats = db.getStats(session.id);
1877
+ const w = (s) => process.stderr.write(s);
1878
+ w("\n");
1879
+ 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"));
1880
+ w(bold(" Shadow-Session Review\n"));
1881
+ w(dim(` Session: ${session.id}
1882
+ `));
1883
+ w(dim(` Nodes: ${stats.nodes} | Events: ${stats.events} | Tasks: ${stats.tasks}
1884
+ `));
1885
+ 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"));
1886
+ w("\n");
1887
+ if (stats.tasks > 0) {
1888
+ try {
1889
+ w(" SOPs generieren...\n");
1890
+ const count = await generateSOPs(db, session.id);
1891
+ w(` ${green("\u2713")} ${count} SOPs generiert
1892
+
1893
+ `);
1894
+ } catch (err) {
1895
+ w(` ${red("\u2717")} SOP-Generierung fehlgeschlagen: ${err}
1896
+
1897
+ `);
1898
+ }
1899
+ }
1900
+ const { exportSOPMarkdown, exportSOPDashboard } = await import("./exporter-BDVDYA3K.js");
1901
+ const sops = db.getSOPs(session.id);
1902
+ if (sops.length > 0) {
1903
+ w(bold(" SOPs zur \xDCberpr\xFCfung:\n\n"));
1904
+ for (const sop of sops) {
1905
+ const md = exportSOPMarkdown(sop);
1906
+ for (const line of md.split("\n")) {
1907
+ process.stdout.write(` ${line}
1908
+ `);
1909
+ }
1910
+ process.stdout.write("\n");
1911
+ 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\n"));
1912
+ }
1913
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("fs");
1914
+ const { join: join2, resolve: resolvePath } = await import("path");
1915
+ mkdirSync2(config.outputDir, { recursive: true });
1916
+ mkdirSync2(join2(config.outputDir, "sops"), { recursive: true });
1917
+ for (const sop of sops) {
1918
+ const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
1919
+ writeFileSync2(join2(config.outputDir, "sops", filename), exportSOPMarkdown(sop));
1920
+ }
1921
+ const allSOPs = db.getAllSOPs();
1922
+ const dashboardHtml = exportSOPDashboard(allSOPs);
1923
+ const dashboardPath = join2(config.outputDir, "sop-dashboard.html");
1924
+ writeFileSync2(dashboardPath, dashboardHtml);
1925
+ const absPath = resolvePath(dashboardPath);
1926
+ w(` ${green("\u2713")} ${sops.length} SOP-Markdown-Dateien geschrieben
1927
+ `);
1928
+ w(` ${green("\u2713")} SOP Dashboard: ${cyan(`file://${absPath}`)}
1929
+ `);
1930
+ w("\n");
1931
+ w(dim(` \xD6ffne im Browser: ${bold(`file://${absPath}`)}
1932
+ `));
1933
+ w("\n");
1934
+ } else {
1935
+ w(dim(" Keine SOPs in dieser Session.\n\n"));
1936
+ }
1937
+ db.close();
1938
+ });
1939
+ shadow.command("pause").description("Shadow-Daemon pausieren").action(() => {
1940
+ const config = defaultConfig();
1941
+ const paused = pauseDaemon(config.pidFile);
1942
+ if (paused) {
1943
+ process.stderr.write("\u23F8 Shadow-Daemon pausiert\n");
1944
+ } else {
1945
+ process.stderr.write("\u26A0 Kein laufender Shadow-Daemon gefunden\n");
1946
+ }
1947
+ });
1948
+ shadow.command("resume").description("Shadow-Daemon fortsetzen").action(() => {
1949
+ const config = defaultConfig();
1950
+ const resumed = resumeDaemon(config.pidFile);
1951
+ if (resumed) {
1952
+ process.stderr.write("\u25B6 Shadow-Daemon fortgesetzt\n");
2208
1953
  } else {
2209
1954
  process.stderr.write("\u26A0 Kein laufender Shadow-Daemon gefunden\n");
2210
1955
  }
@@ -2614,10 +2359,172 @@ ${infraSummary.substring(0, 12e3)}`;
2614
2359
  out(dim(" PID: ~/.cartography/daemon.pid\n"));
2615
2360
  out("\n");
2616
2361
  });
2362
+ program.command("bookmarks").description("Alle Browser-Lesezeichen anzeigen (Chrome, Edge, Brave, Firefox)").action(async () => {
2363
+ const { scanAllBookmarks: scanAllBookmarks2 } = await import("./bookmarks-O7KNR7D3.js");
2364
+ const out = (s) => process.stdout.write(s);
2365
+ process.stderr.write(" Scanning bookmarks...\n\n");
2366
+ const hosts = await scanAllBookmarks2();
2367
+ if (hosts.length === 0) {
2368
+ out(" (Keine Lesezeichen gefunden \u2014 Chrome, Edge, Brave und Firefox werden unterst\xFCtzt)\n\n");
2369
+ return;
2370
+ }
2371
+ const bySource = /* @__PURE__ */ new Map();
2372
+ for (const h of hosts) {
2373
+ if (!bySource.has(h.source)) bySource.set(h.source, []);
2374
+ bySource.get(h.source).push(h);
2375
+ }
2376
+ for (const [source, entries] of bySource) {
2377
+ out(bold(cyan(` ${source.toUpperCase()}`)) + dim(` (${entries.length} Hosts)
2378
+ `));
2379
+ out(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\n"));
2380
+ for (const h of entries) {
2381
+ const isDefault = h.protocol === "https" && h.port === 443 || h.protocol === "http" && h.port === 80;
2382
+ const portStr = isDefault ? "" : `:${h.port}`;
2383
+ out(` ${cyan(h.protocol + "://")}${h.hostname}${dim(portStr)}
2384
+ `);
2385
+ }
2386
+ out("\n");
2387
+ }
2388
+ out(dim(` Total: ${hosts.length} unique hosts
2389
+
2390
+ `));
2391
+ out(dim(" Tipp: ") + "datasynx-cartography discover" + dim(" \u2014 scannt + klassifiziert alle Lesezeichen automatisch\n\n"));
2392
+ });
2393
+ program.command("seed").description("Bekannte Infrastruktur manuell eintragen (Tools, DBs, APIs, etc.)").option("--file <path>", "JSON-Datei mit Node-Definitionen einlesen").option("--session <id>", "In existierende Session eintragen (default: neue Session)").option("--db <path>", "DB-Pfad").action(async (opts) => {
2394
+ const config = defaultConfig({ ...opts.db ? { dbPath: opts.db } : {} });
2395
+ const db = new CartographyDB(config.dbPath);
2396
+ const sessionId = opts.session ?? db.createSession("discover", config);
2397
+ const out = (s) => process.stdout.write(s);
2398
+ const w = (s) => process.stderr.write(s);
2399
+ if (opts.file) {
2400
+ let raw;
2401
+ try {
2402
+ raw = JSON.parse(readFileSync3(resolve(opts.file), "utf8"));
2403
+ } catch (e) {
2404
+ w(red(`
2405
+ \u2717 Datei konnte nicht gelesen werden: ${e}
2406
+
2407
+ `));
2408
+ process.exitCode = 1;
2409
+ return;
2410
+ }
2411
+ if (!Array.isArray(raw)) {
2412
+ w(red('\n \u2717 JSON muss ein Array sein: [{ "type": "...", "name": "...", "host": "..." }]\n\n'));
2413
+ process.exitCode = 1;
2414
+ return;
2415
+ }
2416
+ let saved2 = 0;
2417
+ for (const entry of raw) {
2418
+ const type = entry["type"];
2419
+ const name = entry["name"];
2420
+ const host = entry["host"];
2421
+ const port = entry["port"];
2422
+ const tags = entry["tags"] ?? [];
2423
+ const metadata = entry["metadata"] ?? {};
2424
+ if (!type || !name) {
2425
+ w(yellow(` \u26A0 \xDCbersprungen (kein type/name): ${JSON.stringify(entry)}
2426
+ `));
2427
+ continue;
2428
+ }
2429
+ const id = host ? `${type}:${host}${port ? ":" + port : ""}` : `${type}:${name.toLowerCase().replace(/\s+/g, "-")}`;
2430
+ db.upsertNode(sessionId, {
2431
+ id,
2432
+ type,
2433
+ name,
2434
+ discoveredVia: "manual",
2435
+ confidence: 1,
2436
+ metadata: { ...metadata, ...host ? { host } : {}, ...port ? { port } : {} },
2437
+ tags
2438
+ });
2439
+ out(` ${green("+")} ${cyan(id)} ${dim("(" + type + ")")}
2440
+ `);
2441
+ saved2++;
2442
+ }
2443
+ db.endSession(sessionId);
2444
+ w(`
2445
+ ${green(bold("DONE"))} ${saved2} Nodes gespeichert ${dim("Session: " + sessionId)}
2446
+
2447
+ `);
2448
+ return;
2449
+ }
2450
+ const { NODE_TYPES: NODE_TYPES2 } = await import("./types-NKF6BRMZ.js");
2451
+ if (!process.stdin.isTTY) {
2452
+ w(red("\n \u2717 Interaktiver Modus ben\xF6tigt ein Terminal (--file f\xFCr nicht-interaktiven Betrieb)\n\n"));
2453
+ process.exitCode = 1;
2454
+ return;
2455
+ }
2456
+ w("\n");
2457
+ 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"));
2458
+ w(bold(" Bekannte Infrastruktur eintragen\n"));
2459
+ w(dim(" Beispiele: Datenbanken, APIs, SaaS-Tools, Cloud-Services\n"));
2460
+ 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"));
2461
+ w("\n");
2462
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2463
+ const ask = (q) => new Promise((res) => rl.question(q, res));
2464
+ let saved = 0;
2465
+ const typeList = NODE_TYPES2.map((t, i) => `${dim((i + 1).toString().padStart(2))} ${t}`).join("\n ");
2466
+ while (true) {
2467
+ w("\n");
2468
+ w(dim(" Node-Typen:\n"));
2469
+ w(` ${typeList}
2470
+
2471
+ `);
2472
+ const typeInput = (await ask(` ${cyan("Typ")} ${dim("[Nr. oder Name, Enter=abbrechen]")}: `)).trim();
2473
+ if (!typeInput) break;
2474
+ let nodeType;
2475
+ const asNum = parseInt(typeInput, 10);
2476
+ if (!isNaN(asNum) && asNum >= 1 && asNum <= NODE_TYPES2.length) {
2477
+ nodeType = NODE_TYPES2[asNum - 1];
2478
+ } else if (NODE_TYPES2.includes(typeInput)) {
2479
+ nodeType = typeInput;
2480
+ } else {
2481
+ w(yellow(` \u26A0 Unbekannter Typ: "${typeInput}"
2482
+ `));
2483
+ continue;
2484
+ }
2485
+ const name = (await ask(` ${cyan("Name")} ${dim('[z.B. "Prod PostgreSQL"]')}: `)).trim();
2486
+ if (!name) {
2487
+ w(dim(" (Abgebrochen)\n"));
2488
+ continue;
2489
+ }
2490
+ const hostRaw = (await ask(` ${cyan("Host / IP")} ${dim("[optional, Enter=\xFCberspringen]")}: `)).trim();
2491
+ const portRaw = (await ask(` ${cyan("Port")} ${dim("[optional]")}: `)).trim();
2492
+ const tagsRaw = (await ask(` ${cyan("Tags")} ${dim("[komma-getrennt, optional]")}: `)).trim();
2493
+ const host = hostRaw || void 0;
2494
+ const port = portRaw ? parseInt(portRaw, 10) : void 0;
2495
+ const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
2496
+ const id = host ? `${nodeType}:${host}${port ? ":" + port : ""}` : `${nodeType}:${name.toLowerCase().replace(/\s+/g, "-")}`;
2497
+ db.upsertNode(sessionId, {
2498
+ id,
2499
+ type: nodeType,
2500
+ name,
2501
+ discoveredVia: "manual",
2502
+ confidence: 1,
2503
+ metadata: { ...host ? { host } : {}, ...port ? { port } : {} },
2504
+ tags
2505
+ });
2506
+ out(` ${green("+")} ${cyan(id)}
2507
+ `);
2508
+ saved++;
2509
+ const again = (await ask(` ${dim("Weiteren Node hinzuf\xFCgen? [Y/n]")}: `)).trim().toLowerCase();
2510
+ if (again === "n" || again === "nein") break;
2511
+ }
2512
+ rl.close();
2513
+ db.endSession(sessionId);
2514
+ w("\n");
2515
+ 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"));
2516
+ w(` ${green(bold("DONE"))} ${saved} Node${saved !== 1 ? "s" : ""} gespeichert
2517
+ `);
2518
+ w(` ${dim("Session: " + sessionId)}
2519
+ `);
2520
+ w(` ${dim("Tipp: datasynx-cartography show " + sessionId)}
2521
+
2522
+ `);
2523
+ });
2617
2524
  program.command("doctor").description("Pr\xFCft ob alle Voraussetzungen erf\xFCllt sind").action(async () => {
2618
2525
  const { execSync: execSync3 } = await import("child_process");
2619
- const { existsSync: existsSync6, readFileSync: readFileSync5 } = await import("fs");
2620
- const { join: join4 } = await import("path");
2526
+ const { existsSync: existsSync5, readFileSync: readFileSync4 } = await import("fs");
2527
+ const { join: join2 } = await import("path");
2621
2528
  const out = (s) => process.stdout.write(s);
2622
2529
  const ok = (msg) => out(` \x1B[32m\u2713\x1B[0m ${msg}
2623
2530
  `);
@@ -2648,7 +2555,7 @@ ${infraSummary.substring(0, 12e3)}`;
2648
2555
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2649
2556
  let hasOAuth = false;
2650
2557
  try {
2651
- const creds = JSON.parse(readFileSync5(join4(home, ".claude", ".credentials.json"), "utf8"));
2558
+ const creds = JSON.parse(readFileSync4(join2(home, ".claude", ".credentials.json"), "utf8"));
2652
2559
  const oauth = creds["claudeAiOauth"];
2653
2560
  hasOAuth = typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
2654
2561
  } catch {
@@ -2661,12 +2568,30 @@ ${infraSummary.substring(0, 12e3)}`;
2661
2568
  err("Keine Authentifizierung \u2014 claude login oder export ANTHROPIC_API_KEY=sk-ant-...");
2662
2569
  allGood = false;
2663
2570
  }
2664
- const optional = [
2571
+ try {
2572
+ const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
2573
+ ok(`kubectl ${dim2(v || "(Client OK)")}`);
2574
+ } catch {
2575
+ warn(`kubectl nicht gefunden ${dim2("\u2014 Installation: https://kubernetes.io/docs/tasks/tools/")}`);
2576
+ }
2577
+ const cloudClis = [
2578
+ ["aws", "aws --version", "AWS CLI \u2014 https://aws.amazon.com/cli/"],
2579
+ ["gcloud", "gcloud --version", "Google Cloud SDK \u2014 https://cloud.google.com/sdk/"],
2580
+ ["az", "az --version", "Azure CLI \u2014 https://aka.ms/installazurecliwindows"]
2581
+ ];
2582
+ for (const [name, cmd, hint] of cloudClis) {
2583
+ try {
2584
+ execSync3(cmd, { stdio: "pipe" });
2585
+ ok(`${name} ${dim2("(Cloud-Scanning verf\xFCgbar)")}`);
2586
+ } catch {
2587
+ warn(`${name} nicht gefunden ${dim2("\u2014 Cloud-Scan \xFCbersprungen | " + hint)}`);
2588
+ }
2589
+ }
2590
+ const localTools = [
2665
2591
  ["docker", "docker --version"],
2666
- ["kubectl", "kubectl version --client --short"],
2667
2592
  ["ss", "ss --version"]
2668
2593
  ];
2669
- for (const [name, cmd] of optional) {
2594
+ for (const [name, cmd] of localTools) {
2670
2595
  try {
2671
2596
  execSync3(cmd, { stdio: "pipe" });
2672
2597
  ok(`${name} ${dim2("(Discovery-Tool)")}`);
@@ -2674,8 +2599,8 @@ ${infraSummary.substring(0, 12e3)}`;
2674
2599
  warn(`${name} nicht gefunden ${dim2("\u2014 Discovery ohne " + name + " eingeschr\xE4nkt")}`);
2675
2600
  }
2676
2601
  }
2677
- const dbDir = join4(home, ".cartography");
2678
- if (existsSync6(dbDir)) {
2602
+ const dbDir = join2(home, ".cartography");
2603
+ if (existsSync5(dbDir)) {
2679
2604
  ok(`~/.cartography ${dim2("(Daten-Verzeichnis vorhanden)")}`);
2680
2605
  } else {
2681
2606
  warn("~/.cartography existiert noch nicht " + dim2("\u2014 wird beim ersten Start angelegt"));
@@ -2712,14 +2637,22 @@ ${infraSummary.substring(0, 12e3)}`;
2712
2637
  o(_b(" Commands:\n"));
2713
2638
  o("\n");
2714
2639
  o(` ${_g("discover")} ${_d("Infrastruktur scannen (Claude Sonnet)")}
2640
+ `);
2641
+ o(` ${_g("seed")} ${_d("Bekannte Tools/DBs/APIs manuell eintragen")}
2642
+ `);
2643
+ o(` ${_g("bookmarks")} ${_d("Browser-Lesezeichen anzeigen")}
2715
2644
  `);
2716
2645
  o(` ${_g("shadow start")} ${_d("Background-Daemon starten (Claude Haiku)")}
2717
2646
  `);
2718
- o(` ${_g("shadow stop")} ${_d("Daemon stoppen")}
2647
+ o(` ${_g("shadow pause")} ${_d("Daemon pausieren")}
2648
+ `);
2649
+ o(` ${_g("shadow resume")} ${_d("Daemon fortsetzen")}
2650
+ `);
2651
+ o(` ${_g("shadow stop")} ${_d("Stoppen + SOP-Review + Dashboard")}
2719
2652
  `);
2720
2653
  o(` ${_g("shadow status")} ${_d("Daemon-Status anzeigen")}
2721
2654
  `);
2722
- o(` ${_g("shadow attach")} ${_d("Live an Daemon ankoppeln")}
2655
+ o(` ${_g("shadow attach")} ${_d("Live-Steuerung: [T] [S] [P] [D] [Q]")}
2723
2656
  `);
2724
2657
  o(` ${_g("sops")} ${_d("[session]")} ${_d("SOPs aus Workflows generieren")}
2725
2658
  `);
@@ -2729,7 +2662,7 @@ ${infraSummary.substring(0, 12e3)}`;
2729
2662
  `);
2730
2663
  o(` ${_g("sessions")} ${_d("Alle Sessions auflisten")}
2731
2664
  `);
2732
- o(` ${_g("doctor")} ${_d("Installations-Check")}
2665
+ o(` ${_g("doctor")} ${_d("Installations-Check (kubectl, aws, gcloud, az)")}
2733
2666
  `);
2734
2667
  o(` ${_g("docs")} ${_d("Vollst\xE4ndige Dokumentation")}
2735
2668
  `);
@@ -2739,6 +2672,8 @@ ${infraSummary.substring(0, 12e3)}`;
2739
2672
  o(_b(" Quick Start:\n"));
2740
2673
  o("\n");
2741
2674
  o(` ${_m("$")} ${_b("datasynx-cartography doctor")} ${_d("Alles bereit?")}
2675
+ `);
2676
+ o(` ${_m("$")} ${_b("datasynx-cartography seed")} ${_d("Bekannte Infra eintragen")}
2742
2677
  `);
2743
2678
  o(` ${_m("$")} ${_b("datasynx-cartography discover")} ${_d("Einmal-Scan")}
2744
2679
  `);