@monoes/cli 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: graphify-analyst
3
+ description: "Knowledge graph analyst for codebase architecture and project understanding. Uses graphify's persistent knowledge graph to answer structural questions without reading files — reveals god nodes, community clusters, dependency paths, and cross-component surprises."
4
+ type: graphify-analyst
5
+ color: violet
6
+ priority: high
7
+ triggers:
8
+ - "understand the codebase"
9
+ - "how does * work"
10
+ - "what calls *"
11
+ - "architecture overview"
12
+ - "dependency between"
13
+ - "project structure"
14
+ - "how is * related to"
15
+ - "explain the flow"
16
+ - "what depends on"
17
+ metadata:
18
+ specialization: "Knowledge graph-based codebase analysis"
19
+ requires: "graphify Python package (pip install graphifyy[mcp])"
20
+ capabilities:
21
+ - Zero-file-read architecture understanding via knowledge graph
22
+ - God node identification (most connected core abstractions)
23
+ - Community/subsystem detection and analysis
24
+ - Shortest dependency path between any two components
25
+ - Cross-community surprising connection detection
26
+ - Code vs documentation relationship mapping
27
+ - Confidence-aware edge reasoning (EXTRACTED/INFERRED/AMBIGUOUS)
28
+ ---
29
+
30
+ # Graphify Analyst Agent
31
+
32
+ A specialist in codebase understanding through knowledge graphs. Instead of reading files, this agent queries a persistent, pre-built graph of your entire project — delivering architectural insights in seconds.
33
+
34
+ ## When to Use This Agent
35
+
36
+ Use graphify-analyst **instead of reading files** when you need to:
37
+ - Understand how components relate to each other
38
+ - Find the core abstractions of an unfamiliar codebase
39
+ - Trace a dependency chain from A to B
40
+ - Identify which subsystems exist and what belongs to each
41
+ - Detect unexpected coupling between modules
42
+ - Answer "what is the most important class/function/concept here?"
43
+
44
+ ## Workflow
45
+
46
+ ### Step 1 — Check graph exists
47
+ ```
48
+ mcp__monobrain__graphify_stats
49
+ ```
50
+ If no graph: tell user to run `python -m graphify <project-path>` first.
51
+
52
+ ### Step 2 — Get orientation (always start here)
53
+ ```
54
+ mcp__monobrain__graphify_god_nodes { topN: 15 }
55
+ ```
56
+ The top god nodes ARE the architecture. Everything important will be connected to them.
57
+
58
+ ### Step 3 — Query by concept
59
+ ```
60
+ mcp__monobrain__graphify_query { question: "authentication", mode: "bfs", depth: 3 }
61
+ ```
62
+ Match the question to the user's actual concern. Use `dfs` to trace a specific call path.
63
+
64
+ ### Step 4 — Understand subsystems
65
+ ```
66
+ mcp__monobrain__graphify_community { communityId: 0 } // largest cluster
67
+ ```
68
+ Each community is a logical subsystem. Review all communities to understand module boundaries.
69
+
70
+ ### Step 5 — Trace specific dependencies
71
+ ```
72
+ mcp__monobrain__graphify_shortest_path { source: "Router", target: "Database" }
73
+ ```
74
+ Use to answer "how does X reach Y?" — reveals hidden coupling chains.
75
+
76
+ ### Step 6 — Find surprises
77
+ ```
78
+ mcp__monobrain__graphify_surprises { topN: 10 }
79
+ ```
80
+ Cross-community connections often reveal design smells or important but non-obvious integrations.
81
+
82
+ ## Output Format
83
+
84
+ Always structure your response as:
85
+
86
+ ```
87
+ ## Architecture Overview
88
+ [Top god nodes — the core abstractions]
89
+
90
+ ## Subsystems / Communities
91
+ [What each community represents]
92
+
93
+ ## Key Relationships
94
+ [Most important edges and what they mean]
95
+
96
+ ## Surprising Connections
97
+ [Cross-community edges worth noting]
98
+
99
+ ## Confidence Notes
100
+ [Which edges are EXTRACTED vs INFERRED vs AMBIGUOUS]
101
+
102
+ ## Recommendations
103
+ [Architectural observations, potential improvements]
104
+ ```
105
+
106
+ ## Confidence Interpretation
107
+
108
+ | Confidence | Meaning | Trust Level |
109
+ |---|---|---|
110
+ | EXTRACTED | Explicitly in source (import, call, inheritance) | High — treat as fact |
111
+ | INFERRED | Reasonable deduction with confidence score | Medium — verify if critical |
112
+ | AMBIGUOUS | Uncertain — flagged for review | Low — use as hypothesis only |
113
+
114
+ ## Building the Graph
115
+
116
+ If `graphify_stats` shows no graph:
117
+
118
+ ```bash
119
+ # Install
120
+ pip install graphifyy[mcp]
121
+
122
+ # Build graph for current project
123
+ python -m graphify .
124
+
125
+ # Query via MCP (starts stdio server)
126
+ python -m graphify.serve graphify-out/graph.json --mcp
127
+
128
+ # Or rebuild only code changes (fast, no LLM)
129
+ python -m graphify --update .
130
+ ```
131
+
132
+ ## Limits
133
+
134
+ - Graph must be built before querying — it does not auto-build
135
+ - Very large codebases (500k+ LOC) may have slow initial builds
136
+ - Doc/paper semantic edges require LLM — code AST edges are free
137
+ - Graph reflects state at build time — use `graphify_build { codeOnly: true }` to refresh after code changes
138
+
139
+ ## Integration with Other Agents
140
+
141
+ After graphify-analyst provides architectural context, hand off to:
142
+ - **coder** — for implementation using the architectural understanding
143
+ - **reviewer** — for code review informed by community/dependency context
144
+ - **architect** — for refactoring suggestions based on surprising connections
145
+ - **security-auditor** — for security review focusing on EXTRACTED cross-boundary edges
@@ -55,6 +55,16 @@ export function generateMCPConfig(options) {
55
55
  if (config.ruvSwarm) {
56
56
  mcpServers['ruv-swarm'] = createMCPServerEntry(['ruv-swarm', 'mcp', 'start'], { ...npmEnv }, { optional: true });
57
57
  }
58
+ // Graphify knowledge graph MCP server (project understanding)
59
+ if (config.graphify) {
60
+ mcpServers['graphify'] = {
61
+ command: 'python',
62
+ args: ['-m', 'graphify.serve', 'graphify-out/graph.json'],
63
+ env: {},
64
+ optional: true,
65
+ description: 'Knowledge graph for codebase understanding — run `python -m graphify <path>` first',
66
+ };
67
+ }
58
68
  // Flow Nexus MCP server (cloud features)
59
69
  if (config.flowNexus) {
60
70
  mcpServers['flow-nexus'] = createMCPServerEntry(['flow-nexus@latest', 'mcp', 'start'], { ...npmEnv }, { optional: true, requiresAuth: true });
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Graphify MCP Tools (compiled)
3
+ * Bridges graphify's knowledge graph into monobrain's MCP tool surface.
4
+ */
5
+ import { spawnSync } from 'child_process';
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { join, resolve } from 'path';
8
+ import { getProjectCwd } from './types.js';
9
+
10
+ function getGraphPath(cwd) {
11
+ return resolve(join(cwd, 'graphify-out', 'graph.json'));
12
+ }
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
+ function graphExists(cwd) {
25
+ return existsSync(getGraphPath(cwd));
26
+ }
27
+
28
+ function runGraphifyPython(snippet, cwd) {
29
+ const graphPath = getGraphPath(cwd);
30
+ const script = `
31
+ import json, sys
32
+ sys.path.insert(0, '${cwd.replace(/'/g, "\\'")}')
33
+ graph_path = '${graphPath.replace(/'/g, "\\'")}'
34
+ ${snippet}
35
+ `.trim();
36
+ const result = spawnSync('python', ['-c', script], {
37
+ timeout: 15000,
38
+ encoding: 'utf-8',
39
+ cwd,
40
+ });
41
+ if (result.status !== 0) {
42
+ throw new Error(result.stderr?.trim() || 'graphify python call failed');
43
+ }
44
+ const output = result.stdout?.trim();
45
+ if (!output) throw new Error('No output from graphify');
46
+ return JSON.parse(output);
47
+ }
48
+
49
+ const LOAD_GRAPH_SNIPPET = `
50
+ import json
51
+ from pathlib import Path
52
+ import networkx as nx
53
+ from networkx.readwrite import json_graph
54
+ data = json.loads(Path(graph_path).read_text())
55
+ try:
56
+ G = json_graph.node_link_graph(data, edges='links')
57
+ except TypeError:
58
+ G = json_graph.node_link_graph(data)
59
+ `;
60
+
61
+ export const graphifyBuildTool = {
62
+ name: 'graphify_build',
63
+ description: 'Build (or rebuild) the knowledge graph for a project directory. Extracts AST structure from code files, semantic relationships from docs, and clusters them into communities. Run this first before using other graphify tools.',
64
+ category: 'graphify',
65
+ tags: ['knowledge-graph', 'codebase', 'architecture', 'analysis'],
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ path: { type: 'string', description: 'Path to analyse (defaults to current project root)' },
70
+ codeOnly: { type: 'boolean', description: 'Only re-extract changed code files — fast rebuild', default: false },
71
+ },
72
+ },
73
+ handler: async (params) => {
74
+ const cwd = getProjectCwd();
75
+ 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
+ try {
80
+ const result = spawnSync('python', ['-m', 'graphify', targetPath, ...(params.codeOnly ? ['--update'] : [])], {
81
+ timeout: 120000, encoding: 'utf-8', cwd,
82
+ });
83
+ const graphPath = getGraphPath(cwd);
84
+ const built = existsSync(graphPath);
85
+ return {
86
+ success: result.status === 0,
87
+ graphPath: built ? graphPath : null,
88
+ output: result.stdout?.trim().slice(-500) || '',
89
+ error: result.status !== 0 ? result.stderr?.trim().slice(-500) : undefined,
90
+ message: built ? `Knowledge graph built at ${graphPath}` : 'Build may have failed — graph.json not found',
91
+ };
92
+ }
93
+ catch (err) { return { error: true, message: String(err) }; }
94
+ },
95
+ };
96
+
97
+ export const graphifyQueryTool = {
98
+ name: 'graphify_query',
99
+ description: 'Search the knowledge graph with a natural language question or keywords. Returns relevant nodes and edges as structured context — use this instead of reading many files when you want to understand how components relate.',
100
+ category: 'graphify',
101
+ tags: ['knowledge-graph', 'search', 'architecture', 'codebase'],
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ question: { type: 'string', description: 'Natural language question or keyword' },
106
+ mode: { type: 'string', enum: ['bfs', 'dfs'], default: 'bfs' },
107
+ depth: { type: 'integer', default: 3 },
108
+ tokenBudget: { type: 'integer', default: 2000 },
109
+ },
110
+ required: ['question'],
111
+ },
112
+ handler: async (params) => {
113
+ const cwd = getProjectCwd();
114
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(cwd)}` };
115
+ try {
116
+ return runGraphifyPython(`
117
+ ${LOAD_GRAPH_SNIPPET}
118
+ question = ${JSON.stringify(params.question)}
119
+ terms = [t.lower() for t in question.split() if len(t) > 2]
120
+ mode = ${JSON.stringify(params.mode || 'bfs')}
121
+ depth = ${params.depth || 3}
122
+ scored = []
123
+ for nid, d in G.nodes(data=True):
124
+ label = d.get('label', '').lower()
125
+ source = d.get('source_file', '').lower()
126
+ score = sum(1 for t in terms if t in label) + sum(0.5 for t in terms if t in source)
127
+ if score > 0:
128
+ scored.append((score, nid))
129
+ scored.sort(reverse=True)
130
+ start_nodes = [nid for _, nid in scored[:5]]
131
+ if not start_nodes:
132
+ start_nodes = sorted(G.nodes(), key=lambda n: G.degree(n), reverse=True)[:3]
133
+ visited = set(start_nodes)
134
+ frontier = set(start_nodes)
135
+ edges_seen = []
136
+ if mode == 'bfs':
137
+ for _ in range(depth):
138
+ next_frontier = set()
139
+ for n in frontier:
140
+ for nbr in G.neighbors(n):
141
+ if nbr not in visited:
142
+ next_frontier.add(nbr)
143
+ edges_seen.append((n, nbr))
144
+ visited.update(next_frontier)
145
+ frontier = next_frontier
146
+ else:
147
+ stack = [(n, 0) for n in reversed(start_nodes)]
148
+ while stack:
149
+ node, d = stack.pop()
150
+ if node in visited or d > depth: continue
151
+ visited.add(node)
152
+ for nbr in G.neighbors(node):
153
+ if nbr not in visited:
154
+ stack.append((nbr, d + 1))
155
+ edges_seen.append((node, nbr))
156
+ nodes_out = [{'id': nid, 'label': G.nodes[nid].get('label', nid), 'file': G.nodes[nid].get('source_file', ''), 'location': G.nodes[nid].get('source_location', ''), 'community': G.nodes[nid].get('community'), 'degree': G.degree(nid), 'file_type': G.nodes[nid].get('file_type', '')} for nid in sorted(visited, key=lambda n: G.degree(n), reverse=True)]
157
+ edges_out = [{'from': G.nodes[u].get('label', u), 'to': G.nodes[v].get('label', v), 'relation': G.edges.get((u, v), {}).get('relation', ''), 'confidence': G.edges.get((u, v), {}).get('confidence', '')} for u, v in edges_seen if u in visited and v in visited]
158
+ print(json.dumps({'question': question, 'mode': mode, 'depth': depth, 'nodes': nodes_out[:60], 'edges': edges_out[:80], 'total_nodes': len(visited), 'total_edges': len(edges_seen)}))
159
+ `, cwd);
160
+ }
161
+ catch (err) { return { error: true, message: String(err) }; }
162
+ },
163
+ };
164
+
165
+ export const graphifyGodNodesTool = {
166
+ name: 'graphify_god_nodes',
167
+ description: 'Return the most connected nodes in the knowledge graph — the core abstractions and central concepts of the codebase. Use this at the start of any architectural analysis.',
168
+ category: 'graphify',
169
+ tags: ['knowledge-graph', 'architecture', 'abstractions', 'codebase'],
170
+ inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 15 } } },
171
+ handler: async (params) => {
172
+ const cwd = getProjectCwd();
173
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
174
+ try {
175
+ return runGraphifyPython(`
176
+ ${LOAD_GRAPH_SNIPPET}
177
+ top_n = ${params.topN || 15}
178
+ degree_map = dict(G.degree())
179
+ sorted_nodes = sorted(degree_map, key=degree_map.get, reverse=True)[:top_n]
180
+ nodes_out = [{'label': G.nodes[nid].get('label', nid), 'degree': degree_map[nid], 'file': G.nodes[nid].get('source_file', ''), 'location': G.nodes[nid].get('source_location', ''), 'community': G.nodes[nid].get('community'), 'file_type': G.nodes[nid].get('file_type', ''), 'neighbors': [G.nodes[n].get('label', n) for n in G.neighbors(nid)][:8]} for nid in sorted_nodes]
181
+ print(json.dumps({'god_nodes': nodes_out, 'total_nodes': G.number_of_nodes()}))
182
+ `, cwd);
183
+ }
184
+ catch (err) { return { error: true, message: String(err) }; }
185
+ },
186
+ };
187
+
188
+ export const graphifyGetNodeTool = {
189
+ name: 'graphify_get_node',
190
+ description: 'Get all details for a specific concept/node in the knowledge graph: source location, community, all relationships, and confidence levels.',
191
+ category: 'graphify',
192
+ tags: ['knowledge-graph', 'node', 'details'],
193
+ inputSchema: { type: 'object', properties: { label: { type: 'string', description: 'Node label or ID (case-insensitive)' } }, required: ['label'] },
194
+ handler: async (params) => {
195
+ const cwd = getProjectCwd();
196
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
197
+ try {
198
+ return runGraphifyPython(`
199
+ ${LOAD_GRAPH_SNIPPET}
200
+ term = ${JSON.stringify(params.label.toLowerCase())}
201
+ matches = [nid for nid, d in G.nodes(data=True) if term in d.get('label', '').lower() or term == nid.lower()]
202
+ if not matches:
203
+ print(json.dumps({'error': 'Node not found', 'searched': term}))
204
+ else:
205
+ nid = matches[0]
206
+ d = dict(G.nodes[nid])
207
+ edges = []
208
+ for u, v, ed in G.edges(nid, data=True):
209
+ edges.append({'direction': 'outgoing', 'to': G.nodes[v].get('label', v), 'relation': ed.get('relation', ''), 'confidence': ed.get('confidence', ''), 'confidence_score': ed.get('confidence_score')})
210
+ print(json.dumps({'id': nid, 'label': d.get('label', nid), 'file': d.get('source_file', ''), 'location': d.get('source_location', ''), 'community': d.get('community'), 'file_type': d.get('file_type', ''), 'degree': G.degree(nid), 'edges': edges[:40], 'all_matches': [G.nodes[m].get('label', m) for m in matches[:10]]}))
211
+ `, cwd);
212
+ }
213
+ catch (err) { return { error: true, message: String(err) }; }
214
+ },
215
+ };
216
+
217
+ export const graphifyShortestPathTool = {
218
+ name: 'graphify_shortest_path',
219
+ description: 'Find the shortest relationship path between two concepts in the knowledge graph. Reveals coupling chains between any two components.',
220
+ category: 'graphify',
221
+ tags: ['knowledge-graph', 'path', 'dependencies', 'coupling'],
222
+ inputSchema: {
223
+ type: 'object',
224
+ properties: {
225
+ source: { type: 'string', description: 'Source concept label' },
226
+ target: { type: 'string', description: 'Target concept label' },
227
+ maxHops: { type: 'integer', default: 8 },
228
+ },
229
+ required: ['source', 'target'],
230
+ },
231
+ handler: async (params) => {
232
+ const cwd = getProjectCwd();
233
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
234
+ try {
235
+ return runGraphifyPython(`
236
+ ${LOAD_GRAPH_SNIPPET}
237
+ import networkx as nx
238
+ def find_node(G, term):
239
+ t = term.lower()
240
+ matches = [nid for nid, d in G.nodes(data=True) if t in d.get('label', '').lower() or t == nid.lower()]
241
+ return sorted(matches, key=lambda n: G.degree(n), reverse=True)
242
+ src_nodes = find_node(G, ${JSON.stringify(params.source)})
243
+ tgt_nodes = find_node(G, ${JSON.stringify(params.target)})
244
+ max_hops = ${params.maxHops || 8}
245
+ if not src_nodes:
246
+ print(json.dumps({'error': f'Source not found: ${params.source}'}))
247
+ elif not tgt_nodes:
248
+ print(json.dumps({'error': f'Target not found: ${params.target}'}))
249
+ else:
250
+ UG = G.to_undirected() if G.is_directed() else G
251
+ best_path = None
252
+ best_len = max_hops + 1
253
+ for src in src_nodes[:3]:
254
+ for tgt in tgt_nodes[:3]:
255
+ try:
256
+ p = nx.shortest_path(UG, src, tgt)
257
+ if len(p) < best_len:
258
+ best_len = len(p)
259
+ best_path = p
260
+ except nx.NetworkXNoPath: pass
261
+ if best_path is None:
262
+ print(json.dumps({'found': False, 'message': 'No path found within hop limit'}))
263
+ else:
264
+ steps = []
265
+ for i, nid in enumerate(best_path):
266
+ d = G.nodes[nid]
267
+ step = {'label': d.get('label', nid), 'file': d.get('source_file', ''), 'location': d.get('source_location', '')}
268
+ if i < len(best_path) - 1:
269
+ edge = G.edges.get((nid, best_path[i+1]), G.edges.get((best_path[i+1], nid), {}))
270
+ step['next_relation'] = edge.get('relation', '')
271
+ step['confidence'] = edge.get('confidence', '')
272
+ steps.append(step)
273
+ print(json.dumps({'found': True, 'hops': len(best_path) - 1, 'path': steps}))
274
+ `, cwd);
275
+ }
276
+ catch (err) { return { error: true, message: String(err) }; }
277
+ },
278
+ };
279
+
280
+ export const graphifyGetCommunityTool = {
281
+ name: 'graphify_community',
282
+ description: 'Get all nodes in a specific community cluster. Communities are groups of tightly related components. Use graphify_stats first to see community count.',
283
+ category: 'graphify',
284
+ tags: ['knowledge-graph', 'community', 'clusters', 'subsystems'],
285
+ inputSchema: { type: 'object', properties: { communityId: { type: 'integer', description: 'Community ID (0 = largest)' } }, required: ['communityId'] },
286
+ handler: async (params) => {
287
+ const cwd = getProjectCwd();
288
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
289
+ try {
290
+ return runGraphifyPython(`
291
+ ${LOAD_GRAPH_SNIPPET}
292
+ cid = ${params.communityId}
293
+ members = [{'label': d.get('label', nid), 'file': d.get('source_file', ''), 'location': d.get('source_location', ''), 'degree': G.degree(nid), 'file_type': d.get('file_type', '')} for nid, d in G.nodes(data=True) if d.get('community') == cid]
294
+ members.sort(key=lambda x: x['degree'], reverse=True)
295
+ external_edges = []
296
+ for nid, d in G.nodes(data=True):
297
+ if d.get('community') == cid:
298
+ for nbr in G.neighbors(nid):
299
+ if G.nodes[nbr].get('community') != cid:
300
+ ed = G.edges.get((nid, nbr), {})
301
+ external_edges.append({'from': d.get('label', nid), 'to': G.nodes[nbr].get('label', nbr), 'to_community': G.nodes[nbr].get('community'), 'relation': ed.get('relation', '')})
302
+ print(json.dumps({'community_id': cid, 'member_count': len(members), 'members': members[:50], 'external_connections': external_edges[:30]}))
303
+ `, cwd);
304
+ }
305
+ catch (err) { return { error: true, message: String(err) }; }
306
+ },
307
+ };
308
+
309
+ export const graphifyStatsTool = {
310
+ name: 'graphify_stats',
311
+ description: 'Get summary statistics for the knowledge graph: node count, edge count, community count, confidence breakdown, and top god nodes. Use this first to understand graph size.',
312
+ category: 'graphify',
313
+ tags: ['knowledge-graph', 'stats', 'overview'],
314
+ inputSchema: { type: 'object', properties: {} },
315
+ handler: async (_params) => {
316
+ const cwd = getProjectCwd();
317
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.', hint: `Expected: ${getGraphPath(cwd)}` };
318
+ if (!isGraphifyInstalled()) {
319
+ try {
320
+ const raw = JSON.parse(readFileSync(getGraphPath(cwd), 'utf-8'));
321
+ const nodes = raw.nodes || [];
322
+ const edges = raw.links || raw.edges || [];
323
+ return { nodes: nodes.length, edges: edges.length, note: 'graphify Python package not installed — basic stats only', install: 'pip install graphifyy[mcp]' };
324
+ }
325
+ catch (err) { return { error: true, message: String(err) }; }
326
+ }
327
+ try {
328
+ return runGraphifyPython(`
329
+ ${LOAD_GRAPH_SNIPPET}
330
+ from collections import Counter
331
+ communities = {}
332
+ for nid, d in G.nodes(data=True):
333
+ c = d.get('community')
334
+ if c is not None:
335
+ communities.setdefault(int(c), []).append(nid)
336
+ confidence_counts = Counter(d.get('confidence', 'UNKNOWN') for u, v, d in G.edges(data=True))
337
+ relation_counts = Counter(d.get('relation', 'unknown') for u, v, d in G.edges(data=True))
338
+ file_types = Counter(d.get('file_type', 'unknown') for nid, d in G.nodes(data=True))
339
+ degree_map = dict(G.degree())
340
+ top_nodes = sorted(degree_map, key=degree_map.get, reverse=True)[:5]
341
+ print(json.dumps({'nodes': G.number_of_nodes(), 'edges': G.number_of_edges(), 'communities': len(communities), 'community_sizes': {str(k): len(v) for k, v in sorted(communities.items(), key=lambda x: len(x[1]), reverse=True)[:10]}, 'confidence': dict(confidence_counts), 'top_relations': dict(relation_counts.most_common(10)), 'file_types': dict(file_types), 'graph_path': graph_path, 'top_god_nodes': [G.nodes[n].get('label', n) for n in top_nodes], 'is_directed': G.is_directed()}))
342
+ `, cwd);
343
+ }
344
+ catch (err) { return { error: true, message: String(err) }; }
345
+ },
346
+ };
347
+
348
+ export const graphifySurprisesTool = {
349
+ name: 'graphify_surprises',
350
+ description: 'Find surprising connections between components in different communities with strong relationships. These unexpected couplings often reveal hidden dependencies or important architectural patterns.',
351
+ category: 'graphify',
352
+ tags: ['knowledge-graph', 'architecture', 'coupling', 'surprises'],
353
+ inputSchema: { type: 'object', properties: { topN: { type: 'integer', default: 10 } } },
354
+ handler: async (params) => {
355
+ const cwd = getProjectCwd();
356
+ if (!graphExists(cwd)) return { error: true, message: 'No graph found. Run graphify_build first.' };
357
+ try {
358
+ return runGraphifyPython(`
359
+ ${LOAD_GRAPH_SNIPPET}
360
+ top_n = ${params.topN || 10}
361
+ surprises = []
362
+ for u, v, d in G.edges(data=True):
363
+ cu = G.nodes[u].get('community')
364
+ cv = G.nodes[v].get('community')
365
+ if cu is not None and cv is not None and cu != cv:
366
+ surprises.append({'score': G.degree(u) * G.degree(v), 'from': G.nodes[u].get('label', u), 'from_community': cu, 'from_file': G.nodes[u].get('source_file', ''), 'to': G.nodes[v].get('label', v), 'to_community': cv, 'to_file': G.nodes[v].get('source_file', ''), 'relation': d.get('relation', ''), 'confidence': d.get('confidence', '')})
367
+ surprises.sort(key=lambda x: x['score'], reverse=True)
368
+ print(json.dumps({'surprises': surprises[:top_n], 'total_cross_community_edges': len(surprises)}))
369
+ `, cwd);
370
+ }
371
+ catch (err) { return { error: true, message: String(err) }; }
372
+ },
373
+ };
374
+
375
+ export const graphifyTools = [
376
+ graphifyBuildTool,
377
+ graphifyQueryTool,
378
+ graphifyGodNodesTool,
379
+ graphifyGetNodeTool,
380
+ graphifyShortestPathTool,
381
+ graphifyGetCommunityTool,
382
+ graphifyStatsTool,
383
+ graphifySurprisesTool,
384
+ ];
385
+
386
+ export default graphifyTools;
@@ -23,4 +23,5 @@ export { wasmAgentTools } from './wasm-agent-tools.js';
23
23
  export { ruvllmWasmTools } from './ruvllm-tools.js';
24
24
  export { guidanceTools } from './guidance-tools.js';
25
25
  export { autopilotTools } from './autopilot-tools.js';
26
+ export { graphifyTools } from './graphify-tools.js';
26
27
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/cli",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Monobrain CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",