graphifyy 0.3.17 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15,6 +15,73 @@ var __export = (target, all) => {
15
15
  __defProp(target, name, { get: all[name], enumerable: true });
16
16
  };
17
17
 
18
+ // src/graph.ts
19
+ import Graph from "graphology";
20
+ function createGraph(directed = false) {
21
+ return new Graph({ type: directed ? "directed" : "undirected", multi: false });
22
+ }
23
+ function isDirectedGraph(G) {
24
+ return G.type === "directed";
25
+ }
26
+ function loadGraphFromData(raw) {
27
+ const G = createGraph(raw.directed === true);
28
+ for (const [key, value] of Object.entries(raw.graph ?? {})) {
29
+ G.setAttribute(key, value);
30
+ }
31
+ for (const node of raw.nodes ?? []) {
32
+ const { id, ...attrs } = node;
33
+ G.mergeNode(id, attrs);
34
+ }
35
+ for (const link of raw.links ?? raw.edges ?? []) {
36
+ const { source, target, ...attrs } = link;
37
+ if (!G.hasNode(source) || !G.hasNode(target)) continue;
38
+ try {
39
+ G.mergeEdge(source, target, attrs);
40
+ } catch {
41
+ }
42
+ }
43
+ if (raw.hyperedges && raw.hyperedges.length > 0) {
44
+ G.setAttribute("hyperedges", raw.hyperedges);
45
+ }
46
+ return G;
47
+ }
48
+ function toUndirectedGraph(G) {
49
+ if (!isDirectedGraph(G)) return G.copy();
50
+ const copy = createGraph(false);
51
+ for (const [key, value] of Object.entries(G.getAttributes())) {
52
+ copy.setAttribute(key, value);
53
+ }
54
+ G.forEachNode((nodeId, attrs) => {
55
+ copy.mergeNode(nodeId, attrs);
56
+ });
57
+ G.forEachEdge((_edge, attrs, source, target) => {
58
+ if (!copy.hasNode(source) || !copy.hasNode(target)) return;
59
+ try {
60
+ copy.mergeEdge(source, target, attrs);
61
+ } catch {
62
+ }
63
+ });
64
+ return copy;
65
+ }
66
+ function forEachTraversalNeighbor(G, node, callback) {
67
+ if (isDirectedGraph(G)) {
68
+ G.forEachOutboundNeighbor(node, callback);
69
+ return;
70
+ }
71
+ G.forEachNeighbor(node, callback);
72
+ }
73
+ function traversalNeighbors(G, node) {
74
+ const neighbors = [];
75
+ forEachTraversalNeighbor(G, node, (neighbor) => {
76
+ neighbors.push(neighbor);
77
+ });
78
+ return neighbors;
79
+ }
80
+ var init_graph = __esm({
81
+ "src/graph.ts"() {
82
+ }
83
+ });
84
+
18
85
  // src/hooks.ts
19
86
  var hooks_exports = {};
20
87
  __export(hooks_exports, {
@@ -43,7 +110,7 @@ function installHook(hooksDir, name, script, marker) {
43
110
  writeFileSync(hookPath, content.trimEnd() + "\n\n" + script);
44
111
  return `appended to existing ${name} hook at ${hookPath}`;
45
112
  }
46
- writeFileSync(hookPath, "#!/bin/bash\n" + script);
113
+ writeFileSync(hookPath, "#!/bin/sh\n" + script);
47
114
  chmodSync(hookPath, 493);
48
115
  return `installed at ${hookPath}`;
49
116
  }
@@ -56,7 +123,7 @@ function uninstallHook(hooksDir, name, marker, markerEnd) {
56
123
  escapeRegExp(marker) + "[\\s\\S]*?" + escapeRegExp(markerEnd) + "\\n?"
57
124
  );
58
125
  let newContent = content.replace(regex, "").trim();
59
- if (!newContent || newContent === "#!/bin/bash") {
126
+ if (!newContent || ["#!/bin/bash", "#!/bin/sh"].includes(newContent)) {
60
127
  unlinkSync(hookPath);
61
128
  return `removed ${name} hook at ${hookPath}`;
62
129
  }
@@ -369,7 +436,7 @@ __export(cluster_exports, {
369
436
  });
370
437
  import louvain from "graphology-communities-louvain";
371
438
  function partition(G) {
372
- const result = louvain(G);
439
+ const result = louvain(G.type === "directed" ? toUndirectedGraph(G) : G);
373
440
  const map = /* @__PURE__ */ new Map();
374
441
  for (const [node, cid] of Object.entries(result)) {
375
442
  map.set(node, cid);
@@ -475,11 +542,72 @@ var MAX_COMMUNITY_FRACTION, MIN_SPLIT_SIZE;
475
542
  var init_cluster = __esm({
476
543
  "src/cluster.ts"() {
477
544
  init_collections();
545
+ init_graph();
478
546
  MAX_COMMUNITY_FRACTION = 0.25;
479
547
  MIN_SPLIT_SIZE = 10;
480
548
  }
481
549
  });
482
550
 
551
+ // src/types.ts
552
+ var init_types = __esm({
553
+ "src/types.ts"() {
554
+ }
555
+ });
556
+
557
+ // src/detect.ts
558
+ import {
559
+ readdirSync,
560
+ readFileSync as readFileSync2,
561
+ writeFileSync as writeFileSync2,
562
+ statSync,
563
+ existsSync as existsSync3,
564
+ mkdirSync as mkdirSync2,
565
+ lstatSync
566
+ } from "fs";
567
+ import { join as join2, resolve as resolve2, extname, basename, relative, sep, dirname } from "path";
568
+ import { createHash } from "crypto";
569
+ var CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS;
570
+ var init_detect = __esm({
571
+ "src/detect.ts"() {
572
+ init_types();
573
+ CODE_EXTENSIONS = /* @__PURE__ */ new Set([
574
+ ".py",
575
+ ".ts",
576
+ ".js",
577
+ ".jsx",
578
+ ".tsx",
579
+ ".go",
580
+ ".rs",
581
+ ".java",
582
+ ".cpp",
583
+ ".cc",
584
+ ".cxx",
585
+ ".c",
586
+ ".h",
587
+ ".hpp",
588
+ ".rb",
589
+ ".swift",
590
+ ".kt",
591
+ ".kts",
592
+ ".cs",
593
+ ".scala",
594
+ ".php",
595
+ ".lua",
596
+ ".toc",
597
+ ".zig",
598
+ ".ps1",
599
+ ".ex",
600
+ ".exs",
601
+ ".m",
602
+ ".mm",
603
+ ".jl"
604
+ ]);
605
+ DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".rst"]);
606
+ PAPER_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
607
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
608
+ }
609
+ });
610
+
483
611
  // src/analyze.ts
484
612
  var analyze_exports = {};
485
613
  __export(analyze_exports, {
@@ -521,10 +649,11 @@ function isConceptNode(G, nodeId) {
521
649
  return false;
522
650
  }
523
651
  function fileCategory(path) {
524
- const ext = path.includes(".") ? path.split(".").pop()?.toLowerCase() ?? "" : "";
652
+ const ext = path.includes(".") ? `.${path.split(".").pop()?.toLowerCase() ?? ""}` : "";
525
653
  if (CODE_EXTENSIONS.has(ext)) return "code";
526
654
  if (PAPER_EXTENSIONS.has(ext)) return "paper";
527
655
  if (IMAGE_EXTENSIONS.has(ext)) return "image";
656
+ if (DOC_EXTENSIONS.has(ext)) return "doc";
528
657
  return "doc";
529
658
  }
530
659
  function topLevelDir(path) {
@@ -718,10 +847,10 @@ function suggestQuestions(G, communities, communityLabels, topN = 7) {
718
847
  const cid = nodeCommunity.get(nodeId);
719
848
  const commLabel = cid !== void 0 ? labelMap.get(cid) ?? `Community ${cid}` : "unknown";
720
849
  const neighborComms = /* @__PURE__ */ new Set();
721
- G.forEachNeighbor(nodeId, (n) => {
850
+ for (const n of traversalNeighbors(G, nodeId)) {
722
851
  const nc = nodeCommunity.get(n);
723
852
  if (nc !== void 0 && nc !== cid) neighborComms.add(nc);
724
- });
853
+ }
725
854
  if (neighborComms.size > 0) {
726
855
  const otherLabels = [...neighborComms].map((c) => labelMap.get(c) ?? `Community ${c}`);
727
856
  questions.push({
@@ -850,30 +979,12 @@ function graphDiff(GOld, GNew) {
850
979
  summary: parts.length > 0 ? parts.join(", ") : "no changes"
851
980
  };
852
981
  }
853
- var CODE_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS;
854
982
  var init_analyze = __esm({
855
983
  "src/analyze.ts"() {
856
984
  init_collections();
857
985
  init_cluster();
858
- CODE_EXTENSIONS = /* @__PURE__ */ new Set([
859
- "py",
860
- "ts",
861
- "tsx",
862
- "js",
863
- "go",
864
- "rs",
865
- "java",
866
- "rb",
867
- "cpp",
868
- "c",
869
- "h",
870
- "cs",
871
- "kt",
872
- "scala",
873
- "php"
874
- ]);
875
- PAPER_EXTENSIONS = /* @__PURE__ */ new Set(["pdf"]);
876
- IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "svg"]);
986
+ init_graph();
987
+ init_detect();
877
988
  }
878
989
  });
879
990
 
@@ -882,42 +993,27 @@ var serve_exports = {};
882
993
  __export(serve_exports, {
883
994
  serve: () => serve
884
995
  });
885
- import { readFileSync as readFileSync2 } from "fs";
886
- import Graph from "graphology";
996
+ import { readFileSync as readFileSync3 } from "fs";
887
997
  import { bidirectional } from "graphology-shortest-path/unweighted.js";
888
- import { basename } from "path";
998
+ import { basename as basename2, dirname as dirname2, resolve as resolve3 } from "path";
889
999
  function loadGraph(graphPath) {
890
1000
  let safePath;
891
1001
  try {
892
- safePath = validateGraphPath(graphPath);
1002
+ safePath = validateGraphPath(graphPath, dirname2(resolve3(graphPath)));
893
1003
  } catch (err) {
894
1004
  console.error(`error: ${err instanceof Error ? err.message : err}`);
895
1005
  process.exit(1);
896
1006
  }
897
1007
  let data;
898
1008
  try {
899
- data = JSON.parse(readFileSync2(safePath, "utf-8"));
1009
+ data = JSON.parse(readFileSync3(safePath, "utf-8"));
900
1010
  } catch (err) {
901
1011
  console.error(
902
1012
  `error: graph.json is corrupted (${err instanceof Error ? err.message : err}). Re-run the graphify skill to rebuild it (for Codex: $graphify .).`
903
1013
  );
904
1014
  process.exit(1);
905
1015
  }
906
- const G = new Graph({ type: "undirected", multi: false });
907
- const nodes = data.nodes ?? [];
908
- for (const node of nodes) {
909
- const { id, ...attrs } = node;
910
- G.mergeNode(id, attrs);
911
- }
912
- const links = data.links ?? data.edges ?? [];
913
- for (const link of links) {
914
- const { source, target, ...attrs } = link;
915
- try {
916
- G.mergeEdge(source, target, attrs);
917
- } catch {
918
- }
919
- }
920
- return G;
1016
+ return loadGraphFromData(data);
921
1017
  }
922
1018
  function communitiesFromGraph(G) {
923
1019
  const communities = /* @__PURE__ */ new Map();
@@ -930,6 +1026,15 @@ function communitiesFromGraph(G) {
930
1026
  });
931
1027
  return communities;
932
1028
  }
1029
+ function communityName(G, cid) {
1030
+ if (cid === void 0 || cid === null) return null;
1031
+ const labels = G.getAttribute("community_labels");
1032
+ const fromGraph = labels?.[String(cid)];
1033
+ if (typeof fromGraph === "string" && fromGraph.length > 0) {
1034
+ return sanitizeLabel(fromGraph);
1035
+ }
1036
+ return null;
1037
+ }
933
1038
  function scoreNodes(G, terms) {
934
1039
  const scored = [];
935
1040
  G.forEachNode((nid, data) => {
@@ -948,7 +1053,7 @@ function bfs(G, startNodes, depth) {
948
1053
  for (let i = 0; i < depth; i++) {
949
1054
  const nextFrontier = /* @__PURE__ */ new Set();
950
1055
  for (const n of frontier) {
951
- G.forEachNeighbor(n, (neighbor) => {
1056
+ forEachTraversalNeighbor(G, n, (neighbor) => {
952
1057
  if (!visited.has(neighbor)) {
953
1058
  nextFrontier.add(neighbor);
954
1059
  edges.push([n, neighbor]);
@@ -968,7 +1073,7 @@ function dfs(G, startNodes, depth) {
968
1073
  const [node, d] = stack.pop();
969
1074
  if (visited.has(node) || d > depth) continue;
970
1075
  visited.add(node);
971
- G.forEachNeighbor(node, (neighbor) => {
1076
+ forEachTraversalNeighbor(G, node, (neighbor) => {
972
1077
  if (!visited.has(neighbor)) {
973
1078
  stack.push([neighbor, d + 1]);
974
1079
  edges.push([node, neighbor]);
@@ -1047,7 +1152,7 @@ function toolGetNode(G, args) {
1047
1152
  ` ID: ${nid}`,
1048
1153
  ` Source: ${d.source_file ?? ""} ${d.source_location ?? ""}`,
1049
1154
  ` Type: ${d.file_type ?? ""}`,
1050
- ` Community: ${d.community ?? ""}`,
1155
+ ` Community: ${d.community_name ? `${d.community ?? ""} (${d.community_name})` : communityName(G, d.community) ?? String(d.community ?? "")}`,
1051
1156
  ` Degree: ${G.degree(nid)}`
1052
1157
  ].join("\n");
1053
1158
  }
@@ -1058,7 +1163,7 @@ function toolGetNeighbors(G, args) {
1058
1163
  if (matches.length === 0) return `No node matching '${label}' found.`;
1059
1164
  const nid = matches[0];
1060
1165
  const lines = [`Neighbors of ${G.getNodeAttribute(nid, "label") ?? nid}:`];
1061
- G.forEachNeighbor(nid, (neighbor) => {
1166
+ forEachTraversalNeighbor(G, nid, (neighbor) => {
1062
1167
  const edgeKey = G.edge(nid, neighbor);
1063
1168
  if (!edgeKey) return;
1064
1169
  const d = G.getEdgeAttributes(edgeKey);
@@ -1074,7 +1179,8 @@ function toolGetCommunity(communities, G, args) {
1074
1179
  const cid = Number(args.community_id);
1075
1180
  const nodes = communities.get(cid);
1076
1181
  if (!nodes || nodes.length === 0) return `Community ${cid} not found.`;
1077
- const lines = [`Community ${cid} (${nodes.length} nodes):`];
1182
+ const label = communityName(G, cid);
1183
+ const lines = [label ? `Community ${cid} - ${label} (${nodes.length} nodes):` : `Community ${cid} (${nodes.length} nodes):`];
1078
1184
  for (const n of nodes) {
1079
1185
  const d = G.getNodeAttributes(n);
1080
1186
  lines.push(` ${d.label ?? n} [${d.source_file ?? ""}]`);
@@ -1296,8 +1402,13 @@ async function serve(graphPath = "graphify-out/graph.json", transport) {
1296
1402
  if (!handler) {
1297
1403
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
1298
1404
  }
1299
- const text = handler(args ?? {});
1300
- return { content: [{ type: "text", text }] };
1405
+ try {
1406
+ const text = handler(args ?? {});
1407
+ return { content: [{ type: "text", text }] };
1408
+ } catch (err) {
1409
+ const message = err instanceof Error ? err.message : String(err);
1410
+ return { content: [{ type: "text", text: `Error executing ${name}: ${message}` }] };
1411
+ }
1301
1412
  });
1302
1413
  const serverTransport = transport ?? new StdioServerTransport();
1303
1414
  let keepAlive;
@@ -1305,14 +1416,14 @@ async function serve(graphPath = "graphify-out/graph.json", transport) {
1305
1416
  keepAlive = setInterval(() => void 0, 6e4);
1306
1417
  process.stdin?.resume();
1307
1418
  }
1308
- const closed = new Promise((resolve7) => {
1419
+ const closed = new Promise((resolve9) => {
1309
1420
  const previousOnClose = server.onclose;
1310
1421
  server.onclose = () => {
1311
1422
  if (keepAlive) {
1312
1423
  clearInterval(keepAlive);
1313
1424
  }
1314
1425
  previousOnClose?.();
1315
- resolve7();
1426
+ resolve9();
1316
1427
  };
1317
1428
  });
1318
1429
  await server.connect(serverTransport);
@@ -1323,9 +1434,10 @@ async function serve(graphPath = "graphify-out/graph.json", transport) {
1323
1434
  var isDirectExecution;
1324
1435
  var init_serve = __esm({
1325
1436
  "src/serve.ts"() {
1437
+ init_graph();
1326
1438
  init_security();
1327
1439
  init_analyze();
1328
- isDirectExecution = typeof process !== "undefined" && typeof process.argv[1] === "string" && /^serve\.(?:js|mjs|cjs|ts)$/.test(basename(process.argv[1]));
1440
+ isDirectExecution = typeof process !== "undefined" && typeof process.argv[1] === "string" && /^serve\.(?:js|mjs|cjs|ts)$/.test(basename2(process.argv[1]));
1329
1441
  if (isDirectExecution) {
1330
1442
  const graphPath = process.argv[2] ?? "graphify-out/graph.json";
1331
1443
  serve(graphPath).catch((err) => {
@@ -1337,21 +1449,33 @@ var init_serve = __esm({
1337
1449
  });
1338
1450
 
1339
1451
  // src/cache.ts
1340
- import { createHash } from "crypto";
1341
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync as unlinkSync2, renameSync, existsSync as existsSync3 } from "fs";
1342
- import { join as join2, resolve as resolve2 } from "path";
1452
+ import { createHash as createHash2 } from "crypto";
1453
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, unlinkSync as unlinkSync2, renameSync, existsSync as existsSync4 } from "fs";
1454
+ import { extname as extname2, join as join3, resolve as resolve5 } from "path";
1455
+ function bodyContent(content) {
1456
+ const text = content.toString("utf-8");
1457
+ if (!text.startsWith("---")) {
1458
+ return content;
1459
+ }
1460
+ const end = text.indexOf("\n---", 3);
1461
+ if (end === -1) {
1462
+ return content;
1463
+ }
1464
+ return Buffer.from(text.slice(end + 4), "utf-8");
1465
+ }
1343
1466
  function fileHash(filePath) {
1344
- const content = readFileSync3(filePath);
1345
- const resolved = resolve2(filePath);
1346
- const h = createHash("sha256");
1467
+ const raw = readFileSync4(filePath);
1468
+ const content = extname2(filePath).toLowerCase() === ".md" ? bodyContent(raw) : raw;
1469
+ const resolved = resolve5(filePath);
1470
+ const h = createHash2("sha256");
1347
1471
  h.update(content);
1348
1472
  h.update("\0");
1349
1473
  h.update(resolved);
1350
1474
  return h.digest("hex");
1351
1475
  }
1352
1476
  function cacheDir(root = ".") {
1353
- const d = join2(root, "graphify-out", "cache");
1354
- mkdirSync2(d, { recursive: true });
1477
+ const d = join3(root, "graphify-out", "cache");
1478
+ mkdirSync3(d, { recursive: true });
1355
1479
  return d;
1356
1480
  }
1357
1481
  function loadCached(filePath, root = ".") {
@@ -1361,20 +1485,20 @@ function loadCached(filePath, root = ".") {
1361
1485
  } catch {
1362
1486
  return null;
1363
1487
  }
1364
- const entry = join2(cacheDir(root), `${h}.json`);
1365
- if (!existsSync3(entry)) return null;
1488
+ const entry = join3(cacheDir(root), `${h}.json`);
1489
+ if (!existsSync4(entry)) return null;
1366
1490
  try {
1367
- return JSON.parse(readFileSync3(entry, "utf-8"));
1491
+ return JSON.parse(readFileSync4(entry, "utf-8"));
1368
1492
  } catch {
1369
1493
  return null;
1370
1494
  }
1371
1495
  }
1372
1496
  function saveCached(filePath, result, root = ".") {
1373
1497
  const h = fileHash(filePath);
1374
- const entry = join2(cacheDir(root), `${h}.json`);
1498
+ const entry = join3(cacheDir(root), `${h}.json`);
1375
1499
  const tmp = entry + ".tmp";
1376
1500
  try {
1377
- writeFileSync2(tmp, JSON.stringify(result));
1501
+ writeFileSync3(tmp, JSON.stringify(result));
1378
1502
  renameSync(tmp, entry);
1379
1503
  } catch {
1380
1504
  try {
@@ -1415,8 +1539,8 @@ __export(extract_exports, {
1415
1539
  extractWithDiagnostics: () => extractWithDiagnostics,
1416
1540
  extractZig: () => extractZig
1417
1541
  });
1418
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, lstatSync, realpathSync, existsSync as existsSync4 } from "fs";
1419
- import { resolve as resolve3, basename as basename2, extname, dirname, join as join3, sep } from "path";
1542
+ import { readFileSync as readFileSync5, readdirSync as readdirSync3, lstatSync as lstatSync2, realpathSync, existsSync as existsSync5 } from "fs";
1543
+ import { resolve as resolve7, basename as basename3, extname as extname3, dirname as dirname3, join as join4, sep as sep2 } from "path";
1420
1544
  import { createRequire } from "module";
1421
1545
  import * as TreeSitter from "web-tree-sitter";
1422
1546
  function getModuleRequire() {
@@ -1454,14 +1578,14 @@ function resolveGrammarWasm(langName) {
1454
1578
  for (const candidate of candidates) {
1455
1579
  try {
1456
1580
  const resolved = moduleRequire.resolve(candidate);
1457
- if (existsSync4(resolved)) return resolved;
1581
+ if (existsSync5(resolved)) return resolved;
1458
1582
  } catch {
1459
1583
  }
1460
1584
  }
1461
- const nmDir = join3(process.cwd(), "node_modules");
1585
+ const nmDir = join4(process.cwd(), "node_modules");
1462
1586
  for (const candidate of candidates) {
1463
- const p = join3(nmDir, candidate);
1464
- if (existsSync4(p)) return p;
1587
+ const p = join4(nmDir, candidate);
1588
+ if (existsSync5(p)) return p;
1465
1589
  }
1466
1590
  return null;
1467
1591
  }
@@ -1866,13 +1990,13 @@ async function _extractGeneric(filePath, config) {
1866
1990
  try {
1867
1991
  const parser = new Parser2();
1868
1992
  parser.setLanguage(lang);
1869
- source = readFileSync4(filePath, "utf-8");
1993
+ source = readFileSync5(filePath, "utf-8");
1870
1994
  tree = parseText(parser, source);
1871
1995
  } catch (e) {
1872
1996
  return { nodes: [], edges: [], error: String(e) };
1873
1997
  }
1874
1998
  const root = tree.rootNode;
1875
- const stem = basename2(filePath, extname(filePath));
1999
+ const stem = basename3(filePath, extname3(filePath));
1876
2000
  const strPath = filePath;
1877
2001
  const nodes = [];
1878
2002
  const edges = [];
@@ -1902,7 +2026,7 @@ async function _extractGeneric(filePath, config) {
1902
2026
  });
1903
2027
  }
1904
2028
  const fileNid = _makeId(stem);
1905
- addNode(fileNid, basename2(filePath), 1);
2029
+ addNode(fileNid, basename3(filePath), 1);
1906
2030
  function walk(node, parentClassNid = null) {
1907
2031
  const t = node.type;
1908
2032
  if (config.importTypes.has(t)) {
@@ -2241,10 +2365,10 @@ async function _extractGeneric(filePath, config) {
2241
2365
  source: callerNid,
2242
2366
  target: tgtNid,
2243
2367
  relation: "calls",
2244
- confidence: "INFERRED",
2368
+ confidence: "EXTRACTED",
2245
2369
  source_file: strPath,
2246
2370
  source_location: `L${line}`,
2247
- weight: 0.8
2371
+ weight: 1
2248
2372
  });
2249
2373
  }
2250
2374
  }
@@ -2274,13 +2398,13 @@ async function _extractPythonRationale(filePath, result) {
2274
2398
  try {
2275
2399
  const parser = new Parser2();
2276
2400
  parser.setLanguage(lang);
2277
- source = readFileSync4(filePath, "utf-8");
2401
+ source = readFileSync5(filePath, "utf-8");
2278
2402
  const tree = parseText(parser, source);
2279
2403
  root = tree.rootNode;
2280
2404
  } catch {
2281
2405
  return;
2282
2406
  }
2283
- const stem = basename2(filePath, extname(filePath));
2407
+ const stem = basename3(filePath, extname3(filePath));
2284
2408
  const strPath = filePath;
2285
2409
  const { nodes, edges } = result;
2286
2410
  const seenIds = new Set(nodes.map((n) => n.id));
@@ -2376,7 +2500,7 @@ async function extractPython(filePath) {
2376
2500
  return result;
2377
2501
  }
2378
2502
  async function extractJs(filePath) {
2379
- const ext = extname(filePath);
2503
+ const ext = extname3(filePath);
2380
2504
  const config = ext === ".ts" || ext === ".tsx" ? _TS_CONFIG : _JS_CONFIG;
2381
2505
  return _extractGeneric(filePath, config);
2382
2506
  }
@@ -2421,13 +2545,13 @@ async function extractJulia(filePath) {
2421
2545
  try {
2422
2546
  const parser = new Parser2();
2423
2547
  parser.setLanguage(lang);
2424
- source = readFileSync4(filePath, "utf-8");
2548
+ source = readFileSync5(filePath, "utf-8");
2425
2549
  tree = parseText(parser, source);
2426
2550
  } catch (e) {
2427
2551
  return { nodes: [], edges: [], error: String(e) };
2428
2552
  }
2429
2553
  const root = tree.rootNode;
2430
- const stem = basename2(filePath, extname(filePath));
2554
+ const stem = basename3(filePath, extname3(filePath));
2431
2555
  const strPath = filePath;
2432
2556
  const nodes = [];
2433
2557
  const edges = [];
@@ -2443,7 +2567,7 @@ async function extractJulia(filePath) {
2443
2567
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
2444
2568
  }
2445
2569
  const fileNid = _makeId(stem);
2446
- addNode(fileNid, basename2(filePath), 1);
2570
+ addNode(fileNid, basename3(filePath), 1);
2447
2571
  function funcNameFromSignature(sigNode) {
2448
2572
  for (const child of sigNode.children) {
2449
2573
  if (child.type === "call_expression") {
@@ -2617,14 +2741,14 @@ async function extractGo(filePath) {
2617
2741
  try {
2618
2742
  const parser = new Parser2();
2619
2743
  parser.setLanguage(lang);
2620
- source = readFileSync4(filePath, "utf-8");
2744
+ source = readFileSync5(filePath, "utf-8");
2621
2745
  tree = parseText(parser, source);
2622
2746
  } catch (e) {
2623
2747
  return { nodes: [], edges: [], error: String(e) };
2624
2748
  }
2625
2749
  const root = tree.rootNode;
2626
- const stem = basename2(filePath, extname(filePath));
2627
- const pkgScope = dirname(filePath).split(sep).pop() || stem;
2750
+ const stem = basename3(filePath, extname3(filePath));
2751
+ const pkgScope = dirname3(filePath).split(sep2).pop() || stem;
2628
2752
  const strPath = filePath;
2629
2753
  const nodes = [];
2630
2754
  const edges = [];
@@ -2640,7 +2764,7 @@ async function extractGo(filePath) {
2640
2764
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
2641
2765
  }
2642
2766
  const fileNid = _makeId(stem);
2643
- addNode(fileNid, basename2(filePath), 1);
2767
+ addNode(fileNid, basename3(filePath), 1);
2644
2768
  function walk(node) {
2645
2769
  const t = node.type;
2646
2770
  if (t === "function_declaration") {
@@ -2767,10 +2891,10 @@ async function extractGo(filePath) {
2767
2891
  source: callerNid,
2768
2892
  target: tgtNid,
2769
2893
  relation: "calls",
2770
- confidence: "INFERRED",
2894
+ confidence: "EXTRACTED",
2771
2895
  source_file: strPath,
2772
2896
  source_location: `L${line}`,
2773
- weight: 0.8
2897
+ weight: 1
2774
2898
  });
2775
2899
  }
2776
2900
  }
@@ -2799,13 +2923,13 @@ async function extractRust(filePath) {
2799
2923
  try {
2800
2924
  const parser = new Parser2();
2801
2925
  parser.setLanguage(lang);
2802
- source = readFileSync4(filePath, "utf-8");
2926
+ source = readFileSync5(filePath, "utf-8");
2803
2927
  tree = parseText(parser, source);
2804
2928
  } catch (e) {
2805
2929
  return { nodes: [], edges: [], error: String(e) };
2806
2930
  }
2807
2931
  const root = tree.rootNode;
2808
- const stem = basename2(filePath, extname(filePath));
2932
+ const stem = basename3(filePath, extname3(filePath));
2809
2933
  const strPath = filePath;
2810
2934
  const nodes = [];
2811
2935
  const edges = [];
@@ -2821,7 +2945,7 @@ async function extractRust(filePath) {
2821
2945
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
2822
2946
  }
2823
2947
  const fileNid = _makeId(stem);
2824
- addNode(fileNid, basename2(filePath), 1);
2948
+ addNode(fileNid, basename3(filePath), 1);
2825
2949
  function walk(node, parentImplNid = null) {
2826
2950
  const t = node.type;
2827
2951
  if (t === "function_item") {
@@ -2922,10 +3046,10 @@ async function extractRust(filePath) {
2922
3046
  source: callerNid,
2923
3047
  target: tgtNid,
2924
3048
  relation: "calls",
2925
- confidence: "INFERRED",
3049
+ confidence: "EXTRACTED",
2926
3050
  source_file: strPath,
2927
3051
  source_location: `L${line}`,
2928
- weight: 0.8
3052
+ weight: 1
2929
3053
  });
2930
3054
  }
2931
3055
  }
@@ -2954,13 +3078,13 @@ async function extractZig(filePath) {
2954
3078
  try {
2955
3079
  const parser = new Parser2();
2956
3080
  parser.setLanguage(lang);
2957
- source = readFileSync4(filePath, "utf-8");
3081
+ source = readFileSync5(filePath, "utf-8");
2958
3082
  tree = parseText(parser, source);
2959
3083
  } catch (e) {
2960
3084
  return { nodes: [], edges: [], error: String(e) };
2961
3085
  }
2962
3086
  const root = tree.rootNode;
2963
- const stem = basename2(filePath, extname(filePath));
3087
+ const stem = basename3(filePath, extname3(filePath));
2964
3088
  const strPath = filePath;
2965
3089
  const nodes = [];
2966
3090
  const edges = [];
@@ -2976,7 +3100,7 @@ async function extractZig(filePath) {
2976
3100
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
2977
3101
  }
2978
3102
  const fileNid = _makeId(stem);
2979
- addNode(fileNid, basename2(filePath), 1);
3103
+ addNode(fileNid, basename3(filePath), 1);
2980
3104
  function extractImport(node) {
2981
3105
  for (const child of node.children) {
2982
3106
  if (child.type === "builtin_function") {
@@ -3084,7 +3208,7 @@ async function extractZig(filePath) {
3084
3208
  const pair = `${callerNid}|${tgtNid}`;
3085
3209
  if (!seenCallPairs.has(pair)) {
3086
3210
  seenCallPairs.add(pair);
3087
- addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "INFERRED", 0.8);
3211
+ addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "EXTRACTED", 1);
3088
3212
  }
3089
3213
  }
3090
3214
  }
@@ -3112,13 +3236,13 @@ async function extractPowershell(filePath) {
3112
3236
  try {
3113
3237
  const parser = new Parser2();
3114
3238
  parser.setLanguage(lang);
3115
- source = readFileSync4(filePath, "utf-8");
3239
+ source = readFileSync5(filePath, "utf-8");
3116
3240
  tree = parseText(parser, source);
3117
3241
  } catch (e) {
3118
3242
  return { nodes: [], edges: [], error: String(e) };
3119
3243
  }
3120
3244
  const root = tree.rootNode;
3121
- const stem = basename2(filePath, extname(filePath));
3245
+ const stem = basename3(filePath, extname3(filePath));
3122
3246
  const strPath = filePath;
3123
3247
  const nodes = [];
3124
3248
  const edges = [];
@@ -3134,7 +3258,7 @@ async function extractPowershell(filePath) {
3134
3258
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
3135
3259
  }
3136
3260
  const fileNid = _makeId(stem);
3137
- addNode(fileNid, basename2(filePath), 1);
3261
+ addNode(fileNid, basename3(filePath), 1);
3138
3262
  const _PS_SKIP = /* @__PURE__ */ new Set([
3139
3263
  "using",
3140
3264
  "return",
@@ -3267,7 +3391,7 @@ async function extractPowershell(filePath) {
3267
3391
  const pair = `${callerNid}|${tgtNid}`;
3268
3392
  if (!seenCallPairs.has(pair)) {
3269
3393
  seenCallPairs.add(pair);
3270
- addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "INFERRED", 0.8);
3394
+ addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "EXTRACTED", 1);
3271
3395
  }
3272
3396
  }
3273
3397
  }
@@ -3296,13 +3420,13 @@ async function extractObjc(filePath) {
3296
3420
  try {
3297
3421
  const parser = new Parser2();
3298
3422
  parser.setLanguage(lang);
3299
- source = readFileSync4(filePath, "utf-8");
3423
+ source = readFileSync5(filePath, "utf-8");
3300
3424
  tree = parseText(parser, source);
3301
3425
  } catch (e) {
3302
3426
  return { nodes: [], edges: [], error: String(e) };
3303
3427
  }
3304
3428
  const root = tree.rootNode;
3305
- const stem = basename2(filePath, extname(filePath));
3429
+ const stem = basename3(filePath, extname3(filePath));
3306
3430
  const strPath = filePath;
3307
3431
  const nodes = [];
3308
3432
  const edges = [];
@@ -3318,7 +3442,7 @@ async function extractObjc(filePath) {
3318
3442
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
3319
3443
  }
3320
3444
  const fileNid = _makeId(stem);
3321
- addNode(fileNid, basename2(filePath), 1);
3445
+ addNode(fileNid, basename3(filePath), 1);
3322
3446
  function _read(node) {
3323
3447
  return source.slice(node.startIndex, node.endIndex);
3324
3448
  }
@@ -3473,7 +3597,7 @@ async function extractObjc(filePath) {
3473
3597
  const pair = `${callerNid}|${candidate}`;
3474
3598
  if (!seenCalls.has(pair) && callerNid !== candidate) {
3475
3599
  seenCalls.add(pair);
3476
- addEdge(callerNid, candidate, "calls", bodyNode.startPosition.row + 1, "INFERRED", 0.8);
3600
+ addEdge(callerNid, candidate, "calls", bodyNode.startPosition.row + 1, "EXTRACTED", 1);
3477
3601
  }
3478
3602
  }
3479
3603
  }
@@ -3499,13 +3623,13 @@ async function extractElixir(filePath) {
3499
3623
  try {
3500
3624
  const parser = new Parser2();
3501
3625
  parser.setLanguage(lang);
3502
- source = readFileSync4(filePath, "utf-8");
3626
+ source = readFileSync5(filePath, "utf-8");
3503
3627
  tree = parseText(parser, source);
3504
3628
  } catch (e) {
3505
3629
  return { nodes: [], edges: [], error: String(e) };
3506
3630
  }
3507
3631
  const root = tree.rootNode;
3508
- const stem = basename2(filePath, extname(filePath));
3632
+ const stem = basename3(filePath, extname3(filePath));
3509
3633
  const strPath = filePath;
3510
3634
  const nodes = [];
3511
3635
  const edges = [];
@@ -3521,7 +3645,7 @@ async function extractElixir(filePath) {
3521
3645
  edges.push({ source: src, target: tgt, relation, confidence, source_file: strPath, source_location: `L${line}`, weight });
3522
3646
  }
3523
3647
  const fileNid = _makeId(stem);
3524
- addNode(fileNid, basename2(filePath), 1);
3648
+ addNode(fileNid, basename3(filePath), 1);
3525
3649
  const _IMPORT_KEYWORDS = /* @__PURE__ */ new Set(["alias", "import", "require", "use"]);
3526
3650
  function getAliasText(node) {
3527
3651
  for (const child of node.children) {
@@ -3664,7 +3788,7 @@ async function extractElixir(filePath) {
3664
3788
  const pair = `${callerNid}|${tgtNid}`;
3665
3789
  if (!seenCallPairs.has(pair)) {
3666
3790
  seenCallPairs.add(pair);
3667
- addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "INFERRED", 0.8);
3791
+ addEdge(callerNid, tgtNid, "calls", node.startPosition.row + 1, "EXTRACTED", 1);
3668
3792
  }
3669
3793
  }
3670
3794
  }
@@ -3691,7 +3815,7 @@ async function _resolveCrossFileImports(perFile, paths) {
3691
3815
  for (const node of fileResult.nodes ?? []) {
3692
3816
  const src = node.source_file ?? "";
3693
3817
  if (!src) continue;
3694
- const fileStem = basename2(src, extname(src));
3818
+ const fileStem = basename3(src, extname3(src));
3695
3819
  const label = node.label ?? "";
3696
3820
  const nid = node.id ?? "";
3697
3821
  if (label && !label.endsWith(")") && !label.endsWith(".py") && !label.startsWith("_")) {
@@ -3703,7 +3827,7 @@ async function _resolveCrossFileImports(perFile, paths) {
3703
3827
  const newEdges = [];
3704
3828
  const stemToPath = /* @__PURE__ */ new Map();
3705
3829
  for (const p of paths) {
3706
- stemToPath.set(basename2(p, extname(p)), p);
3830
+ stemToPath.set(basename3(p, extname3(p)), p);
3707
3831
  }
3708
3832
  for (let idx = 0; idx < perFile.length; idx++) {
3709
3833
  let walkImports = function(node) {
@@ -3767,7 +3891,7 @@ async function _resolveCrossFileImports(perFile, paths) {
3767
3891
  };
3768
3892
  const fileResult = perFile[idx];
3769
3893
  const filePath = paths[idx];
3770
- const fileStem = basename2(filePath, extname(filePath));
3894
+ const fileStem = basename3(filePath, extname3(filePath));
3771
3895
  const strPath = filePath;
3772
3896
  const localClasses = fileResult.nodes.filter(
3773
3897
  (n) => n.source_file === strPath && !n.label.endsWith(")") && !n.label.endsWith(".py") && n.id !== _makeId(fileStem)
@@ -3776,7 +3900,7 @@ async function _resolveCrossFileImports(perFile, paths) {
3776
3900
  let source;
3777
3901
  let tree;
3778
3902
  try {
3779
- source = readFileSync4(filePath, "utf-8");
3903
+ source = readFileSync5(filePath, "utf-8");
3780
3904
  tree = parseText(parser, source);
3781
3905
  } catch {
3782
3906
  continue;
@@ -3793,9 +3917,9 @@ async function extractWithDiagnostics(paths) {
3793
3917
  if (paths.length === 0) {
3794
3918
  root = ".";
3795
3919
  } else if (paths.length === 1) {
3796
- root = dirname(paths[0]);
3920
+ root = dirname3(paths[0]);
3797
3921
  } else {
3798
- const parts = paths.map((p) => p.split(sep));
3922
+ const parts = paths.map((p) => p.split(sep2));
3799
3923
  const minLen = Math.min(...parts.map((p) => p.length));
3800
3924
  let commonLen = 0;
3801
3925
  for (let i = 0; i < minLen; i++) {
@@ -3803,7 +3927,7 @@ async function extractWithDiagnostics(paths) {
3803
3927
  if (uniqueAtLevel.size === 1) commonLen++;
3804
3928
  else break;
3805
3929
  }
3806
- root = commonLen > 0 ? parts[0].slice(0, commonLen).join(sep) : ".";
3930
+ root = commonLen > 0 ? parts[0].slice(0, commonLen).join(sep2) : ".";
3807
3931
  }
3808
3932
  } catch {
3809
3933
  root = ".";
@@ -3816,7 +3940,7 @@ async function extractWithDiagnostics(paths) {
3816
3940
  `);
3817
3941
  }
3818
3942
  const filePath = paths[i];
3819
- const ext = extname(filePath);
3943
+ const ext = extname3(filePath);
3820
3944
  const extractor = _DISPATCH[ext];
3821
3945
  if (!extractor) continue;
3822
3946
  const cached = loadCached(filePath, root);
@@ -3842,9 +3966,9 @@ async function extractWithDiagnostics(paths) {
3842
3966
  allNodes.push(...result.nodes ?? []);
3843
3967
  allEdges.push(...result.edges ?? []);
3844
3968
  }
3845
- const pyPaths = paths.filter((p) => extname(p) === ".py");
3969
+ const pyPaths = paths.filter((p) => extname3(p) === ".py");
3846
3970
  if (pyPaths.length > 0) {
3847
- const pyResults = perFile.filter((_r, i) => extname(paths[i]) === ".py");
3971
+ const pyResults = perFile.filter((_r, i) => extname3(paths[i]) === ".py");
3848
3972
  try {
3849
3973
  const crossFileEdges = await _resolveCrossFileImports(pyResults, pyPaths);
3850
3974
  allEdges.push(...crossFileEdges);
@@ -3867,9 +3991,9 @@ async function extract(paths) {
3867
3991
  }
3868
3992
  function collectFiles(target, options) {
3869
3993
  const followSymlinks = options?.followSymlinks ?? false;
3870
- const resolved = resolve3(target);
3994
+ const resolved = resolve7(target);
3871
3995
  try {
3872
- const stat = lstatSync(resolved);
3996
+ const stat = lstatSync2(resolved);
3873
3997
  if (stat.isFile()) return [resolved];
3874
3998
  } catch {
3875
3999
  return [];
@@ -3878,16 +4002,16 @@ function collectFiles(target, options) {
3878
4002
  function walkDir(dir, visited) {
3879
4003
  let entries;
3880
4004
  try {
3881
- entries = readdirSync2(dir);
4005
+ entries = readdirSync3(dir);
3882
4006
  } catch {
3883
4007
  return;
3884
4008
  }
3885
4009
  for (const entry of entries) {
3886
4010
  if (entry.startsWith(".")) continue;
3887
- const fullPath = join3(dir, entry);
4011
+ const fullPath = join4(dir, entry);
3888
4012
  let stat;
3889
4013
  try {
3890
- stat = lstatSync(fullPath);
4014
+ stat = lstatSync2(fullPath);
3891
4015
  } catch {
3892
4016
  continue;
3893
4017
  }
@@ -3898,17 +4022,17 @@ function collectFiles(target, options) {
3898
4022
  const real = realpathSync(fullPath);
3899
4023
  if (visited.has(real)) continue;
3900
4024
  visited.add(real);
3901
- const parentReal = realpathSync(dirname(fullPath));
3902
- if (parentReal === real || parentReal.startsWith(real + sep)) continue;
4025
+ const parentReal = realpathSync(dirname3(fullPath));
4026
+ if (parentReal === real || parentReal.startsWith(real + sep2)) continue;
3903
4027
  } catch {
3904
4028
  continue;
3905
4029
  }
3906
4030
  }
3907
- const pathParts = fullPath.split(sep);
4031
+ const pathParts = fullPath.split(sep2);
3908
4032
  if (pathParts.some((part) => part.startsWith("."))) continue;
3909
4033
  walkDir(fullPath, visited);
3910
4034
  } else if (stat.isFile()) {
3911
- const ext = extname(entry);
4035
+ const ext = extname3(entry);
3912
4036
  if (_EXTENSIONS.has(ext)) {
3913
4037
  results.push(fullPath);
3914
4038
  }
@@ -4270,8 +4394,7 @@ __export(build_exports, {
4270
4394
  build: () => build,
4271
4395
  buildFromJson: () => buildFromJson
4272
4396
  });
4273
- import Graph2 from "graphology";
4274
- function buildFromJson(extraction) {
4397
+ function buildFromJson(extraction, options) {
4275
4398
  const errors = validateExtraction(extraction);
4276
4399
  const realErrors = errors.filter((e) => !e.includes("does not match any node id"));
4277
4400
  if (realErrors.length > 0) {
@@ -4279,7 +4402,7 @@ function buildFromJson(extraction) {
4279
4402
  `[graphify] Extraction warning (${realErrors.length} issues): ${realErrors[0]}`
4280
4403
  );
4281
4404
  }
4282
- const G = new Graph2({ type: "undirected", multi: false });
4405
+ const G = createGraph(options?.directed === true);
4283
4406
  for (const node of extraction.nodes ?? []) {
4284
4407
  const { id, ...attrs } = node;
4285
4408
  G.mergeNode(id, attrs);
@@ -4301,7 +4424,7 @@ function buildFromJson(extraction) {
4301
4424
  }
4302
4425
  return G;
4303
4426
  }
4304
- function build(extractions) {
4427
+ function build(extractions, options) {
4305
4428
  const combined = {
4306
4429
  nodes: [],
4307
4430
  edges: [],
@@ -4316,10 +4439,11 @@ function build(extractions) {
4316
4439
  combined.input_tokens += ext.input_tokens ?? 0;
4317
4440
  combined.output_tokens += ext.output_tokens ?? 0;
4318
4441
  }
4319
- return buildFromJson(combined);
4442
+ return buildFromJson(combined, options);
4320
4443
  }
4321
4444
  var init_build = __esm({
4322
4445
  "src/build.ts"() {
4446
+ init_graph();
4323
4447
  init_validate();
4324
4448
  }
4325
4449
  });
@@ -4499,7 +4623,7 @@ __export(export_exports, {
4499
4623
  toJson: () => toJson,
4500
4624
  toSvg: () => toSvg
4501
4625
  });
4502
- import { writeFileSync as writeFileSync3 } from "fs";
4626
+ import { writeFileSync as writeFileSync4 } from "fs";
4503
4627
  function nodeCommunityMap2(communities) {
4504
4628
  const communityMap = toNumericMap(communities);
4505
4629
  const result = /* @__PURE__ */ new Map();
@@ -4527,14 +4651,17 @@ function normalizeCommunityLabels(labelsOrOptions) {
4527
4651
  }
4528
4652
  return toNumericMap(labelsOrOptions.communityLabels);
4529
4653
  }
4530
- function toJson(G, communities, outputPath) {
4654
+ function toJson(G, communities, outputPath, communityLabelsOrOptions) {
4531
4655
  const nodeComm = nodeCommunityMap2(communities);
4656
+ const communityLabels = normalizeCommunityLabels(communityLabelsOrOptions);
4532
4657
  const nodes = [];
4533
4658
  G.forEachNode((nodeId, attrs) => {
4659
+ const communityId = nodeComm.get(nodeId) ?? null;
4534
4660
  nodes.push({
4535
4661
  id: nodeId,
4536
4662
  ...attrs,
4537
- community: nodeComm.get(nodeId) ?? null
4663
+ community: communityId,
4664
+ community_name: communityId !== null ? sanitizeLabel(communityLabels?.get(communityId) ?? `Community ${communityId}`) : null
4538
4665
  });
4539
4666
  });
4540
4667
  const links = [];
@@ -4551,15 +4678,20 @@ function toJson(G, communities, outputPath) {
4551
4678
  links.push(link);
4552
4679
  });
4553
4680
  const hyperedges = G.getAttribute("hyperedges") ?? [];
4681
+ const communityLabelsObject = communityLabels ? Object.fromEntries(
4682
+ [...communityLabels.entries()].sort((a, b) => a[0] - b[0]).map(([cid, label]) => [String(cid), sanitizeLabel(label)])
4683
+ ) : {};
4554
4684
  const output = {
4555
- directed: false,
4685
+ directed: isDirectedGraph(G),
4556
4686
  multigraph: false,
4557
- graph: {},
4687
+ graph: {
4688
+ community_labels: communityLabelsObject
4689
+ },
4558
4690
  nodes,
4559
4691
  links,
4560
4692
  hyperedges
4561
4693
  };
4562
- writeFileSync3(outputPath, JSON.stringify(output, null, 2), "utf-8");
4694
+ writeFileSync4(outputPath, JSON.stringify(output, null, 2), "utf-8");
4563
4695
  }
4564
4696
  function toCypher(G, outputPath) {
4565
4697
  const lines = ["// Neo4j Cypher import - generated by the graphify skill", ""];
@@ -4581,7 +4713,7 @@ function toCypher(G, outputPath) {
4581
4713
  `MATCH (a {id: '${uEsc}'}), (b {id: '${vEsc}'}) MERGE (a)-[:${rel} {confidence: '${conf}'}]->(b);`
4582
4714
  );
4583
4715
  });
4584
- writeFileSync3(outputPath, lines.join("\n"), "utf-8");
4716
+ writeFileSync4(outputPath, lines.join("\n"), "utf-8");
4585
4717
  }
4586
4718
  function neo4jLabel(label) {
4587
4719
  const sanitized = label.replace(/[^A-Za-z0-9_]/g, "");
@@ -4820,9 +4952,24 @@ function focusNode(nodeId) {
4820
4952
  showInfo(nodeId);
4821
4953
  }
4822
4954
 
4955
+ let hoveredNodeId = null;
4956
+ network.on('hoverNode', params => {
4957
+ hoveredNodeId = params.node;
4958
+ container.style.cursor = 'pointer';
4959
+ });
4960
+ network.on('blurNode', () => {
4961
+ hoveredNodeId = null;
4962
+ container.style.cursor = 'default';
4963
+ });
4964
+ container.addEventListener('click', () => {
4965
+ if (hoveredNodeId !== null) {
4966
+ showInfo(hoveredNodeId);
4967
+ network.selectNodes([hoveredNodeId]);
4968
+ }
4969
+ });
4823
4970
  network.on('click', params => {
4824
4971
  if (params.nodes.length > 0) showInfo(params.nodes[0]);
4825
- else document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
4972
+ else if (hoveredNodeId === null) document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
4826
4973
  });
4827
4974
 
4828
4975
  const searchInput = document.getElementById('search');
@@ -4977,7 +5124,7 @@ ${htmlScript(nodesJson, edgesJson, legendJson)}
4977
5124
  ${hyperedgeScript(hyperedgesJson)}
4978
5125
  </body>
4979
5126
  </html>`;
4980
- writeFileSync3(outputPath, html, "utf-8");
5127
+ writeFileSync4(outputPath, html, "utf-8");
4981
5128
  }
4982
5129
  function toGraphml(G, communities, outputPath) {
4983
5130
  const nodeComm = nodeCommunityMap2(communities);
@@ -4993,7 +5140,7 @@ function toGraphml(G, communities, outputPath) {
4993
5140
  lines.push(' <key id="community" for="node" attr.name="community" attr.type="int"/>');
4994
5141
  lines.push(' <key id="relation" for="edge" attr.name="relation" attr.type="string"/>');
4995
5142
  lines.push(' <key id="confidence" for="edge" attr.name="confidence" attr.type="string"/>');
4996
- lines.push(' <graph id="G" edgedefault="undirected">');
5143
+ lines.push(` <graph id="G" edgedefault="${isDirectedGraph(G) ? "directed" : "undirected"}">`);
4997
5144
  G.forEachNode((nodeId, data) => {
4998
5145
  lines.push(` <node id="${xmlEsc(nodeId)}">`);
4999
5146
  lines.push(` <data key="label">${xmlEsc(data.label ?? nodeId)}</data>`);
@@ -5010,7 +5157,7 @@ function toGraphml(G, communities, outputPath) {
5010
5157
  });
5011
5158
  lines.push(" </graph>");
5012
5159
  lines.push("</graphml>");
5013
- writeFileSync3(outputPath, lines.join("\n"), "utf-8");
5160
+ writeFileSync4(outputPath, lines.join("\n"), "utf-8");
5014
5161
  }
5015
5162
  function toSvg(G, communities, outputPath, communityLabelsOrOptions, figsize = [20, 14]) {
5016
5163
  const communityMap = toNumericMap(communities);
@@ -5083,7 +5230,7 @@ function toSvg(G, communities, outputPath, communityLabelsOrOptions, figsize = [
5083
5230
  }
5084
5231
  }
5085
5232
  svgParts.push("</svg>");
5086
- writeFileSync3(outputPath, svgParts.join("\n"), "utf-8");
5233
+ writeFileSync4(outputPath, svgParts.join("\n"), "utf-8");
5087
5234
  }
5088
5235
  function toCanvas(G, communities, outputPath, communityLabelsOrOptions, nodeFilenames) {
5089
5236
  const communityMap = toNumericMap(communities);
@@ -5092,7 +5239,7 @@ function toCanvas(G, communities, outputPath, communityLabelsOrOptions, nodeFile
5092
5239
  const providedNodeFilenames = options?.nodeFilenames ?? nodeFilenames;
5093
5240
  const CANVAS_COLORS = ["1", "2", "3", "4", "5", "6"];
5094
5241
  function safeName(label) {
5095
- return label.replace(/[\\/*?:"<>|#^[\]]/g, "").trim() || "unnamed";
5242
+ return label.replace(/\r\n/g, " ").replace(/\r/g, " ").replace(/\n/g, " ").replace(/[\\/*?:"<>|#^[\]]/g, "").trim() || "unnamed";
5096
5243
  }
5097
5244
  let filenameMap;
5098
5245
  if (!providedNodeFilenames) {
@@ -5171,13 +5318,13 @@ function toCanvas(G, communities, outputPath, communityLabelsOrOptions, nodeFile
5171
5318
  for (let idx = 0; idx < sortedCids.length; idx++) {
5172
5319
  const cid = sortedCids[idx];
5173
5320
  const members = communityMap.get(cid) ?? [];
5174
- const communityName = communityLabels?.get(cid) ?? `Community ${cid}`;
5321
+ const communityName2 = communityLabels?.get(cid) ?? `Community ${cid}`;
5175
5322
  const [gx, gy, gw, gh] = groupLayout.get(cid) ?? [0, 0, 600, 400];
5176
5323
  const canvasColor = CANVAS_COLORS[idx % CANVAS_COLORS.length];
5177
5324
  canvasNodes.push({
5178
5325
  id: `g${cid}`,
5179
5326
  type: "group",
5180
- label: communityName,
5327
+ label: communityName2,
5181
5328
  x: gx,
5182
5329
  y: gy,
5183
5330
  width: gw,
@@ -5227,12 +5374,13 @@ function toCanvas(G, communities, outputPath, communityLabelsOrOptions, nodeFile
5227
5374
  });
5228
5375
  }
5229
5376
  const canvasData = { nodes: canvasNodes, edges: canvasEdges };
5230
- writeFileSync3(outputPath, JSON.stringify(canvasData, null, 2), "utf-8");
5377
+ writeFileSync4(outputPath, JSON.stringify(canvasData, null, 2), "utf-8");
5231
5378
  }
5232
5379
  var COMMUNITY_COLORS, MAX_NODES_FOR_VIZ, CONFIDENCE_SCORE_DEFAULTS;
5233
5380
  var init_export = __esm({
5234
5381
  "src/export.ts"() {
5235
5382
  init_security();
5383
+ init_graph();
5236
5384
  init_collections();
5237
5385
  COMMUNITY_COLORS = [
5238
5386
  "#4E79A7",
@@ -5261,8 +5409,8 @@ __export(watch_exports, {
5261
5409
  rebuildCode: () => rebuildCode,
5262
5410
  watch: () => watch
5263
5411
  });
5264
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
5265
- import { resolve as pathResolve2, extname as extname2, basename as basename3 } from "path";
5412
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3 } from "fs";
5413
+ import { resolve as pathResolve2, extname as extname4, basename as basename4 } from "path";
5266
5414
  async function rebuildCode(watchPath, followSymlinks = false) {
5267
5415
  try {
5268
5416
  const { collectFiles: collectFiles2, extractWithDiagnostics: extractWithDiagnostics2 } = await Promise.resolve().then(() => (init_extract(), extract_exports));
@@ -5314,7 +5462,7 @@ async function rebuildCode(watchPath, followSymlinks = false) {
5314
5462
  }
5315
5463
  const questions = suggestQuestions2(G, communities, labels);
5316
5464
  const outDir = pathResolve2(watchPath, "graphify-out");
5317
- mkdirSync3(outDir, { recursive: true });
5465
+ mkdirSync4(outDir, { recursive: true });
5318
5466
  const report = generate2(
5319
5467
  G,
5320
5468
  communities,
@@ -5327,10 +5475,10 @@ async function rebuildCode(watchPath, followSymlinks = false) {
5327
5475
  watchPath,
5328
5476
  questions
5329
5477
  );
5330
- writeFileSync4(pathResolve2(outDir, "GRAPH_REPORT.md"), report, "utf-8");
5331
- toJson2(G, communities, pathResolve2(outDir, "graph.json"));
5478
+ writeFileSync5(pathResolve2(outDir, "GRAPH_REPORT.md"), report, "utf-8");
5479
+ toJson2(G, communities, pathResolve2(outDir, "graph.json"), { communityLabels: labels });
5332
5480
  const flagPath = pathResolve2(outDir, "needs_update");
5333
- if (existsSync5(flagPath)) {
5481
+ if (existsSync6(flagPath)) {
5334
5482
  unlinkSync3(flagPath);
5335
5483
  }
5336
5484
  console.log(
@@ -5349,9 +5497,9 @@ async function rebuildCode(watchPath, followSymlinks = false) {
5349
5497
  }
5350
5498
  function notifyOnly(watchPath) {
5351
5499
  const outDir = pathResolve2(watchPath, "graphify-out");
5352
- mkdirSync3(outDir, { recursive: true });
5500
+ mkdirSync4(outDir, { recursive: true });
5353
5501
  const flagPath = pathResolve2(outDir, "needs_update");
5354
- writeFileSync4(flagPath, "1", "utf-8");
5502
+ writeFileSync5(flagPath, "1", "utf-8");
5355
5503
  console.log(`
5356
5504
  [graphify watch] New or changed files detected in ${watchPath}`);
5357
5505
  console.log(
@@ -5363,7 +5511,7 @@ function notifyOnly(watchPath) {
5363
5511
  console.log(`[graphify watch] Flag written to ${flagPath}`);
5364
5512
  }
5365
5513
  function hasNonCode(changedPaths) {
5366
- return changedPaths.some((p) => !CODE_EXTENSIONS2.has(extname2(p).toLowerCase()));
5514
+ return changedPaths.some((p) => !CODE_EXTENSIONS.has(extname4(p).toLowerCase()));
5367
5515
  }
5368
5516
  async function watch(watchPath, debounce = 3) {
5369
5517
  let chokidar;
@@ -5387,7 +5535,7 @@ async function watch(watchPath, debounce = 3) {
5387
5535
  ]
5388
5536
  });
5389
5537
  watcher.on("all", (_event, filePath) => {
5390
- const ext = extname2(filePath).toLowerCase();
5538
+ const ext = extname4(filePath).toLowerCase();
5391
5539
  if (!WATCHED_EXTENSIONS.has(ext)) return;
5392
5540
  const parts = filePath.split("/");
5393
5541
  if (parts.some((part) => part.startsWith(".") && part !== ".")) return;
@@ -5426,62 +5574,17 @@ async function watch(watchPath, debounce = 3) {
5426
5574
  process.on("SIGINT", cleanup);
5427
5575
  process.on("SIGTERM", cleanup);
5428
5576
  }
5429
- var WATCHED_EXTENSIONS, CODE_EXTENSIONS2, isDirectExecution2;
5577
+ var WATCHED_EXTENSIONS, isDirectExecution2;
5430
5578
  var init_watch = __esm({
5431
5579
  "src/watch.ts"() {
5580
+ init_detect();
5432
5581
  WATCHED_EXTENSIONS = /* @__PURE__ */ new Set([
5433
- ".py",
5434
- ".ts",
5435
- ".js",
5436
- ".go",
5437
- ".rs",
5438
- ".java",
5439
- ".cpp",
5440
- ".c",
5441
- ".rb",
5442
- ".swift",
5443
- ".kt",
5444
- ".cs",
5445
- ".scala",
5446
- ".php",
5447
- ".cc",
5448
- ".cxx",
5449
- ".hpp",
5450
- ".h",
5451
- ".kts",
5452
- ".md",
5453
- ".txt",
5454
- ".rst",
5455
- ".pdf",
5456
- ".png",
5457
- ".jpg",
5458
- ".jpeg",
5459
- ".webp",
5460
- ".gif",
5461
- ".svg"
5462
- ]);
5463
- CODE_EXTENSIONS2 = /* @__PURE__ */ new Set([
5464
- ".py",
5465
- ".ts",
5466
- ".js",
5467
- ".go",
5468
- ".rs",
5469
- ".java",
5470
- ".cpp",
5471
- ".c",
5472
- ".rb",
5473
- ".swift",
5474
- ".kt",
5475
- ".cs",
5476
- ".scala",
5477
- ".php",
5478
- ".cc",
5479
- ".cxx",
5480
- ".hpp",
5481
- ".h",
5482
- ".kts"
5582
+ ...CODE_EXTENSIONS,
5583
+ ...DOC_EXTENSIONS,
5584
+ ...PAPER_EXTENSIONS,
5585
+ ...IMAGE_EXTENSIONS
5483
5586
  ]);
5484
- isDirectExecution2 = typeof process !== "undefined" && typeof process.argv[1] === "string" && /^watch\.(?:js|mjs|cjs|ts)$/.test(basename3(process.argv[1]));
5587
+ isDirectExecution2 = typeof process !== "undefined" && typeof process.argv[1] === "string" && /^watch\.(?:js|mjs|cjs|ts)$/.test(basename4(process.argv[1]));
5485
5588
  if (isDirectExecution2) {
5486
5589
  const watchPath = process.argv[2] ?? ".";
5487
5590
  const debounce = process.argv[3] ? parseFloat(process.argv[3]) : 3;
@@ -5499,8 +5602,7 @@ __export(benchmark_exports, {
5499
5602
  printBenchmark: () => printBenchmark,
5500
5603
  runBenchmark: () => runBenchmark
5501
5604
  });
5502
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
5503
- import Graph3 from "graphology";
5605
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
5504
5606
  function estimateTokens(text) {
5505
5607
  return Math.max(1, Math.floor(text.length / CHARS_PER_TOKEN));
5506
5608
  }
@@ -5521,7 +5623,7 @@ function querySubgraphTokens(G, question, depth = 3) {
5521
5623
  for (let d = 0; d < depth; d++) {
5522
5624
  const nextFrontier = /* @__PURE__ */ new Set();
5523
5625
  for (const n of frontier) {
5524
- G.forEachNeighbor(n, (neighbor) => {
5626
+ forEachTraversalNeighbor(G, n, (neighbor) => {
5525
5627
  if (!visited.has(neighbor)) {
5526
5628
  nextFrontier.add(neighbor);
5527
5629
  edgesSeen.push([n, neighbor]);
@@ -5548,26 +5650,12 @@ function querySubgraphTokens(G, question, depth = 3) {
5548
5650
  return estimateTokens(lines.join("\n"));
5549
5651
  }
5550
5652
  function loadGraph2(graphPath) {
5551
- const raw = JSON.parse(readFileSync5(graphPath, "utf-8"));
5552
- const G = new Graph3({ type: "undirected" });
5553
- for (const node of raw.nodes ?? []) {
5554
- const { id, ...attrs } = node;
5555
- G.mergeNode(id, attrs);
5556
- }
5557
- for (const link of raw.links ?? []) {
5558
- const { source, target, ...attrs } = link;
5559
- if (G.hasNode(source) && G.hasNode(target)) {
5560
- try {
5561
- G.mergeEdge(source, target, attrs);
5562
- } catch {
5563
- }
5564
- }
5565
- }
5566
- return G;
5653
+ const raw = JSON.parse(readFileSync6(graphPath, "utf-8"));
5654
+ return loadGraphFromData(raw);
5567
5655
  }
5568
5656
  function runBenchmark(graphPath = "graphify-out/graph.json", corpusWordsOrOptions, questions) {
5569
5657
  const options = typeof corpusWordsOrOptions === "number" ? { corpusWords: corpusWordsOrOptions, questions } : corpusWordsOrOptions ?? {};
5570
- if (!existsSync6(graphPath)) {
5658
+ if (!existsSync7(graphPath)) {
5571
5659
  return { error: `Graph file not found: ${graphPath}. Build the graph first.` };
5572
5660
  }
5573
5661
  const G = loadGraph2(graphPath);
@@ -5627,6 +5715,7 @@ graphify token reduction benchmark`);
5627
5715
  var CHARS_PER_TOKEN, SAMPLE_QUESTIONS;
5628
5716
  var init_benchmark = __esm({
5629
5717
  "src/benchmark.ts"() {
5718
+ init_graph();
5630
5719
  CHARS_PER_TOKEN = 4;
5631
5720
  SAMPLE_QUESTIONS = [
5632
5721
  "how does authentication work",
@@ -5639,24 +5728,26 @@ var init_benchmark = __esm({
5639
5728
  });
5640
5729
 
5641
5730
  // src/cli.ts
5731
+ init_graph();
5642
5732
  import {
5643
- readFileSync as readFileSync6,
5644
- writeFileSync as writeFileSync5,
5645
- existsSync as existsSync7,
5646
- mkdirSync as mkdirSync4,
5647
- copyFileSync,
5733
+ readFileSync as readFileSync7,
5734
+ writeFileSync as writeFileSync6,
5735
+ existsSync as existsSync8,
5736
+ mkdirSync as mkdirSync5,
5648
5737
  realpathSync as realpathSync2,
5649
- statSync
5738
+ statSync as statSync2,
5739
+ unlinkSync as unlinkSync4,
5740
+ rmdirSync
5650
5741
  } from "fs";
5651
- import { join as join4, resolve as resolve5, dirname as dirname2 } from "path";
5742
+ import { join as join5, resolve as resolve8, dirname as dirname4 } from "path";
5652
5743
  import { homedir, platform } from "os";
5653
5744
  import { fileURLToPath } from "url";
5654
5745
  import { Command } from "commander";
5655
5746
  var __filename = fileURLToPath(import.meta.url);
5656
- var __dirname = dirname2(__filename);
5747
+ var __dirname = dirname4(__filename);
5657
5748
  function getVersion() {
5658
5749
  try {
5659
- const pkg = JSON.parse(readFileSync6(join4(__dirname, "..", "package.json"), "utf-8"));
5750
+ const pkg = JSON.parse(readFileSync7(join5(__dirname, "..", "package.json"), "utf-8"));
5660
5751
  return pkg.version ?? "unknown";
5661
5752
  } catch {
5662
5753
  return "unknown";
@@ -5666,42 +5757,57 @@ var VERSION = getVersion();
5666
5757
  var PLATFORM_CONFIG = {
5667
5758
  claude: {
5668
5759
  skill_file: "skill.md",
5669
- skill_dst: join4(".claude", "skills", "graphify", "SKILL.md"),
5760
+ skill_dst: join5(".claude", "skills", "graphify", "SKILL.md"),
5670
5761
  claude_md: true
5671
5762
  },
5672
5763
  codex: {
5673
5764
  skill_file: "skill-codex.md",
5674
- skill_dst: join4(".agents", "skills", "graphify", "SKILL.md"),
5765
+ skill_dst: join5(".agents", "skills", "graphify", "SKILL.md"),
5766
+ claude_md: false
5767
+ },
5768
+ gemini: {
5769
+ skill_file: "skill-gemini.toml",
5770
+ skill_dst: join5(".gemini", "commands", "graphify.toml"),
5675
5771
  claude_md: false
5676
5772
  },
5677
5773
  opencode: {
5678
5774
  skill_file: "skill-opencode.md",
5679
- skill_dst: join4(".config", "opencode", "skills", "graphify", "SKILL.md"),
5775
+ skill_dst: join5(".config", "opencode", "skills", "graphify", "SKILL.md"),
5776
+ claude_md: false
5777
+ },
5778
+ aider: {
5779
+ skill_file: "skill.md",
5780
+ skill_dst: join5(".aider", "graphify", "SKILL.md"),
5781
+ claude_md: false
5782
+ },
5783
+ copilot: {
5784
+ skill_file: "skill.md",
5785
+ skill_dst: join5(".copilot", "skills", "graphify", "SKILL.md"),
5680
5786
  claude_md: false
5681
5787
  },
5682
5788
  claw: {
5683
5789
  skill_file: "skill-claw.md",
5684
- skill_dst: join4(".claw", "skills", "graphify", "SKILL.md"),
5790
+ skill_dst: join5(".claw", "skills", "graphify", "SKILL.md"),
5685
5791
  claude_md: false
5686
5792
  },
5687
5793
  droid: {
5688
5794
  skill_file: "skill-droid.md",
5689
- skill_dst: join4(".factory", "skills", "graphify", "SKILL.md"),
5795
+ skill_dst: join5(".factory", "skills", "graphify", "SKILL.md"),
5690
5796
  claude_md: false
5691
5797
  },
5692
5798
  trae: {
5693
5799
  skill_file: "skill-trae.md",
5694
- skill_dst: join4(".trae", "skills", "graphify", "SKILL.md"),
5800
+ skill_dst: join5(".trae", "skills", "graphify", "SKILL.md"),
5695
5801
  claude_md: false
5696
5802
  },
5697
5803
  "trae-cn": {
5698
5804
  skill_file: "skill-trae.md",
5699
- skill_dst: join4(".trae-cn", "skills", "graphify", "SKILL.md"),
5805
+ skill_dst: join5(".trae-cn", "skills", "graphify", "SKILL.md"),
5700
5806
  claude_md: false
5701
5807
  },
5702
5808
  windows: {
5703
5809
  skill_file: "skill-windows.md",
5704
- skill_dst: join4(".claude", "skills", "graphify", "SKILL.md"),
5810
+ skill_dst: join5(".claude", "skills", "graphify", "SKILL.md"),
5705
5811
  claude_md: true
5706
5812
  }
5707
5813
  };
@@ -5724,18 +5830,233 @@ Rules:
5724
5830
  - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
5725
5831
  - After modifying code files in this session, run \`npx graphify hook-rebuild\` to keep the graph current
5726
5832
  `;
5833
+ var GEMINI_MD_SECTION = `## graphify
5834
+
5835
+ This project has a graphify knowledge graph at graphify-out/.
5836
+
5837
+ Rules:
5838
+ - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
5839
+ - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
5840
+ - In Gemini CLI, the reliable explicit custom command is \`/graphify ...\`
5841
+ - If the user asks to build, update, query, path, or explain the graph, use the installed \`/graphify\` custom command or the configured \`graphify\` MCP server instead of ad-hoc file traversal
5842
+ - After modifying code files in this session, run \`npx graphify hook-rebuild\` to keep the graph current
5843
+ `;
5844
+ var GEMINI_MCP_SERVER = {
5845
+ command: "graphify",
5846
+ args: ["serve", "graphify-out/graph.json"],
5847
+ trust: false,
5848
+ description: "graphify knowledge graph MCP server"
5849
+ };
5850
+ var OPENCODE_PLUGIN_ENTRY = ".opencode/plugins/graphify.js";
5851
+ var OPENCODE_PLUGIN_JS = `// graphify OpenCode plugin
5852
+ // Injects a knowledge graph reminder before bash tool calls when the graph exists.
5853
+ import { existsSync } from "fs";
5854
+ import { join } from "path";
5855
+
5856
+ export const GraphifyPlugin = async ({ directory }) => {
5857
+ let reminded = false;
5858
+
5859
+ return {
5860
+ "tool.execute.before": async (input, output) => {
5861
+ if (reminded) return;
5862
+ if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
5863
+
5864
+ if (input.tool === "bash") {
5865
+ output.args.command =
5866
+ 'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
5867
+ output.args.command;
5868
+ reminded = true;
5869
+ }
5870
+ },
5871
+ };
5872
+ };
5873
+ `;
5727
5874
  var MD_MARKER = "## graphify";
5875
+ var CURSOR_RULE = `---
5876
+ description: graphify knowledge graph context
5877
+ alwaysApply: true
5878
+ ---
5879
+
5880
+ This project has a graphify knowledge graph at graphify-out/.
5881
+
5882
+ - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
5883
+ - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
5884
+ - After modifying code files in this session, run \`npx graphify hook-rebuild\` to keep the graph current
5885
+ `;
5886
+ var AIDER_SEMANTIC_SECTION = `#### Part B - Semantic extraction (sequential extraction on Aider)
5887
+
5888
+ **Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic extraction to do.
5889
+
5890
+ > **Aider platform:** Multi-agent support is still early on Aider. Extraction runs sequentially - read and extract each uncached file yourself instead of dispatching parallel Agent calls.
5891
+
5892
+ Print: \`Semantic extraction: N files (sequential - Aider)\`
5893
+
5894
+ **Step B0 - Check extraction cache first**
5895
+
5896
+ Before reading any docs, papers, or images, check which files already have cached semantic extraction results:
5897
+
5898
+ \`\`\`bash
5899
+ node -e "
5900
+ const fs = require('fs');
5901
+ const { checkSemanticCache } = require('graphifyy');
5902
+
5903
+ const detect = JSON.parse(fs.readFileSync('graphify-out/.graphify_detect.json', 'utf-8'));
5904
+ const allFiles = Object.values(detect.files).flat();
5905
+
5906
+ const [cachedNodes, cachedEdges, cachedHyperedges, uncached] = checkSemanticCache(allFiles);
5907
+
5908
+ if (cachedNodes.length || cachedEdges.length || cachedHyperedges.length) {
5909
+ fs.writeFileSync('graphify-out/.graphify_cached.json', JSON.stringify({nodes: cachedNodes, edges: cachedEdges, hyperedges: cachedHyperedges}));
5910
+ }
5911
+ fs.writeFileSync('graphify-out/.graphify_uncached.txt', uncached.join('\\n'));
5912
+ console.log(\`Cache: \${allFiles.length - uncached.length} files hit, \${uncached.length} files need extraction\`);
5913
+ "
5914
+ \`\`\`
5915
+
5916
+ Only extract files listed in \`graphify-out/.graphify_uncached.txt\`. If all files are cached, skip to Part C directly.
5917
+
5918
+ **Step B1 - Split into chunks**
5919
+
5920
+ Load files from \`graphify-out/.graphify_uncached.txt\`. Split them into logical batches of 20-25 files, but process them sequentially on Aider. Keep files from the same directory together. Each image still deserves focused attention because vision context is expensive.
5921
+
5922
+ **Step B2 - Sequential extraction (Aider)**
5923
+
5924
+ Process each uncached file one at a time. For each file:
5925
+
5926
+ 1. Read the file contents.
5927
+ 2. Extract nodes, edges, and hyperedges using the same graphify rules:
5928
+ - EXTRACTED: relationship explicit in source (import, call, citation, "see section 3.2")
5929
+ - INFERRED: reasonable inference (shared structure, implied dependency)
5930
+ - AMBIGUOUS: uncertain - flag it instead of omitting it
5931
+ - Code files: only add semantic edges AST cannot find. Do not re-extract imports.
5932
+ - Doc/paper files: extract named concepts, entities, citations, and rationale nodes (WHY decisions were made -> \`rationale_for\` edges)
5933
+ - Image files: use vision to understand what the image is, not just OCR
5934
+ - If \`--mode deep\` was given, be more aggressive with INFERRED edges
5935
+ - Add \`semantically_similar_to\` only for genuinely non-obvious cross-cutting similarities
5936
+ - Add hyperedges only when 3+ nodes clearly participate in one shared concept or flow
5937
+ - \`confidence_score\` is REQUIRED on every edge: EXTRACTED=1.0, INFERRED=0.6-0.9, AMBIGUOUS=0.1-0.3
5938
+ 3. Accumulate the results across all files.
5939
+
5940
+ Write the accumulated result to \`graphify-out/.graphify_semantic_new.json\` using this exact schema:
5941
+
5942
+ \`\`\`json
5943
+ {"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0}
5944
+ \`\`\`
5945
+
5946
+ **Step B3 - Cache and merge**
5947
+
5948
+ If more than half the sequential batches failed, stop and tell the user.
5949
+
5950
+ Save new results to cache:
5951
+
5952
+ \`\`\`bash
5953
+ node -e "
5954
+ const fs = require('fs');
5955
+ const { saveSemanticCache } = require('graphifyy');
5956
+
5957
+ const raw = fs.existsSync('graphify-out/.graphify_semantic_new.json') ? JSON.parse(fs.readFileSync('graphify-out/.graphify_semantic_new.json', 'utf-8')) : {nodes:[],edges:[],hyperedges:[]};
5958
+ const saved = saveSemanticCache(raw.nodes || [], raw.edges || [], raw.hyperedges || []);
5959
+ console.log(\`Cached \${saved} files\`);
5960
+ "
5961
+ \`\`\`
5962
+
5963
+ Merge cached + new results into \`graphify-out/.graphify_semantic.json\`:
5964
+
5965
+ \`\`\`bash
5966
+ node -e "
5967
+ const fs = require('fs');
5968
+
5969
+ const cached = fs.existsSync('graphify-out/.graphify_cached.json') ? JSON.parse(fs.readFileSync('graphify-out/.graphify_cached.json', 'utf-8')) : {nodes:[],edges:[],hyperedges:[]};
5970
+ const fresh = fs.existsSync('graphify-out/.graphify_semantic_new.json') ? JSON.parse(fs.readFileSync('graphify-out/.graphify_semantic_new.json', 'utf-8')) : {nodes:[],edges:[],hyperedges:[]};
5971
+
5972
+ const allNodes = [...cached.nodes, ...(fresh.nodes || [])];
5973
+ const allEdges = [...cached.edges, ...(fresh.edges || [])];
5974
+ const allHyperedges = [...(cached.hyperedges || []), ...(fresh.hyperedges || [])];
5975
+
5976
+ const seen = new Set();
5977
+ const dedupedNodes = [];
5978
+ for (const node of allNodes) {
5979
+ if (seen.has(node.id)) continue;
5980
+ seen.add(node.id);
5981
+ dedupedNodes.push(node);
5982
+ }
5983
+
5984
+ fs.writeFileSync('graphify-out/.graphify_semantic.json', JSON.stringify({
5985
+ nodes: dedupedNodes,
5986
+ edges: allEdges,
5987
+ hyperedges: allHyperedges,
5988
+ input_tokens: fresh.input_tokens || 0,
5989
+ output_tokens: fresh.output_tokens || 0
5990
+ }, null, 2));
5991
+
5992
+ console.log(\`Extraction complete - \${dedupedNodes.length} nodes, \${allEdges.length} edges (\${cached.nodes.length} from cache, \${(fresh.nodes || []).length} new)\`);
5993
+ "
5994
+ \`\`\`
5995
+
5996
+ Clean up temp files: \`rm -f graphify-out/.graphify_cached.json graphify-out/.graphify_uncached.txt graphify-out/.graphify_semantic_new.json\``;
5728
5997
  function findSkillFile(filename) {
5729
5998
  const paths = [
5730
- join4(__dirname, "..", "src", "skills", filename),
5731
- join4(__dirname, "skills", filename),
5732
- join4(__dirname, "..", "skills", filename)
5999
+ join5(__dirname, "..", "src", "skills", filename),
6000
+ join5(__dirname, "skills", filename),
6001
+ join5(__dirname, "..", "skills", filename)
5733
6002
  ];
5734
6003
  for (const p of paths) {
5735
- if (existsSync7(p)) return p;
6004
+ if (existsSync8(p)) return p;
5736
6005
  }
5737
6006
  return null;
5738
6007
  }
6008
+ function renderAiderSkill(baseSkill) {
6009
+ return baseSkill.replace(
6010
+ /#### Part B - Semantic extraction \(parallel subagents\)[\s\S]*?(?=\n#### Part C - Merge AST \+ semantic into final extraction)/,
6011
+ AIDER_SEMANTIC_SECTION
6012
+ );
6013
+ }
6014
+ function loadSkillContent(platformName) {
6015
+ const cfg = PLATFORM_CONFIG[platformName];
6016
+ if (!cfg) {
6017
+ console.error(`error: unknown platform '${platformName}'. Choose from: ${Object.keys(PLATFORM_CONFIG).join(", ")}`);
6018
+ process.exit(1);
6019
+ }
6020
+ const skillSrc = findSkillFile(cfg.skill_file);
6021
+ if (!skillSrc) {
6022
+ console.error(`error: ${cfg.skill_file} not found in package - reinstall graphify`);
6023
+ process.exit(1);
6024
+ }
6025
+ const baseSkill = readFileSync7(skillSrc, "utf-8");
6026
+ if (platformName === "aider") {
6027
+ const rendered = renderAiderSkill(baseSkill);
6028
+ if (rendered === baseSkill) {
6029
+ throw new Error("failed to render Aider skill overrides");
6030
+ }
6031
+ return rendered;
6032
+ }
6033
+ return baseSkill;
6034
+ }
6035
+ function uninstallSkill(platformName) {
6036
+ const cfg = PLATFORM_CONFIG[platformName];
6037
+ if (!cfg) {
6038
+ console.error(`error: unknown platform '${platformName}'. Choose from: ${Object.keys(PLATFORM_CONFIG).join(", ")}`);
6039
+ process.exit(1);
6040
+ }
6041
+ const skillDst = join5(homedir(), cfg.skill_dst);
6042
+ const removed = [];
6043
+ if (existsSync8(skillDst)) {
6044
+ unlinkSync4(skillDst);
6045
+ removed.push(`skill removed: ${skillDst}`);
6046
+ }
6047
+ const versionFile = join5(dirname4(skillDst), ".graphify_version");
6048
+ if (existsSync8(versionFile)) {
6049
+ unlinkSync4(versionFile);
6050
+ }
6051
+ for (let dir = dirname4(skillDst); dir !== dirname4(dir); dir = dirname4(dir)) {
6052
+ try {
6053
+ rmdirSync(dir);
6054
+ } catch {
6055
+ break;
6056
+ }
6057
+ }
6058
+ console.log(removed.length > 0 ? removed.join("; ") : "nothing to remove");
6059
+ }
5739
6060
  function getInvocationExample(platformName) {
5740
6061
  return platformName === "codex" ? "$graphify ." : "/graphify .";
5741
6062
  }
@@ -5762,35 +6083,154 @@ function getAgentsMdSection(platformName) {
5762
6083
  }
5763
6084
  return lines.join("\n") + "\n";
5764
6085
  }
6086
+ function installGeminiMcp(projectDir) {
6087
+ const geminiDir = join5(projectDir, ".gemini");
6088
+ if (existsSync8(geminiDir) && !statSync2(geminiDir).isDirectory()) {
6089
+ console.log(" .gemini/settings.json -> skipped (cannot create config dir because .gemini is a file)");
6090
+ return;
6091
+ }
6092
+ const settingsPath = join5(geminiDir, "settings.json");
6093
+ mkdirSync5(dirname4(settingsPath), { recursive: true });
6094
+ let settings = {};
6095
+ if (existsSync8(settingsPath)) {
6096
+ try {
6097
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6098
+ } catch {
6099
+ }
6100
+ }
6101
+ const mcpServers = settings.mcpServers ?? {};
6102
+ const existing = mcpServers.graphify;
6103
+ if (JSON.stringify(existing) === JSON.stringify(GEMINI_MCP_SERVER)) {
6104
+ console.log(" .gemini/settings.json -> graphify MCP already registered (no change)");
6105
+ return;
6106
+ }
6107
+ mcpServers.graphify = GEMINI_MCP_SERVER;
6108
+ settings.mcpServers = mcpServers;
6109
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
6110
+ console.log(" .gemini/settings.json -> graphify MCP server registered");
6111
+ }
6112
+ function uninstallGeminiMcp(projectDir) {
6113
+ const settingsPath = join5(projectDir, ".gemini", "settings.json");
6114
+ if (!existsSync8(settingsPath)) return;
6115
+ let settings;
6116
+ try {
6117
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6118
+ } catch {
6119
+ return;
6120
+ }
6121
+ const mcpServers = { ...settings.mcpServers ?? {} };
6122
+ if (!("graphify" in mcpServers)) return;
6123
+ delete mcpServers.graphify;
6124
+ if (Object.keys(mcpServers).length === 0) {
6125
+ delete settings.mcpServers;
6126
+ } else {
6127
+ settings.mcpServers = mcpServers;
6128
+ }
6129
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
6130
+ console.log(" .gemini/settings.json -> graphify MCP server removed");
6131
+ }
6132
+ function cursorInstall(projectDir = ".") {
6133
+ const rulePath = join5(projectDir, ".cursor", "rules", "graphify.mdc");
6134
+ mkdirSync5(dirname4(rulePath), { recursive: true });
6135
+ if (existsSync8(rulePath)) {
6136
+ console.log(`graphify rule already exists at ${resolve8(rulePath)} (no change)`);
6137
+ } else {
6138
+ writeFileSync6(rulePath, CURSOR_RULE, "utf-8");
6139
+ console.log(`graphify rule written to ${resolve8(rulePath)}`);
6140
+ }
6141
+ console.log();
6142
+ console.log("Cursor will now always include the knowledge graph context.");
6143
+ console.log("Run `$graphify .` or `/graphify .` in your assistant first if you have not built the graph yet.");
6144
+ }
6145
+ function cursorUninstall(projectDir = ".") {
6146
+ const rulePath = join5(projectDir, ".cursor", "rules", "graphify.mdc");
6147
+ if (!existsSync8(rulePath)) {
6148
+ console.log("No graphify Cursor rule found - nothing to do");
6149
+ return;
6150
+ }
6151
+ const { unlinkSync: unlinkSync5 } = __require("fs");
6152
+ unlinkSync5(rulePath);
6153
+ console.log(`graphify Cursor rule removed from ${resolve8(rulePath)}`);
6154
+ }
6155
+ function installOpenCodePlugin(projectDir) {
6156
+ const opencodeDir = join5(projectDir, ".opencode");
6157
+ if (existsSync8(opencodeDir) && !statSync2(opencodeDir).isDirectory()) {
6158
+ console.log(` ${OPENCODE_PLUGIN_ENTRY} -> skipped (cannot create plugin dir because .opencode is a file)`);
6159
+ return;
6160
+ }
6161
+ const pluginPath = join5(projectDir, ".opencode", "plugins", "graphify.js");
6162
+ mkdirSync5(dirname4(pluginPath), { recursive: true });
6163
+ writeFileSync6(pluginPath, OPENCODE_PLUGIN_JS, "utf-8");
6164
+ console.log(` ${OPENCODE_PLUGIN_ENTRY} -> tool.execute.before hook written`);
6165
+ const configPath = join5(projectDir, "opencode.json");
6166
+ let config = {};
6167
+ if (existsSync8(configPath)) {
6168
+ try {
6169
+ config = JSON.parse(readFileSync7(configPath, "utf-8"));
6170
+ } catch {
6171
+ config = {};
6172
+ }
6173
+ }
6174
+ const plugins = Array.isArray(config.plugin) ? [...config.plugin] : [];
6175
+ if (plugins.includes(OPENCODE_PLUGIN_ENTRY)) {
6176
+ console.log(" opencode.json -> plugin already registered (no change)");
6177
+ return;
6178
+ }
6179
+ plugins.push(OPENCODE_PLUGIN_ENTRY);
6180
+ config.plugin = plugins;
6181
+ writeFileSync6(configPath, JSON.stringify(config, null, 2), "utf-8");
6182
+ console.log(" opencode.json -> plugin registered");
6183
+ }
6184
+ function uninstallOpenCodePlugin(projectDir) {
6185
+ const pluginPath = join5(projectDir, ".opencode", "plugins", "graphify.js");
6186
+ if (existsSync8(pluginPath)) {
6187
+ const { unlinkSync: unlinkSync5 } = __require("fs");
6188
+ unlinkSync5(pluginPath);
6189
+ console.log(` ${OPENCODE_PLUGIN_ENTRY} -> removed`);
6190
+ }
6191
+ const configPath = join5(projectDir, "opencode.json");
6192
+ if (!existsSync8(configPath)) return;
6193
+ let config;
6194
+ try {
6195
+ config = JSON.parse(readFileSync7(configPath, "utf-8"));
6196
+ } catch {
6197
+ return;
6198
+ }
6199
+ const plugins = Array.isArray(config.plugin) ? [...config.plugin] : [];
6200
+ if (!plugins.includes(OPENCODE_PLUGIN_ENTRY)) return;
6201
+ const filtered = plugins.filter((entry) => entry !== OPENCODE_PLUGIN_ENTRY);
6202
+ if (filtered.length === 0) {
6203
+ delete config.plugin;
6204
+ } else {
6205
+ config.plugin = filtered;
6206
+ }
6207
+ writeFileSync6(configPath, JSON.stringify(config, null, 2), "utf-8");
6208
+ console.log(" opencode.json -> plugin deregistered");
6209
+ }
5765
6210
  function installSkill(platformName) {
5766
6211
  const cfg = PLATFORM_CONFIG[platformName];
5767
6212
  if (!cfg) {
5768
6213
  console.error(`error: unknown platform '${platformName}'. Choose from: ${Object.keys(PLATFORM_CONFIG).join(", ")}`);
5769
6214
  process.exit(1);
5770
6215
  }
5771
- const skillSrc = findSkillFile(cfg.skill_file);
5772
- if (!skillSrc) {
5773
- console.error(`error: ${cfg.skill_file} not found in package - reinstall graphify`);
5774
- process.exit(1);
5775
- }
5776
- const skillDst = join4(homedir(), cfg.skill_dst);
5777
- mkdirSync4(dirname2(skillDst), { recursive: true });
5778
- copyFileSync(skillSrc, skillDst);
5779
- writeFileSync5(join4(dirname2(skillDst), ".graphify_version"), VERSION, "utf-8");
6216
+ const skillDst = join5(homedir(), cfg.skill_dst);
6217
+ mkdirSync5(dirname4(skillDst), { recursive: true });
6218
+ writeFileSync6(skillDst, loadSkillContent(platformName), "utf-8");
6219
+ writeFileSync6(join5(dirname4(skillDst), ".graphify_version"), VERSION, "utf-8");
5780
6220
  console.log(` skill installed -> ${skillDst}`);
5781
6221
  if (cfg.claude_md) {
5782
- const claudeMd = join4(homedir(), ".claude", "CLAUDE.md");
5783
- if (existsSync7(claudeMd)) {
5784
- const content = readFileSync6(claudeMd, "utf-8");
6222
+ const claudeMd = join5(homedir(), ".claude", "CLAUDE.md");
6223
+ if (existsSync8(claudeMd)) {
6224
+ const content = readFileSync7(claudeMd, "utf-8");
5785
6225
  if (content.includes("graphify")) {
5786
6226
  console.log(` CLAUDE.md -> already registered (no change)`);
5787
6227
  } else {
5788
- writeFileSync5(claudeMd, content.trimEnd() + SKILL_REGISTRATION, "utf-8");
6228
+ writeFileSync6(claudeMd, content.trimEnd() + SKILL_REGISTRATION, "utf-8");
5789
6229
  console.log(` CLAUDE.md -> skill registered in ${claudeMd}`);
5790
6230
  }
5791
6231
  } else {
5792
- mkdirSync4(dirname2(claudeMd), { recursive: true });
5793
- writeFileSync5(claudeMd, SKILL_REGISTRATION.trimStart(), "utf-8");
6232
+ mkdirSync5(dirname4(claudeMd), { recursive: true });
6233
+ writeFileSync6(claudeMd, SKILL_REGISTRATION.trimStart(), "utf-8");
5794
6234
  console.log(` CLAUDE.md -> created at ${claudeMd}`);
5795
6235
  }
5796
6236
  }
@@ -5808,33 +6248,32 @@ function installSkill(platformName) {
5808
6248
  console.log();
5809
6249
  }
5810
6250
  function installClaudeHook(projectDir) {
5811
- const settingsPath = join4(projectDir, ".claude", "settings.json");
5812
- mkdirSync4(dirname2(settingsPath), { recursive: true });
6251
+ const settingsPath = join5(projectDir, ".claude", "settings.json");
6252
+ mkdirSync5(dirname4(settingsPath), { recursive: true });
5813
6253
  let settings = {};
5814
- if (existsSync7(settingsPath)) {
6254
+ if (existsSync8(settingsPath)) {
5815
6255
  try {
5816
- settings = JSON.parse(readFileSync6(settingsPath, "utf-8"));
6256
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
5817
6257
  } catch {
5818
6258
  }
5819
6259
  }
5820
6260
  const hooks = settings.hooks ?? {};
5821
6261
  const preTool = hooks.PreToolUse ?? [];
5822
- if (preTool.some((h) => h.matcher === "Glob|Grep" && JSON.stringify(h).includes("graphify"))) {
5823
- console.log(` .claude/settings.json -> hook already registered (no change)`);
5824
- return;
5825
- }
5826
- preTool.push(SETTINGS_HOOK);
5827
- hooks.PreToolUse = preTool;
6262
+ const filtered = preTool.filter(
6263
+ (h) => !(h.matcher === "Glob|Grep" && JSON.stringify(h).includes("graphify"))
6264
+ );
6265
+ filtered.push(SETTINGS_HOOK);
6266
+ hooks.PreToolUse = filtered;
5828
6267
  settings.hooks = hooks;
5829
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
6268
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
5830
6269
  console.log(` .claude/settings.json -> PreToolUse hook registered`);
5831
6270
  }
5832
6271
  function uninstallClaudeHook(projectDir) {
5833
- const settingsPath = join4(projectDir, ".claude", "settings.json");
5834
- if (!existsSync7(settingsPath)) return;
6272
+ const settingsPath = join5(projectDir, ".claude", "settings.json");
6273
+ if (!existsSync8(settingsPath)) return;
5835
6274
  let settings;
5836
6275
  try {
5837
- settings = JSON.parse(readFileSync6(settingsPath, "utf-8"));
6276
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
5838
6277
  } catch {
5839
6278
  return;
5840
6279
  }
@@ -5846,25 +6285,25 @@ function uninstallClaudeHook(projectDir) {
5846
6285
  if (filtered.length === preTool.length) return;
5847
6286
  hooks.PreToolUse = filtered;
5848
6287
  settings.hooks = hooks;
5849
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
6288
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
5850
6289
  console.log(` .claude/settings.json -> PreToolUse hook removed`);
5851
6290
  }
5852
6291
  function claudeInstall(projectDir = ".") {
5853
6292
  let alreadyConfigured = false;
5854
- const target = join4(projectDir, "CLAUDE.md");
5855
- if (existsSync7(target)) {
5856
- const content = readFileSync6(target, "utf-8");
6293
+ const target = join5(projectDir, "CLAUDE.md");
6294
+ if (existsSync8(target)) {
6295
+ const content = readFileSync7(target, "utf-8");
5857
6296
  if (content.includes(MD_MARKER)) {
5858
6297
  alreadyConfigured = true;
5859
6298
  console.log("graphify already configured in CLAUDE.md");
5860
6299
  } else {
5861
- writeFileSync5(target, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION, "utf-8");
6300
+ writeFileSync6(target, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION, "utf-8");
5862
6301
  }
5863
6302
  } else {
5864
- writeFileSync5(target, CLAUDE_MD_SECTION, "utf-8");
6303
+ writeFileSync6(target, CLAUDE_MD_SECTION, "utf-8");
5865
6304
  }
5866
6305
  if (!alreadyConfigured) {
5867
- console.log(`graphify section written to ${resolve5(target)}`);
6306
+ console.log(`graphify section written to ${resolve8(target)}`);
5868
6307
  }
5869
6308
  installClaudeHook(projectDir);
5870
6309
  console.log();
@@ -5872,68 +6311,112 @@ function claudeInstall(projectDir = ".") {
5872
6311
  console.log("codebase questions and rebuild it after code changes.");
5873
6312
  }
5874
6313
  function claudeUninstall(projectDir = ".") {
5875
- const target = join4(projectDir, "CLAUDE.md");
5876
- if (!existsSync7(target)) {
6314
+ const target = join5(projectDir, "CLAUDE.md");
6315
+ if (!existsSync8(target)) {
5877
6316
  console.log("No CLAUDE.md found in current directory - nothing to do");
5878
6317
  return;
5879
6318
  }
5880
- const content = readFileSync6(target, "utf-8");
6319
+ const content = readFileSync7(target, "utf-8");
5881
6320
  if (!content.includes(MD_MARKER)) {
5882
6321
  console.log("graphify section not found in CLAUDE.md - nothing to do");
5883
6322
  return;
5884
6323
  }
5885
6324
  const cleaned = content.replace(/\n*## graphify\n[\s\S]*?(?=\n## |\s*$)/, "").trim();
5886
6325
  if (cleaned) {
5887
- writeFileSync5(target, cleaned + "\n", "utf-8");
5888
- console.log(`graphify section removed from ${resolve5(target)}`);
6326
+ writeFileSync6(target, cleaned + "\n", "utf-8");
6327
+ console.log(`graphify section removed from ${resolve8(target)}`);
5889
6328
  } else {
5890
- const { unlinkSync: unlinkSync4 } = __require("fs");
5891
- unlinkSync4(target);
5892
- console.log(`CLAUDE.md was empty after removal - deleted ${resolve5(target)}`);
6329
+ const { unlinkSync: unlinkSync5 } = __require("fs");
6330
+ unlinkSync5(target);
6331
+ console.log(`CLAUDE.md was empty after removal - deleted ${resolve8(target)}`);
5893
6332
  }
5894
6333
  uninstallClaudeHook(projectDir);
5895
6334
  }
6335
+ function geminiInstall(projectDir = ".") {
6336
+ let alreadyConfigured = false;
6337
+ const target = join5(projectDir, "GEMINI.md");
6338
+ if (existsSync8(target)) {
6339
+ const content = readFileSync7(target, "utf-8");
6340
+ if (content.includes(MD_MARKER)) {
6341
+ alreadyConfigured = true;
6342
+ console.log("graphify already configured in GEMINI.md");
6343
+ } else {
6344
+ writeFileSync6(target, content.trimEnd() + "\n\n" + GEMINI_MD_SECTION, "utf-8");
6345
+ }
6346
+ } else {
6347
+ writeFileSync6(target, GEMINI_MD_SECTION, "utf-8");
6348
+ }
6349
+ if (!alreadyConfigured) {
6350
+ console.log(`graphify section written to ${resolve8(target)}`);
6351
+ }
6352
+ installGeminiMcp(projectDir);
6353
+ console.log();
6354
+ console.log("Gemini CLI will now check the knowledge graph before answering");
6355
+ console.log("codebase questions and can access graphify via the configured MCP server.");
6356
+ console.log();
6357
+ console.log("Note: install the `/graphify` custom command globally with");
6358
+ console.log("`graphify install --platform gemini` if you have not done that yet.");
6359
+ }
6360
+ function geminiUninstall(projectDir = ".") {
6361
+ const target = join5(projectDir, "GEMINI.md");
6362
+ if (!existsSync8(target)) {
6363
+ console.log("No GEMINI.md found in current directory - nothing to do");
6364
+ } else {
6365
+ const content = readFileSync7(target, "utf-8");
6366
+ if (!content.includes(MD_MARKER)) {
6367
+ console.log("graphify section not found in GEMINI.md - nothing to do");
6368
+ } else {
6369
+ const cleaned = content.replace(/\n*## graphify\n[\s\S]*?(?=\n## |\s*$)/, "").trim();
6370
+ if (cleaned) {
6371
+ writeFileSync6(target, cleaned + "\n", "utf-8");
6372
+ console.log(`graphify section removed from ${resolve8(target)}`);
6373
+ } else {
6374
+ const { unlinkSync: unlinkSync5 } = __require("fs");
6375
+ unlinkSync5(target);
6376
+ console.log(`GEMINI.md was empty after removal - deleted ${resolve8(target)}`);
6377
+ }
6378
+ }
6379
+ }
6380
+ uninstallGeminiMcp(projectDir);
6381
+ }
5896
6382
  function installCodexHook(projectDir) {
5897
- const hooksDir = join4(projectDir, ".codex");
5898
- if (existsSync7(hooksDir) && !statSync(hooksDir).isDirectory()) {
6383
+ const hooksDir = join5(projectDir, ".codex");
6384
+ if (existsSync8(hooksDir) && !statSync2(hooksDir).isDirectory()) {
5899
6385
  console.log(" .codex/hooks.json -> skipped (cannot create hook dir because .codex is a file)");
5900
6386
  return;
5901
6387
  }
5902
- const hooksPath = join4(hooksDir, "hooks.json");
5903
- mkdirSync4(hooksDir, { recursive: true });
6388
+ const hooksPath = join5(hooksDir, "hooks.json");
6389
+ mkdirSync5(hooksDir, { recursive: true });
5904
6390
  let existing = {};
5905
- if (existsSync7(hooksPath)) {
6391
+ if (existsSync8(hooksPath)) {
5906
6392
  try {
5907
- existing = JSON.parse(readFileSync6(hooksPath, "utf-8"));
6393
+ existing = JSON.parse(readFileSync7(hooksPath, "utf-8"));
5908
6394
  } catch {
5909
6395
  }
5910
6396
  }
5911
6397
  const hooks = existing.hooks ?? {};
5912
6398
  const preTool = hooks.PreToolUse ?? [];
5913
- if (preTool.some((h) => JSON.stringify(h).includes("graphify"))) {
5914
- console.log(` .codex/hooks.json -> hook already registered (no change)`);
5915
- return;
5916
- }
5917
- preTool.push({
6399
+ const filtered = preTool.filter((h) => !JSON.stringify(h).includes("graphify"));
6400
+ filtered.push({
5918
6401
  matcher: "Bash",
5919
6402
  hooks: [
5920
6403
  {
5921
6404
  type: "command",
5922
- command: `[ -f graphify-out/graph.json ] && echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}}' || true`
6405
+ command: `[ -f graphify-out/graph.json ] && echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"},"systemMessage":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}' || true`
5923
6406
  }
5924
6407
  ]
5925
6408
  });
5926
- hooks.PreToolUse = preTool;
6409
+ hooks.PreToolUse = filtered;
5927
6410
  existing.hooks = hooks;
5928
- writeFileSync5(hooksPath, JSON.stringify(existing, null, 2), "utf-8");
6411
+ writeFileSync6(hooksPath, JSON.stringify(existing, null, 2), "utf-8");
5929
6412
  console.log(` .codex/hooks.json -> PreToolUse hook registered`);
5930
6413
  }
5931
6414
  function uninstallCodexHook(projectDir) {
5932
- const hooksPath = join4(projectDir, ".codex", "hooks.json");
5933
- if (!existsSync7(hooksPath)) return;
6415
+ const hooksPath = join5(projectDir, ".codex", "hooks.json");
6416
+ if (!existsSync8(hooksPath)) return;
5934
6417
  let existing;
5935
6418
  try {
5936
- existing = JSON.parse(readFileSync6(hooksPath, "utf-8"));
6419
+ existing = JSON.parse(readFileSync7(hooksPath, "utf-8"));
5937
6420
  } catch {
5938
6421
  return;
5939
6422
  }
@@ -5942,67 +6425,71 @@ function uninstallCodexHook(projectDir) {
5942
6425
  const filtered = preTool.filter((h) => !JSON.stringify(h).includes("graphify"));
5943
6426
  hooks.PreToolUse = filtered;
5944
6427
  existing.hooks = hooks;
5945
- writeFileSync5(hooksPath, JSON.stringify(existing, null, 2), "utf-8");
6428
+ writeFileSync6(hooksPath, JSON.stringify(existing, null, 2), "utf-8");
5946
6429
  console.log(` .codex/hooks.json -> PreToolUse hook removed`);
5947
6430
  }
5948
6431
  function agentsInstall(projectDir, platformName) {
5949
6432
  let alreadyConfigured = false;
5950
- const target = join4(projectDir, "AGENTS.md");
6433
+ const target = join5(projectDir, "AGENTS.md");
5951
6434
  const section = getAgentsMdSection(platformName);
5952
- if (existsSync7(target)) {
5953
- const content = readFileSync6(target, "utf-8");
6435
+ if (existsSync8(target)) {
6436
+ const content = readFileSync7(target, "utf-8");
5954
6437
  if (content.includes(MD_MARKER)) {
5955
6438
  alreadyConfigured = true;
5956
6439
  console.log(`graphify already configured in AGENTS.md`);
5957
6440
  } else {
5958
- writeFileSync5(target, content.trimEnd() + "\n\n" + section, "utf-8");
6441
+ writeFileSync6(target, content.trimEnd() + "\n\n" + section, "utf-8");
5959
6442
  }
5960
6443
  } else {
5961
- writeFileSync5(target, section, "utf-8");
6444
+ writeFileSync6(target, section, "utf-8");
5962
6445
  }
5963
6446
  if (!alreadyConfigured) {
5964
- console.log(`graphify section written to ${resolve5(target)}`);
6447
+ console.log(`graphify section written to ${resolve8(target)}`);
5965
6448
  }
5966
6449
  if (platformName === "codex") {
5967
6450
  installCodexHook(projectDir);
6451
+ } else if (platformName === "opencode") {
6452
+ installOpenCodePlugin(projectDir);
5968
6453
  }
5969
6454
  console.log();
5970
6455
  console.log(`${platformName.charAt(0).toUpperCase() + platformName.slice(1)} will now check the knowledge graph before answering`);
5971
6456
  console.log("codebase questions and rebuild it after code changes.");
5972
- if (platformName !== "codex") {
6457
+ if (!["codex", "opencode"].includes(platformName)) {
5973
6458
  console.log();
5974
6459
  console.log("Note: unlike Claude Code, there is no PreToolUse hook equivalent for");
5975
6460
  console.log(`${platformName.charAt(0).toUpperCase() + platformName.slice(1)} \u2014 the AGENTS.md rules are the always-on mechanism.`);
5976
6461
  }
5977
6462
  }
5978
6463
  function agentsUninstall(projectDir, platformName) {
5979
- const target = join4(projectDir, "AGENTS.md");
5980
- if (!existsSync7(target)) {
6464
+ const target = join5(projectDir, "AGENTS.md");
6465
+ if (!existsSync8(target)) {
5981
6466
  console.log("No AGENTS.md found in current directory - nothing to do");
5982
- return;
5983
- }
5984
- const content = readFileSync6(target, "utf-8");
5985
- if (!content.includes(MD_MARKER)) {
5986
- console.log("graphify section not found in AGENTS.md - nothing to do");
5987
- return;
5988
- }
5989
- const cleaned = content.replace(/\n*## graphify\n[\s\S]*?(?=\n## |\s*$)/, "").trim();
5990
- if (cleaned) {
5991
- writeFileSync5(target, cleaned + "\n", "utf-8");
5992
- console.log(`graphify section removed from ${resolve5(target)}`);
5993
6467
  } else {
5994
- const { unlinkSync: unlinkSync4 } = __require("fs");
5995
- unlinkSync4(target);
5996
- console.log(`AGENTS.md was empty after removal - deleted ${resolve5(target)}`);
6468
+ const content = readFileSync7(target, "utf-8");
6469
+ if (!content.includes(MD_MARKER)) {
6470
+ console.log("graphify section not found in AGENTS.md - nothing to do");
6471
+ } else {
6472
+ const cleaned = content.replace(/\n*## graphify\n[\s\S]*?(?=\n## |\s*$)/, "").trim();
6473
+ if (cleaned) {
6474
+ writeFileSync6(target, cleaned + "\n", "utf-8");
6475
+ console.log(`graphify section removed from ${resolve8(target)}`);
6476
+ } else {
6477
+ const { unlinkSync: unlinkSync5 } = __require("fs");
6478
+ unlinkSync5(target);
6479
+ console.log(`AGENTS.md was empty after removal - deleted ${resolve8(target)}`);
6480
+ }
6481
+ }
5997
6482
  }
5998
6483
  if (platformName === "codex") {
5999
6484
  uninstallCodexHook(projectDir);
6485
+ } else if (platformName === "opencode") {
6486
+ uninstallOpenCodePlugin(projectDir);
6000
6487
  }
6001
6488
  }
6002
6489
  function checkSkillVersion(skillDst) {
6003
- const versionFile = join4(dirname2(skillDst), ".graphify_version");
6004
- if (!existsSync7(versionFile)) return;
6005
- const installed = readFileSync6(versionFile, "utf-8").trim();
6490
+ const versionFile = join5(dirname4(skillDst), ".graphify_version");
6491
+ if (!existsSync8(versionFile)) return;
6492
+ const installed = readFileSync7(versionFile, "utf-8").trim();
6006
6493
  if (installed !== VERSION) {
6007
6494
  console.log(
6008
6495
  ` warning: skill is from graphify ${installed}, package is ${VERSION}. Run 'graphify install' to update.`
@@ -6041,7 +6528,7 @@ function getPlatformsToCheck(argv) {
6041
6528
  async function main() {
6042
6529
  for (const platformName of getPlatformsToCheck(process.argv.slice(2))) {
6043
6530
  const cfg = PLATFORM_CONFIG[platformName];
6044
- checkSkillVersion(join4(homedir(), cfg.skill_dst));
6531
+ checkSkillVersion(join5(homedir(), cfg.skill_dst));
6045
6532
  }
6046
6533
  const program = new Command();
6047
6534
  program.name("graphify").description("AI coding assistant skill - turn any folder into a queryable knowledge graph").version(VERSION);
@@ -6053,13 +6540,28 @@ async function main() {
6053
6540
  sub.command("install").description(`Write graphify section to CLAUDE.md + PreToolUse hook`).action(() => claudeInstall());
6054
6541
  sub.command("uninstall").description(`Remove graphify section from CLAUDE.md + PreToolUse hook`).action(() => claudeUninstall());
6055
6542
  }
6056
- for (const cmd of ["codex", "opencode", "claw", "droid", "trae", "trae-cn"]) {
6543
+ for (const cmd of ["gemini"]) {
6544
+ const sub = program.command(cmd).description(`${cmd} skill management`);
6545
+ sub.command("install").description("Write graphify section to GEMINI.md + project MCP config").action(() => geminiInstall());
6546
+ sub.command("uninstall").description("Remove graphify section from GEMINI.md + project MCP config").action(() => geminiUninstall());
6547
+ }
6548
+ {
6549
+ const sub = program.command("cursor").description("cursor skill management");
6550
+ sub.command("install").description("Write .cursor/rules/graphify.mdc").action(() => cursorInstall());
6551
+ sub.command("uninstall").description("Remove .cursor/rules/graphify.mdc").action(() => cursorUninstall());
6552
+ }
6553
+ {
6554
+ const sub = program.command("copilot").description("copilot skill management");
6555
+ sub.command("install").description("Copy graphify skill to ~/.copilot/skills").action(() => installSkill("copilot"));
6556
+ sub.command("uninstall").description("Remove graphify skill from ~/.copilot/skills").action(() => uninstallSkill("copilot"));
6557
+ }
6558
+ for (const cmd of ["aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn"]) {
6057
6559
  const sub = program.command(cmd).description(`${cmd} skill management`);
6058
6560
  sub.command("install").description(
6059
- cmd === "codex" ? "Write graphify section to AGENTS.md + PreToolUse hook" : "Write graphify section to AGENTS.md"
6561
+ cmd === "codex" ? "Write graphify section to AGENTS.md + PreToolUse hook" : cmd === "opencode" ? "Write graphify section to AGENTS.md + tool.execute.before plugin" : "Write graphify section to AGENTS.md"
6060
6562
  ).action(() => agentsInstall(".", cmd));
6061
6563
  sub.command("uninstall").description(
6062
- cmd === "codex" ? "Remove graphify section from AGENTS.md + PreToolUse hook" : "Remove graphify section from AGENTS.md"
6564
+ cmd === "codex" ? "Remove graphify section from AGENTS.md + PreToolUse hook" : cmd === "opencode" ? "Remove graphify section from AGENTS.md + plugin" : "Remove graphify section from AGENTS.md"
6063
6565
  ).action(() => {
6064
6566
  agentsUninstall(".", cmd);
6065
6567
  });
@@ -6090,7 +6592,7 @@ async function main() {
6090
6592
  const { readFileSync: rf } = await import("fs");
6091
6593
  const { resolve: res } = await import("path");
6092
6594
  const gp = res(opts.graph);
6093
- if (!existsSync7(gp)) {
6595
+ if (!existsSync8(gp)) {
6094
6596
  console.error(`error: graph file not found: ${gp}`);
6095
6597
  process.exit(1);
6096
6598
  }
@@ -6099,22 +6601,8 @@ async function main() {
6099
6601
  process.exit(1);
6100
6602
  }
6101
6603
  try {
6102
- const Graph4 = (await import("graphology")).default;
6103
6604
  const raw = JSON.parse(rf(gp, "utf-8"));
6104
- const G = new Graph4({ type: "undirected" });
6105
- for (const node of raw.nodes ?? []) {
6106
- const { id, ...attrs } = node;
6107
- G.mergeNode(id, attrs);
6108
- }
6109
- for (const link of raw.links ?? []) {
6110
- const { source, target, ...attrs } = link;
6111
- if (G.hasNode(source) && G.hasNode(target)) {
6112
- try {
6113
- G.mergeEdge(source, target, attrs);
6114
- } catch {
6115
- }
6116
- }
6117
- }
6605
+ const G = loadGraphFromData(raw);
6118
6606
  const terms = question.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
6119
6607
  const scored = [];
6120
6608
  G.forEachNode((nid, data) => {
@@ -6139,7 +6627,7 @@ async function main() {
6139
6627
  if (d > 2) continue;
6140
6628
  if (d > 0 && visited.has(node)) continue;
6141
6629
  visited.add(node);
6142
- G.forEachNeighbor(node, (neighbor) => {
6630
+ forEachTraversalNeighbor(G, node, (neighbor) => {
6143
6631
  if (!visited.has(neighbor)) {
6144
6632
  stack.push([neighbor, d + 1]);
6145
6633
  edgesSeen.push([node, neighbor]);
@@ -6151,7 +6639,7 @@ async function main() {
6151
6639
  for (let depth = 0; depth < 2; depth++) {
6152
6640
  const nextFrontier = /* @__PURE__ */ new Set();
6153
6641
  for (const n of frontier) {
6154
- G.forEachNeighbor(n, (neighbor) => {
6642
+ forEachTraversalNeighbor(G, n, (neighbor) => {
6155
6643
  if (!visited.has(neighbor)) {
6156
6644
  nextFrontier.add(neighbor);
6157
6645
  edgesSeen.push([n, neighbor]);
@@ -6198,9 +6686,9 @@ async function main() {
6198
6686
  const { runBenchmark: runBenchmark2, printBenchmark: printBenchmark2 } = await Promise.resolve().then(() => (init_benchmark(), benchmark_exports));
6199
6687
  const gp = graphPath ?? "graphify-out/graph.json";
6200
6688
  let corpusWords;
6201
- if (existsSync7(".graphify_detect.json")) {
6689
+ if (existsSync8(".graphify_detect.json")) {
6202
6690
  try {
6203
- const data = JSON.parse(readFileSync6(".graphify_detect.json", "utf-8"));
6691
+ const data = JSON.parse(readFileSync7(".graphify_detect.json", "utf-8"));
6204
6692
  corpusWords = data.total_words;
6205
6693
  } catch {
6206
6694
  }
@@ -6219,7 +6707,7 @@ function isDirectCliExecution() {
6219
6707
  try {
6220
6708
  return realpathSync2(process.argv[1]) === __filename;
6221
6709
  } catch {
6222
- return resolve5(process.argv[1]) === __filename;
6710
+ return resolve8(process.argv[1]) === __filename;
6223
6711
  }
6224
6712
  }
6225
6713
  if (isDirectCliExecution()) {
@@ -6230,9 +6718,14 @@ if (isDirectCliExecution()) {
6230
6718
  }
6231
6719
  export {
6232
6720
  agentsInstall,
6721
+ cursorInstall,
6722
+ cursorUninstall,
6723
+ geminiInstall,
6724
+ geminiUninstall,
6233
6725
  getAgentsMdSection,
6234
6726
  getInvocationExample,
6235
6727
  getPlatformsToCheck,
6728
+ installClaudeHook,
6236
6729
  installCodexHook,
6237
6730
  main
6238
6731
  };