@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/index.js
ADDED
|
@@ -0,0 +1,1161 @@
|
|
|
1
|
+
// src/db.ts
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
var SCHEMA = `
|
|
6
|
+
PRAGMA journal_mode = WAL;
|
|
7
|
+
PRAGMA foreign_keys = ON;
|
|
8
|
+
PRAGMA busy_timeout = 5000;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
mode TEXT NOT NULL CHECK (mode IN ('discover','shadow')),
|
|
13
|
+
started_at TEXT NOT NULL,
|
|
14
|
+
completed_at TEXT,
|
|
15
|
+
config TEXT NOT NULL DEFAULT '{}'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
19
|
+
id TEXT NOT NULL,
|
|
20
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
21
|
+
type TEXT NOT NULL,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
discovered_via TEXT,
|
|
24
|
+
discovered_at TEXT NOT NULL,
|
|
25
|
+
path_id TEXT,
|
|
26
|
+
depth INTEGER DEFAULT 0,
|
|
27
|
+
confidence REAL DEFAULT 0.5,
|
|
28
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
29
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
30
|
+
PRIMARY KEY (id, session_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
36
|
+
source_id TEXT NOT NULL,
|
|
37
|
+
target_id TEXT NOT NULL,
|
|
38
|
+
relationship TEXT NOT NULL,
|
|
39
|
+
evidence TEXT,
|
|
40
|
+
confidence REAL DEFAULT 0.5,
|
|
41
|
+
discovered_at TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS activity_events (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
47
|
+
task_id TEXT,
|
|
48
|
+
timestamp TEXT NOT NULL,
|
|
49
|
+
event_type TEXT NOT NULL,
|
|
50
|
+
process TEXT NOT NULL,
|
|
51
|
+
pid INTEGER NOT NULL,
|
|
52
|
+
target TEXT,
|
|
53
|
+
target_type TEXT,
|
|
54
|
+
port INTEGER,
|
|
55
|
+
duration_ms INTEGER
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
61
|
+
description TEXT,
|
|
62
|
+
started_at TEXT NOT NULL,
|
|
63
|
+
completed_at TEXT,
|
|
64
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
65
|
+
involved_services TEXT NOT NULL DEFAULT '[]',
|
|
66
|
+
status TEXT DEFAULT 'active' CHECK (status IN ('active','completed','cancelled')),
|
|
67
|
+
is_sop_candidate INTEGER DEFAULT 0
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
73
|
+
name TEXT,
|
|
74
|
+
pattern TEXT NOT NULL,
|
|
75
|
+
task_ids TEXT NOT NULL DEFAULT '[]',
|
|
76
|
+
occurrences INTEGER DEFAULT 1,
|
|
77
|
+
first_seen TEXT NOT NULL,
|
|
78
|
+
last_seen TEXT NOT NULL,
|
|
79
|
+
avg_duration_ms INTEGER,
|
|
80
|
+
involved_services TEXT NOT NULL DEFAULT '[]'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS sops (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
workflow_id TEXT NOT NULL,
|
|
86
|
+
title TEXT NOT NULL,
|
|
87
|
+
description TEXT NOT NULL,
|
|
88
|
+
steps TEXT NOT NULL,
|
|
89
|
+
involved_systems TEXT NOT NULL DEFAULT '[]',
|
|
90
|
+
estimated_duration TEXT,
|
|
91
|
+
frequency TEXT,
|
|
92
|
+
generated_at TEXT NOT NULL,
|
|
93
|
+
confidence REAL DEFAULT 0.5
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS node_approvals (
|
|
97
|
+
pattern TEXT PRIMARY KEY,
|
|
98
|
+
action TEXT NOT NULL CHECK (action IN ('save','ignore','auto')),
|
|
99
|
+
created_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_session ON nodes(session_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
107
|
+
`;
|
|
108
|
+
var CartographyDB = class {
|
|
109
|
+
db;
|
|
110
|
+
constructor(dbPath) {
|
|
111
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
112
|
+
this.db = new Database(dbPath);
|
|
113
|
+
this.db.pragma("journal_mode = WAL");
|
|
114
|
+
this.db.pragma("foreign_keys = ON");
|
|
115
|
+
this.db.pragma("busy_timeout = 5000");
|
|
116
|
+
this.migrate();
|
|
117
|
+
}
|
|
118
|
+
migrate() {
|
|
119
|
+
const version = this.db.pragma("user_version", { simple: true });
|
|
120
|
+
if (version === 0) {
|
|
121
|
+
this.db.exec(SCHEMA);
|
|
122
|
+
this.db.pragma("user_version = 1");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
close() {
|
|
126
|
+
this.db.pragma("optimize");
|
|
127
|
+
this.db.close();
|
|
128
|
+
}
|
|
129
|
+
// ── Sessions ────────────────────────────
|
|
130
|
+
createSession(mode, config) {
|
|
131
|
+
const id = crypto.randomUUID();
|
|
132
|
+
this.db.prepare(
|
|
133
|
+
"INSERT INTO sessions (id, mode, started_at, config) VALUES (?, ?, ?, ?)"
|
|
134
|
+
).run(id, mode, (/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(config));
|
|
135
|
+
return id;
|
|
136
|
+
}
|
|
137
|
+
endSession(id) {
|
|
138
|
+
this.db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
139
|
+
}
|
|
140
|
+
getSession(id) {
|
|
141
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
142
|
+
return row ? this.mapSession(row) : void 0;
|
|
143
|
+
}
|
|
144
|
+
getLatestSession(mode) {
|
|
145
|
+
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();
|
|
146
|
+
return row ? this.mapSession(row) : void 0;
|
|
147
|
+
}
|
|
148
|
+
getSessions() {
|
|
149
|
+
const rows = this.db.prepare("SELECT * FROM sessions ORDER BY rowid DESC").all();
|
|
150
|
+
return rows.map((r) => this.mapSession(r));
|
|
151
|
+
}
|
|
152
|
+
mapSession(r) {
|
|
153
|
+
return {
|
|
154
|
+
id: r["id"],
|
|
155
|
+
mode: r["mode"],
|
|
156
|
+
startedAt: r["started_at"],
|
|
157
|
+
completedAt: r["completed_at"] ?? void 0,
|
|
158
|
+
config: r["config"]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// ── Nodes ───────────────────────────────
|
|
162
|
+
upsertNode(sessionId, node, depth = 0) {
|
|
163
|
+
this.db.prepare(`
|
|
164
|
+
INSERT OR REPLACE INTO nodes
|
|
165
|
+
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags)
|
|
166
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
167
|
+
`).run(
|
|
168
|
+
node.id,
|
|
169
|
+
sessionId,
|
|
170
|
+
node.type,
|
|
171
|
+
node.name,
|
|
172
|
+
node.discoveredVia,
|
|
173
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
174
|
+
depth,
|
|
175
|
+
node.confidence,
|
|
176
|
+
JSON.stringify(node.metadata ?? {}),
|
|
177
|
+
JSON.stringify(node.tags ?? [])
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
getNodes(sessionId) {
|
|
181
|
+
const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
|
|
182
|
+
return rows.map((r) => ({
|
|
183
|
+
id: r["id"],
|
|
184
|
+
sessionId: r["session_id"],
|
|
185
|
+
type: r["type"],
|
|
186
|
+
name: r["name"],
|
|
187
|
+
discoveredVia: r["discovered_via"],
|
|
188
|
+
discoveredAt: r["discovered_at"],
|
|
189
|
+
depth: r["depth"],
|
|
190
|
+
confidence: r["confidence"],
|
|
191
|
+
metadata: JSON.parse(r["metadata"]),
|
|
192
|
+
tags: JSON.parse(r["tags"]),
|
|
193
|
+
pathId: r["path_id"]
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
// ── Edges ───────────────────────────────
|
|
197
|
+
insertEdge(sessionId, edge) {
|
|
198
|
+
const id = crypto.randomUUID();
|
|
199
|
+
this.db.prepare(`
|
|
200
|
+
INSERT OR IGNORE INTO edges
|
|
201
|
+
(id, session_id, source_id, target_id, relationship, evidence, confidence, discovered_at)
|
|
202
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
203
|
+
`).run(
|
|
204
|
+
id,
|
|
205
|
+
sessionId,
|
|
206
|
+
edge.sourceId,
|
|
207
|
+
edge.targetId,
|
|
208
|
+
edge.relationship,
|
|
209
|
+
edge.evidence,
|
|
210
|
+
edge.confidence,
|
|
211
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
getEdges(sessionId) {
|
|
215
|
+
const rows = this.db.prepare("SELECT * FROM edges WHERE session_id = ?").all(sessionId);
|
|
216
|
+
return rows.map((r) => ({
|
|
217
|
+
id: r["id"],
|
|
218
|
+
sessionId: r["session_id"],
|
|
219
|
+
sourceId: r["source_id"],
|
|
220
|
+
targetId: r["target_id"],
|
|
221
|
+
relationship: r["relationship"],
|
|
222
|
+
evidence: r["evidence"],
|
|
223
|
+
confidence: r["confidence"],
|
|
224
|
+
discoveredAt: r["discovered_at"]
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
// ── Events ──────────────────────────────
|
|
228
|
+
insertEvent(sessionId, event, taskId) {
|
|
229
|
+
const id = crypto.randomUUID();
|
|
230
|
+
this.db.prepare(`
|
|
231
|
+
INSERT INTO activity_events
|
|
232
|
+
(id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port)
|
|
233
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
234
|
+
`).run(
|
|
235
|
+
id,
|
|
236
|
+
sessionId,
|
|
237
|
+
taskId ?? null,
|
|
238
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
event.eventType,
|
|
240
|
+
event.process,
|
|
241
|
+
event.pid,
|
|
242
|
+
event.target ?? null,
|
|
243
|
+
event.targetType ?? null,
|
|
244
|
+
event.port ?? null
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
getEvents(sessionId, since) {
|
|
248
|
+
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);
|
|
249
|
+
return rows.map((r) => ({
|
|
250
|
+
id: r["id"],
|
|
251
|
+
sessionId: r["session_id"],
|
|
252
|
+
taskId: r["task_id"],
|
|
253
|
+
timestamp: r["timestamp"],
|
|
254
|
+
eventType: r["event_type"],
|
|
255
|
+
process: r["process"],
|
|
256
|
+
pid: r["pid"],
|
|
257
|
+
target: r["target"],
|
|
258
|
+
targetType: r["target_type"],
|
|
259
|
+
port: r["port"],
|
|
260
|
+
durationMs: r["duration_ms"]
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
// ── Tasks ───────────────────────────────
|
|
264
|
+
startTask(sessionId, description) {
|
|
265
|
+
const id = crypto.randomUUID();
|
|
266
|
+
this.db.prepare(`
|
|
267
|
+
INSERT INTO tasks (id, session_id, description, started_at, steps, involved_services, status)
|
|
268
|
+
VALUES (?, ?, ?, ?, '[]', '[]', 'active')
|
|
269
|
+
`).run(id, sessionId, description ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
270
|
+
return id;
|
|
271
|
+
}
|
|
272
|
+
endCurrentTask(sessionId) {
|
|
273
|
+
this.db.prepare(`
|
|
274
|
+
UPDATE tasks SET status = 'completed', completed_at = ?
|
|
275
|
+
WHERE session_id = ? AND status = 'active'
|
|
276
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
|
|
277
|
+
}
|
|
278
|
+
updateTaskDescription(sessionId, description) {
|
|
279
|
+
this.db.prepare(`
|
|
280
|
+
UPDATE tasks SET description = ?
|
|
281
|
+
WHERE session_id = ? AND status = 'active'
|
|
282
|
+
`).run(description, sessionId);
|
|
283
|
+
}
|
|
284
|
+
getActiveTask(sessionId) {
|
|
285
|
+
const row = this.db.prepare(
|
|
286
|
+
"SELECT * FROM tasks WHERE session_id = ? AND status = 'active' LIMIT 1"
|
|
287
|
+
).get(sessionId);
|
|
288
|
+
return row ? this.mapTask(row) : void 0;
|
|
289
|
+
}
|
|
290
|
+
getTasks(sessionId) {
|
|
291
|
+
const rows = this.db.prepare("SELECT * FROM tasks WHERE session_id = ? ORDER BY started_at").all(sessionId);
|
|
292
|
+
return rows.map((r) => this.mapTask(r));
|
|
293
|
+
}
|
|
294
|
+
mapTask(r) {
|
|
295
|
+
return {
|
|
296
|
+
id: r["id"],
|
|
297
|
+
sessionId: r["session_id"],
|
|
298
|
+
description: r["description"],
|
|
299
|
+
startedAt: r["started_at"],
|
|
300
|
+
completedAt: r["completed_at"],
|
|
301
|
+
steps: r["steps"],
|
|
302
|
+
involvedServices: r["involved_services"],
|
|
303
|
+
status: r["status"],
|
|
304
|
+
isSOPCandidate: Boolean(r["is_sop_candidate"])
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// ── Workflows ───────────────────────────
|
|
308
|
+
insertWorkflow(sessionId, data) {
|
|
309
|
+
const id = crypto.randomUUID();
|
|
310
|
+
this.db.prepare(`
|
|
311
|
+
INSERT INTO workflows
|
|
312
|
+
(id, session_id, name, pattern, task_ids, occurrences,
|
|
313
|
+
first_seen, last_seen, avg_duration_ms, involved_services)
|
|
314
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
315
|
+
`).run(
|
|
316
|
+
id,
|
|
317
|
+
sessionId,
|
|
318
|
+
data.name ?? null,
|
|
319
|
+
data.pattern,
|
|
320
|
+
data.taskIds,
|
|
321
|
+
data.occurrences,
|
|
322
|
+
data.firstSeen,
|
|
323
|
+
data.lastSeen,
|
|
324
|
+
data.avgDurationMs,
|
|
325
|
+
data.involvedServices
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
getWorkflows(sessionId) {
|
|
329
|
+
const rows = this.db.prepare("SELECT * FROM workflows WHERE session_id = ?").all(sessionId);
|
|
330
|
+
return rows.map((r) => ({
|
|
331
|
+
id: r["id"],
|
|
332
|
+
sessionId: r["session_id"],
|
|
333
|
+
name: r["name"],
|
|
334
|
+
pattern: r["pattern"],
|
|
335
|
+
taskIds: r["task_ids"],
|
|
336
|
+
occurrences: r["occurrences"],
|
|
337
|
+
firstSeen: r["first_seen"],
|
|
338
|
+
lastSeen: r["last_seen"],
|
|
339
|
+
avgDurationMs: r["avg_duration_ms"],
|
|
340
|
+
involvedServices: r["involved_services"]
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
// ── SOPs ────────────────────────────────
|
|
344
|
+
insertSOP(sop) {
|
|
345
|
+
const id = crypto.randomUUID();
|
|
346
|
+
this.db.prepare(`
|
|
347
|
+
INSERT INTO sops
|
|
348
|
+
(id, workflow_id, title, description, steps, involved_systems,
|
|
349
|
+
estimated_duration, frequency, generated_at, confidence)
|
|
350
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
351
|
+
`).run(
|
|
352
|
+
id,
|
|
353
|
+
sop.workflowId,
|
|
354
|
+
sop.title,
|
|
355
|
+
sop.description,
|
|
356
|
+
JSON.stringify(sop.steps),
|
|
357
|
+
JSON.stringify(sop.involvedSystems),
|
|
358
|
+
sop.estimatedDuration,
|
|
359
|
+
sop.frequency,
|
|
360
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
sop.confidence
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
getSOPs(sessionId) {
|
|
365
|
+
const rows = this.db.prepare(`
|
|
366
|
+
SELECT s.* FROM sops s
|
|
367
|
+
JOIN workflows w ON s.workflow_id = w.id
|
|
368
|
+
WHERE w.session_id = ?
|
|
369
|
+
`).all(sessionId);
|
|
370
|
+
return rows.map((r) => ({
|
|
371
|
+
id: r["id"],
|
|
372
|
+
workflowId: r["workflow_id"],
|
|
373
|
+
title: r["title"],
|
|
374
|
+
description: r["description"],
|
|
375
|
+
steps: JSON.parse(r["steps"]),
|
|
376
|
+
involvedSystems: JSON.parse(r["involved_systems"]),
|
|
377
|
+
estimatedDuration: r["estimated_duration"],
|
|
378
|
+
frequency: r["frequency"],
|
|
379
|
+
confidence: r["confidence"]
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
// ── Approvals ───────────────────────────
|
|
383
|
+
setApproval(pattern, action) {
|
|
384
|
+
this.db.prepare(`
|
|
385
|
+
INSERT OR REPLACE INTO node_approvals (pattern, action, created_at) VALUES (?, ?, ?)
|
|
386
|
+
`).run(pattern, action, (/* @__PURE__ */ new Date()).toISOString());
|
|
387
|
+
}
|
|
388
|
+
getApproval(pattern) {
|
|
389
|
+
const row = this.db.prepare("SELECT action FROM node_approvals WHERE pattern = ?").get(pattern);
|
|
390
|
+
return row?.action;
|
|
391
|
+
}
|
|
392
|
+
// ── Stats ───────────────────────────────
|
|
393
|
+
getStats(sessionId) {
|
|
394
|
+
const nodes = this.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE session_id = ?").get(sessionId).c;
|
|
395
|
+
const edges = this.db.prepare("SELECT COUNT(*) as c FROM edges WHERE session_id = ?").get(sessionId).c;
|
|
396
|
+
const events = this.db.prepare("SELECT COUNT(*) as c FROM activity_events WHERE session_id = ?").get(sessionId).c;
|
|
397
|
+
const tasks = this.db.prepare("SELECT COUNT(*) as c FROM tasks WHERE session_id = ?").get(sessionId).c;
|
|
398
|
+
return { nodes, edges, events, tasks };
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/tools.ts
|
|
403
|
+
import { z as z2 } from "zod";
|
|
404
|
+
|
|
405
|
+
// src/types.ts
|
|
406
|
+
import { z } from "zod";
|
|
407
|
+
var NODE_TYPES = [
|
|
408
|
+
"host",
|
|
409
|
+
"database_server",
|
|
410
|
+
"database",
|
|
411
|
+
"table",
|
|
412
|
+
"web_service",
|
|
413
|
+
"api_endpoint",
|
|
414
|
+
"cache_server",
|
|
415
|
+
"message_broker",
|
|
416
|
+
"queue",
|
|
417
|
+
"topic",
|
|
418
|
+
"container",
|
|
419
|
+
"pod",
|
|
420
|
+
"k8s_cluster",
|
|
421
|
+
"config_file",
|
|
422
|
+
"unknown"
|
|
423
|
+
];
|
|
424
|
+
var EDGE_RELATIONSHIPS = [
|
|
425
|
+
"connects_to",
|
|
426
|
+
"reads_from",
|
|
427
|
+
"writes_to",
|
|
428
|
+
"calls",
|
|
429
|
+
"contains",
|
|
430
|
+
"depends_on"
|
|
431
|
+
];
|
|
432
|
+
var EVENT_TYPES = [
|
|
433
|
+
"process_start",
|
|
434
|
+
"process_end",
|
|
435
|
+
"connection_open",
|
|
436
|
+
"connection_close",
|
|
437
|
+
"window_focus",
|
|
438
|
+
"tool_switch"
|
|
439
|
+
];
|
|
440
|
+
var NodeSchema = z.object({
|
|
441
|
+
id: z.string().describe('Format: "{type}:{host}:{port}" oder "{type}:{name}"'),
|
|
442
|
+
type: z.enum(NODE_TYPES),
|
|
443
|
+
name: z.string(),
|
|
444
|
+
discoveredVia: z.string(),
|
|
445
|
+
confidence: z.number().min(0).max(1).default(0.5),
|
|
446
|
+
metadata: z.record(z.unknown()).default({}),
|
|
447
|
+
tags: z.array(z.string()).default([])
|
|
448
|
+
});
|
|
449
|
+
var EdgeSchema = z.object({
|
|
450
|
+
sourceId: z.string(),
|
|
451
|
+
targetId: z.string(),
|
|
452
|
+
relationship: z.enum(EDGE_RELATIONSHIPS),
|
|
453
|
+
evidence: z.string(),
|
|
454
|
+
confidence: z.number().min(0).max(1).default(0.5)
|
|
455
|
+
});
|
|
456
|
+
var EventSchema = z.object({
|
|
457
|
+
eventType: z.enum(EVENT_TYPES),
|
|
458
|
+
process: z.string(),
|
|
459
|
+
pid: z.number(),
|
|
460
|
+
target: z.string().optional(),
|
|
461
|
+
targetType: z.enum(NODE_TYPES).optional(),
|
|
462
|
+
protocol: z.string().optional(),
|
|
463
|
+
port: z.number().optional()
|
|
464
|
+
});
|
|
465
|
+
var SOPStepSchema = z.object({
|
|
466
|
+
order: z.number(),
|
|
467
|
+
instruction: z.string(),
|
|
468
|
+
tool: z.string(),
|
|
469
|
+
target: z.string().optional(),
|
|
470
|
+
notes: z.string().optional()
|
|
471
|
+
});
|
|
472
|
+
var SOPSchema = z.object({
|
|
473
|
+
title: z.string(),
|
|
474
|
+
description: z.string(),
|
|
475
|
+
steps: z.array(SOPStepSchema),
|
|
476
|
+
involvedSystems: z.array(z.string()),
|
|
477
|
+
estimatedDuration: z.string(),
|
|
478
|
+
frequency: z.string(),
|
|
479
|
+
confidence: z.number().min(0).max(1)
|
|
480
|
+
});
|
|
481
|
+
var MIN_POLL_INTERVAL_MS = 15e3;
|
|
482
|
+
function defaultConfig(overrides = {}) {
|
|
483
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
484
|
+
return {
|
|
485
|
+
mode: "discover",
|
|
486
|
+
maxDepth: 8,
|
|
487
|
+
maxTurns: 50,
|
|
488
|
+
entryPoints: ["localhost"],
|
|
489
|
+
agentModel: "claude-sonnet-4-5-20250929",
|
|
490
|
+
shadowMode: "daemon",
|
|
491
|
+
pollIntervalMs: 3e4,
|
|
492
|
+
inactivityTimeoutMs: 3e5,
|
|
493
|
+
promptTimeoutMs: 6e4,
|
|
494
|
+
trackWindowFocus: false,
|
|
495
|
+
autoSaveNodes: false,
|
|
496
|
+
enableNotifications: true,
|
|
497
|
+
shadowModel: "claude-haiku-4-5-20251001",
|
|
498
|
+
outputDir: "./cartography-output",
|
|
499
|
+
dbPath: `${home}/.cartography/cartography.db`,
|
|
500
|
+
socketPath: `${home}/.cartography/daemon.sock`,
|
|
501
|
+
pidFile: `${home}/.cartography/daemon.pid`,
|
|
502
|
+
verbose: false,
|
|
503
|
+
...overrides
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/tools.ts
|
|
508
|
+
function stripSensitive(target) {
|
|
509
|
+
try {
|
|
510
|
+
const url = new URL(target.startsWith("http") ? target : `tcp://${target}`);
|
|
511
|
+
return `${url.hostname}${url.port ? ":" + url.port : ""}`;
|
|
512
|
+
} catch {
|
|
513
|
+
return target.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function createCartographyTools(db, sessionId) {
|
|
517
|
+
const sdk = await import("@anthropic-ai/claude-code");
|
|
518
|
+
const { tool, createSdkMcpServer } = sdk;
|
|
519
|
+
const tools = [
|
|
520
|
+
tool("save_node", "Infrastructure-Node speichern", {
|
|
521
|
+
id: z2.string(),
|
|
522
|
+
type: z2.enum(NODE_TYPES),
|
|
523
|
+
name: z2.string(),
|
|
524
|
+
discoveredVia: z2.string(),
|
|
525
|
+
confidence: z2.number().min(0).max(1),
|
|
526
|
+
metadata: z2.record(z2.unknown()).optional(),
|
|
527
|
+
tags: z2.array(z2.string()).optional()
|
|
528
|
+
}, async (args) => {
|
|
529
|
+
const node = {
|
|
530
|
+
id: stripSensitive(args["id"]),
|
|
531
|
+
type: args["type"],
|
|
532
|
+
name: args["name"],
|
|
533
|
+
discoveredVia: args["discoveredVia"],
|
|
534
|
+
confidence: args["confidence"],
|
|
535
|
+
metadata: args["metadata"] ?? {},
|
|
536
|
+
tags: args["tags"] ?? []
|
|
537
|
+
};
|
|
538
|
+
db.upsertNode(sessionId, node);
|
|
539
|
+
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
540
|
+
}),
|
|
541
|
+
tool("save_edge", "Verbindung zwischen zwei Nodes speichern", {
|
|
542
|
+
sourceId: z2.string(),
|
|
543
|
+
targetId: z2.string(),
|
|
544
|
+
relationship: z2.enum(EDGE_RELATIONSHIPS),
|
|
545
|
+
evidence: z2.string(),
|
|
546
|
+
confidence: z2.number().min(0).max(1)
|
|
547
|
+
}, async (args) => {
|
|
548
|
+
db.insertEdge(sessionId, {
|
|
549
|
+
sourceId: args["sourceId"],
|
|
550
|
+
targetId: args["targetId"],
|
|
551
|
+
relationship: args["relationship"],
|
|
552
|
+
evidence: args["evidence"],
|
|
553
|
+
confidence: args["confidence"]
|
|
554
|
+
});
|
|
555
|
+
return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
|
|
556
|
+
}),
|
|
557
|
+
tool("save_event", "Activity-Event (Prozess/Verbindung) speichern", {
|
|
558
|
+
eventType: z2.enum(EVENT_TYPES),
|
|
559
|
+
process: z2.string(),
|
|
560
|
+
pid: z2.number(),
|
|
561
|
+
target: z2.string().optional(),
|
|
562
|
+
targetType: z2.enum(NODE_TYPES).optional(),
|
|
563
|
+
port: z2.number().optional()
|
|
564
|
+
}, async (args) => {
|
|
565
|
+
db.insertEvent(sessionId, {
|
|
566
|
+
eventType: args["eventType"],
|
|
567
|
+
process: args["process"],
|
|
568
|
+
pid: args["pid"],
|
|
569
|
+
target: args["target"] ? stripSensitive(args["target"]) : void 0,
|
|
570
|
+
targetType: args["targetType"],
|
|
571
|
+
port: args["port"]
|
|
572
|
+
});
|
|
573
|
+
return { content: [{ type: "text", text: `\u2713 ${args["eventType"]}` }] };
|
|
574
|
+
}),
|
|
575
|
+
tool("get_catalog", "Aktuellen Katalog abrufen (Duplikat-Check)", {
|
|
576
|
+
includeEdges: z2.boolean().default(true)
|
|
577
|
+
}, async (args) => {
|
|
578
|
+
const nodes = db.getNodes(sessionId);
|
|
579
|
+
const edges = args["includeEdges"] ? db.getEdges(sessionId) : [];
|
|
580
|
+
return {
|
|
581
|
+
content: [{
|
|
582
|
+
type: "text",
|
|
583
|
+
text: JSON.stringify({
|
|
584
|
+
count: { nodes: nodes.length, edges: edges.length },
|
|
585
|
+
nodeIds: nodes.map((n) => n.id)
|
|
586
|
+
})
|
|
587
|
+
}]
|
|
588
|
+
};
|
|
589
|
+
}),
|
|
590
|
+
tool("manage_task", "Task starten, beenden oder beschreiben", {
|
|
591
|
+
action: z2.enum(["start", "end", "describe"]),
|
|
592
|
+
description: z2.string().optional()
|
|
593
|
+
}, async (args) => {
|
|
594
|
+
const action = args["action"];
|
|
595
|
+
if (action === "start") {
|
|
596
|
+
const id = db.startTask(sessionId, args["description"]);
|
|
597
|
+
return { content: [{ type: "text", text: `\u2713 Task gestartet: ${id}` }] };
|
|
598
|
+
}
|
|
599
|
+
if (action === "end") {
|
|
600
|
+
db.endCurrentTask(sessionId);
|
|
601
|
+
return { content: [{ type: "text", text: "\u2713 Task beendet" }] };
|
|
602
|
+
}
|
|
603
|
+
db.updateTaskDescription(sessionId, args["description"]);
|
|
604
|
+
return { content: [{ type: "text", text: "\u2713 Beschreibung aktualisiert" }] };
|
|
605
|
+
}),
|
|
606
|
+
tool("save_sop", "Standard Operating Procedure speichern", {
|
|
607
|
+
workflowId: z2.string(),
|
|
608
|
+
title: z2.string(),
|
|
609
|
+
description: z2.string(),
|
|
610
|
+
steps: z2.array(SOPStepSchema),
|
|
611
|
+
involvedSystems: z2.array(z2.string()),
|
|
612
|
+
estimatedDuration: z2.string(),
|
|
613
|
+
frequency: z2.string(),
|
|
614
|
+
confidence: z2.number().min(0).max(1)
|
|
615
|
+
}, async (args) => {
|
|
616
|
+
db.insertSOP({
|
|
617
|
+
workflowId: args["workflowId"],
|
|
618
|
+
title: args["title"],
|
|
619
|
+
description: args["description"],
|
|
620
|
+
steps: args["steps"],
|
|
621
|
+
involvedSystems: args["involvedSystems"],
|
|
622
|
+
estimatedDuration: args["estimatedDuration"],
|
|
623
|
+
frequency: args["frequency"],
|
|
624
|
+
confidence: args["confidence"]
|
|
625
|
+
});
|
|
626
|
+
return { content: [{ type: "text", text: `\u2713 SOP: ${args["title"]}` }] };
|
|
627
|
+
})
|
|
628
|
+
];
|
|
629
|
+
return createSdkMcpServer({
|
|
630
|
+
name: "cartography",
|
|
631
|
+
version: "0.1.0",
|
|
632
|
+
tools
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/safety.ts
|
|
637
|
+
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;
|
|
638
|
+
var BLOCKED_REDIRECTS = />>|>[^>]/;
|
|
639
|
+
var safetyHook = async (input) => {
|
|
640
|
+
if (!("tool_name" in input)) return {};
|
|
641
|
+
if (input.tool_name !== "Bash") return {};
|
|
642
|
+
const cmd = input.tool_input?.command ?? "";
|
|
643
|
+
if (BLOCKED_CMDS.test(cmd) || BLOCKED_REDIRECTS.test(cmd)) {
|
|
644
|
+
return {
|
|
645
|
+
hookSpecificOutput: {
|
|
646
|
+
hookEventName: "PreToolUse",
|
|
647
|
+
permissionDecision: "deny",
|
|
648
|
+
permissionDecisionReason: `BLOCKED: "${cmd}" \u2014 read-only policy`
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
hookSpecificOutput: {
|
|
654
|
+
hookEventName: "PreToolUse",
|
|
655
|
+
permissionDecision: "allow"
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// src/agent.ts
|
|
661
|
+
async function runDiscovery(config, db, sessionId, onOutput) {
|
|
662
|
+
const { query } = await import("@anthropic-ai/claude-code");
|
|
663
|
+
const tools = await createCartographyTools(db, sessionId);
|
|
664
|
+
const systemPrompt = `Du bist ein Infrastruktur-Discovery-Agent.
|
|
665
|
+
Kartographiere die gesamte Systemlandschaft.
|
|
666
|
+
|
|
667
|
+
STRATEGIE:
|
|
668
|
+
1. ss -tlnp + ps aux \u2192 \xDCberblick
|
|
669
|
+
2. Jeden Service tiefer (Datenbanken\u2192Tabellen, APIs\u2192Endpoints, Queues\u2192Topics)
|
|
670
|
+
3. save_node + save_edge f\xFCr alles. get_catalog \u2192 keine Duplikate.
|
|
671
|
+
4. Config-Files folgen: .env (nur Host:Port!), docker-compose.yml, application.yml
|
|
672
|
+
5. Backtrack wenn Spur ersch\xF6pft. Stop wenn alles explored.
|
|
673
|
+
|
|
674
|
+
PORT-MAPPING: 5432=postgres, 3306=mysql, 27017=mongodb, 6379=redis,
|
|
675
|
+
9092=kafka, 5672=rabbitmq, 80/443/8080/3000=web_service,
|
|
676
|
+
9090=prometheus, 8500=consul, 8200=vault, 2379=etcd
|
|
677
|
+
|
|
678
|
+
REGELN:
|
|
679
|
+
- NUR read-only Commands (ss, ps, cat, head, curl -s, docker inspect, kubectl get)
|
|
680
|
+
- Targets NUR Host:Port \u2014 KEINE URLs, Pfade, Credentials
|
|
681
|
+
- Node IDs: "{type}:{host}:{port}" oder "{type}:{name}"
|
|
682
|
+
- Confidence: 0.9 direkt beobachtet, 0.7 aus Config, 0.5 Vermutung
|
|
683
|
+
- KEINE Credentials speichern
|
|
684
|
+
|
|
685
|
+
Entrypoints: ${config.entryPoints.join(", ")}`;
|
|
686
|
+
for await (const msg of query({
|
|
687
|
+
prompt: systemPrompt,
|
|
688
|
+
options: {
|
|
689
|
+
model: config.agentModel,
|
|
690
|
+
maxTurns: config.maxTurns,
|
|
691
|
+
customSystemPrompt: systemPrompt,
|
|
692
|
+
mcpServers: { cartography: tools },
|
|
693
|
+
allowedTools: [
|
|
694
|
+
"Bash",
|
|
695
|
+
"mcp__cartograph__save_node",
|
|
696
|
+
"mcp__cartograph__save_edge",
|
|
697
|
+
"mcp__cartograph__get_catalog"
|
|
698
|
+
],
|
|
699
|
+
hooks: {
|
|
700
|
+
PreToolUse: [{ matcher: "Bash", hooks: [safetyHook] }]
|
|
701
|
+
},
|
|
702
|
+
permissionMode: "bypassPermissions"
|
|
703
|
+
}
|
|
704
|
+
})) {
|
|
705
|
+
if (msg.type === "assistant" && onOutput) {
|
|
706
|
+
for (const block of msg.message.content) {
|
|
707
|
+
if (block.type === "text") {
|
|
708
|
+
onOutput(block.text);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (msg.type === "result") return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function runShadowCycle(config, db, sessionId, prevSnapshot, currSnapshot, onOutput) {
|
|
716
|
+
const { query } = await import("@anthropic-ai/claude-code");
|
|
717
|
+
const tools = await createCartographyTools(db, sessionId);
|
|
718
|
+
const prompt = `Analysiere den Diff zwischen diesen beiden System-Snapshots.
|
|
719
|
+
Finde:
|
|
720
|
+
- Neue/geschlossene TCP-Verbindungen \u2192 save_event
|
|
721
|
+
- Neue/beendete Prozesse \u2192 save_event
|
|
722
|
+
- Bisher unbekannte Services \u2192 get_catalog pr\xFCfen, dann save_node
|
|
723
|
+
- Task-Grenzen (Inaktivit\xE4t, Tool-Wechsel) \u2192 manage_task
|
|
724
|
+
target = NUR Host:Port. Kurz und effizient.
|
|
725
|
+
|
|
726
|
+
=== VORHER ===
|
|
727
|
+
${prevSnapshot}
|
|
728
|
+
|
|
729
|
+
=== JETZT ===
|
|
730
|
+
${currSnapshot}`;
|
|
731
|
+
for await (const msg of query({
|
|
732
|
+
prompt,
|
|
733
|
+
options: {
|
|
734
|
+
model: config.shadowModel,
|
|
735
|
+
maxTurns: 5,
|
|
736
|
+
mcpServers: { cartography: tools },
|
|
737
|
+
allowedTools: [
|
|
738
|
+
"mcp__cartograph__save_event",
|
|
739
|
+
"mcp__cartograph__save_node",
|
|
740
|
+
"mcp__cartograph__save_edge",
|
|
741
|
+
"mcp__cartograph__get_catalog",
|
|
742
|
+
"mcp__cartograph__manage_task"
|
|
743
|
+
],
|
|
744
|
+
permissionMode: "bypassPermissions"
|
|
745
|
+
}
|
|
746
|
+
})) {
|
|
747
|
+
if (onOutput) onOutput(msg);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function generateSOPs(db, sessionId) {
|
|
751
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
752
|
+
const client = new Anthropic();
|
|
753
|
+
const tasks = db.getTasks(sessionId).filter((t) => t.status === "completed");
|
|
754
|
+
if (tasks.length === 0) return 0;
|
|
755
|
+
const clusters = clusterTasks(tasks);
|
|
756
|
+
let generated = 0;
|
|
757
|
+
for (const cluster of clusters) {
|
|
758
|
+
const workflowId = crypto.randomUUID();
|
|
759
|
+
const involved = JSON.parse(cluster[0]?.involvedServices ?? "[]");
|
|
760
|
+
const taskDescriptions = cluster.map((t, i) => `Task ${i + 1}: ${t.description ?? "Unnamed"}
|
|
761
|
+
Steps: ${t.steps}`).join("\n\n");
|
|
762
|
+
const response = await client.messages.create({
|
|
763
|
+
model: "claude-sonnet-4-5-20250929",
|
|
764
|
+
max_tokens: 2048,
|
|
765
|
+
messages: [{
|
|
766
|
+
role: "user",
|
|
767
|
+
content: `Generiere eine SOP (Standard Operating Procedure) f\xFCr diesen wiederkehrenden Workflow.
|
|
768
|
+
Antworte NUR mit validen JSON im Format:
|
|
769
|
+
{
|
|
770
|
+
"title": "...",
|
|
771
|
+
"description": "...",
|
|
772
|
+
"steps": [{"order": 1, "instruction": "...", "tool": "...", "target": "...", "notes": "..."}],
|
|
773
|
+
"involvedSystems": ["..."],
|
|
774
|
+
"estimatedDuration": "~N Minuten",
|
|
775
|
+
"frequency": "Xmal t\xE4glich",
|
|
776
|
+
"confidence": 0.8
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
Tasks:
|
|
780
|
+
${taskDescriptions}
|
|
781
|
+
|
|
782
|
+
Beteiligte Services: ${involved.join(", ")}`
|
|
783
|
+
}]
|
|
784
|
+
});
|
|
785
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
786
|
+
try {
|
|
787
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
788
|
+
if (!jsonMatch) continue;
|
|
789
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
790
|
+
db.insertSOP({ workflowId, ...parsed });
|
|
791
|
+
generated++;
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return generated;
|
|
796
|
+
}
|
|
797
|
+
function clusterTasks(tasks) {
|
|
798
|
+
const clusters = [];
|
|
799
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
800
|
+
for (const task of tasks) {
|
|
801
|
+
if (assigned.has(task.id)) continue;
|
|
802
|
+
const cluster = [task];
|
|
803
|
+
assigned.add(task.id);
|
|
804
|
+
const taskServices = new Set(JSON.parse(task.involvedServices ?? "[]"));
|
|
805
|
+
for (const other of tasks) {
|
|
806
|
+
if (assigned.has(other.id)) continue;
|
|
807
|
+
const otherServices = new Set(JSON.parse(other.involvedServices ?? "[]"));
|
|
808
|
+
const overlap = [...taskServices].filter((s) => otherServices.has(s));
|
|
809
|
+
if (overlap.length > 0) {
|
|
810
|
+
cluster.push(other);
|
|
811
|
+
assigned.add(other.id);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
clusters.push(cluster);
|
|
815
|
+
}
|
|
816
|
+
return clusters;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/exporter.ts
|
|
820
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
821
|
+
import { join } from "path";
|
|
822
|
+
var MERMAID_ICONS = {
|
|
823
|
+
host: "\u{1F5A5}\uFE0F",
|
|
824
|
+
database_server: "\u{1F5C4}\uFE0F",
|
|
825
|
+
database: "\u{1F5C4}\uFE0F",
|
|
826
|
+
table: "\u{1F4CB}",
|
|
827
|
+
web_service: "\u{1F310}",
|
|
828
|
+
api_endpoint: "\u{1F50C}",
|
|
829
|
+
cache_server: "\u26A1",
|
|
830
|
+
message_broker: "\u{1F4E8}",
|
|
831
|
+
queue: "\u{1F4EC}",
|
|
832
|
+
topic: "\u{1F4E2}",
|
|
833
|
+
container: "\u{1F4E6}",
|
|
834
|
+
pod: "\u2638\uFE0F",
|
|
835
|
+
k8s_cluster: "\u2638\uFE0F",
|
|
836
|
+
config_file: "\u{1F4C4}",
|
|
837
|
+
unknown: "\u2753"
|
|
838
|
+
};
|
|
839
|
+
var EDGE_LABELS = {
|
|
840
|
+
connects_to: "",
|
|
841
|
+
reads_from: "reads",
|
|
842
|
+
writes_to: "writes",
|
|
843
|
+
calls: "calls",
|
|
844
|
+
contains: "contains",
|
|
845
|
+
depends_on: "depends"
|
|
846
|
+
};
|
|
847
|
+
function sanitize(id) {
|
|
848
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
849
|
+
}
|
|
850
|
+
function groupByParent(nodes) {
|
|
851
|
+
const groups = /* @__PURE__ */ new Map();
|
|
852
|
+
for (const node of nodes) {
|
|
853
|
+
const group = node.id.split(":")[1] ?? node.type;
|
|
854
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
855
|
+
groups.get(group).push(node);
|
|
856
|
+
}
|
|
857
|
+
return groups;
|
|
858
|
+
}
|
|
859
|
+
function generateTopologyMermaid(nodes, edges) {
|
|
860
|
+
const lines = ["graph TB"];
|
|
861
|
+
const groups = groupByParent(nodes);
|
|
862
|
+
for (const [group, groupNodes] of groups) {
|
|
863
|
+
if (groups.size > 1) {
|
|
864
|
+
lines.push(` subgraph ${sanitize(group)}`);
|
|
865
|
+
}
|
|
866
|
+
for (const node of groupNodes) {
|
|
867
|
+
const icon = MERMAID_ICONS[node.type] ?? "\u2753";
|
|
868
|
+
lines.push(` ${sanitize(node.id)}["${icon} ${node.name}"]`);
|
|
869
|
+
}
|
|
870
|
+
if (groups.size > 1) {
|
|
871
|
+
lines.push(" end");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (const edge of edges) {
|
|
875
|
+
const label = EDGE_LABELS[edge.relationship] ?? "";
|
|
876
|
+
const arrow = label ? `-->|"${label}"|` : "-->";
|
|
877
|
+
lines.push(` ${sanitize(edge.sourceId)} ${arrow} ${sanitize(edge.targetId)}`);
|
|
878
|
+
}
|
|
879
|
+
return lines.join("\n");
|
|
880
|
+
}
|
|
881
|
+
function generateDependencyMermaid(nodes, edges) {
|
|
882
|
+
const lines = ["graph LR"];
|
|
883
|
+
const depEdges = edges.filter(
|
|
884
|
+
(e) => ["calls", "reads_from", "writes_to", "depends_on"].includes(e.relationship)
|
|
885
|
+
);
|
|
886
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
887
|
+
for (const edge of depEdges) {
|
|
888
|
+
usedIds.add(edge.sourceId);
|
|
889
|
+
usedIds.add(edge.targetId);
|
|
890
|
+
}
|
|
891
|
+
const usedNodes = nodes.filter((n) => usedIds.has(n.id));
|
|
892
|
+
for (const node of usedNodes) {
|
|
893
|
+
const icon = MERMAID_ICONS[node.type] ?? "\u2753";
|
|
894
|
+
lines.push(` ${sanitize(node.id)}["${icon} ${node.name}"]`);
|
|
895
|
+
}
|
|
896
|
+
for (const edge of depEdges) {
|
|
897
|
+
const label = EDGE_LABELS[edge.relationship] ?? "";
|
|
898
|
+
const arrow = label ? `-->|"${label}"|` : "-->";
|
|
899
|
+
lines.push(` ${sanitize(edge.sourceId)} ${arrow} ${sanitize(edge.targetId)}`);
|
|
900
|
+
}
|
|
901
|
+
return lines.join("\n");
|
|
902
|
+
}
|
|
903
|
+
function generateWorkflowMermaid(sop) {
|
|
904
|
+
const lines = ["flowchart TD"];
|
|
905
|
+
for (const step of sop.steps) {
|
|
906
|
+
const nodeId = `S${step.order}`;
|
|
907
|
+
const label = `${step.order}. ${step.instruction.substring(0, 60)}`;
|
|
908
|
+
lines.push(` ${nodeId}["${label}"]`);
|
|
909
|
+
if (step.order > 1) {
|
|
910
|
+
lines.push(` S${step.order - 1} --> ${nodeId}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return lines.join("\n");
|
|
914
|
+
}
|
|
915
|
+
function exportBackstageYAML(nodes, edges, org) {
|
|
916
|
+
const owner = org ?? "unknown";
|
|
917
|
+
const docs = [];
|
|
918
|
+
for (const node of nodes) {
|
|
919
|
+
const isComponent = ["web_service", "container", "pod"].includes(node.type);
|
|
920
|
+
const isAPI = node.type === "api_endpoint";
|
|
921
|
+
const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
|
|
922
|
+
const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
|
|
923
|
+
const doc = [
|
|
924
|
+
`apiVersion: backstage.io/v1alpha1`,
|
|
925
|
+
`kind: ${kind}`,
|
|
926
|
+
`metadata:`,
|
|
927
|
+
` name: ${sanitize(node.id)}`,
|
|
928
|
+
` annotations:`,
|
|
929
|
+
` cartography/discovered-at: "${node.discoveredAt}"`,
|
|
930
|
+
` cartography/confidence: "${node.confidence}"`,
|
|
931
|
+
`spec:`,
|
|
932
|
+
` type: ${node.type}`,
|
|
933
|
+
` lifecycle: production`,
|
|
934
|
+
` owner: ${owner}`,
|
|
935
|
+
...deps.length > 0 ? [" dependsOn:", ...deps] : []
|
|
936
|
+
].join("\n");
|
|
937
|
+
docs.push(doc);
|
|
938
|
+
}
|
|
939
|
+
return docs.join("\n---\n");
|
|
940
|
+
}
|
|
941
|
+
function exportJSON(db, sessionId) {
|
|
942
|
+
const nodes = db.getNodes(sessionId);
|
|
943
|
+
const edges = db.getEdges(sessionId);
|
|
944
|
+
const events = db.getEvents(sessionId);
|
|
945
|
+
const tasks = db.getTasks(sessionId);
|
|
946
|
+
const sops = db.getSOPs(sessionId);
|
|
947
|
+
const stats = db.getStats(sessionId);
|
|
948
|
+
return JSON.stringify({
|
|
949
|
+
sessionId,
|
|
950
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
951
|
+
stats,
|
|
952
|
+
nodes,
|
|
953
|
+
edges,
|
|
954
|
+
events,
|
|
955
|
+
tasks,
|
|
956
|
+
sops
|
|
957
|
+
}, null, 2);
|
|
958
|
+
}
|
|
959
|
+
function exportHTML(nodes, edges) {
|
|
960
|
+
const graphData = JSON.stringify({
|
|
961
|
+
nodes: nodes.map((n) => ({ id: n.id, name: n.name, type: n.type, confidence: n.confidence })),
|
|
962
|
+
links: edges.map((e) => ({ source: e.sourceId, target: e.targetId, relationship: e.relationship }))
|
|
963
|
+
});
|
|
964
|
+
return `<!DOCTYPE html>
|
|
965
|
+
<html lang="de">
|
|
966
|
+
<head>
|
|
967
|
+
<meta charset="UTF-8">
|
|
968
|
+
<title>Cartography \u2014 Topology</title>
|
|
969
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
970
|
+
<style>
|
|
971
|
+
body { margin: 0; background: #1a1a2e; color: #eee; font-family: monospace; }
|
|
972
|
+
svg { width: 100vw; height: 100vh; }
|
|
973
|
+
.node circle { stroke: #fff; stroke-width: 1.5px; }
|
|
974
|
+
.node text { font-size: 10px; fill: #eee; }
|
|
975
|
+
.link { stroke: #666; stroke-opacity: 0.6; }
|
|
976
|
+
#info { position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7);
|
|
977
|
+
padding: 10px; border-radius: 4px; font-size: 12px; }
|
|
978
|
+
</style>
|
|
979
|
+
</head>
|
|
980
|
+
<body>
|
|
981
|
+
<div id="info">
|
|
982
|
+
<strong>Cartography</strong><br>
|
|
983
|
+
Nodes: ${nodes.length} | Edges: ${edges.length}<br>
|
|
984
|
+
<small>Drag to explore</small>
|
|
985
|
+
</div>
|
|
986
|
+
<svg></svg>
|
|
987
|
+
<script>
|
|
988
|
+
const data = ${graphData};
|
|
989
|
+
|
|
990
|
+
const TYPE_COLORS = {
|
|
991
|
+
host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
|
|
992
|
+
web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
|
|
993
|
+
message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
|
|
994
|
+
container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
|
|
995
|
+
config_file: '#adb5bd', unknown: '#6c757d',
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const svg = d3.select('svg');
|
|
999
|
+
const width = window.innerWidth, height = window.innerHeight;
|
|
1000
|
+
const g = svg.append('g');
|
|
1001
|
+
|
|
1002
|
+
svg.call(d3.zoom().on('zoom', e => g.attr('transform', e.transform)));
|
|
1003
|
+
|
|
1004
|
+
const sim = d3.forceSimulation(data.nodes)
|
|
1005
|
+
.force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
|
|
1006
|
+
.force('charge', d3.forceManyBody().strength(-200))
|
|
1007
|
+
.force('center', d3.forceCenter(width / 2, height / 2));
|
|
1008
|
+
|
|
1009
|
+
const link = g.append('g').selectAll('line')
|
|
1010
|
+
.data(data.links).join('line').attr('class', 'link');
|
|
1011
|
+
|
|
1012
|
+
const node = g.append('g').selectAll('g')
|
|
1013
|
+
.data(data.nodes).join('g').attr('class', 'node')
|
|
1014
|
+
.call(d3.drag()
|
|
1015
|
+
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
1016
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
1017
|
+
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
node.append('circle').attr('r', 8).attr('fill', d => TYPE_COLORS[d.type] || '#aaa');
|
|
1021
|
+
node.append('text').attr('dx', 12).attr('dy', '.35em').text(d => d.name);
|
|
1022
|
+
node.append('title').text(d => \`\${d.type}: \${d.id}
|
|
1023
|
+
Confidence: \${d.confidence}\`);
|
|
1024
|
+
|
|
1025
|
+
sim.on('tick', () => {
|
|
1026
|
+
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
1027
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
1028
|
+
node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1029
|
+
});
|
|
1030
|
+
</script>
|
|
1031
|
+
</body>
|
|
1032
|
+
</html>`;
|
|
1033
|
+
}
|
|
1034
|
+
function exportSOPMarkdown(sop) {
|
|
1035
|
+
const lines = [
|
|
1036
|
+
`# ${sop.title}`,
|
|
1037
|
+
"",
|
|
1038
|
+
`**Beschreibung:** ${sop.description}`,
|
|
1039
|
+
`**Systeme:** ${sop.involvedSystems.join(", ")}`,
|
|
1040
|
+
`**Dauer:** ${sop.estimatedDuration}`,
|
|
1041
|
+
`**H\xE4ufigkeit:** ${sop.frequency}`,
|
|
1042
|
+
`**Confidence:** ${sop.confidence.toFixed(2)}`,
|
|
1043
|
+
"",
|
|
1044
|
+
"## Schritte",
|
|
1045
|
+
""
|
|
1046
|
+
];
|
|
1047
|
+
for (const step of sop.steps) {
|
|
1048
|
+
lines.push(`${step.order}. **${step.tool}**${step.target ? ` \u2192 \`${step.target}\`` : ""}`);
|
|
1049
|
+
lines.push(` ${step.instruction}`);
|
|
1050
|
+
if (step.notes) lines.push(` _${step.notes}_`);
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
}
|
|
1053
|
+
return lines.join("\n");
|
|
1054
|
+
}
|
|
1055
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
|
|
1056
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
1057
|
+
mkdirSync2(join(outputDir, "sops"), { recursive: true });
|
|
1058
|
+
mkdirSync2(join(outputDir, "workflows"), { recursive: true });
|
|
1059
|
+
const nodes = db.getNodes(sessionId);
|
|
1060
|
+
const edges = db.getEdges(sessionId);
|
|
1061
|
+
if (formats.includes("mermaid")) {
|
|
1062
|
+
writeFileSync(join(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
1063
|
+
writeFileSync(join(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
1064
|
+
process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
|
|
1065
|
+
}
|
|
1066
|
+
if (formats.includes("json")) {
|
|
1067
|
+
writeFileSync(join(outputDir, "catalog.json"), exportJSON(db, sessionId));
|
|
1068
|
+
process.stderr.write("\u2713 catalog.json\n");
|
|
1069
|
+
}
|
|
1070
|
+
if (formats.includes("yaml")) {
|
|
1071
|
+
writeFileSync(join(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
1072
|
+
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
1073
|
+
}
|
|
1074
|
+
if (formats.includes("html")) {
|
|
1075
|
+
writeFileSync(join(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
1076
|
+
process.stderr.write("\u2713 topology.html\n");
|
|
1077
|
+
}
|
|
1078
|
+
if (formats.includes("sops")) {
|
|
1079
|
+
const sops = db.getSOPs(sessionId);
|
|
1080
|
+
for (const sop of sops) {
|
|
1081
|
+
const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
|
|
1082
|
+
writeFileSync(join(outputDir, "sops", filename), exportSOPMarkdown(sop));
|
|
1083
|
+
const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
|
|
1084
|
+
writeFileSync(join(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
|
|
1085
|
+
}
|
|
1086
|
+
if (sops.length > 0) {
|
|
1087
|
+
process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
|
|
1088
|
+
`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/preflight.ts
|
|
1094
|
+
import { execSync } from "child_process";
|
|
1095
|
+
import { existsSync, readFileSync } from "fs";
|
|
1096
|
+
import { join as join2 } from "path";
|
|
1097
|
+
function isOAuthLoggedIn() {
|
|
1098
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
1099
|
+
const credFile = join2(home, ".claude", ".credentials.json");
|
|
1100
|
+
if (!existsSync(credFile)) return false;
|
|
1101
|
+
try {
|
|
1102
|
+
const creds = JSON.parse(readFileSync(credFile, "utf8"));
|
|
1103
|
+
const oauth = creds["claudeAiOauth"];
|
|
1104
|
+
return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
|
|
1105
|
+
} catch {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function checkPrerequisites() {
|
|
1110
|
+
try {
|
|
1111
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
1112
|
+
} catch {
|
|
1113
|
+
process.stderr.write(
|
|
1114
|
+
"\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"
|
|
1115
|
+
);
|
|
1116
|
+
process.exitCode = 1;
|
|
1117
|
+
throw new Error("Claude CLI not found");
|
|
1118
|
+
}
|
|
1119
|
+
const hasApiKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
1120
|
+
const hasOAuth = isOAuthLoggedIn();
|
|
1121
|
+
if (!hasApiKey && !hasOAuth) {
|
|
1122
|
+
process.stderr.write(
|
|
1123
|
+
"\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"
|
|
1124
|
+
);
|
|
1125
|
+
} else if (hasOAuth && !hasApiKey) {
|
|
1126
|
+
process.stderr.write("\u2713 Eingeloggt via claude login (Subscription)\n");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function checkPollInterval(intervalMs) {
|
|
1130
|
+
if (intervalMs < MIN_POLL_INTERVAL_MS) {
|
|
1131
|
+
process.stderr.write(
|
|
1132
|
+
`\u26A0 Minimum Shadow-Intervall: ${MIN_POLL_INTERVAL_MS / 1e3} Sekunden (Agent SDK Overhead)
|
|
1133
|
+
`
|
|
1134
|
+
);
|
|
1135
|
+
return MIN_POLL_INTERVAL_MS;
|
|
1136
|
+
}
|
|
1137
|
+
return intervalMs;
|
|
1138
|
+
}
|
|
1139
|
+
export {
|
|
1140
|
+
CartographyDB,
|
|
1141
|
+
MIN_POLL_INTERVAL_MS,
|
|
1142
|
+
checkPollInterval,
|
|
1143
|
+
checkPrerequisites,
|
|
1144
|
+
createCartographyTools,
|
|
1145
|
+
CartographyDB as default,
|
|
1146
|
+
defaultConfig,
|
|
1147
|
+
exportAll,
|
|
1148
|
+
exportBackstageYAML,
|
|
1149
|
+
exportHTML,
|
|
1150
|
+
exportJSON,
|
|
1151
|
+
exportSOPMarkdown,
|
|
1152
|
+
generateDependencyMermaid,
|
|
1153
|
+
generateSOPs,
|
|
1154
|
+
generateTopologyMermaid,
|
|
1155
|
+
generateWorkflowMermaid,
|
|
1156
|
+
runDiscovery,
|
|
1157
|
+
runShadowCycle,
|
|
1158
|
+
safetyHook,
|
|
1159
|
+
stripSensitive
|
|
1160
|
+
};
|
|
1161
|
+
//# sourceMappingURL=index.js.map
|