codesearch-mcp 0.0.17-alpha.1 → 0.1.0-alpha

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 (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
package/dist/src/graph.js CHANGED
@@ -33,10 +33,80 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildGraphData = buildGraphData;
37
+ exports.buildGraphViews = buildGraphViews;
38
+ exports.buildHtml = buildHtml;
36
39
  exports.generateProjectGraph = generateProjectGraph;
37
40
  const fs = __importStar(require("fs"));
38
41
  const path = __importStar(require("path"));
39
42
  const file_utils_1 = require("./file-utils");
43
+ const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
44
+ /**
45
+ * Build the project graph data (nodes + relation edges) from a project list.
46
+ * Pure apart from reading each project's `.ragmeta.json`. Shared by the console
47
+ * graph, the graph views, and the web dashboard's `/api/graph`.
48
+ */
49
+ function buildGraphData(projects) {
50
+ const nodes = projects.map(p => {
51
+ const meta = (0, file_utils_1.readRagMeta)(p.path);
52
+ return { name: p.name, ...(typeof meta === 'object' ? meta : EMPTY_META) };
53
+ });
54
+ return { nodes, edges: buildEdges(nodes) };
55
+ }
56
+ /**
57
+ * Split a project list into graph tabs. A project whose name contains a `/`
58
+ * belongs to a nested Nx repo (`<repo>/<sub-project>`); everything else is a
59
+ * flat top-level project.
60
+ *
61
+ * - **Workspace** tab: flat projects + one node per Nx repo (collapsed to a
62
+ * single `nxRepo` node); edges from `.ragmeta.json` relations.
63
+ * - **One tab per Nx repo**: its sub-projects as nodes, with edges between them
64
+ * taken from real import dependencies (`projectEdges`, from `code_dependency`).
65
+ *
66
+ * Returns the Workspace view first, then repo views sorted by name.
67
+ */
68
+ function buildGraphViews(projects, projectEdges, workspaceRoot) {
69
+ const flat = [];
70
+ const groups = new Map();
71
+ for (const p of projects) {
72
+ const slash = p.name.indexOf('/');
73
+ if (slash > 0) {
74
+ const repo = p.name.slice(0, slash);
75
+ let list = groups.get(repo);
76
+ if (!list) {
77
+ list = [];
78
+ groups.set(repo, list);
79
+ }
80
+ list.push(p);
81
+ }
82
+ else {
83
+ flat.push(p);
84
+ }
85
+ }
86
+ const repoNames = [...groups.keys()].sort();
87
+ // Workspace: flat projects + one collapsed node per repo (reads repo-root ragmeta).
88
+ const workspaceProjects = [
89
+ ...flat,
90
+ ...repoNames.map(repo => ({ name: repo, path: path.join(workspaceRoot, repo) })),
91
+ ];
92
+ const ws = buildGraphData(workspaceProjects);
93
+ const repoSet = new Set(repoNames);
94
+ for (const n of ws.nodes)
95
+ if (repoSet.has(n.name))
96
+ n.nxRepo = true;
97
+ const workspace = { name: 'Workspace', nodes: ws.nodes, edges: ws.edges };
98
+ // Per-repo: sub-project nodes + intra-repo import edges.
99
+ const repoViews = repoNames.map(repo => {
100
+ const subs = groups.get(repo);
101
+ const { nodes } = buildGraphData(subs);
102
+ const names = new Set(subs.map(s => s.name));
103
+ const edges = projectEdges
104
+ .filter(e => names.has(e.from) && names.has(e.to))
105
+ .map(e => ({ from: e.from, to: e.to, label: 'imports' }));
106
+ return { name: repo, nodes, edges };
107
+ });
108
+ return [workspace, ...repoViews];
109
+ }
40
110
  function buildEdges(nodes) {
41
111
  const edges = [];
42
112
  for (const node of nodes) {
@@ -50,49 +120,68 @@ function buildEdges(nodes) {
50
120
  }
51
121
  return edges;
52
122
  }
53
- function escHtml(s) {
54
- return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
55
- }
56
- function printConsole(nodes, edges, workspacePath) {
57
- if (edges.length === 0)
58
- return;
59
- console.log('\n=== Project Relations ===');
60
- const grouped = new Map();
61
- for (const e of edges) {
62
- if (!grouped.has(e.from))
63
- grouped.set(e.from, []);
64
- grouped.get(e.from).push(e);
65
- }
66
- for (const [from, fromEdges] of grouped) {
67
- const byLabel = new Map();
68
- for (const e of fromEdges) {
69
- if (!byLabel.has(e.label))
70
- byLabel.set(e.label, []);
71
- byLabel.get(e.label).push(e);
123
+ function printConsole(views, workspacePath) {
124
+ const workspace = views[0];
125
+ const repoViews = views.slice(1);
126
+ if (workspace.edges.length > 0) {
127
+ console.log('\n=== Project Relations ===');
128
+ const grouped = new Map();
129
+ for (const e of workspace.edges) {
130
+ if (!grouped.has(e.from))
131
+ grouped.set(e.from, []);
132
+ grouped.get(e.from).push(e);
72
133
  }
73
- for (const [label, labelEdges] of byLabel) {
74
- const targets = labelEdges.map(e => e.to).join(', ');
75
- console.log(` ${from.padEnd(30)} → ${targets.padEnd(30)} [${label}]`);
134
+ for (const [from, fromEdges] of grouped) {
135
+ const byLabel = new Map();
136
+ for (const e of fromEdges) {
137
+ if (!byLabel.has(e.label))
138
+ byLabel.set(e.label, []);
139
+ byLabel.get(e.label).push(e);
140
+ }
141
+ for (const [label, labelEdges] of byLabel) {
142
+ const targets = labelEdges.map(e => e.to).join(', ');
143
+ console.log(` ${from.padEnd(30)} → ${targets.padEnd(30)} [${label}]`);
144
+ }
76
145
  }
77
146
  }
78
- console.log(`\n Full graph: file://${path.join(workspacePath, 'project-graph.html')}`);
147
+ for (const v of repoViews) {
148
+ console.log(` Nx repo ${v.name.padEnd(24)} ${v.nodes.length} projects · ${v.edges.length} internal edges`);
149
+ }
150
+ if (workspace.edges.length > 0 || repoViews.length > 0) {
151
+ console.log(`\n Full graph: file://${path.join(workspacePath, 'project-graph.html')}`);
152
+ }
79
153
  }
80
- function buildHtml(nodes, edges) {
81
- const visNodes = nodes.map(n => ({
154
+ /** Prepare a view for vis-network: colored nodes (+ synthetic nodes for unknown edge targets) and indexed edges. */
155
+ function viewToVis(view) {
156
+ const nodes = view.nodes.map(n => ({
82
157
  id: n.name,
83
158
  label: n.name,
84
159
  title: [n.description, n.stack].filter(Boolean).join('\n') || n.name,
85
- color: n.relations ? '#4A90D9' : '#A8C7E8',
160
+ color: n.nxRepo ? '#B57EDC' : (n.relations ? '#4A90D9' : '#A8C7E8'),
161
+ ...(n.nxRepo ? { borderWidth: 3 } : {}),
86
162
  }));
87
- const known = new Set(nodes.map(n => n.name));
88
- for (const e of edges) {
89
- if (!known.has(e.to)) {
90
- visNodes.push({ id: e.to, label: e.to, title: e.to, color: '#E8A838' });
91
- known.add(e.to);
163
+ const known = new Set(view.nodes.map(n => n.name));
164
+ for (const e of view.edges) {
165
+ for (const end of [e.from, e.to]) {
166
+ if (!known.has(end)) {
167
+ nodes.push({ id: end, label: end, title: end, color: '#E8A838' });
168
+ known.add(end);
169
+ }
92
170
  }
93
171
  }
94
- const visEdges = edges.map((e, i) => ({ id: i, from: e.from, to: e.to, label: e.label, arrows: 'to' }));
95
- const allLabels = [...new Set(edges.map(e => e.label))].sort();
172
+ const edges = view.edges.map((e, i) => ({ id: i, from: e.from, to: e.to, label: e.label, arrows: 'to' }));
173
+ const labels = [...new Set(view.edges.map(e => e.label))].sort();
174
+ return { name: view.name, nodes, edges, labels };
175
+ }
176
+ /**
177
+ * Render the full interactive graph page (vis-network + inner tabs, edge-type
178
+ * filters, legend, project toggle list). Used for the standalone
179
+ * `project-graph.html` and served by the web dashboard at `GET /graph` (embedded
180
+ * via iframe) so both share one implementation. Tab 0 is the Workspace view;
181
+ * double-clicking an Nx-repo node opens that repo's tab.
182
+ */
183
+ function buildHtml(views) {
184
+ const visViews = views.map(viewToVis);
96
185
  return `<!DOCTYPE html>
97
186
  <html lang="en">
98
187
  <head>
@@ -104,7 +193,10 @@ function buildHtml(nodes, edges) {
104
193
  body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #eee; display: flex; flex-direction: column; height: 100vh; }
105
194
  #header { padding: 14px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 20px; flex-shrink: 0; }
106
195
  #header h1 { font-size: 16px; font-weight: 600; white-space: nowrap; }
107
- #header p { font-size: 12px; color: #888; }
196
+ #count { font-size: 12px; color: #888; }
197
+ #view-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
198
+ .view-tab { padding: 5px 12px; border-radius: 8px; border: 1px solid #2a3f6a; background: transparent; color: #88aabb; font-size: 12px; cursor: pointer; }
199
+ .view-tab.active { background: #4A90D9; border-color: #4A90D9; color: #fff; }
108
200
  #filters { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
109
201
  .filter-btn {
110
202
  padding: 4px 10px; border-radius: 12px; border: 1px solid #4A90D9;
@@ -133,77 +225,50 @@ function buildHtml(nodes, edges) {
133
225
  <div id="header">
134
226
  <div>
135
227
  <h1>Project Graph</h1>
136
- <p>${nodes.length} projects · ${edges.length} edges · ${new Date().toLocaleString()}</p>
137
- </div>
138
- <div id="filters">
139
- ${allLabels.map(l => `<button class="filter-btn" data-label="${escHtml(l)}">${escHtml(l)}</button>`).join('\n ')}
228
+ <p id="count"></p>
140
229
  </div>
230
+ <div id="view-tabs"></div>
231
+ <div id="filters"></div>
141
232
  </div>
142
233
  <div id="main">
143
234
  <div id="network"></div>
144
235
  <div id="legend">
145
236
  <h3>Nodes</h3>
237
+ <div class="entry"><span class="dot" style="background:#B57EDC"></span>Nx repo (double-click to open)</div>
146
238
  <div class="entry"><span class="dot" style="background:#4A90D9"></span>has .ragmeta.json</div>
147
- <div class="entry"><span class="dot" style="background:#5a5a7a"></span>no .ragmeta.json</div>
239
+ <div class="entry"><span class="dot" style="background:#A8C7E8"></span>no .ragmeta.json</div>
148
240
  <div class="entry"><span class="dot" style="background:#E8A838"></span>not indexed</div>
149
- <div id="hint">Drag to rearrange.<br>Edge buttons — toggle type.<br>Projects — toggle node.</div>
241
+ <div id="hint">Drag to rearrange.<br>Edge buttons — toggle type.<br>Projects — toggle node.<br>Double-click an Nx repo to open it.</div>
150
242
  <div id="project-list">
151
243
  <h3>Projects</h3>
152
- ${visNodes.map(n => `<button class="proj-btn" data-node="${escHtml(n.id)}"><span class="proj-dot" style="background:${escHtml(n.color)}"></span>${escHtml(n.label)}</button>`).join('\n ')}
244
+ <div id="proj-list-body"></div>
153
245
  </div>
154
246
  </div>
155
247
  </div>
156
248
  <script>
157
- const allEdges = ${JSON.stringify(visEdges)};
158
- const nodesDs = new vis.DataSet(${JSON.stringify(visNodes)});
159
- const edgesDs = new vis.DataSet(allEdges);
249
+ const VIEWS = ${JSON.stringify(visViews)};
160
250
 
161
- const network = new vis.Network(
162
- document.getElementById('network'),
163
- { nodes: nodesDs, edges: edgesDs },
164
- {
165
- nodes: {
166
- shape: 'box',
167
- font: { color: '#fff', size: 13 },
168
- borderWidth: 2,
169
- shadow: true,
170
- margin: { top: 8, bottom: 8, left: 12, right: 12 },
171
- },
172
- edges: {
173
- font: { color: '#aaa', size: 11, align: 'middle', strokeWidth: 0, background: '#1a1a2e' },
174
- color: { color: '#4A90D9', highlight: '#7eb8f7', hover: '#7eb8f7' },
175
- smooth: { type: 'curvedCW', roundness: 0.2 },
176
- arrows: { to: { scaleFactor: 0.6 } },
177
- selectionWidth: 2,
178
- },
179
- physics: {
180
- solver: 'barnesHut',
181
- barnesHut: {
182
- gravitationalConstant: -18000,
183
- springConstant: 0.025,
184
- springLength: 280,
185
- damping: 0.25,
186
- },
187
- stabilization: { iterations: 400, updateInterval: 25 },
188
- },
189
- interaction: { hover: true, tooltipDelay: 80, dragNodes: true },
190
- }
191
- );
192
-
193
- network.on('stabilizationIterationsDone', () => {
194
- network.fit({ animation: { duration: 600, easingFunction: 'easeInOutQuad' } });
195
- setTimeout(() => network.setOptions({ physics: { enabled: false } }), 650);
196
- });
197
-
198
- network.on('hoverEdge', ({ edge }) => {
199
- edgesDs.update({ id: edge, font: { color: '#fff', size: 16, background: '#16213e', strokeWidth: 0 } });
200
- });
201
- network.on('blurEdge', ({ edge }) => {
202
- edgesDs.update({ id: edge, font: { color: '#aaa', size: 11, background: '#1a1a2e', strokeWidth: 0 } });
203
- });
251
+ const NET_OPTIONS = {
252
+ nodes: { shape: 'box', font: { color: '#fff', size: 13 }, borderWidth: 2, shadow: true, margin: { top: 8, bottom: 8, left: 12, right: 12 } },
253
+ edges: {
254
+ font: { color: '#aaa', size: 11, align: 'middle', strokeWidth: 0, background: '#1a1a2e' },
255
+ color: { color: '#4A90D9', highlight: '#7eb8f7', hover: '#7eb8f7' },
256
+ smooth: { type: 'curvedCW', roundness: 0.2 },
257
+ arrows: { to: { scaleFactor: 0.6 } },
258
+ selectionWidth: 2,
259
+ },
260
+ physics: {
261
+ solver: 'barnesHut',
262
+ barnesHut: { gravitationalConstant: -18000, springConstant: 0.025, springLength: 280, damping: 0.25 },
263
+ stabilization: { iterations: 400, updateInterval: 25 },
264
+ },
265
+ interaction: { hover: true, tooltipDelay: 80, dragNodes: true },
266
+ };
204
267
 
205
- const activeLabels = new Set(${JSON.stringify(allLabels)});
206
- const hiddenNodes = new Set();
268
+ let active = -1;
269
+ let network = null;
270
+ let nodesDs = null, edgesDs = null, allEdges = [];
271
+ let activeLabels = new Set(), hiddenNodes = new Set();
207
272
 
208
273
  function refreshEdges() {
209
274
  const visible = allEdges.filter(e =>
@@ -213,46 +278,100 @@ function buildHtml(nodes, edges) {
213
278
  edgesDs.add(visible);
214
279
  }
215
280
 
216
- document.querySelectorAll('.filter-btn').forEach(btn => {
217
- btn.addEventListener('click', () => {
218
- const label = btn.dataset.label;
219
- if (activeLabels.has(label)) { activeLabels.delete(label); btn.classList.add('off'); }
220
- else { activeLabels.add(label); btn.classList.remove('off'); }
221
- refreshEdges();
281
+ function renderTabs() {
282
+ const bar = document.getElementById('view-tabs');
283
+ bar.innerHTML = '';
284
+ if (VIEWS.length < 2) return; // only Workspace — no tab bar needed
285
+ VIEWS.forEach((v, i) => {
286
+ const b = document.createElement('button');
287
+ b.className = 'view-tab' + (i === active ? ' active' : '');
288
+ b.textContent = v.name;
289
+ b.addEventListener('click', () => selectView(i));
290
+ bar.appendChild(b);
222
291
  });
223
- });
292
+ }
224
293
 
225
- document.querySelectorAll('.proj-btn').forEach(btn => {
226
- btn.addEventListener('click', () => {
227
- const id = btn.dataset.node;
228
- if (hiddenNodes.has(id)) {
229
- hiddenNodes.delete(id);
230
- nodesDs.update({ id, hidden: false });
231
- btn.classList.remove('off');
232
- } else {
233
- hiddenNodes.add(id);
234
- nodesDs.update({ id, hidden: true });
235
- btn.classList.add('off');
236
- }
237
- refreshEdges();
294
+ function renderFilters() {
295
+ const f = document.getElementById('filters');
296
+ f.innerHTML = '';
297
+ VIEWS[active].labels.forEach(label => {
298
+ const b = document.createElement('button');
299
+ b.className = 'filter-btn';
300
+ b.textContent = label;
301
+ b.addEventListener('click', () => {
302
+ if (activeLabels.has(label)) { activeLabels.delete(label); b.classList.add('off'); }
303
+ else { activeLabels.add(label); b.classList.remove('off'); }
304
+ refreshEdges();
305
+ });
306
+ f.appendChild(b);
238
307
  });
239
- });
308
+ }
309
+
310
+ function renderProjectList() {
311
+ const body = document.getElementById('proj-list-body');
312
+ body.innerHTML = '';
313
+ VIEWS[active].nodes.forEach(n => {
314
+ const b = document.createElement('button');
315
+ b.className = 'proj-btn';
316
+ const dot = document.createElement('span');
317
+ dot.className = 'proj-dot';
318
+ dot.style.background = n.color;
319
+ b.appendChild(dot);
320
+ b.appendChild(document.createTextNode(n.label));
321
+ b.addEventListener('click', () => {
322
+ if (hiddenNodes.has(n.id)) { hiddenNodes.delete(n.id); nodesDs.update({ id: n.id, hidden: false }); b.classList.remove('off'); }
323
+ else { hiddenNodes.add(n.id); nodesDs.update({ id: n.id, hidden: true }); b.classList.add('off'); }
324
+ refreshEdges();
325
+ });
326
+ body.appendChild(b);
327
+ });
328
+ }
329
+
330
+ function selectView(i) {
331
+ active = i;
332
+ const v = VIEWS[i];
333
+ activeLabels = new Set(v.labels);
334
+ hiddenNodes = new Set();
335
+ allEdges = v.edges;
336
+ nodesDs = new vis.DataSet(v.nodes);
337
+ edgesDs = new vis.DataSet(v.edges);
338
+
339
+ if (!network) {
340
+ network = new vis.Network(document.getElementById('network'), { nodes: nodesDs, edges: edgesDs }, NET_OPTIONS);
341
+ network.on('stabilizationIterationsDone', () => {
342
+ network.fit({ animation: { duration: 600, easingFunction: 'easeInOutQuad' } });
343
+ setTimeout(() => network.setOptions({ physics: { enabled: false } }), 650);
344
+ });
345
+ network.on('hoverEdge', ({ edge }) => edgesDs.update({ id: edge, font: { color: '#fff', size: 16, background: '#16213e', strokeWidth: 0 } }));
346
+ network.on('blurEdge', ({ edge }) => edgesDs.update({ id: edge, font: { color: '#aaa', size: 11, background: '#1a1a2e', strokeWidth: 0 } }));
347
+ network.on('doubleClick', params => {
348
+ if (!params.nodes.length) return;
349
+ const idx = VIEWS.findIndex(vv => vv.name === params.nodes[0]);
350
+ if (idx >= 0 && idx !== active) selectView(idx);
351
+ });
352
+ } else {
353
+ network.setOptions({ physics: { enabled: true } });
354
+ network.setData({ nodes: nodesDs, edges: edgesDs });
355
+ }
356
+
357
+ document.getElementById('count').textContent = v.nodes.length + ' nodes · ' + v.edges.length + ' edges';
358
+ renderTabs();
359
+ renderFilters();
360
+ renderProjectList();
361
+ }
362
+
363
+ selectView(0);
240
364
  </script>
241
365
  </body>
242
366
  </html>`;
243
367
  }
244
- // Single entry point — reads meta once, prints console summary and writes HTML
245
- function generateProjectGraph(projects, workspacePath) {
246
- const EMPTY = { description: null, stack: null, notes: null, relations: null };
247
- const nodes = projects.map(p => {
248
- const meta = (0, file_utils_1.readRagMeta)(p.path);
249
- return { name: p.name, ...(typeof meta === 'object' ? meta : EMPTY) };
250
- });
251
- const edges = buildEdges(nodes);
252
- printConsole(nodes, edges, workspacePath);
368
+ // Single entry point — builds the tabbed views, prints a console summary, writes the HTML.
369
+ function generateProjectGraph(projects, projectEdges, workspacePath) {
370
+ const views = buildGraphViews(projects, projectEdges, workspacePath);
371
+ printConsole(views, workspacePath);
253
372
  const htmlPath = path.join(workspacePath, 'project-graph.html');
254
373
  try {
255
- fs.writeFileSync(htmlPath, buildHtml(nodes, edges), 'utf-8');
374
+ fs.writeFileSync(htmlPath, buildHtml(views), 'utf-8');
256
375
  }
257
376
  catch (err) {
258
377
  console.error(` ⚠ Could not write project-graph.html: ${err.message}`);
package/dist/src/help.js CHANGED
@@ -46,22 +46,37 @@ function printHelp(command) {
46
46
  console.log(' get <key> Print a single value');
47
47
  console.log(' set <key> <value> Set a value');
48
48
  console.log(' reset <key> Remove key (falls back to built-in default)');
49
+ console.log(' edit Open config in $EDITOR (with schema validation)');
49
50
  console.log('');
50
51
  console.log('Keys:');
51
52
  console.log(' db.host, db.port, db.user, db.password, db.name');
52
- console.log(' ollama.host, ollama.model');
53
+ console.log(' embedding.provider, embedding.baseURL, embedding.apiKey, embedding.model,');
54
+ console.log(' embedding.dimensions, embedding.batchSize');
53
55
  console.log(' indexing.chunkSize, indexing.chunkOverlap, indexing.embedBatchSize');
54
56
  console.log(' search.similarityThreshold, search.defaultLimit, search.maxLimit');
55
- console.log(' search.autoTranslate, search.translateModel');
56
- console.log(' reranker.enabled, reranker.model, reranker.url, reranker.overfetch');
57
+ console.log(' reranker.enabled, reranker.provider, reranker.baseURL, reranker.apiKey,');
58
+ console.log(' reranker.model, reranker.url, reranker.overfetch');
57
59
  console.log(' logging.level, logging.dir');
58
- console.log(' workspaceRoot');
60
+ console.log(' workspaceRoot (API keys: use env:VAR_NAME → ~/.config/codesearch-mcp/.env)');
61
+ break;
62
+ case 'rag-web':
63
+ case 'web':
64
+ console.log('Usage: rag-web [--port N]');
65
+ console.log('');
66
+ console.log('Start a local web dashboard (config editor, project overview,');
67
+ console.log('dependency graph) on http://127.0.0.1:<port>.');
68
+ console.log('Requires a valid config.json — repair a broken one with rag-config edit.');
69
+ console.log('');
70
+ console.log('Options:');
71
+ console.log(' --port N Listen port (default 4319)');
72
+ console.log(' -h, --help Show this help message');
59
73
  break;
60
74
  default:
61
75
  console.log('Usage: rag-index [--reindex] [--select]');
62
76
  console.log(' rag-mcp');
63
77
  console.log(' rag-setup');
64
- console.log(' rag-config <show|get|set|reset>');
78
+ console.log(' rag-config <show|get|set|reset|edit>');
79
+ console.log(' rag-web [--port N]');
65
80
  console.log('');
66
81
  console.log('Run any command with -h or --help for details.');
67
82
  }
@@ -26,18 +26,35 @@ export interface CallEdge {
26
26
  line: number | null;
27
27
  col: number | null;
28
28
  }
29
+ /**
30
+ * Line range of a named scope used only to compute a chunk's owning declaration
31
+ * (`ownerPath`). Method/constructor/accessor ranges carry their enclosing class
32
+ * as `container`; top-level declarations use `container: null`. These are NOT
33
+ * persisted to `code_symbol` — they exist only for chunk ownership.
34
+ */
35
+ export interface OwnerRange {
36
+ name: string;
37
+ container: string | null;
38
+ line: number;
39
+ endLine: number;
40
+ }
29
41
  export interface FileAnalysis {
30
42
  edges: DepEdge[];
31
43
  symbols: SymbolEntry[];
32
44
  calls: CallEdge[];
45
+ /** Method/accessor/constructor ranges for owner-path resolution (Phase 2). */
46
+ ownerRanges?: OwnerRange[];
47
+ }
48
+ export interface AnalyzeProjectOpts {
49
+ skipDependencyResolution?: boolean;
50
+ /** Path to root tsconfig (e.g. tsconfig.base.json) for workspace-wide path aliases */
51
+ rootTsConfigPath?: string;
33
52
  }
34
53
  export interface IAstParser {
35
54
  /**
36
55
  * @deprecated Use analyzeProject instead for project-wide context.
37
56
  */
38
57
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
39
- analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
40
- skipDependencyResolution?: boolean;
41
- }): Promise<Map<string, FileAnalysis>>;
58
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: AnalyzeProjectOpts): Promise<Map<string, FileAnalysis>>;
42
59
  clearCache(projectRoot?: string): void;
43
60
  }
@@ -0,0 +1,24 @@
1
+ import type { SymbolEntry } from './IAstParser';
2
+ /** One embeddable unit of source with its 1-based inclusive line range. */
3
+ export interface ChunkPiece {
4
+ content: string;
5
+ startLine: number;
6
+ endLine: number;
7
+ }
8
+ export interface ChunkOpts {
9
+ /** 'ast' = symbol-aligned boundaries; 'character' = fixed-size recursive splitter. */
10
+ strategy: 'ast' | 'character';
11
+ /** Schema files (.prisma/.graphql/.gql) always use the schema separator splitter. */
12
+ isSchemaFile: boolean;
13
+ /** Top-level symbol ranges for the file; may be empty (no analysis available). */
14
+ symbols: SymbolEntry[];
15
+ }
16
+ export interface IChunker {
17
+ /**
18
+ * Split source into embeddable pieces. AST strategy aligns boundaries to
19
+ * top-level declaration ranges and never emits a piece longer than the
20
+ * configured chunk size; falls back to the character splitter for schema
21
+ * files, empty-symbol files, or when strategy is 'character'.
22
+ */
23
+ chunk(code: string, opts: ChunkOpts): Promise<ChunkPiece[]>;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,27 @@
1
+ import type { DrizzleDb, DrizzleTx } from '../services/DatabaseService';
2
+ /** Single-row record of which embedding model built the current index. */
3
+ export interface IndexMeta {
4
+ provider: string;
5
+ model: string;
6
+ dimensions: number;
7
+ }
8
+ /**
9
+ * Database abstraction — services depend on this interface
10
+ * rather than the concrete DatabaseService.
11
+ */
12
+ export interface IDatabase {
13
+ readonly orm: DrizzleDb;
14
+ query<T = any>(sql: string, params?: any[]): Promise<T>;
15
+ transaction<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T>;
16
+ initDb(): Promise<void>;
17
+ initDbWithSchema(): Promise<void>;
18
+ destroy(): Promise<void>;
19
+ /** Read the single `index_meta` row, or null when absent (table missing → null). */
20
+ readIndexMeta(): Promise<IndexMeta | null>;
21
+ /** Upsert the single `index_meta` row. */
22
+ writeIndexMeta(meta: IndexMeta): Promise<void>;
23
+ /** Create `code_chunk` (+ HNSW/service indexes) at `dimensions` if absent. */
24
+ ensureChunkTable(dimensions: number): Promise<void>;
25
+ /** Drop and recreate `code_chunk` at `dimensions` (dimension change). */
26
+ recreateChunkTable(dimensions: number): Promise<void>;
27
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,18 @@
1
+ export type InputType = 'query' | 'document';
1
2
  export interface IEmbedder {
2
- embed(text: string): Promise<number[]>;
3
- translate?(text: string, model: string): Promise<string>;
3
+ /**
4
+ * The vector width this embedder produces, when known without a probe:
5
+ * `embedding.dimensions` override → model registry, else `0` for models whose
6
+ * width is only knowable by embedding once (see `resolveDimension`). Read by
7
+ * `SearcherService.assertIndexMatches` to catch a dimension change on the
8
+ * search side (skipped when `0`). Indexing resolves the true width via
9
+ * `resolveDimension` (which probes when needed), not this field.
10
+ */
11
+ readonly dimensions: number;
12
+ embed(text: string, inputType?: InputType): Promise<number[]>;
13
+ /** One vector per input, in input order. The indexing path. */
14
+ embedBatch(texts: string[], inputType?: InputType): Promise<number[][]>;
4
15
  rerank?(query: string, documents: string[], topN: number): Promise<number[]>;
16
+ listModels?(): Promise<string[]>;
17
+ ensureModelAvailable?(): Promise<void>;
5
18
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Logger abstraction — allows services to depend on a contract
3
+ * rather than the concrete pino-based LoggerService.
4
+ */
5
+ export interface ILogger {
6
+ debug(msg: string, data?: Record<string, unknown>): void;
7
+ info(msg: string, data?: Record<string, unknown>): void;
8
+ warn(msg: string, data?: Record<string, unknown>): void;
9
+ error(msg: string, data?: Record<string, unknown>): void;
10
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MCP Tool Handler interface — each tool implements this contract.
3
+ * Follows the Open/Closed Principle: new tools are added by implementing
4
+ * this interface and registering them, not by modifying McpServer.
5
+ */
6
+ export interface ToolDefinition {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: Record<string, unknown>;
10
+ }
11
+ export interface ToolResponse {
12
+ [key: string]: unknown;
13
+ isError?: boolean;
14
+ content: Array<{
15
+ type: 'text';
16
+ text: string;
17
+ }>;
18
+ }
19
+ export interface IMcpToolHandler {
20
+ definition(): ToolDefinition;
21
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
22
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });