@monoes/cli 1.2.0 → 1.2.2
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.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 +16 -12
- 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.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 +599 -128
- 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 +1 -1
|
@@ -1,49 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Graphify MCP Tools
|
|
2
|
+
* Graphify MCP Tools
|
|
3
3
|
*
|
|
4
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
|
+
*
|
|
5
9
|
* Graph is built automatically on `monobrain init` and stored at
|
|
6
10
|
* .monobrain/graph/graph.json (legacy: graphify-out/graph.json).
|
|
11
|
+
* Rebuild manually: call graphify_build via MCP.
|
|
7
12
|
*/
|
|
8
13
|
import { existsSync, readFileSync } from 'fs';
|
|
9
14
|
import { join, resolve } from 'path';
|
|
10
15
|
import { getProjectCwd } from './types.js';
|
|
11
|
-
|
|
12
16
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
17
|
+
/** Resolve graph path: prefer native monobrain path, fall back to legacy graphify path. */
|
|
14
18
|
function getGraphPath(cwd) {
|
|
15
19
|
const nativePath = resolve(join(cwd, '.monobrain', 'graph', 'graph.json'));
|
|
16
20
|
const legacyPath = resolve(join(cwd, 'graphify-out', 'graph.json'));
|
|
17
|
-
if (existsSync(nativePath))
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
20
26
|
}
|
|
21
|
-
|
|
22
27
|
function graphExists(cwd) {
|
|
23
28
|
return existsSync(getGraphPath(cwd));
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Load the knowledge graph.
|
|
32
|
+
* Tries @monobrain/graph's loadGraph first; falls back to parsing raw JSON.
|
|
33
|
+
*/
|
|
28
34
|
async function loadKnowledgeGraph(cwd) {
|
|
29
35
|
const graphPath = getGraphPath(cwd);
|
|
30
36
|
let rawNodes = [];
|
|
31
37
|
let rawEdges = [];
|
|
32
|
-
|
|
33
38
|
try {
|
|
39
|
+
// Prefer @monobrain/graph's loader which handles format normalization.
|
|
34
40
|
const { loadGraph } = await import('@monoes/graph');
|
|
35
41
|
const loaded = loadGraph(graphPath);
|
|
36
42
|
rawNodes = loaded.nodes;
|
|
37
43
|
rawEdges = loaded.edges;
|
|
38
|
-
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fallback: parse JSON directly
|
|
39
47
|
const data = JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
40
48
|
rawNodes = data.nodes || [];
|
|
41
49
|
rawEdges = data.links || data.edges || [];
|
|
42
50
|
}
|
|
43
|
-
|
|
51
|
+
// Build in-memory graph structures
|
|
44
52
|
const nodes = new Map();
|
|
45
|
-
for (const n of rawNodes)
|
|
46
|
-
|
|
53
|
+
for (const n of rawNodes) {
|
|
54
|
+
nodes.set(n.id, n);
|
|
55
|
+
}
|
|
47
56
|
const adj = new Map();
|
|
48
57
|
const radj = new Map();
|
|
49
58
|
const degree = new Map();
|
|
@@ -53,15 +62,16 @@ async function loadKnowledgeGraph(cwd) {
|
|
|
53
62
|
degree.set(n.id, 0);
|
|
54
63
|
}
|
|
55
64
|
for (const e of rawEdges) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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);
|
|
60
71
|
}
|
|
61
|
-
|
|
62
72
|
return { nodes, adj, radj, edges: rawEdges, degree, graphPath };
|
|
63
73
|
}
|
|
64
|
-
|
|
74
|
+
// ── Shared output helpers ─────────────────────────────────────────────────────
|
|
65
75
|
function nodeOut(g, id) {
|
|
66
76
|
const d = g.nodes.get(id) ?? { id };
|
|
67
77
|
return {
|
|
@@ -74,21 +84,25 @@ function nodeOut(g, id) {
|
|
|
74
84
|
file_type: d.file_type ?? '',
|
|
75
85
|
};
|
|
76
86
|
}
|
|
77
|
-
|
|
87
|
+
/** Score a node against search terms. */
|
|
78
88
|
function scoreNode(g, id, terms) {
|
|
79
89
|
const d = g.nodes.get(id);
|
|
80
|
-
if (!d)
|
|
90
|
+
if (!d)
|
|
91
|
+
return 0;
|
|
81
92
|
const label = (d.label ?? id).toLowerCase();
|
|
82
93
|
const file = (d.source_file ?? '').toLowerCase();
|
|
83
94
|
return terms.reduce((s, t) => {
|
|
84
|
-
if (label.includes(t))
|
|
85
|
-
|
|
95
|
+
if (label.includes(t))
|
|
96
|
+
s += 1;
|
|
97
|
+
if (file.includes(t))
|
|
98
|
+
s += 0.5;
|
|
86
99
|
return s;
|
|
87
100
|
}, 0);
|
|
88
101
|
}
|
|
89
|
-
|
|
90
102
|
// ── Tool Definitions ──────────────────────────────────────────────────────────
|
|
91
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Build or rebuild the knowledge graph for a directory.
|
|
105
|
+
*/
|
|
92
106
|
export const graphifyBuildTool = {
|
|
93
107
|
name: 'graphify_build',
|
|
94
108
|
description: 'Build (or rebuild) the knowledge graph for a project directory. ' +
|
|
@@ -100,8 +114,15 @@ export const graphifyBuildTool = {
|
|
|
100
114
|
inputSchema: {
|
|
101
115
|
type: 'object',
|
|
102
116
|
properties: {
|
|
103
|
-
path: {
|
|
104
|
-
|
|
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
|
+
},
|
|
105
126
|
},
|
|
106
127
|
},
|
|
107
128
|
handler: async (params) => {
|
|
@@ -122,16 +143,19 @@ export const graphifyBuildTool = {
|
|
|
122
143
|
edges: result.analysis.stats.edges,
|
|
123
144
|
message: `Knowledge graph built at ${result.graphPath}`,
|
|
124
145
|
};
|
|
125
|
-
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
126
148
|
return {
|
|
127
149
|
error: true,
|
|
128
150
|
message: String(err),
|
|
129
|
-
hint: '@
|
|
151
|
+
hint: '@monobrain/graph package not available — ensure it is installed and built.',
|
|
130
152
|
};
|
|
131
153
|
}
|
|
132
154
|
},
|
|
133
155
|
};
|
|
134
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Query the knowledge graph with natural language.
|
|
158
|
+
*/
|
|
135
159
|
export const graphifyQueryTool = {
|
|
136
160
|
name: 'graphify_query',
|
|
137
161
|
description: 'Search the knowledge graph with a natural language question or keywords. ' +
|
|
@@ -143,29 +167,52 @@ export const graphifyQueryTool = {
|
|
|
143
167
|
inputSchema: {
|
|
144
168
|
type: 'object',
|
|
145
169
|
properties: {
|
|
146
|
-
question: {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
},
|
|
150
190
|
},
|
|
151
191
|
required: ['question'],
|
|
152
192
|
},
|
|
153
193
|
handler: async (params) => {
|
|
154
194
|
const cwd = getProjectCwd();
|
|
155
|
-
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
|
+
}
|
|
156
202
|
const question = params.question;
|
|
157
203
|
const mode = params.mode || 'bfs';
|
|
158
204
|
const depth = params.depth || 3;
|
|
159
205
|
try {
|
|
160
206
|
const g = await loadKnowledgeGraph(cwd);
|
|
161
207
|
const terms = question.toLowerCase().split(/\s+/).filter(t => t.length > 2);
|
|
162
|
-
|
|
208
|
+
// Score nodes; fall back to highest-degree nodes if no match
|
|
163
209
|
let startNodes = [];
|
|
164
210
|
if (terms.length > 0) {
|
|
165
211
|
const scored = [];
|
|
166
212
|
for (const id of g.nodes.keys()) {
|
|
167
213
|
const s = scoreNode(g, id, terms);
|
|
168
|
-
if (s > 0)
|
|
214
|
+
if (s > 0)
|
|
215
|
+
scored.push([s, id]);
|
|
169
216
|
}
|
|
170
217
|
scored.sort((a, b) => b[0] - a[0]);
|
|
171
218
|
startNodes = scored.slice(0, 5).map(([, id]) => id);
|
|
@@ -175,10 +222,8 @@ export const graphifyQueryTool = {
|
|
|
175
222
|
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
176
223
|
.slice(0, 3);
|
|
177
224
|
}
|
|
178
|
-
|
|
179
225
|
const visited = new Set(startNodes);
|
|
180
226
|
const edgesSeen = [];
|
|
181
|
-
|
|
182
227
|
if (mode === 'bfs') {
|
|
183
228
|
let frontier = new Set(startNodes);
|
|
184
229
|
for (let d = 0; d < depth; d++) {
|
|
@@ -191,15 +236,20 @@ export const graphifyQueryTool = {
|
|
|
191
236
|
}
|
|
192
237
|
}
|
|
193
238
|
}
|
|
194
|
-
for (const n of next)
|
|
239
|
+
for (const n of next)
|
|
240
|
+
visited.add(n);
|
|
195
241
|
frontier = next;
|
|
196
242
|
}
|
|
197
|
-
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// DFS
|
|
198
246
|
const stack = startNodes.map(n => [n, 0]);
|
|
199
247
|
while (stack.length > 0) {
|
|
200
248
|
const [node, d] = stack.pop();
|
|
201
|
-
if (visited.has(node) && d > 0)
|
|
202
|
-
|
|
249
|
+
if (visited.has(node) && d > 0)
|
|
250
|
+
continue;
|
|
251
|
+
if (d > depth)
|
|
252
|
+
continue;
|
|
203
253
|
visited.add(node);
|
|
204
254
|
for (const nbr of g.adj.get(node) ?? []) {
|
|
205
255
|
if (!visited.has(nbr)) {
|
|
@@ -209,33 +259,45 @@ export const graphifyQueryTool = {
|
|
|
209
259
|
}
|
|
210
260
|
}
|
|
211
261
|
}
|
|
212
|
-
|
|
213
262
|
const nodesOut = [...visited]
|
|
214
263
|
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
215
264
|
.slice(0, 60)
|
|
216
265
|
.map(id => nodeOut(g, id));
|
|
217
|
-
|
|
266
|
+
// Build edge lookup for attribute access
|
|
218
267
|
const edgeLookup = new Map();
|
|
219
|
-
for (const e of g.edges)
|
|
220
|
-
|
|
268
|
+
for (const e of g.edges) {
|
|
269
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
270
|
+
}
|
|
221
271
|
const edgesOut = edgesSeen
|
|
222
272
|
.filter(([u, v]) => visited.has(u) && visited.has(v))
|
|
223
273
|
.slice(0, 80)
|
|
224
274
|
.map(([u, v]) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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) };
|
|
295
|
+
}
|
|
236
296
|
},
|
|
237
297
|
};
|
|
238
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Get the most connected (god) nodes — the core abstractions.
|
|
300
|
+
*/
|
|
239
301
|
export const graphifyGodNodesTool = {
|
|
240
302
|
name: 'graphify_god_nodes',
|
|
241
303
|
description: 'Return the most connected nodes in the knowledge graph — the core abstractions ' +
|
|
@@ -243,10 +305,24 @@ export const graphifyGodNodesTool = {
|
|
|
243
305
|
'to understand what the most important components are before diving into details.',
|
|
244
306
|
category: 'graphify',
|
|
245
307
|
tags: ['knowledge-graph', 'architecture', 'abstractions', 'codebase'],
|
|
246
|
-
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
|
+
},
|
|
247
318
|
handler: async (params) => {
|
|
248
319
|
const cwd = getProjectCwd();
|
|
249
|
-
if (!graphExists(cwd))
|
|
320
|
+
if (!graphExists(cwd)) {
|
|
321
|
+
return {
|
|
322
|
+
error: true,
|
|
323
|
+
message: 'No graph found. Run graphify_build first.',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
250
326
|
const topN = params.topN || 15;
|
|
251
327
|
try {
|
|
252
328
|
const g = await loadKnowledgeGraph(cwd);
|
|
@@ -255,7 +331,9 @@ export const graphifyGodNodesTool = {
|
|
|
255
331
|
.slice(0, topN);
|
|
256
332
|
const godNodes = sortedIds.map(id => {
|
|
257
333
|
const d = g.nodes.get(id) ?? { id };
|
|
258
|
-
const neighbors = (g.adj.get(id) ?? [])
|
|
334
|
+
const neighbors = (g.adj.get(id) ?? [])
|
|
335
|
+
.slice(0, 8)
|
|
336
|
+
.map(nid => g.nodes.get(nid)?.label ?? nid);
|
|
259
337
|
return {
|
|
260
338
|
label: d.label ?? id,
|
|
261
339
|
degree: g.degree.get(id) ?? 0,
|
|
@@ -267,20 +345,37 @@ export const graphifyGodNodesTool = {
|
|
|
267
345
|
};
|
|
268
346
|
});
|
|
269
347
|
return { god_nodes: godNodes, total_nodes: g.nodes.size };
|
|
270
|
-
}
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return { error: true, message: String(err) };
|
|
351
|
+
}
|
|
271
352
|
},
|
|
272
353
|
};
|
|
273
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Get full details for a specific node.
|
|
356
|
+
*/
|
|
274
357
|
export const graphifyGetNodeTool = {
|
|
275
358
|
name: 'graphify_get_node',
|
|
276
359
|
description: 'Get all details for a specific concept/node in the knowledge graph: ' +
|
|
277
|
-
'its source location, community, all relationships, and confidence levels.'
|
|
360
|
+
'its source location, community, all relationships, and confidence levels. ' +
|
|
361
|
+
'Use this when you need to deeply understand one specific component.',
|
|
278
362
|
category: 'graphify',
|
|
279
363
|
tags: ['knowledge-graph', 'node', 'details'],
|
|
280
|
-
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
|
+
},
|
|
281
374
|
handler: async (params) => {
|
|
282
375
|
const cwd = getProjectCwd();
|
|
283
|
-
if (!graphExists(cwd))
|
|
376
|
+
if (!graphExists(cwd)) {
|
|
377
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
378
|
+
}
|
|
284
379
|
try {
|
|
285
380
|
const g = await loadKnowledgeGraph(cwd);
|
|
286
381
|
const term = params.label.toLowerCase();
|
|
@@ -288,23 +383,41 @@ export const graphifyGetNodeTool = {
|
|
|
288
383
|
.filter(([id, d]) => (d.label ?? id).toLowerCase().includes(term) || id.toLowerCase() === term)
|
|
289
384
|
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
290
385
|
.map(([id]) => id);
|
|
291
|
-
if (matches.length === 0)
|
|
386
|
+
if (matches.length === 0) {
|
|
387
|
+
return { error: 'Node not found', searched: term };
|
|
388
|
+
}
|
|
292
389
|
const id = matches[0];
|
|
293
390
|
const d = g.nodes.get(id) ?? { id };
|
|
391
|
+
// Build edge lookup
|
|
294
392
|
const edgeLookup = new Map();
|
|
295
|
-
for (const e of g.edges)
|
|
393
|
+
for (const e of g.edges) {
|
|
394
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
395
|
+
}
|
|
296
396
|
const outEdges = (g.adj.get(id) ?? []).slice(0, 40).map(tgt => {
|
|
297
397
|
const e = edgeLookup.get(`${id}__${tgt}`) ?? {};
|
|
298
|
-
return {
|
|
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
|
+
};
|
|
299
405
|
});
|
|
300
406
|
const inEdges = (g.radj.get(id) ?? []).slice(0, 40).map(src => {
|
|
301
407
|
const e = edgeLookup.get(`${src}__${id}`) ?? {};
|
|
302
|
-
return {
|
|
408
|
+
return {
|
|
409
|
+
direction: 'incoming',
|
|
410
|
+
from: g.nodes.get(src)?.label ?? src,
|
|
411
|
+
relation: e.relation ?? '',
|
|
412
|
+
confidence: e.confidence ?? '',
|
|
413
|
+
};
|
|
303
414
|
});
|
|
415
|
+
// Strip well-known fields from attributes output to avoid duplication
|
|
304
416
|
const knownKeys = new Set(['label', 'source_file', 'source_location', 'community', 'file_type', 'id']);
|
|
305
417
|
const attributes = {};
|
|
306
418
|
for (const [k, v] of Object.entries(d)) {
|
|
307
|
-
if (!knownKeys.has(k))
|
|
419
|
+
if (!knownKeys.has(k))
|
|
420
|
+
attributes[k] = v;
|
|
308
421
|
}
|
|
309
422
|
return {
|
|
310
423
|
id,
|
|
@@ -318,14 +431,20 @@ export const graphifyGetNodeTool = {
|
|
|
318
431
|
edges: [...outEdges, ...inEdges],
|
|
319
432
|
all_matches: matches.slice(0, 10).map(m => g.nodes.get(m)?.label ?? m),
|
|
320
433
|
};
|
|
321
|
-
}
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
return { error: true, message: String(err) };
|
|
437
|
+
}
|
|
322
438
|
},
|
|
323
439
|
};
|
|
324
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Find shortest path between two concepts.
|
|
442
|
+
*/
|
|
325
443
|
export const graphifyShortestPathTool = {
|
|
326
444
|
name: 'graphify_shortest_path',
|
|
327
445
|
description: 'Find the shortest relationship path between two concepts in the knowledge graph. ' +
|
|
328
|
-
'
|
|
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?").',
|
|
329
448
|
category: 'graphify',
|
|
330
449
|
tags: ['knowledge-graph', 'path', 'dependencies', 'coupling'],
|
|
331
450
|
inputSchema: {
|
|
@@ -333,17 +452,19 @@ export const graphifyShortestPathTool = {
|
|
|
333
452
|
properties: {
|
|
334
453
|
source: { type: 'string', description: 'Source concept label' },
|
|
335
454
|
target: { type: 'string', description: 'Target concept label' },
|
|
336
|
-
maxHops: { type: 'integer', default: 8 },
|
|
455
|
+
maxHops: { type: 'integer', default: 8, description: 'Maximum hops to search' },
|
|
337
456
|
},
|
|
338
457
|
required: ['source', 'target'],
|
|
339
458
|
},
|
|
340
459
|
handler: async (params) => {
|
|
341
460
|
const cwd = getProjectCwd();
|
|
342
|
-
if (!graphExists(cwd))
|
|
461
|
+
if (!graphExists(cwd)) {
|
|
462
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
463
|
+
}
|
|
343
464
|
try {
|
|
344
465
|
const g = await loadKnowledgeGraph(cwd);
|
|
345
466
|
const maxHops = params.maxHops || 8;
|
|
346
|
-
|
|
467
|
+
/** Find node ids matching a search term, sorted by degree descending. */
|
|
347
468
|
function findNodes(term) {
|
|
348
469
|
const t = term.toLowerCase();
|
|
349
470
|
return [...g.nodes.entries()]
|
|
@@ -351,12 +472,15 @@ export const graphifyShortestPathTool = {
|
|
|
351
472
|
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
352
473
|
.map(([id]) => id);
|
|
353
474
|
}
|
|
354
|
-
|
|
355
475
|
const srcNodes = findNodes(params.source);
|
|
356
476
|
const tgtNodes = findNodes(params.target);
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
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)
|
|
360
484
|
function bfsPath(start, end) {
|
|
361
485
|
const prev = new Map();
|
|
362
486
|
const queue = [start];
|
|
@@ -364,42 +488,56 @@ export const graphifyShortestPathTool = {
|
|
|
364
488
|
while (queue.length > 0) {
|
|
365
489
|
const cur = queue.shift();
|
|
366
490
|
if (cur === end) {
|
|
491
|
+
// Reconstruct path
|
|
367
492
|
const path = [];
|
|
368
493
|
let node = end;
|
|
369
|
-
while (node !== undefined) {
|
|
494
|
+
while (node !== undefined) {
|
|
495
|
+
path.unshift(node);
|
|
496
|
+
node = prev.get(node);
|
|
497
|
+
}
|
|
370
498
|
return path.length - 1 <= maxHops ? path : null;
|
|
371
499
|
}
|
|
500
|
+
// Treat edges as undirected
|
|
372
501
|
const nbrs = [...(g.adj.get(cur) ?? []), ...(g.radj.get(cur) ?? [])];
|
|
373
502
|
for (const nbr of nbrs) {
|
|
374
503
|
if (!visited.has(nbr)) {
|
|
375
504
|
visited.add(nbr);
|
|
376
505
|
prev.set(nbr, cur);
|
|
377
|
-
if (queue.length < 100000)
|
|
506
|
+
if (queue.length < 100000)
|
|
507
|
+
queue.push(nbr);
|
|
378
508
|
}
|
|
379
509
|
}
|
|
380
510
|
}
|
|
381
511
|
return null;
|
|
382
512
|
}
|
|
383
|
-
|
|
384
513
|
let bestPath = null;
|
|
385
514
|
for (const src of srcNodes.slice(0, 3)) {
|
|
386
515
|
for (const tgt of tgtNodes.slice(0, 3)) {
|
|
387
516
|
const p = bfsPath(src, tgt);
|
|
388
|
-
if (p && (!bestPath || p.length < bestPath.length))
|
|
517
|
+
if (p && (!bestPath || p.length < bestPath.length)) {
|
|
518
|
+
bestPath = p;
|
|
519
|
+
}
|
|
389
520
|
}
|
|
390
521
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
394
529
|
const edgeLookup = new Map();
|
|
395
530
|
for (const e of g.edges) {
|
|
396
531
|
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
397
|
-
edgeLookup.set(`${e.target}__${e.source}`, e);
|
|
532
|
+
edgeLookup.set(`${e.target}__${e.source}`, e); // bidirectional lookup
|
|
398
533
|
}
|
|
399
|
-
|
|
400
534
|
const steps = bestPath.map((id, i) => {
|
|
401
535
|
const d = g.nodes.get(id) ?? { id };
|
|
402
|
-
const step = {
|
|
536
|
+
const step = {
|
|
537
|
+
label: d.label ?? id,
|
|
538
|
+
file: d.source_file ?? '',
|
|
539
|
+
location: d.source_location ?? '',
|
|
540
|
+
};
|
|
403
541
|
if (i < bestPath.length - 1) {
|
|
404
542
|
const nextId = bestPath[i + 1];
|
|
405
543
|
const e = edgeLookup.get(`${id}__${nextId}`) ?? edgeLookup.get(`${nextId}__${id}`) ?? {};
|
|
@@ -408,22 +546,38 @@ export const graphifyShortestPathTool = {
|
|
|
408
546
|
}
|
|
409
547
|
return step;
|
|
410
548
|
});
|
|
411
|
-
|
|
412
549
|
return { found: true, hops: bestPath.length - 1, path: steps };
|
|
413
|
-
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
return { error: true, message: String(err) };
|
|
553
|
+
}
|
|
414
554
|
},
|
|
415
555
|
};
|
|
416
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Get all nodes in a community (cluster of related components).
|
|
558
|
+
*/
|
|
417
559
|
export const graphifyGetCommunityTool = {
|
|
418
560
|
name: 'graphify_community',
|
|
419
561
|
description: 'Get all nodes in a specific community cluster. Communities are groups of ' +
|
|
420
|
-
'tightly related components detected by graph clustering. Use
|
|
562
|
+
'tightly related components detected by graph clustering. Use graph_stats first to ' +
|
|
563
|
+
'see community count, then explore communities to understand subsystem boundaries.',
|
|
421
564
|
category: 'graphify',
|
|
422
565
|
tags: ['knowledge-graph', 'community', 'clusters', 'subsystems'],
|
|
423
|
-
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
|
+
},
|
|
424
576
|
handler: async (params) => {
|
|
425
577
|
const cwd = getProjectCwd();
|
|
426
|
-
if (!graphExists(cwd))
|
|
578
|
+
if (!graphExists(cwd)) {
|
|
579
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
580
|
+
}
|
|
427
581
|
try {
|
|
428
582
|
const g = await loadKnowledgeGraph(cwd);
|
|
429
583
|
const cid = params.communityId;
|
|
@@ -432,49 +586,84 @@ export const graphifyGetCommunityTool = {
|
|
|
432
586
|
.sort(([aId], [bId]) => (g.degree.get(bId) ?? 0) - (g.degree.get(aId) ?? 0))
|
|
433
587
|
.slice(0, 50)
|
|
434
588
|
.map(([id, d]) => ({
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
441
596
|
const edgeLookup = new Map();
|
|
442
|
-
for (const e of g.edges)
|
|
597
|
+
for (const e of g.edges) {
|
|
598
|
+
edgeLookup.set(`${e.source}__${e.target}`, e);
|
|
599
|
+
}
|
|
443
600
|
const externalEdges = [];
|
|
444
601
|
for (const [id, d] of g.nodes.entries()) {
|
|
445
|
-
if (d.community !== cid)
|
|
602
|
+
if (d.community !== cid)
|
|
603
|
+
continue;
|
|
446
604
|
for (const nbr of g.adj.get(id) ?? []) {
|
|
447
605
|
const nbrD = g.nodes.get(nbr);
|
|
448
606
|
if (nbrD?.community !== cid) {
|
|
449
607
|
const e = edgeLookup.get(`${id}__${nbr}`) ?? {};
|
|
450
|
-
externalEdges.push({
|
|
608
|
+
externalEdges.push({
|
|
609
|
+
from: d.label ?? id,
|
|
610
|
+
to: nbrD?.label ?? nbr,
|
|
611
|
+
to_community: nbrD?.community ?? null,
|
|
612
|
+
relation: e.relation ?? '',
|
|
613
|
+
});
|
|
451
614
|
}
|
|
452
615
|
}
|
|
453
616
|
}
|
|
454
|
-
return {
|
|
455
|
-
|
|
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) };
|
|
626
|
+
}
|
|
456
627
|
},
|
|
457
628
|
};
|
|
458
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Get graph statistics: node/edge counts, communities, confidence breakdown.
|
|
631
|
+
*/
|
|
459
632
|
export const graphifyStatsTool = {
|
|
460
633
|
name: 'graphify_stats',
|
|
461
634
|
description: 'Get summary statistics for the knowledge graph: node count, edge count, ' +
|
|
462
|
-
'community count, confidence breakdown,
|
|
635
|
+
'community count, confidence breakdown (EXTRACTED/INFERRED/AMBIGUOUS), ' +
|
|
636
|
+
'and top god nodes. Use this first to understand graph size and structure.',
|
|
463
637
|
category: 'graphify',
|
|
464
638
|
tags: ['knowledge-graph', 'stats', 'overview'],
|
|
465
|
-
inputSchema: {
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
properties: {},
|
|
642
|
+
},
|
|
466
643
|
handler: async (_params) => {
|
|
467
644
|
const cwd = getProjectCwd();
|
|
468
|
-
if (!graphExists(cwd))
|
|
645
|
+
if (!graphExists(cwd)) {
|
|
646
|
+
return {
|
|
647
|
+
error: true,
|
|
648
|
+
message: 'No graph found. Run graphify_build first.',
|
|
649
|
+
hint: `Expected: ${getGraphPath(cwd)}`,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
469
652
|
try {
|
|
470
653
|
const g = await loadKnowledgeGraph(cwd);
|
|
654
|
+
// Community sizes
|
|
471
655
|
const communities = new Map();
|
|
472
656
|
for (const d of g.nodes.values()) {
|
|
473
|
-
if (d.community != null)
|
|
657
|
+
if (d.community != null) {
|
|
658
|
+
communities.set(d.community, (communities.get(d.community) ?? 0) + 1);
|
|
659
|
+
}
|
|
474
660
|
}
|
|
475
661
|
const communitySizes = {};
|
|
476
|
-
[...communities.entries()]
|
|
662
|
+
[...communities.entries()]
|
|
663
|
+
.sort((a, b) => b[1] - a[1])
|
|
664
|
+
.slice(0, 10)
|
|
477
665
|
.forEach(([cid, count]) => { communitySizes[String(cid)] = count; });
|
|
666
|
+
// Confidence and relation counts
|
|
478
667
|
const confidenceCounts = {};
|
|
479
668
|
const relationCounts = {};
|
|
480
669
|
const fileTypeCounts = {};
|
|
@@ -489,7 +678,8 @@ export const graphifyStatsTool = {
|
|
|
489
678
|
fileTypeCounts[ft] = (fileTypeCounts[ft] ?? 0) + 1;
|
|
490
679
|
}
|
|
491
680
|
const topRelations = Object.entries(relationCounts)
|
|
492
|
-
.sort((a, b) => b[1] - a[1])
|
|
681
|
+
.sort((a, b) => b[1] - a[1])
|
|
682
|
+
.slice(0, 10)
|
|
493
683
|
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
|
|
494
684
|
const topGodNodes = [...g.nodes.keys()]
|
|
495
685
|
.sort((a, b) => (g.degree.get(b) ?? 0) - (g.degree.get(a) ?? 0))
|
|
@@ -507,20 +697,37 @@ export const graphifyStatsTool = {
|
|
|
507
697
|
top_god_nodes: topGodNodes,
|
|
508
698
|
is_directed: true,
|
|
509
699
|
};
|
|
510
|
-
}
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
return { error: true, message: String(err) };
|
|
703
|
+
}
|
|
511
704
|
},
|
|
512
705
|
};
|
|
513
|
-
|
|
706
|
+
/**
|
|
707
|
+
* Find surprising cross-community connections (architectural insights).
|
|
708
|
+
*/
|
|
514
709
|
export const graphifySurprisesTool = {
|
|
515
710
|
name: 'graphify_surprises',
|
|
516
|
-
description: 'Find surprising connections between components in different communities
|
|
517
|
-
'These unexpected couplings often reveal hidden dependencies
|
|
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.',
|
|
518
714
|
category: 'graphify',
|
|
519
715
|
tags: ['knowledge-graph', 'architecture', 'coupling', 'surprises'],
|
|
520
|
-
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
|
+
},
|
|
521
726
|
handler: async (params) => {
|
|
522
727
|
const cwd = getProjectCwd();
|
|
523
|
-
if (!graphExists(cwd))
|
|
728
|
+
if (!graphExists(cwd)) {
|
|
729
|
+
return { error: true, message: 'No graph found. Run graphify_build first.' };
|
|
730
|
+
}
|
|
524
731
|
try {
|
|
525
732
|
const g = await loadKnowledgeGraph(cwd);
|
|
526
733
|
const topN = params.topN || 10;
|
|
@@ -531,8 +738,9 @@ export const graphifySurprisesTool = {
|
|
|
531
738
|
const cu = uD?.community ?? null;
|
|
532
739
|
const cv = vD?.community ?? null;
|
|
533
740
|
if (cu != null && cv != null && cu !== cv) {
|
|
741
|
+
const score = (g.degree.get(e.source) ?? 0) * (g.degree.get(e.target) ?? 0);
|
|
534
742
|
surprises.push({
|
|
535
|
-
score
|
|
743
|
+
score,
|
|
536
744
|
from: uD?.label ?? e.source,
|
|
537
745
|
from_community: cu,
|
|
538
746
|
from_file: uD?.source_file ?? '',
|
|
@@ -545,11 +753,271 @@ export const graphifySurprisesTool = {
|
|
|
545
753
|
}
|
|
546
754
|
}
|
|
547
755
|
surprises.sort((a, b) => b.score - a.score);
|
|
548
|
-
return {
|
|
549
|
-
|
|
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
|
+
}
|
|
550
764
|
},
|
|
551
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) };
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
// ── Watch PID helpers ─────────────────────────────────────────────────────────
|
|
828
|
+
function getPidPath(cwd) {
|
|
829
|
+
return resolve(join(cwd, '.monobrain', 'graph', 'watch.pid'));
|
|
830
|
+
}
|
|
831
|
+
function readWatchPid(cwd) {
|
|
832
|
+
const pidPath = getPidPath(cwd);
|
|
833
|
+
if (!existsSync(pidPath))
|
|
834
|
+
return null;
|
|
835
|
+
try {
|
|
836
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
837
|
+
return isNaN(pid) ? null : pid;
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function isProcessRunning(pid) {
|
|
844
|
+
try {
|
|
845
|
+
process.kill(pid, 0);
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// ── Watch tools ───────────────────────────────────────────────────────────────
|
|
853
|
+
export const graphifyWatchTool = {
|
|
854
|
+
name: 'graphify_watch',
|
|
855
|
+
description: 'Start a background file watcher that automatically rebuilds the knowledge graph ' +
|
|
856
|
+
'(graph.json + graph.html) whenever source files change. Uses a 2-second debounce ' +
|
|
857
|
+
'so rapid saves do not trigger repeated rebuilds. The watcher runs as a detached ' +
|
|
858
|
+
'background process — call graphify_watch_stop to stop it.',
|
|
859
|
+
category: 'graphify',
|
|
860
|
+
tags: ['knowledge-graph', 'watch', 'auto-rebuild'],
|
|
861
|
+
inputSchema: {
|
|
862
|
+
type: 'object',
|
|
863
|
+
properties: {
|
|
864
|
+
path: {
|
|
865
|
+
type: 'string',
|
|
866
|
+
description: 'Project root to watch (defaults to current project root)',
|
|
867
|
+
},
|
|
868
|
+
debounce: {
|
|
869
|
+
type: 'number',
|
|
870
|
+
description: 'Debounce delay in milliseconds (default 2000)',
|
|
871
|
+
default: 2000,
|
|
872
|
+
},
|
|
873
|
+
extensions: {
|
|
874
|
+
type: 'string',
|
|
875
|
+
description: 'Comma-separated list of file extensions to watch (default: ts,js,tsx,jsx,py,go,rs,java,cs,rb,cpp,c)',
|
|
876
|
+
default: 'ts,js,tsx,jsx,py,go,rs,java,cs,rb,cpp,c',
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
handler: async (params) => {
|
|
881
|
+
const cwd = getProjectCwd();
|
|
882
|
+
const targetPath = params.path || cwd;
|
|
883
|
+
const debounceMs = params.debounce || 2000;
|
|
884
|
+
const extensions = ((params.extensions) || 'ts,js,tsx,jsx,py,go,rs,java,cs,rb,cpp,c')
|
|
885
|
+
.split(',')
|
|
886
|
+
.map((e) => e.trim())
|
|
887
|
+
.filter(Boolean);
|
|
888
|
+
// Check if already running
|
|
889
|
+
const existingPid = readWatchPid(targetPath);
|
|
890
|
+
if (existingPid !== null && isProcessRunning(existingPid)) {
|
|
891
|
+
return {
|
|
892
|
+
success: false,
|
|
893
|
+
message: `Watcher already running (PID ${existingPid})`,
|
|
894
|
+
hint: 'Call graphify_watch_stop first to restart.',
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const pidPath = getPidPath(targetPath);
|
|
898
|
+
const outputDir = resolve(join(targetPath, '.monobrain', 'graph'));
|
|
899
|
+
const watcherScript = `
|
|
900
|
+
import { watch } from 'chokidar';
|
|
901
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
902
|
+
|
|
903
|
+
const TARGET = ${JSON.stringify(targetPath)};
|
|
904
|
+
const OUTPUT_DIR = ${JSON.stringify(outputDir)};
|
|
905
|
+
const PID_PATH = ${JSON.stringify(pidPath)};
|
|
906
|
+
const DEBOUNCE_MS = ${debounceMs};
|
|
907
|
+
const EXTS = new Set(${JSON.stringify(extensions)});
|
|
908
|
+
const IGNORE = [/node_modules/, /\\.git/, /\\.monobrain/, /dist[\\/]/, /\\.next/, /\\.turbo/, /coverage/];
|
|
909
|
+
|
|
910
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
911
|
+
writeFileSync(PID_PATH, String(process.pid));
|
|
912
|
+
console.log('[graphify-watch] PID', process.pid, '— watching', TARGET);
|
|
913
|
+
|
|
914
|
+
let timer = null;
|
|
915
|
+
|
|
916
|
+
async function rebuild() {
|
|
917
|
+
const start = Date.now();
|
|
918
|
+
console.log('[graphify-watch] Change detected — rebuilding graph…');
|
|
919
|
+
try {
|
|
920
|
+
const { buildGraph } = await import('@monoes/graph');
|
|
921
|
+
const { exportHTML } = await import('@monoes/graph');
|
|
922
|
+
const { graph: serialized } = await buildGraph(TARGET, { outputDir: OUTPUT_DIR });
|
|
923
|
+
exportHTML(serialized, OUTPUT_DIR);
|
|
924
|
+
console.log('[graphify-watch] Done in', Date.now() - start, 'ms');
|
|
925
|
+
} catch (err) {
|
|
926
|
+
console.error('[graphify-watch] Build error:', err.message ?? err);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const watcher = watch(TARGET, {
|
|
931
|
+
ignored: (p) => IGNORE.some((r) => r.test(p)),
|
|
932
|
+
persistent: true,
|
|
933
|
+
ignoreInitial: true,
|
|
934
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
|
|
935
|
+
});
|
|
552
936
|
|
|
937
|
+
watcher.on('all', (event, filePath) => {
|
|
938
|
+
const ext = filePath.split('.').pop() ?? '';
|
|
939
|
+
if (!EXTS.has(ext)) return;
|
|
940
|
+
if (timer) clearTimeout(timer);
|
|
941
|
+
timer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
watcher.on('error', (err) => console.error('[graphify-watch] Watcher error:', err));
|
|
945
|
+
process.on('SIGINT', () => process.exit(0));
|
|
946
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
947
|
+
`;
|
|
948
|
+
try {
|
|
949
|
+
const { spawn } = await import('child_process');
|
|
950
|
+
const { writeFileSync, mkdirSync } = await import('fs');
|
|
951
|
+
mkdirSync(outputDir, { recursive: true });
|
|
952
|
+
const scriptPath = resolve(join(outputDir, '_watcher.mjs'));
|
|
953
|
+
writeFileSync(scriptPath, watcherScript);
|
|
954
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
955
|
+
detached: true,
|
|
956
|
+
stdio: 'ignore',
|
|
957
|
+
env: { ...process.env },
|
|
958
|
+
});
|
|
959
|
+
child.unref();
|
|
960
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
961
|
+
const pid = readWatchPid(targetPath) ?? child.pid;
|
|
962
|
+
return {
|
|
963
|
+
success: true,
|
|
964
|
+
pid,
|
|
965
|
+
watching: targetPath,
|
|
966
|
+
debounceMs,
|
|
967
|
+
extensions,
|
|
968
|
+
message: `Graph watcher started (PID ${pid}). graph.json + graph.html will rebuild on file changes.`,
|
|
969
|
+
hint: 'Call graphify_watch_stop to stop.',
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
catch (err) {
|
|
973
|
+
return { error: true, message: String(err) };
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
export const graphifyWatchStopTool = {
|
|
978
|
+
name: 'graphify_watch_stop',
|
|
979
|
+
description: 'Stop the background file watcher started by graphify_watch.',
|
|
980
|
+
category: 'graphify',
|
|
981
|
+
tags: ['knowledge-graph', 'watch'],
|
|
982
|
+
inputSchema: {
|
|
983
|
+
type: 'object',
|
|
984
|
+
properties: {
|
|
985
|
+
path: {
|
|
986
|
+
type: 'string',
|
|
987
|
+
description: 'Project root (defaults to current project root)',
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
handler: async (params) => {
|
|
992
|
+
const cwd = getProjectCwd();
|
|
993
|
+
const targetPath = params.path || cwd;
|
|
994
|
+
const pid = readWatchPid(targetPath);
|
|
995
|
+
if (pid === null) {
|
|
996
|
+
return { success: false, message: 'No watcher PID found — watcher may not be running.' };
|
|
997
|
+
}
|
|
998
|
+
if (!isProcessRunning(pid)) {
|
|
999
|
+
try {
|
|
1000
|
+
const { unlinkSync } = await import('fs');
|
|
1001
|
+
unlinkSync(getPidPath(targetPath));
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* ignore */ }
|
|
1004
|
+
return { success: false, message: `Process ${pid} is not running (stale PID cleaned up).` };
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
process.kill(pid, 'SIGTERM');
|
|
1008
|
+
try {
|
|
1009
|
+
const { unlinkSync } = await import('fs');
|
|
1010
|
+
unlinkSync(getPidPath(targetPath));
|
|
1011
|
+
}
|
|
1012
|
+
catch { /* ignore */ }
|
|
1013
|
+
return { success: true, message: `Watcher (PID ${pid}) stopped.` };
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
return { error: true, message: `Failed to stop PID ${pid}: ${String(err)}` };
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
553
1021
|
export const graphifyTools = [
|
|
554
1022
|
graphifyBuildTool,
|
|
555
1023
|
graphifyQueryTool,
|
|
@@ -559,6 +1027,9 @@ export const graphifyTools = [
|
|
|
559
1027
|
graphifyGetCommunityTool,
|
|
560
1028
|
graphifyStatsTool,
|
|
561
1029
|
graphifySurprisesTool,
|
|
1030
|
+
graphifyVisualizeTool,
|
|
1031
|
+
graphifyWatchTool,
|
|
1032
|
+
graphifyWatchStopTool,
|
|
562
1033
|
];
|
|
563
|
-
|
|
564
1034
|
export default graphifyTools;
|
|
1035
|
+
//# sourceMappingURL=graphify-tools.js.map
|