@j0hanz/filesystem-mcp 1.14.0 → 1.15.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/LICENSE +21 -0
- package/README.md +143 -71
- 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
|
@@ -1,12 +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';
|
|
6
7
|
import { assertSafeGlobPattern } from '../globs.js';
|
|
7
8
|
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, toPosixPath, validateExistingDirectory, validateExistingPath, validateExistingPathDetailed, } from '../paths.js';
|
|
8
9
|
import { applyIndexedErrors, applyIndexedValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './core.js';
|
|
9
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
|
+
}
|
|
10
51
|
const ACCESS_DEPS = {
|
|
11
52
|
normalizePath,
|
|
12
53
|
isPathWithinDirectories,
|
|
@@ -49,7 +90,7 @@ function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, sy
|
|
|
49
90
|
async function getSymlinkTarget(pathToRead, signal) {
|
|
50
91
|
assertNotAborted(signal);
|
|
51
92
|
try {
|
|
52
|
-
return await withAbort(
|
|
93
|
+
return await withAbort(readlinkAsync(pathToRead), signal);
|
|
53
94
|
}
|
|
54
95
|
catch (error) {
|
|
55
96
|
if (isAbortError(error))
|
|
@@ -62,14 +103,14 @@ export async function getFileInfo(filePath, options = {}) {
|
|
|
62
103
|
assertNotAborted(signal);
|
|
63
104
|
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
|
|
64
105
|
assertAllowedFileAccess(requestedPath, resolvedPath);
|
|
65
|
-
const { base: name, ext: rawExt } =
|
|
106
|
+
const { base: name, ext: rawExt } = parse(requestedPath);
|
|
66
107
|
const ext = rawExt.toLowerCase();
|
|
67
108
|
const includeMimeType = options.includeMimeType !== false;
|
|
68
109
|
const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
|
|
69
110
|
const symlinkTarget = isSymlink
|
|
70
111
|
? await getSymlinkTarget(requestedPath, signal)
|
|
71
112
|
: undefined;
|
|
72
|
-
const stats = await withAbort(
|
|
113
|
+
const stats = await withAbort(statAsync(resolvedPath), signal);
|
|
73
114
|
return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
|
|
74
115
|
}
|
|
75
116
|
function buildEmptyResult() {
|
|
@@ -131,7 +172,7 @@ export async function getMultipleFileInfo(paths, options = {}) {
|
|
|
131
172
|
: undefined,
|
|
132
173
|
buildValue: (resolvedIndex, error) => ({
|
|
133
174
|
path: paths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
134
|
-
error
|
|
175
|
+
error,
|
|
135
176
|
}),
|
|
136
177
|
});
|
|
137
178
|
return {
|
|
@@ -168,12 +209,12 @@ function resolveMaxDepth(normalized) {
|
|
|
168
209
|
return normalized.pattern ? normalized.maxDepth : 1;
|
|
169
210
|
}
|
|
170
211
|
async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
171
|
-
const dirents = await withAbort(
|
|
212
|
+
const dirents = await withAbort(readdirAsync(basePath, { withFileTypes: true }), signal);
|
|
172
213
|
if (!needsStats) {
|
|
173
214
|
for (const dirent of dirents) {
|
|
174
215
|
if (!normalized.includeHidden && isHidden(dirent.name))
|
|
175
216
|
continue;
|
|
176
|
-
yield { path:
|
|
217
|
+
yield { path: join(basePath, dirent.name), dirent };
|
|
177
218
|
}
|
|
178
219
|
return;
|
|
179
220
|
}
|
|
@@ -181,12 +222,12 @@ async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
|
181
222
|
for (const dirent of dirents) {
|
|
182
223
|
if (!normalized.includeHidden && isHidden(dirent.name))
|
|
183
224
|
continue;
|
|
184
|
-
filtered.push({ dirent, entryPath:
|
|
225
|
+
filtered.push({ dirent, entryPath: join(basePath, dirent.name) });
|
|
185
226
|
}
|
|
186
227
|
const { results, errors } = await processInParallel(filtered, async ({ entryPath, dirent }) => ({
|
|
187
228
|
path: entryPath,
|
|
188
229
|
dirent,
|
|
189
|
-
stats: await withAbort(
|
|
230
|
+
stats: await withAbort(lstatAsync(entryPath), signal),
|
|
190
231
|
}), PARALLEL_CONCURRENCY, signal);
|
|
191
232
|
if (errors.length > 0) {
|
|
192
233
|
throw errors[0]?.error ?? new Error('Failed to read entry stats');
|
|
@@ -216,13 +257,13 @@ function shouldUseFastPath(normalized, maxDepth) {
|
|
|
216
257
|
maxDepth === 1);
|
|
217
258
|
}
|
|
218
259
|
function resolveRelativePath(basePath, entryPath) {
|
|
219
|
-
return
|
|
260
|
+
return relative(basePath, entryPath) || basename(entryPath);
|
|
220
261
|
}
|
|
221
262
|
async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath) {
|
|
222
263
|
if (entryType !== 'symlink' || !includeSymlinkTargets) {
|
|
223
264
|
return undefined;
|
|
224
265
|
}
|
|
225
|
-
return
|
|
266
|
+
return readlinkAsync(entryPath).catch(() => undefined);
|
|
226
267
|
}
|
|
227
268
|
function updateTotals(entryType, totals) {
|
|
228
269
|
if (entryType === 'file')
|
|
@@ -234,7 +275,7 @@ function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarg
|
|
|
234
275
|
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
235
276
|
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
236
277
|
return {
|
|
237
|
-
name:
|
|
278
|
+
name: basename(entry.path),
|
|
238
279
|
path: entry.path,
|
|
239
280
|
relativePath: resolveRelativePath(basePath, entry.path),
|
|
240
281
|
type: entryType,
|
|
@@ -423,7 +464,7 @@ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher,
|
|
|
423
464
|
return null;
|
|
424
465
|
}
|
|
425
466
|
const relativePosix = toPosixPath(resolveRelativePath(root, entry.path));
|
|
426
|
-
const name =
|
|
467
|
+
const name = basename(entry.path);
|
|
427
468
|
return { type, relativePosix, name };
|
|
428
469
|
}
|
|
429
470
|
function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
|
|
@@ -523,7 +564,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
523
564
|
? null
|
|
524
565
|
: await loadRootGitignore(root, signal);
|
|
525
566
|
const rootNode = {
|
|
526
|
-
name:
|
|
567
|
+
name: basename(root) || root,
|
|
527
568
|
type: 'directory',
|
|
528
569
|
relativePath: '.',
|
|
529
570
|
children: [],
|
|
@@ -675,7 +716,7 @@ function resolveNormalizedReadOptions(options) {
|
|
|
675
716
|
}
|
|
676
717
|
async function validateFile(filePath, index, signal) {
|
|
677
718
|
const validPath = await validateExistingPath(filePath, signal);
|
|
678
|
-
const stats = await withAbort(
|
|
719
|
+
const stats = await withAbort(statAsync(validPath), signal);
|
|
679
720
|
return { filePath, index, validPath, stats };
|
|
680
721
|
}
|
|
681
722
|
function markRemainingSkipped(startIndex, total, skippedBudget) {
|
|
@@ -810,7 +851,7 @@ function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
|
|
|
810
851
|
continue;
|
|
811
852
|
output[index] = {
|
|
812
853
|
path: filePath,
|
|
813
|
-
error: `Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)
|
|
854
|
+
error: new Error(`Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`),
|
|
814
855
|
};
|
|
815
856
|
}
|
|
816
857
|
}
|
|
@@ -829,7 +870,7 @@ export async function readMultipleFiles(filePaths, options = {}) {
|
|
|
829
870
|
resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
|
|
830
871
|
buildValue: (resolvedIndex, error) => ({
|
|
831
872
|
path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
832
|
-
error
|
|
873
|
+
error,
|
|
833
874
|
}),
|
|
834
875
|
});
|
|
835
876
|
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
@@ -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 {};
|