@j0hanz/filesystem-mcp 1.8.0 → 1.9.0
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 +635 -377
- package/dist/cli.js +2 -2
- package/dist/completions.js +2 -3
- package/dist/index.js +2 -2
- package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
- package/dist/lib/file-operations/{common.js → core.js} +45 -0
- package/dist/lib/file-operations/metadata.d.ts +73 -0
- package/dist/lib/file-operations/metadata.js +889 -0
- package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +35 -2
- package/dist/lib/file-operations/{search-content.js → search.js} +413 -15
- package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
- package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
- package/dist/lib/fs-helpers.js +1 -2
- package/dist/lib/observability.js +1 -1
- package/dist/lib/{path-validation.d.ts → paths.d.ts} +3 -0
- package/dist/lib/{path-validation.js → paths.js} +105 -1
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.js +33 -0
- package/dist/resources/generated-instructions.js +6 -6
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +3 -3
- package/dist/resources/workflows.js +10 -23
- package/dist/schemas.js +15 -15
- package/dist/server/bootstrap.d.ts +19 -1
- package/dist/server/bootstrap.js +103 -9
- package/dist/server/roots-manager.d.ts +2 -2
- package/dist/server/roots-manager.js +3 -3
- package/dist/tools/apply-patch.js +4 -3
- package/dist/tools/calculate-hash.js +4 -4
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +3 -3
- package/dist/tools/diff-files.js +2 -2
- package/dist/tools/edit-file.js +14 -13
- package/dist/tools/list-directory.js +5 -7
- package/dist/tools/move-file.js +2 -1
- package/dist/tools/read-multiple.js +3 -4
- package/dist/tools/read.js +3 -3
- package/dist/tools/replace-in-files.js +8 -10
- package/dist/tools/roots.js +4 -6
- package/dist/tools/search-content.js +7 -9
- package/dist/tools/search-files.js +4 -6
- package/dist/tools/shared.d.ts +2 -1
- package/dist/tools/shared.js +2 -1
- package/dist/tools/stat-many.js +4 -3
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +1 -1
- package/dist/tools/tree.js +3 -4
- package/dist/tools/write-file.js +1 -1
- package/package.json +4 -4
- package/dist/lib/file-operations/file-info.d.ts +0 -10
- package/dist/lib/file-operations/file-info.js +0 -143
- package/dist/lib/file-operations/gitignore.d.ts +0 -6
- package/dist/lib/file-operations/gitignore.js +0 -45
- package/dist/lib/file-operations/glob-helpers.d.ts +0 -18
- package/dist/lib/file-operations/glob-helpers.js +0 -23
- package/dist/lib/file-operations/list-directory.d.ts +0 -14
- package/dist/lib/file-operations/list-directory.js +0 -252
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
- package/dist/lib/file-operations/read-multiple-files.js +0 -252
- package/dist/lib/file-operations/search-files.d.ts +0 -27
- package/dist/lib/file-operations/search-files.js +0 -216
- package/dist/lib/file-operations/search-matcher.d.ts +0 -10
- package/dist/lib/file-operations/search-matcher.js +0 -72
- package/dist/lib/file-operations/search-worker.d.ts +0 -2
- package/dist/lib/file-operations/search-worker.js +0 -131
- package/dist/lib/file-operations/tree.d.ts +0 -28
- package/dist/lib/file-operations/tree.js +0 -265
- package/dist/lib/option-utils.d.ts +0 -3
- package/dist/lib/option-utils.js +0 -15
- package/dist/lib/path-format.d.ts +0 -1
- package/dist/lib/path-format.js +0 -7
- package/dist/lib/path-policy.d.ts +0 -2
- package/dist/lib/path-policy.js +0 -100
- package/dist/lib/progress-reporting.d.ts +0 -11
- package/dist/lib/progress-reporting.js +0 -13
- package/dist/lib/type-guards.d.ts +0 -1
- package/dist/lib/type-guards.js +0 -3
- package/dist/server/capabilities.d.ts +0 -10
- package/dist/server/capabilities.js +0 -48
- package/dist/server/logging.d.ts +0 -7
- package/dist/server/logging.js +0 -41
- package/dist/server/types.d.ts +0 -4
- package/dist/server/types.js +0 -1
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import * as fsp from 'node:fs/promises';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type { ContentMatch, SearchContentResult } from '../../config.js';
|
|
4
|
-
|
|
3
|
+
import type { ContentMatch, SearchContentResult, SearchFilesResult } from '../../config.js';
|
|
4
|
+
export declare const MatcherOptionsSchema: z.ZodObject<{
|
|
5
|
+
caseSensitive: z.ZodBoolean;
|
|
6
|
+
wholeWord: z.ZodBoolean;
|
|
7
|
+
isLiteral: z.ZodBoolean;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
|
|
10
|
+
export type Matcher = (line: string) => number;
|
|
11
|
+
export declare function validatePattern(pattern: string, options: MatcherOptions): void;
|
|
12
|
+
export declare function buildMatcher(pattern: string, options: MatcherOptions): Matcher;
|
|
5
13
|
export interface ScanFileOptions {
|
|
6
14
|
maxFileSize: number;
|
|
7
15
|
skipBinary: boolean;
|
|
@@ -66,4 +74,29 @@ interface WorkerScanResult {
|
|
|
66
74
|
}
|
|
67
75
|
export declare function scanFileInWorker(resolvedPath: string, requestedPath: string, matcher: Matcher, options: ScanFileOptions, maxMatches: number, isCancelled: () => boolean, isBinaryDetector: BinaryDetector): Promise<WorkerScanResult>;
|
|
68
76
|
export declare function searchContent(basePath: string, pattern: string, options?: SearchContentOptions): Promise<SearchContentResult>;
|
|
77
|
+
type SortBy = 'name' | 'size' | 'modified' | 'path';
|
|
78
|
+
interface SearchFilesOptions {
|
|
79
|
+
maxResults?: number;
|
|
80
|
+
sortBy?: SortBy;
|
|
81
|
+
maxDepth?: number;
|
|
82
|
+
maxFilesScanned?: number;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
baseNameMatch?: boolean;
|
|
85
|
+
skipSymlinks?: boolean;
|
|
86
|
+
includeHidden?: boolean;
|
|
87
|
+
respectGitignore?: boolean;
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
onProgress?: (progress: {
|
|
90
|
+
total?: number;
|
|
91
|
+
current: number;
|
|
92
|
+
}) => void;
|
|
93
|
+
}
|
|
94
|
+
interface Sortable {
|
|
95
|
+
name?: string;
|
|
96
|
+
size?: number;
|
|
97
|
+
modified?: Date;
|
|
98
|
+
path?: string;
|
|
99
|
+
}
|
|
100
|
+
export declare function sortSearchResults(results: Sortable[], sortBy: SortBy): void;
|
|
101
|
+
export declare function searchFiles(basePath: string, pattern: string, excludePatterns?: readonly string[], options?: SearchFilesOptions): Promise<SearchFilesResult>;
|
|
69
102
|
export {};
|
|
@@ -1,21 +1,89 @@
|
|
|
1
1
|
import * as fsp from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
|
|
5
|
+
import RE2 from 're2';
|
|
6
|
+
import safeRegex from 'safe-regex2';
|
|
5
7
|
import { z } from 'zod';
|
|
6
8
|
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';
|
|
7
9
|
import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
|
|
8
10
|
import { assertNotAborted, isProbablyBinary, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
|
|
9
|
-
import {
|
|
10
|
-
import { assertAllowedFileAccess, isSensitivePath } from '../
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
import { startPerfMeasure } from '../observability.js';
|
|
12
|
+
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
|
|
13
|
+
import { mergeOptions, omitOptionKeys, reportPeriodicProgress, } from '../utils.js';
|
|
14
|
+
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
|
|
15
|
+
import { buildGlobOptions, globEntries } from './traversal.js';
|
|
16
|
+
export const MatcherOptionsSchema = z.strictObject({
|
|
17
|
+
caseSensitive: z.boolean(),
|
|
18
|
+
wholeWord: z.boolean(),
|
|
19
|
+
isLiteral: z.boolean(),
|
|
20
|
+
});
|
|
21
|
+
function countRegexLineMatches(regex, line) {
|
|
22
|
+
regex.lastIndex = 0;
|
|
23
|
+
let count = 0;
|
|
24
|
+
while (regex.exec(line) !== null) {
|
|
25
|
+
count++;
|
|
26
|
+
if (regex.lastIndex === 0)
|
|
27
|
+
regex.lastIndex++;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
function escapeLiteral(pattern) {
|
|
32
|
+
return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
}
|
|
34
|
+
function buildRegexPattern(pattern, options) {
|
|
35
|
+
const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
|
|
36
|
+
return options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
37
|
+
}
|
|
38
|
+
export function validatePattern(pattern, options) {
|
|
39
|
+
if (options.isLiteral && pattern.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
if (options.isLiteral && !options.wholeWord)
|
|
42
|
+
return;
|
|
43
|
+
const final = buildRegexPattern(pattern, options);
|
|
44
|
+
if (!safeRegex(final)) {
|
|
45
|
+
throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${pattern}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function buildLiteralMatcher(pattern, options) {
|
|
49
|
+
if (!options.caseSensitive) {
|
|
50
|
+
const final = escapeLiteral(pattern);
|
|
51
|
+
const regex = new RegExp(final, 'gi');
|
|
52
|
+
return (line) => countRegexLineMatches(regex, line);
|
|
53
|
+
}
|
|
54
|
+
// Fast path for case-sensitive literal
|
|
55
|
+
const needle = pattern;
|
|
56
|
+
if (needle.length === 0)
|
|
57
|
+
return () => 0;
|
|
58
|
+
return (line) => {
|
|
59
|
+
if (line.length === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
let count = 0;
|
|
62
|
+
let pos = line.indexOf(needle);
|
|
63
|
+
while (pos !== -1) {
|
|
64
|
+
count++;
|
|
65
|
+
pos = line.indexOf(needle, pos + needle.length);
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildRegexMatcher(final, caseSensitive) {
|
|
71
|
+
const regex = new RE2(final, caseSensitive ? 'g' : 'gi');
|
|
72
|
+
return (line) => countRegexLineMatches(regex, line);
|
|
73
|
+
}
|
|
74
|
+
export function buildMatcher(pattern, options) {
|
|
75
|
+
if (options.isLiteral && pattern.length === 0)
|
|
76
|
+
return () => 0;
|
|
77
|
+
if (options.isLiteral && !options.wholeWord) {
|
|
78
|
+
// fast path for simple literal search
|
|
79
|
+
return buildLiteralMatcher(pattern, options);
|
|
80
|
+
}
|
|
81
|
+
const final = buildRegexPattern(pattern, options);
|
|
82
|
+
validatePattern(pattern, options); // Re-validate to be safe
|
|
83
|
+
return buildRegexMatcher(final, options.caseSensitive);
|
|
84
|
+
}
|
|
17
85
|
// --- Configuration & Schemas ---
|
|
18
|
-
const
|
|
86
|
+
const SEARCH_CONTENT_MAX_RESULTS = 500;
|
|
19
87
|
const SearchOptionsSchema = z.strictObject({
|
|
20
88
|
filePattern: z.string().min(1),
|
|
21
89
|
excludePatterns: z.array(z.string()),
|
|
@@ -36,7 +104,7 @@ const DEFAULTS = {
|
|
|
36
104
|
filePattern: '**/*',
|
|
37
105
|
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
38
106
|
caseSensitive: false,
|
|
39
|
-
maxResults:
|
|
107
|
+
maxResults: SEARCH_CONTENT_MAX_RESULTS,
|
|
40
108
|
maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
|
|
41
109
|
maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
|
|
42
110
|
timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
|
|
@@ -252,7 +320,7 @@ function createScanSummary() {
|
|
|
252
320
|
stoppedReason: undefined,
|
|
253
321
|
};
|
|
254
322
|
}
|
|
255
|
-
function
|
|
323
|
+
function buildSearchContentResult(root, pattern, filePattern, matches, summary) {
|
|
256
324
|
const baseSummary = {
|
|
257
325
|
filesScanned: summary.filesScanned,
|
|
258
326
|
filesMatched: summary.filesMatched,
|
|
@@ -578,7 +646,7 @@ async function searchSingleFile(details, opts, pattern, signal) {
|
|
|
578
646
|
}, signal, opts.maxResults);
|
|
579
647
|
if (result.matched)
|
|
580
648
|
summary.filesMatched = 1;
|
|
581
|
-
return
|
|
649
|
+
return buildSearchContentResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
582
650
|
}
|
|
583
651
|
async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
584
652
|
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
@@ -643,12 +711,12 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
|
643
711
|
const matches = shouldUseWorkers()
|
|
644
712
|
? await executeParallel(countingStream(), pattern, opts, signal, summary)
|
|
645
713
|
: await executeSequential(countingStream(), pattern, opts, signal, summary);
|
|
646
|
-
return
|
|
714
|
+
return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
|
|
647
715
|
}
|
|
648
716
|
function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
649
717
|
const timeoutSummary = createScanSummary();
|
|
650
718
|
markTruncated(timeoutSummary, 'timeout');
|
|
651
|
-
return
|
|
719
|
+
return buildSearchContentResult(basePath, pattern, filePattern, [], timeoutSummary);
|
|
652
720
|
}
|
|
653
721
|
export async function searchContent(basePath, pattern, options = {}) {
|
|
654
722
|
if (!basePath.trim())
|
|
@@ -676,3 +744,333 @@ export async function searchContent(basePath, pattern, options = {}) {
|
|
|
676
744
|
throw error;
|
|
677
745
|
}
|
|
678
746
|
}
|
|
747
|
+
// Internal default for find tool - not exposed to MCP users
|
|
748
|
+
const SEARCH_FILES_MAX_RESULTS = 1000;
|
|
749
|
+
function normalizeSearchFilesOptions(options) {
|
|
750
|
+
const normalized = {
|
|
751
|
+
maxResults: options.maxResults ?? SEARCH_FILES_MAX_RESULTS,
|
|
752
|
+
sortBy: options.sortBy ?? 'path',
|
|
753
|
+
maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
|
|
754
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
755
|
+
baseNameMatch: options.baseNameMatch ?? false,
|
|
756
|
+
skipSymlinks: options.skipSymlinks ?? true,
|
|
757
|
+
includeHidden: options.includeHidden ?? false,
|
|
758
|
+
respectGitignore: options.respectGitignore ?? false,
|
|
759
|
+
};
|
|
760
|
+
if (options.maxDepth !== undefined) {
|
|
761
|
+
normalized.maxDepth = options.maxDepth;
|
|
762
|
+
}
|
|
763
|
+
return normalized;
|
|
764
|
+
}
|
|
765
|
+
function buildSearchFilesResult(entry, entryType, needsStats) {
|
|
766
|
+
let resolvedType = 'other';
|
|
767
|
+
if (entryType === 'directory') {
|
|
768
|
+
resolvedType = 'directory';
|
|
769
|
+
}
|
|
770
|
+
else if (entryType === 'file') {
|
|
771
|
+
resolvedType = 'file';
|
|
772
|
+
}
|
|
773
|
+
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
774
|
+
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
775
|
+
return {
|
|
776
|
+
path: entry.path,
|
|
777
|
+
type: resolvedType,
|
|
778
|
+
...(size !== undefined ? { size } : {}),
|
|
779
|
+
...(modified !== undefined ? { modified } : {}),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function shouldStopCollecting(state, normalized, signal) {
|
|
783
|
+
const stopReason = resolveStopReason({
|
|
784
|
+
signal,
|
|
785
|
+
current: state.filesScanned,
|
|
786
|
+
max: normalized.maxFilesScanned,
|
|
787
|
+
abortedReason: 'timeout',
|
|
788
|
+
maxReason: 'maxFiles',
|
|
789
|
+
});
|
|
790
|
+
if (stopReason !== undefined) {
|
|
791
|
+
state.truncated = true;
|
|
792
|
+
state.stoppedReason = stopReason;
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
function shouldIncludeEntry(entryType, normalized) {
|
|
798
|
+
return !normalized.skipSymlinks || entryType !== 'symlink';
|
|
799
|
+
}
|
|
800
|
+
function createCollectState() {
|
|
801
|
+
return {
|
|
802
|
+
results: [],
|
|
803
|
+
filesScanned: 0,
|
|
804
|
+
truncated: false,
|
|
805
|
+
skippedInaccessible: 0,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
|
|
809
|
+
const options = buildGlobOptions({
|
|
810
|
+
cwd: root,
|
|
811
|
+
pattern,
|
|
812
|
+
excludePatterns,
|
|
813
|
+
includeHidden: normalized.includeHidden,
|
|
814
|
+
baseNameMatch: normalized.baseNameMatch,
|
|
815
|
+
caseSensitiveMatch: true,
|
|
816
|
+
followSymbolicLinks: false,
|
|
817
|
+
onlyFiles: true,
|
|
818
|
+
stats: needsStats,
|
|
819
|
+
...(normalized.maxDepth !== undefined
|
|
820
|
+
? { maxDepth: normalized.maxDepth }
|
|
821
|
+
: {}),
|
|
822
|
+
});
|
|
823
|
+
return globEntries(options);
|
|
824
|
+
}
|
|
825
|
+
function buildCollectResult(state) {
|
|
826
|
+
const outcome = {
|
|
827
|
+
results: state.results,
|
|
828
|
+
filesScanned: state.filesScanned,
|
|
829
|
+
truncated: state.truncated,
|
|
830
|
+
skippedInaccessible: state.skippedInaccessible,
|
|
831
|
+
};
|
|
832
|
+
if (state.stoppedReason !== undefined) {
|
|
833
|
+
outcome.stoppedReason = state.stoppedReason;
|
|
834
|
+
}
|
|
835
|
+
return outcome;
|
|
836
|
+
}
|
|
837
|
+
function handleEntry(entry, entryType, needsStats, normalized, state) {
|
|
838
|
+
state.results.push(buildSearchFilesResult(entry, entryType, needsStats));
|
|
839
|
+
if (state.results.length >= normalized.maxResults) {
|
|
840
|
+
state.truncated = true;
|
|
841
|
+
state.stoppedReason = 'maxResults';
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
|
|
845
|
+
for await (const entry of stream) {
|
|
846
|
+
if (shouldStopCollecting(state, normalized, signal))
|
|
847
|
+
break;
|
|
848
|
+
state.filesScanned++;
|
|
849
|
+
reportPeriodicProgress(onProgress, state.filesScanned, {
|
|
850
|
+
total: normalized.maxFilesScanned,
|
|
851
|
+
throttleModulo: 25,
|
|
852
|
+
});
|
|
853
|
+
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const entryType = resolveEntryType(entry.dirent);
|
|
857
|
+
if (!shouldIncludeEntry(entryType, normalized)) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
|
|
861
|
+
if (!isAccessible) {
|
|
862
|
+
state.skippedInaccessible++;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
handleEntry(entry, entryType, needsStats, normalized, state);
|
|
866
|
+
if (state.truncated)
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
reportPeriodicProgress(onProgress, state.filesScanned, {
|
|
870
|
+
total: normalized.maxFilesScanned,
|
|
871
|
+
throttleModulo: 25,
|
|
872
|
+
force: true,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
876
|
+
if (!matcher)
|
|
877
|
+
return false;
|
|
878
|
+
return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
|
|
879
|
+
}
|
|
880
|
+
async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
881
|
+
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
882
|
+
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
883
|
+
const state = createCollectState();
|
|
884
|
+
const rootDirectories = [root];
|
|
885
|
+
const accessDeps = {
|
|
886
|
+
normalizePath,
|
|
887
|
+
isPathWithinDirectories,
|
|
888
|
+
isSensitivePath,
|
|
889
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
890
|
+
};
|
|
891
|
+
const gitignoreMatcher = normalized.respectGitignore
|
|
892
|
+
? await loadRootGitignore(root, signal)
|
|
893
|
+
: null;
|
|
894
|
+
await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
|
|
895
|
+
return buildCollectResult(state);
|
|
896
|
+
}
|
|
897
|
+
function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
|
|
898
|
+
const summary = {
|
|
899
|
+
matched: results.length,
|
|
900
|
+
truncated,
|
|
901
|
+
skippedInaccessible,
|
|
902
|
+
filesScanned,
|
|
903
|
+
};
|
|
904
|
+
return withOptionalStoppedReason(summary, stoppedReason);
|
|
905
|
+
}
|
|
906
|
+
function compareNameThenPath(a, b) {
|
|
907
|
+
const nameCompare = compareStringValues(a.name, b.name);
|
|
908
|
+
if (nameCompare !== 0)
|
|
909
|
+
return nameCompare;
|
|
910
|
+
return compareStringValues(a.path, b.path);
|
|
911
|
+
}
|
|
912
|
+
function comparePathThenName(a, b) {
|
|
913
|
+
const pathCompare = compareStringValues(a.path, b.path);
|
|
914
|
+
if (pathCompare !== 0)
|
|
915
|
+
return pathCompare;
|
|
916
|
+
return compareStringValues(a.name, b.name);
|
|
917
|
+
}
|
|
918
|
+
const SORT_COMPARATORS = {
|
|
919
|
+
size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
|
|
920
|
+
modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
|
|
921
|
+
path: (a, b) => comparePathThenName(a, b),
|
|
922
|
+
name: (a, b) => compareNameThenPath(a, b),
|
|
923
|
+
};
|
|
924
|
+
export function sortSearchResults(results, sortBy) {
|
|
925
|
+
if (sortBy === 'name') {
|
|
926
|
+
stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const comparator = SORT_COMPARATORS[sortBy];
|
|
930
|
+
results.sort(comparator);
|
|
931
|
+
}
|
|
932
|
+
async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
933
|
+
const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
|
|
934
|
+
sortSearchResults(results, normalized.sortBy);
|
|
935
|
+
return {
|
|
936
|
+
results,
|
|
937
|
+
summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
|
|
941
|
+
const normalized = normalizeSearchFilesOptions(options);
|
|
942
|
+
return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
|
|
943
|
+
const root = await validateExistingDirectory(basePath, signal);
|
|
944
|
+
const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
|
|
945
|
+
return {
|
|
946
|
+
basePath: root,
|
|
947
|
+
pattern,
|
|
948
|
+
results,
|
|
949
|
+
summary,
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const matcherCache = new Map();
|
|
954
|
+
const MAX_MATCHER_CACHE_SIZE = 100;
|
|
955
|
+
function getMatcherCacheKey(pattern, options) {
|
|
956
|
+
const cs = options.caseSensitive ? '1' : '0';
|
|
957
|
+
const ww = options.wholeWord ? '1' : '0';
|
|
958
|
+
const lit = options.isLiteral ? '1' : '0';
|
|
959
|
+
return `${pattern}|${cs}|${ww}|${lit}`;
|
|
960
|
+
}
|
|
961
|
+
function getCachedMatcher(pattern, options) {
|
|
962
|
+
const key = getMatcherCacheKey(pattern, options);
|
|
963
|
+
const cached = matcherCache.get(key);
|
|
964
|
+
if (cached) {
|
|
965
|
+
refreshMatcherCacheEntry(key, cached);
|
|
966
|
+
return cached;
|
|
967
|
+
}
|
|
968
|
+
const matcher = buildMatcher(pattern, options);
|
|
969
|
+
refreshMatcherCacheEntry(key, matcher);
|
|
970
|
+
evictOldestMatcherIfNeeded();
|
|
971
|
+
return matcher;
|
|
972
|
+
}
|
|
973
|
+
function refreshMatcherCacheEntry(key, matcher) {
|
|
974
|
+
matcherCache.delete(key);
|
|
975
|
+
matcherCache.set(key, matcher);
|
|
976
|
+
}
|
|
977
|
+
function evictOldestMatcherIfNeeded() {
|
|
978
|
+
if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
|
|
979
|
+
return;
|
|
980
|
+
const firstKey = matcherCache.keys().next().value;
|
|
981
|
+
if (firstKey !== undefined) {
|
|
982
|
+
matcherCache.delete(firstKey);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const cancelledRequests = new Set();
|
|
986
|
+
const activeRequests = new Set();
|
|
987
|
+
let shuttingDown = false;
|
|
988
|
+
function maybeFinishShutdown() {
|
|
989
|
+
if (!shuttingDown)
|
|
990
|
+
return;
|
|
991
|
+
if (activeRequests.size > 0)
|
|
992
|
+
return;
|
|
993
|
+
parentPort?.close();
|
|
994
|
+
}
|
|
995
|
+
function consumeCancelled(id) {
|
|
996
|
+
if (!cancelledRequests.has(id)) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
cancelledRequests.delete(id);
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
function markCancelledIfActive(id) {
|
|
1003
|
+
if (activeRequests.has(id)) {
|
|
1004
|
+
cancelledRequests.add(id);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function buildScanResponse(id, result) {
|
|
1008
|
+
return {
|
|
1009
|
+
type: 'result',
|
|
1010
|
+
id,
|
|
1011
|
+
result,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function buildErrorResponse(id, error) {
|
|
1015
|
+
return {
|
|
1016
|
+
type: 'error',
|
|
1017
|
+
id,
|
|
1018
|
+
error: formatUnknownErrorMessage(error),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
async function handleScanRequest(request) {
|
|
1022
|
+
const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
|
|
1023
|
+
if (consumeCancelled(id))
|
|
1024
|
+
return;
|
|
1025
|
+
activeRequests.add(id);
|
|
1026
|
+
const endMeasure = startPerfMeasure('searchWorker.scan', {
|
|
1027
|
+
maxMatches,
|
|
1028
|
+
});
|
|
1029
|
+
let ok = false;
|
|
1030
|
+
try {
|
|
1031
|
+
const matcher = getCachedMatcher(pattern, matcherOptions);
|
|
1032
|
+
const isCancelled = () => cancelledRequests.has(id);
|
|
1033
|
+
const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
|
|
1034
|
+
if (consumeCancelled(id))
|
|
1035
|
+
return;
|
|
1036
|
+
parentPort?.postMessage(buildScanResponse(id, result));
|
|
1037
|
+
ok = true;
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
if (consumeCancelled(id))
|
|
1041
|
+
return;
|
|
1042
|
+
parentPort?.postMessage(buildErrorResponse(id, err));
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
activeRequests.delete(id);
|
|
1046
|
+
cancelledRequests.delete(id);
|
|
1047
|
+
endMeasure?.(ok);
|
|
1048
|
+
maybeFinishShutdown();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function handleMessage(message) {
|
|
1052
|
+
switch (message.type) {
|
|
1053
|
+
case 'scan':
|
|
1054
|
+
if (shuttingDown)
|
|
1055
|
+
return;
|
|
1056
|
+
void handleScanRequest(message);
|
|
1057
|
+
break;
|
|
1058
|
+
case 'cancel':
|
|
1059
|
+
markCancelledIfActive(message.id);
|
|
1060
|
+
break;
|
|
1061
|
+
case 'shutdown':
|
|
1062
|
+
shuttingDown = true;
|
|
1063
|
+
for (const id of activeRequests) {
|
|
1064
|
+
markCancelledIfActive(id);
|
|
1065
|
+
}
|
|
1066
|
+
maybeFinishShutdown();
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (parentPort) {
|
|
1071
|
+
parentPort.on('message', handleMessage);
|
|
1072
|
+
const data = workerData;
|
|
1073
|
+
if (data?.debug) {
|
|
1074
|
+
console.error(`[SearchWorker] Started with threadId=${String(threadId)}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Stats } from 'node:fs';
|
|
2
|
-
import type { DirentLike } from './
|
|
2
|
+
import type { DirentLike } from './core.js';
|
|
3
3
|
interface GlobEntry {
|
|
4
4
|
path: string;
|
|
5
5
|
relativePath?: string;
|
|
@@ -20,4 +20,21 @@ export interface GlobEntriesOptions {
|
|
|
20
20
|
suppressErrors?: boolean;
|
|
21
21
|
}
|
|
22
22
|
export declare function globEntries(options: GlobEntriesOptions): AsyncGenerator<GlobEntry>;
|
|
23
|
+
export interface GlobConfig {
|
|
24
|
+
cwd: string;
|
|
25
|
+
pattern: string;
|
|
26
|
+
excludePatterns?: readonly string[];
|
|
27
|
+
includeHidden?: boolean;
|
|
28
|
+
baseNameMatch?: boolean;
|
|
29
|
+
caseSensitiveMatch?: boolean;
|
|
30
|
+
followSymbolicLinks?: boolean;
|
|
31
|
+
onlyFiles?: boolean;
|
|
32
|
+
stats?: boolean;
|
|
33
|
+
maxDepth?: number;
|
|
34
|
+
suppressErrors?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds standard options for globEntries to ensure consistency across search tools.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildGlobOptions(config: GlobConfig): Parameters<typeof globEntries>[0];
|
|
23
40
|
export {};
|
|
@@ -2,8 +2,8 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { glob as fsGlob } from 'node:fs/promises';
|
|
4
4
|
import { getToolContextSnapshot, publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, startPerfMeasure, } from '../observability.js';
|
|
5
|
-
import { toPosixPath } from '../
|
|
6
|
-
import { isRecord } from '../
|
|
5
|
+
import { toPosixPath } from '../paths.js';
|
|
6
|
+
import { isRecord } from '../utils.js';
|
|
7
7
|
const GLOB_MAGIC_RE = /[*?[\]{}!]/u;
|
|
8
8
|
const DEFAULT_MAX_HIDDEN_DEPTH = 10;
|
|
9
9
|
const GLOB_BATCH_CONCURRENCY = 64;
|
|
@@ -344,3 +344,26 @@ export async function* globEntries(options) {
|
|
|
344
344
|
endMeasure?.(ok);
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Builds standard options for globEntries to ensure consistency across search tools.
|
|
349
|
+
*/
|
|
350
|
+
export function buildGlobOptions(config) {
|
|
351
|
+
const options = {
|
|
352
|
+
cwd: config.cwd,
|
|
353
|
+
pattern: config.pattern,
|
|
354
|
+
excludePatterns: config.excludePatterns ?? [],
|
|
355
|
+
includeHidden: config.includeHidden ?? false,
|
|
356
|
+
baseNameMatch: config.baseNameMatch ?? false,
|
|
357
|
+
caseSensitiveMatch: config.caseSensitiveMatch ?? true,
|
|
358
|
+
followSymbolicLinks: config.followSymbolicLinks ?? false,
|
|
359
|
+
onlyFiles: config.onlyFiles ?? true,
|
|
360
|
+
stats: config.stats ?? false,
|
|
361
|
+
};
|
|
362
|
+
if (config.suppressErrors) {
|
|
363
|
+
options.suppressErrors = config.suppressErrors;
|
|
364
|
+
}
|
|
365
|
+
if (config.maxDepth !== undefined) {
|
|
366
|
+
options.maxDepth = config.maxDepth;
|
|
367
|
+
}
|
|
368
|
+
return options;
|
|
369
|
+
}
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -6,8 +6,7 @@ import { Writable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
8
8
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
9
|
-
import { assertAllowedFileAccess } from './
|
|
10
|
-
import { validateExistingPath } from './path-validation.js';
|
|
9
|
+
import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
|
|
11
10
|
function createAbortError(message = 'Operation aborted') {
|
|
12
11
|
return new DOMException(message, 'AbortError');
|
|
13
12
|
}
|
|
@@ -3,7 +3,7 @@ import { hash } from 'node:crypto';
|
|
|
3
3
|
import { channel, tracingChannel } from 'node:diagnostics_channel';
|
|
4
4
|
import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
|
|
5
5
|
import { parseTrueEnvFlag } from './constants.js';
|
|
6
|
-
import { isRecord } from './
|
|
6
|
+
import { isRecord } from './utils.js';
|
|
7
7
|
// --- Configuration ---
|
|
8
8
|
const ENV = process.env;
|
|
9
9
|
let _cachedConfig;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Root } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { McpError } from './errors.js';
|
|
3
|
+
export declare function toPosixPath(value: string): string;
|
|
4
|
+
export declare function isSensitivePath(requestedPath: string, resolvedPath?: string): boolean;
|
|
5
|
+
export declare function assertAllowedFileAccess(requestedPath: string, resolvedPath?: string): void;
|
|
3
6
|
/**
|
|
4
7
|
* Normalizes any path-like input to an absolute path suitable for comparisons.
|
|
5
8
|
* - Expands "~" home directory shorthand.
|