@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
|
|
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",
|