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

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 (38) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +59 -36
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +67 -16
  5. package/dist/src/commands/BackfillCommand.js +15 -5
  6. package/dist/src/commands/ConfigCommand.d.ts +12 -0
  7. package/dist/src/commands/ConfigCommand.js +98 -0
  8. package/dist/src/commands/IndexCommand.js +1 -1
  9. package/dist/src/config-store.d.ts +53 -0
  10. package/dist/src/config-store.js +197 -0
  11. package/dist/src/config.d.ts +22 -8
  12. package/dist/src/config.js +103 -58
  13. package/dist/src/container.d.ts +1 -1
  14. package/dist/src/container.js +8 -7
  15. package/dist/src/db/index.d.ts +1 -1
  16. package/dist/src/db/index.js +2 -1
  17. package/dist/src/db/schema.d.ts +160 -0
  18. package/dist/src/db/schema.js +12 -1
  19. package/dist/src/help.js +28 -12
  20. package/dist/src/interfaces/IAstParser.d.ts +14 -1
  21. package/dist/src/interfaces/IEmbedder.d.ts +1 -0
  22. package/dist/src/project-selector.d.ts +14 -5
  23. package/dist/src/project-selector.js +74 -60
  24. package/dist/src/services/AstParserService.d.ts +5 -1
  25. package/dist/src/services/AstParserService.js +107 -16
  26. package/dist/src/services/DatabaseService.js +18 -0
  27. package/dist/src/services/IndexerService.d.ts +7 -0
  28. package/dist/src/services/IndexerService.js +160 -40
  29. package/dist/src/services/LoggerService.d.ts +5 -1
  30. package/dist/src/services/LoggerService.js +8 -5
  31. package/dist/src/services/McpServer.d.ts +1 -0
  32. package/dist/src/services/McpServer.js +72 -4
  33. package/dist/src/services/OllamaEmbedder.d.ts +2 -0
  34. package/dist/src/services/OllamaEmbedder.js +29 -0
  35. package/dist/src/services/SearcherService.d.ts +13 -0
  36. package/dist/src/services/SearcherService.js +118 -22
  37. package/dist/src/setup.js +57 -62
  38. package/package.json +3 -3
@@ -58,12 +58,13 @@ class SearcherService {
58
58
  this.logger = logger;
59
59
  }
60
60
  async prepareQuery(raw) {
61
+ const t0 = Date.now();
61
62
  if (this.config.search.autoTranslate && this.isNonEnglish(raw) && this.embedder.translate) {
62
63
  const query = await this.embedder.translate(raw, this.config.search.translateModel);
63
- this.logger.debug('SearcherService: prepareQuery', { translated: true, raw: raw.slice(0, 120), query: query.slice(0, 120) });
64
+ this.logger.debug('SearcherService: prepareQuery', { translated: true, raw: raw.slice(0, 120), query: query.slice(0, 120), ms: Date.now() - t0 });
64
65
  return { query, translated: true };
65
66
  }
66
- this.logger.debug('SearcherService: prepareQuery', { translated: false, query: raw.slice(0, 120) });
67
+ this.logger.debug('SearcherService: prepareQuery', { translated: false, query: raw.slice(0, 120), ms: Date.now() - t0 });
67
68
  return { query: raw, translated: false };
68
69
  }
69
70
  async listProjects() {
@@ -77,32 +78,51 @@ class SearcherService {
77
78
  });
78
79
  }
79
80
  async searchChunks(query, limit, project, threshold) {
81
+ const t0 = Date.now();
80
82
  const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
83
+ const fetchLimit = this.embedder.rerank
84
+ ? Math.min(limit * this.config.search.rerankerOverfetch, 200)
85
+ : limit;
81
86
  this.logger.debug('SearcherService: searchChunks', {
82
- query: query.slice(0, 120), limit, project, threshold: effectiveThreshold,
87
+ query: query.slice(0, 120), limit, fetchLimit, project, threshold: effectiveThreshold,
83
88
  });
84
89
  const embedding = await this.embedder.embed(query);
85
- const params = [`[${embedding}]`, effectiveThreshold, limit];
90
+ const params = [`[${embedding}]`, effectiveThreshold, fetchLimit];
86
91
  let projectFilter = '';
87
92
  if (project) {
88
93
  params.push(project);
89
94
  projectFilter = `AND "serviceName" = $${params.length}`;
90
95
  }
91
- const results = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
96
+ const candidates = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
92
97
  ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
93
98
  FROM code_chunk
94
99
  WHERE embedding <=> $1 < $2
95
100
  ${projectFilter}
96
101
  ORDER BY embedding <=> $1
97
102
  LIMIT $3`, params);
98
- this.logger.debug('SearcherService: searchChunks results', {
99
- count: results.length,
100
- scores: results.map(r => r.score),
101
- files: results.map(r => r.filePath),
103
+ this.logger.debug('SearcherService: searchChunks candidates', {
104
+ count: candidates.length,
105
+ scores: candidates.map(r => r.score),
106
+ files: candidates.map(r => r.filePath),
107
+ ms: Date.now() - t0,
102
108
  });
103
- return results;
109
+ if (!this.embedder.rerank || candidates.length === 0)
110
+ return candidates;
111
+ try {
112
+ const indices = await this.embedder.rerank(query, candidates.map(r => r.content), limit);
113
+ this.logger.debug('SearcherService: searchChunks rerank done', { indices, ms: Date.now() - t0 });
114
+ return indices.filter(i => i >= 0 && i < candidates.length).map(i => candidates[i]);
115
+ }
116
+ catch (err) {
117
+ this.logger.warn('SearcherService: rerank failed, falling back to vector order', {
118
+ reason: err.message,
119
+ ms: Date.now() - t0,
120
+ });
121
+ return candidates.slice(0, limit);
122
+ }
104
123
  }
105
124
  async fetchNeighbors(results) {
125
+ const t0 = Date.now();
106
126
  if (results.length === 0)
107
127
  return [];
108
128
  this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
@@ -128,13 +148,64 @@ class SearcherService {
128
148
  prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
129
149
  nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
130
150
  }));
131
- this.logger.debug('SearcherService: fetchNeighbors results', {
151
+ this.logger.debug('SearcherService: fetchNeighbors result', {
132
152
  withPrev: neighbors.filter(n => n.prevChunk).length,
133
153
  withNext: neighbors.filter(n => n.nextChunk).length,
154
+ ms: Date.now() - t0,
134
155
  });
135
156
  return neighbors;
136
157
  }
158
+ async fetchGraphContext(results) {
159
+ const t0 = Date.now();
160
+ const seen = new Map();
161
+ for (const r of results) {
162
+ const key = `${r.serviceName}::${r.filePath}`;
163
+ if (!seen.has(key))
164
+ seen.set(key, { filePath: r.filePath, project: r.serviceName });
165
+ }
166
+ const pairs = [...seen.values()];
167
+ this.logger.debug('SearcherService: fetchGraphContext', { files: pairs.length });
168
+ if (pairs.length === 0)
169
+ return new Map();
170
+ const n = pairs.length;
171
+ const firstHalf = pairs.map((_, i) => `($${i * 2 + 1}::text, $${i * 2 + 2}::text)`).join(', ');
172
+ const secondHalf = pairs.map((_, i) => `($${n * 2 + i * 2 + 1}::text, $${n * 2 + i * 2 + 2}::text)`).join(', ');
173
+ const params = [
174
+ ...pairs.flatMap(p => [p.filePath, p.project]),
175
+ ...pairs.flatMap(p => [p.filePath, p.project]),
176
+ ];
177
+ const rows = await this.db.query(`SELECT r.fp AS "filePath", 'import' AS direction, d."toFile" AS related
178
+ FROM (VALUES ${firstHalf}) AS r(fp, proj)
179
+ JOIN code_dependency d ON d."fromFile" = r.fp AND d.project = r.proj
180
+
181
+ UNION ALL
182
+
183
+ SELECT r.fp AS "filePath", 'usedby' AS direction, d."fromFile" AS related
184
+ FROM (VALUES ${secondHalf}) AS r(fp, proj)
185
+ JOIN code_dependency d ON d."toFile" = r.fp AND d.project = r.proj
186
+
187
+ LIMIT 2000`, params);
188
+ const ctx = new Map();
189
+ for (const { filePath } of pairs)
190
+ ctx.set(filePath, { imports: new Set(), usedBy: new Set() });
191
+ for (const row of rows) {
192
+ const g = ctx.get(row.filePath);
193
+ if (row.direction === 'import')
194
+ g.imports.add(row.related);
195
+ else
196
+ g.usedBy.add(row.related);
197
+ }
198
+ this.logger.debug('SearcherService: fetchGraphContext result', { files: pairs.length, rows: rows.length, ms: Date.now() - t0 });
199
+ const result = new Map();
200
+ for (const [fp, g] of ctx)
201
+ result.set(fp, {
202
+ imports: [...g.imports].slice(0, 50),
203
+ usedBy: [...g.usedBy].slice(0, 50),
204
+ });
205
+ return result;
206
+ }
137
207
  async getDependencies(filePath, project, symbol = null, transitive = false) {
208
+ const t0 = Date.now();
138
209
  this.logger.debug('SearcherService: getDependencies', { filePath, project, symbol, transitive });
139
210
  const params = [filePath, project];
140
211
  let symbolFilter = '';
@@ -148,7 +219,7 @@ class SearcherService {
148
219
  ${symbolFilter}
149
220
  ORDER BY "toFile"`, params);
150
221
  if (!transitive) {
151
- this.logger.debug('SearcherService: getDependencies result', { direct: direct.length });
222
+ this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, ms: Date.now() - t0 });
152
223
  return direct;
153
224
  }
154
225
  const indirect = await this.db.query(`WITH RECURSIVE deps AS (
@@ -173,10 +244,11 @@ class SearcherService {
173
244
  WHERE depth > 1
174
245
  GROUP BY "toFile"
175
246
  ORDER BY MIN(depth), "toFile"`, [filePath, project]);
176
- this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length });
247
+ this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
177
248
  return [...direct, ...indirect];
178
249
  }
179
250
  async getDependents(filePath, project, symbol = null, transitive = false) {
251
+ const t0 = Date.now();
180
252
  this.logger.debug('SearcherService: getDependents', { filePath, project, symbol, transitive });
181
253
  const params = [filePath, project];
182
254
  let symbolFilter = '';
@@ -190,7 +262,7 @@ class SearcherService {
190
262
  ${symbolFilter}
191
263
  ORDER BY "fromFile"`, params);
192
264
  if (!transitive) {
193
- this.logger.debug('SearcherService: getDependents result', { direct: direct.length });
265
+ this.logger.debug('SearcherService: getDependents result', { direct: direct.length, ms: Date.now() - t0 });
194
266
  return direct;
195
267
  }
196
268
  const indirect = await this.db.query(`WITH RECURSIVE rdeps AS (
@@ -215,10 +287,29 @@ class SearcherService {
215
287
  WHERE depth > 1
216
288
  GROUP BY "fromFile"
217
289
  ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
218
- this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length });
290
+ this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
219
291
  return [...direct, ...indirect];
220
292
  }
293
+ async getCallers(symbol, project, calleeFile = null) {
294
+ const t0 = Date.now();
295
+ this.logger.debug('SearcherService: getCallers', { symbol, project, calleeFile });
296
+ const params = [symbol, project];
297
+ let fileFilter = '';
298
+ if (calleeFile) {
299
+ params.push(calleeFile);
300
+ fileFilter = `AND "calleeFile" = $${params.length}`;
301
+ }
302
+ const rows = await this.db.query(`SELECT "fromFile", "fromSymbol", "calleeName", "calleeFile", line
303
+ FROM code_call
304
+ WHERE "calleeName" = $1 AND project = $2
305
+ ${fileFilter}
306
+ ORDER BY "fromFile", line
307
+ LIMIT 200`, params);
308
+ this.logger.debug('SearcherService: getCallers result', { count: rows.length, ms: Date.now() - t0 });
309
+ return rows;
310
+ }
221
311
  grepSymbol(symbol, project, excludeFile) {
312
+ const t0 = Date.now();
222
313
  this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
223
314
  const projectDir = path.join(this.config.workspaceRoot, project);
224
315
  const result = (0, child_process_1.spawnSync)('grep', [
@@ -245,7 +336,7 @@ class SearcherService {
245
336
  return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
246
337
  });
247
338
  const truncated = all.length > GREP_MAX_RESULTS;
248
- this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated });
339
+ this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated, ms: Date.now() - t0 });
249
340
  return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
250
341
  }
251
342
  // Sync wrapper: validates name (throws synchronously), then returns Promise.
@@ -258,6 +349,7 @@ class SearcherService {
258
349
  return this.findSymbolFromIndex(name, project, kind);
259
350
  }
260
351
  async findSymbolFromIndex(name, project, kind) {
352
+ const t0 = Date.now();
261
353
  this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'index' });
262
354
  try {
263
355
  const params = [name];
@@ -285,15 +377,16 @@ class SearcherService {
285
377
  kind: row.kind,
286
378
  signature: row.signature ?? row.name,
287
379
  }));
288
- this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated });
380
+ this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated, ms: Date.now() - t0 });
289
381
  return { hits, truncated };
290
382
  }
291
383
  catch {
292
- this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { name, project });
384
+ this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { name, project, ms: Date.now() - t0 });
293
385
  return this.grepFindSymbol(name, project, kind);
294
386
  }
295
387
  }
296
388
  grepFindSymbol(name, project, kind) {
389
+ const t0 = Date.now();
297
390
  this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'grep' });
298
391
  const searchDir = project
299
392
  ? path.join(this.config.workspaceRoot, project)
@@ -335,10 +428,11 @@ class SearcherService {
335
428
  signature: matchedLine.trim(),
336
429
  });
337
430
  }
338
- this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT });
431
+ this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT, ms: Date.now() - t0 });
339
432
  return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
340
433
  }
341
434
  listFiles(relativePath, project, depth, pattern) {
435
+ const t0 = Date.now();
342
436
  this.logger.debug('SearcherService: listFiles', { relativePath, project, depth, pattern });
343
437
  const projectRoot = path.join(this.config.workspaceRoot, project);
344
438
  const resolvedTarget = path.resolve(projectRoot, relativePath);
@@ -355,13 +449,14 @@ class SearcherService {
355
449
  const clampedDepth = Math.min(5, Math.max(1, depth));
356
450
  const lines = [];
357
451
  this.walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
358
- this.logger.debug('SearcherService: listFiles result', { entries: lines.length });
452
+ this.logger.debug('SearcherService: listFiles result', { entries: lines.length, ms: Date.now() - t0 });
359
453
  if (lines.length === 0) {
360
454
  return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
361
455
  }
362
456
  return { tree: lines.join('\n') };
363
457
  }
364
458
  async readFileAbsolute(absPath, maxLines) {
459
+ const t0 = Date.now();
365
460
  this.logger.debug('SearcherService: readFileAbsolute', { absPath, maxLines });
366
461
  const resolvedRoot = path.resolve(this.config.workspaceRoot);
367
462
  const resolved = path.resolve(absPath);
@@ -395,10 +490,11 @@ class SearcherService {
395
490
  throw err;
396
491
  }
397
492
  const relPath = path.relative(resolvedRoot, resolved);
398
- this.logger.debug('SearcherService: readFileAbsolute result', { linesRead: lines.length, hasMore });
493
+ this.logger.debug('SearcherService: readFileAbsolute result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
399
494
  return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
400
495
  }
401
496
  async readFileRange(filePath, startLine, endLine) {
497
+ const t0 = Date.now();
402
498
  this.logger.debug('SearcherService: readFileRange', { filePath, startLine, endLine });
403
499
  const resolvedRoot = path.resolve(this.config.workspaceRoot);
404
500
  const absPath = path.resolve(resolvedRoot, filePath);
@@ -429,7 +525,7 @@ class SearcherService {
429
525
  break;
430
526
  }
431
527
  }
432
- this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore });
528
+ this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
433
529
  return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
434
530
  }
435
531
  isNonEnglish(text) {
package/dist/src/setup.js CHANGED
@@ -40,11 +40,14 @@ const path = __importStar(require("path"));
40
40
  const os = __importStar(require("os"));
41
41
  const child_process_1 = require("child_process");
42
42
  const constants_1 = require("./constants");
43
- const CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
44
- const DEFAULT_LOG_DIR = path.join(CONFIG_DIR, 'logs');
45
- const CONFIG_FILE = path.join(CONFIG_DIR, '.env');
46
- const DOCKER_COMPOSE_SRC = path.join(__dirname, '..', '..', 'docker', 'docker-compose.yml');
47
- const DOCKER_COMPOSE_DST = path.join(CONFIG_DIR, 'docker-compose.yml');
43
+ const config_store_1 = require("./config-store");
44
+ const config_1 = require("./config");
45
+ const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
46
+ const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
47
+ ? path.join(__dirname, '..')
48
+ : path.join(__dirname, '..', '..');
49
+ const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml');
50
+ const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
48
51
  function ask(rl, question) {
49
52
  return new Promise(resolve => rl.question(question, resolve));
50
53
  }
@@ -57,14 +60,6 @@ function expandHome(p) {
57
60
  return path.join(os.homedir(), p.slice(2));
58
61
  return p;
59
62
  }
60
- function writeEnv(vars) {
61
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
62
- const content = Object.entries(vars)
63
- .map(([k, v]) => `${k}=${v}`)
64
- .join('\n') + '\n';
65
- fs.writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
66
- console.log(`\nConfig saved to ${CONFIG_FILE}`);
67
- }
68
63
  async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
69
64
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
70
65
  const deadline = Date.now() + maxWaitSec * 1000;
@@ -129,7 +124,7 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
129
124
  return false;
130
125
  }
131
126
  }
132
- async function setupLocalDocker(ollamaHost, dbPort, dbUser, dbPass, logger, steps) {
127
+ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger, steps) {
133
128
  console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
134
129
  const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
135
130
  let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
@@ -137,57 +132,29 @@ async function setupLocalDocker(ollamaHost, dbPort, dbUser, dbPass, logger, step
137
132
  .replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
138
133
  .replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
139
134
  .replace('"5433:5432"', `"${dbPort}:5432"`);
140
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
135
+ fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
141
136
  fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
142
137
  console.log('\nStarting Docker containers...');
143
138
  const upResult = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d'], { stdio: 'inherit' });
144
139
  if (upResult.status !== 0) {
145
140
  const reason = 'Failed to start Docker containers. Make sure Docker is running.';
146
141
  console.error(reason);
147
- logger?.error('setupLocalDocker: docker startup failed', { reason, steps });
142
+ logger?.error('startDockerContainers: docker startup failed', { reason, steps });
148
143
  process.exit(1);
149
144
  }
150
145
  await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
151
146
  await waitForOllama(ollamaHost, 60, logger, steps);
152
- // Resolve the ollama container ID via compose — compose project name (and thus the
153
- // default container name) depends on the parent directory, so hardcoding it breaks when
154
- // users install via npm -g or move the compose file around.
155
147
  const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
156
148
  .stdout?.trim();
157
149
  if (!ollamaId) {
158
150
  const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
159
151
  console.error(reason);
160
152
  console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
161
- logger?.error('setupLocalDocker: ollama container not found', { reason, steps });
153
+ logger?.error('startDockerContainers: ollama container not found', { reason, steps });
162
154
  process.exit(1);
163
155
  }
164
156
  console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
165
157
  (0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
166
- return { DB_HOST: 'localhost', DB_PORT: dbPort, DB_USER: dbUser, DB_PASS: dbPass };
167
- }
168
- async function setupRemoteDb(rl, logger, steps, logStep) {
169
- const lp = async (label, def) => {
170
- const answer = await prompt(rl, label, def);
171
- logStep?.(label, answer);
172
- return answer;
173
- };
174
- let vars = {};
175
- while (true) {
176
- vars.DB_HOST = await lp('DB host', 'localhost');
177
- vars.DB_PORT = await lp('DB port', '5432');
178
- vars.DB_USER = await lp('DB user', 'postgres');
179
- vars.DB_PASS = await lp('DB password', '');
180
- console.log('\nTesting connection...');
181
- const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres', logger, steps);
182
- if (ok) {
183
- console.log('Connection successful.');
184
- break;
185
- }
186
- const retry = await ask(rl, 'Retry? (y/n): ');
187
- if (retry.trim().toLowerCase() !== 'y')
188
- break;
189
- }
190
- return vars;
191
158
  }
192
159
  async function runSetup(logger) {
193
160
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -210,14 +177,12 @@ async function runSetup(logger) {
210
177
  };
211
178
  console.log('=== codesearch-mcp setup ===');
212
179
  console.log('Press Enter to keep the default value shown in parentheses.\n');
213
- const dbChoice = await loggedAsk('Infrastructure:\n 1. Local Docker (auto-start PostgreSQL + Ollama)\n 2. Remote (Supabase, Railway, self-hosted DB + local Ollama)\n\nEnter choice (1): ');
214
- const useDocker = (dbChoice.trim() || '1') === '1';
215
180
  const workspaceExample = (() => {
216
181
  const candidates = ['Projects', 'projects', 'workspace', 'code', 'Code', 'dev', 'Development'];
217
182
  const found = candidates.find(d => fs.existsSync(path.join(os.homedir(), d)));
218
183
  return path.join(os.homedir(), found ?? 'projects');
219
184
  })();
220
- console.log(`\nWorkspace root — full path to the directory that contains all your projects`);
185
+ console.log(`Workspace root — full path to the directory that contains all your projects`);
221
186
  console.log(` (e.g. ${workspaceExample})`);
222
187
  let workspaceInput;
223
188
  do {
@@ -227,12 +192,39 @@ async function runSetup(logger) {
227
192
  } while (!workspaceInput.trim());
228
193
  const workspaceRoot = expandHome(workspaceInput.trim());
229
194
  const ollamaHost = await loggedPrompt('Ollama API URL', 'http://localhost:11434');
230
- const dbVars = useDocker
231
- ? await setupLocalDocker(ollamaHost, await loggedPrompt('DB port', '5433'), await loggedPrompt('DB user', 'dev'), await loggedPrompt('DB password', 'dev'), logger, steps)
232
- : await setupRemoteDb(rl, logger, steps, logStep);
195
+ console.log('\n--- Database ---');
196
+ const dbHost = await loggedPrompt('DB host', 'localhost');
197
+ const dbPort = await loggedPrompt('DB port', '5433');
198
+ const dbUser = await loggedPrompt('DB user', 'dev');
199
+ const dbPass = await loggedPrompt('DB password', 'dev');
200
+ process.stdout.write('\nTesting connection...');
201
+ const alreadyConnected = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
202
+ if (alreadyConnected)
203
+ console.log(' Database is already reachable.');
204
+ const dockerDefault = alreadyConnected ? 'n' : 'y';
205
+ const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
206
+ const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
207
+ if (startDocker) {
208
+ await startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger, steps);
209
+ }
210
+ else if (!alreadyConnected) {
211
+ while (true) {
212
+ console.log('\nTesting connection...');
213
+ const ok = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
214
+ if (ok) {
215
+ console.log('Connection successful.');
216
+ break;
217
+ }
218
+ const retry = await ask(rl, 'Retry? (y/n): ');
219
+ if (retry.trim().toLowerCase() !== 'y')
220
+ break;
221
+ }
222
+ }
223
+ const dbVars = { host: dbHost, port: parseInt(dbPort, 10), user: dbUser, password: dbPass };
233
224
  console.log('\n--- Logging ---');
234
225
  console.log(' error — failures only (recommended)');
235
226
  console.log(' warn — + recoverable issues');
227
+ console.log(' info — + general progress messages');
236
228
  console.log(' debug — full diagnostic output (verbose)');
237
229
  const validLevels = ['error', 'warn', 'info', 'debug'];
238
230
  let logLevel;
@@ -243,22 +235,25 @@ async function runSetup(logger) {
243
235
  } while (!validLevels.includes(logLevel));
244
236
  const logDirInput = await loggedAsk(`Log dir (leave blank for default: ${DEFAULT_LOG_DIR}, or "false" to disable): `);
245
237
  const logDir = logDirInput.trim();
246
- const envVars = {
247
- ...dbVars,
248
- WORKSPACE_ROOT: workspaceRoot,
249
- OLLAMA_HOST: ollamaHost,
250
- LOG_LEVEL: logLevel,
238
+ const cfg = {
239
+ db: {
240
+ host: dbVars.host,
241
+ port: dbVars.port,
242
+ user: dbVars.user,
243
+ password: dbVars.password,
244
+ },
245
+ ollama: { host: ollamaHost },
246
+ logging: { level: logLevel },
247
+ workspaceRoot,
251
248
  };
252
249
  if (logDir)
253
- envVars['LOG_DIR'] = logDir;
254
- writeEnv(envVars);
250
+ cfg.logging.dir = logDir;
251
+ (0, config_store_1.writeConfigJson)(cfg);
252
+ console.log(`\nConfig saved to ${config_store_1.CONFIG_FILE}`);
255
253
  rl.close();
256
254
  console.log('\nInitializing database schema...');
257
- const dotenv = await Promise.resolve().then(() => __importStar(require('dotenv')));
258
- dotenv.config({ path: CONFIG_FILE, override: true });
259
- const { config } = await Promise.resolve().then(() => __importStar(require('./config')));
260
255
  const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
261
- const dbService = new DatabaseService(config);
256
+ const dbService = new DatabaseService(new config_1.Config());
262
257
  let initError = null;
263
258
  try {
264
259
  await dbService.initDbWithSchema();
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.16-alpha",
3
+ "version": "0.0.17-alpha.1",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
6
  "main": "dist/app.js",
7
7
  "bin": {
8
8
  "rag-index": "dist/app.js",
9
9
  "rag-mcp": "dist/app.js",
10
- "rag-setup": "dist/app.js"
10
+ "rag-setup": "dist/app.js",
11
+ "rag-config": "dist/app.js"
11
12
  },
12
13
  "files": [
13
14
  "dist/",
@@ -33,7 +34,6 @@
33
34
  "@langchain/textsplitters": "^1.0.1",
34
35
  "@modelcontextprotocol/sdk": "^1.0.0",
35
36
  "cli-progress": "^3.12.0",
36
- "dotenv": "^17.4.2",
37
37
  "drizzle-orm": "^0.45.2",
38
38
  "ignore": "^7.0.5",
39
39
  "ollama": "^0.5.0",