codesearch-mcp 0.1.0-alpha → 0.2.1-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 (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +146 -41
  3. package/dist/app.js +4 -19
  4. package/dist/src/commands/IndexCommand.js +68 -7
  5. package/dist/src/config-error.d.ts +7 -0
  6. package/dist/src/config-error.js +14 -0
  7. package/dist/src/config-store.d.ts +30 -12
  8. package/dist/src/config-store.js +81 -26
  9. package/dist/src/config.d.ts +5 -4
  10. package/dist/src/config.js +14 -16
  11. package/dist/src/constants.d.ts +8 -2
  12. package/dist/src/constants.js +8 -5
  13. package/dist/src/db/index.d.ts +1 -1
  14. package/dist/src/db/index.js +2 -1
  15. package/dist/src/db/schema.d.ts +75 -0
  16. package/dist/src/db/schema.js +10 -1
  17. package/dist/src/enriched-prompt.d.ts +8 -0
  18. package/dist/src/enriched-prompt.js +87 -0
  19. package/dist/src/help.js +1 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +18 -2
  21. package/dist/src/interfaces/IDatabase.d.ts +31 -0
  22. package/dist/src/interfaces/ILogger.d.ts +7 -0
  23. package/dist/src/interfaces/ISearcher.d.ts +3 -0
  24. package/dist/src/services/AstChunker.d.ts +11 -0
  25. package/dist/src/services/AstChunker.js +44 -0
  26. package/dist/src/services/AstParserService.d.ts +39 -7
  27. package/dist/src/services/AstParserService.js +504 -104
  28. package/dist/src/services/DatabaseService.d.ts +17 -0
  29. package/dist/src/services/DatabaseService.js +130 -2
  30. package/dist/src/services/EmbedderFactory.d.ts +1 -1
  31. package/dist/src/services/EmbedderFactory.js +2 -5
  32. package/dist/src/services/IndexerService.d.ts +4 -14
  33. package/dist/src/services/IndexerService.js +54 -156
  34. package/dist/src/services/InfinityEmbedder.d.ts +14 -55
  35. package/dist/src/services/InfinityEmbedder.js +30 -141
  36. package/dist/src/services/LoggerService.d.ts +3 -1
  37. package/dist/src/services/LoggerService.js +17 -3
  38. package/dist/src/services/McpServer.js +3 -3
  39. package/dist/src/services/SearcherService.d.ts +39 -4
  40. package/dist/src/services/SearcherService.js +254 -81
  41. package/dist/src/services/WebServerService.d.ts +41 -3
  42. package/dist/src/services/WebServerService.js +90 -2
  43. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  44. package/dist/src/services/infinity-provisioning.js +516 -0
  45. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  46. package/dist/src/services/legacy-ollama-index.js +61 -0
  47. package/dist/src/services/reranker.d.ts +3 -3
  48. package/dist/src/services/reranker.js +3 -3
  49. package/dist/src/setup.d.ts +37 -0
  50. package/dist/src/setup.js +111 -157
  51. package/dist/src/symbol-name.d.ts +44 -0
  52. package/dist/src/symbol-name.js +68 -0
  53. package/dist/src/symbol-rows.d.ts +12 -0
  54. package/dist/src/symbol-rows.js +38 -0
  55. package/dist/src/tools/FindSymbolTool.js +30 -7
  56. package/dist/src/tools/GetCallersTool.js +20 -5
  57. package/docker/docker-compose.yml +5 -15
  58. package/docs/architecture.svg +103 -0
  59. package/docs/web-dashboard.md +36 -17
  60. package/package.json +1 -6
  61. package/dist/src/commands/BackfillCommand.d.ts +0 -7
  62. package/dist/src/commands/BackfillCommand.js +0 -37
  63. package/dist/src/services/OllamaEmbedder.d.ts +0 -25
  64. package/dist/src/services/OllamaEmbedder.js +0 -111
package/dist/src/setup.js CHANGED
@@ -33,17 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EMBEDDING_PROVIDER_CHOICES = void 0;
36
37
  exports.dockerPromptDefault = dockerPromptDefault;
38
+ exports.prepareComposeAndStartDb = prepareComposeAndStartDb;
39
+ exports.provisionInfinity = provisionInfinity;
37
40
  exports.runSetup = runSetup;
38
41
  const readline = __importStar(require("readline"));
39
42
  const fs = __importStar(require("fs"));
40
43
  const path = __importStar(require("path"));
41
44
  const os = __importStar(require("os"));
42
45
  const child_process_1 = require("child_process");
43
- const constants_1 = require("./constants");
44
46
  const config_store_1 = require("./config-store");
45
47
  const config_1 = require("./config");
46
- const InfinityEmbedder_1 = require("./services/InfinityEmbedder");
48
+ const infinity_provisioning_1 = require("./services/infinity-provisioning");
47
49
  const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
48
50
  const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
49
51
  ? path.join(__dirname, '..')
@@ -52,7 +54,6 @@ const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml
52
54
  const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
53
55
  const DOCKER_GPU_OVERRIDE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.infinity-gpu.yml');
54
56
  const DOCKER_GPU_OVERRIDE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.infinity-gpu.yml');
55
- const DEFAULT_INFINITY_RERANK_MODEL = 'Alibaba-NLP/gte-reranker-modernbert-base';
56
57
  function ask(rl, question) {
57
58
  return new Promise(resolve => rl.question(question, resolve));
58
59
  }
@@ -66,14 +67,15 @@ function expandHome(p) {
66
67
  return p;
67
68
  }
68
69
  const PROVIDER_DEFAULTS = {
69
- 'ollama': { baseURL: 'http://localhost:11434', model: constants_1.DEFAULT_EMBED_MODEL, envVar: '' },
70
70
  'openai-compatible': { baseURL: 'https://api.openai.com/v1', model: 'text-embedding-3-small', envVar: 'OPENAI_API_KEY' },
71
71
  'voyage': { baseURL: 'https://api.voyageai.com/v1', model: 'voyage-code-3', envVar: 'VOYAGE_API_KEY' },
72
72
  'infinity': { baseURL: 'http://localhost:7997', model: 'jinaai/jina-embeddings-v2-base-code', envVar: '' },
73
73
  };
74
+ /** Providers the wizard offers, in prompt order; the first is the preselected default. Exported for unit testing. */
75
+ exports.EMBEDDING_PROVIDER_CHOICES = ['infinity', 'openai-compatible', 'voyage'];
74
76
  /** Providers that run a local inference backend we can probe / provision. */
75
77
  function isLocalBackend(p) {
76
- return p === 'ollama' || p === 'infinity';
78
+ return p === 'infinity';
77
79
  }
78
80
  /**
79
81
  * Probe whether the embedding backend is already reachable, so the "start
@@ -83,24 +85,7 @@ function isLocalBackend(p) {
83
85
  async function checkEmbeddingBackend(provider, baseURL) {
84
86
  if (!isLocalBackend(provider))
85
87
  return true;
86
- if (provider === 'infinity')
87
- return (0, InfinityEmbedder_1.probeInfinityHealth)(baseURL);
88
- // ollama — probe /api/tags
89
- try {
90
- const http = await Promise.resolve().then(() => __importStar(require('http')));
91
- await new Promise((resolve, reject) => {
92
- const req = http.get(`${baseURL.replace(/\/+$/, '')}/api/tags`, res => {
93
- res.resume();
94
- res.statusCode === 200 ? resolve() : reject(new Error(`status ${res.statusCode}`));
95
- });
96
- req.on('error', reject);
97
- req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
98
- });
99
- return true;
100
- }
101
- catch {
102
- return false;
103
- }
88
+ return (0, infinity_provisioning_1.probeInfinityHealth)(baseURL);
104
89
  }
105
90
  /**
106
91
  * Default answer for the "start Docker containers?" prompt: no only when both
@@ -110,15 +95,6 @@ async function checkEmbeddingBackend(provider, baseURL) {
110
95
  function dockerPromptDefault(dbReachable, embReachable) {
111
96
  return (dbReachable && embReachable) ? 'n' : 'y';
112
97
  }
113
- /** True when an NVIDIA GPU + `nvidia-smi` is available on this host. */
114
- function hasNvidiaGpu() {
115
- try {
116
- return (0, child_process_1.spawnSync)('nvidia-smi', ['-L'], { stdio: 'ignore' }).status === 0;
117
- }
118
- catch {
119
- return false;
120
- }
121
- }
122
98
  /**
123
99
  * Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
124
100
  * chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
@@ -160,58 +136,6 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, st
160
136
  logger?.error('waitForDb: timeout', { reason, steps: JSON.stringify(steps) });
161
137
  process.exit(1);
162
138
  }
163
- async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
164
- const deadline = Date.now() + maxWaitSec * 1000;
165
- process.stdout.write('Waiting for Ollama to be ready');
166
- while (Date.now() < deadline) {
167
- try {
168
- const http = await Promise.resolve().then(() => __importStar(require('http')));
169
- await new Promise((resolve, reject) => {
170
- const req = http.get(`${host}/api/tags`, res => {
171
- res.resume();
172
- res.statusCode === 200 ? resolve() : reject(new Error(`status ${res.statusCode}`));
173
- });
174
- req.on('error', reject);
175
- req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
176
- });
177
- console.log(' ready.');
178
- return;
179
- }
180
- catch {
181
- process.stdout.write('.');
182
- await new Promise(r => setTimeout(r, 2000));
183
- }
184
- }
185
- const reason = 'Ollama did not become ready in time.';
186
- console.error(`\n${reason}`);
187
- logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
188
- process.exit(1);
189
- }
190
- /**
191
- * Poll Infinity's `/health` until ready. Unlike the DB/Ollama waiters this is
192
- * NON-fatal: on CPU the first boot downloads the model set AND runs a lengthy
193
- * per-model warmup (a reranker warmup batch alone can take minutes), so a
194
- * timeout here does not mean failure — the container is usually just still
195
- * warming up. Returns whether it became ready; the caller keeps going and
196
- * writes the (valid) config either way. Generous default (30 min) for CPU.
197
- */
198
- async function waitForInfinity(baseURL, maxWaitSec = 1800, logger, steps) {
199
- const deadline = Date.now() + maxWaitSec * 1000;
200
- process.stdout.write('Waiting for Infinity to be ready (first boot downloads + warms up the model set, this can take several minutes on CPU)');
201
- while (Date.now() < deadline) {
202
- if (await (0, InfinityEmbedder_1.probeInfinityHealth)(baseURL)) {
203
- console.log(' ready.');
204
- return true;
205
- }
206
- process.stdout.write('.');
207
- await new Promise(r => setTimeout(r, 3000));
208
- }
209
- const reason = 'Infinity did not answer /health in time — it is likely still warming up.';
210
- console.warn(`\n${reason}`);
211
- console.warn(` The container is running; check "docker logs codesearch-mcp-infinity-1" and, once /health responds, run: rag-index`);
212
- logger?.warn('waitForInfinity: timeout (non-fatal)', { reason, steps: JSON.stringify(steps) });
213
- return false;
214
- }
215
139
  async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
216
140
  try {
217
141
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
@@ -227,8 +151,16 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
227
151
  return false;
228
152
  }
229
153
  }
230
- async function startDockerContainers(opts) {
231
- const { provider, ollamaHost, embBaseURL, dbPort, dbUser, dbPass, embModel, rerankModel, startDb, startBackend, logger, steps } = opts;
154
+ /**
155
+ * Regenerate `CONFIG_DIR/docker-compose.yml` from the shipped template
156
+ * (carrying over an existing literal `INFINITY_MODEL_ID`, written atomically),
157
+ * copy the GPU override, and start the DB container when requested. Runs
158
+ * BEFORE the config file is written — its failure aborts setup unsaved,
159
+ * matching the existing "abort without saving" behavior of the DB step. Does
160
+ * NOT touch Infinity provisioning (moved after `writeConfigJson` — see D6a).
161
+ */
162
+ async function prepareComposeAndStartDb(opts) {
163
+ const { dbPort, dbUser, dbPass, startDb, logger, steps } = opts;
232
164
  console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
233
165
  const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
234
166
  let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
@@ -236,72 +168,99 @@ async function startDockerContainers(opts) {
236
168
  .replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
237
169
  .replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
238
170
  .replace('"5433:5432"', `"${dbPort}:5432"`);
171
+ // Carry over an existing literal INFINITY_MODEL_ID — never the set of the
172
+ // config still being entered — so a setup that aborts before the config is
173
+ // written leaves a file consistent with the previously saved config.
174
+ let existingModelId = null;
175
+ try {
176
+ existingModelId = (0, infinity_provisioning_1.extractModelIdValue)(fs.readFileSync(DOCKER_COMPOSE_DST, 'utf-8'));
177
+ }
178
+ catch { /* no prior file */ }
179
+ if (existingModelId) {
180
+ composeContent = composeContent.replace(/INFINITY_MODEL_ID:\s*"[^"]*"/, `INFINITY_MODEL_ID: "${existingModelId}"`);
181
+ }
239
182
  fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
240
- fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
183
+ let existingMode = 0o600;
184
+ try {
185
+ existingMode = fs.statSync(DOCKER_COMPOSE_DST).mode;
186
+ }
187
+ catch { /* first-ever write */ }
188
+ (0, infinity_provisioning_1.atomicWriteFile)(DOCKER_COMPOSE_DST, composeContent, existingMode);
241
189
  // The GPU override is copied verbatim so the managed Infinity path can layer it.
242
190
  try {
243
191
  fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
244
192
  }
245
193
  catch { /* absent → CPU only */ }
246
- const dockerFail = (reason) => {
247
- console.error(reason);
248
- logger?.error('startDockerContainers: docker startup failed', { reason, steps: JSON.stringify(steps) });
194
+ if (!startDb)
195
+ return;
196
+ console.log('\nStarting the database container...');
197
+ const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'db'], { stdio: 'inherit' });
198
+ if (r.status !== 0) {
199
+ console.error('Failed to start the database container. Make sure Docker is running.');
200
+ logger?.error('prepareComposeAndStartDb: docker startup failed', { steps: JSON.stringify(steps) });
249
201
  process.exit(1);
250
- };
251
- // --- Database ---
252
- if (startDb) {
253
- console.log('\nStarting the database container...');
254
- const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'db'], { stdio: 'inherit' });
255
- if (r.status !== 0)
256
- dockerFail('Failed to start the database container. Make sure Docker is running.');
257
- await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
258
202
  }
259
- // --- Embedding backend ---
260
- if (!startBackend) {
261
- if (isLocalBackend(provider))
262
- console.log(`${provider} is already reachable — not starting a managed container.`);
263
- else
264
- console.log('Cloud embedding provider — no local backend to start.');
265
- return;
203
+ await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
204
+ }
205
+ /**
206
+ * Infinity provisioning (D6a) — runs AFTER the config file is written, so no
207
+ * outcome here can leave the config unsaved. `savedConfig` is a fresh read of
208
+ * the just-written file (never the process's own startup config). Exit-status
209
+ * rules: 0 for served / timeout (warning) / external instructions / declined
210
+ * Docker; 1 for any other provisioning failure — the cause is printed and the
211
+ * remaining setup steps (DB schema init) still run.
212
+ */
213
+ async function provisionInfinity(savedConfig, startDocker, logger, deps = {}, composePathOverride) {
214
+ const composePath = composePathOverride ?? DOCKER_COMPOSE_DST;
215
+ const baseURL = savedConfig.embedding.baseURL;
216
+ const base = (0, infinity_provisioning_1.serverRoot)(baseURL);
217
+ const set = (0, infinity_provisioning_1.requiredModelSet)(savedConfig);
218
+ const managed = (0, infinity_provisioning_1.isManagedInfinity)(baseURL, composePath);
219
+ // Targeted write happens for a managed backend regardless of what follows
220
+ // (yes/no, served/missing), so the compose file always matches the saved
221
+ // config — even when the running container already serves the set.
222
+ if (managed) {
223
+ try {
224
+ (0, infinity_provisioning_1.writeComposeModelSet)(composePath, set);
225
+ }
226
+ catch (err) {
227
+ console.error(`\nFailed to write the managed compose file: ${err.message}`);
228
+ logger?.error('provisionInfinity: compose write failed', { reason: err.message });
229
+ process.exitCode = 1;
230
+ }
266
231
  }
267
- if (provider === 'ollama') {
268
- console.log('\nStarting the Ollama container...');
269
- const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'ollama'], { stdio: 'inherit' });
270
- if (r.status !== 0)
271
- dockerFail('Failed to start the Ollama container. Make sure Docker is running.');
272
- await waitForOllama(ollamaHost, 60, logger, steps);
273
- const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
274
- .stdout?.trim();
275
- if (!ollamaId) {
276
- const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
277
- console.error(reason);
278
- console.error(` docker exec <ollama-container> ollama pull ${embModel}`);
279
- logger?.error('startDockerContainers: ollama container not found', { reason, steps: JSON.stringify(steps) });
280
- process.exit(1);
232
+ if (startDocker) {
233
+ try {
234
+ await (0, infinity_provisioning_1.ensureModelSetServed)(baseURL, composePath, set, deps);
235
+ console.log(`\nInfinity is serving the configured model set: ${set.join(';')}`);
236
+ }
237
+ catch (err) {
238
+ const msg = err.message;
239
+ if (/did not become ready within/.test(msg)) {
240
+ console.warn(`\nWarning: ${msg}`);
241
+ console.warn('The container is running and likely still warming up. Check again once /health responds, then run: rag-index');
242
+ logger?.warn('provisionInfinity: readiness timeout (non-fatal)', { reason: msg });
243
+ }
244
+ else {
245
+ console.error(`\nInfinity provisioning failed: ${msg}`);
246
+ logger?.error('provisionInfinity: failed', { reason: msg });
247
+ process.exitCode = 1;
248
+ }
281
249
  }
282
- console.log(`Pulling ${embModel} model (this may take a while)...`);
283
- // `-t` allocates a pseudo-TTY so `ollama pull` renders its live progress bar;
284
- // only when our stdout is itself a TTY (docker refuses `-t` without one).
285
- const ttyFlag = process.stdout.isTTY ? ['-t'] : [];
286
- (0, child_process_1.spawnSync)('docker', ['exec', ...ttyFlag, ollamaId, 'ollama', 'pull', embModel], { stdio: 'inherit' });
287
250
  return;
288
251
  }
289
- if (provider === 'infinity') {
290
- // Both models are bound at boot via a ";"-separated INFINITY_MODEL_ID.
291
- const modelIds = [embModel, ...(rerankModel ? [rerankModel] : [])].join(';');
292
- // Only layer the GPU override when it actually exists on disk — otherwise a
293
- // GPU host whose override copy failed would get `-f <missing>` → hard failure
294
- // instead of the intended CPU fallback.
295
- const gpu = hasNvidiaGpu() && fs.existsSync(DOCKER_GPU_OVERRIDE_DST);
296
- const files = ['-f', DOCKER_COMPOSE_DST, ...(gpu ? ['-f', DOCKER_GPU_OVERRIDE_DST] : [])];
297
- console.log(`\nStarting the Infinity container (${gpu ? 'GPU' : 'CPU'}) serving: ${modelIds}`);
298
- // `--force-recreate` so a changed model set recreates the container (D7) —
299
- // Infinity binds its models at startup and won't pick up a new set otherwise.
300
- const r = (0, child_process_1.spawnSync)('docker', ['compose', ...files, 'up', '-d', '--force-recreate', 'infinity'], { stdio: 'inherit', env: { ...process.env, INFINITY_MODEL_ID: modelIds } });
301
- if (r.status !== 0)
302
- dockerFail('Failed to start the Infinity container. Make sure Docker is running.');
303
- await waitForInfinity(embBaseURL, 1800, logger, steps);
252
+ // Docker declined — no Docker command; print what would apply the set.
253
+ const evaluation = await (0, infinity_provisioning_1.evaluateBackend)(baseURL, composePath, set, deps);
254
+ if (evaluation.action === 'served')
304
255
  return;
256
+ if (evaluation.managed) {
257
+ const files = (0, infinity_provisioning_1.composeFileArgs)(composePath);
258
+ console.log(`\nInfinity does not yet serve the required model set (${set.join(';')}).`);
259
+ console.log('Run this to apply it:');
260
+ console.log(` docker compose ${files.join(' ')} up -d --force-recreate infinity`);
261
+ }
262
+ else {
263
+ console.log(`\n${(0, infinity_provisioning_1.manualInstructionMessage)(evaluation, base, set)}`);
305
264
  }
306
265
  }
307
266
  async function runSetup(logger) {
@@ -340,24 +299,22 @@ async function runSetup(logger) {
340
299
  } while (!workspaceInput.trim());
341
300
  const workspaceRoot = expandHome(workspaceInput.trim());
342
301
  console.log('\n--- Embedding backend ---');
343
- console.log(' ollama — local, zero-setup (default)');
344
- console.log(' infinity — local, one container serves embeddings + reranking (GPU/CPU)');
345
- console.log(' openai-compatible — OpenAI cloud, or a local Infinity/TEI/vLLM server');
302
+ console.log(' infinity — local, one container serves embeddings + reranking (GPU/CPU) (default)');
303
+ console.log(' openai-compatible — OpenAI cloud, or a local Infinity/TEI/vLLM/Ollama (via its /v1 endpoint) server');
346
304
  console.log(' voyage — Voyage AI cloud');
347
- const validProviders = ['ollama', 'infinity', 'openai-compatible', 'voyage'];
348
305
  let embProvider;
349
306
  do {
350
- embProvider = (await loggedPrompt('Provider (ollama/infinity/openai-compatible/voyage)', 'ollama'));
351
- if (!validProviders.includes(embProvider))
352
- console.log(` Invalid provider. Choose: ${validProviders.join(', ')}`);
353
- } while (!validProviders.includes(embProvider));
307
+ embProvider = (await loggedPrompt('Provider (infinity/openai-compatible/voyage)', exports.EMBEDDING_PROVIDER_CHOICES[0]));
308
+ if (!exports.EMBEDDING_PROVIDER_CHOICES.includes(embProvider))
309
+ console.log(` Invalid provider. Choose: ${exports.EMBEDDING_PROVIDER_CHOICES.join(', ')}`);
310
+ } while (!exports.EMBEDDING_PROVIDER_CHOICES.includes(embProvider));
354
311
  const pd = PROVIDER_DEFAULTS[embProvider];
355
312
  const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
356
313
  const embModel = await loggedPrompt('Embedding model', pd.model);
357
314
  // Infinity serves a reranker from the same container — offer to provision one.
358
315
  let rerankModel = '';
359
316
  if (embProvider === 'infinity') {
360
- rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, DEFAULT_INFINITY_RERANK_MODEL)).trim();
317
+ rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, config_store_1.CONFIG_DEFAULTS['reranker.model'])).trim();
361
318
  }
362
319
  // API keys go into .env (chmod 600); config.json only stores the env: reference.
363
320
  let apiKeyRef = '';
@@ -373,8 +330,6 @@ async function runSetup(logger) {
373
330
  console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
374
331
  }
375
332
  }
376
- // Ollama host used by the Docker helper below (only meaningful for ollama).
377
- const ollamaHost = embProvider === 'ollama' ? embBaseURL : 'http://localhost:11434';
378
333
  console.log('\n--- Database ---');
379
334
  const dbHost = await loggedPrompt('DB host', 'localhost');
380
335
  const dbPort = await loggedPrompt('DB port', '5433');
@@ -398,15 +353,9 @@ async function runSetup(logger) {
398
353
  const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
399
354
  const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
400
355
  if (startDocker) {
401
- await startDockerContainers({
402
- provider: embProvider,
403
- ollamaHost,
404
- embBaseURL,
356
+ await prepareComposeAndStartDb({
405
357
  dbPort, dbUser, dbPass,
406
- embModel,
407
- rerankModel: rerankModel || undefined,
408
358
  startDb: !dbReachable,
409
- startBackend: isLocalBackend(embProvider) && !embReachable,
410
359
  logger, steps,
411
360
  });
412
361
  }
@@ -469,6 +418,11 @@ async function runSetup(logger) {
469
418
  (0, config_store_1.writeConfigJson)(cfg);
470
419
  console.log(`\nConfig saved to ${config_store_1.CONFIG_FILE}`);
471
420
  rl.close();
421
+ // Infinity provisioning happens only now — after the config is safely on
422
+ // disk — so a provisioning failure never leaves the wizard's answers unsaved.
423
+ if (embProvider === 'infinity') {
424
+ await provisionInfinity(new config_1.Config(), startDocker, logger);
425
+ }
472
426
  console.log('\nInitializing database schema...');
473
427
  const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
474
428
  const dbService = new DatabaseService(new config_1.Config());
@@ -0,0 +1,44 @@
1
+ export declare const BARE_NAME_RE: RegExp;
2
+ export declare const QUALIFIED_NAME_RE: RegExp;
3
+ export type FindSymbolQuery = {
4
+ form: 'bare';
5
+ name: string;
6
+ } | {
7
+ form: 'qualified';
8
+ container: string;
9
+ name: string;
10
+ } | {
11
+ form: 'quoted';
12
+ container: string | null;
13
+ name: string;
14
+ } | {
15
+ form: 'computed';
16
+ container: string | null;
17
+ name: string;
18
+ };
19
+ /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
20
+ export declare function splitQualified(input: string): {
21
+ container: string;
22
+ name: string;
23
+ };
24
+ /**
25
+ * Parse a `find_symbol` name into one of the four accepted forms (bare,
26
+ * qualified, quoted, computed), or null when it matches none — the caller
27
+ * should reject with `Invalid symbol name` without querying anything.
28
+ */
29
+ export declare function parseFindSymbolName(input: string): FindSymbolQuery | null;
30
+ /**
31
+ * Parse a `get_callers` symbol into bare or qualified form only — the quoted
32
+ * and computed forms of `find_symbol` are meaningless here (calls through
33
+ * element access are never recorded in the call graph).
34
+ */
35
+ export declare function parseCallerSymbol(input: string): {
36
+ form: 'bare';
37
+ name: string;
38
+ } | {
39
+ form: 'qualified';
40
+ container: string;
41
+ name: string;
42
+ } | null;
43
+ /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
44
+ export declare function escapeLike(s: string): string;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QUALIFIED_NAME_RE = exports.BARE_NAME_RE = void 0;
4
+ exports.splitQualified = splitQualified;
5
+ exports.parseFindSymbolName = parseFindSymbolName;
6
+ exports.parseCallerSymbol = parseCallerSymbol;
7
+ exports.escapeLike = escapeLike;
8
+ exports.BARE_NAME_RE = /^#?[\w$]+$/;
9
+ exports.QUALIFIED_NAME_RE = /^#?[\w$]+(\.#?[\w$]+)+$/;
10
+ const PFX_SRC = '(?:#?[\\w$]+(?:\\.#?[\\w$]+)*\\.)?';
11
+ const QUOTED_RE = new RegExp(`^(${PFX_SRC})('[^'\\r\\n]+'|"[^"\\r\\n]+")$`);
12
+ const COMPUTED_RE = new RegExp(`^(${PFX_SRC})(\\[[^\\]\\r\\n]+\\])$`);
13
+ const MAX_QUOTED_COMPUTED_LEN = 256;
14
+ /** Strip a qualified name's trailing `.` container prefix, if present. */
15
+ function stripTrailingDot(prefix) {
16
+ if (!prefix)
17
+ return null;
18
+ return prefix.slice(0, -1);
19
+ }
20
+ /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
21
+ function splitQualified(input) {
22
+ const idx = input.lastIndexOf('.');
23
+ return { container: input.slice(0, idx), name: input.slice(idx + 1) };
24
+ }
25
+ /**
26
+ * Parse a `find_symbol` name into one of the four accepted forms (bare,
27
+ * qualified, quoted, computed), or null when it matches none — the caller
28
+ * should reject with `Invalid symbol name` without querying anything.
29
+ */
30
+ function parseFindSymbolName(input) {
31
+ if (input.length <= MAX_QUOTED_COMPUTED_LEN) {
32
+ const quoted = input.match(QUOTED_RE);
33
+ if (quoted) {
34
+ return { form: 'quoted', container: stripTrailingDot(quoted[1]), name: quoted[2].slice(1, -1) };
35
+ }
36
+ const computed = input.match(COMPUTED_RE);
37
+ if (computed) {
38
+ return { form: 'computed', container: stripTrailingDot(computed[1]), name: computed[2].replace(/\s+/g, '') };
39
+ }
40
+ }
41
+ if (exports.QUALIFIED_NAME_RE.test(input)) {
42
+ const { container, name } = splitQualified(input);
43
+ return { form: 'qualified', container, name };
44
+ }
45
+ if (exports.BARE_NAME_RE.test(input)) {
46
+ return { form: 'bare', name: input };
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Parse a `get_callers` symbol into bare or qualified form only — the quoted
52
+ * and computed forms of `find_symbol` are meaningless here (calls through
53
+ * element access are never recorded in the call graph).
54
+ */
55
+ function parseCallerSymbol(input) {
56
+ if (exports.QUALIFIED_NAME_RE.test(input)) {
57
+ const { container, name } = splitQualified(input);
58
+ return { form: 'qualified', container, name };
59
+ }
60
+ if (exports.BARE_NAME_RE.test(input)) {
61
+ return { form: 'bare', name: input };
62
+ }
63
+ return null;
64
+ }
65
+ /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
66
+ function escapeLike(s) {
67
+ return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { SymbolEntry } from './interfaces/IAstParser';
2
+ export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
3
+ /**
4
+ * Combine top-level symbols and members into the rows written to `code_symbol`.
5
+ * Overload signatures of one declaration — same `(container, name, kind,
6
+ * isStatic)` — collapse to a single representative row: the one with a body
7
+ * (the implementation) when present, otherwise the earliest by line (the first
8
+ * signature in source order). Static and instance members sharing a name are
9
+ * never collapsed into each other (`isStatic` is part of the group key). A
10
+ * final dedup by `(container, name, kind, line)` matches the DB's unique index.
11
+ */
12
+ export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSymbolRows = buildSymbolRows;
4
+ /**
5
+ * Combine top-level symbols and members into the rows written to `code_symbol`.
6
+ * Overload signatures of one declaration — same `(container, name, kind,
7
+ * isStatic)` — collapse to a single representative row: the one with a body
8
+ * (the implementation) when present, otherwise the earliest by line (the first
9
+ * signature in source order). Static and instance members sharing a name are
10
+ * never collapsed into each other (`isStatic` is part of the group key). A
11
+ * final dedup by `(container, name, kind, line)` matches the DB's unique index.
12
+ */
13
+ function buildSymbolRows(symbols, members) {
14
+ const groups = new Map();
15
+ for (const entry of [...symbols, ...members]) {
16
+ const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
17
+ const group = groups.get(key);
18
+ if (group)
19
+ group.push(entry);
20
+ else
21
+ groups.set(key, [entry]);
22
+ }
23
+ const collapsed = [];
24
+ for (const group of groups.values()) {
25
+ if (group.length === 1) {
26
+ collapsed.push(group[0]);
27
+ continue;
28
+ }
29
+ const implementation = group.find(e => e.hasBody);
30
+ collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
31
+ }
32
+ const dedup = new Map();
33
+ for (const entry of collapsed) {
34
+ const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
35
+ dedup.set(key, entry);
36
+ }
37
+ return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
38
+ }
@@ -2,7 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FindSymbolTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
- const VALID_KINDS = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
5
+ const VALID_KINDS = [
6
+ 'function', 'class', 'type', 'interface', 'variable',
7
+ 'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
8
+ 'all',
9
+ ];
6
10
  class FindSymbolTool {
7
11
  searcher;
8
12
  constructor(searcher) {
@@ -11,11 +15,16 @@ class FindSymbolTool {
11
15
  definition() {
12
16
  return {
13
17
  name: 'find_symbol',
14
- description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables, methods) by exact name. Use when you know the symbol name and want its definition location fast.',
18
+ description: 'Find TypeScript/JavaScript symbol definitions by exact name: top-level declarations (functions, classes, types, ' +
19
+ 'interfaces, variables), class/object-literal members (methods, get/set accessors, constructors, function ' +
20
+ 'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
21
+ '(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
22
+ "(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
23
+ 'want its definition location fast.',
15
24
  inputSchema: {
16
25
  type: 'object',
17
26
  properties: {
18
- name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
27
+ name: { type: 'string', description: 'Bare, qualified (Class.method), quoted, or computed symbol name (case-sensitive)' },
19
28
  project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
20
29
  kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
21
30
  },
@@ -28,15 +37,29 @@ class FindSymbolTool {
28
37
  const project = args.project ? String(args.project) : null;
29
38
  const rawKind = args.kind;
30
39
  const kind = VALID_KINDS.includes(rawKind) ? rawKind : 'all';
31
- const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
32
- if (hits.length === 0) {
33
- return (0, format_helpers_1.textResponse)(`No definitions found for symbol: ${name}`);
40
+ let parsed;
41
+ try {
42
+ parsed = await this.searcher.findSymbol(name, project, kind);
43
+ }
44
+ catch (err) {
45
+ return (0, format_helpers_1.errorResponse)(err.message);
34
46
  }
47
+ const { hits, truncated, grepFailed } = parsed;
48
+ const notice = await this.searcher.outdatedNotice(project);
35
49
  const lines = [];
50
+ if (notice)
51
+ lines.push(notice + '\n');
52
+ if (hits.length === 0) {
53
+ const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
54
+ lines.push(`No definitions found for symbol: ${name}${suffix}`);
55
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
56
+ }
36
57
  if (truncated)
37
58
  lines.push(`*(truncated to ${hits.length} results)*\n`);
38
59
  for (const h of hits) {
39
- lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
60
+ const staticTag = h.isStatic ? ' static' : '';
61
+ const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
62
+ lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
40
63
  lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
41
64
  }
42
65
  return (0, format_helpers_1.textResponse)(lines.join('\n'));