@j0hanz/filesystem-context-mcp 1.1.0 → 1.2.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 +106 -29
- package/dist/__tests__/lib/errors.test.js +3 -23
- package/dist/__tests__/lib/errors.test.js.map +1 -1
- package/dist/__tests__/lib/file-operations.test.js +34 -0
- package/dist/__tests__/lib/file-operations.test.js.map +1 -1
- package/dist/__tests__/lib/path-validation.test.js +8 -0
- package/dist/__tests__/lib/path-validation.test.js.map +1 -1
- package/dist/__tests__/schemas/validators.test.js +101 -122
- package/dist/__tests__/schemas/validators.test.js.map +1 -1
- package/dist/__tests__/security/filesystem-boundary.test.js +1 -1
- package/dist/__tests__/security/filesystem-boundary.test.js.map +1 -1
- package/dist/config/types.d.ts +1 -85
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -2
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/instructions.md +1 -2
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +66 -258
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/errors.d.ts +25 -3
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +4 -94
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/file-operations/analyze-directory.d.ts +8 -0
- package/dist/lib/file-operations/analyze-directory.d.ts.map +1 -0
- package/dist/lib/file-operations/analyze-directory.js +117 -0
- package/dist/lib/file-operations/analyze-directory.js.map +1 -0
- package/dist/lib/file-operations/directory-items.d.ts +20 -0
- package/dist/lib/file-operations/directory-items.d.ts.map +1 -0
- package/dist/lib/file-operations/directory-items.js +85 -0
- package/dist/lib/file-operations/directory-items.js.map +1 -0
- package/dist/lib/file-operations/directory-iteration.d.ts +17 -0
- package/dist/lib/file-operations/directory-iteration.d.ts.map +1 -0
- package/dist/lib/file-operations/directory-iteration.js +55 -0
- package/dist/lib/file-operations/directory-iteration.js.map +1 -0
- package/dist/lib/file-operations/directory-tree.d.ts +9 -0
- package/dist/lib/file-operations/directory-tree.d.ts.map +1 -0
- package/dist/lib/file-operations/directory-tree.js +175 -0
- package/dist/lib/file-operations/directory-tree.js.map +1 -0
- package/dist/lib/file-operations/file-info.d.ts +3 -0
- package/dist/lib/file-operations/file-info.d.ts.map +1 -0
- package/dist/lib/file-operations/file-info.js +56 -0
- package/dist/lib/file-operations/file-info.js.map +1 -0
- package/dist/lib/file-operations/list-directory.d.ts +10 -0
- package/dist/lib/file-operations/list-directory.d.ts.map +1 -0
- package/dist/lib/file-operations/list-directory.js +189 -0
- package/dist/lib/file-operations/list-directory.js.map +1 -0
- package/dist/lib/file-operations/read-media-file.d.ts +5 -0
- package/dist/lib/file-operations/read-media-file.d.ts.map +1 -0
- package/dist/lib/file-operations/read-media-file.js +31 -0
- package/dist/lib/file-operations/read-media-file.js.map +1 -0
- package/dist/lib/file-operations/read-multiple-files.d.ts +16 -0
- package/dist/lib/file-operations/read-multiple-files.d.ts.map +1 -0
- package/dist/lib/file-operations/read-multiple-files.js +98 -0
- package/dist/lib/file-operations/read-multiple-files.js.map +1 -0
- package/dist/lib/file-operations/search-content.d.ts +16 -0
- package/dist/lib/file-operations/search-content.d.ts.map +1 -0
- package/dist/lib/file-operations/search-content.js +431 -0
- package/dist/lib/file-operations/search-content.js.map +1 -0
- package/dist/lib/file-operations/search-files.d.ts +9 -0
- package/dist/lib/file-operations/search-files.d.ts.map +1 -0
- package/dist/lib/file-operations/search-files.js +139 -0
- package/dist/lib/file-operations/search-files.js.map +1 -0
- package/dist/lib/file-operations/sorting.d.ts +12 -0
- package/dist/lib/file-operations/sorting.d.ts.map +1 -0
- package/dist/lib/file-operations/sorting.js +24 -0
- package/dist/lib/file-operations/sorting.js.map +1 -0
- package/dist/lib/file-operations.d.ts +9 -57
- package/dist/lib/file-operations.d.ts.map +1 -1
- package/dist/lib/file-operations.js +9 -773
- package/dist/lib/file-operations.js.map +1 -1
- package/dist/lib/fs-helpers/binary-detect.d.ts +3 -0
- package/dist/lib/fs-helpers/binary-detect.d.ts.map +1 -0
- package/dist/lib/fs-helpers/binary-detect.js +54 -0
- package/dist/lib/fs-helpers/binary-detect.js.map +1 -0
- package/dist/lib/fs-helpers/concurrency.d.ts +11 -0
- package/dist/lib/fs-helpers/concurrency.d.ts.map +1 -0
- package/dist/lib/fs-helpers/concurrency.js +95 -0
- package/dist/lib/fs-helpers/concurrency.js.map +1 -0
- package/dist/lib/fs-helpers/fs-utils.d.ts +5 -0
- package/dist/lib/fs-helpers/fs-utils.d.ts.map +1 -0
- package/dist/lib/fs-helpers/fs-utils.js +13 -0
- package/dist/lib/fs-helpers/fs-utils.js.map +1 -0
- package/dist/lib/fs-helpers/readers/head-file.d.ts +2 -0
- package/dist/lib/fs-helpers/readers/head-file.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers/head-file.js +73 -0
- package/dist/lib/fs-helpers/readers/head-file.js.map +1 -0
- package/dist/lib/fs-helpers/readers/line-range.d.ts +7 -0
- package/dist/lib/fs-helpers/readers/line-range.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers/line-range.js +46 -0
- package/dist/lib/fs-helpers/readers/line-range.js.map +1 -0
- package/dist/lib/fs-helpers/readers/read-file.d.ts +16 -0
- package/dist/lib/fs-helpers/readers/read-file.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers/read-file.js +87 -0
- package/dist/lib/fs-helpers/readers/read-file.js.map +1 -0
- package/dist/lib/fs-helpers/readers/tail-file.d.ts +2 -0
- package/dist/lib/fs-helpers/readers/tail-file.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers/tail-file.js +98 -0
- package/dist/lib/fs-helpers/readers/tail-file.js.map +1 -0
- package/dist/lib/fs-helpers/readers/utf8.d.ts +3 -0
- package/dist/lib/fs-helpers/readers/utf8.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers/utf8.js +22 -0
- package/dist/lib/fs-helpers/readers/utf8.js.map +1 -0
- package/dist/lib/fs-helpers/readers.d.ts +4 -0
- package/dist/lib/fs-helpers/readers.d.ts.map +1 -0
- package/dist/lib/fs-helpers/readers.js +4 -0
- package/dist/lib/fs-helpers/readers.js.map +1 -0
- package/dist/lib/fs-helpers.d.ts +4 -25
- package/dist/lib/fs-helpers.d.ts.map +1 -1
- package/dist/lib/fs-helpers.js +4 -327
- package/dist/lib/fs-helpers.js.map +1 -1
- package/dist/lib/path-validation/allowed-directories.d.ts +9 -0
- package/dist/lib/path-validation/allowed-directories.d.ts.map +1 -0
- package/dist/lib/path-validation/allowed-directories.js +94 -0
- package/dist/lib/path-validation/allowed-directories.js.map +1 -0
- package/dist/lib/path-validation/errors.d.ts +5 -0
- package/dist/lib/path-validation/errors.d.ts.map +1 -0
- package/dist/lib/path-validation/errors.js +33 -0
- package/dist/lib/path-validation/errors.js.map +1 -0
- package/dist/lib/path-validation/roots.d.ts +3 -0
- package/dist/lib/path-validation/roots.d.ts.map +1 -0
- package/dist/lib/path-validation/roots.js +49 -0
- package/dist/lib/path-validation/roots.js.map +1 -0
- package/dist/lib/path-validation/validators.d.ts +9 -0
- package/dist/lib/path-validation/validators.d.ts.map +1 -0
- package/dist/lib/path-validation/validators.js +70 -0
- package/dist/lib/path-validation/validators.js.map +1 -0
- package/dist/lib/path-validation.d.ts +3 -7
- package/dist/lib/path-validation.d.ts.map +1 -1
- package/dist/lib/path-validation.js +3 -141
- package/dist/lib/path-validation.js.map +1 -1
- package/dist/schemas/input-helpers.d.ts +8 -0
- package/dist/schemas/input-helpers.d.ts.map +1 -0
- package/dist/schemas/input-helpers.js +44 -0
- package/dist/schemas/input-helpers.js.map +1 -0
- package/dist/schemas/inputs.d.ts +8 -5
- package/dist/schemas/inputs.d.ts.map +1 -1
- package/dist/schemas/inputs.js +41 -64
- package/dist/schemas/inputs.js.map +1 -1
- package/dist/schemas/output-helpers.d.ts +24 -0
- package/dist/schemas/output-helpers.d.ts.map +1 -0
- package/dist/schemas/output-helpers.js +13 -0
- package/dist/schemas/output-helpers.js.map +1 -0
- package/dist/schemas/outputs.d.ts +476 -42
- package/dist/schemas/outputs.d.ts.map +1 -1
- package/dist/schemas/outputs.js +26 -41
- package/dist/schemas/outputs.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -42
- package/dist/server.js.map +1 -1
- package/dist/tools/analyze-directory.d.ts.map +1 -1
- package/dist/tools/analyze-directory.js +115 -53
- package/dist/tools/analyze-directory.js.map +1 -1
- package/dist/tools/directory-tree.d.ts.map +1 -1
- package/dist/tools/directory-tree.js +86 -49
- package/dist/tools/directory-tree.js.map +1 -1
- package/dist/tools/get-file-info.d.ts.map +1 -1
- package/dist/tools/get-file-info.js +71 -37
- package/dist/tools/get-file-info.js.map +1 -1
- package/dist/tools/list-allowed-dirs.d.ts.map +1 -1
- package/dist/tools/list-allowed-dirs.js +48 -35
- package/dist/tools/list-allowed-dirs.js.map +1 -1
- package/dist/tools/list-directory.d.ts.map +1 -1
- package/dist/tools/list-directory.js +129 -58
- package/dist/tools/list-directory.js.map +1 -1
- package/dist/tools/read-file.d.ts.map +1 -1
- package/dist/tools/read-file.js +70 -56
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/read-media-file.d.ts.map +1 -1
- package/dist/tools/read-media-file.js +39 -41
- package/dist/tools/read-media-file.js.map +1 -1
- package/dist/tools/read-multiple-files.d.ts.map +1 -1
- package/dist/tools/read-multiple-files.js +58 -50
- package/dist/tools/read-multiple-files.js.map +1 -1
- package/dist/tools/search-content.d.ts.map +1 -1
- package/dist/tools/search-content.js +146 -89
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/search-files.d.ts.map +1 -1
- package/dist/tools/search-files.js +121 -50
- package/dist/tools/search-files.js.map +1 -1
- package/dist/tools/tool-response.d.ts +9 -0
- package/dist/tools/tool-response.d.ts.map +1 -0
- package/dist/tools/tool-response.js +7 -0
- package/dist/tools/tool-response.js.map +1 -0
- package/package.json +2 -1
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -88
- package/dist/__tests__/errors.test.js.map +0 -1
- package/dist/__tests__/file-operations.test.d.ts +0 -2
- package/dist/__tests__/file-operations.test.d.ts.map +0 -1
- package/dist/__tests__/file-operations.test.js +0 -230
- package/dist/__tests__/file-operations.test.js.map +0 -1
- package/dist/__tests__/lib/formatters.test.d.ts +0 -2
- package/dist/__tests__/lib/formatters.test.d.ts.map +0 -1
- package/dist/__tests__/lib/formatters.test.js +0 -248
- package/dist/__tests__/lib/formatters.test.js.map +0 -1
- package/dist/__tests__/lib/image-parsing.test.d.ts +0 -2
- package/dist/__tests__/lib/image-parsing.test.d.ts.map +0 -1
- package/dist/__tests__/lib/image-parsing.test.js +0 -262
- package/dist/__tests__/lib/image-parsing.test.js.map +0 -1
- package/dist/__tests__/path-validation.test.d.ts +0 -2
- package/dist/__tests__/path-validation.test.d.ts.map +0 -1
- package/dist/__tests__/path-validation.test.js +0 -92
- package/dist/__tests__/path-validation.test.js.map +0 -1
- package/dist/lib/directory-helpers.d.ts +0 -4
- package/dist/lib/directory-helpers.d.ts.map +0 -1
- package/dist/lib/directory-helpers.js +0 -36
- package/dist/lib/directory-helpers.js.map +0 -1
- package/dist/lib/formatters.d.ts +0 -10
- package/dist/lib/formatters.d.ts.map +0 -1
- package/dist/lib/formatters.js +0 -202
- package/dist/lib/formatters.js.map +0 -1
- package/dist/lib/image-parsing.d.ts +0 -4
- package/dist/lib/image-parsing.d.ts.map +0 -1
- package/dist/lib/image-parsing.js +0 -124
- package/dist/lib/image-parsing.js.map +0 -1
- package/dist/lib/mcp-logger.d.ts +0 -11
- package/dist/lib/mcp-logger.d.ts.map +0 -1
- package/dist/lib/mcp-logger.js +0 -49
- package/dist/lib/mcp-logger.js.map +0 -1
- package/dist/lib/roots-utils.d.ts +0 -7
- package/dist/lib/roots-utils.d.ts.map +0 -1
- package/dist/lib/roots-utils.js +0 -39
- package/dist/lib/roots-utils.js.map +0 -1
- package/dist/lib/search-helpers.d.ts +0 -16
- package/dist/lib/search-helpers.d.ts.map +0 -1
- package/dist/lib/search-helpers.js +0 -169
- package/dist/lib/search-helpers.js.map +0 -1
- package/dist/lib/sorting.d.ts +0 -12
- package/dist/lib/sorting.d.ts.map +0 -1
- package/dist/lib/sorting.js +0 -41
- package/dist/lib/sorting.js.map +0 -1
- package/dist/lib/types.d.ts +0 -6
- package/dist/lib/types.d.ts.map +0 -1
- package/dist/lib/types.js +0 -2
- package/dist/lib/types.js.map +0 -1
- package/dist/prompts/analyze-codebase.d.ts +0 -3
- package/dist/prompts/analyze-codebase.d.ts.map +0 -1
- package/dist/prompts/analyze-codebase.js +0 -144
- package/dist/prompts/analyze-codebase.js.map +0 -1
- package/dist/prompts/filesystem-query.d.ts +0 -3
- package/dist/prompts/filesystem-query.d.ts.map +0 -1
- package/dist/prompts/filesystem-query.js +0 -168
- package/dist/prompts/filesystem-query.js.map +0 -1
- package/dist/prompts/find-duplicates.d.ts +0 -3
- package/dist/prompts/find-duplicates.d.ts.map +0 -1
- package/dist/prompts/find-duplicates.js +0 -77
- package/dist/prompts/find-duplicates.js.map +0 -1
- package/dist/prompts/index.d.ts +0 -3
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js +0 -13
- package/dist/prompts/index.js.map +0 -1
- package/dist/prompts/project-overview.d.ts +0 -3
- package/dist/prompts/project-overview.d.ts.map +0 -1
- package/dist/prompts/project-overview.js +0 -122
- package/dist/prompts/project-overview.js.map +0 -1
- package/dist/prompts/search-and-replace.d.ts +0 -3
- package/dist/prompts/search-and-replace.d.ts.map +0 -1
- package/dist/prompts/search-and-replace.js +0 -130
- package/dist/prompts/search-and-replace.js.map +0 -1
- package/dist/prompts/shared.d.ts +0 -11
- package/dist/prompts/shared.d.ts.map +0 -1
- package/dist/prompts/shared.js +0 -32
- package/dist/prompts/shared.js.map +0 -1
- package/dist/resources/index.d.ts +0 -3
- package/dist/resources/index.d.ts.map +0 -1
- package/dist/resources/index.js +0 -54
- package/dist/resources/index.js.map +0 -1
- package/dist/schemas/validators.d.ts +0 -12
- package/dist/schemas/validators.d.ts.map +0 -1
- package/dist/schemas/validators.js +0 -35
- package/dist/schemas/validators.js.map +0 -1
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -2
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/response-helpers.d.ts +0 -22
- package/dist/utils/response-helpers.d.ts.map +0 -1
- package/dist/utils/response-helpers.js +0 -24
- package/dist/utils/response-helpers.js.map +0 -1
|
@@ -1,774 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { validateExistingPath, validateExistingPathDetailed, } from './path-validation.js';
|
|
11
|
-
import { isSimpleSafePattern, prepareSearchPattern, scanFileForContent, } from './search-helpers.js';
|
|
12
|
-
function createExcludeMatcher(excludePatterns) {
|
|
13
|
-
if (excludePatterns.length === 0) {
|
|
14
|
-
return () => false;
|
|
15
|
-
}
|
|
16
|
-
const matchers = excludePatterns.map((pattern) => new Minimatch(pattern));
|
|
17
|
-
return (name, relativePath) => matchers.some((m) => m.match(name) || m.match(relativePath));
|
|
18
|
-
}
|
|
19
|
-
function classifyAccessError(error) {
|
|
20
|
-
if (error instanceof McpError &&
|
|
21
|
-
(error.code === ErrorCode.E_ACCESS_DENIED ||
|
|
22
|
-
error.code === ErrorCode.E_SYMLINK_NOT_ALLOWED)) {
|
|
23
|
-
return 'symlink';
|
|
24
|
-
}
|
|
25
|
-
return 'inaccessible';
|
|
26
|
-
}
|
|
27
|
-
function insertSorted(arr, item, compare, maxLen) {
|
|
28
|
-
if (maxLen <= 0)
|
|
29
|
-
return;
|
|
30
|
-
const idx = arr.findIndex((el) => compare(item, el) < 0);
|
|
31
|
-
if (idx === -1) {
|
|
32
|
-
if (arr.length < maxLen)
|
|
33
|
-
arr.push(item);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
arr.splice(idx, 0, item);
|
|
37
|
-
if (arr.length > maxLen)
|
|
38
|
-
arr.pop();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const SORT_COMPARATORS = {
|
|
42
|
-
size: (a, b) => (b.size ?? 0) - (a.size ?? 0),
|
|
43
|
-
modified: (a, b) => (b.modified?.getTime() ?? 0) - (a.modified?.getTime() ?? 0),
|
|
44
|
-
type: (a, b) => {
|
|
45
|
-
if (a.type !== b.type)
|
|
46
|
-
return a.type === 'directory' ? -1 : 1;
|
|
47
|
-
return (a.name ?? '').localeCompare(b.name ?? '');
|
|
48
|
-
},
|
|
49
|
-
path: (a, b) => (a.path ?? '').localeCompare(b.path ?? ''),
|
|
50
|
-
name: (a, b) => (a.name ?? '').localeCompare(b.name ?? ''),
|
|
51
|
-
};
|
|
52
|
-
function sortByField(items, sortBy) {
|
|
53
|
-
const comparator = SORT_COMPARATORS[sortBy];
|
|
54
|
-
items.sort(comparator);
|
|
55
|
-
}
|
|
56
|
-
function sortSearchResults(results, sortBy) {
|
|
57
|
-
if (sortBy === 'name') {
|
|
58
|
-
results.sort((a, b) => path.basename(a.path ?? '').localeCompare(path.basename(b.path ?? '')));
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
sortByField(results, sortBy);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function getPermissions(mode) {
|
|
65
|
-
const PERM_STRINGS = [
|
|
66
|
-
'---',
|
|
67
|
-
'--x',
|
|
68
|
-
'-w-',
|
|
69
|
-
'-wx',
|
|
70
|
-
'r--',
|
|
71
|
-
'r-x',
|
|
72
|
-
'rw-',
|
|
73
|
-
'rwx',
|
|
74
|
-
];
|
|
75
|
-
const ownerIndex = (mode >> 6) & 0b111;
|
|
76
|
-
const groupIndex = (mode >> 3) & 0b111;
|
|
77
|
-
const otherIndex = mode & 0b111;
|
|
78
|
-
const owner = PERM_STRINGS[ownerIndex] ?? '---';
|
|
79
|
-
const group = PERM_STRINGS[groupIndex] ?? '---';
|
|
80
|
-
const other = PERM_STRINGS[otherIndex] ?? '---';
|
|
81
|
-
return `${owner}${group}${other}`;
|
|
82
|
-
}
|
|
83
|
-
export async function getFileInfo(filePath) {
|
|
84
|
-
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath);
|
|
85
|
-
const name = path.basename(requestedPath);
|
|
86
|
-
const ext = path.extname(name).toLowerCase();
|
|
87
|
-
const mimeType = ext ? getMimeType(ext) : undefined;
|
|
88
|
-
let symlinkTarget;
|
|
89
|
-
if (isSymlink) {
|
|
90
|
-
try {
|
|
91
|
-
symlinkTarget = await fs.readlink(requestedPath);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Symlink target unreadable
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const stats = await fs.stat(resolvedPath);
|
|
98
|
-
return {
|
|
99
|
-
name,
|
|
100
|
-
path: requestedPath,
|
|
101
|
-
type: isSymlink ? 'symlink' : getFileType(stats),
|
|
102
|
-
size: stats.size,
|
|
103
|
-
created: stats.birthtime,
|
|
104
|
-
modified: stats.mtime,
|
|
105
|
-
accessed: stats.atime,
|
|
106
|
-
permissions: getPermissions(stats.mode),
|
|
107
|
-
isHidden: isHidden(name),
|
|
108
|
-
mimeType,
|
|
109
|
-
symlinkTarget,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
export async function listDirectory(dirPath, options = {}) {
|
|
113
|
-
const { recursive = false, includeHidden = false, maxDepth = DEFAULT_MAX_DEPTH, maxEntries, sortBy = 'name', includeSymlinkTargets = false, } = options;
|
|
114
|
-
const validPath = await validateExistingPath(dirPath);
|
|
115
|
-
const entries = [];
|
|
116
|
-
let totalFiles = 0;
|
|
117
|
-
let totalDirectories = 0;
|
|
118
|
-
let maxDepthReached = 0;
|
|
119
|
-
let truncated = false;
|
|
120
|
-
let skippedInaccessible = 0;
|
|
121
|
-
let symlinksNotFollowed = 0;
|
|
122
|
-
const stopIfNeeded = () => {
|
|
123
|
-
if (maxEntries !== undefined && entries.length >= maxEntries) {
|
|
124
|
-
truncated = true;
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return false;
|
|
128
|
-
};
|
|
129
|
-
await runWorkQueue([{ currentPath: validPath, depth: 0 }], async ({ currentPath, depth }, enqueue) => {
|
|
130
|
-
if (depth > maxDepth)
|
|
131
|
-
return;
|
|
132
|
-
if (stopIfNeeded())
|
|
133
|
-
return;
|
|
134
|
-
maxDepthReached = Math.max(maxDepthReached, depth);
|
|
135
|
-
let items;
|
|
136
|
-
try {
|
|
137
|
-
items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
skippedInaccessible++;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const visibleItems = includeHidden
|
|
144
|
-
? items
|
|
145
|
-
: items.filter((item) => !isHidden(item.name));
|
|
146
|
-
const { results: processedEntries, errors: processingErrors } = await processInParallel(visibleItems, async (item) => {
|
|
147
|
-
const fullPath = path.join(currentPath, item.name);
|
|
148
|
-
const relativePath = path.relative(validPath, fullPath) || item.name;
|
|
149
|
-
try {
|
|
150
|
-
if (item.isSymbolicLink()) {
|
|
151
|
-
symlinksNotFollowed++;
|
|
152
|
-
const stats = await fs.lstat(fullPath);
|
|
153
|
-
let symlinkTarget;
|
|
154
|
-
if (includeSymlinkTargets) {
|
|
155
|
-
try {
|
|
156
|
-
symlinkTarget = await fs.readlink(fullPath);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Symlink target unreadable
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const entry = {
|
|
163
|
-
name: item.name,
|
|
164
|
-
path: fullPath,
|
|
165
|
-
relativePath,
|
|
166
|
-
type: 'symlink',
|
|
167
|
-
size: stats.size,
|
|
168
|
-
modified: stats.mtime,
|
|
169
|
-
symlinkTarget,
|
|
170
|
-
};
|
|
171
|
-
return { entry };
|
|
172
|
-
}
|
|
173
|
-
const stats = await fs.stat(fullPath);
|
|
174
|
-
const isDir = item.isDirectory();
|
|
175
|
-
const type = isDir
|
|
176
|
-
? 'directory'
|
|
177
|
-
: item.isFile()
|
|
178
|
-
? 'file'
|
|
179
|
-
: getFileType(stats);
|
|
180
|
-
const entry = {
|
|
181
|
-
name: item.name,
|
|
182
|
-
path: fullPath,
|
|
183
|
-
relativePath,
|
|
184
|
-
type,
|
|
185
|
-
size: type === 'file' ? stats.size : undefined,
|
|
186
|
-
modified: stats.mtime,
|
|
187
|
-
};
|
|
188
|
-
const enqueueDir = recursive && isDir && depth + 1 <= maxDepth
|
|
189
|
-
? {
|
|
190
|
-
currentPath: await validateExistingPath(fullPath),
|
|
191
|
-
depth: depth + 1,
|
|
192
|
-
}
|
|
193
|
-
: undefined;
|
|
194
|
-
return { entry, enqueueDir };
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
skippedInaccessible++;
|
|
198
|
-
const entry = {
|
|
199
|
-
name: item.name,
|
|
200
|
-
path: fullPath,
|
|
201
|
-
relativePath,
|
|
202
|
-
type: item.isDirectory()
|
|
203
|
-
? 'directory'
|
|
204
|
-
: item.isFile()
|
|
205
|
-
? 'file'
|
|
206
|
-
: 'other',
|
|
207
|
-
};
|
|
208
|
-
return { entry };
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
skippedInaccessible += processingErrors.length;
|
|
212
|
-
for (const { entry, enqueueDir } of processedEntries) {
|
|
213
|
-
if (stopIfNeeded())
|
|
214
|
-
break;
|
|
215
|
-
entries.push(entry);
|
|
216
|
-
if (entry.type === 'directory')
|
|
217
|
-
totalDirectories++;
|
|
218
|
-
if (entry.type === 'file')
|
|
219
|
-
totalFiles++;
|
|
220
|
-
if (enqueueDir)
|
|
221
|
-
enqueue(enqueueDir);
|
|
222
|
-
}
|
|
223
|
-
}, DIR_TRAVERSAL_CONCURRENCY);
|
|
224
|
-
sortByField(entries, sortBy);
|
|
225
|
-
return {
|
|
226
|
-
path: validPath,
|
|
227
|
-
entries,
|
|
228
|
-
summary: {
|
|
229
|
-
totalEntries: entries.length,
|
|
230
|
-
totalFiles,
|
|
231
|
-
totalDirectories,
|
|
232
|
-
maxDepthReached,
|
|
233
|
-
truncated,
|
|
234
|
-
skippedInaccessible,
|
|
235
|
-
symlinksNotFollowed,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
|
|
240
|
-
const validPath = await validateExistingPath(basePath);
|
|
241
|
-
const { maxResults, sortBy = 'path', maxDepth } = options;
|
|
242
|
-
const results = [];
|
|
243
|
-
let skippedInaccessible = 0;
|
|
244
|
-
let truncated = false;
|
|
245
|
-
let filesScanned = 0;
|
|
246
|
-
const batch = [];
|
|
247
|
-
const flushBatch = async () => {
|
|
248
|
-
if (batch.length === 0)
|
|
249
|
-
return;
|
|
250
|
-
const toProcess = batch.splice(0, batch.length);
|
|
251
|
-
const settled = await Promise.allSettled(toProcess.map(async (match) => {
|
|
252
|
-
const stats = await fs.stat(match);
|
|
253
|
-
const { size, mtime: modified } = stats;
|
|
254
|
-
return {
|
|
255
|
-
path: match,
|
|
256
|
-
type: getFileType(stats),
|
|
257
|
-
size: stats.isFile() ? size : undefined,
|
|
258
|
-
modified,
|
|
259
|
-
};
|
|
260
|
-
}));
|
|
261
|
-
for (const r of settled) {
|
|
262
|
-
if (r.status === 'fulfilled') {
|
|
263
|
-
if (maxResults !== undefined && results.length >= maxResults) {
|
|
264
|
-
truncated = true;
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
results.push(r.value);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
skippedInaccessible++;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const stream = fg.stream(pattern, {
|
|
275
|
-
cwd: validPath,
|
|
276
|
-
absolute: true,
|
|
277
|
-
onlyFiles: false,
|
|
278
|
-
dot: true,
|
|
279
|
-
ignore: excludePatterns,
|
|
280
|
-
suppressErrors: true,
|
|
281
|
-
followSymbolicLinks: false,
|
|
282
|
-
deep: maxDepth,
|
|
283
|
-
});
|
|
284
|
-
for await (const entry of stream) {
|
|
285
|
-
const matchPath = typeof entry === 'string' ? entry : String(entry);
|
|
286
|
-
filesScanned++;
|
|
287
|
-
if (maxResults !== undefined && results.length >= maxResults) {
|
|
288
|
-
truncated = true;
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
batch.push(matchPath);
|
|
292
|
-
if (batch.length >= PARALLEL_CONCURRENCY) {
|
|
293
|
-
await flushBatch();
|
|
294
|
-
if (maxResults !== undefined && results.length >= maxResults) {
|
|
295
|
-
truncated = true;
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
await flushBatch();
|
|
301
|
-
sortSearchResults(results, sortBy);
|
|
302
|
-
return {
|
|
303
|
-
basePath: validPath,
|
|
304
|
-
pattern,
|
|
305
|
-
results,
|
|
306
|
-
summary: {
|
|
307
|
-
matched: results.length,
|
|
308
|
-
truncated,
|
|
309
|
-
skippedInaccessible,
|
|
310
|
-
filesScanned,
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
export { readFile };
|
|
315
|
-
export async function readMultipleFiles(filePaths, options = {}) {
|
|
316
|
-
const { encoding = 'utf-8', maxSize = MAX_TEXT_FILE_SIZE, maxTotalSize = 100 * 1024 * 1024, head, tail, } = options;
|
|
317
|
-
if (filePaths.length === 0)
|
|
318
|
-
return [];
|
|
319
|
-
const output = filePaths.map((filePath) => ({ path: filePath }));
|
|
320
|
-
let totalSize = 0;
|
|
321
|
-
const fileSizes = new Map();
|
|
322
|
-
for (const filePath of filePaths) {
|
|
323
|
-
try {
|
|
324
|
-
const validPath = await validateExistingPath(filePath);
|
|
325
|
-
const stats = await fs.stat(validPath);
|
|
326
|
-
fileSizes.set(filePath, stats.size);
|
|
327
|
-
totalSize += stats.size;
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
fileSizes.set(filePath, 0);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (totalSize > maxTotalSize) {
|
|
334
|
-
throw new McpError(ErrorCode.E_TOO_LARGE, `Total size of all files (${totalSize} bytes) exceeds limit (${maxTotalSize} bytes)`, undefined, { totalSize, maxTotalSize, fileCount: filePaths.length });
|
|
335
|
-
}
|
|
336
|
-
const { results, errors } = await processInParallel(filePaths.map((filePath, index) => ({ filePath, index })), async ({ filePath, index }) => {
|
|
337
|
-
const result = await readFile(filePath, {
|
|
338
|
-
encoding,
|
|
339
|
-
maxSize,
|
|
340
|
-
head,
|
|
341
|
-
tail,
|
|
342
|
-
});
|
|
343
|
-
return {
|
|
344
|
-
index,
|
|
345
|
-
value: { path: result.path, content: result.content },
|
|
346
|
-
};
|
|
347
|
-
}, PARALLEL_CONCURRENCY);
|
|
348
|
-
for (const r of results) {
|
|
349
|
-
output[r.index] = r.value;
|
|
350
|
-
}
|
|
351
|
-
for (const e of errors) {
|
|
352
|
-
const filePath = filePaths[e.index] ?? '(unknown)';
|
|
353
|
-
output[e.index] = {
|
|
354
|
-
path: filePath,
|
|
355
|
-
error: e.error.message,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
return output;
|
|
359
|
-
}
|
|
360
|
-
export async function searchContent(basePath, searchPattern, options = {}) {
|
|
361
|
-
const { filePattern = '**/*', excludePatterns = [], caseSensitive = false, maxResults = DEFAULT_MAX_RESULTS, maxFileSize = MAX_SEARCHABLE_FILE_SIZE, maxFilesScanned, timeoutMs, skipBinary = true, contextLines = 0, wholeWord = false, isLiteral = false, } = options;
|
|
362
|
-
const validPath = await validateExistingPath(basePath);
|
|
363
|
-
const deadlineMs = timeoutMs !== undefined ? Date.now() + timeoutMs : undefined;
|
|
364
|
-
// Prepare the search pattern with optional literal escaping and word boundaries
|
|
365
|
-
const finalPattern = prepareSearchPattern(searchPattern, {
|
|
366
|
-
isLiteral,
|
|
367
|
-
wholeWord,
|
|
368
|
-
});
|
|
369
|
-
const needsReDoSCheck = !isLiteral && !isSimpleSafePattern(finalPattern);
|
|
370
|
-
if (needsReDoSCheck && !safeRegex(finalPattern)) {
|
|
371
|
-
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Potentially unsafe regular expression (ReDoS risk): ${searchPattern}. ` +
|
|
372
|
-
'Avoid patterns with nested quantifiers, overlapping alternations, or exponential backtracking.', basePath, { reason: 'ReDoS risk detected' });
|
|
373
|
-
}
|
|
374
|
-
let regex;
|
|
375
|
-
try {
|
|
376
|
-
regex = new RegExp(finalPattern, caseSensitive ? 'g' : 'gi');
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
-
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regular expression: ${finalPattern} (${message})`, basePath, { searchPattern: finalPattern });
|
|
381
|
-
}
|
|
382
|
-
const matches = [];
|
|
383
|
-
let filesScanned = 0;
|
|
384
|
-
let filesMatched = 0;
|
|
385
|
-
let skippedTooLarge = 0;
|
|
386
|
-
let skippedBinary = 0;
|
|
387
|
-
let skippedInaccessible = 0;
|
|
388
|
-
let linesSkippedDueToRegexTimeout = 0;
|
|
389
|
-
let truncated = false;
|
|
390
|
-
let stoppedReason;
|
|
391
|
-
let firstPathValidated = false;
|
|
392
|
-
const stopNow = (reason) => {
|
|
393
|
-
truncated = true;
|
|
394
|
-
stoppedReason = reason;
|
|
395
|
-
return true;
|
|
396
|
-
};
|
|
397
|
-
const stream = fg.stream(filePattern, {
|
|
398
|
-
cwd: validPath,
|
|
399
|
-
absolute: true,
|
|
400
|
-
onlyFiles: true,
|
|
401
|
-
dot: false,
|
|
402
|
-
ignore: excludePatterns,
|
|
403
|
-
suppressErrors: true,
|
|
404
|
-
followSymbolicLinks: false,
|
|
405
|
-
});
|
|
406
|
-
try {
|
|
407
|
-
for await (const entry of stream) {
|
|
408
|
-
const file = typeof entry === 'string' ? entry : String(entry);
|
|
409
|
-
if (!firstPathValidated) {
|
|
410
|
-
try {
|
|
411
|
-
await validateExistingPath(file);
|
|
412
|
-
firstPathValidated = true;
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
console.error('[SECURITY] fast-glob returned invalid path:', file);
|
|
416
|
-
stopNow('maxFiles');
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (deadlineMs !== undefined && Date.now() > deadlineMs) {
|
|
421
|
-
stopNow('timeout');
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
if (maxFilesScanned !== undefined && filesScanned >= maxFilesScanned) {
|
|
425
|
-
stopNow('maxFiles');
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
if (matches.length >= maxResults) {
|
|
429
|
-
stopNow('maxResults');
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
try {
|
|
433
|
-
const handle = await fs.open(file, 'r');
|
|
434
|
-
let shouldScan = true;
|
|
435
|
-
try {
|
|
436
|
-
const stats = await handle.stat();
|
|
437
|
-
filesScanned++;
|
|
438
|
-
if (stats.size > maxFileSize) {
|
|
439
|
-
skippedTooLarge++;
|
|
440
|
-
shouldScan = false;
|
|
441
|
-
}
|
|
442
|
-
else if (skipBinary) {
|
|
443
|
-
const binary = await isProbablyBinary(file, handle);
|
|
444
|
-
if (binary) {
|
|
445
|
-
skippedBinary++;
|
|
446
|
-
shouldScan = false;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
finally {
|
|
451
|
-
await handle.close().catch(() => { });
|
|
452
|
-
}
|
|
453
|
-
if (!shouldScan)
|
|
454
|
-
continue;
|
|
455
|
-
const scanResult = await scanFileForContent(file, regex, {
|
|
456
|
-
maxResults,
|
|
457
|
-
contextLines,
|
|
458
|
-
deadlineMs,
|
|
459
|
-
currentMatchCount: matches.length,
|
|
460
|
-
isLiteral,
|
|
461
|
-
searchString: isLiteral ? searchPattern : undefined,
|
|
462
|
-
caseSensitive,
|
|
463
|
-
});
|
|
464
|
-
matches.push(...scanResult.matches);
|
|
465
|
-
linesSkippedDueToRegexTimeout +=
|
|
466
|
-
scanResult.linesSkippedDueToRegexTimeout;
|
|
467
|
-
if (scanResult.fileHadMatches)
|
|
468
|
-
filesMatched++;
|
|
469
|
-
if (deadlineMs !== undefined && Date.now() > deadlineMs) {
|
|
470
|
-
stopNow('timeout');
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
if (matches.length >= maxResults) {
|
|
474
|
-
stopNow('maxResults');
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
if (stoppedReason !== undefined)
|
|
478
|
-
break;
|
|
479
|
-
}
|
|
480
|
-
catch {
|
|
481
|
-
skippedInaccessible++;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
finally {
|
|
486
|
-
// Ensure the stream is closed
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
basePath: validPath,
|
|
490
|
-
pattern: searchPattern,
|
|
491
|
-
filePattern,
|
|
492
|
-
matches,
|
|
493
|
-
summary: {
|
|
494
|
-
filesScanned,
|
|
495
|
-
filesMatched,
|
|
496
|
-
matches: matches.length,
|
|
497
|
-
truncated,
|
|
498
|
-
skippedTooLarge,
|
|
499
|
-
skippedBinary,
|
|
500
|
-
skippedInaccessible,
|
|
501
|
-
linesSkippedDueToRegexTimeout,
|
|
502
|
-
stoppedReason,
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
export async function analyzeDirectory(dirPath, options = {}) {
|
|
507
|
-
const { maxDepth = DEFAULT_MAX_DEPTH, topN = DEFAULT_TOP_N, excludePatterns = [], includeHidden = false, } = options;
|
|
508
|
-
const validPath = await validateExistingPath(dirPath);
|
|
509
|
-
let totalFiles = 0;
|
|
510
|
-
let totalDirectories = 0;
|
|
511
|
-
let totalSize = 0;
|
|
512
|
-
let currentMaxDepth = 0;
|
|
513
|
-
let skippedInaccessible = 0;
|
|
514
|
-
let symlinksNotFollowed = 0;
|
|
515
|
-
const fileTypes = {};
|
|
516
|
-
const largestFiles = [];
|
|
517
|
-
const recentlyModified = [];
|
|
518
|
-
const shouldExclude = createExcludeMatcher(excludePatterns);
|
|
519
|
-
await runWorkQueue([{ currentPath: validPath, depth: 0 }], async ({ currentPath, depth }, enqueue) => {
|
|
520
|
-
if (depth > maxDepth)
|
|
521
|
-
return;
|
|
522
|
-
currentMaxDepth = Math.max(currentMaxDepth, depth);
|
|
523
|
-
let items;
|
|
524
|
-
try {
|
|
525
|
-
items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
526
|
-
}
|
|
527
|
-
catch {
|
|
528
|
-
skippedInaccessible++;
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
for (const item of items) {
|
|
532
|
-
const fullPath = path.join(currentPath, item.name);
|
|
533
|
-
const relativePath = path.relative(validPath, fullPath);
|
|
534
|
-
if (!includeHidden && isHidden(item.name)) {
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
if (shouldExclude(item.name, relativePath)) {
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
const validated = await validateExistingPathDetailed(fullPath);
|
|
542
|
-
if (validated.isSymlink || item.isSymbolicLink()) {
|
|
543
|
-
symlinksNotFollowed++;
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
const stats = await fs.stat(validated.resolvedPath);
|
|
547
|
-
if (stats.isDirectory()) {
|
|
548
|
-
totalDirectories++;
|
|
549
|
-
if (depth + 1 <= maxDepth) {
|
|
550
|
-
enqueue({
|
|
551
|
-
currentPath: validated.resolvedPath,
|
|
552
|
-
depth: depth + 1,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
else if (stats.isFile()) {
|
|
557
|
-
totalFiles++;
|
|
558
|
-
totalSize += stats.size;
|
|
559
|
-
const ext = path.extname(item.name).toLowerCase() || '(no extension)';
|
|
560
|
-
fileTypes[ext] = (fileTypes[ext] ?? 0) + 1;
|
|
561
|
-
insertSorted(largestFiles, { path: validated.resolvedPath, size: stats.size }, (a, b) => b.size - a.size, topN);
|
|
562
|
-
insertSorted(recentlyModified, { path: validated.resolvedPath, modified: stats.mtime }, (a, b) => b.modified.getTime() - a.modified.getTime(), topN);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
catch (error) {
|
|
566
|
-
if (classifyAccessError(error) === 'symlink') {
|
|
567
|
-
symlinksNotFollowed++;
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
skippedInaccessible++;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}, DIR_TRAVERSAL_CONCURRENCY);
|
|
575
|
-
const analysis = {
|
|
576
|
-
path: validPath,
|
|
577
|
-
totalFiles,
|
|
578
|
-
totalDirectories,
|
|
579
|
-
totalSize,
|
|
580
|
-
fileTypes,
|
|
581
|
-
largestFiles,
|
|
582
|
-
recentlyModified,
|
|
583
|
-
maxDepth: currentMaxDepth,
|
|
584
|
-
};
|
|
585
|
-
return {
|
|
586
|
-
analysis,
|
|
587
|
-
summary: {
|
|
588
|
-
truncated: false,
|
|
589
|
-
skippedInaccessible,
|
|
590
|
-
symlinksNotFollowed,
|
|
591
|
-
},
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
export async function getDirectoryTree(dirPath, options = {}) {
|
|
595
|
-
const { maxDepth = DEFAULT_MAX_DEPTH, excludePatterns = [], includeHidden = false, includeSize = false, maxFiles, } = options;
|
|
596
|
-
const validPath = await validateExistingPath(dirPath);
|
|
597
|
-
const rootStats = await fs.stat(validPath);
|
|
598
|
-
if (!rootStats.isDirectory()) {
|
|
599
|
-
throw new McpError(ErrorCode.E_NOT_DIRECTORY, `Not a directory: ${dirPath}`, dirPath);
|
|
600
|
-
}
|
|
601
|
-
let totalFiles = 0;
|
|
602
|
-
let totalDirectories = 0;
|
|
603
|
-
let maxDepthReached = 0;
|
|
604
|
-
let skippedInaccessible = 0;
|
|
605
|
-
let symlinksNotFollowed = 0;
|
|
606
|
-
let truncated = false;
|
|
607
|
-
const shouldExclude = createExcludeMatcher(excludePatterns);
|
|
608
|
-
const hitMaxFiles = () => {
|
|
609
|
-
return maxFiles !== undefined && totalFiles >= maxFiles;
|
|
610
|
-
};
|
|
611
|
-
const collectedEntries = [];
|
|
612
|
-
const directoriesFound = new Set([validPath]);
|
|
613
|
-
await runWorkQueue([{ currentPath: validPath, depth: 0 }], async ({ currentPath, depth }, enqueue) => {
|
|
614
|
-
if (hitMaxFiles()) {
|
|
615
|
-
truncated = true;
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (depth > maxDepth) {
|
|
619
|
-
truncated = true;
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
maxDepthReached = Math.max(maxDepthReached, depth);
|
|
623
|
-
let items;
|
|
624
|
-
try {
|
|
625
|
-
items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
skippedInaccessible++;
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
for (const item of items) {
|
|
632
|
-
if (hitMaxFiles()) {
|
|
633
|
-
truncated = true;
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
const { name } = item;
|
|
637
|
-
if (!includeHidden && name.startsWith('.')) {
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const fullPath = path.join(currentPath, name);
|
|
641
|
-
const relativePath = path.relative(validPath, fullPath);
|
|
642
|
-
if (shouldExclude(name, relativePath)) {
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
if (item.isSymbolicLink()) {
|
|
646
|
-
symlinksNotFollowed++;
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
try {
|
|
650
|
-
const { resolvedPath, isSymlink } = await validateExistingPathDetailed(fullPath);
|
|
651
|
-
if (isSymlink) {
|
|
652
|
-
symlinksNotFollowed++;
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const stats = await fs.stat(resolvedPath);
|
|
656
|
-
if (stats.isFile()) {
|
|
657
|
-
totalFiles++;
|
|
658
|
-
collectedEntries.push({
|
|
659
|
-
parentPath: currentPath,
|
|
660
|
-
name,
|
|
661
|
-
type: 'file',
|
|
662
|
-
size: includeSize ? stats.size : undefined,
|
|
663
|
-
depth,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
else if (stats.isDirectory()) {
|
|
667
|
-
totalDirectories++;
|
|
668
|
-
directoriesFound.add(resolvedPath);
|
|
669
|
-
collectedEntries.push({
|
|
670
|
-
parentPath: currentPath,
|
|
671
|
-
name,
|
|
672
|
-
type: 'directory',
|
|
673
|
-
depth,
|
|
674
|
-
});
|
|
675
|
-
if (depth + 1 <= maxDepth) {
|
|
676
|
-
enqueue({ currentPath: resolvedPath, depth: depth + 1 });
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
truncated = true;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
if (classifyAccessError(error) === 'symlink') {
|
|
685
|
-
symlinksNotFollowed++;
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
skippedInaccessible++;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}, DIR_TRAVERSAL_CONCURRENCY);
|
|
693
|
-
const childrenByParent = new Map();
|
|
694
|
-
for (const dirPath of directoriesFound) {
|
|
695
|
-
childrenByParent.set(dirPath, []);
|
|
696
|
-
}
|
|
697
|
-
for (const entry of collectedEntries) {
|
|
698
|
-
const treeEntry = {
|
|
699
|
-
name: entry.name,
|
|
700
|
-
type: entry.type,
|
|
701
|
-
};
|
|
702
|
-
if (entry.type === 'file' && entry.size !== undefined) {
|
|
703
|
-
treeEntry.size = entry.size;
|
|
704
|
-
}
|
|
705
|
-
if (entry.type === 'directory') {
|
|
706
|
-
const fullPath = path.join(entry.parentPath, entry.name);
|
|
707
|
-
treeEntry.children = childrenByParent.get(fullPath) ?? [];
|
|
708
|
-
}
|
|
709
|
-
const siblings = childrenByParent.get(entry.parentPath);
|
|
710
|
-
if (siblings) {
|
|
711
|
-
siblings.push(treeEntry);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const sortChildren = (entries) => {
|
|
715
|
-
entries.sort((a, b) => {
|
|
716
|
-
if (a.type !== b.type) {
|
|
717
|
-
return a.type === 'directory' ? -1 : 1;
|
|
718
|
-
}
|
|
719
|
-
return a.name.localeCompare(b.name);
|
|
720
|
-
});
|
|
721
|
-
};
|
|
722
|
-
for (const children of childrenByParent.values()) {
|
|
723
|
-
sortChildren(children);
|
|
724
|
-
}
|
|
725
|
-
const rootName = path.basename(validPath);
|
|
726
|
-
const tree = {
|
|
727
|
-
name: rootName || validPath,
|
|
728
|
-
type: 'directory',
|
|
729
|
-
children: childrenByParent.get(validPath) ?? [],
|
|
730
|
-
};
|
|
731
|
-
return {
|
|
732
|
-
tree,
|
|
733
|
-
summary: {
|
|
734
|
-
totalFiles,
|
|
735
|
-
totalDirectories,
|
|
736
|
-
maxDepthReached,
|
|
737
|
-
truncated,
|
|
738
|
-
skippedInaccessible,
|
|
739
|
-
symlinksNotFollowed,
|
|
740
|
-
},
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
export async function readMediaFile(filePath, { maxSize = MAX_MEDIA_FILE_SIZE } = {}) {
|
|
744
|
-
const validPath = await validateExistingPath(filePath);
|
|
745
|
-
const stats = await fs.stat(validPath);
|
|
746
|
-
const { size } = stats;
|
|
747
|
-
if (!stats.isFile()) {
|
|
748
|
-
throw new McpError(ErrorCode.E_NOT_FILE, `Not a file: ${filePath}`, filePath);
|
|
749
|
-
}
|
|
750
|
-
if (size > maxSize) {
|
|
751
|
-
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes)`, filePath, { size, maxSize });
|
|
752
|
-
}
|
|
753
|
-
const ext = path.extname(validPath).toLowerCase();
|
|
754
|
-
const mimeType = getMimeType(ext);
|
|
755
|
-
const buffer = await fs.readFile(validPath);
|
|
756
|
-
const data = buffer.toString('base64');
|
|
757
|
-
let width;
|
|
758
|
-
let height;
|
|
759
|
-
if (mimeType.startsWith('image/')) {
|
|
760
|
-
const dimensions = parseImageDimensions(buffer, ext);
|
|
761
|
-
if (dimensions) {
|
|
762
|
-
({ width, height } = dimensions);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return {
|
|
766
|
-
path: validPath,
|
|
767
|
-
mimeType,
|
|
768
|
-
size,
|
|
769
|
-
data,
|
|
770
|
-
width,
|
|
771
|
-
height,
|
|
772
|
-
};
|
|
773
|
-
}
|
|
1
|
+
export { analyzeDirectory } from './file-operations/analyze-directory.js';
|
|
2
|
+
export { getDirectoryTree } from './file-operations/directory-tree.js';
|
|
3
|
+
export { getFileInfo } from './file-operations/file-info.js';
|
|
4
|
+
export { listDirectory } from './file-operations/list-directory.js';
|
|
5
|
+
export { readMediaFile } from './file-operations/read-media-file.js';
|
|
6
|
+
export { readMultipleFiles } from './file-operations/read-multiple-files.js';
|
|
7
|
+
export { searchContent } from './file-operations/search-content.js';
|
|
8
|
+
export { searchFiles } from './file-operations/search-files.js';
|
|
9
|
+
export { readFile } from './fs-helpers.js';
|
|
774
10
|
//# sourceMappingURL=file-operations.js.map
|