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.
- package/CHANGELOG.md +37 -1
- package/README.md +59 -36
- package/dist/app.d.ts +1 -1
- package/dist/app.js +67 -16
- package/dist/src/commands/BackfillCommand.js +15 -5
- package/dist/src/commands/ConfigCommand.d.ts +12 -0
- package/dist/src/commands/ConfigCommand.js +98 -0
- package/dist/src/commands/IndexCommand.js +1 -1
- package/dist/src/config-store.d.ts +53 -0
- package/dist/src/config-store.js +197 -0
- package/dist/src/config.d.ts +22 -8
- package/dist/src/config.js +103 -58
- package/dist/src/container.d.ts +1 -1
- package/dist/src/container.js +8 -7
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +160 -0
- package/dist/src/db/schema.js +12 -1
- package/dist/src/help.js +28 -12
- package/dist/src/interfaces/IAstParser.d.ts +14 -1
- package/dist/src/interfaces/IEmbedder.d.ts +1 -0
- package/dist/src/project-selector.d.ts +14 -5
- package/dist/src/project-selector.js +74 -60
- package/dist/src/services/AstParserService.d.ts +5 -1
- package/dist/src/services/AstParserService.js +107 -16
- package/dist/src/services/DatabaseService.js +18 -0
- package/dist/src/services/IndexerService.d.ts +7 -0
- package/dist/src/services/IndexerService.js +160 -40
- package/dist/src/services/LoggerService.d.ts +5 -1
- package/dist/src/services/LoggerService.js +8 -5
- package/dist/src/services/McpServer.d.ts +1 -0
- package/dist/src/services/McpServer.js +72 -4
- package/dist/src/services/OllamaEmbedder.d.ts +2 -0
- package/dist/src/services/OllamaEmbedder.js +29 -0
- package/dist/src/services/SearcherService.d.ts +13 -0
- package/dist/src/services/SearcherService.js +118 -22
- package/dist/src/setup.js +57 -62
- 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,
|
|
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
|
|
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
|
|
99
|
-
count:
|
|
100
|
-
scores:
|
|
101
|
-
files:
|
|
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
|
-
|
|
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
|
|
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
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
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('
|
|
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('
|
|
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(
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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(
|
|
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.
|
|
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",
|