edhindex 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -0
- package/dist/cli/commands/config_cmd.d.ts +2 -0
- package/dist/cli/commands/config_cmd.d.ts.map +1 -0
- package/dist/cli/commands/config_cmd.js +53 -0
- package/dist/cli/commands/config_cmd.js.map +1 -0
- package/dist/cli/commands/doctor_cmd.d.ts +2 -0
- package/dist/cli/commands/doctor_cmd.d.ts.map +1 -0
- package/dist/cli/commands/doctor_cmd.js +32 -0
- package/dist/cli/commands/doctor_cmd.js.map +1 -0
- package/dist/cli/commands/graph.d.ts +2 -0
- package/dist/cli/commands/graph.d.ts.map +1 -0
- package/dist/cli/commands/graph.js +26 -0
- package/dist/cli/commands/graph.js.map +1 -0
- package/dist/cli/commands/index_cmd.d.ts +2 -0
- package/dist/cli/commands/index_cmd.d.ts.map +1 -0
- package/dist/cli/commands/index_cmd.js +110 -0
- package/dist/cli/commands/index_cmd.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +53 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/knowledge-graph.d.ts +8 -0
- package/dist/cli/commands/knowledge-graph.d.ts.map +1 -0
- package/dist/cli/commands/knowledge-graph.js +76 -0
- package/dist/cli/commands/knowledge-graph.js.map +1 -0
- package/dist/cli/commands/models.d.ts +2 -0
- package/dist/cli/commands/models.d.ts.map +1 -0
- package/dist/cli/commands/models.js +17 -0
- package/dist/cli/commands/models.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +2 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +35 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +40 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +265 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +47 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +121 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/index.d.ts +28 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +65 -0
- package/dist/config/index.js.map +1 -0
- package/dist/doctor/index.d.ts +8 -0
- package/dist/doctor/index.d.ts.map +1 -0
- package/dist/doctor/index.js +117 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/embeddings/provider.d.ts +9 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +2 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/embeddings/transformers.d.ts +26 -0
- package/dist/embeddings/transformers.d.ts.map +1 -0
- package/dist/embeddings/transformers.js +120 -0
- package/dist/embeddings/transformers.js.map +1 -0
- package/dist/graph/build.d.ts +13 -0
- package/dist/graph/build.d.ts.map +1 -0
- package/dist/graph/build.js +33 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/builder.d.ts +24 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +231 -0
- package/dist/graph/builder.js.map +1 -0
- package/dist/graph/render.d.ts +3 -0
- package/dist/graph/render.d.ts.map +1 -0
- package/dist/graph/render.js +60 -0
- package/dist/graph/render.js.map +1 -0
- package/dist/graph/serve.d.ts +6 -0
- package/dist/graph/serve.d.ts.map +1 -0
- package/dist/graph/serve.js +389 -0
- package/dist/graph/serve.js.map +1 -0
- package/dist/graph/service.d.ts +29 -0
- package/dist/graph/service.d.ts.map +1 -0
- package/dist/graph/service.js +73 -0
- package/dist/graph/service.js.map +1 -0
- package/dist/graph/storage.d.ts +33 -0
- package/dist/graph/storage.d.ts.map +1 -0
- package/dist/graph/storage.js +164 -0
- package/dist/graph/storage.js.map +1 -0
- package/dist/graph/svg.d.ts +27 -0
- package/dist/graph/svg.d.ts.map +1 -0
- package/dist/graph/svg.js +212 -0
- package/dist/graph/svg.js.map +1 -0
- package/dist/graph/types.d.ts +34 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +9 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/indexer/chunker.d.ts +18 -0
- package/dist/indexer/chunker.d.ts.map +1 -0
- package/dist/indexer/chunker.js +139 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/file-utils.d.ts +17 -0
- package/dist/indexer/file-utils.d.ts.map +1 -0
- package/dist/indexer/file-utils.js +123 -0
- package/dist/indexer/file-utils.js.map +1 -0
- package/dist/indexer/ignore.d.ts +5 -0
- package/dist/indexer/ignore.d.ts.map +1 -0
- package/dist/indexer/ignore.js +96 -0
- package/dist/indexer/ignore.js.map +1 -0
- package/dist/indexer/incremental.d.ts +16 -0
- package/dist/indexer/incremental.d.ts.map +1 -0
- package/dist/indexer/incremental.js +55 -0
- package/dist/indexer/incremental.js.map +1 -0
- package/dist/indexer/indexer.d.ts +36 -0
- package/dist/indexer/indexer.d.ts.map +1 -0
- package/dist/indexer/indexer.js +121 -0
- package/dist/indexer/indexer.js.map +1 -0
- package/dist/indexer/parser.d.ts +30 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +225 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/logging.d.ts +15 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +42 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +226 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp-config.d.ts +4 -0
- package/dist/mcp-config.d.ts.map +1 -0
- package/dist/mcp-config.js +175 -0
- package/dist/mcp-config.js.map +1 -0
- package/dist/progress.d.ts +12 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +16 -0
- package/dist/progress.js.map +1 -0
- package/dist/retrieval/bm25.d.ts +6 -0
- package/dist/retrieval/bm25.d.ts.map +1 -0
- package/dist/retrieval/bm25.js +20 -0
- package/dist/retrieval/bm25.js.map +1 -0
- package/dist/retrieval/reranker.d.ts +11 -0
- package/dist/retrieval/reranker.d.ts.map +1 -0
- package/dist/retrieval/reranker.js +46 -0
- package/dist/retrieval/reranker.js.map +1 -0
- package/dist/retrieval/search.d.ts +35 -0
- package/dist/retrieval/search.d.ts.map +1 -0
- package/dist/retrieval/search.js +95 -0
- package/dist/retrieval/search.js.map +1 -0
- package/dist/security.d.ts +5 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +42 -0
- package/dist/security.js.map +1 -0
- package/dist/storage/fts.d.ts +30 -0
- package/dist/storage/fts.d.ts.map +1 -0
- package/dist/storage/fts.js +107 -0
- package/dist/storage/fts.js.map +1 -0
- package/dist/storage/metadata.d.ts +46 -0
- package/dist/storage/metadata.d.ts.map +1 -0
- package/dist/storage/metadata.js +133 -0
- package/dist/storage/metadata.js.map +1 -0
- package/dist/vector/lancedb.d.ts +25 -0
- package/dist/vector/lancedb.d.ts.map +1 -0
- package/dist/vector/lancedb.js +93 -0
- package/dist/vector/lancedb.js.map +1 -0
- package/dist/vector/store.d.ts +16 -0
- package/dist/vector/store.d.ts.map +1 -0
- package/dist/vector/store.js +2 -0
- package/dist/vector/store.js.map +1 -0
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +46 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher/index.d.ts +15 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +65 -0
- package/dist/watcher/index.js.map +1 -0
- package/package.json +35 -4
- package/src/cli/commands/graph.ts +30 -0
- package/src/cli/commands/init.ts +41 -3
- package/src/cli/commands/knowledge-graph.ts +85 -0
- package/src/cli/commands/reset.ts +40 -0
- package/src/cli/commands/start.ts +54 -21
- package/src/cli/index.ts +33 -0
- package/src/config/index.ts +13 -0
- package/src/graph/build.ts +45 -0
- package/src/graph/builder.ts +243 -0
- package/src/graph/render.ts +67 -0
- package/src/graph/serve.ts +399 -0
- package/src/graph/service.ts +84 -0
- package/src/graph/storage.ts +181 -0
- package/src/graph/svg.ts +241 -0
- package/src/graph/types.ts +68 -0
- package/src/mcp/server.ts +212 -85
- package/src/mcp-config.ts +185 -0
- package/src/storage/metadata.ts +9 -1
- package/.edhindexignore +0 -66
- package/BUILD_BRIEF.md +0 -228
- package/tsconfig.json +0 -21
package/src/graph/svg.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export interface FileNode {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
language: string;
|
|
5
|
+
chunkCount: number;
|
|
6
|
+
imports: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseImportPaths(raw: string): string[] {
|
|
10
|
+
if (!raw) return [];
|
|
11
|
+
const results: string[] = [];
|
|
12
|
+
for (const line of raw.split('\n')) {
|
|
13
|
+
const t = line.trim();
|
|
14
|
+
let m = t.match(/from\s+['"](\..*?)['"]/);
|
|
15
|
+
if (!m) m = t.match(/require\s*\(\s*['"](\..*?)['"]\s*\)/);
|
|
16
|
+
if (!m) m = t.match(/import\s+['"](\..*?)['"]/);
|
|
17
|
+
if (m) results.push(m[1]);
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveImport(sourceFile: string, importPath: string, indexedFiles: Set<string>): string | null {
|
|
23
|
+
const raw = importPath.replace(/^['"]|['"]$/g, '');
|
|
24
|
+
const dir = sourceFile.includes('/') ? sourceFile.slice(0, sourceFile.lastIndexOf('/')) : '';
|
|
25
|
+
|
|
26
|
+
function normalize(base: string, rel: string): string {
|
|
27
|
+
const parts = (base ? base.split('/') : []).concat(rel.split('/'));
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
for (const p of parts) {
|
|
30
|
+
if (p === '.' || p === '') continue;
|
|
31
|
+
if (p === '..') { out.pop(); continue; }
|
|
32
|
+
out.push(p);
|
|
33
|
+
}
|
|
34
|
+
return out.join('/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const full = normalize(dir, raw);
|
|
38
|
+
|
|
39
|
+
const candidates = [full];
|
|
40
|
+
for (const jsExt of ['.js', '.jsx']) {
|
|
41
|
+
if (full.endsWith(jsExt)) {
|
|
42
|
+
candidates.push(full.replace(jsExt, ''), full.replace(jsExt, '.ts'), full.replace(jsExt, '.tsx'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
if (indexedFiles.has(c)) return c;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.mts', '.cts', '.mjs', '.cjs']) {
|
|
51
|
+
if (indexedFiles.has(full + ext)) return full + ext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const idx of ['index.ts', 'index.tsx', 'index.js', 'index.jsx', '__init__.py', 'index.py']) {
|
|
55
|
+
if (indexedFiles.has(full + '/' + idx)) return full + '/' + idx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LayoutNode {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
language: string;
|
|
65
|
+
chunkCount: number;
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
color: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface LayoutEdge {
|
|
72
|
+
source: string;
|
|
73
|
+
target: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const LANG_COLORS: Record<string, string> = {
|
|
77
|
+
ts: '#3178c6',
|
|
78
|
+
js: '#f7df1e',
|
|
79
|
+
py: '#3572A5',
|
|
80
|
+
go: '#00ADD8',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function shortenPath(p: string): string {
|
|
84
|
+
if (p.length <= 45) return p;
|
|
85
|
+
return '...' + p.slice(-42);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function forceLayout(nodes: LayoutNode[], edges: LayoutEdge[], width: number, height: number) {
|
|
89
|
+
for (const n of nodes) {
|
|
90
|
+
n.x = width * 0.2 + Math.random() * width * 0.6;
|
|
91
|
+
n.y = height * 0.2 + Math.random() * height * 0.6;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const vx = new Float64Array(nodes.length);
|
|
95
|
+
const vy = new Float64Array(nodes.length);
|
|
96
|
+
const adj = new Map<string, number[]>();
|
|
97
|
+
for (const node of nodes) adj.set(node.id, []);
|
|
98
|
+
for (const e of edges) {
|
|
99
|
+
adj.get(e.source)?.push(edges.indexOf(e));
|
|
100
|
+
adj.get(e.target)?.push(edges.indexOf(e));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (let iter = 0; iter < 150; iter++) {
|
|
104
|
+
const cooling = 1 - iter / 150;
|
|
105
|
+
vx.fill(0); vy.fill(0);
|
|
106
|
+
|
|
107
|
+
// Repulsion
|
|
108
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
109
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
110
|
+
let dx = nodes[i].x - nodes[j].x;
|
|
111
|
+
let dy = nodes[i].y - nodes[j].y;
|
|
112
|
+
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
113
|
+
if (dist < 1) { dist = 1; dx = 1; }
|
|
114
|
+
const force = 8000 / (dist * dist);
|
|
115
|
+
vx[i] += (dx / dist) * force;
|
|
116
|
+
vy[i] += (dy / dist) * force;
|
|
117
|
+
vx[j] -= (dx / dist) * force;
|
|
118
|
+
vy[j] -= (dy / dist) * force;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Attraction along edges
|
|
123
|
+
for (const e of edges) {
|
|
124
|
+
const si = nodes.findIndex(n => n.id === e.source);
|
|
125
|
+
const ti = nodes.findIndex(n => n.id === e.target);
|
|
126
|
+
if (si === -1 || ti === -1) continue;
|
|
127
|
+
const dx = nodes[ti].x - nodes[si].x;
|
|
128
|
+
const dy = nodes[ti].y - nodes[si].y;
|
|
129
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
130
|
+
if (dist < 1) continue;
|
|
131
|
+
const force = dist / 80;
|
|
132
|
+
vx[si] += (dx / dist) * force;
|
|
133
|
+
vy[si] += (dy / dist) * force;
|
|
134
|
+
vx[ti] -= (dx / dist) * force;
|
|
135
|
+
vy[ti] -= (dy / dist) * force;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Center gravity
|
|
139
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
140
|
+
vx[i] -= (nodes[i].x - width / 2) * 0.005;
|
|
141
|
+
vy[i] -= (nodes[i].y - height / 2) * 0.005;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Apply
|
|
145
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
146
|
+
nodes[i].x += vx[i] * cooling;
|
|
147
|
+
nodes[i].y += vy[i] * cooling;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fit to viewport with padding
|
|
152
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
153
|
+
for (const n of nodes) {
|
|
154
|
+
if (n.x < minX) minX = n.x;
|
|
155
|
+
if (n.y < minY) minY = n.y;
|
|
156
|
+
if (n.x > maxX) maxX = n.x;
|
|
157
|
+
if (n.y > maxY) maxY = n.y;
|
|
158
|
+
}
|
|
159
|
+
const pad = 60;
|
|
160
|
+
const scale = Math.min((width - pad * 2) / (maxX - minX || 1), (height - pad * 2) / (maxY - minY || 1));
|
|
161
|
+
for (const n of nodes) {
|
|
162
|
+
n.x = (n.x - minX) * scale + pad;
|
|
163
|
+
n.y = (n.y - minY) * scale + pad;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function generateSVG(rootPath: string, nodes: FileNode[], edges: { source: string; target: string }[]): string {
|
|
168
|
+
const W = 1200, H = 900;
|
|
169
|
+
|
|
170
|
+
const layoutNodes: LayoutNode[] = nodes.map(n => ({
|
|
171
|
+
id: n.id, label: shortenPath(n.id), language: n.language,
|
|
172
|
+
chunkCount: n.chunkCount, x: 0, y: 0, color: LANG_COLORS[n.language] || '#888',
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
forceLayout(layoutNodes, edges, W, H);
|
|
176
|
+
|
|
177
|
+
const nodeIdSet = new Set(layoutNodes.map(n => n.id));
|
|
178
|
+
|
|
179
|
+
let edgeSvgs = '';
|
|
180
|
+
for (const e of edges) {
|
|
181
|
+
const s = layoutNodes.find(n => n.id === e.source);
|
|
182
|
+
const t = layoutNodes.find(n => n.id === e.target);
|
|
183
|
+
if (s && t) {
|
|
184
|
+
edgeSvgs += `<line x1="${s.x.toFixed(1)}" y1="${s.y.toFixed(1)}" x2="${t.x.toFixed(1)}" y2="${t.y.toFixed(1)}" stroke="#555" stroke-width="1.5" stroke-opacity="0.5"/>\n`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let nodeSvgs = '';
|
|
189
|
+
for (const n of layoutNodes) {
|
|
190
|
+
const r = Math.max(4, Math.sqrt(n.chunkCount) * 3);
|
|
191
|
+
nodeSvgs += `<circle cx="${n.x.toFixed(1)}" cy="${n.y.toFixed(1)}" r="${r}" fill="${n.color}" stroke="#fff" stroke-width="1" opacity="0.85">`;
|
|
192
|
+
nodeSvgs += `<title>${escapeXml(n.id)}\n${n.language} · ${n.chunkCount} chunks</title>`;
|
|
193
|
+
nodeSvgs += `</circle>\n`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let labelSvgs = '';
|
|
197
|
+
for (const n of layoutNodes) {
|
|
198
|
+
const r = Math.max(4, Math.sqrt(n.chunkCount) * 3);
|
|
199
|
+
labelSvgs += `<text x="${n.x.toFixed(1)}" y="${(n.y - r - 4).toFixed(1)}" text-anchor="middle" font-size="9" fill="#ccc" font-family="sans-serif">${escapeXml(n.label)}</text>\n`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let legendSvgs = '';
|
|
203
|
+
let ly = H - 80;
|
|
204
|
+
for (const [lang, color] of Object.entries(LANG_COLORS)) {
|
|
205
|
+
legendSvgs += `<circle cx="20" cy="${ly}" r="5" fill="${color}"/><text x="32" y="${ly + 4}" font-size="11" fill="#aaa" font-family="sans-serif">${langLabel(lang)}</text>\n`;
|
|
206
|
+
ly += 18;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fileCount = nodes.length;
|
|
210
|
+
const edgeCount = edges.length;
|
|
211
|
+
|
|
212
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="100%" height="100%" style="background:#1a1a2e">
|
|
213
|
+
<style>
|
|
214
|
+
circle { transition: opacity 0.2s; cursor: pointer; }
|
|
215
|
+
circle:hover { opacity: 1 !important; stroke-width: 2 !important; }
|
|
216
|
+
line { transition: opacity 0.2s; }
|
|
217
|
+
</style>
|
|
218
|
+
<rect x="0" y="0" width="${W}" height="${H}" fill="#1a1a2e"/>
|
|
219
|
+
<text x="20" y="30" font-size="16" font-weight="bold" fill="#e0e0e0" font-family="sans-serif">EDHIndex — Dependency Graph</text>
|
|
220
|
+
<text x="20" y="48" font-size="11" fill="#666" font-family="sans-serif">${escapeXml(rootPath)}</text>
|
|
221
|
+
<text x="${W - 20}" y="30" font-size="12" fill="#888" text-anchor="end" font-family="sans-serif">${fileCount} files · ${edgeCount} edges</text>
|
|
222
|
+
<g id="edges">
|
|
223
|
+
${edgeSvgs}</g>
|
|
224
|
+
<g id="nodes">
|
|
225
|
+
${nodeSvgs}</g>
|
|
226
|
+
<g id="labels">
|
|
227
|
+
${labelSvgs}</g>
|
|
228
|
+
<g id="legend">
|
|
229
|
+
<rect x="8" y="${H - 110}" width="100" height="${Object.keys(LANG_COLORS).length * 18 + 10}" fill="none"/>
|
|
230
|
+
${legendSvgs}</g>
|
|
231
|
+
</svg>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function escapeXml(s: string): string {
|
|
235
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function langLabel(lang: string): string {
|
|
239
|
+
const map: Record<string, string> = { ts: 'TypeScript', js: 'JavaScript', py: 'Python', go: 'Go' };
|
|
240
|
+
return map[lang] || lang;
|
|
241
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type NodeType =
|
|
2
|
+
| 'workspace'
|
|
3
|
+
| 'folder'
|
|
4
|
+
| 'file'
|
|
5
|
+
| 'module'
|
|
6
|
+
| 'class'
|
|
7
|
+
| 'interface'
|
|
8
|
+
| 'enum'
|
|
9
|
+
| 'function'
|
|
10
|
+
| 'method'
|
|
11
|
+
| 'variable'
|
|
12
|
+
| 'import'
|
|
13
|
+
| 'export';
|
|
14
|
+
|
|
15
|
+
export type EdgeType =
|
|
16
|
+
| 'contains'
|
|
17
|
+
| 'imports'
|
|
18
|
+
| 'exports'
|
|
19
|
+
| 'inherits'
|
|
20
|
+
| 'implements'
|
|
21
|
+
| 'calls'
|
|
22
|
+
| 'references'
|
|
23
|
+
| 'defines'
|
|
24
|
+
| 'belongs_to';
|
|
25
|
+
|
|
26
|
+
export interface GraphNode {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
type: NodeType;
|
|
30
|
+
language: string;
|
|
31
|
+
file: string;
|
|
32
|
+
startLine: number;
|
|
33
|
+
endLine: number;
|
|
34
|
+
metadata: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GraphEdge {
|
|
38
|
+
id: string;
|
|
39
|
+
source: string;
|
|
40
|
+
target: string;
|
|
41
|
+
type: EdgeType;
|
|
42
|
+
metadata: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GraphData {
|
|
46
|
+
nodes: GraphNode[];
|
|
47
|
+
edges: GraphEdge[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type GraphLayout = 'force' | 'hierarchical' | 'circular' | 'grid';
|
|
51
|
+
|
|
52
|
+
export interface GraphConfig {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
layout: GraphLayout;
|
|
55
|
+
animations: boolean;
|
|
56
|
+
showFolders: boolean;
|
|
57
|
+
showFiles: boolean;
|
|
58
|
+
showFunctions: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_GRAPH_CONFIG: GraphConfig = {
|
|
62
|
+
enabled: true,
|
|
63
|
+
layout: 'force',
|
|
64
|
+
animations: true,
|
|
65
|
+
showFolders: true,
|
|
66
|
+
showFiles: true,
|
|
67
|
+
showFunctions: true,
|
|
68
|
+
};
|
package/src/mcp/server.ts
CHANGED
|
@@ -2,125 +2,252 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import {
|
|
3
3
|
ListToolsRequestSchema,
|
|
4
4
|
CallToolRequestSchema,
|
|
5
|
+
ListResourcesRequestSchema,
|
|
6
|
+
ReadResourceRequestSchema,
|
|
5
7
|
McpError,
|
|
6
8
|
ErrorCode,
|
|
7
9
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
10
|
import { SearchEngine, SearchResult } from '../retrieval/search.js';
|
|
11
|
+
import { GraphService } from '../graph/service.js';
|
|
12
|
+
import { renderGraphSVG } from '../graph/render.js';
|
|
9
13
|
import { logger } from '../logging.js';
|
|
10
14
|
|
|
11
15
|
export function createMCPServer(searchEngine: SearchEngine, rootPath: string): Server {
|
|
16
|
+
let graphSvc: GraphService | null = null;
|
|
17
|
+
|
|
18
|
+
function getGraph(): GraphService {
|
|
19
|
+
if (!graphSvc) {
|
|
20
|
+
try {
|
|
21
|
+
graphSvc = new GraphService(rootPath);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new McpError(ErrorCode.InternalError, 'No graph available — run `edhindex start` to build the index');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return graphSvc;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
const server = new Server(
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
version: '1.0.0',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
capabilities: {
|
|
19
|
-
tools: {},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
30
|
+
{ name: 'edhindex', version: '1.0.0' },
|
|
31
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
22
32
|
);
|
|
23
33
|
|
|
24
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
maxResults: {
|
|
38
|
-
type: 'number',
|
|
39
|
-
description: 'Maximum results to return (default 10)',
|
|
40
|
-
default: 10,
|
|
41
|
-
},
|
|
42
|
-
fileFilter: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
description: 'Optional file path filter (glob pattern)',
|
|
45
|
-
},
|
|
46
|
-
languageFilter: {
|
|
47
|
-
type: 'string',
|
|
48
|
-
description: 'Optional language filter (ts, js, py, go)',
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
required: ['query'],
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
35
|
+
tools: [
|
|
36
|
+
{
|
|
37
|
+
name: 'search_codebase',
|
|
38
|
+
description: `Search the indexed codebase using hybrid (keyword + semantic + reranked) search. Workspace: ${rootPath}. Searches file contents, symbols, and code structure.`,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
query: { type: 'string', description: 'Search query' },
|
|
43
|
+
maxResults: { type: 'number', description: 'Max results (default 10)', default: 10 },
|
|
44
|
+
fileFilter: { type: 'string', description: 'File path filter (glob)' },
|
|
45
|
+
languageFilter: { type: 'string', description: 'Language filter (ts, js, py, go)' },
|
|
52
46
|
},
|
|
47
|
+
required: ['query'],
|
|
53
48
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'get_graph',
|
|
52
|
+
description: 'Get the full knowledge graph — all nodes and edges representing the indexed codebase structure. Returns nodes (files, folders, classes, functions, etc.) and their relationships (imports, exports, contains, defines).',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
limit: { type: 'number', description: 'Max nodes to return (default all)' },
|
|
57
|
+
offset: { type: 'number', description: 'Pagination offset' },
|
|
58
|
+
type: { type: 'string', description: 'Filter by node type (class, function, file, folder, etc.)' },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'get_node',
|
|
64
|
+
description: 'Get details for a specific node by ID, including all neighbors (connected nodes and edges).',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
id: { type: 'string', description: 'Node ID (e.g. "kg:sym:src/app.ts:App")' },
|
|
69
|
+
file: { type: 'string', description: 'Alternative: find node by file path' },
|
|
70
|
+
symbol: { type: 'string', description: 'Symbol name (used with file)' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'search_graph',
|
|
76
|
+
description: 'Search nodes in the knowledge graph by name, symbol, or file path. Returns matching nodes with their connections.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
query: { type: 'string', description: 'Search query to match against node labels and file paths' },
|
|
81
|
+
type: { type: 'string', description: 'Filter by node type (class, function, file, etc.)' },
|
|
82
|
+
},
|
|
83
|
+
required: ['query'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'get_graph_stats',
|
|
88
|
+
description: 'Get statistics about the knowledge graph — total node count, edge count, breakdown by type.',
|
|
89
|
+
inputSchema: { type: 'object', properties: {} },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}));
|
|
57
93
|
|
|
58
94
|
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
|
|
59
|
-
|
|
60
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
95
|
+
const name = request.params.name;
|
|
63
96
|
const args = request.params.arguments || {};
|
|
64
|
-
const query = String(args.query || '').trim();
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
switch (name) {
|
|
99
|
+
case 'search_codebase': return handleSearch(args, searchEngine, rootPath);
|
|
100
|
+
case 'get_graph': return handleGetGraph(args, getGraph());
|
|
101
|
+
case 'get_node': return handleGetNode(args, getGraph());
|
|
102
|
+
case 'search_graph': return handleSearchGraph(args, getGraph());
|
|
103
|
+
case 'get_graph_stats': return handleGraphStats(getGraph());
|
|
104
|
+
default:
|
|
105
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
68
106
|
}
|
|
107
|
+
});
|
|
69
108
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
}
|
|
109
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
110
|
+
resources: [
|
|
111
|
+
{
|
|
112
|
+
uri: `edhindex://${encodeURIComponent(rootPath)}/graph`,
|
|
113
|
+
name: 'Knowledge Graph',
|
|
114
|
+
description: 'Full knowledge graph of the codebase — nodes and edges',
|
|
115
|
+
mimeType: 'application/json',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}));
|
|
82
119
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
121
|
+
const uri = request.params.uri;
|
|
122
|
+
if (uri.includes('/graph')) {
|
|
123
|
+
const svc = getGraph();
|
|
124
|
+
const data = svc.getGraphData();
|
|
125
|
+
return {
|
|
126
|
+
contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }],
|
|
127
|
+
};
|
|
89
128
|
}
|
|
129
|
+
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
90
130
|
});
|
|
91
131
|
|
|
92
|
-
|
|
93
|
-
server.onerror = (error: any) => {
|
|
94
|
-
logger.error('MCP Server error:', error);
|
|
95
|
-
};
|
|
132
|
+
server.onerror = (error: any) => logger.error('MCP Server error:', error);
|
|
96
133
|
|
|
97
134
|
return server;
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (r.symbol) lines.push(` Symbol: ${r.symbol}${r.kind ? ` (${r.kind})` : ''}`);
|
|
113
|
-
lines.push(` ${'─'.repeat(60)}`);
|
|
114
|
-
textParts.push(lines.join('\n'));
|
|
137
|
+
async function handleSearch(args: any, searchEngine: SearchEngine, rootPath: string) {
|
|
138
|
+
const query = String(args.query || '').trim();
|
|
139
|
+
if (!query) throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
|
140
|
+
|
|
141
|
+
const maxResults = Math.min(Math.max(Number(args.maxResults) || 10, 1), 100);
|
|
142
|
+
const fileFilter = args.fileFilter ? String(args.fileFilter) : undefined;
|
|
143
|
+
const languageFilter = args.languageFilter ? String(args.languageFilter) : undefined;
|
|
144
|
+
|
|
145
|
+
if (fileFilter) {
|
|
146
|
+
const { sanitizePath } = await import('../security.js');
|
|
147
|
+
if (!sanitizePath(rootPath, fileFilter)) {
|
|
148
|
+
throw new McpError(ErrorCode.InvalidParams, 'File filter is outside the workspace');
|
|
115
149
|
}
|
|
116
150
|
}
|
|
117
151
|
|
|
152
|
+
try {
|
|
153
|
+
const results = await searchEngine.search({ query, maxResults, fileFilter, languageFilter });
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: formatResults(results),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
} catch (e: any) {
|
|
163
|
+
logger.error('Search failed:', e);
|
|
164
|
+
throw new McpError(ErrorCode.InternalError, `Search failed: ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleGetGraph(args: any, svc: GraphService) {
|
|
169
|
+
const data = svc.getGraphData(args.limit, args.offset);
|
|
170
|
+
const type = args.type;
|
|
171
|
+
const nodes = type ? data.nodes.filter(n => n.type === type) : data.nodes;
|
|
172
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
173
|
+
const edges = data.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
174
|
+
|
|
175
|
+
const svg = renderGraphSVG(nodes, edges);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{ type: 'text', text: `Knowledge Graph: ${nodes.length} nodes, ${edges.length} edges\n\nGraph visualization (SVG):` },
|
|
180
|
+
{ type: 'text', text: svg },
|
|
181
|
+
{ type: 'text', text: `\n\nFull data:\n${JSON.stringify({ nodes, edges, totalNodes: svc.getStats().nodeCount, totalEdges: svc.getStats().edgeCount }, null, 2)}` },
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handleGetNode(args: any, svc: GraphService) {
|
|
187
|
+
let node = null;
|
|
188
|
+
if (args.id) {
|
|
189
|
+
node = svc.getNode(args.id);
|
|
190
|
+
} else if (args.file) {
|
|
191
|
+
node = svc.getNodeByPath(args.file, args.symbol);
|
|
192
|
+
}
|
|
193
|
+
if (!node) throw new McpError(ErrorCode.InvalidParams, 'Node not found');
|
|
194
|
+
|
|
195
|
+
const neighbors = svc.getNeighbors(node.id);
|
|
196
|
+
const children = svc.getChildren(node.id);
|
|
197
|
+
|
|
118
198
|
return {
|
|
119
199
|
content: [
|
|
120
200
|
{
|
|
121
201
|
type: 'text',
|
|
122
|
-
text:
|
|
202
|
+
text: JSON.stringify({ node, neighbors, children }, null, 2),
|
|
123
203
|
},
|
|
124
204
|
],
|
|
125
205
|
};
|
|
126
206
|
}
|
|
207
|
+
|
|
208
|
+
function handleSearchGraph(args: any, svc: GraphService) {
|
|
209
|
+
const query = String(args.query || '').trim();
|
|
210
|
+
if (!query) throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
|
211
|
+
|
|
212
|
+
let results = svc.searchNodes(query);
|
|
213
|
+
if (args.type) results = results.filter(n => n.type === args.type);
|
|
214
|
+
|
|
215
|
+
const enriched = results.slice(0, 30).map(n => {
|
|
216
|
+
const neighbors = svc.getNeighbors(n.id);
|
|
217
|
+
return { node: n, connectionCount: neighbors.length };
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: 'text',
|
|
224
|
+
text: JSON.stringify({ results: enriched, total: results.length }, null, 2),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function handleGraphStats(svc: GraphService) {
|
|
231
|
+
const stats = svc.getStats();
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: JSON.stringify(stats, null, 2),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatResults(results: SearchResult[]) {
|
|
243
|
+
if (results.length === 0) return 'No results found.';
|
|
244
|
+
const lines: string[] = [`Found ${results.length} result(s):\n`];
|
|
245
|
+
for (let i = 0; i < results.length; i++) {
|
|
246
|
+
const r = results[i];
|
|
247
|
+
lines.push(`[${i + 1}] ${r.file}:${r.startLine}-${r.endLine}`);
|
|
248
|
+
lines.push(` Language: ${r.language} | Type: ${r.matchType} | Score: ${r.score.toFixed(3)}`);
|
|
249
|
+
if (r.symbol) lines.push(` Symbol: ${r.symbol}${r.kind ? ` (${r.kind})` : ''}`);
|
|
250
|
+
lines.push(` ${'─'.repeat(60)}`);
|
|
251
|
+
}
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|