codesearch-mcp 0.0.18-alpha → 0.1.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +75 -6
  3. package/dist/app.js +36 -0
  4. package/dist/src/commands/IndexCommand.js +14 -1
  5. package/dist/src/commands/McpCommand.js +6 -2
  6. package/dist/src/config-store.d.ts +49 -11
  7. package/dist/src/config-store.js +79 -19
  8. package/dist/src/config.d.ts +32 -8
  9. package/dist/src/config.js +84 -20
  10. package/dist/src/constants.d.ts +36 -2
  11. package/dist/src/constants.js +58 -7
  12. package/dist/src/container.js +5 -3
  13. package/dist/src/db/index.d.ts +1 -1
  14. package/dist/src/db/index.js +2 -1
  15. package/dist/src/db/schema.d.ts +111 -0
  16. package/dist/src/db/schema.js +15 -1
  17. package/dist/src/help.js +5 -4
  18. package/dist/src/interfaces/IAstParser.d.ts +14 -0
  19. package/dist/src/interfaces/IChunker.d.ts +24 -0
  20. package/dist/src/interfaces/IChunker.js +2 -0
  21. package/dist/src/interfaces/IDatabase.d.ts +14 -0
  22. package/dist/src/interfaces/IEmbedder.d.ts +14 -2
  23. package/dist/src/interfaces/ISearcher.d.ts +1 -0
  24. package/dist/src/interfaces/index.d.ts +2 -1
  25. package/dist/src/owner-path.d.ts +10 -0
  26. package/dist/src/owner-path.js +27 -0
  27. package/dist/src/services/AstChunker.d.ts +31 -0
  28. package/dist/src/services/AstChunker.js +114 -0
  29. package/dist/src/services/AstParserService.d.ts +6 -0
  30. package/dist/src/services/AstParserService.js +31 -2
  31. package/dist/src/services/DatabaseService.d.ts +13 -1
  32. package/dist/src/services/DatabaseService.js +70 -25
  33. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  34. package/dist/src/services/EmbedderFactory.js +29 -0
  35. package/dist/src/services/IndexerService.d.ts +17 -5
  36. package/dist/src/services/IndexerService.js +101 -38
  37. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  38. package/dist/src/services/InfinityEmbedder.js +199 -0
  39. package/dist/src/services/OllamaEmbedder.d.ts +16 -5
  40. package/dist/src/services/OllamaEmbedder.js +84 -36
  41. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  42. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  43. package/dist/src/services/SearcherService.d.ts +7 -1
  44. package/dist/src/services/SearcherService.js +35 -16
  45. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  46. package/dist/src/services/VoyageEmbedder.js +72 -0
  47. package/dist/src/services/WebServerService.d.ts +16 -11
  48. package/dist/src/services/WebServerService.js +44 -24
  49. package/dist/src/services/http-retry.d.ts +19 -0
  50. package/dist/src/services/http-retry.js +54 -0
  51. package/dist/src/services/reranker.d.ts +15 -0
  52. package/dist/src/services/reranker.js +44 -0
  53. package/dist/src/setup.d.ts +6 -0
  54. package/dist/src/setup.js +243 -25
  55. package/dist/src/tools/SearchCodebaseTool.js +1 -1
  56. package/dist/src/tools/format-helpers.js +2 -1
  57. package/dist/src/web/dashboard.js +5 -0
  58. package/docker/docker-compose.infinity-gpu.yml +19 -0
  59. package/docker/docker-compose.yml +25 -0
  60. package/package.json +21 -2
  61. package/dist/src/ast.d.ts +0 -2
  62. package/dist/src/ast.js +0 -110
  63. package/dist/src/commands/SetupCommand.d.ts +0 -7
  64. package/dist/src/commands/SetupCommand.js +0 -14
  65. package/dist/src/db/data-source.d.ts +0 -2
  66. package/dist/src/db/data-source.js +0 -13
  67. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  68. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  69. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  70. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  71. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  72. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  73. package/dist/src/db/init.d.ts +0 -2
  74. package/dist/src/db/init.js +0 -62
  75. package/dist/src/env-loader.d.ts +0 -1
  76. package/dist/src/env-loader.js +0 -41
  77. package/dist/src/indexer.d.ts +0 -17
  78. package/dist/src/indexer.js +0 -242
  79. package/dist/src/mcp.d.ts +0 -1
  80. package/dist/src/mcp.js +0 -403
  81. package/dist/src/ollama.d.ts +0 -2
  82. package/dist/src/ollama.js +0 -6
  83. package/dist/src/searcher.d.ts +0 -74
  84. package/dist/src/searcher.js +0 -446
  85. package/dist/vitest.config.d.ts +0 -2
  86. package/dist/vitest.config.js +0 -9
@@ -1,446 +0,0 @@
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.prepareQuery = prepareQuery;
37
- exports.listProjects = listProjects;
38
- exports.searchChunks = searchChunks;
39
- exports.fetchNeighbors = fetchNeighbors;
40
- exports.getDependencies = getDependencies;
41
- exports.getDependents = getDependents;
42
- exports.grepSymbol = grepSymbol;
43
- exports.findSymbol = findSymbol;
44
- exports.listFiles = listFiles;
45
- exports.readFileAbsolute = readFileAbsolute;
46
- exports.readFileRange = readFileRange;
47
- const fs = __importStar(require("fs"));
48
- const path = __importStar(require("path"));
49
- const readline = __importStar(require("readline"));
50
- const child_process_1 = require("child_process");
51
- const db_1 = require("./db");
52
- const config_1 = require("./config");
53
- const ollama_1 = require("./ollama");
54
- const file_utils_1 = require("./file-utils");
55
- // Detect non-English input by presence of non-ASCII characters (Cyrillic, etc.)
56
- function isNonEnglish(text) {
57
- return /[^\x00-\x7F]/.test(text);
58
- }
59
- // Translate query to English using a local Ollama model
60
- async function translateToEnglish(text) {
61
- const response = await ollama_1.ollama.chat({
62
- model: config_1.config.search.translateModel,
63
- messages: [{ role: 'user', content: `Translate the following text to English. Return only the translation, no explanations:\n${text}` }],
64
- });
65
- return response.message.content.trim();
66
- }
67
- // Translate if AUTO_TRANSLATE is enabled and input is non-English
68
- async function prepareQuery(raw) {
69
- if (config_1.config.search.autoTranslate && isNonEnglish(raw)) {
70
- return { query: await translateToEnglish(raw), translated: true };
71
- }
72
- return { query: raw, translated: false };
73
- }
74
- const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
75
- async function listProjects() {
76
- const rows = await db_1.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
77
- FROM code_chunk
78
- GROUP BY "serviceName"
79
- ORDER BY "serviceName"`);
80
- return rows.map(r => ({
81
- ...r,
82
- ...((0, file_utils_1.readRagMeta)(path.join(config_1.config.workspaceRoot, r.serviceName)) ?? EMPTY_META),
83
- }));
84
- }
85
- // Embed query and run vector similarity search
86
- async function searchChunks(query, limit, project) {
87
- const { embedding } = await ollama_1.ollama.embeddings({ model: config_1.config.ollama.model, prompt: query });
88
- const params = [`[${embedding}]`, config_1.config.search.similarityThreshold, limit];
89
- let projectFilter = '';
90
- if (project) {
91
- params.push(project);
92
- projectFilter = `AND "serviceName" = $${params.length}`;
93
- }
94
- // Cast score to float8 so TypeORM returns a number, not a string
95
- return db_1.db.query(`SELECT "serviceName", "filePath", "startLine", content,
96
- ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
97
- FROM code_chunk
98
- WHERE embedding <=> $1 < $2
99
- ${projectFilter}
100
- ORDER BY embedding <=> $1
101
- LIMIT $3`, params);
102
- }
103
- // Fetch the chunk immediately before and after each result in the same file (Small-to-Big).
104
- // Single query with LATERAL joins — avoids 2N round-trips.
105
- async function fetchNeighbors(results) {
106
- if (results.length === 0)
107
- return [];
108
- const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
109
- const params = results.flatMap(r => [r.filePath, r.startLine]);
110
- const rows = await db_1.db.query(`SELECT r.idx,
111
- prev."serviceName" AS prev_sn, prev."filePath" AS prev_fp, prev."startLine" AS prev_sl, prev.content AS prev_c,
112
- next."serviceName" AS next_sn, next."filePath" AS next_fp, next."startLine" AS next_sl, next.content AS next_c
113
- FROM (VALUES ${values}) r(idx, fp, sl)
114
- LEFT JOIN LATERAL (
115
- SELECT "serviceName", "filePath", "startLine", content
116
- FROM code_chunk WHERE "filePath" = r.fp AND "startLine" < r.sl
117
- ORDER BY "startLine" DESC LIMIT 1
118
- ) prev ON true
119
- LEFT JOIN LATERAL (
120
- SELECT "serviceName", "filePath", "startLine", content
121
- FROM code_chunk WHERE "filePath" = r.fp AND "startLine" > r.sl
122
- ORDER BY "startLine" ASC LIMIT 1
123
- ) next ON true
124
- ORDER BY r.idx`, params);
125
- return rows.map((row, i) => ({
126
- result: results[i],
127
- prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
128
- nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
129
- }));
130
- }
131
- // Return all files imported by filePath (direct or transitive)
132
- async function getDependencies(filePath, project, symbol = null, transitive = false) {
133
- const params = [filePath, project];
134
- let symbolFilter = '';
135
- if (symbol) {
136
- params.push(symbol);
137
- symbolFilter = `AND "toSymbol" = $${params.length}`;
138
- }
139
- const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
140
- FROM code_dependency
141
- WHERE "fromFile" = $1 AND project = $2
142
- ${symbolFilter}
143
- ORDER BY "toFile"`, params);
144
- if (!transitive)
145
- return direct;
146
- // Indirect: depth>1 only, deduplicated by toFile (symbols dropped for clarity)
147
- const indirect = await db_1.db.query(`WITH RECURSIVE deps AS (
148
- SELECT "toFile", 1 AS depth, "toFile" AS via
149
- FROM code_dependency
150
- WHERE "fromFile" = $1 AND project = $2
151
-
152
- UNION
153
-
154
- SELECT d."toFile", deps.depth + 1, deps."toFile" AS via
155
- FROM code_dependency d
156
- JOIN deps ON deps."toFile" = d."fromFile"
157
- WHERE d.project = $2
158
- AND d."toFile" <> $1
159
- AND deps.depth < 20
160
- )
161
- SELECT "toFile", MIN(depth) AS depth,
162
- (array_agg(via ORDER BY depth))[1] AS via,
163
- NULL::text AS "fromFile", NULL::text AS "fromSymbol",
164
- NULL::text AS "toSymbol", NULL::text AS "depType"
165
- FROM deps
166
- WHERE depth > 1
167
- GROUP BY "toFile"
168
- ORDER BY MIN(depth), "toFile"`, [filePath, project]);
169
- return [...direct, ...indirect];
170
- }
171
- // Return all files that import filePath (direct or transitive)
172
- async function getDependents(filePath, project, symbol = null, transitive = false) {
173
- const params = [filePath, project];
174
- let symbolFilter = '';
175
- if (symbol) {
176
- params.push(symbol);
177
- symbolFilter = `AND "toSymbol" = $${params.length}`;
178
- }
179
- const direct = await db_1.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
180
- FROM code_dependency
181
- WHERE "toFile" = $1 AND project = $2
182
- ${symbolFilter}
183
- ORDER BY "fromFile"`, params);
184
- if (!transitive)
185
- return direct;
186
- // Indirect: depth>1 only, deduplicated by fromFile
187
- const indirect = await db_1.db.query(`WITH RECURSIVE rdeps AS (
188
- SELECT "fromFile", 1 AS depth, "fromFile" AS via
189
- FROM code_dependency
190
- WHERE "toFile" = $1 AND project = $2
191
-
192
- UNION
193
-
194
- SELECT d."fromFile", rdeps.depth + 1, rdeps."fromFile" AS via
195
- FROM code_dependency d
196
- JOIN rdeps ON rdeps."fromFile" = d."toFile"
197
- WHERE d.project = $2
198
- AND d."fromFile" <> $1
199
- AND rdeps.depth < 20
200
- )
201
- SELECT "fromFile", MIN(depth) AS depth,
202
- (array_agg(via ORDER BY depth))[1] AS via,
203
- NULL::text AS "toFile", NULL::text AS "fromSymbol",
204
- NULL::text AS "toSymbol", NULL::text AS "depType"
205
- FROM rdeps
206
- WHERE depth > 1
207
- GROUP BY "fromFile"
208
- ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
209
- return [...direct, ...indirect];
210
- }
211
- const GREP_MAX_RESULTS = 200;
212
- // Grep stops after this many raw lines; generous multiple of GREP_MAX_RESULTS to account for
213
- // the declaring file's own matches being excluded from the final count.
214
- const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
215
- const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
216
- // Text search for a symbol across all TS/JS files in the project directory.
217
- // Uses grep -F -w (literal whole-word match) via spawnSync to avoid shell injection.
218
- // Excludes the declaring file itself. Uses IGNORE_DIRS for consistency with the indexer.
219
- function grepSymbol(symbol, project, excludeFile) {
220
- const projectDir = path.join(config_1.config.workspaceRoot, project);
221
- const result = (0, child_process_1.spawnSync)('grep', [
222
- '-rn', '-F', '-w',
223
- '-m', String(GREP_RAW_LIMIT),
224
- '--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
225
- ...GREP_EXCLUDE_DIRS,
226
- symbol,
227
- projectDir,
228
- ], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
229
- if (result.error || !result.stdout) {
230
- if (result.error)
231
- console.error(`[grepSymbol] grep failed: ${result.error.message}`);
232
- return { matches: [], truncated: false };
233
- }
234
- const excludeAbsPath = path.join(config_1.config.workspaceRoot, excludeFile);
235
- const all = result.stdout.split('\n').flatMap(line => {
236
- const match = line.match(/^(.+?):(\d+):/);
237
- if (!match)
238
- return [];
239
- const [, absFile, lineNum] = match;
240
- if (absFile === excludeAbsPath)
241
- return [];
242
- return [{ filePath: path.relative(config_1.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
243
- });
244
- return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
245
- }
246
- const FIND_SYMBOL_LIMIT = 50;
247
- const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
248
- function buildFindSymbolPattern(name, kind) {
249
- const patterns = {
250
- function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${name}[[:space:](<]`,
251
- class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${name}[[:space:]{<]`,
252
- type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
253
- interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
254
- variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
255
- };
256
- if (kind === 'all')
257
- return Object.values(patterns).join('|');
258
- return patterns[kind];
259
- }
260
- function inferKind(line, name) {
261
- const before = line.slice(0, line.indexOf(name));
262
- if (/\bfunction\s+$/.test(before))
263
- return 'function';
264
- if (/\bclass\s+$/.test(before))
265
- return 'class';
266
- if (/\binterface\s+$/.test(before))
267
- return 'interface';
268
- if (/\btype\s+$/.test(before))
269
- return 'type';
270
- return 'variable';
271
- }
272
- function findSymbol(name, project, kind) {
273
- if (!/^[\w$]+$/.test(name))
274
- throw new Error('Invalid symbol name');
275
- const searchDir = project
276
- ? path.join(config_1.config.workspaceRoot, project)
277
- : config_1.config.workspaceRoot;
278
- const pattern = buildFindSymbolPattern(name, kind);
279
- const result = (0, child_process_1.spawnSync)('grep', [
280
- '-rn', '-E',
281
- '-m', String(FIND_SYMBOL_RAW_LIMIT),
282
- '--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
283
- ...GREP_EXCLUDE_DIRS,
284
- pattern,
285
- searchDir,
286
- ], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
287
- if (result.error) {
288
- console.error(`[findSymbol] grep failed: ${result.error.message}`);
289
- return { hits: [], truncated: false };
290
- }
291
- if (!result.stdout)
292
- return { hits: [], truncated: false };
293
- const hits = [];
294
- for (const raw of result.stdout.split('\n')) {
295
- if (hits.length >= FIND_SYMBOL_LIMIT)
296
- break;
297
- // grep -n format: "/abs/path/file.ts:42: export function Foo(...)"
298
- // File paths can contain colons on some systems, so split carefully:
299
- // find first colon after a path separator, use lastIndexOf on the line number part
300
- const m = raw.match(/^(.+?):(\d+):(.*)$/);
301
- if (!m)
302
- continue;
303
- const [, absFile, lineNumStr, matchedLine] = m;
304
- const lineNum = parseInt(lineNumStr, 10);
305
- if (isNaN(lineNum))
306
- continue;
307
- const col = matchedLine.indexOf(name);
308
- if (col === -1)
309
- continue;
310
- hits.push({
311
- filePath: path.relative(config_1.config.workspaceRoot, absFile),
312
- line: lineNum,
313
- column: col + 1,
314
- kind: inferKind(matchedLine, name),
315
- signature: matchedLine.trim(),
316
- });
317
- }
318
- return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
319
- }
320
- // ── list_files ────────────────────────────────────────────────────────────────
321
- function walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
322
- let entries;
323
- try {
324
- entries = fs.readdirSync(absDir, { withFileTypes: true });
325
- }
326
- catch {
327
- return;
328
- }
329
- entries.sort((a, b) => {
330
- const ad = a.isDirectory() ? 0 : 1;
331
- const bd = b.isDirectory() ? 0 : 1;
332
- if (ad !== bd)
333
- return ad - bd;
334
- return a.name.localeCompare(b.name);
335
- });
336
- for (const entry of entries) {
337
- if (file_utils_1.IGNORE_DIRS.has(entry.name))
338
- continue;
339
- const indent = ' '.repeat(currentDepth);
340
- if (entry.isDirectory()) {
341
- lines.push(`${indent}${entry.name}/`);
342
- if (currentDepth < maxDepth) {
343
- walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
344
- }
345
- }
346
- else {
347
- if (pattern && !entry.name.includes(pattern))
348
- continue;
349
- lines.push(`${indent}${entry.name}`);
350
- }
351
- }
352
- }
353
- function listFiles(relativePath, project, depth, pattern) {
354
- const projectRoot = path.join(config_1.config.workspaceRoot, project);
355
- const resolvedTarget = path.resolve(projectRoot, relativePath);
356
- const resolvedProject = path.resolve(projectRoot);
357
- if (resolvedTarget !== resolvedProject && !resolvedTarget.startsWith(resolvedProject + path.sep)) {
358
- return { tree: '', error: 'Path escapes project root' };
359
- }
360
- if (!fs.existsSync(resolvedTarget)) {
361
- return { tree: '', error: `Path not found: ${relativePath}` };
362
- }
363
- if (!fs.statSync(resolvedTarget).isDirectory()) {
364
- return { tree: '', error: `Not a directory: ${relativePath}` };
365
- }
366
- const clampedDepth = Math.min(5, Math.max(1, depth));
367
- const lines = [];
368
- walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
369
- if (lines.length === 0) {
370
- return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
371
- }
372
- return { tree: lines.join('\n') };
373
- }
374
- // ── readFileAbsolute ──────────────────────────────────────────────────────────
375
- // Variant of readFileRange that accepts an absolute path and skips the DB index check.
376
- // Used by the read_file MCP tool, which receives absolute paths from search_codebase.
377
- async function readFileAbsolute(absPath, maxLines) {
378
- const resolvedRoot = path.resolve(config_1.config.workspaceRoot);
379
- const resolved = path.resolve(absPath);
380
- if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
381
- return { ok: false, reason: 'invalid_path' };
382
- }
383
- if (!fs.existsSync(resolved))
384
- return { ok: false, reason: 'not_on_disk' };
385
- const lines = [];
386
- const stream = fs.createReadStream(resolved, { encoding: 'utf-8' });
387
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
388
- let lineNo = 0;
389
- let hasMore = false;
390
- try {
391
- for await (const line of rl) {
392
- lineNo++;
393
- if (lineNo <= maxLines)
394
- lines.push(line);
395
- if (lineNo >= maxLines) {
396
- hasMore = true;
397
- rl.close();
398
- stream.destroy();
399
- break;
400
- }
401
- }
402
- }
403
- catch (err) {
404
- rl.close();
405
- stream.destroy();
406
- throw err;
407
- }
408
- const relPath = path.relative(resolvedRoot, resolved);
409
- return { ok: true, filePath: relPath, startLine: 1, endLine: lineNo, lines, hasMore };
410
- }
411
- // Read a line range from a file that is present in the index
412
- async function readFileRange(filePath, startLine, endLine) {
413
- // Defensive: reject paths that escape the workspace root (e.g. "../../etc/passwd")
414
- const resolvedRoot = path.resolve(config_1.config.workspaceRoot);
415
- const absPath = path.resolve(resolvedRoot, filePath);
416
- if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
417
- return { ok: false, reason: 'invalid_path' };
418
- }
419
- const [{ exists }] = await db_1.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
420
- if (!exists)
421
- return { ok: false, reason: 'not_indexed' };
422
- if (!fs.existsSync(absPath))
423
- return { ok: false, reason: 'not_on_disk' };
424
- // Stream line-by-line — avoids loading entire file into memory.
425
- // Stops reading as soon as we pass endLine.
426
- const lines = [];
427
- const stream = fs.createReadStream(absPath, { encoding: 'utf-8' });
428
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
429
- let lineNo = 0;
430
- let lastRead = 0;
431
- let hasMore = false;
432
- for await (const line of rl) {
433
- lineNo++;
434
- lastRead = lineNo;
435
- if (lineNo >= startLine && lineNo <= endLine)
436
- lines.push(line);
437
- if (lineNo >= endLine) {
438
- hasMore = true;
439
- rl.close();
440
- stream.destroy();
441
- break;
442
- }
443
- }
444
- // If we broke out early, lastRead is endLine. If file was shorter, lastRead is file length.
445
- return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
446
- }
@@ -1,2 +0,0 @@
1
- declare const _default: UserConfig;
2
- export default _default;
@@ -1,9 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const config_1 = require("vitest/config");
4
- exports.default = (0, config_1.defineConfig)({
5
- test: {
6
- environment: 'node',
7
- include: ['tests/**/*.test.ts'],
8
- },
9
- });