@monoes/cli 1.0.9 → 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.
- package/.claude/agents/analysis/graphify/graphify-analyst.md +145 -0
- package/dist/src/commands/init.js +2 -0
- package/dist/src/init/executor.js +36 -0
- package/dist/src/init/mcp-generator.js +10 -0
- package/dist/src/init/types.js +5 -0
- package/dist/src/mcp-tools/graphify-tools.js +564 -0
- package/dist/src/mcp-tools/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphify MCP Tools (compiled)
|
|
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).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join, resolve } from 'path';
|
|
10
|
+
import { getProjectCwd } from './types.js';
|
|
11
|
+
|
|
12
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function graphExists(cwd) {
|
|
23
|
+
return existsSync(getGraphPath(cwd));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Graph loading ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
async function loadKnowledgeGraph(cwd) {
|
|
29
|
+
const graphPath = getGraphPath(cwd);
|
|
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 || [];
|
|
42
|
+
}
|
|
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);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Tool Definitions ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export const graphifyBuildTool = {
|
|
93
|
+
name: 'graphify_build',
|
|
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.',
|
|
98
|
+
category: 'graphify',
|
|
99
|
+
tags: ['knowledge-graph', 'codebase', 'architecture', 'analysis'],
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
path: { type: 'string', description: 'Path to analyse (defaults to current project root)' },
|
|
104
|
+
codeOnly: { type: 'boolean', description: 'Only re-extract changed code files — fast rebuild', default: false },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
handler: async (params) => {
|
|
108
|
+
const cwd = getProjectCwd();
|
|
109
|
+
const targetPath = params.path || cwd;
|
|
110
|
+
try {
|
|
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,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
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.',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const graphifyQueryTool = {
|
|
136
|
+
name: 'graphify_query',
|
|
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.',
|
|
141
|
+
category: 'graphify',
|
|
142
|
+
tags: ['knowledge-graph', 'search', 'architecture', 'codebase'],
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
question: { type: 'string', description: 'Natural language question or keyword' },
|
|
147
|
+
mode: { type: 'string', enum: ['bfs', 'dfs'], default: 'bfs' },
|
|
148
|
+
depth: { type: 'integer', default: 3 },
|
|
149
|
+
tokenBudget: { type: 'integer', default: 2000 },
|
|
150
|
+
},
|
|
151
|
+
required: ['question'],
|
|
152
|
+
},
|
|
153
|
+
handler: async (params) => {
|
|
154
|
+
const cwd = getProjectCwd();
|
|
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;
|
|
159
|
+
try {
|
|
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) }; }
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const graphifyGodNodesTool = {
|
|
240
|
+
name: 'graphify_god_nodes',
|
|
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.',
|
|
244
|
+
category: 'graphify',
|
|
245
|
+
tags: ['knowledge-graph', 'architecture', 'abstractions', 'codebase'],
|
|
246
|
+
inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 15 } } },
|
|
247
|
+
handler: async (params) => {
|
|
248
|
+
const cwd = getProjectCwd();
|
|
249
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
250
|
+
const topN = params.topN || 15;
|
|
251
|
+
try {
|
|
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) }; }
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const graphifyGetNodeTool = {
|
|
275
|
+
name: 'graphify_get_node',
|
|
276
|
+
description: 'Get all details for a specific concept/node in the knowledge graph: ' +
|
|
277
|
+
'its source location, community, all relationships, and confidence levels.',
|
|
278
|
+
category: 'graphify',
|
|
279
|
+
tags: ['knowledge-graph', 'node', 'details'],
|
|
280
|
+
inputSchema: { type: 'object', properties: { label: { type: 'string', description: 'Node label or ID (case-insensitive)' } }, required: ['label'] },
|
|
281
|
+
handler: async (params) => {
|
|
282
|
+
const cwd = getProjectCwd();
|
|
283
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
284
|
+
try {
|
|
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) }; }
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const graphifyShortestPathTool = {
|
|
326
|
+
name: 'graphify_shortest_path',
|
|
327
|
+
description: 'Find the shortest relationship path between two concepts in the knowledge graph. ' +
|
|
328
|
+
'Reveals coupling chains between any two components.',
|
|
329
|
+
category: 'graphify',
|
|
330
|
+
tags: ['knowledge-graph', 'path', 'dependencies', 'coupling'],
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
source: { type: 'string', description: 'Source concept label' },
|
|
335
|
+
target: { type: 'string', description: 'Target concept label' },
|
|
336
|
+
maxHops: { type: 'integer', default: 8 },
|
|
337
|
+
},
|
|
338
|
+
required: ['source', 'target'],
|
|
339
|
+
},
|
|
340
|
+
handler: async (params) => {
|
|
341
|
+
const cwd = getProjectCwd();
|
|
342
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
343
|
+
try {
|
|
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) }; }
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export const graphifyGetCommunityTool = {
|
|
418
|
+
name: 'graphify_community',
|
|
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.',
|
|
421
|
+
category: 'graphify',
|
|
422
|
+
tags: ['knowledge-graph', 'community', 'clusters', 'subsystems'],
|
|
423
|
+
inputSchema: { type: 'object', properties: { communityId: { type: 'integer', description: 'Community ID (0 = largest)' } }, required: ['communityId'] },
|
|
424
|
+
handler: async (params) => {
|
|
425
|
+
const cwd = getProjectCwd();
|
|
426
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
427
|
+
try {
|
|
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) }; }
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const graphifyStatsTool = {
|
|
460
|
+
name: 'graphify_stats',
|
|
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.',
|
|
463
|
+
category: 'graphify',
|
|
464
|
+
tags: ['knowledge-graph', 'stats', 'overview'],
|
|
465
|
+
inputSchema: { type: 'object', properties: {} },
|
|
466
|
+
handler: async (_params) => {
|
|
467
|
+
const cwd = getProjectCwd();
|
|
468
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(cwd)}` };
|
|
469
|
+
try {
|
|
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) }; }
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
export const graphifySurprisesTool = {
|
|
515
|
+
name: 'graphify_surprises',
|
|
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.',
|
|
518
|
+
category: 'graphify',
|
|
519
|
+
tags: ['knowledge-graph', 'architecture', 'coupling', 'surprises'],
|
|
520
|
+
inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 10 } } },
|
|
521
|
+
handler: async (params) => {
|
|
522
|
+
const cwd = getProjectCwd();
|
|
523
|
+
if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
524
|
+
try {
|
|
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) }; }
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
export const graphifyTools = [
|
|
554
|
+
graphifyBuildTool,
|
|
555
|
+
graphifyQueryTool,
|
|
556
|
+
graphifyGodNodesTool,
|
|
557
|
+
graphifyGetNodeTool,
|
|
558
|
+
graphifyShortestPathTool,
|
|
559
|
+
graphifyGetCommunityTool,
|
|
560
|
+
graphifyStatsTool,
|
|
561
|
+
graphifySurprisesTool,
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
export default graphifyTools;
|
|
@@ -23,4 +23,5 @@ export { wasmAgentTools } from './wasm-agent-tools.js';
|
|
|
23
23
|
export { ruvllmWasmTools } from './ruvllm-tools.js';
|
|
24
24
|
export { guidanceTools } from './guidance-tools.js';
|
|
25
25
|
export { autopilotTools } from './autopilot-tools.js';
|
|
26
|
+
export { graphifyTools } from './graphify-tools.js';
|
|
26
27
|
//# sourceMappingURL=index.js.map
|