@j0hanz/filesystem-mcp 1.19.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +19 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +11 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +38 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +109 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +345 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +26 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +66 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +12 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +71 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +142 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +110 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +394 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +330 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +442 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +291 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +398 -152
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +435 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +29 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +202 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -563
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -472
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -1,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
- }