@j0hanz/filesystem-mcp 1.11.1 → 1.13.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.
@@ -5,8 +5,11 @@ import { readMultipleFiles } from '../lib/file-operations/metadata.js';
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- export const READ_MULTIPLE_FILES_TOOL = {
9
- name: 'read_many',
8
+ const READ_MANY_TOOL_NAME = 'read_many';
9
+ const READ_MANY_TOOL_LABEL = '🕮 read_many';
10
+ const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
11
+ export const READ_MANY_TOOL = {
12
+ name: READ_MANY_TOOL_NAME,
10
13
  title: 'Read Multiple Files',
11
14
  description: 'Read multiple text files in one request with contents and metadata. ' +
12
15
  'For a single file, use `read`.',
@@ -19,26 +22,42 @@ export const READ_MULTIPLE_FILES_TOOL = {
19
22
  'Per-file `truncationReason` can be `head`, `tail`, `range`, or `externalized`.',
20
23
  ],
21
24
  };
25
+ function buildReadManyResourceName(filePath) {
26
+ return `read:${path.basename(filePath)}`;
27
+ }
22
28
  function toStructuredReadManyResult(result) {
23
- return {
29
+ const structuredResult = {
24
30
  path: result.path,
25
- ...(result.content !== undefined ? { content: result.content } : {}),
26
- ...(result.truncated ? { truncated: result.truncated } : {}),
27
- ...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
28
- ...(result.head !== undefined ? { head: result.head } : {}),
29
- ...(result.tail !== undefined ? { tail: result.tail } : {}),
30
- ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
31
- ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
32
- ...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
33
- ...(result.totalLines !== undefined
34
- ? { totalLines: result.totalLines }
35
- : {}),
36
- ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
37
- ...(result.truncationReason
38
- ? { truncationReason: result.truncationReason }
39
- : {}),
40
- ...(result.error ? { error: result.error } : {}),
41
31
  };
32
+ if (result.content !== undefined)
33
+ structuredResult.content = result.content;
34
+ if (result.truncated)
35
+ structuredResult.truncated = true;
36
+ if (result.resourceUri)
37
+ structuredResult.resourceUri = result.resourceUri;
38
+ if (result.head !== undefined)
39
+ structuredResult.head = result.head;
40
+ if (result.tail !== undefined)
41
+ structuredResult.tail = result.tail;
42
+ if (result.startLine !== undefined) {
43
+ structuredResult.startLine = result.startLine;
44
+ }
45
+ if (result.endLine !== undefined)
46
+ structuredResult.endLine = result.endLine;
47
+ if (result.hasMoreLines)
48
+ structuredResult.hasMoreLines = true;
49
+ if (result.totalLines !== undefined) {
50
+ structuredResult.totalLines = result.totalLines;
51
+ }
52
+ if (result.linesRead !== undefined) {
53
+ structuredResult.linesRead = result.linesRead;
54
+ }
55
+ if (result.truncationReason) {
56
+ structuredResult.truncationReason = result.truncationReason;
57
+ }
58
+ if (result.error)
59
+ structuredResult.error = result.error;
60
+ return structuredResult;
42
61
  }
43
62
  function resolveReadManyTruncationReason(result) {
44
63
  if (!result.truncated)
@@ -60,7 +79,7 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
60
79
  if (!result.content) {
61
80
  return baseResult;
62
81
  }
63
- const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(result.path)}`, mimeType: 'text/plain' });
82
+ const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: buildReadManyResourceName(result.path), mimeType: 'text/plain' });
64
83
  if (!externalized) {
65
84
  return baseResult;
66
85
  }
@@ -72,66 +91,83 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
72
91
  truncationReason: 'externalized',
73
92
  };
74
93
  }
75
- function buildReadManyResourceLinks(results) {
76
- return results.flatMap((result) => {
77
- if (!result.resourceUri)
78
- return [];
79
- return [
80
- buildResourceLink({
81
- uri: result.resourceUri,
82
- name: `read:${path.basename(result.path)}`,
83
- description: 'Full file contents',
84
- }),
85
- ];
86
- });
94
+ function buildReadManyTextSection(result) {
95
+ const header = `=== ${result.path} ===`;
96
+ if (result.error) {
97
+ return `${header}\nError: ${result.error}`;
98
+ }
99
+ return `${header}\n${result.content ?? ''}`;
87
100
  }
88
- function buildReadManyTextResult(results) {
89
- return results
90
- .map((result) => {
91
- const header = `=== ${result.path} ===`;
92
- if (result.error) {
93
- return `${header}\nError: ${result.error}`;
101
+ function buildReadMultipleOptions(args, signal, onReadComplete) {
102
+ const options = {};
103
+ if (signal)
104
+ options.signal = signal;
105
+ if (args.head !== undefined)
106
+ options.head = args.head;
107
+ if (args.tail !== undefined)
108
+ options.tail = args.tail;
109
+ if (args.startLine !== undefined)
110
+ options.startLine = args.startLine;
111
+ if (args.endLine !== undefined)
112
+ options.endLine = args.endLine;
113
+ if (onReadComplete)
114
+ options.onReadComplete = onReadComplete;
115
+ return options;
116
+ }
117
+ function buildReadManyResponsePayload(results, resourceStore) {
118
+ const resourceLinks = [];
119
+ const structuredResults = [];
120
+ const textSections = [];
121
+ let succeeded = 0;
122
+ for (const result of results) {
123
+ const mappedResult = maybeExternalizeReadManyResult(result, resourceStore);
124
+ structuredResults.push(toStructuredReadManyResult(mappedResult));
125
+ textSections.push(buildReadManyTextSection(mappedResult));
126
+ if (mappedResult.resourceUri) {
127
+ resourceLinks.push(buildResourceLink({
128
+ uri: mappedResult.resourceUri,
129
+ name: buildReadManyResourceName(mappedResult.path),
130
+ description: FULL_FILE_CONTENTS_DESCRIPTION,
131
+ }));
94
132
  }
95
- return `${header}\n${result.content ?? ''}`;
96
- })
97
- .join('\n\n');
133
+ if (mappedResult.error === undefined)
134
+ succeeded += 1;
135
+ }
136
+ const total = structuredResults.length;
137
+ return {
138
+ resourceLinks,
139
+ structuredResults,
140
+ summary: {
141
+ total,
142
+ succeeded,
143
+ failed: total - succeeded,
144
+ },
145
+ text: textSections.join('\n\n'),
146
+ };
98
147
  }
99
148
  async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
100
- const options = {
101
- ...(signal ? { signal } : {}),
102
- ...(args.head !== undefined ? { head: args.head } : {}),
103
- ...(args.tail !== undefined ? { tail: args.tail } : {}),
104
- ...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
105
- ...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
106
- ...(onReadComplete ? { onReadComplete } : {}),
107
- };
149
+ const options = buildReadMultipleOptions(args, signal, onReadComplete);
108
150
  const results = await readMultipleFiles(args.paths, options);
109
- const mappedResults = results.map((result) => maybeExternalizeReadManyResult(result, resourceStore));
110
- const succeeded = mappedResults.filter((r) => r.error === undefined).length;
111
- const failed = mappedResults.length - succeeded;
151
+ const payload = buildReadManyResponsePayload(results, resourceStore);
112
152
  const structured = {
113
153
  ok: true,
114
- results: mappedResults.map((result) => toStructuredReadManyResult(result)),
115
- summary: {
116
- total: mappedResults.length,
117
- succeeded,
118
- failed,
119
- },
154
+ results: payload.structuredResults,
155
+ summary: payload.summary,
120
156
  };
121
- return buildToolResponse(buildReadManyTextResult(mappedResults), structured, buildReadManyResourceLinks(mappedResults));
157
+ return buildToolResponse(payload.text, structured, payload.resourceLinks);
122
158
  }
123
159
  export function registerReadMultipleFilesTool(server, options = {}) {
124
160
  const handler = (args, extra) => {
125
161
  const primaryPath = args.paths[0] ?? '';
126
162
  return executeToolWithDiagnostics({
127
- toolName: 'read_many',
163
+ toolName: READ_MANY_TOOL_NAME,
128
164
  extra,
129
165
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
130
166
  context: { path: primaryPath },
131
167
  run: async (signal) => {
132
168
  const context = buildBatchPathContext(args.paths, 'files');
133
169
  const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
134
- toolLabel: '🕮 read_many',
170
+ toolLabel: READ_MANY_TOOL_LABEL,
135
171
  context,
136
172
  totalItems: args.paths.length,
137
173
  itemVerb: 'read',
@@ -142,11 +178,11 @@ export function registerReadMultipleFilesTool(server, options = {}) {
142
178
  const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
143
179
  const total = sc.summary?.total ?? 0;
144
180
  const finalCurrent = resolveFinalProgressCurrent(progress, total);
145
- progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
181
+ progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
146
182
  return result;
147
183
  }
148
184
  catch (error) {
149
- progress.fail(`🕮 read_many: ${context} • failed`);
185
+ progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • failed`);
150
186
  throw error;
151
187
  }
152
188
  },
@@ -157,7 +193,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
157
193
  guard: options.isInitialized,
158
194
  });
159
195
  const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
160
- if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
196
+ if (registerToolTaskIfAvailable(server, READ_MANY_TOOL_NAME, READ_MANY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
161
197
  return;
162
- server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), validatedHandler);
198
+ server.registerTool(READ_MANY_TOOL_NAME, withDefaultIcons({ ...READ_MANY_TOOL }, options.iconInfo), validatedHandler);
163
199
  }
@@ -20,51 +20,61 @@ export const READ_FILE_TOOL = {
20
20
  ],
21
21
  taskSupport: 'forbidden',
22
22
  };
23
- async function handleReadFile(args, signal, resourceStore) {
23
+ const READ_TOOL_NAME = 'read';
24
+ const READ_TOOL_LABEL = '🕮 read';
25
+ const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
26
+ function buildReadResourceName(filePath) {
27
+ return `read:${path.basename(filePath)}`;
28
+ }
29
+ function buildReadOptions(args, signal) {
24
30
  const options = {
25
31
  encoding: 'utf-8',
26
32
  maxSize: MAX_TEXT_FILE_SIZE,
27
33
  skipBinary: true,
28
34
  };
29
- if (args.head !== undefined) {
35
+ if (signal)
36
+ options.signal = signal;
37
+ if (args.head !== undefined)
30
38
  options.head = args.head;
31
- }
32
- if (args.tail !== undefined) {
39
+ if (args.tail !== undefined)
33
40
  options.tail = args.tail;
34
- }
35
- if (args.startLine !== undefined) {
41
+ if (args.startLine !== undefined)
36
42
  options.startLine = args.startLine;
37
- }
38
- if (args.endLine !== undefined) {
43
+ if (args.endLine !== undefined)
39
44
  options.endLine = args.endLine;
40
- }
41
- if (signal) {
42
- options.signal = signal;
43
- }
44
- const result = await readFile(args.path, options);
45
+ return options;
46
+ }
47
+ function toStructuredReadFileResult(args, result) {
45
48
  const structured = {
46
49
  ok: true,
47
50
  path: args.path,
48
51
  content: result.content,
49
- ...(result.truncated ? { truncated: result.truncated } : {}),
50
- ...(result.totalLines !== undefined
51
- ? { totalLines: result.totalLines }
52
- : {}),
53
- ...(result.head !== undefined ? { head: result.head } : {}),
54
- ...(result.tail !== undefined ? { tail: result.tail } : {}),
55
- ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
56
- ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
57
- ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
58
- ...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
59
52
  };
60
- const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(args.path)}`, mimeType: 'text/plain' });
61
- if (args.includeHash) {
62
- structured.contentHash = createHash('sha256')
63
- .update(result.content, 'utf-8')
64
- .digest('hex');
65
- }
53
+ if (result.truncated)
54
+ structured.truncated = true;
55
+ if (result.totalLines !== undefined)
56
+ structured.totalLines = result.totalLines;
57
+ if (result.head !== undefined)
58
+ structured.head = result.head;
59
+ if (result.tail !== undefined)
60
+ structured.tail = result.tail;
61
+ if (result.startLine !== undefined)
62
+ structured.startLine = result.startLine;
63
+ if (result.endLine !== undefined)
64
+ structured.endLine = result.endLine;
65
+ if (result.linesRead !== undefined)
66
+ structured.linesRead = result.linesRead;
67
+ if (result.hasMoreLines)
68
+ structured.hasMoreLines = true;
69
+ return structured;
70
+ }
71
+ function maybeBuildExternalizedReadResponse(filePath, content, structured, resourceStore) {
72
+ const externalized = maybeExternalizeTextContent(resourceStore, content, {
73
+ name: buildReadResourceName(filePath),
74
+ mimeType: 'text/plain',
75
+ });
66
76
  if (!externalized) {
67
- return buildToolResponse(result.content, structured);
77
+ return undefined;
68
78
  }
69
79
  const { entry, preview } = externalized;
70
80
  const structuredWithResource = {
@@ -74,7 +84,7 @@ async function handleReadFile(args, signal, resourceStore) {
74
84
  resourceUri: entry.uri,
75
85
  };
76
86
  const text = [
77
- `Output too large to inline (${result.content.length} chars).`,
87
+ `Output too large to inline (${content.length} chars).`,
78
88
  'Preview:',
79
89
  preview,
80
90
  ].join('\n');
@@ -83,14 +93,70 @@ async function handleReadFile(args, signal, resourceStore) {
83
93
  uri: entry.uri,
84
94
  name: entry.name,
85
95
  mimeType: entry.mimeType,
86
- description: 'Full file contents',
96
+ description: FULL_FILE_CONTENTS_DESCRIPTION,
87
97
  expiresAt: entry.expiresAt,
88
98
  }),
89
99
  ]);
90
100
  }
101
+ function buildReadProgressMessage(args) {
102
+ const name = path.basename(args.path);
103
+ if (args.startLine !== undefined) {
104
+ const end = args.endLine ?? '…';
105
+ return `${READ_TOOL_LABEL}: ${name} [lines ${args.startLine}–${end}]`;
106
+ }
107
+ if (args.head !== undefined)
108
+ return `${READ_TOOL_LABEL}: ${name} [head ${args.head}]`;
109
+ if (args.tail !== undefined)
110
+ return `${READ_TOOL_LABEL}: ${name} [tail ${args.tail}]`;
111
+ return `${READ_TOOL_LABEL}: ${name}`;
112
+ }
113
+ function buildReadCompletionMessage(args, result) {
114
+ const name = path.basename(args.path);
115
+ if (result.isError)
116
+ return `${READ_TOOL_LABEL}: ${name} • failed`;
117
+ const structured = result.structuredContent;
118
+ if (!structured.ok)
119
+ return `${READ_TOOL_LABEL}: ${name} • failed`;
120
+ const lines = structured.linesRead ?? structured.totalLines;
121
+ if (structured.startLine !== undefined) {
122
+ const end = structured.linesRead
123
+ ? structured.startLine + structured.linesRead - 1
124
+ : (structured.endLine ?? '…');
125
+ return `${READ_TOOL_LABEL}: ${name} • lines ${structured.startLine}–${end}`;
126
+ }
127
+ if (structured.head !== undefined) {
128
+ return structured.hasMoreLines
129
+ ? `${READ_TOOL_LABEL}: ${name} • first ${String(lines ?? structured.head)} lines`
130
+ : `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.head)} lines`;
131
+ }
132
+ if (structured.tail !== undefined) {
133
+ return structured.hasMoreLines
134
+ ? `${READ_TOOL_LABEL}: ${name} • last ${String(lines ?? structured.tail)} lines`
135
+ : `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.tail)} lines`;
136
+ }
137
+ if (structured.truncated) {
138
+ return `${READ_TOOL_LABEL}: ${name} • truncated [${String(lines)} lines]`;
139
+ }
140
+ return `${READ_TOOL_LABEL}: ${name} • ${String(lines)} lines`;
141
+ }
142
+ async function handleReadFile(args, signal, resourceStore) {
143
+ const options = buildReadOptions(args, signal);
144
+ const result = await readFile(args.path, options);
145
+ const structured = toStructuredReadFileResult(args, result);
146
+ if (args.includeHash) {
147
+ structured.contentHash = createHash('sha256')
148
+ .update(result.content, 'utf-8')
149
+ .digest('hex');
150
+ }
151
+ const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
152
+ if (externalizedResponse) {
153
+ return externalizedResponse;
154
+ }
155
+ return buildToolResponse(result.content, structured);
156
+ }
91
157
  export function registerReadFileTool(server, options = {}) {
92
158
  const handler = (args, extra) => executeToolWithDiagnostics({
93
- toolName: 'read',
159
+ toolName: READ_TOOL_NAME,
94
160
  extra,
95
161
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
96
162
  context: { path: args.path },
@@ -99,49 +165,11 @@ export function registerReadFileTool(server, options = {}) {
99
165
  });
100
166
  const wrappedHandler = wrapToolHandler(handler, {
101
167
  guard: options.isInitialized,
102
- progressMessage: (args) => {
103
- const name = path.basename(args.path);
104
- if (args.startLine !== undefined) {
105
- const end = args.endLine ?? '…';
106
- return `🕮 read: ${name} [lines ${args.startLine}–${end}]`;
107
- }
108
- if (args.head !== undefined)
109
- return `🕮 read: ${name} [head ${args.head}]`;
110
- if (args.tail !== undefined)
111
- return `🕮 read: ${name} [tail ${args.tail}]`;
112
- return `🕮 read: ${name}`;
113
- },
114
- completionMessage: (args, result) => {
115
- const name = path.basename(args.path);
116
- if (result.isError)
117
- return `🕮 read: ${name} • failed`;
118
- const sc = result.structuredContent;
119
- if (!sc.ok)
120
- return `🕮 read: ${name} • failed`;
121
- const lines = sc.linesRead ?? sc.totalLines;
122
- if (sc.startLine !== undefined) {
123
- const end = sc.linesRead
124
- ? sc.startLine + sc.linesRead - 1
125
- : (sc.endLine ?? '…');
126
- return `🕮 read: ${name} • lines ${sc.startLine}–${end}`;
127
- }
128
- if (sc.head !== undefined) {
129
- return sc.hasMoreLines
130
- ? `🕮 read: ${name} • first ${lines ?? sc.head} lines`
131
- : `🕮 read: ${name} • ${lines ?? sc.head} lines`;
132
- }
133
- if (sc.tail !== undefined) {
134
- return sc.hasMoreLines
135
- ? `🕮 read: ${name} • last ${lines ?? sc.tail} lines`
136
- : `🕮 read: ${name} • ${lines ?? sc.tail} lines`;
137
- }
138
- if (sc.truncated)
139
- return `🕮 read: ${name} • truncated [${String(lines)} lines]`;
140
- return `🕮 read: ${name} • ${String(lines)} lines`;
141
- },
168
+ progressMessage: buildReadProgressMessage,
169
+ completionMessage: buildReadCompletionMessage,
142
170
  });
143
171
  const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
144
- if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
172
+ if (registerToolTaskIfAvailable(server, READ_TOOL_NAME, READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
145
173
  return;
146
- server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
174
+ server.registerTool(READ_TOOL_NAME, withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
147
175
  }
@@ -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;