@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.
- 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 -563
- 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 -472
- 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,1194 +0,0 @@
|
|
|
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
|
-
});
|
|
53
|
-
import { AsyncResource } from 'node:async_hooks';
|
|
54
|
-
import { open, stat } from 'node:fs/promises';
|
|
55
|
-
import { basename, dirname } from 'node:path';
|
|
56
|
-
import { debuglog } from 'node:util';
|
|
57
|
-
import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
|
|
58
|
-
import RE2 from 're2';
|
|
59
|
-
import { z } from 'zod';
|
|
60
|
-
import { assertNotAborted, withAbort, withTimedAbortSignal } from '../abort.js';
|
|
61
|
-
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
|
|
62
|
-
import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
|
|
63
|
-
import { isProbablyBinary } from '../fs-helpers.js';
|
|
64
|
-
import { isSafeGlobPattern } from '../globs.js';
|
|
65
|
-
import { startPerfMeasure } from '../observability.js';
|
|
66
|
-
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
|
|
67
|
-
import { mergeOptions, omitOptionKeys } from '../utils.js';
|
|
68
|
-
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
|
|
69
|
-
import { buildGlobOptions, globEntries } from './traversal.js';
|
|
70
|
-
function countRegexLineMatches(regex, line) {
|
|
71
|
-
regex.lastIndex = 0;
|
|
72
|
-
let count = 0;
|
|
73
|
-
while (regex.exec(line) !== null) {
|
|
74
|
-
count++;
|
|
75
|
-
if (regex.lastIndex === 0)
|
|
76
|
-
regex.lastIndex++;
|
|
77
|
-
}
|
|
78
|
-
return count;
|
|
79
|
-
}
|
|
80
|
-
function escapeLiteral(pattern) {
|
|
81
|
-
return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
-
}
|
|
83
|
-
function buildRegexPattern(pattern, options) {
|
|
84
|
-
const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
|
|
85
|
-
return options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
86
|
-
}
|
|
87
|
-
function buildLiteralMatcher(pattern, options) {
|
|
88
|
-
if (!options.caseSensitive) {
|
|
89
|
-
const final = escapeLiteral(pattern);
|
|
90
|
-
const regex = new RE2(final, 'gi');
|
|
91
|
-
return (line) => countRegexLineMatches(regex, line);
|
|
92
|
-
}
|
|
93
|
-
// Fast path for case-sensitive literal
|
|
94
|
-
const needle = pattern;
|
|
95
|
-
if (needle.length === 0)
|
|
96
|
-
return () => 0;
|
|
97
|
-
return (line) => {
|
|
98
|
-
if (line.length === 0)
|
|
99
|
-
return 0;
|
|
100
|
-
let count = 0;
|
|
101
|
-
let pos = line.indexOf(needle);
|
|
102
|
-
while (pos !== -1) {
|
|
103
|
-
count++;
|
|
104
|
-
pos = line.indexOf(needle, pos + needle.length);
|
|
105
|
-
}
|
|
106
|
-
return count;
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function buildRegexMatcher(final, caseSensitive) {
|
|
110
|
-
const flags = caseSensitive ? 'g' : 'gi';
|
|
111
|
-
const regex = new RE2(final, flags);
|
|
112
|
-
return (line) => countRegexLineMatches(regex, line);
|
|
113
|
-
}
|
|
114
|
-
function buildMatcher(pattern, options) {
|
|
115
|
-
if (options.isLiteral && pattern.length === 0)
|
|
116
|
-
return () => 0;
|
|
117
|
-
if (options.isLiteral && !options.wholeWord) {
|
|
118
|
-
// fast path for simple literal search
|
|
119
|
-
return buildLiteralMatcher(pattern, options);
|
|
120
|
-
}
|
|
121
|
-
const final = buildRegexPattern(pattern, options);
|
|
122
|
-
return buildRegexMatcher(final, options.caseSensitive);
|
|
123
|
-
}
|
|
124
|
-
// --- Configuration & Schemas ---
|
|
125
|
-
const SEARCH_CONTENT_MAX_RESULTS = 500;
|
|
126
|
-
const SafeFilePatternSchema = z
|
|
127
|
-
.string()
|
|
128
|
-
.min(1, 'Pattern required')
|
|
129
|
-
.max(1000, 'Max 1000 chars')
|
|
130
|
-
.refine((value) => isSafeGlobPattern(value), {
|
|
131
|
-
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
132
|
-
});
|
|
133
|
-
const SearchOptionsSchema = z.strictObject({
|
|
134
|
-
filePattern: SafeFilePatternSchema,
|
|
135
|
-
excludePatterns: z.array(z.string()),
|
|
136
|
-
caseSensitive: z.boolean(),
|
|
137
|
-
maxResults: z.int().min(0),
|
|
138
|
-
maxFileSize: z.int().min(0),
|
|
139
|
-
maxFilesScanned: z.int().min(0),
|
|
140
|
-
timeoutMs: z.int().min(0),
|
|
141
|
-
skipBinary: z.boolean(),
|
|
142
|
-
contextLines: z.int().min(0),
|
|
143
|
-
wholeWord: z.boolean(),
|
|
144
|
-
isLiteral: z.boolean(),
|
|
145
|
-
includeHidden: z.boolean(),
|
|
146
|
-
baseNameMatch: z.boolean(),
|
|
147
|
-
caseSensitiveFileMatch: z.boolean(),
|
|
148
|
-
});
|
|
149
|
-
const DEFAULTS = {
|
|
150
|
-
filePattern: '**/*',
|
|
151
|
-
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
152
|
-
caseSensitive: false,
|
|
153
|
-
maxResults: SEARCH_CONTENT_MAX_RESULTS,
|
|
154
|
-
maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
|
|
155
|
-
maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
|
|
156
|
-
timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
|
|
157
|
-
skipBinary: true,
|
|
158
|
-
contextLines: 0,
|
|
159
|
-
wholeWord: false,
|
|
160
|
-
isLiteral: true,
|
|
161
|
-
includeHidden: false,
|
|
162
|
-
baseNameMatch: false,
|
|
163
|
-
caseSensitiveFileMatch: true,
|
|
164
|
-
};
|
|
165
|
-
const ERROR_SCAN_CANCELLED = 'Scan cancelled';
|
|
166
|
-
const ERROR_WORKER_POOL_CLOSED = 'Worker pool closed';
|
|
167
|
-
const SEARCH_WORKER_NAME_PREFIX = 'filesystem-search';
|
|
168
|
-
const SEARCH_WORKER_RESOURCE_TYPE = 'SearchWorkerTask';
|
|
169
|
-
// --- Helpers ---
|
|
170
|
-
function resolveOptions(options) {
|
|
171
|
-
const normalizedOptions = omitOptionKeys(options, ['signal', 'onProgress']);
|
|
172
|
-
const merged = mergeOptions(DEFAULTS, normalizedOptions);
|
|
173
|
-
const result = SearchOptionsSchema.safeParse(merged);
|
|
174
|
-
if (!result.success) {
|
|
175
|
-
throw new McpError(ErrorCode.INVALID_INPUT, `Invalid search options:\n${z.prettifyError(result.error)}`, undefined, { errors: z.treeifyError(result.error) });
|
|
176
|
-
}
|
|
177
|
-
return result.data;
|
|
178
|
-
}
|
|
179
|
-
function getRemainingMatchCapacity(maxMatches, currentMatches, minimum = 0) {
|
|
180
|
-
return Math.max(minimum, maxMatches - currentMatches);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Manages a sliding window of lines and pending context-after buffers.
|
|
184
|
-
*/
|
|
185
|
-
class ContextBuffer {
|
|
186
|
-
capacity;
|
|
187
|
-
buffer; // Ring buffer fixed size
|
|
188
|
-
head = 0; // Next write index
|
|
189
|
-
size = 0; // Current count of items
|
|
190
|
-
pending = [];
|
|
191
|
-
constructor(contextLines) {
|
|
192
|
-
this.capacity = Math.max(0, contextLines);
|
|
193
|
-
this.buffer = new Array(this.capacity);
|
|
194
|
-
}
|
|
195
|
-
add(line) {
|
|
196
|
-
// 1. Fill Pending 'After' Contexts
|
|
197
|
-
if (this.pending.length > 0) {
|
|
198
|
-
let writeIndex = 0;
|
|
199
|
-
for (const p of this.pending) {
|
|
200
|
-
if (p.remaining > 0) {
|
|
201
|
-
p.buffer.push(line);
|
|
202
|
-
p.remaining--;
|
|
203
|
-
}
|
|
204
|
-
if (p.remaining > 0) {
|
|
205
|
-
this.pending[writeIndex] = p;
|
|
206
|
-
writeIndex++;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
this.pending.length = writeIndex;
|
|
210
|
-
}
|
|
211
|
-
// 2. Maintain 'Before' Buffer
|
|
212
|
-
if (this.capacity > 0) {
|
|
213
|
-
this.buffer[this.head] = line;
|
|
214
|
-
this.head = (this.head + 1) % this.capacity;
|
|
215
|
-
if (this.size < this.capacity) {
|
|
216
|
-
this.size++;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
snapshotBefore() {
|
|
221
|
-
if (this.size === 0)
|
|
222
|
-
return [];
|
|
223
|
-
const result = new Array(this.size);
|
|
224
|
-
if (this.size < this.capacity) {
|
|
225
|
-
for (let i = 0; i < this.size; i++) {
|
|
226
|
-
result[i] = this.buffer[i] ?? '';
|
|
227
|
-
}
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
let outIndex = 0;
|
|
231
|
-
for (let i = this.head; i < this.capacity; i++) {
|
|
232
|
-
result[outIndex] = this.buffer[i] ?? '';
|
|
233
|
-
outIndex++;
|
|
234
|
-
}
|
|
235
|
-
for (let i = 0; i < this.head; i++) {
|
|
236
|
-
result[outIndex] = this.buffer[i] ?? '';
|
|
237
|
-
outIndex++;
|
|
238
|
-
}
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
|
-
scheduleAfter() {
|
|
242
|
-
if (this.capacity === 0)
|
|
243
|
-
return [];
|
|
244
|
-
const buffer = [];
|
|
245
|
-
this.pending.push({ buffer, remaining: this.capacity });
|
|
246
|
-
return buffer;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
function trimContent(line) {
|
|
250
|
-
return line.length > MAX_LINE_CONTENT_LENGTH
|
|
251
|
-
? `${line.slice(0, MAX_LINE_CONTENT_LENGTH)}\u2026`
|
|
252
|
-
: line;
|
|
253
|
-
}
|
|
254
|
-
function processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx) {
|
|
255
|
-
const trimmedLine = trimContent(rawLine);
|
|
256
|
-
if (ctx) {
|
|
257
|
-
matches.push({
|
|
258
|
-
file: requestedPath,
|
|
259
|
-
line: lineNumber,
|
|
260
|
-
content: trimmedLine,
|
|
261
|
-
matchCount,
|
|
262
|
-
contextBefore: ctx.snapshotBefore(),
|
|
263
|
-
contextAfter: ctx.scheduleAfter(),
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
matches.push({
|
|
268
|
-
file: requestedPath,
|
|
269
|
-
line: lineNumber,
|
|
270
|
-
content: trimmedLine,
|
|
271
|
-
matchCount,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return trimmedLine;
|
|
275
|
-
}
|
|
276
|
-
async function readMatches(handle, requestedPath, matcher, options, maxMatches, isCancelled, signal) {
|
|
277
|
-
if (maxMatches <= 0) {
|
|
278
|
-
return [];
|
|
279
|
-
}
|
|
280
|
-
const matches = [];
|
|
281
|
-
const hasContext = options.contextLines > 0;
|
|
282
|
-
const ctx = hasContext ? new ContextBuffer(options.contextLines) : undefined;
|
|
283
|
-
let lineNumber = 1;
|
|
284
|
-
// Use for-await with readLines for memory efficiency
|
|
285
|
-
const lines = handle.readLines({ encoding: 'utf-8', signal });
|
|
286
|
-
try {
|
|
287
|
-
for await (const rawLine of lines) {
|
|
288
|
-
if (matches.length >= maxMatches)
|
|
289
|
-
break;
|
|
290
|
-
if (isCancelled())
|
|
291
|
-
break;
|
|
292
|
-
const matchCount = matcher(rawLine);
|
|
293
|
-
let trimmedLine = '';
|
|
294
|
-
if (matchCount > 0) {
|
|
295
|
-
trimmedLine = processLineMatch(matches, rawLine, lineNumber, matchCount, requestedPath, ctx);
|
|
296
|
-
}
|
|
297
|
-
else if (hasContext) {
|
|
298
|
-
trimmedLine = trimContent(rawLine);
|
|
299
|
-
}
|
|
300
|
-
if (ctx) {
|
|
301
|
-
ctx.add(trimmedLine);
|
|
302
|
-
}
|
|
303
|
-
lineNumber++;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
finally {
|
|
307
|
-
try {
|
|
308
|
-
lines.close();
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// Ignore close errors; handle cleanup is still managed by the caller.
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return matches;
|
|
315
|
-
}
|
|
316
|
-
async function scanFileResolved(resolvedPath, requestedPath, matcher, options, signal, maxMatches = Number.POSITIVE_INFINITY, injectedBinaryDetector) {
|
|
317
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
318
|
-
try {
|
|
319
|
-
assertNotAborted(signal);
|
|
320
|
-
const handle = __addDisposableResource(env_1, await withAbort(open(resolvedPath, 'r'), signal), true);
|
|
321
|
-
const stats = await withAbort(handle.stat(), signal);
|
|
322
|
-
if (stats.size > options.maxFileSize) {
|
|
323
|
-
return {
|
|
324
|
-
matches: [],
|
|
325
|
-
matched: false,
|
|
326
|
-
skippedTooLarge: true,
|
|
327
|
-
skippedBinary: false,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
if (options.skipBinary) {
|
|
331
|
-
const detect = injectedBinaryDetector ?? isProbablyBinary;
|
|
332
|
-
if (await detect(resolvedPath, handle, signal)) {
|
|
333
|
-
return {
|
|
334
|
-
matches: [],
|
|
335
|
-
matched: false,
|
|
336
|
-
skippedTooLarge: false,
|
|
337
|
-
skippedBinary: true,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const matches = await readMatches(handle, requestedPath, matcher, options, maxMatches, () => Boolean(signal?.aborted), signal);
|
|
342
|
-
return {
|
|
343
|
-
matches,
|
|
344
|
-
matched: matches.length > 0,
|
|
345
|
-
skippedTooLarge: false,
|
|
346
|
-
skippedBinary: false,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
catch (e_1) {
|
|
350
|
-
env_1.error = e_1;
|
|
351
|
-
env_1.hasError = true;
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
const result_1 = __disposeResources(env_1);
|
|
355
|
-
if (result_1)
|
|
356
|
-
await result_1;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
function buildScanFileOptions(opts) {
|
|
360
|
-
return {
|
|
361
|
-
maxFileSize: opts.maxFileSize,
|
|
362
|
-
skipBinary: opts.skipBinary,
|
|
363
|
-
contextLines: opts.contextLines,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function buildMatcherOptions(opts) {
|
|
367
|
-
return {
|
|
368
|
-
caseSensitive: opts.caseSensitive,
|
|
369
|
-
wholeWord: opts.wholeWord,
|
|
370
|
-
isLiteral: opts.isLiteral,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
function applyScanOutcome(summary, outcome) {
|
|
374
|
-
if (outcome.matched)
|
|
375
|
-
summary.filesMatched++;
|
|
376
|
-
if (outcome.skippedBinary)
|
|
377
|
-
summary.skippedBinary++;
|
|
378
|
-
if (outcome.skippedTooLarge)
|
|
379
|
-
summary.skippedTooLarge++;
|
|
380
|
-
}
|
|
381
|
-
function markTruncated(summary, reason) {
|
|
382
|
-
summary.truncated = true;
|
|
383
|
-
summary.stoppedReason = reason;
|
|
384
|
-
}
|
|
385
|
-
function createScanSummary() {
|
|
386
|
-
return {
|
|
387
|
-
filesScanned: 0,
|
|
388
|
-
filesMatched: 0,
|
|
389
|
-
skippedTooLarge: 0,
|
|
390
|
-
skippedBinary: 0,
|
|
391
|
-
skippedInaccessible: 0,
|
|
392
|
-
truncated: false,
|
|
393
|
-
stoppedReason: undefined,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
function buildSearchContentResult(root, pattern, filePattern, matches, summary) {
|
|
397
|
-
const baseSummary = {
|
|
398
|
-
filesScanned: summary.filesScanned,
|
|
399
|
-
filesMatched: summary.filesMatched,
|
|
400
|
-
matches: matches.length,
|
|
401
|
-
truncated: summary.truncated,
|
|
402
|
-
skippedTooLarge: summary.skippedTooLarge,
|
|
403
|
-
skippedBinary: summary.skippedBinary,
|
|
404
|
-
skippedInaccessible: summary.skippedInaccessible,
|
|
405
|
-
};
|
|
406
|
-
return {
|
|
407
|
-
basePath: root,
|
|
408
|
-
pattern,
|
|
409
|
-
filePattern,
|
|
410
|
-
matches,
|
|
411
|
-
summary: withOptionalStoppedReason(baseSummary, summary.stoppedReason),
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
const isSourceContext = import.meta.url.endsWith('.ts');
|
|
415
|
-
const WORKER_SCRIPT_URL = new URL(import.meta.url);
|
|
416
|
-
const hasWorkerScript = true;
|
|
417
|
-
class SearchWorkerTaskResource extends AsyncResource {
|
|
418
|
-
#settled = false;
|
|
419
|
-
constructor() {
|
|
420
|
-
super(SEARCH_WORKER_RESOURCE_TYPE);
|
|
421
|
-
}
|
|
422
|
-
resolve(resolver, result) {
|
|
423
|
-
this.finish(resolver, result);
|
|
424
|
-
}
|
|
425
|
-
reject(rejector, error) {
|
|
426
|
-
this.finish(rejector, error);
|
|
427
|
-
}
|
|
428
|
-
finish(callback, value) {
|
|
429
|
-
if (this.#settled) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
this.#settled = true;
|
|
433
|
-
this.runInAsyncScope(callback, undefined, value);
|
|
434
|
-
this.emitDestroy();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
class SearchWorkerPool {
|
|
438
|
-
size;
|
|
439
|
-
debug;
|
|
440
|
-
workers;
|
|
441
|
-
pending = new Map();
|
|
442
|
-
nextRequestId = 0;
|
|
443
|
-
closed = false;
|
|
444
|
-
workerRoundRobin = 0;
|
|
445
|
-
constructor(size, debug) {
|
|
446
|
-
this.size = size;
|
|
447
|
-
this.debug = debug;
|
|
448
|
-
if (size <= 0)
|
|
449
|
-
throw new Error('Pool size must be positive');
|
|
450
|
-
this.workers = Array.from({ length: size }, () => undefined);
|
|
451
|
-
}
|
|
452
|
-
normalizeWorkerError(error, fallbackMessage) {
|
|
453
|
-
if (error instanceof Error) {
|
|
454
|
-
return error;
|
|
455
|
-
}
|
|
456
|
-
return new Error(`${fallbackMessage}: ${formatUnknownErrorMessage(error)}`);
|
|
457
|
-
}
|
|
458
|
-
rejectPendingForWorker(workerIndex, error) {
|
|
459
|
-
for (const [id, pendingRequest] of this.pending) {
|
|
460
|
-
if (pendingRequest.workerIndex !== workerIndex) {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
this.pending.delete(id);
|
|
464
|
-
pendingRequest.reject(error);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
markWorkerAsUnavailable(workerIndex, expectedWorker) {
|
|
468
|
-
if (this.closed)
|
|
469
|
-
return;
|
|
470
|
-
if (this.workers[workerIndex] !== expectedWorker)
|
|
471
|
-
return;
|
|
472
|
-
this.workers[workerIndex] = undefined;
|
|
473
|
-
}
|
|
474
|
-
getWorker(workerIndex) {
|
|
475
|
-
const existing = this.workers[workerIndex];
|
|
476
|
-
if (existing)
|
|
477
|
-
return existing;
|
|
478
|
-
const worker = this.initWorker(workerIndex);
|
|
479
|
-
this.workers[workerIndex] = worker;
|
|
480
|
-
return worker;
|
|
481
|
-
}
|
|
482
|
-
initWorker(index) {
|
|
483
|
-
const worker = new Worker(WORKER_SCRIPT_URL, {
|
|
484
|
-
name: `${SEARCH_WORKER_NAME_PREFIX}-${String(index)}`,
|
|
485
|
-
workerData: { debug: this.debug },
|
|
486
|
-
execArgv: isSourceContext ? ['--import', 'tsx/esm'] : undefined,
|
|
487
|
-
});
|
|
488
|
-
worker.on('message', (msg) => {
|
|
489
|
-
const p = this.pending.get(msg.id);
|
|
490
|
-
if (!p)
|
|
491
|
-
return;
|
|
492
|
-
this.pending.delete(msg.id);
|
|
493
|
-
if (msg.type === 'result')
|
|
494
|
-
p.resolve(msg.result);
|
|
495
|
-
else
|
|
496
|
-
p.reject(new Error(msg.error));
|
|
497
|
-
});
|
|
498
|
-
worker.on('messageerror', (error) => {
|
|
499
|
-
const normalized = this.normalizeWorkerError(error, `Worker ${String(index)} failed to deserialize a message`);
|
|
500
|
-
this.rejectPendingForWorker(index, normalized);
|
|
501
|
-
this.markWorkerAsUnavailable(index, worker);
|
|
502
|
-
});
|
|
503
|
-
worker.on('error', (error) => {
|
|
504
|
-
this.rejectPendingForWorker(index, error);
|
|
505
|
-
this.markWorkerAsUnavailable(index, worker);
|
|
506
|
-
});
|
|
507
|
-
worker.on('exit', (exitCode) => {
|
|
508
|
-
if (this.closed)
|
|
509
|
-
return;
|
|
510
|
-
this.rejectPendingForWorker(index, new Error(`Worker ${String(index)} exited with code ${String(exitCode)}`));
|
|
511
|
-
this.markWorkerAsUnavailable(index, worker);
|
|
512
|
-
});
|
|
513
|
-
worker.unref();
|
|
514
|
-
return worker;
|
|
515
|
-
}
|
|
516
|
-
scan(req) {
|
|
517
|
-
if (this.closed)
|
|
518
|
-
throw new Error(ERROR_WORKER_POOL_CLOSED);
|
|
519
|
-
const id = this.nextRequestId++;
|
|
520
|
-
let workerIndex = 0;
|
|
521
|
-
const workerPendingCounts = new Array(this.size).fill(0);
|
|
522
|
-
for (const p of this.pending.values()) {
|
|
523
|
-
const idx = p.workerIndex;
|
|
524
|
-
workerPendingCounts[idx] = (workerPendingCounts[idx] ?? 0) + 1;
|
|
525
|
-
}
|
|
526
|
-
let minPending = workerPendingCounts[0] ?? 0;
|
|
527
|
-
for (let i = 1; i < this.size; i++) {
|
|
528
|
-
const pendingCount = workerPendingCounts[i] ?? 0;
|
|
529
|
-
if (pendingCount < minPending) {
|
|
530
|
-
minPending = pendingCount;
|
|
531
|
-
workerIndex = i;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
const worker = this.getWorker(workerIndex);
|
|
535
|
-
const promise = new Promise((resolve, reject) => {
|
|
536
|
-
const resource = new SearchWorkerTaskResource();
|
|
537
|
-
const pendingRequest = {
|
|
538
|
-
resolve: (result) => {
|
|
539
|
-
resource.resolve(resolve, result);
|
|
540
|
-
},
|
|
541
|
-
reject: (error) => {
|
|
542
|
-
resource.reject(reject, error);
|
|
543
|
-
},
|
|
544
|
-
workerIndex,
|
|
545
|
-
};
|
|
546
|
-
this.pending.set(id, pendingRequest);
|
|
547
|
-
try {
|
|
548
|
-
worker.postMessage({ type: 'scan', id, ...req });
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
this.pending.delete(id);
|
|
552
|
-
pendingRequest.reject(this.normalizeWorkerError(error, `Failed to post scan request ${String(id)} to worker ${String(workerIndex)}`));
|
|
553
|
-
this.markWorkerAsUnavailable(workerIndex, worker);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
return {
|
|
557
|
-
id,
|
|
558
|
-
promise,
|
|
559
|
-
cancel: () => {
|
|
560
|
-
const entry = this.pending.get(id);
|
|
561
|
-
if (entry) {
|
|
562
|
-
this.pending.delete(id);
|
|
563
|
-
try {
|
|
564
|
-
worker.postMessage({ type: 'cancel', id });
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
// Worker may already be terminating
|
|
568
|
-
}
|
|
569
|
-
entry.reject(new Error(ERROR_SCAN_CANCELLED));
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
async close() {
|
|
575
|
-
this.closed = true;
|
|
576
|
-
for (const p of this.pending.values())
|
|
577
|
-
p.reject(new Error(ERROR_WORKER_POOL_CLOSED));
|
|
578
|
-
this.pending.clear();
|
|
579
|
-
const terminations = [];
|
|
580
|
-
for (let index = 0; index < this.workers.length; index += 1) {
|
|
581
|
-
const worker = this.workers[index];
|
|
582
|
-
if (!worker)
|
|
583
|
-
continue;
|
|
584
|
-
terminations.push(worker.terminate());
|
|
585
|
-
this.workers[index] = undefined;
|
|
586
|
-
}
|
|
587
|
-
await Promise.all(terminations);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function isWorkerPoolAvailable() {
|
|
591
|
-
return !isSourceContext && hasWorkerScript;
|
|
592
|
-
}
|
|
593
|
-
function shouldUseWorkers() {
|
|
594
|
-
return isWorkerPoolAvailable() && SEARCH_WORKERS >= 2;
|
|
595
|
-
}
|
|
596
|
-
let poolInstance = null;
|
|
597
|
-
function getPool() {
|
|
598
|
-
if (!poolInstance) {
|
|
599
|
-
const debug = process.env['FS_CONTEXT_SEARCH_WORKERS_DEBUG'] === '1';
|
|
600
|
-
poolInstance = new SearchWorkerPool(SEARCH_WORKERS, debug);
|
|
601
|
-
}
|
|
602
|
-
return poolInstance;
|
|
603
|
-
}
|
|
604
|
-
// --- Execution Strategies ---
|
|
605
|
-
async function executeSequential(files, pattern, opts, signal, summary) {
|
|
606
|
-
const matches = [];
|
|
607
|
-
const matcher = buildMatcher(pattern, buildMatcherOptions(opts));
|
|
608
|
-
const scanOpts = buildScanFileOptions(opts);
|
|
609
|
-
for await (const file of files) {
|
|
610
|
-
if (signal.aborted) {
|
|
611
|
-
markTruncated(summary, 'timeout');
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
if (matches.length >= opts.maxResults) {
|
|
615
|
-
markTruncated(summary, 'maxResults');
|
|
616
|
-
break;
|
|
617
|
-
}
|
|
618
|
-
try {
|
|
619
|
-
assertAllowedFileAccess(file.requestedPath, file.resolvedPath);
|
|
620
|
-
const remaining = getRemainingMatchCapacity(opts.maxResults, matches.length);
|
|
621
|
-
const result = await scanFileResolved(file.resolvedPath, file.requestedPath, matcher, scanOpts, signal, remaining);
|
|
622
|
-
applyScanOutcome(summary, result);
|
|
623
|
-
matches.push(...result.matches);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
// Ignore access errors during mass scan
|
|
627
|
-
summary.skippedInaccessible++;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return matches;
|
|
631
|
-
}
|
|
632
|
-
async function fillWorkerPool(context) {
|
|
633
|
-
const { pool, pending, iterator, pattern, matcherOpts, scanOpts, maxResults, currentMatches, summary, } = context;
|
|
634
|
-
while (pending.size < SEARCH_WORKERS) {
|
|
635
|
-
const result = await iterator.next();
|
|
636
|
-
if (result.done)
|
|
637
|
-
return true;
|
|
638
|
-
try {
|
|
639
|
-
const remaining = getRemainingMatchCapacity(maxResults, currentMatches, 1);
|
|
640
|
-
const task = pool.scan({
|
|
641
|
-
resolvedPath: result.value.resolvedPath,
|
|
642
|
-
requestedPath: result.value.requestedPath,
|
|
643
|
-
pattern,
|
|
644
|
-
matcherOptions: matcherOpts,
|
|
645
|
-
scanOptions: scanOpts,
|
|
646
|
-
maxMatches: remaining,
|
|
647
|
-
});
|
|
648
|
-
pending.add(task);
|
|
649
|
-
}
|
|
650
|
-
catch {
|
|
651
|
-
summary.skippedInaccessible++;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
656
|
-
function processScanResult(winner, summary, matches, maxResults) {
|
|
657
|
-
if (winner.error) {
|
|
658
|
-
if (winner.error.message !== ERROR_SCAN_CANCELLED) {
|
|
659
|
-
summary.skippedInaccessible++;
|
|
660
|
-
}
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (winner.result) {
|
|
664
|
-
const res = winner.result;
|
|
665
|
-
applyScanOutcome(summary, res);
|
|
666
|
-
const remaining = getRemainingMatchCapacity(maxResults, matches.length);
|
|
667
|
-
if (remaining > 0 && res.matches.length > 0) {
|
|
668
|
-
const take = Math.min(remaining, res.matches.length);
|
|
669
|
-
for (let index = 0; index < take; index += 1) {
|
|
670
|
-
const match = res.matches[index];
|
|
671
|
-
if (match)
|
|
672
|
-
matches.push(match);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
async function waitForWinner(pending) {
|
|
678
|
-
const raceCandidates = [];
|
|
679
|
-
for (const task of pending) {
|
|
680
|
-
task.racePromise ??= task.promise.then((result) => ({ task, result, error: undefined }), (err) => ({
|
|
681
|
-
task,
|
|
682
|
-
result: undefined,
|
|
683
|
-
error: err instanceof Error
|
|
684
|
-
? err
|
|
685
|
-
: new Error(formatUnknownErrorMessage(err)),
|
|
686
|
-
}));
|
|
687
|
-
raceCandidates.push(task.racePromise);
|
|
688
|
-
}
|
|
689
|
-
return Promise.race(raceCandidates);
|
|
690
|
-
}
|
|
691
|
-
function updateParallelTruncation(summary, signal, matchesLength, maxResults) {
|
|
692
|
-
if (signal.aborted) {
|
|
693
|
-
markTruncated(summary, 'timeout');
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
if (matchesLength >= maxResults) {
|
|
697
|
-
markTruncated(summary, 'maxResults');
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
async function executeParallel(files, pattern, opts, signal, summary) {
|
|
701
|
-
const pool = getPool();
|
|
702
|
-
const matches = [];
|
|
703
|
-
const scanOpts = buildScanFileOptions(opts);
|
|
704
|
-
const matcherOpts = buildMatcherOptions(opts);
|
|
705
|
-
const pending = new Set();
|
|
706
|
-
const iterator = files[Symbol.asyncIterator]();
|
|
707
|
-
let exhausted = false;
|
|
708
|
-
const onAbort = () => {
|
|
709
|
-
markTruncated(summary, 'timeout');
|
|
710
|
-
for (const t of pending)
|
|
711
|
-
t.cancel();
|
|
712
|
-
};
|
|
713
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
714
|
-
try {
|
|
715
|
-
for (;;) {
|
|
716
|
-
if (signal.aborted || matches.length >= opts.maxResults) {
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
if (!exhausted) {
|
|
720
|
-
exhausted = await fillWorkerPool({
|
|
721
|
-
pool,
|
|
722
|
-
pending,
|
|
723
|
-
iterator,
|
|
724
|
-
pattern,
|
|
725
|
-
matcherOpts,
|
|
726
|
-
scanOpts,
|
|
727
|
-
maxResults: opts.maxResults,
|
|
728
|
-
currentMatches: matches.length,
|
|
729
|
-
summary,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
if (pending.size === 0 && exhausted) {
|
|
733
|
-
break;
|
|
734
|
-
}
|
|
735
|
-
// Wait for at least one
|
|
736
|
-
const winner = await waitForWinner(pending);
|
|
737
|
-
pending.delete(winner.task);
|
|
738
|
-
processScanResult(winner, summary, matches, opts.maxResults);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
finally {
|
|
742
|
-
signal.removeEventListener('abort', onAbort);
|
|
743
|
-
// Cancel remaining
|
|
744
|
-
for (const t of pending)
|
|
745
|
-
t.cancel();
|
|
746
|
-
if (iterator.return)
|
|
747
|
-
await iterator.return();
|
|
748
|
-
}
|
|
749
|
-
updateParallelTruncation(summary, signal, matches.length, opts.maxResults);
|
|
750
|
-
return matches;
|
|
751
|
-
}
|
|
752
|
-
// --- Entry Points ---
|
|
753
|
-
async function scanFileInWorker(resolvedPath, requestedPath, matcher, options, maxMatches, isCancelled, isBinaryDetector) {
|
|
754
|
-
// Direct scan used by worker script
|
|
755
|
-
const res = await scanFileResolved(resolvedPath, requestedPath, matcher, options, undefined, maxMatches, isBinaryDetector);
|
|
756
|
-
return {
|
|
757
|
-
matches: res.matches,
|
|
758
|
-
matched: res.matched,
|
|
759
|
-
skippedBinary: res.skippedBinary,
|
|
760
|
-
skippedTooLarge: res.skippedTooLarge,
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
async function searchSingleFile(details, opts, pattern, signal) {
|
|
764
|
-
const summary = createScanSummary();
|
|
765
|
-
summary.filesScanned = 1;
|
|
766
|
-
const matcher = buildMatcher(pattern, opts);
|
|
767
|
-
const result = await scanFileResolved(details.resolvedPath, details.requestedPath, matcher, {
|
|
768
|
-
...buildScanFileOptions(opts),
|
|
769
|
-
}, signal, opts.maxResults);
|
|
770
|
-
if (result.matched)
|
|
771
|
-
summary.filesMatched = 1;
|
|
772
|
-
return buildSearchContentResult(dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
|
|
773
|
-
}
|
|
774
|
-
async function searchDirectory(details, opts, pattern, signal, onProgress) {
|
|
775
|
-
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
776
|
-
const rootDirectories = [root];
|
|
777
|
-
const stream = globEntries(buildGlobOptions({
|
|
778
|
-
cwd: root,
|
|
779
|
-
pattern: opts.filePattern,
|
|
780
|
-
excludePatterns: opts.excludePatterns,
|
|
781
|
-
includeHidden: opts.includeHidden,
|
|
782
|
-
baseNameMatch: opts.baseNameMatch,
|
|
783
|
-
caseSensitiveMatch: opts.caseSensitiveFileMatch,
|
|
784
|
-
followSymbolicLinks: false,
|
|
785
|
-
onlyFiles: true,
|
|
786
|
-
stats: false,
|
|
787
|
-
suppressErrors: true,
|
|
788
|
-
}));
|
|
789
|
-
const summary = createScanSummary();
|
|
790
|
-
async function* fileGenerator() {
|
|
791
|
-
for await (const entry of stream) {
|
|
792
|
-
if (signal.aborted)
|
|
793
|
-
break;
|
|
794
|
-
if (summary.filesScanned >= opts.maxFilesScanned)
|
|
795
|
-
break;
|
|
796
|
-
if (!entry.dirent.isFile())
|
|
797
|
-
continue;
|
|
798
|
-
const normalized = normalizePath(entry.path);
|
|
799
|
-
if (!isPathWithinDirectories(normalized, rootDirectories))
|
|
800
|
-
continue;
|
|
801
|
-
if (isSensitivePath(entry.path, normalized))
|
|
802
|
-
continue;
|
|
803
|
-
summary.filesScanned++;
|
|
804
|
-
onProgress?.({
|
|
805
|
-
current: summary.filesScanned,
|
|
806
|
-
total: opts.maxFilesScanned,
|
|
807
|
-
});
|
|
808
|
-
yield { resolvedPath: normalized, requestedPath: entry.path };
|
|
809
|
-
}
|
|
810
|
-
if (summary.filesScanned >= opts.maxFilesScanned) {
|
|
811
|
-
summary.truncated = true;
|
|
812
|
-
summary.stoppedReason = 'maxFiles';
|
|
813
|
-
}
|
|
814
|
-
onProgress?.({
|
|
815
|
-
current: summary.filesScanned,
|
|
816
|
-
total: opts.maxFilesScanned,
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
const matches = shouldUseWorkers()
|
|
820
|
-
? await executeParallel(fileGenerator(), pattern, opts, signal, summary)
|
|
821
|
-
: await executeSequential(fileGenerator(), pattern, opts, signal, summary);
|
|
822
|
-
return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
|
|
823
|
-
}
|
|
824
|
-
function buildTimeoutSearchResult(basePath, pattern, filePattern) {
|
|
825
|
-
const timeoutSummary = createScanSummary();
|
|
826
|
-
markTruncated(timeoutSummary, 'timeout');
|
|
827
|
-
return buildSearchContentResult(basePath, pattern, filePattern, [], timeoutSummary);
|
|
828
|
-
}
|
|
829
|
-
export async function searchContent(basePath, pattern, options = {}) {
|
|
830
|
-
if (!basePath.trim())
|
|
831
|
-
throw new McpError(ErrorCode.INVALID_INPUT, 'basePath required');
|
|
832
|
-
if (typeof pattern !== 'string')
|
|
833
|
-
throw new McpError(ErrorCode.INVALID_INPUT, 'pattern required');
|
|
834
|
-
const opts = resolveOptions(options);
|
|
835
|
-
try {
|
|
836
|
-
return await withTimedAbortSignal(options.signal, opts.timeoutMs, async (signal) => {
|
|
837
|
-
const details = await validateExistingPathDetailed(basePath, signal);
|
|
838
|
-
const stats = await withAbort(stat(details.resolvedPath), signal);
|
|
839
|
-
if (stats.isFile()) {
|
|
840
|
-
return searchSingleFile(details, opts, pattern, signal);
|
|
841
|
-
}
|
|
842
|
-
if (!stats.isDirectory()) {
|
|
843
|
-
throw new McpError(ErrorCode.INVALID_INPUT, 'Path must be file or directory', basePath);
|
|
844
|
-
}
|
|
845
|
-
return searchDirectory(details, opts, pattern, signal, options.onProgress);
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
catch (error) {
|
|
849
|
-
if (isTimeoutLikeError(error)) {
|
|
850
|
-
return buildTimeoutSearchResult(basePath, pattern, opts.filePattern);
|
|
851
|
-
}
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Internal default for find tool - not exposed to MCP users
|
|
856
|
-
const SEARCH_FILES_MAX_RESULTS = 1000;
|
|
857
|
-
function normalizeSearchFilesOptions(options) {
|
|
858
|
-
const normalized = {
|
|
859
|
-
maxResults: options.maxResults ?? SEARCH_FILES_MAX_RESULTS,
|
|
860
|
-
sortBy: options.sortBy ?? 'path',
|
|
861
|
-
maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
|
|
862
|
-
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
863
|
-
baseNameMatch: options.baseNameMatch ?? false,
|
|
864
|
-
skipSymlinks: options.skipSymlinks ?? true,
|
|
865
|
-
includeHidden: options.includeHidden ?? false,
|
|
866
|
-
respectGitignore: options.respectGitignore ?? false,
|
|
867
|
-
};
|
|
868
|
-
if (options.maxDepth !== undefined) {
|
|
869
|
-
normalized.maxDepth = options.maxDepth;
|
|
870
|
-
}
|
|
871
|
-
return normalized;
|
|
872
|
-
}
|
|
873
|
-
function buildSearchFilesResult(entry, entryType, needsStats) {
|
|
874
|
-
let resolvedType = 'other';
|
|
875
|
-
if (entryType === 'directory') {
|
|
876
|
-
resolvedType = 'directory';
|
|
877
|
-
}
|
|
878
|
-
else if (entryType === 'file') {
|
|
879
|
-
resolvedType = 'file';
|
|
880
|
-
}
|
|
881
|
-
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
882
|
-
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
883
|
-
return {
|
|
884
|
-
path: entry.path,
|
|
885
|
-
type: resolvedType,
|
|
886
|
-
...(size !== undefined ? { size } : {}),
|
|
887
|
-
...(modified !== undefined ? { modified } : {}),
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
function shouldStopCollecting(state, normalized, signal) {
|
|
891
|
-
const stopReason = resolveStopReason({
|
|
892
|
-
signal,
|
|
893
|
-
current: state.filesScanned,
|
|
894
|
-
max: normalized.maxFilesScanned,
|
|
895
|
-
abortedReason: 'timeout',
|
|
896
|
-
maxReason: 'maxFiles',
|
|
897
|
-
});
|
|
898
|
-
if (stopReason !== undefined) {
|
|
899
|
-
state.truncated = true;
|
|
900
|
-
state.stoppedReason = stopReason;
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
function shouldIncludeEntry(entryType, normalized) {
|
|
906
|
-
return !normalized.skipSymlinks || entryType !== 'symlink';
|
|
907
|
-
}
|
|
908
|
-
function createCollectState() {
|
|
909
|
-
return {
|
|
910
|
-
results: [],
|
|
911
|
-
filesScanned: 0,
|
|
912
|
-
truncated: false,
|
|
913
|
-
skippedInaccessible: 0,
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
|
|
917
|
-
const options = buildGlobOptions({
|
|
918
|
-
cwd: root,
|
|
919
|
-
pattern,
|
|
920
|
-
excludePatterns,
|
|
921
|
-
includeHidden: normalized.includeHidden,
|
|
922
|
-
baseNameMatch: normalized.baseNameMatch,
|
|
923
|
-
caseSensitiveMatch: true,
|
|
924
|
-
followSymbolicLinks: false,
|
|
925
|
-
onlyFiles: true,
|
|
926
|
-
stats: needsStats,
|
|
927
|
-
...(normalized.maxDepth !== undefined
|
|
928
|
-
? { maxDepth: normalized.maxDepth }
|
|
929
|
-
: {}),
|
|
930
|
-
});
|
|
931
|
-
return globEntries(options);
|
|
932
|
-
}
|
|
933
|
-
function buildCollectResult(state) {
|
|
934
|
-
const outcome = {
|
|
935
|
-
results: state.results,
|
|
936
|
-
filesScanned: state.filesScanned,
|
|
937
|
-
truncated: state.truncated,
|
|
938
|
-
skippedInaccessible: state.skippedInaccessible,
|
|
939
|
-
};
|
|
940
|
-
if (state.stoppedReason !== undefined) {
|
|
941
|
-
outcome.stoppedReason = state.stoppedReason;
|
|
942
|
-
}
|
|
943
|
-
return outcome;
|
|
944
|
-
}
|
|
945
|
-
function handleEntry(entry, entryType, needsStats, normalized, state) {
|
|
946
|
-
state.results.push(buildSearchFilesResult(entry, entryType, needsStats));
|
|
947
|
-
if (state.results.length >= normalized.maxResults) {
|
|
948
|
-
state.truncated = true;
|
|
949
|
-
state.stoppedReason = 'maxResults';
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
async function collectFromStream(stream, signal, context) {
|
|
953
|
-
const { root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, accessDeps, onProgress, } = context;
|
|
954
|
-
for await (const entry of stream) {
|
|
955
|
-
if (shouldStopCollecting(state, normalized, signal))
|
|
956
|
-
break;
|
|
957
|
-
state.filesScanned++;
|
|
958
|
-
onProgress?.({
|
|
959
|
-
current: state.filesScanned,
|
|
960
|
-
total: normalized.maxFilesScanned,
|
|
961
|
-
});
|
|
962
|
-
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const entryType = resolveEntryType(entry.dirent);
|
|
966
|
-
if (!shouldIncludeEntry(entryType, normalized)) {
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
|
|
970
|
-
if (!isAccessible) {
|
|
971
|
-
state.skippedInaccessible++;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
handleEntry(entry, entryType, needsStats, normalized, state);
|
|
975
|
-
if (state.truncated)
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
onProgress?.({
|
|
979
|
-
current: state.filesScanned,
|
|
980
|
-
total: normalized.maxFilesScanned,
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
984
|
-
if (!matcher)
|
|
985
|
-
return false;
|
|
986
|
-
return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
|
|
987
|
-
}
|
|
988
|
-
async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
989
|
-
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
990
|
-
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
991
|
-
const state = createCollectState();
|
|
992
|
-
const rootDirectories = [root];
|
|
993
|
-
const accessDeps = {
|
|
994
|
-
normalizePath,
|
|
995
|
-
isPathWithinDirectories,
|
|
996
|
-
isSensitivePath,
|
|
997
|
-
validateSymlinkPath: validateExistingPathDetailed,
|
|
998
|
-
};
|
|
999
|
-
const gitignoreMatcher = normalized.respectGitignore
|
|
1000
|
-
? await loadRootGitignore(root, signal)
|
|
1001
|
-
: null;
|
|
1002
|
-
await collectFromStream(stream, signal, {
|
|
1003
|
-
root,
|
|
1004
|
-
rootDirectories,
|
|
1005
|
-
gitignoreMatcher,
|
|
1006
|
-
normalized,
|
|
1007
|
-
needsStats,
|
|
1008
|
-
state,
|
|
1009
|
-
accessDeps,
|
|
1010
|
-
...(onProgress ? { onProgress } : {}),
|
|
1011
|
-
});
|
|
1012
|
-
return buildCollectResult(state);
|
|
1013
|
-
}
|
|
1014
|
-
function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
|
|
1015
|
-
const summary = {
|
|
1016
|
-
matched: results.length,
|
|
1017
|
-
truncated,
|
|
1018
|
-
skippedInaccessible,
|
|
1019
|
-
filesScanned,
|
|
1020
|
-
};
|
|
1021
|
-
return withOptionalStoppedReason(summary, stoppedReason);
|
|
1022
|
-
}
|
|
1023
|
-
function compareNameThenPath(a, b) {
|
|
1024
|
-
const nameCompare = compareStringValues(a.name, b.name);
|
|
1025
|
-
if (nameCompare !== 0)
|
|
1026
|
-
return nameCompare;
|
|
1027
|
-
return compareStringValues(a.path, b.path);
|
|
1028
|
-
}
|
|
1029
|
-
function comparePathThenName(a, b) {
|
|
1030
|
-
const pathCompare = compareStringValues(a.path, b.path);
|
|
1031
|
-
if (pathCompare !== 0)
|
|
1032
|
-
return pathCompare;
|
|
1033
|
-
return compareStringValues(a.name, b.name);
|
|
1034
|
-
}
|
|
1035
|
-
const SORT_COMPARATORS = {
|
|
1036
|
-
size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
|
|
1037
|
-
modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
|
|
1038
|
-
path: (a, b) => comparePathThenName(a, b),
|
|
1039
|
-
name: (a, b) => compareNameThenPath(a, b),
|
|
1040
|
-
};
|
|
1041
|
-
function sortSearchResults(results, sortBy) {
|
|
1042
|
-
if (sortBy === 'name') {
|
|
1043
|
-
stableSortByDerivedString(results, (item) => basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
const comparator = SORT_COMPARATORS[sortBy];
|
|
1047
|
-
results.sort(comparator);
|
|
1048
|
-
}
|
|
1049
|
-
async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
1050
|
-
const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
|
|
1051
|
-
sortSearchResults(results, normalized.sortBy);
|
|
1052
|
-
return {
|
|
1053
|
-
results,
|
|
1054
|
-
summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
|
|
1058
|
-
const normalized = normalizeSearchFilesOptions(options);
|
|
1059
|
-
return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
|
|
1060
|
-
const root = await validateExistingDirectory(basePath, signal);
|
|
1061
|
-
const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
|
|
1062
|
-
return {
|
|
1063
|
-
basePath: root,
|
|
1064
|
-
pattern,
|
|
1065
|
-
results,
|
|
1066
|
-
summary,
|
|
1067
|
-
};
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
const matcherCache = new Map();
|
|
1071
|
-
const MAX_MATCHER_CACHE_SIZE = 100;
|
|
1072
|
-
function getMatcherCacheKey(pattern, options) {
|
|
1073
|
-
const cs = options.caseSensitive ? '1' : '0';
|
|
1074
|
-
const ww = options.wholeWord ? '1' : '0';
|
|
1075
|
-
const lit = options.isLiteral ? '1' : '0';
|
|
1076
|
-
return `${pattern}|${cs}|${ww}|${lit}`;
|
|
1077
|
-
}
|
|
1078
|
-
function getCachedMatcher(pattern, options) {
|
|
1079
|
-
const key = getMatcherCacheKey(pattern, options);
|
|
1080
|
-
const cached = matcherCache.get(key);
|
|
1081
|
-
if (cached) {
|
|
1082
|
-
refreshMatcherCacheEntry(key, cached);
|
|
1083
|
-
return cached;
|
|
1084
|
-
}
|
|
1085
|
-
const matcher = buildMatcher(pattern, options);
|
|
1086
|
-
refreshMatcherCacheEntry(key, matcher);
|
|
1087
|
-
evictOldestMatcherIfNeeded();
|
|
1088
|
-
return matcher;
|
|
1089
|
-
}
|
|
1090
|
-
function refreshMatcherCacheEntry(key, matcher) {
|
|
1091
|
-
matcherCache.delete(key);
|
|
1092
|
-
matcherCache.set(key, matcher);
|
|
1093
|
-
}
|
|
1094
|
-
function evictOldestMatcherIfNeeded() {
|
|
1095
|
-
if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
|
|
1096
|
-
return;
|
|
1097
|
-
const firstKey = matcherCache.keys().next().value;
|
|
1098
|
-
if (firstKey !== undefined) {
|
|
1099
|
-
matcherCache.delete(firstKey);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const cancelledRequests = new Set();
|
|
1103
|
-
const activeRequests = new Set();
|
|
1104
|
-
let shuttingDown = false;
|
|
1105
|
-
function maybeFinishShutdown() {
|
|
1106
|
-
if (!shuttingDown)
|
|
1107
|
-
return;
|
|
1108
|
-
if (activeRequests.size > 0)
|
|
1109
|
-
return;
|
|
1110
|
-
parentPort?.close();
|
|
1111
|
-
}
|
|
1112
|
-
function consumeCancelled(id) {
|
|
1113
|
-
if (!cancelledRequests.has(id)) {
|
|
1114
|
-
return false;
|
|
1115
|
-
}
|
|
1116
|
-
cancelledRequests.delete(id);
|
|
1117
|
-
return true;
|
|
1118
|
-
}
|
|
1119
|
-
function markCancelledIfActive(id) {
|
|
1120
|
-
if (activeRequests.has(id)) {
|
|
1121
|
-
cancelledRequests.add(id);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
function buildScanResponse(id, result) {
|
|
1125
|
-
return {
|
|
1126
|
-
type: 'result',
|
|
1127
|
-
id,
|
|
1128
|
-
result,
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
function buildErrorResponse(id, error) {
|
|
1132
|
-
return {
|
|
1133
|
-
type: 'error',
|
|
1134
|
-
id,
|
|
1135
|
-
error: formatUnknownErrorMessage(error),
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
async function handleScanRequest(request) {
|
|
1139
|
-
const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
|
|
1140
|
-
if (consumeCancelled(id))
|
|
1141
|
-
return;
|
|
1142
|
-
activeRequests.add(id);
|
|
1143
|
-
const endMeasure = startPerfMeasure('searchWorker.scan', {
|
|
1144
|
-
maxMatches,
|
|
1145
|
-
});
|
|
1146
|
-
let ok = false;
|
|
1147
|
-
try {
|
|
1148
|
-
const matcher = getCachedMatcher(pattern, matcherOptions);
|
|
1149
|
-
const isCancelled = () => cancelledRequests.has(id);
|
|
1150
|
-
const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
|
|
1151
|
-
if (consumeCancelled(id))
|
|
1152
|
-
return;
|
|
1153
|
-
parentPort?.postMessage(buildScanResponse(id, result));
|
|
1154
|
-
ok = true;
|
|
1155
|
-
}
|
|
1156
|
-
catch (err) {
|
|
1157
|
-
if (consumeCancelled(id))
|
|
1158
|
-
return;
|
|
1159
|
-
parentPort?.postMessage(buildErrorResponse(id, err));
|
|
1160
|
-
}
|
|
1161
|
-
finally {
|
|
1162
|
-
activeRequests.delete(id);
|
|
1163
|
-
cancelledRequests.delete(id);
|
|
1164
|
-
endMeasure?.(ok);
|
|
1165
|
-
maybeFinishShutdown();
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
function handleMessage(message) {
|
|
1169
|
-
switch (message.type) {
|
|
1170
|
-
case 'scan':
|
|
1171
|
-
if (shuttingDown)
|
|
1172
|
-
return;
|
|
1173
|
-
void handleScanRequest(message);
|
|
1174
|
-
break;
|
|
1175
|
-
case 'cancel':
|
|
1176
|
-
markCancelledIfActive(message.id);
|
|
1177
|
-
break;
|
|
1178
|
-
case 'shutdown':
|
|
1179
|
-
shuttingDown = true;
|
|
1180
|
-
for (const id of activeRequests) {
|
|
1181
|
-
markCancelledIfActive(id);
|
|
1182
|
-
}
|
|
1183
|
-
maybeFinishShutdown();
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
const log = debuglog('search-worker');
|
|
1188
|
-
if (parentPort) {
|
|
1189
|
-
parentPort.on('message', handleMessage);
|
|
1190
|
-
const data = workerData;
|
|
1191
|
-
if (data?.debug) {
|
|
1192
|
-
log(`Started with threadId=${String(threadId)}`);
|
|
1193
|
-
}
|
|
1194
|
-
}
|