codesearch-mcp 0.2.2-alpha → 0.3.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +60 -33
  3. package/dist/app.js +46 -22
  4. package/dist/src/chunking/AstChunker.d.ts +12 -0
  5. package/dist/src/chunking/AstChunker.js +30 -0
  6. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  7. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  8. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  9. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  10. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  11. package/dist/src/chunking/FragmentMerger.js +61 -0
  12. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  13. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  14. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  15. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  16. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  17. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  18. package/dist/src/chunking/RunPacker.d.ts +12 -0
  19. package/dist/src/chunking/RunPacker.js +84 -0
  20. package/dist/src/chunking/create-chunker.d.ts +3 -0
  21. package/dist/src/chunking/create-chunker.js +15 -0
  22. package/dist/src/chunking/interfaces.d.ts +27 -0
  23. package/dist/src/chunking/interfaces.js +2 -0
  24. package/dist/src/commands/ConfigCommand.js +1 -9
  25. package/dist/src/commands/EvalCommand.d.ts +11 -0
  26. package/dist/src/commands/EvalCommand.js +264 -0
  27. package/dist/src/commands/IndexCommand.js +70 -66
  28. package/dist/src/commands/McpCommand.js +2 -8
  29. package/dist/src/commands/WebCommand.js +2 -5
  30. package/dist/src/config-error.d.ts +0 -4
  31. package/dist/src/config-error.js +0 -4
  32. package/dist/src/config-store.d.ts +1 -54
  33. package/dist/src/config-store.js +8 -69
  34. package/dist/src/config.d.ts +8 -17
  35. package/dist/src/config.js +35 -34
  36. package/dist/src/constants.d.ts +2 -28
  37. package/dist/src/constants.js +6 -28
  38. package/dist/src/container.d.ts +6 -0
  39. package/dist/src/container.js +12 -3
  40. package/dist/src/db/index.d.ts +1 -1
  41. package/dist/src/db/index.js +1 -2
  42. package/dist/src/db/schema.d.ts +22 -29
  43. package/dist/src/db/schema.js +3 -14
  44. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  45. package/dist/src/declarations/DeclarationTree.js +66 -0
  46. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  47. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  48. package/dist/src/declarations/SourceLines.d.ts +37 -0
  49. package/dist/src/declarations/SourceLines.js +415 -0
  50. package/dist/src/dependency-traversal.d.ts +10 -0
  51. package/dist/src/dependency-traversal.js +39 -0
  52. package/dist/src/enriched-prompt.d.ts +0 -5
  53. package/dist/src/enriched-prompt.js +0 -5
  54. package/dist/src/error-record.d.ts +12 -0
  55. package/dist/src/error-record.js +248 -0
  56. package/dist/src/eval-corpus.d.ts +16 -0
  57. package/dist/src/eval-corpus.js +169 -0
  58. package/dist/src/eval-results.d.ts +15 -0
  59. package/dist/src/eval-results.js +397 -0
  60. package/dist/src/file-utils.d.ts +10 -0
  61. package/dist/src/file-utils.js +19 -1
  62. package/dist/src/graph.d.ts +0 -25
  63. package/dist/src/graph.js +0 -28
  64. package/dist/src/help.js +4 -1
  65. package/dist/src/hnsw-settings.d.ts +3 -0
  66. package/dist/src/hnsw-settings.js +24 -0
  67. package/dist/src/interfaces/IAstParser.d.ts +5 -30
  68. package/dist/src/interfaces/IChunker.d.ts +3 -12
  69. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  70. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  71. package/dist/src/interfaces/IDatabase.d.ts +2 -36
  72. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  73. package/dist/src/interfaces/ILogger.d.ts +1 -11
  74. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  75. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  76. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  77. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
  78. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  79. package/dist/src/interfaces/ISearcher.d.ts +26 -20
  80. package/dist/src/interfaces/index.d.ts +1 -0
  81. package/dist/src/interrupt.js +0 -2
  82. package/dist/src/owner-path.d.ts +9 -10
  83. package/dist/src/owner-path.js +11 -23
  84. package/dist/src/project-scope.d.ts +7 -0
  85. package/dist/src/project-scope.js +37 -0
  86. package/dist/src/project-selector.d.ts +1 -0
  87. package/dist/src/project-selector.js +13 -6
  88. package/dist/src/retrieval-eval.d.ts +24 -0
  89. package/dist/src/retrieval-eval.js +500 -0
  90. package/dist/src/search/result-shaping.d.ts +4 -0
  91. package/dist/src/search/result-shaping.js +19 -0
  92. package/dist/src/services/AstParserService.d.ts +28 -23
  93. package/dist/src/services/AstParserService.js +391 -98
  94. package/dist/src/services/DatabaseService.d.ts +2 -19
  95. package/dist/src/services/DatabaseService.js +44 -81
  96. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  97. package/dist/src/services/EmbedderFactory.js +0 -5
  98. package/dist/src/services/IndexerService.d.ts +2 -10
  99. package/dist/src/services/IndexerService.js +29 -45
  100. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  101. package/dist/src/services/InfinityEmbedder.js +0 -15
  102. package/dist/src/services/LoggerService.d.ts +7 -5
  103. package/dist/src/services/LoggerService.js +37 -41
  104. package/dist/src/services/McpServer.d.ts +25 -10
  105. package/dist/src/services/McpServer.js +101 -64
  106. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  107. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  108. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  109. package/dist/src/services/NxMonorepoResolver.js +5 -56
  110. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  111. package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
  112. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  113. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  114. package/dist/src/services/SearcherService.d.ts +127 -70
  115. package/dist/src/services/SearcherService.js +526 -305
  116. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  117. package/dist/src/services/VoyageEmbedder.js +0 -6
  118. package/dist/src/services/WebServerService.d.ts +1 -53
  119. package/dist/src/services/WebServerService.js +11 -57
  120. package/dist/src/services/http-retry.d.ts +0 -7
  121. package/dist/src/services/http-retry.js +1 -10
  122. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  123. package/dist/src/services/infinity-provisioning.js +5 -88
  124. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  125. package/dist/src/services/legacy-ollama-index.js +0 -23
  126. package/dist/src/services/reranker.d.ts +0 -9
  127. package/dist/src/services/reranker.js +46 -12
  128. package/dist/src/setup.d.ts +11 -23
  129. package/dist/src/setup.js +98 -71
  130. package/dist/src/symbol-name.d.ts +0 -12
  131. package/dist/src/symbol-name.js +0 -13
  132. package/dist/src/symbol-rows.d.ts +2 -11
  133. package/dist/src/symbol-rows.js +5 -13
  134. package/dist/src/tool-call-log.d.ts +2 -0
  135. package/dist/src/tool-call-log.js +24 -0
  136. package/dist/src/tools/FindSymbolTool.js +14 -5
  137. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  138. package/dist/src/tools/GetCallersTool.js +16 -6
  139. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  140. package/dist/src/tools/GetDependenciesTool.js +15 -8
  141. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  142. package/dist/src/tools/GetDependentsTool.js +31 -16
  143. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  144. package/dist/src/tools/ListFilesTool.js +14 -12
  145. package/dist/src/tools/ListProjectsTool.js +5 -1
  146. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  147. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  148. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  149. package/dist/src/tools/ReadFileTool.js +12 -10
  150. package/dist/src/tools/SearchCodebaseTool.js +7 -2
  151. package/dist/src/tools/format-helpers.d.ts +10 -1
  152. package/dist/src/tools/format-helpers.js +145 -8
  153. package/dist/src/web/dashboard.d.ts +0 -8
  154. package/dist/src/web/dashboard.js +7 -10
  155. package/package.json +10 -9
  156. package/dist/src/services/AstChunker.d.ts +0 -42
  157. package/dist/src/services/AstChunker.js +0 -158
  158. package/docs/web-dashboard.md +0 -206
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class GetDependentsTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -2,52 +2,67 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GetDependentsTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
- const format_helpers_2 = require("./format-helpers");
6
5
  class GetDependentsTool {
7
6
  searcher;
8
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
9
9
  this.searcher = searcher;
10
+ this.logger = logger;
10
11
  }
11
12
  definition() {
12
13
  return {
13
14
  name: 'get_dependents',
14
- description: 'Show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
15
+ description: 'Show which files import a given file (reverse graph), in any project. Use before refactoring to find all callers and avoid breaking changes. The direct section prints one line per importing file, with the names imported and the enclosing declarations that use them — only those resolved unambiguously, so the list can be partial or absent.',
15
16
  inputSchema: {
16
17
  type: 'object',
17
18
  properties: {
18
- file_path: { type: 'string', description: 'File path as returned by search_codebase' },
19
- project: { type: 'string', description: 'Project name (use list_projects to find it)' },
20
- symbol: { type: 'string', description: 'Exported symbol name. Two behaviors: (1) if graph dependents exist, narrows them to imports of this symbol; (2) if the import graph returns zero results, triggers a whole-word text-search fallback (grep -F -w) across all .ts/.tsx/.js/.jsx files in the project to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
19
+ file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
20
+ symbol: { type: 'string', description: 'Exported symbol name. Two behaviors: (1) if graph dependents exist, narrows them to imports of this symbol; (2) if the import graph returns zero results, triggers a whole-word text-search fallback (grep -F -w) across all .ts/.tsx/.js/.jsx files of the project owning the file to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
21
21
  transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
22
22
  },
23
- required: ['file_path', 'project'],
23
+ required: ['file_path'],
24
24
  },
25
25
  };
26
26
  }
27
27
  async handle(args) {
28
- const filePath = String(args.file_path);
29
- const project = String(args.project);
28
+ const input = args.file_path == null ? '' : String(args.file_path);
30
29
  const symbol = args.symbol ? String(args.symbol) : null;
31
30
  const transitive = Boolean(args.transitive);
32
- const rows = await this.searcher.getDependents(filePath, project, symbol, transitive);
31
+ const resolved = await this.searcher.resolveToolPath(input);
32
+ if (!resolved.ok)
33
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependents', 'file_path', input, input, resolved);
34
+ const filePath = resolved.relPath;
35
+ const rows = await this.searcher.getDependents(filePath, symbol, transitive);
33
36
  if (rows.length > 0) {
34
- return (0, format_helpers_1.textResponse)((0, format_helpers_2.formatDependents)(filePath, rows, transitive));
37
+ return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependents)(filePath, rows, transitive));
35
38
  }
39
+ const failure = await this.searcher.checkGraphPath(filePath);
40
+ if (failure)
41
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependents', 'file_path', input, filePath, failure);
36
42
  const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
37
43
  if (!symbol) {
38
44
  return (0, format_helpers_1.textResponse)(`${header}\n\nTip: pass \`symbol\` to trigger a text-search fallback for runtime usages (e.g. via singleton or container).`);
39
45
  }
40
- const { matches, truncated } = await this.searcher.grepSymbol(symbol, project, filePath);
41
- if (matches.length === 0) {
42
- return (0, format_helpers_1.textResponse)(`${header}\n\n**Fallback \`[text search]\` for \`${symbol}\`:** No results.`);
46
+ const rootDir = this.searcher.fallbackRoot(filePath);
47
+ if (rootDir === null)
48
+ return (0, format_helpers_1.textResponse)(header);
49
+ const fallbackHeader = `**Fallback \`[text search]\` for \`${symbol}\`:**`;
50
+ const grep = await this.searcher.grepSymbol(symbol, rootDir, filePath);
51
+ if (!grep.ok) {
52
+ return (0, format_helpers_1.textResponse)(`${header}\n\n${fallbackHeader} failed`);
53
+ }
54
+ if (grep.matches.length === 0) {
55
+ return (0, format_helpers_1.textResponse)(`${header}\n\n${fallbackHeader} No results.`);
43
56
  }
44
57
  const grouped = new Map();
45
- for (const m of matches) {
58
+ for (const m of grep.matches) {
46
59
  if (!grouped.has(m.filePath))
47
60
  grouped.set(m.filePath, []);
48
61
  grouped.get(m.filePath).push(m.line);
49
62
  }
50
- const fallbackLines = [`\n**Fallback \`[text search]\` for \`${symbol}\`:**${truncated ? ' *(truncated to 200 results)*' : ''}`];
63
+ const notes = (grep.truncated ? ' *(truncated to 200 results)*' : '') +
64
+ (grep.partial ? ' *(partial — some files could not be searched)*' : '');
65
+ const fallbackLines = [`\n${fallbackHeader}${notes}`];
51
66
  for (const [fp, lines] of grouped) {
52
67
  fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
53
68
  }
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class ListFilesTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -4,36 +4,38 @@ exports.ListFilesTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
5
  class ListFilesTool {
6
6
  searcher;
7
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
8
9
  this.searcher = searcher;
10
+ this.logger = logger;
9
11
  }
10
12
  definition() {
11
13
  return {
12
14
  name: 'list_files',
13
- description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
15
+ description: 'List files and directories under a workspace directory as an indented tree. Use to understand project structure before reading files; list_projects prints each project\'s directory.',
14
16
  inputSchema: {
15
17
  type: 'object',
16
18
  properties: {
17
- path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
18
- project: { type: 'string', description: 'Project name (from list_projects)' },
19
+ path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)('directory') },
19
20
  depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
20
21
  pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
21
22
  },
22
- required: ['path', 'project'],
23
+ required: ['path'],
23
24
  },
24
25
  };
25
26
  }
26
27
  async handle(args) {
27
- const relPath = String(args.path ?? '.');
28
- const project = String(args.project ?? '');
28
+ const input = args.path == null ? '' : String(args.path);
29
29
  const rawDepth = Number(args.depth);
30
30
  const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
31
31
  const pattern = args.pattern ? String(args.pattern) : null;
32
- const { tree, error } = this.searcher.listFiles(relPath, project, depth, pattern);
33
- if (error) {
34
- return (0, format_helpers_1.textResponse)(`list_files error: ${error}`);
35
- }
36
- return (0, format_helpers_1.textResponse)(`**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\``);
32
+ const resolved = await this.searcher.resolveToolPath(input);
33
+ if (!resolved.ok)
34
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'list_files', 'path', input, input, resolved, 'directory');
35
+ const result = await this.searcher.listFiles(resolved.relPath, depth, pattern);
36
+ if (!result.ok)
37
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'list_files', 'path', input, resolved.relPath, result, 'directory');
38
+ return (0, format_helpers_1.textResponse)(`**Directory:** ${resolved.relPath}\n\`\`\`\n${result.tree}\n\`\`\``);
37
39
  }
38
40
  }
39
41
  exports.ListFilesTool = ListFilesTool;
@@ -10,7 +10,7 @@ class ListProjectsTool {
10
10
  definition() {
11
11
  return {
12
12
  name: 'list_projects',
13
- description: 'List all indexed projects with their chunk counts and metadata (description, stack, notes, relations from .ragmeta.json).',
13
+ description: 'List all indexed projects with their chunk counts, workspace-relative directory (`path:`) and metadata (description, stack, notes, relations from .ragmeta.json).',
14
14
  inputSchema: { type: 'object', properties: {} },
15
15
  };
16
16
  }
@@ -21,6 +21,10 @@ class ListProjectsTool {
21
21
  }
22
22
  return (0, format_helpers_1.textResponse)(rows.map(r => {
23
23
  const lines = [`${r.serviceName}: ${r.chunks} chunks`];
24
+ if (r.relPath) {
25
+ const state = !r.rootError ? '' : r.rootError === 'ENOENT' ? ' (not on disk)' : ` (${r.rootError})`;
26
+ lines.push(` path: ${r.relPath}${state}`);
27
+ }
24
28
  if (r.packageName)
25
29
  lines.push(` package: ${r.packageName}`);
26
30
  if (r.type)
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class ReadFileRangeTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -4,17 +4,19 @@ exports.ReadFileRangeTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
5
  class ReadFileRangeTool {
6
6
  searcher;
7
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
8
9
  this.searcher = searcher;
10
+ this.logger = logger;
9
11
  }
10
12
  definition() {
11
13
  return {
12
14
  name: 'read_file_range',
13
- description: 'Read a specific line range from an indexed source file. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
15
+ description: 'Read a specific line range from a file under the workspace, indexed or not. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
14
16
  inputSchema: {
15
17
  type: 'object',
16
18
  properties: {
17
- file_path: { type: 'string', description: 'File path as returned by search_codebase' },
19
+ file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
18
20
  start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
19
21
  end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
20
22
  },
@@ -23,16 +25,15 @@ class ReadFileRangeTool {
23
25
  };
24
26
  }
25
27
  async handle(args) {
26
- const filePath = String(args.file_path);
28
+ const input = args.file_path == null ? '' : String(args.file_path);
27
29
  const startLine = Math.max(1, Number(args.start_line) || 1);
28
30
  const endLine = Number(args.end_line) || startLine + 99;
29
- const range = await this.searcher.readFileRange(filePath, startLine, endLine);
30
- if (!range.ok) {
31
- const message = range.reason === 'invalid_path' ? `Invalid file path (outside workspace): ${filePath}` :
32
- range.reason === 'not_indexed' ? `File is not indexed: ${filePath}. Run indexing first.` :
33
- `File was deleted from disk after indexing: ${filePath}. Re-run indexing to update.`;
34
- return (0, format_helpers_1.textResponse)(message);
35
- }
31
+ const resolved = await this.searcher.resolveToolPath(input);
32
+ if (!resolved.ok)
33
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file_range', 'file_path', input, input, resolved);
34
+ const range = await this.searcher.readFileRange(resolved.relPath, startLine, endLine);
35
+ if (!range.ok)
36
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file_range', 'file_path', input, resolved.relPath, range);
36
37
  const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
37
38
  let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
38
39
  if (range.hasMore) {
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class ReadFileTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -4,17 +4,19 @@ exports.ReadFileTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
5
  class ReadFileTool {
6
6
  searcher;
7
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
8
9
  this.searcher = searcher;
10
+ this.logger = logger;
9
11
  }
10
12
  definition() {
11
13
  return {
12
14
  name: 'read_file',
13
- description: 'Read a file by absolute path. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
15
+ description: 'Read a complete file under the workspace, indexed or not. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
14
16
  inputSchema: {
15
17
  type: 'object',
16
18
  properties: {
17
- file_path: { type: 'string', description: 'Absolute path to the file. Note: search_codebase returns relative paths — construct the absolute path from your workspace root.' },
19
+ file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
18
20
  max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
19
21
  },
20
22
  required: ['file_path'],
@@ -22,15 +24,15 @@ class ReadFileTool {
22
24
  };
23
25
  }
24
26
  async handle(args) {
25
- const filePath = String(args.file_path ?? '');
27
+ const input = args.file_path == null ? '' : String(args.file_path);
26
28
  const rawMax = Number(args.max_lines);
27
29
  const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
28
- const result = await this.searcher.readFileAbsolute(filePath, maxLines);
29
- if (!result.ok) {
30
- const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
31
- `File not found on disk: ${filePath}`;
32
- return (0, format_helpers_1.textResponse)(message);
33
- }
30
+ const resolved = await this.searcher.resolveToolPath(input);
31
+ if (!resolved.ok)
32
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file', 'file_path', input, input, resolved);
33
+ const result = await this.searcher.readFile(resolved.relPath, maxLines);
34
+ if (!result.ok)
35
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file', 'file_path', input, resolved.relPath, result);
34
36
  const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
35
37
  let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
36
38
  if (result.hasMore) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SearchCodebaseTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
+ const result_shaping_1 = require("../search/result-shaping");
5
6
  class SearchCodebaseTool {
6
7
  searcher;
7
8
  defaultLimit;
@@ -28,6 +29,8 @@ class SearchCodebaseTool {
28
29
  include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
29
30
  include_graph_context: { type: 'boolean', description: 'Append direct imports and dependents list to each result. Shows "Imports: X · Y" and "Used by: Z" without extra tool calls.' },
30
31
  threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.defaultThreshold}. Use 0.40–0.45 for precise technical queries, 0.55–0.60 for broad exploration.` },
32
+ per_file: { type: 'number', description: `Max results from one file (integer ≥ 1, default ${result_shaping_1.DEFAULT_PER_FILE}). Skipped results are backfilled from the next candidates. Raise it when one file is expected to hold several answers (e.g. several methods of one class).` },
33
+ rerank: { type: 'boolean', description: 'Override whether this call reranks, regardless of config. false skips reranking (use for "what does X do" / "where is X" lookups described without an exact name — reranking measurably hurts these; if you know the exact symbol name, use find_symbol instead). true forces reranking on if a reranker is configured (no-op otherwise). Omit to use config.reranker.enabled (fine for conceptual/architectural queries, where reranking is roughly neutral).' },
31
34
  },
32
35
  required: ['query'],
33
36
  },
@@ -45,11 +48,13 @@ class SearchCodebaseTool {
45
48
  const threshold = rawThreshold != null && !isNaN(parsedThreshold)
46
49
  ? Math.min(1, Math.max(0, parsedThreshold))
47
50
  : undefined;
51
+ const rerank = typeof args.rerank === 'boolean' ? args.rerank : undefined;
52
+ const perFile = Number.isInteger(args.per_file) && args.per_file >= 1 ? args.per_file : result_shaping_1.DEFAULT_PER_FILE;
48
53
  const { query, translated } = await this.searcher.prepareQuery(raw);
49
- const results = await this.searcher.searchChunks(query, limit, project, threshold);
54
+ const results = await this.searcher.searchChunks(query, limit, project, threshold, rerank, perFile);
50
55
  this.logger.debug('SearchCodebaseTool: handle', {
51
56
  query: raw.slice(0, 120), project, threshold: threshold ?? this.defaultThreshold,
52
- limit, translated, count: results.length, ms: Date.now() - t0,
57
+ limit, perFile, translated, count: results.length, ms: Date.now() - t0,
53
58
  });
54
59
  if (results.length === 0) {
55
60
  return (0, format_helpers_1.textResponse)('No relevant results found.');
@@ -1,8 +1,17 @@
1
- import type { ChunkRow, DependencyRow, GraphContext } from '../services/SearcherService';
1
+ import type { ChunkRow, DependencyRow, GraphContext, FileRangeResult, PathFailure, PathRefusal } from '../services/SearcherService';
2
2
  import type { ToolResponse } from '../interfaces/IMcpToolHandler';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare function textResponse(text: string): ToolResponse;
4
5
  export declare function errorResponse(text: string): ToolResponse;
5
6
  export declare function formatChunk(r: ChunkRow, label?: string): string;
6
7
  export declare function formatGraphContext(ctx: GraphContext | undefined): string;
7
8
  export declare function formatDependencies(filePath: string, rows: DependencyRow[], transitive: boolean): string;
8
9
  export declare function formatDependents(filePath: string, rows: DependencyRow[], transitive: boolean): string;
10
+ export type PathKind = 'file' | 'directory';
11
+ export declare const PATH_EXAMPLE: Record<PathKind, string>;
12
+ export declare function pathArgDescription(kind?: PathKind): string;
13
+ export type PathError = PathRefusal | PathFailure | Extract<FileRangeResult, {
14
+ reason: 'past_end' | 'bad_range';
15
+ }>;
16
+ export declare function pathErrorText(argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): string;
17
+ export declare function pathErrorResponse(logger: ILogger, tool: string, argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): ToolResponse;
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PATH_EXAMPLE = void 0;
3
4
  exports.textResponse = textResponse;
4
5
  exports.errorResponse = errorResponse;
5
6
  exports.formatChunk = formatChunk;
6
7
  exports.formatGraphContext = formatGraphContext;
7
8
  exports.formatDependencies = formatDependencies;
8
9
  exports.formatDependents = formatDependents;
10
+ exports.pathArgDescription = pathArgDescription;
11
+ exports.pathErrorText = pathErrorText;
12
+ exports.pathErrorResponse = pathErrorResponse;
9
13
  function textResponse(text) {
10
14
  return { content: [{ type: 'text', text }] };
11
15
  }
@@ -24,13 +28,87 @@ function formatGraphContext(ctx) {
24
28
  parts.push(`**Used by:** ${ctx.usedBy.join(' · ')}`);
25
29
  return parts.length > 0 ? '\n' + parts.join('\n') : '';
26
30
  }
31
+ const MAX_NAMES = 8;
32
+ const MAX_IN = 3;
33
+ function nameEntry(r) {
34
+ switch (r.depType) {
35
+ case 'import': return r.toSymbol ? { text: r.toSymbol, isReexport: false } : null;
36
+ case 'import-default': return { text: r.toSymbol ?? 'default', isReexport: false };
37
+ case 'import-namespace': return { text: r.toSymbol ?? '* as namespace', isReexport: false };
38
+ case 'import-side-effect': return { text: 'side-effect', isReexport: false };
39
+ case 'reexport': return r.toSymbol ? { text: r.toSymbol, isReexport: true } : null;
40
+ case 'reexport-namespace': return { text: r.toSymbol ?? 'export *', isReexport: true };
41
+ default: return null;
42
+ }
43
+ }
44
+ function codeSpan(value) {
45
+ const text = value.replace(/\s+/g, ' ');
46
+ const fence = '`'.repeat(Math.max(0, ...(text.match(/`+/g) ?? []).map(run => run.length)) + 1);
47
+ const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : '';
48
+ return `${fence}${pad}${text}${pad}${fence}`;
49
+ }
50
+ function capped(values, max) {
51
+ const shown = values.slice(0, max);
52
+ const more = values.length - shown.length;
53
+ const text = shown.map(codeSpan).join(', ');
54
+ return more > 0 ? `${text} (+${more} more)` : text;
55
+ }
56
+ function groupNames(rows) {
57
+ const plain = [];
58
+ const reexport = [];
59
+ for (const r of rows) {
60
+ const e = nameEntry(r);
61
+ if (!e)
62
+ continue;
63
+ const bucket = e.isReexport ? reexport : plain;
64
+ if (!bucket.includes(e.text))
65
+ bucket.push(e.text);
66
+ }
67
+ const total = plain.length + reexport.length;
68
+ const shownPlainCount = Math.min(plain.length, MAX_NAMES);
69
+ const shownReexportCount = Math.min(reexport.length, MAX_NAMES - shownPlainCount);
70
+ const more = total - shownPlainCount - shownReexportCount;
71
+ const parts = [];
72
+ if (shownPlainCount > 0) {
73
+ parts.push(plain.slice(0, shownPlainCount).map(codeSpan).join(', '));
74
+ }
75
+ if (shownReexportCount > 0) {
76
+ parts.push(`re-export ${reexport.slice(0, shownReexportCount).map(codeSpan).join(', ')}`);
77
+ }
78
+ let text = parts.join(', ');
79
+ if (more > 0)
80
+ text += ` (+${more} more)`;
81
+ return text;
82
+ }
83
+ function groupIn(rows) {
84
+ const values = [];
85
+ for (const r of rows) {
86
+ if (r.fromSymbol && !values.includes(r.fromSymbol))
87
+ values.push(r.fromSymbol);
88
+ }
89
+ return capped(values, MAX_IN);
90
+ }
91
+ function groupByFile(rows, key) {
92
+ const groups = new Map();
93
+ for (const r of rows) {
94
+ const k = r[key];
95
+ if (!groups.has(k))
96
+ groups.set(k, []);
97
+ groups.get(k).push(r);
98
+ }
99
+ return groups;
100
+ }
101
+ function directHeader(fileCount) {
102
+ return `**Direct:** ${fileCount} file${fileCount === 1 ? '' : 's'}`;
103
+ }
27
104
  function formatDependencies(filePath, rows, transitive) {
28
105
  const direct = rows.filter(r => !transitive || (r.depth ?? 1) === 1);
29
106
  const indirect = transitive ? rows.filter(r => (r.depth ?? 1) > 1) : [];
30
- const lines = [`**Dependencies of** \`${filePath}\`\n`, '**Direct:**'];
31
- for (const r of direct) {
32
- const sym = r.toSymbol ? ` (${r.depType} \`${r.toSymbol}\`)` : ` (${r.depType})`;
33
- lines.push(` → \`${r.toFile}\`${sym}`);
107
+ const groups = groupByFile(direct, 'toFile');
108
+ const lines = [`**Dependencies of** \`${filePath}\`\n`, directHeader(groups.size)];
109
+ for (const [toFile, groupRows] of groups) {
110
+ const names = groupNames(groupRows);
111
+ lines.push(` → \`${toFile}\`${names ? ` — ${names}` : ''}`);
34
112
  }
35
113
  if (indirect.length > 0) {
36
114
  lines.push('\n**Transitive:**');
@@ -43,10 +121,15 @@ function formatDependencies(filePath, rows, transitive) {
43
121
  function formatDependents(filePath, rows, transitive) {
44
122
  const direct = rows.filter(r => !transitive || (r.depth ?? 1) === 1);
45
123
  const indirect = transitive ? rows.filter(r => (r.depth ?? 1) > 1) : [];
46
- const lines = [`**Dependents of** \`${filePath}\`\n`, '**Direct:**'];
47
- for (const r of direct) {
48
- const sym = r.fromSymbol ? ` (in \`${r.fromSymbol}\`)` : '';
49
- lines.push(` ← \`${r.fromFile}\`${sym}`);
124
+ const groups = groupByFile(direct, 'fromFile');
125
+ const lines = [`**Dependents of** \`${filePath}\`\n`, directHeader(groups.size)];
126
+ for (const [fromFile, groupRows] of groups) {
127
+ const names = groupNames(groupRows);
128
+ const inPart = groupIn(groupRows);
129
+ let line = ` ← \`${fromFile}\`${names ? ` — ${names}` : ''}`;
130
+ if (inPart)
131
+ line += ` · in ${inPart}`;
132
+ lines.push(line);
50
133
  }
51
134
  if (indirect.length > 0) {
52
135
  lines.push('\n**Transitive:**');
@@ -56,3 +139,57 @@ function formatDependents(filePath, rows, transitive) {
56
139
  }
57
140
  return lines.join('\n');
58
141
  }
142
+ exports.PATH_EXAMPLE = { file: 'my-app/src/index.ts', directory: 'my-app/src' };
143
+ function pathArgDescription(kind = 'file') {
144
+ const root = kind === 'directory' ? ', "." for the workspace root' : '';
145
+ const noun = kind === 'directory' ? 'directory' : 'path';
146
+ return `Workspace-relative ${noun}, as printed by search_codebase (e.g. ${exports.PATH_EXAMPLE[kind]})${root}`;
147
+ }
148
+ function acceptedForm(example) {
149
+ return `Paths are workspace-relative, as printed by search_codebase — e.g. ${example}.`;
150
+ }
151
+ function notIndexedExplanation(e) {
152
+ const excluded = (why) => `This file is not indexed (${why}), so the graph has no data for it. Use read_file or Grep instead.`;
153
+ switch (e.kind) {
154
+ case 'no_project': return excluded('it is outside every project');
155
+ case 'type': return excluded('files of this type are not indexed');
156
+ case 'ignored_dir': return excluded(`${e.segment}/ is an ignored directory`);
157
+ case 'ragignore': return excluded('it matches .ragignore');
158
+ case 'project_not_indexed': return `Project ${e.project} is not in the index. If it is not selected, select it with rag-index --select; then run rag-index.`;
159
+ case 'unknown':
160
+ return 'The file exists on disk but the index has no data for it: either it was added after the last index run ' +
161
+ '(run rag-index), or it has no indexable content (empty, comments only, or imports of external packages only).';
162
+ }
163
+ }
164
+ function pathErrorText(argName, input, relPath, err, kind = 'file') {
165
+ const example = exports.PATH_EXAMPLE[kind];
166
+ switch (err.reason) {
167
+ case 'invalid':
168
+ return err.missing
169
+ ? `Missing ${argName}\n${acceptedForm(example)}`
170
+ : `Invalid path (\\ is not a separator): ${input}\n${acceptedForm(example)}`;
171
+ case 'absolute':
172
+ return `Absolute path: ${input} — use ${err.relative}\n${acceptedForm(err.relative)}`;
173
+ case 'outside_workspace':
174
+ return `${input} is outside the workspace\n${acceptedForm(example)}`;
175
+ case 'not_found': {
176
+ const lines = [`Not found: ${relPath}`, acceptedForm(err.candidates[0] ?? example)];
177
+ if (err.candidates.length > 0)
178
+ lines.push('Did you mean:', ...err.candidates.map(c => `- ${c}`));
179
+ return lines.join('\n');
180
+ }
181
+ case 'deleted':
182
+ return `Deleted from disk: ${relPath}\nThe index still holds this file, but it no longer exists on disk. Run rag-index to update the index.`;
183
+ case 'not_indexed':
184
+ return `Not indexed: ${relPath}\n${notIndexedExplanation(err.explanation)}`;
185
+ case 'not_a_file': return `Not a file: ${relPath}`;
186
+ case 'not_a_directory': return `Not a directory: ${relPath}`;
187
+ case 'unreadable': return `Cannot read: ${relPath} (${err.code})`;
188
+ case 'past_end': return `${relPath} has ${err.lineCount} lines; start_line ${err.startLine} is past the end`;
189
+ case 'bad_range': return `end_line ${err.endLine} is before start_line ${err.startLine}`;
190
+ }
191
+ }
192
+ function pathErrorResponse(logger, tool, argName, input, relPath, err, kind = 'file') {
193
+ logger.warn('Tool path error', { tool, input, reason: err.reason });
194
+ return errorResponse(pathErrorText(argName, input, relPath, err, kind));
195
+ }
@@ -1,9 +1 @@
1
- /**
2
- * Renders the RAG dashboard as a single self-contained HTML page (inline CSS +
3
- * JS), mirroring the approach in graph.ts. The page is schema-driven: it fetches
4
- * /api/schema (from buildJsonSchema — the same source as Step 1's editor support)
5
- * and /api/config, then renders a typed form. The Graph tab embeds the full
6
- * standalone graph page (GET /graph — same buildHtml as project-graph.html) in
7
- * an iframe rather than re-implementing the network here.
8
- */
9
1
  export declare function renderDashboardHtml(): string;
@@ -1,14 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderDashboardHtml = renderDashboardHtml;
4
- /**
5
- * Renders the RAG dashboard as a single self-contained HTML page (inline CSS +
6
- * JS), mirroring the approach in graph.ts. The page is schema-driven: it fetches
7
- * /api/schema (from buildJsonSchema — the same source as Step 1's editor support)
8
- * and /api/config, then renders a typed form. The Graph tab embeds the full
9
- * standalone graph page (GET /graph — same buildHtml as project-graph.html) in
10
- * an iframe rather than re-implementing the network here.
11
- */
12
4
  function renderDashboardHtml() {
13
5
  return `<!DOCTYPE html>
14
6
  <html lang="en">
@@ -168,7 +160,12 @@ function renderDashboardHtml() {
168
160
  if (spec.type === 'boolean') {
169
161
  const cb = document.createElement('input');
170
162
  cb.type = 'checkbox'; cb.dataset.key = key; cb.dataset.type = 'boolean';
171
- cb.checked = value === true;
163
+ cb.checked = isUnset ? def === true : value === true;
164
+ // An absent boolean can resolve from context (reranker.enabled), so it is saved only once changed.
165
+ if (isUnset) {
166
+ cb.dataset.unset = '1';
167
+ cb.addEventListener('change', () => { delete cb.dataset.unset; });
168
+ }
172
169
  return cb;
173
170
  }
174
171
  if (Array.isArray(enumVals)) return selectEl(key, enumVals, value, def);
@@ -236,7 +233,7 @@ function renderDashboardHtml() {
236
233
  document.querySelectorAll('[data-key]').forEach(el => {
237
234
  const key = el.dataset.key, type = el.dataset.type;
238
235
  let val;
239
- if (type === 'boolean') val = el.checked;
236
+ if (type === 'boolean') { if (el.dataset.unset) return; val = el.checked; }
240
237
  else if (el.value === '') return; // unset → omit (stay minimal)
241
238
  else if (type === 'number') { const n = Number(el.value); if (Number.isNaN(n)) return; val = n; }
242
239
  else val = el.value;