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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
package/dist/src/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;
@@ -79,7 +157,7 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, st
79
157
  }
80
158
  const reason = 'PostgreSQL did not become ready in time.';
81
159
  console.error(`\n${reason}`);
82
- logger?.error('waitForDb: timeout', { reason, steps });
160
+ logger?.error('waitForDb: timeout', { reason, steps: JSON.stringify(steps) });
83
161
  process.exit(1);
84
162
  }
85
163
  async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
@@ -106,9 +184,34 @@ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
106
184
  }
107
185
  const reason = 'Ollama did not become ready in time.';
108
186
  console.error(`\n${reason}`);
109
- logger?.error('waitForOllama: timeout', { reason, steps });
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')));
@@ -120,11 +223,12 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
120
223
  catch (err) {
121
224
  const msg = err instanceof Error ? err.message : String(err);
122
225
  console.error(` Connection failed: ${msg}`);
123
- logger?.error('checkRemoteDb: connection failed', { reason: msg, steps });
226
+ logger?.error('checkRemoteDb: connection failed', { reason: msg, steps: JSON.stringify(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.';
141
- console.error(reason);
142
- logger?.error('startDockerContainers: docker startup failed', { reason, steps });
143
- process.exit(1);
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);
144
244
  }
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:';
245
+ catch { /* absent → CPU only */ }
246
+ const dockerFail = (reason) => {
151
247
  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 });
248
+ logger?.error('startDockerContainers: docker startup failed', { reason, steps: JSON.stringify(steps) });
154
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);
258
+ }
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);
@@ -266,11 +484,11 @@ async function runSetup(logger) {
266
484
  }
267
485
  if (initError) {
268
486
  console.error(`Database schema init failed: ${initError.message}`);
269
- logger?.error('runSetup: database schema init failed', { reason: initError.message, steps });
487
+ logger?.error('runSetup: database schema init failed', { reason: initError.message, steps: JSON.stringify(steps) });
270
488
  process.exit(1);
271
489
  }
272
490
  console.log('\nSetup complete!');
273
491
  console.log('\nNext steps:');
274
- console.log(` rag-index ${workspaceRoot}`);
492
+ console.log(' rag-index');
275
493
  console.log(' claude mcp add codesearch-mcp -s user -- rag-mcp');
276
494
  }
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class FindSymbolTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FindSymbolTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ const VALID_KINDS = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
6
+ class FindSymbolTool {
7
+ searcher;
8
+ constructor(searcher) {
9
+ this.searcher = searcher;
10
+ }
11
+ definition() {
12
+ return {
13
+ 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.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
19
+ project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
20
+ kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
21
+ },
22
+ required: ['name'],
23
+ },
24
+ };
25
+ }
26
+ async handle(args) {
27
+ const name = String(args.name ?? '');
28
+ const project = args.project ? String(args.project) : null;
29
+ const rawKind = args.kind;
30
+ 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}`);
34
+ }
35
+ const lines = [];
36
+ if (truncated)
37
+ lines.push(`*(truncated to ${hits.length} results)*\n`);
38
+ for (const h of hits) {
39
+ lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
40
+ lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
41
+ }
42
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
43
+ }
44
+ }
45
+ exports.FindSymbolTool = FindSymbolTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetCallersTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetCallersTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class GetCallersTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'get_callers',
13
+ description: 'Find all call sites of a function or method by exact name. Uses the call graph index — faster and more precise than grep. Use after find_symbol to trace where a symbol is actually called. Returns empty if rag-index backfill --calls has not been run.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ symbol: { type: 'string', description: 'Exact function or method name (case-sensitive)' },
18
+ project: { type: 'string', description: 'Project name (from list_projects)' },
19
+ callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
20
+ },
21
+ required: ['symbol', 'project'],
22
+ },
23
+ };
24
+ }
25
+ async handle(args) {
26
+ const symbol = String(args.symbol ?? '');
27
+ const project = String(args.project ?? '');
28
+ const calleeFile = args.callee_file ? String(args.callee_file) : null;
29
+ const rows = await this.searcher.getCallers(symbol, project, calleeFile);
30
+ if (rows.length === 0) {
31
+ return (0, format_helpers_1.textResponse)(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index backfill --calls\` if the call graph has not been populated yet.`);
32
+ }
33
+ const lines = [`**Call sites of \`${symbol}\`** (${rows.length} found)\n`];
34
+ for (const r of rows) {
35
+ const loc = r.line != null ? ` line ${r.line}` : '';
36
+ const ctx = r.fromSymbol ? `, in \`${r.fromSymbol}\`` : '';
37
+ lines.push(` → \`${r.fromFile}\`${loc}${ctx}`);
38
+ }
39
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
40
+ }
41
+ }
42
+ exports.GetCallersTool = GetCallersTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetDependenciesTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetDependenciesTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class GetDependenciesTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'get_dependencies',
13
+ description: 'Show what files a given file imports (direct or transitive). Use to understand what a file depends on before modifying it. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
18
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
19
+ symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
20
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
21
+ },
22
+ required: ['file_path', 'project'],
23
+ },
24
+ };
25
+ }
26
+ async handle(args) {
27
+ const filePath = String(args.file_path);
28
+ const project = String(args.project);
29
+ const symbol = args.symbol ? String(args.symbol) : null;
30
+ const transitive = Boolean(args.transitive);
31
+ const rows = await this.searcher.getDependencies(filePath, project, symbol, transitive);
32
+ if (rows.length === 0) {
33
+ return (0, format_helpers_1.textResponse)(`No dependencies found for: ${filePath}`);
34
+ }
35
+ return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependencies)(filePath, rows, transitive));
36
+ }
37
+ }
38
+ exports.GetDependenciesTool = GetDependenciesTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetDependentsTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetDependentsTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ const format_helpers_2 = require("./format-helpers");
6
+ class GetDependentsTool {
7
+ searcher;
8
+ constructor(searcher) {
9
+ this.searcher = searcher;
10
+ }
11
+ definition() {
12
+ return {
13
+ name: 'get_dependents',
14
+ description: 'Show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
19
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
20
+ symbol: { type: 'string', description: 'Exported symbol name. Two behaviors: (1) if graph dependents exist, narrows them to imports of this symbol; (2) if the import graph returns zero results, triggers a whole-word text-search fallback (grep -F -w) across all .ts/.tsx/.js/.jsx files in the project to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
21
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
22
+ },
23
+ required: ['file_path', 'project'],
24
+ },
25
+ };
26
+ }
27
+ async handle(args) {
28
+ const filePath = String(args.file_path);
29
+ const project = String(args.project);
30
+ const symbol = args.symbol ? String(args.symbol) : null;
31
+ const transitive = Boolean(args.transitive);
32
+ const rows = await this.searcher.getDependents(filePath, project, symbol, transitive);
33
+ if (rows.length > 0) {
34
+ return (0, format_helpers_1.textResponse)((0, format_helpers_2.formatDependents)(filePath, rows, transitive));
35
+ }
36
+ const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
37
+ if (!symbol) {
38
+ return (0, format_helpers_1.textResponse)(`${header}\n\nTip: pass \`symbol\` to trigger a text-search fallback for runtime usages (e.g. via singleton or container).`);
39
+ }
40
+ const { matches, truncated } = await this.searcher.grepSymbol(symbol, project, filePath);
41
+ if (matches.length === 0) {
42
+ return (0, format_helpers_1.textResponse)(`${header}\n\n**Fallback \`[text search]\` for \`${symbol}\`:** No results.`);
43
+ }
44
+ const grouped = new Map();
45
+ for (const m of matches) {
46
+ if (!grouped.has(m.filePath))
47
+ grouped.set(m.filePath, []);
48
+ grouped.get(m.filePath).push(m.line);
49
+ }
50
+ const fallbackLines = [`\n**Fallback \`[text search]\` for \`${symbol}\`:**${truncated ? ' *(truncated to 200 results)*' : ''}`];
51
+ for (const [fp, lines] of grouped) {
52
+ fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
53
+ }
54
+ return (0, format_helpers_1.textResponse)(header + fallbackLines.join('\n'));
55
+ }
56
+ }
57
+ exports.GetDependentsTool = GetDependentsTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class ListFilesTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }