@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/README.md +6 -59
- package/dist/{chunk-EVJP2FWQ.js → chunk-FFNOC6HF.js} +1 -33
- package/dist/chunk-FFNOC6HF.js.map +1 -0
- package/dist/cli.js +792 -987
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -118
- package/dist/index.js +3 -183
- package/dist/index.js.map +1 -1
- package/dist/{types-NKF6BRMZ.js → types-ROE3Z6HY.js} +2 -8
- package/package.json +1 -2
- package/dist/chunk-EVJP2FWQ.js.map +0 -1
- package/dist/chunk-LNMM7BTH.js +0 -989
- package/dist/chunk-LNMM7BTH.js.map +0 -1
- package/dist/exporter-PWVD7Y6T.js +0 -24
- package/dist/types-NKF6BRMZ.js.map +0 -1
- /package/dist/{exporter-PWVD7Y6T.js.map → types-ROE3Z6HY.js.map} +0 -0
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-
|
|
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'
|
|
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:
|
|
603
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
653
604
|
const { homedir } = await import("os");
|
|
654
|
-
const { existsSync:
|
|
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
|
|
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) =>
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
755
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
805
756
|
const hint = args["searchHint"];
|
|
806
757
|
const run = (cmd) => {
|
|
807
758
|
try {
|
|
808
|
-
return
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
{
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
"
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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>
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
//
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
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
|
-
//
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
};
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2151
|
+
if (existsSync2(htmlPath)) {
|
|
2106
2152
|
w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
|
|
2107
2153
|
`);
|
|
2108
2154
|
}
|
|
2109
|
-
if (
|
|
2155
|
+
if (existsSync2(topoPath)) {
|
|
2110
2156
|
try {
|
|
2111
|
-
const code =
|
|
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 (
|
|
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(" \
|
|
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
|
|
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:
|
|
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(
|
|
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-
|
|
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:
|
|
2869
|
-
const { existsSync:
|
|
2870
|
-
const { join:
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2946
|
-
if (
|
|
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"));
|