codesearch-mcp 0.0.18-alpha → 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 (86) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +75 -6
  3. package/dist/app.js +36 -0
  4. package/dist/src/commands/IndexCommand.js +14 -1
  5. package/dist/src/commands/McpCommand.js +6 -2
  6. package/dist/src/config-store.d.ts +49 -11
  7. package/dist/src/config-store.js +79 -19
  8. package/dist/src/config.d.ts +32 -8
  9. package/dist/src/config.js +84 -20
  10. package/dist/src/constants.d.ts +36 -2
  11. package/dist/src/constants.js +58 -7
  12. package/dist/src/container.js +5 -3
  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 +111 -0
  16. package/dist/src/db/schema.js +15 -1
  17. package/dist/src/help.js +5 -4
  18. package/dist/src/interfaces/IAstParser.d.ts +14 -0
  19. package/dist/src/interfaces/IChunker.d.ts +24 -0
  20. package/dist/src/interfaces/IChunker.js +2 -0
  21. package/dist/src/interfaces/IDatabase.d.ts +14 -0
  22. package/dist/src/interfaces/IEmbedder.d.ts +14 -2
  23. package/dist/src/interfaces/ISearcher.d.ts +1 -0
  24. package/dist/src/interfaces/index.d.ts +2 -1
  25. package/dist/src/owner-path.d.ts +10 -0
  26. package/dist/src/owner-path.js +27 -0
  27. package/dist/src/services/AstChunker.d.ts +31 -0
  28. package/dist/src/services/AstChunker.js +114 -0
  29. package/dist/src/services/AstParserService.d.ts +6 -0
  30. package/dist/src/services/AstParserService.js +31 -2
  31. package/dist/src/services/DatabaseService.d.ts +13 -1
  32. package/dist/src/services/DatabaseService.js +70 -25
  33. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  34. package/dist/src/services/EmbedderFactory.js +29 -0
  35. package/dist/src/services/IndexerService.d.ts +17 -5
  36. package/dist/src/services/IndexerService.js +101 -38
  37. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  38. package/dist/src/services/InfinityEmbedder.js +199 -0
  39. package/dist/src/services/OllamaEmbedder.d.ts +16 -5
  40. package/dist/src/services/OllamaEmbedder.js +84 -36
  41. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  42. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  43. package/dist/src/services/SearcherService.d.ts +7 -1
  44. package/dist/src/services/SearcherService.js +35 -16
  45. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  46. package/dist/src/services/VoyageEmbedder.js +72 -0
  47. package/dist/src/services/WebServerService.d.ts +16 -11
  48. package/dist/src/services/WebServerService.js +44 -24
  49. package/dist/src/services/http-retry.d.ts +19 -0
  50. package/dist/src/services/http-retry.js +54 -0
  51. package/dist/src/services/reranker.d.ts +15 -0
  52. package/dist/src/services/reranker.js +44 -0
  53. package/dist/src/setup.d.ts +6 -0
  54. package/dist/src/setup.js +243 -25
  55. package/dist/src/tools/SearchCodebaseTool.js +1 -1
  56. package/dist/src/tools/format-helpers.js +2 -1
  57. package/dist/src/web/dashboard.js +5 -0
  58. package/docker/docker-compose.infinity-gpu.yml +19 -0
  59. package/docker/docker-compose.yml +25 -0
  60. package/package.json +21 -2
  61. package/dist/src/ast.d.ts +0 -2
  62. package/dist/src/ast.js +0 -110
  63. package/dist/src/commands/SetupCommand.d.ts +0 -7
  64. package/dist/src/commands/SetupCommand.js +0 -14
  65. package/dist/src/db/data-source.d.ts +0 -2
  66. package/dist/src/db/data-source.js +0 -13
  67. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  68. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  69. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  70. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  71. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  72. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  73. package/dist/src/db/init.d.ts +0 -2
  74. package/dist/src/db/init.js +0 -62
  75. package/dist/src/env-loader.d.ts +0 -1
  76. package/dist/src/env-loader.js +0 -41
  77. package/dist/src/indexer.d.ts +0 -17
  78. package/dist/src/indexer.js +0 -242
  79. package/dist/src/mcp.d.ts +0 -1
  80. package/dist/src/mcp.js +0 -403
  81. package/dist/src/ollama.d.ts +0 -2
  82. package/dist/src/ollama.js +0 -6
  83. package/dist/src/searcher.d.ts +0 -74
  84. package/dist/src/searcher.js +0 -446
  85. package/dist/vitest.config.d.ts +0 -2
  86. package/dist/vitest.config.js +0 -9
package/dist/src/setup.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.dockerPromptDefault = dockerPromptDefault;
36
37
  exports.runSetup = runSetup;
37
38
  const readline = __importStar(require("readline"));
38
39
  const fs = __importStar(require("fs"));
@@ -42,12 +43,16 @@ const child_process_1 = require("child_process");
42
43
  const constants_1 = require("./constants");
43
44
  const config_store_1 = require("./config-store");
44
45
  const config_1 = require("./config");
46
+ const InfinityEmbedder_1 = require("./services/InfinityEmbedder");
45
47
  const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
46
48
  const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
47
49
  ? path.join(__dirname, '..')
48
50
  : path.join(__dirname, '..', '..');
49
51
  const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml');
50
52
  const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
53
+ const DOCKER_GPU_OVERRIDE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.infinity-gpu.yml');
54
+ 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';
51
56
  function ask(rl, question) {
52
57
  return new Promise(resolve => rl.question(question, resolve));
53
58
  }
@@ -60,6 +65,79 @@ function expandHome(p) {
60
65
  return path.join(os.homedir(), p.slice(2));
61
66
  return p;
62
67
  }
68
+ const PROVIDER_DEFAULTS = {
69
+ 'ollama': { baseURL: 'http://localhost:11434', model: constants_1.DEFAULT_EMBED_MODEL, envVar: '' },
70
+ 'openai-compatible': { baseURL: 'https://api.openai.com/v1', model: 'text-embedding-3-small', envVar: 'OPENAI_API_KEY' },
71
+ 'voyage': { baseURL: 'https://api.voyageai.com/v1', model: 'voyage-code-3', envVar: 'VOYAGE_API_KEY' },
72
+ 'infinity': { baseURL: 'http://localhost:7997', model: 'jinaai/jina-embeddings-v2-base-code', envVar: '' },
73
+ };
74
+ /** Providers that run a local inference backend we can probe / provision. */
75
+ function isLocalBackend(p) {
76
+ return p === 'ollama' || p === 'infinity';
77
+ }
78
+ /**
79
+ * Probe whether the embedding backend is already reachable, so the "start
80
+ * Docker?" prompt can default to no (mirrors the DB `checkRemoteDb` flow).
81
+ * Cloud providers have no local backend → always "reachable" (nothing to start).
82
+ */
83
+ async function checkEmbeddingBackend(provider, baseURL) {
84
+ if (!isLocalBackend(provider))
85
+ 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
+ }
104
+ }
105
+ /**
106
+ * Default answer for the "start Docker containers?" prompt: no only when both
107
+ * the database and the embedding backend are already reachable. Pure — exported
108
+ * for unit testing.
109
+ */
110
+ function dockerPromptDefault(dbReachable, embReachable) {
111
+ return (dbReachable && embReachable) ? 'n' : 'y';
112
+ }
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
+ /**
123
+ * Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
124
+ * chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
125
+ */
126
+ function writeEnvKey(name, value) {
127
+ fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
128
+ let existing = '';
129
+ try {
130
+ existing = fs.readFileSync(config_store_1.ENV_FILE, 'utf-8');
131
+ }
132
+ catch { /* absent */ }
133
+ const kept = existing.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith(`${name}=`));
134
+ kept.push(`${name}=${value}`);
135
+ fs.writeFileSync(config_store_1.ENV_FILE, kept.join('\n') + '\n', { mode: 0o600 });
136
+ try {
137
+ fs.chmodSync(config_store_1.ENV_FILE, 0o600);
138
+ }
139
+ catch { /* best effort */ }
140
+ }
63
141
  async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
64
142
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
65
143
  const deadline = Date.now() + maxWaitSec * 1000;
@@ -109,6 +187,31 @@ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
109
187
  logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
110
188
  process.exit(1);
111
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
+ }
112
215
  async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
113
216
  try {
114
217
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
@@ -124,7 +227,8 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
124
227
  return false;
125
228
  }
126
229
  }
127
- async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger, steps) {
230
+ async function startDockerContainers(opts) {
231
+ const { provider, ollamaHost, embBaseURL, dbPort, dbUser, dbPass, embModel, rerankModel, startDb, startBackend, logger, steps } = opts;
128
232
  console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
129
233
  const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
130
234
  let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
@@ -134,33 +238,77 @@ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger,
134
238
  .replace('"5433:5432"', `"${dbPort}:5432"`);
135
239
  fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
136
240
  fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
137
- console.log('\nStarting Docker containers...');
138
- const upResult = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d'], { stdio: 'inherit' });
139
- if (upResult.status !== 0) {
140
- const reason = 'Failed to start Docker containers. Make sure Docker is running.';
241
+ // The GPU override is copied verbatim so the managed Infinity path can layer it.
242
+ try {
243
+ fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
244
+ }
245
+ catch { /* absent → CPU only */ }
246
+ const dockerFail = (reason) => {
141
247
  console.error(reason);
142
248
  logger?.error('startDockerContainers: docker startup failed', { reason, steps: JSON.stringify(steps) });
143
249
  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);
144
258
  }
145
- await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
146
- await waitForOllama(ollamaHost, 60, logger, steps);
147
- const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
148
- .stdout?.trim();
149
- if (!ollamaId) {
150
- const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
151
- console.error(reason);
152
- console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
153
- logger?.error('startDockerContainers: ollama container not found', { reason, steps: JSON.stringify(steps) });
154
- process.exit(1);
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;
266
+ }
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);
281
+ }
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
+ return;
288
+ }
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);
304
+ return;
155
305
  }
156
- console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
157
- (0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
158
306
  }
159
307
  async function runSetup(logger) {
160
308
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
161
309
  const steps = [];
162
310
  function logStep(q, raw) {
163
- const a = /password/i.test(q) ? '***' : raw.trim();
311
+ const a = /password|key|secret|token/i.test(q) ? '***' : raw.trim();
164
312
  logger?.debug('setup: prompt', { q, a });
165
313
  steps.push({ q, a });
166
314
  }
@@ -191,23 +339,78 @@ async function runSetup(logger) {
191
339
  console.log(' Path is required.');
192
340
  } while (!workspaceInput.trim());
193
341
  const workspaceRoot = expandHome(workspaceInput.trim());
194
- const ollamaHost = await loggedPrompt('Ollama API URL', 'http://localhost:11434');
342
+ 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');
346
+ console.log(' voyage — Voyage AI cloud');
347
+ const validProviders = ['ollama', 'infinity', 'openai-compatible', 'voyage'];
348
+ let embProvider;
349
+ 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));
354
+ const pd = PROVIDER_DEFAULTS[embProvider];
355
+ const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
356
+ const embModel = await loggedPrompt('Embedding model', pd.model);
357
+ // Infinity serves a reranker from the same container — offer to provision one.
358
+ let rerankModel = '';
359
+ if (embProvider === 'infinity') {
360
+ rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, DEFAULT_INFINITY_RERANK_MODEL)).trim();
361
+ }
362
+ // API keys go into .env (chmod 600); config.json only stores the env: reference.
363
+ let apiKeyRef = '';
364
+ if (!isLocalBackend(embProvider)) {
365
+ const key = (await loggedAsk(`API key (stored in ${config_store_1.ENV_FILE}, leave blank to set later): `)).trim();
366
+ if (key) {
367
+ writeEnvKey(pd.envVar, key);
368
+ apiKeyRef = `env:${pd.envVar}`;
369
+ console.log(` Saved API key to ${config_store_1.ENV_FILE} as ${pd.envVar} (config references env:${pd.envVar}).`);
370
+ }
371
+ else {
372
+ apiKeyRef = `env:${pd.envVar}`;
373
+ console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
374
+ }
375
+ }
376
+ // Ollama host used by the Docker helper below (only meaningful for ollama).
377
+ const ollamaHost = embProvider === 'ollama' ? embBaseURL : 'http://localhost:11434';
195
378
  console.log('\n--- Database ---');
196
379
  const dbHost = await loggedPrompt('DB host', 'localhost');
197
380
  const dbPort = await loggedPrompt('DB port', '5433');
198
381
  const dbUser = await loggedPrompt('DB user', 'dev');
199
382
  const dbPass = await loggedPrompt('DB password', 'dev');
200
383
  process.stdout.write('\nTesting connection...');
201
- const alreadyConnected = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
202
- if (alreadyConnected)
384
+ const dbReachable = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
385
+ if (dbReachable)
203
386
  console.log(' Database is already reachable.');
204
- const dockerDefault = alreadyConnected ? 'n' : 'y';
387
+ // Symmetric to the DB probe: a reachable embedding backend defaults "start
388
+ // Docker?" to no. Cloud providers have no local backend to start.
389
+ process.stdout.write('Checking embedding backend...');
390
+ const embReachable = await checkEmbeddingBackend(embProvider, embBaseURL);
391
+ if (isLocalBackend(embProvider)) {
392
+ console.log(embReachable ? ` ${embProvider} is already reachable.` : ` ${embProvider} is not reachable — it can be started via Docker.`);
393
+ }
394
+ else {
395
+ console.log(' cloud provider (no local backend to start).');
396
+ }
397
+ const dockerDefault = dockerPromptDefault(dbReachable, embReachable);
205
398
  const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
206
399
  const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
207
400
  if (startDocker) {
208
- await startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger, steps);
401
+ await startDockerContainers({
402
+ provider: embProvider,
403
+ ollamaHost,
404
+ embBaseURL,
405
+ dbPort, dbUser, dbPass,
406
+ embModel,
407
+ rerankModel: rerankModel || undefined,
408
+ startDb: !dbReachable,
409
+ startBackend: isLocalBackend(embProvider) && !embReachable,
410
+ logger, steps,
411
+ });
209
412
  }
210
- else if (!alreadyConnected) {
413
+ else if (!dbReachable) {
211
414
  while (true) {
212
415
  console.log('\nTesting connection...');
213
416
  const ok = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
@@ -242,10 +445,25 @@ async function runSetup(logger) {
242
445
  user: dbVars.user,
243
446
  password: dbVars.password,
244
447
  },
245
- ollama: { host: ollamaHost },
448
+ embedding: {
449
+ provider: embProvider,
450
+ baseURL: embBaseURL,
451
+ model: embModel,
452
+ ...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
453
+ },
246
454
  logging: { level: logLevel },
247
455
  workspaceRoot,
248
456
  };
457
+ // Infinity serves the reranker from the same container — auto-wire reranker.*
458
+ // to that server. `url` derives from provider + baseURL in the Config constructor.
459
+ if (embProvider === 'infinity' && rerankModel) {
460
+ cfg.reranker = {
461
+ enabled: true,
462
+ provider: 'openai-compatible',
463
+ baseURL: embBaseURL,
464
+ model: rerankModel,
465
+ };
466
+ }
249
467
  if (logDir)
250
468
  cfg.logging.dir = logDir;
251
469
  (0, config_store_1.writeConfigJson)(cfg);
@@ -18,7 +18,7 @@ class SearchCodebaseTool {
18
18
  definition() {
19
19
  return {
20
20
  name: 'search_codebase',
21
- description: 'Search local project codebase by semantic similarity',
21
+ description: 'Search local project codebase by semantic similarity. Each result shows File, Project, an optional Symbol breadcrumb (owning declaration, e.g. `IndexerService.ingestFile`), Line, the code, and Relevance.',
22
22
  inputSchema: {
23
23
  type: 'object',
24
24
  properties: {
@@ -13,7 +13,8 @@ function errorResponse(text) {
13
13
  return { isError: true, content: [{ type: 'text', text }] };
14
14
  }
15
15
  function formatChunk(r, label) {
16
- return `${label ? `*(${label})*\n` : ''}**File:** ${r.filePath}\n**Project:** ${r.serviceName}\n**Line:** ${r.startLine}\n\`\`\`\n${r.content}\n\`\`\``;
16
+ const symbolLine = r.ownerPath ? `**Symbol:** ${r.ownerPath}\n` : '';
17
+ return `${label ? `*(${label})*\n` : ''}**File:** ${r.filePath}\n**Project:** ${r.serviceName}\n${symbolLine}**Line:** ${r.startLine}\n\`\`\`\n${r.content}\n\`\`\``;
17
18
  }
18
19
  function formatGraphContext(ctx) {
19
20
  const parts = [];
@@ -176,7 +176,9 @@ function renderDashboardHtml() {
176
176
  const isNum = spec.type === 'number' || spec.type === 'integer';
177
177
  inp.dataset.key = key;
178
178
  inp.dataset.type = isNum ? 'number' : 'string';
179
+ const isEnvRef = typeof value === 'string' && value.startsWith('env:');
179
180
  if (key === 'db.password') inp.type = 'password';
181
+ else if (key.endsWith('.apiKey')) inp.type = isEnvRef ? 'text' : 'password';
180
182
  else if (isNum) {
181
183
  inp.type = 'number';
182
184
  // Only apply small, meaningful bounds (skip zod's safe-integer noise).
@@ -197,6 +199,9 @@ function renderDashboardHtml() {
197
199
  const label = document.createElement('label');
198
200
  let html = key.split('.').pop();
199
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
+ }
200
205
  if (def !== undefined) {
201
206
  html += '<span class="defval' + (isUnset ? ' using' : '') + '">'
202
207
  + (isUnset ? '↪ using default: ' : 'default: ') + fmtDefault(def) + '</span>';
@@ -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:
package/package.json CHANGED
@@ -1,8 +1,26 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.18-alpha",
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",
@@ -19,9 +37,10 @@
19
37
  "CHANGELOG.md"
20
38
  ],
21
39
  "scripts": {
40
+ "prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
22
41
  "build": "tsc",
23
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);\"",
24
- "prepublishOnly": "npm run build && npm run typecheck",
43
+ "prepublishOnly": "npm run build && npm run typecheck && npm test",
25
44
  "start:mcp": "ts-node app.ts mcp",
26
45
  "start:web": "ts-node app.ts web",
27
46
  "index:workspace": "ts-node app.ts index",
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'>[]>;
package/dist/src/ast.js DELETED
@@ -1,110 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.extractImportEdges = extractImportEdges;
37
- const fs = __importStar(require("fs"));
38
- const path = __importStar(require("path"));
39
- const ts_morph_1 = require("ts-morph");
40
- // Per-project ts-morph instances keyed by projectRoot. Each project gets its own instance so that
41
- // projects without a tsconfig (key previously shared as '') don't leak files into each other.
42
- // Loading the tsconfig gives ts-morph the compilerOptions.paths needed to resolve aliases like @core/*.
43
- const morphProjects = new Map();
44
- const FALLBACK_OPTS = { skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false, compilerOptions: { allowJs: true, noEmit: true } };
45
- function getMorphProject(projectRoot) {
46
- if (!morphProjects.has(projectRoot)) {
47
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
48
- const hasTsconfig = fs.existsSync(tsconfigPath);
49
- let project;
50
- try {
51
- const opts = hasTsconfig
52
- ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
53
- : FALLBACK_OPTS;
54
- project = new ts_morph_1.Project(opts);
55
- }
56
- catch {
57
- // tsconfig is invalid — fall back so other files in the project can still be processed
58
- project = new ts_morph_1.Project(FALLBACK_OPTS);
59
- }
60
- morphProjects.set(projectRoot, project);
61
- }
62
- return morphProjects.get(projectRoot);
63
- }
64
- // Extract import edges from a TS/JS file using ts-morph.
65
- // projectRoot must be the project root (where tsconfig.json lives) for alias resolution to work.
66
- // Defaults to the file's directory — callers that need alias resolution must pass the project root explicitly.
67
- // Removes the source file from the morph project after parsing to prevent memory accumulation.
68
- async function extractImportEdges(filePath, serviceName, projectRoot = path.dirname(filePath)) {
69
- // Only process JS/TS files; skip declaration files
70
- if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath))
71
- return [];
72
- const morphProject = getMorphProject(projectRoot);
73
- // Remove stale cached version before re-adding
74
- const existing = morphProject.getSourceFile(filePath);
75
- if (existing)
76
- morphProject.removeSourceFile(existing);
77
- let sourceFile;
78
- try {
79
- sourceFile = morphProject.addSourceFileAtPath(filePath);
80
- }
81
- catch {
82
- return []; // file disappeared between stat and parse
83
- }
84
- const edges = [];
85
- for (const decl of sourceFile.getImportDeclarations()) {
86
- const resolved = decl.getModuleSpecifierSourceFile();
87
- if (!resolved)
88
- continue; // external package or unresolvable
89
- const toFile = resolved.getFilePath();
90
- if (toFile.includes('/node_modules/'))
91
- continue; // skip package type declarations
92
- const namedImports = decl.getNamedImports();
93
- const defaultImport = decl.getDefaultImport();
94
- const namespaceImport = decl.getNamespaceImport();
95
- if (namedImports.length > 0) {
96
- for (const named of namedImports) {
97
- edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: named.getName(), depType: 'import' });
98
- }
99
- }
100
- else if (defaultImport) {
101
- edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import-default' });
102
- }
103
- else if (namespaceImport) {
104
- edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import-namespace' });
105
- }
106
- }
107
- // Remove from memory — prevents accumulation across large workspaces
108
- morphProject.removeSourceFile(sourceFile);
109
- return edges;
110
- }
@@ -1,7 +0,0 @@
1
- import type { Container } from '../container';
2
- import type { ICommand } from './ICommand';
3
- export declare class SetupCommand implements ICommand {
4
- private container;
5
- constructor(container: Container);
6
- execute(): Promise<void>;
7
- }
@@ -1,14 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SetupCommand = void 0;
4
- const setup_1 = require("../setup");
5
- class SetupCommand {
6
- container;
7
- constructor(container) {
8
- this.container = container;
9
- }
10
- async execute() {
11
- await (0, setup_1.runSetup)(this.container.logger);
12
- }
13
- }
14
- exports.SetupCommand = SetupCommand;
@@ -1,2 +0,0 @@
1
- import { DataSource } from 'typeorm';
2
- export declare const db: DataSource;
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.db = void 0;
4
- const typeorm_1 = require("typeorm");
5
- const config_1 = require("../config");
6
- const code_chunk_entity_1 = require("./entities/code-chunk.entity");
7
- const code_dependency_entity_1 = require("./entities/code-dependency.entity");
8
- exports.db = new typeorm_1.DataSource({
9
- type: 'postgres',
10
- ...config_1.config.db,
11
- entities: [code_chunk_entity_1.CodeChunk, code_dependency_entity_1.CodeDependency],
12
- synchronize: false,
13
- });
@@ -1,9 +0,0 @@
1
- export declare class CodeChunk {
2
- id: string;
3
- serviceName: string;
4
- filePath: string;
5
- startLine: number | null;
6
- content: string;
7
- embedding: string;
8
- lastModified: number;
9
- }