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.
Files changed (199) hide show
  1. package/README.md +228 -0
  2. package/dist/cli/commands/config_cmd.d.ts +2 -0
  3. package/dist/cli/commands/config_cmd.d.ts.map +1 -0
  4. package/dist/cli/commands/config_cmd.js +53 -0
  5. package/dist/cli/commands/config_cmd.js.map +1 -0
  6. package/dist/cli/commands/doctor_cmd.d.ts +2 -0
  7. package/dist/cli/commands/doctor_cmd.d.ts.map +1 -0
  8. package/dist/cli/commands/doctor_cmd.js +32 -0
  9. package/dist/cli/commands/doctor_cmd.js.map +1 -0
  10. package/dist/cli/commands/graph.d.ts +2 -0
  11. package/dist/cli/commands/graph.d.ts.map +1 -0
  12. package/dist/cli/commands/graph.js +26 -0
  13. package/dist/cli/commands/graph.js.map +1 -0
  14. package/dist/cli/commands/index_cmd.d.ts +2 -0
  15. package/dist/cli/commands/index_cmd.d.ts.map +1 -0
  16. package/dist/cli/commands/index_cmd.js +110 -0
  17. package/dist/cli/commands/index_cmd.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +2 -0
  19. package/dist/cli/commands/init.d.ts.map +1 -0
  20. package/dist/cli/commands/init.js +53 -0
  21. package/dist/cli/commands/init.js.map +1 -0
  22. package/dist/cli/commands/knowledge-graph.d.ts +8 -0
  23. package/dist/cli/commands/knowledge-graph.d.ts.map +1 -0
  24. package/dist/cli/commands/knowledge-graph.js +76 -0
  25. package/dist/cli/commands/knowledge-graph.js.map +1 -0
  26. package/dist/cli/commands/models.d.ts +2 -0
  27. package/dist/cli/commands/models.d.ts.map +1 -0
  28. package/dist/cli/commands/models.js +17 -0
  29. package/dist/cli/commands/models.js.map +1 -0
  30. package/dist/cli/commands/reset.d.ts +2 -0
  31. package/dist/cli/commands/reset.d.ts.map +1 -0
  32. package/dist/cli/commands/reset.js +35 -0
  33. package/dist/cli/commands/reset.js.map +1 -0
  34. package/dist/cli/commands/search.d.ts +2 -0
  35. package/dist/cli/commands/search.d.ts.map +1 -0
  36. package/dist/cli/commands/search.js +40 -0
  37. package/dist/cli/commands/search.js.map +1 -0
  38. package/dist/cli/commands/start.d.ts +2 -0
  39. package/dist/cli/commands/start.d.ts.map +1 -0
  40. package/dist/cli/commands/start.js +265 -0
  41. package/dist/cli/commands/start.js.map +1 -0
  42. package/dist/cli/commands/status.d.ts +2 -0
  43. package/dist/cli/commands/status.d.ts.map +1 -0
  44. package/dist/cli/commands/status.js +47 -0
  45. package/dist/cli/commands/status.js.map +1 -0
  46. package/dist/cli/index.d.ts +3 -0
  47. package/dist/cli/index.d.ts.map +1 -0
  48. package/dist/cli/index.js +121 -0
  49. package/dist/cli/index.js.map +1 -0
  50. package/dist/config/index.d.ts +28 -0
  51. package/dist/config/index.d.ts.map +1 -0
  52. package/dist/config/index.js +65 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/doctor/index.d.ts +8 -0
  55. package/dist/doctor/index.d.ts.map +1 -0
  56. package/dist/doctor/index.js +117 -0
  57. package/dist/doctor/index.js.map +1 -0
  58. package/dist/embeddings/provider.d.ts +9 -0
  59. package/dist/embeddings/provider.d.ts.map +1 -0
  60. package/dist/embeddings/provider.js +2 -0
  61. package/dist/embeddings/provider.js.map +1 -0
  62. package/dist/embeddings/transformers.d.ts +26 -0
  63. package/dist/embeddings/transformers.d.ts.map +1 -0
  64. package/dist/embeddings/transformers.js +120 -0
  65. package/dist/embeddings/transformers.js.map +1 -0
  66. package/dist/graph/build.d.ts +13 -0
  67. package/dist/graph/build.d.ts.map +1 -0
  68. package/dist/graph/build.js +33 -0
  69. package/dist/graph/build.js.map +1 -0
  70. package/dist/graph/builder.d.ts +24 -0
  71. package/dist/graph/builder.d.ts.map +1 -0
  72. package/dist/graph/builder.js +231 -0
  73. package/dist/graph/builder.js.map +1 -0
  74. package/dist/graph/render.d.ts +3 -0
  75. package/dist/graph/render.d.ts.map +1 -0
  76. package/dist/graph/render.js +60 -0
  77. package/dist/graph/render.js.map +1 -0
  78. package/dist/graph/serve.d.ts +6 -0
  79. package/dist/graph/serve.d.ts.map +1 -0
  80. package/dist/graph/serve.js +389 -0
  81. package/dist/graph/serve.js.map +1 -0
  82. package/dist/graph/service.d.ts +29 -0
  83. package/dist/graph/service.d.ts.map +1 -0
  84. package/dist/graph/service.js +73 -0
  85. package/dist/graph/service.js.map +1 -0
  86. package/dist/graph/storage.d.ts +33 -0
  87. package/dist/graph/storage.d.ts.map +1 -0
  88. package/dist/graph/storage.js +164 -0
  89. package/dist/graph/storage.js.map +1 -0
  90. package/dist/graph/svg.d.ts +27 -0
  91. package/dist/graph/svg.d.ts.map +1 -0
  92. package/dist/graph/svg.js +212 -0
  93. package/dist/graph/svg.js.map +1 -0
  94. package/dist/graph/types.d.ts +34 -0
  95. package/dist/graph/types.d.ts.map +1 -0
  96. package/dist/graph/types.js +9 -0
  97. package/dist/graph/types.js.map +1 -0
  98. package/dist/indexer/chunker.d.ts +18 -0
  99. package/dist/indexer/chunker.d.ts.map +1 -0
  100. package/dist/indexer/chunker.js +139 -0
  101. package/dist/indexer/chunker.js.map +1 -0
  102. package/dist/indexer/file-utils.d.ts +17 -0
  103. package/dist/indexer/file-utils.d.ts.map +1 -0
  104. package/dist/indexer/file-utils.js +123 -0
  105. package/dist/indexer/file-utils.js.map +1 -0
  106. package/dist/indexer/ignore.d.ts +5 -0
  107. package/dist/indexer/ignore.d.ts.map +1 -0
  108. package/dist/indexer/ignore.js +96 -0
  109. package/dist/indexer/ignore.js.map +1 -0
  110. package/dist/indexer/incremental.d.ts +16 -0
  111. package/dist/indexer/incremental.d.ts.map +1 -0
  112. package/dist/indexer/incremental.js +55 -0
  113. package/dist/indexer/incremental.js.map +1 -0
  114. package/dist/indexer/indexer.d.ts +36 -0
  115. package/dist/indexer/indexer.d.ts.map +1 -0
  116. package/dist/indexer/indexer.js +121 -0
  117. package/dist/indexer/indexer.js.map +1 -0
  118. package/dist/indexer/parser.d.ts +30 -0
  119. package/dist/indexer/parser.d.ts.map +1 -0
  120. package/dist/indexer/parser.js +225 -0
  121. package/dist/indexer/parser.js.map +1 -0
  122. package/dist/logging.d.ts +15 -0
  123. package/dist/logging.d.ts.map +1 -0
  124. package/dist/logging.js +42 -0
  125. package/dist/logging.js.map +1 -0
  126. package/dist/mcp/server.d.ts +4 -0
  127. package/dist/mcp/server.d.ts.map +1 -0
  128. package/dist/mcp/server.js +226 -0
  129. package/dist/mcp/server.js.map +1 -0
  130. package/dist/mcp-config.d.ts +4 -0
  131. package/dist/mcp-config.d.ts.map +1 -0
  132. package/dist/mcp-config.js +175 -0
  133. package/dist/mcp-config.js.map +1 -0
  134. package/dist/progress.d.ts +12 -0
  135. package/dist/progress.d.ts.map +1 -0
  136. package/dist/progress.js +16 -0
  137. package/dist/progress.js.map +1 -0
  138. package/dist/retrieval/bm25.d.ts +6 -0
  139. package/dist/retrieval/bm25.d.ts.map +1 -0
  140. package/dist/retrieval/bm25.js +20 -0
  141. package/dist/retrieval/bm25.js.map +1 -0
  142. package/dist/retrieval/reranker.d.ts +11 -0
  143. package/dist/retrieval/reranker.d.ts.map +1 -0
  144. package/dist/retrieval/reranker.js +46 -0
  145. package/dist/retrieval/reranker.js.map +1 -0
  146. package/dist/retrieval/search.d.ts +35 -0
  147. package/dist/retrieval/search.d.ts.map +1 -0
  148. package/dist/retrieval/search.js +95 -0
  149. package/dist/retrieval/search.js.map +1 -0
  150. package/dist/security.d.ts +5 -0
  151. package/dist/security.d.ts.map +1 -0
  152. package/dist/security.js +42 -0
  153. package/dist/security.js.map +1 -0
  154. package/dist/storage/fts.d.ts +30 -0
  155. package/dist/storage/fts.d.ts.map +1 -0
  156. package/dist/storage/fts.js +107 -0
  157. package/dist/storage/fts.js.map +1 -0
  158. package/dist/storage/metadata.d.ts +46 -0
  159. package/dist/storage/metadata.d.ts.map +1 -0
  160. package/dist/storage/metadata.js +133 -0
  161. package/dist/storage/metadata.js.map +1 -0
  162. package/dist/vector/lancedb.d.ts +25 -0
  163. package/dist/vector/lancedb.d.ts.map +1 -0
  164. package/dist/vector/lancedb.js +93 -0
  165. package/dist/vector/lancedb.js.map +1 -0
  166. package/dist/vector/store.d.ts +16 -0
  167. package/dist/vector/store.d.ts.map +1 -0
  168. package/dist/vector/store.js +2 -0
  169. package/dist/vector/store.js.map +1 -0
  170. package/dist/version.d.ts +12 -0
  171. package/dist/version.d.ts.map +1 -0
  172. package/dist/version.js +46 -0
  173. package/dist/version.js.map +1 -0
  174. package/dist/watcher/index.d.ts +15 -0
  175. package/dist/watcher/index.d.ts.map +1 -0
  176. package/dist/watcher/index.js +65 -0
  177. package/dist/watcher/index.js.map +1 -0
  178. package/package.json +35 -4
  179. package/src/cli/commands/graph.ts +30 -0
  180. package/src/cli/commands/init.ts +41 -3
  181. package/src/cli/commands/knowledge-graph.ts +85 -0
  182. package/src/cli/commands/reset.ts +40 -0
  183. package/src/cli/commands/start.ts +54 -21
  184. package/src/cli/index.ts +33 -0
  185. package/src/config/index.ts +13 -0
  186. package/src/graph/build.ts +45 -0
  187. package/src/graph/builder.ts +243 -0
  188. package/src/graph/render.ts +67 -0
  189. package/src/graph/serve.ts +399 -0
  190. package/src/graph/service.ts +84 -0
  191. package/src/graph/storage.ts +181 -0
  192. package/src/graph/svg.ts +241 -0
  193. package/src/graph/types.ts +68 -0
  194. package/src/mcp/server.ts +212 -85
  195. package/src/mcp-config.ts +185 -0
  196. package/src/storage/metadata.ts +9 -1
  197. package/.edhindexignore +0 -66
  198. package/BUILD_BRIEF.md +0 -228
  199. package/tsconfig.json +0 -21
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
- name: 'edhindex',
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
- return {
26
- tools: [
27
- {
28
- name: 'search_codebase',
29
- description: `Search the indexed codebase using hybrid (keyword + semantic + reranked) search. Workspace: ${rootPath}. Searches file contents, symbols, and code structure.`,
30
- inputSchema: {
31
- type: 'object',
32
- properties: {
33
- query: {
34
- type: 'string',
35
- description: 'Search query - natural language or code terms',
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
- if (request.params.name !== 'search_codebase') {
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
- if (!query) {
67
- throw new McpError(ErrorCode.InvalidParams, 'query is required');
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
- const maxResults = Math.min(Math.max(Number(args.maxResults) || 10, 1), 100);
71
- const fileFilter = args.fileFilter ? String(args.fileFilter) : undefined;
72
- const languageFilter = args.languageFilter ? String(args.languageFilter) : undefined;
73
-
74
- // Security: sanitize and restrict to workspace
75
- if (fileFilter) {
76
- const { sanitizePath } = await import('../security.js');
77
- const safe = sanitizePath(rootPath, fileFilter);
78
- if (!safe) {
79
- throw new McpError(ErrorCode.InvalidParams, 'File filter is outside the workspace');
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
- try {
84
- const results = await searchEngine.search({ query, maxResults, fileFilter, languageFilter });
85
- return formatResults(results);
86
- } catch (e: any) {
87
- logger.error('Search failed:', e);
88
- throw new McpError(ErrorCode.InternalError, `Search failed: ${e.message}`);
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
- // Error handler
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 formatResults(results: SearchResult[]) {
101
- const textParts: string[] = [];
102
-
103
- if (results.length === 0) {
104
- textParts.push('No results found.');
105
- } else {
106
- textParts.push(`Found ${results.length} result(s):\n`);
107
- for (let i = 0; i < results.length; i++) {
108
- const r = results[i];
109
- const lines: string[] = [];
110
- lines.push(`[${i + 1}] ${r.file}:${r.startLine}-${r.endLine}`);
111
- lines.push(` Language: ${r.language} | Type: ${r.matchType} | Score: ${r.score.toFixed(3)}`);
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: textParts.join('\n'),
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
+ }