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,399 @@
1
+ import { createServer, IncomingMessage, ServerResponse } from 'node:http';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { GraphService } from './service.js';
5
+
6
+ const NODE_COLORS: Record<string, string> = {
7
+ workspace: '#ffd700', folder: '#8bc34a', file: '#64b5f6',
8
+ module: '#ce93d8', class: '#ff7043', interface: '#4dd0e1',
9
+ enum: '#ffb74d', function: '#81c784', method: '#aed581',
10
+ variable: '#e0e0e0',
11
+ };
12
+
13
+ const NODE_SHAPES: Record<string, string> = {
14
+ workspace: 'ellipse', folder: 'roundrectangle', file: 'roundrectangle',
15
+ class: 'diamond', interface: 'diamond', enum: 'hexagon',
16
+ function: 'triangle', method: 'triangle',
17
+ };
18
+
19
+ function prepareGraphJSON(rootPath: string) {
20
+ const svc = new GraphService(rootPath);
21
+ const data = svc.getGraphData();
22
+ const nodeIds = new Set(data.nodes.map(n => n.id));
23
+ const validEdges = data.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
24
+ const nodesJson = JSON.stringify(data.nodes.map(n => ({
25
+ data: {
26
+ id: n.id, label: n.label, type: n.type,
27
+ lang: n.language, file: n.file,
28
+ startLine: n.startLine, endLine: n.endLine,
29
+ color: NODE_COLORS[n.type] || '#888',
30
+ shape: NODE_SHAPES[n.type] || 'ellipse',
31
+ },
32
+ })));
33
+ const edgesJson = JSON.stringify(validEdges.map(e => ({
34
+ data: { id: e.id, source: e.source, target: e.target, type: e.type },
35
+ })));
36
+ svc.close();
37
+ return { nodesJson, edgesJson };
38
+ }
39
+
40
+ export function writeViewerFile(rootPath: string, outputPath: string) {
41
+ const { nodesJson, edgesJson } = prepareGraphJSON(rootPath);
42
+ const html = generateViewerHTML(nodesJson, edgesJson, rootPath);
43
+ writeFileSync(outputPath, html, 'utf-8');
44
+ return outputPath;
45
+ }
46
+
47
+ export async function startGraphServer(rootPath: string, port: number = 0): Promise<{ port: number; close: () => void }> {
48
+ const svc = new GraphService(rootPath);
49
+ const data = svc.getGraphData();
50
+ const nodeIds = new Set(data.nodes.map(n => n.id));
51
+ const validEdges = data.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
52
+ const nodesJson = JSON.stringify(data.nodes.map(n => ({
53
+ data: {
54
+ id: n.id, label: n.label, type: n.type,
55
+ lang: n.language, file: n.file,
56
+ startLine: n.startLine, endLine: n.endLine,
57
+ color: NODE_COLORS[n.type] || '#888',
58
+ shape: NODE_SHAPES[n.type] || 'ellipse',
59
+ },
60
+ })));
61
+ const edgesJson = JSON.stringify(validEdges.map(e => ({
62
+ data: { id: e.id, source: e.source, target: e.target, type: e.type },
63
+ })));
64
+
65
+ const server = createServer((req: IncomingMessage, res: ServerResponse) => {
66
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
67
+ const path = url.pathname;
68
+
69
+ res.setHeader('Access-Control-Allow-Origin', '*');
70
+
71
+ if (path === '/api/graph') {
72
+ res.writeHead(200, { 'Content-Type': 'application/json' });
73
+ res.end(JSON.stringify(data));
74
+ return;
75
+ }
76
+
77
+ if (path === '/api/stats') {
78
+ res.writeHead(200, { 'Content-Type': 'application/json' });
79
+ res.end(JSON.stringify(svc.getStats()));
80
+ return;
81
+ }
82
+
83
+ if (path === '/api/search') {
84
+ const q = url.searchParams.get('q') || '';
85
+ const results = q ? svc.searchNodes(q) : [];
86
+ res.writeHead(200, { 'Content-Type': 'application/json' });
87
+ res.end(JSON.stringify(results.slice(0, 30)));
88
+ return;
89
+ }
90
+
91
+ if (path.startsWith('/api/node/')) {
92
+ const id = decodeURIComponent(path.slice(10));
93
+ const node = svc.getNode(id);
94
+ if (!node) { res.writeHead(404); res.end('Not found'); return; }
95
+ const neighbors = svc.getNeighbors(id);
96
+ const children = svc.getChildren(id);
97
+ res.writeHead(200, { 'Content-Type': 'application/json' });
98
+ res.end(JSON.stringify({ node, neighbors, children }));
99
+ return;
100
+ }
101
+
102
+ // Serve graph viewer HTML
103
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
104
+ res.end(generateViewerHTML(nodesJson, edgesJson, rootPath));
105
+ });
106
+
107
+ return new Promise((resolve) => {
108
+ server.listen(port, '127.0.0.1', () => {
109
+ const addr = server.address();
110
+ const actualPort = typeof addr === 'object' && addr ? addr.port : port;
111
+ resolve({ port: actualPort, close: () => { svc.close(); server.close(); } });
112
+ });
113
+ });
114
+ }
115
+
116
+ function generateViewerHTML(nodesJson: string, edgesJson: string, rootPath: string): string {
117
+ return `<!DOCTYPE html>
118
+ <html lang="en">
119
+ <head>
120
+ <meta charset="UTF-8">
121
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
+ <title>EDHIndex Knowledge Graph</title>
123
+ <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
124
+ <style>
125
+ *{margin:0;padding:0;box-sizing:border-box}
126
+ :root{--bg:#0d1117;--surface:#161b22;--surface2:#21262d;--text:#c9d1d9;--text2:#8b949e;--border:#30363d;--accent:#58a6ff}
127
+ body.light{--bg:#f6f8fa;--surface:#fff;--surface2:#f0f0f0;--text:#24292f;--text2:#656d76;--border:#d0d7de;--accent:#0969da}
128
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);overflow:hidden;height:100vh}
129
+ #cy{position:absolute;top:0;left:0;right:0;bottom:0}
130
+ #error-box{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99;background:#1a1a2e;border:1px solid #f33;border-radius:12px;padding:24px;max-width:500px;display:none;color:#f88;font-size:14px;text-align:center}
131
+ #error-box h3{margin-bottom:8px}
132
+ #error-box .detail{font-size:12px;color:#999;margin-top:8px;word-break:break-all}
133
+ #topbar{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:20;display:flex;gap:6px;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:6px 12px;backdrop-filter:blur(12px);box-shadow:0 4px 24px rgba(0,0,0,.4)}
134
+ #topbar input{padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface2);color:var(--text);font-size:13px;width:200px;outline:none;transition:border-color .15s}
135
+ #topbar input:focus{border-color:var(--accent)}
136
+ #topbar select{padding:5px 8px;border:1px solid var(--border);border-radius:6px;background:var(--surface2);color:var(--text);font-size:12px;cursor:pointer;outline:none}
137
+ #topbar button{border:none;border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer;background:var(--surface2);color:var(--text);transition:all .15s;display:flex;align-items:center;gap:4px}
138
+ #topbar button:hover{background:var(--accent);color:#fff}
139
+ #topbar .title{color:var(--text);font-weight:600;font-size:13px;margin-right:4px}
140
+ #topbar .sub{color:var(--text2);font-size:11px;margin-right:8px}
141
+ #panel{position:fixed;right:16px;top:68px;width:300px;max-height:calc(100vh - 90px);z-index:20;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.5);display:none;font-size:13px}
142
+ #panel h3{font-size:15px;margin-bottom:10px;color:var(--accent);display:flex;align-items:center;gap:6px}
143
+ #panel .tag{font-size:10px;padding:2px 6px;border-radius:4px;background:var(--surface2);color:var(--text2);margin-left:auto}
144
+ #panel .row{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--surface2);font-size:12px}
145
+ #panel .row:last-child{border-bottom:none}
146
+ #panel .lbl{color:var(--text2);flex-shrink:0}
147
+ #panel .val{color:var(--text);text-align:right;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
148
+ #panel .section{font-size:11px;color:var(--text2);margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
149
+ #panel .tag-list{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
150
+ #panel .tag-list span{padding:2px 6px;border-radius:4px;background:var(--surface2);font-size:11px;color:var(--text)}
151
+ #panel .tag-list .dim{opacity:.5}
152
+ #close-panel{position:absolute;top:8px;right:10px;border:none;background:none;color:var(--text2);cursor:pointer;font-size:16px;padding:4px}
153
+ #close-panel:hover{color:var(--text)}
154
+ #search-results{position:fixed;top:52px;left:50%;transform:translateX(-50%);z-index:19;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:4px 0;max-height:260px;overflow-y:auto;width:320px;display:none;box-shadow:0 8px 24px rgba(0,0,0,.4)}
155
+ #search-results div{padding:7px 12px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .1s}
156
+ #search-results div:hover{background:var(--surface2)}
157
+ #search-results .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
158
+ #legend{position:fixed;left:16px;bottom:20px;z-index:20;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:11px;box-shadow:0 4px 16px rgba(0,0,0,.3);display:flex;flex-direction:column;gap:3px}
159
+ #legend .row{display:flex;align-items:center;gap:6px}
160
+ #legend .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
161
+ #stats{position:fixed;right:16px;bottom:20px;z-index:20;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:11px;text-align:right;color:var(--text2);box-shadow:0 4px 16px rgba(0,0,0,.3)}
162
+ .empty{color:var(--text2);text-align:center;padding:20px;font-size:12px}
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <div id="error-box"><h3>⚠ Failed to render graph</h3><p id="error-msg">Cytoscape.js library failed to load.</p><p class="detail" id="error-detail"></p></div>
167
+ <div id="cy"></div>
168
+ <div id="topbar">
169
+ <span class="title">KG</span><span class="sub">${rootPath}</span>
170
+ <input id="search" type="text" placeholder="Search graph..." spellcheck="false">
171
+ <select id="layout-select">
172
+ <option value="cose">Force</option>
173
+ <option value="breadthfirst">Hierarchy</option>
174
+ <option value="circle">Circle</option>
175
+ <option value="grid">Grid</option>
176
+ <option value="concentric">Concentric</option>
177
+ </select>
178
+ <button id="btn-fit">⤢ Fit</button>
179
+ <button id="btn-theme">🌓</button>
180
+ </div>
181
+ <div id="search-results"></div>
182
+ <div id="panel"><button id="close-panel">✕</button><h3 id="panel-title">Node</h3><div id="panel-body"></div></div>
183
+ <div id="legend"></div>
184
+ <div id="stats"></div>
185
+
186
+ <script>
187
+ const elements = { nodes: ${nodesJson}, edges: ${edgesJson} };
188
+ function showError(msg, detail) {
189
+ const box = document.getElementById('error-box');
190
+ box.style.display = 'block';
191
+ document.getElementById('error-msg').textContent = msg;
192
+ if (detail) document.getElementById('error-detail').textContent = detail;
193
+ }
194
+ if (typeof cytoscape === 'undefined') {
195
+ showError('Cytoscape.js library failed to load. Check your internet connection and refresh.');
196
+ } else { try {
197
+ const types = [...new Set(elements.nodes.map(n => n.data.type))];
198
+ const colors = ${JSON.stringify(NODE_COLORS)};
199
+
200
+ // Legend
201
+ const legend = document.getElementById('legend');
202
+ const typeOrder = ['workspace','folder','file','module','class','interface','enum','function','method'];
203
+ for (const t of typeOrder) {
204
+ if (colors[t]) {
205
+ const r = document.createElement('div'); r.className = 'row';
206
+ r.innerHTML = '<span class="dot" style="background:'+colors[t]+'"></span>'+t.charAt(0).toUpperCase()+t.slice(1);
207
+ legend.appendChild(r);
208
+ }
209
+ }
210
+
211
+ const cy = cytoscape({
212
+ container: document.getElementById('cy'),
213
+ elements,
214
+ style: [
215
+ { selector: 'node', style: {
216
+ 'background-color': 'data(color)',
217
+ 'label': 'data(label)',
218
+ 'color': '#8b949e',
219
+ 'font-size': '10px',
220
+ 'text-valign': 'bottom',
221
+ 'text-halign': 'center',
222
+ 'text-margin-y': 4,
223
+ 'width': 22, 'height': 22,
224
+ 'border-width': 1, 'border-color': '#fff', 'border-opacity': 0.15,
225
+ 'transition-property': 'background-color, border-color, opacity',
226
+ 'transition-duration': 200,
227
+ }},
228
+ { selector: 'edge', style: {
229
+ 'width': 1.2, 'line-color': '#30363d',
230
+ 'target-arrow-color': '#30363d',
231
+ 'target-arrow-shape': 'triangle', 'arrow-scale': 0.5,
232
+ 'curve-style': 'bezier', 'opacity': 0.5,
233
+ }},
234
+ { selector: 'edge[type = "imports"]', style: { 'line-color': '#4dd0e1', 'target-arrow-color': '#4dd0e1', 'opacity': 0.7 }},
235
+ { selector: 'edge[type = "exports"]', style: { 'line-color': '#ffb74d', 'target-arrow-color': '#ffb74d', 'opacity': 0.7 }},
236
+ { selector: 'edge[type = "defines"]', style: { 'line-color': '#81c784', 'opacity': 0.35, 'width': 0.8 }},
237
+ { selector: 'edge[type = "contains"]', style: { 'line-color': '#555', 'opacity': 0.2, 'width': 0.6, 'target-arrow-shape': 'none' }},
238
+ { selector: 'node[type = "folder"]', style: { 'shape': 'round-rectangle', 'background-color': '#8bc34a' }},
239
+ { selector: 'node[type = "file"]', style: { 'shape': 'round-rectangle', 'background-color': '#64b5f6' }},
240
+ { selector: 'node[type = "class"]', style: { 'shape': 'diamond', 'width': 28, 'height': 28 }},
241
+ { selector: 'node[type = "interface"]', style: { 'shape': 'diamond', 'width': 24, 'height': 24, 'background-color': '#4dd0e1' }},
242
+ { selector: 'node[type = "function"]', style: { 'shape': 'round-triangle', 'width': 20, 'height': 20 }},
243
+ { selector: 'node[type = "method"]', style: { 'shape': 'round-triangle', 'width': 16, 'height': 16 }},
244
+ { selector: 'node[type = "enum"]', style: { 'shape': 'hexagon', 'background-color': '#ffb74d' }},
245
+ { selector: 'node:selected', style: { 'border-width': 3, 'border-color': '#ffd700', 'border-opacity': 1, 'label': 'data(label)', 'color': '#ffd700', 'font-weight': 'bold' }},
246
+ { selector: 'node.highlight', style: { 'border-width': 2, 'border-color': '#ffd700', 'border-opacity': 0.7 }},
247
+ { selector: 'edge.highlight', style: { 'opacity': 0.8, 'width': 2, 'line-color': '#ffd700' }},
248
+ { selector: '.faded', style: { 'opacity': 0.15 }},
249
+ ],
250
+ wheelSensitivity: 0.3,
251
+ minZoom: 0.1,
252
+ maxZoom: 4,
253
+ });
254
+
255
+ // Layouts — all built into cytoscape core, no external deps
256
+ const layouts = {
257
+ cose: { name: 'cose', animate: true, animationDuration: 500, nodeRepulsion: () => 12000, idealEdgeLength: () => 100, gravity: 0.08, numIter: 1000 },
258
+ breadthfirst: { name: 'breadthfirst', animate: true, animationDuration: 500, directed: true, spacingFactor: 1.2 },
259
+ circle: { name: 'circle', animate: true, animationDuration: 500 },
260
+ grid: { name: 'grid', animate: true, animationDuration: 500 },
261
+ concentric: { name: 'concentric', animate: true, animationDuration: 500, concentric: n => n.degree() },
262
+ };
263
+
264
+ function runLayout(name) {
265
+ const opts = layouts[name] || layouts.cose;
266
+ cy.layout(opts).run();
267
+ }
268
+
269
+ document.getElementById('layout-select').value = 'cose';
270
+ setTimeout(() => runLayout('cose'), 100);
271
+
272
+ document.getElementById('layout-select').addEventListener('change', e => runLayout(e.target.value));
273
+ document.getElementById('btn-fit').addEventListener('click', () => cy.fit(undefined, 50));
274
+
275
+ // Theme
276
+ let dark = true;
277
+ document.getElementById('btn-theme').addEventListener('click', () => {
278
+ dark = !dark;
279
+ document.body.classList.toggle('light', !dark);
280
+ cy.style().selector('node').style('color', dark ? '#8b949e' : '#57606a').update();
281
+ });
282
+
283
+ // Search
284
+ const search = document.getElementById('search');
285
+ const results = document.getElementById('search-results');
286
+ search.addEventListener('input', () => {
287
+ const q = search.value.trim().toLowerCase();
288
+ if (!q) { results.style.display = 'none'; return; }
289
+ const matches = cy.nodes().filter(n => n.data('label').toLowerCase().includes(q) || n.data('file').toLowerCase().includes(q));
290
+ if (!matches.length) { results.innerHTML = '<div class="empty">No matches</div>'; results.style.display = 'block'; return; }
291
+ const top = matches.slice(0, 25);
292
+ results.innerHTML = top.map(n =>
293
+ '<div data-id="'+n.id()+'"><span class="dot" style="background:'+n.data('color')+'"></span>'+n.data('label')+' <span style="color:#8b949e;font-size:10px">'+n.data('type')+'</span></div>'
294
+ ).join('');
295
+ results.style.display = 'block';
296
+ });
297
+
298
+ results.addEventListener('click', e => {
299
+ const div = e.target.closest('[data-id]');
300
+ if (!div) return;
301
+ const n = cy.getElementById(div.dataset.id);
302
+ if (n.length) {
303
+ cy.fit(n, 80);
304
+ n.select();
305
+ showNodeDetails(n);
306
+ results.style.display = 'none';
307
+ search.value = n.data('label');
308
+ }
309
+ });
310
+
311
+ document.addEventListener('click', e => { if (!e.target.closest('#topbar')) results.style.display = 'none'; });
312
+
313
+ // Node details
314
+ const panel = document.getElementById('panel');
315
+ const panelBody = document.getElementById('panel-body');
316
+ const panelTitle = document.getElementById('panel-title');
317
+ document.getElementById('close-panel').addEventListener('click', () => panel.style.display = 'none');
318
+
319
+ cy.on('tap', 'node', e => showNodeDetails(e.target));
320
+ cy.on('tap', () => {
321
+ const s = cy.$('node:selected');
322
+ if (s.length && panel.style.display !== 'none') showNodeDetails(s[0]);
323
+ });
324
+
325
+ function showNodeDetails(n) {
326
+ if (!n) return;
327
+ const d = n.data();
328
+ panel.style.display = 'block';
329
+ panelTitle.innerHTML = d.label + ' <span class="tag">'+d.type+'</span>';
330
+ const connected = n.connectedEdges().map(e => {
331
+ const t = e.source().id() === n.id() ? e.target() : e.source();
332
+ return { id: t.id(), label: t.data('label'), type: e.data('type'), ttype: t.data('type') };
333
+ });
334
+ const children = n.outgoers('node');
335
+ const parents = n.incomers('node');
336
+ let html = '';
337
+ html += '<div class="section">Properties</div>';
338
+ if (d.lang) html += row('Language', d.lang);
339
+ html += row('File', d.file || '—');
340
+ if (d.startLine || d.endLine) html += row('Lines', d.startLine + ' – ' + d.endLine);
341
+ if (connected.filter(e => e.type === 'imports').length) {
342
+ html += '<div class="section">Imports ('+connected.filter(e => e.type === 'imports').length+')</div>';
343
+ html += '<div class="tag-list">'+connected.filter(e => e.type === 'imports').slice(0, 10).map(e => '<span>'+e.label+'</span>').join('')+'</div>';
344
+ }
345
+ if (connected.filter(e => e.type === 'imports' && e.ttype === 'file').length > 10) {
346
+ html += '<div class="tag-list"><span class="dim">+'+(connected.filter(e => e.type === 'imports').length - 10)+' more</span></div>';
347
+ }
348
+ if (children.length) {
349
+ html += '<div class="section">Children ('+children.length+')</div>';
350
+ html += '<div class="tag-list">'+children.slice(0, 15).map(c => '<span>'+c.data('label')+'</span>').join('')+'</div>';
351
+ if (children.length > 15) html += '<div class="tag-list"><span class="dim">+'+(children.length-15)+' more</span></div>';
352
+ }
353
+ if (connected.length) {
354
+ html += '<div class="section">Connections ('+connected.length+')</div>';
355
+ const groups = {};
356
+ connected.forEach(c => { groups[c.type] = groups[c.type] || []; groups[c.type].push(c.label); });
357
+ for (const [type, labels] of Object.entries(groups)) {
358
+ html += '<div style="margin-top:4px"><span style="font-size:11px;color:#8b949e">'+type+':</span></div>';
359
+ html += '<div class="tag-list">'+labels.slice(0, 5).map(l => '<span>'+l+'</span>').join('')+'</div>';
360
+ }
361
+ }
362
+ panelBody.innerHTML = html;
363
+ }
364
+
365
+ function row(lbl, val) {
366
+ return '<div class="row"><span class="lbl">'+lbl+'</span><span class="val">'+val+'</span></div>';
367
+ }
368
+
369
+ // Highlight connected on hover
370
+ cy.on('mouseover', 'node', e => {
371
+ const n = e.target;
372
+ const conn = n.connectedEdges();
373
+ const connNodes = conn.connectedNodes();
374
+ cy.elements().addClass('faded');
375
+ n.removeClass('faded');
376
+ conn.removeClass('faded');
377
+ connNodes.removeClass('faded');
378
+ conn.addClass('highlight');
379
+ connNodes.addClass('highlight');
380
+ });
381
+
382
+ cy.on('mouseout', 'node', () => cy.elements().removeClass('faded highlight'));
383
+
384
+ // Stats
385
+ const nc = cy.nodes().length;
386
+ const ec = cy.edges().length;
387
+ document.getElementById('stats').innerHTML = '<div>'+nc+' nodes</div><div>'+ec+' edges</div>';
388
+
389
+ // Keyboard
390
+ document.addEventListener('keydown', e => {
391
+ if (e.key === '/' && !e.target.matches('input,textarea')) { e.preventDefault(); search.focus(); }
392
+ if (e.key === 'Escape') { panel.style.display = 'none'; results.style.display = 'none'; search.blur(); }
393
+ if (e.key === 'f' && !e.target.matches('input,textarea')) cy.fit(undefined, 50);
394
+ });
395
+ } catch(e) { showError('Graph initialization failed', e.message); } }
396
+ </script>
397
+ </body>
398
+ </html>`;
399
+ }
@@ -0,0 +1,84 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { MetadataStore } from '../storage/metadata.js';
4
+ import { GraphStore } from './storage.js';
5
+ import { GraphBuilder } from './builder.js';
6
+ import type { GraphNode, GraphEdge } from './types.js';
7
+ import { logger } from '../logging.js';
8
+
9
+ export class GraphService {
10
+ private rootPath: string;
11
+ private indexDir: string;
12
+ private graph: GraphStore;
13
+ private meta: MetadataStore;
14
+
15
+ constructor(rootPath: string) {
16
+ this.rootPath = rootPath;
17
+ this.indexDir = join(rootPath, '.edhindex');
18
+ if (!existsSync(this.indexDir)) throw new Error('No index found');
19
+ this.meta = new MetadataStore(this.indexDir);
20
+ this.graph = new GraphStore(this.indexDir);
21
+ }
22
+
23
+ build() {
24
+ const builder = new GraphBuilder(this.rootPath, this.meta, this.graph);
25
+ builder.buildFull();
26
+ }
27
+
28
+ updateFile(filePath: string) {
29
+ const builder = new GraphBuilder(this.rootPath, this.meta, this.graph);
30
+ builder.updateFile(filePath);
31
+ }
32
+
33
+ getNode(id: string): GraphNode | null {
34
+ return this.graph.getNode(id);
35
+ }
36
+
37
+ getNeighbors(id: string): { node: GraphNode; edge: GraphEdge }[] {
38
+ return this.graph.getNeighbors(id);
39
+ }
40
+
41
+ getChildren(id: string): GraphNode[] {
42
+ return this.graph.getChildren(id);
43
+ }
44
+
45
+ searchNodes(query: string): GraphNode[] {
46
+ return this.graph.searchNodes(query);
47
+ }
48
+
49
+ getStats(): { nodeCount: number; edgeCount: number; typeCounts: Record<string, number> } {
50
+ const nodes = this.graph.getAllNodes();
51
+ const typeCounts: Record<string, number> = {};
52
+ for (const n of nodes) {
53
+ typeCounts[n.type] = (typeCounts[n.type] || 0) + 1;
54
+ }
55
+ return {
56
+ nodeCount: this.graph.getNodeCount(),
57
+ edgeCount: this.graph.getEdgeCount(),
58
+ typeCounts,
59
+ };
60
+ }
61
+
62
+ getGraphData(limit?: number, offset?: number): { nodes: GraphNode[]; edges: GraphEdge[] } {
63
+ const allNodes = this.graph.getAllNodes();
64
+ const allEdges = this.graph.getAllEdges();
65
+ const slice = (arr: any[]) => {
66
+ if (limit === undefined) return arr;
67
+ return arr.slice(offset || 0, (offset || 0) + limit);
68
+ };
69
+ return { nodes: slice(allNodes), edges: slice(allEdges) };
70
+ }
71
+
72
+ getNodeByPath(file: string, symbol?: string): GraphNode | null {
73
+ const nodes = this.graph.getAllNodes();
74
+ if (symbol) {
75
+ return nodes.find(n => n.file === file && n.label === symbol) || null;
76
+ }
77
+ return nodes.find(n => n.file === file && n.type === 'file') || null;
78
+ }
79
+
80
+ close() {
81
+ this.graph.close();
82
+ this.meta.close();
83
+ }
84
+ }
@@ -0,0 +1,181 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import type { GraphNode, GraphEdge, GraphData } from './types.js';
5
+ import { logger } from '../logging.js';
6
+
7
+ export class GraphStore {
8
+ private db: Database.Database;
9
+ private path: string;
10
+
11
+ constructor(indexDir: string) {
12
+ this.path = join(indexDir, 'graph.sqlite');
13
+ const exists = existsSync(this.path);
14
+ this.db = new Database(this.path);
15
+ if (!exists) this.createSchema();
16
+ }
17
+
18
+ private createSchema() {
19
+ this.db.exec(`
20
+ CREATE TABLE IF NOT EXISTS nodes (
21
+ id TEXT PRIMARY KEY,
22
+ label TEXT NOT NULL,
23
+ type TEXT NOT NULL,
24
+ language TEXT NOT NULL DEFAULT '',
25
+ file TEXT NOT NULL DEFAULT '',
26
+ start_line INTEGER NOT NULL DEFAULT 0,
27
+ end_line INTEGER NOT NULL DEFAULT 0,
28
+ metadata TEXT NOT NULL DEFAULT '{}'
29
+ );
30
+ CREATE TABLE IF NOT EXISTS edges (
31
+ id TEXT PRIMARY KEY,
32
+ source TEXT NOT NULL,
33
+ target TEXT NOT NULL,
34
+ type TEXT NOT NULL,
35
+ metadata TEXT NOT NULL DEFAULT '{}'
36
+ );
37
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source);
38
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target);
39
+ CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
40
+ CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
41
+ CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file);
42
+ CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);
43
+ PRAGMA journal_mode = WAL;
44
+ PRAGMA synchronous = NORMAL;
45
+ `);
46
+ }
47
+
48
+ begin() { this.db.exec('BEGIN TRANSACTION'); }
49
+ commit() { this.db.exec('COMMIT'); }
50
+ rollback() { this.db.exec('ROLLBACK'); }
51
+
52
+ upsertNode(node: GraphNode) {
53
+ this.db.prepare(`
54
+ INSERT OR REPLACE INTO nodes (id, label, type, language, file, start_line, end_line, metadata)
55
+ VALUES (@id, @label, @type, @language, @file, @startLine, @endLine, @metadata)
56
+ `).run({ ...node, metadata: JSON.stringify(node.metadata) });
57
+ }
58
+
59
+ upsertEdge(edge: GraphEdge) {
60
+ this.db.prepare(`
61
+ INSERT OR REPLACE INTO edges (id, source, target, type, metadata)
62
+ VALUES (@id, @source, @target, @type, @metadata)
63
+ `).run({ ...edge, metadata: JSON.stringify(edge.metadata) });
64
+ }
65
+
66
+ deleteNodes(ids: string[]) {
67
+ if (ids.length === 0) return;
68
+ const del = this.db.prepare('DELETE FROM nodes WHERE id = ?');
69
+ for (const id of ids) del.run(id);
70
+ }
71
+
72
+ deleteEdgesForNodes(ids: string[]) {
73
+ if (ids.length === 0) return;
74
+ const del = this.db.prepare('DELETE FROM edges WHERE source = ? OR target = ?');
75
+ for (const id of ids) { del.run(id, id); }
76
+ }
77
+
78
+ deleteEdgesForFile(file: string) {
79
+ this.db.prepare(`
80
+ DELETE FROM edges WHERE id IN (
81
+ SELECT e.id FROM edges e JOIN nodes n ON e.source = n.id OR e.target = n.id
82
+ WHERE n.file = ?
83
+ )
84
+ `).run(file);
85
+ }
86
+
87
+ deleteNodesForFile(file: string) {
88
+ this.db.prepare('DELETE FROM nodes WHERE file = ?').run(file);
89
+ }
90
+
91
+ getNode(id: string): GraphNode | null {
92
+ const row = this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id) as Record<string, unknown> | undefined;
93
+ return row ? this.rowToNode(row) : null;
94
+ }
95
+
96
+ getNodes(ids: string[]): GraphNode[] {
97
+ if (ids.length === 0) return [];
98
+ const placeholders = ids.map(() => '?').join(',');
99
+ return this.db.prepare(`SELECT * FROM nodes WHERE id IN (${placeholders})`).all(...ids).map(r => this.rowToNode(r as Record<string, unknown>));
100
+ }
101
+
102
+ getAllNodes(): GraphNode[] {
103
+ return this.db.prepare('SELECT * FROM nodes').all().map(r => this.rowToNode(r as Record<string, unknown>));
104
+ }
105
+
106
+ getAllEdges(): GraphEdge[] {
107
+ return this.db.prepare('SELECT * FROM edges').all().map(r => this.rowToEdge(r as Record<string, unknown>));
108
+ }
109
+
110
+ getNeighbors(id: string): { node: GraphNode; edge: GraphEdge }[] {
111
+ const rows = this.db.prepare(`
112
+ SELECT n.*, e.id as edge_id, e.source as edge_source, e.target as edge_target,
113
+ e.type as edge_type, e.metadata as edge_metadata
114
+ FROM edges e JOIN nodes n ON (e.source = ? AND n.id = e.target) OR (e.target = ? AND n.id = e.source)
115
+ `).all(id, id) as Record<string, unknown>[];
116
+ return rows.map(r => ({
117
+ node: this.rowToNode(r),
118
+ edge: {
119
+ id: r.edge_id as string,
120
+ source: r.edge_source as string,
121
+ target: r.edge_target as string,
122
+ type: r.edge_type as import('./types.js').EdgeType,
123
+ metadata: JSON.parse(r.edge_metadata as string),
124
+ },
125
+ }));
126
+ }
127
+
128
+ getChildren(id: string): GraphNode[] {
129
+ return this.db.prepare(`
130
+ SELECT n.* FROM nodes n JOIN edges e ON e.target = n.id
131
+ WHERE e.source = ? AND e.type = 'contains'
132
+ `).all(id).map(r => this.rowToNode(r as Record<string, unknown>));
133
+ }
134
+
135
+ searchNodes(query: string): GraphNode[] {
136
+ const q = `%${query}%`;
137
+ return this.db.prepare(`
138
+ SELECT * FROM nodes WHERE label LIKE ? OR file LIKE ? OR id LIKE ?
139
+ LIMIT 50
140
+ `).all(q, q, q).map(r => this.rowToNode(r as Record<string, unknown>));
141
+ }
142
+
143
+ getNodeCount(): number {
144
+ return (this.db.prepare('SELECT COUNT(*) as c FROM nodes').get() as { c: number }).c;
145
+ }
146
+
147
+ getEdgeCount(): number {
148
+ return (this.db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number }).c;
149
+ }
150
+
151
+ clear() {
152
+ this.db.exec('DELETE FROM nodes; DELETE FROM edges');
153
+ }
154
+
155
+ close() {
156
+ this.db.close();
157
+ }
158
+
159
+ private rowToNode(r: Record<string, unknown>): GraphNode {
160
+ return {
161
+ id: r.id as string,
162
+ label: r.label as string,
163
+ type: r.type as import('./types.js').NodeType,
164
+ language: r.language as string,
165
+ file: r.file as string,
166
+ startLine: r.start_line as number,
167
+ endLine: r.end_line as number,
168
+ metadata: JSON.parse(r.metadata as string),
169
+ };
170
+ }
171
+
172
+ private rowToEdge(r: Record<string, unknown>): GraphEdge {
173
+ return {
174
+ id: r.id as string,
175
+ source: r.source as string,
176
+ target: r.target as string,
177
+ type: r.type as import('./types.js').EdgeType,
178
+ metadata: JSON.parse(r.metadata as string),
179
+ };
180
+ }
181
+ }