@datasynx/agentic-ai-cartography 0.1.7 → 0.1.8

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
@@ -665,7 +665,7 @@ function stripSensitive(target) {
665
665
  return target.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
666
666
  }
667
667
  }
668
- async function createCartographyTools(db, sessionId) {
668
+ async function createCartographyTools(db, sessionId, opts = {}) {
669
669
  const sdk = await import("@anthropic-ai/claude-code");
670
670
  const { tool, createSdkMcpServer } = sdk;
671
671
  const tools = [
@@ -755,6 +755,20 @@ async function createCartographyTools(db, sessionId) {
755
755
  db.updateTaskDescription(sessionId, args["description"]);
756
756
  return { content: [{ type: "text", text: "\u2713 Beschreibung aktualisiert" }] };
757
757
  }),
758
+ 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")
761
+ }, async (args) => {
762
+ const question = args["question"];
763
+ const context = args["context"];
764
+ if (opts.onAskUser) {
765
+ const answer = await opts.onAskUser(question, context);
766
+ return { content: [{ type: "text", text: answer }] };
767
+ }
768
+ return {
769
+ content: [{ type: "text", text: "(Kein interaktiver Modus \u2014 bitte ohne diese Information fortfahren)" }]
770
+ };
771
+ }),
758
772
  tool("scan_bookmarks", "Alle Browser-Lesezeichen scannen \u2014 nur Hostnamen, keine pers\xF6nlichen Daten", {
759
773
  minConfidence: z2.number().min(0).max(1).default(0.5).optional()
760
774
  }, async () => {
@@ -830,40 +844,54 @@ var safetyHook = async (input) => {
830
844
  };
831
845
 
832
846
  // src/agent.ts
833
- async function runDiscovery(config, db, sessionId, onEvent) {
847
+ async function runDiscovery(config, db, sessionId, onEvent, onAskUser) {
834
848
  const { query } = await import("@anthropic-ai/claude-code");
835
- const tools = await createCartographyTools(db, sessionId);
836
- const systemPrompt = `Du bist ein Infrastruktur-Discovery-Agent.
837
- Kartographiere die gesamte Systemlandschaft \u2014 lokale Infrastruktur UND SaaS-Tools.
849
+ const tools = await createCartographyTools(db, sessionId, { onAskUser });
850
+ const systemPrompt = `Du bist ein Infrastruktur-Discovery-Agent. Kartographiere die gesamte Systemlandschaft \u2014 lokale Services UND SaaS-Tools des Users.
851
+
852
+ \u2501\u2501 PFLICHT-REIHENFOLGE \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
853
+ SCHRITT 1 \u2014 Browser-Lesezeichen (IMMER ZUERST):
854
+ scan_bookmarks() aufrufen \u2192 jede zur\xFCckgegebene Domain klassifizieren:
855
+ \u2022 Business-Tools (GitHub, Notion, Jira, Linear, Vercel, AWS, Datadog, etc.) \u2192 save_node als saas_tool
856
+ \u2022 Interne Hosts (IPs, custom.company.com:PORT) \u2192 save_node als web_service
857
+ \u2022 Pers\xF6nliches (Social Media, News, Streaming, Shopping) \u2192 IGNORIEREN, NICHT speichern
838
858
 
839
- STRATEGIE:
840
- 1. ss -tlnp + ps aux \u2192 \xDCberblick lokaler Services
841
- 2. Jeden Service tiefer (Datenbanken\u2192Tabellen, APIs\u2192Endpoints, Queues\u2192Topics)
842
- 3. scan_bookmarks \u2192 Browser-Lesezeichen analysieren: Welche Domains sind Business-Tools?
843
- - Nur Tools mit Business-Daten als saas_tool speichern (GitHub, Notion, Jira, AWS, etc.)
844
- - Pers\xF6nliches (Social Media, News, Shopping) IGNORIEREN
845
- - Interne Hosts (IP-Adressen, custom.domain.com) als web_service speichern
846
- 4. save_node + save_edge f\xFCr alles. get_catalog \u2192 keine Duplikate.
847
- 5. Config-Files folgen: .env (nur Host:Port!), docker-compose.yml, application.yml
848
- 6. Backtrack wenn Spur ersch\xF6pft. Stop wenn alles explored.
859
+ SCHRITT 2 \u2014 Lokale Infrastruktur:
860
+ ss -tlnp && ps aux \u2192 alle lauschenden Ports/Prozesse identifizieren
861
+ Jeden Service vertiefen: DB\u2192Schemas, API\u2192Endpoints, Queue\u2192Topics
862
+
863
+ SCHRITT 3 \u2014 Config-Files:
864
+ .env, docker-compose.yml, application.yml, kubernetes/*.yml
865
+ Nur Host:Port extrahieren \u2014 KEINE Credentials
866
+
867
+ SCHRITT 4 \u2014 R\xFCckfragen bei Unklarheit:
868
+ ask_user() nutzen wenn: Dienst unklar ist, Kontext fehlt, oder User Input sinnvoll w\xE4re
869
+ Beispiele: "Welche Umgebung ist das (dev/staging/prod)?", "Ist <host> ein internes Tool?"
870
+
871
+ SCHRITT 5 \u2014 Fertig wenn alle Spuren ersch\xF6pft.
872
+ \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
849
873
 
850
874
  PORT-MAPPING: 5432=postgres, 3306=mysql, 27017=mongodb, 6379=redis,
851
875
  9092=kafka, 5672=rabbitmq, 80/443/8080/3000=web_service,
852
876
  9090=prometheus, 8500=consul, 8200=vault, 2379=etcd
853
877
 
854
878
  REGELN:
855
- - NUR read-only Commands (ss, ps, cat, head, curl -s, docker inspect, kubectl get)
856
- - Targets NUR Host:Port \u2014 KEINE URLs, Pfade, Credentials
857
- - Node IDs: "{type}:{host}:{port}" oder "{type}:{name}"
858
- - saas_tool Node IDs: "saas_tool:{hostname}" (z.B. "saas_tool:github.com")
859
- - Confidence: 0.9 direkt beobachtet, 0.7 aus Config/Lesezeichen, 0.5 Vermutung
860
- - KEINE Credentials, KEINE pers\xF6nlichen Daten speichern
861
- - metadata: { description, url_base, category } \u2014 NUR technische Daten
879
+ \u2022 Nur read-only (ss, ps, cat, head, curl -s, docker inspect, kubectl get)
880
+ \u2022 Node IDs: "type:host:port" oder "type:name" \u2014 keine Pfade, keine Credentials
881
+ \u2022 saas_tool IDs: "saas_tool:github.com", "saas_tool:notion.so"
882
+ \u2022 Confidence: 0.9 direkt gesehen, 0.7 aus Config/Bookmarks, 0.5 Vermutung
883
+ \u2022 metadata erlaubt: { description, category, port, version } \u2014 keine Passw\xF6rter
884
+ \u2022 get_catalog vor save_node \u2192 Duplikate vermeiden
885
+ \u2022 Edges speichern wenn Verbindungen klar erkennbar sind
862
886
 
863
887
  Entrypoints: ${config.entryPoints.join(", ")}`;
888
+ const initialPrompt = `Starte Discovery jetzt.
889
+ F\xFChre SOFORT als erstes scan_bookmarks aus \u2014 noch bevor du ss oder ps verwendest.
890
+ Dann systematisch lokale Services, dann Config-Files.
891
+ Nutze ask_user wenn du Kontext vom User brauchst.`;
864
892
  let turnCount = 0;
865
893
  for await (const msg of query({
866
- prompt: systemPrompt,
894
+ prompt: initialPrompt,
867
895
  options: {
868
896
  model: config.agentModel,
869
897
  maxTurns: config.maxTurns,
@@ -874,7 +902,8 @@ Entrypoints: ${config.entryPoints.join(", ")}`;
874
902
  "mcp__cartograph__save_node",
875
903
  "mcp__cartograph__save_edge",
876
904
  "mcp__cartograph__get_catalog",
877
- "mcp__cartograph__scan_bookmarks"
905
+ "mcp__cartograph__scan_bookmarks",
906
+ "mcp__cartograph__ask_user"
878
907
  ],
879
908
  hooks: {
880
909
  PreToolUse: [{ matcher: "Bash", hooks: [safetyHook] }]
@@ -1024,6 +1053,25 @@ function clusterTasks(tasks) {
1024
1053
  // src/exporter.ts
1025
1054
  import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
1026
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"];
1027
1075
  var MERMAID_ICONS = {
1028
1076
  host: "\u{1F5A5}",
1029
1077
  database_server: "\u{1F5C4}",
@@ -1076,18 +1124,18 @@ function nodeLabel(node) {
1076
1124
  const parts = node.id.split(":");
1077
1125
  const location = parts.length >= 3 ? `${parts[1]}:${parts[2]}` : parts[1] ?? "";
1078
1126
  const conf = `${Math.round(node.confidence * 100)}%`;
1079
- const loc = location ? `<br/><small>${location}</small>` : "";
1080
- return `"${icon} <b>${node.name}</b>${loc}<br/><small>${node.type} \xB7 ${conf}</small>"`;
1081
- }
1082
- function groupByHost(nodes) {
1083
- const groups = /* @__PURE__ */ new Map();
1084
- for (const node of nodes) {
1085
- const parts = node.id.split(":");
1086
- const group = node.type === "saas_tool" ? "__saas__" : parts.length >= 3 ? parts[1] : "__local__";
1087
- if (!groups.has(group)) groups.set(group, []);
1088
- groups.get(group).push(node);
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
+ }
1089
1135
  }
1090
- return groups;
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>"]`;
1091
1139
  }
1092
1140
  function generateTopologyMermaid(nodes, edges) {
1093
1141
  if (nodes.length === 0) return 'graph TB\n empty["No nodes discovered yet"]';
@@ -1098,25 +1146,29 @@ function generateTopologyMermaid(nodes, edges) {
1098
1146
  lines.push(` classDef ${type.replace(/_/g, "")} ${style}`);
1099
1147
  }
1100
1148
  lines.push("");
1101
- const groups = groupByHost(nodes);
1102
- for (const [group, groupNodes] of groups) {
1103
- const isSubgraph = groups.size > 1;
1104
- if (isSubgraph) {
1105
- const label = group === "__saas__" ? "SaaS Tools \u2601" : group === "__local__" ? "Local" : group;
1106
- lines.push(` subgraph ${sanitize(group)}["${label}"]`);
1107
- }
1108
- for (const node of groupNodes) {
1109
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
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, "")}`);
1110
1162
  }
1111
- if (isSubgraph) lines.push(" end");
1163
+ lines.push(" end");
1112
1164
  lines.push("");
1113
1165
  }
1114
1166
  for (const edge of edges) {
1115
1167
  const src = sanitize(edge.sourceId);
1116
1168
  const tgt = sanitize(edge.targetId);
1117
1169
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
1118
- const conf = edge.confidence < 0.6 ? " ?" : "";
1119
- lines.push(` ${src} -->|"${label}${conf}"| ${tgt}`);
1170
+ const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
1171
+ lines.push(` ${src} ${arrow} ${tgt}`);
1120
1172
  }
1121
1173
  return lines.join("\n");
1122
1174
  }
@@ -1206,8 +1258,23 @@ function exportJSON(db, sessionId) {
1206
1258
  }
1207
1259
  function exportHTML(nodes, edges) {
1208
1260
  const graphData = JSON.stringify({
1209
- nodes: nodes.map((n) => ({ id: n.id, name: n.name, type: n.type, confidence: n.confidence })),
1210
- links: edges.map((e) => ({ source: e.sourceId, target: e.targetId, relationship: e.relationship }))
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
+ }))
1211
1278
  });
1212
1279
  return `<!DOCTYPE html>
1213
1280
  <html lang="de">
@@ -1216,22 +1283,50 @@ function exportHTML(nodes, edges) {
1216
1283
  <title>Cartography \u2014 Topology</title>
1217
1284
  <script src="https://d3js.org/d3.v7.min.js"></script>
1218
1285
  <style>
1219
- body { margin: 0; background: #1a1a2e; color: #eee; font-family: monospace; }
1220
- svg { width: 100vw; height: 100vh; }
1221
- .node circle { stroke: #fff; stroke-width: 1.5px; }
1222
- .node text { font-size: 10px; fill: #eee; }
1223
- .link { stroke: #666; stroke-opacity: 0.6; }
1224
- #info { position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7);
1225
- padding: 10px; border-radius: 4px; font-size: 12px; }
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; }
1226
1315
  </style>
1227
1316
  </head>
1228
1317
  <body>
1229
- <div id="info">
1230
- <strong>Cartography</strong><br>
1231
- Nodes: ${nodes.length} | Edges: ${edges.length}<br>
1232
- <small>Drag to explore</small>
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>
1233
1329
  </div>
1234
- <svg></svg>
1235
1330
  <script>
1236
1331
  const data = ${graphData};
1237
1332
 
@@ -1240,41 +1335,100 @@ const TYPE_COLORS = {
1240
1335
  web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
1241
1336
  message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
1242
1337
  container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
1243
- config_file: '#adb5bd', unknown: '#6c757d',
1338
+ config_file: '#adb5bd', saas_tool: '#da8bff', unknown: '#6c757d',
1244
1339
  };
1245
1340
 
1246
- const svg = d3.select('svg');
1247
- const width = window.innerWidth, height = window.innerHeight;
1248
- const g = svg.append('g');
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');
1249
1386
 
1250
- svg.call(d3.zoom().on('zoom', e => g.attr('transform', e.transform)));
1387
+ svgEl.call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform)));
1251
1388
 
1252
1389
  const sim = d3.forceSimulation(data.nodes)
1253
- .force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
1254
- .force('charge', d3.forceManyBody().strength(-200))
1255
- .force('center', d3.forceCenter(width / 2, height / 2));
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));
1256
1394
 
1257
- const link = g.append('g').selectAll('line')
1258
- .data(data.links).join('line').attr('class', 'link');
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);
1259
1401
 
1260
- const node = g.append('g').selectAll('g')
1261
- .data(data.nodes).join('g').attr('class', 'node')
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')
1262
1407
  .call(d3.drag()
1263
1408
  .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
1264
1409
  .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
1265
1410
  .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
1266
- );
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}\`);
1267
1420
 
1268
- node.append('circle').attr('r', 8).attr('fill', d => TYPE_COLORS[d.type] || '#aaa');
1269
- node.append('text').attr('dx', 12).attr('dy', '.35em').text(d => d.name);
1270
- node.append('title').text(d => \`\${d.type}: \${d.id}
1271
- Confidence: \${d.confidence}\`);
1421
+ node.append('text').attr('dx', d => radius(d) + 4).attr('dy', '.35em').text(d => d.name);
1272
1422
 
1273
1423
  sim.on('tick', () => {
1274
1424
  link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
1275
1425
  .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
1276
1426
  node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
1277
1427
  });
1428
+
1429
+ svgEl.on('click', () => {
1430
+ sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Klicke einen Node um Details anzuzeigen.</p>';
1431
+ });
1278
1432
  </script>
1279
1433
  </body>
1280
1434
  </html>`;
@@ -1792,10 +1946,17 @@ if (process.env.CARTOGRAPHYY_DAEMON === "1") {
1792
1946
  } else {
1793
1947
  main();
1794
1948
  }
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`;
1795
1956
  function main() {
1796
1957
  const program = new Command();
1797
1958
  const CMD = "datasynx-cartography";
1798
- const VERSION = "0.1.7";
1959
+ const VERSION = "0.1.8";
1799
1960
  program.name(CMD).description("AI-powered Infrastructure Cartography & SOP Generation").version(VERSION);
1800
1961
  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) => {
1801
1962
  checkPrerequisites();
@@ -1813,12 +1974,6 @@ function main() {
1813
1974
  const db = new CartographyDB(config.dbPath);
1814
1975
  const sessionId = db.createSession("discover", config);
1815
1976
  const w = process.stderr.write.bind(process.stderr);
1816
- const bold = (s) => `\x1B[1m${s}\x1B[0m`;
1817
- const dim = (s) => `\x1B[2m${s}\x1B[0m`;
1818
- const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
1819
- const green = (s) => `\x1B[32m${s}\x1B[0m`;
1820
- const yellow = (s) => `\x1B[33m${s}\x1B[0m`;
1821
- const magenta = (s) => `\x1B[35m${s}\x1B[0m`;
1822
1977
  const SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1823
1978
  let spinIdx = 0;
1824
1979
  let spinnerTimer = null;
@@ -1892,6 +2047,12 @@ function main() {
1892
2047
  startSpinner(`Turn ${turnNum}/${config.maxTurns} ${dim(`nodes:${nodeCount} edges:${edgeCount}`)}`);
1893
2048
  } else if (toolName === "get_catalog") {
1894
2049
  startSpinner(`Catalog-Check ${dim("(Duplikate vermeiden)")}`);
2050
+ } else if (toolName === "scan_bookmarks") {
2051
+ logLine(cyan("\u{1F516}"), `Browser-Lesezeichen werden gescannt\u2026`);
2052
+ startSpinner(`scan_bookmarks`);
2053
+ } else if (toolName === "ask_user") {
2054
+ const q = (event.input["question"] ?? "").substring(0, 100);
2055
+ logLine(yellow("?"), `${bold("Agent fragt:")} ${q}`);
1895
2056
  } else {
1896
2057
  startSpinner(`${toolName}...`);
1897
2058
  }
@@ -1904,8 +2065,28 @@ function main() {
1904
2065
  break;
1905
2066
  }
1906
2067
  };
2068
+ const onAskUser = async (question, context) => {
2069
+ stopSpinner();
2070
+ w("\n");
2071
+ 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"));
2072
+ w(` ${yellow(bold("?"))} ${bold("Agent fragt:")} ${question}
2073
+ `);
2074
+ if (context) w(` ${dim(context)}
2075
+ `);
2076
+ if (!process.stdin.isTTY) {
2077
+ w(` ${dim("(Kein Terminal \u2014 Agent f\xE4hrt ohne Antwort fort)")}
2078
+
2079
+ `);
2080
+ return "(Kein interaktiver Modus \u2014 bitte ohne diese Information fortfahren)";
2081
+ }
2082
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2083
+ const answer = await new Promise((resolve2) => rl.question(` ${cyan("\u2192")} `, resolve2));
2084
+ rl.close();
2085
+ w("\n");
2086
+ return answer || "(Keine Antwort \u2014 bitte fortfahren)";
2087
+ };
1907
2088
  try {
1908
- await runDiscovery(config, db, sessionId, handleEvent);
2089
+ await runDiscovery(config, db, sessionId, handleEvent, onAskUser);
1909
2090
  } catch (err) {
1910
2091
  stopSpinner();
1911
2092
  w(`
@@ -2141,13 +2322,154 @@ Session: ${session.id}
2141
2322
  }
2142
2323
  db.close();
2143
2324
  });
2325
+ program.command("overview").description("\xDCbersicht aller Cartographies + SOPs").option("--db <path>", "DB-Pfad").action((opts) => {
2326
+ const config = defaultConfig();
2327
+ const db = new CartographyDB(opts.db ?? config.dbPath);
2328
+ const sessions = db.getSessions();
2329
+ const b = bold, d = dim;
2330
+ const w = (s) => process.stdout.write(s);
2331
+ w("\n");
2332
+ w(` ${b("CARTOGRAPHY OVERVIEW")}
2333
+ `);
2334
+ w(d(" \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\u2500\u2500\u2500\u2500\u2500\n"));
2335
+ if (sessions.length === 0) {
2336
+ w(` ${d("Noch keine Sessions. Starte mit:")} ${green("datasynx-cartography discover")}
2337
+
2338
+ `);
2339
+ db.close();
2340
+ return;
2341
+ }
2342
+ let totalNodes = 0, totalEdges = 0, totalSops = 0;
2343
+ for (const s of sessions) {
2344
+ const st = db.getStats(s.id);
2345
+ totalNodes += st.nodes;
2346
+ totalEdges += st.edges;
2347
+ totalSops += db.getSOPs(s.id).length;
2348
+ }
2349
+ w(` ${b(String(sessions.length))} Sessions \xB7 ${b(String(totalNodes))} Nodes \xB7 `);
2350
+ w(`${b(String(totalEdges))} Edges \xB7 ${b(String(totalSops))} SOPs
2351
+
2352
+ `);
2353
+ for (const session of sessions) {
2354
+ const stats = db.getStats(session.id);
2355
+ const nodes = db.getNodes(session.id);
2356
+ const sops = db.getSOPs(session.id);
2357
+ const status = session.completedAt ? green("\u2713") : yellow("\u25CF");
2358
+ const age = session.startedAt.substring(0, 16).replace("T", " ");
2359
+ const sid = cyan(session.id.substring(0, 8));
2360
+ w(` ${status} ${sid} ${b("[" + session.mode + "]")} ${d(age)}
2361
+ `);
2362
+ w(` ${d("Nodes: " + stats.nodes + " Edges: " + stats.edges + " SOPs: " + sops.length)}
2363
+ `);
2364
+ const byType = /* @__PURE__ */ new Map();
2365
+ for (const n of nodes) byType.set(n.type, (byType.get(n.type) ?? 0) + 1);
2366
+ if (byType.size > 0) {
2367
+ const parts = [...byType.entries()].map(([t, c]) => `${t}:${c}`).join(" ");
2368
+ w(` ${d(parts)}
2369
+ `);
2370
+ }
2371
+ const topNodes = nodes.slice(0, 5).map((n) => n.id).join(", ");
2372
+ if (topNodes) w(` ${d("Nodes: " + topNodes + (nodes.length > 5 ? " \u2026" : ""))}
2373
+ `);
2374
+ for (const sop of sops.slice(0, 3)) {
2375
+ w(` ${green("\u25BA")} ${sop.title} ${d("(" + sop.estimatedDuration + ")")}
2376
+ `);
2377
+ }
2378
+ if (sops.length > 3) w(` ${d("\u2026 +" + (sops.length - 3) + " weitere SOPs")}
2379
+ `);
2380
+ w("\n");
2381
+ }
2382
+ db.close();
2383
+ });
2384
+ program.command("chat [session-id]").description("Interaktiver Chat \xFCber die kartographierte Infrastruktur").option("--db <path>", "DB-Pfad").option("--model <m>", "Model", "claude-sonnet-4-5-20250929").action(async (sessionIdArg, opts) => {
2385
+ const config = defaultConfig();
2386
+ const db = new CartographyDB(opts.db ?? config.dbPath);
2387
+ const sessions = db.getSessions();
2388
+ const session = sessionIdArg ? sessions.find((s) => s.id.startsWith(sessionIdArg)) : sessions.filter((s) => s.completedAt).at(-1) ?? sessions.at(-1);
2389
+ if (!session) {
2390
+ process.stderr.write("Keine Session gefunden. F\xFChre zuerst discover aus.\n");
2391
+ db.close();
2392
+ return;
2393
+ }
2394
+ const nodes = db.getNodes(session.id);
2395
+ const edges = db.getEdges(session.id);
2396
+ const sops = db.getSOPs(session.id);
2397
+ const w = (s) => process.stdout.write(s);
2398
+ w("\n");
2399
+ 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\u2500\u2500\u2500\u2500\u2500
2400
+ `));
2401
+ w(` ${bold("CARTOGRAPHY CHAT")} ${dim("Session " + session.id.substring(0, 8))}
2402
+ `);
2403
+ w(` ${dim(String(nodes.length) + " Nodes \xB7 " + edges.length + " Edges \xB7 " + sops.length + " SOPs")}
2404
+ `);
2405
+ 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\u2500\u2500\u2500\u2500\u2500
2406
+ `));
2407
+ w(` ${dim("Frage alles \xFCber deine Infrastruktur. exit = beenden.\n\n")}`);
2408
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
2409
+ const client = new Anthropic();
2410
+ const infraSummary = JSON.stringify({
2411
+ nodes: nodes.map((n) => ({
2412
+ id: n.id,
2413
+ name: n.name,
2414
+ type: n.type,
2415
+ confidence: n.confidence,
2416
+ metadata: n.metadata,
2417
+ tags: n.tags
2418
+ })),
2419
+ edges: edges.map((e) => ({ from: e.sourceId, to: e.targetId, rel: e.relationship, conf: e.confidence })),
2420
+ sops: sops.map((s) => ({ title: s.title, description: s.description, steps: s.steps.length, duration: s.estimatedDuration }))
2421
+ });
2422
+ const systemPrompt = `Du bist ein Infrastruktur-Analyst f\xFCr Cartography.
2423
+ Du hast Zugriff auf die vollst\xE4ndig kartographierte Infrastruktur dieser Session.
2424
+ Beantworte Fragen pr\xE4zise und hilfreich. Nutze die Daten konkret.
2425
+ Du kannst SOPs erkl\xE4ren, Abh\xE4ngigkeiten analysieren, Risiken benennen, Optimierungen vorschlagen.
2426
+
2427
+ INFRASTRUKTUR-SNAPSHOT (${nodes.length} Nodes, ${edges.length} Edges, ${sops.length} SOPs):
2428
+ ${infraSummary.substring(0, 12e3)}`;
2429
+ const history = [];
2430
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2431
+ const ask = () => new Promise((resolve2) => rl.question(` ${cyan(">")} `, resolve2));
2432
+ while (true) {
2433
+ let userInput;
2434
+ try {
2435
+ userInput = await ask();
2436
+ } catch {
2437
+ break;
2438
+ }
2439
+ if (!userInput.trim()) continue;
2440
+ if (["exit", "quit", ":q"].includes(userInput.trim().toLowerCase())) break;
2441
+ history.push({ role: "user", content: userInput });
2442
+ try {
2443
+ const resp = await client.messages.create({
2444
+ model: opts.model,
2445
+ max_tokens: 1024,
2446
+ system: systemPrompt,
2447
+ messages: history
2448
+ });
2449
+ const reply = resp.content.find((b) => b.type === "text")?.text ?? "";
2450
+ history.push({ role: "assistant", content: reply });
2451
+ w("\n");
2452
+ for (const line of reply.split("\n")) {
2453
+ w(` ${line}
2454
+ `);
2455
+ }
2456
+ w("\n");
2457
+ } catch (err) {
2458
+ w(` ${red("\u2717")} Fehler: ${err}
2459
+
2460
+ `);
2461
+ }
2462
+ }
2463
+ rl.close();
2464
+ db.close();
2465
+ w(`
2466
+ ${dim("Chat beendet.")}
2467
+
2468
+ `);
2469
+ });
2144
2470
  program.command("docs").description("Alle Features und Befehle auf einen Blick").action(() => {
2145
2471
  const out = process.stdout.write.bind(process.stdout);
2146
- const b = (s) => `\x1B[1m${s}\x1B[0m`;
2147
- const dim = (s) => `\x1B[2m${s}\x1B[0m`;
2148
- const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
2149
- const green = (s) => `\x1B[32m${s}\x1B[0m`;
2150
- const yellow = (s) => `\x1B[33m${s}\x1B[0m`;
2472
+ const b = bold;
2151
2473
  const line = () => out(dim("\u2500".repeat(60)) + "\n");
2152
2474
  out("\n");
2153
2475
  out(b(" DATASYNX CARTOGRAPHY") + " " + dim("v" + VERSION) + "\n");
@@ -2303,10 +2625,10 @@ Session: ${session.id}
2303
2625
  `);
2304
2626
  const warn = (msg) => out(` \x1B[33m\u26A0\x1B[0m ${msg}
2305
2627
  `);
2306
- const dim = (s) => `\x1B[2m${s}\x1B[0m`;
2628
+ const dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
2307
2629
  let allGood = true;
2308
2630
  out("\n \x1B[1mDatasynx Cartography \u2014 Doctor\x1B[0m\n");
2309
- 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\n"));
2631
+ out(dim2(" \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"));
2310
2632
  const nodeVer = process.versions.node;
2311
2633
  const [major] = nodeVer.split(".").map(Number);
2312
2634
  if ((major ?? 0) >= 18) {
@@ -2317,7 +2639,7 @@ Session: ${session.id}
2317
2639
  }
2318
2640
  try {
2319
2641
  const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
2320
- ok(`Claude CLI ${dim(v)}`);
2642
+ ok(`Claude CLI ${dim2(v)}`);
2321
2643
  } catch {
2322
2644
  err("Claude CLI nicht gefunden \u2014 npm i -g @anthropic-ai/claude-code");
2323
2645
  allGood = false;
@@ -2347,18 +2669,18 @@ Session: ${session.id}
2347
2669
  for (const [name, cmd] of optional) {
2348
2670
  try {
2349
2671
  execSync3(cmd, { stdio: "pipe" });
2350
- ok(`${name} ${dim("(Discovery-Tool)")}`);
2672
+ ok(`${name} ${dim2("(Discovery-Tool)")}`);
2351
2673
  } catch {
2352
- warn(`${name} nicht gefunden ${dim("\u2014 Discovery ohne " + name + " eingeschr\xE4nkt")}`);
2674
+ warn(`${name} nicht gefunden ${dim2("\u2014 Discovery ohne " + name + " eingeschr\xE4nkt")}`);
2353
2675
  }
2354
2676
  }
2355
2677
  const dbDir = join4(home, ".cartography");
2356
2678
  if (existsSync6(dbDir)) {
2357
- ok(`~/.cartography ${dim("(Daten-Verzeichnis vorhanden)")}`);
2679
+ ok(`~/.cartography ${dim2("(Daten-Verzeichnis vorhanden)")}`);
2358
2680
  } else {
2359
- warn("~/.cartography existiert noch nicht " + dim("\u2014 wird beim ersten Start angelegt"));
2681
+ warn("~/.cartography existiert noch nicht " + dim2("\u2014 wird beim ersten Start angelegt"));
2360
2682
  }
2361
- 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\n"));
2683
+ out(dim2(" \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"));
2362
2684
  if (allGood) {
2363
2685
  out(" \x1B[32m\x1B[1mAlle Checks bestanden \u2014 datasynx-cartography discover\x1B[0m\n\n");
2364
2686
  } else {