@j0hanz/filesystem-mcp 1.19.1 → 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.
- package/README.md +333 -838
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +169 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +168 -123
- package/dist/cli.js.map +1 -0
- package/dist/core/concurrency.d.ts +70 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +173 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cursor.d.ts +23 -0
- package/dist/core/cursor.d.ts.map +1 -0
- package/dist/core/cursor.js +62 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +96 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +356 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-uri.d.ts +24 -0
- package/dist/core/file-uri.d.ts.map +1 -0
- package/dist/core/file-uri.js +52 -0
- package/dist/core/file-uri.js.map +1 -0
- package/dist/core/fmt.d.ts +38 -0
- package/dist/core/fmt.d.ts.map +1 -0
- package/dist/core/fmt.js +109 -0
- package/dist/core/fmt.js.map +1 -0
- package/dist/core/fs.d.ts +108 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +243 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/glob.d.ts +41 -0
- package/dist/core/glob.d.ts.map +1 -0
- package/dist/core/glob.js +399 -0
- package/dist/core/glob.js.map +1 -0
- package/dist/core/input-required.d.ts +147 -0
- package/dist/core/input-required.d.ts.map +1 -0
- package/dist/core/input-required.js +261 -0
- package/dist/core/input-required.js.map +1 -0
- package/dist/core/mime.d.ts +15 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +188 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/core/observability.d.ts +10 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/observability.js +76 -0
- package/dist/core/observability.js.map +1 -0
- package/dist/core/page-store.d.ts +41 -0
- package/dist/core/page-store.d.ts.map +1 -0
- package/dist/core/page-store.js +74 -0
- package/dist/core/page-store.js.map +1 -0
- package/dist/core/path-completer.d.ts +10 -0
- package/dist/core/path-completer.d.ts.map +1 -0
- package/dist/core/path-completer.js +227 -0
- package/dist/core/path-completer.js.map +1 -0
- package/dist/core/path-discovery.d.ts +8 -0
- package/dist/core/path-discovery.d.ts.map +1 -0
- package/dist/core/path-discovery.js +105 -0
- package/dist/core/path-discovery.js.map +1 -0
- package/dist/core/path-utils.d.ts +9 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +121 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/path.d.ts +148 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +674 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/primitives.d.ts +23 -0
- package/dist/core/primitives.d.ts.map +1 -0
- package/dist/core/primitives.js +54 -0
- package/dist/core/primitives.js.map +1 -0
- package/dist/core/read.d.ts +74 -0
- package/dist/core/read.d.ts.map +1 -0
- package/dist/core/read.js +562 -0
- package/dist/core/read.js.map +1 -0
- package/dist/core/schema.d.ts +139 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +345 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.d.ts +100 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +248 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sensitive.d.ts +6 -0
- package/dist/core/sensitive.d.ts.map +1 -0
- package/dist/core/sensitive.js +142 -0
- package/dist/core/sensitive.js.map +1 -0
- package/dist/core/store.d.ts +57 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/util.d.ts +26 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +66 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/watcher-registry.d.ts +68 -0
- package/dist/core/watcher-registry.d.ts.map +1 -0
- package/dist/core/watcher-registry.js +312 -0
- package/dist/core/watcher-registry.js.map +1 -0
- package/dist/http-policy.d.ts +121 -0
- package/dist/http-policy.d.ts.map +1 -0
- package/dist/http-policy.js +340 -0
- package/dist/http-policy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -41
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +71 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts.d.ts +8 -6
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +74 -168
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +73 -9
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +392 -163
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +62 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -1
- package/dist/server.js.map +1 -0
- package/dist/tools/batch.d.ts +89 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +157 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.d.ts.map +1 -0
- package/dist/tools/create.js +142 -0
- package/dist/tools/create.js.map +1 -0
- package/dist/tools/define.d.ts +110 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +394 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/delete-file.d.ts +2 -4
- package/dist/tools/delete-file.d.ts.map +1 -0
- package/dist/tools/delete-file.js +330 -87
- package/dist/tools/delete-file.js.map +1 -0
- package/dist/tools/diff.d.ts +2 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +94 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +442 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +26 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list.d.ts +2 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +291 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/move.d.ts +2 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +321 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/patch.d.ts +2 -0
- package/dist/tools/patch.d.ts.map +1 -0
- package/dist/tools/patch.js +155 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/progress.d.ts +52 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +172 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/read.d.ts +2 -4
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +398 -152
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replace-in-files.d.ts +2 -4
- package/dist/tools/replace-in-files.d.ts.map +1 -0
- package/dist/tools/replace-in-files.js +435 -264
- package/dist/tools/replace-in-files.js.map +1 -0
- package/dist/tools/roots.d.ts +2 -4
- package/dist/tools/roots.d.ts.map +1 -0
- package/dist/tools/roots.js +29 -52
- package/dist/tools/roots.js.map +1 -0
- package/dist/tools/search-content.d.ts +2 -4
- package/dist/tools/search-content.d.ts.map +1 -0
- package/dist/tools/search-content.js +262 -228
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/search-files.d.ts +2 -4
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +162 -161
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/stat.d.ts +2 -4
- package/dist/tools/stat.d.ts.map +1 -0
- package/dist/tools/stat.js +202 -62
- package/dist/tools/stat.js.map +1 -0
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +276 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/shared.d.ts +45 -0
- package/dist/transport/shared.d.ts.map +1 -0
- package/dist/transport/shared.js +65 -0
- package/dist/transport/shared.js.map +1 -0
- package/dist/transport/stdio.d.ts +32 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +279 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +37 -46
- package/dist/assets/logo.svg +0 -35
- package/dist/completions.d.ts +0 -2
- package/dist/completions.js +0 -572
- package/dist/config.d.ts +0 -119
- package/dist/config.js +0 -31
- package/dist/lib/abort.d.ts +0 -7
- package/dist/lib/abort.js +0 -81
- package/dist/lib/constants.d.ts +0 -42
- package/dist/lib/constants.js +0 -313
- package/dist/lib/errors.d.ts +0 -28
- package/dist/lib/errors.js +0 -301
- package/dist/lib/file-operations/core.d.ts +0 -53
- package/dist/lib/file-operations/core.js +0 -144
- package/dist/lib/file-operations/metadata.d.ts +0 -77
- package/dist/lib/file-operations/metadata.js +0 -878
- package/dist/lib/file-operations/search.d.ts +0 -46
- package/dist/lib/file-operations/search.js +0 -1194
- package/dist/lib/file-operations/traversal.d.ts +0 -40
- package/dist/lib/file-operations/traversal.js +0 -310
- package/dist/lib/fs-helpers.d.ts +0 -46
- package/dist/lib/fs-helpers.js +0 -606
- package/dist/lib/globs.d.ts +0 -2
- package/dist/lib/globs.js +0 -19
- package/dist/lib/logger.d.ts +0 -27
- package/dist/lib/logger.js +0 -91
- package/dist/lib/observability.d.ts +0 -33
- package/dist/lib/observability.js +0 -373
- package/dist/lib/paths.d.ts +0 -35
- package/dist/lib/paths.js +0 -634
- package/dist/lib/resource-store.d.ts +0 -28
- package/dist/lib/resource-store.js +0 -195
- package/dist/lib/utils.d.ts +0 -7
- package/dist/lib/utils.js +0 -37
- package/dist/lib/zod-codecs.d.ts +0 -2
- package/dist/lib/zod-codecs.js +0 -18
- package/dist/pkg-info.d.ts +0 -7
- package/dist/pkg-info.js +0 -9
- package/dist/resources/generated-instructions.d.ts +0 -1
- package/dist/resources/generated-instructions.js +0 -105
- package/dist/resources/tool-catalog.d.ts +0 -2
- package/dist/resources/tool-catalog.js +0 -78
- package/dist/resources/tool-info.d.ts +0 -9
- package/dist/resources/tool-info.js +0 -206
- package/dist/resources/workflows.d.ts +0 -1
- package/dist/resources/workflows.js +0 -72
- package/dist/schemas.d.ts +0 -505
- package/dist/schemas.js +0 -676
- package/dist/server/bootstrap.d.ts +0 -6
- package/dist/server/bootstrap.js +0 -585
- package/dist/server/event-store.d.ts +0 -18
- package/dist/server/event-store.js +0 -71
- package/dist/server/roots-manager.d.ts +0 -28
- package/dist/server/roots-manager.js +0 -227
- package/dist/server/task-store.d.ts +0 -10
- package/dist/server/task-store.js +0 -91
- package/dist/tools/apply-patch.d.ts +0 -4
- package/dist/tools/apply-patch.js +0 -235
- package/dist/tools/calculate-hash.d.ts +0 -4
- package/dist/tools/calculate-hash.js +0 -187
- package/dist/tools/contract.d.ts +0 -50
- package/dist/tools/contract.js +0 -1
- package/dist/tools/create-directory.d.ts +0 -4
- package/dist/tools/create-directory.js +0 -68
- package/dist/tools/diff-files.d.ts +0 -4
- package/dist/tools/diff-files.js +0 -136
- package/dist/tools/edit-file.d.ts +0 -4
- package/dist/tools/edit-file.js +0 -256
- package/dist/tools/icons.d.ts +0 -15
- package/dist/tools/icons.js +0 -24
- package/dist/tools/list-directory.d.ts +0 -4
- package/dist/tools/list-directory.js +0 -218
- package/dist/tools/move-file.d.ts +0 -4
- package/dist/tools/move-file.js +0 -178
- package/dist/tools/read-multiple.d.ts +0 -4
- package/dist/tools/read-multiple.js +0 -207
- package/dist/tools/shared.d.ts +0 -172
- package/dist/tools/shared.js +0 -599
- package/dist/tools/stat-many.d.ts +0 -4
- package/dist/tools/stat-many.js +0 -104
- package/dist/tools/task-support.d.ts +0 -29
- package/dist/tools/task-support.js +0 -473
- package/dist/tools/tree.d.ts +0 -4
- package/dist/tools/tree.js +0 -102
- package/dist/tools/write-file.d.ts +0 -4
- package/dist/tools/write-file.js +0 -60
- package/dist/tools.d.ts +0 -5
- 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 {
|
|
3
|
-
import
|
|
54
|
+
import { dirname, join } from 'node:path';
|
|
55
|
+
import * as z from 'zod/v4';
|
|
4
56
|
import { createTwoFilesPatch } from 'diff';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
let m;
|
|
212
|
+
while ((m = regex.exec(content)) !== null) {
|
|
65
213
|
matchCount++;
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
243
|
+
function createCaseSensitiveLiteralMatcher(searchPattern) {
|
|
79
244
|
const patternLength = searchPattern.length;
|
|
80
|
-
const searchBuffer =
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
162
|
-
let fileHandle;
|
|
314
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
163
315
|
try {
|
|
164
|
-
const
|
|
165
|
-
fileHandle =
|
|
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
|
|
320
|
+
throw new FsError(ErrorCode.TOO_LARGE, `File too large: ${validPath} (${String(stats.size)} bytes > ${String(maxFileSize)} bytes)`);
|
|
169
321
|
}
|
|
170
|
-
|
|
171
|
-
if (matcher.testBuffer)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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,
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
388
|
+
return { root: resolvedPath };
|
|
282
389
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|