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.
- package/CHANGELOG.md +34 -0
- package/README.md +15 -8
- package/dist/app.js +45 -7
- package/dist/src/commands/EvalCommand.d.ts +10 -0
- package/dist/src/commands/EvalCommand.js +231 -0
- package/dist/src/commands/IndexCommand.js +79 -53
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +5 -29
- package/dist/src/db/schema.js +2 -8
- package/dist/src/dependency-traversal.d.ts +16 -0
- package/dist/src/dependency-traversal.js +45 -0
- package/dist/src/enriched-prompt.d.ts +3 -2
- package/dist/src/enriched-prompt.js +3 -2
- package/dist/src/eval-results.d.ts +12 -0
- package/dist/src/eval-results.js +114 -0
- package/dist/src/file-utils.d.ts +15 -0
- package/dist/src/file-utils.js +24 -1
- package/dist/src/help.js +4 -1
- package/dist/src/interfaces/IAstParser.d.ts +13 -1
- package/dist/src/interfaces/IDatabase.d.ts +0 -14
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +11 -16
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/project-scope.d.ts +19 -0
- package/dist/src/project-scope.js +49 -0
- package/dist/src/project-selector.d.ts +6 -0
- package/dist/src/project-selector.js +18 -0
- package/dist/src/retrieval-eval.d.ts +16 -0
- package/dist/src/retrieval-eval.js +214 -0
- package/dist/src/services/AstParserService.d.ts +66 -1
- package/dist/src/services/AstParserService.js +430 -25
- package/dist/src/services/DatabaseService.d.ts +0 -12
- package/dist/src/services/DatabaseService.js +2 -52
- package/dist/src/services/IndexerService.d.ts +8 -0
- package/dist/src/services/IndexerService.js +24 -11
- package/dist/src/services/McpServer.d.ts +7 -0
- package/dist/src/services/McpServer.js +42 -38
- package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
- package/dist/src/services/SearcherService.d.ts +189 -25
- package/dist/src/services/SearcherService.js +505 -203
- package/dist/src/symbol-rows.d.ts +14 -9
- package/dist/src/symbol-rows.js +17 -11
- package/dist/src/tools/FindSymbolTool.js +14 -5
- package/dist/src/tools/GetCallersTool.d.ts +3 -1
- package/dist/src/tools/GetCallersTool.js +16 -6
- package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
- package/dist/src/tools/GetDependenciesTool.js +15 -8
- package/dist/src/tools/GetDependentsTool.d.ts +3 -1
- package/dist/src/tools/GetDependentsTool.js +31 -16
- package/dist/src/tools/ListFilesTool.d.ts +3 -1
- package/dist/src/tools/ListFilesTool.js +14 -12
- package/dist/src/tools/ListProjectsTool.js +5 -1
- package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
- package/dist/src/tools/ReadFileRangeTool.js +12 -11
- package/dist/src/tools/ReadFileTool.d.ts +3 -1
- package/dist/src/tools/ReadFileTool.js +12 -10
- package/dist/src/tools/SearchCodebaseTool.js +3 -1
- package/dist/src/tools/format-helpers.d.ts +16 -1
- package/dist/src/tools/format-helpers.js +64 -0
- 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
|
|
342
|
-
//
|
|
343
|
-
|
|
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,
|
|
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
|
|
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
|
-
-
|
|
72
|
-
|
|
73
|
-
- **
|
|
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
|
|
79
|
-
- **read_file** — read a complete file
|
|
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
|
|
90
|
-
7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file
|
|
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\`
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
135
|
-
getDependents(filePath: string,
|
|
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
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
-
|
|
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(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
175
|
-
* from a clean "no matches",
|
|
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
|
}
|