codesearch-mcp 0.0.8-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.
Files changed (52) hide show
  1. package/README.md +26 -14
  2. package/dist/app.js +24 -144
  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/indexer.js +1 -0
  28. package/dist/src/interfaces/IAstParser.d.ts +24 -0
  29. package/dist/src/interfaces/IAstParser.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +4 -0
  31. package/dist/src/interfaces/IEmbedder.js +2 -0
  32. package/dist/src/interrupt.d.ts +1 -3
  33. package/dist/src/interrupt.js +14 -16
  34. package/dist/src/mcp.js +101 -9
  35. package/dist/src/searcher.d.ts +18 -0
  36. package/dist/src/searcher.js +171 -1
  37. package/dist/src/services/AstParserService.d.ts +14 -0
  38. package/dist/src/services/AstParserService.js +256 -0
  39. package/dist/src/services/DatabaseService.d.ts +14 -0
  40. package/dist/src/services/DatabaseService.js +133 -0
  41. package/dist/src/services/IndexerService.d.ts +39 -0
  42. package/dist/src/services/IndexerService.js +286 -0
  43. package/dist/src/services/LoggerService.d.ts +12 -0
  44. package/dist/src/services/LoggerService.js +73 -0
  45. package/dist/src/services/McpServer.d.ts +13 -0
  46. package/dist/src/services/McpServer.js +414 -0
  47. package/dist/src/services/OllamaEmbedder.d.ts +9 -0
  48. package/dist/src/services/OllamaEmbedder.js +27 -0
  49. package/dist/src/services/SearcherService.d.ts +91 -0
  50. package/dist/src/services/SearcherService.js +453 -0
  51. package/dist/src/setup.js +23 -7
  52. package/package.json +11 -10
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CodeSymbol = void 0;
13
+ const typeorm_1 = require("typeorm");
14
+ let CodeSymbol = class CodeSymbol {
15
+ id;
16
+ project;
17
+ filePath;
18
+ name;
19
+ kind;
20
+ signature;
21
+ line;
22
+ col;
23
+ };
24
+ exports.CodeSymbol = CodeSymbol;
25
+ __decorate([
26
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
27
+ __metadata("design:type", String)
28
+ ], CodeSymbol.prototype, "id", void 0);
29
+ __decorate([
30
+ (0, typeorm_1.Column)(),
31
+ __metadata("design:type", String)
32
+ ], CodeSymbol.prototype, "project", void 0);
33
+ __decorate([
34
+ (0, typeorm_1.Column)(),
35
+ __metadata("design:type", String)
36
+ ], CodeSymbol.prototype, "filePath", void 0);
37
+ __decorate([
38
+ (0, typeorm_1.Column)(),
39
+ __metadata("design:type", String)
40
+ ], CodeSymbol.prototype, "name", void 0);
41
+ __decorate([
42
+ (0, typeorm_1.Column)(),
43
+ __metadata("design:type", String)
44
+ ], CodeSymbol.prototype, "kind", void 0);
45
+ __decorate([
46
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
47
+ __metadata("design:type", Object)
48
+ ], CodeSymbol.prototype, "signature", void 0);
49
+ __decorate([
50
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
51
+ __metadata("design:type", Object)
52
+ ], CodeSymbol.prototype, "line", void 0);
53
+ __decorate([
54
+ (0, typeorm_1.Column)({ type: 'int', nullable: true }),
55
+ __metadata("design:type", Object)
56
+ ], CodeSymbol.prototype, "col", void 0);
57
+ exports.CodeSymbol = CodeSymbol = __decorate([
58
+ (0, typeorm_1.Entity)()
59
+ ], CodeSymbol);
@@ -1,4 +1,3 @@
1
1
  export { CodeChunk } from './entities/code-chunk.entity';
2
2
  export { CodeDependency } from './entities/code-dependency.entity';
3
- export { db } from './data-source';
4
- export { initDb, initDbWithSchema } from './init';
3
+ export { CodeSymbol } from './entities/code-symbol.entity';
@@ -1,12 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initDbWithSchema = exports.initDb = exports.db = exports.CodeDependency = exports.CodeChunk = void 0;
3
+ exports.CodeSymbol = exports.CodeDependency = exports.CodeChunk = void 0;
4
4
  var code_chunk_entity_1 = require("./entities/code-chunk.entity");
5
5
  Object.defineProperty(exports, "CodeChunk", { enumerable: true, get: function () { return code_chunk_entity_1.CodeChunk; } });
6
6
  var code_dependency_entity_1 = require("./entities/code-dependency.entity");
7
7
  Object.defineProperty(exports, "CodeDependency", { enumerable: true, get: function () { return code_dependency_entity_1.CodeDependency; } });
8
- var data_source_1 = require("./data-source");
9
- Object.defineProperty(exports, "db", { enumerable: true, get: function () { return data_source_1.db; } });
10
- var init_1 = require("./init");
11
- Object.defineProperty(exports, "initDb", { enumerable: true, get: function () { return init_1.initDb; } });
12
- Object.defineProperty(exports, "initDbWithSchema", { enumerable: true, get: function () { return init_1.initDbWithSchema; } });
8
+ var code_symbol_entity_1 = require("./entities/code-symbol.entity");
9
+ Object.defineProperty(exports, "CodeSymbol", { enumerable: true, get: function () { return code_symbol_entity_1.CodeSymbol; } });
@@ -6,7 +6,8 @@ export interface ProjectMeta {
6
6
  notes: string | null;
7
7
  relations: Relations | null;
8
8
  }
9
- export declare function readRagMeta(projectDir: string): ProjectMeta | null;
9
+ export type RagMetaResult = ProjectMeta | 'missing' | 'invalid';
10
+ export declare function readRagMeta(projectDir: string): RagMetaResult;
10
11
  export declare const IGNORE_DIRS: Set<string>;
11
12
  export declare function loadIgnore(projectDir: string): ReturnType<typeof ignore>;
12
13
  export declare function collectFiles(dir: string, ig: ReturnType<typeof ignore>, rootDir: string): string[];
@@ -43,12 +43,10 @@ exports.collectFiles = collectFiles;
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  const ignore_1 = __importDefault(require("ignore"));
46
- // Returns null if file is missing or contains invalid JSON.
47
- // Returns ProjectMeta (fields may be null) if file exists and parses correctly.
48
46
  function readRagMeta(projectDir) {
49
47
  const metaPath = path.join(projectDir, '.ragmeta.json');
50
48
  if (!fs.existsSync(metaPath))
51
- return null;
49
+ return 'missing';
52
50
  try {
53
51
  const raw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
54
52
  return {
@@ -59,7 +57,7 @@ function readRagMeta(projectDir) {
59
57
  };
60
58
  }
61
59
  catch {
62
- return null;
60
+ return 'invalid';
63
61
  }
64
62
  }
65
63
  exports.IGNORE_DIRS = new Set([
@@ -79,7 +77,14 @@ function loadIgnore(projectDir) {
79
77
  }
80
78
  function collectFiles(dir, ig, rootDir) {
81
79
  const result = [];
82
- for (const file of fs.readdirSync(dir)) {
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(dir);
83
+ }
84
+ catch {
85
+ return result;
86
+ }
87
+ for (const file of entries) {
83
88
  const fullPath = path.join(dir, file);
84
89
  const relPath = path.relative(rootDir, fullPath);
85
90
  if (ig.ignores(relPath))
package/dist/src/graph.js CHANGED
@@ -243,10 +243,11 @@ function buildHtml(nodes, edges) {
243
243
  }
244
244
  // Single entry point — reads meta once, prints console summary and writes HTML
245
245
  function generateProjectGraph(projects, workspacePath) {
246
- const nodes = projects.map(p => ({
247
- name: p.name,
248
- ...((0, file_utils_1.readRagMeta)(p.path) ?? { description: null, stack: null, notes: null, relations: null }),
249
- }));
246
+ const EMPTY = { description: null, stack: null, notes: null, relations: null };
247
+ const nodes = projects.map(p => {
248
+ const meta = (0, file_utils_1.readRagMeta)(p.path);
249
+ return { name: p.name, ...(typeof meta === 'object' ? meta : EMPTY) };
250
+ });
250
251
  const edges = buildEdges(nodes);
251
252
  printConsole(nodes, edges, workspacePath);
252
253
  const htmlPath = path.join(workspacePath, 'project-graph.html');
package/dist/src/help.js CHANGED
@@ -19,7 +19,7 @@ function printHelp(command) {
19
19
  console.log(' -h, --help Show this help message');
20
20
  console.log('');
21
21
  console.log('Subcommands:');
22
- console.log(' backfill Populate dependency graph for already-indexed files');
22
+ console.log(' backfill Populate symbol index (code_symbol) for already-indexed files');
23
23
  break;
24
24
  case 'rag-mcp':
25
25
  case 'mcp':
@@ -236,6 +236,7 @@ async function scanAndIndex(dir, serviceName, force = false, signal, preCollecte
236
236
  bar.increment();
237
237
  }
238
238
  bar.stop();
239
+ process.stdout.write('\x1B[J'); // erase any blank lines cli-progress left below the bar
239
240
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
240
241
  return stats;
241
242
  }
@@ -0,0 +1,24 @@
1
+ export interface DepEdge {
2
+ project: string;
3
+ fromFile: string;
4
+ fromSymbol: string | null;
5
+ toFile: string;
6
+ toSymbol: string | null;
7
+ depType: string;
8
+ }
9
+ export interface SymbolEntry {
10
+ project: string;
11
+ filePath: string;
12
+ name: string;
13
+ kind: 'function' | 'class' | 'interface' | 'type' | 'variable';
14
+ signature: string | null;
15
+ line: number | null;
16
+ col: number | null;
17
+ }
18
+ export interface FileAnalysis {
19
+ edges: DepEdge[];
20
+ symbols: SymbolEntry[];
21
+ }
22
+ export interface IAstParser {
23
+ analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export interface IEmbedder {
2
+ embed(text: string): Promise<number[]>;
3
+ translate?(text: string, model: string): Promise<string>;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,3 +1 @@
1
- export declare const signal: AbortSignal;
2
- export declare function setupInterrupt(): void;
3
- export declare function teardownInterrupt(): void;
1
+ export declare function setupInterrupt(controller: AbortController): () => void;
@@ -1,27 +1,25 @@
1
1
  "use strict";
2
- // Graceful interruption signal for long-running CLI operations.
3
- // Listens for ESC or Ctrl+C on stdin and aborts the shared controller.
4
2
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.signal = void 0;
6
3
  exports.setupInterrupt = setupInterrupt;
7
- exports.teardownInterrupt = teardownInterrupt;
8
- const controller = new AbortController();
9
- exports.signal = controller.signal;
10
- function setupInterrupt() {
4
+ // Graceful interruption for long-running CLI operations.
5
+ // Caller creates the AbortController; this function wires stdin and returns a cleanup function.
6
+ function setupInterrupt(controller) {
11
7
  if (!process.stdin.isTTY)
12
- return;
8
+ return () => { };
13
9
  process.stdin.setRawMode(true);
14
10
  process.stdin.resume();
15
11
  process.stdin.setEncoding('utf8');
16
- process.stdin.on('data', (key) => {
12
+ const handler = (key) => {
17
13
  if (key === '\x1b' || key === '\x03')
18
14
  controller.abort();
19
- });
15
+ };
16
+ process.stdin.on('data', handler);
20
17
  console.log('Press ESC or Ctrl+C to interrupt.\n');
21
- }
22
- function teardownInterrupt() {
23
- if (!process.stdin.isTTY)
24
- return;
25
- process.stdin.setRawMode(false);
26
- process.stdin.pause();
18
+ return () => {
19
+ process.stdin.off('data', handler);
20
+ if (process.stdin.isTTY) {
21
+ process.stdin.setRawMode(false);
22
+ process.stdin.pause();
23
+ }
24
+ };
27
25
  }
package/dist/src/mcp.js CHANGED
@@ -64,8 +64,11 @@ You have access to a local semantic code search server. Use these tools to navig
64
64
 
65
65
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
66
66
  2. Use \`search_codebase\` to find relevant code by meaning.
67
- 3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and lines around \`startLine\` to get the full picture.
67
+ 3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and \`start_line\` set to the **Line** number shown in the result. Example: if the result shows **Line:** 150, pass \`start_line: 150\`.
68
68
  4. For exploratory searches where you expect results to span chunk boundaries, pass \`include_neighbors: true\` to automatically include the chunk before and after each match.
69
+ 5. Use \`find_symbol\` when you know the exact name of a function or class and want its definition location immediately — faster than semantic search.
70
+ 6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
71
+ 7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
69
72
 
70
73
  ## When to use read_file_range vs include_neighbors
71
74
 
@@ -84,6 +87,9 @@ Lower score = better match (cosine distance). Results above the configured thres
84
87
 
85
88
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use when you need to understand what a file depends on.
86
89
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes.
90
+ - **find_symbol** — find where a symbol (function, class, type, interface, variable) is defined by exact name. Faster than semantic search when you know the name.
91
+ - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
92
+ - **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
87
93
 
88
94
  Both tools require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain.
89
95
  `.trim();
@@ -153,8 +159,8 @@ async function startMCP() {
153
159
  type: 'object',
154
160
  properties: {
155
161
  file_path: { type: 'string', description: 'File path as returned by search_codebase' },
156
- start_line: { type: 'number', description: 'First line to read (1-based, inclusive). Default: 1' },
157
- end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99' },
162
+ start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
163
+ end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
158
164
  },
159
165
  required: ['file_path'],
160
166
  },
@@ -187,6 +193,45 @@ async function startMCP() {
187
193
  required: ['file_path', 'project'],
188
194
  },
189
195
  },
196
+ {
197
+ name: 'find_symbol',
198
+ description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables) by exact name. Use when you know the symbol name and want its definition location fast.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
203
+ project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
204
+ kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
205
+ },
206
+ required: ['name'],
207
+ },
208
+ },
209
+ {
210
+ name: 'list_files',
211
+ description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
216
+ project: { type: 'string', description: 'Project name (from list_projects)' },
217
+ depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
218
+ pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
219
+ },
220
+ required: ['path', 'project'],
221
+ },
222
+ },
223
+ {
224
+ name: 'read_file',
225
+ description: 'Read a file from the workspace by absolute path (as returned by search_codebase). Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ file_path: { type: 'string', description: 'Absolute path to the file (as returned in filePath by search_codebase)' },
230
+ max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
231
+ },
232
+ required: ['file_path'],
233
+ },
234
+ },
190
235
  ],
191
236
  }));
192
237
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
@@ -254,12 +299,11 @@ async function startMCP() {
254
299
  return { content: [{ type: 'text', text: message }] };
255
300
  }
256
301
  const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
257
- return {
258
- content: [{
259
- type: 'text',
260
- text: `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``,
261
- }],
262
- };
302
+ let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
303
+ if (range.hasMore) {
304
+ text += `\n\n[file has more lines — call read_file_range again with start_line: ${range.endLine + 1}]`;
305
+ }
306
+ return { content: [{ type: 'text', text }] };
263
307
  }
264
308
  if (req.params.name === 'get_dependencies') {
265
309
  const filePath = String(req.params.arguments?.file_path);
@@ -301,6 +345,54 @@ async function startMCP() {
301
345
  }
302
346
  return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
303
347
  }
348
+ if (req.params.name === 'find_symbol') {
349
+ const name = String(req.params.arguments?.name ?? '');
350
+ const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
351
+ const rawKind = req.params.arguments?.kind;
352
+ const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
353
+ const kind = validKinds.includes(rawKind) ? rawKind : 'all';
354
+ const { hits, truncated } = (0, searcher_1.findSymbol)(name, project, kind);
355
+ if (hits.length === 0) {
356
+ return { content: [{ type: 'text', text: `No definitions found for symbol: ${name}` }] };
357
+ }
358
+ const lines = [];
359
+ if (truncated)
360
+ lines.push(`*(truncated to ${hits.length} results)*\n`);
361
+ for (const h of hits) {
362
+ lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
363
+ lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
364
+ }
365
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
366
+ }
367
+ if (req.params.name === 'list_files') {
368
+ const relPath = String(req.params.arguments?.path ?? '.');
369
+ const project = String(req.params.arguments?.project ?? '');
370
+ const rawDepth = Number(req.params.arguments?.depth);
371
+ const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
372
+ const pattern = req.params.arguments?.pattern ? String(req.params.arguments.pattern) : null;
373
+ const { tree, error } = (0, searcher_1.listFiles)(relPath, project, depth, pattern);
374
+ if (error) {
375
+ return { content: [{ type: 'text', text: `list_files error: ${error}` }] };
376
+ }
377
+ return { content: [{ type: 'text', text: `**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\`` }] };
378
+ }
379
+ if (req.params.name === 'read_file') {
380
+ const filePath = String(req.params.arguments?.file_path ?? '');
381
+ const rawMax = Number(req.params.arguments?.max_lines);
382
+ const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
383
+ const result = await (0, searcher_1.readFileAbsolute)(filePath, maxLines);
384
+ if (!result.ok) {
385
+ const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
386
+ `File not found on disk: ${filePath}`;
387
+ return { content: [{ type: 'text', text: message }] };
388
+ }
389
+ const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
390
+ let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
391
+ if (result.hasMore) {
392
+ text += `\n\n[truncated: file has more lines — use read_file_range with file_path: "${result.filePath}", start_line: ${result.endLine + 1} to continue]`;
393
+ }
394
+ return { content: [{ type: 'text', text }] };
395
+ }
304
396
  throw new Error(`Unknown tool: ${req.params.name}`);
305
397
  }
306
398
  catch (err) {
@@ -19,6 +19,7 @@ export type FileRangeResult = {
19
19
  startLine: number;
20
20
  endLine: number;
21
21
  lines: string[];
22
+ hasMore: boolean;
22
23
  } | {
23
24
  ok: false;
24
25
  reason: 'not_indexed' | 'not_on_disk' | 'invalid_path';
@@ -53,4 +54,21 @@ export declare function grepSymbol(symbol: string, project: string, excludeFile:
53
54
  matches: GrepMatch[];
54
55
  truncated: boolean;
55
56
  };
57
+ export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
58
+ export interface SymbolHit {
59
+ filePath: string;
60
+ line: number;
61
+ column: number;
62
+ kind: Exclude<SymbolKind, 'all'>;
63
+ signature: string;
64
+ }
65
+ export declare function findSymbol(name: string, project: string | null, kind: SymbolKind): {
66
+ hits: SymbolHit[];
67
+ truncated: boolean;
68
+ };
69
+ export declare function listFiles(relativePath: string, project: string, depth: number, pattern: string | null): {
70
+ tree: string;
71
+ error?: string;
72
+ };
73
+ export declare function readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
56
74
  export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
@@ -40,6 +40,9 @@ exports.fetchNeighbors = fetchNeighbors;
40
40
  exports.getDependencies = getDependencies;
41
41
  exports.getDependents = getDependents;
42
42
  exports.grepSymbol = grepSymbol;
43
+ exports.findSymbol = findSymbol;
44
+ exports.listFiles = listFiles;
45
+ exports.readFileAbsolute = readFileAbsolute;
43
46
  exports.readFileRange = readFileRange;
44
47
  const fs = __importStar(require("fs"));
45
48
  const path = __importStar(require("path"));
@@ -240,6 +243,171 @@ function grepSymbol(symbol, project, excludeFile) {
240
243
  });
241
244
  return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
242
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
+ }
243
411
  // Read a line range from a file that is present in the index
244
412
  async function readFileRange(filePath, startLine, endLine) {
245
413
  // Defensive: reject paths that escape the workspace root (e.g. "../../etc/passwd")
@@ -260,17 +428,19 @@ async function readFileRange(filePath, startLine, endLine) {
260
428
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
261
429
  let lineNo = 0;
262
430
  let lastRead = 0;
431
+ let hasMore = false;
263
432
  for await (const line of rl) {
264
433
  lineNo++;
265
434
  lastRead = lineNo;
266
435
  if (lineNo >= startLine && lineNo <= endLine)
267
436
  lines.push(line);
268
437
  if (lineNo >= endLine) {
438
+ hasMore = true;
269
439
  rl.close();
270
440
  stream.destroy();
271
441
  break;
272
442
  }
273
443
  }
274
444
  // If we broke out early, lastRead is endLine. If file was shorter, lastRead is file length.
275
- return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines };
445
+ return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
276
446
  }