@datasynx/agentic-ai-cartography 1.1.1 → 2.0.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/README.md +197 -33
- package/dist/bookmarks-VS56KVCO.js +25 -0
- package/dist/chunk-CJ2PITFA.js +785 -0
- package/dist/chunk-CJ2PITFA.js.map +1 -0
- package/dist/chunk-D6SRSLBF.js +48 -0
- package/dist/{chunk-WJR63RWY.js → chunk-J6FDZ6HZ.js} +11 -2
- package/dist/chunk-J6FDZ6HZ.js.map +1 -0
- package/dist/chunk-UGSNG3QJ.js +49 -0
- package/dist/chunk-UGSNG3QJ.js.map +1 -0
- package/dist/chunk-W7YE6AAH.js +1516 -0
- package/dist/chunk-W7YE6AAH.js.map +1 -0
- package/dist/cli.js +133 -664
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +60115 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +734 -0
- package/dist/index.d.ts +363 -7
- package/dist/index.js +1462 -161
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +33 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/onnxruntime_binding-6Q6HXASN.node +0 -0
- package/dist/onnxruntime_binding-EKZT2NRK.node +0 -0
- package/dist/onnxruntime_binding-P6S7V3CI.node +0 -0
- package/dist/onnxruntime_binding-PJNNIIUO.node +0 -0
- package/dist/onnxruntime_binding-UN6SPTQK.node +0 -0
- package/dist/sdk-A6NLO3DJ.js +12294 -0
- package/dist/sdk-A6NLO3DJ.js.map +1 -0
- package/dist/sdk-G5D4WQZ4.js +12293 -0
- package/dist/sdk-G5D4WQZ4.js.map +1 -0
- package/dist/sdk-QSTAREST.js +4869 -0
- package/dist/sdk-QSTAREST.js.map +1 -0
- package/dist/sqlite-vec-EZN67B2V.js +40 -0
- package/dist/sqlite-vec-EZN67B2V.js.map +1 -0
- package/dist/sqlite-vec-UK5YYE5T.js +39 -0
- package/dist/sqlite-vec-UK5YYE5T.js.map +1 -0
- package/dist/transformers.node-BTYUTJK5.js +42884 -0
- package/dist/transformers.node-BTYUTJK5.js.map +1 -0
- package/dist/transformers.node-J6PRTTOX.js +42883 -0
- package/dist/transformers.node-J6PRTTOX.js.map +1 -0
- package/dist/{types-54623ALF.js → types-JG27FR3E.js} +5 -2
- package/dist/types-JG27FR3E.js.map +1 -0
- package/package.json +51 -16
- package/server.json +28 -0
- package/dist/bookmarks-BWNVQGPG.js +0 -14
- package/dist/chunk-QKNYI3SU.js +0 -459
- package/dist/chunk-QKNYI3SU.js.map +0 -1
- package/dist/chunk-WJR63RWY.js.map +0 -1
- /package/dist/{bookmarks-BWNVQGPG.js.map → bookmarks-VS56KVCO.js.map} +0 -0
- /package/dist/{types-54623ALF.js.map → chunk-D6SRSLBF.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./chunk-D6SRSLBF.js";
|
|
2
|
+
|
|
1
3
|
// src/db.ts
|
|
2
4
|
import Database from "better-sqlite3";
|
|
3
5
|
import { mkdirSync } from "fs";
|
|
@@ -24,6 +26,14 @@ var NODE_TYPES = [
|
|
|
24
26
|
"saas_tool",
|
|
25
27
|
"unknown"
|
|
26
28
|
];
|
|
29
|
+
var NODE_TYPE_GROUPS = {
|
|
30
|
+
saas: ["saas_tool"],
|
|
31
|
+
web: ["web_service", "api_endpoint"],
|
|
32
|
+
data: ["database_server", "database", "table", "cache_server"],
|
|
33
|
+
messaging: ["message_broker", "queue", "topic"],
|
|
34
|
+
infra: ["host", "container", "pod", "k8s_cluster"],
|
|
35
|
+
config: ["config_file"]
|
|
36
|
+
};
|
|
27
37
|
var EDGE_RELATIONSHIPS = [
|
|
28
38
|
"connects_to",
|
|
29
39
|
"reads_from",
|
|
@@ -33,7 +43,7 @@ var EDGE_RELATIONSHIPS = [
|
|
|
33
43
|
"depends_on"
|
|
34
44
|
];
|
|
35
45
|
var NodeSchema = z.object({
|
|
36
|
-
id: z.string().describe('Format: "{type}:{host}:{port}"
|
|
46
|
+
id: z.string().describe('Format: "{type}:{host}:{port}" or "{type}:{name}"'),
|
|
37
47
|
type: z.enum(NODE_TYPES),
|
|
38
48
|
name: z.string(),
|
|
39
49
|
discoveredVia: z.string(),
|
|
@@ -295,11 +305,15 @@ CREATE TABLE IF NOT EXISTS node_approvals (
|
|
|
295
305
|
);
|
|
296
306
|
|
|
297
307
|
CREATE INDEX IF NOT EXISTS idx_nodes_session ON nodes(session_id);
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(session_id, type);
|
|
298
309
|
CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
310
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(session_id, source_id);
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(session_id, target_id);
|
|
299
312
|
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
300
313
|
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
301
314
|
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
302
315
|
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_connections_lookup ON connections(session_id, source_asset_id, target_asset_id);
|
|
303
317
|
`;
|
|
304
318
|
var CartographyDB = class {
|
|
305
319
|
db;
|
|
@@ -315,7 +329,8 @@ var CartographyDB = class {
|
|
|
315
329
|
const version = this.db.pragma("user_version", { simple: true });
|
|
316
330
|
if (version === 0) {
|
|
317
331
|
this.db.exec(SCHEMA);
|
|
318
|
-
this.db.pragma("user_version =
|
|
332
|
+
this.db.pragma("user_version = 4");
|
|
333
|
+
return;
|
|
319
334
|
} else if (version === 1) {
|
|
320
335
|
const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
|
|
321
336
|
if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
|
|
@@ -331,14 +346,36 @@ var CartographyDB = class {
|
|
|
331
346
|
created_at TEXT NOT NULL
|
|
332
347
|
);
|
|
333
348
|
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
349
|
+
CREATE INDEX IF NOT EXISTS idx_connections_lookup ON connections(session_id, source_asset_id, target_asset_id);
|
|
350
|
+
`);
|
|
351
|
+
this.db.pragma("user_version = 3");
|
|
352
|
+
}
|
|
353
|
+
if (version === 2) {
|
|
354
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_connections_lookup ON connections(session_id, source_asset_id, target_asset_id)");
|
|
355
|
+
this.db.pragma("user_version = 3");
|
|
356
|
+
}
|
|
357
|
+
const current = this.db.pragma("user_version", { simple: true });
|
|
358
|
+
if (current < 4) {
|
|
359
|
+
this.db.exec(`
|
|
360
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(session_id, type);
|
|
361
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(session_id, source_id);
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(session_id, target_id);
|
|
334
363
|
`);
|
|
335
|
-
this.db.pragma("user_version =
|
|
364
|
+
this.db.pragma("user_version = 4");
|
|
336
365
|
}
|
|
337
366
|
}
|
|
338
367
|
close() {
|
|
339
368
|
this.db.pragma("optimize");
|
|
340
369
|
this.db.close();
|
|
341
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Advanced: the underlying better-sqlite3 connection. Used by the optional
|
|
373
|
+
* semantic-search layer to load the `sqlite-vec` extension and manage its
|
|
374
|
+
* virtual table. Prefer the typed methods above for everything else.
|
|
375
|
+
*/
|
|
376
|
+
rawConnection() {
|
|
377
|
+
return this.db;
|
|
378
|
+
}
|
|
342
379
|
// ── Sessions ────────────────────────────
|
|
343
380
|
createSession(mode, config) {
|
|
344
381
|
const id = crypto.randomUUID();
|
|
@@ -395,10 +432,19 @@ var CartographyDB = class {
|
|
|
395
432
|
node.qualityScore ?? null
|
|
396
433
|
);
|
|
397
434
|
}
|
|
398
|
-
getNodes(sessionId) {
|
|
399
|
-
|
|
435
|
+
getNodes(sessionId, opts) {
|
|
436
|
+
let sql = "SELECT * FROM nodes WHERE session_id = ?";
|
|
437
|
+
if (opts?.limit) {
|
|
438
|
+
sql += ` LIMIT ${opts.limit}`;
|
|
439
|
+
if (opts.offset) sql += ` OFFSET ${opts.offset}`;
|
|
440
|
+
}
|
|
441
|
+
const rows = this.db.prepare(sql).all(sessionId);
|
|
400
442
|
return rows.map((r) => this.mapNode(r));
|
|
401
443
|
}
|
|
444
|
+
getNodeCount(sessionId) {
|
|
445
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM nodes WHERE session_id = ?").get(sessionId);
|
|
446
|
+
return row.cnt;
|
|
447
|
+
}
|
|
402
448
|
mapNode(r) {
|
|
403
449
|
const v = NodeRowSchema.parse(r);
|
|
404
450
|
return {
|
|
@@ -442,8 +488,13 @@ var CartographyDB = class {
|
|
|
442
488
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
443
489
|
);
|
|
444
490
|
}
|
|
445
|
-
getEdges(sessionId) {
|
|
446
|
-
|
|
491
|
+
getEdges(sessionId, opts) {
|
|
492
|
+
let sql = "SELECT * FROM edges WHERE session_id = ?";
|
|
493
|
+
if (opts?.limit) {
|
|
494
|
+
sql += ` LIMIT ${opts.limit}`;
|
|
495
|
+
if (opts.offset) sql += ` OFFSET ${opts.offset}`;
|
|
496
|
+
}
|
|
497
|
+
const rows = this.db.prepare(sql).all(sessionId);
|
|
447
498
|
return rows.map((r) => {
|
|
448
499
|
const v = EdgeRowSchema.parse(r);
|
|
449
500
|
return {
|
|
@@ -645,6 +696,133 @@ var CartographyDB = class {
|
|
|
645
696
|
}
|
|
646
697
|
return rows.length;
|
|
647
698
|
}
|
|
699
|
+
// ── Graph queries (read-only context layer) ─────────────────────────────────
|
|
700
|
+
/** Fetch a single node by id within a session. */
|
|
701
|
+
getNode(sessionId, nodeId) {
|
|
702
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE session_id = ? AND id = ?").get(sessionId, nodeId);
|
|
703
|
+
return row ? this.mapNode(row) : void 0;
|
|
704
|
+
}
|
|
705
|
+
/** Batch-fetch nodes by id, keyed for O(1) lookup. Chunked to stay under SQLite's bind-variable limit. */
|
|
706
|
+
getNodesByIds(sessionId, ids) {
|
|
707
|
+
const out = /* @__PURE__ */ new Map();
|
|
708
|
+
for (let i = 0; i < ids.length; i += 900) {
|
|
709
|
+
const chunk = ids.slice(i, i + 900);
|
|
710
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
711
|
+
const rows = this.db.prepare(
|
|
712
|
+
`SELECT * FROM nodes WHERE session_id = ? AND id IN (${placeholders})`
|
|
713
|
+
).all(sessionId, ...chunk);
|
|
714
|
+
for (const r of rows) {
|
|
715
|
+
const n = this.mapNode(r);
|
|
716
|
+
out.set(n.id, n);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
/** Fetch all nodes of one or more types. */
|
|
722
|
+
getNodesByType(sessionId, types) {
|
|
723
|
+
if (types.length === 0) return [];
|
|
724
|
+
const placeholders = types.map(() => "?").join(",");
|
|
725
|
+
const rows = this.db.prepare(
|
|
726
|
+
`SELECT * FROM nodes WHERE session_id = ? AND type IN (${placeholders})`
|
|
727
|
+
).all(sessionId, ...types);
|
|
728
|
+
return rows.map((r) => this.mapNode(r));
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Lexical search over node id, name, domain, sub-domain and tags.
|
|
732
|
+
* Case-insensitive substring match — the deterministic fallback for semantic search.
|
|
733
|
+
*/
|
|
734
|
+
searchNodes(sessionId, query, opts) {
|
|
735
|
+
const q = `%${query.trim().toLowerCase()}%`;
|
|
736
|
+
const params = [sessionId, q, q, q, q, q];
|
|
737
|
+
let sql = `
|
|
738
|
+
SELECT * FROM nodes
|
|
739
|
+
WHERE session_id = ?
|
|
740
|
+
AND (
|
|
741
|
+
lower(id) LIKE ? OR lower(name) LIKE ?
|
|
742
|
+
OR lower(COALESCE(domain, '')) LIKE ?
|
|
743
|
+
OR lower(COALESCE(sub_domain, '')) LIKE ?
|
|
744
|
+
OR lower(tags) LIKE ?
|
|
745
|
+
)`;
|
|
746
|
+
if (opts?.types && opts.types.length > 0) {
|
|
747
|
+
sql += ` AND type IN (${opts.types.map(() => "?").join(",")})`;
|
|
748
|
+
params.push(...opts.types);
|
|
749
|
+
}
|
|
750
|
+
sql += " ORDER BY confidence DESC";
|
|
751
|
+
if (opts?.limit) sql += ` LIMIT ${Math.max(1, Math.floor(opts.limit))}`;
|
|
752
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
753
|
+
return rows.map((r) => this.mapNode(r));
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Traverse the dependency graph from a node using a recursive CTE with a
|
|
757
|
+
* path-based cycle guard. `downstream` follows source→target (what the node
|
|
758
|
+
* depends on / points to); `upstream` follows target→source (what depends on it).
|
|
759
|
+
*/
|
|
760
|
+
getDependencies(sessionId, nodeId, opts = {}) {
|
|
761
|
+
const direction = opts.direction ?? "downstream";
|
|
762
|
+
const maxDepth = Math.max(1, Math.min(opts.maxDepth ?? 8, 64));
|
|
763
|
+
const root = this.getNode(sessionId, nodeId);
|
|
764
|
+
const depthById = /* @__PURE__ */ new Map();
|
|
765
|
+
const collect = (dir) => {
|
|
766
|
+
const [from, to] = dir === "downstream" ? ["source_id", "target_id"] : ["target_id", "source_id"];
|
|
767
|
+
const sql = `
|
|
768
|
+
WITH RECURSIVE walk(node_id, depth, path) AS (
|
|
769
|
+
SELECT ?, 0, char(10) || ? || char(10)
|
|
770
|
+
UNION ALL
|
|
771
|
+
SELECT e.${to}, w.depth + 1, w.path || e.${to} || char(10)
|
|
772
|
+
FROM edges e JOIN walk w ON e.${from} = w.node_id
|
|
773
|
+
WHERE e.session_id = ?
|
|
774
|
+
AND w.depth < ?
|
|
775
|
+
AND instr(w.path, char(10) || e.${to} || char(10)) = 0
|
|
776
|
+
)
|
|
777
|
+
SELECT node_id, MIN(depth) AS depth FROM walk WHERE node_id != ? GROUP BY node_id`;
|
|
778
|
+
const rows = this.db.prepare(sql).all(nodeId, nodeId, sessionId, maxDepth, nodeId);
|
|
779
|
+
for (const r of rows) {
|
|
780
|
+
const prev = depthById.get(r.node_id);
|
|
781
|
+
if (prev === void 0 || r.depth < prev) depthById.set(r.node_id, r.depth);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
if (direction === "both") {
|
|
785
|
+
collect("downstream");
|
|
786
|
+
collect("upstream");
|
|
787
|
+
} else collect(direction);
|
|
788
|
+
const byId = this.getNodesByIds(sessionId, [...depthById.keys()]);
|
|
789
|
+
const nodes = [...depthById.entries()].map(([id, depth]) => {
|
|
790
|
+
const n = byId.get(id);
|
|
791
|
+
return n ? { ...n, depth } : void 0;
|
|
792
|
+
}).filter((n) => n !== void 0).sort((a, b) => a.depth - b.depth);
|
|
793
|
+
const reachable = /* @__PURE__ */ new Set([nodeId, ...depthById.keys()]);
|
|
794
|
+
const edges = this.getEdges(sessionId).filter((e) => reachable.has(e.sourceId) && reachable.has(e.targetId));
|
|
795
|
+
return { root, direction, maxDepth, nodes, edges };
|
|
796
|
+
}
|
|
797
|
+
/** Lightweight aggregate index of the whole topology — the progressive-disclosure summary. */
|
|
798
|
+
getGraphSummary(sessionId) {
|
|
799
|
+
const totals = {
|
|
800
|
+
nodes: this.db.prepare("SELECT COUNT(*) c FROM nodes WHERE session_id = ?").get(sessionId).c,
|
|
801
|
+
edges: this.db.prepare("SELECT COUNT(*) c FROM edges WHERE session_id = ?").get(sessionId).c
|
|
802
|
+
};
|
|
803
|
+
const byType = {};
|
|
804
|
+
for (const r of this.db.prepare("SELECT type, COUNT(*) c FROM nodes WHERE session_id = ? GROUP BY type").all(sessionId)) {
|
|
805
|
+
byType[r.type] = r.c;
|
|
806
|
+
}
|
|
807
|
+
const byDomain = {};
|
|
808
|
+
for (const r of this.db.prepare("SELECT COALESCE(domain, '(none)') d, COUNT(*) c FROM nodes WHERE session_id = ? GROUP BY d").all(sessionId)) {
|
|
809
|
+
byDomain[r.d] = r.c;
|
|
810
|
+
}
|
|
811
|
+
const byRelationship = {};
|
|
812
|
+
for (const r of this.db.prepare("SELECT relationship rel, COUNT(*) c FROM edges WHERE session_id = ? GROUP BY rel").all(sessionId)) {
|
|
813
|
+
byRelationship[r.rel] = r.c;
|
|
814
|
+
}
|
|
815
|
+
const topConnected = this.db.prepare(`
|
|
816
|
+
SELECT n.id, n.name, n.type, COUNT(e.id) AS degree
|
|
817
|
+
FROM nodes n
|
|
818
|
+
LEFT JOIN edges e ON e.session_id = n.session_id AND (e.source_id = n.id OR e.target_id = n.id)
|
|
819
|
+
WHERE n.session_id = ?
|
|
820
|
+
GROUP BY n.id, n.name, n.type
|
|
821
|
+
ORDER BY degree DESC, n.confidence DESC
|
|
822
|
+
LIMIT 10
|
|
823
|
+
`).all(sessionId);
|
|
824
|
+
return { sessionId, totals, nodesByType: byType, nodesByDomain: byDomain, edgesByRelationship: byRelationship, topConnected };
|
|
825
|
+
}
|
|
648
826
|
// ── Stats ───────────────────────────────
|
|
649
827
|
getStats(sessionId) {
|
|
650
828
|
const nodes = this.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE session_id = ?").get(sessionId).c;
|
|
@@ -655,8 +833,354 @@ var CartographyDB = class {
|
|
|
655
833
|
}
|
|
656
834
|
};
|
|
657
835
|
|
|
658
|
-
// src/
|
|
836
|
+
// src/mcp/server.ts
|
|
837
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
659
838
|
import { z as z3 } from "zod";
|
|
839
|
+
var SERVER_NAME = "cartography";
|
|
840
|
+
var SERVER_VERSION = "2.0.0";
|
|
841
|
+
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
842
|
+
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
843
|
+
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
844
|
+
function compactNode(n) {
|
|
845
|
+
return {
|
|
846
|
+
id: n.id,
|
|
847
|
+
type: n.type,
|
|
848
|
+
name: n.name,
|
|
849
|
+
confidence: n.confidence,
|
|
850
|
+
...n.domain ? { domain: n.domain } : {},
|
|
851
|
+
...n.tags.length ? { tags: n.tags } : {}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function json(data) {
|
|
855
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
856
|
+
}
|
|
857
|
+
function summaryText(s) {
|
|
858
|
+
const lines = [
|
|
859
|
+
`# Infrastructure topology \u2014 session ${s.sessionId}`,
|
|
860
|
+
``,
|
|
861
|
+
`Totals: ${s.totals.nodes} nodes, ${s.totals.edges} edges`,
|
|
862
|
+
``,
|
|
863
|
+
`Nodes by type:`,
|
|
864
|
+
...Object.entries(s.nodesByType).sort((a, b) => b[1] - a[1]).map(([t, c]) => ` - ${t}: ${c}`),
|
|
865
|
+
``,
|
|
866
|
+
`Nodes by domain:`,
|
|
867
|
+
...Object.entries(s.nodesByDomain).sort((a, b) => b[1] - a[1]).map(([d, c]) => ` - ${d}: ${c}`),
|
|
868
|
+
``,
|
|
869
|
+
`Edges by relationship:`,
|
|
870
|
+
...Object.entries(s.edgesByRelationship).sort((a, b) => b[1] - a[1]).map(([r, c]) => ` - ${r}: ${c}`),
|
|
871
|
+
``,
|
|
872
|
+
`Most connected:`,
|
|
873
|
+
...s.topConnected.map((n) => ` - ${n.id} (${n.type}) \u2014 degree ${n.degree}`),
|
|
874
|
+
``,
|
|
875
|
+
`Read cartography://nodes/{id} or cartography://dependencies/{id} for detail.`
|
|
876
|
+
];
|
|
877
|
+
return lines.join("\n");
|
|
878
|
+
}
|
|
879
|
+
function createMcpServer(opts = {}) {
|
|
880
|
+
const db = opts.db ?? new CartographyDB(opts.dbPath ?? defaultConfig().dbPath);
|
|
881
|
+
const search = opts.search ?? lexicalSearch;
|
|
882
|
+
const resolveSession = () => {
|
|
883
|
+
if (opts.session && opts.session !== "latest") return opts.session;
|
|
884
|
+
return db.getLatestSession("discover")?.id ?? db.getLatestSession()?.id;
|
|
885
|
+
};
|
|
886
|
+
const server = new McpServer(
|
|
887
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
888
|
+
{
|
|
889
|
+
capabilities: { resources: { subscribe: true, listChanged: true }, tools: {}, prompts: {}, logging: {} },
|
|
890
|
+
instructions: "Cartography exposes a discovered infrastructure/SaaS topology. Start by reading cartography://graph/summary for a low-token overview, then drill into specific nodes via cartography://nodes/{id} or query with the query_infrastructure / get_dependencies tools."
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
server.registerResource(
|
|
894
|
+
"graph-summary",
|
|
895
|
+
"cartography://graph/summary",
|
|
896
|
+
{ title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
|
|
897
|
+
(uri) => {
|
|
898
|
+
const sid = resolveSession();
|
|
899
|
+
if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
|
|
900
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(db.getGraphSummary(sid)) }] };
|
|
901
|
+
}
|
|
902
|
+
);
|
|
903
|
+
server.registerResource(
|
|
904
|
+
"nodes-index",
|
|
905
|
+
"cartography://nodes",
|
|
906
|
+
{ title: "Node index", description: "Lightweight list of all nodes (id, type, name only).", mimeType: "application/json" },
|
|
907
|
+
(uri) => {
|
|
908
|
+
const sid = resolveSession();
|
|
909
|
+
const nodes = sid ? db.getNodes(sid) : [];
|
|
910
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ count: nodes.length, nodes: nodes.map((n) => ({ id: n.id, type: n.type, name: n.name })) }, null, 2) }] };
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
server.registerResource(
|
|
914
|
+
"node-detail",
|
|
915
|
+
new ResourceTemplate("cartography://nodes/{id}", { list: void 0 }),
|
|
916
|
+
{ title: "Node detail", description: "Full node record plus its incident edges.", mimeType: "application/json" },
|
|
917
|
+
(uri, variables) => {
|
|
918
|
+
const sid = resolveSession();
|
|
919
|
+
const id = decodeURIComponent(String(variables["id"]));
|
|
920
|
+
const node = sid ? db.getNode(sid, id) : void 0;
|
|
921
|
+
if (!node) return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: `node not found: ${id}` }) }] };
|
|
922
|
+
const edges = db.getEdges(sid).filter((e) => e.sourceId === id || e.targetId === id);
|
|
923
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ node, edges }, null, 2) }] };
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
const typedListResource = (name, uri, title, types) => server.registerResource(name, uri, { title, description: `Nodes of type: ${types.join(", ")}.`, mimeType: "application/json" }, (u) => {
|
|
927
|
+
const sid = resolveSession();
|
|
928
|
+
const nodes = sid ? db.getNodesByType(sid, types) : [];
|
|
929
|
+
return { contents: [{ uri: u.href, mimeType: "application/json", text: JSON.stringify({ count: nodes.length, nodes: nodes.map(compactNode) }, null, 2) }] };
|
|
930
|
+
});
|
|
931
|
+
typedListResource("services", "cartography://services", "Services", SERVICE_TYPES);
|
|
932
|
+
typedListResource("databases", "cartography://databases", "Data stores", DATA_TYPES);
|
|
933
|
+
server.registerResource(
|
|
934
|
+
"dependencies",
|
|
935
|
+
new ResourceTemplate("cartography://dependencies/{id}", { list: void 0 }),
|
|
936
|
+
{ title: "Dependencies", description: "Transitive downstream dependencies of a node.", mimeType: "application/json" },
|
|
937
|
+
(uri, variables) => {
|
|
938
|
+
const sid = resolveSession();
|
|
939
|
+
const id = decodeURIComponent(String(variables["id"]));
|
|
940
|
+
if (!sid) return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "no session" }) }] };
|
|
941
|
+
const r = db.getDependencies(sid, id, { direction: "downstream", maxDepth: 8 });
|
|
942
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ root: id, count: r.nodes.length, nodes: r.nodes.map((n) => ({ ...compactNode(n), depth: n.depth })) }, null, 2) }] };
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
server.registerResource(
|
|
946
|
+
"sessions",
|
|
947
|
+
"cartography://sessions",
|
|
948
|
+
{ title: "Discovery sessions", description: "All discovery sessions in the catalog.", mimeType: "application/json" },
|
|
949
|
+
(uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(db.getSessions(), null, 2) }] })
|
|
950
|
+
);
|
|
951
|
+
server.registerTool(
|
|
952
|
+
"get_summary",
|
|
953
|
+
{ title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected).", inputSchema: {} },
|
|
954
|
+
() => {
|
|
955
|
+
const sid = resolveSession();
|
|
956
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
957
|
+
return json(db.getGraphSummary(sid));
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
server.registerTool(
|
|
961
|
+
"query_infrastructure",
|
|
962
|
+
{
|
|
963
|
+
title: "Query infrastructure",
|
|
964
|
+
description: "Search the topology by name/id/domain (optionally filtered by node type). Returns compact node records.",
|
|
965
|
+
inputSchema: {
|
|
966
|
+
query: z3.string().describe('Free-text query, e.g. "postgres", "auth", "github"'),
|
|
967
|
+
types: z3.array(z3.enum(NODE_TYPES)).optional().describe("Restrict to these node types"),
|
|
968
|
+
limit: z3.number().int().min(1).max(200).default(25).optional()
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
async (args) => {
|
|
972
|
+
const sid = resolveSession();
|
|
973
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
974
|
+
const results = await search(db, sid, args.query, { types: args.types, limit: args.limit ?? 25 });
|
|
975
|
+
return json({ count: results.length, results: results.map((r) => ({ ...compactNode(r.node), ...r.score !== void 0 ? { score: r.score } : {} })) });
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
server.registerTool(
|
|
979
|
+
"search_topology",
|
|
980
|
+
{
|
|
981
|
+
title: "Search topology (semantic)",
|
|
982
|
+
description: "Find nodes related to a concept by meaning (semantic search when available, lexical otherwise).",
|
|
983
|
+
inputSchema: { query: z3.string(), limit: z3.number().int().min(1).max(100).default(10).optional() }
|
|
984
|
+
},
|
|
985
|
+
async (args) => {
|
|
986
|
+
const sid = resolveSession();
|
|
987
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
988
|
+
const results = await search(db, sid, args.query, { limit: args.limit ?? 10 });
|
|
989
|
+
return json({ count: results.length, results: results.map((r) => ({ ...compactNode(r.node), ...r.score !== void 0 ? { score: r.score } : {} })) });
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
server.registerTool(
|
|
993
|
+
"list_services",
|
|
994
|
+
{
|
|
995
|
+
title: "List services",
|
|
996
|
+
description: "List discovered services or data stores.",
|
|
997
|
+
inputSchema: { kind: z3.enum(["services", "databases", "all"]).default("all").optional() }
|
|
998
|
+
},
|
|
999
|
+
(args) => {
|
|
1000
|
+
const sid = resolveSession();
|
|
1001
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
1002
|
+
const kind = args.kind ?? "all";
|
|
1003
|
+
const types = kind === "services" ? SERVICE_TYPES : kind === "databases" ? DATA_TYPES : [...SERVICE_TYPES, ...DATA_TYPES];
|
|
1004
|
+
return json(db.getNodesByType(sid, types).map(compactNode));
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
server.registerTool(
|
|
1008
|
+
"get_node",
|
|
1009
|
+
{ title: "Get node", description: "Fetch a single node with its incident edges.", inputSchema: { id: z3.string() } },
|
|
1010
|
+
(args) => {
|
|
1011
|
+
const sid = resolveSession();
|
|
1012
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
1013
|
+
const node = db.getNode(sid, args.id);
|
|
1014
|
+
if (!node) return json({ error: `node not found: ${args.id}` });
|
|
1015
|
+
const edges = db.getEdges(sid).filter((e) => e.sourceId === args.id || e.targetId === args.id);
|
|
1016
|
+
return json({ node, edges });
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
server.registerTool(
|
|
1020
|
+
"get_dependencies",
|
|
1021
|
+
{
|
|
1022
|
+
title: "Get dependencies",
|
|
1023
|
+
description: "Traverse the dependency graph from a node (downstream/upstream/both) with a depth limit.",
|
|
1024
|
+
inputSchema: {
|
|
1025
|
+
id: z3.string(),
|
|
1026
|
+
direction: z3.enum(["downstream", "upstream", "both"]).default("downstream").optional(),
|
|
1027
|
+
maxDepth: z3.number().int().min(1).max(64).default(8).optional()
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
(args) => {
|
|
1031
|
+
const sid = resolveSession();
|
|
1032
|
+
if (!sid) return json({ error: "No discovery session found." });
|
|
1033
|
+
const r = db.getDependencies(sid, args.id, { direction: args.direction ?? "downstream", maxDepth: args.maxDepth ?? 8 });
|
|
1034
|
+
return json({
|
|
1035
|
+
root: r.root ? compactNode(r.root) : null,
|
|
1036
|
+
direction: r.direction,
|
|
1037
|
+
count: r.nodes.length,
|
|
1038
|
+
nodes: r.nodes.map((n) => ({ ...compactNode(n), depth: n.depth })),
|
|
1039
|
+
edges: r.edges.map((e) => ({ from: e.sourceId, to: e.targetId, rel: e.relationship }))
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
);
|
|
1043
|
+
if (opts.discovery) {
|
|
1044
|
+
const discovery = opts.discovery;
|
|
1045
|
+
server.registerTool(
|
|
1046
|
+
"run_discovery",
|
|
1047
|
+
{
|
|
1048
|
+
title: "Run discovery",
|
|
1049
|
+
description: "Scan the local system (read-only) and update the catalog. Returns counts of nodes/edges found.",
|
|
1050
|
+
inputSchema: { hint: z3.string().optional().describe("Optional focus, e.g. tool names to look for") }
|
|
1051
|
+
},
|
|
1052
|
+
async (args) => {
|
|
1053
|
+
let sid = resolveSession();
|
|
1054
|
+
if (!sid) sid = db.createSession("discover", defaultConfig());
|
|
1055
|
+
const result = await discovery(db, sid, { hint: args.hint });
|
|
1056
|
+
server.server.sendResourceUpdated({ uri: "cartography://graph/summary" }).catch(() => {
|
|
1057
|
+
});
|
|
1058
|
+
server.server.sendResourceListChanged?.();
|
|
1059
|
+
return json({ session: sid, ...result });
|
|
1060
|
+
}
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
server.registerPrompt(
|
|
1064
|
+
"audit-attack-surface",
|
|
1065
|
+
{ title: "Audit attack surface", description: "Review the discovered topology for externally-reachable services and risky dependencies." },
|
|
1066
|
+
() => ({
|
|
1067
|
+
messages: [{
|
|
1068
|
+
role: "user",
|
|
1069
|
+
content: { type: "text", text: "Read cartography://graph/summary and cartography://services. Identify externally-reachable services, data stores with broad inbound dependencies, and any node with low confidence that warrants verification. Use get_dependencies to assess blast radius. Summarize the attack surface and concrete hardening recommendations." }
|
|
1070
|
+
}]
|
|
1071
|
+
})
|
|
1072
|
+
);
|
|
1073
|
+
server.registerPrompt(
|
|
1074
|
+
"map-service-dependencies",
|
|
1075
|
+
{
|
|
1076
|
+
title: "Map service dependencies",
|
|
1077
|
+
description: "Produce a dependency map for a given service.",
|
|
1078
|
+
argsSchema: { service: z3.string().describe("Service node id or name") }
|
|
1079
|
+
},
|
|
1080
|
+
(args) => ({
|
|
1081
|
+
messages: [{
|
|
1082
|
+
role: "user",
|
|
1083
|
+
content: { type: "text", text: `Use query_infrastructure to locate "${args.service}", then get_dependencies (direction=both) to map everything it depends on and everything that depends on it. Present the result as a clear dependency tree and call out single points of failure.` }
|
|
1084
|
+
}]
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
server.registerPrompt(
|
|
1088
|
+
"onboard-to-system",
|
|
1089
|
+
{ title: "Onboard to system", description: "Explain the system landscape to a new engineer." },
|
|
1090
|
+
() => ({
|
|
1091
|
+
messages: [{
|
|
1092
|
+
role: "user",
|
|
1093
|
+
content: { type: "text", text: "Read cartography://graph/summary, then cartography://services and cartography://databases. Write a concise onboarding briefing for a new engineer: what the major systems are, how they connect, which data stores are central, and where to look first." }
|
|
1094
|
+
}]
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
return server;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/mcp/transports.ts
|
|
1101
|
+
import { randomUUID } from "crypto";
|
|
1102
|
+
import http from "http";
|
|
1103
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1104
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1105
|
+
async function runStdio(server) {
|
|
1106
|
+
const transport = new StdioServerTransport();
|
|
1107
|
+
await server.connect(transport);
|
|
1108
|
+
}
|
|
1109
|
+
async function readJsonBody(req) {
|
|
1110
|
+
const chunks = [];
|
|
1111
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1112
|
+
if (chunks.length === 0) return void 0;
|
|
1113
|
+
try {
|
|
1114
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1115
|
+
} catch {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function runHttp(factory, opts = {}) {
|
|
1120
|
+
const host = opts.host ?? "127.0.0.1";
|
|
1121
|
+
const port = opts.port ?? 3737;
|
|
1122
|
+
const allowedHosts = opts.allowedHosts ?? [`${host}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
|
|
1123
|
+
const transports = /* @__PURE__ */ new Map();
|
|
1124
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const url = req.url ?? "";
|
|
1127
|
+
if (!url.startsWith("/mcp")) {
|
|
1128
|
+
res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1132
|
+
const existing = sessionId ? transports.get(sessionId) : void 0;
|
|
1133
|
+
if (existing) {
|
|
1134
|
+
const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
|
|
1135
|
+
await existing.handleRequest(req, res, body2);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (req.method !== "POST") {
|
|
1139
|
+
res.writeHead(400, { "content-type": "application/json" }).end('{"error":"missing or unknown mcp-session-id"}');
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const body = await readJsonBody(req);
|
|
1143
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1144
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1145
|
+
enableDnsRebindingProtection: true,
|
|
1146
|
+
allowedHosts,
|
|
1147
|
+
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
1148
|
+
onsessioninitialized: (id) => {
|
|
1149
|
+
transports.set(id, transport);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
transport.onclose = () => {
|
|
1153
|
+
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
1154
|
+
};
|
|
1155
|
+
await factory().connect(transport);
|
|
1156
|
+
await transport.handleRequest(req, res, body);
|
|
1157
|
+
} catch {
|
|
1158
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" }).end('{"error":"internal error"}');
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
await new Promise((resolve) => httpServer.listen(port, host, resolve));
|
|
1162
|
+
return httpServer;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/scanners/types.ts
|
|
1166
|
+
var ScannerRegistry = class {
|
|
1167
|
+
scanners = /* @__PURE__ */ new Map();
|
|
1168
|
+
register(scanner) {
|
|
1169
|
+
if (this.scanners.has(scanner.id)) throw new Error(`scanner already registered: ${scanner.id}`);
|
|
1170
|
+
this.scanners.set(scanner.id, scanner);
|
|
1171
|
+
return this;
|
|
1172
|
+
}
|
|
1173
|
+
get(id) {
|
|
1174
|
+
return this.scanners.get(id);
|
|
1175
|
+
}
|
|
1176
|
+
list() {
|
|
1177
|
+
return [...this.scanners.values()];
|
|
1178
|
+
}
|
|
1179
|
+
/** Scanners whose `platforms` include the given platform. */
|
|
1180
|
+
forPlatform(platform) {
|
|
1181
|
+
return this.list().filter((s) => s.platforms === "all" || s.platforms.includes(platform));
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
660
1184
|
|
|
661
1185
|
// src/bookmarks.ts
|
|
662
1186
|
import { tmpdir } from "os";
|
|
@@ -668,6 +1192,340 @@ import { homedir } from "os";
|
|
|
668
1192
|
import { join } from "path";
|
|
669
1193
|
import { execSync } from "child_process";
|
|
670
1194
|
import { existsSync } from "fs";
|
|
1195
|
+
|
|
1196
|
+
// src/allowlist.ts
|
|
1197
|
+
var READONLY_BINARIES = /* @__PURE__ */ new Set([
|
|
1198
|
+
// shell & text utilities
|
|
1199
|
+
"echo",
|
|
1200
|
+
"printf",
|
|
1201
|
+
"true",
|
|
1202
|
+
"false",
|
|
1203
|
+
"test",
|
|
1204
|
+
"cat",
|
|
1205
|
+
"head",
|
|
1206
|
+
"tail",
|
|
1207
|
+
"grep",
|
|
1208
|
+
"egrep",
|
|
1209
|
+
"fgrep",
|
|
1210
|
+
"awk",
|
|
1211
|
+
"sed",
|
|
1212
|
+
"cut",
|
|
1213
|
+
"sort",
|
|
1214
|
+
"uniq",
|
|
1215
|
+
"wc",
|
|
1216
|
+
"tr",
|
|
1217
|
+
"xargs",
|
|
1218
|
+
"tee",
|
|
1219
|
+
"ls",
|
|
1220
|
+
"find",
|
|
1221
|
+
"which",
|
|
1222
|
+
"command",
|
|
1223
|
+
"type",
|
|
1224
|
+
"basename",
|
|
1225
|
+
"dirname",
|
|
1226
|
+
"realpath",
|
|
1227
|
+
"readlink",
|
|
1228
|
+
"stat",
|
|
1229
|
+
"file",
|
|
1230
|
+
"printenv",
|
|
1231
|
+
"date",
|
|
1232
|
+
"hostname",
|
|
1233
|
+
"uname",
|
|
1234
|
+
"whoami",
|
|
1235
|
+
"id",
|
|
1236
|
+
"pwd",
|
|
1237
|
+
"expr",
|
|
1238
|
+
"seq",
|
|
1239
|
+
"tac",
|
|
1240
|
+
"rev",
|
|
1241
|
+
"column",
|
|
1242
|
+
"paste",
|
|
1243
|
+
// network & process inspection (read-only)
|
|
1244
|
+
"ss",
|
|
1245
|
+
"netstat",
|
|
1246
|
+
"lsof",
|
|
1247
|
+
"ps",
|
|
1248
|
+
"ip",
|
|
1249
|
+
"ifconfig",
|
|
1250
|
+
"arp",
|
|
1251
|
+
"dig",
|
|
1252
|
+
"nslookup",
|
|
1253
|
+
"host",
|
|
1254
|
+
// database clients (read-only usage is enforced separately for risky verbs)
|
|
1255
|
+
"psql",
|
|
1256
|
+
"mysql",
|
|
1257
|
+
"mysqladmin",
|
|
1258
|
+
"mongosh",
|
|
1259
|
+
"redis-cli",
|
|
1260
|
+
"sqlite3",
|
|
1261
|
+
"pg_lsclusters",
|
|
1262
|
+
"clickhouse-client",
|
|
1263
|
+
// macOS
|
|
1264
|
+
"mdfind"
|
|
1265
|
+
]);
|
|
1266
|
+
var CONDITIONAL_BINARIES = /* @__PURE__ */ new Set(["tee"]);
|
|
1267
|
+
var PKG_MANAGERS = /* @__PURE__ */ new Set(["dpkg", "rpm", "snap", "flatpak", "brew", "winget", "choco", "scoop", "apt-cache"]);
|
|
1268
|
+
var MUTATING_PKG = /^(install|uninstall|reinstall|remove|purge|erase|upgrade|update|add|delete|pin|enable|disable|-i|--install|-r|--remove|-P|--purge|-e|--erase|-U|--upgrade|-F|--freshen)$/i;
|
|
1269
|
+
var COMMAND_RUNNERS = /* @__PURE__ */ new Set(["xargs", "env", "nice", "nohup", "timeout", "time", "stdbuf", "watch", "sudo"]);
|
|
1270
|
+
var DANGEROUS_PS = /\b(Remove-Item|Remove-ItemProperty|Move-Item|Copy-Item|Rename-Item|New-Item|New-Service|Set-Content|Add-Content|Clear-Content|Out-File|Set-ItemProperty|Set-Service|Stop-Process|Stop-Service|Start-Service|Restart-Service|Stop-Computer|Restart-Computer|Format-Volume|Clear-Disk|Remove-\w+|Uninstall-\w+|Install-\w+|Set-\w+|New-\w+|Start-\w+|Stop-\w+|Restart-\w+|Invoke-Expression|iex|Invoke-WebRequest|Invoke-RestMethod|Invoke-Command|Start-Process|Register-\w+|Unregister-\w+|Disable-\w+|Enable-\w+|Reset-\w+|del|rmdir|rd)\b/i;
|
|
1271
|
+
var DANGEROUS_POSIX = /\b(rm|rmdir|mv|dd|mkfs|chmod|chown|chgrp|kill|killall|pkill|reboot|shutdown|poweroff|halt|truncate|shred|fdisk|parted)\b/i;
|
|
1272
|
+
var SUBCOMMAND_RULES = {
|
|
1273
|
+
kubectl: (t) => allowFirstVerb(t, ["get", "describe", "top", "logs", "explain", "config", "version", "cluster-info", "api-resources", "api-versions", "auth"]),
|
|
1274
|
+
docker: (t) => allowFirstVerb(t, ["ps", "images", "inspect", "version", "info", "logs", "stats", "top", "port", "history", "diff", "system", "context", "volume", "network", "image", "container"]) && !hasMutatingDockerVerb(t),
|
|
1275
|
+
podman: (t) => SUBCOMMAND_RULES["docker"](t),
|
|
1276
|
+
helm: (t) => allowFirstVerb(t, ["list", "ls", "status", "get", "show", "history", "version", "repo", "search", "env"]),
|
|
1277
|
+
systemctl: (t) => allowFirstVerb(t, ["status", "show", "list-units", "list-unit-files", "list-sockets", "list-timers", "list-dependencies", "is-active", "is-enabled", "is-failed", "cat", "get-default", "show-environment"]),
|
|
1278
|
+
service: (t) => t.some((x) => /^status$/i.test(x)),
|
|
1279
|
+
// cloud CLIs: read-only actions only — must contain a read verb, never a mutating one
|
|
1280
|
+
aws: (t) => containsAwsReadAction(t) && !hasMutatingCloudVerb(t),
|
|
1281
|
+
gcloud: (t) => (hasToken(t, ["list", "describe"]) || isInfoOnly(t)) && !hasMutatingCloudVerb(t),
|
|
1282
|
+
az: (t) => (hasToken(t, ["list", "show"]) || isInfoOnly(t)) && !hasMutatingCloudVerb(t),
|
|
1283
|
+
// version control (read-only verbs only)
|
|
1284
|
+
git: (t) => allowFirstVerb(t, ["status", "log", "show", "diff", "branch", "remote", "config", "rev-parse", "ls-files", "ls-remote", "describe", "tag", "shortlog", "cat-file", "symbolic-ref"]),
|
|
1285
|
+
gh: (t) => allowFirstVerb(t, ["repo", "pr", "issue", "release", "api", "auth", "status"]) && hasToken(t, ["list", "view", "status", "get"])
|
|
1286
|
+
};
|
|
1287
|
+
var FETCH_RULES = {
|
|
1288
|
+
curl: (t) => !t.some((x) => /^-X$/i.test(x) || /^--request$/i.test(x) || /^-[dF]$/.test(x) || /^--data/i.test(x) || /^--form$/i.test(x) || /^-[oO]$/.test(x) || /^--output$/i.test(x) || /^--upload-file$/i.test(x)),
|
|
1289
|
+
wget: (t) => !t.some((x) => /^-O$/.test(x) || /^--output-document/i.test(x) || /^--post-data/i.test(x) || /^--method/i.test(x) || /^-i$/.test(x))
|
|
1290
|
+
};
|
|
1291
|
+
var READONLY_PS_VERBS = /* @__PURE__ */ new Set([
|
|
1292
|
+
"get",
|
|
1293
|
+
"select",
|
|
1294
|
+
"where",
|
|
1295
|
+
"measure",
|
|
1296
|
+
"sort",
|
|
1297
|
+
"format",
|
|
1298
|
+
"out",
|
|
1299
|
+
"convertto",
|
|
1300
|
+
"convertfrom",
|
|
1301
|
+
"compare",
|
|
1302
|
+
"test",
|
|
1303
|
+
"resolve",
|
|
1304
|
+
"split",
|
|
1305
|
+
"join",
|
|
1306
|
+
"group",
|
|
1307
|
+
"foreach",
|
|
1308
|
+
"write",
|
|
1309
|
+
"read",
|
|
1310
|
+
"show",
|
|
1311
|
+
"find",
|
|
1312
|
+
"search",
|
|
1313
|
+
"tee"
|
|
1314
|
+
]);
|
|
1315
|
+
var READONLY_PS_BARE = /* @__PURE__ */ new Set(["where", "select", "sort", "foreach", "ft", "fl", "gci", "gc", "gm", "gps", "gsv", "echo", "write-host", "write-output"]);
|
|
1316
|
+
function allowFirstVerb(tokens, verbs) {
|
|
1317
|
+
const verb = tokens.find((t) => !t.startsWith("-"));
|
|
1318
|
+
return verb !== void 0 && verbs.includes(verb.toLowerCase());
|
|
1319
|
+
}
|
|
1320
|
+
function hasToken(tokens, any) {
|
|
1321
|
+
const lower = tokens.map((t) => t.toLowerCase());
|
|
1322
|
+
return any.some((a) => lower.includes(a));
|
|
1323
|
+
}
|
|
1324
|
+
function isInfoOnly(tokens) {
|
|
1325
|
+
return hasToken(tokens, ["config", "account", "version", "info"]);
|
|
1326
|
+
}
|
|
1327
|
+
var MUTATING_CLOUD = /^(create|delete|update|put|set|add|remove|deploy|run|start|stop|restart|reboot|terminate|modify|attach|detach|associate|disassociate|enable|disable|invoke|exec|apply|destroy|scale|patch|register|deregister|import|copy|move|rename|reset|rotate|revoke|grant)([-_].*)?$/i;
|
|
1328
|
+
function hasMutatingCloudVerb(tokens) {
|
|
1329
|
+
return tokens.some((t) => !t.startsWith("-") && MUTATING_CLOUD.test(t));
|
|
1330
|
+
}
|
|
1331
|
+
function containsAwsReadAction(tokens) {
|
|
1332
|
+
return tokens.some((t) => /^(describe|list|get|lookup|search|scan|view|ls)[-_a-z0-9]*$/i.test(t) || t.toLowerCase() === "ls");
|
|
1333
|
+
}
|
|
1334
|
+
function hasMutatingDockerVerb(tokens) {
|
|
1335
|
+
return tokens.some((t) => /^(run|rm|rmi|exec|build|push|pull|start|stop|kill|create|commit|cp|save|load|tag|login|logout|prune|kill|restart|pause|unpause|rename|update|export|import)$/i.test(t));
|
|
1336
|
+
}
|
|
1337
|
+
function splitSegments(cmd) {
|
|
1338
|
+
const segments = [];
|
|
1339
|
+
let buf = "";
|
|
1340
|
+
let quote = null;
|
|
1341
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
1342
|
+
const c = cmd[i];
|
|
1343
|
+
const next = cmd[i + 1];
|
|
1344
|
+
if (quote) {
|
|
1345
|
+
buf += c;
|
|
1346
|
+
if (c === quote) quote = null;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (c === '"' || c === "'") {
|
|
1350
|
+
quote = c;
|
|
1351
|
+
buf += c;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (c === "|" && next === "|" || c === "&" && next === "&") {
|
|
1355
|
+
segments.push(buf);
|
|
1356
|
+
buf = "";
|
|
1357
|
+
i++;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (c === "|" || c === ";" || c === "\n") {
|
|
1361
|
+
segments.push(buf);
|
|
1362
|
+
buf = "";
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
buf += c;
|
|
1366
|
+
}
|
|
1367
|
+
segments.push(buf);
|
|
1368
|
+
return segments.map((s) => s.trim()).filter(Boolean);
|
|
1369
|
+
}
|
|
1370
|
+
function tokenize(segment) {
|
|
1371
|
+
const tokens = [];
|
|
1372
|
+
let buf = "";
|
|
1373
|
+
let quote = null;
|
|
1374
|
+
let started = false;
|
|
1375
|
+
const push = () => {
|
|
1376
|
+
if (started) {
|
|
1377
|
+
tokens.push(buf);
|
|
1378
|
+
buf = "";
|
|
1379
|
+
started = false;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
for (let i = 0; i < segment.length; i++) {
|
|
1383
|
+
const c = segment[i];
|
|
1384
|
+
if (quote) {
|
|
1385
|
+
if (c === quote) quote = null;
|
|
1386
|
+
else buf += c;
|
|
1387
|
+
started = true;
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (c === '"' || c === "'") {
|
|
1391
|
+
quote = c;
|
|
1392
|
+
started = true;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (c === " " || c === " ") {
|
|
1396
|
+
push();
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
buf += c;
|
|
1400
|
+
started = true;
|
|
1401
|
+
}
|
|
1402
|
+
push();
|
|
1403
|
+
return tokens;
|
|
1404
|
+
}
|
|
1405
|
+
function baseName(executable) {
|
|
1406
|
+
const noPath = executable.split(/[\\/]/).pop() ?? executable;
|
|
1407
|
+
return noPath.toLowerCase();
|
|
1408
|
+
}
|
|
1409
|
+
function findIsReadOnly(rest) {
|
|
1410
|
+
return !rest.some((t) => /^-(exec|execdir|ok|okdir|delete|fprintf|fprint|fls)$/i.test(t));
|
|
1411
|
+
}
|
|
1412
|
+
function awkSedIsReadOnly(exe, rest) {
|
|
1413
|
+
const program = rest.join(" ");
|
|
1414
|
+
if (exe === "awk") return !/\bsystem\s*\(/.test(program) && !/\|\s*["']/.test(program) && !/print\s*>/.test(program);
|
|
1415
|
+
return !/(^|;|\{|\s)e\b/.test(program) && !/s[^\s]*\/[a-z]*e[a-z]*\b/i.test(program) && !/\bw\s+\S/.test(program);
|
|
1416
|
+
}
|
|
1417
|
+
function isWriteRedirect(segment) {
|
|
1418
|
+
const stripped = segment.replace(/\d?>>?\s*\/dev\/null/g, "").replace(/\d?>\s*&\s*\d/g, "").replace(/\d?>\s*\$null/gi, "");
|
|
1419
|
+
return /(^|[^0-9&])>>?/.test(stripped);
|
|
1420
|
+
}
|
|
1421
|
+
function checkReadOnly(command, opts = {}) {
|
|
1422
|
+
const cmd = command.trim();
|
|
1423
|
+
if (!cmd) return { allowed: false, reason: "empty command" };
|
|
1424
|
+
if (opts.shell === "powershell") {
|
|
1425
|
+
if (isWriteRedirect(cmd)) return { allowed: false, reason: "file-writing redirect is not allowed" };
|
|
1426
|
+
if (DANGEROUS_PS.test(cmd)) return { allowed: false, reason: "mutating PowerShell cmdlet is not allowed" };
|
|
1427
|
+
if (DANGEROUS_POSIX.test(cmd)) return { allowed: false, reason: "destructive command is not allowed" };
|
|
1428
|
+
return { allowed: true };
|
|
1429
|
+
}
|
|
1430
|
+
if (/\$\(|`/.test(cmd)) return { allowed: false, reason: "command substitution is not allowed" };
|
|
1431
|
+
if (isWriteRedirect(cmd)) return { allowed: false, reason: "file-writing redirect is not allowed" };
|
|
1432
|
+
for (const segment of splitSegments(cmd)) {
|
|
1433
|
+
const r = checkSegment(segment);
|
|
1434
|
+
if (!r.allowed) return r;
|
|
1435
|
+
}
|
|
1436
|
+
return { allowed: true };
|
|
1437
|
+
}
|
|
1438
|
+
function checkSegment(segment) {
|
|
1439
|
+
let tokens = tokenize(segment).filter((t) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)).filter((t) => t !== "{" && t !== "}" && t !== "(" && t !== ")");
|
|
1440
|
+
if (tokens.length === 0) return { allowed: true };
|
|
1441
|
+
let exe = baseName(tokens[0]);
|
|
1442
|
+
let rest = tokens.slice(1);
|
|
1443
|
+
while (COMMAND_RUNNERS.has(exe)) {
|
|
1444
|
+
const inner = [];
|
|
1445
|
+
let i = 0;
|
|
1446
|
+
for (; i < rest.length; i++) {
|
|
1447
|
+
const t = rest[i];
|
|
1448
|
+
if (t.startsWith("-")) {
|
|
1449
|
+
if (/^-(I|n|L|P|d|s|E|u|g)$/.test(t)) i++;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
inner.push(...rest.slice(i));
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
if (inner.length === 0) return { allowed: true };
|
|
1456
|
+
exe = baseName(inner[0]);
|
|
1457
|
+
rest = inner.slice(1);
|
|
1458
|
+
}
|
|
1459
|
+
if (exe === "find") {
|
|
1460
|
+
if (!findIsReadOnly(rest)) return { allowed: false, reason: "find: -exec/-delete is not allowed" };
|
|
1461
|
+
return { allowed: true };
|
|
1462
|
+
}
|
|
1463
|
+
if (exe === "awk" || exe === "sed") {
|
|
1464
|
+
if (!awkSedIsReadOnly(exe, rest)) return { allowed: false, reason: `${exe}: program may not shell out or write files` };
|
|
1465
|
+
return { allowed: true };
|
|
1466
|
+
}
|
|
1467
|
+
if (PKG_MANAGERS.has(exe)) {
|
|
1468
|
+
if (rest.some((t) => MUTATING_PKG.test(t))) return { allowed: false, reason: `${exe}: only list/query sub-commands are allowed` };
|
|
1469
|
+
return { allowed: true };
|
|
1470
|
+
}
|
|
1471
|
+
if (FETCH_RULES[exe]) {
|
|
1472
|
+
if (!FETCH_RULES[exe](rest)) return { allowed: false, reason: `${exe}: only read-only GET requests are allowed` };
|
|
1473
|
+
return { allowed: true };
|
|
1474
|
+
}
|
|
1475
|
+
if (SUBCOMMAND_RULES[exe]) {
|
|
1476
|
+
if (!SUBCOMMAND_RULES[exe](rest)) return { allowed: false, reason: `${exe}: sub-command is not read-only` };
|
|
1477
|
+
return { allowed: true };
|
|
1478
|
+
}
|
|
1479
|
+
if (CONDITIONAL_BINARIES.has(exe)) {
|
|
1480
|
+
if (rest.some((t) => !t.startsWith("-") && t !== "/dev/null")) return { allowed: false, reason: "tee may only write to /dev/null" };
|
|
1481
|
+
return { allowed: true };
|
|
1482
|
+
}
|
|
1483
|
+
if (READONLY_BINARIES.has(exe)) return { allowed: true };
|
|
1484
|
+
if (READONLY_PS_BARE.has(exe)) return { allowed: true };
|
|
1485
|
+
if (exe.includes("-") && /^[a-z]+-[a-z]/.test(exe)) {
|
|
1486
|
+
const verb = exe.split("-")[0];
|
|
1487
|
+
if (READONLY_PS_VERBS.has(verb)) return { allowed: true };
|
|
1488
|
+
return { allowed: false, reason: `PowerShell cmdlet not read-only: ${exe}` };
|
|
1489
|
+
}
|
|
1490
|
+
return { allowed: false, reason: `command not on read-only allowlist: ${exe}` };
|
|
1491
|
+
}
|
|
1492
|
+
function isReadOnlyCommand(command) {
|
|
1493
|
+
return checkReadOnly(command).allowed;
|
|
1494
|
+
}
|
|
1495
|
+
function assertReadOnly(command) {
|
|
1496
|
+
const r = checkReadOnly(command);
|
|
1497
|
+
if (!r.allowed) throw new Error(`Blocked by read-only allowlist: ${r.reason}`);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/logger.ts
|
|
1501
|
+
var verboseMode = false;
|
|
1502
|
+
function setVerbose(v) {
|
|
1503
|
+
verboseMode = v;
|
|
1504
|
+
}
|
|
1505
|
+
function log(level, message, context) {
|
|
1506
|
+
if (level === "DEBUG" && !verboseMode) return;
|
|
1507
|
+
const entry = {
|
|
1508
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1509
|
+
level,
|
|
1510
|
+
message,
|
|
1511
|
+
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
1512
|
+
};
|
|
1513
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
1514
|
+
}
|
|
1515
|
+
function logDebug(message, context) {
|
|
1516
|
+
log("DEBUG", message, context);
|
|
1517
|
+
}
|
|
1518
|
+
function logInfo(message, context) {
|
|
1519
|
+
log("INFO", message, context);
|
|
1520
|
+
}
|
|
1521
|
+
function logWarn(message, context) {
|
|
1522
|
+
log("WARN", message, context);
|
|
1523
|
+
}
|
|
1524
|
+
function logError(message, context) {
|
|
1525
|
+
log("ERROR", message, context);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/platform.ts
|
|
671
1529
|
var PLATFORM = process.platform;
|
|
672
1530
|
var IS_WIN = PLATFORM === "win32";
|
|
673
1531
|
var IS_MAC = PLATFORM === "darwin";
|
|
@@ -717,6 +1575,11 @@ function safeEnv() {
|
|
|
717
1575
|
return env;
|
|
718
1576
|
}
|
|
719
1577
|
function run(cmd, opts = {}) {
|
|
1578
|
+
const policy = checkReadOnly(cmd, { shell: IS_WIN ? "powershell" : "posix" });
|
|
1579
|
+
if (!policy.allowed) {
|
|
1580
|
+
logWarn(`Blocked non-read-only command: ${policy.reason}`);
|
|
1581
|
+
return "";
|
|
1582
|
+
}
|
|
720
1583
|
try {
|
|
721
1584
|
return execSync(cmd, {
|
|
722
1585
|
stdio: "pipe",
|
|
@@ -820,6 +1683,18 @@ function findFiles(dirs, patterns, maxDepth, limit) {
|
|
|
820
1683
|
const findCmds = dirs.map((d) => `find "${d}" -maxdepth ${maxDepth} \\( ${nameArgs} \\) 2>/dev/null`).join("; ");
|
|
821
1684
|
return run(`{ ${findCmds}; } | head -${limit}`, { timeout: 15e3 });
|
|
822
1685
|
}
|
|
1686
|
+
function scanListeningPorts() {
|
|
1687
|
+
if (IS_WIN) {
|
|
1688
|
+
return run(
|
|
1689
|
+
`Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue; "$($_.LocalAddress):$($_.LocalPort) PID=$($_.OwningProcess) $($p.ProcessName)" } | Sort-Object -Unique`,
|
|
1690
|
+
{ timeout: 15e3 }
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (IS_MAC) {
|
|
1694
|
+
return run("sudo lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null || lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null", { timeout: 15e3 });
|
|
1695
|
+
}
|
|
1696
|
+
return run("ss -tlnp 2>/dev/null", { timeout: 1e4 });
|
|
1697
|
+
}
|
|
823
1698
|
function scanWindowsPrograms() {
|
|
824
1699
|
if (!IS_WIN) return "";
|
|
825
1700
|
return run(
|
|
@@ -888,92 +1763,62 @@ function readChromeLike(filePath, source) {
|
|
|
888
1763
|
return [];
|
|
889
1764
|
}
|
|
890
1765
|
}
|
|
891
|
-
async function
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const tmp = join2(tmpdir(), `cartograph_ff_bm_${Date.now()}.sqlite`);
|
|
1766
|
+
async function queryBrowserDb(srcPath, tmpPrefix, query) {
|
|
1767
|
+
if (!existsSync2(srcPath)) return [];
|
|
1768
|
+
const tmp = join2(tmpdir(), `cartograph_${tmpPrefix}_${Date.now()}.sqlite`);
|
|
895
1769
|
try {
|
|
896
|
-
copyFileSync(
|
|
1770
|
+
copyFileSync(srcPath, tmp);
|
|
897
1771
|
const { default: Database2 } = await import("better-sqlite3");
|
|
898
1772
|
const db = new Database2(tmp, { readonly: true, fileMustExist: true });
|
|
899
|
-
const rows = db.prepare(`
|
|
900
|
-
SELECT DISTINCT p.url
|
|
901
|
-
FROM moz_places p
|
|
902
|
-
JOIN moz_bookmarks b ON b.fk = p.id
|
|
903
|
-
WHERE b.type = 1 AND p.url NOT LIKE 'place:%'
|
|
904
|
-
LIMIT 3000
|
|
905
|
-
`).all();
|
|
906
|
-
db.close();
|
|
907
|
-
return rows.map((r) => extractHost(r.url, "firefox")).filter((h) => h !== null);
|
|
908
|
-
} catch {
|
|
909
|
-
return [];
|
|
910
|
-
} finally {
|
|
911
1773
|
try {
|
|
912
|
-
|
|
913
|
-
}
|
|
1774
|
+
return db.prepare(query).all();
|
|
1775
|
+
} finally {
|
|
1776
|
+
db.close();
|
|
914
1777
|
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
async function readFirefoxHistory(profileDir) {
|
|
918
|
-
const src = join2(profileDir, "places.sqlite");
|
|
919
|
-
if (!existsSync2(src)) return [];
|
|
920
|
-
const tmp = join2(tmpdir(), `cartograph_ff_hist_${Date.now()}.sqlite`);
|
|
921
|
-
try {
|
|
922
|
-
copyFileSync(src, tmp);
|
|
923
|
-
const { default: Database2 } = await import("better-sqlite3");
|
|
924
|
-
const db = new Database2(tmp, { readonly: true, fileMustExist: true });
|
|
925
|
-
const rows = db.prepare(`
|
|
926
|
-
SELECT url, visit_count
|
|
927
|
-
FROM moz_places
|
|
928
|
-
WHERE url NOT LIKE 'place:%'
|
|
929
|
-
AND visit_count > 0
|
|
930
|
-
ORDER BY visit_count DESC
|
|
931
|
-
LIMIT 5000
|
|
932
|
-
`).all();
|
|
933
|
-
db.close();
|
|
934
|
-
return rows.map((r) => {
|
|
935
|
-
const h = extractHost(r.url, "firefox");
|
|
936
|
-
if (!h) return null;
|
|
937
|
-
return { ...h, visitCount: r.visit_count };
|
|
938
|
-
}).filter((h) => h !== null);
|
|
939
1778
|
} catch {
|
|
940
1779
|
return [];
|
|
941
1780
|
} finally {
|
|
942
1781
|
try {
|
|
943
|
-
|
|
1782
|
+
unlinkSync(tmp);
|
|
944
1783
|
} catch {
|
|
945
1784
|
}
|
|
946
1785
|
}
|
|
947
1786
|
}
|
|
1787
|
+
async function readFirefoxBookmarks(profileDir) {
|
|
1788
|
+
const rows = await queryBrowserDb(
|
|
1789
|
+
join2(profileDir, "places.sqlite"),
|
|
1790
|
+
"ff_bm",
|
|
1791
|
+
`SELECT DISTINCT p.url FROM moz_places p
|
|
1792
|
+
JOIN moz_bookmarks b ON b.fk = p.id
|
|
1793
|
+
WHERE b.type = 1 AND p.url NOT LIKE 'place:%' LIMIT 3000`
|
|
1794
|
+
);
|
|
1795
|
+
return rows.map((r) => extractHost(r.url, "firefox")).filter((h) => h !== null);
|
|
1796
|
+
}
|
|
1797
|
+
async function readFirefoxHistory(profileDir) {
|
|
1798
|
+
const rows = await queryBrowserDb(
|
|
1799
|
+
join2(profileDir, "places.sqlite"),
|
|
1800
|
+
"ff_hist",
|
|
1801
|
+
`SELECT url, visit_count FROM moz_places
|
|
1802
|
+
WHERE url NOT LIKE 'place:%' AND visit_count > 0
|
|
1803
|
+
ORDER BY visit_count DESC LIMIT 5000`
|
|
1804
|
+
);
|
|
1805
|
+
return rows.map((r) => {
|
|
1806
|
+
const h = extractHost(r.url, "firefox");
|
|
1807
|
+
return h ? { ...h, visitCount: r.visit_count } : null;
|
|
1808
|
+
}).filter((h) => h !== null);
|
|
1809
|
+
}
|
|
948
1810
|
async function readChromiumHistory(historyPath, source) {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
ORDER BY visit_count DESC
|
|
961
|
-
LIMIT 5000
|
|
962
|
-
`).all();
|
|
963
|
-
db.close();
|
|
964
|
-
return rows.map((r) => {
|
|
965
|
-
const h = extractHost(r.url, source);
|
|
966
|
-
if (!h) return null;
|
|
967
|
-
return { ...h, visitCount: r.visit_count };
|
|
968
|
-
}).filter((h) => h !== null);
|
|
969
|
-
} catch {
|
|
970
|
-
return [];
|
|
971
|
-
} finally {
|
|
972
|
-
try {
|
|
973
|
-
(await import("fs")).unlinkSync(tmp);
|
|
974
|
-
} catch {
|
|
975
|
-
}
|
|
976
|
-
}
|
|
1811
|
+
const rows = await queryBrowserDb(
|
|
1812
|
+
historyPath,
|
|
1813
|
+
"ch_hist",
|
|
1814
|
+
`SELECT url, visit_count FROM urls
|
|
1815
|
+
WHERE hidden = 0 AND visit_count > 0
|
|
1816
|
+
ORDER BY visit_count DESC LIMIT 5000`
|
|
1817
|
+
);
|
|
1818
|
+
return rows.map((r) => {
|
|
1819
|
+
const h = extractHost(r.url, source);
|
|
1820
|
+
return h ? { ...h, visitCount: r.visit_count } : null;
|
|
1821
|
+
}).filter((h) => h !== null);
|
|
977
1822
|
}
|
|
978
1823
|
var IS_LINUX2 = !IS_MAC && !IS_WIN;
|
|
979
1824
|
function chromeLikePaths(base) {
|
|
@@ -1097,17 +1942,474 @@ async function scanAllHistory() {
|
|
|
1097
1942
|
return [...byHost.values()].sort((a, b) => b.visitCount - a.visitCount);
|
|
1098
1943
|
}
|
|
1099
1944
|
|
|
1945
|
+
// src/scanners/bookmarks.ts
|
|
1946
|
+
var PERSONAL = [
|
|
1947
|
+
"facebook.",
|
|
1948
|
+
"instagram.",
|
|
1949
|
+
"twitter.",
|
|
1950
|
+
"x.com",
|
|
1951
|
+
"tiktok.",
|
|
1952
|
+
"reddit.",
|
|
1953
|
+
"youtube.",
|
|
1954
|
+
"netflix.",
|
|
1955
|
+
"spotify.",
|
|
1956
|
+
"twitch.",
|
|
1957
|
+
"pinterest.",
|
|
1958
|
+
"snapchat.",
|
|
1959
|
+
"whatsapp.",
|
|
1960
|
+
"amazon.",
|
|
1961
|
+
"ebay.",
|
|
1962
|
+
"aliexpress.",
|
|
1963
|
+
"cnn.",
|
|
1964
|
+
"bbc.",
|
|
1965
|
+
"nytimes.",
|
|
1966
|
+
"espn.",
|
|
1967
|
+
"booking.",
|
|
1968
|
+
"airbnb.",
|
|
1969
|
+
"tripadvisor.",
|
|
1970
|
+
"wikipedia."
|
|
1971
|
+
];
|
|
1972
|
+
var BUSINESS = [
|
|
1973
|
+
"github.",
|
|
1974
|
+
"gitlab.",
|
|
1975
|
+
"bitbucket.",
|
|
1976
|
+
"atlassian.",
|
|
1977
|
+
"jira.",
|
|
1978
|
+
"confluence.",
|
|
1979
|
+
"notion.",
|
|
1980
|
+
"linear.",
|
|
1981
|
+
"slack.",
|
|
1982
|
+
"zoom.",
|
|
1983
|
+
"figma.",
|
|
1984
|
+
"miro.",
|
|
1985
|
+
"vercel.",
|
|
1986
|
+
"netlify.",
|
|
1987
|
+
"heroku.",
|
|
1988
|
+
"datadog",
|
|
1989
|
+
"sentry.",
|
|
1990
|
+
"grafana.",
|
|
1991
|
+
"pagerduty.",
|
|
1992
|
+
"aws.amazon.",
|
|
1993
|
+
"console.cloud.google",
|
|
1994
|
+
"portal.azure",
|
|
1995
|
+
"cloudflare.",
|
|
1996
|
+
"hubspot.",
|
|
1997
|
+
"salesforce.",
|
|
1998
|
+
"stripe.",
|
|
1999
|
+
"twilio.",
|
|
2000
|
+
"sendgrid.",
|
|
2001
|
+
"mailchimp.",
|
|
2002
|
+
"segment.",
|
|
2003
|
+
"mixpanel.",
|
|
2004
|
+
"amplitude.",
|
|
2005
|
+
"looker.",
|
|
2006
|
+
"tableau.",
|
|
2007
|
+
"snowflake.",
|
|
2008
|
+
"databricks.",
|
|
2009
|
+
"mongodb.",
|
|
2010
|
+
"redis.",
|
|
2011
|
+
"elastic.",
|
|
2012
|
+
"openai.",
|
|
2013
|
+
"anthropic.",
|
|
2014
|
+
"huggingface.",
|
|
2015
|
+
"docker.",
|
|
2016
|
+
"npmjs.",
|
|
2017
|
+
"pypi.",
|
|
2018
|
+
"circleci.",
|
|
2019
|
+
"travis-ci.",
|
|
2020
|
+
"jenkins.",
|
|
2021
|
+
"terraform.",
|
|
2022
|
+
"hashicorp.",
|
|
2023
|
+
"okta.",
|
|
2024
|
+
"auth0.",
|
|
2025
|
+
"1password.",
|
|
2026
|
+
"asana.",
|
|
2027
|
+
"trello.",
|
|
2028
|
+
"monday."
|
|
2029
|
+
];
|
|
2030
|
+
function classify(hostname) {
|
|
2031
|
+
const h = hostname.toLowerCase();
|
|
2032
|
+
if (PERSONAL.some((p) => h.includes(p))) return null;
|
|
2033
|
+
if (BUSINESS.some((b) => h.includes(b))) return { type: "saas_tool", confidence: 0.7 };
|
|
2034
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(h) || /\.(internal|local|corp|lan)\b/.test(h)) {
|
|
2035
|
+
return { type: "web_service", confidence: 0.6 };
|
|
2036
|
+
}
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
var bookmarksScanner = {
|
|
2040
|
+
id: "bookmarks",
|
|
2041
|
+
title: "Browser bookmarks",
|
|
2042
|
+
platforms: "all",
|
|
2043
|
+
detect: () => true,
|
|
2044
|
+
async scan() {
|
|
2045
|
+
const hosts = await scanAllBookmarks();
|
|
2046
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2047
|
+
const nodes = [];
|
|
2048
|
+
for (const host of hosts) {
|
|
2049
|
+
const klass = classify(host.hostname);
|
|
2050
|
+
if (!klass) continue;
|
|
2051
|
+
const id = `${klass.type}:${host.hostname}`;
|
|
2052
|
+
if (seen.has(id)) continue;
|
|
2053
|
+
seen.add(id);
|
|
2054
|
+
nodes.push({
|
|
2055
|
+
id,
|
|
2056
|
+
type: klass.type,
|
|
2057
|
+
name: host.hostname,
|
|
2058
|
+
discoveredVia: "bookmark",
|
|
2059
|
+
confidence: klass.confidence,
|
|
2060
|
+
tags: ["bookmark"],
|
|
2061
|
+
metadata: { protocol: host.protocol, ...host.port ? { port: host.port } : {} }
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
return { nodes, edges: [] };
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// src/scanners/installed-apps.ts
|
|
2069
|
+
var KNOWN_TOOLS = {
|
|
2070
|
+
ide: ["code", "code-insiders", "cursor", "windsurf", "zed", "nvim", "vim", "emacs", "idea", "webstorm", "pycharm", "goland", "datagrip", "clion", "rider", "phpstorm"],
|
|
2071
|
+
"dev-tool": ["git", "gh", "docker", "docker-compose", "podman", "kubectl", "helm", "terraform", "ansible", "vagrant", "packer", "consul", "vault", "nomad"],
|
|
2072
|
+
runtime: ["node", "npm", "pnpm", "yarn", "bun", "deno", "python", "python3", "pip", "poetry", "ruby", "rails", "java", "mvn", "gradle", "go", "cargo", "rustc", "php", "composer", "dotnet"],
|
|
2073
|
+
database: ["psql", "mysql", "mongosh", "redis-cli", "sqlite3", "clickhouse-client"],
|
|
2074
|
+
cloud: ["aws", "gcloud", "az", "heroku", "fly", "vercel", "netlify", "wrangler", "supabase"],
|
|
2075
|
+
browser: ["google-chrome", "chromium", "firefox", "brave", "opera"],
|
|
2076
|
+
observability: ["prometheus", "grafana-cli", "datadog-agent", "newrelic-agent"]
|
|
2077
|
+
};
|
|
2078
|
+
var installedAppsScanner = {
|
|
2079
|
+
id: "installed-apps",
|
|
2080
|
+
title: "Installed apps & developer tools",
|
|
2081
|
+
platforms: "all",
|
|
2082
|
+
allowedCommands: ["which", "command", "Get-Command"],
|
|
2083
|
+
detect: () => true,
|
|
2084
|
+
async scan(ctx) {
|
|
2085
|
+
const nodes = [];
|
|
2086
|
+
const hintTerms = (ctx.hint ?? "").toLowerCase().split(/[\s,]+/).filter(Boolean);
|
|
2087
|
+
for (const [category, tools] of Object.entries(KNOWN_TOOLS)) {
|
|
2088
|
+
for (const tool of tools) {
|
|
2089
|
+
const path = commandExists(tool);
|
|
2090
|
+
if (!path) continue;
|
|
2091
|
+
const boosted = hintTerms.some((t) => tool.includes(t));
|
|
2092
|
+
nodes.push({
|
|
2093
|
+
id: `saas_tool:${tool}`,
|
|
2094
|
+
type: "saas_tool",
|
|
2095
|
+
name: tool,
|
|
2096
|
+
discoveredVia: "installed-app",
|
|
2097
|
+
confidence: boosted ? 0.95 : 0.9,
|
|
2098
|
+
tags: [category],
|
|
2099
|
+
metadata: { category, path }
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return { nodes, edges: [] };
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
// src/scanners/ports.ts
|
|
2108
|
+
var PORT_MAP = {
|
|
2109
|
+
5432: { type: "database_server", service: "postgresql" },
|
|
2110
|
+
3306: { type: "database_server", service: "mysql" },
|
|
2111
|
+
1433: { type: "database_server", service: "sqlserver" },
|
|
2112
|
+
27017: { type: "database_server", service: "mongodb" },
|
|
2113
|
+
9200: { type: "database_server", service: "elasticsearch" },
|
|
2114
|
+
6379: { type: "cache_server", service: "redis" },
|
|
2115
|
+
11211: { type: "cache_server", service: "memcached" },
|
|
2116
|
+
9092: { type: "message_broker", service: "kafka" },
|
|
2117
|
+
5672: { type: "message_broker", service: "rabbitmq" },
|
|
2118
|
+
4222: { type: "message_broker", service: "nats" },
|
|
2119
|
+
9090: { type: "web_service", service: "prometheus" },
|
|
2120
|
+
3e3: { type: "web_service", service: "http-app" },
|
|
2121
|
+
8080: { type: "web_service", service: "http-app" },
|
|
2122
|
+
8e3: { type: "web_service", service: "http-app" },
|
|
2123
|
+
80: { type: "web_service", service: "http" },
|
|
2124
|
+
443: { type: "web_service", service: "https" },
|
|
2125
|
+
8200: { type: "web_service", service: "vault" },
|
|
2126
|
+
8500: { type: "web_service", service: "consul" },
|
|
2127
|
+
2379: { type: "web_service", service: "etcd" },
|
|
2128
|
+
5601: { type: "web_service", service: "kibana" },
|
|
2129
|
+
15672: { type: "web_service", service: "rabbitmq-management" }
|
|
2130
|
+
};
|
|
2131
|
+
function extractListeningPorts(raw) {
|
|
2132
|
+
const ports = /* @__PURE__ */ new Set();
|
|
2133
|
+
for (const m of raw.matchAll(/[:.](\d{2,5})\b/g)) {
|
|
2134
|
+
const p = Number(m[1]);
|
|
2135
|
+
if (p in PORT_MAP) ports.add(p);
|
|
2136
|
+
}
|
|
2137
|
+
return [...ports];
|
|
2138
|
+
}
|
|
2139
|
+
var portsScanner = {
|
|
2140
|
+
id: "local-ports",
|
|
2141
|
+
title: "Local listening ports",
|
|
2142
|
+
platforms: "all",
|
|
2143
|
+
allowedCommands: ["ss", "lsof", "Get-NetTCPConnection"],
|
|
2144
|
+
detect: () => true,
|
|
2145
|
+
async scan() {
|
|
2146
|
+
const raw = scanListeningPorts();
|
|
2147
|
+
const nodes = [];
|
|
2148
|
+
for (const port of extractListeningPorts(raw)) {
|
|
2149
|
+
const { type, service } = PORT_MAP[port];
|
|
2150
|
+
nodes.push({
|
|
2151
|
+
id: `${type}:localhost:${port}`,
|
|
2152
|
+
type,
|
|
2153
|
+
name: `${service} (:${port})`,
|
|
2154
|
+
discoveredVia: "listening-port",
|
|
2155
|
+
confidence: 0.9,
|
|
2156
|
+
tags: ["local", service],
|
|
2157
|
+
metadata: { port, service, host: "localhost" }
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
return { nodes, edges: [] };
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
// src/scanners/registry.ts
|
|
2165
|
+
function defaultRegistry() {
|
|
2166
|
+
return new ScannerRegistry().register(bookmarksScanner).register(installedAppsScanner).register(portsScanner);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/discovery/local.ts
|
|
2170
|
+
async function runLocalDiscovery(db, sessionId, opts = {}) {
|
|
2171
|
+
const registry = opts.registry ?? defaultRegistry();
|
|
2172
|
+
const ctx = { hint: opts.hint, platform: PLATFORM, run };
|
|
2173
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2174
|
+
const edges = [];
|
|
2175
|
+
const ran = [];
|
|
2176
|
+
for (const scanner of registry.forPlatform(PLATFORM)) {
|
|
2177
|
+
try {
|
|
2178
|
+
if (!await scanner.detect(ctx)) continue;
|
|
2179
|
+
const result = await scanner.scan(ctx);
|
|
2180
|
+
ran.push(scanner.id);
|
|
2181
|
+
for (const node of result.nodes) {
|
|
2182
|
+
const prev = nodes.get(node.id);
|
|
2183
|
+
if (!prev || node.confidence > prev.confidence) nodes.set(node.id, node);
|
|
2184
|
+
}
|
|
2185
|
+
edges.push(...result.edges);
|
|
2186
|
+
opts.onProgress?.(`${scanner.title}: +${result.nodes.length} nodes`);
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
opts.onProgress?.(`${scanner.title}: failed (${err instanceof Error ? err.message : String(err)})`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
for (const node of nodes.values()) db.upsertNode(sessionId, node);
|
|
2192
|
+
for (const edge of edges) {
|
|
2193
|
+
if (nodes.has(edge.sourceId) && nodes.has(edge.targetId)) db.insertEdge(sessionId, edge);
|
|
2194
|
+
}
|
|
2195
|
+
return { nodes: nodes.size, edges: edges.length, scanners: ran };
|
|
2196
|
+
}
|
|
2197
|
+
function localDiscoveryFn(registry) {
|
|
2198
|
+
return async (db, sessionId, opts) => {
|
|
2199
|
+
const r = await runLocalDiscovery(db, sessionId, { hint: opts.hint, registry });
|
|
2200
|
+
return { nodes: r.nodes, edges: r.edges };
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/semantic/hash.ts
|
|
2205
|
+
function fnv1a(s) {
|
|
2206
|
+
let h = 2166136261;
|
|
2207
|
+
for (let i = 0; i < s.length; i++) {
|
|
2208
|
+
h ^= s.charCodeAt(i);
|
|
2209
|
+
h = Math.imul(h, 16777619);
|
|
2210
|
+
}
|
|
2211
|
+
return h >>> 0;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/semantic/embeddings.ts
|
|
2215
|
+
async function createLocalEmbedder(model = "Xenova/all-MiniLM-L6-v2") {
|
|
2216
|
+
try {
|
|
2217
|
+
const tf = await import("./transformers.node-J6PRTTOX.js");
|
|
2218
|
+
const extractor = await tf.pipeline("feature-extraction", model);
|
|
2219
|
+
return {
|
|
2220
|
+
id: `local:${model}`,
|
|
2221
|
+
dimensions: 384,
|
|
2222
|
+
async embed(texts) {
|
|
2223
|
+
const out = [];
|
|
2224
|
+
for (const text of texts) {
|
|
2225
|
+
const tensor = await extractor(text, { pooling: "mean", normalize: true });
|
|
2226
|
+
out.push(Float32Array.from(tensor.data));
|
|
2227
|
+
}
|
|
2228
|
+
return out;
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
} catch {
|
|
2232
|
+
return void 0;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
function createHashEmbedder(dimensions = 256) {
|
|
2236
|
+
return {
|
|
2237
|
+
id: `hash:${dimensions}`,
|
|
2238
|
+
dimensions,
|
|
2239
|
+
async embed(texts) {
|
|
2240
|
+
return texts.map((text) => hashEmbed(text, dimensions));
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function hashEmbed(text, dim) {
|
|
2245
|
+
const v = new Float32Array(dim);
|
|
2246
|
+
const tokens = text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
2247
|
+
for (const tok of tokens) {
|
|
2248
|
+
for (const gram of [tok, ...trigrams(tok)]) {
|
|
2249
|
+
const h = fnv1a(gram);
|
|
2250
|
+
v[h % dim] += 1;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
let norm = 0;
|
|
2254
|
+
for (const x of v) norm += x * x;
|
|
2255
|
+
norm = Math.sqrt(norm) || 1;
|
|
2256
|
+
for (let i = 0; i < dim; i++) v[i] = v[i] / norm;
|
|
2257
|
+
return v;
|
|
2258
|
+
}
|
|
2259
|
+
function trigrams(s) {
|
|
2260
|
+
if (s.length < 3) return [];
|
|
2261
|
+
const out = [];
|
|
2262
|
+
for (let i = 0; i <= s.length - 3; i++) out.push(s.slice(i, i + 3));
|
|
2263
|
+
return out;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/semantic/store.ts
|
|
2267
|
+
function nodeText(n) {
|
|
2268
|
+
const desc = typeof n.metadata?.["description"] === "string" ? n.metadata["description"] : "";
|
|
2269
|
+
const category = typeof n.metadata?.["category"] === "string" ? n.metadata["category"] : "";
|
|
2270
|
+
return [n.name, n.id.replace(/[:_]/g, " "), `type ${n.type}`, n.domain ?? "", n.subDomain ?? "", category, n.tags.join(" "), desc].filter(Boolean).join(" \u2014 ");
|
|
2271
|
+
}
|
|
2272
|
+
function hash(s) {
|
|
2273
|
+
return fnv1a(s).toString(16);
|
|
2274
|
+
}
|
|
2275
|
+
function toBuffer(v) {
|
|
2276
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
|
|
2277
|
+
}
|
|
2278
|
+
var VectorStore = class {
|
|
2279
|
+
constructor(db, embedder) {
|
|
2280
|
+
this.db = db;
|
|
2281
|
+
this.embedder = embedder;
|
|
2282
|
+
}
|
|
2283
|
+
loaded = false;
|
|
2284
|
+
/** Load sqlite-vec and ensure the schema exists. Returns false if unavailable. */
|
|
2285
|
+
async init() {
|
|
2286
|
+
if (this.loaded) return true;
|
|
2287
|
+
try {
|
|
2288
|
+
const conn = this.db.rawConnection();
|
|
2289
|
+
const sqliteVec = await import("./sqlite-vec-UK5YYE5T.js");
|
|
2290
|
+
sqliteVec.load(conn);
|
|
2291
|
+
conn.exec(`
|
|
2292
|
+
CREATE TABLE IF NOT EXISTS vec_index (
|
|
2293
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2294
|
+
session_id TEXT NOT NULL,
|
|
2295
|
+
node_id TEXT NOT NULL,
|
|
2296
|
+
hash TEXT NOT NULL,
|
|
2297
|
+
UNIQUE(session_id, node_id)
|
|
2298
|
+
);
|
|
2299
|
+
CREATE TABLE IF NOT EXISTS vec_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
2300
|
+
`);
|
|
2301
|
+
const dimRow = conn.prepare("SELECT value FROM vec_meta WHERE key = 'dims'").get();
|
|
2302
|
+
const dims = this.embedder.dimensions;
|
|
2303
|
+
if (dimRow && Number(dimRow.value) !== dims) {
|
|
2304
|
+
conn.exec("DROP TABLE IF EXISTS vec_nodes; DELETE FROM vec_index;");
|
|
2305
|
+
}
|
|
2306
|
+
conn.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS vec_nodes USING vec0(embedding float[${dims}])`);
|
|
2307
|
+
conn.prepare("INSERT OR REPLACE INTO vec_meta(key, value) VALUES (?, ?)").run("dims", String(dims));
|
|
2308
|
+
conn.prepare("INSERT OR REPLACE INTO vec_meta(key, value) VALUES (?, ?)").run("embedder", this.embedder.id);
|
|
2309
|
+
this.loaded = true;
|
|
2310
|
+
return true;
|
|
2311
|
+
} catch {
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
/** Incrementally embed and index any new/changed nodes for a session. */
|
|
2316
|
+
async index(sessionId) {
|
|
2317
|
+
if (!await this.init()) return { embedded: 0, total: 0 };
|
|
2318
|
+
const conn = this.db.rawConnection();
|
|
2319
|
+
const nodes = this.db.getNodes(sessionId);
|
|
2320
|
+
const getRow = conn.prepare("SELECT rowid, hash FROM vec_index WHERE session_id = ? AND node_id = ?");
|
|
2321
|
+
const insIndex = conn.prepare("INSERT INTO vec_index (session_id, node_id, hash) VALUES (?, ?, ?)");
|
|
2322
|
+
const updHash = conn.prepare("UPDATE vec_index SET hash = ? WHERE rowid = ?");
|
|
2323
|
+
const delVec = conn.prepare("DELETE FROM vec_nodes WHERE rowid = ?");
|
|
2324
|
+
const insVec = conn.prepare("INSERT INTO vec_nodes (rowid, embedding) VALUES (?, ?)");
|
|
2325
|
+
const pending = [];
|
|
2326
|
+
for (const n of nodes) {
|
|
2327
|
+
const text = nodeText(n);
|
|
2328
|
+
const h = hash(`${this.embedder.id}:${text}`);
|
|
2329
|
+
const existing = getRow.get(sessionId, n.id);
|
|
2330
|
+
if (existing) {
|
|
2331
|
+
if (existing.hash === h) continue;
|
|
2332
|
+
updHash.run(h, existing.rowid);
|
|
2333
|
+
delVec.run(BigInt(existing.rowid));
|
|
2334
|
+
pending.push({ rowid: BigInt(existing.rowid), text });
|
|
2335
|
+
} else {
|
|
2336
|
+
const info = insIndex.run(sessionId, n.id, h);
|
|
2337
|
+
pending.push({ rowid: BigInt(info.lastInsertRowid), text });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (pending.length > 0) {
|
|
2341
|
+
const vectors = await this.embedder.embed(pending.map((p) => p.text));
|
|
2342
|
+
const tx = conn.transaction(() => {
|
|
2343
|
+
pending.forEach((p, i) => insVec.run(p.rowid, toBuffer(vectors[i])));
|
|
2344
|
+
});
|
|
2345
|
+
tx();
|
|
2346
|
+
}
|
|
2347
|
+
return { embedded: pending.length, total: nodes.length };
|
|
2348
|
+
}
|
|
2349
|
+
/** k-nearest-neighbour search within a session. Returns node ids + distances. */
|
|
2350
|
+
async search(sessionId, query, k) {
|
|
2351
|
+
if (!await this.init()) return [];
|
|
2352
|
+
await this.index(sessionId);
|
|
2353
|
+
const conn = this.db.rawConnection();
|
|
2354
|
+
const [qv] = await this.embedder.embed([query]);
|
|
2355
|
+
if (!qv) return [];
|
|
2356
|
+
const overfetch = Math.max(k * 5, k);
|
|
2357
|
+
const knn = conn.prepare(
|
|
2358
|
+
"SELECT rowid, distance FROM vec_nodes WHERE embedding MATCH ? ORDER BY distance LIMIT ?"
|
|
2359
|
+
).all(toBuffer(qv), overfetch);
|
|
2360
|
+
const meta = conn.prepare("SELECT node_id AS nodeId, session_id AS sessionId FROM vec_index WHERE rowid = ?");
|
|
2361
|
+
const out = [];
|
|
2362
|
+
for (const row of knn) {
|
|
2363
|
+
const m = meta.get(row.rowid);
|
|
2364
|
+
if (m && m.sessionId === sessionId) out.push({ nodeId: m.nodeId, distance: row.distance });
|
|
2365
|
+
if (out.length >= k) break;
|
|
2366
|
+
}
|
|
2367
|
+
return out;
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
// src/semantic/search.ts
|
|
2372
|
+
var lexical = (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
2373
|
+
var lexicalSearch2 = () => async (d, sid, q, opts) => lexical(d, sid, q, opts);
|
|
2374
|
+
async function createSemanticSearch(db, embedder) {
|
|
2375
|
+
const provider = embedder ?? await createLocalEmbedder();
|
|
2376
|
+
if (!provider) return lexicalSearch2();
|
|
2377
|
+
const store = new VectorStore(db, provider);
|
|
2378
|
+
const ok = await store.init();
|
|
2379
|
+
if (!ok) return lexicalSearch2();
|
|
2380
|
+
return async (d, sid, query, opts) => {
|
|
2381
|
+
const hits = await store.search(sid, query, opts.limit);
|
|
2382
|
+
if (hits.length === 0) return lexical(d, sid, query, opts);
|
|
2383
|
+
const byId = d.getNodesByIds(sid, hits.map((h) => h.nodeId));
|
|
2384
|
+
const results = [];
|
|
2385
|
+
for (const h of hits) {
|
|
2386
|
+
const node = byId.get(h.nodeId);
|
|
2387
|
+
if (!node) continue;
|
|
2388
|
+
if (opts.types && opts.types.length > 0 && !opts.types.includes(node.type)) continue;
|
|
2389
|
+
results.push({ node, score: Math.max(0, 1 - h.distance / 2) });
|
|
2390
|
+
}
|
|
2391
|
+
return results.length > 0 ? results : lexical(d, sid, query, opts);
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
|
|
1100
2395
|
// src/tools.ts
|
|
2396
|
+
import { z as z4 } from "zod";
|
|
1101
2397
|
function createScanRunner(runFn, opts = {}) {
|
|
1102
2398
|
const threshold = opts.threshold ?? 3;
|
|
1103
2399
|
let consecutiveFailures = 0;
|
|
1104
2400
|
let tripped = false;
|
|
1105
2401
|
return (cmd) => {
|
|
1106
|
-
if (tripped)
|
|
2402
|
+
if (tripped) {
|
|
2403
|
+
logDebug(`Circuit breaker: skipping "${cmd}" (${consecutiveFailures} consecutive failures)`);
|
|
2404
|
+
return "(skipped \u2014 circuit breaker: too many consecutive failures)";
|
|
2405
|
+
}
|
|
1107
2406
|
const result = runFn(cmd, { timeout: opts.timeout ?? 2e4, env: opts.env });
|
|
1108
2407
|
if (!result) {
|
|
1109
2408
|
consecutiveFailures++;
|
|
1110
|
-
if (consecutiveFailures >= threshold)
|
|
2409
|
+
if (consecutiveFailures >= threshold) {
|
|
2410
|
+
tripped = true;
|
|
2411
|
+
logDebug(`Circuit breaker tripped after ${threshold} failures, last command: "${cmd}"`);
|
|
2412
|
+
}
|
|
1111
2413
|
return "(error or not available)";
|
|
1112
2414
|
}
|
|
1113
2415
|
consecutiveFailures = 0;
|
|
@@ -1115,27 +2417,31 @@ function createScanRunner(runFn, opts = {}) {
|
|
|
1115
2417
|
};
|
|
1116
2418
|
}
|
|
1117
2419
|
function stripSensitive(target) {
|
|
2420
|
+
const raw = target.trim();
|
|
2421
|
+
if (!raw) return raw;
|
|
1118
2422
|
try {
|
|
1119
|
-
const url = new URL(
|
|
1120
|
-
|
|
2423
|
+
const url = new URL(raw.startsWith("http") ? raw : `tcp://${raw}`);
|
|
2424
|
+
const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
|
|
2425
|
+
return stripped || raw;
|
|
1121
2426
|
} catch {
|
|
1122
|
-
|
|
2427
|
+
const stripped = raw.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
|
|
2428
|
+
return stripped || raw;
|
|
1123
2429
|
}
|
|
1124
2430
|
}
|
|
1125
2431
|
async function createCartographyTools(db, sessionId, opts = {}) {
|
|
1126
|
-
const { tool, createSdkMcpServer } = await import("
|
|
2432
|
+
const { tool, createSdkMcpServer } = await import("./sdk-G5D4WQZ4.js");
|
|
1127
2433
|
const tools = [
|
|
1128
2434
|
tool("save_node", "Save an infrastructure node to the catalog", {
|
|
1129
|
-
id:
|
|
1130
|
-
type:
|
|
1131
|
-
name:
|
|
1132
|
-
discoveredVia:
|
|
1133
|
-
confidence:
|
|
1134
|
-
metadata:
|
|
1135
|
-
tags:
|
|
1136
|
-
domain:
|
|
1137
|
-
subDomain:
|
|
1138
|
-
qualityScore:
|
|
2435
|
+
id: z4.string(),
|
|
2436
|
+
type: z4.enum(NODE_TYPES),
|
|
2437
|
+
name: z4.string(),
|
|
2438
|
+
discoveredVia: z4.string(),
|
|
2439
|
+
confidence: z4.number().min(0).max(1),
|
|
2440
|
+
metadata: z4.record(z4.string(), z4.unknown()).optional(),
|
|
2441
|
+
tags: z4.array(z4.string()).optional(),
|
|
2442
|
+
domain: z4.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
|
|
2443
|
+
subDomain: z4.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
|
|
2444
|
+
qualityScore: z4.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
|
|
1139
2445
|
}, async (args) => {
|
|
1140
2446
|
const node = {
|
|
1141
2447
|
id: stripSensitive(args["id"]),
|
|
@@ -1153,11 +2459,11 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1153
2459
|
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
1154
2460
|
}),
|
|
1155
2461
|
tool("save_edge", "Save a relationship (edge) between two nodes \u2014 ALWAYS save edges when connections are clear", {
|
|
1156
|
-
sourceId:
|
|
1157
|
-
targetId:
|
|
1158
|
-
relationship:
|
|
1159
|
-
evidence:
|
|
1160
|
-
confidence:
|
|
2462
|
+
sourceId: z4.string(),
|
|
2463
|
+
targetId: z4.string(),
|
|
2464
|
+
relationship: z4.enum(EDGE_RELATIONSHIPS),
|
|
2465
|
+
evidence: z4.string(),
|
|
2466
|
+
confidence: z4.number().min(0).max(1)
|
|
1161
2467
|
}, async (args) => {
|
|
1162
2468
|
db.insertEdge(sessionId, {
|
|
1163
2469
|
sourceId: args["sourceId"],
|
|
@@ -1169,7 +2475,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1169
2475
|
return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
|
|
1170
2476
|
}),
|
|
1171
2477
|
tool("get_catalog", "Get the current catalog \u2014 use before save_node to avoid duplicates", {
|
|
1172
|
-
includeEdges:
|
|
2478
|
+
includeEdges: z4.boolean().default(true)
|
|
1173
2479
|
}, async (args) => {
|
|
1174
2480
|
const nodes = db.getNodes(sessionId);
|
|
1175
2481
|
const edges = args["includeEdges"] ? db.getEdges(sessionId) : [];
|
|
@@ -1184,8 +2490,8 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1184
2490
|
};
|
|
1185
2491
|
}),
|
|
1186
2492
|
tool("ask_user", "Ask the user a question \u2014 for clarifications, missing context, or consent (e.g. before scanning browser history)", {
|
|
1187
|
-
question:
|
|
1188
|
-
context:
|
|
2493
|
+
question: z4.string().describe("The question for the user (clear and specific)"),
|
|
2494
|
+
context: z4.string().optional().describe("Optional context explaining why this is relevant")
|
|
1189
2495
|
}, async (args) => {
|
|
1190
2496
|
const question = args["question"];
|
|
1191
2497
|
const context = args["context"];
|
|
@@ -1198,7 +2504,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1198
2504
|
};
|
|
1199
2505
|
}),
|
|
1200
2506
|
tool("scan_bookmarks", "Scan all browser bookmarks \u2014 hostnames only, no personal data (Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Firefox)", {
|
|
1201
|
-
minConfidence:
|
|
2507
|
+
minConfidence: z4.number().min(0).max(1).default(0.5).optional()
|
|
1202
2508
|
}, async () => {
|
|
1203
2509
|
const hosts = await scanAllBookmarks();
|
|
1204
2510
|
return {
|
|
@@ -1218,7 +2524,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1218
2524
|
};
|
|
1219
2525
|
}),
|
|
1220
2526
|
tool("scan_browser_history", "Scan browser history \u2014 anonymized hostnames + visit frequency. ALWAYS call ask_user for consent before using this tool.", {
|
|
1221
|
-
minVisits:
|
|
2527
|
+
minVisits: z4.number().min(1).default(3).optional().describe("Minimum visit count to include a host (filters rarely-visited sites)")
|
|
1222
2528
|
}, async (args) => {
|
|
1223
2529
|
const minVisits = args["minVisits"] ?? 3;
|
|
1224
2530
|
const hosts = await scanAllHistory();
|
|
@@ -1240,7 +2546,7 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
1240
2546
|
};
|
|
1241
2547
|
}),
|
|
1242
2548
|
tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
|
|
1243
|
-
deep:
|
|
2549
|
+
deep: z4.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
|
|
1244
2550
|
}, async (args) => {
|
|
1245
2551
|
const deep = args["deep"] ?? false;
|
|
1246
2552
|
const results = {};
|
|
@@ -1312,7 +2618,7 @@ ${v}`).join("\n\n");
|
|
|
1312
2618
|
return { content: [{ type: "text", text: out }] };
|
|
1313
2619
|
}),
|
|
1314
2620
|
tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
|
|
1315
|
-
namespace:
|
|
2621
|
+
namespace: z4.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
|
|
1316
2622
|
}, async (args) => {
|
|
1317
2623
|
const ns = args["namespace"];
|
|
1318
2624
|
const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
|
|
@@ -1343,8 +2649,8 @@ ${runK(c)}`).join("\n\n");
|
|
|
1343
2649
|
return { content: [{ type: "text", text: out }] };
|
|
1344
2650
|
}),
|
|
1345
2651
|
tool("scan_aws_resources", "Scan AWS infrastructure via AWS CLI \u2014 100% readonly (describe, list)", {
|
|
1346
|
-
region:
|
|
1347
|
-
profile:
|
|
2652
|
+
region: z4.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
|
|
2653
|
+
profile: z4.string().optional().describe("AWS CLI profile")
|
|
1348
2654
|
}, async (args) => {
|
|
1349
2655
|
const region = args["region"];
|
|
1350
2656
|
const profile = args["profile"];
|
|
@@ -1367,7 +2673,7 @@ ${runAws(c)}`).join("\n\n");
|
|
|
1367
2673
|
return { content: [{ type: "text", text: out }] };
|
|
1368
2674
|
}),
|
|
1369
2675
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
1370
|
-
project:
|
|
2676
|
+
project: z4.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
1371
2677
|
}, async (args) => {
|
|
1372
2678
|
const project = args["project"];
|
|
1373
2679
|
const pf = project ? `--project ${project}` : "";
|
|
@@ -1388,8 +2694,8 @@ ${runGcp(c)}`).join("\n\n");
|
|
|
1388
2694
|
return { content: [{ type: "text", text: out }] };
|
|
1389
2695
|
}),
|
|
1390
2696
|
tool("scan_azure_resources", "Scan Azure infrastructure via az CLI \u2014 100% readonly (list, show)", {
|
|
1391
|
-
subscription:
|
|
1392
|
-
resourceGroup:
|
|
2697
|
+
subscription: z4.string().optional().describe("Azure Subscription ID"),
|
|
2698
|
+
resourceGroup: z4.string().optional().describe("Filter by resource group")
|
|
1393
2699
|
}, async (args) => {
|
|
1394
2700
|
const sub = args["subscription"];
|
|
1395
2701
|
const rg = args["resourceGroup"];
|
|
@@ -1412,7 +2718,7 @@ ${runAz(c)}`).join("\n\n");
|
|
|
1412
2718
|
return { content: [{ type: "text", text: out }] };
|
|
1413
2719
|
}),
|
|
1414
2720
|
tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
|
|
1415
|
-
searchHint:
|
|
2721
|
+
searchHint: z4.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
|
|
1416
2722
|
}, async (args) => {
|
|
1417
2723
|
const hint = args["searchHint"];
|
|
1418
2724
|
const results = {};
|
|
@@ -1597,18 +2903,20 @@ ${v}`).join("\n\n");
|
|
|
1597
2903
|
}
|
|
1598
2904
|
|
|
1599
2905
|
// src/safety.ts
|
|
1600
|
-
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|Remove-Item|Move-Item|Copy-Item|Stop-Process|Stop-Service|Restart-Service|Start-Service|Set-Service|Invoke-WebRequest\s+.*-Method\s+(POST|PUT|DELETE|PATCH)|del\s|rmdir\s|Format-Volume|Clear-Disk|Stop-Computer|Restart-Computer|Uninstall-Package|Install-Package|Install-Module)\b/i;
|
|
1601
|
-
var BLOCKED_REDIRECTS = />>|>[^>]|Out-File|Set-Content|Add-Content/;
|
|
1602
2906
|
var safetyHook = async (input, _toolUseID, _options) => {
|
|
1603
2907
|
if (!("tool_name" in input)) return {};
|
|
1604
2908
|
if (input.tool_name !== "Bash") return {};
|
|
1605
|
-
const cmd = input.tool_input?.command ?? "";
|
|
1606
|
-
if (
|
|
2909
|
+
const cmd = (input.tool_input?.command ?? "").trim();
|
|
2910
|
+
if (!cmd) {
|
|
2911
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
|
|
2912
|
+
}
|
|
2913
|
+
const decision = checkReadOnly(cmd);
|
|
2914
|
+
if (!decision.allowed) {
|
|
1607
2915
|
return {
|
|
1608
2916
|
hookSpecificOutput: {
|
|
1609
2917
|
hookEventName: "PreToolUse",
|
|
1610
2918
|
permissionDecision: "deny",
|
|
1611
|
-
permissionDecisionReason: `BLOCKED:
|
|
2919
|
+
permissionDecisionReason: `BLOCKED: ${decision.reason} \u2014 read-only allowlist policy`
|
|
1612
2920
|
}
|
|
1613
2921
|
};
|
|
1614
2922
|
}
|
|
@@ -1622,7 +2930,7 @@ var safetyHook = async (input, _toolUseID, _options) => {
|
|
|
1622
2930
|
|
|
1623
2931
|
// src/agent.ts
|
|
1624
2932
|
async function runDiscovery(config, db, sessionId, onEvent, onAskUser, hint) {
|
|
1625
|
-
const { query } = await import("
|
|
2933
|
+
const { query } = await import("./sdk-G5D4WQZ4.js");
|
|
1626
2934
|
const tools = await createCartographyTools(db, sessionId, { onAskUser });
|
|
1627
2935
|
const hintSection = hint ? `
|
|
1628
2936
|
\u26A1 USER HINT (HIGH PRIORITY): The user wants to find these specific tools: "${hint}"
|
|
@@ -1964,6 +3272,11 @@ function layoutClusters(groups, hexSize) {
|
|
|
1964
3272
|
}
|
|
1965
3273
|
function findFreeOrigin(occupied, count, gap) {
|
|
1966
3274
|
const key = (q, r) => `${q},${r}`;
|
|
3275
|
+
const parsedOccupied = [];
|
|
3276
|
+
for (const oKey of occupied) {
|
|
3277
|
+
const [oq, or] = oKey.split(",").map(Number);
|
|
3278
|
+
parsedOccupied.push({ q: oq, r: or });
|
|
3279
|
+
}
|
|
1967
3280
|
for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
|
|
1968
3281
|
const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
|
|
1969
3282
|
for (const candidate of candidates) {
|
|
@@ -1974,9 +3287,8 @@ function findFreeOrigin(occupied, count, gap) {
|
|
|
1974
3287
|
fits = false;
|
|
1975
3288
|
break;
|
|
1976
3289
|
}
|
|
1977
|
-
for (const
|
|
1978
|
-
|
|
1979
|
-
if (hexDistance(tp, { q: oq, r: or }) < gap) {
|
|
3290
|
+
for (const oc of parsedOccupied) {
|
|
3291
|
+
if (hexDistance(tp, oc) < gap) {
|
|
1980
3292
|
fits = false;
|
|
1981
3293
|
break;
|
|
1982
3294
|
}
|
|
@@ -2086,12 +3398,9 @@ function buildMapData(nodes, edges, options) {
|
|
|
2086
3398
|
|
|
2087
3399
|
// src/exporter.ts
|
|
2088
3400
|
function nodeLayer(type) {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (["message_broker", "queue", "topic"].includes(type)) return "messaging";
|
|
2093
|
-
if (["host", "container", "pod", "k8s_cluster"].includes(type)) return "infra";
|
|
2094
|
-
if (type === "config_file") return "config";
|
|
3401
|
+
for (const [layer, types] of Object.entries(NODE_TYPE_GROUPS)) {
|
|
3402
|
+
if (types.includes(type)) return layer;
|
|
3403
|
+
}
|
|
2095
3404
|
return "other";
|
|
2096
3405
|
}
|
|
2097
3406
|
var LAYER_LABELS = {
|
|
@@ -3414,7 +4723,7 @@ function checkPrerequisites() {
|
|
|
3414
4723
|
execSync2("claude --version", { stdio: "pipe" });
|
|
3415
4724
|
} catch {
|
|
3416
4725
|
process.stderr.write(
|
|
3417
|
-
"\n\u274C Claude CLI
|
|
4726
|
+
"\n\u274C Claude CLI not found.\n Datasynx Cartography requires the Claude CLI as a runtime dependency.\n\n Install:\n npm install -g @anthropic-ai/claude-code\n # or\n curl -fsSL https://claude.ai/install.sh | bash\n\n Then: claude login\n\n"
|
|
3418
4727
|
);
|
|
3419
4728
|
process.exitCode = 1;
|
|
3420
4729
|
throw new Error("Claude CLI not found");
|
|
@@ -3423,57 +4732,40 @@ function checkPrerequisites() {
|
|
|
3423
4732
|
const hasOAuth = isOAuthLoggedIn();
|
|
3424
4733
|
if (!hasApiKey && !hasOAuth) {
|
|
3425
4734
|
process.stderr.write(
|
|
3426
|
-
"\u26A0
|
|
4735
|
+
"\u26A0 No authentication found. Please choose one of the following options:\n\n Option A \u2014 claude.ai Subscription (recommended):\n claude login\n\n Option B \u2014 API Key:\n export ANTHROPIC_API_KEY=sk-ant-...\n\n"
|
|
3427
4736
|
);
|
|
3428
4737
|
} else if (hasOAuth && !hasApiKey) {
|
|
3429
|
-
process.stderr.write("\u2713
|
|
4738
|
+
process.stderr.write("\u2713 Logged in via claude login (Subscription)\n");
|
|
3430
4739
|
}
|
|
3431
4740
|
}
|
|
3432
|
-
|
|
3433
|
-
// src/logger.ts
|
|
3434
|
-
var verboseMode = false;
|
|
3435
|
-
function setVerbose(v) {
|
|
3436
|
-
verboseMode = v;
|
|
3437
|
-
}
|
|
3438
|
-
function log(level, message, context) {
|
|
3439
|
-
if (level === "DEBUG" && !verboseMode) return;
|
|
3440
|
-
const entry = {
|
|
3441
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3442
|
-
level,
|
|
3443
|
-
message,
|
|
3444
|
-
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
3445
|
-
};
|
|
3446
|
-
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
3447
|
-
}
|
|
3448
|
-
function logDebug(message, context) {
|
|
3449
|
-
log("DEBUG", message, context);
|
|
3450
|
-
}
|
|
3451
|
-
function logInfo(message, context) {
|
|
3452
|
-
log("INFO", message, context);
|
|
3453
|
-
}
|
|
3454
|
-
function logWarn(message, context) {
|
|
3455
|
-
log("WARN", message, context);
|
|
3456
|
-
}
|
|
3457
|
-
function logError(message, context) {
|
|
3458
|
-
log("ERROR", message, context);
|
|
3459
|
-
}
|
|
3460
4741
|
export {
|
|
3461
4742
|
CartographyDB,
|
|
4743
|
+
ScannerRegistry,
|
|
4744
|
+
VectorStore,
|
|
4745
|
+
assertReadOnly,
|
|
3462
4746
|
assignColors,
|
|
4747
|
+
bookmarksScanner,
|
|
3463
4748
|
buildMapData,
|
|
3464
4749
|
checkPrerequisites,
|
|
4750
|
+
checkReadOnly,
|
|
3465
4751
|
cleanupTempFiles,
|
|
3466
4752
|
computeCentroid,
|
|
3467
4753
|
computeClusterBounds,
|
|
3468
4754
|
createCartographyTools,
|
|
3469
|
-
|
|
4755
|
+
createHashEmbedder,
|
|
4756
|
+
createLocalEmbedder,
|
|
4757
|
+
createMcpServer,
|
|
4758
|
+
createScanRunner,
|
|
4759
|
+
createSemanticSearch,
|
|
3470
4760
|
defaultConfig,
|
|
4761
|
+
defaultRegistry,
|
|
3471
4762
|
edgesToConnections,
|
|
3472
4763
|
exportAll,
|
|
3473
4764
|
exportBackstageYAML,
|
|
3474
4765
|
exportDiscoveryApp,
|
|
3475
4766
|
exportJGF,
|
|
3476
4767
|
exportJSON,
|
|
4768
|
+
extractListeningPorts,
|
|
3477
4769
|
generateDependencyMermaid,
|
|
3478
4770
|
generateTopologyMermaid,
|
|
3479
4771
|
groupByDomain,
|
|
@@ -3483,7 +4775,10 @@ export {
|
|
|
3483
4775
|
hexRing,
|
|
3484
4776
|
hexSpiral,
|
|
3485
4777
|
hexToPixel,
|
|
4778
|
+
installedAppsScanner,
|
|
4779
|
+
isReadOnlyCommand,
|
|
3486
4780
|
layoutClusters,
|
|
4781
|
+
localDiscoveryFn,
|
|
3487
4782
|
log,
|
|
3488
4783
|
logDebug,
|
|
3489
4784
|
logError,
|
|
@@ -3491,10 +4786,16 @@ export {
|
|
|
3491
4786
|
logWarn,
|
|
3492
4787
|
nodesToAssets,
|
|
3493
4788
|
pixelToHex,
|
|
4789
|
+
portsScanner,
|
|
3494
4790
|
runDiscovery,
|
|
4791
|
+
runHttp,
|
|
4792
|
+
runLocalDiscovery,
|
|
4793
|
+
runStdio,
|
|
4794
|
+
safeEnv,
|
|
3495
4795
|
safetyHook,
|
|
3496
4796
|
setVerbose,
|
|
3497
4797
|
shadeVariant,
|
|
4798
|
+
splitSegments,
|
|
3498
4799
|
stripSensitive
|
|
3499
4800
|
};
|
|
3500
4801
|
//# sourceMappingURL=index.js.map
|