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