@datasynx/agentic-ai-cartography 0.2.6 → 0.3.1

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,19 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  EDGE_RELATIONSHIPS,
4
- EVENT_TYPES,
5
- MIN_POLL_INTERVAL_MS,
6
4
  NODE_TYPES,
7
5
  SOPStepSchema,
8
6
  defaultConfig
9
- } from "./chunk-EVJP2FWQ.js";
7
+ } from "./chunk-FFNOC6HF.js";
10
8
  import {
11
9
  scanAllBookmarks,
12
10
  scanAllHistory
13
11
  } from "./chunk-2VIAXA5T.js";
14
- import {
15
- exportAll
16
- } from "./chunk-LNMM7BTH.js";
17
12
 
18
13
  // src/cli.ts
19
14
  import { Command } from "commander";
@@ -54,16 +49,6 @@ function checkPrerequisites() {
54
49
  process.stderr.write("\u2713 Eingeloggt via claude login (Subscription)\n");
55
50
  }
56
51
  }
57
- function checkPollInterval(intervalMs) {
58
- if (intervalMs < MIN_POLL_INTERVAL_MS) {
59
- process.stderr.write(
60
- `\u26A0 Minimum Shadow-Intervall: ${MIN_POLL_INTERVAL_MS / 1e3} Sekunden (Agent SDK Overhead)
61
- `
62
- );
63
- return MIN_POLL_INTERVAL_MS;
64
- }
65
- return intervalMs;
66
- }
67
52
 
68
53
  // src/db.ts
69
54
  import Database from "better-sqlite3";
@@ -76,7 +61,7 @@ PRAGMA busy_timeout = 5000;
76
61
 
77
62
  CREATE TABLE IF NOT EXISTS sessions (
78
63
  id TEXT PRIMARY KEY,
79
- mode TEXT NOT NULL CHECK (mode IN ('discover','shadow')),
64
+ mode TEXT NOT NULL CHECK (mode IN ('discover')),
80
65
  started_at TEXT NOT NULL,
81
66
  completed_at TEXT,
82
67
  config TEXT NOT NULL DEFAULT '{}'
@@ -541,24 +526,6 @@ async function createCartographyTools(db, sessionId, opts = {}) {
541
526
  });
542
527
  return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
543
528
  }),
544
- tool("save_event", "Save an activity event (process/connection observed)", {
545
- eventType: z.enum(EVENT_TYPES),
546
- process: z.string(),
547
- pid: z.number(),
548
- target: z.string().optional(),
549
- targetType: z.enum(NODE_TYPES).optional(),
550
- port: z.number().optional()
551
- }, async (args) => {
552
- db.insertEvent(sessionId, {
553
- eventType: args["eventType"],
554
- process: args["process"],
555
- pid: args["pid"],
556
- target: args["target"] ? stripSensitive(args["target"]) : void 0,
557
- targetType: args["targetType"],
558
- port: args["port"]
559
- });
560
- return { content: [{ type: "text", text: `\u2713 ${args["eventType"]}` }] };
561
- }),
562
529
  tool("get_catalog", "Get the current catalog \u2014 use before save_node to avoid duplicates", {
563
530
  includeEdges: z.boolean().default(true)
564
531
  }, async (args) => {
@@ -574,22 +541,6 @@ async function createCartographyTools(db, sessionId, opts = {}) {
574
541
  }]
575
542
  };
576
543
  }),
577
- tool("manage_task", "Start, end or describe a workflow task", {
578
- action: z.enum(["start", "end", "describe"]),
579
- description: z.string().optional()
580
- }, async (args) => {
581
- const action = args["action"];
582
- if (action === "start") {
583
- const id = db.startTask(sessionId, args["description"]);
584
- return { content: [{ type: "text", text: `\u2713 Task started: ${id}` }] };
585
- }
586
- if (action === "end") {
587
- db.endCurrentTask(sessionId);
588
- return { content: [{ type: "text", text: "\u2713 Task ended" }] };
589
- }
590
- db.updateTaskDescription(sessionId, args["description"]);
591
- return { content: [{ type: "text", text: "\u2713 Description updated" }] };
592
- }),
593
544
  tool("ask_user", "Ask the user a question \u2014 for clarifications, missing context, or consent (e.g. before scanning browser history)", {
594
545
  question: z.string().describe("The question for the user (clear and specific)"),
595
546
  context: z.string().optional().describe("Optional context explaining why this is relevant")
@@ -649,14 +600,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
649
600
  tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
650
601
  deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
651
602
  }, async (args) => {
652
- const { execSync: execSync3 } = await import("child_process");
603
+ const { execSync: execSync2 } = await import("child_process");
653
604
  const { homedir } = await import("os");
654
- const { existsSync: existsSync5 } = await import("fs");
605
+ const { existsSync: existsSync3 } = await import("fs");
655
606
  const deep = args["deep"] ?? false;
656
607
  const HOME = homedir();
657
608
  const run = (cmd) => {
658
609
  try {
659
- return execSync3(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
610
+ return execSync2(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
660
611
  } catch {
661
612
  return "";
662
613
  }
@@ -667,7 +618,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
667
618
  results["MYSQL_DATABASES"] = run('mysql --connect-timeout=3 -e "SHOW DATABASES;" 2>/dev/null') || "(mysql not running or requires auth)";
668
619
  results["MONGODB_DATABASES"] = run(`mongosh --quiet --eval "db.adminCommand({listDatabases:1}).databases.map(d=>d.name).join('\\n')" 2>/dev/null`) || "(mongosh not available)";
669
620
  results["REDIS_INFO"] = run("redis-cli info server 2>/dev/null | head -5") || "(redis-cli not available)";
670
- const appDirs = [`${HOME}/.config`, `${HOME}/.local/share`, `${HOME}/Library/Application Support`, "/var/lib"].filter((d) => existsSync5(d));
621
+ const appDirs = [`${HOME}/.config`, `${HOME}/.local/share`, `${HOME}/Library/Application Support`, "/var/lib"].filter((d) => existsSync3(d));
671
622
  if (appDirs.length > 0) {
672
623
  const findCmds = appDirs.map((d) => `find "${d}" -maxdepth 4 \\( -name "*.sqlite" -o -name "*.sqlite3" -o -name "*.db" \\) 2>/dev/null`).join("; ");
673
624
  results["SQLITE_APP_FILES"] = run(`{ ${findCmds}; } | head -80`) || "(none found)";
@@ -683,12 +634,12 @@ ${v}`).join("\n\n");
683
634
  tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
684
635
  namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
685
636
  }, async (args) => {
686
- const { execSync: execSync3 } = await import("child_process");
637
+ const { execSync: execSync2 } = await import("child_process");
687
638
  const ns = args["namespace"];
688
639
  const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
689
640
  const run = (cmd) => {
690
641
  try {
691
- return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
642
+ return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
692
643
  } catch (e) {
693
644
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
694
645
  }
@@ -712,7 +663,7 @@ ${run(c)}`).join("\n\n");
712
663
  region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
713
664
  profile: z.string().optional().describe("AWS CLI profile")
714
665
  }, async (args) => {
715
- const { execSync: execSync3 } = await import("child_process");
666
+ const { execSync: execSync2 } = await import("child_process");
716
667
  const region = args["region"];
717
668
  const profile = args["profile"];
718
669
  const env = { ...process.env };
@@ -720,7 +671,7 @@ ${run(c)}`).join("\n\n");
720
671
  const pf = profile ? `--profile ${profile}` : "";
721
672
  const run = (cmd) => {
722
673
  try {
723
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
674
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
724
675
  } catch (e) {
725
676
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
726
677
  }
@@ -742,12 +693,12 @@ ${run(c)}`).join("\n\n");
742
693
  tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
743
694
  project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
744
695
  }, async (args) => {
745
- const { execSync: execSync3 } = await import("child_process");
696
+ const { execSync: execSync2 } = await import("child_process");
746
697
  const project = args["project"];
747
698
  const pf = project ? `--project ${project}` : "";
748
699
  const run = (cmd) => {
749
700
  try {
750
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
701
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
751
702
  } catch (e) {
752
703
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
753
704
  }
@@ -771,14 +722,14 @@ ${run(c)}`).join("\n\n");
771
722
  subscription: z.string().optional().describe("Azure Subscription ID"),
772
723
  resourceGroup: z.string().optional().describe("Filter by resource group")
773
724
  }, async (args) => {
774
- const { execSync: execSync3 } = await import("child_process");
725
+ const { execSync: execSync2 } = await import("child_process");
775
726
  const sub = args["subscription"];
776
727
  const rg = args["resourceGroup"];
777
728
  const sf = sub ? `--subscription ${sub}` : "";
778
729
  const rf = rg ? `--resource-group ${rg}` : "";
779
730
  const run = (cmd) => {
780
731
  try {
781
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
732
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
782
733
  } catch (e) {
783
734
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
784
735
  }
@@ -801,11 +752,11 @@ ${run(c)}`).join("\n\n");
801
752
  tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
802
753
  searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
803
754
  }, async (args) => {
804
- const { execSync: execSync3 } = await import("child_process");
755
+ const { execSync: execSync2 } = await import("child_process");
805
756
  const hint = args["searchHint"];
806
757
  const run = (cmd) => {
807
758
  try {
808
- return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
759
+ return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
809
760
  } catch {
810
761
  return "";
811
762
  }
@@ -1182,690 +1133,795 @@ Use ask_user when you need context from the user.`;
1182
1133
  }
1183
1134
  }
1184
1135
  }
1185
- async function runShadowCycle(config, db, sessionId, prevSnapshot, currSnapshot, onOutput) {
1186
- const { query } = await import("@anthropic-ai/claude-code");
1187
- const tools = await createCartographyTools(db, sessionId);
1188
- const prompt = `Analyze the diff between these two system snapshots.
1189
- Find:
1190
- - New/closed TCP connections \u2192 save_event
1191
- - New/terminated processes \u2192 save_event
1192
- - Previously unknown services \u2192 check get_catalog, then save_node
1193
- - Task boundaries (inactivity, tool switches) \u2192 manage_task
1194
- target = host:port ONLY. Be concise and efficient.
1195
-
1196
- === BEFORE ===
1197
- ${prevSnapshot}
1198
-
1199
- === NOW ===
1200
- ${currSnapshot}`;
1201
- for await (const msg of query({
1202
- prompt,
1203
- options: {
1204
- model: config.shadowModel,
1205
- maxTurns: 5,
1206
- mcpServers: { cartography: tools },
1207
- allowedTools: [
1208
- "mcp__cartograph__save_event",
1209
- "mcp__cartograph__save_node",
1210
- "mcp__cartograph__save_edge",
1211
- "mcp__cartograph__get_catalog",
1212
- "mcp__cartograph__manage_task"
1213
- ],
1214
- permissionMode: "bypassPermissions"
1215
- }
1216
- })) {
1217
- if (onOutput) onOutput(msg);
1218
- }
1136
+
1137
+ // src/exporter.ts
1138
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
1139
+ import { join as join2 } from "path";
1140
+ function nodeLayer(type) {
1141
+ if (type === "saas_tool") return "saas";
1142
+ if (["web_service", "api_endpoint"].includes(type)) return "web";
1143
+ if (["database_server", "database", "table", "cache_server"].includes(type)) return "data";
1144
+ if (["message_broker", "queue", "topic"].includes(type)) return "messaging";
1145
+ if (["host", "container", "pod", "k8s_cluster"].includes(type)) return "infra";
1146
+ if (type === "config_file") return "config";
1147
+ return "other";
1219
1148
  }
1220
- async function generateSOPs(db, sessionId) {
1221
- const Anthropic = (await import("@anthropic-ai/sdk")).default;
1222
- const client = new Anthropic();
1223
- const tasks = db.getTasks(sessionId).filter((t) => t.status === "completed");
1224
- if (tasks.length === 0) return 0;
1225
- const clusters = clusterTasks(tasks);
1226
- let generated = 0;
1227
- for (const cluster of clusters) {
1228
- const workflowId = crypto.randomUUID();
1229
- const involved = JSON.parse(cluster[0]?.involvedServices ?? "[]");
1230
- const taskDescriptions = cluster.map((t, i) => `Task ${i + 1}: ${t.description ?? "Unnamed"}
1231
- Steps: ${t.steps}`).join("\n\n");
1232
- const response = await client.messages.create({
1233
- model: "claude-sonnet-4-5-20250929",
1234
- max_tokens: 2048,
1235
- messages: [{
1236
- role: "user",
1237
- content: `Generate a Standard Operating Procedure (SOP) for this recurring workflow.
1238
- Reply ONLY with valid JSON in this format:
1239
- {
1240
- "title": "...",
1241
- "description": "...",
1242
- "steps": [{"order": 1, "instruction": "...", "tool": "...", "target": "...", "notes": "..."}],
1243
- "involvedSystems": ["..."],
1244
- "estimatedDuration": "~N minutes",
1245
- "frequency": "X times daily",
1246
- "confidence": 0.8
1149
+ var LAYER_LABELS = {
1150
+ saas: "\u2601 SaaS Tools",
1151
+ web: "\u{1F310} Web / API",
1152
+ data: "\u{1F5C4} Data Layer",
1153
+ messaging: "\u{1F4E8} Messaging",
1154
+ infra: "\u{1F5A5} Infrastructure",
1155
+ config: "\u{1F4C4} Config",
1156
+ other: "\u2753 Other"
1157
+ };
1158
+ var LAYER_ORDER = ["saas", "web", "data", "messaging", "infra", "config", "other"];
1159
+ var MERMAID_ICONS = {
1160
+ host: "\u{1F5A5}",
1161
+ database_server: "\u{1F5C4}",
1162
+ database: "\u{1F5C4}",
1163
+ table: "\u{1F4CB}",
1164
+ web_service: "\u{1F310}",
1165
+ api_endpoint: "\u{1F50C}",
1166
+ cache_server: "\u26A1",
1167
+ message_broker: "\u{1F4E8}",
1168
+ queue: "\u{1F4EC}",
1169
+ topic: "\u{1F4E2}",
1170
+ container: "\u{1F4E6}",
1171
+ pod: "\u2638",
1172
+ k8s_cluster: "\u2638",
1173
+ config_file: "\u{1F4C4}",
1174
+ saas_tool: "\u2601",
1175
+ unknown: "\u2753"
1176
+ };
1177
+ var EDGE_LABELS = {
1178
+ connects_to: "\u2192",
1179
+ reads_from: "reads",
1180
+ writes_to: "writes",
1181
+ calls: "calls",
1182
+ contains: "contains",
1183
+ depends_on: "depends on"
1184
+ };
1185
+ var MERMAID_CLASSES = {
1186
+ host: "fill:#1e3352,stroke:#4a82c4,color:#cce",
1187
+ database_server: "fill:#1e3352,stroke:#4a82c4,color:#cce",
1188
+ database: "fill:#163352,stroke:#3a8ad4,color:#bdf",
1189
+ table: "fill:#0f2a40,stroke:#2a6090,color:#9bd",
1190
+ web_service: "fill:#1a3a1a,stroke:#3a9a3a,color:#bfb",
1191
+ api_endpoint: "fill:#0f2a0f,stroke:#2a7a2a,color:#9d9",
1192
+ cache_server: "fill:#3a2a0a,stroke:#ca8a0a,color:#fda",
1193
+ message_broker: "fill:#2a1a3a,stroke:#7a3aaa,color:#daf",
1194
+ queue: "fill:#1f1030,stroke:#5a2a8a,color:#caf",
1195
+ topic: "fill:#1f1030,stroke:#5a2a8a,color:#caf",
1196
+ container: "fill:#1a2a3a,stroke:#3a6a9a,color:#acd",
1197
+ pod: "fill:#0f1f2f,stroke:#2a5a8a,color:#8bc",
1198
+ k8s_cluster: "fill:#0a1520,stroke:#1a4a7a,color:#7ab",
1199
+ config_file: "fill:#2a2a1a,stroke:#7a7a2a,color:#ddc",
1200
+ saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
1201
+ unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
1202
+ };
1203
+ function sanitize(id) {
1204
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
1247
1205
  }
1248
-
1249
- Tasks:
1250
- ${taskDescriptions}
1251
-
1252
- Involved services: ${involved.join(", ")}`
1253
- }]
1254
- });
1255
- const text = response.content[0]?.type === "text" ? response.content[0].text : "";
1256
- try {
1257
- const jsonMatch = text.match(/\{[\s\S]*\}/);
1258
- if (!jsonMatch) continue;
1259
- const parsed = JSON.parse(jsonMatch[0]);
1260
- db.insertSOP({ workflowId, ...parsed });
1261
- generated++;
1262
- } catch {
1263
- }
1264
- }
1265
- return generated;
1206
+ function nodeLabel(node) {
1207
+ const icon = MERMAID_ICONS[node.type] ?? "?";
1208
+ const parts = node.id.split(":");
1209
+ const location = parts.length >= 3 ? `${parts[1]}:${parts[2]}` : parts[1] ?? "";
1210
+ const conf = `${Math.round(node.confidence * 100)}%`;
1211
+ const meta = node.metadata;
1212
+ const extras = [];
1213
+ for (const key of ["category", "version", "description"]) {
1214
+ const v = meta[key];
1215
+ if (typeof v === "string" && v.length > 0) {
1216
+ extras.push(v.substring(0, 28));
1217
+ break;
1218
+ }
1219
+ }
1220
+ const locLine = location ? `<br/><small>${location}</small>` : "";
1221
+ const extraLine = extras.length ? `<br/><small>${extras[0]}</small>` : "";
1222
+ return `["${icon} <b>${node.name}</b>${locLine}${extraLine}<br/><small>${node.type} \xB7 ${conf}</small>"]`;
1266
1223
  }
1267
- function clusterTasks(tasks) {
1268
- const clusters = [];
1269
- const assigned = /* @__PURE__ */ new Set();
1270
- for (const task of tasks) {
1271
- if (assigned.has(task.id)) continue;
1272
- const cluster = [task];
1273
- assigned.add(task.id);
1274
- const taskServices = new Set(JSON.parse(task.involvedServices ?? "[]"));
1275
- for (const other of tasks) {
1276
- if (assigned.has(other.id)) continue;
1277
- const otherServices = new Set(JSON.parse(other.involvedServices ?? "[]"));
1278
- const overlap = [...taskServices].filter((s) => otherServices.has(s));
1279
- if (overlap.length > 0) {
1280
- cluster.push(other);
1281
- assigned.add(other.id);
1282
- }
1224
+ function generateTopologyMermaid(nodes, edges) {
1225
+ if (nodes.length === 0) return 'graph TB\n empty["No nodes discovered yet"]';
1226
+ const lines = ["graph TB"];
1227
+ const usedTypes = new Set(nodes.map((n) => n.type));
1228
+ for (const type of usedTypes) {
1229
+ const style = MERMAID_CLASSES[type] ?? MERMAID_CLASSES["unknown"];
1230
+ lines.push(` classDef ${type.replace(/_/g, "")} ${style}`);
1231
+ }
1232
+ lines.push("");
1233
+ const layerMap = /* @__PURE__ */ new Map();
1234
+ for (const node of nodes) {
1235
+ const layer = nodeLayer(node.type);
1236
+ if (!layerMap.has(layer)) layerMap.set(layer, []);
1237
+ layerMap.get(layer).push(node);
1238
+ }
1239
+ for (const layerKey of LAYER_ORDER) {
1240
+ const layerNodes = layerMap.get(layerKey);
1241
+ if (!layerNodes || layerNodes.length === 0) continue;
1242
+ const label = LAYER_LABELS[layerKey] ?? layerKey;
1243
+ lines.push(` subgraph ${layerKey}["${label}"]`);
1244
+ for (const node of layerNodes) {
1245
+ lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
1246
+ }
1247
+ lines.push(" end");
1248
+ lines.push("");
1249
+ }
1250
+ for (const edge of edges) {
1251
+ const src = sanitize(edge.sourceId);
1252
+ const tgt = sanitize(edge.targetId);
1253
+ const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
1254
+ const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
1255
+ lines.push(` ${src} ${arrow} ${tgt}`);
1256
+ }
1257
+ return lines.join("\n");
1258
+ }
1259
+ function generateDependencyMermaid(nodes, edges) {
1260
+ const depEdges = edges.filter(
1261
+ (e) => ["calls", "reads_from", "writes_to", "depends_on"].includes(e.relationship)
1262
+ );
1263
+ if (depEdges.length === 0) return 'graph LR\n empty["No dependency edges found"]';
1264
+ const lines = ["graph LR"];
1265
+ const usedIds = /* @__PURE__ */ new Set();
1266
+ for (const edge of depEdges) {
1267
+ usedIds.add(edge.sourceId);
1268
+ usedIds.add(edge.targetId);
1269
+ }
1270
+ const usedNodes = nodes.filter((n) => usedIds.has(n.id));
1271
+ const usedTypes = new Set(usedNodes.map((n) => n.type));
1272
+ for (const type of usedTypes) {
1273
+ const style = MERMAID_CLASSES[type] ?? MERMAID_CLASSES["unknown"];
1274
+ lines.push(` classDef ${type.replace(/_/g, "")} ${style}`);
1275
+ }
1276
+ lines.push("");
1277
+ for (const node of usedNodes) {
1278
+ lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
1279
+ }
1280
+ lines.push("");
1281
+ for (const edge of depEdges) {
1282
+ const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
1283
+ lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
1284
+ }
1285
+ return lines.join("\n");
1286
+ }
1287
+ function generateWorkflowMermaid(sop) {
1288
+ const lines = ["flowchart TD"];
1289
+ for (const step of sop.steps) {
1290
+ const nodeId = `S${step.order}`;
1291
+ const label = `${step.order}. ${step.instruction.substring(0, 60)}`;
1292
+ lines.push(` ${nodeId}["${label}"]`);
1293
+ if (step.order > 1) {
1294
+ lines.push(` S${step.order - 1} --> ${nodeId}`);
1283
1295
  }
1284
- clusters.push(cluster);
1285
1296
  }
1286
- return clusters;
1297
+ return lines.join("\n");
1298
+ }
1299
+ function exportBackstageYAML(nodes, edges, org) {
1300
+ const owner = org ?? "unknown";
1301
+ const docs = [];
1302
+ for (const node of nodes) {
1303
+ const isComponent = ["web_service", "container", "pod"].includes(node.type);
1304
+ const isAPI = node.type === "api_endpoint";
1305
+ const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
1306
+ const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
1307
+ const doc = [
1308
+ `apiVersion: backstage.io/v1alpha1`,
1309
+ `kind: ${kind}`,
1310
+ `metadata:`,
1311
+ ` name: ${sanitize(node.id)}`,
1312
+ ` annotations:`,
1313
+ ` cartography/discovered-at: "${node.discoveredAt}"`,
1314
+ ` cartography/confidence: "${node.confidence}"`,
1315
+ `spec:`,
1316
+ ` type: ${node.type}`,
1317
+ ` lifecycle: production`,
1318
+ ` owner: ${owner}`,
1319
+ ...deps.length > 0 ? [" dependsOn:", ...deps] : []
1320
+ ].join("\n");
1321
+ docs.push(doc);
1322
+ }
1323
+ return docs.join("\n---\n");
1287
1324
  }
1325
+ function exportJSON(db, sessionId) {
1326
+ const nodes = db.getNodes(sessionId);
1327
+ const edges = db.getEdges(sessionId);
1328
+ const events = db.getEvents(sessionId);
1329
+ const tasks = db.getTasks(sessionId);
1330
+ const sops = db.getSOPs(sessionId);
1331
+ const stats = db.getStats(sessionId);
1332
+ return JSON.stringify({
1333
+ sessionId,
1334
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1335
+ stats,
1336
+ nodes,
1337
+ edges,
1338
+ events,
1339
+ tasks,
1340
+ sops
1341
+ }, null, 2);
1342
+ }
1343
+ function exportHTML(nodes, edges) {
1344
+ const graphData = JSON.stringify({
1345
+ nodes: nodes.map((n) => ({
1346
+ id: n.id,
1347
+ name: n.name,
1348
+ type: n.type,
1349
+ layer: nodeLayer(n.type),
1350
+ confidence: n.confidence,
1351
+ discoveredVia: n.discoveredVia,
1352
+ discoveredAt: n.discoveredAt,
1353
+ tags: n.tags,
1354
+ metadata: n.metadata
1355
+ })),
1356
+ links: edges.map((e) => ({
1357
+ source: e.sourceId,
1358
+ target: e.targetId,
1359
+ relationship: e.relationship,
1360
+ confidence: e.confidence,
1361
+ evidence: e.evidence
1362
+ }))
1363
+ });
1364
+ return `<!DOCTYPE html>
1365
+ <html lang="en">
1366
+ <head>
1367
+ <meta charset="UTF-8">
1368
+ <title>Cartography \u2014 Infrastructure Map</title>
1369
+ <script src="https://d3js.org/d3.v7.min.js"></script>
1370
+ <style>
1371
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1372
+ body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; height: 100vh; }
1288
1373
 
1289
- // src/cli.ts
1290
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
1291
- import { resolve } from "path";
1292
- import { createInterface } from "readline";
1374
+ /* \u2500\u2500 Left node panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1375
+ #node-panel {
1376
+ width: 220px; min-width: 220px; height: 100vh; overflow: hidden;
1377
+ background: #0d1117; border-right: 1px solid #1b2028;
1378
+ display: flex; flex-direction: column;
1379
+ }
1380
+ #node-panel-header {
1381
+ padding: 10px 12px 8px; border-bottom: 1px solid #1b2028;
1382
+ font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.6px;
1383
+ }
1384
+ #node-search {
1385
+ width: calc(100% - 16px); margin: 8px; padding: 5px 8px;
1386
+ background: #161b22; border: 1px solid #30363d; border-radius: 5px;
1387
+ color: #e6edf3; font-size: 11px; font-family: inherit; outline: none;
1388
+ }
1389
+ #node-search:focus { border-color: #58a6ff; }
1390
+ #node-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
1391
+ .node-list-item {
1392
+ padding: 5px 12px; cursor: pointer; font-size: 11px;
1393
+ display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent;
1394
+ }
1395
+ .node-list-item:hover { background: #161b22; }
1396
+ .node-list-item.active { background: #1a2436; border-left-color: #58a6ff; }
1397
+ .node-list-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
1398
+ .node-list-name { color: #c9d1d9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
1399
+ .node-list-type { color: #484f58; font-size: 9px; flex-shrink: 0; }
1293
1400
 
1294
- // src/daemon.ts
1295
- import { execSync as execSync2, spawn } from "child_process";
1296
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, unlinkSync as unlinkSync2 } from "fs";
1297
-
1298
- // src/ipc.ts
1299
- import net from "net";
1300
- import { EventEmitter } from "events";
1301
- import { chmodSync, existsSync as existsSync2, unlinkSync } from "fs";
1302
- var IPCServer = class extends EventEmitter {
1303
- server = null;
1304
- clients = /* @__PURE__ */ new Set();
1305
- start(socketPath) {
1306
- this.server = net.createServer((socket) => {
1307
- this.clients.add(socket);
1308
- this.emit("client-connect", socket);
1309
- let buf = "";
1310
- socket.on("data", (chunk) => {
1311
- buf += chunk.toString();
1312
- const lines = buf.split("\n");
1313
- buf = lines.pop() ?? "";
1314
- for (const line of lines) {
1315
- if (!line.trim()) continue;
1316
- try {
1317
- const msg = JSON.parse(line);
1318
- this.emit("message", msg, socket);
1319
- } catch {
1320
- }
1321
- }
1322
- });
1323
- socket.on("close", () => {
1324
- this.clients.delete(socket);
1325
- this.emit("client-disconnect", socket);
1326
- });
1327
- socket.on("error", () => {
1328
- this.clients.delete(socket);
1329
- });
1330
- });
1331
- this.server.listen(socketPath, () => {
1332
- try {
1333
- chmodSync(socketPath, 384);
1334
- } catch {
1335
- }
1336
- });
1337
- }
1338
- broadcast(msg) {
1339
- const line = JSON.stringify(msg) + "\n";
1340
- for (const socket of this.clients) {
1341
- try {
1342
- socket.write(line);
1343
- } catch {
1344
- this.clients.delete(socket);
1345
- }
1346
- }
1347
- }
1348
- hasClients() {
1349
- return this.clients.size > 0;
1350
- }
1351
- stop() {
1352
- for (const socket of this.clients) {
1353
- socket.destroy();
1354
- }
1355
- this.clients.clear();
1356
- this.server?.close();
1357
- this.server = null;
1358
- }
1401
+ /* \u2500\u2500 Center graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1402
+ #graph { flex: 1; height: 100vh; position: relative; }
1403
+ svg { width: 100%; height: 100%; }
1404
+ .hull { opacity: 0.12; stroke-width: 1.5; stroke-opacity: 0.25; }
1405
+ .hull-label { font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; fill-opacity: 0.5; pointer-events: none; }
1406
+ .link { stroke-opacity: 0.4; }
1407
+ .link-label { font-size: 8px; fill: #6e7681; pointer-events: none; opacity: 0; }
1408
+ .node-hex { stroke-width: 1.8; cursor: pointer; transition: opacity 0.15s; }
1409
+ .node-hex:hover { filter: brightness(1.3); stroke-width: 3; }
1410
+ .node-hex.selected { stroke-width: 3.5; filter: brightness(1.5); }
1411
+ .node-label { font-size: 10px; fill: #c9d1d9; pointer-events: none; opacity: 0; }
1412
+
1413
+ /* \u2500\u2500 Right sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1414
+ #sidebar {
1415
+ width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
1416
+ background: #0d1117; border-left: 1px solid #1b2028;
1417
+ padding: 16px; font-size: 12px; line-height: 1.6;
1418
+ }
1419
+ #sidebar h2 { margin: 0 0 8px; font-size: 14px; color: #58a6ff; }
1420
+ #sidebar .meta-table { width: 100%; border-collapse: collapse; }
1421
+ #sidebar .meta-table td { padding: 3px 6px; border-bottom: 1px solid #161b22; vertical-align: top; }
1422
+ #sidebar .meta-table td:first-child { color: #6e7681; white-space: nowrap; width: 90px; }
1423
+ #sidebar .tag { display: inline-block; background: #161b22; border-radius: 3px; padding: 1px 5px; margin: 1px; font-size: 10px; }
1424
+ #sidebar .conf-bar { height: 5px; border-radius: 3px; background: #161b22; margin-top: 3px; }
1425
+ #sidebar .conf-fill { height: 100%; border-radius: 3px; }
1426
+ #sidebar .edges-list { margin-top: 12px; }
1427
+ #sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #161b22; color: #6e7681; font-size: 11px; }
1428
+ #sidebar .edge-item span { color: #c9d1d9; }
1429
+ #sidebar .action-row { display: flex; gap: 6px; margin-top: 14px; }
1430
+ .btn-delete {
1431
+ flex: 1; padding: 6px 10px; background: transparent; border: 1px solid #6e191d;
1432
+ color: #f85149; border-radius: 5px; font-size: 11px; font-family: inherit;
1433
+ cursor: pointer; text-align: center;
1434
+ }
1435
+ .btn-delete:hover { background: #3d0c0c; }
1436
+ .hint { color: #3d434b; font-size: 11px; margin-top: 8px; }
1437
+
1438
+ /* \u2500\u2500 HUD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1439
+ #hud { position: absolute; top: 10px; left: 10px; background: rgba(10,14,20,0.88);
1440
+ padding: 10px 14px; border-radius: 8px; font-size: 12px; border: 1px solid #1b2028; pointer-events: none; }
1441
+ #hud strong { color: #58a6ff; }
1442
+ #hud .stats { color: #6e7681; }
1443
+ #hud .zoom-level { color: #3d434b; font-size: 10px; margin-top: 2px; }
1444
+
1445
+ /* \u2500\u2500 Toolbar (filters + JGF export) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1446
+ #toolbar { position: absolute; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 4px; pointer-events: auto; align-items: center; }
1447
+ .filter-btn {
1448
+ background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
1449
+ color: #c9d1d9; padding: 4px 10px; font-size: 11px; cursor: pointer;
1450
+ font-family: inherit; display: flex; align-items: center; gap: 5px;
1451
+ }
1452
+ .filter-btn:hover { border-color: #30363d; }
1453
+ .filter-btn.off { opacity: 0.35; }
1454
+ .filter-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
1455
+ .export-btn {
1456
+ background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
1457
+ color: #58a6ff; padding: 4px 12px; font-size: 11px; cursor: pointer;
1458
+ font-family: inherit;
1459
+ }
1460
+ .export-btn:hover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
1461
+ </style>
1462
+ </head>
1463
+ <body>
1464
+
1465
+ <!-- Left: node list panel -->
1466
+ <div id="node-panel">
1467
+ <div id="node-panel-header">Nodes (${nodes.length})</div>
1468
+ <input id="node-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false">
1469
+ <div id="node-list"></div>
1470
+ </div>
1471
+
1472
+ <!-- Center: graph -->
1473
+ <div id="graph">
1474
+ <div id="hud">
1475
+ <strong>Cartography</strong> &nbsp;
1476
+ <span class="stats" id="hud-stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
1477
+ <span class="zoom-level">Scroll = zoom \xB7 Drag = pan \xB7 Click = details</span>
1478
+ </div>
1479
+ <div id="toolbar"></div>
1480
+ <svg></svg>
1481
+ </div>
1482
+
1483
+ <!-- Right: detail sidebar -->
1484
+ <div id="sidebar">
1485
+ <h2>Infrastructure Map</h2>
1486
+ <p class="hint">Click a node to view details.</p>
1487
+ </div>
1488
+
1489
+ <script>
1490
+ const data = ${graphData};
1491
+
1492
+ // \u2500\u2500 Color palette per node type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1493
+ const TYPE_COLORS = {
1494
+ host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
1495
+ web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
1496
+ message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
1497
+ container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
1498
+ config_file: '#adb5bd', saas_tool: '#c084fc', table: '#f97316', unknown: '#6c757d',
1359
1499
  };
1360
- var IPCClient = class extends EventEmitter {
1361
- socket = null;
1362
- connect(socketPath) {
1363
- return new Promise((resolve2, reject) => {
1364
- const socket = net.createConnection(socketPath, () => {
1365
- resolve2();
1366
- });
1367
- socket.on("error", (err) => {
1368
- reject(err);
1369
- });
1370
- let buf = "";
1371
- socket.on("data", (chunk) => {
1372
- buf += chunk.toString();
1373
- const lines = buf.split("\n");
1374
- buf = lines.pop() ?? "";
1375
- for (const line of lines) {
1376
- if (!line.trim()) continue;
1377
- try {
1378
- const msg = JSON.parse(line);
1379
- this.emit("message", msg);
1380
- } catch {
1381
- }
1382
- }
1383
- });
1384
- socket.on("close", () => {
1385
- this.emit("disconnect");
1386
- });
1387
- this.socket = socket;
1388
- });
1389
- }
1390
- send(msg) {
1391
- if (!this.socket) return;
1392
- try {
1393
- this.socket.write(JSON.stringify(msg) + "\n");
1394
- } catch {
1395
- }
1396
- }
1397
- disconnect() {
1398
- this.socket?.destroy();
1399
- this.socket = null;
1400
- }
1500
+
1501
+ const LAYER_COLORS = {
1502
+ saas: '#c084fc', web: '#6bcb77', data: '#ff6b6b',
1503
+ messaging: '#c77dff', infra: '#4a9eff', config: '#adb5bd', other: '#6c757d',
1401
1504
  };
1402
- function cleanStaleSocket(socketPath) {
1403
- if (existsSync2(socketPath)) {
1404
- try {
1405
- unlinkSync(socketPath);
1406
- } catch {
1407
- }
1408
- }
1505
+ const LAYER_NAMES = {
1506
+ saas: 'SaaS Tools', web: 'Web / API', data: 'Data Layer',
1507
+ messaging: 'Messaging', infra: 'Infrastructure', config: 'Config', other: 'Other',
1508
+ };
1509
+
1510
+ // \u2500\u2500 Hexagon path \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1511
+ const HEX_SIZE = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
1512
+ function hexSize(d) { return HEX_SIZE[d.type] || HEX_SIZE.default; }
1513
+ function hexPath(size) {
1514
+ const pts = [];
1515
+ for (let i = 0; i < 6; i++) {
1516
+ const angle = (Math.PI / 3) * i - Math.PI / 6;
1517
+ pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
1518
+ }
1519
+ return 'M' + pts.map(p => p.join(',')).join('L') + 'Z';
1409
1520
  }
1410
1521
 
1411
- // src/notify.ts
1412
- import notifier from "node-notifier";
1413
- var NotificationService = class {
1414
- constructor(enabled) {
1415
- this.enabled = enabled;
1416
- }
1417
- nodeDiscovered(nodeId, via) {
1418
- this.send(`\u{1F4CD} Node entdeckt: ${nodeId}`, `Via: ${via}`);
1419
- }
1420
- workflowDetected(count, desc) {
1421
- this.send(`\u{1F504} ${count} Workflow(s) erkannt`, desc);
1422
- }
1423
- taskBoundary(gapMinutes) {
1424
- this.send("\u23F8 Task-Grenze erkannt", `${gapMinutes} Minuten Inaktivit\xE4t`);
1425
- }
1426
- send(title, message) {
1427
- if (!this.enabled) return;
1428
- try {
1429
- notifier.notify({ title, message, sound: false });
1430
- } catch {
1431
- }
1432
- }
1433
- };
1522
+ // \u2500\u2500 Left panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1523
+ const nodeListEl = document.getElementById('node-list');
1524
+ const nodeSearchEl = document.getElementById('node-search');
1525
+ let selectedNodeId = null;
1434
1526
 
1435
- // src/daemon.ts
1436
- function takeSnapshot(config) {
1437
- const run = (cmd) => {
1438
- try {
1439
- return execSync2(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }).toString();
1440
- } catch {
1441
- return `(${cmd}: not available)`;
1442
- }
1443
- };
1444
- const ss = run('ss -tnp 2>/dev/null || ss -tn 2>/dev/null || echo "ss not available"');
1445
- const ps = run("ps aux --sort=-start_time 2>/dev/null | head -50");
1446
- let win = "";
1447
- if (config.trackWindowFocus) {
1448
- try {
1449
- win = execSync2("xdotool getactivewindow getwindowname 2>/dev/null", {
1450
- stdio: ["pipe", "pipe", "pipe"],
1451
- timeout: 2e3
1452
- }).toString().trim();
1453
- } catch {
1454
- win = "";
1455
- }
1527
+ function buildNodeList(filter) {
1528
+ const q = (filter || '').toLowerCase();
1529
+ nodeListEl.innerHTML = '';
1530
+ const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name));
1531
+ for (const d of sorted) {
1532
+ if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
1533
+ const item = document.createElement('div');
1534
+ item.className = 'node-list-item' + (d.id === selectedNodeId ? ' active' : '');
1535
+ item.dataset.id = d.id;
1536
+ const color = TYPE_COLORS[d.type] || '#aaa';
1537
+ item.innerHTML = \`<span class="node-list-dot" style="background:\${color}"></span>
1538
+ <span class="node-list-name" title="\${d.id}">\${d.name}</span>
1539
+ <span class="node-list-type">\${d.type.replace(/_/g,' ')}</span>\`;
1540
+ item.onclick = () => { selectNode(d); focusNode(d); };
1541
+ nodeListEl.appendChild(item);
1456
1542
  }
1457
- return `=== TCP ===
1458
- ${ss}
1459
- === PS ===
1460
- ${ps}
1461
- === Window ===
1462
- ${win}`;
1463
1543
  }
1464
- var ShadowDaemon = class {
1465
- constructor(config, db, ipc, notify) {
1466
- this.config = config;
1467
- this.db = db;
1468
- this.ipc = ipc;
1469
- this.notify = notify;
1470
- }
1471
- running = false;
1472
- paused = false;
1473
- prevSnapshot = "";
1474
- cyclesRun = 0;
1475
- cyclesSkipped = 0;
1476
- lastTaskCount = 0;
1477
- sessionId = "";
1478
- async run() {
1479
- this.running = true;
1480
- this.sessionId = this.db.createSession("shadow", this.config);
1481
- process.on("SIGTERM", () => this.stop());
1482
- process.on("SIGINT", () => this.stop());
1483
- process.on("SIGUSR1", () => this.pause());
1484
- process.on("SIGUSR2", () => this.resume());
1485
- this.ipc.on("message", (msg) => {
1486
- switch (msg.type) {
1487
- case "command":
1488
- if (msg.command === "pause") this.pause();
1489
- else if (msg.command === "resume") this.resume();
1490
- else if (msg.command === "stop") this.stop();
1491
- else if (msg.command === "status") {
1492
- this.ipc.broadcast({ type: "status", data: this.getStatus() });
1493
- } else if (msg.command === "new-task") {
1494
- this.db.startTask(this.sessionId);
1495
- this.ipc.broadcast({ type: "info", message: "Task gestartet" });
1496
- } else if (msg.command === "end-task") {
1497
- this.db.endCurrentTask(this.sessionId);
1498
- this.ipc.broadcast({ type: "info", message: "Task beendet" });
1499
- }
1500
- break;
1501
- case "task-description":
1502
- this.db.updateTaskDescription(this.sessionId, msg.description);
1503
- break;
1504
- case "prompt-response":
1505
- if (msg.id.startsWith("sop-suggest:")) {
1506
- const taskId = msg.id.replace("sop-suggest:", "");
1507
- if (msg.answer === "ja" || msg.answer === "yes" || msg.answer === "Ja, als SOP speichern") {
1508
- this.db.markTaskAsSOPCandidate(taskId);
1509
- this.ipc.broadcast({ type: "info", message: `Task als SOP-Kandidat markiert` });
1510
- }
1511
- }
1512
- break;
1513
- }
1514
- });
1515
- while (this.running) {
1516
- if (this.paused) {
1517
- this.ipc.broadcast({ type: "status", data: this.getStatus() });
1518
- await sleep(this.config.pollIntervalMs);
1519
- continue;
1520
- }
1521
- const snapshot = takeSnapshot(this.config);
1522
- if (snapshot !== this.prevSnapshot) {
1523
- try {
1524
- await runShadowCycle(
1525
- this.config,
1526
- this.db,
1527
- this.sessionId,
1528
- this.prevSnapshot,
1529
- snapshot,
1530
- (msg) => {
1531
- if (this.ipc.hasClients()) {
1532
- this.ipc.broadcast({ type: "agent-output", text: JSON.stringify(msg) });
1533
- }
1534
- }
1535
- );
1536
- this.cyclesRun++;
1537
- } catch (err) {
1538
- process.stderr.write(`\u26A0 Cycle error: ${err}
1539
- `);
1540
- }
1541
- this.prevSnapshot = snapshot;
1542
- this.checkForCompletedTasks();
1543
- } else {
1544
- this.cyclesSkipped++;
1545
- }
1546
- this.ipc.broadcast({ type: "status", data: this.getStatus() });
1547
- if (!this.ipc.hasClients()) {
1548
- const stats = this.db.getStats(this.sessionId);
1549
- if (stats.events > 0 && this.cyclesRun % 10 === 0) {
1550
- this.notify.workflowDetected(stats.tasks, `${stats.events} events so far`);
1551
- }
1552
- }
1553
- await sleep(this.config.pollIntervalMs);
1554
- }
1555
- this.db.endSession(this.sessionId);
1556
- this.ipc.stop();
1557
- cleanup(this.config);
1558
- return this.sessionId;
1559
- }
1560
- pause() {
1561
- if (!this.paused) {
1562
- this.paused = true;
1563
- this.ipc.broadcast({ type: "info", message: "\u23F8 Shadow-Daemon pausiert" });
1564
- }
1565
- }
1566
- resume() {
1567
- if (this.paused) {
1568
- this.paused = false;
1569
- this.ipc.broadcast({ type: "info", message: "\u25B6 Shadow-Daemon fortgesetzt" });
1570
- }
1571
- }
1572
- stop() {
1573
- this.running = false;
1574
- }
1575
- getSessionId() {
1576
- return this.sessionId;
1577
- }
1578
- checkForCompletedTasks() {
1579
- const tasks = this.db.getTasks(this.sessionId);
1580
- const completedCount = tasks.filter((t) => t.status === "completed").length;
1581
- if (completedCount > this.lastTaskCount) {
1582
- const newlyCompleted = tasks.filter((t) => t.status === "completed" && !t.isSOPCandidate).slice(-1);
1583
- for (const task of newlyCompleted) {
1584
- const desc = task.description ?? `Task ${task.id.substring(0, 8)}`;
1585
- if (this.ipc.hasClients()) {
1586
- this.ipc.broadcast({
1587
- type: "prompt",
1588
- id: `sop-suggest:${task.id}`,
1589
- prompt: {
1590
- kind: "task-boundary",
1591
- context: { taskId: task.id, description: desc },
1592
- options: ["Ja, als SOP speichern", "Nein, \xFCberspringen"],
1593
- defaultAnswer: "Ja, als SOP speichern",
1594
- timeoutMs: 3e4,
1595
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1596
- }
1597
- });
1598
- } else {
1599
- this.db.markTaskAsSOPCandidate(task.id);
1600
- }
1601
- }
1602
- this.lastTaskCount = completedCount;
1603
- }
1604
- }
1605
- getStatus() {
1606
- const stats = this.db.getStats(this.sessionId);
1607
- const sops = this.db.getSOPs(this.sessionId);
1608
- return {
1609
- pid: process.pid,
1610
- uptime: process.uptime(),
1611
- nodeCount: stats.nodes,
1612
- eventCount: stats.events,
1613
- taskCount: stats.tasks,
1614
- sopCount: sops.length,
1615
- pendingPrompts: 0,
1616
- autoSave: this.config.autoSaveNodes,
1617
- mode: this.config.shadowMode,
1618
- agentActive: false,
1619
- paused: this.paused,
1620
- cyclesRun: this.cyclesRun,
1621
- cyclesSkipped: this.cyclesSkipped
1622
- };
1623
- }
1624
- };
1625
- function forkDaemon(config) {
1626
- const child = spawn(
1627
- process.execPath,
1628
- [process.argv[1] ?? "datasynx-cartography", "shadow", "start", "--foreground", "--daemon-child"],
1629
- {
1630
- detached: true,
1631
- stdio: "ignore",
1632
- env: {
1633
- ...process.env,
1634
- CARTOGRAPHYY_DAEMON: "1",
1635
- CARTOGRAPHYY_CONFIG: JSON.stringify(config)
1636
- }
1637
- }
1544
+
1545
+ nodeSearchEl.addEventListener('input', e => buildNodeList(e.target.value));
1546
+
1547
+ // \u2500\u2500 Sidebar detail view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1548
+ const sidebar = document.getElementById('sidebar');
1549
+
1550
+ function selectNode(d) {
1551
+ selectedNodeId = d.id;
1552
+ buildNodeList(nodeSearchEl.value);
1553
+ showNode(d);
1554
+ // highlight hex
1555
+ d3.selectAll('.node-hex').classed('selected', nd => nd.id === d.id);
1556
+ }
1557
+
1558
+ function showNode(d) {
1559
+ const c = TYPE_COLORS[d.type] || '#aaa';
1560
+ const confPct = Math.round(d.confidence * 100);
1561
+ const tags = (d.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
1562
+ const metaRows = Object.entries(d.metadata || {})
1563
+ .filter(([,v]) => v !== null && v !== undefined && String(v).length > 0)
1564
+ .map(([k,v]) => \`<tr><td>\${k}</td><td>\${JSON.stringify(v)}</td></tr>\`)
1565
+ .join('');
1566
+ const related = data.links.filter(l =>
1567
+ (l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id
1638
1568
  );
1639
- child.unref();
1640
- const pid = child.pid;
1641
- if (!pid) throw new Error("Failed to fork daemon");
1642
- writeFileSync(config.pidFile, String(pid), "utf8");
1643
- return pid;
1569
+ const edgeItems = related.map(l => {
1570
+ const isOut = (l.source.id||l.source) === d.id;
1571
+ const other = isOut ? (l.target.id||l.target) : (l.source.id||l.source);
1572
+ return \`<div class="edge-item">\${isOut ? '\u2192' : '\u2190'} <span>\${other}</span> <small>[\${l.relationship}]</small></div>\`;
1573
+ }).join('');
1574
+
1575
+ sidebar.innerHTML = \`
1576
+ <h2>\${d.name}</h2>
1577
+ <table class="meta-table">
1578
+ <tr><td>ID</td><td style="font-size:10px;word-break:break-all">\${d.id}</td></tr>
1579
+ <tr><td>Type</td><td><span style="color:\${c}">\${d.type}</span></td></tr>
1580
+ <tr><td>Layer</td><td>\${d.layer}</td></tr>
1581
+ <tr><td>Confidence</td><td>
1582
+ \${confPct}%
1583
+ <div class="conf-bar"><div class="conf-fill" style="width:\${confPct}%;background:\${c}"></div></div>
1584
+ </td></tr>
1585
+ <tr><td>Discovered via</td><td>\${d.discoveredVia || '\u2014'}</td></tr>
1586
+ <tr><td>Timestamp</td><td>\${d.discoveredAt ? d.discoveredAt.substring(0,19).replace('T',' ') : '\u2014'}</td></tr>
1587
+ \${tags ? '<tr><td>Tags</td><td>'+tags+'</td></tr>' : ''}
1588
+ \${metaRows}
1589
+ </table>
1590
+ \${related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>'+edgeItems+'</div>' : ''}
1591
+ <div class="action-row">
1592
+ <button class="btn-delete" onclick="deleteNode('\${d.id}')">\u{1F5D1} Delete node</button>
1593
+ </div>
1594
+ \`;
1644
1595
  }
1645
- function isDaemonRunning(pidFile) {
1646
- if (!existsSync3(pidFile)) return { running: false };
1647
- try {
1648
- const pid = parseInt(readFileSync2(pidFile, "utf8").trim(), 10);
1649
- if (isNaN(pid)) return { running: false };
1650
- process.kill(pid, 0);
1651
- return { running: true, pid };
1652
- } catch {
1653
- try {
1654
- unlinkSync2(pidFile);
1655
- } catch {
1656
- }
1657
- return { running: false };
1658
- }
1596
+
1597
+ // \u2500\u2500 Delete node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1598
+ function deleteNode(id) {
1599
+ const idx = data.nodes.findIndex(n => n.id === id);
1600
+ if (idx === -1) return;
1601
+ data.nodes.splice(idx, 1);
1602
+ data.links = data.links.filter(l =>
1603
+ (l.source.id || l.source) !== id && (l.target.id || l.target) !== id
1604
+ );
1605
+ selectedNodeId = null;
1606
+ sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
1607
+ document.getElementById('hud-stats').textContent =
1608
+ data.nodes.length + ' nodes \xB7 ' + data.links.length + ' edges';
1609
+ rebuildGraph();
1610
+ buildNodeList(nodeSearchEl.value);
1659
1611
  }
1660
- function stopDaemon(pidFile) {
1661
- const { running, pid } = isDaemonRunning(pidFile);
1662
- if (!running || !pid) return false;
1663
- try {
1664
- process.kill(pid, "SIGTERM");
1665
- try {
1666
- unlinkSync2(pidFile);
1667
- } catch {
1612
+
1613
+ // \u2500\u2500 SVG setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1614
+ const svgEl = d3.select('svg');
1615
+ const graphDiv = document.getElementById('graph');
1616
+ const W = () => graphDiv.clientWidth;
1617
+ const H = () => graphDiv.clientHeight;
1618
+ const g = svgEl.append('g');
1619
+
1620
+ svgEl.append('defs').append('marker')
1621
+ .attr('id', 'arrow').attr('viewBox', '0 0 10 6')
1622
+ .attr('refX', 10).attr('refY', 3)
1623
+ .attr('markerWidth', 8).attr('markerHeight', 6)
1624
+ .attr('orient', 'auto')
1625
+ .append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
1626
+
1627
+ let currentZoom = 1;
1628
+ const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', e => {
1629
+ g.attr('transform', e.transform);
1630
+ currentZoom = e.transform.k;
1631
+ updateLOD(currentZoom);
1632
+ });
1633
+ svgEl.call(zoomBehavior);
1634
+
1635
+ // \u2500\u2500 Layer filter state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1636
+ const layers = [...new Set(data.nodes.map(d => d.layer))];
1637
+ const layerVisible = {};
1638
+ layers.forEach(l => layerVisible[l] = true);
1639
+
1640
+ const toolbarEl = document.getElementById('toolbar');
1641
+
1642
+ // Filter buttons
1643
+ layers.forEach(layer => {
1644
+ const btn = document.createElement('button');
1645
+ btn.className = 'filter-btn';
1646
+ btn.innerHTML = \`<span class="filter-dot" style="background:\${LAYER_COLORS[layer]||'#666'}"></span>\${LAYER_NAMES[layer]||layer}\`;
1647
+ btn.onclick = () => {
1648
+ layerVisible[layer] = !layerVisible[layer];
1649
+ btn.classList.toggle('off', !layerVisible[layer]);
1650
+ updateVisibility();
1651
+ };
1652
+ toolbarEl.appendChild(btn);
1653
+ });
1654
+
1655
+ // JGF export button
1656
+ const jgfBtn = document.createElement('button');
1657
+ jgfBtn.className = 'export-btn';
1658
+ jgfBtn.textContent = '\u2193 JGF';
1659
+ jgfBtn.title = 'Export JSON Graph Format';
1660
+ jgfBtn.onclick = exportJGF;
1661
+ toolbarEl.appendChild(jgfBtn);
1662
+
1663
+ // \u2500\u2500 JGF export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1664
+ function exportJGF() {
1665
+ const jgf = {
1666
+ graph: {
1667
+ directed: true,
1668
+ type: 'cartography',
1669
+ label: 'Infrastructure Map',
1670
+ metadata: { exportedAt: new Date().toISOString() },
1671
+ nodes: Object.fromEntries(data.nodes.map(n => [n.id, {
1672
+ label: n.name,
1673
+ metadata: { type: n.type, layer: n.layer, confidence: n.confidence,
1674
+ discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt,
1675
+ tags: n.tags, ...n.metadata }
1676
+ }])),
1677
+ edges: data.links.map(l => ({
1678
+ source: l.source.id || l.source,
1679
+ target: l.target.id || l.target,
1680
+ relation: l.relationship,
1681
+ metadata: { confidence: l.confidence, evidence: l.evidence }
1682
+ })),
1668
1683
  }
1669
- return true;
1670
- } catch {
1671
- return false;
1672
- }
1673
- }
1674
- function pauseDaemon(pidFile) {
1675
- const { running, pid } = isDaemonRunning(pidFile);
1676
- if (!running || !pid) return false;
1677
- try {
1678
- process.kill(pid, "SIGUSR1");
1679
- return true;
1680
- } catch {
1681
- return false;
1682
- }
1683
- }
1684
- function resumeDaemon(pidFile) {
1685
- const { running, pid } = isDaemonRunning(pidFile);
1686
- if (!running || !pid) return false;
1687
- try {
1688
- process.kill(pid, "SIGUSR2");
1689
- return true;
1690
- } catch {
1691
- return false;
1692
- }
1693
- }
1694
- function cleanup(config) {
1695
- try {
1696
- unlinkSync2(config.socketPath);
1697
- } catch {
1698
- }
1699
- try {
1700
- unlinkSync2(config.pidFile);
1701
- } catch {
1702
- }
1703
- }
1704
- async function startDaemonProcess(config) {
1705
- cleanStaleSocket(config.socketPath);
1706
- const db = new CartographyDB(config.dbPath);
1707
- const ipc = new IPCServer();
1708
- const notify = new NotificationService(config.enableNotifications);
1709
- ipc.start(config.socketPath);
1710
- const daemon = new ShadowDaemon(config, db, ipc, notify);
1711
- await daemon.run();
1712
- db.close();
1684
+ };
1685
+ const blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
1686
+ const url = URL.createObjectURL(blob);
1687
+ const a = document.createElement('a');
1688
+ a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
1689
+ URL.revokeObjectURL(url);
1713
1690
  }
1714
- function sleep(ms) {
1715
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1691
+
1692
+ // \u2500\u2500 Cluster force \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1693
+ function clusterForce(alpha) {
1694
+ const centroids = {};
1695
+ const counts = {};
1696
+ data.nodes.forEach(d => {
1697
+ if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
1698
+ centroids[d.layer].x += d.x || 0;
1699
+ centroids[d.layer].y += d.y || 0;
1700
+ counts[d.layer]++;
1701
+ });
1702
+ for (const l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
1703
+ const strength = alpha * 0.15;
1704
+ data.nodes.forEach(d => {
1705
+ const c = centroids[d.layer];
1706
+ if (c) { d.vx += (c.x - d.x) * strength; d.vy += (c.y - d.y) * strength; }
1707
+ });
1716
1708
  }
1717
1709
 
1718
- // src/client.ts
1719
- var ForegroundClient = class {
1720
- async run(config) {
1721
- process.stderr.write("\u{1F441} Datasynx Cartography Shadow (foreground) gestartet\n");
1722
- process.stderr.write(` Intervall: ${config.pollIntervalMs / 1e3}s | Modell: ${config.shadowModel}
1723
- `);
1724
- process.stderr.write(" Ctrl+C zum Beenden\n\n");
1725
- await startDaemonProcess({ ...config, shadowMode: "foreground" });
1726
- }
1727
- };
1728
- var AttachClient = class {
1729
- isPaused = false;
1730
- async attach(socketPath) {
1731
- const client = new IPCClient();
1732
- try {
1733
- await client.connect(socketPath);
1734
- } catch {
1735
- process.stderr.write(`\u274C Kann nicht an Daemon ankoppeln: ${socketPath}
1736
- `);
1737
- process.stderr.write(" Ist der Daemon gestartet? datasynx-cartography shadow status\n");
1738
- process.exitCode = 1;
1710
+ // \u2500\u2500 Hull group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1711
+ const hullGroup = g.append('g').attr('class', 'hulls');
1712
+ const hullPaths = {};
1713
+ const hullLabels = {};
1714
+ layers.forEach(layer => {
1715
+ hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
1716
+ .attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
1717
+ hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
1718
+ .attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
1719
+ });
1720
+
1721
+ function updateHulls() {
1722
+ layers.forEach(layer => {
1723
+ if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
1724
+ const pts = data.nodes.filter(d => d.layer === layer && layerVisible[d.layer]).map(d => [d.x, d.y]);
1725
+ if (pts.length < 3) {
1726
+ hullPaths[layer].attr('d', null);
1727
+ if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
1728
+ else hullLabels[layer].attr('x', -9999);
1739
1729
  return;
1740
1730
  }
1741
- process.stderr.write("\u{1F4E1} Verbunden mit Shadow-Daemon\n");
1742
- process.stderr.write(" [T] Neuer Task [S] Status [P] Pause/Resume [D] Trennen [Q] Stoppen\n\n");
1743
- if (process.stdin.isTTY) {
1744
- process.stdin.setRawMode(true);
1745
- }
1746
- process.stdin.resume();
1747
- process.stdin.setEncoding("utf8");
1748
- process.stdin.on("data", (key) => {
1749
- const k = key.toLowerCase();
1750
- if (k === "t") {
1751
- process.stdout.write("\nTask-Beschreibung: ");
1752
- process.stdin.once("data", (desc) => {
1753
- client.send({ type: "task-description", description: desc.trim() });
1754
- client.send({ type: "command", command: "new-task" });
1755
- process.stdout.write(`
1756
- \u2713 Neuer Task gestartet: ${desc.trim()}
1757
- `);
1758
- });
1759
- return;
1760
- }
1761
- if (k === "s") {
1762
- client.send({ type: "command", command: "status" });
1763
- return;
1764
- }
1765
- if (k === "p") {
1766
- if (this.isPaused) {
1767
- client.send({ type: "command", command: "resume" });
1768
- process.stderr.write("\n\u25B6 Resume gesendet\n");
1769
- } else {
1770
- client.send({ type: "command", command: "pause" });
1771
- process.stderr.write("\n\u23F8 Pause gesendet\n");
1772
- }
1773
- this.isPaused = !this.isPaused;
1774
- return;
1775
- }
1776
- if (k === "d" || k === "") {
1777
- process.stderr.write("\n\u{1F4E1} Getrennt. Daemon l\xE4uft weiter.\n");
1778
- client.disconnect();
1779
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1780
- process.stdin.pause();
1781
- return;
1782
- }
1783
- if (k === "q") {
1784
- client.send({ type: "command", command: "stop" });
1785
- process.stderr.write("\n\u{1F6D1} Daemon wird gestoppt...\n");
1786
- setTimeout(() => {
1787
- client.disconnect();
1788
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1789
- process.stdin.pause();
1790
- }, 1e3);
1791
- return;
1792
- }
1793
- });
1794
- client.on("message", (msg) => {
1795
- switch (msg.type) {
1796
- case "status":
1797
- this.isPaused = msg.data.paused;
1798
- renderStatus(msg.data);
1799
- break;
1800
- case "event":
1801
- process.stdout.write(
1802
- ` [${new Date(msg.data.timestamp).toLocaleTimeString()}] ${msg.data.eventType} ${msg.data.process}` + (msg.data.target ? ` \u2192 ${msg.data.target}` : "") + "\n"
1803
- );
1804
- break;
1805
- case "agent-output":
1806
- if (msg.text) process.stdout.write(` \u{1F916} ${msg.text}
1807
- `);
1808
- break;
1809
- case "info":
1810
- process.stdout.write(` \u2139 ${msg.message}
1811
- `);
1812
- break;
1813
- case "prompt":
1814
- renderPrompt(msg.id, msg.prompt.kind, msg.prompt.context, msg.prompt.options, (answer) => {
1815
- client.send({ type: "prompt-response", id: msg.id, answer });
1816
- });
1817
- break;
1818
- }
1819
- });
1820
- client.on("disconnect", () => {
1821
- process.stderr.write("\n\u26A0 Verbindung zum Daemon verloren\n");
1822
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1823
- process.stdin.pause();
1731
+ const hull = d3.polygonHull(pts);
1732
+ if (!hull) { hullPaths[layer].attr('d', null); return; }
1733
+ const cx = d3.mean(hull, p => p[0]);
1734
+ const cy = d3.mean(hull, p => p[1]);
1735
+ const padded = hull.map(p => {
1736
+ const dx = p[0] - cx, dy = p[1] - cy;
1737
+ const len = Math.sqrt(dx*dx + dy*dy) || 1;
1738
+ return [p[0] + dx/len * 40, p[1] + dy/len * 40];
1824
1739
  });
1825
- }
1826
- };
1827
- function renderStatus(status) {
1828
- const state = status.paused ? "\x1B[33m\u23F8 PAUSED\x1B[0m" : "\x1B[32m\u25CF RUNNING\x1B[0m";
1829
- process.stdout.write(
1830
- `
1831
- \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
1832
- ${state} PID: ${status.pid} | Uptime: ${Math.round(status.uptime)}s
1833
- Nodes: ${status.nodeCount} | Events: ${status.eventCount} | Tasks: ${status.taskCount} | SOPs: ${status.sopCount}
1834
- Cycles: ${status.cyclesRun} run, ${status.cyclesSkipped} skipped
1835
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1836
- `
1740
+ hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
1741
+ hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, p => Math.abs(p[1] - cy)) - 30);
1742
+ });
1743
+ }
1744
+
1745
+ // \u2500\u2500 Graph rendering (rebuildable after delete) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1746
+ let linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
1747
+ const linkGroup = g.append('g');
1748
+ const nodeGroup = g.append('g');
1749
+
1750
+ function focusNode(d) {
1751
+ if (!d.x || !d.y) return;
1752
+ const w = W(), h = H();
1753
+ svgEl.transition().duration(500).call(
1754
+ zoomBehavior.transform,
1755
+ d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
1837
1756
  );
1838
1757
  }
1839
- function renderPrompt(id, kind, context, options, callback) {
1840
- if (id.startsWith("sop-suggest:")) {
1841
- const desc = context["description"] ?? "Unbenannter Task";
1842
- process.stdout.write(`
1843
- \u{1F4CB} Task abgeschlossen: "${desc}"
1844
- `);
1845
- process.stdout.write(` Als SOP speichern?
1846
- `);
1847
- options.forEach((opt, i) => process.stdout.write(` [${i + 1}] ${opt}
1848
- `));
1849
- process.stdout.write(" \u2192 ");
1850
- process.stdin.once("data", (data) => {
1851
- const idx = parseInt(data.trim(), 10) - 1;
1852
- callback(options[idx] ?? options[0] ?? "");
1758
+
1759
+ function rebuildGraph() {
1760
+ if (sim) sim.stop();
1761
+
1762
+ // Links
1763
+ linkSel = linkGroup.selectAll('line').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
1764
+ linkSel.exit().remove();
1765
+ const linkEnter = linkSel.enter().append('line').attr('class', 'link');
1766
+ linkSel = linkEnter.merge(linkSel)
1767
+ .attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
1768
+ .attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
1769
+ .attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
1770
+ .attr('marker-end', 'url(#arrow)');
1771
+ linkSel.select('title').remove();
1772
+ linkSel.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
1773
+ \${d.evidence||''}\`);
1774
+
1775
+ // Link labels
1776
+ linkLabelSel = linkGroup.selectAll('text').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
1777
+ linkLabelSel.exit().remove();
1778
+ linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel)
1779
+ .text(d => d.relationship);
1780
+
1781
+ // Nodes
1782
+ nodeSel = nodeGroup.selectAll('g').data(data.nodes, d => d.id);
1783
+ nodeSel.exit().remove();
1784
+ const nodeEnter = nodeSel.enter().append('g')
1785
+ .call(d3.drag()
1786
+ .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
1787
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
1788
+ .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
1789
+ )
1790
+ .on('click', (e, d) => { e.stopPropagation(); selectNode(d); });
1791
+ nodeEnter.append('path').attr('class', 'node-hex');
1792
+ nodeEnter.append('title');
1793
+ nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
1794
+
1795
+ nodeSel = nodeEnter.merge(nodeSel);
1796
+ nodeSel.select('.node-hex')
1797
+ .attr('d', d => hexPath(hexSize(d)))
1798
+ .attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
1799
+ .attr('stroke', d => { const c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
1800
+ .attr('fill-opacity', d => 0.6 + d.confidence * 0.4)
1801
+ .classed('selected', d => d.id === selectedNodeId);
1802
+ nodeSel.select('title').text(d => \`\${d.name} (\${d.type})
1803
+ conf: \${Math.round(d.confidence*100)}%\`);
1804
+ nodeLabelSel = nodeSel.select('.node-label')
1805
+ .attr('dy', d => hexSize(d) + 13)
1806
+ .text(d => d.name.length > 20 ? d.name.substring(0, 18) + '\u2026' : d.name);
1807
+
1808
+ // Simulation
1809
+ sim = d3.forceSimulation(data.nodes)
1810
+ .force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
1811
+ .force('charge', d3.forceManyBody().strength(-280))
1812
+ .force('center', d3.forceCenter(W() / 2, H() / 2))
1813
+ .force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
1814
+ .force('cluster', clusterForce)
1815
+ .on('tick', () => {
1816
+ updateHulls();
1817
+ linkSel.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
1818
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
1819
+ linkLabelSel.attr('x', d => (d.source.x + d.target.x) / 2)
1820
+ .attr('y', d => (d.source.y + d.target.y) / 2 - 4);
1821
+ nodeSel.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
1853
1822
  });
1854
- return;
1823
+ }
1824
+
1825
+ // \u2500\u2500 LOD & visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1826
+ function updateLOD(k) {
1827
+ if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
1828
+ if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
1829
+ d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
1830
+ }
1831
+
1832
+ function updateVisibility() {
1833
+ if (!nodeSel) return;
1834
+ nodeSel.style('display', d => layerVisible[d.layer] ? null : 'none');
1835
+ linkSel.style('display', d => {
1836
+ const s = data.nodes.find(n => n.id === (d.source.id||d.source));
1837
+ const t = data.nodes.find(n => n.id === (d.target.id||d.target));
1838
+ return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
1839
+ });
1840
+ linkLabelSel.style('display', d => {
1841
+ const s = data.nodes.find(n => n.id === (d.source.id||d.source));
1842
+ const t = data.nodes.find(n => n.id === (d.target.id||d.target));
1843
+ return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
1844
+ });
1845
+ }
1846
+
1847
+ // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1848
+ rebuildGraph();
1849
+ buildNodeList();
1850
+ updateLOD(1);
1851
+
1852
+ svgEl.on('click', () => {
1853
+ selectedNodeId = null;
1854
+ d3.selectAll('.node-hex').classed('selected', false);
1855
+ buildNodeList(nodeSearchEl.value);
1856
+ sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
1857
+ });
1858
+ </script>
1859
+ </body>
1860
+ </html>`;
1861
+ }
1862
+ function exportSOPMarkdown(sop) {
1863
+ const lines = [
1864
+ `# ${sop.title}`,
1865
+ "",
1866
+ `**Description:** ${sop.description}`,
1867
+ `**Systems:** ${sop.involvedSystems.join(", ")}`,
1868
+ `**Duration:** ${sop.estimatedDuration}`,
1869
+ `**Frequency:** ${sop.frequency}`,
1870
+ `**Confidence:** ${sop.confidence.toFixed(2)}`,
1871
+ "",
1872
+ "## Steps",
1873
+ ""
1874
+ ];
1875
+ for (const step of sop.steps) {
1876
+ lines.push(`${step.order}. **${step.tool}**${step.target ? ` \u2192 \`${step.target}\`` : ""}`);
1877
+ lines.push(` ${step.instruction}`);
1878
+ if (step.notes) lines.push(` _${step.notes}_`);
1879
+ lines.push("");
1855
1880
  }
1856
- process.stdout.write(`
1857
- \u2753 ${kind}
1881
+ return lines.join("\n");
1882
+ }
1883
+ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
1884
+ mkdirSync2(outputDir, { recursive: true });
1885
+ mkdirSync2(join2(outputDir, "sops"), { recursive: true });
1886
+ mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
1887
+ const nodes = db.getNodes(sessionId);
1888
+ const edges = db.getEdges(sessionId);
1889
+ if (formats.includes("mermaid")) {
1890
+ writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
1891
+ writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
1892
+ process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
1893
+ }
1894
+ if (formats.includes("json")) {
1895
+ writeFileSync(join2(outputDir, "catalog.json"), exportJSON(db, sessionId));
1896
+ process.stderr.write("\u2713 catalog.json\n");
1897
+ }
1898
+ if (formats.includes("yaml")) {
1899
+ writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
1900
+ process.stderr.write("\u2713 catalog-info.yaml\n");
1901
+ }
1902
+ if (formats.includes("html")) {
1903
+ writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
1904
+ process.stderr.write("\u2713 topology.html\n");
1905
+ }
1906
+ if (formats.includes("sops")) {
1907
+ const sops = db.getSOPs(sessionId);
1908
+ for (const sop of sops) {
1909
+ const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
1910
+ writeFileSync(join2(outputDir, "sops", filename), exportSOPMarkdown(sop));
1911
+ const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
1912
+ writeFileSync(join2(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
1913
+ }
1914
+ if (sops.length > 0) {
1915
+ process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
1858
1916
  `);
1859
- options.forEach((opt, i) => process.stdout.write(` [${i + 1}] ${opt}
1860
- `));
1861
- process.stdout.write("Antwort: ");
1862
- process.stdin.once("data", (data) => {
1863
- const idx = parseInt(data.trim(), 10) - 1;
1864
- callback(options[idx] ?? options[0] ?? "");
1865
- });
1917
+ }
1918
+ }
1866
1919
  }
1867
1920
 
1868
1921
  // src/cli.ts
1922
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1923
+ import { resolve } from "path";
1924
+ import { createInterface } from "readline";
1869
1925
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
1870
1926
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1871
1927
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -1873,16 +1929,7 @@ var green = (s) => `\x1B[32m${s}\x1B[0m`;
1873
1929
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
1874
1930
  var magenta = (s) => `\x1B[35m${s}\x1B[0m`;
1875
1931
  var red = (s) => `\x1B[31m${s}\x1B[0m`;
1876
- if (process.env.CARTOGRAPHYY_DAEMON === "1") {
1877
- const config = JSON.parse(process.env.CARTOGRAPHYY_CONFIG ?? "{}");
1878
- startDaemonProcess(config).catch((err) => {
1879
- process.stderr.write(`Daemon fatal: ${err}
1880
- `);
1881
- process.exitCode = 1;
1882
- });
1883
- } else {
1884
- main();
1885
- }
1932
+ main();
1886
1933
  function main() {
1887
1934
  const program = new Command();
1888
1935
  const CMD = "datasynx-cartography";
@@ -1891,7 +1938,6 @@ function main() {
1891
1938
  program.command("discover").description("Scan and map your infrastructure").option("--entry <hosts...>", "Entry points", ["localhost"]).option("--depth <n>", "Max crawl depth", "8").option("--max-turns <n>", "Max agent turns", "50").option("--model <m>", "Agent model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organization name (for Backstage)").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--db <path>", "DB path").option("-v, --verbose", "Show agent reasoning", false).action(async (opts) => {
1892
1939
  checkPrerequisites();
1893
1940
  const config = defaultConfig({
1894
- mode: "discover",
1895
1941
  entryPoints: opts.entry,
1896
1942
  maxDepth: parseInt(opts.depth, 10),
1897
1943
  maxTurns: parseInt(opts.maxTurns, 10),
@@ -2102,13 +2148,13 @@ function main() {
2102
2148
  const htmlPath = resolve(config.outputDir, "topology.html");
2103
2149
  const topoPath = resolve(config.outputDir, "topology.mermaid");
2104
2150
  w("\n");
2105
- if (existsSync4(htmlPath)) {
2151
+ if (existsSync2(htmlPath)) {
2106
2152
  w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
2107
2153
  `);
2108
2154
  }
2109
- if (existsSync4(topoPath)) {
2155
+ if (existsSync2(topoPath)) {
2110
2156
  try {
2111
- const code = readFileSync3(topoPath, "utf8");
2157
+ const code = readFileSync2(topoPath, "utf8");
2112
2158
  const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
2113
2159
  w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
2114
2160
  `);
@@ -2156,7 +2202,7 @@ function main() {
2156
2202
  `);
2157
2203
  w("\n");
2158
2204
  exportAll(db, sessionId, config.outputDir);
2159
- if (existsSync4(htmlPath)) {
2205
+ if (existsSync2(htmlPath)) {
2160
2206
  w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("topology.html updated"))}
2161
2207
  `);
2162
2208
  }
@@ -2165,172 +2211,6 @@ function main() {
2165
2211
  }
2166
2212
  db.close();
2167
2213
  });
2168
- const shadow = program.command("shadow").description("Manage the shadow daemon");
2169
- shadow.command("start").description("Start the shadow daemon").option("--interval <ms>", "Poll interval in ms", "30000").option("--inactivity <ms>", "Task boundary gap in ms", "300000").option("--track-windows", "Track window focus (requires xdotool)", false).option("--auto-save", "Save nodes without prompting", false).option("--no-notifications", "Disable desktop notifications").option("--model <m>", "Analysis model", "claude-haiku-4-5-20251001").option("--foreground", "Run in foreground (no daemon fork)", false).option("--db <path>", "DB path").option("--daemon-child", "Internal: marks this as a daemon child process").action(async (opts) => {
2170
- checkPrerequisites();
2171
- const intervalMs = checkPollInterval(parseInt(opts.interval, 10));
2172
- const config = defaultConfig({
2173
- mode: "shadow",
2174
- shadowMode: opts.foreground ? "foreground" : "daemon",
2175
- pollIntervalMs: intervalMs,
2176
- inactivityTimeoutMs: parseInt(opts.inactivity, 10),
2177
- trackWindowFocus: opts.trackWindows,
2178
- autoSaveNodes: opts.autoSave,
2179
- enableNotifications: opts.notifications !== false,
2180
- shadowModel: opts.model,
2181
- ...opts.db ? { dbPath: opts.db } : {}
2182
- });
2183
- const { running } = isDaemonRunning(config.pidFile);
2184
- if (running) {
2185
- process.stderr.write("\u274C Shadow daemon is already running. Use: datasynx-cartography shadow status\n");
2186
- process.exitCode = 1;
2187
- return;
2188
- }
2189
- if (opts.foreground) {
2190
- const client = new ForegroundClient();
2191
- await client.run(config);
2192
- } else {
2193
- const pid = forkDaemon(config);
2194
- process.stderr.write(`\u{1F441} Shadow daemon started (PID ${pid})
2195
- `);
2196
- process.stderr.write(` Interval: ${intervalMs / 1e3}s | Model: ${config.shadowModel}
2197
- `);
2198
- process.stderr.write(" datasynx-cartography shadow attach \u2014 attach to live events\n");
2199
- process.stderr.write(" datasynx-cartography shadow stop \u2014 stop daemon\n\n");
2200
- }
2201
- });
2202
- shadow.command("stop").description("Stop shadow daemon + SOP review").option("-o, --output <dir>", "Output directory for SOPs + dashboard", "./datasynx-output").option("--no-review", "Skip SOP review").action(async (opts) => {
2203
- const config = defaultConfig({ outputDir: opts.output });
2204
- const stopped = stopDaemon(config.pidFile);
2205
- if (!stopped) {
2206
- process.stderr.write("\u26A0 No running shadow daemon found\n");
2207
- return;
2208
- }
2209
- process.stderr.write("\u2713 Shadow daemon stopped\n");
2210
- if (opts.review === false) return;
2211
- await new Promise((r) => setTimeout(r, 500));
2212
- const db = new CartographyDB(config.dbPath);
2213
- const session = db.getLatestSession("shadow");
2214
- if (!session) {
2215
- db.close();
2216
- return;
2217
- }
2218
- const stats = db.getStats(session.id);
2219
- const w = (s) => process.stderr.write(s);
2220
- w("\n");
2221
- 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"));
2222
- w(bold(" Shadow Session Review\n"));
2223
- w(dim(` Session: ${session.id}
2224
- `));
2225
- w(dim(` Nodes: ${stats.nodes} | Events: ${stats.events} | Tasks: ${stats.tasks}
2226
- `));
2227
- 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"));
2228
- w("\n");
2229
- if (stats.tasks > 0) {
2230
- try {
2231
- w(" Generating SOPs...\n");
2232
- const count = await generateSOPs(db, session.id);
2233
- w(` ${green("\u2713")} ${count} SOPs generated
2234
-
2235
- `);
2236
- } catch (err) {
2237
- w(` ${red("\u2717")} SOP generation failed: ${err}
2238
-
2239
- `);
2240
- }
2241
- }
2242
- const { exportSOPMarkdown, exportSOPDashboard } = await import("./exporter-PWVD7Y6T.js");
2243
- const sops = db.getSOPs(session.id);
2244
- if (sops.length > 0) {
2245
- w(bold(" SOPs for review:\n\n"));
2246
- for (const sop of sops) {
2247
- const md = exportSOPMarkdown(sop);
2248
- for (const line of md.split("\n")) {
2249
- process.stdout.write(` ${line}
2250
- `);
2251
- }
2252
- process.stdout.write("\n");
2253
- 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"));
2254
- }
2255
- const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("fs");
2256
- const { join: join2, resolve: resolvePath } = await import("path");
2257
- mkdirSync2(config.outputDir, { recursive: true });
2258
- mkdirSync2(join2(config.outputDir, "sops"), { recursive: true });
2259
- for (const sop of sops) {
2260
- const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
2261
- writeFileSync2(join2(config.outputDir, "sops", filename), exportSOPMarkdown(sop));
2262
- }
2263
- const allSOPs = db.getAllSOPs();
2264
- const dashboardHtml = exportSOPDashboard(allSOPs);
2265
- const dashboardPath = join2(config.outputDir, "sop-dashboard.html");
2266
- writeFileSync2(dashboardPath, dashboardHtml);
2267
- const absPath = resolvePath(dashboardPath);
2268
- w(` ${green("\u2713")} ${sops.length} SOP markdown files written
2269
- `);
2270
- w(` ${green("\u2713")} SOP Dashboard: ${cyan(`file://${absPath}`)}
2271
- `);
2272
- w("\n");
2273
- w(dim(` Open in browser: ${bold(`file://${absPath}`)}
2274
- `));
2275
- w("\n");
2276
- } else {
2277
- w(dim(" No SOPs in this session.\n\n"));
2278
- }
2279
- db.close();
2280
- });
2281
- shadow.command("pause").description("Pause the shadow daemon").action(() => {
2282
- const config = defaultConfig();
2283
- const paused = pauseDaemon(config.pidFile);
2284
- if (paused) {
2285
- process.stderr.write("\u23F8 Shadow daemon paused\n");
2286
- } else {
2287
- process.stderr.write("\u26A0 No running shadow daemon found\n");
2288
- }
2289
- });
2290
- shadow.command("resume").description("Resume the shadow daemon").action(() => {
2291
- const config = defaultConfig();
2292
- const resumed = resumeDaemon(config.pidFile);
2293
- if (resumed) {
2294
- process.stderr.write("\u25B6 Shadow daemon resumed\n");
2295
- } else {
2296
- process.stderr.write("\u26A0 No running shadow daemon found\n");
2297
- }
2298
- });
2299
- shadow.command("status").description("Show shadow daemon status").action(() => {
2300
- const config = defaultConfig();
2301
- const { running, pid } = isDaemonRunning(config.pidFile);
2302
- if (running) {
2303
- process.stdout.write(`\u2713 Shadow daemon running (PID ${pid})
2304
- `);
2305
- process.stdout.write(` Socket: ${config.socketPath}
2306
- `);
2307
- } else {
2308
- process.stdout.write("\u2717 Shadow daemon stopped\n");
2309
- }
2310
- });
2311
- shadow.command("attach").description("Attach to a running shadow daemon").action(async () => {
2312
- const config = defaultConfig();
2313
- const client = new AttachClient();
2314
- await client.attach(config.socketPath);
2315
- });
2316
- program.command("sops [session-id]").description("Generate SOPs from observed workflows").action(async (sessionId) => {
2317
- checkPrerequisites();
2318
- const config = defaultConfig();
2319
- const db = new CartographyDB(config.dbPath);
2320
- const session = sessionId ? db.getSession(sessionId) : db.getLatestSession("shadow");
2321
- if (!session) {
2322
- process.stderr.write("\u274C No shadow session found. Run: datasynx-cartography shadow start\n");
2323
- db.close();
2324
- process.exitCode = 1;
2325
- return;
2326
- }
2327
- process.stderr.write(`\u{1F504} Generating SOPs from session ${session.id}...
2328
- `);
2329
- const count = await generateSOPs(db, session.id);
2330
- process.stderr.write(`\u2713 ${count} SOPs generated
2331
- `);
2332
- db.close();
2333
- });
2334
2214
  program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,sops").action((sessionId, opts) => {
2335
2215
  const config = defaultConfig({ outputDir: opts.output });
2336
2216
  const db = new CartographyDB(config.dbPath);
@@ -2594,46 +2474,7 @@ ${infraSummary.substring(0, 12e3)}`;
2594
2474
  out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
2595
2475
  out("\n");
2596
2476
  line();
2597
- out(b(cyan(" SHADOW DAEMON\n")));
2598
- out("\n");
2599
- out(` ${green("datasynx-cartography shadow start")}
2600
- `);
2601
- out(` Starts a background daemon that takes a system snapshot every 30s
2602
- `);
2603
- out(` (ss + ps). Claude Haiku is called only when something changes.
2604
- `);
2605
- out("\n");
2606
- out(dim(" Options:\n"));
2607
- out(dim(" --interval <ms> Poll interval (default: 30000, min: 15000)\n"));
2608
- out(dim(" --inactivity <ms> Task boundary gap (default: 300000 = 5 min)\n"));
2609
- out(dim(" --model <m> Analysis model (default: claude-haiku-4-5-...)\n"));
2610
- out(dim(" --track-windows Track window focus (requires xdotool)\n"));
2611
- out(dim(" --auto-save Save nodes without prompting\n"));
2612
- out(dim(" --no-notifications Disable desktop notifications\n"));
2613
- out(dim(" --foreground Run in terminal (no daemon fork)\n"));
2614
- out("\n");
2615
- out(` ${green("datasynx-cartography shadow stop")} ${dim("Stop via SIGTERM")}
2616
- `);
2617
- out(` ${green("datasynx-cartography shadow status")} ${dim("Show PID + socket path")}
2618
- `);
2619
- out(` ${green("datasynx-cartography shadow attach")} ${dim("Live events in terminal, hotkeys: [T] [S] [D] [Q]")}
2620
- `);
2621
- out("\n");
2622
- out(dim(" Hotkeys in attach mode:\n"));
2623
- out(dim(" [T] Start new task (with description)\n"));
2624
- out(dim(" [S] Show status dump (nodes, events, tasks, cycles)\n"));
2625
- out(dim(" [D] Detach \u2014 daemon keeps running\n"));
2626
- out(dim(" [Q] Stop daemon and quit\n"));
2627
- out("\n");
2628
- line();
2629
2477
  out(b(cyan(" ANALYSIS & EXPORT\n")));
2630
- out("\n");
2631
- out(` ${green("datasynx-cartography sops [session-id]")}
2632
- `);
2633
- out(` Clusters completed tasks and generates SOPs via Claude Sonnet.
2634
- `);
2635
- out(` Uses the Anthropic Messages API (no agent loop, one request per cluster).
2636
- `);
2637
2478
  out("\n");
2638
2479
  out(` ${green("datasynx-cartography export [session-id]")}
2639
2480
  `);
@@ -2651,17 +2492,7 @@ ${infraSummary.substring(0, 12e3)}`;
2651
2492
  out(yellow(" Mode Model Interval per Hour per 8h Day\n"));
2652
2493
  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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
2653
2494
  out(` Discovery Sonnet one-shot $0.15\u20130.50 one-shot
2654
- `);
2655
- out(` Shadow Haiku 30s $0.12\u20130.36 $0.96\u20132.88
2656
- `);
2657
- out(` Shadow Haiku 60s $0.06\u20130.18 $0.48\u20131.44
2658
- `);
2659
- out(` Shadow (quiet) Haiku 30s ~$0.02 ~$0.16
2660
- `);
2661
- out(` SOP gen Sonnet one-shot $0.01\u20130.03 one-shot
2662
2495
  `);
2663
- out("\n");
2664
- out(dim(' * "quiet" = diff-check skips 90%+ cycles when system is unchanged\n'));
2665
2496
  out("\n");
2666
2497
  line();
2667
2498
  out(b(cyan(" ARCHITECTURE\n")));
@@ -2669,19 +2500,12 @@ ${infraSummary.substring(0, 12e3)}`;
2669
2500
  out(dim(" CLI (Commander)\n"));
2670
2501
  out(dim(" \u2514\u2500\u2500 Preflight: Claude CLI check + API key + interval validation\n"));
2671
2502
  out(dim(" \u2514\u2500\u2500 Agent Orchestrator (agent.ts)\n"));
2672
- out(dim(" \u251C\u2500\u2500 runDiscovery() \u2192 Claude Sonnet + Bash + MCP Tools\n"));
2673
- out(dim(" \u251C\u2500\u2500 runShadowCycle() \u2192 Claude Haiku + MCP Tools only (no Bash)\n"));
2674
- out(dim(" \u2514\u2500\u2500 generateSOPs() \u2192 Anthropic Messages API (no agent loop)\n"));
2503
+ out(dim(" \u2514\u2500\u2500 runDiscovery() \u2192 Claude Sonnet + Bash + MCP Tools\n"));
2675
2504
  out(dim(" \u2514\u2500\u2500 Custom MCP Tools (tools.ts)\n"));
2676
- out(dim(" save_node, save_edge, save_event,\n"));
2505
+ out(dim(" save_node, save_edge,\n"));
2677
2506
  out(dim(" scan_bookmarks, scan_browser_history,\n"));
2678
2507
  out(dim(" scan_installed_apps, scan_local_databases\n"));
2679
2508
  out(dim(" \u2514\u2500\u2500 CartographyDB (SQLite WAL)\n"));
2680
- out(dim(" Shadow Daemon (daemon.ts)\n"));
2681
- out(dim(" \u251C\u2500\u2500 takeSnapshot() \u2192 ss + ps [no Claude!]\n"));
2682
- out(dim(" \u251C\u2500\u2500 Diff-Check \u2192 calls Claude only on changes\n"));
2683
- out(dim(" \u251C\u2500\u2500 IPC Server (Unix socket ~/.cartography/daemon.sock)\n"));
2684
- out(dim(" \u2514\u2500\u2500 NotificationService (desktop alerts when no client attached)\n"));
2685
2509
  out("\n");
2686
2510
  line();
2687
2511
  out(b(cyan(" SETUP\n")));
@@ -2695,11 +2519,8 @@ ${infraSummary.substring(0, 12e3)}`;
2695
2519
  out("\n");
2696
2520
  out(dim(" # 3. Go\n"));
2697
2521
  out(" datasynx-cartography discover\n");
2698
- out(" datasynx-cartography shadow start\n");
2699
2522
  out("\n");
2700
- out(dim(" Data: ~/.cartography/cartography.db\n"));
2701
- out(dim(" Socket: ~/.cartography/daemon.sock\n"));
2702
- out(dim(" PID: ~/.cartography/daemon.pid\n"));
2523
+ out(dim(" Data: ~/.cartography/cartography.db\n"));
2703
2524
  out("\n");
2704
2525
  });
2705
2526
  program.command("bookmarks").description("View all browser bookmarks (Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Firefox)").action(async () => {
@@ -2742,7 +2563,7 @@ ${infraSummary.substring(0, 12e3)}`;
2742
2563
  if (opts.file) {
2743
2564
  let raw;
2744
2565
  try {
2745
- raw = JSON.parse(readFileSync3(resolve(opts.file), "utf8"));
2566
+ raw = JSON.parse(readFileSync2(resolve(opts.file), "utf8"));
2746
2567
  } catch (e) {
2747
2568
  w(red(`
2748
2569
  \u2717 Could not read file: ${e}
@@ -2790,7 +2611,7 @@ ${infraSummary.substring(0, 12e3)}`;
2790
2611
  `);
2791
2612
  return;
2792
2613
  }
2793
- const { NODE_TYPES: NODE_TYPES2 } = await import("./types-NKF6BRMZ.js");
2614
+ const { NODE_TYPES: NODE_TYPES2 } = await import("./types-ROE3Z6HY.js");
2794
2615
  if (!process.stdin.isTTY) {
2795
2616
  w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
2796
2617
  process.exitCode = 1;
@@ -2865,9 +2686,9 @@ ${infraSummary.substring(0, 12e3)}`;
2865
2686
  `);
2866
2687
  });
2867
2688
  program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
2868
- const { execSync: execSync3 } = await import("child_process");
2869
- const { existsSync: existsSync5, readFileSync: readFileSync4 } = await import("fs");
2870
- const { join: join2 } = await import("path");
2689
+ const { execSync: execSync2 } = await import("child_process");
2690
+ const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
2691
+ const { join: join3 } = await import("path");
2871
2692
  const out = (s) => process.stdout.write(s);
2872
2693
  const ok = (msg) => out(` \x1B[32m\u2713\x1B[0m ${msg}
2873
2694
  `);
@@ -2888,7 +2709,7 @@ ${infraSummary.substring(0, 12e3)}`;
2888
2709
  allGood = false;
2889
2710
  }
2890
2711
  try {
2891
- const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
2712
+ const v = execSync2("claude --version", { stdio: "pipe" }).toString().trim();
2892
2713
  ok(`Claude CLI ${dim2(v)}`);
2893
2714
  } catch {
2894
2715
  err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
@@ -2898,7 +2719,7 @@ ${infraSummary.substring(0, 12e3)}`;
2898
2719
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2899
2720
  let hasOAuth = false;
2900
2721
  try {
2901
- const creds = JSON.parse(readFileSync4(join2(home, ".claude", ".credentials.json"), "utf8"));
2722
+ const creds = JSON.parse(readFileSync3(join3(home, ".claude", ".credentials.json"), "utf8"));
2902
2723
  const oauth = creds["claudeAiOauth"];
2903
2724
  hasOAuth = typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
2904
2725
  } catch {
@@ -2912,7 +2733,7 @@ ${infraSummary.substring(0, 12e3)}`;
2912
2733
  allGood = false;
2913
2734
  }
2914
2735
  try {
2915
- const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
2736
+ const v = execSync2("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
2916
2737
  ok(`kubectl ${dim2(v || "(client OK)")}`);
2917
2738
  } catch {
2918
2739
  warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
@@ -2924,7 +2745,7 @@ ${infraSummary.substring(0, 12e3)}`;
2924
2745
  ];
2925
2746
  for (const [name, cmd, hint] of cloudClis) {
2926
2747
  try {
2927
- execSync3(cmd, { stdio: "pipe" });
2748
+ execSync2(cmd, { stdio: "pipe" });
2928
2749
  ok(`${name} ${dim2("(cloud scanning available)")}`);
2929
2750
  } catch {
2930
2751
  warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
@@ -2936,14 +2757,14 @@ ${infraSummary.substring(0, 12e3)}`;
2936
2757
  ];
2937
2758
  for (const [name, cmd] of localTools) {
2938
2759
  try {
2939
- execSync3(cmd, { stdio: "pipe" });
2760
+ execSync2(cmd, { stdio: "pipe" });
2940
2761
  ok(`${name} ${dim2("(discovery tool)")}`);
2941
2762
  } catch {
2942
2763
  warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);
2943
2764
  }
2944
2765
  }
2945
- const dbDir = join2(home, ".cartography");
2946
- if (existsSync5(dbDir)) {
2766
+ const dbDir = join3(home, ".cartography");
2767
+ if (existsSync3(dbDir)) {
2947
2768
  ok(`~/.cartography ${dim2("(data directory exists)")}`);
2948
2769
  } else {
2949
2770
  warn("~/.cartography does not exist yet " + dim2("\u2014 will be created on first run"));
@@ -2984,20 +2805,6 @@ ${infraSummary.substring(0, 12e3)}`;
2984
2805
  o(` ${_g("seed")} ${_d("Manually add known tools/DBs/APIs")}
2985
2806
  `);
2986
2807
  o(` ${_g("bookmarks")} ${_d("View browser bookmarks")}
2987
- `);
2988
- o(` ${_g("shadow start")} ${_d("Start background daemon (Claude Haiku)")}
2989
- `);
2990
- o(` ${_g("shadow pause")} ${_d("Pause daemon")}
2991
- `);
2992
- o(` ${_g("shadow resume")} ${_d("Resume daemon")}
2993
- `);
2994
- o(` ${_g("shadow stop")} ${_d("Stop + SOP review + dashboard")}
2995
- `);
2996
- o(` ${_g("shadow status")} ${_d("Show daemon status")}
2997
- `);
2998
- o(` ${_g("shadow attach")} ${_d("Live control: [T] [S] [P] [D] [Q]")}
2999
- `);
3000
- o(` ${_g("sops")} ${_d("[session]")} ${_d("Generate SOPs from workflows")}
3001
2808
  `);
3002
2809
  o(` ${_g("export")} ${_d("[session]")} ${_d("Export Mermaid, JSON, YAML, HTML")}
3003
2810
  `);
@@ -3019,8 +2826,6 @@ ${infraSummary.substring(0, 12e3)}`;
3019
2826
  o(` ${_m("$")} ${_b("datasynx-cartography seed")} ${_d("Add known infrastructure")}
3020
2827
  `);
3021
2828
  o(` ${_m("$")} ${_b("datasynx-cartography discover")} ${_d("One-time scan")}
3022
- `);
3023
- o(` ${_m("$")} ${_b("datasynx-cartography shadow start")} ${_d("Continuous monitoring")}
3024
2829
  `);
3025
2830
  o("\n");
3026
2831
  o(_d(" Docs: datasynx-cartography docs\n"));