codesearch-mcp 0.2.2-alpha → 0.2.3-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 (62) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +15 -8
  3. package/dist/app.js +45 -7
  4. package/dist/src/commands/EvalCommand.d.ts +10 -0
  5. package/dist/src/commands/EvalCommand.js +231 -0
  6. package/dist/src/commands/IndexCommand.js +79 -53
  7. package/dist/src/db/index.d.ts +1 -1
  8. package/dist/src/db/index.js +1 -2
  9. package/dist/src/db/schema.d.ts +5 -29
  10. package/dist/src/db/schema.js +2 -8
  11. package/dist/src/dependency-traversal.d.ts +16 -0
  12. package/dist/src/dependency-traversal.js +45 -0
  13. package/dist/src/enriched-prompt.d.ts +3 -2
  14. package/dist/src/enriched-prompt.js +3 -2
  15. package/dist/src/eval-results.d.ts +12 -0
  16. package/dist/src/eval-results.js +114 -0
  17. package/dist/src/file-utils.d.ts +15 -0
  18. package/dist/src/file-utils.js +24 -1
  19. package/dist/src/help.js +4 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +13 -1
  21. package/dist/src/interfaces/IDatabase.d.ts +0 -14
  22. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
  23. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  24. package/dist/src/interfaces/ISearcher.d.ts +11 -16
  25. package/dist/src/interfaces/index.d.ts +1 -0
  26. package/dist/src/project-scope.d.ts +19 -0
  27. package/dist/src/project-scope.js +49 -0
  28. package/dist/src/project-selector.d.ts +6 -0
  29. package/dist/src/project-selector.js +18 -0
  30. package/dist/src/retrieval-eval.d.ts +16 -0
  31. package/dist/src/retrieval-eval.js +214 -0
  32. package/dist/src/services/AstParserService.d.ts +66 -1
  33. package/dist/src/services/AstParserService.js +430 -25
  34. package/dist/src/services/DatabaseService.d.ts +0 -12
  35. package/dist/src/services/DatabaseService.js +2 -52
  36. package/dist/src/services/IndexerService.d.ts +8 -0
  37. package/dist/src/services/IndexerService.js +24 -11
  38. package/dist/src/services/McpServer.d.ts +7 -0
  39. package/dist/src/services/McpServer.js +42 -38
  40. package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
  41. package/dist/src/services/SearcherService.d.ts +189 -25
  42. package/dist/src/services/SearcherService.js +505 -203
  43. package/dist/src/symbol-rows.d.ts +14 -9
  44. package/dist/src/symbol-rows.js +17 -11
  45. package/dist/src/tools/FindSymbolTool.js +14 -5
  46. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  47. package/dist/src/tools/GetCallersTool.js +16 -6
  48. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  49. package/dist/src/tools/GetDependenciesTool.js +15 -8
  50. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  51. package/dist/src/tools/GetDependentsTool.js +31 -16
  52. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  53. package/dist/src/tools/ListFilesTool.js +14 -12
  54. package/dist/src/tools/ListProjectsTool.js +5 -1
  55. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  56. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  57. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  58. package/dist/src/tools/ReadFileTool.js +12 -10
  59. package/dist/src/tools/SearchCodebaseTool.js +3 -1
  60. package/dist/src/tools/format-helpers.d.ts +16 -1
  61. package/dist/src/tools/format-helpers.js +64 -0
  62. package/package.json +9 -8
@@ -3,6 +3,7 @@ import type { IEmbedder } from '../interfaces/IEmbedder';
3
3
  import type { IAstParser } from '../interfaces/IAstParser';
4
4
  import type { IChunker } from '../interfaces/IChunker';
5
5
  import type { IDatabase } from '../interfaces/IDatabase';
6
+ import type { DiscoveredProject } from '../interfaces/IProjectDiscovery';
6
7
  import type { ILogger } from '../interfaces/ILogger';
7
8
  import type { IndexMeta } from '../interfaces/IDatabase';
8
9
  export interface IndexStats {
@@ -39,6 +40,13 @@ export declare class IndexerService {
39
40
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
40
41
  rootTsConfigPath?: string;
41
42
  }): Promise<IndexStats>;
43
+ /**
44
+ * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
45
+ * (a unit's children may belong to different monorepos). No pruning, no
46
+ * console output — callers (CLI or a future hook) own reporting. A rejecting
47
+ * `scanAndIndex` call stops the loop before the next target and propagates.
48
+ */
49
+ scanProjects(targets: DiscoveredProject[], force: boolean, signal: AbortSignal, filesPerProject: string[][]): Promise<IndexStats[]>;
42
50
  pruneProjects(keep: string[]): Promise<{
43
51
  removed: string[];
44
52
  failed: string[];
@@ -254,12 +254,6 @@ class IndexerService {
254
254
  }
255
255
  }
256
256
  this.ast.clearCache(dir);
257
- // The marker is written exactly when the scan reaches here without having
258
- // been interrupted — never on the early-return abort path above, and never
259
- // when anything in this method throws (the error propagates instead).
260
- if (!signal?.aborted) {
261
- await this.db.upsertSymbolIndexVersion(serviceName);
262
- }
263
257
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
264
258
  this.logger.debug('IndexerService: project indexed', {
265
259
  project: serviceName,
@@ -271,6 +265,22 @@ class IndexerService {
271
265
  });
272
266
  return stats;
273
267
  }
268
+ /**
269
+ * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
270
+ * (a unit's children may belong to different monorepos). No pruning, no
271
+ * console output — callers (CLI or a future hook) own reporting. A rejecting
272
+ * `scanAndIndex` call stops the loop before the next target and propagates.
273
+ */
274
+ async scanProjects(targets, force, signal, filesPerProject) {
275
+ const allStats = [];
276
+ for (let i = 0; i < targets.length; i++) {
277
+ if (signal.aborted)
278
+ break;
279
+ const stats = await this.scanAndIndex(targets[i].path, targets[i].name, force, signal, filesPerProject[i], { rootTsConfigPath: targets[i].rootTsConfigPath ?? undefined });
280
+ allStats.push(stats);
281
+ }
282
+ return allStats;
283
+ }
274
284
  async pruneProjects(keep) {
275
285
  if (keep.length === 0)
276
286
  return { removed: [], failed: [] };
@@ -315,7 +325,7 @@ class IndexerService {
315
325
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
316
326
  }
317
327
  const tAst = Date.now();
318
- const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
328
+ const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], graphqlNames: rawGraphqlNames = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
319
329
  // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
320
330
  // Normalize edges to workspace-relative paths
321
331
  const edges = rawEdges
@@ -338,9 +348,12 @@ class IndexerService {
338
348
  line: c.line,
339
349
  col: c.col,
340
350
  }));
341
- // symbols ∪ members → code_symbol rows: overload sets collapse to their
342
- // representative row, then a final dedup by (container, name, kind, line).
343
- const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers).map(s => ({ ...s, filePath: relFilePath }));
351
+ // symbols ∪ members ∪ graphqlNames → code_symbol rows: overload sets
352
+ // collapse to their representative row (graphqlNames bypass the
353
+ // collapse), then a final dedup by (container, name, kind, line).
354
+ const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers, rawGraphqlNames).map(s => ({ ...s, filePath: relFilePath }));
355
+ // Side-effect imports bind no name, so they stay out of the Dependencies: line.
356
+ const promptEdges = edges.filter(e => e.depType !== 'import-side-effect');
344
357
  const astMs = Date.now() - tAst;
345
358
  const chunks = await this.chunker.chunk(code, {
346
359
  strategy: this.config.indexer.chunkStrategy,
@@ -357,7 +370,7 @@ class IndexerService {
357
370
  const embedBatchSize = this.config.embedding.batchSize;
358
371
  for (let i = 0; i < chunks.length; i += embedBatchSize) {
359
372
  const batch = chunks.slice(i, i + embedBatchSize);
360
- const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, edges, rawSymbols));
373
+ const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, promptEdges, rawSymbols));
361
374
  const vectors = await this.embedder.embedBatch(prompts, 'document');
362
375
  for (let j = 0; j < batch.length; j++) {
363
376
  const piece = batch[j];
@@ -1,6 +1,13 @@
1
1
  import type { Config } from '../config';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
3
  import type { ILogger } from '../interfaces/ILogger';
4
+ import type { IMcpToolHandler, ToolResponse } from '../interfaces/IMcpToolHandler';
5
+ /**
6
+ * The dispatch logic behind `CallToolRequestSchema` — pulled out so it is testable
7
+ * without a live stdio transport. A rejecting handler becomes `Tool error: <message>`.
8
+ */
9
+ export declare function dispatchTool(tools: Map<string, IMcpToolHandler>, name: string, args: Record<string, unknown>, logger: ILogger): Promise<ToolResponse>;
10
+ export declare function buildInstructions(t: number): string;
4
11
  /**
5
12
  * McpServer — thin orchestration layer that delegates tool handling
6
13
  * to individual IMcpToolHandler implementations (Open/Closed Principle).
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.McpServer = void 0;
37
+ exports.dispatchTool = dispatchTool;
38
+ exports.buildInstructions = buildInstructions;
37
39
  const fs = __importStar(require("fs"));
38
40
  const path = __importStar(require("path"));
39
41
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
@@ -48,6 +50,24 @@ function readPackageVersion() {
48
50
  return '0.0.0';
49
51
  }
50
52
  }
53
+ /**
54
+ * The dispatch logic behind `CallToolRequestSchema` — pulled out so it is testable
55
+ * without a live stdio transport. A rejecting handler becomes `Tool error: <message>`.
56
+ */
57
+ async function dispatchTool(tools, name, args, logger) {
58
+ const handler = tools.get(name);
59
+ if (!handler) {
60
+ return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
61
+ }
62
+ try {
63
+ return await handler.handle(args);
64
+ }
65
+ catch (err) {
66
+ const reason = err instanceof Error ? err.message : String(err);
67
+ logger.error('McpServer: tool handler error', { tool: name, reason });
68
+ return { isError: true, content: [{ type: 'text', text: `Tool error: ${reason}` }] };
69
+ }
70
+ }
51
71
  function buildInstructions(t) {
52
72
  return `
53
73
  You have access to a local code intelligence server. Use these tools to search codebases by meaning, navigate import graphs, look up symbol definitions, and read source files.
@@ -58,7 +78,7 @@ Pick one workflow based on what you need:
58
78
 
59
79
  | Type | Signals | Workflow |
60
80
  |---|---|---|
61
- | **Lookup** | "where is X defined", "find Y" | find_symbol (if name known) → search_codebase (if not found) |
81
+ | **Lookup** | "where is X defined", "find Y" | find_symbol first when the exact name is known — exact and faster. Call search_codebase too, in parallel, when you also want surrounding context or the name might not match verbatim; fall back to it alone if find_symbol comes back empty. |
62
82
  | **Caller trace** | "who calls X", "find usages of Y", "what invokes Z" | find_symbol → get_callers |
63
83
  | **Conceptual** | "how does X work", "what is Y" | search_codebase → read_file_range |
64
84
  | **Flow audit** | "how does X connect to Y", "audit this flow", "trace the path" | search_codebase → **get_dependencies + get_dependents** → Grep |
@@ -68,26 +88,28 @@ Pick one workflow based on what you need:
68
88
 
69
89
  ## Tools
70
90
 
71
- - **list_projects** — call this first to see what projects are indexed and their exact names.
72
- - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
73
- - **read_file_range** — read a specific line range from any indexed file. Expects a **relative path** as returned by search_codebase.
91
+ Every file or directory argument (\`file_path\`, \`callee_file\`, \`path\`) is workspace-relative, exactly as printed by search_codebase (e.g. \`my-app/src/index.ts\`; \`.\` is the workspace root) — copy paths between tools unchanged.
92
+
93
+ - **list_projects** — call this first to see what projects are indexed, their exact names and their directories (\`path:\`).
94
+ - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk. **Pass \`project\` whenever the target project is known or guessable from the question** — this workspace indexes many projects at once, and an unscoped query competes against all of them, which \`rerank\` cannot fix (reranking only reorders whatever the vector search already retrieved). Pass \`rerank: false\` for "what does X do" / "where is X" lookups described without an exact name — measurement shows reranking hurts these (-0.23 to -0.31 MRR@10). If you already know the exact symbol name, use \`find_symbol\` instead, not \`search_codebase\` — it is faster and exact, and the rerank setting does not matter there. Leave reranking at its default (omit \`rerank\`, or pass \`true\`) for conceptual/architectural queries, where it helps (+0.08 MRR@10).
95
+ - **read_file_range** — read a specific line range from any file under the workspace, indexed or not.
74
96
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
75
97
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
76
98
  - **find_symbol** — find where a symbol is defined by exact name: top-level declarations, class/object-literal members (methods, get/set accessors, constructors, function properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name (\`assertUsersInOrg\`), a qualified name (\`RepairsService.assertUsersInOrg\`, \`NS.Parser.parse\`) to disambiguate same-named members, a quoted name for string-literal members (\`Router.'kebab-name'\`), or a computed name (\`Stack.[Symbol.iterator]\`). Faster than semantic search when you know the name.
77
99
  - **get_callers** — find all call sites of a function or method by exact name. Accepts a bare or qualified (\`Class.method\`) symbol, using the same suffix matching as \`find_symbol\`. Uses the call graph index — faster and more precise than grep. A call through an interface-typed receiver is recorded under the interface member (e.g. \`IEmbedder.embedBatch\`), not the implementing class. Use after find_symbol to trace where a symbol is actually called.
78
- - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
79
- - **read_file** — read a complete file by **absolute path** with line numbers. Truncates at max_lines with a hint to continue.
100
+ - **list_files** — browse the directory tree of a directory. Use to understand project structure before reading files.
101
+ - **read_file** — read a complete file with line numbers. Truncates at max_lines with a hint to continue.
80
102
 
81
103
  ## Recommended workflow
82
104
 
83
105
  ### Lookup / Conceptual
84
106
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
85
- 2. Use \`search_codebase\` to find relevant code by meaning. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
107
+ 2. Use \`search_codebase\` to find relevant code by meaning. **Always pass \`project\` when the question names or implies one** — an unscoped search ranks candidates across every indexed project together, so an unrelated project's weak match can outrank the right project's real one. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
86
108
  3. If a result looks **truncated or incomplete**, call \`read_file_range\` with the returned \`file_path\` and \`start_line\`.
87
109
  4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`.
88
110
  5. To immediately see a file's imports and callers without extra tool calls, pass \`include_graph_context: true\` to \`search_codebase\`. Use this when doing a quick structural survey; switch to \`get_dependencies\`/\`get_dependents\` for full transitive graphs.
89
- 6. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep.
90
- 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file by absolute path.
111
+ 6. Use \`find_symbol\` when you know the exact name — faster and exact, unlike semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. When you also want surrounding context (callers' intent, nearby helpers, how it's described elsewhere), call \`search_codebase\` in parallel rather than after — the two tools answer different questions and neither blocks the other. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep. For a NestJS GraphQL operation, resolve field or type, pass the matching \`graphql_query\`/\`graphql_mutation\`/\`graphql_subscription\`/\`graphql_field\`/\`graphql_type\` kind to look it up by the name the schema exposes, not the TypeScript declaration name.
112
+ 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file.
91
113
 
92
114
  ### Flow audit (mandatory steps — do not skip 3 and 4)
93
115
  1. Use \`list_projects\` to confirm project names.
@@ -97,8 +119,6 @@ Pick one workflow based on what you need:
97
119
  5. For each peripheral file found in steps 3–4: use \`read_file_range\` or \`find_symbol\` to inspect it.
98
120
  6. Only after steps 3–4 draw architectural conclusions — \`search_codebase\` alone misses peripheral files because they are semantically thin (configs, types, loaders contain no domain vocabulary).
99
121
 
100
- > **Note:** If \`get_dependencies\` / \`get_dependents\` return empty, the file likely uses path aliases (e.g. \`@core/\`, \`@/\`, \`@db/\`). Fall back to \`Grep\` on the symbol name across the project.
101
-
102
122
  ### Impact analysis
103
123
  1. \`get_dependents(target_file)\` — direct callers.
104
124
  2. \`get_dependents(target_file, transitive: true)\` — full reverse graph.
@@ -115,7 +135,7 @@ Pick one workflow based on what you need:
115
135
 
116
136
  ## Dependency graph
117
137
 
118
- \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
138
+ \`get_dependencies\` and \`get_dependents\` take \`file_path\` alone and list edges in every project. A path that names no file is an error, never an empty result. Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`import-side-effect\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
119
139
 
120
140
  If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you care about) — this triggers a grep fallback that finds runtime usages in DI containers, string references, and dynamic lookups that the import graph misses.
121
141
 
@@ -161,27 +181,11 @@ class McpServer {
161
181
  }],
162
182
  };
163
183
  }
164
- const handler = this.tools.get(req.params.name);
165
- if (!handler) {
166
- return {
167
- isError: true,
168
- content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
169
- };
170
- }
171
- try {
172
- const t0 = Date.now();
173
- const result = await handler.handle((req.params.arguments ?? {}));
184
+ const t0 = Date.now();
185
+ const result = await dispatchTool(this.tools, req.params.name, (req.params.arguments ?? {}), this.logger);
186
+ if (!result.isError)
174
187
  this.logger.debug('McpServer: tool handled', { tool: req.params.name, ms: Date.now() - t0 });
175
- return result;
176
- }
177
- catch (err) {
178
- const reason = err instanceof Error ? err.message : String(err);
179
- this.logger.error('McpServer: tool handler error', { tool: req.params.name, reason });
180
- return {
181
- isError: true,
182
- content: [{ type: 'text', text: `Tool error: ${reason}` }],
183
- };
184
- }
188
+ return result;
185
189
  });
186
190
  await server.connect(new stdio_js_1.StdioServerTransport());
187
191
  this.logger.info('McpServer: started', { version: this.version });
@@ -194,13 +198,13 @@ class McpServer {
194
198
  const handlers = [
195
199
  new tools_1.ListProjectsTool(this.searcher),
196
200
  new tools_1.SearchCodebaseTool(this.searcher, this.config.search.defaultLimit, this.config.search.maxLimit, this.config.search.similarityThreshold, this.logger),
197
- new tools_1.ReadFileRangeTool(this.searcher),
198
- new tools_1.GetDependenciesTool(this.searcher),
199
- new tools_1.GetDependentsTool(this.searcher),
201
+ new tools_1.ReadFileRangeTool(this.searcher, this.logger),
202
+ new tools_1.GetDependenciesTool(this.searcher, this.logger),
203
+ new tools_1.GetDependentsTool(this.searcher, this.logger),
200
204
  new tools_1.FindSymbolTool(this.searcher),
201
- new tools_1.GetCallersTool(this.searcher),
202
- new tools_1.ListFilesTool(this.searcher),
203
- new tools_1.ReadFileTool(this.searcher),
205
+ new tools_1.GetCallersTool(this.searcher, this.logger),
206
+ new tools_1.ListFilesTool(this.searcher, this.logger),
207
+ new tools_1.ReadFileTool(this.searcher, this.logger),
204
208
  ];
205
209
  const map = new Map();
206
210
  for (const h of handlers) {
@@ -51,7 +51,8 @@ class OpenAICompatibleEmbedder {
51
51
  const res = await this.doFetch(this.url, {
52
52
  method: 'POST',
53
53
  headers,
54
- signal: AbortSignal.timeout(120_000),
54
+ // CPU inference of one batch of token-dense text (e.g. pseudo-localized strings) takes well over 2 min.
55
+ signal: AbortSignal.timeout(240_000),
55
56
  body: JSON.stringify(body),
56
57
  });
57
58
  if (!res.ok) {
@@ -1,7 +1,8 @@
1
+ import * as fs from 'fs';
1
2
  import type { Config } from '../config';
2
3
  import type { IEmbedder } from '../interfaces';
3
4
  import type { IDatabase } from '../interfaces';
4
- import { type ProjectMeta } from '../file-utils';
5
+ import { type IndexingExclusion, type ProjectMeta } from '../file-utils';
5
6
  import type { ILogger } from '../interfaces';
6
7
  import type { IProjectDiscovery } from '../interfaces';
7
8
  export interface ChunkRow {
@@ -23,6 +24,58 @@ export interface GraphContext {
23
24
  imports: string[];
24
25
  usedBy: string[];
25
26
  }
27
+ /** A tool path argument refused on its form alone, before any lookup. */
28
+ export type PathRefusal = {
29
+ ok: false;
30
+ reason: 'invalid';
31
+ missing: boolean;
32
+ } | {
33
+ ok: false;
34
+ reason: 'absolute';
35
+ relative: string;
36
+ } | {
37
+ ok: false;
38
+ reason: 'outside_workspace';
39
+ };
40
+ export type ResolvedToolPath = {
41
+ ok: true;
42
+ relPath: string;
43
+ absPath: string;
44
+ } | PathRefusal;
45
+ export type NotIndexedExplanation = IndexingExclusion | {
46
+ kind: 'no_project';
47
+ } | {
48
+ kind: 'project_not_indexed';
49
+ project: string;
50
+ } | {
51
+ kind: 'unknown';
52
+ };
53
+ /** A well-formed path that leads to no readable data. */
54
+ export type PathFailure = {
55
+ ok: false;
56
+ reason: 'outside_workspace';
57
+ } | {
58
+ ok: false;
59
+ reason: 'unreadable';
60
+ code: string;
61
+ } | {
62
+ ok: false;
63
+ reason: 'not_found';
64
+ candidates: string[];
65
+ } | {
66
+ ok: false;
67
+ reason: 'deleted';
68
+ } | {
69
+ ok: false;
70
+ reason: 'not_indexed';
71
+ explanation: NotIndexedExplanation;
72
+ } | {
73
+ ok: false;
74
+ reason: 'not_a_file';
75
+ } | {
76
+ ok: false;
77
+ reason: 'not_a_directory';
78
+ };
26
79
  export type FileRangeResult = {
27
80
  ok: true;
28
81
  filePath: string;
@@ -30,10 +83,36 @@ export type FileRangeResult = {
30
83
  endLine: number;
31
84
  lines: string[];
32
85
  hasMore: boolean;
86
+ } | PathFailure | {
87
+ ok: false;
88
+ reason: 'past_end';
89
+ lineCount: number;
90
+ startLine: number;
91
+ } | {
92
+ ok: false;
93
+ reason: 'bad_range';
94
+ startLine: number;
95
+ endLine: number;
96
+ };
97
+ export type ListFilesResult = {
98
+ ok: true;
99
+ tree: string;
100
+ } | PathFailure;
101
+ export type GrepSymbolResult = {
102
+ ok: true;
103
+ matches: GrepMatch[];
104
+ truncated: boolean;
105
+ partial: boolean;
33
106
  } | {
34
107
  ok: false;
35
- reason: 'not_indexed' | 'not_on_disk' | 'invalid_path';
36
108
  };
109
+ /** Filesystem access of the path tools — injectable so error codes are testable without permissions. */
110
+ export interface SearcherFs {
111
+ realpath(p: string): Promise<string>;
112
+ stat(p: string): Promise<fs.Stats>;
113
+ readdir(p: string): Promise<fs.Dirent[]>;
114
+ createReadStream(p: string): NodeJS.ReadableStream;
115
+ }
37
116
  export interface DependencyRow {
38
117
  fromFile: string;
39
118
  fromSymbol: string | null;
@@ -50,12 +129,16 @@ export interface ProjectRow extends ProjectMeta {
50
129
  type?: 'app' | 'lib';
51
130
  /** Real filesystem root of the project (resolved via discovery). */
52
131
  path?: string;
132
+ /** `path` in workspace-relative form (`.` for the workspace root). */
133
+ relPath?: string;
134
+ /** Error code of `stat` on `path`, null when it exists. */
135
+ rootError?: string | null;
53
136
  }
54
137
  export interface GrepMatch {
55
138
  filePath: string;
56
139
  line: number;
57
140
  }
58
- export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'get_accessor' | 'set_accessor' | 'constructor' | 'function_property' | 'method_signature' | 'all';
141
+ export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'get_accessor' | 'set_accessor' | 'constructor' | 'function_property' | 'method_signature' | 'graphql_query' | 'graphql_mutation' | 'graphql_subscription' | 'graphql_field' | 'graphql_type' | 'all';
59
142
  export interface CallerRow {
60
143
  fromFile: string;
61
144
  fromSymbol: string | null;
@@ -73,6 +156,8 @@ export interface SymbolHit {
73
156
  qualified: string;
74
157
  isStatic: boolean;
75
158
  source: 'index' | 'grep';
159
+ /** Qualified TypeScript name of the decorated declaration (GraphQL rows only); null otherwise. */
160
+ target: string | null;
76
161
  }
77
162
  export declare class SearcherService {
78
163
  private db;
@@ -80,7 +165,18 @@ export declare class SearcherService {
80
165
  private discovery;
81
166
  private config;
82
167
  private logger;
83
- constructor(db: IDatabase, embedder: IEmbedder, discovery: IProjectDiscovery, config: Config, logger?: ILogger);
168
+ private readonly fs;
169
+ constructor(db: IDatabase, embedder: IEmbedder, discovery: IProjectDiscovery, config: Config, logger?: ILogger, fsDeps?: Partial<SearcherFs>);
170
+ /**
171
+ * Whether `code_symbol.target` exists — `true` once a check finds it (never
172
+ * re-checked again in this process), `false` until then (re-checked on
173
+ * every lookup, so a `rag-index` run that adds the column takes effect
174
+ * without restarting `rag-mcp`). See "find_symbol works before the target
175
+ * column exists" (`graphql-symbols`).
176
+ */
177
+ private targetColumnPresent;
178
+ /** Real path of `workspaceRoot`; cached only once resolved, so a transient failure is retried. */
179
+ private realRootCache?;
84
180
  /** Lazily-built, cached map of project name → real filesystem root. */
85
181
  private rootMap?;
86
182
  /** Cached, lazily-built map of leaf project name → real filesystem root (via discovery). */
@@ -92,6 +188,65 @@ export declare class SearcherService {
92
188
  * project since removed from disk, or a flat project not surfaced by discovery).
93
189
  */
94
190
  private resolveProjectRoot;
191
+ private toRel;
192
+ private toAbs;
193
+ private workspaceRoot;
194
+ private realWorkspaceRoot;
195
+ /**
196
+ * Lexical validation of a tool path argument: refusals for an empty value,
197
+ * `\`, an absolute path and a `..` escape; otherwise the canonical
198
+ * workspace-relative form. The only filesystem access is the cached real path
199
+ * of `workspaceRoot`.
200
+ */
201
+ resolveToolPath(input: string | null | undefined): Promise<ResolvedToolPath>;
202
+ /**
203
+ * Whether the real location of `absPath` — of the target, else of its nearest
204
+ * existing ancestor plus the remaining segments — lies under the real
205
+ * workspace root.
206
+ */
207
+ private realLocation;
208
+ /**
209
+ * Workspace boundary of the disk tools. A real location outside the workspace
210
+ * is allowed only for content the indexer already read (a symlinked share):
211
+ * the file itself for `file`, any file below it for `dir`.
212
+ */
213
+ checkRealLocation(relPath: string, absPath: string, kind: 'file' | 'dir'): Promise<{
214
+ ok: true;
215
+ } | Extract<PathFailure, {
216
+ reason: 'outside_workspace' | 'unreadable';
217
+ }>>;
218
+ private pathExists;
219
+ /** Whether any table of the index holds `relPath` (a canonical path, never `../…`). */
220
+ isIndexedPath(relPath: string): Promise<boolean>;
221
+ /** Whether the indexer read the file at `relPath`. */
222
+ isContentPath(relPath: string): Promise<boolean>;
223
+ private hasContentUnder;
224
+ /**
225
+ * Up to three index paths ending in `/<relPath>` (`file`) or directories
226
+ * ending in `/<relPath>` that hold one (`dir`), in byte order. For `dir`, the
227
+ * root of a project named `relPath` comes first.
228
+ */
229
+ findPathCandidates(relPath: string, kind: 'file' | 'dir'): Promise<string[]>;
230
+ /** `findPathCandidates`, with a failed lookup logged and answered by no candidates. */
231
+ private candidatesOrNone;
232
+ /**
233
+ * `relPath` with a leading project name replaced by that project's root
234
+ * (`repo/app/src/x.ts` → `repo/apps/app/src/x.ts`), or null when no name
235
+ * prefixes it, the root equals the name, or the result names nothing.
236
+ */
237
+ private substituteProjectName;
238
+ private findProjectRootByName;
239
+ /** Longest workspace-relative project root accepted by `match`; discovery is re-run once before giving up. */
240
+ private findProjectRoot;
241
+ /** Root directory (workspace-relative) of the project the text-search fallback for `relPath` runs in. */
242
+ fallbackRoot(relPath: string): string | null;
243
+ /**
244
+ * Why a graph tool has no data for `relPath`, or null when the path is in the
245
+ * index path set. Touches the disk only for an unknown path, and answers a
246
+ * real location outside the workspace as `not_found` without looking at it.
247
+ */
248
+ checkGraphPath(relPath: string): Promise<PathFailure | null>;
249
+ private explainNotIndexed;
95
250
  /**
96
251
  * Project-level dependency edges aggregated from `code_dependency`: an edge
97
252
  * `A → B` means some file in project A imports a file owned by project B.
@@ -119,7 +274,7 @@ export declare class SearcherService {
119
274
  assertIndexMatches(): Promise<void>;
120
275
  private compareIndexed;
121
276
  listProjects(): Promise<ProjectRow[]>;
122
- searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
277
+ searchChunks(query: string, limit: number, project: string | null, threshold?: number, rerankOverride?: boolean): Promise<SearchResult[]>;
123
278
  /**
124
279
  * Emit one structured search-tuning line (via `logger.metric`, at `info`
125
280
  * regardless of `logging.level`). `candidates` are in cosine order, so a
@@ -131,34 +286,42 @@ export declare class SearcherService {
131
286
  private emitSearchMetrics;
132
287
  fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
133
288
  fetchGraphContext(results: SearchResult[]): Promise<Map<string, GraphContext>>;
134
- getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
135
- getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
289
+ getDependencies(filePath: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
290
+ getDependents(filePath: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
136
291
  getCallers(symbol: string, project: string, calleeFile?: string | null): Promise<CallerRow[]>;
137
292
  /**
138
- * One-line warning when any project in `scope` (the given `project`, or
139
- * every project with `code_chunk`/`code_symbol` rows) is outdated — shown
140
- * first in `find_symbol`/`get_callers` output. Any failure to determine the
141
- * scope or read markers only drops the notice (warn logged); the lookup
142
- * itself is unaffected.
293
+ * Whole-word text search for `symbol` under `rootDir` (workspace-relative).
294
+ * grep exiting ≥ 2 after printing matches is a partial result; without
295
+ * matches, or when grep cannot be spawned, the search failed.
143
296
  */
144
- outdatedNotice(project: string | null): Promise<string | null>;
145
- grepSymbol(symbol: string, project: string, excludeFile: string): Promise<{
146
- matches: GrepMatch[];
147
- truncated: boolean;
148
- }>;
297
+ grepSymbol(symbol: string, rootDir: string, excludeFile: string): Promise<GrepSymbolResult>;
149
298
  findSymbol(name: string, project: string | null, kind: SymbolKind): Promise<{
150
299
  hits: SymbolHit[];
151
300
  truncated: boolean;
152
301
  grepFailed: boolean;
153
302
  }>;
303
+ /**
304
+ * Whether `code_symbol.target` exists. A present result is cached for the
305
+ * process; an absent one is re-checked on every call. Propagates on a
306
+ * failing check, same as any other failed index query.
307
+ */
308
+ private hasTargetColumn;
154
309
  private findSymbolFromIndex;
155
310
  private grepFindSymbol;
156
- listFiles(relativePath: string, project: string, depth: number, pattern: string | null): {
157
- tree: string;
158
- error?: string;
159
- };
160
- readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
161
- readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
311
+ listFiles(relPath: string, depth: number, pattern: string | null): Promise<ListFilesResult>;
312
+ /**
313
+ * The read tools' common front: workspace boundary, then existence and type.
314
+ * A missing file still in the index is `deleted`; otherwise `not_found`.
315
+ */
316
+ private openableFile;
317
+ /**
318
+ * Streams the file's lines (as `readline` yields them) to `onLine` until it
319
+ * returns false. An open or read error discards everything read so far.
320
+ */
321
+ private readLines;
322
+ readFile(relPath: string, maxLines: number): Promise<FileRangeResult>;
323
+ readFileRange(relPath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
324
+ /** Throws when `absDir` itself cannot be read; an unreadable subdirectory is marked in place. */
162
325
  private walkDir;
163
326
  /**
164
327
  * ERE pattern for a grep-fallback kind, or null when that kind is never
@@ -171,8 +334,9 @@ export declare class SearcherService {
171
334
  private inferKind;
172
335
  /**
173
336
  * Async wrapper around grep to avoid blocking the event loop. `ok: false`
174
- * means grep could not be spawned or exited with status ≥ 2 (distinguishable
175
- * from a clean "no matches", which is `ok: true, output: null`).
337
+ * means grep could not be spawned (`output: null`) or exited with status ≥ 2,
338
+ * keeping whatever it printed — distinguishable from a clean "no matches",
339
+ * which is `ok: true, output: null`.
176
340
  */
177
341
  private spawnGrep;
178
342
  }