@j0hanz/filesystem-mcp 1.1.2 → 1.2.1
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/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +4 -2
- package/dist/config.js +2 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +15 -8
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +232 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +96 -44
- package/dist/tools/apply-patch.js +23 -22
- package/dist/tools/calculate-hash.js +41 -43
- package/dist/tools/create-directory.js +17 -19
- package/dist/tools/delete-file.js +35 -37
- package/dist/tools/diff-files.js +15 -19
- package/dist/tools/edit-file.js +15 -18
- package/dist/tools/list-directory.js +24 -23
- package/dist/tools/move-file.js +17 -19
- package/dist/tools/read-multiple.js +55 -66
- package/dist/tools/read.js +26 -30
- package/dist/tools/replace-in-files.js +27 -33
- package/dist/tools/roots.js +8 -8
- package/dist/tools/search-content.js +73 -72
- package/dist/tools/search-files.js +44 -50
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +44 -66
- package/dist/tools/stat.js +10 -37
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +12 -28
- package/dist/tools/write-file.js +17 -19
- package/dist/tools.js +23 -18
- package/package.json +6 -7
|
@@ -2,6 +2,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
2
2
|
import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
3
3
|
import { processInParallel, readFile, readFileWithStats, withAbort, } from '../fs-helpers.js';
|
|
4
4
|
import { validateExistingPath } from '../path-validation.js';
|
|
5
|
+
const UNKNOWN_PATH = '(unknown)';
|
|
5
6
|
function estimateReadSize(stats, maxSize) {
|
|
6
7
|
// `readFile`/`readFileWithStats` are always invoked with a `maxSize` cap, so the
|
|
7
8
|
// combined budget should reflect the maximum number of bytes we might actually read.
|
|
@@ -12,45 +13,33 @@ function buildReadOptions(options) {
|
|
|
12
13
|
encoding: options.encoding,
|
|
13
14
|
maxSize: options.maxSize,
|
|
14
15
|
};
|
|
15
|
-
|
|
16
|
-
readOptions.head = options.head;
|
|
17
|
-
}
|
|
18
|
-
if (options.startLine !== undefined) {
|
|
19
|
-
readOptions.startLine = options.startLine;
|
|
20
|
-
}
|
|
21
|
-
if (options.endLine !== undefined) {
|
|
22
|
-
readOptions.endLine = options.endLine;
|
|
23
|
-
}
|
|
16
|
+
applyLineSelection(readOptions, options);
|
|
24
17
|
return readOptions;
|
|
25
18
|
}
|
|
26
19
|
function buildReadMultipleResult(filePath, result) {
|
|
27
|
-
const
|
|
20
|
+
const output = {
|
|
28
21
|
path: filePath,
|
|
29
22
|
content: result.content,
|
|
30
23
|
truncated: result.truncated,
|
|
31
24
|
readMode: result.readMode,
|
|
32
25
|
};
|
|
33
26
|
if (result.totalLines !== undefined)
|
|
34
|
-
|
|
27
|
+
output.totalLines = result.totalLines;
|
|
35
28
|
if (result.head !== undefined)
|
|
36
|
-
|
|
29
|
+
output.head = result.head;
|
|
37
30
|
if (result.startLine !== undefined)
|
|
38
|
-
|
|
31
|
+
output.startLine = result.startLine;
|
|
39
32
|
if (result.endLine !== undefined)
|
|
40
|
-
|
|
33
|
+
output.endLine = result.endLine;
|
|
41
34
|
if (result.linesRead !== undefined)
|
|
42
|
-
|
|
35
|
+
output.linesRead = result.linesRead;
|
|
43
36
|
if (result.hasMoreLines !== undefined) {
|
|
44
|
-
|
|
37
|
+
output.hasMoreLines = result.hasMoreLines;
|
|
45
38
|
}
|
|
46
|
-
return
|
|
39
|
+
return output;
|
|
47
40
|
}
|
|
48
|
-
async function readSingleFile(task,
|
|
41
|
+
async function readSingleFile(task, readOptions) {
|
|
49
42
|
const { filePath, index, validPath, stats } = task;
|
|
50
|
-
const readOptions = buildReadOptions(options);
|
|
51
|
-
if (signal) {
|
|
52
|
-
readOptions.signal = signal;
|
|
53
|
-
}
|
|
54
43
|
const result = validPath && stats
|
|
55
44
|
? await readFileWithStats(filePath, validPath, stats, readOptions)
|
|
56
45
|
: await readFile(filePath, readOptions);
|
|
@@ -60,7 +49,11 @@ async function readSingleFile(task, options, signal) {
|
|
|
60
49
|
};
|
|
61
50
|
}
|
|
62
51
|
async function readFilesInParallel(filesToProcess, options, signal) {
|
|
63
|
-
|
|
52
|
+
const readOptions = buildReadOptions(options);
|
|
53
|
+
if (signal) {
|
|
54
|
+
readOptions.signal = signal;
|
|
55
|
+
}
|
|
56
|
+
return processInParallel(filesToProcess, async (task) => readSingleFile(task, readOptions), PARALLEL_CONCURRENCY, signal);
|
|
64
57
|
}
|
|
65
58
|
function normalizeReadMultipleOptions(options) {
|
|
66
59
|
const normalized = {
|
|
@@ -68,24 +61,23 @@ function normalizeReadMultipleOptions(options) {
|
|
|
68
61
|
maxSize: Math.min(options.maxSize ?? MAX_TEXT_FILE_SIZE, MAX_TEXT_FILE_SIZE),
|
|
69
62
|
maxTotalSize: options.maxTotalSize ?? DEFAULT_READ_MANY_MAX_TOTAL_SIZE,
|
|
70
63
|
};
|
|
71
|
-
|
|
72
|
-
normalized.head = options.head;
|
|
73
|
-
}
|
|
74
|
-
if (options.startLine !== undefined) {
|
|
75
|
-
normalized.startLine = options.startLine;
|
|
76
|
-
}
|
|
77
|
-
if (options.endLine !== undefined) {
|
|
78
|
-
normalized.endLine = options.endLine;
|
|
79
|
-
}
|
|
64
|
+
applyLineSelection(normalized, options);
|
|
80
65
|
return normalized;
|
|
81
66
|
}
|
|
67
|
+
function applyLineSelection(target, source) {
|
|
68
|
+
if (source.head !== undefined)
|
|
69
|
+
target.head = source.head;
|
|
70
|
+
if (source.startLine !== undefined)
|
|
71
|
+
target.startLine = source.startLine;
|
|
72
|
+
if (source.endLine !== undefined)
|
|
73
|
+
target.endLine = source.endLine;
|
|
74
|
+
}
|
|
82
75
|
function resolveNormalizedOptions(options) {
|
|
83
76
|
const { signal, ...rest } = options;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
return resolved;
|
|
77
|
+
return {
|
|
78
|
+
normalized: normalizeReadMultipleOptions(rest),
|
|
79
|
+
...(signal ? { signal } : {}),
|
|
80
|
+
};
|
|
89
81
|
}
|
|
90
82
|
async function validateFile(filePath, index, signal) {
|
|
91
83
|
const validPath = await validateExistingPath(filePath, signal);
|
|
@@ -108,7 +100,7 @@ async function tryValidateFile(filePath, index, signal) {
|
|
|
108
100
|
async function validateBatch(tasks, signal) {
|
|
109
101
|
if (tasks.length === 0)
|
|
110
102
|
return new Map();
|
|
111
|
-
const { results } = await processInParallel(tasks, async (task) =>
|
|
103
|
+
const { results } = await processInParallel(tasks, async (task) => tryValidateFile(task.filePath, task.index, signal), PARALLEL_CONCURRENCY, signal);
|
|
112
104
|
const infos = new Map();
|
|
113
105
|
for (const info of results) {
|
|
114
106
|
if (!info)
|
|
@@ -124,7 +116,9 @@ async function applyBudgetForRange(options) {
|
|
|
124
116
|
const filePath = filePaths[index];
|
|
125
117
|
if (!filePath)
|
|
126
118
|
continue;
|
|
127
|
-
const
|
|
119
|
+
const cached = validated.get(index);
|
|
120
|
+
const info = cached ??
|
|
121
|
+
(await resolveValidatedInfo(filePath, index, validated, signal));
|
|
128
122
|
if (!info)
|
|
129
123
|
continue;
|
|
130
124
|
const { exceeded, totalSize: nextTotalSize } = applyBudget(totalSize, estimateReadSize(info.stats, maxSize), maxTotalSize, index, totalFiles, skippedBudget);
|
|
@@ -176,24 +170,23 @@ async function collectFileBudget(filePaths, maxTotalSize, maxSize, signal) {
|
|
|
176
170
|
return { skippedBudget, validated };
|
|
177
171
|
}
|
|
178
172
|
async function resolveValidatedInfo(filePath, index, validated, signal) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
validated.set(index, info);
|
|
183
|
-
}
|
|
173
|
+
const existing = validated.get(index);
|
|
174
|
+
if (existing) {
|
|
175
|
+
return existing;
|
|
184
176
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
skippedBudget.add(index);
|
|
190
|
-
markRemainingSkipped(index + 1, totalFiles, skippedBudget);
|
|
191
|
-
return { totalSize, exceeded: true };
|
|
177
|
+
const info = await tryValidateFile(filePath, index, signal);
|
|
178
|
+
if (info) {
|
|
179
|
+
validated.set(index, info);
|
|
180
|
+
return info;
|
|
192
181
|
}
|
|
193
|
-
return
|
|
182
|
+
return undefined;
|
|
194
183
|
}
|
|
195
184
|
function buildOutput(filePaths) {
|
|
196
|
-
|
|
185
|
+
const output = new Array(filePaths.length);
|
|
186
|
+
for (let index = 0; index < filePaths.length; index += 1) {
|
|
187
|
+
output[index] = { path: filePaths[index] ?? UNKNOWN_PATH };
|
|
188
|
+
}
|
|
189
|
+
return output;
|
|
197
190
|
}
|
|
198
191
|
function applyResults(output, results) {
|
|
199
192
|
for (const result of results) {
|
|
@@ -219,7 +212,7 @@ function applyErrors(output, errors, filesToProcess, filePaths) {
|
|
|
219
212
|
const originalIndex = resolveErrorOriginalIndex(failure.index, filesToProcess, filePaths.length);
|
|
220
213
|
if (originalIndex === undefined)
|
|
221
214
|
continue;
|
|
222
|
-
const filePath = filePaths[originalIndex] ??
|
|
215
|
+
const filePath = filePaths[originalIndex] ?? UNKNOWN_PATH;
|
|
223
216
|
output[originalIndex] = {
|
|
224
217
|
path: filePath,
|
|
225
218
|
error: failure.error.message,
|
|
@@ -227,19 +220,34 @@ function applyErrors(output, errors, filesToProcess, filePaths) {
|
|
|
227
220
|
}
|
|
228
221
|
}
|
|
229
222
|
function buildFilesToProcess(filePaths, validated, skippedBudget) {
|
|
230
|
-
|
|
231
|
-
|
|
223
|
+
const filesToProcess = [];
|
|
224
|
+
for (let index = 0; index < filePaths.length; index += 1) {
|
|
225
|
+
if (skippedBudget.has(index))
|
|
226
|
+
continue;
|
|
227
|
+
const filePath = filePaths[index];
|
|
228
|
+
if (!filePath)
|
|
229
|
+
continue;
|
|
232
230
|
const cached = validated.get(index);
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
if (cached) {
|
|
232
|
+
filesToProcess.push({
|
|
235
233
|
filePath,
|
|
236
234
|
index,
|
|
237
235
|
validPath: cached.validPath,
|
|
238
236
|
stats: cached.stats,
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
filesToProcess.push({ filePath, index });
|
|
241
|
+
}
|
|
242
|
+
return filesToProcess;
|
|
243
|
+
}
|
|
244
|
+
function applyBudget(totalSize, estimatedSize, maxTotalSize, index, totalFiles, skippedBudget) {
|
|
245
|
+
if (totalSize + estimatedSize > maxTotalSize) {
|
|
246
|
+
skippedBudget.add(index);
|
|
247
|
+
markRemainingSkipped(index + 1, totalFiles, skippedBudget);
|
|
248
|
+
return { totalSize, exceeded: true };
|
|
249
|
+
}
|
|
250
|
+
return { totalSize: totalSize + estimatedSize, exceeded: false };
|
|
243
251
|
}
|
|
244
252
|
function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
|
|
245
253
|
for (const index of skippedBudget) {
|
|
@@ -10,6 +10,7 @@ import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } f
|
|
|
10
10
|
import { assertNotAborted, createTimedAbortSignal, isProbablyBinary, withAbort, } from '../fs-helpers.js';
|
|
11
11
|
import { assertAllowedFileAccess, isSensitivePath } from '../path-policy.js';
|
|
12
12
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
13
|
+
import { withOptionalStoppedReason } from './common.js';
|
|
13
14
|
import { globEntries } from './glob-engine.js';
|
|
14
15
|
// --- Configuration & Schemas ---
|
|
15
16
|
const INTERNAL_MAX_RESULTS = 500;
|
|
@@ -64,6 +65,16 @@ function resolveOptions(options) {
|
|
|
64
65
|
}
|
|
65
66
|
return result.data;
|
|
66
67
|
}
|
|
68
|
+
function countRegexLineMatches(regex, line) {
|
|
69
|
+
regex.lastIndex = 0;
|
|
70
|
+
let count = 0;
|
|
71
|
+
while (regex.exec(line) !== null) {
|
|
72
|
+
count++;
|
|
73
|
+
if (regex.lastIndex === 0)
|
|
74
|
+
regex.lastIndex++;
|
|
75
|
+
}
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
67
78
|
function escapeLiteral(pattern) {
|
|
68
79
|
return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
80
|
}
|
|
@@ -85,16 +96,7 @@ function buildLiteralMatcher(pattern, options) {
|
|
|
85
96
|
if (!options.caseSensitive) {
|
|
86
97
|
const final = escapeLiteral(pattern);
|
|
87
98
|
const regex = new RegExp(final, 'gi');
|
|
88
|
-
return (line) =>
|
|
89
|
-
regex.lastIndex = 0;
|
|
90
|
-
let count = 0;
|
|
91
|
-
while (regex.exec(line) !== null) {
|
|
92
|
-
count++;
|
|
93
|
-
if (regex.lastIndex === 0)
|
|
94
|
-
regex.lastIndex++;
|
|
95
|
-
}
|
|
96
|
-
return count;
|
|
97
|
-
};
|
|
99
|
+
return (line) => countRegexLineMatches(regex, line);
|
|
98
100
|
}
|
|
99
101
|
// Fast path for case-sensitive literal
|
|
100
102
|
const needle = pattern;
|
|
@@ -114,16 +116,7 @@ function buildLiteralMatcher(pattern, options) {
|
|
|
114
116
|
}
|
|
115
117
|
function buildRegexMatcher(final, caseSensitive) {
|
|
116
118
|
const regex = new RE2(final, caseSensitive ? 'g' : 'gi');
|
|
117
|
-
return (line) =>
|
|
118
|
-
regex.lastIndex = 0;
|
|
119
|
-
let count = 0;
|
|
120
|
-
while (regex.exec(line) !== null) {
|
|
121
|
-
count++;
|
|
122
|
-
if (regex.lastIndex === 0)
|
|
123
|
-
regex.lastIndex++; // Avoid infinite loop on zero-width match
|
|
124
|
-
}
|
|
125
|
-
return count;
|
|
126
|
-
};
|
|
119
|
+
return (line) => countRegexLineMatches(regex, line);
|
|
127
120
|
}
|
|
128
121
|
export function buildMatcher(pattern, options) {
|
|
129
122
|
if (options.isLiteral && pattern.length === 0)
|
|
@@ -213,7 +206,8 @@ function trimContent(line) {
|
|
|
213
206
|
}
|
|
214
207
|
async function readMatches(handle, requestedPath, matcher, options, maxMatches, isCancelled, signal) {
|
|
215
208
|
const matches = [];
|
|
216
|
-
const
|
|
209
|
+
const hasContext = options.contextLines > 0;
|
|
210
|
+
const ctx = hasContext ? new ContextBuffer(options.contextLines) : undefined;
|
|
217
211
|
let lineNumber = 1;
|
|
218
212
|
// Use for-await with readLines for memory efficiency
|
|
219
213
|
const lines = handle.readLines({ encoding: 'utf-8', signal });
|
|
@@ -224,22 +218,34 @@ async function readMatches(handle, requestedPath, matcher, options, maxMatches,
|
|
|
224
218
|
if (isCancelled())
|
|
225
219
|
break;
|
|
226
220
|
const matchCount = matcher(rawLine);
|
|
227
|
-
|
|
221
|
+
let content;
|
|
222
|
+
const getContent = () => {
|
|
223
|
+
content ??= trimContent(rawLine);
|
|
224
|
+
return content;
|
|
225
|
+
};
|
|
228
226
|
if (matchCount > 0) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
227
|
+
if (ctx) {
|
|
228
|
+
matches.push({
|
|
229
|
+
file: requestedPath,
|
|
230
|
+
line: lineNumber,
|
|
231
|
+
content: getContent(),
|
|
232
|
+
matchCount,
|
|
233
|
+
contextBefore: ctx.snapshotBefore(),
|
|
234
|
+
contextAfter: ctx.scheduleAfter(),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
matches.push({
|
|
239
|
+
file: requestedPath,
|
|
240
|
+
line: lineNumber,
|
|
241
|
+
content: getContent(),
|
|
242
|
+
matchCount,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (ctx) {
|
|
247
|
+
ctx.add(getContent());
|
|
241
248
|
}
|
|
242
|
-
ctx.add(content);
|
|
243
249
|
lineNumber++;
|
|
244
250
|
}
|
|
245
251
|
}
|
|
@@ -304,24 +310,22 @@ function createScanSummary() {
|
|
|
304
310
|
};
|
|
305
311
|
}
|
|
306
312
|
function buildSearchResult(root, pattern, filePattern, matches, summary) {
|
|
313
|
+
const baseSummary = {
|
|
314
|
+
filesScanned: summary.filesScanned,
|
|
315
|
+
filesMatched: summary.filesMatched,
|
|
316
|
+
matches: matches.length,
|
|
317
|
+
truncated: summary.truncated,
|
|
318
|
+
skippedTooLarge: summary.skippedTooLarge,
|
|
319
|
+
skippedBinary: summary.skippedBinary,
|
|
320
|
+
skippedInaccessible: summary.skippedInaccessible,
|
|
321
|
+
linesSkippedDueToRegexTimeout: 0,
|
|
322
|
+
};
|
|
307
323
|
return {
|
|
308
324
|
basePath: root,
|
|
309
325
|
pattern,
|
|
310
326
|
filePattern,
|
|
311
327
|
matches,
|
|
312
|
-
summary:
|
|
313
|
-
filesScanned: summary.filesScanned,
|
|
314
|
-
filesMatched: summary.filesMatched,
|
|
315
|
-
matches: matches.length,
|
|
316
|
-
truncated: summary.truncated,
|
|
317
|
-
skippedTooLarge: summary.skippedTooLarge,
|
|
318
|
-
skippedBinary: summary.skippedBinary,
|
|
319
|
-
skippedInaccessible: summary.skippedInaccessible,
|
|
320
|
-
linesSkippedDueToRegexTimeout: 0,
|
|
321
|
-
...(summary.stoppedReason
|
|
322
|
-
? { stoppedReason: summary.stoppedReason }
|
|
323
|
-
: {}),
|
|
324
|
-
},
|
|
328
|
+
summary: withOptionalStoppedReason(baseSummary, summary.stoppedReason),
|
|
325
329
|
};
|
|
326
330
|
}
|
|
327
331
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -448,9 +452,15 @@ class SearchWorkerPool {
|
|
|
448
452
|
for (const p of this.pending.values())
|
|
449
453
|
p.reject(new Error(ERROR_WORKER_POOL_CLOSED));
|
|
450
454
|
this.pending.clear();
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
455
|
+
const terminations = [];
|
|
456
|
+
for (let index = 0; index < this.workers.length; index += 1) {
|
|
457
|
+
const worker = this.workers[index];
|
|
458
|
+
if (!worker)
|
|
459
|
+
continue;
|
|
460
|
+
terminations.push(worker.terminate());
|
|
461
|
+
this.workers[index] = undefined;
|
|
462
|
+
}
|
|
463
|
+
await Promise.all(terminations);
|
|
454
464
|
}
|
|
455
465
|
}
|
|
456
466
|
function isWorkerPoolAvailable() {
|
|
@@ -545,9 +555,14 @@ function processScanResult(winner, summary, matches, maxResults) {
|
|
|
545
555
|
summary.skippedBinary++;
|
|
546
556
|
if (res.skippedTooLarge)
|
|
547
557
|
summary.skippedTooLarge++;
|
|
548
|
-
const
|
|
549
|
-
if (
|
|
550
|
-
|
|
558
|
+
const remaining = maxResults - matches.length;
|
|
559
|
+
if (remaining > 0 && res.matches.length > 0) {
|
|
560
|
+
const take = Math.min(remaining, res.matches.length);
|
|
561
|
+
for (let index = 0; index < take; index += 1) {
|
|
562
|
+
const match = res.matches[index];
|
|
563
|
+
if (match)
|
|
564
|
+
matches.push(match);
|
|
565
|
+
}
|
|
551
566
|
}
|
|
552
567
|
}
|
|
553
568
|
}
|
|
@@ -559,14 +574,17 @@ function reportSearchProgress(onProgress, current, total, force = false) {
|
|
|
559
574
|
onProgress({ current, total });
|
|
560
575
|
}
|
|
561
576
|
async function waitForWinner(pending) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
task:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
577
|
+
const raceCandidates = [];
|
|
578
|
+
for (const task of pending) {
|
|
579
|
+
raceCandidates.push(task.promise.then((result) => ({ task, result, error: undefined }), (err) => ({
|
|
580
|
+
task,
|
|
581
|
+
result: undefined,
|
|
582
|
+
error: err instanceof Error
|
|
583
|
+
? err
|
|
584
|
+
: new Error(formatUnknownErrorMessage(err)),
|
|
585
|
+
})));
|
|
586
|
+
}
|
|
587
|
+
return Promise.race(raceCandidates);
|
|
570
588
|
}
|
|
571
589
|
async function executeParallel(files, pattern, opts, signal, summary) {
|
|
572
590
|
const pool = getPool();
|
|
@@ -661,12 +679,13 @@ export async function searchContent(basePath, pattern, options = {}) {
|
|
|
661
679
|
}, signal, opts.maxResults);
|
|
662
680
|
if (result.matched)
|
|
663
681
|
summary.filesMatched = 1;
|
|
664
|
-
return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern,
|
|
682
|
+
return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
665
683
|
}
|
|
666
684
|
if (!stats.isDirectory()) {
|
|
667
685
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Path must be file or directory`, basePath);
|
|
668
686
|
}
|
|
669
687
|
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
688
|
+
const rootDirectories = [root];
|
|
670
689
|
// Glob
|
|
671
690
|
const stream = globEntries({
|
|
672
691
|
cwd: root,
|
|
@@ -693,7 +712,7 @@ export async function searchContent(basePath, pattern, options = {}) {
|
|
|
693
712
|
// Helper to resolve
|
|
694
713
|
// We duplicate simple resolution logic to keep it fast
|
|
695
714
|
const normalized = normalizePath(entry.path);
|
|
696
|
-
if (!isPathWithinDirectories(normalized,
|
|
715
|
+
if (!isPathWithinDirectories(normalized, rootDirectories))
|
|
697
716
|
continue;
|
|
698
717
|
if (isSensitivePath(entry.path, normalized))
|
|
699
718
|
continue;
|
|
@@ -3,8 +3,9 @@ import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constan
|
|
|
3
3
|
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
4
|
import { isSensitivePath } from '../path-policy.js';
|
|
5
5
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
|
+
import { needsStatsForSort, withOptionalStoppedReason } from './common.js';
|
|
6
7
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
7
|
-
import { globEntries } from './glob-engine.js';
|
|
8
|
+
import { globEntries, resolveEntryType } from './glob-engine.js';
|
|
8
9
|
// Internal default for find tool - not exposed to MCP users
|
|
9
10
|
const INTERNAL_MAX_RESULTS = 1000;
|
|
10
11
|
function normalizeOptions(options) {
|
|
@@ -23,15 +24,6 @@ function normalizeOptions(options) {
|
|
|
23
24
|
}
|
|
24
25
|
return normalized;
|
|
25
26
|
}
|
|
26
|
-
function resolveEntryType(dirent) {
|
|
27
|
-
if (dirent.isDirectory())
|
|
28
|
-
return 'directory';
|
|
29
|
-
if (dirent.isSymbolicLink())
|
|
30
|
-
return 'symlink';
|
|
31
|
-
if (dirent.isFile())
|
|
32
|
-
return 'file';
|
|
33
|
-
return 'other';
|
|
34
|
-
}
|
|
35
27
|
function buildSearchResult(entry, entryType, needsStats) {
|
|
36
28
|
let resolvedType = 'other';
|
|
37
29
|
if (entryType === 'directory') {
|
|
@@ -49,9 +41,6 @@ function buildSearchResult(entry, entryType, needsStats) {
|
|
|
49
41
|
...(modified !== undefined ? { modified } : {}),
|
|
50
42
|
};
|
|
51
43
|
}
|
|
52
|
-
function needsStatsForSort(sortBy) {
|
|
53
|
-
return sortBy === 'size' || sortBy === 'modified';
|
|
54
|
-
}
|
|
55
44
|
function markStopped(state, reason) {
|
|
56
45
|
state.truncated = true;
|
|
57
46
|
state.stoppedReason = reason;
|
|
@@ -108,8 +97,6 @@ function buildCollectResult(state) {
|
|
|
108
97
|
return outcome;
|
|
109
98
|
}
|
|
110
99
|
function handleEntry(entry, entryType, needsStats, normalized, state) {
|
|
111
|
-
if (!shouldIncludeEntry(entryType, normalized))
|
|
112
|
-
return;
|
|
113
100
|
state.results.push(buildSearchResult(entry, entryType, needsStats));
|
|
114
101
|
if (state.results.length >= normalized.maxResults) {
|
|
115
102
|
markStopped(state, 'maxResults');
|
|
@@ -122,21 +109,20 @@ function reportSearchFilesProgress(onProgress, current, total, force = false) {
|
|
|
122
109
|
return;
|
|
123
110
|
onProgress({ current, total });
|
|
124
111
|
}
|
|
125
|
-
async function collectFromStream(stream, root, gitignoreMatcher, normalized, needsStats, state, signal, onProgress) {
|
|
112
|
+
async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress) {
|
|
126
113
|
for await (const entry of stream) {
|
|
127
114
|
if (shouldStopCollecting(state, normalized, signal))
|
|
128
115
|
break;
|
|
129
116
|
state.filesScanned++;
|
|
130
117
|
reportSearchFilesProgress(onProgress, state.filesScanned, normalized.maxFilesScanned);
|
|
131
|
-
if (gitignoreMatcher
|
|
132
|
-
isIgnoredByGitignore(gitignoreMatcher, root, entry.path)) {
|
|
118
|
+
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path)) {
|
|
133
119
|
continue;
|
|
134
120
|
}
|
|
135
121
|
const entryType = resolveEntryType(entry.dirent);
|
|
136
122
|
if (!shouldIncludeEntry(entryType, normalized)) {
|
|
137
123
|
continue;
|
|
138
124
|
}
|
|
139
|
-
const isAccessible = await isEntryAccessible(entry, entryType,
|
|
125
|
+
const isAccessible = await isEntryAccessible(entry, entryType, rootDirectories, signal);
|
|
140
126
|
if (!isAccessible) {
|
|
141
127
|
state.skippedInaccessible++;
|
|
142
128
|
continue;
|
|
@@ -147,7 +133,12 @@ async function collectFromStream(stream, root, gitignoreMatcher, normalized, nee
|
|
|
147
133
|
}
|
|
148
134
|
reportSearchFilesProgress(onProgress, state.filesScanned, normalized.maxFilesScanned, true);
|
|
149
135
|
}
|
|
150
|
-
|
|
136
|
+
function isEntryIgnoredByGitignore(matcher, root, entryPath) {
|
|
137
|
+
if (!matcher)
|
|
138
|
+
return false;
|
|
139
|
+
return isIgnoredByGitignore(matcher, root, entryPath);
|
|
140
|
+
}
|
|
141
|
+
async function isEntryAccessible(entry, entryType, rootDirectories, signal) {
|
|
151
142
|
if (entryType === 'symlink') {
|
|
152
143
|
try {
|
|
153
144
|
const validated = await validateExistingPathDetailed(entry.path, signal);
|
|
@@ -158,7 +149,7 @@ async function isEntryAccessible(entry, entryType, root, signal) {
|
|
|
158
149
|
}
|
|
159
150
|
}
|
|
160
151
|
const resolvedPath = normalizePath(entry.path);
|
|
161
|
-
if (!isPathWithinDirectories(resolvedPath,
|
|
152
|
+
if (!isPathWithinDirectories(resolvedPath, rootDirectories)) {
|
|
162
153
|
return false;
|
|
163
154
|
}
|
|
164
155
|
return !isSensitivePath(entry.path, resolvedPath);
|
|
@@ -167,10 +158,11 @@ async function collectSearchResults(root, pattern, excludePatterns, normalized,
|
|
|
167
158
|
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
168
159
|
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
169
160
|
const state = createCollectState();
|
|
161
|
+
const rootDirectories = [root];
|
|
170
162
|
const gitignoreMatcher = normalized.respectGitignore
|
|
171
163
|
? await loadRootGitignore(root, signal)
|
|
172
164
|
: null;
|
|
173
|
-
await collectFromStream(stream, root, gitignoreMatcher, normalized, needsStats, state, signal, onProgress);
|
|
165
|
+
await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress);
|
|
174
166
|
return buildCollectResult(state);
|
|
175
167
|
}
|
|
176
168
|
function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
|
|
@@ -179,9 +171,8 @@ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, ski
|
|
|
179
171
|
truncated,
|
|
180
172
|
skippedInaccessible,
|
|
181
173
|
filesScanned,
|
|
182
|
-
...(stoppedReason !== undefined ? { stoppedReason } : {}),
|
|
183
174
|
};
|
|
184
|
-
return summary;
|
|
175
|
+
return withOptionalStoppedReason(summary, stoppedReason);
|
|
185
176
|
}
|
|
186
177
|
const collator = new Intl.Collator(undefined, { numeric: true });
|
|
187
178
|
function compareString(a, b) {
|
|
@@ -213,11 +204,17 @@ const SORT_COMPARATORS = {
|
|
|
213
204
|
};
|
|
214
205
|
export function sortSearchResults(results, sortBy) {
|
|
215
206
|
if (sortBy === 'name') {
|
|
216
|
-
const decorated =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
207
|
+
const decorated = [];
|
|
208
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
209
|
+
const item = results[index];
|
|
210
|
+
if (!item)
|
|
211
|
+
continue;
|
|
212
|
+
decorated.push({
|
|
213
|
+
item,
|
|
214
|
+
baseName: path.basename(item.path ?? ''),
|
|
215
|
+
index,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
221
218
|
decorated.sort((a, b) => {
|
|
222
219
|
const baseCompare = compareString(a.baseName, b.baseName);
|
|
223
220
|
if (baseCompare !== 0)
|
|
@@ -227,7 +224,12 @@ export function sortSearchResults(results, sortBy) {
|
|
|
227
224
|
return pathCompare;
|
|
228
225
|
return a.index - b.index;
|
|
229
226
|
});
|
|
230
|
-
|
|
227
|
+
for (let index = 0; index < decorated.length; index += 1) {
|
|
228
|
+
const entry = decorated[index];
|
|
229
|
+
if (entry) {
|
|
230
|
+
results[index] = entry.item;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
233
235
|
const comparator = SORT_COMPARATORS[sortBy];
|