@monoes/cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,66 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Graphify MCP Tools (compiled)
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Bridges @monobrain/graph's knowledge graph into monobrain's MCP tool surface.
|
|
5
|
+
* Graph is built automatically on `monobrain init` and stored at
|
|
6
|
+
* .monobrain/graph/graph.json (legacy: graphify-out/graph.json).
|
|
4
7
|
*/
|
|
5
|
-
import { spawnSync } from 'child_process';
|
|
6
8
|
import { existsSync, readFileSync } from 'fs';
|
|
7
9
|
import { join, resolve } from 'path';
|
|
8
10
|
import { getProjectCwd } from './types.js';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
return resolve(join(cwd, 'graphify-out', 'graph.json'));
|
|
12
|
-
}
|
|
12
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
catch { return false; }
|
|
14
|
+
function getGraphPath(cwd) {
|
|
15
|
+
const nativePath = resolve(join(cwd, '.monobrain', 'graph', 'graph.json'));
|
|
16
|
+
const legacyPath = resolve(join(cwd, 'graphify-out', 'graph.json'));
|
|
17
|
+
if (existsSync(nativePath)) return nativePath;
|
|
18
|
+
if (existsSync(legacyPath)) return legacyPath;
|
|
19
|
+
return nativePath;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
function graphExists(cwd) {
|
|
25
23
|
return existsSync(getGraphPath(cwd));
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
// ── Graph loading ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
async function loadKnowledgeGraph(cwd) {
|
|
29
29
|
const graphPath = getGraphPath(cwd);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw new Error(result.stderr?.trim() || 'graphify python call failed');
|
|
30
|
+
let rawNodes = [];
|
|
31
|
+
let rawEdges = [];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { loadGraph } = await import('@monoes/graph');
|
|
35
|
+
const loaded = loadGraph(graphPath);
|
|
36
|
+
rawNodes = loaded.nodes;
|
|
37
|
+
rawEdges = loaded.edges;
|
|
38
|
+
} catch {
|
|
39
|
+
const data = JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
40
|
+
rawNodes = data.nodes || [];
|
|
41
|
+
rawEdges = data.links || data.edges || [];
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
|
|
44
|
+
const nodes = new Map();
|
|
45
|
+
for (const n of rawNodes) nodes.set(n.id, n);
|
|
46
|
+
|
|
47
|
+
const adj = new Map();
|
|
48
|
+
const radj = new Map();
|
|
49
|
+
const degree = new Map();
|
|
50
|
+
for (const n of rawNodes) {
|
|
51
|
+
adj.set(n.id, []);
|
|
52
|
+
radj.set(n.id, []);
|
|
53
|
+
degree.set(n.id, 0);
|
|
54
|
+
}
|
|
55
|
+
for (const e of rawEdges) {
|
|
56
|
+
adj.get(e.source)?.push(e.target);
|
|
57
|
+
radj.get(e.target)?.push(e.source);
|
|
58
|
+
degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
|
|
59
|
+
degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { nodes, adj, radj, edges: rawEdges, degree, graphPath };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nodeOut(g, id) {
|
|
66
|
+
const d = g.nodes.get(id) ?? { id };
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
label: d.label ?? id,
|
|
70
|
+
file: d.source_file ?? '',
|
|
71
|
+
location: d.source_location ?? '',
|
|
72
|
+
community: d.community ?? null,
|
|
73
|
+
degree: g.degree.get(id) ?? 0,
|
|
74
|
+
file_type: d.file_type ?? '',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scoreNode(g, id, terms) {
|
|
79
|
+
const d = g.nodes.get(id);
|
|
80
|
+
if (!d) return 0;
|
|
81
|
+
const label = (d.label ?? id).toLowerCase();
|
|
82
|
+
const file = (d.source_file ?? '').toLowerCase();
|
|
83
|
+
return terms.reduce((s, t) => {
|
|
84
|
+
if (label.includes(t)) s += 1;
|
|
85
|
+
if (file.includes(t)) s += 0.5;
|
|
86
|
+
return s;
|
|
87
|
+
}, 0);
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
import json
|
|
51
|
-
from pathlib import Path
|
|
52
|
-
import networkx as nx
|
|
53
|
-
from networkx.readwrite import json_graph
|
|
54
|
-
data = json.loads(Path(graph_path).read_text())
|
|
55
|
-
try:
|
|
56
|
-
G = json_graph.node_link_graph(data, edges='links')
|
|
57
|
-
except TypeError:
|
|
58
|
-
G = json_graph.node_link_graph(data)
|
|
59
|
-
`;
|
|
90
|
+
// ── Tool Definitions ──────────────────────────────────────────────────────────
|
|
60
91
|
|
|
61
92
|
export const graphifyBuildTool = {
|
|
62
93
|
name: 'graphify_build',
|
|
63
|
-
description: 'Build (or rebuild) the knowledge graph for a project directory.
|
|
94
|
+
description: 'Build (or rebuild) the knowledge graph for a project directory. ' +
|
|
95
|
+
'Extracts AST structure from code files, semantic relationships from docs, ' +
|
|
96
|
+
'and clusters them into communities. Run this first before using other graphify tools. ' +
|
|
97
|
+
'Code-only changes are fast (tree-sitter, no LLM). Doc changes require an LLM call.',
|
|
64
98
|
category: 'graphify',
|
|
65
99
|
tags: ['knowledge-graph', 'codebase', 'architecture', 'analysis'],
|
|
66
100
|
inputSchema: {
|
|
@@ -73,30 +107,37 @@ export const graphifyBuildTool = {
|
|
|
73
107
|
handler: async (params) => {
|
|
74
108
|
const cwd = getProjectCwd();
|
|
75
109
|
const targetPath = params.path || cwd;
|
|
76
|
-
if (!isGraphifyInstalled()) {
|
|
77
|
-
return { error: true, message: 'graphify not installed. Run: pip install graphifyy[mcp]', hint: 'After installing, call graphify_build again.' };
|
|
78
|
-
}
|
|
79
110
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
111
|
+
const { buildGraph } = await import('@monoes/graph');
|
|
112
|
+
const outputDir = join(targetPath, '.monobrain', 'graph');
|
|
113
|
+
const result = await buildGraph(targetPath, {
|
|
114
|
+
codeOnly: Boolean(params.codeOnly),
|
|
115
|
+
outputDir,
|
|
82
116
|
});
|
|
83
|
-
const graphPath = getGraphPath(cwd);
|
|
84
|
-
const built = existsSync(graphPath);
|
|
85
117
|
return {
|
|
86
|
-
success:
|
|
87
|
-
graphPath:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
success: true,
|
|
119
|
+
graphPath: result.graphPath,
|
|
120
|
+
filesProcessed: result.filesProcessed,
|
|
121
|
+
nodes: result.analysis.stats.nodes,
|
|
122
|
+
edges: result.analysis.stats.edges,
|
|
123
|
+
message: `Knowledge graph built at ${result.graphPath}`,
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
error: true,
|
|
128
|
+
message: String(err),
|
|
129
|
+
hint: '@monoes/graph package not available — ensure it is installed and built.',
|
|
91
130
|
};
|
|
92
131
|
}
|
|
93
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
94
132
|
},
|
|
95
133
|
};
|
|
96
134
|
|
|
97
135
|
export const graphifyQueryTool = {
|
|
98
136
|
name: 'graphify_query',
|
|
99
|
-
description: 'Search the knowledge graph with a natural language question or keywords.
|
|
137
|
+
description: 'Search the knowledge graph with a natural language question or keywords. ' +
|
|
138
|
+
'Returns relevant nodes and edges as structured context — use this instead of reading ' +
|
|
139
|
+
'many files when you want to understand how components relate. ' +
|
|
140
|
+
'BFS mode gives broad context; DFS traces a specific call path.',
|
|
100
141
|
category: 'graphify',
|
|
101
142
|
tags: ['knowledge-graph', 'search', 'architecture', 'codebase'],
|
|
102
143
|
inputSchema: {
|
|
@@ -112,82 +153,128 @@ export const graphifyQueryTool = {
|
|
|
112
153
|
handler: async (params) => {
|
|
113
154
|
const cwd = getProjectCwd();
|
|
114
155
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(cwd)}` };
|
|
156
|
+
const question = params.question;
|
|
157
|
+
const mode = params.mode || 'bfs';
|
|
158
|
+
const depth = params.depth || 3;
|
|
115
159
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
stack.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
161
|
+
const terms = question.toLowerCase().split(/\s+/).filter(t => t.length > 2);
|
|
162
|
+
|
|
163
|
+
let startNodes = [];
|
|
164
|
+
if (terms.length > 0) {
|
|
165
|
+
const scored = [];
|
|
166
|
+
for (const id of g.nodes.keys()) {
|
|
167
|
+
const s = scoreNode(g, id, terms);
|
|
168
|
+
if (s > 0) scored.push([s, id]);
|
|
169
|
+
}
|
|
170
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
171
|
+
startNodes = scored.slice(0, 5).map(([, id]) => id);
|
|
172
|
+
}
|
|
173
|
+
if (startNodes.length === 0) {
|
|
174
|
+
startNodes = [...g.nodes.keys()]
|
|
175
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
176
|
+
.slice(0, 3);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const visited = new Set(startNodes);
|
|
180
|
+
const edgesSeen = [];
|
|
181
|
+
|
|
182
|
+
if (mode === 'bfs') {
|
|
183
|
+
let frontier = new Set(startNodes);
|
|
184
|
+
for (let d = 0; d < depth; d++) {
|
|
185
|
+
const next = new Set();
|
|
186
|
+
for (const n of frontier) {
|
|
187
|
+
for (const nbr of g.adj.get(n) ?? []) {
|
|
188
|
+
if (!visited.has(nbr)) {
|
|
189
|
+
next.add(nbr);
|
|
190
|
+
edgesSeen.push([n, nbr]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const n of next) visited.add(n);
|
|
195
|
+
frontier = next;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
const stack = startNodes.map(n => [n, 0]);
|
|
199
|
+
while (stack.length > 0) {
|
|
200
|
+
const [node, d] = stack.pop();
|
|
201
|
+
if (visited.has(node) && d > 0) continue;
|
|
202
|
+
if (d > depth) continue;
|
|
203
|
+
visited.add(node);
|
|
204
|
+
for (const nbr of g.adj.get(node) ?? []) {
|
|
205
|
+
if (!visited.has(nbr)) {
|
|
206
|
+
stack.push([nbr, d + 1]);
|
|
207
|
+
edgesSeen.push([node, nbr]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const nodesOut = [...visited]
|
|
214
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
215
|
+
.slice(0, 60)
|
|
216
|
+
.map(id => nodeOut(g, id));
|
|
217
|
+
|
|
218
|
+
const edgeLookup = new Map();
|
|
219
|
+
for (const e of g.edges) edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
220
|
+
|
|
221
|
+
const edgesOut = edgesSeen
|
|
222
|
+
.filter(([u, v]) => visited.has(u) && visited.has(v))
|
|
223
|
+
.slice(0, 80)
|
|
224
|
+
.map(([u, v]) => {
|
|
225
|
+
const e = edgeLookup.get(`${u}__${v}`) ?? {};
|
|
226
|
+
return {
|
|
227
|
+
from: g.nodes.get(u)?.label ?? u,
|
|
228
|
+
to: g.nodes.get(v)?.label ?? v,
|
|
229
|
+
relation: e.relation ?? '',
|
|
230
|
+
confidence: e.confidence ?? '',
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return { question, mode, depth, nodes: nodesOut, edges: edgesOut, total_nodes: visited.size, total_edges: edgesSeen.length };
|
|
235
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
162
236
|
},
|
|
163
237
|
};
|
|
164
238
|
|
|
165
239
|
export const graphifyGodNodesTool = {
|
|
166
240
|
name: 'graphify_god_nodes',
|
|
167
|
-
description: 'Return the most connected nodes in the knowledge graph — the core abstractions
|
|
241
|
+
description: 'Return the most connected nodes in the knowledge graph — the core abstractions ' +
|
|
242
|
+
'and central concepts of the codebase. Use this at the start of any architectural analysis ' +
|
|
243
|
+
'to understand what the most important components are before diving into details.',
|
|
168
244
|
category: 'graphify',
|
|
169
245
|
tags: ['knowledge-graph', 'architecture', 'abstractions', 'codebase'],
|
|
170
246
|
inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 15 } } },
|
|
171
247
|
handler: async (params) => {
|
|
172
248
|
const cwd = getProjectCwd();
|
|
173
249
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
250
|
+
const topN = params.topN || 15;
|
|
174
251
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
252
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
253
|
+
const sortedIds = [...g.nodes.keys()]
|
|
254
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
255
|
+
.slice(0, topN);
|
|
256
|
+
const godNodes = sortedIds.map(id => {
|
|
257
|
+
const d = g.nodes.get(id) ?? { id };
|
|
258
|
+
const neighbors = (g.adj.get(id) ?? []).slice(0, 8).map(nid => g.nodes.get(nid)?.label ?? nid);
|
|
259
|
+
return {
|
|
260
|
+
label: d.label ?? id,
|
|
261
|
+
degree: g.degree.get(id) ?? 0,
|
|
262
|
+
file: d.source_file ?? '',
|
|
263
|
+
location: d.source_location ?? '',
|
|
264
|
+
community: d.community ?? null,
|
|
265
|
+
file_type: d.file_type ?? '',
|
|
266
|
+
neighbors,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
return { god_nodes: godNodes, total_nodes: g.nodes.size };
|
|
270
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
185
271
|
},
|
|
186
272
|
};
|
|
187
273
|
|
|
188
274
|
export const graphifyGetNodeTool = {
|
|
189
275
|
name: 'graphify_get_node',
|
|
190
|
-
description: 'Get all details for a specific concept/node in the knowledge graph:
|
|
276
|
+
description: 'Get all details for a specific concept/node in the knowledge graph: ' +
|
|
277
|
+
'its source location, community, all relationships, and confidence levels.',
|
|
191
278
|
category: 'graphify',
|
|
192
279
|
tags: ['knowledge-graph', 'node', 'details'],
|
|
193
280
|
inputSchema: { type: 'object', properties: { label: { type: 'string', description: 'Node label or ID (case-insensitive)' } }, required: ['label'] },
|
|
@@ -195,28 +282,50 @@ export const graphifyGetNodeTool = {
|
|
|
195
282
|
const cwd = getProjectCwd();
|
|
196
283
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
197
284
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
285
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
286
|
+
const term = params.label.toLowerCase();
|
|
287
|
+
const matches = [...g.nodes.entries()]
|
|
288
|
+
.filter(([id, d]) => (d.label ?? id).toLowerCase().includes(term) || id.toLowerCase() === term)
|
|
289
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
290
|
+
.map(([id]) => id);
|
|
291
|
+
if (matches.length === 0) return { error: 'Node not found', searched: term };
|
|
292
|
+
const id = matches[0];
|
|
293
|
+
const d = g.nodes.get(id) ?? { id };
|
|
294
|
+
const edgeLookup = new Map();
|
|
295
|
+
for (const e of g.edges) edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
296
|
+
const outEdges = (g.adj.get(id) ?? []).slice(0, 40).map(tgt => {
|
|
297
|
+
const e = edgeLookup.get(`${id}__${tgt}`) ?? {};
|
|
298
|
+
return { direction: 'outgoing', to: g.nodes.get(tgt)?.label ?? tgt, relation: e.relation ?? '', confidence: e.confidence ?? '', confidence_score: e.confidence_score ?? null };
|
|
299
|
+
});
|
|
300
|
+
const inEdges = (g.radj.get(id) ?? []).slice(0, 40).map(src => {
|
|
301
|
+
const e = edgeLookup.get(`${src}__${id}`) ?? {};
|
|
302
|
+
return { direction: 'incoming', from: g.nodes.get(src)?.label ?? src, relation: e.relation ?? '', confidence: e.confidence ?? '' };
|
|
303
|
+
});
|
|
304
|
+
const knownKeys = new Set(['label', 'source_file', 'source_location', 'community', 'file_type', 'id']);
|
|
305
|
+
const attributes = {};
|
|
306
|
+
for (const [k, v] of Object.entries(d)) {
|
|
307
|
+
if (!knownKeys.has(k)) attributes[k] = v;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
label: d.label ?? id,
|
|
312
|
+
file: d.source_file ?? '',
|
|
313
|
+
location: d.source_location ?? '',
|
|
314
|
+
community: d.community ?? null,
|
|
315
|
+
file_type: d.file_type ?? '',
|
|
316
|
+
degree: g.degree.get(id) ?? 0,
|
|
317
|
+
attributes,
|
|
318
|
+
edges: [...outEdges, ...inEdges],
|
|
319
|
+
all_matches: matches.slice(0, 10).map(m => g.nodes.get(m)?.label ?? m),
|
|
320
|
+
};
|
|
321
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
214
322
|
},
|
|
215
323
|
};
|
|
216
324
|
|
|
217
325
|
export const graphifyShortestPathTool = {
|
|
218
326
|
name: 'graphify_shortest_path',
|
|
219
|
-
description: 'Find the shortest relationship path between two concepts in the knowledge graph.
|
|
327
|
+
description: 'Find the shortest relationship path between two concepts in the knowledge graph. ' +
|
|
328
|
+
'Reveals coupling chains between any two components.',
|
|
220
329
|
category: 'graphify',
|
|
221
330
|
tags: ['knowledge-graph', 'path', 'dependencies', 'coupling'],
|
|
222
331
|
inputSchema: {
|
|
@@ -232,54 +341,83 @@ export const graphifyShortestPathTool = {
|
|
|
232
341
|
const cwd = getProjectCwd();
|
|
233
342
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
234
343
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
344
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
345
|
+
const maxHops = params.maxHops || 8;
|
|
346
|
+
|
|
347
|
+
function findNodes(term) {
|
|
348
|
+
const t = term.toLowerCase();
|
|
349
|
+
return [...g.nodes.entries()]
|
|
350
|
+
.filter(([id, d]) => (d.label ?? id).toLowerCase().includes(t) || id.toLowerCase() === t)
|
|
351
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
352
|
+
.map(([id]) => id);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const srcNodes = findNodes(params.source);
|
|
356
|
+
const tgtNodes = findNodes(params.target);
|
|
357
|
+
if (!srcNodes.length) return { error: true, message: `Source not found: ${params.source}` };
|
|
358
|
+
if (!tgtNodes.length) return { error: true, message: `Target not found: ${params.target}` };
|
|
359
|
+
|
|
360
|
+
function bfsPath(start, end) {
|
|
361
|
+
const prev = new Map();
|
|
362
|
+
const queue = [start];
|
|
363
|
+
const visited = new Set([start]);
|
|
364
|
+
while (queue.length > 0) {
|
|
365
|
+
const cur = queue.shift();
|
|
366
|
+
if (cur === end) {
|
|
367
|
+
const path = [];
|
|
368
|
+
let node = end;
|
|
369
|
+
while (node !== undefined) { path.unshift(node); node = prev.get(node); }
|
|
370
|
+
return path.length - 1 <= maxHops ? path : null;
|
|
371
|
+
}
|
|
372
|
+
const nbrs = [...(g.adj.get(cur) ?? []), ...(g.radj.get(cur) ?? [])];
|
|
373
|
+
for (const nbr of nbrs) {
|
|
374
|
+
if (!visited.has(nbr)) {
|
|
375
|
+
visited.add(nbr);
|
|
376
|
+
prev.set(nbr, cur);
|
|
377
|
+
if (queue.length < 100000) queue.push(nbr);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let bestPath = null;
|
|
385
|
+
for (const src of srcNodes.slice(0, 3)) {
|
|
386
|
+
for (const tgt of tgtNodes.slice(0, 3)) {
|
|
387
|
+
const p = bfsPath(src, tgt);
|
|
388
|
+
if (p && (!bestPath || p.length < bestPath.length)) bestPath = p;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!bestPath) return { found: false, message: `No path within ${maxHops} hops between "${params.source}" and "${params.target}"` };
|
|
393
|
+
|
|
394
|
+
const edgeLookup = new Map();
|
|
395
|
+
for (const e of g.edges) {
|
|
396
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
397
|
+
edgeLookup.set(`${e.target}__${e.source}`, e);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const steps = bestPath.map((id, i) => {
|
|
401
|
+
const d = g.nodes.get(id) ?? { id };
|
|
402
|
+
const step = { label: d.label ?? id, file: d.source_file ?? '', location: d.source_location ?? '' };
|
|
403
|
+
if (i < bestPath.length - 1) {
|
|
404
|
+
const nextId = bestPath[i + 1];
|
|
405
|
+
const e = edgeLookup.get(`${id}__${nextId}`) ?? edgeLookup.get(`${nextId}__${id}`) ?? {};
|
|
406
|
+
step.next_relation = e.relation ?? '';
|
|
407
|
+
step.confidence = e.confidence ?? '';
|
|
408
|
+
}
|
|
409
|
+
return step;
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return { found: true, hops: bestPath.length - 1, path: steps };
|
|
413
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
277
414
|
},
|
|
278
415
|
};
|
|
279
416
|
|
|
280
417
|
export const graphifyGetCommunityTool = {
|
|
281
418
|
name: 'graphify_community',
|
|
282
|
-
description: 'Get all nodes in a specific community cluster. Communities are groups of
|
|
419
|
+
description: 'Get all nodes in a specific community cluster. Communities are groups of ' +
|
|
420
|
+
'tightly related components detected by graph clustering. Use graphify_stats first to see community count.',
|
|
283
421
|
category: 'graphify',
|
|
284
422
|
tags: ['knowledge-graph', 'community', 'clusters', 'subsystems'],
|
|
285
423
|
inputSchema: { type: 'object', properties: { communityId: { type: 'integer', description: 'Community ID (0 = largest)' } }, required: ['communityId'] },
|
|
@@ -287,67 +425,96 @@ export const graphifyGetCommunityTool = {
|
|
|
287
425
|
const cwd = getProjectCwd();
|
|
288
426
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
289
427
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
428
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
429
|
+
const cid = params.communityId;
|
|
430
|
+
const members = [...g.nodes.entries()]
|
|
431
|
+
.filter(([, d]) => d.community === cid)
|
|
432
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
433
|
+
.slice(0, 50)
|
|
434
|
+
.map(([id, d]) => ({
|
|
435
|
+
label: d.label ?? id,
|
|
436
|
+
file: d.source_file ?? '',
|
|
437
|
+
location: d.source_location ?? '',
|
|
438
|
+
degree: g.degree.get(id) ?? 0,
|
|
439
|
+
file_type: d.file_type ?? '',
|
|
440
|
+
}));
|
|
441
|
+
const edgeLookup = new Map();
|
|
442
|
+
for (const e of g.edges) edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
443
|
+
const externalEdges = [];
|
|
444
|
+
for (const [id, d] of g.nodes.entries()) {
|
|
445
|
+
if (d.community !== cid) continue;
|
|
446
|
+
for (const nbr of g.adj.get(id) ?? []) {
|
|
447
|
+
const nbrD = g.nodes.get(nbr);
|
|
448
|
+
if (nbrD?.community !== cid) {
|
|
449
|
+
const e = edgeLookup.get(`${id}__${nbr}`) ?? {};
|
|
450
|
+
externalEdges.push({ from: d.label ?? id, to: nbrD?.label ?? nbr, to_community: nbrD?.community ?? null, relation: e.relation ?? '' });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { community_id: cid, member_count: members.length, members, external_connections: externalEdges.slice(0, 30) };
|
|
455
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
306
456
|
},
|
|
307
457
|
};
|
|
308
458
|
|
|
309
459
|
export const graphifyStatsTool = {
|
|
310
460
|
name: 'graphify_stats',
|
|
311
|
-
description: 'Get summary statistics for the knowledge graph: node count, edge count,
|
|
461
|
+
description: 'Get summary statistics for the knowledge graph: node count, edge count, ' +
|
|
462
|
+
'community count, confidence breakdown, and top god nodes. Use this first to understand graph size.',
|
|
312
463
|
category: 'graphify',
|
|
313
464
|
tags: ['knowledge-graph', 'stats', 'overview'],
|
|
314
465
|
inputSchema: { type: 'object', properties: {} },
|
|
315
466
|
handler: async (_params) => {
|
|
316
467
|
const cwd = getProjectCwd();
|
|
317
468
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(cwd)}` };
|
|
318
|
-
if (!isGraphifyInstalled()) {
|
|
319
|
-
try {
|
|
320
|
-
const raw = JSON.parse(readFileSync(getGraphPath(cwd), 'utf-8'));
|
|
321
|
-
const nodes = raw.nodes || [];
|
|
322
|
-
const edges = raw.links || raw.edges || [];
|
|
323
|
-
return { nodes: nodes.length, edges: edges.length, note: 'graphify Python package not installed — basic stats only', install: 'pip install graphifyy[mcp]' };
|
|
324
|
-
}
|
|
325
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
326
|
-
}
|
|
327
469
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
communities
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
470
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
471
|
+
const communities = new Map();
|
|
472
|
+
for (const d of g.nodes.values()) {
|
|
473
|
+
if (d.community != null) communities.set(d.community, (communities.get(d.community) ?? 0) + 1);
|
|
474
|
+
}
|
|
475
|
+
const communitySizes = {};
|
|
476
|
+
[...communities.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
477
|
+
.forEach(([cid, count]) => { communitySizes[String(cid)] = count; });
|
|
478
|
+
const confidenceCounts = {};
|
|
479
|
+
const relationCounts = {};
|
|
480
|
+
const fileTypeCounts = {};
|
|
481
|
+
for (const e of g.edges) {
|
|
482
|
+
const conf = e.confidence ?? 'UNKNOWN';
|
|
483
|
+
confidenceCounts[conf] = (confidenceCounts[conf] ?? 0) + 1;
|
|
484
|
+
const rel = e.relation ?? 'unknown';
|
|
485
|
+
relationCounts[rel] = (relationCounts[rel] ?? 0) + 1;
|
|
486
|
+
}
|
|
487
|
+
for (const d of g.nodes.values()) {
|
|
488
|
+
const ft = d.file_type ?? 'unknown';
|
|
489
|
+
fileTypeCounts[ft] = (fileTypeCounts[ft] ?? 0) + 1;
|
|
490
|
+
}
|
|
491
|
+
const topRelations = Object.entries(relationCounts)
|
|
492
|
+
.sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
493
|
+
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
|
|
494
|
+
const topGodNodes = [...g.nodes.keys()]
|
|
495
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
496
|
+
.slice(0, 5)
|
|
497
|
+
.map(id => g.nodes.get(id)?.label ?? id);
|
|
498
|
+
return {
|
|
499
|
+
nodes: g.nodes.size,
|
|
500
|
+
edges: g.edges.length,
|
|
501
|
+
communities: communities.size,
|
|
502
|
+
community_sizes: communitySizes,
|
|
503
|
+
confidence: confidenceCounts,
|
|
504
|
+
top_relations: topRelations,
|
|
505
|
+
file_types: fileTypeCounts,
|
|
506
|
+
graph_path: g.graphPath,
|
|
507
|
+
top_god_nodes: topGodNodes,
|
|
508
|
+
is_directed: true,
|
|
509
|
+
};
|
|
510
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
345
511
|
},
|
|
346
512
|
};
|
|
347
513
|
|
|
348
514
|
export const graphifySurprisesTool = {
|
|
349
515
|
name: 'graphify_surprises',
|
|
350
|
-
description: 'Find surprising connections between components in different communities with strong relationships.
|
|
516
|
+
description: 'Find surprising connections between components in different communities with strong relationships. ' +
|
|
517
|
+
'These unexpected couplings often reveal hidden dependencies or important architectural patterns.',
|
|
351
518
|
category: 'graphify',
|
|
352
519
|
tags: ['knowledge-graph', 'architecture', 'coupling', 'surprises'],
|
|
353
520
|
inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 10 } } },
|
|
@@ -355,20 +522,31 @@ export const graphifySurprisesTool = {
|
|
|
355
522
|
const cwd = getProjectCwd();
|
|
356
523
|
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
357
524
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
surprises.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
525
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
526
|
+
const topN = params.topN || 10;
|
|
527
|
+
const surprises = [];
|
|
528
|
+
for (const e of g.edges) {
|
|
529
|
+
const uD = g.nodes.get(e.source);
|
|
530
|
+
const vD = g.nodes.get(e.target);
|
|
531
|
+
const cu = uD?.community ?? null;
|
|
532
|
+
const cv = vD?.community ?? null;
|
|
533
|
+
if (cu != null && cv != null && cu !== cv) {
|
|
534
|
+
surprises.push({
|
|
535
|
+
score: (g.degree.get(e.source) ?? 0) * (g.degree.get(e.target) ?? 0),
|
|
536
|
+
from: uD?.label ?? e.source,
|
|
537
|
+
from_community: cu,
|
|
538
|
+
from_file: uD?.source_file ?? '',
|
|
539
|
+
to: vD?.label ?? e.target,
|
|
540
|
+
to_community: cv,
|
|
541
|
+
to_file: vD?.source_file ?? '',
|
|
542
|
+
relation: e.relation ?? '',
|
|
543
|
+
confidence: e.confidence ?? '',
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
surprises.sort((a, b) => b.score - a.score);
|
|
548
|
+
return { surprises: surprises.slice(0, topN), total_cross_community_edges: surprises.length };
|
|
549
|
+
} catch (err) { return { error: true, message: String(err) }; }
|
|
372
550
|
},
|
|
373
551
|
};
|
|
374
552
|
|