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
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardHtml = renderDashboardHtml;
4
+ /**
5
+ * Renders the RAG dashboard as a single self-contained HTML page (inline CSS +
6
+ * JS), mirroring the approach in graph.ts. The page is schema-driven: it fetches
7
+ * /api/schema (from buildJsonSchema — the same source as Step 1's editor support)
8
+ * and /api/config, then renders a typed form. The Graph tab embeds the full
9
+ * standalone graph page (GET /graph — same buildHtml as project-graph.html) in
10
+ * an iframe rather than re-implementing the network here.
11
+ */
12
+ function renderDashboardHtml() {
13
+ return `<!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1">
18
+ <title>RAG Dashboard</title>
19
+ <style>
20
+ * { margin: 0; padding: 0; box-sizing: border-box; }
21
+ body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
22
+ #header { padding: 12px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 24px; flex-shrink: 0; }
23
+ #header h1 { font-size: 16px; font-weight: 600; }
24
+ #tabs { display: flex; gap: 6px; }
25
+ .tab { padding: 6px 14px; border-radius: 8px; border: 1px solid transparent; background: transparent; color: #88aabb; font-size: 13px; cursor: pointer; }
26
+ .tab.active { background: #4A90D9; color: #fff; }
27
+ #main { flex: 1; overflow: auto; padding: 24px; }
28
+ .panel { display: none; max-width: 900px; margin: 0 auto; }
29
+ .panel.active { display: block; }
30
+ /* Graph panel uses the full available width (not the 900px form column). */
31
+ #graph-panel { max-width: none; }
32
+
33
+ /* Config form */
34
+ .section { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 16px 18px; margin-bottom: 16px; }
35
+ .section h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #7aa; margin-bottom: 14px; }
36
+ .field { display: grid; grid-template-columns: 220px 1fr; align-items: center; gap: 14px; margin-bottom: 12px; }
37
+ .field:last-child { margin-bottom: 0; }
38
+ .field label { font-size: 13px; color: #cde; }
39
+ .field .desc { display: block; font-size: 11px; color: #667; margin-top: 2px; }
40
+ .field .defval { display: block; font-size: 11px; color: #667; margin-top: 2px; }
41
+ .field .defval.using { color: #c9a24a; } /* amber — this field is currently using the default */
42
+ .field input[type=text], .field input[type=password], .field input[type=number], .field select {
43
+ width: 100%; padding: 7px 9px; border-radius: 6px; border: 1px solid #2a3f6a;
44
+ background: #0f1a33; color: #eee; font-size: 13px; font-family: inherit;
45
+ }
46
+ .field input:focus, .field select:focus { outline: none; border-color: #4A90D9; }
47
+ .field.invalid input, .field.invalid select { border-color: #e0555a; }
48
+ .field input[type=checkbox] { width: 16px; height: 16px; accent-color: #4A90D9; }
49
+ #actions { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
50
+ button.primary { padding: 9px 20px; border-radius: 8px; border: none; background: #4A90D9; color: #fff; font-size: 13px; cursor: pointer; }
51
+ button.primary:hover { background: #5a9fe0; }
52
+ button.primary:disabled { opacity: .5; cursor: default; }
53
+ #banner { display: none; padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; white-space: pre-wrap; }
54
+ #banner.err { display: block; background: #3a1a1f; border: 1px solid #e0555a; color: #f0b3b6; }
55
+ #banner.ok { display: block; background: #16332a; border: 1px solid #3fae7a; color: #a8e6c4; }
56
+
57
+ /* Projects */
58
+ #cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
59
+ .card { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 14px 16px; }
60
+ .card h3 { font-size: 14px; margin-bottom: 4px; }
61
+ .card .chunks { font-size: 11px; color: #4A90D9; margin-bottom: 8px; }
62
+ .card .stack { font-size: 12px; color: #8ab; margin-bottom: 6px; }
63
+ .card .descr { font-size: 12px; color: #99a; line-height: 1.4; }
64
+ .card .rel { font-size: 11px; color: #778; margin-top: 8px; }
65
+ .empty { color: #667; font-size: 13px; padding: 20px 0; }
66
+
67
+ /* Graph — embeds the full standalone graph page (/graph) in an iframe, so
68
+ the filters/legend/project-list sidebar comes for free. A definite height
69
+ is required; a flex/percentage height would collapse the iframe. */
70
+ #graph-frame { width: 100%; height: calc(100vh - 130px); border: 1px solid #0f3460; border-radius: 10px; background: #131a30; display: block; }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <div id="header">
75
+ <h1>RAG Dashboard</h1>
76
+ <div id="tabs">
77
+ <button class="tab active" data-tab="config">Config</button>
78
+ <button class="tab" data-tab="projects">Projects</button>
79
+ <button class="tab" data-tab="graph">Graph</button>
80
+ </div>
81
+ </div>
82
+ <div id="main">
83
+ <div class="panel active" id="config-panel">
84
+ <div id="banner"></div>
85
+ <form id="config-form"></form>
86
+ <div id="actions">
87
+ <button class="primary" id="save-btn" type="button">Save</button>
88
+ <span id="save-hint" style="font-size:12px;color:#667"></span>
89
+ </div>
90
+ </div>
91
+ <div class="panel" id="projects-panel">
92
+ <div id="cards"></div>
93
+ </div>
94
+ <div class="panel" id="graph-panel">
95
+ <iframe id="graph-frame" title="Project dependency graph"></iframe>
96
+ </div>
97
+ </div>
98
+
99
+ <script>
100
+ const MASK = '********';
101
+ let schema = null;
102
+ let config = null;
103
+ let defaults = {};
104
+ let fieldOptions = {};
105
+ let graphLoaded = false;
106
+
107
+ // ---- tabs ----
108
+ document.querySelectorAll('.tab').forEach(btn => {
109
+ btn.addEventListener('click', () => {
110
+ document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
111
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
112
+ btn.classList.add('active');
113
+ const tab = btn.dataset.tab;
114
+ document.getElementById(tab + '-panel').classList.add('active');
115
+ if (tab === 'projects') loadProjects();
116
+ if (tab === 'graph') loadGraph();
117
+ });
118
+ });
119
+
120
+ // ---- config form ----
121
+ function nested(obj, path) {
122
+ return path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
123
+ }
124
+
125
+ function fmtDefault(v) {
126
+ if (v === '') return "'' (empty)";
127
+ return String(v);
128
+ }
129
+
130
+ // A dropdown limited to the given values. Preserves an unknown current value
131
+ // as an extra option so saving never silently drops it.
132
+ function selectEl(key, values, value, def) {
133
+ const sel = document.createElement('select');
134
+ sel.dataset.key = key; sel.dataset.type = 'string';
135
+ const blank = document.createElement('option');
136
+ blank.value = ''; blank.textContent = def !== undefined ? '(default: ' + def + ')' : '(default)';
137
+ sel.appendChild(blank);
138
+ const all = values.slice();
139
+ if (value && !all.includes(value)) all.unshift(value);
140
+ all.forEach(v => {
141
+ const o = document.createElement('option'); o.value = v; o.textContent = v;
142
+ if (value === v) o.selected = true;
143
+ sel.appendChild(o);
144
+ });
145
+ return sel;
146
+ }
147
+
148
+ // Free-text input with a datalist of suggestions (combobox).
149
+ function comboEl(key, values, value, def) {
150
+ const frag = document.createDocumentFragment();
151
+ const inp = document.createElement('input');
152
+ inp.type = 'text'; inp.dataset.key = key; inp.dataset.type = 'string';
153
+ const listId = 'dl-' + key.replace(/\\./g, '-');
154
+ inp.setAttribute('list', listId);
155
+ if (value !== undefined && value !== null) inp.value = value;
156
+ else if (def !== undefined && def !== '') inp.placeholder = String(def);
157
+ const dl = document.createElement('datalist'); dl.id = listId;
158
+ values.forEach(v => { const o = document.createElement('option'); o.value = v; dl.appendChild(o); });
159
+ frag.appendChild(inp); frag.appendChild(dl);
160
+ return frag;
161
+ }
162
+
163
+ function fieldInput(key, spec, value, def) {
164
+ const isUnset = value === undefined || value === null;
165
+ const opt = fieldOptions[key];
166
+ if (opt) return opt.strict ? selectEl(key, opt.values, value, def) : comboEl(key, opt.values, value, def);
167
+ const enumVals = spec.enum;
168
+ if (spec.type === 'boolean') {
169
+ const cb = document.createElement('input');
170
+ cb.type = 'checkbox'; cb.dataset.key = key; cb.dataset.type = 'boolean';
171
+ cb.checked = value === true;
172
+ return cb;
173
+ }
174
+ if (Array.isArray(enumVals)) return selectEl(key, enumVals, value, def);
175
+ const inp = document.createElement('input');
176
+ const isNum = spec.type === 'number' || spec.type === 'integer';
177
+ inp.dataset.key = key;
178
+ inp.dataset.type = isNum ? 'number' : 'string';
179
+ const isEnvRef = typeof value === 'string' && value.startsWith('env:');
180
+ if (key === 'db.password') inp.type = 'password';
181
+ else if (key.endsWith('.apiKey')) inp.type = isEnvRef ? 'text' : 'password';
182
+ else if (isNum) {
183
+ inp.type = 'number';
184
+ // Only apply small, meaningful bounds (skip zod's safe-integer noise).
185
+ if (typeof spec.minimum === 'number' && Math.abs(spec.minimum) < 1e6) inp.min = spec.minimum;
186
+ if (typeof spec.maximum === 'number' && Math.abs(spec.maximum) < 1e6) inp.max = spec.maximum;
187
+ if (spec.maximum === 1) inp.step = '0.05';
188
+ } else inp.type = 'text';
189
+ if (!isUnset) inp.value = value;
190
+ // Show the applied default as placeholder when the field is left unset.
191
+ else if (def !== undefined && def !== '') inp.placeholder = String(def);
192
+ return inp;
193
+ }
194
+
195
+ function renderField(container, key, spec, value, def) {
196
+ const isUnset = value === undefined || value === null;
197
+ const wrap = document.createElement('div');
198
+ wrap.className = 'field'; wrap.dataset.fieldKey = key;
199
+ const label = document.createElement('label');
200
+ let html = key.split('.').pop();
201
+ if (spec.description) html += '<span class="desc">' + spec.description + '</span>';
202
+ if (key.endsWith('.apiKey') && typeof value === 'string' && value.startsWith('env:')) {
203
+ html += '<span class="defval using">✔ set via ' + value.slice(4) + '</span>';
204
+ }
205
+ if (def !== undefined) {
206
+ html += '<span class="defval' + (isUnset ? ' using' : '') + '">'
207
+ + (isUnset ? '↪ using default: ' : 'default: ') + fmtDefault(def) + '</span>';
208
+ }
209
+ label.innerHTML = html;
210
+ wrap.appendChild(label);
211
+ wrap.appendChild(fieldInput(key, spec, value, def));
212
+ container.appendChild(wrap);
213
+ }
214
+
215
+ function renderForm() {
216
+ const form = document.getElementById('config-form');
217
+ form.innerHTML = '';
218
+ const props = schema.properties || {};
219
+ for (const [group, gspec] of Object.entries(props)) {
220
+ const sec = document.createElement('div'); sec.className = 'section';
221
+ const h = document.createElement('h2'); h.textContent = group; sec.appendChild(h);
222
+ if (gspec.type === 'object' && gspec.properties) {
223
+ for (const [field, fspec] of Object.entries(gspec.properties)) {
224
+ const key = group + '.' + field;
225
+ renderField(sec, key, fspec, nested(config, key), defaults[key]);
226
+ }
227
+ } else {
228
+ renderField(sec, group, gspec, nested(config, group), defaults[group]);
229
+ }
230
+ form.appendChild(sec);
231
+ }
232
+ }
233
+
234
+ function collectForm() {
235
+ const out = {};
236
+ document.querySelectorAll('[data-key]').forEach(el => {
237
+ const key = el.dataset.key, type = el.dataset.type;
238
+ let val;
239
+ if (type === 'boolean') val = el.checked;
240
+ else if (el.value === '') return; // unset → omit (stay minimal)
241
+ else if (type === 'number') { const n = Number(el.value); if (Number.isNaN(n)) return; val = n; }
242
+ else val = el.value;
243
+ const parts = key.split('.');
244
+ if (parts.length === 1) out[parts[0]] = val;
245
+ else { (out[parts[0]] = out[parts[0]] || {})[parts[1]] = val; }
246
+ });
247
+ return out;
248
+ }
249
+
250
+ function showBanner(cls, msg) {
251
+ const b = document.getElementById('banner');
252
+ b.className = cls; b.textContent = msg;
253
+ }
254
+ function clearInvalid() {
255
+ document.querySelectorAll('.field.invalid').forEach(f => f.classList.remove('invalid'));
256
+ }
257
+
258
+ async function save() {
259
+ clearInvalid();
260
+ const btn = document.getElementById('save-btn');
261
+ btn.disabled = true;
262
+ try {
263
+ const res = await fetch('/api/config', {
264
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify(collectForm()),
266
+ });
267
+ const data = await res.json();
268
+ if (res.ok) {
269
+ showBanner('ok', 'Saved.');
270
+ await loadConfig(); // refresh (re-mask password, normalize)
271
+ } else {
272
+ showBanner('err', data.error || 'Validation failed');
273
+ // best-effort inline highlight: mark fields whose key appears in the message
274
+ document.querySelectorAll('[data-field-key]').forEach(f => {
275
+ if ((data.error || '').includes(f.dataset.fieldKey)) f.classList.add('invalid');
276
+ });
277
+ }
278
+ } catch (e) {
279
+ showBanner('err', 'Request failed: ' + e.message);
280
+ } finally {
281
+ btn.disabled = false;
282
+ }
283
+ }
284
+ document.getElementById('save-btn').addEventListener('click', save);
285
+
286
+ async function loadConfig() {
287
+ [schema, config, defaults, fieldOptions] = await Promise.all([
288
+ fetch('/api/schema').then(r => r.json()),
289
+ fetch('/api/config').then(r => r.json()),
290
+ fetch('/api/defaults').then(r => r.json()),
291
+ fetch('/api/field-options').then(r => r.json()),
292
+ ]);
293
+ config = config || {};
294
+ defaults = defaults || {};
295
+ fieldOptions = fieldOptions || {};
296
+ renderForm();
297
+ }
298
+
299
+ // ---- projects ----
300
+ async function loadProjects() {
301
+ const cards = document.getElementById('cards');
302
+ cards.innerHTML = '<div class="empty">Loading…</div>';
303
+ try {
304
+ const projects = await fetch('/api/projects').then(r => r.json());
305
+ if (!projects.length) { cards.innerHTML = '<div class="empty">No indexed projects.</div>'; return; }
306
+ cards.innerHTML = '';
307
+ projects.forEach(p => {
308
+ const rel = p.relations ? Object.entries(p.relations)
309
+ .map(([k, v]) => k + ': ' + (Array.isArray(v) ? v.join(', ') : v)).join(' · ') : '';
310
+ const card = document.createElement('div'); card.className = 'card';
311
+ card.innerHTML =
312
+ '<h3></h3><div class="chunks"></div>' +
313
+ (p.stack ? '<div class="stack"></div>' : '') +
314
+ (p.description ? '<div class="descr"></div>' : '') +
315
+ (rel ? '<div class="rel"></div>' : '');
316
+ card.querySelector('h3').textContent = p.serviceName;
317
+ card.querySelector('.chunks').textContent = p.chunks + ' chunks';
318
+ if (p.stack) card.querySelector('.stack').textContent = p.stack;
319
+ if (p.description) card.querySelector('.descr').textContent = p.description;
320
+ if (rel) card.querySelector('.rel').textContent = rel;
321
+ cards.appendChild(card);
322
+ });
323
+ } catch (e) {
324
+ cards.innerHTML = '<div class="empty">Failed to load projects: ' + e.message + '</div>';
325
+ }
326
+ }
327
+
328
+ // ---- graph ----
329
+ // Reuse the full standalone graph page (same HTML as project-graph.html) via
330
+ // an iframe instead of re-implementing the network here. Loaded lazily on the
331
+ // first Graph-tab open so the graph page isn't fetched until needed.
332
+ function loadGraph() {
333
+ if (graphLoaded) return;
334
+ graphLoaded = true;
335
+ document.getElementById('graph-frame').src = '/graph';
336
+ }
337
+
338
+ loadConfig().catch(e => showBanner('err', 'Failed to load config: ' + e.message));
339
+ </script>
340
+ </body>
341
+ </html>`;
342
+ }
@@ -0,0 +1,19 @@
1
+ # GPU override for the Infinity service. Layered on top of docker-compose.yml
2
+ # (docker compose -f docker-compose.yml -f docker-compose.infinity-gpu.yml …)
3
+ # only when an NVIDIA GPU + container runtime is detected. Kept separate because
4
+ # a `deploy.reservations.devices` GPU request aborts startup on GPU-less hosts;
5
+ # the base file therefore stays CPU-only and always starts.
6
+ services:
7
+ infinity:
8
+ # Warmup is cheap on GPU (seconds) — re-enable it so the first real request
9
+ # is already fast. On CPU (base file only) it stays off.
10
+ environment:
11
+ INFINITY_MODEL_WARMUP: "true"
12
+ deploy:
13
+ resources:
14
+ reservations:
15
+ devices:
16
+ - driver: nvidia
17
+ count: all
18
+ capabilities:
19
+ - gpu
@@ -19,6 +19,31 @@ services:
19
19
  volumes:
20
20
  - ollama_data:/root/.ollama
21
21
 
22
+ # One Infinity container serves both /embeddings and /rerank. The model set is
23
+ # bound at startup via INFINITY_MODEL_ID (";"-separated for multiple models,
24
+ # e.g. "BAAI/bge-m3;BAAI/bge-reranker-v2-m3"). Defaults to CPU so it starts on
25
+ # any host; GPU is opt-in via docker-compose.infinity-gpu.yml (see rag-setup).
26
+ # Behind an "infinity" profile so `docker compose up -d` (ollama path) doesn't
27
+ # pull this multi-GB image — it starts only when explicitly targeted.
28
+ # Model warmup defaults OFF here: on CPU it runs multi-minute benchmark
29
+ # batches at every boot (a pure perf measurement, not required to serve). The
30
+ # GPU override turns it back on, where it costs seconds.
31
+ infinity:
32
+ image: michaelf34/infinity:latest
33
+ restart: unless-stopped
34
+ ports:
35
+ - "7997:7997"
36
+ environment:
37
+ INFINITY_MODEL_ID: ${INFINITY_MODEL_ID:-jinaai/jina-embeddings-v2-base-code}
38
+ INFINITY_MODEL_WARMUP: ${INFINITY_MODEL_WARMUP:-false}
39
+ INFINITY_PORT: "7997"
40
+ command: ["v2"]
41
+ volumes:
42
+ - infinity_cache:/app/.cache
43
+ profiles:
44
+ - infinity
45
+
22
46
  volumes:
23
47
  pgdata:
24
48
  ollama_data:
49
+ infinity_cache:
@@ -0,0 +1,187 @@
1
+ # Web dashboard (`rag-web`)
2
+
3
+ A local web UI for configuring and inspecting the RAG server. It provides a
4
+ schema-driven config editor, an overview of indexed projects, and an interactive
5
+ project-dependency graph — all served from a small built-in HTTP server with no
6
+ external framework and no new dependencies (`node:http`).
7
+
8
+ The dashboard is a thin presentation layer over primitives that already exist:
9
+ config I/O and validation (`src/config-store.ts`), project data
10
+ (`SearcherService.listProjects`), and the graph builder (`src/graph.ts`). It adds
11
+ no business logic of its own beyond request routing and a password-preservation
12
+ merge.
13
+
14
+ ---
15
+
16
+ ## Running
17
+
18
+ ```bash
19
+ rag-web # http://127.0.0.1:4319
20
+ rag-web --port 8080 # custom port
21
+ npm run start:web # dev mode (ts-node, no build)
22
+ ```
23
+
24
+ If the `rag-web` binary is not on your `PATH`, either run it from the project
25
+ (`node dist/app.js web`) or register it once with `npm link`.
26
+
27
+ Keep the terminal open — the server runs until you press `Ctrl+C`. The browser
28
+ can only reach the dashboard while the process is alive.
29
+
30
+ ---
31
+
32
+ ## Security model
33
+
34
+ - **Binds to `127.0.0.1` only.** The server is never exposed on the network;
35
+ it is a single-user local dev tool, so there is no authentication.
36
+ - **The DB password is never sent to the browser.** `GET /api/config` returns it
37
+ masked as `********`. On save, if the field is still the mask, the stored
38
+ password is preserved (`mergeConfigForSave`); a genuinely changed value is
39
+ written through.
40
+ - **No arbitrary file serving.** Only the known routes below respond; there is no
41
+ static file handler, so no path-traversal surface.
42
+
43
+ ---
44
+
45
+ ## Requirements & failure modes
46
+
47
+ - Requires a valid `config.json` — `rag-web` goes through the normal container
48
+ bootstrap and `workspaceRoot` guard. To repair a broken config, use
49
+ `rag-config edit` (CLI) instead.
50
+ - A failed **DB** connection is non-fatal: the config editor still works; the
51
+ Projects and Graph tabs show an in-page error.
52
+ - A failed **Ollama** connection only affects model suggestions (see
53
+ *Field options*); everything else works.
54
+
55
+ ---
56
+
57
+ ## UI tabs
58
+
59
+ ### Config
60
+ A form generated from the config JSON Schema (`buildJsonSchema()` — the same
61
+ schema that powers editor autocomplete on `config.json`). Fields are grouped by
62
+ section and typed automatically:
63
+
64
+ | Schema shape | Rendered as |
65
+ |---|---|
66
+ | `boolean` | checkbox |
67
+ | `enum` (e.g. `logging.level`) | `<select>` |
68
+ | `integer` / `number` | number input (with sensible `min`/`max`/`step`) |
69
+ | `string` | text input |
70
+ | field in `/api/field-options`, `strict` | `<select>` limited to the list |
71
+ | field in `/api/field-options`, non-strict | text input + `<datalist>` (combobox) |
72
+
73
+ Two affordances make the effective configuration obvious:
74
+
75
+ - **Defaults.** Any key left unset shows `↪ using default: <value>` (amber) and
76
+ the applied value as the input placeholder. A key that *is* set shows a muted
77
+ `default: <value>` for reference. Source: `GET /api/defaults`
78
+ (`CONFIG_DEFAULTS`).
79
+ - **Field options.** Fields whose valid values are constrained get a dropdown
80
+ instead of free text. Source: `GET /api/field-options`. See below.
81
+
82
+ Saving validates through `parseConfig` and, on failure, shows the validation
83
+ message and highlights the offending fields; on success the config is written
84
+ (with the `$schema` reference preserved) and the form reloads.
85
+
86
+ ### Projects
87
+ Cards for each indexed project from `SearcherService.listProjects()` — name,
88
+ chunk count, and any `.ragmeta.json` metadata (stack, description, relations).
89
+
90
+ ### Graph
91
+ The **full standalone graph page embedded in an `<iframe>`** (`src="/graph"`),
92
+ loaded lazily on first open. `GET /graph` serves the exact same HTML as the
93
+ generated `project-graph.html` (`buildHtml()` in `src/graph.ts`), so the whole
94
+ sidebar comes for free: the node-color legend, the edge-type filter buttons, and
95
+ the project toggle list, plus hover highlighting and auto-fit. The dashboard
96
+ does **not** re-implement vis-network — reusing one page keeps the two graph
97
+ views identical. `GET /api/graph` still returns the raw `{ nodes, edges }` JSON
98
+ (`buildGraphData()`) for programmatic use.
99
+
100
+ ---
101
+
102
+ ## Field options (model dropdowns)
103
+
104
+ Some settings have a constrained set of valid values, so the form offers a list
105
+ rather than free text. `GET /api/field-options` returns
106
+ `{ [dottedKey]: { values: string[], strict: boolean } }`:
107
+
108
+ - **`ollama.model` — strict.** The embedding model is tied to the vector column
109
+ dimension (`bge-m3` → 1024). Values come from `SUPPORTED_EMBED_MODELS`
110
+ (`src/constants.ts`); the dropdown does **not** allow free text, so a
111
+ dimension-incompatible model cannot be set. To add a model, confirm its
112
+ dimension matches and append it to `SUPPORTED_EMBED_MODELS`.
113
+ - **`search.translateModel`, `reranker.model` — non-strict.** Suggestions are the
114
+ user's installed Ollama models (`ollama.list()`), but free text is allowed
115
+ because these are not dimension-bound and the user may pull a new model later.
116
+ If Ollama is unreachable the list is empty and the field degrades to plain
117
+ text.
118
+
119
+ A strict `<select>` preserves an unknown current value as an extra option, so
120
+ saving never silently drops a value that isn't in the list.
121
+
122
+ ---
123
+
124
+ ## HTTP API
125
+
126
+ All routes are served from `127.0.0.1:<port>`.
127
+
128
+ | Method | Path | Response |
129
+ |---|---|---|
130
+ | `GET` | `/` | Dashboard HTML (self-contained) |
131
+ | `GET` | `/api/schema` | JSON Schema of the config (`buildJsonSchema()`) |
132
+ | `GET` | `/api/defaults` | `CONFIG_DEFAULTS` — dotted key → default value |
133
+ | `GET` | `/api/field-options` | dotted key → `{ values, strict }` |
134
+ | `GET` | `/api/config` | Current config, `db.password` masked |
135
+ | `PUT` | `/api/config` | Save. Body = config JSON. `200 {ok:true}` or `400 {error}` |
136
+ | `GET` | `/api/projects` | `SearcherService.listProjects()` |
137
+ | `GET` | `/api/graph` | `{ nodes, edges }` JSON (`buildGraphData()`) |
138
+ | `GET` | `/graph` | Full interactive graph page (`buildHtml()`), embedded by the Graph tab's iframe |
139
+
140
+ ---
141
+
142
+ ## Architecture
143
+
144
+ | File | Responsibility |
145
+ |---|---|
146
+ | `src/commands/WebCommand.ts` | CLI entry. `db.initDb()`, starts the server, keeps `execute()` pending until `SIGINT`/`SIGTERM` (mirrors `McpCommand`), then `server.stop()`. Parses `--port` (default `4319`). |
147
+ | `src/services/WebServerService.ts` | `WebServer(searcher, config, logger)` — `node:http` server, routing, and the API handlers. Exports the pure `mergeConfigForSave()` and the `FieldOption` type. |
148
+ | `src/web/dashboard.ts` | `renderDashboardHtml()` — the entire page (HTML + CSS + JS) as one self-contained string, mirroring `graph.ts:buildHtml`. |
149
+
150
+ Reused primitives (no duplication):
151
+
152
+ - `readConfigJson` / `parseConfig` / `writeConfigJson` / `buildJsonSchema` /
153
+ `CONFIG_DEFAULTS` — `src/config-store.ts`
154
+ - `buildGraphData` — `src/graph.ts`
155
+ - `listProjects` — `src/services/SearcherService.ts`
156
+ - `SUPPORTED_EMBED_MODELS` — `src/constants.ts`
157
+
158
+ Wiring: `app.ts` detects the `web` mode and dispatches
159
+ `new WebCommand(container, effectiveArgs)`. The command uses the shared
160
+ `Container` services (`db`, `searcher`, `config`, `logger`) — no container
161
+ changes were needed.
162
+
163
+ ---
164
+
165
+ ## Extending
166
+
167
+ - **Constrain another field to a list** — add an entry to `fieldOptions()` in
168
+ `WebServerService.ts` (`{ values, strict }`). The form renders it automatically
169
+ (strict → dropdown, non-strict → combobox).
170
+ - **Add a tab / endpoint** — add a route in `WebServer.handle`, a fetch + render
171
+ in `dashboard.ts`, and a tab button. Keep handlers thin — delegate to a service.
172
+ - **Actions (e.g. trigger reindex)** — deferred by design. This is heavier (a
173
+ long-running process with live progress, likely via SSE, plus concurrency
174
+ control) and belongs in its own change rather than the read/config-edit shell.
175
+
176
+ ---
177
+
178
+ ## Keep in sync
179
+
180
+ - `CONFIG_DEFAULTS` (`config-store.ts`) is the **single source of truth** for
181
+ built-in defaults: `resolvedSchema` in `config.ts` references it in its
182
+ `.default(...)` calls, so the runtime default and the dashboard-displayed
183
+ default cannot diverge — no manual sync needed. (A unit test still asserts
184
+ every key is a valid config key.)
185
+ - `SUPPORTED_EMBED_MODELS` (`constants.ts`) must contain only models whose
186
+ embedding dimension matches the `vector(...)` column — this one is still a
187
+ manual invariant, guarded only at the membership level by a unit test.
package/package.json CHANGED
@@ -1,26 +1,48 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.17-alpha.1",
3
+ "version": "0.1.0-alpha",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "rag",
10
+ "semantic-search",
11
+ "code-search",
12
+ "pgvector",
13
+ "embeddings",
14
+ "claude",
15
+ "ollama",
16
+ "infinity"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "publishConfig": {
22
+ "tag": "alpha"
23
+ },
6
24
  "main": "dist/app.js",
7
25
  "bin": {
8
26
  "rag-index": "dist/app.js",
9
27
  "rag-mcp": "dist/app.js",
10
28
  "rag-setup": "dist/app.js",
11
- "rag-config": "dist/app.js"
29
+ "rag-config": "dist/app.js",
30
+ "rag-web": "dist/app.js"
12
31
  },
13
32
  "files": [
14
33
  "dist/",
15
34
  "docker/",
35
+ "docs/",
16
36
  "README.md",
17
37
  "CHANGELOG.md"
18
38
  ],
19
39
  "scripts": {
40
+ "prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
20
41
  "build": "tsc",
21
42
  "postbuild": "node -e \"const fs=require('fs');const f='dist/app.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,0o755);\"",
22
- "prepublishOnly": "npm run build && npm run typecheck",
43
+ "prepublishOnly": "npm run build && npm run typecheck && npm test",
23
44
  "start:mcp": "ts-node app.ts mcp",
45
+ "start:web": "ts-node app.ts web",
24
46
  "index:workspace": "ts-node app.ts index",
25
47
  "index:reindex": "ts-node app.ts index --reindex",
26
48
  "index:backfill": "ts-node app.ts backfill",
package/dist/src/ast.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { CodeDependency } from './db';
2
- export declare function extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<Omit<CodeDependency, 'id'>[]>;