@j0hanz/filesystem-mcp 1.7.2 → 1.7.3
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/dist/lib/file-operations/common.d.ts +42 -0
- package/dist/lib/file-operations/common.js +87 -0
- package/dist/lib/file-operations/file-info.js +13 -19
- package/dist/lib/file-operations/glob-engine.d.ts +1 -6
- package/dist/lib/file-operations/glob-engine.js +0 -9
- package/dist/lib/file-operations/list-directory.js +19 -39
- package/dist/lib/file-operations/read-multiple-files.js +11 -19
- package/dist/lib/file-operations/search-content.js +106 -113
- package/dist/lib/file-operations/search-files.js +28 -72
- package/dist/lib/file-operations/tree.d.ts +2 -2
- package/dist/lib/file-operations/tree.js +20 -32
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +14 -14
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +5 -5
- package/dist/resources/workflows.js +17 -17
- package/dist/schemas.js +21 -90
- package/dist/server/bootstrap.js +2 -2
- package/dist/tools/list-directory.js +3 -21
- package/dist/tools/read-multiple.js +21 -19
- package/dist/tools/search-files.js +3 -21
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/stat-many.js +20 -20
- package/package.json +1 -1
|
@@ -2,3 +2,45 @@ export declare function needsStatsForSort(sortBy: string): boolean;
|
|
|
2
2
|
export declare function withOptionalStoppedReason<T extends object, R extends string>(summary: T, stoppedReason: R | undefined): T | (T & {
|
|
3
3
|
stoppedReason: R;
|
|
4
4
|
});
|
|
5
|
+
export interface DirentLike {
|
|
6
|
+
isDirectory(): boolean;
|
|
7
|
+
isFile(): boolean;
|
|
8
|
+
isSymbolicLink(): boolean;
|
|
9
|
+
}
|
|
10
|
+
export type EntryType = 'file' | 'directory' | 'symlink' | 'other';
|
|
11
|
+
export interface IndexedValue<T> {
|
|
12
|
+
index: number;
|
|
13
|
+
value: T;
|
|
14
|
+
}
|
|
15
|
+
export interface IndexedError {
|
|
16
|
+
index: number;
|
|
17
|
+
error: Error;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveEntryType(dirent: DirentLike): EntryType;
|
|
20
|
+
export declare function resolveStopReason<R extends string>(options: {
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
current: number;
|
|
23
|
+
max: number;
|
|
24
|
+
abortedReason: R;
|
|
25
|
+
maxReason: R;
|
|
26
|
+
}): R | undefined;
|
|
27
|
+
export declare function compareStringValues(left?: string, right?: string): number;
|
|
28
|
+
export declare function compareOptionalNumberDesc(left: number | undefined, right: number | undefined, tieBreak: () => number): number;
|
|
29
|
+
export declare function stableSortByDerivedString<T>(items: T[], derive: (item: T) => string, tieBreak: (left: T, right: T) => number): void;
|
|
30
|
+
export declare function applyIndexedValues<T>(output: T[], results: readonly IndexedValue<T>[]): void;
|
|
31
|
+
export declare function applyIndexedErrors<T>(options: {
|
|
32
|
+
output: T[];
|
|
33
|
+
errors: readonly IndexedError[];
|
|
34
|
+
resolveIndex: (failureIndex: number) => number | undefined;
|
|
35
|
+
buildValue: (resolvedIndex: number, error: Error) => T;
|
|
36
|
+
}): void;
|
|
37
|
+
export interface EntryAccessDependencies {
|
|
38
|
+
normalizePath: (inputPath: string) => string;
|
|
39
|
+
isPathWithinDirectories: (normalizedPath: string, rootDirectories: readonly string[]) => boolean;
|
|
40
|
+
isSensitivePath: (requestedPath: string, resolvedPath: string) => boolean;
|
|
41
|
+
validateSymlinkPath: (inputPath: string, signal: AbortSignal) => Promise<{
|
|
42
|
+
requestedPath: string;
|
|
43
|
+
resolvedPath: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export declare function isEntryAccessibleByType(entryPath: string, entryType: EntryType, rootDirectories: readonly string[], signal: AbortSignal, deps: EntryAccessDependencies): Promise<boolean>;
|
|
@@ -1,9 +1,96 @@
|
|
|
1
1
|
export function needsStatsForSort(sortBy) {
|
|
2
2
|
return sortBy === 'size' || sortBy === 'modified';
|
|
3
3
|
}
|
|
4
|
+
const collator = new Intl.Collator(undefined, { numeric: true });
|
|
4
5
|
export function withOptionalStoppedReason(summary, stoppedReason) {
|
|
5
6
|
if (stoppedReason === undefined) {
|
|
6
7
|
return summary;
|
|
7
8
|
}
|
|
8
9
|
return { ...summary, stoppedReason };
|
|
9
10
|
}
|
|
11
|
+
export function resolveEntryType(dirent) {
|
|
12
|
+
if (dirent.isSymbolicLink())
|
|
13
|
+
return 'symlink';
|
|
14
|
+
if (dirent.isDirectory())
|
|
15
|
+
return 'directory';
|
|
16
|
+
if (dirent.isFile())
|
|
17
|
+
return 'file';
|
|
18
|
+
return 'other';
|
|
19
|
+
}
|
|
20
|
+
export function resolveStopReason(options) {
|
|
21
|
+
if (options.signal.aborted)
|
|
22
|
+
return options.abortedReason;
|
|
23
|
+
if (options.current >= options.max)
|
|
24
|
+
return options.maxReason;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export function compareStringValues(left, right) {
|
|
28
|
+
return collator.compare(left ?? '', right ?? '');
|
|
29
|
+
}
|
|
30
|
+
export function compareOptionalNumberDesc(left, right, tieBreak) {
|
|
31
|
+
const diff = (right ?? 0) - (left ?? 0);
|
|
32
|
+
if (diff !== 0)
|
|
33
|
+
return diff;
|
|
34
|
+
return tieBreak();
|
|
35
|
+
}
|
|
36
|
+
export function stableSortByDerivedString(items, derive, tieBreak) {
|
|
37
|
+
const decorated = [];
|
|
38
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
39
|
+
const item = items[index];
|
|
40
|
+
if (item === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
decorated.push({
|
|
43
|
+
item,
|
|
44
|
+
derived: derive(item),
|
|
45
|
+
index,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
decorated.sort((left, right) => {
|
|
49
|
+
const derivedCompare = compareStringValues(left.derived, right.derived);
|
|
50
|
+
if (derivedCompare !== 0)
|
|
51
|
+
return derivedCompare;
|
|
52
|
+
const tiedCompare = tieBreak(left.item, right.item);
|
|
53
|
+
if (tiedCompare !== 0)
|
|
54
|
+
return tiedCompare;
|
|
55
|
+
return left.index - right.index;
|
|
56
|
+
});
|
|
57
|
+
for (let index = 0; index < decorated.length; index += 1) {
|
|
58
|
+
const entry = decorated[index];
|
|
59
|
+
if (!entry)
|
|
60
|
+
continue;
|
|
61
|
+
items[index] = entry.item;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function applyIndexedValues(output, results) {
|
|
65
|
+
for (const result of results) {
|
|
66
|
+
if (result.index < 0 || result.index >= output.length)
|
|
67
|
+
continue;
|
|
68
|
+
output[result.index] = result.value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function applyIndexedErrors(options) {
|
|
72
|
+
for (const failure of options.errors) {
|
|
73
|
+
const resolvedIndex = options.resolveIndex(failure.index);
|
|
74
|
+
if (resolvedIndex === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
if (resolvedIndex < 0 || resolvedIndex >= options.output.length)
|
|
77
|
+
continue;
|
|
78
|
+
options.output[resolvedIndex] = options.buildValue(resolvedIndex, failure.error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function isEntryAccessibleByType(entryPath, entryType, rootDirectories, signal, deps) {
|
|
82
|
+
if (entryType !== 'symlink') {
|
|
83
|
+
const normalizedPath = deps.normalizePath(entryPath);
|
|
84
|
+
if (!deps.isPathWithinDirectories(normalizedPath, rootDirectories)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return !deps.isSensitivePath(entryPath, normalizedPath);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const validated = await deps.validateSymlinkPath(entryPath, signal);
|
|
91
|
+
return !deps.isSensitivePath(validated.requestedPath, validated.resolvedPath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -5,6 +5,7 @@ import { isAbortError } from '../errors.js';
|
|
|
5
5
|
import { assertNotAborted, getFileType, isHidden, processInParallel, withAbort, } from '../fs-helpers.js';
|
|
6
6
|
import { assertAllowedFileAccess } from '../path-policy.js';
|
|
7
7
|
import { validateExistingPathDetailed } from '../path-validation.js';
|
|
8
|
+
import { applyIndexedErrors, applyIndexedValues } from './common.js';
|
|
8
9
|
const PERM_STRINGS = [
|
|
9
10
|
'---',
|
|
10
11
|
'--x',
|
|
@@ -95,23 +96,6 @@ async function readFileInfoInParallel(paths, options) {
|
|
|
95
96
|
return { index, value };
|
|
96
97
|
}, PARALLEL_CONCURRENCY, options.signal);
|
|
97
98
|
}
|
|
98
|
-
function applyResults(output, results) {
|
|
99
|
-
for (const result of results) {
|
|
100
|
-
output[result.index] = result.value;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function applyErrors(output, errors, paths) {
|
|
104
|
-
for (const failure of errors) {
|
|
105
|
-
const { index } = failure;
|
|
106
|
-
if (!isValidOutputIndex(index, output.length))
|
|
107
|
-
continue;
|
|
108
|
-
const filePath = paths[index] ?? UNKNOWN_PATH;
|
|
109
|
-
output[index] = { path: filePath, error: failure.error.message };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function isValidOutputIndex(index, length) {
|
|
113
|
-
return index >= 0 && index < length;
|
|
114
|
-
}
|
|
115
99
|
function calculateSummary(results) {
|
|
116
100
|
let succeeded = 0;
|
|
117
101
|
let failed = 0;
|
|
@@ -140,8 +124,18 @@ export async function getMultipleFileInfo(paths, options = {}) {
|
|
|
140
124
|
output[index] = { path: paths[index] ?? UNKNOWN_PATH };
|
|
141
125
|
}
|
|
142
126
|
const { results, errors } = await readFileInfoInParallel(paths, options);
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
applyIndexedValues(output, results);
|
|
128
|
+
applyIndexedErrors({
|
|
129
|
+
output,
|
|
130
|
+
errors,
|
|
131
|
+
resolveIndex: (failureIndex) => failureIndex >= 0 && failureIndex < output.length
|
|
132
|
+
? failureIndex
|
|
133
|
+
: undefined,
|
|
134
|
+
buildValue: (resolvedIndex, error) => ({
|
|
135
|
+
path: paths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
136
|
+
error: error.message,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
145
139
|
return {
|
|
146
140
|
results: output,
|
|
147
141
|
summary: calculateSummary(output),
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { Stats } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
isDirectory(): boolean;
|
|
4
|
-
isFile(): boolean;
|
|
5
|
-
isSymbolicLink(): boolean;
|
|
6
|
-
}
|
|
7
|
-
export declare function resolveEntryType(dirent: DirentLike): 'file' | 'directory' | 'symlink' | 'other';
|
|
2
|
+
import type { DirentLike } from './common.js';
|
|
8
3
|
interface GlobEntry {
|
|
9
4
|
path: string;
|
|
10
5
|
relativePath?: string;
|
|
@@ -4,15 +4,6 @@ import { glob as fsGlob } from 'node:fs/promises';
|
|
|
4
4
|
import { getToolContextSnapshot, publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, startPerfMeasure, } from '../observability.js';
|
|
5
5
|
import { toPosixPath } from '../path-format.js';
|
|
6
6
|
import { isRecord } from '../type-guards.js';
|
|
7
|
-
export function resolveEntryType(dirent) {
|
|
8
|
-
if (dirent.isDirectory())
|
|
9
|
-
return 'directory';
|
|
10
|
-
if (dirent.isSymbolicLink())
|
|
11
|
-
return 'symlink';
|
|
12
|
-
if (dirent.isFile())
|
|
13
|
-
return 'file';
|
|
14
|
-
return 'other';
|
|
15
|
-
}
|
|
16
7
|
const GLOB_MAGIC_RE = /[*?[\]{}!]/u;
|
|
17
8
|
const DEFAULT_MAX_HIDDEN_DEPTH = 10;
|
|
18
9
|
const GLOB_BATCH_CONCURRENCY = 64;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fsp from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_SEARCH_TIMEOUT_MS, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
4
|
-
import { createTimedAbortSignal, processInParallel, withAbort, } from '../fs-helpers.js';
|
|
4
|
+
import { createTimedAbortSignal, isHidden, processInParallel, withAbort, } from '../fs-helpers.js';
|
|
5
5
|
import { isSensitivePath } from '../path-policy.js';
|
|
6
6
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
7
|
-
import { needsStatsForSort, withOptionalStoppedReason } from './common.js';
|
|
8
|
-
import { globEntries
|
|
7
|
+
import { isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './common.js';
|
|
8
|
+
import { globEntries } from './glob-engine.js';
|
|
9
9
|
function normalizePattern(pattern) {
|
|
10
10
|
if (!pattern || pattern.length === 0)
|
|
11
11
|
return undefined;
|
|
@@ -40,18 +40,11 @@ function resolveMaxDepth(normalized) {
|
|
|
40
40
|
}
|
|
41
41
|
return normalized.maxDepth;
|
|
42
42
|
}
|
|
43
|
-
function getStopReason(signal, acceptedCount, maxEntries) {
|
|
44
|
-
if (signal.aborted)
|
|
45
|
-
return 'aborted';
|
|
46
|
-
if (acceptedCount >= maxEntries)
|
|
47
|
-
return 'maxEntries';
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
43
|
async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
51
44
|
const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
|
|
52
45
|
const entries = [];
|
|
53
46
|
for (const dirent of dirents) {
|
|
54
|
-
if (!normalized.includeHidden && dirent.name
|
|
47
|
+
if (!normalized.includeHidden && isHidden(dirent.name)) {
|
|
55
48
|
continue;
|
|
56
49
|
}
|
|
57
50
|
entries.push({ dirent, entryPath: path.join(basePath, dirent.name) });
|
|
@@ -149,32 +142,6 @@ function trackSymlink(entryType, includeSymlinkTargets, counters) {
|
|
|
149
142
|
counters.symlinksNotFollowed += 1;
|
|
150
143
|
}
|
|
151
144
|
}
|
|
152
|
-
async function isEntryAccessible(entryPath, entryType, basePathDirectories, signal, counters) {
|
|
153
|
-
if (entryType !== 'symlink') {
|
|
154
|
-
const normalized = normalizePath(entryPath);
|
|
155
|
-
if (!isPathWithinDirectories(normalized, basePathDirectories)) {
|
|
156
|
-
counters.skippedInaccessible += 1;
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
if (isSensitivePath(entryPath, normalized)) {
|
|
160
|
-
counters.skippedInaccessible += 1;
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
const validated = await validateExistingPathDetailed(entryPath, signal);
|
|
167
|
-
if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
|
|
168
|
-
counters.skippedInaccessible += 1;
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
counters.skippedInaccessible += 1;
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
145
|
function appendEntry(entry, entryType, symlinkTarget, ctx) {
|
|
179
146
|
updateTotals(entryType, ctx.totals);
|
|
180
147
|
ctx.entries.push(buildDirectoryEntry(ctx.basePath, entry, entryType, ctx.needsStats, symlinkTarget));
|
|
@@ -214,6 +181,12 @@ async function collectEntries(basePath, normalized, signal, needsStats, maxDepth
|
|
|
214
181
|
const totals = { files: 0, directories: 0 };
|
|
215
182
|
const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
|
|
216
183
|
const basePathDirectories = [basePath];
|
|
184
|
+
const accessDeps = {
|
|
185
|
+
normalizePath,
|
|
186
|
+
isPathWithinDirectories,
|
|
187
|
+
isSensitivePath,
|
|
188
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
189
|
+
};
|
|
217
190
|
let truncated = false;
|
|
218
191
|
let stoppedReason;
|
|
219
192
|
const pending = [];
|
|
@@ -232,7 +205,13 @@ async function collectEntries(basePath, normalized, signal, needsStats, maxDepth
|
|
|
232
205
|
entries,
|
|
233
206
|
};
|
|
234
207
|
for await (const entry of stream) {
|
|
235
|
-
const stopReason =
|
|
208
|
+
const stopReason = resolveStopReason({
|
|
209
|
+
signal,
|
|
210
|
+
current: acceptedCount,
|
|
211
|
+
max: normalized.maxEntries,
|
|
212
|
+
abortedReason: 'aborted',
|
|
213
|
+
maxReason: 'maxEntries',
|
|
214
|
+
});
|
|
236
215
|
if (stopReason) {
|
|
237
216
|
truncated = true;
|
|
238
217
|
stoppedReason = stopReason;
|
|
@@ -240,8 +219,9 @@ async function collectEntries(basePath, normalized, signal, needsStats, maxDepth
|
|
|
240
219
|
}
|
|
241
220
|
const entryType = resolveEntryType(entry.dirent);
|
|
242
221
|
trackSymlink(entryType, normalized.includeSymlinkTargets, counters);
|
|
243
|
-
const accessible = await
|
|
222
|
+
const accessible = await isEntryAccessibleByType(entry.path, entryType, basePathDirectories, signal, accessDeps);
|
|
244
223
|
if (!accessible) {
|
|
224
|
+
counters.skippedInaccessible += 1;
|
|
245
225
|
continue;
|
|
246
226
|
}
|
|
247
227
|
acceptedCount += 1;
|
|
@@ -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
|
+
import { applyIndexedErrors, applyIndexedValues } from './common.js';
|
|
5
6
|
const UNKNOWN_PATH = '(unknown)';
|
|
6
7
|
function estimateReadSize(stats, maxSize) {
|
|
7
8
|
// `readFile`/`readFileWithStats` are always invoked with a `maxSize` cap, so the
|
|
@@ -173,11 +174,6 @@ function buildOutput(filePaths) {
|
|
|
173
174
|
}
|
|
174
175
|
return output;
|
|
175
176
|
}
|
|
176
|
-
function applyResults(output, results) {
|
|
177
|
-
for (const result of results) {
|
|
178
|
-
output[result.index] = result.value;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
177
|
function resolveErrorOriginalIndex(failureIndex, filesToProcess, totalInputFiles) {
|
|
182
178
|
// processInParallel implementations vary: some return error indices relative to
|
|
183
179
|
// the submitted batch (filesToProcess), others may forward the task/index.
|
|
@@ -192,18 +188,6 @@ function resolveErrorOriginalIndex(failureIndex, filesToProcess, totalInputFiles
|
|
|
192
188
|
}
|
|
193
189
|
return undefined;
|
|
194
190
|
}
|
|
195
|
-
function applyErrors(output, errors, filesToProcess, filePaths) {
|
|
196
|
-
for (const failure of errors) {
|
|
197
|
-
const originalIndex = resolveErrorOriginalIndex(failure.index, filesToProcess, filePaths.length);
|
|
198
|
-
if (originalIndex === undefined)
|
|
199
|
-
continue;
|
|
200
|
-
const filePath = filePaths[originalIndex] ?? UNKNOWN_PATH;
|
|
201
|
-
output[originalIndex] = {
|
|
202
|
-
path: filePath,
|
|
203
|
-
error: failure.error.message,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
191
|
function buildFilesToProcess(filePaths, validated, skippedBudget) {
|
|
208
192
|
const filesToProcess = [];
|
|
209
193
|
for (let index = 0; index < filePaths.length; index += 1) {
|
|
@@ -253,8 +237,16 @@ export async function readMultipleFiles(filePaths, options = {}) {
|
|
|
253
237
|
const { skippedBudget, validated } = await collectFileBudget(filePaths, normalized.maxTotalSize, normalized.maxSize, signal);
|
|
254
238
|
const filesToProcess = buildFilesToProcess(filePaths, validated, skippedBudget);
|
|
255
239
|
const { results, errors } = await readFilesInParallel(filesToProcess, normalized, signal, options.onReadComplete);
|
|
256
|
-
|
|
257
|
-
|
|
240
|
+
applyIndexedValues(output, results);
|
|
241
|
+
applyIndexedErrors({
|
|
242
|
+
output,
|
|
243
|
+
errors,
|
|
244
|
+
resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
|
|
245
|
+
buildValue: (resolvedIndex, error) => ({
|
|
246
|
+
path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
247
|
+
error: error.message,
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
258
250
|
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
259
251
|
return output;
|
|
260
252
|
}
|
|
@@ -291,6 +291,25 @@ async function scanFileResolved(resolvedPath, requestedPath, matcher, options, s
|
|
|
291
291
|
await handle.close();
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
+
function buildScanFileOptions(opts) {
|
|
295
|
+
return {
|
|
296
|
+
maxFileSize: opts.maxFileSize,
|
|
297
|
+
skipBinary: opts.skipBinary,
|
|
298
|
+
contextLines: opts.contextLines,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function applyScanOutcome(summary, outcome) {
|
|
302
|
+
if (outcome.matched)
|
|
303
|
+
summary.filesMatched++;
|
|
304
|
+
if (outcome.skippedBinary)
|
|
305
|
+
summary.skippedBinary++;
|
|
306
|
+
if (outcome.skippedTooLarge)
|
|
307
|
+
summary.skippedTooLarge++;
|
|
308
|
+
}
|
|
309
|
+
function markTruncated(summary, reason) {
|
|
310
|
+
summary.truncated = true;
|
|
311
|
+
summary.stoppedReason = reason;
|
|
312
|
+
}
|
|
294
313
|
function createScanSummary() {
|
|
295
314
|
return {
|
|
296
315
|
filesScanned: 0,
|
|
@@ -474,32 +493,21 @@ function getPool() {
|
|
|
474
493
|
async function executeSequential(files, pattern, opts, signal, summary) {
|
|
475
494
|
const matches = [];
|
|
476
495
|
const matcher = buildMatcher(pattern, opts);
|
|
477
|
-
const scanOpts =
|
|
478
|
-
maxFileSize: opts.maxFileSize,
|
|
479
|
-
skipBinary: opts.skipBinary,
|
|
480
|
-
contextLines: opts.contextLines,
|
|
481
|
-
};
|
|
496
|
+
const scanOpts = buildScanFileOptions(opts);
|
|
482
497
|
for await (const file of files) {
|
|
483
498
|
if (signal.aborted) {
|
|
484
|
-
summary
|
|
485
|
-
summary.stoppedReason = 'timeout';
|
|
499
|
+
markTruncated(summary, 'timeout');
|
|
486
500
|
break;
|
|
487
501
|
}
|
|
488
502
|
if (matches.length >= opts.maxResults) {
|
|
489
|
-
summary
|
|
490
|
-
summary.stoppedReason = 'maxResults';
|
|
503
|
+
markTruncated(summary, 'maxResults');
|
|
491
504
|
break;
|
|
492
505
|
}
|
|
493
506
|
try {
|
|
494
507
|
assertAllowedFileAccess(file.requestedPath, file.resolvedPath);
|
|
495
508
|
const remaining = opts.maxResults - matches.length;
|
|
496
509
|
const result = await scanFileResolved(file.resolvedPath, file.requestedPath, matcher, scanOpts, signal, remaining);
|
|
497
|
-
|
|
498
|
-
summary.filesMatched++;
|
|
499
|
-
if (result.skippedBinary)
|
|
500
|
-
summary.skippedBinary++;
|
|
501
|
-
if (result.skippedTooLarge)
|
|
502
|
-
summary.skippedTooLarge++;
|
|
510
|
+
applyScanOutcome(summary, result);
|
|
503
511
|
matches.push(...result.matches);
|
|
504
512
|
}
|
|
505
513
|
catch {
|
|
@@ -542,12 +550,7 @@ function processScanResult(winner, summary, matches, maxResults) {
|
|
|
542
550
|
}
|
|
543
551
|
if (winner.result) {
|
|
544
552
|
const res = winner.result;
|
|
545
|
-
|
|
546
|
-
summary.filesMatched++;
|
|
547
|
-
if (res.skippedBinary)
|
|
548
|
-
summary.skippedBinary++;
|
|
549
|
-
if (res.skippedTooLarge)
|
|
550
|
-
summary.skippedTooLarge++;
|
|
553
|
+
applyScanOutcome(summary, res);
|
|
551
554
|
const remaining = maxResults - matches.length;
|
|
552
555
|
if (remaining > 0 && res.matches.length > 0) {
|
|
553
556
|
const take = Math.min(remaining, res.matches.length);
|
|
@@ -582,11 +585,7 @@ async function waitForWinner(pending) {
|
|
|
582
585
|
async function executeParallel(files, pattern, opts, signal, summary) {
|
|
583
586
|
const pool = getPool();
|
|
584
587
|
const matches = [];
|
|
585
|
-
const scanOpts =
|
|
586
|
-
maxFileSize: opts.maxFileSize,
|
|
587
|
-
skipBinary: opts.skipBinary,
|
|
588
|
-
contextLines: opts.contextLines,
|
|
589
|
-
};
|
|
588
|
+
const scanOpts = buildScanFileOptions(opts);
|
|
590
589
|
const matcherOpts = {
|
|
591
590
|
caseSensitive: opts.caseSensitive,
|
|
592
591
|
wholeWord: opts.wholeWord,
|
|
@@ -596,8 +595,7 @@ async function executeParallel(files, pattern, opts, signal, summary) {
|
|
|
596
595
|
const iterator = files[Symbol.asyncIterator]();
|
|
597
596
|
let exhausted = false;
|
|
598
597
|
const onAbort = () => {
|
|
599
|
-
summary
|
|
600
|
-
summary.stoppedReason = 'timeout';
|
|
598
|
+
markTruncated(summary, 'timeout');
|
|
601
599
|
for (const t of pending)
|
|
602
600
|
t.cancel();
|
|
603
601
|
};
|
|
@@ -629,12 +627,10 @@ async function executeParallel(files, pattern, opts, signal, summary) {
|
|
|
629
627
|
}
|
|
630
628
|
// Update summary truncation
|
|
631
629
|
if (signal.aborted) {
|
|
632
|
-
summary
|
|
633
|
-
summary.stoppedReason = 'timeout';
|
|
630
|
+
markTruncated(summary, 'timeout');
|
|
634
631
|
}
|
|
635
632
|
else if (matches.length >= opts.maxResults) {
|
|
636
|
-
summary
|
|
637
|
-
summary.stoppedReason = 'maxResults';
|
|
633
|
+
markTruncated(summary, 'maxResults');
|
|
638
634
|
}
|
|
639
635
|
return matches;
|
|
640
636
|
}
|
|
@@ -649,6 +645,80 @@ export async function scanFileInWorker(resolvedPath, requestedPath, matcher, opt
|
|
|
649
645
|
skippedTooLarge: res.skippedTooLarge,
|
|
650
646
|
};
|
|
651
647
|
}
|
|
648
|
+
async function searchSingleFile(details, opts, pattern, signal) {
|
|
649
|
+
const summary = createScanSummary();
|
|
650
|
+
summary.filesScanned = 1;
|
|
651
|
+
const matcher = buildMatcher(pattern, opts);
|
|
652
|
+
const result = await scanFileResolved(details.resolvedPath, details.requestedPath, matcher, {
|
|
653
|
+
...buildScanFileOptions(opts),
|
|
654
|
+
}, signal, opts.maxResults);
|
|
655
|
+
if (result.matched)
|
|
656
|
+
summary.filesMatched = 1;
|
|
657
|
+
return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
658
|
+
}
|
|
659
|
+
async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
660
|
+
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
661
|
+
const rootDirectories = [root];
|
|
662
|
+
const stream = globEntries({
|
|
663
|
+
cwd: root,
|
|
664
|
+
pattern: opts.filePattern,
|
|
665
|
+
excludePatterns: opts.excludePatterns,
|
|
666
|
+
includeHidden: opts.includeHidden,
|
|
667
|
+
baseNameMatch: opts.baseNameMatch,
|
|
668
|
+
caseSensitiveMatch: opts.caseSensitiveFileMatch,
|
|
669
|
+
followSymbolicLinks: false,
|
|
670
|
+
onlyFiles: true,
|
|
671
|
+
stats: false,
|
|
672
|
+
suppressErrors: true,
|
|
673
|
+
});
|
|
674
|
+
async function* fileGenerator() {
|
|
675
|
+
let scanned = 0;
|
|
676
|
+
for await (const entry of stream) {
|
|
677
|
+
if (signal.aborted)
|
|
678
|
+
break;
|
|
679
|
+
if (scanned >= opts.maxFilesScanned)
|
|
680
|
+
break;
|
|
681
|
+
if (!entry.dirent.isFile())
|
|
682
|
+
continue;
|
|
683
|
+
const normalized = normalizePath(entry.path);
|
|
684
|
+
if (!isPathWithinDirectories(normalized, rootDirectories))
|
|
685
|
+
continue;
|
|
686
|
+
if (isSensitivePath(entry.path, normalized))
|
|
687
|
+
continue;
|
|
688
|
+
scanned++;
|
|
689
|
+
reportSearchProgress(onProgress, scanned, opts.maxFilesScanned);
|
|
690
|
+
yield { resolvedPath: normalized, requestedPath: entry.path };
|
|
691
|
+
}
|
|
692
|
+
reportSearchProgress(onProgress, scanned, opts.maxFilesScanned, true);
|
|
693
|
+
}
|
|
694
|
+
const summary = createScanSummary();
|
|
695
|
+
const resolvedStream = fileGenerator();
|
|
696
|
+
async function* countingStream() {
|
|
697
|
+
for await (const f of resolvedStream) {
|
|
698
|
+
summary.filesScanned++;
|
|
699
|
+
yield f;
|
|
700
|
+
}
|
|
701
|
+
if (summary.filesScanned >= opts.maxFilesScanned) {
|
|
702
|
+
summary.truncated = true;
|
|
703
|
+
summary.stoppedReason = 'maxFiles';
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const matcherOpts = {
|
|
707
|
+
caseSensitive: opts.caseSensitive,
|
|
708
|
+
wholeWord: opts.wholeWord,
|
|
709
|
+
isLiteral: opts.isLiteral,
|
|
710
|
+
};
|
|
711
|
+
validatePattern(pattern, matcherOpts);
|
|
712
|
+
const matches = shouldUseWorkers()
|
|
713
|
+
? await executeParallel(countingStream(), pattern, opts, signal, summary)
|
|
714
|
+
: await executeSequential(countingStream(), pattern, opts, signal, summary);
|
|
715
|
+
return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
|
|
716
|
+
}
|
|
717
|
+
function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
718
|
+
const timeoutSummary = createScanSummary();
|
|
719
|
+
markTruncated(timeoutSummary, 'timeout');
|
|
720
|
+
return buildSearchResult(basePath, pattern, filePattern, [], timeoutSummary);
|
|
721
|
+
}
|
|
652
722
|
export async function searchContent(basePath, pattern, options = {}) {
|
|
653
723
|
if (!basePath.trim())
|
|
654
724
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'basePath required');
|
|
@@ -659,94 +729,17 @@ export async function searchContent(basePath, pattern, options = {}) {
|
|
|
659
729
|
try {
|
|
660
730
|
const details = await validateExistingPathDetailed(basePath, signal);
|
|
661
731
|
const stats = await withAbort(fsp.stat(details.resolvedPath), signal);
|
|
662
|
-
// Check if simple file scan
|
|
663
732
|
if (stats.isFile()) {
|
|
664
|
-
|
|
665
|
-
summary.filesScanned = 1;
|
|
666
|
-
// Single file execution
|
|
667
|
-
const matcher = buildMatcher(pattern, opts);
|
|
668
|
-
const result = await scanFileResolved(details.resolvedPath, details.requestedPath, matcher, {
|
|
669
|
-
maxFileSize: opts.maxFileSize,
|
|
670
|
-
skipBinary: opts.skipBinary,
|
|
671
|
-
contextLines: opts.contextLines,
|
|
672
|
-
}, signal, opts.maxResults);
|
|
673
|
-
if (result.matched)
|
|
674
|
-
summary.filesMatched = 1;
|
|
675
|
-
return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
733
|
+
return await searchSingleFile(details, opts, pattern, signal);
|
|
676
734
|
}
|
|
677
735
|
if (!stats.isDirectory()) {
|
|
678
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT,
|
|
736
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Path must be file or directory', basePath);
|
|
679
737
|
}
|
|
680
|
-
|
|
681
|
-
const rootDirectories = [root];
|
|
682
|
-
// Glob
|
|
683
|
-
const stream = globEntries({
|
|
684
|
-
cwd: root,
|
|
685
|
-
pattern: opts.filePattern,
|
|
686
|
-
excludePatterns: opts.excludePatterns,
|
|
687
|
-
includeHidden: opts.includeHidden,
|
|
688
|
-
baseNameMatch: opts.baseNameMatch,
|
|
689
|
-
caseSensitiveMatch: opts.caseSensitiveFileMatch,
|
|
690
|
-
followSymbolicLinks: false,
|
|
691
|
-
onlyFiles: true,
|
|
692
|
-
stats: false,
|
|
693
|
-
suppressErrors: true,
|
|
694
|
-
});
|
|
695
|
-
// Generator adapter to resolve paths
|
|
696
|
-
async function* fileGenerator() {
|
|
697
|
-
let scanned = 0;
|
|
698
|
-
for await (const entry of stream) {
|
|
699
|
-
if (signal.aborted)
|
|
700
|
-
break;
|
|
701
|
-
if (scanned >= opts.maxFilesScanned)
|
|
702
|
-
break;
|
|
703
|
-
if (!entry.dirent.isFile())
|
|
704
|
-
continue;
|
|
705
|
-
// Helper to resolve
|
|
706
|
-
// We duplicate simple resolution logic to keep it fast
|
|
707
|
-
const normalized = normalizePath(entry.path);
|
|
708
|
-
if (!isPathWithinDirectories(normalized, rootDirectories))
|
|
709
|
-
continue;
|
|
710
|
-
if (isSensitivePath(entry.path, normalized))
|
|
711
|
-
continue;
|
|
712
|
-
scanned++;
|
|
713
|
-
reportSearchProgress(options.onProgress, scanned, opts.maxFilesScanned);
|
|
714
|
-
yield { resolvedPath: normalized, requestedPath: entry.path };
|
|
715
|
-
}
|
|
716
|
-
reportSearchProgress(options.onProgress, scanned, opts.maxFilesScanned, true);
|
|
717
|
-
}
|
|
718
|
-
// Choose Strategy
|
|
719
|
-
// We recreate summary to track actual scans
|
|
720
|
-
const summary = createScanSummary();
|
|
721
|
-
const resolvedStream = fileGenerator();
|
|
722
|
-
// Wrap generator to count scanned files in summary
|
|
723
|
-
async function* countingStream() {
|
|
724
|
-
for await (const f of resolvedStream) {
|
|
725
|
-
summary.filesScanned++;
|
|
726
|
-
yield f;
|
|
727
|
-
}
|
|
728
|
-
if (summary.filesScanned >= opts.maxFilesScanned) {
|
|
729
|
-
summary.truncated = true;
|
|
730
|
-
summary.stoppedReason = 'maxFiles';
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
const matcherOpts = {
|
|
734
|
-
caseSensitive: opts.caseSensitive,
|
|
735
|
-
wholeWord: opts.wholeWord,
|
|
736
|
-
isLiteral: opts.isLiteral,
|
|
737
|
-
};
|
|
738
|
-
validatePattern(pattern, matcherOpts);
|
|
739
|
-
const matches = shouldUseWorkers()
|
|
740
|
-
? await executeParallel(countingStream(), pattern, opts, signal, summary)
|
|
741
|
-
: await executeSequential(countingStream(), pattern, opts, signal, summary);
|
|
742
|
-
return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
|
|
738
|
+
return await searchDirectory(details, opts, pattern, signal, options.onProgress);
|
|
743
739
|
}
|
|
744
740
|
catch (error) {
|
|
745
741
|
if (isTimeoutLikeError(error)) {
|
|
746
|
-
|
|
747
|
-
timeoutSummary.truncated = true;
|
|
748
|
-
timeoutSummary.stoppedReason = 'timeout';
|
|
749
|
-
return buildSearchResult(basePath, pattern, opts.filePattern, [], timeoutSummary);
|
|
742
|
+
return buildTimeoutSearchResult(basePath, pattern, opts.filePattern);
|
|
750
743
|
}
|
|
751
744
|
throw error;
|
|
752
745
|
}
|