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.
- package/CHANGELOG.md +65 -1
- package/README.md +60 -33
- package/dist/app.js +46 -22
- package/dist/src/chunking/AstChunker.d.ts +12 -0
- package/dist/src/chunking/AstChunker.js +30 -0
- package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
- package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
- package/dist/src/chunking/FragmentMerger.d.ts +11 -0
- package/dist/src/chunking/FragmentMerger.js +61 -0
- package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
- package/dist/src/chunking/ImportOnlyFilter.js +14 -0
- package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
- package/dist/src/chunking/LangchainTextSplitter.js +133 -0
- package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
- package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
- package/dist/src/chunking/RunPacker.d.ts +12 -0
- package/dist/src/chunking/RunPacker.js +84 -0
- package/dist/src/chunking/create-chunker.d.ts +3 -0
- package/dist/src/chunking/create-chunker.js +15 -0
- package/dist/src/chunking/interfaces.d.ts +27 -0
- package/dist/src/chunking/interfaces.js +2 -0
- package/dist/src/commands/ConfigCommand.js +1 -9
- package/dist/src/commands/EvalCommand.d.ts +11 -0
- package/dist/src/commands/EvalCommand.js +264 -0
- package/dist/src/commands/IndexCommand.js +70 -66
- package/dist/src/commands/McpCommand.js +2 -8
- package/dist/src/commands/WebCommand.js +2 -5
- package/dist/src/config-error.d.ts +0 -4
- package/dist/src/config-error.js +0 -4
- package/dist/src/config-store.d.ts +1 -54
- package/dist/src/config-store.js +8 -69
- package/dist/src/config.d.ts +8 -17
- package/dist/src/config.js +35 -34
- package/dist/src/constants.d.ts +2 -28
- package/dist/src/constants.js +6 -28
- package/dist/src/container.d.ts +6 -0
- package/dist/src/container.js +12 -3
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +22 -29
- package/dist/src/db/schema.js +3 -14
- package/dist/src/declarations/DeclarationTree.d.ts +19 -0
- package/dist/src/declarations/DeclarationTree.js +66 -0
- package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
- package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
- package/dist/src/declarations/SourceLines.d.ts +37 -0
- package/dist/src/declarations/SourceLines.js +415 -0
- package/dist/src/dependency-traversal.d.ts +10 -0
- package/dist/src/dependency-traversal.js +39 -0
- package/dist/src/enriched-prompt.d.ts +0 -5
- package/dist/src/enriched-prompt.js +0 -5
- package/dist/src/error-record.d.ts +12 -0
- package/dist/src/error-record.js +248 -0
- package/dist/src/eval-corpus.d.ts +16 -0
- package/dist/src/eval-corpus.js +169 -0
- package/dist/src/eval-results.d.ts +15 -0
- package/dist/src/eval-results.js +397 -0
- package/dist/src/file-utils.d.ts +10 -0
- package/dist/src/file-utils.js +19 -1
- package/dist/src/graph.d.ts +0 -25
- package/dist/src/graph.js +0 -28
- package/dist/src/help.js +4 -1
- package/dist/src/hnsw-settings.d.ts +3 -0
- package/dist/src/hnsw-settings.js +24 -0
- package/dist/src/interfaces/IAstParser.d.ts +5 -30
- package/dist/src/interfaces/IChunker.d.ts +3 -12
- package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
- package/dist/src/interfaces/ICorpusInspector.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +2 -36
- package/dist/src/interfaces/IEmbedder.d.ts +0 -9
- package/dist/src/interfaces/ILogger.d.ts +1 -11
- package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
- package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
- package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +26 -20
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/interrupt.js +0 -2
- package/dist/src/owner-path.d.ts +9 -10
- package/dist/src/owner-path.js +11 -23
- package/dist/src/project-scope.d.ts +7 -0
- package/dist/src/project-scope.js +37 -0
- package/dist/src/project-selector.d.ts +1 -0
- package/dist/src/project-selector.js +13 -6
- package/dist/src/retrieval-eval.d.ts +24 -0
- package/dist/src/retrieval-eval.js +500 -0
- package/dist/src/search/result-shaping.d.ts +4 -0
- package/dist/src/search/result-shaping.js +19 -0
- package/dist/src/services/AstParserService.d.ts +28 -23
- package/dist/src/services/AstParserService.js +391 -98
- package/dist/src/services/DatabaseService.d.ts +2 -19
- package/dist/src/services/DatabaseService.js +44 -81
- package/dist/src/services/EmbedderFactory.d.ts +0 -4
- package/dist/src/services/EmbedderFactory.js +0 -5
- package/dist/src/services/IndexerService.d.ts +2 -10
- package/dist/src/services/IndexerService.js +29 -45
- package/dist/src/services/InfinityEmbedder.d.ts +0 -21
- package/dist/src/services/InfinityEmbedder.js +0 -15
- package/dist/src/services/LoggerService.d.ts +7 -5
- package/dist/src/services/LoggerService.js +37 -41
- package/dist/src/services/McpServer.d.ts +25 -10
- package/dist/src/services/McpServer.js +101 -64
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
- package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
- package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
- package/dist/src/services/NxMonorepoResolver.js +5 -56
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
- package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
- package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
- package/dist/src/services/ProjectDiscoveryService.js +0 -4
- package/dist/src/services/SearcherService.d.ts +127 -70
- package/dist/src/services/SearcherService.js +526 -305
- package/dist/src/services/VoyageEmbedder.d.ts +0 -6
- package/dist/src/services/VoyageEmbedder.js +0 -6
- package/dist/src/services/WebServerService.d.ts +1 -53
- package/dist/src/services/WebServerService.js +11 -57
- package/dist/src/services/http-retry.d.ts +0 -7
- package/dist/src/services/http-retry.js +1 -10
- package/dist/src/services/infinity-provisioning.d.ts +0 -74
- package/dist/src/services/infinity-provisioning.js +5 -88
- package/dist/src/services/legacy-ollama-index.d.ts +0 -16
- package/dist/src/services/legacy-ollama-index.js +0 -23
- package/dist/src/services/reranker.d.ts +0 -9
- package/dist/src/services/reranker.js +46 -12
- package/dist/src/setup.d.ts +11 -23
- package/dist/src/setup.js +98 -71
- package/dist/src/symbol-name.d.ts +0 -12
- package/dist/src/symbol-name.js +0 -13
- package/dist/src/symbol-rows.d.ts +2 -11
- package/dist/src/symbol-rows.js +5 -13
- package/dist/src/tool-call-log.d.ts +2 -0
- package/dist/src/tool-call-log.js +24 -0
- 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 +7 -2
- package/dist/src/tools/format-helpers.d.ts +10 -1
- package/dist/src/tools/format-helpers.js +145 -8
- package/dist/src/web/dashboard.d.ts +0 -8
- package/dist/src/web/dashboard.js +7 -10
- package/package.json +10 -9
- package/dist/src/services/AstChunker.d.ts +0 -42
- package/dist/src/services/AstChunker.js +0 -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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
19
|
-
|
|
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'
|
|
23
|
+
required: ['file_path'],
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
async handle(args) {
|
|
28
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
41
|
-
if (
|
|
42
|
-
return (0, format_helpers_1.textResponse)(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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'
|
|
23
|
+
required: ['path'],
|
|
23
24
|
},
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
async handle(args) {
|
|
27
|
-
const
|
|
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
|
|
33
|
-
if (
|
|
34
|
-
return (0, format_helpers_1.
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
30
|
-
if (!
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return (0, format_helpers_1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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;
|