@j0hanz/filesystem-mcp 1.11.1 → 1.12.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.
@@ -1,12 +1,13 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import { Buffer } from 'node:buffer';
3
4
  import { createTwoFilesPatch } from 'diff';
4
5
  import RE2 from 're2';
5
6
  import safeRegex from 'safe-regex2';
6
7
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
7
8
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
8
9
  import { globEntries } from '../lib/file-operations/traversal.js';
9
- import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
10
+ import { atomicWriteFile } from '../lib/fs-helpers.js';
10
11
  import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
11
12
  import { reportPeriodicProgress } from '../lib/utils.js';
12
13
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
@@ -57,42 +58,57 @@ function createRegexMatcher(pattern, caseSensitive) {
57
58
  throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
58
59
  }
59
60
  }
60
- function createRegexReplacementMatcher(regex) {
61
- const count = (content) => {
62
- regex.lastIndex = 0;
61
+ class RegexReplacementMatcher {
62
+ regex;
63
+ constructor(regex) {
64
+ this.regex = regex;
65
+ }
66
+ count(content) {
67
+ this.regex.lastIndex = 0;
63
68
  let matchCount = 0;
64
- while (regex.exec(content) !== null) {
69
+ while (this.regex.exec(content) !== null) {
65
70
  matchCount++;
66
- if (regex.lastIndex === 0) {
67
- regex.lastIndex++;
71
+ if (this.regex.lastIndex === 0) {
72
+ this.regex.lastIndex++;
68
73
  }
69
74
  }
70
75
  return matchCount;
71
- };
72
- const replace = (content, replacement) => {
73
- regex.lastIndex = 0;
74
- return content.replace(regex, replacement);
75
- };
76
- return { count, replace };
76
+ }
77
+ replace(content, replacement) {
78
+ this.regex.lastIndex = 0;
79
+ return content.replace(this.regex, replacement);
80
+ }
77
81
  }
78
- function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
79
- if (!caseSensitive) {
80
- const escaped = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
81
- const regex = new RE2(escaped, 'gi');
82
- return createRegexReplacementMatcher(regex);
82
+ class LiteralReplacementMatcher {
83
+ searchPattern;
84
+ caseSensitive;
85
+ patternLength;
86
+ searchBuffer;
87
+ constructor(searchPattern, caseSensitive) {
88
+ this.searchPattern = searchPattern;
89
+ this.caseSensitive = caseSensitive;
90
+ this.patternLength = searchPattern.length;
91
+ this.searchBuffer = caseSensitive
92
+ ? Buffer.from(searchPattern, 'utf8')
93
+ : null;
94
+ }
95
+ testBuffer(buffer) {
96
+ if (!this.searchBuffer)
97
+ return true;
98
+ return buffer.indexOf(this.searchBuffer) !== -1;
83
99
  }
84
- const count = (content) => {
100
+ count(content) {
85
101
  let matchCount = 0;
86
- let pos = content.indexOf(searchPattern);
87
- const patternLength = searchPattern.length;
102
+ let pos = content.indexOf(this.searchPattern);
88
103
  while (pos !== -1) {
89
104
  matchCount++;
90
- pos = content.indexOf(searchPattern, pos + patternLength);
105
+ pos = content.indexOf(this.searchPattern, pos + this.patternLength);
91
106
  }
92
107
  return matchCount;
93
- };
94
- const replace = (content, replacement) => content.replaceAll(searchPattern, () => replacement);
95
- return { count, replace };
108
+ }
109
+ replace(content, replacement) {
110
+ return content.replaceAll(this.searchPattern, () => replacement);
111
+ }
96
112
  }
97
113
  function buildReplacementPlan(content, replacement, matcher) {
98
114
  const matchCount = matcher.count(content);
@@ -135,7 +151,7 @@ async function processEntry(entryPath, context) {
135
151
  summary.totalMatches += plan.matchCount;
136
152
  summary.filesChanged++;
137
153
  recordChangedFile(summary, validPath, plan.matchCount);
138
- maybeAppendPatchDiff(summary, {
154
+ await maybeAppendPatchDiff(summary, {
139
155
  filePath: validPath,
140
156
  originalContent: plan.originalContent,
141
157
  updatedContent: plan.updatedContent,
@@ -157,24 +173,50 @@ async function processEntry(entryPath, context) {
157
173
  }
158
174
  }
159
175
  async function readReplacementPlan(validPath, context) {
160
- const stats = await withAbort(fs.stat(validPath), context.signal);
161
- if (stats.size > context.maxFileSize) {
162
- throw new Error(formatFileTooLargeError(validPath, stats.size, context.maxFileSize));
176
+ let fileHandle;
177
+ try {
178
+ const fd = await fs.open(validPath, 'r');
179
+ fileHandle = fd;
180
+ const stats = await fileHandle.stat();
181
+ if (stats.size > context.maxFileSize) {
182
+ throw new Error(formatFileTooLargeError(validPath, stats.size, context.maxFileSize));
183
+ }
184
+ let content;
185
+ if (context.matcher.testBuffer) {
186
+ const buffer = await fileHandle.readFile({ signal: context.signal });
187
+ if (!context.matcher.testBuffer(buffer)) {
188
+ return undefined;
189
+ }
190
+ content = buffer.toString('utf-8');
191
+ }
192
+ else {
193
+ content = await fileHandle.readFile({
194
+ encoding: 'utf-8',
195
+ signal: context.signal,
196
+ });
197
+ }
198
+ return buildReplacementPlan(content, context.replacement, context.matcher);
199
+ }
200
+ finally {
201
+ if (fileHandle) {
202
+ await fileHandle.close();
203
+ }
163
204
  }
164
- const content = await fs.readFile(validPath, {
165
- encoding: 'utf-8',
166
- signal: context.signal,
167
- });
168
- return buildReplacementPlan(content, context.replacement, context.matcher);
169
205
  }
170
- function maybeAppendPatchDiff(summary, params) {
206
+ async function maybeAppendPatchDiff(summary, params) {
171
207
  if (!params.includeDiff)
172
208
  return;
173
209
  if (summary.diff.length >= MAX_DIFF_SIZE) {
174
210
  summary.diffTruncated = true;
175
211
  return;
176
212
  }
177
- const patch = createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified');
213
+ const patch = await new Promise((resolve) => {
214
+ createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
215
+ callback: (res) => {
216
+ resolve(res ?? '');
217
+ },
218
+ });
219
+ });
178
220
  if (summary.diff.length + patch.length <=
179
221
  MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
180
222
  summary.diff += patch;
@@ -247,9 +289,14 @@ function createReplacementRegex(args) {
247
289
  function createReplacementMatcher(args) {
248
290
  const regex = createReplacementRegex(args);
249
291
  if (regex) {
250
- return createRegexReplacementMatcher(regex);
292
+ return new RegexReplacementMatcher(regex);
293
+ }
294
+ if (!args.caseSensitive) {
295
+ const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
+ const caseInsensitiveRegex = new RE2(escaped, 'gi');
297
+ return new RegexReplacementMatcher(caseInsensitiveRegex);
251
298
  }
252
- return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
299
+ return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
253
300
  }
254
301
  export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
255
302
  const maxFileSize = MAX_TEXT_FILE_SIZE;