codesearch-mcp 0.0.9-alpha → 0.0.10-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.
- package/README.md +26 -14
- package/dist/app.js +24 -143
- package/dist/src/commands/BackfillCommand.d.ts +7 -0
- package/dist/src/commands/BackfillCommand.js +27 -0
- package/dist/src/commands/ICommand.d.ts +3 -0
- package/dist/src/commands/ICommand.js +2 -0
- package/dist/src/commands/IndexCommand.d.ts +9 -0
- package/dist/src/commands/IndexCommand.js +75 -0
- package/dist/src/commands/McpCommand.d.ts +7 -0
- package/dist/src/commands/McpCommand.js +21 -0
- package/dist/src/commands/SetupCommand.d.ts +4 -0
- package/dist/src/commands/SetupCommand.js +10 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +41 -2
- package/dist/src/container.d.ts +15 -0
- package/dist/src/container.js +21 -0
- package/dist/src/db/entities/code-chunk.entity.d.ts +1 -1
- package/dist/src/db/entities/code-chunk.entity.js +2 -2
- package/dist/src/db/entities/code-symbol.entity.d.ts +10 -0
- package/dist/src/db/entities/code-symbol.entity.js +59 -0
- package/dist/src/db/index.d.ts +1 -2
- package/dist/src/db/index.js +3 -6
- package/dist/src/file-utils.d.ts +2 -1
- package/dist/src/file-utils.js +10 -5
- package/dist/src/graph.js +5 -4
- package/dist/src/help.js +1 -1
- package/dist/src/interfaces/IAstParser.d.ts +24 -0
- package/dist/src/interfaces/IAstParser.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +4 -0
- package/dist/src/interfaces/IEmbedder.js +2 -0
- package/dist/src/interrupt.d.ts +1 -3
- package/dist/src/interrupt.js +14 -16
- package/dist/src/services/AstParserService.d.ts +14 -0
- package/dist/src/services/AstParserService.js +256 -0
- package/dist/src/services/DatabaseService.d.ts +14 -0
- package/dist/src/services/DatabaseService.js +133 -0
- package/dist/src/services/IndexerService.d.ts +39 -0
- package/dist/src/services/IndexerService.js +286 -0
- package/dist/src/services/LoggerService.d.ts +12 -0
- package/dist/src/services/LoggerService.js +73 -0
- package/dist/src/services/McpServer.d.ts +13 -0
- package/dist/src/services/McpServer.js +414 -0
- package/dist/src/services/OllamaEmbedder.d.ts +9 -0
- package/dist/src/services/OllamaEmbedder.js +27 -0
- package/dist/src/services/SearcherService.d.ts +91 -0
- package/dist/src/services/SearcherService.js +453 -0
- package/dist/src/setup.js +23 -7
- 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', '
|
|
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,
|
|
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 {
|
|
199
|
-
await
|
|
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.
|
|
3
|
+
"version": "0.0.10-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":
|
|
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":
|
|
19
|
-
"postbuild":
|
|
20
|
-
"prepublishOnly":
|
|
21
|
-
"start: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":
|
|
24
|
-
"typecheck":
|
|
25
|
-
"test":
|
|
26
|
-
"test:watch":
|
|
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",
|