@monoes/cli 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/appliance/rvfa-format.d.ts +1 -1
- package/dist/src/appliance/rvfa-format.d.ts.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +2 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/init/executor.d.ts +0 -3
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +50 -10
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/helpers-generator.d.ts.map +1 -1
- package/dist/src/init/helpers-generator.js +38 -1
- package/dist/src/init/helpers-generator.js.map +1 -1
- package/dist/src/init/mcp-generator.d.ts.map +1 -1
- package/dist/src/init/mcp-generator.js +2 -12
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/statusline-generator.d.ts +0 -3
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +9 -4
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +4 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js +5 -0
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/mcp-tools/graphify-tools.d.ts +52 -0
- package/dist/src/mcp-tools/graphify-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/graphify-tools.js +712 -258
- package/dist/src/mcp-tools/graphify-tools.js.map +1 -0
- package/dist/src/mcp-tools/index.d.ts +1 -0
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/update/checker.d.ts.map +1 -1
- package/dist/src/update/checker.js +2 -5
- package/dist/src/update/checker.js.map +1 -1
- package/dist/src/workflow/dsl-schema.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -1,222 +1,450 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Graphify MCP Tools
|
|
3
|
-
*
|
|
2
|
+
* Graphify MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Bridges @monobrain/graph's knowledge graph into monobrain's MCP tool surface.
|
|
5
|
+
* Agents can query the codebase knowledge graph without reading files —
|
|
6
|
+
* god_nodes(), query_graph(), shortest_path() give structural understanding
|
|
7
|
+
* in milliseconds vs. reading dozens of source files.
|
|
8
|
+
*
|
|
9
|
+
* Graph is built automatically on `monobrain init` and stored at
|
|
10
|
+
* .monobrain/graph/graph.json (legacy: graphify-out/graph.json).
|
|
11
|
+
* Rebuild manually: call graphify_build via MCP.
|
|
4
12
|
*/
|
|
5
|
-
import { spawnSync } from 'child_process';
|
|
6
13
|
import { existsSync, readFileSync } from 'fs';
|
|
7
14
|
import { join, resolve } from 'path';
|
|
8
15
|
import { getProjectCwd } from './types.js';
|
|
9
|
-
|
|
16
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
17
|
+
/** Resolve graph path: prefer native monobrain path, fall back to legacy graphify path. */
|
|
10
18
|
function getGraphPath(cwd) {
|
|
11
|
-
|
|
19
|
+
const nativePath = resolve(join(cwd, '.monobrain', 'graph', 'graph.json'));
|
|
20
|
+
const legacyPath = resolve(join(cwd, 'graphify-out', 'graph.json'));
|
|
21
|
+
if (existsSync(nativePath))
|
|
22
|
+
return nativePath;
|
|
23
|
+
if (existsSync(legacyPath))
|
|
24
|
+
return legacyPath;
|
|
25
|
+
return nativePath; // return expected path even if not yet built
|
|
12
26
|
}
|
|
13
|
-
|
|
14
|
-
function isGraphifyInstalled() {
|
|
15
|
-
try {
|
|
16
|
-
const result = spawnSync('python', ['-c', 'import graphify; print("ok")'], {
|
|
17
|
-
timeout: 5000, encoding: 'utf-8',
|
|
18
|
-
});
|
|
19
|
-
return result.status === 0 && result.stdout.includes('ok');
|
|
20
|
-
}
|
|
21
|
-
catch { return false; }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
27
|
function graphExists(cwd) {
|
|
25
28
|
return existsSync(getGraphPath(cwd));
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Load the knowledge graph.
|
|
32
|
+
* Tries @monobrain/graph's loadGraph first; falls back to parsing raw JSON.
|
|
33
|
+
*/
|
|
34
|
+
async function loadKnowledgeGraph(cwd) {
|
|
29
35
|
const graphPath = getGraphPath(cwd);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
let rawNodes = [];
|
|
37
|
+
let rawEdges = [];
|
|
38
|
+
try {
|
|
39
|
+
// Prefer @monobrain/graph's loader which handles format normalization.
|
|
40
|
+
const { loadGraph } = await import('@monoes/graph');
|
|
41
|
+
const loaded = loadGraph(graphPath);
|
|
42
|
+
rawNodes = loaded.nodes;
|
|
43
|
+
rawEdges = loaded.edges;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fallback: parse JSON directly
|
|
47
|
+
const data = JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
48
|
+
rawNodes = data.nodes || [];
|
|
49
|
+
rawEdges = data.links || data.edges || [];
|
|
50
|
+
}
|
|
51
|
+
// Build in-memory graph structures
|
|
52
|
+
const nodes = new Map();
|
|
53
|
+
for (const n of rawNodes) {
|
|
54
|
+
nodes.set(n.id, n);
|
|
55
|
+
}
|
|
56
|
+
const adj = new Map();
|
|
57
|
+
const radj = new Map();
|
|
58
|
+
const degree = new Map();
|
|
59
|
+
for (const n of rawNodes) {
|
|
60
|
+
adj.set(n.id, []);
|
|
61
|
+
radj.set(n.id, []);
|
|
62
|
+
degree.set(n.id, 0);
|
|
43
63
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
for (const e of rawEdges) {
|
|
65
|
+
const src = e.source;
|
|
66
|
+
const tgt = e.target;
|
|
67
|
+
adj.get(src)?.push(tgt);
|
|
68
|
+
radj.get(tgt)?.push(src);
|
|
69
|
+
degree.set(src, (degree.get(src) ?? 0) + 1);
|
|
70
|
+
degree.set(tgt, (degree.get(tgt) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
return { nodes, adj, radj, edges: rawEdges, degree, graphPath };
|
|
73
|
+
}
|
|
74
|
+
// ── Shared output helpers ─────────────────────────────────────────────────────
|
|
75
|
+
function nodeOut(g, id) {
|
|
76
|
+
const d = g.nodes.get(id) ?? { id };
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
label: d.label ?? id,
|
|
80
|
+
file: d.source_file ?? '',
|
|
81
|
+
location: d.source_location ?? '',
|
|
82
|
+
community: d.community ?? null,
|
|
83
|
+
degree: g.degree.get(id) ?? 0,
|
|
84
|
+
file_type: d.file_type ?? '',
|
|
85
|
+
};
|
|
47
86
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
/** Score a node against search terms. */
|
|
88
|
+
function scoreNode(g, id, terms) {
|
|
89
|
+
const d = g.nodes.get(id);
|
|
90
|
+
if (!d)
|
|
91
|
+
return 0;
|
|
92
|
+
const label = (d.label ?? id).toLowerCase();
|
|
93
|
+
const file = (d.source_file ?? '').toLowerCase();
|
|
94
|
+
return terms.reduce((s, t) => {
|
|
95
|
+
if (label.includes(t))
|
|
96
|
+
s += 1;
|
|
97
|
+
if (file.includes(t))
|
|
98
|
+
s += 0.5;
|
|
99
|
+
return s;
|
|
100
|
+
}, 0);
|
|
101
|
+
}
|
|
102
|
+
// ── Tool Definitions ──────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Build or rebuild the knowledge graph for a directory.
|
|
105
|
+
*/
|
|
61
106
|
export const graphifyBuildTool = {
|
|
62
107
|
name: 'graphify_build',
|
|
63
|
-
description: 'Build (or rebuild) the knowledge graph for a project directory.
|
|
108
|
+
description: 'Build (or rebuild) the knowledge graph for a project directory. ' +
|
|
109
|
+
'Extracts AST structure from code files, semantic relationships from docs, ' +
|
|
110
|
+
'and clusters them into communities. Run this first before using other graphify tools. ' +
|
|
111
|
+
'Code-only changes are fast (tree-sitter, no LLM). Doc changes require an LLM call.',
|
|
64
112
|
category: 'graphify',
|
|
65
113
|
tags: ['knowledge-graph', 'codebase', 'architecture', 'analysis'],
|
|
66
114
|
inputSchema: {
|
|
67
115
|
type: 'object',
|
|
68
116
|
properties: {
|
|
69
|
-
path: {
|
|
70
|
-
|
|
117
|
+
path: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Path to analyse (defaults to current project root)',
|
|
120
|
+
},
|
|
121
|
+
codeOnly: {
|
|
122
|
+
type: 'boolean',
|
|
123
|
+
description: 'Only re-extract changed code files — no LLM, fast rebuild',
|
|
124
|
+
default: false,
|
|
125
|
+
},
|
|
71
126
|
},
|
|
72
127
|
},
|
|
73
128
|
handler: async (params) => {
|
|
74
129
|
const cwd = getProjectCwd();
|
|
75
130
|
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
131
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
132
|
+
const { buildGraph } = await import('@monoes/graph');
|
|
133
|
+
const outputDir = join(targetPath, '.monobrain', 'graph');
|
|
134
|
+
const result = await buildGraph(targetPath, {
|
|
135
|
+
codeOnly: Boolean(params.codeOnly),
|
|
136
|
+
outputDir,
|
|
82
137
|
});
|
|
83
|
-
const graphPath = getGraphPath(cwd);
|
|
84
|
-
const built = existsSync(graphPath);
|
|
85
138
|
return {
|
|
86
|
-
success:
|
|
87
|
-
graphPath:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
139
|
+
success: true,
|
|
140
|
+
graphPath: result.graphPath,
|
|
141
|
+
filesProcessed: result.filesProcessed,
|
|
142
|
+
nodes: result.analysis.stats.nodes,
|
|
143
|
+
edges: result.analysis.stats.edges,
|
|
144
|
+
message: `Knowledge graph built at ${result.graphPath}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return {
|
|
149
|
+
error: true,
|
|
150
|
+
message: String(err),
|
|
151
|
+
hint: '@monobrain/graph package not available — ensure it is installed and built.',
|
|
91
152
|
};
|
|
92
153
|
}
|
|
93
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
94
154
|
},
|
|
95
155
|
};
|
|
96
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Query the knowledge graph with natural language.
|
|
158
|
+
*/
|
|
97
159
|
export const graphifyQueryTool = {
|
|
98
160
|
name: 'graphify_query',
|
|
99
|
-
description: 'Search the knowledge graph with a natural language question or keywords.
|
|
161
|
+
description: 'Search the knowledge graph with a natural language question or keywords. ' +
|
|
162
|
+
'Returns relevant nodes and edges as structured context — use this instead of reading ' +
|
|
163
|
+
'many files when you want to understand how components relate. ' +
|
|
164
|
+
'BFS mode gives broad context; DFS traces a specific call path.',
|
|
100
165
|
category: 'graphify',
|
|
101
166
|
tags: ['knowledge-graph', 'search', 'architecture', 'codebase'],
|
|
102
167
|
inputSchema: {
|
|
103
168
|
type: 'object',
|
|
104
169
|
properties: {
|
|
105
|
-
question: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
question: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Natural language question or keyword (e.g. "authentication flow", "how does caching work")',
|
|
173
|
+
},
|
|
174
|
+
mode: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
enum: ['bfs', 'dfs'],
|
|
177
|
+
default: 'bfs',
|
|
178
|
+
description: 'bfs = broad context, dfs = trace specific path',
|
|
179
|
+
},
|
|
180
|
+
depth: {
|
|
181
|
+
type: 'integer',
|
|
182
|
+
default: 3,
|
|
183
|
+
description: 'Traversal depth (1–6)',
|
|
184
|
+
},
|
|
185
|
+
tokenBudget: {
|
|
186
|
+
type: 'integer',
|
|
187
|
+
default: 2000,
|
|
188
|
+
description: 'Approximate max output tokens',
|
|
189
|
+
},
|
|
109
190
|
},
|
|
110
191
|
required: ['question'],
|
|
111
192
|
},
|
|
112
193
|
handler: async (params) => {
|
|
113
194
|
const cwd = getProjectCwd();
|
|
114
|
-
if (!graphExists(cwd))
|
|
195
|
+
if (!graphExists(cwd)) {
|
|
196
|
+
return {
|
|
197
|
+
error: true,
|
|
198
|
+
message: 'No graph found. Run graphify_build first.',
|
|
199
|
+
hint: `Expected: ${getGraphPath(cwd)}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const question = params.question;
|
|
203
|
+
const mode = params.mode || 'bfs';
|
|
204
|
+
const depth = params.depth || 3;
|
|
115
205
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
207
|
+
const terms = question.toLowerCase().split(/\s+/).filter(t => t.length > 2);
|
|
208
|
+
// Score nodes; fall back to highest-degree nodes if no match
|
|
209
|
+
let startNodes = [];
|
|
210
|
+
if (terms.length > 0) {
|
|
211
|
+
const scored = [];
|
|
212
|
+
for (const id of g.nodes.keys()) {
|
|
213
|
+
const s = scoreNode(g, id, terms);
|
|
214
|
+
if (s > 0)
|
|
215
|
+
scored.push([s, id]);
|
|
216
|
+
}
|
|
217
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
218
|
+
startNodes = scored.slice(0, 5).map(([, id]) => id);
|
|
219
|
+
}
|
|
220
|
+
if (startNodes.length === 0) {
|
|
221
|
+
startNodes = [...g.nodes.keys()]
|
|
222
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
223
|
+
.slice(0, 3);
|
|
224
|
+
}
|
|
225
|
+
const visited = new Set(startNodes);
|
|
226
|
+
const edgesSeen = [];
|
|
227
|
+
if (mode === 'bfs') {
|
|
228
|
+
let frontier = new Set(startNodes);
|
|
229
|
+
for (let d = 0; d < depth; d++) {
|
|
230
|
+
const next = new Set();
|
|
231
|
+
for (const n of frontier) {
|
|
232
|
+
for (const nbr of g.adj.get(n) ?? []) {
|
|
233
|
+
if (!visited.has(nbr)) {
|
|
234
|
+
next.add(nbr);
|
|
235
|
+
edgesSeen.push([n, nbr]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const n of next)
|
|
240
|
+
visited.add(n);
|
|
241
|
+
frontier = next;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// DFS
|
|
246
|
+
const stack = startNodes.map(n => [n, 0]);
|
|
247
|
+
while (stack.length > 0) {
|
|
248
|
+
const [node, d] = stack.pop();
|
|
249
|
+
if (visited.has(node) && d > 0)
|
|
250
|
+
continue;
|
|
251
|
+
if (d > depth)
|
|
252
|
+
continue;
|
|
253
|
+
visited.add(node);
|
|
254
|
+
for (const nbr of g.adj.get(node) ?? []) {
|
|
255
|
+
if (!visited.has(nbr)) {
|
|
256
|
+
stack.push([nbr, d + 1]);
|
|
257
|
+
edgesSeen.push([node, nbr]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const nodesOut = [...visited]
|
|
263
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
264
|
+
.slice(0, 60)
|
|
265
|
+
.map(id => nodeOut(g, id));
|
|
266
|
+
// Build edge lookup for attribute access
|
|
267
|
+
const edgeLookup = new Map();
|
|
268
|
+
for (const e of g.edges) {
|
|
269
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
270
|
+
}
|
|
271
|
+
const edgesOut = edgesSeen
|
|
272
|
+
.filter(([u, v]) => visited.has(u) && visited.has(v))
|
|
273
|
+
.slice(0, 80)
|
|
274
|
+
.map(([u, v]) => {
|
|
275
|
+
const e = edgeLookup.get(`${u}__${v}`) ?? {};
|
|
276
|
+
return {
|
|
277
|
+
from: (g.nodes.get(u)?.label ?? u),
|
|
278
|
+
to: (g.nodes.get(v)?.label ?? v),
|
|
279
|
+
relation: e.relation ?? '',
|
|
280
|
+
confidence: e.confidence ?? '',
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
question,
|
|
285
|
+
mode,
|
|
286
|
+
depth,
|
|
287
|
+
nodes: nodesOut,
|
|
288
|
+
edges: edgesOut,
|
|
289
|
+
total_nodes: visited.size,
|
|
290
|
+
total_edges: edgesSeen.length,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
return { error: true, message: String(err) };
|
|
160
295
|
}
|
|
161
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
162
296
|
},
|
|
163
297
|
};
|
|
164
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Get the most connected (god) nodes — the core abstractions.
|
|
300
|
+
*/
|
|
165
301
|
export const graphifyGodNodesTool = {
|
|
166
302
|
name: 'graphify_god_nodes',
|
|
167
|
-
description: 'Return the most connected nodes in the knowledge graph — the core abstractions
|
|
303
|
+
description: 'Return the most connected nodes in the knowledge graph — the core abstractions ' +
|
|
304
|
+
'and central concepts of the codebase. Use this at the start of any architectural analysis ' +
|
|
305
|
+
'to understand what the most important components are before diving into details.',
|
|
168
306
|
category: 'graphify',
|
|
169
307
|
tags: ['knowledge-graph', 'architecture', 'abstractions', 'codebase'],
|
|
170
|
-
inputSchema: {
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
topN: {
|
|
312
|
+
type: 'integer',
|
|
313
|
+
default: 15,
|
|
314
|
+
description: 'Number of god nodes to return',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
171
318
|
handler: async (params) => {
|
|
172
319
|
const cwd = getProjectCwd();
|
|
173
|
-
if (!graphExists(cwd))
|
|
320
|
+
if (!graphExists(cwd)) {
|
|
321
|
+
return {
|
|
322
|
+
error: true,
|
|
323
|
+
message: 'No graph found. Run graphify_build first.',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const topN = params.topN || 15;
|
|
174
327
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
328
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
329
|
+
const sortedIds = [...g.nodes.keys()]
|
|
330
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
331
|
+
.slice(0, topN);
|
|
332
|
+
const godNodes = sortedIds.map(id => {
|
|
333
|
+
const d = g.nodes.get(id) ?? { id };
|
|
334
|
+
const neighbors = (g.adj.get(id) ?? [])
|
|
335
|
+
.slice(0, 8)
|
|
336
|
+
.map(nid => g.nodes.get(nid)?.label ?? nid);
|
|
337
|
+
return {
|
|
338
|
+
label: d.label ?? id,
|
|
339
|
+
degree: g.degree.get(id) ?? 0,
|
|
340
|
+
file: d.source_file ?? '',
|
|
341
|
+
location: d.source_location ?? '',
|
|
342
|
+
community: d.community ?? null,
|
|
343
|
+
file_type: d.file_type ?? '',
|
|
344
|
+
neighbors,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
return { god_nodes: godNodes, total_nodes: g.nodes.size };
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return { error: true, message: String(err) };
|
|
183
351
|
}
|
|
184
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
185
352
|
},
|
|
186
353
|
};
|
|
187
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Get full details for a specific node.
|
|
356
|
+
*/
|
|
188
357
|
export const graphifyGetNodeTool = {
|
|
189
358
|
name: 'graphify_get_node',
|
|
190
|
-
description: 'Get all details for a specific concept/node in the knowledge graph:
|
|
359
|
+
description: 'Get all details for a specific concept/node in the knowledge graph: ' +
|
|
360
|
+
'its source location, community, all relationships, and confidence levels. ' +
|
|
361
|
+
'Use this when you need to deeply understand one specific component.',
|
|
191
362
|
category: 'graphify',
|
|
192
363
|
tags: ['knowledge-graph', 'node', 'details'],
|
|
193
|
-
inputSchema: {
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
label: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
description: 'Node label or ID to look up (case-insensitive)',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
required: ['label'],
|
|
373
|
+
},
|
|
194
374
|
handler: async (params) => {
|
|
195
375
|
const cwd = getProjectCwd();
|
|
196
|
-
if (!graphExists(cwd))
|
|
376
|
+
if (!graphExists(cwd)) {
|
|
377
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
378
|
+
}
|
|
197
379
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
380
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
381
|
+
const term = params.label.toLowerCase();
|
|
382
|
+
const matches = [...g.nodes.entries()]
|
|
383
|
+
.filter(([id, d]) => (d.label ?? id).toLowerCase().includes(term) || id.toLowerCase() === term)
|
|
384
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
385
|
+
.map(([id]) => id);
|
|
386
|
+
if (matches.length === 0) {
|
|
387
|
+
return { error: 'Node not found', searched: term };
|
|
388
|
+
}
|
|
389
|
+
const id = matches[0];
|
|
390
|
+
const d = g.nodes.get(id) ?? { id };
|
|
391
|
+
// Build edge lookup
|
|
392
|
+
const edgeLookup = new Map();
|
|
393
|
+
for (const e of g.edges) {
|
|
394
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
395
|
+
}
|
|
396
|
+
const outEdges = (g.adj.get(id) ?? []).slice(0, 40).map(tgt => {
|
|
397
|
+
const e = edgeLookup.get(`${id}__${tgt}`) ?? {};
|
|
398
|
+
return {
|
|
399
|
+
direction: 'outgoing',
|
|
400
|
+
to: g.nodes.get(tgt)?.label ?? tgt,
|
|
401
|
+
relation: e.relation ?? '',
|
|
402
|
+
confidence: e.confidence ?? '',
|
|
403
|
+
confidence_score: e.confidence_score ?? null,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
const inEdges = (g.radj.get(id) ?? []).slice(0, 40).map(src => {
|
|
407
|
+
const e = edgeLookup.get(`${src}__${id}`) ?? {};
|
|
408
|
+
return {
|
|
409
|
+
direction: 'incoming',
|
|
410
|
+
from: g.nodes.get(src)?.label ?? src,
|
|
411
|
+
relation: e.relation ?? '',
|
|
412
|
+
confidence: e.confidence ?? '',
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
// Strip well-known fields from attributes output to avoid duplication
|
|
416
|
+
const knownKeys = new Set(['label', 'source_file', 'source_location', 'community', 'file_type', 'id']);
|
|
417
|
+
const attributes = {};
|
|
418
|
+
for (const [k, v] of Object.entries(d)) {
|
|
419
|
+
if (!knownKeys.has(k))
|
|
420
|
+
attributes[k] = v;
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
id,
|
|
424
|
+
label: d.label ?? id,
|
|
425
|
+
file: d.source_file ?? '',
|
|
426
|
+
location: d.source_location ?? '',
|
|
427
|
+
community: d.community ?? null,
|
|
428
|
+
file_type: d.file_type ?? '',
|
|
429
|
+
degree: g.degree.get(id) ?? 0,
|
|
430
|
+
attributes,
|
|
431
|
+
edges: [...outEdges, ...inEdges],
|
|
432
|
+
all_matches: matches.slice(0, 10).map(m => g.nodes.get(m)?.label ?? m),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
return { error: true, message: String(err) };
|
|
212
437
|
}
|
|
213
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
214
438
|
},
|
|
215
439
|
};
|
|
216
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Find shortest path between two concepts.
|
|
442
|
+
*/
|
|
217
443
|
export const graphifyShortestPathTool = {
|
|
218
444
|
name: 'graphify_shortest_path',
|
|
219
|
-
description: 'Find the shortest relationship path between two concepts in the knowledge graph.
|
|
445
|
+
description: 'Find the shortest relationship path between two concepts in the knowledge graph. ' +
|
|
446
|
+
'Use this to trace how component A depends on or relates to component B, ' +
|
|
447
|
+
'revealing hidden coupling chains (e.g. "how does the router connect to the database?").',
|
|
220
448
|
category: 'graphify',
|
|
221
449
|
tags: ['knowledge-graph', 'path', 'dependencies', 'coupling'],
|
|
222
450
|
inputSchema: {
|
|
@@ -224,154 +452,379 @@ export const graphifyShortestPathTool = {
|
|
|
224
452
|
properties: {
|
|
225
453
|
source: { type: 'string', description: 'Source concept label' },
|
|
226
454
|
target: { type: 'string', description: 'Target concept label' },
|
|
227
|
-
maxHops: { type: 'integer', default: 8 },
|
|
455
|
+
maxHops: { type: 'integer', default: 8, description: 'Maximum hops to search' },
|
|
228
456
|
},
|
|
229
457
|
required: ['source', 'target'],
|
|
230
458
|
},
|
|
231
459
|
handler: async (params) => {
|
|
232
460
|
const cwd = getProjectCwd();
|
|
233
|
-
if (!graphExists(cwd))
|
|
461
|
+
if (!graphExists(cwd)) {
|
|
462
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
463
|
+
}
|
|
234
464
|
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
|
-
|
|
465
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
466
|
+
const maxHops = params.maxHops || 8;
|
|
467
|
+
/** Find node ids matching a search term, sorted by degree descending. */
|
|
468
|
+
function findNodes(term) {
|
|
469
|
+
const t = term.toLowerCase();
|
|
470
|
+
return [...g.nodes.entries()]
|
|
471
|
+
.filter(([id, d]) => (d.label ?? id).toLowerCase().includes(t) || id.toLowerCase() === t)
|
|
472
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
473
|
+
.map(([id]) => id);
|
|
474
|
+
}
|
|
475
|
+
const srcNodes = findNodes(params.source);
|
|
476
|
+
const tgtNodes = findNodes(params.target);
|
|
477
|
+
if (srcNodes.length === 0) {
|
|
478
|
+
return { error: true, message: `Source not found: ${params.source}` };
|
|
479
|
+
}
|
|
480
|
+
if (tgtNodes.length === 0) {
|
|
481
|
+
return { error: true, message: `Target not found: ${params.target}` };
|
|
482
|
+
}
|
|
483
|
+
// BFS on undirected graph (use both adj and radj as neighbours)
|
|
484
|
+
function bfsPath(start, end) {
|
|
485
|
+
const prev = new Map();
|
|
486
|
+
const queue = [start];
|
|
487
|
+
const visited = new Set([start]);
|
|
488
|
+
while (queue.length > 0) {
|
|
489
|
+
const cur = queue.shift();
|
|
490
|
+
if (cur === end) {
|
|
491
|
+
// Reconstruct path
|
|
492
|
+
const path = [];
|
|
493
|
+
let node = end;
|
|
494
|
+
while (node !== undefined) {
|
|
495
|
+
path.unshift(node);
|
|
496
|
+
node = prev.get(node);
|
|
497
|
+
}
|
|
498
|
+
return path.length - 1 <= maxHops ? path : null;
|
|
499
|
+
}
|
|
500
|
+
// Treat edges as undirected
|
|
501
|
+
const nbrs = [...(g.adj.get(cur) ?? []), ...(g.radj.get(cur) ?? [])];
|
|
502
|
+
for (const nbr of nbrs) {
|
|
503
|
+
if (!visited.has(nbr)) {
|
|
504
|
+
visited.add(nbr);
|
|
505
|
+
prev.set(nbr, cur);
|
|
506
|
+
if (queue.length < 100000)
|
|
507
|
+
queue.push(nbr);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
let bestPath = null;
|
|
514
|
+
for (const src of srcNodes.slice(0, 3)) {
|
|
515
|
+
for (const tgt of tgtNodes.slice(0, 3)) {
|
|
516
|
+
const p = bfsPath(src, tgt);
|
|
517
|
+
if (p && (!bestPath || p.length < bestPath.length)) {
|
|
518
|
+
bestPath = p;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (!bestPath) {
|
|
523
|
+
return {
|
|
524
|
+
found: false,
|
|
525
|
+
message: `No path within ${maxHops} hops between "${params.source}" and "${params.target}"`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// Build edge lookup
|
|
529
|
+
const edgeLookup = new Map();
|
|
530
|
+
for (const e of g.edges) {
|
|
531
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
532
|
+
edgeLookup.set(`${e.target}__${e.source}`, e); // bidirectional lookup
|
|
533
|
+
}
|
|
534
|
+
const steps = bestPath.map((id, i) => {
|
|
535
|
+
const d = g.nodes.get(id) ?? { id };
|
|
536
|
+
const step = {
|
|
537
|
+
label: d.label ?? id,
|
|
538
|
+
file: d.source_file ?? '',
|
|
539
|
+
location: d.source_location ?? '',
|
|
540
|
+
};
|
|
541
|
+
if (i < bestPath.length - 1) {
|
|
542
|
+
const nextId = bestPath[i + 1];
|
|
543
|
+
const e = edgeLookup.get(`${id}__${nextId}`) ?? edgeLookup.get(`${nextId}__${id}`) ?? {};
|
|
544
|
+
step.next_relation = e.relation ?? '';
|
|
545
|
+
step.confidence = e.confidence ?? '';
|
|
546
|
+
}
|
|
547
|
+
return step;
|
|
548
|
+
});
|
|
549
|
+
return { found: true, hops: bestPath.length - 1, path: steps };
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
return { error: true, message: String(err) };
|
|
275
553
|
}
|
|
276
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
277
554
|
},
|
|
278
555
|
};
|
|
279
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Get all nodes in a community (cluster of related components).
|
|
558
|
+
*/
|
|
280
559
|
export const graphifyGetCommunityTool = {
|
|
281
560
|
name: 'graphify_community',
|
|
282
|
-
description: 'Get all nodes in a specific community cluster. Communities are groups of
|
|
561
|
+
description: 'Get all nodes in a specific community cluster. Communities are groups of ' +
|
|
562
|
+
'tightly related components detected by graph clustering. Use graph_stats first to ' +
|
|
563
|
+
'see community count, then explore communities to understand subsystem boundaries.',
|
|
283
564
|
category: 'graphify',
|
|
284
565
|
tags: ['knowledge-graph', 'community', 'clusters', 'subsystems'],
|
|
285
|
-
inputSchema: {
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: 'object',
|
|
568
|
+
properties: {
|
|
569
|
+
communityId: {
|
|
570
|
+
type: 'integer',
|
|
571
|
+
description: 'Community ID (0 = largest community)',
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
required: ['communityId'],
|
|
575
|
+
},
|
|
286
576
|
handler: async (params) => {
|
|
287
577
|
const cwd = getProjectCwd();
|
|
288
|
-
if (!graphExists(cwd))
|
|
578
|
+
if (!graphExists(cwd)) {
|
|
579
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
580
|
+
}
|
|
289
581
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
582
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
583
|
+
const cid = params.communityId;
|
|
584
|
+
const members = [...g.nodes.entries()]
|
|
585
|
+
.filter(([, d]) => d.community === cid)
|
|
586
|
+
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
587
|
+
.slice(0, 50)
|
|
588
|
+
.map(([id, d]) => ({
|
|
589
|
+
label: d.label ?? id,
|
|
590
|
+
file: d.source_file ?? '',
|
|
591
|
+
location: d.source_location ?? '',
|
|
592
|
+
degree: g.degree.get(id) ?? 0,
|
|
593
|
+
file_type: d.file_type ?? '',
|
|
594
|
+
}));
|
|
595
|
+
// Build edge lookup
|
|
596
|
+
const edgeLookup = new Map();
|
|
597
|
+
for (const e of g.edges) {
|
|
598
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
599
|
+
}
|
|
600
|
+
const externalEdges = [];
|
|
601
|
+
for (const [id, d] of g.nodes.entries()) {
|
|
602
|
+
if (d.community !== cid)
|
|
603
|
+
continue;
|
|
604
|
+
for (const nbr of g.adj.get(id) ?? []) {
|
|
605
|
+
const nbrD = g.nodes.get(nbr);
|
|
606
|
+
if (nbrD?.community !== cid) {
|
|
607
|
+
const e = edgeLookup.get(`${id}__${nbr}`) ?? {};
|
|
608
|
+
externalEdges.push({
|
|
609
|
+
from: d.label ?? id,
|
|
610
|
+
to: nbrD?.label ?? nbr,
|
|
611
|
+
to_community: nbrD?.community ?? null,
|
|
612
|
+
relation: e.relation ?? '',
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
community_id: cid,
|
|
619
|
+
member_count: members.length,
|
|
620
|
+
members,
|
|
621
|
+
external_connections: externalEdges.slice(0, 30),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
return { error: true, message: String(err) };
|
|
304
626
|
}
|
|
305
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
306
627
|
},
|
|
307
628
|
};
|
|
308
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Get graph statistics: node/edge counts, communities, confidence breakdown.
|
|
631
|
+
*/
|
|
309
632
|
export const graphifyStatsTool = {
|
|
310
633
|
name: 'graphify_stats',
|
|
311
|
-
description: 'Get summary statistics for the knowledge graph: node count, edge count,
|
|
634
|
+
description: 'Get summary statistics for the knowledge graph: node count, edge count, ' +
|
|
635
|
+
'community count, confidence breakdown (EXTRACTED/INFERRED/AMBIGUOUS), ' +
|
|
636
|
+
'and top god nodes. Use this first to understand graph size and structure.',
|
|
312
637
|
category: 'graphify',
|
|
313
638
|
tags: ['knowledge-graph', 'stats', 'overview'],
|
|
314
|
-
inputSchema: {
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
properties: {},
|
|
642
|
+
},
|
|
315
643
|
handler: async (_params) => {
|
|
316
644
|
const cwd = getProjectCwd();
|
|
317
|
-
if (!graphExists(cwd))
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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) }; }
|
|
645
|
+
if (!graphExists(cwd)) {
|
|
646
|
+
return {
|
|
647
|
+
error: true,
|
|
648
|
+
message: 'No graph found. Run graphify_build first.',
|
|
649
|
+
hint: `Expected: ${getGraphPath(cwd)}`,
|
|
650
|
+
};
|
|
326
651
|
}
|
|
327
652
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
653
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
654
|
+
// Community sizes
|
|
655
|
+
const communities = new Map();
|
|
656
|
+
for (const d of g.nodes.values()) {
|
|
657
|
+
if (d.community != null) {
|
|
658
|
+
communities.set(d.community, (communities.get(d.community) ?? 0) + 1);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const communitySizes = {};
|
|
662
|
+
[...communities.entries()]
|
|
663
|
+
.sort((a, b) => b[1] - a[1])
|
|
664
|
+
.slice(0, 10)
|
|
665
|
+
.forEach(([cid, count]) => { communitySizes[String(cid)] = count; });
|
|
666
|
+
// Confidence and relation counts
|
|
667
|
+
const confidenceCounts = {};
|
|
668
|
+
const relationCounts = {};
|
|
669
|
+
const fileTypeCounts = {};
|
|
670
|
+
for (const e of g.edges) {
|
|
671
|
+
const conf = e.confidence ?? 'UNKNOWN';
|
|
672
|
+
confidenceCounts[conf] = (confidenceCounts[conf] ?? 0) + 1;
|
|
673
|
+
const rel = e.relation ?? 'unknown';
|
|
674
|
+
relationCounts[rel] = (relationCounts[rel] ?? 0) + 1;
|
|
675
|
+
}
|
|
676
|
+
for (const d of g.nodes.values()) {
|
|
677
|
+
const ft = d.file_type ?? 'unknown';
|
|
678
|
+
fileTypeCounts[ft] = (fileTypeCounts[ft] ?? 0) + 1;
|
|
679
|
+
}
|
|
680
|
+
const topRelations = Object.entries(relationCounts)
|
|
681
|
+
.sort((a, b) => b[1] - a[1])
|
|
682
|
+
.slice(0, 10)
|
|
683
|
+
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
|
|
684
|
+
const topGodNodes = [...g.nodes.keys()]
|
|
685
|
+
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
686
|
+
.slice(0, 5)
|
|
687
|
+
.map(id => g.nodes.get(id)?.label ?? id);
|
|
688
|
+
return {
|
|
689
|
+
nodes: g.nodes.size,
|
|
690
|
+
edges: g.edges.length,
|
|
691
|
+
communities: communities.size,
|
|
692
|
+
community_sizes: communitySizes,
|
|
693
|
+
confidence: confidenceCounts,
|
|
694
|
+
top_relations: topRelations,
|
|
695
|
+
file_types: fileTypeCounts,
|
|
696
|
+
graph_path: g.graphPath,
|
|
697
|
+
top_god_nodes: topGodNodes,
|
|
698
|
+
is_directed: true,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
return { error: true, message: String(err) };
|
|
343
703
|
}
|
|
344
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
345
704
|
},
|
|
346
705
|
};
|
|
347
|
-
|
|
706
|
+
/**
|
|
707
|
+
* Find surprising cross-community connections (architectural insights).
|
|
708
|
+
*/
|
|
348
709
|
export const graphifySurprisesTool = {
|
|
349
710
|
name: 'graphify_surprises',
|
|
350
|
-
description: 'Find surprising connections between components in different communities
|
|
711
|
+
description: 'Find surprising connections between components that are in different communities ' +
|
|
712
|
+
'but have strong relationships. These unexpected couplings often reveal hidden dependencies, ' +
|
|
713
|
+
'design smells, or important architectural patterns worth understanding.',
|
|
351
714
|
category: 'graphify',
|
|
352
715
|
tags: ['knowledge-graph', 'architecture', 'coupling', 'surprises'],
|
|
353
|
-
inputSchema: {
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {
|
|
719
|
+
topN: {
|
|
720
|
+
type: 'integer',
|
|
721
|
+
default: 10,
|
|
722
|
+
description: 'Number of surprising connections to return',
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
},
|
|
354
726
|
handler: async (params) => {
|
|
355
727
|
const cwd = getProjectCwd();
|
|
356
|
-
if (!graphExists(cwd))
|
|
728
|
+
if (!graphExists(cwd)) {
|
|
729
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
730
|
+
}
|
|
357
731
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
732
|
+
const g = await loadKnowledgeGraph(cwd);
|
|
733
|
+
const topN = params.topN || 10;
|
|
734
|
+
const surprises = [];
|
|
735
|
+
for (const e of g.edges) {
|
|
736
|
+
const uD = g.nodes.get(e.source);
|
|
737
|
+
const vD = g.nodes.get(e.target);
|
|
738
|
+
const cu = uD?.community ?? null;
|
|
739
|
+
const cv = vD?.community ?? null;
|
|
740
|
+
if (cu != null && cv != null && cu !== cv) {
|
|
741
|
+
const score = (g.degree.get(e.source) ?? 0) * (g.degree.get(e.target) ?? 0);
|
|
742
|
+
surprises.push({
|
|
743
|
+
score,
|
|
744
|
+
from: uD?.label ?? e.source,
|
|
745
|
+
from_community: cu,
|
|
746
|
+
from_file: uD?.source_file ?? '',
|
|
747
|
+
to: vD?.label ?? e.target,
|
|
748
|
+
to_community: cv,
|
|
749
|
+
to_file: vD?.source_file ?? '',
|
|
750
|
+
relation: e.relation ?? '',
|
|
751
|
+
confidence: e.confidence ?? '',
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
surprises.sort((a, b) => b.score - a.score);
|
|
756
|
+
return {
|
|
757
|
+
surprises: surprises.slice(0, topN),
|
|
758
|
+
total_cross_community_edges: surprises.length,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
return { error: true, message: String(err) };
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
/**
|
|
767
|
+
* Generate or open the interactive HTML visualization of the knowledge graph.
|
|
768
|
+
*/
|
|
769
|
+
export const graphifyVisualizeTool = {
|
|
770
|
+
name: 'graphify_visualize',
|
|
771
|
+
description: 'Generate (and optionally open) the interactive HTML knowledge graph explorer. ' +
|
|
772
|
+
'Produces graph.html alongside graph.json — a self-contained visualization with force-directed ' +
|
|
773
|
+
'layout, community coloring, god-node panel, sidebar details, minimap, and search. ' +
|
|
774
|
+
'No internet connection or server required — just open the HTML file in a browser.',
|
|
775
|
+
category: 'graphify',
|
|
776
|
+
tags: ['knowledge-graph', 'visualization', 'html', 'browser'],
|
|
777
|
+
inputSchema: {
|
|
778
|
+
type: 'object',
|
|
779
|
+
properties: {
|
|
780
|
+
open: {
|
|
781
|
+
type: 'boolean',
|
|
782
|
+
description: 'Open the HTML file in the default browser after generating (macOS/Linux)',
|
|
783
|
+
default: false,
|
|
784
|
+
},
|
|
785
|
+
path: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
description: 'Project path (defaults to current project root)',
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
handler: async (params) => {
|
|
792
|
+
const cwd = getProjectCwd();
|
|
793
|
+
const targetPath = params.path || cwd;
|
|
794
|
+
if (!graphExists(targetPath)) {
|
|
795
|
+
return {
|
|
796
|
+
error: true,
|
|
797
|
+
message: 'No graph found. Run graphify_build first.',
|
|
798
|
+
hint: `Expected: ${getGraphPath(targetPath)}`,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const graphPath = getGraphPath(targetPath);
|
|
803
|
+
const { readFileSync } = await import('fs');
|
|
804
|
+
const { join, dirname } = await import('path');
|
|
805
|
+
const outputDir = dirname(graphPath);
|
|
806
|
+
const { exportHTML } = await import('@monoes/graph');
|
|
807
|
+
const raw = JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
808
|
+
const htmlPath = exportHTML(raw, outputDir);
|
|
809
|
+
if (params.open) {
|
|
810
|
+
const { spawn } = await import('child_process');
|
|
811
|
+
const opener = process.platform === 'darwin' ? 'open'
|
|
812
|
+
: process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
813
|
+
spawn(opener, [htmlPath], { detached: true, stdio: 'ignore' }).unref();
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
success: true,
|
|
817
|
+
htmlPath,
|
|
818
|
+
message: `Interactive visualization generated at ${htmlPath}`,
|
|
819
|
+
hint: params.open ? 'Opening in browser…' : `Open in browser: open "${htmlPath}"`,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
return { error: true, message: String(err) };
|
|
370
824
|
}
|
|
371
|
-
catch (err) { return { error: true, message: String(err) }; }
|
|
372
825
|
},
|
|
373
826
|
};
|
|
374
|
-
|
|
827
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
375
828
|
export const graphifyTools = [
|
|
376
829
|
graphifyBuildTool,
|
|
377
830
|
graphifyQueryTool,
|
|
@@ -381,6 +834,7 @@ export const graphifyTools = [
|
|
|
381
834
|
graphifyGetCommunityTool,
|
|
382
835
|
graphifyStatsTool,
|
|
383
836
|
graphifySurprisesTool,
|
|
837
|
+
graphifyVisualizeTool,
|
|
384
838
|
];
|
|
385
|
-
|
|
386
839
|
export default graphifyTools;
|
|
840
|
+
//# sourceMappingURL=graphify-tools.js.map
|