codesearch-mcp 0.0.9-alpha → 0.0.11-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 (48) hide show
  1. package/README.md +26 -14
  2. package/dist/app.js +24 -143
  3. package/dist/src/commands/BackfillCommand.d.ts +7 -0
  4. package/dist/src/commands/BackfillCommand.js +27 -0
  5. package/dist/src/commands/ICommand.d.ts +3 -0
  6. package/dist/src/commands/ICommand.js +2 -0
  7. package/dist/src/commands/IndexCommand.d.ts +9 -0
  8. package/dist/src/commands/IndexCommand.js +75 -0
  9. package/dist/src/commands/McpCommand.d.ts +7 -0
  10. package/dist/src/commands/McpCommand.js +21 -0
  11. package/dist/src/commands/SetupCommand.d.ts +4 -0
  12. package/dist/src/commands/SetupCommand.js +10 -0
  13. package/dist/src/config.d.ts +1 -0
  14. package/dist/src/config.js +41 -2
  15. package/dist/src/container.d.ts +15 -0
  16. package/dist/src/container.js +21 -0
  17. package/dist/src/db/entities/code-chunk.entity.d.ts +1 -1
  18. package/dist/src/db/entities/code-chunk.entity.js +2 -2
  19. package/dist/src/db/entities/code-symbol.entity.d.ts +10 -0
  20. package/dist/src/db/entities/code-symbol.entity.js +59 -0
  21. package/dist/src/db/index.d.ts +1 -2
  22. package/dist/src/db/index.js +3 -6
  23. package/dist/src/file-utils.d.ts +2 -1
  24. package/dist/src/file-utils.js +10 -5
  25. package/dist/src/graph.js +5 -4
  26. package/dist/src/help.js +1 -1
  27. package/dist/src/interfaces/IAstParser.d.ts +24 -0
  28. package/dist/src/interfaces/IAstParser.js +2 -0
  29. package/dist/src/interfaces/IEmbedder.d.ts +4 -0
  30. package/dist/src/interfaces/IEmbedder.js +2 -0
  31. package/dist/src/interrupt.d.ts +1 -3
  32. package/dist/src/interrupt.js +14 -16
  33. package/dist/src/services/AstParserService.d.ts +14 -0
  34. package/dist/src/services/AstParserService.js +256 -0
  35. package/dist/src/services/DatabaseService.d.ts +14 -0
  36. package/dist/src/services/DatabaseService.js +133 -0
  37. package/dist/src/services/IndexerService.d.ts +39 -0
  38. package/dist/src/services/IndexerService.js +286 -0
  39. package/dist/src/services/LoggerService.d.ts +12 -0
  40. package/dist/src/services/LoggerService.js +73 -0
  41. package/dist/src/services/McpServer.d.ts +13 -0
  42. package/dist/src/services/McpServer.js +411 -0
  43. package/dist/src/services/OllamaEmbedder.d.ts +9 -0
  44. package/dist/src/services/OllamaEmbedder.js +27 -0
  45. package/dist/src/services/SearcherService.d.ts +91 -0
  46. package/dist/src/services/SearcherService.js +453 -0
  47. package/dist/src/setup.js +23 -7
  48. package/package.json +11 -10
@@ -0,0 +1,453 @@
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.SearcherService = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const readline = __importStar(require("readline"));
40
+ const child_process_1 = require("child_process");
41
+ const file_utils_1 = require("../file-utils");
42
+ const LoggerService_1 = require("./LoggerService");
43
+ const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
44
+ const GREP_MAX_RESULTS = 200;
45
+ const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
46
+ const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
47
+ const FIND_SYMBOL_LIMIT = 50;
48
+ const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
49
+ class SearcherService {
50
+ db;
51
+ embedder;
52
+ config;
53
+ logger;
54
+ constructor(db, embedder, config, logger = LoggerService_1.LoggerService.silent()) {
55
+ this.db = db;
56
+ this.embedder = embedder;
57
+ this.config = config;
58
+ this.logger = logger;
59
+ }
60
+ async prepareQuery(raw) {
61
+ if (this.config.search.autoTranslate && this.isNonEnglish(raw) && this.embedder.translate) {
62
+ return { query: await this.embedder.translate(raw, this.config.search.translateModel), translated: true };
63
+ }
64
+ return { query: raw, translated: false };
65
+ }
66
+ async listProjects() {
67
+ const rows = await this.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
68
+ FROM code_chunk
69
+ GROUP BY "serviceName"
70
+ ORDER BY "serviceName"`);
71
+ return rows.map(r => {
72
+ const meta = (0, file_utils_1.readRagMeta)(path.join(this.config.workspaceRoot, r.serviceName));
73
+ return { ...r, ...(typeof meta === 'object' ? meta : EMPTY_META) };
74
+ });
75
+ }
76
+ async searchChunks(query, limit, project) {
77
+ const embedding = await this.embedder.embed(query);
78
+ const params = [`[${embedding}]`, this.config.search.similarityThreshold, limit];
79
+ let projectFilter = '';
80
+ if (project) {
81
+ params.push(project);
82
+ projectFilter = `AND "serviceName" = $${params.length}`;
83
+ }
84
+ return this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
85
+ ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
86
+ FROM code_chunk
87
+ WHERE embedding <=> $1 < $2
88
+ ${projectFilter}
89
+ ORDER BY embedding <=> $1
90
+ LIMIT $3`, params);
91
+ }
92
+ async fetchNeighbors(results) {
93
+ if (results.length === 0)
94
+ return [];
95
+ const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
96
+ const params = results.flatMap(r => [r.filePath, r.startLine]);
97
+ const rows = await this.db.query(`SELECT r.idx,
98
+ prev."serviceName" AS prev_sn, prev."filePath" AS prev_fp, prev."startLine" AS prev_sl, prev.content AS prev_c,
99
+ next."serviceName" AS next_sn, next."filePath" AS next_fp, next."startLine" AS next_sl, next.content AS next_c
100
+ FROM (VALUES ${values}) r(idx, fp, sl)
101
+ LEFT JOIN LATERAL (
102
+ SELECT "serviceName", "filePath", "startLine", content
103
+ FROM code_chunk WHERE "filePath" = r.fp AND "startLine" < r.sl
104
+ ORDER BY "startLine" DESC LIMIT 1
105
+ ) prev ON true
106
+ LEFT JOIN LATERAL (
107
+ SELECT "serviceName", "filePath", "startLine", content
108
+ FROM code_chunk WHERE "filePath" = r.fp AND "startLine" > r.sl
109
+ ORDER BY "startLine" ASC LIMIT 1
110
+ ) next ON true
111
+ ORDER BY r.idx`, params);
112
+ return rows.map((row, i) => ({
113
+ result: results[i],
114
+ prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
115
+ nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
116
+ }));
117
+ }
118
+ async getDependencies(filePath, project, symbol = null, transitive = false) {
119
+ const params = [filePath, project];
120
+ let symbolFilter = '';
121
+ if (symbol) {
122
+ params.push(symbol);
123
+ symbolFilter = `AND "toSymbol" = $${params.length}`;
124
+ }
125
+ const direct = await this.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
126
+ FROM code_dependency
127
+ WHERE "fromFile" = $1 AND project = $2
128
+ ${symbolFilter}
129
+ ORDER BY "toFile"`, params);
130
+ if (!transitive)
131
+ return direct;
132
+ const indirect = await this.db.query(`WITH RECURSIVE deps AS (
133
+ SELECT "toFile", 1 AS depth, "toFile" AS via
134
+ FROM code_dependency
135
+ WHERE "fromFile" = $1 AND project = $2
136
+
137
+ UNION
138
+
139
+ SELECT d."toFile", deps.depth + 1, deps."toFile" AS via
140
+ FROM code_dependency d
141
+ JOIN deps ON deps."toFile" = d."fromFile"
142
+ WHERE d.project = $2
143
+ AND d."toFile" <> $1
144
+ AND deps.depth < 20
145
+ )
146
+ SELECT "toFile", MIN(depth) AS depth,
147
+ (array_agg(via ORDER BY depth))[1] AS via,
148
+ NULL::text AS "fromFile", NULL::text AS "fromSymbol",
149
+ NULL::text AS "toSymbol", NULL::text AS "depType"
150
+ FROM deps
151
+ WHERE depth > 1
152
+ GROUP BY "toFile"
153
+ ORDER BY MIN(depth), "toFile"`, [filePath, project]);
154
+ return [...direct, ...indirect];
155
+ }
156
+ async getDependents(filePath, project, symbol = null, transitive = false) {
157
+ const params = [filePath, project];
158
+ let symbolFilter = '';
159
+ if (symbol) {
160
+ params.push(symbol);
161
+ symbolFilter = `AND "toSymbol" = $${params.length}`;
162
+ }
163
+ const direct = await this.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
164
+ FROM code_dependency
165
+ WHERE "toFile" = $1 AND project = $2
166
+ ${symbolFilter}
167
+ ORDER BY "fromFile"`, params);
168
+ if (!transitive)
169
+ return direct;
170
+ const indirect = await this.db.query(`WITH RECURSIVE rdeps AS (
171
+ SELECT "fromFile", 1 AS depth, "fromFile" AS via
172
+ FROM code_dependency
173
+ WHERE "toFile" = $1 AND project = $2
174
+
175
+ UNION
176
+
177
+ SELECT d."fromFile", rdeps.depth + 1, rdeps."fromFile" AS via
178
+ FROM code_dependency d
179
+ JOIN rdeps ON rdeps."fromFile" = d."toFile"
180
+ WHERE d.project = $2
181
+ AND d."fromFile" <> $1
182
+ AND rdeps.depth < 20
183
+ )
184
+ SELECT "fromFile", MIN(depth) AS depth,
185
+ (array_agg(via ORDER BY depth))[1] AS via,
186
+ NULL::text AS "toFile", NULL::text AS "fromSymbol",
187
+ NULL::text AS "toSymbol", NULL::text AS "depType"
188
+ FROM rdeps
189
+ WHERE depth > 1
190
+ GROUP BY "fromFile"
191
+ ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
192
+ return [...direct, ...indirect];
193
+ }
194
+ grepSymbol(symbol, project, excludeFile) {
195
+ const projectDir = path.join(this.config.workspaceRoot, project);
196
+ const result = (0, child_process_1.spawnSync)('grep', [
197
+ '-rn', '-F', '-w',
198
+ '-m', String(GREP_RAW_LIMIT),
199
+ '--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
200
+ ...GREP_EXCLUDE_DIRS,
201
+ symbol,
202
+ projectDir,
203
+ ], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
204
+ if (result.error || !result.stdout) {
205
+ if (result.error)
206
+ this.logger.error('SearcherService: grep failed', { method: 'grepSymbol', reason: result.error.message });
207
+ return { matches: [], truncated: false };
208
+ }
209
+ const excludeAbsPath = path.join(this.config.workspaceRoot, excludeFile);
210
+ const all = result.stdout.split('\n').flatMap(line => {
211
+ const match = line.match(/^(.+?):(\d+):/);
212
+ if (!match)
213
+ return [];
214
+ const [, absFile, lineNum] = match;
215
+ if (absFile === excludeAbsPath)
216
+ return [];
217
+ return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
218
+ });
219
+ return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
220
+ }
221
+ // Sync wrapper: validates name (throws synchronously), then returns Promise.
222
+ // Falls back to grep when the symbol index (code_symbol table) is unavailable.
223
+ findSymbol(name, project, kind) {
224
+ if (!/^[\w$]+$/.test(name))
225
+ throw new Error('Invalid symbol name');
226
+ return this.findSymbolFromIndex(name, project, kind);
227
+ }
228
+ async findSymbolFromIndex(name, project, kind) {
229
+ try {
230
+ const params = [name];
231
+ let projectFilter = '';
232
+ if (project) {
233
+ params.push(project);
234
+ projectFilter = `AND project = $${params.length}`;
235
+ }
236
+ let kindFilter = '';
237
+ if (kind !== 'all') {
238
+ params.push(kind);
239
+ kindFilter = `AND kind = $${params.length}`;
240
+ }
241
+ params.push(FIND_SYMBOL_LIMIT + 1);
242
+ const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col
243
+ FROM code_symbol
244
+ WHERE name = $1 ${projectFilter} ${kindFilter}
245
+ ORDER BY project, "filePath"
246
+ LIMIT $${params.length}`, params);
247
+ const truncated = rows.length > FIND_SYMBOL_LIMIT;
248
+ const hits = rows.slice(0, FIND_SYMBOL_LIMIT).map(row => ({
249
+ filePath: row.filePath,
250
+ line: row.line ?? 1,
251
+ column: row.col ?? 1,
252
+ kind: row.kind,
253
+ signature: row.signature ?? row.name,
254
+ }));
255
+ return { hits, truncated };
256
+ }
257
+ catch {
258
+ this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { name, project });
259
+ return this.grepFindSymbol(name, project, kind);
260
+ }
261
+ }
262
+ grepFindSymbol(name, project, kind) {
263
+ const searchDir = project
264
+ ? path.join(this.config.workspaceRoot, project)
265
+ : this.config.workspaceRoot;
266
+ const pattern = this.buildFindSymbolPattern(name, kind);
267
+ const result = (0, child_process_1.spawnSync)('grep', [
268
+ '-rn', '-E',
269
+ '-m', String(FIND_SYMBOL_RAW_LIMIT),
270
+ '--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
271
+ ...GREP_EXCLUDE_DIRS,
272
+ pattern,
273
+ searchDir,
274
+ ], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
275
+ if (result.error) {
276
+ this.logger.error('SearcherService: grep failed', { method: 'findSymbol', reason: result.error.message });
277
+ return { hits: [], truncated: false };
278
+ }
279
+ if (!result.stdout)
280
+ return { hits: [], truncated: false };
281
+ const hits = [];
282
+ for (const raw of result.stdout.split('\n')) {
283
+ if (hits.length >= FIND_SYMBOL_LIMIT)
284
+ break;
285
+ const m = raw.match(/^(.+?):(\d+):(.*)$/);
286
+ if (!m)
287
+ continue;
288
+ const [, absFile, lineNumStr, matchedLine] = m;
289
+ const lineNum = parseInt(lineNumStr, 10);
290
+ if (isNaN(lineNum))
291
+ continue;
292
+ const col = matchedLine.indexOf(name);
293
+ if (col === -1)
294
+ continue;
295
+ hits.push({
296
+ filePath: path.relative(this.config.workspaceRoot, absFile),
297
+ line: lineNum,
298
+ column: col + 1,
299
+ kind: this.inferKind(matchedLine, name),
300
+ signature: matchedLine.trim(),
301
+ });
302
+ }
303
+ return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
304
+ }
305
+ listFiles(relativePath, project, depth, pattern) {
306
+ const projectRoot = path.join(this.config.workspaceRoot, project);
307
+ const resolvedTarget = path.resolve(projectRoot, relativePath);
308
+ const resolvedProject = path.resolve(projectRoot);
309
+ if (resolvedTarget !== resolvedProject && !resolvedTarget.startsWith(resolvedProject + path.sep)) {
310
+ return { tree: '', error: 'Path escapes project root' };
311
+ }
312
+ if (!fs.existsSync(resolvedTarget)) {
313
+ return { tree: '', error: `Path not found: ${relativePath}` };
314
+ }
315
+ if (!fs.statSync(resolvedTarget).isDirectory()) {
316
+ return { tree: '', error: `Not a directory: ${relativePath}` };
317
+ }
318
+ const clampedDepth = Math.min(5, Math.max(1, depth));
319
+ const lines = [];
320
+ this.walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
321
+ if (lines.length === 0) {
322
+ return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
323
+ }
324
+ return { tree: lines.join('\n') };
325
+ }
326
+ async readFileAbsolute(absPath, maxLines) {
327
+ const resolvedRoot = path.resolve(this.config.workspaceRoot);
328
+ const resolved = path.resolve(absPath);
329
+ if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
330
+ return { ok: false, reason: 'invalid_path' };
331
+ }
332
+ if (!fs.existsSync(resolved))
333
+ return { ok: false, reason: 'not_on_disk' };
334
+ const lines = [];
335
+ const stream = fs.createReadStream(resolved, { encoding: 'utf-8' });
336
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
337
+ let lineNo = 0;
338
+ let hasMore = false;
339
+ try {
340
+ for await (const line of rl) {
341
+ lineNo++;
342
+ if (lineNo <= maxLines) {
343
+ lines.push(line);
344
+ }
345
+ else {
346
+ hasMore = true;
347
+ rl.close();
348
+ stream.destroy();
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ catch (err) {
354
+ rl.close();
355
+ stream.destroy();
356
+ throw err;
357
+ }
358
+ const relPath = path.relative(resolvedRoot, resolved);
359
+ return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
360
+ }
361
+ async readFileRange(filePath, startLine, endLine) {
362
+ const resolvedRoot = path.resolve(this.config.workspaceRoot);
363
+ const absPath = path.resolve(resolvedRoot, filePath);
364
+ if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
365
+ return { ok: false, reason: 'invalid_path' };
366
+ }
367
+ const [{ exists }] = await this.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
368
+ if (!exists)
369
+ return { ok: false, reason: 'not_indexed' };
370
+ if (!fs.existsSync(absPath))
371
+ return { ok: false, reason: 'not_on_disk' };
372
+ const lines = [];
373
+ const stream = fs.createReadStream(absPath, { encoding: 'utf-8' });
374
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
375
+ let lineNo = 0;
376
+ let lastRead = 0;
377
+ let hasMore = false;
378
+ for await (const line of rl) {
379
+ lineNo++;
380
+ if (lineNo >= startLine && lineNo <= endLine)
381
+ lines.push(line);
382
+ if (lineNo <= endLine)
383
+ lastRead = lineNo;
384
+ if (lineNo > endLine) {
385
+ hasMore = true;
386
+ rl.close();
387
+ stream.destroy();
388
+ break;
389
+ }
390
+ }
391
+ return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
392
+ }
393
+ isNonEnglish(text) {
394
+ return /[^\x00-\x7F]/.test(text);
395
+ }
396
+ walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
397
+ let entries;
398
+ try {
399
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
400
+ }
401
+ catch {
402
+ return;
403
+ }
404
+ entries.sort((a, b) => {
405
+ const ad = a.isDirectory() ? 0 : 1;
406
+ const bd = b.isDirectory() ? 0 : 1;
407
+ if (ad !== bd)
408
+ return ad - bd;
409
+ return a.name.localeCompare(b.name);
410
+ });
411
+ for (const entry of entries) {
412
+ if (file_utils_1.IGNORE_DIRS.has(entry.name))
413
+ continue;
414
+ const indent = ' '.repeat(currentDepth);
415
+ if (entry.isDirectory()) {
416
+ lines.push(`${indent}${entry.name}/`);
417
+ if (currentDepth < maxDepth) {
418
+ this.walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
419
+ }
420
+ }
421
+ else {
422
+ if (pattern && !entry.name.includes(pattern))
423
+ continue;
424
+ lines.push(`${indent}${entry.name}`);
425
+ }
426
+ }
427
+ }
428
+ buildFindSymbolPattern(name, kind) {
429
+ const patterns = {
430
+ function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${name}[[:space:](<]`,
431
+ class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${name}[[:space:]{<]`,
432
+ type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
433
+ interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
434
+ variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
435
+ };
436
+ if (kind === 'all')
437
+ return Object.values(patterns).join('|');
438
+ return patterns[kind];
439
+ }
440
+ inferKind(line, name) {
441
+ const before = line.slice(0, line.indexOf(name));
442
+ if (/\bfunction\s+$/.test(before))
443
+ return 'function';
444
+ if (/\bclass\s+$/.test(before))
445
+ return 'class';
446
+ if (/\binterface\s+$/.test(before))
447
+ return 'interface';
448
+ if (/\btype\s+$/.test(before))
449
+ return 'type';
450
+ return 'variable';
451
+ }
452
+ }
453
+ exports.SearcherService = SearcherService;
package/dist/src/setup.js CHANGED
@@ -132,7 +132,7 @@ async function setupLocalDocker(ollamaHost) {
132
132
  console.error('Failed to start Docker containers. Make sure Docker is running.');
133
133
  process.exit(1);
134
134
  }
135
- await waitForDb('localhost', 5433, 'dev', 'dev', 'rag');
135
+ await waitForDb('localhost', 5433, 'dev', 'dev', 'postgres');
136
136
  await waitForOllama(ollamaHost);
137
137
  // Resolve the ollama container ID via compose — compose project name (and thus the
138
138
  // default container name) depends on the parent directory, so hardcoding it breaks when
@@ -151,7 +151,6 @@ async function setupLocalDocker(ollamaHost) {
151
151
  DB_PORT: '5433',
152
152
  DB_USER: 'dev',
153
153
  DB_PASS: 'dev',
154
- DB_NAME: 'rag',
155
154
  };
156
155
  }
157
156
  async function setupRemoteDb(rl) {
@@ -161,9 +160,8 @@ async function setupRemoteDb(rl) {
161
160
  vars.DB_PORT = await prompt(rl, 'DB port', '5432');
162
161
  vars.DB_USER = await prompt(rl, 'DB user', 'postgres');
163
162
  vars.DB_PASS = await prompt(rl, 'DB password', '');
164
- vars.DB_NAME = await prompt(rl, 'DB name', 'rag');
165
163
  console.log('\nTesting connection...');
166
- const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, vars.DB_NAME);
164
+ const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres');
167
165
  if (ok) {
168
166
  console.log('Connection successful.');
169
167
  break;
@@ -186,20 +184,38 @@ async function runSetup() {
186
184
  const dbVars = useDocker
187
185
  ? await setupLocalDocker(ollamaHost)
188
186
  : await setupRemoteDb(rl);
187
+ console.log('\n--- Logging ---');
188
+ console.log(' error — failures only (recommended)');
189
+ console.log(' warn — + recoverable issues');
190
+ console.log(' debug — full diagnostic output (verbose)');
191
+ const validLevels = ['error', 'warn', 'info', 'debug'];
192
+ let logLevel;
193
+ do {
194
+ logLevel = await prompt(rl, 'Log level (error/warn/info/debug)', 'error');
195
+ if (!validLevels.includes(logLevel))
196
+ console.log(` Invalid level. Choose: ${validLevels.join(', ')}`);
197
+ } while (!validLevels.includes(logLevel));
198
+ const defaultLogFile = path.join(os.homedir(), '.config', 'codesearch-mcp', 'rag.log');
199
+ const logFileInput = await ask(rl, `Log file (${defaultLogFile}, or "false" to disable): `);
200
+ const logFile = logFileInput.trim() || defaultLogFile;
189
201
  writeEnv({
190
202
  ...dbVars,
191
203
  WORKSPACE_ROOT: workspaceRoot,
192
204
  OLLAMA_HOST: ollamaHost,
205
+ LOG_LEVEL: logLevel,
206
+ LOG_FILE: logFile,
193
207
  });
194
208
  rl.close();
195
209
  console.log('\nInitializing database schema...');
196
210
  const dotenv = await Promise.resolve().then(() => __importStar(require('dotenv')));
197
211
  dotenv.config({ path: CONFIG_FILE, override: true });
198
- const { initDbWithSchema } = await Promise.resolve().then(() => __importStar(require('./db')));
199
- await initDbWithSchema();
212
+ const { config } = await Promise.resolve().then(() => __importStar(require('./config')));
213
+ const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
214
+ const dbService = new DatabaseService(config);
215
+ await dbService.initDbWithSchema();
216
+ await dbService.destroy();
200
217
  console.log('\nSetup complete!');
201
218
  console.log('\nNext steps:');
202
219
  console.log(` rag-index ${workspaceRoot}`);
203
220
  console.log(' claude mcp add codesearch-mcp -s user -- rag-mcp');
204
- process.exit(0);
205
221
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.9-alpha",
3
+ "version": "0.0.11-alpha",
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
- "rag-mcp": "dist/app.js",
9
+ "rag-mcp": "dist/app.js",
10
10
  "rag-setup": "dist/app.js"
11
11
  },
12
12
  "files": [
@@ -15,15 +15,15 @@
15
15
  "README.md"
16
16
  ],
17
17
  "scripts": {
18
- "build": "tsc",
19
- "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);\"",
20
- "prepublishOnly": "npm run build && npm run typecheck",
21
- "start:mcp": "ts-node app.ts mcp",
18
+ "build": "tsc",
19
+ "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);\"",
20
+ "prepublishOnly": "npm run build && npm run typecheck",
21
+ "start:mcp": "ts-node app.ts mcp",
22
22
  "index:workspace": "ts-node app.ts index",
23
- "index:reindex": "ts-node app.ts index --reindex",
24
- "typecheck": "tsc --noEmit",
25
- "test": "vitest run",
26
- "test:watch": "vitest"
23
+ "index:reindex": "ts-node app.ts index --reindex",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
27
  },
28
28
  "dependencies": {
29
29
  "@inquirer/prompts": "^8.4.1",
@@ -34,6 +34,7 @@
34
34
  "ignore": "^7.0.5",
35
35
  "ollama": "^0.5.0",
36
36
  "pg": "^8.13.0",
37
+ "pino": "^10.3.1",
37
38
  "reflect-metadata": "^0.2.2",
38
39
  "ts-morph": "^28.0.0",
39
40
  "typeorm": "^0.3.20",