@j0hanz/filesystem-mcp 1.19.0 → 2.0.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.
Files changed (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +19 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +11 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +38 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +109 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +345 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +26 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +66 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +12 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +71 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +142 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +110 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +394 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +330 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +442 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +291 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +398 -152
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +435 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +29 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +202 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -563
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -472
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -1,31 +1,146 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import { Buffer } from 'node:buffer';
2
- import { open } from 'node:fs/promises';
3
- import { basename, relative } from 'node:path';
54
+ import { dirname, join } from 'node:path';
55
+ import * as z from 'zod/v4';
4
56
  import { createTwoFilesPatch } from 'diff';
5
- import RE2 from 're2';
6
- import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
7
- import { classifyError, ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
8
- import { globEntries } from '../lib/file-operations/traversal.js';
9
- import { atomicWriteFile } from '../lib/fs-helpers.js';
10
- import { Logger } from '../lib/logger.js';
11
- import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
12
- import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
13
- import { FILE_EDIT_ICONS } from './icons.js';
14
- import { buildStructuredError, buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, } from './shared.js';
15
- import { registerStandardTool } from './task-support.js';
16
- export const SEARCH_AND_REPLACE_TOOL = {
17
- name: 'search_and_replace',
18
- title: 'Search and Replace',
19
- description: 'Bulk search-and-replace across files matching a glob. ' +
20
- 'Replaces ALL occurrences per file (unlike `edit`: first only). ' +
21
- 'Always `dryRun:true` first \u2014 returns a unified diff. ' +
22
- 'Literal matching by default; `isRegex:true` enables RE2 with capture groups ($1, $2).',
23
- inputSchema: SearchAndReplaceInputSchema,
24
- outputSchema: SearchAndReplaceOutputSchema,
25
- annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
26
- icons: FILE_EDIT_ICONS,
27
- taskSupport: 'optional',
28
- };
57
+ import { processEntriesConcurrently, StoppedReasonSchema } from '../core/concurrency.js';
58
+ import { ErrorCode, formatUnknownErrorMessage, FsError, Problem, rethrowIfAborted, } from '../core/errors.js';
59
+ import { buildFileResourceLink } from '../core/file-uri.js';
60
+ import { truncateProgressPattern } from '../core/fmt.js';
61
+ import { DEFAULT_EXCLUDE_PATTERNS, globEntries } from '../core/glob.js';
62
+ import { detectMimeFromContent } from '../core/mime.js';
63
+ import { toPosixRelative } from '../core/path.js';
64
+ import { escapeRegexLiteral } from '../core/primitives.js';
65
+ import { readFileBufferWithLimit } from '../core/read.js';
66
+ import { defaultFalseBoolean, includeHiddenField, includeIgnoredField, isBlank, maxDepthField, NonNegInt, OperationSummarySchema, OptionalPath, PerFileErrorSchema, SafeGlobPattern, } from '../core/schema.js';
67
+ import { compileRegex, freeRegex } from '../core/search.js';
68
+ import { DEFAULT_SEARCH_RESULTS, DEFAULT_SEARCH_TIMEOUT_MS, getMaxTextFileSize, MAX_SEARCH_RESULTS, PARALLEL_CONCURRENCY, } from '../core/util.js';
69
+ import { defineTool } from './define.js';
70
+ const SearchAndReplaceInputSchema = z.strictObject({
71
+ path: OptionalPath.describe('File to rewrite, or directory to rewrite under. Omitting it targets the ENTIRE first allowed root — ' +
72
+ 'scope it deliberately, and pair a wide scope with dryRun=true first'),
73
+ pattern: SafeGlobPattern.optional().describe('Glob to restrict replacements to specific file types (e.g. **/*.ts); default: all text files'),
74
+ searchPattern: z
75
+ .string()
76
+ .min(1)
77
+ .max(10000)
78
+ .refine((val) => !isBlank(val), {
79
+ message: 'searchPattern cannot be empty or whitespace-only',
80
+ })
81
+ .describe('Exact literal text or RE2 regex pattern to search for. When isRegex=true, uses RE2 syntax (no lookahead, lookbehind, or backreferences are supported). Cannot be empty or whitespace-only.')
82
+ .meta({ examples: ['TODO', 'function\\s+(\\w+)', 'import.*from'] }),
83
+ replacement: z
84
+ .string()
85
+ .max(10000)
86
+ .describe('Replacement text. Use capture group references ($1, $2, etc.) when isRegex=true. Use an empty string to delete all matches.')
87
+ .meta({ examples: ['$1_renamed', '', 'TODO: fix'] }),
88
+ isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regex (default: literal text match)'),
89
+ includeHidden: includeHiddenField(),
90
+ includeIgnored: includeIgnoredField(),
91
+ caseSensitive: defaultFalseBoolean('Enable case-sensitive matching (default: case-insensitive)'),
92
+ wholeWord: defaultFalseBoolean('Match whole words only (word boundary anchoring)'),
93
+ dryRun: defaultFalseBoolean('Preview replacements without writing to disk (default: false = apply changes)'),
94
+ returnDiff: defaultFalseBoolean('Include a unified diff of all changes in the response'),
95
+ maxResults: z
96
+ .uint32()
97
+ .min(1)
98
+ .max(MAX_SEARCH_RESULTS)
99
+ .optional()
100
+ .default(DEFAULT_SEARCH_RESULTS)
101
+ .describe('Maximum total match count across all files before stopping'),
102
+ maxFiles: z
103
+ .uint32()
104
+ .min(1)
105
+ .max(MAX_SEARCH_RESULTS)
106
+ .optional()
107
+ .describe('Maximum number of files to process'),
108
+ maxDepth: maxDepthField(),
109
+ });
110
+ const ReplacePerPathSchema = z.strictObject({
111
+ path: z.string().describe('File path relative to the search root'),
112
+ value: z
113
+ .strictObject({ matches: NonNegInt.describe('Replacements applied in this file') })
114
+ .optional()
115
+ .describe('Replacement outcome; present on success'),
116
+ error: PerFileErrorSchema.optional().describe('Error details; present on failure'),
117
+ });
118
+ // The same `{ results, summary }` envelope `read`, `stat`, and `delete` use, so
119
+ // a caller learns one shape for every tool that spans many paths. The former
120
+ // `filesModified` / `failedFiles` counters are `summary.succeeded` / `.failed`,
121
+ // and the old `primaryFile` block is gone — the resource_link content block
122
+ // already points at the first modified file.
123
+ const SearchAndReplaceOutputSchema = z.strictObject({
124
+ results: z
125
+ .array(ReplacePerPathSchema)
126
+ .describe('Per-file results: modified files, then any that could not be processed'),
127
+ summary: OperationSummarySchema,
128
+ totalMatches: NonNegInt.describe('Total number of replacements made across all files'),
129
+ filesScanned: NonNegInt.describe('Total number of files examined'),
130
+ resultsTruncated: z
131
+ .boolean()
132
+ .optional()
133
+ .describe('True when the results list holds fewer entries than summary.total: the changed-file or failed-file cap was hit. Trust summary over results.length.'),
134
+ diff: z
135
+ .string()
136
+ .optional()
137
+ .describe('Unified diff of all changes (present when returnDiff=true or dryRun=true)'),
138
+ diffTruncated: z
139
+ .boolean()
140
+ .optional()
141
+ .describe('True when the diff was cut due to the size limit'),
142
+ stoppedReason: StoppedReasonSchema.describe('Why enumeration stopped early: maxResults = match cap reached, maxFiles = file cap reached, timeout = time limit hit or cancelled. Absent when every matching file was enumerated. Marks the sweep incomplete, not the writes; files already dispatched still complete.'),
143
+ });
29
144
  const MAX_FAILURES = 20;
30
145
  const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
31
146
  const MAX_CHANGED_FILES = 100;
@@ -37,53 +152,99 @@ function recordFailure(failures, failure) {
37
152
  failures.push(failure);
38
153
  }
39
154
  function recordChangedFile(summary, filePath, matchCount) {
40
- const relativePath = relative(summary.root, filePath);
155
+ const relativePath = toPosixRelative(summary.root, filePath);
41
156
  if (summary.changedFiles.length < MAX_CHANGED_FILES) {
42
157
  summary.changedFiles.push({ path: relativePath, matches: matchCount });
43
158
  return;
44
159
  }
45
160
  summary.changedFilesTruncated = true;
46
161
  }
47
- function createRegexMatcher(pattern, caseSensitive) {
48
- const flags = caseSensitive ? 'g' : 'gi';
49
- try {
50
- return new RE2(pattern, flags);
51
- }
52
- catch (error) {
53
- throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
54
- }
162
+ const DOLLAR_TOKEN = /\$(\$|&|`|'|<([^>]*)>|\d{1,2})/g;
163
+ /**
164
+ * Expand `$`-substitutions in a replacement template the way `RegExp` does.
165
+ *
166
+ * We cannot hand the template to RE2's own string replacer: it throws
167
+ * `Invalid replacement string` on any `$` not followed by a substitution it
168
+ * recognises (so a replacement of `$100` or a trailing `$` fails outright,
169
+ * where `RegExp` inserts the `$` literally), and it renders an out-of-range
170
+ * `$5` as `$4`. Passing a function to `String.replace` disables RE2's handling
171
+ * entirely, which leaves this the single owner of the syntax.
172
+ */
173
+ function expandDollarTokens(template, match, groups, offset, input, named) {
174
+ return template.replace(DOLLAR_TOKEN, (token, kind, name) => {
175
+ if (kind === '$')
176
+ return '$';
177
+ if (kind === '&')
178
+ return match;
179
+ if (kind === '`')
180
+ return input.slice(0, offset);
181
+ if (kind === "'")
182
+ return input.slice(offset + match.length);
183
+ if (name !== undefined) {
184
+ if (named && Object.hasOwn(named, name))
185
+ return named[name] ?? '';
186
+ return token;
187
+ }
188
+ // `$12` prefers group 12, then falls back to group 1 followed by a literal
189
+ // `2`, and stays literal when neither exists — RegExp's own precedence.
190
+ const two = Number.parseInt(kind, 10);
191
+ if (kind.length === 2 && two >= 1 && two <= groups.length)
192
+ return groups[two - 1] ?? '';
193
+ const one = Number.parseInt(kind.slice(0, 1), 10);
194
+ if (one >= 1 && one <= groups.length) {
195
+ return (groups[one - 1] ?? '') + (kind.length === 2 ? kind.slice(1) : '');
196
+ }
197
+ return token;
198
+ });
55
199
  }
56
- function createRegexReplacementMatcher(regex) {
200
+ function createRegexReplacementMatcher(regex, expandReplacement) {
57
201
  return {
58
202
  testBuffer(buffer) {
59
- return regex.test(buffer);
203
+ // The regex is global and shared across every file in the batch, so a
204
+ // previous file's match would otherwise start this scan mid-string.
205
+ regex.lastIndex = 0;
206
+ return regex.test(buffer.toString('utf-8'));
60
207
  },
61
208
  count(content) {
62
209
  regex.lastIndex = 0;
63
210
  let matchCount = 0;
64
- while (regex.exec(content) !== null) {
211
+ let m;
212
+ while ((m = regex.exec(content)) !== null) {
65
213
  matchCount++;
66
- if (regex.lastIndex === 0) {
214
+ // Treat an absent group 0 as zero-length: not bumping lastIndex here
215
+ // would spin forever.
216
+ if ((m[0]?.length ?? 0) === 0)
67
217
  regex.lastIndex++;
68
- }
69
218
  }
70
219
  return matchCount;
71
220
  },
72
221
  replace(content, replacement) {
73
222
  regex.lastIndex = 0;
74
- return content.replace(regex, replacement);
223
+ // Only isRegex=true opts into $1/$& substitution. A literal search reaches
224
+ // this matcher too (case-insensitive and wholeWord both need a regex), and
225
+ // there the replacement must be inserted verbatim.
226
+ if (!expandReplacement)
227
+ return content.replace(regex, () => replacement);
228
+ return content.replace(regex, (match, ...rest) => {
229
+ // In ECMAScript/RE2, trailing arguments are [offset, input] or [offset, input, namedGroups].
230
+ const named = rest.length >= 3 && typeof rest[rest.length - 3] === 'number'
231
+ ? rest.pop()
232
+ : undefined;
233
+ const input = rest.pop();
234
+ const offset = rest.pop();
235
+ return expandDollarTokens(replacement, match, rest, offset, input, named);
236
+ });
237
+ },
238
+ dispose() {
239
+ freeRegex(regex);
75
240
  },
76
241
  };
77
242
  }
78
- function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
243
+ function createCaseSensitiveLiteralMatcher(searchPattern) {
79
244
  const patternLength = searchPattern.length;
80
- const searchBuffer = caseSensitive
81
- ? Buffer.from(searchPattern, 'utf8')
82
- : null;
245
+ const searchBuffer = Buffer.from(searchPattern, 'utf8');
83
246
  return {
84
247
  testBuffer(buffer) {
85
- if (!searchBuffer)
86
- return true;
87
248
  return buffer.indexOf(searchBuffer) !== -1;
88
249
  },
89
250
  count(content) {
@@ -98,6 +259,9 @@ function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
98
259
  replace(content, replacement) {
99
260
  return content.replaceAll(searchPattern, () => replacement);
100
261
  },
262
+ dispose() {
263
+ // no compiled pattern to release
264
+ },
101
265
  };
102
266
  }
103
267
  function buildReplacementPlan(content, replacement, matcher) {
@@ -111,28 +275,22 @@ function buildReplacementPlan(content, replacement, matcher) {
111
275
  updatedContent: matcher.replace(content, replacement),
112
276
  };
113
277
  }
114
- function formatFileTooLargeError(filePath, size, maxFileSize) {
115
- return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
116
- }
117
278
  async function processEntry(entryPath, ctx) {
118
279
  const { options, signal, summary } = ctx;
119
- let validPath;
120
- try {
121
- validPath = await validatePathForWrite(entryPath, signal);
122
- }
123
- catch (error) {
124
- summary.failedFiles++;
125
- recordFailure(summary.failures, {
126
- path: entryPath,
127
- error: buildStructuredError(error, ErrorCode.UNKNOWN, entryPath),
128
- });
129
- return;
130
- }
280
+ const validPath = entryPath;
131
281
  try {
132
282
  const plan = await readReplacementPlan(validPath, ctx);
133
283
  if (!plan) {
134
284
  return;
135
285
  }
286
+ if (!options.dryRun) {
287
+ await ctx.fs.writeFile(entryPath, plan.updatedContent, {
288
+ encoding: 'utf-8',
289
+ signal,
290
+ });
291
+ }
292
+ // Bookkeep only after the write succeeds (or in dryRun, where there is no
293
+ // write): a failed write must not count the file as changed.
136
294
  summary.totalMatches += plan.matchCount;
137
295
  summary.filesChanged++;
138
296
  recordChangedFile(summary, validPath, plan.matchCount);
@@ -141,109 +299,62 @@ async function processEntry(entryPath, ctx) {
141
299
  originalContent: plan.originalContent,
142
300
  updatedContent: plan.updatedContent,
143
301
  includeDiff: options.dryRun || options.returnDiff,
302
+ ...(signal ? { signal } : {}),
144
303
  });
145
- if (!options.dryRun) {
146
- await atomicWriteFile(validPath, plan.updatedContent, {
147
- encoding: 'utf-8',
148
- signal,
149
- });
150
- }
151
304
  }
152
305
  catch (error) {
153
306
  summary.failedFiles++;
154
307
  recordFailure(summary.failures, {
155
- path: validPath,
156
- error: buildStructuredError(error, ErrorCode.UNKNOWN, validPath),
308
+ path: toPosixRelative(summary.root, validPath),
309
+ error: Problem.fromUnknown(error, ErrorCode.UNKNOWN, validPath),
157
310
  });
158
311
  }
159
312
  }
160
313
  async function readReplacementPlan(validPath, ctx) {
161
- const { matcher, replacement, maxFileSize, signal } = ctx;
162
- let fileHandle;
314
+ const env_1 = { stack: [], error: void 0, hasError: false };
163
315
  try {
164
- const fd = await open(validPath, 'r');
165
- fileHandle = fd;
316
+ const { matcher, replacement, maxFileSize, signal } = ctx;
317
+ const fileHandle = __addDisposableResource(env_1, await ctx.fs.open(validPath, 'r'), true);
166
318
  const stats = await fileHandle.stat();
167
319
  if (stats.size > maxFileSize) {
168
- throw new Error(formatFileTooLargeError(validPath, stats.size, maxFileSize));
320
+ throw new FsError(ErrorCode.TOO_LARGE, `File too large: ${validPath} (${String(stats.size)} bytes > ${String(maxFileSize)} bytes)`);
169
321
  }
170
- let content;
171
- if (matcher.testBuffer) {
172
- const buffer = await fileHandle.readFile({ signal });
173
- if (!matcher.testBuffer(buffer)) {
174
- return undefined;
175
- }
176
- content = buffer.toString('utf-8');
177
- }
178
- else {
179
- content = await fileHandle.readFile({
180
- encoding: 'utf-8',
181
- signal,
182
- });
183
- }
184
- return buildReplacementPlan(content, replacement, matcher);
322
+ const buffer = await readFileBufferWithLimit(fileHandle, maxFileSize, validPath, signal);
323
+ if (!matcher.testBuffer(buffer))
324
+ return undefined;
325
+ return buildReplacementPlan(buffer.toString('utf-8'), replacement, matcher);
326
+ }
327
+ catch (e_1) {
328
+ env_1.error = e_1;
329
+ env_1.hasError = true;
185
330
  }
186
331
  finally {
187
- if (fileHandle) {
188
- await fileHandle.close();
189
- }
332
+ const result_1 = __disposeResources(env_1);
333
+ if (result_1)
334
+ await result_1;
190
335
  }
191
336
  }
192
337
  async function maybeAppendPatchDiff(summary, params) {
193
338
  if (!params.includeDiff)
194
339
  return;
340
+ const header = toPosixRelative(summary.root, params.filePath);
341
+ const patch = await new Promise((resolve) => {
342
+ createTwoFilesPatch(header, header, params.originalContent, params.updatedContent, 'Original', 'Modified', {
343
+ callback: (res) => {
344
+ resolve(res ?? '');
345
+ },
346
+ });
347
+ });
195
348
  if (summary.diff.length >= MAX_DIFF_SIZE) {
196
349
  summary.diffTruncated = true;
197
350
  return;
198
351
  }
199
- const patch = await new Promise((resolve) => {
200
- // Defer to event loop to avoid blocking on large diffs
201
- setImmediate(() => {
202
- createTwoFilesPatch(basename(params.filePath), basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
203
- callback: (res) => {
204
- resolve(res ?? '');
205
- },
206
- });
207
- });
208
- });
209
- if (summary.diff.length + patch.length <=
210
- MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
352
+ if (summary.diff.length + patch.length <= MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
211
353
  summary.diff += patch;
212
354
  return;
213
355
  }
214
356
  summary.diffTruncated = true;
215
357
  }
216
- async function processEntriesConcurrently(entries, options) {
217
- const pending = new Set();
218
- const { signal, concurrency, maxEntries, onEntry, runEntry } = options;
219
- let dispatched = 0;
220
- let stoppedByLimit = false;
221
- const waitForSlot = async () => {
222
- if (pending.size < concurrency)
223
- return;
224
- await Promise.race(pending);
225
- };
226
- for await (const entry of entries) {
227
- if (signal?.aborted)
228
- break;
229
- if (maxEntries !== undefined && dispatched >= maxEntries) {
230
- stoppedByLimit = true;
231
- break;
232
- }
233
- await waitForSlot();
234
- onEntry();
235
- dispatched++;
236
- const task = runEntry(entry.path);
237
- pending.add(task);
238
- void task.finally(() => {
239
- pending.delete(task);
240
- });
241
- }
242
- if (pending.size > 0) {
243
- await Promise.allSettled([...pending]);
244
- }
245
- return { stoppedByLimit };
246
- }
247
358
  function createReplaceSummary(root) {
248
359
  return {
249
360
  root,
@@ -258,144 +369,204 @@ function createReplaceSummary(root) {
258
369
  diffTruncated: false,
259
370
  };
260
371
  }
261
- async function resolveSearchRoot(pathValue, signal) {
262
- return pathValue
263
- ? validateExistingPath(pathValue, signal)
264
- : resolvePathOrRoot(pathValue);
265
- }
266
- function createReplacementRegex(args) {
267
- if (!args.isRegex)
268
- return undefined;
269
- return createRegexMatcher(args.searchPattern, args.caseSensitive);
270
- }
271
- function createReplacementMatcher(args) {
272
- const regex = createReplacementRegex(args);
273
- if (regex) {
274
- return createRegexReplacementMatcher(regex);
372
+ async function resolveSearchRoot(pathValue, fs) {
373
+ if (!pathValue) {
374
+ return { root: fs.pathGuard.resolvePathOrRoot(undefined) };
275
375
  }
276
- if (!args.caseSensitive) {
277
- const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
278
- const caseInsensitiveRegex = new RE2(escaped, 'gi');
279
- return createRegexReplacementMatcher(caseInsensitiveRegex);
376
+ const resolvedPath = await fs.pathGuard.validateExistingPath(pathValue);
377
+ const { stats: fileStats } = await fs.stat(resolvedPath);
378
+ if (fileStats.isFile()) {
379
+ // A single explicit file target bypasses the glob machinery entirely:
380
+ // routing it through globEntries with baseNameMatch would rewrite the
381
+ // escaped basename to `**/${basename}` and match every same-named file
382
+ // under the parent tree, not just this one.
383
+ return {
384
+ root: dirname(resolvedPath),
385
+ singleFile: resolvedPath,
386
+ };
280
387
  }
281
- return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
388
+ return { root: resolvedPath };
282
389
  }
283
- async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
284
- const maxFileSize = MAX_TEXT_FILE_SIZE;
285
- const root = await resolveSearchRoot(args.path, signal);
286
- const matcher = createReplacementMatcher(args);
287
- const entries = globEntries({
288
- cwd: root,
289
- pattern: args.filePattern,
290
- excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
291
- includeHidden: args.includeHidden,
292
- baseNameMatch: false,
293
- caseSensitiveMatch: true, // Default to sensitive for file paths
294
- followSymbolicLinks: false,
295
- onlyFiles: true,
296
- stats: false,
297
- suppressErrors: true,
298
- });
299
- const summary = createReplaceSummary(root);
300
- const t0 = performance.now();
301
- const context = {
302
- options: {
303
- dryRun: args.dryRun,
304
- returnDiff: args.returnDiff,
305
- },
306
- replacement: args.replacement,
307
- matcher,
308
- maxFileSize,
309
- signal,
310
- summary,
311
- };
312
- const { stoppedByLimit } = await processEntriesConcurrently(entries, {
313
- signal,
314
- concurrency: REPLACE_CONCURRENCY,
315
- ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
316
- onEntry: () => {
317
- summary.processedFiles++;
318
- onProgress({ current: summary.processedFiles });
319
- },
320
- runEntry: (entryPath) => processEntry(entryPath, context),
321
- });
322
- summary.perfTimeMs = performance.now() - t0;
323
- if (stoppedByLimit) {
324
- summary.stoppedReason = 'maxFiles';
325
- }
326
- onProgress({ current: summary.processedFiles });
327
- if (!args.dryRun && summary.totalMatches > 0) {
328
- Logger.info(`search_and_replace: ${summary.filesChanged} file(s), ${summary.totalMatches} match(es)`);
390
+ function buildSearchPattern(args) {
391
+ if (args.isRegex) {
392
+ return args.wholeWord ? `\\b(?:${args.searchPattern})\\b` : args.searchPattern;
329
393
  }
330
- return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
331
- }
332
- export function registerSearchAndReplaceTool(server, options = {}) {
333
- const handler = (args, ctx) => executeToolWithDiagnostics({
334
- toolName: 'search_and_replace',
335
- ctx,
336
- outputSchema: SearchAndReplaceOutputSchema,
337
- timedSignal: {},
338
- ...(args.path ? { context: { path: args.path } } : {}),
339
- run: async (signal) => {
340
- const dryLabel = args.dryRun ? ' [dry run]' : '';
341
- const truncatedPattern = truncateProgressPattern(args.searchPattern);
342
- const context = `"${truncatedPattern}" in ${args.filePattern}${dryLabel}`;
343
- const progress = createToolProgressSession(ctx, `${SEARCH_AND_REPLACE_TOOL.title}: ${context}`);
344
- const progressWithMessage = ({ current, total, }) => {
345
- progress.update({
346
- current,
347
- ...(total !== undefined ? { total } : {}),
348
- message: `${SEARCH_AND_REPLACE_TOOL.title}: ${truncatedPattern} [${current} files]`,
349
- });
350
- };
351
- try {
352
- const result = await handleSearchAndReplace(args, signal, progressWithMessage);
353
- const sc = result.structuredContent;
354
- const finalCurrent = resolveFinalProgressCurrent(progress, (sc.processedFiles ?? 0) + 1);
355
- const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
356
- const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
357
- let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
358
- if (sc.failedFiles)
359
- endSuffix += `, ${sc.failedFiles} failed`;
360
- progress.complete(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${endSuffix}`, finalCurrent);
361
- if (!args.dryRun) {
362
- void ctx.log?.('info', `search_and_replace: ${String(sc.matches ?? 0)} matches in ${String(sc.filesChanged ?? 0)} files`);
363
- }
364
- return result;
365
- }
366
- catch (error) {
367
- progress.fail(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${classifyError(error)}`);
368
- throw error;
369
- }
370
- },
371
- onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
372
- });
373
- registerStandardTool(server, SEARCH_AND_REPLACE_TOOL, handler, options);
394
+ const escaped = escapeRegexLiteral(args.searchPattern);
395
+ return args.wholeWord ? `\\b(?:${escaped})\\b` : escaped;
374
396
  }
375
- function buildSearchAndReplaceText(summary, dryRun) {
376
- const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
377
- const dryRunSuffix = dryRun ? ' (Dry run)' : '';
378
- const timing = summary.perfTimeMs
379
- ? ` [\u23F1\uFE0F ${summary.perfTimeMs.toFixed(0)}ms]`
380
- : '';
381
- return `Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${dryRunSuffix}${timing}`;
397
+ function createReplacementMatcher(args) {
398
+ // Use regex when isRegex, wholeWord, or case-insensitive (all require RE2)
399
+ if (args.isRegex || args.wholeWord || !args.caseSensitive) {
400
+ const regex = compileRegex(buildSearchPattern(args), { caseSensitive: args.caseSensitive });
401
+ return createRegexReplacementMatcher(regex, args.isRegex);
402
+ }
403
+ return createCaseSensitiveLiteralMatcher(args.searchPattern);
382
404
  }
383
405
  function buildSearchAndReplaceStructuredResult(summary, args) {
406
+ const results = [
407
+ ...summary.changedFiles.map((f) => ({ path: f.path, value: { matches: f.matches } })),
408
+ ...summary.failures.map((f) => ({ path: f.path, error: f.error })),
409
+ ];
384
410
  return {
385
- ok: true,
386
- matches: summary.totalMatches,
387
- filesChanged: summary.filesChanged,
388
- processedFiles: summary.processedFiles,
389
- ...(summary.failedFiles > 0 ? { failedFiles: summary.failedFiles } : {}),
390
- ...(summary.failures.length > 0 ? { failures: summary.failures } : {}),
391
- ...(summary.changedFiles.length > 0
392
- ? { changedFiles: summary.changedFiles }
393
- : {}),
394
- ...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
395
- ...((args.dryRun || args.returnDiff) && summary.diff
396
- ? { diff: summary.diff }
411
+ results,
412
+ // `failedFiles` counts every failure; `failures` itself is capped at
413
+ // MAX_FAILURES, so the summary must not be derived from the array length.
414
+ summary: {
415
+ total: summary.filesChanged + summary.failedFiles,
416
+ succeeded: summary.filesChanged,
417
+ failed: summary.failedFiles,
418
+ },
419
+ totalMatches: summary.totalMatches,
420
+ filesScanned: summary.processedFiles,
421
+ // Failures are capped at MAX_FAILURES independently of the changed-file
422
+ // cap, so either cap can leave `results` shorter than `summary.total`.
423
+ ...(summary.changedFilesTruncated || summary.failures.length < summary.failedFiles
424
+ ? { resultsTruncated: true }
397
425
  : {}),
426
+ ...((args.dryRun || args.returnDiff) && summary.diff ? { diff: summary.diff } : {}),
398
427
  ...(summary.diffTruncated ? { diffTruncated: true } : {}),
399
428
  ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
400
429
  };
401
430
  }
431
+ async function handleSearchAndReplace(args, ctx) {
432
+ const maxFileSize = getMaxTextFileSize();
433
+ const { root, singleFile } = await resolveSearchRoot(args.path, ctx.fs);
434
+ const effectivePattern = args.pattern ?? '**/*';
435
+ // An explicit single-file target bypasses baseNameMatch/exclude/hidden/
436
+ // gitignore filtering — it should always be processed as the one file named.
437
+ // The async generator matches globEntries' AsyncIterable contract; it needs
438
+ // no await (single yield), hence the disable.
439
+ const entries = singleFile
440
+ ? // eslint-disable-next-line @typescript-eslint/require-await
441
+ (async function* singleFileEntry() {
442
+ yield { path: singleFile };
443
+ })()
444
+ : globEntries({
445
+ cwd: root,
446
+ pattern: effectivePattern,
447
+ excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
448
+ includeHidden: args.includeHidden,
449
+ respectGitignore: !args.includeIgnored,
450
+ baseNameMatch: true,
451
+ onlyFiles: true,
452
+ suppressErrors: true,
453
+ ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
454
+ });
455
+ const summary = createReplaceSummary(root);
456
+ // The matcher may own a compiled RE2 pattern, whose wasm memory re2-wasm
457
+ // never reclaims on its own. Nothing past the scan touches it, so free it the
458
+ // moment the scan is done — success, failure, or abort alike.
459
+ const matcher = createReplacementMatcher(args);
460
+ let scan;
461
+ try {
462
+ const context = {
463
+ options: {
464
+ dryRun: args.dryRun,
465
+ returnDiff: args.returnDiff,
466
+ },
467
+ replacement: args.replacement,
468
+ matcher,
469
+ maxFileSize,
470
+ signal: ctx.signal,
471
+ summary,
472
+ fs: ctx.fs,
473
+ };
474
+ scan = await processEntriesConcurrently(entries, {
475
+ signal: ctx.signal,
476
+ concurrency: REPLACE_CONCURRENCY,
477
+ ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
478
+ shouldStop: () => summary.totalMatches >= args.maxResults,
479
+ onEntry: () => {
480
+ summary.processedFiles++;
481
+ ctx.onProgress?.({ current: summary.processedFiles });
482
+ },
483
+ onError: (entryPath, err) => {
484
+ summary.failedFiles++;
485
+ recordFailure(summary.failures, {
486
+ path: toPosixRelative(summary.root, entryPath),
487
+ error: Problem.fromUnknown(err, ErrorCode.UNKNOWN, entryPath),
488
+ });
489
+ },
490
+ runEntry: (entryPath) => processEntry(entryPath, context),
491
+ });
492
+ }
493
+ finally {
494
+ matcher.dispose();
495
+ }
496
+ const stoppedReason = scan.resolve();
497
+ if (stoppedReason !== undefined)
498
+ summary.stoppedReason = stoppedReason;
499
+ ctx.onProgress?.({ current: summary.processedFiles });
500
+ if (!args.dryRun && summary.totalMatches > 0) {
501
+ ctx.log?.('info', `${summary.filesChanged} file(s) changed, ${summary.totalMatches} match(es)`, 'replace_text');
502
+ }
503
+ const structured = buildSearchAndReplaceStructuredResult(summary, args);
504
+ // Store primary file in resource store if available (skip in dryRun to avoid reading stale disk content)
505
+ if (!args.dryRun &&
506
+ ctx.resourceStore &&
507
+ summary.changedFiles.length > 0 &&
508
+ summary.filesChanged > 0) {
509
+ const primaryFile = summary.changedFiles[0];
510
+ if (!primaryFile)
511
+ return { structured };
512
+ const primaryFilePath = primaryFile.path;
513
+ const fullPath = join(summary.root, primaryFilePath);
514
+ try {
515
+ const { content: rawBuffer } = await ctx.fs.readRaw(fullPath, {
516
+ signal: ctx.signal,
517
+ });
518
+ const mimeInfo = detectMimeFromContent(fullPath, rawBuffer.toString('utf-8'));
519
+ const link = buildFileResourceLink(fullPath, mimeInfo.mimeType, rawBuffer.length);
520
+ return { structured, link };
521
+ }
522
+ catch (error) {
523
+ rethrowIfAborted(error);
524
+ // Gracefully fall back if resource storage fails
525
+ ctx.log?.('error', `Failed to store primary file in resource store: ${formatUnknownErrorMessage(error)}`, 'replace_text');
526
+ }
527
+ }
528
+ return { structured };
529
+ }
530
+ export const SEARCH_AND_REPLACE = defineTool({
531
+ name: 'replace_text',
532
+ title: 'Search and Replace',
533
+ description: 'Bulk search-and-replace across files matching a glob pattern. ' +
534
+ 'Replaces ALL occurrences per file (unlike edit, which replaces only the first match). ' +
535
+ 'Set returnDiff=true to preview changes as a unified diff before or after writing. ' +
536
+ 'Literal matching by default; set isRegex=true to enable RE2 regex with capture groups ($1, $2).',
537
+ input: SearchAndReplaceInputSchema,
538
+ output: SearchAndReplaceOutputSchema,
539
+ // results[].value XOR results[].error, and `summary` counts files while
540
+ // `totalMatches` counts replacements — not inferable from the description.
541
+ publishOutputSchema: true,
542
+ annotations: {
543
+ readOnlyHint: false,
544
+ idempotentHint: false,
545
+ destructiveHint: true,
546
+ openWorldHint: false,
547
+ },
548
+ timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
549
+ defaultErrorCode: ErrorCode.UNKNOWN,
550
+ progress: (args) => {
551
+ const dryLabel = args.dryRun ? ' [dry run]' : '';
552
+ return {
553
+ label: `Replace${dryLabel}`,
554
+ subject: `${truncateProgressPattern(args.searchPattern)} → ${truncateProgressPattern(args.replacement)}`,
555
+ };
556
+ },
557
+ accessPaths: (args) => (args.path ? [args.path] : []),
558
+ run: async (args, ctx) => {
559
+ const truncatedPattern = truncateProgressPattern(args.searchPattern);
560
+ const { structured, link } = await handleSearchAndReplace(args, ctx);
561
+ const dryLabel = args.dryRun ? ' [dry run]' : '';
562
+ const summaryText = `replace_text: '${truncatedPattern}'${dryLabel}` +
563
+ ` \u00b7 ${String(structured.totalMatches)} match(es)` +
564
+ ` in ${String(structured.summary.succeeded)} file(s)` +
565
+ (structured.summary.failed > 0 ? ` \u00b7 ${String(structured.summary.failed)} failed` : '');
566
+ if (link) {
567
+ return { structured, text: summaryText, resources: [link] };
568
+ }
569
+ return { structured, text: summaryText };
570
+ },
571
+ });
572
+ //# sourceMappingURL=replace-in-files.js.map