@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.
Files changed (66) hide show
  1. package/README.md +47 -47
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -13
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +59 -39
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +60 -19
  17. package/dist/lib/file-operations/search.js +83 -84
  18. package/dist/lib/file-operations/traversal.js +13 -15
  19. package/dist/lib/fs-helpers.d.ts +3 -10
  20. package/dist/lib/fs-helpers.js +20 -98
  21. package/dist/lib/globs.js +1 -1
  22. package/dist/lib/logger.d.ts +28 -0
  23. package/dist/lib/logger.js +91 -0
  24. package/dist/lib/observability.d.ts +7 -0
  25. package/dist/lib/observability.js +19 -9
  26. package/dist/lib/paths.js +55 -55
  27. package/dist/lib/resource-store.js +4 -4
  28. package/dist/lib/utils.d.ts +0 -12
  29. package/dist/lib/utils.js +0 -13
  30. package/dist/resources/generated-instructions.js +40 -31
  31. package/dist/resources/tool-catalog.js +32 -26
  32. package/dist/resources/tool-info.js +34 -29
  33. package/dist/resources/workflows.js +39 -18
  34. package/dist/resources.d.ts +1 -1
  35. package/dist/resources.js +4 -4
  36. package/dist/schemas.d.ts +66 -66
  37. package/dist/schemas.js +21 -44
  38. package/dist/server/bootstrap.d.ts +12 -11
  39. package/dist/server/bootstrap.js +95 -86
  40. package/dist/server/roots-manager.d.ts +5 -2
  41. package/dist/server/roots-manager.js +8 -6
  42. package/dist/server/task-store.d.ts +10 -0
  43. package/dist/server/task-store.js +73 -0
  44. package/dist/tools/apply-patch.js +26 -18
  45. package/dist/tools/calculate-hash.js +11 -22
  46. package/dist/tools/create-directory.js +10 -8
  47. package/dist/tools/delete-file.js +17 -15
  48. package/dist/tools/diff-files.js +15 -15
  49. package/dist/tools/edit-file.js +7 -4
  50. package/dist/tools/list-directory.js +6 -6
  51. package/dist/tools/move-file.js +98 -81
  52. package/dist/tools/read-multiple.js +4 -4
  53. package/dist/tools/read.js +5 -5
  54. package/dist/tools/replace-in-files.js +20 -23
  55. package/dist/tools/roots.js +1 -1
  56. package/dist/tools/search-content.js +32 -41
  57. package/dist/tools/search-files.js +54 -41
  58. package/dist/tools/shared.d.ts +3 -0
  59. package/dist/tools/shared.js +70 -28
  60. package/dist/tools/stat-many.js +11 -7
  61. package/dist/tools/stat.js +4 -4
  62. package/dist/tools/task-support.d.ts +10 -9
  63. package/dist/tools/task-support.js +94 -23
  64. package/dist/tools/tree.js +3 -3
  65. package/dist/tools/write-file.js +10 -7
  66. 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 { existsSync } from 'node:fs';
57
- import { fileURLToPath, pathToFileURL } from 'node:url';
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 { assertNotAborted, isProbablyBinary, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
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, reportPeriodicProgress, } from '../utils.js';
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 RegExp(final, 'gi');
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: z.string().min(1),
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.E_INVALID_INPUT, `Invalid search options:\n${z.prettifyError(result.error)}`, undefined, { errors: z.treeifyError(result.error) });
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
- const trimmedLine = hasContext || matchCount > 0 ? trimContent(rawLine) : '';
293
+ let trimmedLine = '';
278
294
  if (matchCount > 0) {
279
- if (ctx) {
280
- matches.push({
281
- file: requestedPath,
282
- line: lineNumber,
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(fsp.open(resolvedPath, 'r'), signal), true);
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 currentDir = path.dirname(fileURLToPath(import.meta.url));
413
- const isSourceContext = currentDir.endsWith('src\\lib\\file-operations') ||
414
- currentDir.endsWith('src/lib/file-operations');
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
- const workerIndex = this.workerRoundRobin % this.size;
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(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
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 (scanned >= opts.maxFilesScanned)
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
- scanned++;
793
- reportPeriodicProgress(onProgress, scanned, {
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(countingStream(), pattern, opts, signal, summary)
821
- : await executeSequential(countingStream(), pattern, opts, signal, summary);
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.E_INVALID_INPUT, 'basePath required');
831
+ throw new McpError(ErrorCode.INVALID_INPUT, 'basePath required');
832
832
  if (typeof pattern !== 'string')
833
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'pattern required');
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(fsp.stat(details.resolvedPath), signal);
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.E_INVALID_INPUT, 'Path must be file or directory', basePath);
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
- reportPeriodicProgress(onProgress, state.filesScanned, {
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
- reportPeriodicProgress(onProgress, state.filesScanned, {
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) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
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 * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
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 = path.resolve(options.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 path.isAbsolute(parentPath)
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 path.isAbsolute(match) ? match : path.resolve(cwd, match);
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 = path.resolve(base, match.name);
145
+ const absolutePath = resolve(base, match.name);
149
146
  if (maxDepth !== undefined) {
150
- const rel = path.relative(cwd, absolutePath);
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 fs.stat(absolutePath)
174
- : await fs.lstat(absolutePath);
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 (!path.isAbsolute(match)) {
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
- const buffer = [];
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.splice(0, buffer.length);
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)
@@ -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 declare function assertNotAborted(signal?: AbortSignal, message?: string): void;
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?: fsp.FileHandle, signal?: AbortSignal): Promise<boolean>;
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 {};
@@ -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
- function createAbortError(message = 'Operation aborted') {
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.E_INVALID_INPUT, message ?? `${name} must be a positive integer`);
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 createAbortError();
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 = path.extname(filePath).toLowerCase();
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(fsp.open(filePath, READ_ONLY_FILE_FLAG), signal);
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.E_INVALID_INPUT, 'head cannot be used together with startLine/endLine');
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.E_INVALID_INPUT, 'tail cannot be used together with head/startLine/endLine');
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.E_INVALID_INPUT, 'endLine must be greater than or equal to startLine (default: 1)');
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.E_TOO_LARGE, `File exceeds maximum size (${bytesRead} > ${maxSize}): ${requestedPath}`, requestedPath, { size: bytesRead, maxSize });
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.E_INVALID_INPUT, `Binary file detected: ${filePath}. Refusing to read as text.`, filePath);
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.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes). Use head parameter to preview the first N lines.`, filePath, { size, maxSize });
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.E_INVALID_INPUT, `Missing ${key} option`, filePath);
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.E_NOT_FILE, `Not a file: ${filePath}`, filePath);
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(fsp.stat(validPath), normalized.signal);
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 fsp.writeFile(tempPath, content, { encoding, signal });
656
- await withAbort(fsp.rename(tempPath, filePath), signal);
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 fsp.unlink(tempPath).catch(() => { });
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.E_INVALID_PATTERN, message);
17
+ throw new McpError(ErrorCode.INVALID_PATTERN, message);
18
18
  }
19
19
  }