@j0hanz/filesystem-mcp 1.14.0 → 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 +47 -47
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -13
- 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 +59 -39
- 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 +60 -19
- package/dist/lib/file-operations/search.js +83 -84
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +20 -98
- package/dist/lib/globs.js +1 -1
- 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/resources/generated-instructions.js +40 -31
- package/dist/resources/tool-catalog.js +32 -26
- package/dist/resources/tool-info.js +34 -29
- package/dist/resources/workflows.js +39 -18
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +66 -66
- package/dist/schemas.js +21 -44
- 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 +8 -6
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +26 -18
- package/dist/tools/calculate-hash.js +11 -22
- package/dist/tools/create-directory.js +10 -8
- package/dist/tools/delete-file.js +17 -15
- package/dist/tools/diff-files.js +15 -15
- package/dist/tools/edit-file.js +7 -4
- package/dist/tools/list-directory.js +6 -6
- package/dist/tools/move-file.js +98 -81
- package/dist/tools/read-multiple.js +4 -4
- package/dist/tools/read.js +5 -5
- package/dist/tools/replace-in-files.js +20 -23
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +32 -41
- package/dist/tools/search-files.js +54 -41
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +70 -28
- package/dist/tools/stat-many.js +11 -7
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +3 -3
- package/dist/tools/write-file.js +10 -7
- package/package.json +9 -9
|
@@ -50,22 +50,21 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import * as fsp from 'node:fs/promises';
|
|
54
|
-
import * as path from 'node:path';
|
|
55
53
|
import { AsyncResource } from 'node:async_hooks';
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
54
|
+
import { open, stat } from 'node:fs/promises';
|
|
55
|
+
import { basename, dirname } from 'node:path';
|
|
58
56
|
import { debuglog } from 'node:util';
|
|
59
57
|
import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
|
|
60
58
|
import RE2 from 're2';
|
|
61
|
-
import safeRegex from 'safe-regex2';
|
|
62
59
|
import { z } from 'zod';
|
|
60
|
+
import { assertNotAborted, withAbort, withTimedAbortSignal } from '../abort.js';
|
|
63
61
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
|
|
64
62
|
import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
|
|
65
|
-
import {
|
|
63
|
+
import { isProbablyBinary } from '../fs-helpers.js';
|
|
64
|
+
import { isSafeGlobPattern } from '../globs.js';
|
|
66
65
|
import { startPerfMeasure } from '../observability.js';
|
|
67
66
|
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
|
|
68
|
-
import { mergeOptions, omitOptionKeys
|
|
67
|
+
import { mergeOptions, omitOptionKeys } from '../utils.js';
|
|
69
68
|
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
|
|
70
69
|
import { buildGlobOptions, globEntries } from './traversal.js';
|
|
71
70
|
function countRegexLineMatches(regex, line) {
|
|
@@ -85,20 +84,10 @@ function buildRegexPattern(pattern, options) {
|
|
|
85
84
|
const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
|
|
86
85
|
return options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
87
86
|
}
|
|
88
|
-
function validatePattern(pattern, options) {
|
|
89
|
-
if (options.isLiteral && pattern.length === 0)
|
|
90
|
-
return;
|
|
91
|
-
if (options.isLiteral && !options.wholeWord)
|
|
92
|
-
return;
|
|
93
|
-
const final = buildRegexPattern(pattern, options);
|
|
94
|
-
if (!safeRegex(final)) {
|
|
95
|
-
throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${pattern}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
87
|
function buildLiteralMatcher(pattern, options) {
|
|
99
88
|
if (!options.caseSensitive) {
|
|
100
89
|
const final = escapeLiteral(pattern);
|
|
101
|
-
const regex = new
|
|
90
|
+
const regex = new RE2(final, 'gi');
|
|
102
91
|
return (line) => countRegexLineMatches(regex, line);
|
|
103
92
|
}
|
|
104
93
|
// Fast path for case-sensitive literal
|
|
@@ -130,13 +119,19 @@ function buildMatcher(pattern, options) {
|
|
|
130
119
|
return buildLiteralMatcher(pattern, options);
|
|
131
120
|
}
|
|
132
121
|
const final = buildRegexPattern(pattern, options);
|
|
133
|
-
validatePattern(pattern, options); // Re-validate to be safe
|
|
134
122
|
return buildRegexMatcher(final, options.caseSensitive);
|
|
135
123
|
}
|
|
136
124
|
// --- Configuration & Schemas ---
|
|
137
125
|
const SEARCH_CONTENT_MAX_RESULTS = 500;
|
|
126
|
+
const SafeFilePatternSchema = z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1, 'Pattern required')
|
|
129
|
+
.max(1000, 'Max 1000 chars')
|
|
130
|
+
.refine((value) => isSafeGlobPattern(value), {
|
|
131
|
+
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
132
|
+
});
|
|
138
133
|
const SearchOptionsSchema = z.strictObject({
|
|
139
|
-
filePattern:
|
|
134
|
+
filePattern: SafeFilePatternSchema,
|
|
140
135
|
excludePatterns: z.array(z.string()),
|
|
141
136
|
caseSensitive: z.boolean(),
|
|
142
137
|
maxResults: z.int().min(0),
|
|
@@ -169,7 +164,6 @@ const DEFAULTS = {
|
|
|
169
164
|
};
|
|
170
165
|
const ERROR_SCAN_CANCELLED = 'Scan cancelled';
|
|
171
166
|
const ERROR_WORKER_POOL_CLOSED = 'Worker pool closed';
|
|
172
|
-
const SEARCH_PROGRESS_THROTTLE_MODULO = 25;
|
|
173
167
|
const SEARCH_WORKER_NAME_PREFIX = 'filesystem-search';
|
|
174
168
|
const SEARCH_WORKER_RESOURCE_TYPE = 'SearchWorkerTask';
|
|
175
169
|
// --- Helpers ---
|
|
@@ -178,7 +172,7 @@ function resolveOptions(options) {
|
|
|
178
172
|
const merged = mergeOptions(DEFAULTS, normalizedOptions);
|
|
179
173
|
const result = SearchOptionsSchema.safeParse(merged);
|
|
180
174
|
if (!result.success) {
|
|
181
|
-
throw new McpError(ErrorCode.
|
|
175
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Invalid search options:\n${z.prettifyError(result.error)}`, undefined, { errors: z.treeifyError(result.error) });
|
|
182
176
|
}
|
|
183
177
|
return result.data;
|
|
184
178
|
}
|
|
@@ -254,9 +248,31 @@ class ContextBuffer {
|
|
|
254
248
|
}
|
|
255
249
|
function trimContent(line) {
|
|
256
250
|
return line.length > MAX_LINE_CONTENT_LENGTH
|
|
257
|
-
? line.slice(0, MAX_LINE_CONTENT_LENGTH)
|
|
251
|
+
? `${line.slice(0, MAX_LINE_CONTENT_LENGTH)}\u2026`
|
|
258
252
|
: line;
|
|
259
253
|
}
|
|
254
|
+
function processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx) {
|
|
255
|
+
const trimmedLine = trimContent(rawLine);
|
|
256
|
+
if (ctx) {
|
|
257
|
+
matches.push({
|
|
258
|
+
file: requestedPath,
|
|
259
|
+
line: lineNumber,
|
|
260
|
+
content: trimmedLine,
|
|
261
|
+
matchCount,
|
|
262
|
+
contextBefore: ctx.snapshotBefore(),
|
|
263
|
+
contextAfter: ctx.scheduleAfter(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
matches.push({
|
|
268
|
+
file: requestedPath,
|
|
269
|
+
line: lineNumber,
|
|
270
|
+
content: trimmedLine,
|
|
271
|
+
matchCount,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return trimmedLine;
|
|
275
|
+
}
|
|
260
276
|
async function readMatches(handle, requestedPath, matcher, options, maxMatches, isCancelled, signal) {
|
|
261
277
|
if (maxMatches <= 0) {
|
|
262
278
|
return [];
|
|
@@ -274,26 +290,12 @@ async function readMatches(handle, requestedPath, matcher, options, maxMatches,
|
|
|
274
290
|
if (isCancelled())
|
|
275
291
|
break;
|
|
276
292
|
const matchCount = matcher(rawLine);
|
|
277
|
-
|
|
293
|
+
let trimmedLine = '';
|
|
278
294
|
if (matchCount > 0) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
content: trimmedLine,
|
|
284
|
-
matchCount,
|
|
285
|
-
contextBefore: ctx.snapshotBefore(),
|
|
286
|
-
contextAfter: ctx.scheduleAfter(),
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
matches.push({
|
|
291
|
-
file: requestedPath,
|
|
292
|
-
line: lineNumber,
|
|
293
|
-
content: trimmedLine,
|
|
294
|
-
matchCount,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
295
|
+
trimmedLine = processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx);
|
|
296
|
+
}
|
|
297
|
+
else if (hasContext) {
|
|
298
|
+
trimmedLine = trimContent(rawLine);
|
|
297
299
|
}
|
|
298
300
|
if (ctx) {
|
|
299
301
|
ctx.add(trimmedLine);
|
|
@@ -315,7 +317,7 @@ async function scanFileResolved(resolvedPath, requestedPath, matcher, options, s
|
|
|
315
317
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
316
318
|
try {
|
|
317
319
|
assertNotAborted(signal);
|
|
318
|
-
const handle = __addDisposableResource(env_1, await withAbort(
|
|
320
|
+
const handle = __addDisposableResource(env_1, await withAbort(open(resolvedPath, 'r'), signal), true);
|
|
319
321
|
const stats = await withAbort(handle.stat(), signal);
|
|
320
322
|
if (stats.size > options.maxFileSize) {
|
|
321
323
|
return {
|
|
@@ -409,12 +411,9 @@ function buildSearchContentResult(root, pattern, filePattern, matches, summary)
|
|
|
409
411
|
summary: withOptionalStoppedReason(baseSummary, summary.stoppedReason),
|
|
410
412
|
};
|
|
411
413
|
}
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
const WORKER_SCRIPT_PATH = path.join(currentDir, isSourceContext ? 'search-worker.ts' : 'search-worker.js');
|
|
416
|
-
const WORKER_SCRIPT_URL = pathToFileURL(WORKER_SCRIPT_PATH);
|
|
417
|
-
const hasWorkerScript = existsSync(WORKER_SCRIPT_PATH);
|
|
414
|
+
const isSourceContext = import.meta.url.endsWith('.ts');
|
|
415
|
+
const WORKER_SCRIPT_URL = new URL(import.meta.url);
|
|
416
|
+
const hasWorkerScript = true;
|
|
418
417
|
class SearchWorkerTaskResource extends AsyncResource {
|
|
419
418
|
#settled = false;
|
|
420
419
|
constructor() {
|
|
@@ -518,9 +517,21 @@ class SearchWorkerPool {
|
|
|
518
517
|
if (this.closed)
|
|
519
518
|
throw new Error(ERROR_WORKER_POOL_CLOSED);
|
|
520
519
|
const id = this.nextRequestId++;
|
|
521
|
-
|
|
520
|
+
let workerIndex = 0;
|
|
521
|
+
const workerPendingCounts = new Array(this.size).fill(0);
|
|
522
|
+
for (const p of this.pending.values()) {
|
|
523
|
+
const idx = p.workerIndex;
|
|
524
|
+
workerPendingCounts[idx] = (workerPendingCounts[idx] ?? 0) + 1;
|
|
525
|
+
}
|
|
526
|
+
let minPending = workerPendingCounts[0] ?? 0;
|
|
527
|
+
for (let i = 1; i < this.size; i++) {
|
|
528
|
+
const pendingCount = workerPendingCounts[i] ?? 0;
|
|
529
|
+
if (pendingCount < minPending) {
|
|
530
|
+
minPending = pendingCount;
|
|
531
|
+
workerIndex = i;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
522
534
|
const worker = this.getWorker(workerIndex);
|
|
523
|
-
this.workerRoundRobin++;
|
|
524
535
|
const promise = new Promise((resolve, reject) => {
|
|
525
536
|
const resource = new SearchWorkerTaskResource();
|
|
526
537
|
const pendingRequest = {
|
|
@@ -758,7 +769,7 @@ async function searchSingleFile(details, opts, pattern, signal) {
|
|
|
758
769
|
}, signal, opts.maxResults);
|
|
759
770
|
if (result.matched)
|
|
760
771
|
summary.filesMatched = 1;
|
|
761
|
-
return buildSearchContentResult(
|
|
772
|
+
return buildSearchContentResult(dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
762
773
|
}
|
|
763
774
|
async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
764
775
|
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
@@ -775,12 +786,12 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
|
775
786
|
stats: false,
|
|
776
787
|
suppressErrors: true,
|
|
777
788
|
}));
|
|
789
|
+
const summary = createScanSummary();
|
|
778
790
|
async function* fileGenerator() {
|
|
779
|
-
let scanned = 0;
|
|
780
791
|
for await (const entry of stream) {
|
|
781
792
|
if (signal.aborted)
|
|
782
793
|
break;
|
|
783
|
-
if (
|
|
794
|
+
if (summary.filesScanned >= opts.maxFilesScanned)
|
|
784
795
|
break;
|
|
785
796
|
if (!entry.dirent.isFile())
|
|
786
797
|
continue;
|
|
@@ -789,36 +800,25 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
|
789
800
|
continue;
|
|
790
801
|
if (isSensitivePath(entry.path, normalized))
|
|
791
802
|
continue;
|
|
792
|
-
|
|
793
|
-
|
|
803
|
+
summary.filesScanned++;
|
|
804
|
+
onProgress?.({
|
|
805
|
+
current: summary.filesScanned,
|
|
794
806
|
total: opts.maxFilesScanned,
|
|
795
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
796
807
|
});
|
|
797
808
|
yield { resolvedPath: normalized, requestedPath: entry.path };
|
|
798
809
|
}
|
|
799
|
-
reportPeriodicProgress(onProgress, scanned, {
|
|
800
|
-
total: opts.maxFilesScanned,
|
|
801
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
802
|
-
force: true,
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
const summary = createScanSummary();
|
|
806
|
-
const resolvedStream = fileGenerator();
|
|
807
|
-
async function* countingStream() {
|
|
808
|
-
for await (const f of resolvedStream) {
|
|
809
|
-
summary.filesScanned++;
|
|
810
|
-
yield f;
|
|
811
|
-
}
|
|
812
810
|
if (summary.filesScanned >= opts.maxFilesScanned) {
|
|
813
811
|
summary.truncated = true;
|
|
814
812
|
summary.stoppedReason = 'maxFiles';
|
|
815
813
|
}
|
|
814
|
+
onProgress?.({
|
|
815
|
+
current: summary.filesScanned,
|
|
816
|
+
total: opts.maxFilesScanned,
|
|
817
|
+
});
|
|
816
818
|
}
|
|
817
|
-
const matcherOpts = buildMatcherOptions(opts);
|
|
818
|
-
validatePattern(pattern, matcherOpts);
|
|
819
819
|
const matches = shouldUseWorkers()
|
|
820
|
-
? await executeParallel(
|
|
821
|
-
: await executeSequential(
|
|
820
|
+
? await executeParallel(fileGenerator(), pattern, opts, signal, summary)
|
|
821
|
+
: await executeSequential(fileGenerator(), pattern, opts, signal, summary);
|
|
822
822
|
return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
|
|
823
823
|
}
|
|
824
824
|
function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
@@ -828,19 +828,19 @@ function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
|
828
828
|
}
|
|
829
829
|
export async function searchContent(basePath, pattern, options = {}) {
|
|
830
830
|
if (!basePath.trim())
|
|
831
|
-
throw new McpError(ErrorCode.
|
|
831
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'basePath required');
|
|
832
832
|
if (typeof pattern !== 'string')
|
|
833
|
-
throw new McpError(ErrorCode.
|
|
833
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'pattern required');
|
|
834
834
|
const opts = resolveOptions(options);
|
|
835
835
|
try {
|
|
836
836
|
return await withTimedAbortSignal(options.signal, opts.timeoutMs, async (signal) => {
|
|
837
837
|
const details = await validateExistingPathDetailed(basePath, signal);
|
|
838
|
-
const stats = await withAbort(
|
|
838
|
+
const stats = await withAbort(stat(details.resolvedPath), signal);
|
|
839
839
|
if (stats.isFile()) {
|
|
840
840
|
return searchSingleFile(details, opts, pattern, signal);
|
|
841
841
|
}
|
|
842
842
|
if (!stats.isDirectory()) {
|
|
843
|
-
throw new McpError(ErrorCode.
|
|
843
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path must be file or directory', basePath);
|
|
844
844
|
}
|
|
845
845
|
return searchDirectory(details, opts, pattern, signal, options.onProgress);
|
|
846
846
|
});
|
|
@@ -955,9 +955,9 @@ async function collectFromStream(stream, signal, context) {
|
|
|
955
955
|
if (shouldStopCollecting(state, normalized, signal))
|
|
956
956
|
break;
|
|
957
957
|
state.filesScanned++;
|
|
958
|
-
|
|
958
|
+
onProgress?.({
|
|
959
|
+
current: state.filesScanned,
|
|
959
960
|
total: normalized.maxFilesScanned,
|
|
960
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
961
961
|
});
|
|
962
962
|
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
|
|
963
963
|
continue;
|
|
@@ -975,10 +975,9 @@ async function collectFromStream(stream, signal, context) {
|
|
|
975
975
|
if (state.truncated)
|
|
976
976
|
break;
|
|
977
977
|
}
|
|
978
|
-
|
|
978
|
+
onProgress?.({
|
|
979
|
+
current: state.filesScanned,
|
|
979
980
|
total: normalized.maxFilesScanned,
|
|
980
|
-
throttleModulo: SEARCH_PROGRESS_THROTTLE_MODULO,
|
|
981
|
-
force: true,
|
|
982
981
|
});
|
|
983
982
|
}
|
|
984
983
|
function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
@@ -1041,7 +1040,7 @@ const SORT_COMPARATORS = {
|
|
|
1041
1040
|
};
|
|
1042
1041
|
function sortSearchResults(results, sortBy) {
|
|
1043
1042
|
if (sortBy === 'name') {
|
|
1044
|
-
stableSortByDerivedString(results, (item) =>
|
|
1043
|
+
stableSortByDerivedString(results, (item) => basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
1045
1044
|
return;
|
|
1046
1045
|
}
|
|
1047
1046
|
const comparator = SORT_COMPARATORS[sortBy];
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { glob as fsGlob } from 'node:fs/promises';
|
|
1
|
+
import { glob as fsGlob, lstat, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
3
|
import { getToolContextSnapshot, publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, startPerfMeasure, } from '../observability.js';
|
|
5
4
|
import { toPosixPath } from '../paths.js';
|
|
6
5
|
import { isRecord } from '../utils.js';
|
|
@@ -96,7 +95,7 @@ function assertOptionsShape(options) {
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
function normalizeOptions(options) {
|
|
99
|
-
const cwd =
|
|
98
|
+
const cwd = resolve(options.cwd);
|
|
100
99
|
const normalizedPattern = normalizePattern(options.pattern, options.baseNameMatch);
|
|
101
100
|
const patterns = options.includeHidden
|
|
102
101
|
? buildHiddenPatterns(normalizedPattern, options.maxDepth ?? DEFAULT_MAX_HIDDEN_DEPTH)
|
|
@@ -136,18 +135,16 @@ function isGlobDirentLike(value) {
|
|
|
136
135
|
function resolveDirentBase(cwd, parentPath) {
|
|
137
136
|
if (!parentPath)
|
|
138
137
|
return cwd;
|
|
139
|
-
return
|
|
140
|
-
? parentPath
|
|
141
|
-
: path.resolve(cwd, parentPath);
|
|
138
|
+
return isAbsolute(parentPath) ? parentPath : resolve(cwd, parentPath);
|
|
142
139
|
}
|
|
143
140
|
function resolveStringMatchPath(cwd, match) {
|
|
144
|
-
return
|
|
141
|
+
return isAbsolute(match) ? match : resolve(cwd, match);
|
|
145
142
|
}
|
|
146
143
|
function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
|
|
147
144
|
const base = resolveDirentBase(cwd, match.parentPath);
|
|
148
|
-
const absolutePath =
|
|
145
|
+
const absolutePath = resolve(base, match.name);
|
|
149
146
|
if (maxDepth !== undefined) {
|
|
150
|
-
const rel =
|
|
147
|
+
const rel = relative(cwd, absolutePath);
|
|
151
148
|
if (getRelativeDepth(rel) > maxDepth)
|
|
152
149
|
return;
|
|
153
150
|
}
|
|
@@ -170,12 +167,12 @@ async function resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followS
|
|
|
170
167
|
seen.add(absolutePath);
|
|
171
168
|
try {
|
|
172
169
|
const stats = followSymlinks
|
|
173
|
-
? await
|
|
174
|
-
: await
|
|
170
|
+
? await stat(absolutePath)
|
|
171
|
+
: await lstat(absolutePath);
|
|
175
172
|
if (onlyFiles && !stats.isFile())
|
|
176
173
|
return null;
|
|
177
174
|
const entry = { path: absolutePath, dirent: stats };
|
|
178
|
-
if (!
|
|
175
|
+
if (!isAbsolute(match)) {
|
|
179
176
|
entry.relativePath = match;
|
|
180
177
|
}
|
|
181
178
|
if (returnStats)
|
|
@@ -190,12 +187,13 @@ async function resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followS
|
|
|
190
187
|
}
|
|
191
188
|
async function* processIterable(iterable, context) {
|
|
192
189
|
const { cwd, maxDepth, seen, onlyFiles, followSymlinks, returnStats, suppressErrors, } = context;
|
|
193
|
-
|
|
190
|
+
let buffer = [];
|
|
194
191
|
const flush = async function* () {
|
|
195
192
|
if (buffer.length === 0)
|
|
196
193
|
return;
|
|
197
194
|
// Process buffer concurrently
|
|
198
|
-
const currentBuffer = buffer
|
|
195
|
+
const currentBuffer = buffer;
|
|
196
|
+
buffer = [];
|
|
199
197
|
const results = await Promise.all(currentBuffer.map((match) => resolveStringMatch(match, cwd, maxDepth, seen, onlyFiles, followSymlinks, returnStats, suppressErrors)));
|
|
200
198
|
for (const entry of results) {
|
|
201
199
|
if (entry !== null)
|
package/dist/lib/fs-helpers.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises';
|
|
2
1
|
import type { Stats } from 'node:fs';
|
|
2
|
+
import { type FileHandle } from 'node:fs/promises';
|
|
3
3
|
import type { FileType } from '../config.js';
|
|
4
|
-
export
|
|
5
|
-
export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
6
|
-
export declare function createTimedAbortSignal(baseSignal: AbortSignal | undefined, timeoutMs?: number): {
|
|
7
|
-
signal: AbortSignal;
|
|
8
|
-
cleanup: () => void;
|
|
9
|
-
};
|
|
10
|
-
export declare function withTimedAbortSignal<T>(baseSignal: AbortSignal | undefined, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
4
|
+
export { assertNotAborted, createTimedAbortSignal, withAbort, withTimedAbortSignal, } from './abort.js';
|
|
11
5
|
interface ParallelResult<R> {
|
|
12
6
|
results: R[];
|
|
13
7
|
errors: {
|
|
@@ -18,7 +12,7 @@ interface ParallelResult<R> {
|
|
|
18
12
|
export declare function processInParallel<T, R>(items: T[], processor: (item: T) => Promise<R>, concurrency?: number, signal?: AbortSignal): Promise<ParallelResult<R>>;
|
|
19
13
|
export declare function getFileType(stats: Stats): FileType;
|
|
20
14
|
export declare function isHidden(name: string): boolean;
|
|
21
|
-
export declare function isProbablyBinary(filePath: string, existingHandle?:
|
|
15
|
+
export declare function isProbablyBinary(filePath: string, existingHandle?: FileHandle, signal?: AbortSignal): Promise<boolean>;
|
|
22
16
|
type ReadMode = 'head' | 'full' | 'range' | 'tail';
|
|
23
17
|
interface ReadFileOptions {
|
|
24
18
|
encoding?: BufferEncoding;
|
|
@@ -49,4 +43,3 @@ export declare function atomicWriteFile(filePath: string, content: string, optio
|
|
|
49
43
|
encoding?: BufferEncoding;
|
|
50
44
|
signal?: AbortSignal | undefined;
|
|
51
45
|
}): Promise<void>;
|
|
52
|
-
export {};
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -50,36 +50,16 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import * as fsp from 'node:fs/promises';
|
|
54
|
-
import * as path from 'node:path';
|
|
55
53
|
import { isUtf8 } from 'node:buffer';
|
|
56
54
|
import { randomUUID } from 'node:crypto';
|
|
55
|
+
import { open, rename, stat, unlink, writeFile, } from 'node:fs/promises';
|
|
56
|
+
import { extname } from 'node:path';
|
|
57
|
+
import { assertNotAborted, withAbort } from './abort.js';
|
|
57
58
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
58
59
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
59
60
|
import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
|
|
60
|
-
|
|
61
|
-
return new DOMException(message, 'AbortError');
|
|
62
|
-
}
|
|
61
|
+
export { assertNotAborted, createTimedAbortSignal, withAbort, withTimedAbortSignal, } from './abort.js';
|
|
63
62
|
const READ_ONLY_FILE_FLAG = 'r';
|
|
64
|
-
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
65
|
-
function normalizeAbortReason(reason, message) {
|
|
66
|
-
if (reason instanceof Error)
|
|
67
|
-
return reason;
|
|
68
|
-
return createAbortError(message);
|
|
69
|
-
}
|
|
70
|
-
function isFiniteNumber(value) {
|
|
71
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
72
|
-
}
|
|
73
|
-
export function assertNotAborted(signal, message) {
|
|
74
|
-
if (!signal)
|
|
75
|
-
return;
|
|
76
|
-
try {
|
|
77
|
-
signal.throwIfAborted();
|
|
78
|
-
}
|
|
79
|
-
catch (reason) {
|
|
80
|
-
throw normalizeAbortReason(reason, message);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
63
|
function assertPositiveSafeIntegerOption(name, value, message) {
|
|
84
64
|
if (value === undefined)
|
|
85
65
|
return;
|
|
@@ -87,73 +67,15 @@ function assertPositiveSafeIntegerOption(name, value, message) {
|
|
|
87
67
|
!Number.isFinite(value) ||
|
|
88
68
|
!Number.isSafeInteger(value) ||
|
|
89
69
|
value < 1) {
|
|
90
|
-
throw new McpError(ErrorCode.
|
|
70
|
+
throw new McpError(ErrorCode.INVALID_INPUT, message ?? `${name} must be a positive integer`);
|
|
91
71
|
}
|
|
92
72
|
}
|
|
93
73
|
function normalizeConcurrency(concurrency) {
|
|
94
74
|
assertPositiveSafeIntegerOption('concurrency', concurrency);
|
|
95
75
|
return concurrency;
|
|
96
76
|
}
|
|
97
|
-
function getAbortError(signal, message) {
|
|
98
|
-
try {
|
|
99
|
-
signal.throwIfAborted();
|
|
100
|
-
}
|
|
101
|
-
catch (reason) {
|
|
102
|
-
return normalizeAbortReason(reason, message);
|
|
103
|
-
}
|
|
104
|
-
return createAbortError(message);
|
|
105
|
-
}
|
|
106
|
-
export function withAbort(promise, signal) {
|
|
107
|
-
if (!signal)
|
|
108
|
-
return promise;
|
|
109
|
-
signal.throwIfAborted();
|
|
110
|
-
return new Promise((resolve, reject) => {
|
|
111
|
-
const onAbort = () => {
|
|
112
|
-
reject(getAbortError(signal));
|
|
113
|
-
};
|
|
114
|
-
if (signal.aborted) {
|
|
115
|
-
onAbort();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
-
promise.then((value) => {
|
|
120
|
-
signal.removeEventListener('abort', onAbort);
|
|
121
|
-
resolve(value);
|
|
122
|
-
}, (error) => {
|
|
123
|
-
signal.removeEventListener('abort', onAbort);
|
|
124
|
-
reject(normalizeUnknownError(error));
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
129
|
-
const timeoutSignal = isFiniteNumber(timeoutMs)
|
|
130
|
-
? AbortSignal.timeout(timeoutMs)
|
|
131
|
-
: undefined;
|
|
132
|
-
if (baseSignal && timeoutSignal) {
|
|
133
|
-
return {
|
|
134
|
-
signal: AbortSignal.any([baseSignal, timeoutSignal]),
|
|
135
|
-
cleanup: () => { },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
if (baseSignal) {
|
|
139
|
-
return { signal: baseSignal, cleanup: () => { } };
|
|
140
|
-
}
|
|
141
|
-
if (timeoutSignal) {
|
|
142
|
-
return { signal: timeoutSignal, cleanup: () => { } };
|
|
143
|
-
}
|
|
144
|
-
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
145
|
-
}
|
|
146
|
-
export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
147
|
-
const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
|
|
148
|
-
try {
|
|
149
|
-
return await run(signal);
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
cleanup();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
77
|
function createParallelAbortError() {
|
|
156
|
-
return
|
|
78
|
+
return new DOMException('Operation aborted', 'AbortError');
|
|
157
79
|
}
|
|
158
80
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
159
81
|
const itemCount = items.length;
|
|
@@ -218,11 +140,11 @@ export function isHidden(name) {
|
|
|
218
140
|
return name.startsWith('.');
|
|
219
141
|
}
|
|
220
142
|
function hasKnownBinaryExtension(filePath) {
|
|
221
|
-
const ext =
|
|
143
|
+
const ext = extname(filePath).toLowerCase();
|
|
222
144
|
return KNOWN_BINARY_EXTENSIONS.has(ext);
|
|
223
145
|
}
|
|
224
146
|
async function openReadableFileHandle(filePath, signal) {
|
|
225
|
-
return withAbort(
|
|
147
|
+
return withAbort(open(filePath, READ_ONLY_FILE_FLAG), signal);
|
|
226
148
|
}
|
|
227
149
|
async function readProbe(handle, signal) {
|
|
228
150
|
const buffer = Buffer.alloc(BINARY_CHECK_BUFFER_SIZE);
|
|
@@ -282,15 +204,15 @@ function validateReadOptions(options) {
|
|
|
282
204
|
assertPositiveSafeIntegerOption('startLine', options.startLine, 'startLine must be at least 1');
|
|
283
205
|
assertPositiveSafeIntegerOption('endLine', options.endLine, 'endLine must be at least 1');
|
|
284
206
|
if (hasHead && (hasStart || hasEnd)) {
|
|
285
|
-
throw new McpError(ErrorCode.
|
|
207
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'head cannot be used together with startLine/endLine');
|
|
286
208
|
}
|
|
287
209
|
if (hasTail && (hasHead || hasStart || hasEnd)) {
|
|
288
|
-
throw new McpError(ErrorCode.
|
|
210
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'tail cannot be used together with head/startLine/endLine');
|
|
289
211
|
}
|
|
290
212
|
{
|
|
291
213
|
const effectiveStart = options.startLine ?? 1;
|
|
292
214
|
if (options.endLine !== undefined && options.endLine < effectiveStart) {
|
|
293
|
-
throw new McpError(ErrorCode.
|
|
215
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'endLine must be greater than or equal to startLine (default: 1)');
|
|
294
216
|
}
|
|
295
217
|
}
|
|
296
218
|
}
|
|
@@ -345,7 +267,7 @@ function resolveReadMode(options) {
|
|
|
345
267
|
}
|
|
346
268
|
const STREAM_CHUNK_SIZE = 64 * 1024;
|
|
347
269
|
function createTooLargeError(bytesRead, maxSize, requestedPath) {
|
|
348
|
-
return new McpError(ErrorCode.
|
|
270
|
+
return new McpError(ErrorCode.TOO_LARGE, `File exceeds size limit (${bytesRead} > ${maxSize} bytes)`, requestedPath, { size: bytesRead, maxSize });
|
|
349
271
|
}
|
|
350
272
|
async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
|
|
351
273
|
const stream = handle.createReadStream({
|
|
@@ -521,19 +443,19 @@ async function assertNotBinary(validPath, filePath, normalized) {
|
|
|
521
443
|
const isBinary = await isProbablyBinary(validPath, undefined, normalized.signal);
|
|
522
444
|
if (!isBinary)
|
|
523
445
|
return;
|
|
524
|
-
throw new McpError(ErrorCode.
|
|
446
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Binary file detected.', filePath);
|
|
525
447
|
}
|
|
526
448
|
function assertSizeWithinLimit(size, maxSize, filePath) {
|
|
527
449
|
if (size <= maxSize)
|
|
528
450
|
return;
|
|
529
|
-
throw new McpError(ErrorCode.
|
|
451
|
+
throw new McpError(ErrorCode.TOO_LARGE, `File too large (${size} > ${maxSize} bytes). Use head to preview.`, filePath, { size, maxSize });
|
|
530
452
|
}
|
|
531
453
|
function requireReadOption(normalized, key, filePath) {
|
|
532
454
|
const value = normalized[key];
|
|
533
455
|
if (value !== undefined) {
|
|
534
456
|
return value;
|
|
535
457
|
}
|
|
536
|
-
throw new McpError(ErrorCode.
|
|
458
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Missing ${key} option`, filePath);
|
|
537
459
|
}
|
|
538
460
|
async function executeHeadRead(context) {
|
|
539
461
|
const head = requireReadOption(context.normalized, 'head', context.filePath);
|
|
@@ -604,7 +526,7 @@ async function readByMode(context) {
|
|
|
604
526
|
}
|
|
605
527
|
function assertFileStats(filePath, stats) {
|
|
606
528
|
if (!stats.isFile()) {
|
|
607
|
-
throw new McpError(ErrorCode.
|
|
529
|
+
throw new McpError(ErrorCode.NOT_FILE, 'Not a regular file', filePath);
|
|
608
530
|
}
|
|
609
531
|
}
|
|
610
532
|
async function readFileWithStatsInternal(filePath, validPath, stats, normalized) {
|
|
@@ -644,7 +566,7 @@ export async function readFile(filePath, options = {}) {
|
|
|
644
566
|
const normalized = prepareReadOptions(options);
|
|
645
567
|
const validPath = await validateExistingPath(filePath, normalized.signal);
|
|
646
568
|
assertNotAborted(normalized.signal);
|
|
647
|
-
const stats = await withAbort(
|
|
569
|
+
const stats = await withAbort(stat(validPath), normalized.signal);
|
|
648
570
|
return readFileWithStatsInternal(filePath, validPath, stats, normalized);
|
|
649
571
|
}
|
|
650
572
|
export async function atomicWriteFile(filePath, content, options = {}) {
|
|
@@ -652,13 +574,13 @@ export async function atomicWriteFile(filePath, content, options = {}) {
|
|
|
652
574
|
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
653
575
|
try {
|
|
654
576
|
assertNotAborted(signal);
|
|
655
|
-
await
|
|
656
|
-
await withAbort(
|
|
577
|
+
await writeFile(tempPath, content, { encoding, signal });
|
|
578
|
+
await withAbort(rename(tempPath, filePath), signal);
|
|
657
579
|
}
|
|
658
580
|
catch (error) {
|
|
659
581
|
// Attempt cleanup on error, but don't overwrite the original error
|
|
660
582
|
try {
|
|
661
|
-
await
|
|
583
|
+
await unlink(tempPath).catch(() => { });
|
|
662
584
|
}
|
|
663
585
|
catch {
|
|
664
586
|
// Ignore cleanup errors
|
package/dist/lib/globs.js
CHANGED
|
@@ -14,6 +14,6 @@ export function isSafeGlobPattern(value) {
|
|
|
14
14
|
}
|
|
15
15
|
export function assertSafeGlobPattern(value, message = 'Invalid glob or unsafe path (absolute/.. forbidden)') {
|
|
16
16
|
if (!isSafeGlobPattern(value)) {
|
|
17
|
-
throw new McpError(ErrorCode.
|
|
17
|
+
throw new McpError(ErrorCode.INVALID_PATTERN, message);
|
|
18
18
|
}
|
|
19
19
|
}
|