@datasynx/agentic-ai-cartography 0.1.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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/cli.js +1954 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +350 -0
- package/dist/index.js +1161 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1954 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/preflight.ts
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
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
|
+
"unknown"
|
|
29
|
+
];
|
|
30
|
+
var EDGE_RELATIONSHIPS = [
|
|
31
|
+
"connects_to",
|
|
32
|
+
"reads_from",
|
|
33
|
+
"writes_to",
|
|
34
|
+
"calls",
|
|
35
|
+
"contains",
|
|
36
|
+
"depends_on"
|
|
37
|
+
];
|
|
38
|
+
var EVENT_TYPES = [
|
|
39
|
+
"process_start",
|
|
40
|
+
"process_end",
|
|
41
|
+
"connection_open",
|
|
42
|
+
"connection_close",
|
|
43
|
+
"window_focus",
|
|
44
|
+
"tool_switch"
|
|
45
|
+
];
|
|
46
|
+
var NodeSchema = z.object({
|
|
47
|
+
id: z.string().describe('Format: "{type}:{host}:{port}" oder "{type}:{name}"'),
|
|
48
|
+
type: z.enum(NODE_TYPES),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
discoveredVia: z.string(),
|
|
51
|
+
confidence: z.number().min(0).max(1).default(0.5),
|
|
52
|
+
metadata: z.record(z.unknown()).default({}),
|
|
53
|
+
tags: z.array(z.string()).default([])
|
|
54
|
+
});
|
|
55
|
+
var EdgeSchema = z.object({
|
|
56
|
+
sourceId: z.string(),
|
|
57
|
+
targetId: z.string(),
|
|
58
|
+
relationship: z.enum(EDGE_RELATIONSHIPS),
|
|
59
|
+
evidence: z.string(),
|
|
60
|
+
confidence: z.number().min(0).max(1).default(0.5)
|
|
61
|
+
});
|
|
62
|
+
var EventSchema = z.object({
|
|
63
|
+
eventType: z.enum(EVENT_TYPES),
|
|
64
|
+
process: z.string(),
|
|
65
|
+
pid: z.number(),
|
|
66
|
+
target: z.string().optional(),
|
|
67
|
+
targetType: z.enum(NODE_TYPES).optional(),
|
|
68
|
+
protocol: z.string().optional(),
|
|
69
|
+
port: z.number().optional()
|
|
70
|
+
});
|
|
71
|
+
var SOPStepSchema = z.object({
|
|
72
|
+
order: z.number(),
|
|
73
|
+
instruction: z.string(),
|
|
74
|
+
tool: z.string(),
|
|
75
|
+
target: z.string().optional(),
|
|
76
|
+
notes: z.string().optional()
|
|
77
|
+
});
|
|
78
|
+
var SOPSchema = z.object({
|
|
79
|
+
title: z.string(),
|
|
80
|
+
description: z.string(),
|
|
81
|
+
steps: z.array(SOPStepSchema),
|
|
82
|
+
involvedSystems: z.array(z.string()),
|
|
83
|
+
estimatedDuration: z.string(),
|
|
84
|
+
frequency: z.string(),
|
|
85
|
+
confidence: z.number().min(0).max(1)
|
|
86
|
+
});
|
|
87
|
+
var MIN_POLL_INTERVAL_MS = 15e3;
|
|
88
|
+
function defaultConfig(overrides = {}) {
|
|
89
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
90
|
+
return {
|
|
91
|
+
mode: "discover",
|
|
92
|
+
maxDepth: 8,
|
|
93
|
+
maxTurns: 50,
|
|
94
|
+
entryPoints: ["localhost"],
|
|
95
|
+
agentModel: "claude-sonnet-4-5-20250929",
|
|
96
|
+
shadowMode: "daemon",
|
|
97
|
+
pollIntervalMs: 3e4,
|
|
98
|
+
inactivityTimeoutMs: 3e5,
|
|
99
|
+
promptTimeoutMs: 6e4,
|
|
100
|
+
trackWindowFocus: false,
|
|
101
|
+
autoSaveNodes: false,
|
|
102
|
+
enableNotifications: true,
|
|
103
|
+
shadowModel: "claude-haiku-4-5-20251001",
|
|
104
|
+
outputDir: "./cartography-output",
|
|
105
|
+
dbPath: `${home}/.cartography/cartography.db`,
|
|
106
|
+
socketPath: `${home}/.cartography/daemon.sock`,
|
|
107
|
+
pidFile: `${home}/.cartography/daemon.pid`,
|
|
108
|
+
verbose: false,
|
|
109
|
+
...overrides
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/preflight.ts
|
|
114
|
+
function isOAuthLoggedIn() {
|
|
115
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
116
|
+
const credFile = join(home, ".claude", ".credentials.json");
|
|
117
|
+
if (!existsSync(credFile)) return false;
|
|
118
|
+
try {
|
|
119
|
+
const creds = JSON.parse(readFileSync(credFile, "utf8"));
|
|
120
|
+
const oauth = creds["claudeAiOauth"];
|
|
121
|
+
return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function checkPrerequisites() {
|
|
127
|
+
try {
|
|
128
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
129
|
+
} catch {
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
"\n\u274C Claude CLI nicht gefunden.\n Cartography braucht die Claude CLI als Runtime-Dependency.\n\n Installieren:\n npm install -g @anthropic-ai/claude-code\n # oder\n curl -fsSL https://claude.ai/install.sh | bash\n\n Danach: claude login\n\n"
|
|
132
|
+
);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
throw new Error("Claude CLI not found");
|
|
135
|
+
}
|
|
136
|
+
const hasApiKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
137
|
+
const hasOAuth = isOAuthLoggedIn();
|
|
138
|
+
if (!hasApiKey && !hasOAuth) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
"\u26A0 Keine Authentifizierung gefunden. Bitte eine der folgenden Optionen:\n\n Option A \u2014 claude.ai Subscription (empfohlen):\n claude login\n\n Option B \u2014 API Key:\n export ANTHROPIC_API_KEY=sk-ant-...\n\n"
|
|
141
|
+
);
|
|
142
|
+
} else if (hasOAuth && !hasApiKey) {
|
|
143
|
+
process.stderr.write("\u2713 Eingeloggt via claude login (Subscription)\n");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function checkPollInterval(intervalMs) {
|
|
147
|
+
if (intervalMs < MIN_POLL_INTERVAL_MS) {
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`\u26A0 Minimum Shadow-Intervall: ${MIN_POLL_INTERVAL_MS / 1e3} Sekunden (Agent SDK Overhead)
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
return MIN_POLL_INTERVAL_MS;
|
|
153
|
+
}
|
|
154
|
+
return intervalMs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/db.ts
|
|
158
|
+
import Database from "better-sqlite3";
|
|
159
|
+
import { mkdirSync } from "fs";
|
|
160
|
+
import { dirname } from "path";
|
|
161
|
+
var SCHEMA = `
|
|
162
|
+
PRAGMA journal_mode = WAL;
|
|
163
|
+
PRAGMA foreign_keys = ON;
|
|
164
|
+
PRAGMA busy_timeout = 5000;
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
167
|
+
id TEXT PRIMARY KEY,
|
|
168
|
+
mode TEXT NOT NULL CHECK (mode IN ('discover','shadow')),
|
|
169
|
+
started_at TEXT NOT NULL,
|
|
170
|
+
completed_at TEXT,
|
|
171
|
+
config TEXT NOT NULL DEFAULT '{}'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
175
|
+
id TEXT NOT NULL,
|
|
176
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
177
|
+
type TEXT NOT NULL,
|
|
178
|
+
name TEXT NOT NULL,
|
|
179
|
+
discovered_via TEXT,
|
|
180
|
+
discovered_at TEXT NOT NULL,
|
|
181
|
+
path_id TEXT,
|
|
182
|
+
depth INTEGER DEFAULT 0,
|
|
183
|
+
confidence REAL DEFAULT 0.5,
|
|
184
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
185
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
186
|
+
PRIMARY KEY (id, session_id)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
192
|
+
source_id TEXT NOT NULL,
|
|
193
|
+
target_id TEXT NOT NULL,
|
|
194
|
+
relationship TEXT NOT NULL,
|
|
195
|
+
evidence TEXT,
|
|
196
|
+
confidence REAL DEFAULT 0.5,
|
|
197
|
+
discovered_at TEXT NOT NULL
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS activity_events (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
203
|
+
task_id TEXT,
|
|
204
|
+
timestamp TEXT NOT NULL,
|
|
205
|
+
event_type TEXT NOT NULL,
|
|
206
|
+
process TEXT NOT NULL,
|
|
207
|
+
pid INTEGER NOT NULL,
|
|
208
|
+
target TEXT,
|
|
209
|
+
target_type TEXT,
|
|
210
|
+
port INTEGER,
|
|
211
|
+
duration_ms INTEGER
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
215
|
+
id TEXT PRIMARY KEY,
|
|
216
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
217
|
+
description TEXT,
|
|
218
|
+
started_at TEXT NOT NULL,
|
|
219
|
+
completed_at TEXT,
|
|
220
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
221
|
+
involved_services TEXT NOT NULL DEFAULT '[]',
|
|
222
|
+
status TEXT DEFAULT 'active' CHECK (status IN ('active','completed','cancelled')),
|
|
223
|
+
is_sop_candidate INTEGER DEFAULT 0
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
227
|
+
id TEXT PRIMARY KEY,
|
|
228
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
229
|
+
name TEXT,
|
|
230
|
+
pattern TEXT NOT NULL,
|
|
231
|
+
task_ids TEXT NOT NULL DEFAULT '[]',
|
|
232
|
+
occurrences INTEGER DEFAULT 1,
|
|
233
|
+
first_seen TEXT NOT NULL,
|
|
234
|
+
last_seen TEXT NOT NULL,
|
|
235
|
+
avg_duration_ms INTEGER,
|
|
236
|
+
involved_services TEXT NOT NULL DEFAULT '[]'
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
CREATE TABLE IF NOT EXISTS sops (
|
|
240
|
+
id TEXT PRIMARY KEY,
|
|
241
|
+
workflow_id TEXT NOT NULL,
|
|
242
|
+
title TEXT NOT NULL,
|
|
243
|
+
description TEXT NOT NULL,
|
|
244
|
+
steps TEXT NOT NULL,
|
|
245
|
+
involved_systems TEXT NOT NULL DEFAULT '[]',
|
|
246
|
+
estimated_duration TEXT,
|
|
247
|
+
frequency TEXT,
|
|
248
|
+
generated_at TEXT NOT NULL,
|
|
249
|
+
confidence REAL DEFAULT 0.5
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
CREATE TABLE IF NOT EXISTS node_approvals (
|
|
253
|
+
pattern TEXT PRIMARY KEY,
|
|
254
|
+
action TEXT NOT NULL CHECK (action IN ('save','ignore','auto')),
|
|
255
|
+
created_at TEXT NOT NULL
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_session ON nodes(session_id);
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
263
|
+
`;
|
|
264
|
+
var CartographyDB = class {
|
|
265
|
+
db;
|
|
266
|
+
constructor(dbPath) {
|
|
267
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
268
|
+
this.db = new Database(dbPath);
|
|
269
|
+
this.db.pragma("journal_mode = WAL");
|
|
270
|
+
this.db.pragma("foreign_keys = ON");
|
|
271
|
+
this.db.pragma("busy_timeout = 5000");
|
|
272
|
+
this.migrate();
|
|
273
|
+
}
|
|
274
|
+
migrate() {
|
|
275
|
+
const version = this.db.pragma("user_version", { simple: true });
|
|
276
|
+
if (version === 0) {
|
|
277
|
+
this.db.exec(SCHEMA);
|
|
278
|
+
this.db.pragma("user_version = 1");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
close() {
|
|
282
|
+
this.db.pragma("optimize");
|
|
283
|
+
this.db.close();
|
|
284
|
+
}
|
|
285
|
+
// ── Sessions ────────────────────────────
|
|
286
|
+
createSession(mode, config) {
|
|
287
|
+
const id = crypto.randomUUID();
|
|
288
|
+
this.db.prepare(
|
|
289
|
+
"INSERT INTO sessions (id, mode, started_at, config) VALUES (?, ?, ?, ?)"
|
|
290
|
+
).run(id, mode, (/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(config));
|
|
291
|
+
return id;
|
|
292
|
+
}
|
|
293
|
+
endSession(id) {
|
|
294
|
+
this.db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
295
|
+
}
|
|
296
|
+
getSession(id) {
|
|
297
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
298
|
+
return row ? this.mapSession(row) : void 0;
|
|
299
|
+
}
|
|
300
|
+
getLatestSession(mode) {
|
|
301
|
+
const row = mode ? this.db.prepare("SELECT * FROM sessions WHERE mode = ? ORDER BY rowid DESC LIMIT 1").get(mode) : this.db.prepare("SELECT * FROM sessions ORDER BY rowid DESC LIMIT 1").get();
|
|
302
|
+
return row ? this.mapSession(row) : void 0;
|
|
303
|
+
}
|
|
304
|
+
getSessions() {
|
|
305
|
+
const rows = this.db.prepare("SELECT * FROM sessions ORDER BY rowid DESC").all();
|
|
306
|
+
return rows.map((r) => this.mapSession(r));
|
|
307
|
+
}
|
|
308
|
+
mapSession(r) {
|
|
309
|
+
return {
|
|
310
|
+
id: r["id"],
|
|
311
|
+
mode: r["mode"],
|
|
312
|
+
startedAt: r["started_at"],
|
|
313
|
+
completedAt: r["completed_at"] ?? void 0,
|
|
314
|
+
config: r["config"]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// ── Nodes ───────────────────────────────
|
|
318
|
+
upsertNode(sessionId, node, depth = 0) {
|
|
319
|
+
this.db.prepare(`
|
|
320
|
+
INSERT OR REPLACE INTO nodes
|
|
321
|
+
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags)
|
|
322
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
323
|
+
`).run(
|
|
324
|
+
node.id,
|
|
325
|
+
sessionId,
|
|
326
|
+
node.type,
|
|
327
|
+
node.name,
|
|
328
|
+
node.discoveredVia,
|
|
329
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
330
|
+
depth,
|
|
331
|
+
node.confidence,
|
|
332
|
+
JSON.stringify(node.metadata ?? {}),
|
|
333
|
+
JSON.stringify(node.tags ?? [])
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
getNodes(sessionId) {
|
|
337
|
+
const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
|
|
338
|
+
return rows.map((r) => ({
|
|
339
|
+
id: r["id"],
|
|
340
|
+
sessionId: r["session_id"],
|
|
341
|
+
type: r["type"],
|
|
342
|
+
name: r["name"],
|
|
343
|
+
discoveredVia: r["discovered_via"],
|
|
344
|
+
discoveredAt: r["discovered_at"],
|
|
345
|
+
depth: r["depth"],
|
|
346
|
+
confidence: r["confidence"],
|
|
347
|
+
metadata: JSON.parse(r["metadata"]),
|
|
348
|
+
tags: JSON.parse(r["tags"]),
|
|
349
|
+
pathId: r["path_id"]
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
// ── Edges ───────────────────────────────
|
|
353
|
+
insertEdge(sessionId, edge) {
|
|
354
|
+
const id = crypto.randomUUID();
|
|
355
|
+
this.db.prepare(`
|
|
356
|
+
INSERT OR IGNORE INTO edges
|
|
357
|
+
(id, session_id, source_id, target_id, relationship, evidence, confidence, discovered_at)
|
|
358
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
359
|
+
`).run(
|
|
360
|
+
id,
|
|
361
|
+
sessionId,
|
|
362
|
+
edge.sourceId,
|
|
363
|
+
edge.targetId,
|
|
364
|
+
edge.relationship,
|
|
365
|
+
edge.evidence,
|
|
366
|
+
edge.confidence,
|
|
367
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
getEdges(sessionId) {
|
|
371
|
+
const rows = this.db.prepare("SELECT * FROM edges WHERE session_id = ?").all(sessionId);
|
|
372
|
+
return rows.map((r) => ({
|
|
373
|
+
id: r["id"],
|
|
374
|
+
sessionId: r["session_id"],
|
|
375
|
+
sourceId: r["source_id"],
|
|
376
|
+
targetId: r["target_id"],
|
|
377
|
+
relationship: r["relationship"],
|
|
378
|
+
evidence: r["evidence"],
|
|
379
|
+
confidence: r["confidence"],
|
|
380
|
+
discoveredAt: r["discovered_at"]
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
// ── Events ──────────────────────────────
|
|
384
|
+
insertEvent(sessionId, event, taskId) {
|
|
385
|
+
const id = crypto.randomUUID();
|
|
386
|
+
this.db.prepare(`
|
|
387
|
+
INSERT INTO activity_events
|
|
388
|
+
(id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port)
|
|
389
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
390
|
+
`).run(
|
|
391
|
+
id,
|
|
392
|
+
sessionId,
|
|
393
|
+
taskId ?? null,
|
|
394
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
395
|
+
event.eventType,
|
|
396
|
+
event.process,
|
|
397
|
+
event.pid,
|
|
398
|
+
event.target ?? null,
|
|
399
|
+
event.targetType ?? null,
|
|
400
|
+
event.port ?? null
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
getEvents(sessionId, since) {
|
|
404
|
+
const rows = since ? this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp").all(sessionId, since) : this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? ORDER BY timestamp").all(sessionId);
|
|
405
|
+
return rows.map((r) => ({
|
|
406
|
+
id: r["id"],
|
|
407
|
+
sessionId: r["session_id"],
|
|
408
|
+
taskId: r["task_id"],
|
|
409
|
+
timestamp: r["timestamp"],
|
|
410
|
+
eventType: r["event_type"],
|
|
411
|
+
process: r["process"],
|
|
412
|
+
pid: r["pid"],
|
|
413
|
+
target: r["target"],
|
|
414
|
+
targetType: r["target_type"],
|
|
415
|
+
port: r["port"],
|
|
416
|
+
durationMs: r["duration_ms"]
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
// ── Tasks ───────────────────────────────
|
|
420
|
+
startTask(sessionId, description) {
|
|
421
|
+
const id = crypto.randomUUID();
|
|
422
|
+
this.db.prepare(`
|
|
423
|
+
INSERT INTO tasks (id, session_id, description, started_at, steps, involved_services, status)
|
|
424
|
+
VALUES (?, ?, ?, ?, '[]', '[]', 'active')
|
|
425
|
+
`).run(id, sessionId, description ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
426
|
+
return id;
|
|
427
|
+
}
|
|
428
|
+
endCurrentTask(sessionId) {
|
|
429
|
+
this.db.prepare(`
|
|
430
|
+
UPDATE tasks SET status = 'completed', completed_at = ?
|
|
431
|
+
WHERE session_id = ? AND status = 'active'
|
|
432
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
|
|
433
|
+
}
|
|
434
|
+
updateTaskDescription(sessionId, description) {
|
|
435
|
+
this.db.prepare(`
|
|
436
|
+
UPDATE tasks SET description = ?
|
|
437
|
+
WHERE session_id = ? AND status = 'active'
|
|
438
|
+
`).run(description, sessionId);
|
|
439
|
+
}
|
|
440
|
+
getActiveTask(sessionId) {
|
|
441
|
+
const row = this.db.prepare(
|
|
442
|
+
"SELECT * FROM tasks WHERE session_id = ? AND status = 'active' LIMIT 1"
|
|
443
|
+
).get(sessionId);
|
|
444
|
+
return row ? this.mapTask(row) : void 0;
|
|
445
|
+
}
|
|
446
|
+
getTasks(sessionId) {
|
|
447
|
+
const rows = this.db.prepare("SELECT * FROM tasks WHERE session_id = ? ORDER BY started_at").all(sessionId);
|
|
448
|
+
return rows.map((r) => this.mapTask(r));
|
|
449
|
+
}
|
|
450
|
+
mapTask(r) {
|
|
451
|
+
return {
|
|
452
|
+
id: r["id"],
|
|
453
|
+
sessionId: r["session_id"],
|
|
454
|
+
description: r["description"],
|
|
455
|
+
startedAt: r["started_at"],
|
|
456
|
+
completedAt: r["completed_at"],
|
|
457
|
+
steps: r["steps"],
|
|
458
|
+
involvedServices: r["involved_services"],
|
|
459
|
+
status: r["status"],
|
|
460
|
+
isSOPCandidate: Boolean(r["is_sop_candidate"])
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// ── Workflows ───────────────────────────
|
|
464
|
+
insertWorkflow(sessionId, data) {
|
|
465
|
+
const id = crypto.randomUUID();
|
|
466
|
+
this.db.prepare(`
|
|
467
|
+
INSERT INTO workflows
|
|
468
|
+
(id, session_id, name, pattern, task_ids, occurrences,
|
|
469
|
+
first_seen, last_seen, avg_duration_ms, involved_services)
|
|
470
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
471
|
+
`).run(
|
|
472
|
+
id,
|
|
473
|
+
sessionId,
|
|
474
|
+
data.name ?? null,
|
|
475
|
+
data.pattern,
|
|
476
|
+
data.taskIds,
|
|
477
|
+
data.occurrences,
|
|
478
|
+
data.firstSeen,
|
|
479
|
+
data.lastSeen,
|
|
480
|
+
data.avgDurationMs,
|
|
481
|
+
data.involvedServices
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
getWorkflows(sessionId) {
|
|
485
|
+
const rows = this.db.prepare("SELECT * FROM workflows WHERE session_id = ?").all(sessionId);
|
|
486
|
+
return rows.map((r) => ({
|
|
487
|
+
id: r["id"],
|
|
488
|
+
sessionId: r["session_id"],
|
|
489
|
+
name: r["name"],
|
|
490
|
+
pattern: r["pattern"],
|
|
491
|
+
taskIds: r["task_ids"],
|
|
492
|
+
occurrences: r["occurrences"],
|
|
493
|
+
firstSeen: r["first_seen"],
|
|
494
|
+
lastSeen: r["last_seen"],
|
|
495
|
+
avgDurationMs: r["avg_duration_ms"],
|
|
496
|
+
involvedServices: r["involved_services"]
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
// ── SOPs ────────────────────────────────
|
|
500
|
+
insertSOP(sop) {
|
|
501
|
+
const id = crypto.randomUUID();
|
|
502
|
+
this.db.prepare(`
|
|
503
|
+
INSERT INTO sops
|
|
504
|
+
(id, workflow_id, title, description, steps, involved_systems,
|
|
505
|
+
estimated_duration, frequency, generated_at, confidence)
|
|
506
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
507
|
+
`).run(
|
|
508
|
+
id,
|
|
509
|
+
sop.workflowId,
|
|
510
|
+
sop.title,
|
|
511
|
+
sop.description,
|
|
512
|
+
JSON.stringify(sop.steps),
|
|
513
|
+
JSON.stringify(sop.involvedSystems),
|
|
514
|
+
sop.estimatedDuration,
|
|
515
|
+
sop.frequency,
|
|
516
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
517
|
+
sop.confidence
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
getSOPs(sessionId) {
|
|
521
|
+
const rows = this.db.prepare(`
|
|
522
|
+
SELECT s.* FROM sops s
|
|
523
|
+
JOIN workflows w ON s.workflow_id = w.id
|
|
524
|
+
WHERE w.session_id = ?
|
|
525
|
+
`).all(sessionId);
|
|
526
|
+
return rows.map((r) => ({
|
|
527
|
+
id: r["id"],
|
|
528
|
+
workflowId: r["workflow_id"],
|
|
529
|
+
title: r["title"],
|
|
530
|
+
description: r["description"],
|
|
531
|
+
steps: JSON.parse(r["steps"]),
|
|
532
|
+
involvedSystems: JSON.parse(r["involved_systems"]),
|
|
533
|
+
estimatedDuration: r["estimated_duration"],
|
|
534
|
+
frequency: r["frequency"],
|
|
535
|
+
confidence: r["confidence"]
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
// ── Approvals ───────────────────────────
|
|
539
|
+
setApproval(pattern, action) {
|
|
540
|
+
this.db.prepare(`
|
|
541
|
+
INSERT OR REPLACE INTO node_approvals (pattern, action, created_at) VALUES (?, ?, ?)
|
|
542
|
+
`).run(pattern, action, (/* @__PURE__ */ new Date()).toISOString());
|
|
543
|
+
}
|
|
544
|
+
getApproval(pattern) {
|
|
545
|
+
const row = this.db.prepare("SELECT action FROM node_approvals WHERE pattern = ?").get(pattern);
|
|
546
|
+
return row?.action;
|
|
547
|
+
}
|
|
548
|
+
// ── Stats ───────────────────────────────
|
|
549
|
+
getStats(sessionId) {
|
|
550
|
+
const nodes = this.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE session_id = ?").get(sessionId).c;
|
|
551
|
+
const edges = this.db.prepare("SELECT COUNT(*) as c FROM edges WHERE session_id = ?").get(sessionId).c;
|
|
552
|
+
const events = this.db.prepare("SELECT COUNT(*) as c FROM activity_events WHERE session_id = ?").get(sessionId).c;
|
|
553
|
+
const tasks = this.db.prepare("SELECT COUNT(*) as c FROM tasks WHERE session_id = ?").get(sessionId).c;
|
|
554
|
+
return { nodes, edges, events, tasks };
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// src/tools.ts
|
|
559
|
+
import { z as z2 } from "zod";
|
|
560
|
+
function stripSensitive(target) {
|
|
561
|
+
try {
|
|
562
|
+
const url = new URL(target.startsWith("http") ? target : `tcp://${target}`);
|
|
563
|
+
return `${url.hostname}${url.port ? ":" + url.port : ""}`;
|
|
564
|
+
} catch {
|
|
565
|
+
return target.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function createCartographyTools(db, sessionId) {
|
|
569
|
+
const sdk = await import("@anthropic-ai/claude-code");
|
|
570
|
+
const { tool, createSdkMcpServer } = sdk;
|
|
571
|
+
const tools = [
|
|
572
|
+
tool("save_node", "Infrastructure-Node speichern", {
|
|
573
|
+
id: z2.string(),
|
|
574
|
+
type: z2.enum(NODE_TYPES),
|
|
575
|
+
name: z2.string(),
|
|
576
|
+
discoveredVia: z2.string(),
|
|
577
|
+
confidence: z2.number().min(0).max(1),
|
|
578
|
+
metadata: z2.record(z2.unknown()).optional(),
|
|
579
|
+
tags: z2.array(z2.string()).optional()
|
|
580
|
+
}, async (args) => {
|
|
581
|
+
const node = {
|
|
582
|
+
id: stripSensitive(args["id"]),
|
|
583
|
+
type: args["type"],
|
|
584
|
+
name: args["name"],
|
|
585
|
+
discoveredVia: args["discoveredVia"],
|
|
586
|
+
confidence: args["confidence"],
|
|
587
|
+
metadata: args["metadata"] ?? {},
|
|
588
|
+
tags: args["tags"] ?? []
|
|
589
|
+
};
|
|
590
|
+
db.upsertNode(sessionId, node);
|
|
591
|
+
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
592
|
+
}),
|
|
593
|
+
tool("save_edge", "Verbindung zwischen zwei Nodes speichern", {
|
|
594
|
+
sourceId: z2.string(),
|
|
595
|
+
targetId: z2.string(),
|
|
596
|
+
relationship: z2.enum(EDGE_RELATIONSHIPS),
|
|
597
|
+
evidence: z2.string(),
|
|
598
|
+
confidence: z2.number().min(0).max(1)
|
|
599
|
+
}, async (args) => {
|
|
600
|
+
db.insertEdge(sessionId, {
|
|
601
|
+
sourceId: args["sourceId"],
|
|
602
|
+
targetId: args["targetId"],
|
|
603
|
+
relationship: args["relationship"],
|
|
604
|
+
evidence: args["evidence"],
|
|
605
|
+
confidence: args["confidence"]
|
|
606
|
+
});
|
|
607
|
+
return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
|
|
608
|
+
}),
|
|
609
|
+
tool("save_event", "Activity-Event (Prozess/Verbindung) speichern", {
|
|
610
|
+
eventType: z2.enum(EVENT_TYPES),
|
|
611
|
+
process: z2.string(),
|
|
612
|
+
pid: z2.number(),
|
|
613
|
+
target: z2.string().optional(),
|
|
614
|
+
targetType: z2.enum(NODE_TYPES).optional(),
|
|
615
|
+
port: z2.number().optional()
|
|
616
|
+
}, async (args) => {
|
|
617
|
+
db.insertEvent(sessionId, {
|
|
618
|
+
eventType: args["eventType"],
|
|
619
|
+
process: args["process"],
|
|
620
|
+
pid: args["pid"],
|
|
621
|
+
target: args["target"] ? stripSensitive(args["target"]) : void 0,
|
|
622
|
+
targetType: args["targetType"],
|
|
623
|
+
port: args["port"]
|
|
624
|
+
});
|
|
625
|
+
return { content: [{ type: "text", text: `\u2713 ${args["eventType"]}` }] };
|
|
626
|
+
}),
|
|
627
|
+
tool("get_catalog", "Aktuellen Katalog abrufen (Duplikat-Check)", {
|
|
628
|
+
includeEdges: z2.boolean().default(true)
|
|
629
|
+
}, async (args) => {
|
|
630
|
+
const nodes = db.getNodes(sessionId);
|
|
631
|
+
const edges = args["includeEdges"] ? db.getEdges(sessionId) : [];
|
|
632
|
+
return {
|
|
633
|
+
content: [{
|
|
634
|
+
type: "text",
|
|
635
|
+
text: JSON.stringify({
|
|
636
|
+
count: { nodes: nodes.length, edges: edges.length },
|
|
637
|
+
nodeIds: nodes.map((n) => n.id)
|
|
638
|
+
})
|
|
639
|
+
}]
|
|
640
|
+
};
|
|
641
|
+
}),
|
|
642
|
+
tool("manage_task", "Task starten, beenden oder beschreiben", {
|
|
643
|
+
action: z2.enum(["start", "end", "describe"]),
|
|
644
|
+
description: z2.string().optional()
|
|
645
|
+
}, async (args) => {
|
|
646
|
+
const action = args["action"];
|
|
647
|
+
if (action === "start") {
|
|
648
|
+
const id = db.startTask(sessionId, args["description"]);
|
|
649
|
+
return { content: [{ type: "text", text: `\u2713 Task gestartet: ${id}` }] };
|
|
650
|
+
}
|
|
651
|
+
if (action === "end") {
|
|
652
|
+
db.endCurrentTask(sessionId);
|
|
653
|
+
return { content: [{ type: "text", text: "\u2713 Task beendet" }] };
|
|
654
|
+
}
|
|
655
|
+
db.updateTaskDescription(sessionId, args["description"]);
|
|
656
|
+
return { content: [{ type: "text", text: "\u2713 Beschreibung aktualisiert" }] };
|
|
657
|
+
}),
|
|
658
|
+
tool("save_sop", "Standard Operating Procedure speichern", {
|
|
659
|
+
workflowId: z2.string(),
|
|
660
|
+
title: z2.string(),
|
|
661
|
+
description: z2.string(),
|
|
662
|
+
steps: z2.array(SOPStepSchema),
|
|
663
|
+
involvedSystems: z2.array(z2.string()),
|
|
664
|
+
estimatedDuration: z2.string(),
|
|
665
|
+
frequency: z2.string(),
|
|
666
|
+
confidence: z2.number().min(0).max(1)
|
|
667
|
+
}, async (args) => {
|
|
668
|
+
db.insertSOP({
|
|
669
|
+
workflowId: args["workflowId"],
|
|
670
|
+
title: args["title"],
|
|
671
|
+
description: args["description"],
|
|
672
|
+
steps: args["steps"],
|
|
673
|
+
involvedSystems: args["involvedSystems"],
|
|
674
|
+
estimatedDuration: args["estimatedDuration"],
|
|
675
|
+
frequency: args["frequency"],
|
|
676
|
+
confidence: args["confidence"]
|
|
677
|
+
});
|
|
678
|
+
return { content: [{ type: "text", text: `\u2713 SOP: ${args["title"]}` }] };
|
|
679
|
+
})
|
|
680
|
+
];
|
|
681
|
+
return createSdkMcpServer({
|
|
682
|
+
name: "cartography",
|
|
683
|
+
version: "0.1.0",
|
|
684
|
+
tools
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/safety.ts
|
|
689
|
+
var BLOCKED_CMDS = /\b(rm|mv|cp|dd|mkfs|chmod|chown|chgrp|kill|killall|pkill|reboot|shutdown|poweroff|halt|systemctl\s+(start|stop|restart|enable|disable)|service\s+(start|stop|restart)|docker\s+(rm|rmi|stop|kill|exec|run|build|push)|kubectl\s+(delete|apply|edit|exec|run|create|patch)|apt|yum|dnf|pacman|pip\s+install|npm\s+(install|uninstall)|curl\s+.*-X\s*(POST|PUT|DELETE|PATCH)|wget\s+-O|tee\s)\b/i;
|
|
690
|
+
var BLOCKED_REDIRECTS = />>|>[^>]/;
|
|
691
|
+
var safetyHook = async (input) => {
|
|
692
|
+
if (!("tool_name" in input)) return {};
|
|
693
|
+
if (input.tool_name !== "Bash") return {};
|
|
694
|
+
const cmd = input.tool_input?.command ?? "";
|
|
695
|
+
if (BLOCKED_CMDS.test(cmd) || BLOCKED_REDIRECTS.test(cmd)) {
|
|
696
|
+
return {
|
|
697
|
+
hookSpecificOutput: {
|
|
698
|
+
hookEventName: "PreToolUse",
|
|
699
|
+
permissionDecision: "deny",
|
|
700
|
+
permissionDecisionReason: `BLOCKED: "${cmd}" \u2014 read-only policy`
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
hookSpecificOutput: {
|
|
706
|
+
hookEventName: "PreToolUse",
|
|
707
|
+
permissionDecision: "allow"
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/agent.ts
|
|
713
|
+
async function runDiscovery(config, db, sessionId, onOutput) {
|
|
714
|
+
const { query } = await import("@anthropic-ai/claude-code");
|
|
715
|
+
const tools = await createCartographyTools(db, sessionId);
|
|
716
|
+
const systemPrompt = `Du bist ein Infrastruktur-Discovery-Agent.
|
|
717
|
+
Kartographiere die gesamte Systemlandschaft.
|
|
718
|
+
|
|
719
|
+
STRATEGIE:
|
|
720
|
+
1. ss -tlnp + ps aux \u2192 \xDCberblick
|
|
721
|
+
2. Jeden Service tiefer (Datenbanken\u2192Tabellen, APIs\u2192Endpoints, Queues\u2192Topics)
|
|
722
|
+
3. save_node + save_edge f\xFCr alles. get_catalog \u2192 keine Duplikate.
|
|
723
|
+
4. Config-Files folgen: .env (nur Host:Port!), docker-compose.yml, application.yml
|
|
724
|
+
5. Backtrack wenn Spur ersch\xF6pft. Stop wenn alles explored.
|
|
725
|
+
|
|
726
|
+
PORT-MAPPING: 5432=postgres, 3306=mysql, 27017=mongodb, 6379=redis,
|
|
727
|
+
9092=kafka, 5672=rabbitmq, 80/443/8080/3000=web_service,
|
|
728
|
+
9090=prometheus, 8500=consul, 8200=vault, 2379=etcd
|
|
729
|
+
|
|
730
|
+
REGELN:
|
|
731
|
+
- NUR read-only Commands (ss, ps, cat, head, curl -s, docker inspect, kubectl get)
|
|
732
|
+
- Targets NUR Host:Port \u2014 KEINE URLs, Pfade, Credentials
|
|
733
|
+
- Node IDs: "{type}:{host}:{port}" oder "{type}:{name}"
|
|
734
|
+
- Confidence: 0.9 direkt beobachtet, 0.7 aus Config, 0.5 Vermutung
|
|
735
|
+
- KEINE Credentials speichern
|
|
736
|
+
|
|
737
|
+
Entrypoints: ${config.entryPoints.join(", ")}`;
|
|
738
|
+
for await (const msg of query({
|
|
739
|
+
prompt: systemPrompt,
|
|
740
|
+
options: {
|
|
741
|
+
model: config.agentModel,
|
|
742
|
+
maxTurns: config.maxTurns,
|
|
743
|
+
customSystemPrompt: systemPrompt,
|
|
744
|
+
mcpServers: { cartography: tools },
|
|
745
|
+
allowedTools: [
|
|
746
|
+
"Bash",
|
|
747
|
+
"mcp__cartograph__save_node",
|
|
748
|
+
"mcp__cartograph__save_edge",
|
|
749
|
+
"mcp__cartograph__get_catalog"
|
|
750
|
+
],
|
|
751
|
+
hooks: {
|
|
752
|
+
PreToolUse: [{ matcher: "Bash", hooks: [safetyHook] }]
|
|
753
|
+
},
|
|
754
|
+
permissionMode: "bypassPermissions"
|
|
755
|
+
}
|
|
756
|
+
})) {
|
|
757
|
+
if (msg.type === "assistant" && onOutput) {
|
|
758
|
+
for (const block of msg.message.content) {
|
|
759
|
+
if (block.type === "text") {
|
|
760
|
+
onOutput(block.text);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (msg.type === "result") return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async function runShadowCycle(config, db, sessionId, prevSnapshot, currSnapshot, onOutput) {
|
|
768
|
+
const { query } = await import("@anthropic-ai/claude-code");
|
|
769
|
+
const tools = await createCartographyTools(db, sessionId);
|
|
770
|
+
const prompt = `Analysiere den Diff zwischen diesen beiden System-Snapshots.
|
|
771
|
+
Finde:
|
|
772
|
+
- Neue/geschlossene TCP-Verbindungen \u2192 save_event
|
|
773
|
+
- Neue/beendete Prozesse \u2192 save_event
|
|
774
|
+
- Bisher unbekannte Services \u2192 get_catalog pr\xFCfen, dann save_node
|
|
775
|
+
- Task-Grenzen (Inaktivit\xE4t, Tool-Wechsel) \u2192 manage_task
|
|
776
|
+
target = NUR Host:Port. Kurz und effizient.
|
|
777
|
+
|
|
778
|
+
=== VORHER ===
|
|
779
|
+
${prevSnapshot}
|
|
780
|
+
|
|
781
|
+
=== JETZT ===
|
|
782
|
+
${currSnapshot}`;
|
|
783
|
+
for await (const msg of query({
|
|
784
|
+
prompt,
|
|
785
|
+
options: {
|
|
786
|
+
model: config.shadowModel,
|
|
787
|
+
maxTurns: 5,
|
|
788
|
+
mcpServers: { cartography: tools },
|
|
789
|
+
allowedTools: [
|
|
790
|
+
"mcp__cartograph__save_event",
|
|
791
|
+
"mcp__cartograph__save_node",
|
|
792
|
+
"mcp__cartograph__save_edge",
|
|
793
|
+
"mcp__cartograph__get_catalog",
|
|
794
|
+
"mcp__cartograph__manage_task"
|
|
795
|
+
],
|
|
796
|
+
permissionMode: "bypassPermissions"
|
|
797
|
+
}
|
|
798
|
+
})) {
|
|
799
|
+
if (onOutput) onOutput(msg);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function generateSOPs(db, sessionId) {
|
|
803
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
804
|
+
const client = new Anthropic();
|
|
805
|
+
const tasks = db.getTasks(sessionId).filter((t) => t.status === "completed");
|
|
806
|
+
if (tasks.length === 0) return 0;
|
|
807
|
+
const clusters = clusterTasks(tasks);
|
|
808
|
+
let generated = 0;
|
|
809
|
+
for (const cluster of clusters) {
|
|
810
|
+
const workflowId = crypto.randomUUID();
|
|
811
|
+
const involved = JSON.parse(cluster[0]?.involvedServices ?? "[]");
|
|
812
|
+
const taskDescriptions = cluster.map((t, i) => `Task ${i + 1}: ${t.description ?? "Unnamed"}
|
|
813
|
+
Steps: ${t.steps}`).join("\n\n");
|
|
814
|
+
const response = await client.messages.create({
|
|
815
|
+
model: "claude-sonnet-4-5-20250929",
|
|
816
|
+
max_tokens: 2048,
|
|
817
|
+
messages: [{
|
|
818
|
+
role: "user",
|
|
819
|
+
content: `Generiere eine SOP (Standard Operating Procedure) f\xFCr diesen wiederkehrenden Workflow.
|
|
820
|
+
Antworte NUR mit validen JSON im Format:
|
|
821
|
+
{
|
|
822
|
+
"title": "...",
|
|
823
|
+
"description": "...",
|
|
824
|
+
"steps": [{"order": 1, "instruction": "...", "tool": "...", "target": "...", "notes": "..."}],
|
|
825
|
+
"involvedSystems": ["..."],
|
|
826
|
+
"estimatedDuration": "~N Minuten",
|
|
827
|
+
"frequency": "Xmal t\xE4glich",
|
|
828
|
+
"confidence": 0.8
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
Tasks:
|
|
832
|
+
${taskDescriptions}
|
|
833
|
+
|
|
834
|
+
Beteiligte Services: ${involved.join(", ")}`
|
|
835
|
+
}]
|
|
836
|
+
});
|
|
837
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
838
|
+
try {
|
|
839
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
840
|
+
if (!jsonMatch) continue;
|
|
841
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
842
|
+
db.insertSOP({ workflowId, ...parsed });
|
|
843
|
+
generated++;
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return generated;
|
|
848
|
+
}
|
|
849
|
+
function clusterTasks(tasks) {
|
|
850
|
+
const clusters = [];
|
|
851
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
852
|
+
for (const task of tasks) {
|
|
853
|
+
if (assigned.has(task.id)) continue;
|
|
854
|
+
const cluster = [task];
|
|
855
|
+
assigned.add(task.id);
|
|
856
|
+
const taskServices = new Set(JSON.parse(task.involvedServices ?? "[]"));
|
|
857
|
+
for (const other of tasks) {
|
|
858
|
+
if (assigned.has(other.id)) continue;
|
|
859
|
+
const otherServices = new Set(JSON.parse(other.involvedServices ?? "[]"));
|
|
860
|
+
const overlap = [...taskServices].filter((s) => otherServices.has(s));
|
|
861
|
+
if (overlap.length > 0) {
|
|
862
|
+
cluster.push(other);
|
|
863
|
+
assigned.add(other.id);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
clusters.push(cluster);
|
|
867
|
+
}
|
|
868
|
+
return clusters;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/exporter.ts
|
|
872
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
873
|
+
import { join as join2 } from "path";
|
|
874
|
+
var MERMAID_ICONS = {
|
|
875
|
+
host: "\u{1F5A5}\uFE0F",
|
|
876
|
+
database_server: "\u{1F5C4}\uFE0F",
|
|
877
|
+
database: "\u{1F5C4}\uFE0F",
|
|
878
|
+
table: "\u{1F4CB}",
|
|
879
|
+
web_service: "\u{1F310}",
|
|
880
|
+
api_endpoint: "\u{1F50C}",
|
|
881
|
+
cache_server: "\u26A1",
|
|
882
|
+
message_broker: "\u{1F4E8}",
|
|
883
|
+
queue: "\u{1F4EC}",
|
|
884
|
+
topic: "\u{1F4E2}",
|
|
885
|
+
container: "\u{1F4E6}",
|
|
886
|
+
pod: "\u2638\uFE0F",
|
|
887
|
+
k8s_cluster: "\u2638\uFE0F",
|
|
888
|
+
config_file: "\u{1F4C4}",
|
|
889
|
+
unknown: "\u2753"
|
|
890
|
+
};
|
|
891
|
+
var EDGE_LABELS = {
|
|
892
|
+
connects_to: "",
|
|
893
|
+
reads_from: "reads",
|
|
894
|
+
writes_to: "writes",
|
|
895
|
+
calls: "calls",
|
|
896
|
+
contains: "contains",
|
|
897
|
+
depends_on: "depends"
|
|
898
|
+
};
|
|
899
|
+
function sanitize(id) {
|
|
900
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
901
|
+
}
|
|
902
|
+
function groupByParent(nodes) {
|
|
903
|
+
const groups = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const node of nodes) {
|
|
905
|
+
const group = node.id.split(":")[1] ?? node.type;
|
|
906
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
907
|
+
groups.get(group).push(node);
|
|
908
|
+
}
|
|
909
|
+
return groups;
|
|
910
|
+
}
|
|
911
|
+
function generateTopologyMermaid(nodes, edges) {
|
|
912
|
+
const lines = ["graph TB"];
|
|
913
|
+
const groups = groupByParent(nodes);
|
|
914
|
+
for (const [group, groupNodes] of groups) {
|
|
915
|
+
if (groups.size > 1) {
|
|
916
|
+
lines.push(` subgraph ${sanitize(group)}`);
|
|
917
|
+
}
|
|
918
|
+
for (const node of groupNodes) {
|
|
919
|
+
const icon = MERMAID_ICONS[node.type] ?? "\u2753";
|
|
920
|
+
lines.push(` ${sanitize(node.id)}["${icon} ${node.name}"]`);
|
|
921
|
+
}
|
|
922
|
+
if (groups.size > 1) {
|
|
923
|
+
lines.push(" end");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
for (const edge of edges) {
|
|
927
|
+
const label = EDGE_LABELS[edge.relationship] ?? "";
|
|
928
|
+
const arrow = label ? `-->|"${label}"|` : "-->";
|
|
929
|
+
lines.push(` ${sanitize(edge.sourceId)} ${arrow} ${sanitize(edge.targetId)}`);
|
|
930
|
+
}
|
|
931
|
+
return lines.join("\n");
|
|
932
|
+
}
|
|
933
|
+
function generateDependencyMermaid(nodes, edges) {
|
|
934
|
+
const lines = ["graph LR"];
|
|
935
|
+
const depEdges = edges.filter(
|
|
936
|
+
(e) => ["calls", "reads_from", "writes_to", "depends_on"].includes(e.relationship)
|
|
937
|
+
);
|
|
938
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
939
|
+
for (const edge of depEdges) {
|
|
940
|
+
usedIds.add(edge.sourceId);
|
|
941
|
+
usedIds.add(edge.targetId);
|
|
942
|
+
}
|
|
943
|
+
const usedNodes = nodes.filter((n) => usedIds.has(n.id));
|
|
944
|
+
for (const node of usedNodes) {
|
|
945
|
+
const icon = MERMAID_ICONS[node.type] ?? "\u2753";
|
|
946
|
+
lines.push(` ${sanitize(node.id)}["${icon} ${node.name}"]`);
|
|
947
|
+
}
|
|
948
|
+
for (const edge of depEdges) {
|
|
949
|
+
const label = EDGE_LABELS[edge.relationship] ?? "";
|
|
950
|
+
const arrow = label ? `-->|"${label}"|` : "-->";
|
|
951
|
+
lines.push(` ${sanitize(edge.sourceId)} ${arrow} ${sanitize(edge.targetId)}`);
|
|
952
|
+
}
|
|
953
|
+
return lines.join("\n");
|
|
954
|
+
}
|
|
955
|
+
function generateWorkflowMermaid(sop) {
|
|
956
|
+
const lines = ["flowchart TD"];
|
|
957
|
+
for (const step of sop.steps) {
|
|
958
|
+
const nodeId = `S${step.order}`;
|
|
959
|
+
const label = `${step.order}. ${step.instruction.substring(0, 60)}`;
|
|
960
|
+
lines.push(` ${nodeId}["${label}"]`);
|
|
961
|
+
if (step.order > 1) {
|
|
962
|
+
lines.push(` S${step.order - 1} --> ${nodeId}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return lines.join("\n");
|
|
966
|
+
}
|
|
967
|
+
function exportBackstageYAML(nodes, edges, org) {
|
|
968
|
+
const owner = org ?? "unknown";
|
|
969
|
+
const docs = [];
|
|
970
|
+
for (const node of nodes) {
|
|
971
|
+
const isComponent = ["web_service", "container", "pod"].includes(node.type);
|
|
972
|
+
const isAPI = node.type === "api_endpoint";
|
|
973
|
+
const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
|
|
974
|
+
const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
|
|
975
|
+
const doc = [
|
|
976
|
+
`apiVersion: backstage.io/v1alpha1`,
|
|
977
|
+
`kind: ${kind}`,
|
|
978
|
+
`metadata:`,
|
|
979
|
+
` name: ${sanitize(node.id)}`,
|
|
980
|
+
` annotations:`,
|
|
981
|
+
` cartography/discovered-at: "${node.discoveredAt}"`,
|
|
982
|
+
` cartography/confidence: "${node.confidence}"`,
|
|
983
|
+
`spec:`,
|
|
984
|
+
` type: ${node.type}`,
|
|
985
|
+
` lifecycle: production`,
|
|
986
|
+
` owner: ${owner}`,
|
|
987
|
+
...deps.length > 0 ? [" dependsOn:", ...deps] : []
|
|
988
|
+
].join("\n");
|
|
989
|
+
docs.push(doc);
|
|
990
|
+
}
|
|
991
|
+
return docs.join("\n---\n");
|
|
992
|
+
}
|
|
993
|
+
function exportJSON(db, sessionId) {
|
|
994
|
+
const nodes = db.getNodes(sessionId);
|
|
995
|
+
const edges = db.getEdges(sessionId);
|
|
996
|
+
const events = db.getEvents(sessionId);
|
|
997
|
+
const tasks = db.getTasks(sessionId);
|
|
998
|
+
const sops = db.getSOPs(sessionId);
|
|
999
|
+
const stats = db.getStats(sessionId);
|
|
1000
|
+
return JSON.stringify({
|
|
1001
|
+
sessionId,
|
|
1002
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1003
|
+
stats,
|
|
1004
|
+
nodes,
|
|
1005
|
+
edges,
|
|
1006
|
+
events,
|
|
1007
|
+
tasks,
|
|
1008
|
+
sops
|
|
1009
|
+
}, null, 2);
|
|
1010
|
+
}
|
|
1011
|
+
function exportHTML(nodes, edges) {
|
|
1012
|
+
const graphData = JSON.stringify({
|
|
1013
|
+
nodes: nodes.map((n) => ({ id: n.id, name: n.name, type: n.type, confidence: n.confidence })),
|
|
1014
|
+
links: edges.map((e) => ({ source: e.sourceId, target: e.targetId, relationship: e.relationship }))
|
|
1015
|
+
});
|
|
1016
|
+
return `<!DOCTYPE html>
|
|
1017
|
+
<html lang="de">
|
|
1018
|
+
<head>
|
|
1019
|
+
<meta charset="UTF-8">
|
|
1020
|
+
<title>Cartography \u2014 Topology</title>
|
|
1021
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
1022
|
+
<style>
|
|
1023
|
+
body { margin: 0; background: #1a1a2e; color: #eee; font-family: monospace; }
|
|
1024
|
+
svg { width: 100vw; height: 100vh; }
|
|
1025
|
+
.node circle { stroke: #fff; stroke-width: 1.5px; }
|
|
1026
|
+
.node text { font-size: 10px; fill: #eee; }
|
|
1027
|
+
.link { stroke: #666; stroke-opacity: 0.6; }
|
|
1028
|
+
#info { position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7);
|
|
1029
|
+
padding: 10px; border-radius: 4px; font-size: 12px; }
|
|
1030
|
+
</style>
|
|
1031
|
+
</head>
|
|
1032
|
+
<body>
|
|
1033
|
+
<div id="info">
|
|
1034
|
+
<strong>Cartography</strong><br>
|
|
1035
|
+
Nodes: ${nodes.length} | Edges: ${edges.length}<br>
|
|
1036
|
+
<small>Drag to explore</small>
|
|
1037
|
+
</div>
|
|
1038
|
+
<svg></svg>
|
|
1039
|
+
<script>
|
|
1040
|
+
const data = ${graphData};
|
|
1041
|
+
|
|
1042
|
+
const TYPE_COLORS = {
|
|
1043
|
+
host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
|
|
1044
|
+
web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
|
|
1045
|
+
message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
|
|
1046
|
+
container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
|
|
1047
|
+
config_file: '#adb5bd', unknown: '#6c757d',
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const svg = d3.select('svg');
|
|
1051
|
+
const width = window.innerWidth, height = window.innerHeight;
|
|
1052
|
+
const g = svg.append('g');
|
|
1053
|
+
|
|
1054
|
+
svg.call(d3.zoom().on('zoom', e => g.attr('transform', e.transform)));
|
|
1055
|
+
|
|
1056
|
+
const sim = d3.forceSimulation(data.nodes)
|
|
1057
|
+
.force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
|
|
1058
|
+
.force('charge', d3.forceManyBody().strength(-200))
|
|
1059
|
+
.force('center', d3.forceCenter(width / 2, height / 2));
|
|
1060
|
+
|
|
1061
|
+
const link = g.append('g').selectAll('line')
|
|
1062
|
+
.data(data.links).join('line').attr('class', 'link');
|
|
1063
|
+
|
|
1064
|
+
const node = g.append('g').selectAll('g')
|
|
1065
|
+
.data(data.nodes).join('g').attr('class', 'node')
|
|
1066
|
+
.call(d3.drag()
|
|
1067
|
+
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
1068
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
1069
|
+
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
node.append('circle').attr('r', 8).attr('fill', d => TYPE_COLORS[d.type] || '#aaa');
|
|
1073
|
+
node.append('text').attr('dx', 12).attr('dy', '.35em').text(d => d.name);
|
|
1074
|
+
node.append('title').text(d => \`\${d.type}: \${d.id}
|
|
1075
|
+
Confidence: \${d.confidence}\`);
|
|
1076
|
+
|
|
1077
|
+
sim.on('tick', () => {
|
|
1078
|
+
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
1079
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
1080
|
+
node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1081
|
+
});
|
|
1082
|
+
</script>
|
|
1083
|
+
</body>
|
|
1084
|
+
</html>`;
|
|
1085
|
+
}
|
|
1086
|
+
function exportSOPMarkdown(sop) {
|
|
1087
|
+
const lines = [
|
|
1088
|
+
`# ${sop.title}`,
|
|
1089
|
+
"",
|
|
1090
|
+
`**Beschreibung:** ${sop.description}`,
|
|
1091
|
+
`**Systeme:** ${sop.involvedSystems.join(", ")}`,
|
|
1092
|
+
`**Dauer:** ${sop.estimatedDuration}`,
|
|
1093
|
+
`**H\xE4ufigkeit:** ${sop.frequency}`,
|
|
1094
|
+
`**Confidence:** ${sop.confidence.toFixed(2)}`,
|
|
1095
|
+
"",
|
|
1096
|
+
"## Schritte",
|
|
1097
|
+
""
|
|
1098
|
+
];
|
|
1099
|
+
for (const step of sop.steps) {
|
|
1100
|
+
lines.push(`${step.order}. **${step.tool}**${step.target ? ` \u2192 \`${step.target}\`` : ""}`);
|
|
1101
|
+
lines.push(` ${step.instruction}`);
|
|
1102
|
+
if (step.notes) lines.push(` _${step.notes}_`);
|
|
1103
|
+
lines.push("");
|
|
1104
|
+
}
|
|
1105
|
+
return lines.join("\n");
|
|
1106
|
+
}
|
|
1107
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
|
|
1108
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
1109
|
+
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
1110
|
+
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
1111
|
+
const nodes = db.getNodes(sessionId);
|
|
1112
|
+
const edges = db.getEdges(sessionId);
|
|
1113
|
+
if (formats.includes("mermaid")) {
|
|
1114
|
+
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
1115
|
+
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
1116
|
+
process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
|
|
1117
|
+
}
|
|
1118
|
+
if (formats.includes("json")) {
|
|
1119
|
+
writeFileSync(join2(outputDir, "catalog.json"), exportJSON(db, sessionId));
|
|
1120
|
+
process.stderr.write("\u2713 catalog.json\n");
|
|
1121
|
+
}
|
|
1122
|
+
if (formats.includes("yaml")) {
|
|
1123
|
+
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
1124
|
+
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
1125
|
+
}
|
|
1126
|
+
if (formats.includes("html")) {
|
|
1127
|
+
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
1128
|
+
process.stderr.write("\u2713 topology.html\n");
|
|
1129
|
+
}
|
|
1130
|
+
if (formats.includes("sops")) {
|
|
1131
|
+
const sops = db.getSOPs(sessionId);
|
|
1132
|
+
for (const sop of sops) {
|
|
1133
|
+
const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
|
|
1134
|
+
writeFileSync(join2(outputDir, "sops", filename), exportSOPMarkdown(sop));
|
|
1135
|
+
const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
|
|
1136
|
+
writeFileSync(join2(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
|
|
1137
|
+
}
|
|
1138
|
+
if (sops.length > 0) {
|
|
1139
|
+
process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
|
|
1140
|
+
`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/daemon.ts
|
|
1146
|
+
import { execSync as execSync2, spawn } from "child_process";
|
|
1147
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
1148
|
+
|
|
1149
|
+
// src/ipc.ts
|
|
1150
|
+
import net from "net";
|
|
1151
|
+
import { EventEmitter } from "events";
|
|
1152
|
+
import { chmodSync, existsSync as existsSync2, unlinkSync } from "fs";
|
|
1153
|
+
var IPCServer = class extends EventEmitter {
|
|
1154
|
+
server = null;
|
|
1155
|
+
clients = /* @__PURE__ */ new Set();
|
|
1156
|
+
start(socketPath) {
|
|
1157
|
+
this.server = net.createServer((socket) => {
|
|
1158
|
+
this.clients.add(socket);
|
|
1159
|
+
this.emit("client-connect", socket);
|
|
1160
|
+
let buf = "";
|
|
1161
|
+
socket.on("data", (chunk) => {
|
|
1162
|
+
buf += chunk.toString();
|
|
1163
|
+
const lines = buf.split("\n");
|
|
1164
|
+
buf = lines.pop() ?? "";
|
|
1165
|
+
for (const line of lines) {
|
|
1166
|
+
if (!line.trim()) continue;
|
|
1167
|
+
try {
|
|
1168
|
+
const msg = JSON.parse(line);
|
|
1169
|
+
this.emit("message", msg, socket);
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
socket.on("close", () => {
|
|
1175
|
+
this.clients.delete(socket);
|
|
1176
|
+
this.emit("client-disconnect", socket);
|
|
1177
|
+
});
|
|
1178
|
+
socket.on("error", () => {
|
|
1179
|
+
this.clients.delete(socket);
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
this.server.listen(socketPath, () => {
|
|
1183
|
+
try {
|
|
1184
|
+
chmodSync(socketPath, 384);
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
broadcast(msg) {
|
|
1190
|
+
const line = JSON.stringify(msg) + "\n";
|
|
1191
|
+
for (const socket of this.clients) {
|
|
1192
|
+
try {
|
|
1193
|
+
socket.write(line);
|
|
1194
|
+
} catch {
|
|
1195
|
+
this.clients.delete(socket);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
hasClients() {
|
|
1200
|
+
return this.clients.size > 0;
|
|
1201
|
+
}
|
|
1202
|
+
stop() {
|
|
1203
|
+
for (const socket of this.clients) {
|
|
1204
|
+
socket.destroy();
|
|
1205
|
+
}
|
|
1206
|
+
this.clients.clear();
|
|
1207
|
+
this.server?.close();
|
|
1208
|
+
this.server = null;
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
var IPCClient = class extends EventEmitter {
|
|
1212
|
+
socket = null;
|
|
1213
|
+
connect(socketPath) {
|
|
1214
|
+
return new Promise((resolve, reject) => {
|
|
1215
|
+
const socket = net.createConnection(socketPath, () => {
|
|
1216
|
+
resolve();
|
|
1217
|
+
});
|
|
1218
|
+
socket.on("error", (err) => {
|
|
1219
|
+
reject(err);
|
|
1220
|
+
});
|
|
1221
|
+
let buf = "";
|
|
1222
|
+
socket.on("data", (chunk) => {
|
|
1223
|
+
buf += chunk.toString();
|
|
1224
|
+
const lines = buf.split("\n");
|
|
1225
|
+
buf = lines.pop() ?? "";
|
|
1226
|
+
for (const line of lines) {
|
|
1227
|
+
if (!line.trim()) continue;
|
|
1228
|
+
try {
|
|
1229
|
+
const msg = JSON.parse(line);
|
|
1230
|
+
this.emit("message", msg);
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
socket.on("close", () => {
|
|
1236
|
+
this.emit("disconnect");
|
|
1237
|
+
});
|
|
1238
|
+
this.socket = socket;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
send(msg) {
|
|
1242
|
+
if (!this.socket) return;
|
|
1243
|
+
try {
|
|
1244
|
+
this.socket.write(JSON.stringify(msg) + "\n");
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
disconnect() {
|
|
1249
|
+
this.socket?.destroy();
|
|
1250
|
+
this.socket = null;
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
function cleanStaleSocket(socketPath) {
|
|
1254
|
+
if (existsSync2(socketPath)) {
|
|
1255
|
+
try {
|
|
1256
|
+
unlinkSync(socketPath);
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/notify.ts
|
|
1263
|
+
import notifier from "node-notifier";
|
|
1264
|
+
var NotificationService = class {
|
|
1265
|
+
constructor(enabled) {
|
|
1266
|
+
this.enabled = enabled;
|
|
1267
|
+
}
|
|
1268
|
+
nodeDiscovered(nodeId, via) {
|
|
1269
|
+
this.send(`\u{1F4CD} Node entdeckt: ${nodeId}`, `Via: ${via}`);
|
|
1270
|
+
}
|
|
1271
|
+
workflowDetected(count, desc) {
|
|
1272
|
+
this.send(`\u{1F504} ${count} Workflow(s) erkannt`, desc);
|
|
1273
|
+
}
|
|
1274
|
+
taskBoundary(gapMinutes) {
|
|
1275
|
+
this.send("\u23F8 Task-Grenze erkannt", `${gapMinutes} Minuten Inaktivit\xE4t`);
|
|
1276
|
+
}
|
|
1277
|
+
send(title, message) {
|
|
1278
|
+
if (!this.enabled) return;
|
|
1279
|
+
try {
|
|
1280
|
+
notifier.notify({ title, message, sound: false });
|
|
1281
|
+
} catch {
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
// src/daemon.ts
|
|
1287
|
+
function takeSnapshot(config) {
|
|
1288
|
+
const run = (cmd) => {
|
|
1289
|
+
try {
|
|
1290
|
+
return execSync2(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }).toString();
|
|
1291
|
+
} catch {
|
|
1292
|
+
return `(${cmd}: not available)`;
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
const ss = run('ss -tnp 2>/dev/null || ss -tn 2>/dev/null || echo "ss not available"');
|
|
1296
|
+
const ps = run("ps aux --sort=-start_time 2>/dev/null | head -50");
|
|
1297
|
+
let win = "";
|
|
1298
|
+
if (config.trackWindowFocus) {
|
|
1299
|
+
try {
|
|
1300
|
+
win = execSync2("xdotool getactivewindow getwindowname 2>/dev/null", {
|
|
1301
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1302
|
+
timeout: 2e3
|
|
1303
|
+
}).toString().trim();
|
|
1304
|
+
} catch {
|
|
1305
|
+
win = "";
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return `=== TCP ===
|
|
1309
|
+
${ss}
|
|
1310
|
+
=== PS ===
|
|
1311
|
+
${ps}
|
|
1312
|
+
=== Window ===
|
|
1313
|
+
${win}`;
|
|
1314
|
+
}
|
|
1315
|
+
var ShadowDaemon = class {
|
|
1316
|
+
constructor(config, db, ipc, notify) {
|
|
1317
|
+
this.config = config;
|
|
1318
|
+
this.db = db;
|
|
1319
|
+
this.ipc = ipc;
|
|
1320
|
+
this.notify = notify;
|
|
1321
|
+
}
|
|
1322
|
+
running = false;
|
|
1323
|
+
prevSnapshot = "";
|
|
1324
|
+
cyclesRun = 0;
|
|
1325
|
+
cyclesSkipped = 0;
|
|
1326
|
+
async run() {
|
|
1327
|
+
this.running = true;
|
|
1328
|
+
const sessionId = this.db.createSession("shadow", this.config);
|
|
1329
|
+
process.on("SIGTERM", () => this.stop());
|
|
1330
|
+
process.on("SIGINT", () => this.stop());
|
|
1331
|
+
while (this.running) {
|
|
1332
|
+
const snapshot = takeSnapshot(this.config);
|
|
1333
|
+
if (snapshot !== this.prevSnapshot) {
|
|
1334
|
+
try {
|
|
1335
|
+
await runShadowCycle(
|
|
1336
|
+
this.config,
|
|
1337
|
+
this.db,
|
|
1338
|
+
sessionId,
|
|
1339
|
+
this.prevSnapshot,
|
|
1340
|
+
snapshot,
|
|
1341
|
+
(msg) => {
|
|
1342
|
+
if (this.ipc.hasClients()) {
|
|
1343
|
+
this.ipc.broadcast({ type: "agent-output", text: JSON.stringify(msg) });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
this.cyclesRun++;
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
process.stderr.write(`\u26A0 Cycle error: ${err}
|
|
1350
|
+
`);
|
|
1351
|
+
}
|
|
1352
|
+
this.prevSnapshot = snapshot;
|
|
1353
|
+
} else {
|
|
1354
|
+
this.cyclesSkipped++;
|
|
1355
|
+
}
|
|
1356
|
+
const status = this.getStatus(sessionId);
|
|
1357
|
+
this.ipc.broadcast({ type: "status", data: status });
|
|
1358
|
+
if (!this.ipc.hasClients()) {
|
|
1359
|
+
const stats = this.db.getStats(sessionId);
|
|
1360
|
+
if (stats.events > 0 && this.cyclesRun % 10 === 0) {
|
|
1361
|
+
this.notify.workflowDetected(stats.tasks, `${stats.events} events so far`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
await sleep(this.config.pollIntervalMs);
|
|
1365
|
+
}
|
|
1366
|
+
this.db.endSession(sessionId);
|
|
1367
|
+
this.ipc.stop();
|
|
1368
|
+
cleanup(this.config);
|
|
1369
|
+
}
|
|
1370
|
+
stop() {
|
|
1371
|
+
this.running = false;
|
|
1372
|
+
}
|
|
1373
|
+
getStatus(sessionId) {
|
|
1374
|
+
const stats = this.db.getStats(sessionId);
|
|
1375
|
+
return {
|
|
1376
|
+
pid: process.pid,
|
|
1377
|
+
uptime: process.uptime(),
|
|
1378
|
+
nodeCount: stats.nodes,
|
|
1379
|
+
eventCount: stats.events,
|
|
1380
|
+
taskCount: stats.tasks,
|
|
1381
|
+
pendingPrompts: 0,
|
|
1382
|
+
autoSave: this.config.autoSaveNodes,
|
|
1383
|
+
mode: this.config.shadowMode,
|
|
1384
|
+
agentActive: false,
|
|
1385
|
+
cyclesRun: this.cyclesRun,
|
|
1386
|
+
cyclesSkipped: this.cyclesSkipped
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
function forkDaemon(config) {
|
|
1391
|
+
const child = spawn(
|
|
1392
|
+
process.execPath,
|
|
1393
|
+
[process.argv[1] ?? "cartography", "shadow", "start", "--foreground", "--daemon-child"],
|
|
1394
|
+
{
|
|
1395
|
+
detached: true,
|
|
1396
|
+
stdio: "ignore",
|
|
1397
|
+
env: {
|
|
1398
|
+
...process.env,
|
|
1399
|
+
CARTOGRAPHYY_DAEMON: "1",
|
|
1400
|
+
CARTOGRAPHYY_CONFIG: JSON.stringify(config)
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
);
|
|
1404
|
+
child.unref();
|
|
1405
|
+
const pid = child.pid;
|
|
1406
|
+
if (!pid) throw new Error("Failed to fork daemon");
|
|
1407
|
+
writeFileSync2(config.pidFile, String(pid), "utf8");
|
|
1408
|
+
return pid;
|
|
1409
|
+
}
|
|
1410
|
+
function isDaemonRunning(pidFile) {
|
|
1411
|
+
if (!existsSync3(pidFile)) return { running: false };
|
|
1412
|
+
try {
|
|
1413
|
+
const pid = parseInt(readFileSync2(pidFile, "utf8").trim(), 10);
|
|
1414
|
+
if (isNaN(pid)) return { running: false };
|
|
1415
|
+
process.kill(pid, 0);
|
|
1416
|
+
return { running: true, pid };
|
|
1417
|
+
} catch {
|
|
1418
|
+
try {
|
|
1419
|
+
unlinkSync2(pidFile);
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
return { running: false };
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function stopDaemon(pidFile) {
|
|
1426
|
+
const { running, pid } = isDaemonRunning(pidFile);
|
|
1427
|
+
if (!running || !pid) return false;
|
|
1428
|
+
try {
|
|
1429
|
+
process.kill(pid, "SIGTERM");
|
|
1430
|
+
try {
|
|
1431
|
+
unlinkSync2(pidFile);
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
return true;
|
|
1435
|
+
} catch {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function cleanup(config) {
|
|
1440
|
+
try {
|
|
1441
|
+
unlinkSync2(config.socketPath);
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
unlinkSync2(config.pidFile);
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async function startDaemonProcess(config) {
|
|
1450
|
+
cleanStaleSocket(config.socketPath);
|
|
1451
|
+
const db = new CartographyDB(config.dbPath);
|
|
1452
|
+
const ipc = new IPCServer();
|
|
1453
|
+
const notify = new NotificationService(config.enableNotifications);
|
|
1454
|
+
ipc.start(config.socketPath);
|
|
1455
|
+
const daemon = new ShadowDaemon(config, db, ipc, notify);
|
|
1456
|
+
await daemon.run();
|
|
1457
|
+
db.close();
|
|
1458
|
+
}
|
|
1459
|
+
function sleep(ms) {
|
|
1460
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/client.ts
|
|
1464
|
+
var ForegroundClient = class {
|
|
1465
|
+
async run(config) {
|
|
1466
|
+
process.stderr.write("\u{1F441} Cartography Shadow (foreground) gestartet\n");
|
|
1467
|
+
process.stderr.write(` Intervall: ${config.pollIntervalMs / 1e3}s | Modell: ${config.shadowModel}
|
|
1468
|
+
`);
|
|
1469
|
+
process.stderr.write(" Ctrl+C zum Beenden\n\n");
|
|
1470
|
+
await startDaemonProcess({ ...config, shadowMode: "foreground" });
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
var AttachClient = class {
|
|
1474
|
+
async attach(socketPath) {
|
|
1475
|
+
const client = new IPCClient();
|
|
1476
|
+
try {
|
|
1477
|
+
await client.connect(socketPath);
|
|
1478
|
+
} catch {
|
|
1479
|
+
process.stderr.write(`\u274C Kann nicht an Daemon ankoppeln: ${socketPath}
|
|
1480
|
+
`);
|
|
1481
|
+
process.stderr.write(" Ist der Daemon gestartet? cartography shadow status\n");
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
process.stderr.write("\u{1F4E1} Verbunden mit Shadow-Daemon\n");
|
|
1486
|
+
process.stderr.write(" [T] Neuer Task [S] Status [D] Trennen [Q] Daemon stoppen\n\n");
|
|
1487
|
+
if (process.stdin.isTTY) {
|
|
1488
|
+
process.stdin.setRawMode(true);
|
|
1489
|
+
}
|
|
1490
|
+
process.stdin.resume();
|
|
1491
|
+
process.stdin.setEncoding("utf8");
|
|
1492
|
+
process.stdin.on("data", (key) => {
|
|
1493
|
+
const k = key.toLowerCase();
|
|
1494
|
+
if (k === "t") {
|
|
1495
|
+
process.stdout.write("\nTask-Beschreibung: ");
|
|
1496
|
+
process.stdin.once("data", (desc) => {
|
|
1497
|
+
client.send({ type: "task-description", description: desc.trim() });
|
|
1498
|
+
client.send({ type: "command", command: "new-task" });
|
|
1499
|
+
process.stdout.write(`
|
|
1500
|
+
\u2713 Neuer Task gestartet: ${desc.trim()}
|
|
1501
|
+
`);
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
if (k === "s") {
|
|
1506
|
+
client.send({ type: "command", command: "status" });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
if (k === "d" || k === "") {
|
|
1510
|
+
process.stderr.write("\n\u{1F4E1} Getrennt. Daemon l\xE4uft weiter.\n");
|
|
1511
|
+
client.disconnect();
|
|
1512
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1513
|
+
process.stdin.pause();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (k === "q") {
|
|
1517
|
+
client.send({ type: "command", command: "stop" });
|
|
1518
|
+
process.stderr.write("\n\u{1F6D1} Daemon wird gestoppt...\n");
|
|
1519
|
+
setTimeout(() => {
|
|
1520
|
+
client.disconnect();
|
|
1521
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1522
|
+
process.stdin.pause();
|
|
1523
|
+
}, 1e3);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
client.on("message", (msg) => {
|
|
1528
|
+
switch (msg.type) {
|
|
1529
|
+
case "status":
|
|
1530
|
+
renderStatus(msg.data);
|
|
1531
|
+
break;
|
|
1532
|
+
case "event":
|
|
1533
|
+
process.stdout.write(
|
|
1534
|
+
` [${new Date(msg.data.timestamp).toLocaleTimeString()}] ${msg.data.eventType} ${msg.data.process}` + (msg.data.target ? ` \u2192 ${msg.data.target}` : "") + "\n"
|
|
1535
|
+
);
|
|
1536
|
+
break;
|
|
1537
|
+
case "agent-output":
|
|
1538
|
+
if (msg.text) process.stdout.write(` \u{1F916} ${msg.text}
|
|
1539
|
+
`);
|
|
1540
|
+
break;
|
|
1541
|
+
case "info":
|
|
1542
|
+
process.stdout.write(` \u2139 ${msg.message}
|
|
1543
|
+
`);
|
|
1544
|
+
break;
|
|
1545
|
+
case "prompt":
|
|
1546
|
+
renderPrompt(msg.prompt.kind, msg.prompt.options, (answer) => {
|
|
1547
|
+
client.send({ type: "prompt-response", id: msg.id, answer });
|
|
1548
|
+
});
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
client.on("disconnect", () => {
|
|
1553
|
+
process.stderr.write("\n\u26A0 Verbindung zum Daemon verloren\n");
|
|
1554
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1555
|
+
process.stdin.pause();
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
function renderStatus(status) {
|
|
1560
|
+
process.stdout.write(
|
|
1561
|
+
`
|
|
1562
|
+
\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
|
|
1563
|
+
PID: ${status.pid} | Uptime: ${Math.round(status.uptime)}s
|
|
1564
|
+
Nodes: ${status.nodeCount} | Events: ${status.eventCount} | Tasks: ${status.taskCount}
|
|
1565
|
+
Cycles: ${status.cyclesRun} run, ${status.cyclesSkipped} skipped
|
|
1566
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1567
|
+
`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
function renderPrompt(kind, options, callback) {
|
|
1571
|
+
process.stdout.write(`
|
|
1572
|
+
\u2753 ${kind}
|
|
1573
|
+
`);
|
|
1574
|
+
options.forEach((opt, i) => process.stdout.write(` [${i + 1}] ${opt}
|
|
1575
|
+
`));
|
|
1576
|
+
process.stdout.write("Antwort: ");
|
|
1577
|
+
process.stdin.once("data", (data) => {
|
|
1578
|
+
const idx = parseInt(data.trim(), 10) - 1;
|
|
1579
|
+
callback(options[idx] ?? options[0] ?? "");
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/cli.ts
|
|
1584
|
+
if (process.env.CARTOGRAPHYY_DAEMON === "1") {
|
|
1585
|
+
const config = JSON.parse(process.env.CARTOGRAPHYY_CONFIG ?? "{}");
|
|
1586
|
+
startDaemonProcess(config).catch((err) => {
|
|
1587
|
+
process.stderr.write(`Daemon fatal: ${err}
|
|
1588
|
+
`);
|
|
1589
|
+
process.exitCode = 1;
|
|
1590
|
+
});
|
|
1591
|
+
} else {
|
|
1592
|
+
main();
|
|
1593
|
+
}
|
|
1594
|
+
function main() {
|
|
1595
|
+
const program = new Command();
|
|
1596
|
+
program.name("cartography").description("AI-powered Infrastructure Cartography & SOP Generation").version("0.1.0");
|
|
1597
|
+
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", "./cartography-output").option("--db <path>", "DB-Pfad").option("-v, --verbose", "Agent-Reasoning anzeigen", false).action(async (opts) => {
|
|
1598
|
+
checkPrerequisites();
|
|
1599
|
+
const config = defaultConfig({
|
|
1600
|
+
mode: "discover",
|
|
1601
|
+
entryPoints: opts.entry,
|
|
1602
|
+
maxDepth: parseInt(opts.depth, 10),
|
|
1603
|
+
maxTurns: parseInt(opts.maxTurns, 10),
|
|
1604
|
+
agentModel: opts.model,
|
|
1605
|
+
organization: opts.org,
|
|
1606
|
+
outputDir: opts.output,
|
|
1607
|
+
...opts.db ? { dbPath: opts.db } : {},
|
|
1608
|
+
verbose: opts.verbose
|
|
1609
|
+
});
|
|
1610
|
+
const db = new CartographyDB(config.dbPath);
|
|
1611
|
+
const sessionId = db.createSession("discover", config);
|
|
1612
|
+
process.stderr.write(`\u{1F50D} Scanning ${config.entryPoints.join(", ")}...
|
|
1613
|
+
`);
|
|
1614
|
+
process.stderr.write(` Model: ${config.agentModel} | MaxTurns: ${config.maxTurns}
|
|
1615
|
+
|
|
1616
|
+
`);
|
|
1617
|
+
try {
|
|
1618
|
+
await runDiscovery(config, db, sessionId, (text) => {
|
|
1619
|
+
if (config.verbose) process.stdout.write(text + "\n");
|
|
1620
|
+
});
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
process.stderr.write(`\u274C Discovery fehlgeschlagen: ${err}
|
|
1623
|
+
`);
|
|
1624
|
+
db.close();
|
|
1625
|
+
process.exitCode = 1;
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
db.endSession(sessionId);
|
|
1629
|
+
const stats = db.getStats(sessionId);
|
|
1630
|
+
process.stderr.write(`
|
|
1631
|
+
\u2713 ${stats.nodes} nodes, ${stats.edges} edges discovered
|
|
1632
|
+
`);
|
|
1633
|
+
exportAll(db, sessionId, config.outputDir);
|
|
1634
|
+
process.stderr.write(`\u2713 Exported to: ${config.outputDir}
|
|
1635
|
+
`);
|
|
1636
|
+
db.close();
|
|
1637
|
+
});
|
|
1638
|
+
const shadow = program.command("shadow").description("Shadow-Daemon verwalten");
|
|
1639
|
+
shadow.command("start").description("Shadow-Daemon starten").option("--interval <ms>", "Poll-Intervall in ms", "30000").option("--inactivity <ms>", "Task-Grenze in ms", "300000").option("--track-windows", "Fenster-Focus tracken", false).option("--auto-save", "Nodes ohne R\xFCckfrage speichern", false).option("--no-notifications", "Desktop-Notifications deaktivieren").option("--model <m>", "Analysis-Model", "claude-haiku-4-5-20251001").option("--foreground", "Kein Daemon, im Terminal bleiben", false).option("--db <path>", "DB-Pfad").option("--daemon-child", "Internal: marks this as a daemon child process").action(async (opts) => {
|
|
1640
|
+
checkPrerequisites();
|
|
1641
|
+
const intervalMs = checkPollInterval(parseInt(opts.interval, 10));
|
|
1642
|
+
const config = defaultConfig({
|
|
1643
|
+
mode: "shadow",
|
|
1644
|
+
shadowMode: opts.foreground ? "foreground" : "daemon",
|
|
1645
|
+
pollIntervalMs: intervalMs,
|
|
1646
|
+
inactivityTimeoutMs: parseInt(opts.inactivity, 10),
|
|
1647
|
+
trackWindowFocus: opts.trackWindows,
|
|
1648
|
+
autoSaveNodes: opts.autoSave,
|
|
1649
|
+
enableNotifications: opts.notifications !== false,
|
|
1650
|
+
shadowModel: opts.model,
|
|
1651
|
+
...opts.db ? { dbPath: opts.db } : {}
|
|
1652
|
+
});
|
|
1653
|
+
const { running } = isDaemonRunning(config.pidFile);
|
|
1654
|
+
if (running) {
|
|
1655
|
+
process.stderr.write("\u274C Shadow-Daemon l\xE4uft bereits. cartography shadow status\n");
|
|
1656
|
+
process.exitCode = 1;
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (opts.foreground) {
|
|
1660
|
+
const client = new ForegroundClient();
|
|
1661
|
+
await client.run(config);
|
|
1662
|
+
} else {
|
|
1663
|
+
const pid = forkDaemon(config);
|
|
1664
|
+
process.stderr.write(`\u{1F441} Shadow daemon started (PID ${pid})
|
|
1665
|
+
`);
|
|
1666
|
+
process.stderr.write(` Intervall: ${intervalMs / 1e3}s | Modell: ${config.shadowModel}
|
|
1667
|
+
`);
|
|
1668
|
+
process.stderr.write(" cartography shadow attach \u2014 ankoppeln\n");
|
|
1669
|
+
process.stderr.write(" cartography shadow stop \u2014 stoppen\n\n");
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
shadow.command("stop").description("Shadow-Daemon stoppen").action(() => {
|
|
1673
|
+
const config = defaultConfig();
|
|
1674
|
+
const stopped = stopDaemon(config.pidFile);
|
|
1675
|
+
if (stopped) {
|
|
1676
|
+
process.stderr.write("\u2713 Shadow-Daemon gestoppt\n");
|
|
1677
|
+
} else {
|
|
1678
|
+
process.stderr.write("\u26A0 Kein laufender Shadow-Daemon gefunden\n");
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
shadow.command("status").description("Shadow-Daemon Status anzeigen").action(() => {
|
|
1682
|
+
const config = defaultConfig();
|
|
1683
|
+
const { running, pid } = isDaemonRunning(config.pidFile);
|
|
1684
|
+
if (running) {
|
|
1685
|
+
process.stdout.write(`\u2713 Shadow-Daemon l\xE4uft (PID ${pid})
|
|
1686
|
+
`);
|
|
1687
|
+
process.stdout.write(` Socket: ${config.socketPath}
|
|
1688
|
+
`);
|
|
1689
|
+
} else {
|
|
1690
|
+
process.stdout.write("\u2717 Shadow-Daemon gestoppt\n");
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
shadow.command("attach").description("An laufenden Shadow-Daemon ankoppeln").action(async () => {
|
|
1694
|
+
const config = defaultConfig();
|
|
1695
|
+
const client = new AttachClient();
|
|
1696
|
+
await client.attach(config.socketPath);
|
|
1697
|
+
});
|
|
1698
|
+
program.command("sops [session-id]").description("SOPs aus beobachteten Workflows generieren").action(async (sessionId) => {
|
|
1699
|
+
checkPrerequisites();
|
|
1700
|
+
const config = defaultConfig();
|
|
1701
|
+
const db = new CartographyDB(config.dbPath);
|
|
1702
|
+
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession("shadow");
|
|
1703
|
+
if (!session) {
|
|
1704
|
+
process.stderr.write("\u274C Keine Shadow-Session gefunden. cartography shadow start\n");
|
|
1705
|
+
db.close();
|
|
1706
|
+
process.exitCode = 1;
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
process.stderr.write(`\u{1F504} Generiere SOPs aus Session ${session.id}...
|
|
1710
|
+
`);
|
|
1711
|
+
const count = await generateSOPs(db, session.id);
|
|
1712
|
+
process.stderr.write(`\u2713 ${count} SOPs generiert
|
|
1713
|
+
`);
|
|
1714
|
+
db.close();
|
|
1715
|
+
});
|
|
1716
|
+
program.command("export [session-id]").description("Alle Outputs generieren").option("-o, --output <dir>", "Output-Dir", "./cartography-output").option("--format <fmt...>", "Formate: mermaid,json,yaml,html,sops").action((sessionId, opts) => {
|
|
1717
|
+
const config = defaultConfig({ outputDir: opts.output });
|
|
1718
|
+
const db = new CartographyDB(config.dbPath);
|
|
1719
|
+
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
1720
|
+
if (!session) {
|
|
1721
|
+
process.stderr.write("\u274C Keine Session gefunden\n");
|
|
1722
|
+
db.close();
|
|
1723
|
+
process.exitCode = 1;
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "sops"];
|
|
1727
|
+
exportAll(db, session.id, opts.output, formats);
|
|
1728
|
+
process.stderr.write(`\u2713 Exported to: ${opts.output}
|
|
1729
|
+
`);
|
|
1730
|
+
db.close();
|
|
1731
|
+
});
|
|
1732
|
+
program.command("show [session-id]").description("Session-Details anzeigen").action((sessionId) => {
|
|
1733
|
+
const config = defaultConfig();
|
|
1734
|
+
const db = new CartographyDB(config.dbPath);
|
|
1735
|
+
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
1736
|
+
if (!session) {
|
|
1737
|
+
process.stderr.write("\u274C Keine Session gefunden\n");
|
|
1738
|
+
db.close();
|
|
1739
|
+
process.exitCode = 1;
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const stats = db.getStats(session.id);
|
|
1743
|
+
const nodes = db.getNodes(session.id);
|
|
1744
|
+
process.stdout.write(`
|
|
1745
|
+
Session: ${session.id}
|
|
1746
|
+
`);
|
|
1747
|
+
process.stdout.write(` Mode: ${session.mode}
|
|
1748
|
+
`);
|
|
1749
|
+
process.stdout.write(` Started: ${session.startedAt}
|
|
1750
|
+
`);
|
|
1751
|
+
if (session.completedAt) process.stdout.write(` Ended: ${session.completedAt}
|
|
1752
|
+
`);
|
|
1753
|
+
process.stdout.write(` Nodes: ${stats.nodes}
|
|
1754
|
+
`);
|
|
1755
|
+
process.stdout.write(` Edges: ${stats.edges}
|
|
1756
|
+
`);
|
|
1757
|
+
process.stdout.write(` Events: ${stats.events}
|
|
1758
|
+
`);
|
|
1759
|
+
process.stdout.write(` Tasks: ${stats.tasks}
|
|
1760
|
+
`);
|
|
1761
|
+
if (nodes.length > 0) {
|
|
1762
|
+
process.stdout.write("\n Discovered nodes:\n");
|
|
1763
|
+
for (const node of nodes.slice(0, 20)) {
|
|
1764
|
+
process.stdout.write(` ${node.id} (${node.type}, confidence: ${node.confidence})
|
|
1765
|
+
`);
|
|
1766
|
+
}
|
|
1767
|
+
if (nodes.length > 20) {
|
|
1768
|
+
process.stdout.write(` ... and ${nodes.length - 20} more
|
|
1769
|
+
`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
process.stdout.write("\n");
|
|
1773
|
+
db.close();
|
|
1774
|
+
});
|
|
1775
|
+
program.command("sessions").description("Alle Sessions auflisten").action(() => {
|
|
1776
|
+
const config = defaultConfig();
|
|
1777
|
+
const db = new CartographyDB(config.dbPath);
|
|
1778
|
+
const sessions = db.getSessions();
|
|
1779
|
+
if (sessions.length === 0) {
|
|
1780
|
+
process.stdout.write("Keine Sessions gefunden\n");
|
|
1781
|
+
db.close();
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
for (const session of sessions) {
|
|
1785
|
+
const stats = db.getStats(session.id);
|
|
1786
|
+
const status = session.completedAt ? "\u2713" : "\u25CF";
|
|
1787
|
+
process.stdout.write(
|
|
1788
|
+
`${status} ${session.id.substring(0, 8)} [${session.mode}] ${session.startedAt.substring(0, 19)} nodes:${stats.nodes} edges:${stats.edges}
|
|
1789
|
+
`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
db.close();
|
|
1793
|
+
});
|
|
1794
|
+
program.command("docs").description("Alle Features und Befehle auf einen Blick").action(() => {
|
|
1795
|
+
const out = process.stdout.write.bind(process.stdout);
|
|
1796
|
+
const b = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
1797
|
+
const dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
1798
|
+
const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
1799
|
+
const green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
1800
|
+
const yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
1801
|
+
const line = () => out(dim("\u2500".repeat(60)) + "\n");
|
|
1802
|
+
out("\n");
|
|
1803
|
+
out(b(" CARTOGRAPHY") + " " + dim("v0.1.0") + "\n");
|
|
1804
|
+
out(dim(" AI-powered Infrastructure Cartography & SOP Generation\n"));
|
|
1805
|
+
out("\n");
|
|
1806
|
+
line();
|
|
1807
|
+
out(b(cyan(" DISCOVERY\n")));
|
|
1808
|
+
out("\n");
|
|
1809
|
+
out(` ${green("cartography discover")}
|
|
1810
|
+
`);
|
|
1811
|
+
out(` Scannt die lokale Infrastruktur (Claude Sonnet).
|
|
1812
|
+
`);
|
|
1813
|
+
out(` Claude f\xFChrt eigenst\xE4ndig ss, ps, curl, docker inspect, kubectl get
|
|
1814
|
+
`);
|
|
1815
|
+
out(` aus und speichert alles in SQLite.
|
|
1816
|
+
`);
|
|
1817
|
+
out("\n");
|
|
1818
|
+
out(dim(" Optionen:\n"));
|
|
1819
|
+
out(dim(" --entry <hosts...> Startpunkte (default: localhost)\n"));
|
|
1820
|
+
out(dim(" --depth <n> Max Tiefe (default: 8)\n"));
|
|
1821
|
+
out(dim(" --max-turns <n> Max Agent-Turns (default: 50)\n"));
|
|
1822
|
+
out(dim(" --model <m> Model (default: claude-sonnet-4-5-...)\n"));
|
|
1823
|
+
out(dim(" --org <name> Organisation f\xFCr Backstage YAML\n"));
|
|
1824
|
+
out(dim(" -o, --output <dir> Output-Verzeichnis (default: ./cartography-output)\n"));
|
|
1825
|
+
out(dim(" -v, --verbose Agent-Reasoning anzeigen\n"));
|
|
1826
|
+
out("\n");
|
|
1827
|
+
out(dim(" Output:\n"));
|
|
1828
|
+
out(dim(" cartography-output/\n"));
|
|
1829
|
+
out(dim(" catalog.json Maschinenlesbarer Komplett-Dump\n"));
|
|
1830
|
+
out(dim(" catalog-info.yaml Backstage Service-Katalog\n"));
|
|
1831
|
+
out(dim(" topology.mermaid Infrastruktur-Topologie (graph TB)\n"));
|
|
1832
|
+
out(dim(" dependencies.mermaid Service-Dependencies (graph LR)\n"));
|
|
1833
|
+
out(dim(" topology.html Interaktiver D3.js Force-Graph\n"));
|
|
1834
|
+
out(dim(" sops/ Generierte SOPs als Markdown\n"));
|
|
1835
|
+
out(dim(" workflows/ Workflow-Flowcharts als Mermaid\n"));
|
|
1836
|
+
out("\n");
|
|
1837
|
+
line();
|
|
1838
|
+
out(b(cyan(" SHADOW DAEMON\n")));
|
|
1839
|
+
out("\n");
|
|
1840
|
+
out(` ${green("cartography shadow start")}
|
|
1841
|
+
`);
|
|
1842
|
+
out(` Startet einen Background-Daemon, der alle 30s einen System-Snapshot
|
|
1843
|
+
`);
|
|
1844
|
+
out(` nimmt (ss + ps). Nur bei \xC4nderung ruft er Claude Haiku auf.
|
|
1845
|
+
`);
|
|
1846
|
+
out("\n");
|
|
1847
|
+
out(dim(" Optionen:\n"));
|
|
1848
|
+
out(dim(" --interval <ms> Poll-Intervall (default: 30000, min: 15000)\n"));
|
|
1849
|
+
out(dim(" --inactivity <ms> Task-Grenze (default: 300000 = 5 min)\n"));
|
|
1850
|
+
out(dim(" --model <m> Analysis-Model (default: claude-haiku-4-5-...)\n"));
|
|
1851
|
+
out(dim(" --track-windows Fenster-Focus tracken (ben\xF6tigt xdotool)\n"));
|
|
1852
|
+
out(dim(" --auto-save Nodes ohne R\xFCckfrage speichern\n"));
|
|
1853
|
+
out(dim(" --no-notifications Desktop-Notifications deaktivieren\n"));
|
|
1854
|
+
out(dim(" --foreground Kein Daemon, im Terminal bleiben\n"));
|
|
1855
|
+
out("\n");
|
|
1856
|
+
out(` ${green("cartography shadow stop")} ${dim("Daemon per SIGTERM beenden")}
|
|
1857
|
+
`);
|
|
1858
|
+
out(` ${green("cartography shadow status")} ${dim("PID + Socket-Pfad anzeigen")}
|
|
1859
|
+
`);
|
|
1860
|
+
out(` ${green("cartography shadow attach")} ${dim("Live-Events im Terminal, Hotkeys: [T] [S] [D] [Q]")}
|
|
1861
|
+
`);
|
|
1862
|
+
out("\n");
|
|
1863
|
+
out(dim(" Hotkeys im Attach-Modus:\n"));
|
|
1864
|
+
out(dim(" [T] Neuen Task starten (mit Beschreibung)\n"));
|
|
1865
|
+
out(dim(" [S] Status-Dump anzeigen (Nodes, Events, Tasks, Cycles)\n"));
|
|
1866
|
+
out(dim(" [D] Trennen \u2014 Daemon l\xE4uft weiter\n"));
|
|
1867
|
+
out(dim(" [Q] Daemon stoppen und beenden\n"));
|
|
1868
|
+
out("\n");
|
|
1869
|
+
line();
|
|
1870
|
+
out(b(cyan(" ANALYSE & EXPORT\n")));
|
|
1871
|
+
out("\n");
|
|
1872
|
+
out(` ${green("cartography sops [session-id]")}
|
|
1873
|
+
`);
|
|
1874
|
+
out(` Clustert abgeschlossene Tasks und generiert SOPs via Claude Sonnet.
|
|
1875
|
+
`);
|
|
1876
|
+
out(` Nutzt die Anthropic Messages API (kein Agent-Loop, ein Request pro Cluster).
|
|
1877
|
+
`);
|
|
1878
|
+
out("\n");
|
|
1879
|
+
out(` ${green("cartography export [session-id]")}
|
|
1880
|
+
`);
|
|
1881
|
+
out(dim(" --format <fmt...> mermaid, json, yaml, html, sops (default: alle)\n"));
|
|
1882
|
+
out(dim(" -o, --output <dir> Output-Verzeichnis\n"));
|
|
1883
|
+
out("\n");
|
|
1884
|
+
out(` ${green("cartography show [session-id]")} ${dim("Session-Details + Node-Liste")}
|
|
1885
|
+
`);
|
|
1886
|
+
out(` ${green("cartography sessions")} ${dim("Alle Sessions tabellarisch auflisten")}
|
|
1887
|
+
`);
|
|
1888
|
+
out("\n");
|
|
1889
|
+
line();
|
|
1890
|
+
out(b(cyan(" KOSTEN (Richtwerte)\n")));
|
|
1891
|
+
out("\n");
|
|
1892
|
+
out(yellow(" Modus Model Intervall pro Stunde pro 8h-Tag\n"));
|
|
1893
|
+
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"));
|
|
1894
|
+
out(` Discovery Sonnet einmalig $0.15\u20130.50 einmalig
|
|
1895
|
+
`);
|
|
1896
|
+
out(` Shadow Haiku 30s $0.12\u20130.36 $0.96\u20132.88
|
|
1897
|
+
`);
|
|
1898
|
+
out(` Shadow Haiku 60s $0.06\u20130.18 $0.48\u20131.44
|
|
1899
|
+
`);
|
|
1900
|
+
out(` Shadow (ruhig) Haiku 30s ~$0.02 ~$0.16
|
|
1901
|
+
`);
|
|
1902
|
+
out(` SOP-Gen Sonnet einmalig $0.01\u20130.03 einmalig
|
|
1903
|
+
`);
|
|
1904
|
+
out("\n");
|
|
1905
|
+
out(dim(' * "ruhig" = Diff-Check \xFCberspringt 90%+ Cycles, wenn System unver\xE4ndert\n'));
|
|
1906
|
+
out("\n");
|
|
1907
|
+
line();
|
|
1908
|
+
out(b(cyan(" ARCHITEKTUR\n")));
|
|
1909
|
+
out("\n");
|
|
1910
|
+
out(dim(" CLI (Commander)\n"));
|
|
1911
|
+
out(dim(" \u2514\u2500\u2500 Preflight: Claude CLI check + API key + Intervall-Validierung\n"));
|
|
1912
|
+
out(dim(" \u2514\u2500\u2500 Agent Orchestrator (agent.ts)\n"));
|
|
1913
|
+
out(dim(" \u251C\u2500\u2500 runDiscovery() \u2192 Claude Sonnet + Bash + MCP Tools\n"));
|
|
1914
|
+
out(dim(" \u251C\u2500\u2500 runShadowCycle() \u2192 Claude Haiku + nur MCP Tools (kein Bash!)\n"));
|
|
1915
|
+
out(dim(" \u2514\u2500\u2500 generateSOPs() \u2192 Anthropic Messages API (kein Agent-Loop)\n"));
|
|
1916
|
+
out(dim(" \u2514\u2500\u2500 Custom MCP Tools (tools.ts)\n"));
|
|
1917
|
+
out(dim(" save_node, save_edge, save_event,\n"));
|
|
1918
|
+
out(dim(" get_catalog, manage_task, save_sop\n"));
|
|
1919
|
+
out(dim(" \u2514\u2500\u2500 CartographyDB (SQLite WAL)\n"));
|
|
1920
|
+
out(dim(" Shadow Daemon (daemon.ts)\n"));
|
|
1921
|
+
out(dim(" \u251C\u2500\u2500 takeSnapshot() \u2192 ss + ps [kein Claude!]\n"));
|
|
1922
|
+
out(dim(" \u251C\u2500\u2500 Diff-Check \u2192 nur bei \xC4nderung: runShadowCycle()\n"));
|
|
1923
|
+
out(dim(" \u251C\u2500\u2500 IPC Server (Unix Socket ~/.cartography/daemon.sock)\n"));
|
|
1924
|
+
out(dim(" \u2514\u2500\u2500 NotificationService (Desktop wenn kein Client attached)\n"));
|
|
1925
|
+
out("\n");
|
|
1926
|
+
line();
|
|
1927
|
+
out(b(cyan(" SETUP\n")));
|
|
1928
|
+
out("\n");
|
|
1929
|
+
out(dim(" # 1. Claude CLI (Runtime-Dependency)\n"));
|
|
1930
|
+
out(" npm install -g @anthropic-ai/claude-code\n");
|
|
1931
|
+
out(" claude login\n");
|
|
1932
|
+
out("\n");
|
|
1933
|
+
out(dim(" # 2. API Key (falls nicht via claude login)\n"));
|
|
1934
|
+
out(" export ANTHROPIC_API_KEY=sk-ant-...\n");
|
|
1935
|
+
out("\n");
|
|
1936
|
+
out(dim(" # 3. Los\n"));
|
|
1937
|
+
out(" cartography discover\n");
|
|
1938
|
+
out(" cartography shadow start\n");
|
|
1939
|
+
out("\n");
|
|
1940
|
+
out(dim(" Daten: ~/.cartography/cartography.db\n"));
|
|
1941
|
+
out(dim(" Socket: ~/.cartography/daemon.sock\n"));
|
|
1942
|
+
out(dim(" PID: ~/.cartography/daemon.pid\n"));
|
|
1943
|
+
out("\n");
|
|
1944
|
+
});
|
|
1945
|
+
program.exitOverride((err) => {
|
|
1946
|
+
if (err.code === "commander.helpDisplayed") {
|
|
1947
|
+
process.exitCode = 0;
|
|
1948
|
+
} else {
|
|
1949
|
+
process.exitCode = 2;
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
program.parse(process.argv);
|
|
1953
|
+
}
|
|
1954
|
+
//# sourceMappingURL=cli.js.map
|