@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
3
|
import ignore, {} from 'ignore';
|
|
4
4
|
import { isNodeError } from '../errors.js';
|
|
5
5
|
import { toPosixPath } from '../paths.js';
|
|
@@ -40,7 +40,8 @@ export function compareOptionalNumberDesc(left, right, tieBreak) {
|
|
|
40
40
|
}
|
|
41
41
|
export function stableSortByDerivedString(items, derive, tieBreak) {
|
|
42
42
|
const decorated = [];
|
|
43
|
-
|
|
43
|
+
const len = items.length;
|
|
44
|
+
for (let index = 0; index < len; index++) {
|
|
44
45
|
const item = items[index];
|
|
45
46
|
if (item === undefined)
|
|
46
47
|
continue;
|
|
@@ -59,7 +60,8 @@ export function stableSortByDerivedString(items, derive, tieBreak) {
|
|
|
59
60
|
return tiedCompare;
|
|
60
61
|
return left.index - right.index;
|
|
61
62
|
});
|
|
62
|
-
|
|
63
|
+
const decoratedLen = decorated.length;
|
|
64
|
+
for (let index = 0; index < decoratedLen; index++) {
|
|
63
65
|
const entry = decorated[index];
|
|
64
66
|
if (!entry)
|
|
65
67
|
continue;
|
|
@@ -74,13 +76,14 @@ export function applyIndexedValues(output, results) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
export function applyIndexedErrors(options) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const { output, errors, resolveIndex, buildValue } = options;
|
|
80
|
+
for (const failure of errors) {
|
|
81
|
+
const resolvedIndex = resolveIndex(failure.index);
|
|
79
82
|
if (resolvedIndex === undefined)
|
|
80
83
|
continue;
|
|
81
|
-
if (resolvedIndex < 0 || resolvedIndex >=
|
|
84
|
+
if (resolvedIndex < 0 || resolvedIndex >= output.length)
|
|
82
85
|
continue;
|
|
83
|
-
|
|
86
|
+
output[resolvedIndex] = buildValue(resolvedIndex, failure.error);
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
export async function isEntryAccessibleByType(entryPath, entryType, rootDirectories, signal, deps) {
|
|
@@ -101,8 +104,9 @@ export async function isEntryAccessibleByType(entryPath, entryType, rootDirector
|
|
|
101
104
|
}
|
|
102
105
|
function parseGitignoreLines(contents) {
|
|
103
106
|
const lines = [];
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const parts = contents.split(/\r?\n/u);
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
const trimmed = part.trim();
|
|
106
110
|
if (trimmed.length > 0) {
|
|
107
111
|
lines.push(trimmed);
|
|
108
112
|
}
|
|
@@ -110,13 +114,15 @@ function parseGitignoreLines(contents) {
|
|
|
110
114
|
return lines;
|
|
111
115
|
}
|
|
112
116
|
export async function loadRootGitignore(root, signal) {
|
|
113
|
-
const gitignorePath =
|
|
114
|
-
let contents;
|
|
117
|
+
const gitignorePath = join(root, '.gitignore');
|
|
115
118
|
try {
|
|
116
|
-
contents = await
|
|
119
|
+
const contents = await readFile(gitignorePath, {
|
|
117
120
|
encoding: 'utf-8',
|
|
118
121
|
signal,
|
|
119
122
|
});
|
|
123
|
+
const matcher = ignore();
|
|
124
|
+
matcher.add(parseGitignoreLines(contents));
|
|
125
|
+
return matcher;
|
|
120
126
|
}
|
|
121
127
|
catch (error) {
|
|
122
128
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
@@ -124,16 +130,13 @@ export async function loadRootGitignore(root, signal) {
|
|
|
124
130
|
}
|
|
125
131
|
throw error;
|
|
126
132
|
}
|
|
127
|
-
const matcher = ignore();
|
|
128
|
-
matcher.add(parseGitignoreLines(contents));
|
|
129
|
-
return matcher;
|
|
130
133
|
}
|
|
131
134
|
export function isIgnoredByGitignore(matcher, root, absolutePath, options = {}) {
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
+
let { relativePath } = options;
|
|
136
|
+
relativePath ??= relative(root, absolutePath);
|
|
137
|
+
if (relativePath.length === 0)
|
|
135
138
|
return false;
|
|
136
|
-
const normalized = toPosixPath(
|
|
139
|
+
const normalized = toPosixPath(relativePath);
|
|
137
140
|
if (options.isDirectory) {
|
|
138
141
|
return matcher.ignores(normalized.endsWith('/') ? normalized : `${normalized}/`);
|
|
139
142
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileInfo, GetMultipleFileInfoResult, ListDirectoryResult } from '../../config.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type EntryType } from './core.js';
|
|
3
3
|
interface FileInfoOptions {
|
|
4
4
|
includeMimeType?: boolean | undefined;
|
|
5
5
|
signal?: AbortSignal | undefined;
|
|
@@ -60,7 +60,7 @@ interface ReadMultipleResult {
|
|
|
60
60
|
endLine?: number;
|
|
61
61
|
linesRead?: number;
|
|
62
62
|
hasMoreLines?: boolean;
|
|
63
|
-
error?:
|
|
63
|
+
error?: Error;
|
|
64
64
|
}
|
|
65
65
|
interface ReadMultipleOptions {
|
|
66
66
|
encoding?: BufferEncoding;
|
|
@@ -1,11 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { lstat, readdir, readlink, stat, } from 'node:fs';
|
|
2
|
+
import { basename, join, parse, relative } from 'node:path';
|
|
3
|
+
import { assertNotAborted, withAbort, withTimedAbortSignal } from '../abort.js';
|
|
3
4
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, getMimeType, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
4
5
|
import { isAbortError } from '../errors.js';
|
|
5
|
-
import {
|
|
6
|
+
import { getFileType, isHidden, processInParallel, readFile, readFileWithStats, } from '../fs-helpers.js';
|
|
7
|
+
import { assertSafeGlobPattern } from '../globs.js';
|
|
6
8
|
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, toPosixPath, validateExistingDirectory, validateExistingPath, validateExistingPathDetailed, } from '../paths.js';
|
|
7
9
|
import { applyIndexedErrors, applyIndexedValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './core.js';
|
|
8
10
|
import { globEntries } from './traversal.js';
|
|
11
|
+
function statAsync(filePath) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
stat(filePath, (err, stats) => {
|
|
14
|
+
if (err)
|
|
15
|
+
reject(err);
|
|
16
|
+
else
|
|
17
|
+
resolve(stats);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function readlinkAsync(filePath) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
readlink(filePath, (err, linkString) => {
|
|
24
|
+
if (err)
|
|
25
|
+
reject(err);
|
|
26
|
+
else
|
|
27
|
+
resolve(linkString);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function readdirAsync(dirPath, options) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
readdir(dirPath, options, (err, files) => {
|
|
34
|
+
if (err)
|
|
35
|
+
reject(err);
|
|
36
|
+
else
|
|
37
|
+
resolve(files);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function lstatAsync(filePath) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
lstat(filePath, (err, stats) => {
|
|
44
|
+
if (err)
|
|
45
|
+
reject(err);
|
|
46
|
+
else
|
|
47
|
+
resolve(stats);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
9
51
|
const ACCESS_DEPS = {
|
|
10
52
|
normalizePath,
|
|
11
53
|
isPathWithinDirectories,
|
|
@@ -48,7 +90,7 @@ function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, sy
|
|
|
48
90
|
async function getSymlinkTarget(pathToRead, signal) {
|
|
49
91
|
assertNotAborted(signal);
|
|
50
92
|
try {
|
|
51
|
-
return await withAbort(
|
|
93
|
+
return await withAbort(readlinkAsync(pathToRead), signal);
|
|
52
94
|
}
|
|
53
95
|
catch (error) {
|
|
54
96
|
if (isAbortError(error))
|
|
@@ -61,14 +103,14 @@ export async function getFileInfo(filePath, options = {}) {
|
|
|
61
103
|
assertNotAborted(signal);
|
|
62
104
|
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
|
|
63
105
|
assertAllowedFileAccess(requestedPath, resolvedPath);
|
|
64
|
-
const { base: name, ext: rawExt } =
|
|
106
|
+
const { base: name, ext: rawExt } = parse(requestedPath);
|
|
65
107
|
const ext = rawExt.toLowerCase();
|
|
66
108
|
const includeMimeType = options.includeMimeType !== false;
|
|
67
109
|
const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
|
|
68
110
|
const symlinkTarget = isSymlink
|
|
69
111
|
? await getSymlinkTarget(requestedPath, signal)
|
|
70
112
|
: undefined;
|
|
71
|
-
const stats = await withAbort(
|
|
113
|
+
const stats = await withAbort(statAsync(resolvedPath), signal);
|
|
72
114
|
return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
|
|
73
115
|
}
|
|
74
116
|
function buildEmptyResult() {
|
|
@@ -130,7 +172,7 @@ export async function getMultipleFileInfo(paths, options = {}) {
|
|
|
130
172
|
: undefined,
|
|
131
173
|
buildValue: (resolvedIndex, error) => ({
|
|
132
174
|
path: paths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
133
|
-
error
|
|
175
|
+
error,
|
|
134
176
|
}),
|
|
135
177
|
});
|
|
136
178
|
return {
|
|
@@ -149,6 +191,7 @@ function normalizeListOptions(options) {
|
|
|
149
191
|
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
150
192
|
};
|
|
151
193
|
if (options.pattern && options.pattern.length > 0) {
|
|
194
|
+
assertSafeGlobPattern(options.pattern);
|
|
152
195
|
normalized.pattern = options.pattern;
|
|
153
196
|
}
|
|
154
197
|
return normalized;
|
|
@@ -166,12 +209,12 @@ function resolveMaxDepth(normalized) {
|
|
|
166
209
|
return normalized.pattern ? normalized.maxDepth : 1;
|
|
167
210
|
}
|
|
168
211
|
async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
169
|
-
const dirents = await withAbort(
|
|
212
|
+
const dirents = await withAbort(readdirAsync(basePath, { withFileTypes: true }), signal);
|
|
170
213
|
if (!needsStats) {
|
|
171
214
|
for (const dirent of dirents) {
|
|
172
215
|
if (!normalized.includeHidden && isHidden(dirent.name))
|
|
173
216
|
continue;
|
|
174
|
-
yield { path:
|
|
217
|
+
yield { path: join(basePath, dirent.name), dirent };
|
|
175
218
|
}
|
|
176
219
|
return;
|
|
177
220
|
}
|
|
@@ -179,12 +222,12 @@ async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
|
179
222
|
for (const dirent of dirents) {
|
|
180
223
|
if (!normalized.includeHidden && isHidden(dirent.name))
|
|
181
224
|
continue;
|
|
182
|
-
filtered.push({ dirent, entryPath:
|
|
225
|
+
filtered.push({ dirent, entryPath: join(basePath, dirent.name) });
|
|
183
226
|
}
|
|
184
227
|
const { results, errors } = await processInParallel(filtered, async ({ entryPath, dirent }) => ({
|
|
185
228
|
path: entryPath,
|
|
186
229
|
dirent,
|
|
187
|
-
stats: await withAbort(
|
|
230
|
+
stats: await withAbort(lstatAsync(entryPath), signal),
|
|
188
231
|
}), PARALLEL_CONCURRENCY, signal);
|
|
189
232
|
if (errors.length > 0) {
|
|
190
233
|
throw errors[0]?.error ?? new Error('Failed to read entry stats');
|
|
@@ -214,13 +257,13 @@ function shouldUseFastPath(normalized, maxDepth) {
|
|
|
214
257
|
maxDepth === 1);
|
|
215
258
|
}
|
|
216
259
|
function resolveRelativePath(basePath, entryPath) {
|
|
217
|
-
return
|
|
260
|
+
return relative(basePath, entryPath) || basename(entryPath);
|
|
218
261
|
}
|
|
219
262
|
async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath) {
|
|
220
263
|
if (entryType !== 'symlink' || !includeSymlinkTargets) {
|
|
221
264
|
return undefined;
|
|
222
265
|
}
|
|
223
|
-
return
|
|
266
|
+
return readlinkAsync(entryPath).catch(() => undefined);
|
|
224
267
|
}
|
|
225
268
|
function updateTotals(entryType, totals) {
|
|
226
269
|
if (entryType === 'file')
|
|
@@ -232,7 +275,7 @@ function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarg
|
|
|
232
275
|
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
233
276
|
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
234
277
|
return {
|
|
235
|
-
name:
|
|
278
|
+
name: basename(entry.path),
|
|
236
279
|
path: entry.path,
|
|
237
280
|
relativePath: resolveRelativePath(basePath, entry.path),
|
|
238
281
|
type: entryType,
|
|
@@ -421,7 +464,7 @@ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher,
|
|
|
421
464
|
return null;
|
|
422
465
|
}
|
|
423
466
|
const relativePosix = toPosixPath(resolveRelativePath(root, entry.path));
|
|
424
|
-
const name =
|
|
467
|
+
const name = basename(entry.path);
|
|
425
468
|
return { type, relativePosix, name };
|
|
426
469
|
}
|
|
427
470
|
function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
|
|
@@ -521,7 +564,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
521
564
|
? null
|
|
522
565
|
: await loadRootGitignore(root, signal);
|
|
523
566
|
const rootNode = {
|
|
524
|
-
name:
|
|
567
|
+
name: basename(root) || root,
|
|
525
568
|
type: 'directory',
|
|
526
569
|
relativePath: '.',
|
|
527
570
|
children: [],
|
|
@@ -590,6 +633,7 @@ function buildReadOptions(options) {
|
|
|
590
633
|
const readOptions = {
|
|
591
634
|
encoding: options.encoding,
|
|
592
635
|
maxSize: options.maxSize,
|
|
636
|
+
skipBinary: true,
|
|
593
637
|
};
|
|
594
638
|
applyLineSelection(readOptions, options);
|
|
595
639
|
return readOptions;
|
|
@@ -653,10 +697,13 @@ function applyLineSelection(target, source) {
|
|
|
653
697
|
target.head = source.head;
|
|
654
698
|
if (source.tail !== undefined)
|
|
655
699
|
target.tail = source.tail;
|
|
656
|
-
if (source.
|
|
657
|
-
target.startLine = source.startLine;
|
|
658
|
-
if (source.endLine !== undefined)
|
|
700
|
+
if (source.endLine !== undefined) {
|
|
701
|
+
target.startLine = source.startLine ?? 1;
|
|
659
702
|
target.endLine = source.endLine;
|
|
703
|
+
}
|
|
704
|
+
else if (source.startLine !== undefined) {
|
|
705
|
+
target.startLine = source.startLine;
|
|
706
|
+
}
|
|
660
707
|
}
|
|
661
708
|
function resolveNormalizedReadOptions(options) {
|
|
662
709
|
const { signal, ...rest } = options;
|
|
@@ -669,7 +716,7 @@ function resolveNormalizedReadOptions(options) {
|
|
|
669
716
|
}
|
|
670
717
|
async function validateFile(filePath, index, signal) {
|
|
671
718
|
const validPath = await validateExistingPath(filePath, signal);
|
|
672
|
-
const stats = await withAbort(
|
|
719
|
+
const stats = await withAbort(statAsync(validPath), signal);
|
|
673
720
|
return { filePath, index, validPath, stats };
|
|
674
721
|
}
|
|
675
722
|
function markRemainingSkipped(startIndex, total, skippedBudget) {
|
|
@@ -804,7 +851,7 @@ function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
|
|
|
804
851
|
continue;
|
|
805
852
|
output[index] = {
|
|
806
853
|
path: filePath,
|
|
807
|
-
error: `Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)
|
|
854
|
+
error: new Error(`Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`),
|
|
808
855
|
};
|
|
809
856
|
}
|
|
810
857
|
}
|
|
@@ -823,7 +870,7 @@ export async function readMultipleFiles(filePaths, options = {}) {
|
|
|
823
870
|
resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
|
|
824
871
|
buildValue: (resolvedIndex, error) => ({
|
|
825
872
|
path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
826
|
-
error
|
|
873
|
+
error,
|
|
827
874
|
}),
|
|
828
875
|
});
|
|
829
876
|
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
@@ -12,7 +12,6 @@ declare const SearchOptionsSchema: z.ZodObject<{
|
|
|
12
12
|
contextLines: z.ZodInt;
|
|
13
13
|
wholeWord: z.ZodBoolean;
|
|
14
14
|
isLiteral: z.ZodBoolean;
|
|
15
|
-
multiline: z.ZodBoolean;
|
|
16
15
|
includeHidden: z.ZodBoolean;
|
|
17
16
|
baseNameMatch: z.ZodBoolean;
|
|
18
17
|
caseSensitiveFileMatch: z.ZodBoolean;
|