@monoes/cli 1.2.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.
Files changed (34) hide show
  1. package/dist/src/appliance/rvfa-format.d.ts +1 -1
  2. package/dist/src/appliance/rvfa-format.d.ts.map +1 -1
  3. package/dist/src/commands/init.d.ts.map +1 -1
  4. package/dist/src/commands/init.js.map +1 -1
  5. package/dist/src/init/executor.d.ts +0 -3
  6. package/dist/src/init/executor.d.ts.map +1 -1
  7. package/dist/src/init/executor.js +16 -12
  8. package/dist/src/init/executor.js.map +1 -1
  9. package/dist/src/init/helpers-generator.d.ts.map +1 -1
  10. package/dist/src/init/helpers-generator.js +38 -1
  11. package/dist/src/init/helpers-generator.js.map +1 -1
  12. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  13. package/dist/src/init/mcp-generator.js +2 -12
  14. package/dist/src/init/mcp-generator.js.map +1 -1
  15. package/dist/src/init/statusline-generator.d.ts +0 -3
  16. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  17. package/dist/src/init/statusline-generator.js +9 -4
  18. package/dist/src/init/statusline-generator.js.map +1 -1
  19. package/dist/src/init/types.d.ts +4 -0
  20. package/dist/src/init/types.d.ts.map +1 -1
  21. package/dist/src/init/types.js.map +1 -1
  22. package/dist/src/mcp-tools/graphify-tools.d.ts +52 -0
  23. package/dist/src/mcp-tools/graphify-tools.d.ts.map +1 -0
  24. package/dist/src/mcp-tools/graphify-tools.js +405 -129
  25. package/dist/src/mcp-tools/graphify-tools.js.map +1 -0
  26. package/dist/src/mcp-tools/index.d.ts +1 -0
  27. package/dist/src/mcp-tools/index.d.ts.map +1 -1
  28. package/dist/src/mcp-tools/index.js.map +1 -1
  29. package/dist/src/update/checker.d.ts.map +1 -1
  30. package/dist/src/update/checker.js +2 -5
  31. package/dist/src/update/checker.js.map +1 -1
  32. package/dist/src/workflow/dsl-schema.d.ts +1 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +1 -1
@@ -1,49 +1,58 @@
1
1
  /**
2
- * Graphify MCP Tools (compiled)
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)) return nativePath;
18
- if (existsSync(legacyPath)) return legacyPath;
19
- return nativePath;
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
- // ── Graph loading ─────────────────────────────────────────────────────────────
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
- } catch {
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) nodes.set(n.id, n);
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
- 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);
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) return 0;
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)) s += 1;
85
- if (file.includes(t)) s += 0.5;
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: { 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 },
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
- } catch (err) {
146
+ }
147
+ catch (err) {
126
148
  return {
127
149
  error: true,
128
150
  message: String(err),
129
- hint: '@monoes/graph package not available — ensure it is installed and built.',
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: { 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 },
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)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(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) scored.push([s, id]);
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) visited.add(n);
239
+ for (const n of next)
240
+ visited.add(n);
195
241
  frontier = next;
196
242
  }
197
- } else {
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) continue;
202
- if (d > depth) continue;
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) edgeLookup.set(`${e.source}__${e.target}`, e);
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
- 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) }; }
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: { type: 'object', properties: { topN: { type: 'integer', default: 15 } } },
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)) return { error: true, message: 'No graph found. Run graphify_build first.' };
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) ?? []).slice(0, 8).map(nid => g.nodes.get(nid)?.label ?? nid);
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
- } catch (err) { return { error: true, message: String(err) }; }
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: { type: 'object', properties: { label: { type: 'string', description: 'Node label or ID (case-insensitive)' } }, required: ['label'] },
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)) return { error: true, message: 'No graph found. Run graphify_build first.' };
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) return { error: 'Node not found', searched: term };
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) edgeLookup.set(`${e.source}__${e.target}`, e);
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 { direction: 'outgoing', to: g.nodes.get(tgt)?.label ?? tgt, relation: e.relation ?? '', confidence: e.confidence ?? '', confidence_score: e.confidence_score ?? null };
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 { direction: 'incoming', from: g.nodes.get(src)?.label ?? src, relation: e.relation ?? '', confidence: e.confidence ?? '' };
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)) attributes[k] = v;
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
- } catch (err) { return { error: true, message: String(err) }; }
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
- 'Reveals coupling chains between any two components.',
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)) return { error: true, message: 'No graph found. Run graphify_build first.' };
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 (!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
-
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) { path.unshift(node); node = prev.get(node); }
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) queue.push(nbr);
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)) bestPath = p;
517
+ if (p && (!bestPath || p.length < bestPath.length)) {
518
+ bestPath = p;
519
+ }
389
520
  }
390
521
  }
391
-
392
- if (!bestPath) return { found: false, message: `No path within ${maxHops} hops between "${params.source}" and "${params.target}"` };
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 = { label: d.label ?? id, file: d.source_file ?? '', location: d.source_location ?? '' };
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
- } catch (err) { return { error: true, message: String(err) }; }
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 graphify_stats first to see community count.',
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: { type: 'object', properties: { communityId: { type: 'integer', description: 'Community ID (0 = largest)' } }, required: ['communityId'] },
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)) return { error: true, message: 'No graph found. Run graphify_build first.' };
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
- 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
- }));
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) edgeLookup.set(`${e.source}__${e.target}`, e);
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) continue;
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({ from: d.label ?? id, to: nbrD?.label ?? nbr, to_community: nbrD?.community ?? null, relation: e.relation ?? '' });
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 { community_id: cid, member_count: members.length, members, external_connections: externalEdges.slice(0, 30) };
455
- } catch (err) { return { error: true, message: String(err) }; }
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, and top god nodes. Use this first to understand graph size.',
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: { type: 'object', properties: {} },
639
+ inputSchema: {
640
+ type: 'object',
641
+ properties: {},
642
+ },
466
643
  handler: async (_params) => {
467
644
  const cwd = getProjectCwd();
468
- if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(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) communities.set(d.community, (communities.get(d.community) ?? 0) + 1);
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()].sort((a, b) => b[1] - a[1]).slice(0, 10)
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]).slice(0, 10)
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
- } catch (err) { return { error: true, message: String(err) }; }
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 with strong relationships. ' +
517
- 'These unexpected couplings often reveal hidden dependencies or important architectural patterns.',
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: { type: 'object', properties: { topN: { type: 'integer', default: 10 } } },
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)) return { error: true, message: 'No graph found. Run graphify_build first.' };
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: (g.degree.get(e.source) ?? 0) * (g.degree.get(e.target) ?? 0),
743
+ score,
536
744
  from: uD?.label ?? e.source,
537
745
  from_community: cu,
538
746
  from_file: uD?.source_file ?? '',
@@ -545,11 +753,78 @@ export const graphifySurprisesTool = {
545
753
  }
546
754
  }
547
755
  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) }; }
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) };
824
+ }
550
825
  },
551
826
  };
552
-
827
+ // ── Exports ───────────────────────────────────────────────────────────────────
553
828
  export const graphifyTools = [
554
829
  graphifyBuildTool,
555
830
  graphifyQueryTool,
@@ -559,6 +834,7 @@ export const graphifyTools = [
559
834
  graphifyGetCommunityTool,
560
835
  graphifyStatsTool,
561
836
  graphifySurprisesTool,
837
+ graphifyVisualizeTool,
562
838
  ];
563
-
564
839
  export default graphifyTools;
840
+ //# sourceMappingURL=graphify-tools.js.map