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/README.ja-JP.md +60 -17
- package/README.md +41 -13
- package/README.zh-CN.md +54 -17
- package/dist/cli.js +862 -369
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1070 -598
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -6
- package/dist/index.d.ts +36 -6
- package/dist/index.js +1092 -614
- package/dist/index.js.map +1 -1
- package/dist/skill-runtime.js +1182 -669
- package/dist/skill-runtime.js.map +1 -1
- package/package.json +14 -4
- package/src/skills/skill-claw.md +1 -0
- package/src/skills/skill-codex.md +69 -11
- package/src/skills/skill-droid.md +73 -6
- package/src/skills/skill-gemini.toml +207 -0
- package/src/skills/skill-opencode.md +73 -6
- package/src/skills/skill-trae.md +1 -0
- package/src/skills/skill-windows.md +76 -5
- package/src/skills/skill.md +82 -8
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/
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
859
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1300
|
-
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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
|
|
1342
|
-
import { join as
|
|
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
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
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 =
|
|
1354
|
-
|
|
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 =
|
|
1365
|
-
if (!
|
|
1488
|
+
const entry = join3(cacheDir(root), `${h}.json`);
|
|
1489
|
+
if (!existsSync4(entry)) return null;
|
|
1366
1490
|
try {
|
|
1367
|
-
return JSON.parse(
|
|
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 =
|
|
1498
|
+
const entry = join3(cacheDir(root), `${h}.json`);
|
|
1375
1499
|
const tmp = entry + ".tmp";
|
|
1376
1500
|
try {
|
|
1377
|
-
|
|
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
|
|
1419
|
-
import { resolve as
|
|
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 (
|
|
1581
|
+
if (existsSync5(resolved)) return resolved;
|
|
1458
1582
|
} catch {
|
|
1459
1583
|
}
|
|
1460
1584
|
}
|
|
1461
|
-
const nmDir =
|
|
1585
|
+
const nmDir = join4(process.cwd(), "node_modules");
|
|
1462
1586
|
for (const candidate of candidates) {
|
|
1463
|
-
const p =
|
|
1464
|
-
if (
|
|
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 =
|
|
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 =
|
|
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,
|
|
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: "
|
|
2368
|
+
confidence: "EXTRACTED",
|
|
2245
2369
|
source_file: strPath,
|
|
2246
2370
|
source_location: `L${line}`,
|
|
2247
|
-
weight:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
2627
|
-
const pkgScope =
|
|
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,
|
|
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: "
|
|
2894
|
+
confidence: "EXTRACTED",
|
|
2771
2895
|
source_file: strPath,
|
|
2772
2896
|
source_location: `L${line}`,
|
|
2773
|
-
weight:
|
|
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 =
|
|
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 =
|
|
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,
|
|
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: "
|
|
3049
|
+
confidence: "EXTRACTED",
|
|
2926
3050
|
source_file: strPath,
|
|
2927
3051
|
source_location: `L${line}`,
|
|
2928
|
-
weight:
|
|
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 =
|
|
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 =
|
|
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,
|
|
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, "
|
|
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 =
|
|
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 =
|
|
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,
|
|
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, "
|
|
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 =
|
|
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 =
|
|
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,
|
|
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, "
|
|
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 =
|
|
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 =
|
|
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,
|
|
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, "
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3920
|
+
root = dirname3(paths[0]);
|
|
3797
3921
|
} else {
|
|
3798
|
-
const parts = paths.map((p) => p.split(
|
|
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(
|
|
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 =
|
|
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) =>
|
|
3969
|
+
const pyPaths = paths.filter((p) => extname3(p) === ".py");
|
|
3846
3970
|
if (pyPaths.length > 0) {
|
|
3847
|
-
const pyResults = perFile.filter((_r, i) =>
|
|
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 =
|
|
3994
|
+
const resolved = resolve7(target);
|
|
3871
3995
|
try {
|
|
3872
|
-
const stat =
|
|
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 =
|
|
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 =
|
|
4011
|
+
const fullPath = join4(dir, entry);
|
|
3888
4012
|
let stat;
|
|
3889
4013
|
try {
|
|
3890
|
-
stat =
|
|
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(
|
|
3902
|
-
if (parentReal === real || parentReal.startsWith(real +
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
5265
|
-
import { resolve as pathResolve2, extname as
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
5500
|
+
mkdirSync4(outDir, { recursive: true });
|
|
5353
5501
|
const flagPath = pathResolve2(outDir, "needs_update");
|
|
5354
|
-
|
|
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) => !
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
5552
|
-
|
|
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 (!
|
|
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
|
|
5644
|
-
writeFileSync as
|
|
5645
|
-
existsSync as
|
|
5646
|
-
mkdirSync as
|
|
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
|
|
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 =
|
|
5747
|
+
var __dirname = dirname4(__filename);
|
|
5657
5748
|
function getVersion() {
|
|
5658
5749
|
try {
|
|
5659
|
-
const pkg = JSON.parse(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
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 (
|
|
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
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
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 =
|
|
5783
|
-
if (
|
|
5784
|
-
const content =
|
|
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
|
-
|
|
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
|
-
|
|
5793
|
-
|
|
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 =
|
|
5812
|
-
|
|
6251
|
+
const settingsPath = join5(projectDir, ".claude", "settings.json");
|
|
6252
|
+
mkdirSync5(dirname4(settingsPath), { recursive: true });
|
|
5813
6253
|
let settings = {};
|
|
5814
|
-
if (
|
|
6254
|
+
if (existsSync8(settingsPath)) {
|
|
5815
6255
|
try {
|
|
5816
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
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
|
-
|
|
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 =
|
|
5834
|
-
if (!
|
|
6272
|
+
const settingsPath = join5(projectDir, ".claude", "settings.json");
|
|
6273
|
+
if (!existsSync8(settingsPath)) return;
|
|
5835
6274
|
let settings;
|
|
5836
6275
|
try {
|
|
5837
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
5855
|
-
if (
|
|
5856
|
-
const content =
|
|
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
|
-
|
|
6300
|
+
writeFileSync6(target, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION, "utf-8");
|
|
5862
6301
|
}
|
|
5863
6302
|
} else {
|
|
5864
|
-
|
|
6303
|
+
writeFileSync6(target, CLAUDE_MD_SECTION, "utf-8");
|
|
5865
6304
|
}
|
|
5866
6305
|
if (!alreadyConfigured) {
|
|
5867
|
-
console.log(`graphify section written to ${
|
|
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 =
|
|
5876
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
5888
|
-
console.log(`graphify section removed from ${
|
|
6326
|
+
writeFileSync6(target, cleaned + "\n", "utf-8");
|
|
6327
|
+
console.log(`graphify section removed from ${resolve8(target)}`);
|
|
5889
6328
|
} else {
|
|
5890
|
-
const { unlinkSync:
|
|
5891
|
-
|
|
5892
|
-
console.log(`CLAUDE.md was empty after removal - deleted ${
|
|
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 =
|
|
5898
|
-
if (
|
|
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 =
|
|
5903
|
-
|
|
6388
|
+
const hooksPath = join5(hooksDir, "hooks.json");
|
|
6389
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
5904
6390
|
let existing = {};
|
|
5905
|
-
if (
|
|
6391
|
+
if (existsSync8(hooksPath)) {
|
|
5906
6392
|
try {
|
|
5907
|
-
existing = JSON.parse(
|
|
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
|
-
|
|
5914
|
-
|
|
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","
|
|
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 =
|
|
6409
|
+
hooks.PreToolUse = filtered;
|
|
5927
6410
|
existing.hooks = hooks;
|
|
5928
|
-
|
|
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 =
|
|
5933
|
-
if (!
|
|
6415
|
+
const hooksPath = join5(projectDir, ".codex", "hooks.json");
|
|
6416
|
+
if (!existsSync8(hooksPath)) return;
|
|
5934
6417
|
let existing;
|
|
5935
6418
|
try {
|
|
5936
|
-
existing = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
6433
|
+
const target = join5(projectDir, "AGENTS.md");
|
|
5951
6434
|
const section = getAgentsMdSection(platformName);
|
|
5952
|
-
if (
|
|
5953
|
-
const content =
|
|
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
|
-
|
|
6441
|
+
writeFileSync6(target, content.trimEnd() + "\n\n" + section, "utf-8");
|
|
5959
6442
|
}
|
|
5960
6443
|
} else {
|
|
5961
|
-
|
|
6444
|
+
writeFileSync6(target, section, "utf-8");
|
|
5962
6445
|
}
|
|
5963
6446
|
if (!alreadyConfigured) {
|
|
5964
|
-
console.log(`graphify section written to ${
|
|
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 (
|
|
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 =
|
|
5980
|
-
if (!
|
|
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
|
|
5995
|
-
|
|
5996
|
-
|
|
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 =
|
|
6004
|
-
if (!
|
|
6005
|
-
const installed =
|
|
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(
|
|
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 ["
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
6689
|
+
if (existsSync8(".graphify_detect.json")) {
|
|
6202
6690
|
try {
|
|
6203
|
-
const data = JSON.parse(
|
|
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
|
|
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
|
};
|