@j0hanz/filesystem-mcp 1.13.2 → 1.14.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 +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
|
@@ -50,22 +50,21 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import * as fsp from 'node:fs/promises';
|
|
54
|
-
import * as path from 'node:path';
|
|
55
53
|
import { AsyncResource } from 'node:async_hooks';
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
54
|
+
import { open, stat } from 'node:fs/promises';
|
|
55
|
+
import { basename, dirname } from 'node:path';
|
|
58
56
|
import { debuglog } from 'node:util';
|
|
59
57
|
import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
|
|
60
58
|
import RE2 from 're2';
|
|
61
|
-
import safeRegex from 'safe-regex2';
|
|
62
59
|
import { z } from 'zod';
|
|
60
|
+
import { assertNotAborted, withAbort, withTimedAbortSignal } from '../abort.js';
|
|
63
61
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
|
|
64
62
|
import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
|
|
65
|
-
import {
|
|
63
|
+
import { isProbablyBinary } from '../fs-helpers.js';
|
|
64
|
+
import { isSafeGlobPattern } from '../globs.js';
|
|
66
65
|
import { startPerfMeasure } from '../observability.js';
|
|
67
66
|
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
|
|
68
|
-
import { mergeOptions, omitOptionKeys
|
|
67
|
+
import { mergeOptions, omitOptionKeys } from '../utils.js';
|
|
69
68
|
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
|
|
70
69
|
import { buildGlobOptions, globEntries } from './traversal.js';
|
|
71
70
|
function countRegexLineMatches(regex, line) {
|
|
@@ -85,20 +84,10 @@ function buildRegexPattern(pattern, options) {
|
|
|
85
84
|
const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
|
|
86
85
|
return options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
87
86
|
}
|
|
88
|
-
function validatePattern(pattern, options) {
|
|
89
|
-
if (options.isLiteral && pattern.length === 0)
|
|
90
|
-
return;
|
|
91
|
-
if (options.isLiteral && !options.wholeWord)
|
|
92
|
-
return;
|
|
93
|
-
const final = buildRegexPattern(pattern, options);
|
|
94
|
-
if (!safeRegex(final)) {
|
|
95
|
-
throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${pattern}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
87
|
function buildLiteralMatcher(pattern, options) {
|
|
99
88
|
if (!options.caseSensitive) {
|
|
100
89
|
const final = escapeLiteral(pattern);
|
|
101
|
-
const regex = new
|
|
90
|
+
const regex = new RE2(final, 'gi');
|
|
102
91
|
return (line) => countRegexLineMatches(regex, line);
|
|
103
92
|
}
|
|
104
93
|
// Fast path for case-sensitive literal
|
|
@@ -117,10 +106,8 @@ function buildLiteralMatcher(pattern, options) {
|
|
|
117
106
|
return count;
|
|
118
107
|
};
|
|
119
108
|
}
|
|
120
|
-
function buildRegexMatcher(final, caseSensitive
|
|
121
|
-
|
|
122
|
-
if (multiline)
|
|
123
|
-
flags += 'm';
|
|
109
|
+
function buildRegexMatcher(final, caseSensitive) {
|
|
110
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
124
111
|
const regex = new RE2(final, flags);
|
|
125
112
|
return (line) => countRegexLineMatches(regex, line);
|
|
126
113
|
}
|
|
@@ -132,13 +119,19 @@ function buildMatcher(pattern, options) {
|
|
|
132
119
|
return buildLiteralMatcher(pattern, options);
|
|
133
120
|
}
|
|
134
121
|
const final = buildRegexPattern(pattern, options);
|
|
135
|
-
|
|
136
|
-
return buildRegexMatcher(final, options.caseSensitive, options.multiline);
|
|
122
|
+
return buildRegexMatcher(final, options.caseSensitive);
|
|
137
123
|
}
|
|
138
124
|
// --- Configuration & Schemas ---
|
|
139
125
|
const SEARCH_CONTENT_MAX_RESULTS = 500;
|
|
126
|
+
const SafeFilePatternSchema = z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1, 'Pattern required')
|
|
129
|
+
.max(1000, 'Max 1000 chars')
|
|
130
|
+
.refine((value) => isSafeGlobPattern(value), {
|
|
131
|
+
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
132
|
+
});
|
|
140
133
|
const SearchOptionsSchema = z.strictObject({
|
|
141
|
-
filePattern:
|
|
134
|
+
filePattern: SafeFilePatternSchema,
|
|
142
135
|
excludePatterns: z.array(z.string()),
|
|
143
136
|
caseSensitive: z.boolean(),
|
|
144
137
|
maxResults: z.int().min(0),
|
|
@@ -149,7 +142,6 @@ const SearchOptionsSchema = z.strictObject({
|
|
|
149
142
|
contextLines: z.int().min(0),
|
|
150
143
|
wholeWord: z.boolean(),
|
|
151
144
|
isLiteral: z.boolean(),
|
|
152
|
-
multiline: z.boolean(),
|
|
153
145
|
includeHidden: z.boolean(),
|
|
154
146
|
baseNameMatch: z.boolean(),
|
|
155
147
|
caseSensitiveFileMatch: z.boolean(),
|
|
@@ -166,14 +158,12 @@ const DEFAULTS = {
|
|
|
166
158
|
contextLines: 0,
|
|
167
159
|
wholeWord: false,
|
|
168
160
|
isLiteral: true,
|
|
169
|
-
multiline: false,
|
|
170
161
|
includeHidden: false,
|
|
171
162
|
baseNameMatch: false,
|
|
172
163
|
caseSensitiveFileMatch: true,
|
|
173
164
|
};
|
|
174
165
|
const ERROR_SCAN_CANCELLED = 'Scan cancelled';
|
|
175
166
|
const ERROR_WORKER_POOL_CLOSED = 'Worker pool closed';
|
|
176
|
-
const SEARCH_PROGRESS_THROTTLE_MODULO = 25;
|
|
177
167
|
const SEARCH_WORKER_NAME_PREFIX = 'filesystem-search';
|
|
178
168
|
const SEARCH_WORKER_RESOURCE_TYPE = 'SearchWorkerTask';
|
|
179
169
|
// --- Helpers ---
|
|
@@ -182,7 +172,7 @@ function resolveOptions(options) {
|
|
|
182
172
|
const merged = mergeOptions(DEFAULTS, normalizedOptions);
|
|
183
173
|
const result = SearchOptionsSchema.safeParse(merged);
|
|
184
174
|
if (!result.success) {
|
|
185
|
-
throw new McpError(ErrorCode.
|
|
175
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Invalid search options:\n${z.prettifyError(result.error)}`, undefined, { errors: z.treeifyError(result.error) });
|
|
186
176
|
}
|
|
187
177
|
return result.data;
|
|
188
178
|
}
|
|
@@ -258,9 +248,31 @@ class ContextBuffer {
|
|
|
258
248
|
}
|
|
259
249
|
function trimContent(line) {
|
|
260
250
|
return line.length > MAX_LINE_CONTENT_LENGTH
|
|
261
|
-
? line.slice(0, MAX_LINE_CONTENT_LENGTH)
|
|
251
|
+
? `${line.slice(0, MAX_LINE_CONTENT_LENGTH)}\u2026`
|
|
262
252
|
: line;
|
|
263
253
|
}
|
|
254
|
+
function processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx) {
|
|
255
|
+
const trimmedLine = trimContent(rawLine);
|
|
256
|
+
if (ctx) {
|
|
257
|
+
matches.push({
|
|
258
|
+
file: requestedPath,
|
|
259
|
+
line: lineNumber,
|
|
260
|
+
content: trimmedLine,
|
|
261
|
+
matchCount,
|
|
262
|
+
contextBefore: ctx.snapshotBefore(),
|
|
263
|
+
contextAfter: ctx.scheduleAfter(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
matches.push({
|
|
268
|
+
file: requestedPath,
|
|
269
|
+
line: lineNumber,
|
|
270
|
+
content: trimmedLine,
|
|
271
|
+
matchCount,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return trimmedLine;
|
|
275
|
+
}
|
|
264
276
|
async function readMatches(handle, requestedPath, matcher, options, maxMatches, isCancelled, signal) {
|
|
265
277
|
if (maxMatches <= 0) {
|
|
266
278
|
return [];
|
|
@@ -278,26 +290,12 @@ async function readMatches(handle, requestedPath, matcher, options, maxMatches,
|
|
|
278
290
|
if (isCancelled())
|
|
279
291
|
break;
|
|
280
292
|
const matchCount = matcher(rawLine);
|
|
281
|
-
|
|
293
|
+
let trimmedLine = '';
|
|
282
294
|
if (matchCount > 0) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
content: trimmedLine,
|
|
288
|
-
matchCount,
|
|
289
|
-
contextBefore: ctx.snapshotBefore(),
|
|
290
|
-
contextAfter: ctx.scheduleAfter(),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
matches.push({
|
|
295
|
-
file: requestedPath,
|
|
296
|
-
line: lineNumber,
|
|
297
|
-
content: trimmedLine,
|
|
298
|
-
matchCount,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
295
|
+
trimmedLine = processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx);
|
|
296
|
+
}
|
|
297
|
+
else if (hasContext) {
|
|
298
|
+
trimmedLine = trimContent(rawLine);
|
|
301
299
|
}
|
|
302
300
|
if (ctx) {
|
|
303
301
|
ctx.add(trimmedLine);
|
|
@@ -319,7 +317,7 @@ async function scanFileResolved(resolvedPath, requestedPath, matcher, options, s
|
|
|
319
317
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
320
318
|
try {
|
|
321
319
|
assertNotAborted(signal);
|
|
322
|
-
const handle = __addDisposableResource(env_1, await withAbort(
|
|
320
|
+
const handle = __addDisposableResource(env_1, await withAbort(open(resolvedPath, 'r'), signal), true);
|
|
323
321
|
const stats = await withAbort(handle.stat(), signal);
|
|
324
322
|
if (stats.size > options.maxFileSize) {
|
|
325
323
|
return {
|
|
@@ -370,7 +368,6 @@ function buildMatcherOptions(opts) {
|
|
|
370
368
|
caseSensitive: opts.caseSensitive,
|
|
371
369
|
wholeWord: opts.wholeWord,
|
|
372
370
|
isLiteral: opts.isLiteral,
|
|
373
|
-
multiline: opts.multiline,
|
|
374
371
|
};
|
|
375
372
|
}
|
|
376
373
|
function applyScanOutcome(summary, outcome) {
|
|
@@ -405,7 +402,6 @@ function buildSearchContentResult(root, pattern, filePattern, matches, summary)
|
|
|
405
402
|
skippedTooLarge: summary.skippedTooLarge,
|
|
406
403
|
skippedBinary: summary.skippedBinary,
|
|
407
404
|
skippedInaccessible: summary.skippedInaccessible,
|
|
408
|
-
linesSkippedDueToRegexTimeout: 0,
|
|
409
405
|
};
|
|
410
406
|
return {
|
|
411
407
|
basePath: root,
|
|
@@ -415,12 +411,9 @@ function buildSearchContentResult(root, pattern, filePattern, matches, summary)
|
|
|
415
411
|
summary: withOptionalStoppedReason(baseSummary, summary.stoppedReason),
|
|
416
412
|
};
|
|
417
413
|
}
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const WORKER_SCRIPT_PATH = path.join(currentDir, isSourceContext ? 'search-worker.ts' : 'search-worker.js');
|
|
422
|
-
const WORKER_SCRIPT_URL = pathToFileURL(WORKER_SCRIPT_PATH);
|
|
423
|
-
const hasWorkerScript = existsSync(WORKER_SCRIPT_PATH);
|
|
414
|
+
const isSourceContext = import.meta.url.endsWith('.ts');
|
|
415
|
+
const WORKER_SCRIPT_URL = new URL(import.meta.url);
|
|
416
|
+
const hasWorkerScript = true;
|
|
424
417
|
class SearchWorkerTaskResource extends AsyncResource {
|
|
425
418
|
#settled = false;
|
|
426
419
|
constructor() {
|
|
@@ -524,9 +517,21 @@ class SearchWorkerPool {
|
|
|
524
517
|
if (this.closed)
|
|
525
518
|
throw new Error(ERROR_WORKER_POOL_CLOSED);
|
|
526
519
|
const id = this.nextRequestId++;
|
|
527
|
-
|
|
520
|
+
let workerIndex = 0;
|
|
521
|
+
const workerPendingCounts = new Array(this.size).fill(0);
|
|
522
|
+
for (const p of this.pending.values()) {
|
|
523
|
+
const idx = p.workerIndex;
|
|
524
|
+
workerPendingCounts[idx] = (workerPendingCounts[idx] ?? 0) + 1;
|
|
525
|
+
}
|
|
526
|
+
let minPending = workerPendingCounts[0] ?? 0;
|
|
527
|
+
for (let i = 1; i < this.size; i++) {
|
|
528
|
+
const pendingCount = workerPendingCounts[i] ?? 0;
|
|
529
|
+
if (pendingCount < minPending) {
|
|
530
|
+
minPending = pendingCount;
|
|
531
|
+
workerIndex = i;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
528
534
|
const worker = this.getWorker(workerIndex);
|
|
529
|
-
this.workerRoundRobin++;
|
|
530
535
|
const promise = new Promise((resolve, reject) => {
|
|
531
536
|
const resource = new SearchWorkerTaskResource();
|
|
532
537
|
const pendingRequest = {
|
|
@@ -764,7 +769,7 @@ async function searchSingleFile(details, opts, pattern, signal) {
|
|
|
764
769
|
}, signal, opts.maxResults);
|
|
765
770
|
if (result.matched)
|
|
766
771
|
summary.filesMatched = 1;
|
|
767
|
-
return buildSearchContentResult(
|
|
772
|
+
return buildSearchContentResult(dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
768
773
|
}
|
|
769
774
|
async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
770
775
|
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
@@ -781,12 +786,12 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
|
781
786
|
stats: false,
|
|
782
787
|
suppressErrors: true,
|
|
783
788
|
}));
|
|
789
|
+
const summary = createScanSummary();
|
|
784
790
|
async function* fileGenerator() {
|
|
785
|
-
let scanned = 0;
|
|
786
791
|
for await (const entry of stream) {
|
|
787
792
|
if (signal.aborted)
|
|
788
793
|
break;
|
|
789
|
-
if (
|
|
794
|
+
if (summary.filesScanned >= opts.maxFilesScanned)
|
|
790
795
|
break;
|
|
791
796
|
if (!entry.dirent.isFile())
|
|
792
797
|
continue;
|
|
@@ -795,36 +800,25 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
|
795
800
|
continue;
|
|
796
801
|
if (isSensitivePath(entry.path, normalized))
|
|
797
802
|
continue;
|
|
798
|
-
|
|
799
|
-
|
|
803
|
+
summary.filesScanned++;
|
|
804
|
+
onProgress?.({
|
|
805
|
+
current: summary.filesScanned,
|
|
800
806
|
total: opts.maxFilesScanned,
|
|
801
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
802
807
|
});
|
|
803
808
|
yield { resolvedPath: normalized, requestedPath: entry.path };
|
|
804
809
|
}
|
|
805
|
-
reportPeriodicProgress(onProgress, scanned, {
|
|
806
|
-
total: opts.maxFilesScanned,
|
|
807
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
808
|
-
force: true,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
const summary = createScanSummary();
|
|
812
|
-
const resolvedStream = fileGenerator();
|
|
813
|
-
async function* countingStream() {
|
|
814
|
-
for await (const f of resolvedStream) {
|
|
815
|
-
summary.filesScanned++;
|
|
816
|
-
yield f;
|
|
817
|
-
}
|
|
818
810
|
if (summary.filesScanned >= opts.maxFilesScanned) {
|
|
819
811
|
summary.truncated = true;
|
|
820
812
|
summary.stoppedReason = 'maxFiles';
|
|
821
813
|
}
|
|
814
|
+
onProgress?.({
|
|
815
|
+
current: summary.filesScanned,
|
|
816
|
+
total: opts.maxFilesScanned,
|
|
817
|
+
});
|
|
822
818
|
}
|
|
823
|
-
const matcherOpts = buildMatcherOptions(opts);
|
|
824
|
-
validatePattern(pattern, matcherOpts);
|
|
825
819
|
const matches = shouldUseWorkers()
|
|
826
|
-
? await executeParallel(
|
|
827
|
-
: await executeSequential(
|
|
820
|
+
? await executeParallel(fileGenerator(), pattern, opts, signal, summary)
|
|
821
|
+
: await executeSequential(fileGenerator(), pattern, opts, signal, summary);
|
|
828
822
|
return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
|
|
829
823
|
}
|
|
830
824
|
function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
@@ -834,19 +828,19 @@ function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
|
834
828
|
}
|
|
835
829
|
export async function searchContent(basePath, pattern, options = {}) {
|
|
836
830
|
if (!basePath.trim())
|
|
837
|
-
throw new McpError(ErrorCode.
|
|
831
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'basePath required');
|
|
838
832
|
if (typeof pattern !== 'string')
|
|
839
|
-
throw new McpError(ErrorCode.
|
|
833
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'pattern required');
|
|
840
834
|
const opts = resolveOptions(options);
|
|
841
835
|
try {
|
|
842
836
|
return await withTimedAbortSignal(options.signal, opts.timeoutMs, async (signal) => {
|
|
843
837
|
const details = await validateExistingPathDetailed(basePath, signal);
|
|
844
|
-
const stats = await withAbort(
|
|
838
|
+
const stats = await withAbort(stat(details.resolvedPath), signal);
|
|
845
839
|
if (stats.isFile()) {
|
|
846
840
|
return searchSingleFile(details, opts, pattern, signal);
|
|
847
841
|
}
|
|
848
842
|
if (!stats.isDirectory()) {
|
|
849
|
-
throw new McpError(ErrorCode.
|
|
843
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path must be file or directory', basePath);
|
|
850
844
|
}
|
|
851
845
|
return searchDirectory(details, opts, pattern, signal, options.onProgress);
|
|
852
846
|
});
|
|
@@ -961,9 +955,9 @@ async function collectFromStream(stream, signal, context) {
|
|
|
961
955
|
if (shouldStopCollecting(state, normalized, signal))
|
|
962
956
|
break;
|
|
963
957
|
state.filesScanned++;
|
|
964
|
-
|
|
958
|
+
onProgress?.({
|
|
959
|
+
current: state.filesScanned,
|
|
965
960
|
total: normalized.maxFilesScanned,
|
|
966
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
967
961
|
});
|
|
968
962
|
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
|
|
969
963
|
continue;
|
|
@@ -981,10 +975,9 @@ async function collectFromStream(stream, signal, context) {
|
|
|
981
975
|
if (state.truncated)
|
|
982
976
|
break;
|
|
983
977
|
}
|
|
984
|
-
|
|
978
|
+
onProgress?.({
|
|
979
|
+
current: state.filesScanned,
|
|
985
980
|
total: normalized.maxFilesScanned,
|
|
986
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
987
|
-
force: true,
|
|
988
981
|
});
|
|
989
982
|
}
|
|
990
983
|
function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
@@ -1047,7 +1040,7 @@ const SORT_COMPARATORS = {
|
|
|
1047
1040
|
};
|
|
1048
1041
|
function sortSearchResults(results, sortBy) {
|
|
1049
1042
|
if (sortBy === 'name') {
|
|
1050
|
-
stableSortByDerivedString(results, (item) =>
|
|
1043
|
+
stableSortByDerivedString(results, (item) => basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
1051
1044
|
return;
|
|
1052
1045
|
}
|
|
1053
1046
|
const comparator = SORT_COMPARATORS[sortBy];
|
|
@@ -1080,8 +1073,7 @@ function getMatcherCacheKey(pattern, options) {
|
|
|
1080
1073
|
const cs = options.caseSensitive ? '1' : '0';
|
|
1081
1074
|
const ww = options.wholeWord ? '1' : '0';
|
|
1082
1075
|
const lit = options.isLiteral ? '1' : '0';
|
|
1083
|
-
|
|
1084
|
-
return `${pattern}|${cs}|${ww}|${lit}|${ml}`;
|
|
1076
|
+
return `${pattern}|${cs}|${ww}|${lit}`;
|
|
1085
1077
|
}
|
|
1086
1078
|
function getCachedMatcher(pattern, options) {
|
|
1087
1079
|
const key = getMatcherCacheKey(pattern, options);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { glob as fsGlob } from 'node:fs/promises';
|
|
1
|
+
import { glob as fsGlob, lstat, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
3
|
import { getToolContextSnapshot, publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, startPerfMeasure, } from '../observability.js';
|
|
5
4
|
import { toPosixPath } from '../paths.js';
|
|
6
5
|
import { isRecord } from '../utils.js';
|
|
@@ -96,7 +95,7 @@ function assertOptionsShape(options) {
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
function normalizeOptions(options) {
|
|
99
|
-
const cwd =
|
|
98
|
+
const cwd = resolve(options.cwd);
|
|
100
99
|
const normalizedPattern = normalizePattern(options.pattern, options.baseNameMatch);
|
|
101
100
|
const patterns = options.includeHidden
|
|
102
101
|
? buildHiddenPatterns(normalizedPattern, options.maxDepth ?? DEFAULT_MAX_HIDDEN_DEPTH)
|
|
@@ -136,18 +135,16 @@ function isGlobDirentLike(value) {
|
|
|
136
135
|
function resolveDirentBase(cwd, parentPath) {
|
|
137
136
|
if (!parentPath)
|
|
138
137
|
return cwd;
|
|
139
|
-
return
|
|
140
|
-
? parentPath
|
|
141
|
-
: path.resolve(cwd, parentPath);
|
|
138
|
+
return isAbsolute(parentPath) ? parentPath : resolve(cwd, parentPath);
|
|
142
139
|
}
|
|
143
140
|
function resolveStringMatchPath(cwd, match) {
|
|
144
|
-
return
|
|
141
|
+
return isAbsolute(match) ? match : resolve(cwd, match);
|
|
145
142
|
}
|
|
146
143
|
function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
|
|
147
144
|
const base = resolveDirentBase(cwd, match.parentPath);
|
|
148
|
-
const absolutePath =
|
|
145
|
+
const absolutePath = resolve(base, match.name);
|
|
149
146
|
if (maxDepth !== undefined) {
|
|
150
|
-
const rel =
|
|
147
|
+
const rel = relative(cwd, absolutePath);
|
|
151
148
|
if (getRelativeDepth(rel) > maxDepth)
|
|
152
149
|
return;
|
|
153
150
|
}
|
|
@@ -170,12 +167,12 @@ async function resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followS
|
|
|
170
167
|
seen.add(absolutePath);
|
|
171
168
|
try {
|
|
172
169
|
const stats = followSymlinks
|
|
173
|
-
? await
|
|
174
|
-
: await
|
|
170
|
+
? await stat(absolutePath)
|
|
171
|
+
: await lstat(absolutePath);
|
|
175
172
|
if (onlyFiles && !stats.isFile())
|
|
176
173
|
return null;
|
|
177
174
|
const entry = { path: absolutePath, dirent: stats };
|
|
178
|
-
if (!
|
|
175
|
+
if (!isAbsolute(match)) {
|
|
179
176
|
entry.relativePath = match;
|
|
180
177
|
}
|
|
181
178
|
if (returnStats)
|
|
@@ -190,12 +187,13 @@ async function resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followS
|
|
|
190
187
|
}
|
|
191
188
|
async function* processIterable(iterable, context) {
|
|
192
189
|
const { cwd, maxDepth, seen, onlyFiles, followSymlinks, returnStats, suppressErrors, } = context;
|
|
193
|
-
|
|
190
|
+
let buffer = [];
|
|
194
191
|
const flush = async function* () {
|
|
195
192
|
if (buffer.length === 0)
|
|
196
193
|
return;
|
|
197
194
|
// Process buffer concurrently
|
|
198
|
-
const currentBuffer = buffer
|
|
195
|
+
const currentBuffer = buffer;
|
|
196
|
+
buffer = [];
|
|
199
197
|
const results = await Promise.all(currentBuffer.map((match) => resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followSymlinks, returnStats, suppressErrors)));
|
|
200
198
|
for (const entry of results) {
|
|
201
199
|
if (entry !== null)
|
package/dist/lib/fs-helpers.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises';
|
|
2
1
|
import type { Stats } from 'node:fs';
|
|
2
|
+
import { type FileHandle } from 'node:fs/promises';
|
|
3
3
|
import type { FileType } from '../config.js';
|
|
4
|
-
export
|
|
5
|
-
export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
6
|
-
export declare function createTimedAbortSignal(baseSignal: AbortSignal | undefined, timeoutMs?: number): {
|
|
7
|
-
signal: AbortSignal;
|
|
8
|
-
cleanup: () => void;
|
|
9
|
-
};
|
|
10
|
-
export declare function withTimedAbortSignal<T>(baseSignal: AbortSignal | undefined, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
4
|
+
export { assertNotAborted, createTimedAbortSignal, withAbort, withTimedAbortSignal, } from './abort.js';
|
|
11
5
|
interface ParallelResult<R> {
|
|
12
6
|
results: R[];
|
|
13
7
|
errors: {
|
|
@@ -18,7 +12,7 @@ interface ParallelResult<R> {
|
|
|
18
12
|
export declare function processInParallel<T, R>(items: T[], processor: (item: T) => Promise<R>, concurrency?: number, signal?: AbortSignal): Promise<ParallelResult<R>>;
|
|
19
13
|
export declare function getFileType(stats: Stats): FileType;
|
|
20
14
|
export declare function isHidden(name: string): boolean;
|
|
21
|
-
export declare function isProbablyBinary(filePath: string, existingHandle?:
|
|
15
|
+
export declare function isProbablyBinary(filePath: string, existingHandle?: FileHandle, signal?: AbortSignal): Promise<boolean>;
|
|
22
16
|
type ReadMode = 'head' | 'full' | 'range' | 'tail';
|
|
23
17
|
interface ReadFileOptions {
|
|
24
18
|
encoding?: BufferEncoding;
|
|
@@ -49,4 +43,3 @@ export declare function atomicWriteFile(filePath: string, content: string, optio
|
|
|
49
43
|
encoding?: BufferEncoding;
|
|
50
44
|
signal?: AbortSignal | undefined;
|
|
51
45
|
}): Promise<void>;
|
|
52
|
-
export {};
|