@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
@@ -0,0 +1,248 @@
1
+ import { stat as fsStat, readFile } from 'node:fs/promises';
2
+ import { RE2 } from '@adguard/re2-wasm';
3
+ import { StopReasonTracker } from './concurrency.js';
4
+ import { globEntries } from './glob.js';
5
+ import { escapeRegexLiteral } from './primitives.js';
6
+ import { getMaxTextFileSize } from './util.js';
7
+ /** Occurrence-counting bound, so one pathological line cannot spin forever. */
8
+ const MAX_MATCHES_PER_LINE = 100_000;
9
+ /**
10
+ * Compile a pattern on RE2 rather than on V8's irregexp.
11
+ *
12
+ * Patterns arrive from the MCP client, so a backtracking engine would let one
13
+ * request pin the event loop with no way out: an abort signal cannot preempt a
14
+ * synchronous `exec`, and on stdio that wedges the whole server. RE2 matches in
15
+ * time linear in the input and cannot backtrack at all, so the hazard is gone
16
+ * rather than bounded.
17
+ *
18
+ * RE2 rejects the constructs the tool schema documents as unsupported —
19
+ * lookahead, lookbehind, backreferences — with its own {@link SyntaxError},
20
+ * which the tool layer turns into a normal tool error. It always matches in
21
+ * Unicode mode and requires the `u` flag to say so.
22
+ *
23
+ * Every compiled pattern owns memory in re2-wasm's fixed 16 MB heap, which
24
+ * `ALLOW_MEMORY_GROWTH` is off for. re2-wasm never frees it and a
25
+ * FinalizationRegistry does not keep up (V8 sees no pressure from the wasm
26
+ * heap), so exhaustion is an emscripten `abort()` that kills regex search for
27
+ * the rest of the process. Every caller MUST pass the result to
28
+ * {@link freeRegex} when it is done with it.
29
+ */
30
+ export function compileRegex(pattern, options = {}) {
31
+ const flags = options.caseSensitive ? 'gu' : 'giu';
32
+ try {
33
+ return new RE2(pattern, flags);
34
+ }
35
+ catch (error) {
36
+ if (!(error instanceof SyntaxError))
37
+ throw error;
38
+ throw new SyntaxError(`${error.message} — lookahead, lookbehind, and backreferences are not supported.`, { cause: error });
39
+ }
40
+ }
41
+ /**
42
+ * Release a compiled pattern's wasm memory. re2-wasm exposes no disposal of its
43
+ * own, so this reaches the embind handle it holds privately; a version that
44
+ * renames the field degrades to the pre-existing leak rather than throwing.
45
+ * Idempotent — a second call on an already-freed handle is swallowed. Using a
46
+ * {@link Regex} after freeing it is undefined behaviour in the wasm heap, so
47
+ * free only in a `finally` that owns the compile.
48
+ */
49
+ export function freeRegex(regex) {
50
+ const handle = regex?.wrapper;
51
+ try {
52
+ handle?.delete?.();
53
+ }
54
+ catch {
55
+ // already deleted, or a re2-wasm build without embind disposal
56
+ }
57
+ }
58
+ /**
59
+ * Count non-overlapping occurrences of a global regex in a single line. Guards
60
+ * zero-length matches (e.g. `a*`) so they cannot loop forever.
61
+ */
62
+ function countLineMatches(regex, line) {
63
+ regex.lastIndex = 0;
64
+ let count = 0;
65
+ let match;
66
+ while ((match = regex.exec(line)) !== null) {
67
+ count++;
68
+ if (match.index === regex.lastIndex)
69
+ regex.lastIndex++; // advance past zero-length match
70
+ if (count >= MAX_MATCHES_PER_LINE)
71
+ break;
72
+ }
73
+ return count;
74
+ }
75
+ /** Owns the compiled pattern's lifetime; the scan itself is `scanContent`. */
76
+ export async function searchContent(directory, pattern, options, pathGuard) {
77
+ const regexPattern = options.isRegex ? pattern || '' : escapeRegexLiteral(pattern || '');
78
+ const regex = compileRegex(regexPattern, { caseSensitive: Boolean(options.caseSensitive) });
79
+ try {
80
+ return await scanContent(directory, options, pathGuard, regex);
81
+ }
82
+ finally {
83
+ freeRegex(regex);
84
+ }
85
+ }
86
+ async function* guardedEntries(entries, pathGuard, signal, counters) {
87
+ // The signal carries both client cancellation and the tool's search timeout,
88
+ // so an abort means "return what we have, marked incomplete" rather than
89
+ // throw — but it must never be reported as a finished scan.
90
+ for await (const entry of entries) {
91
+ if (signal?.aborted) {
92
+ counters.stoppedByAbort = true;
93
+ return;
94
+ }
95
+ try {
96
+ await pathGuard.validateExistingPath(entry.path);
97
+ }
98
+ catch {
99
+ counters.skippedInaccessible++;
100
+ continue;
101
+ }
102
+ yield entry;
103
+ }
104
+ }
105
+ async function scanContent(directory, options, pathGuard, regex) {
106
+ const matches = [];
107
+ const maxResults = options.maxResults ?? 100;
108
+ const maxFileSize = options.maxFileSize ?? getMaxTextFileSize();
109
+ const entries = globEntries({
110
+ cwd: directory,
111
+ pattern: options.filePattern ?? '**/*',
112
+ excludePatterns: options.excludePatterns ?? [],
113
+ includeHidden: Boolean(options.includeHidden),
114
+ respectGitignore: Boolean(options.respectGitignore),
115
+ maxDepth: options.maxDepth ?? 100,
116
+ suppressErrors: true,
117
+ });
118
+ let filesScanned = 0;
119
+ let filesMatched = 0;
120
+ let matchingLines = 0;
121
+ let skippedTooLarge = 0;
122
+ const counters = { skippedInaccessible: 0, stoppedByAbort: false };
123
+ for await (const entry of guardedEntries(entries, pathGuard, options.signal, counters)) {
124
+ if (matches.length >= maxResults)
125
+ break;
126
+ // Skip oversized files before reading to avoid unbounded memory use. Count
127
+ // them: "no matches" for a reason other than the pattern must be visible.
128
+ try {
129
+ const stats = await fsStat(entry.path);
130
+ if (stats.size > maxFileSize) {
131
+ skippedTooLarge++;
132
+ continue;
133
+ }
134
+ }
135
+ catch {
136
+ counters.skippedInaccessible++;
137
+ continue;
138
+ }
139
+ filesScanned++;
140
+ try {
141
+ const content = await readFile(entry.path, { encoding: 'utf-8', signal: options.signal });
142
+ const lines = content.split('\n');
143
+ let matchedFile = false;
144
+ for (let i = 0; i < lines.length; i++) {
145
+ const line = lines[i];
146
+ if (line === undefined)
147
+ continue;
148
+ // One scan per line: countLineMatches resets lastIndex itself, so it
149
+ // doubles as the "does this line match" test.
150
+ const occurrences = countLineMatches(regex, line);
151
+ if (occurrences > 0) {
152
+ matchedFile = true;
153
+ matchingLines++;
154
+ matches.push({
155
+ file: entry.path,
156
+ line: i + 1,
157
+ content: line,
158
+ matchCount: occurrences,
159
+ });
160
+ if (matches.length >= maxResults)
161
+ break;
162
+ }
163
+ }
164
+ if (matchedFile)
165
+ filesMatched++;
166
+ }
167
+ catch {
168
+ // A read failure while the signal is aborted IS the abort, not an
169
+ // unreadable file — stop rather than spend another iteration and then
170
+ // report a cut-short scan as complete.
171
+ if (options.signal?.aborted) {
172
+ counters.stoppedByAbort = true;
173
+ break;
174
+ }
175
+ // ignore read errors (e.g. binary files)
176
+ }
177
+ }
178
+ const tracker = new StopReasonTracker();
179
+ if (matches.length >= maxResults)
180
+ tracker.hitMaxResults();
181
+ if (counters.stoppedByAbort)
182
+ tracker.hitAbort();
183
+ const stoppedReason = tracker.resolve();
184
+ return {
185
+ basePath: directory,
186
+ matches,
187
+ summary: {
188
+ matchingLines,
189
+ filesScanned,
190
+ filesMatched,
191
+ truncated: tracker.truncated,
192
+ skippedInaccessible: counters.skippedInaccessible,
193
+ skippedTooLarge,
194
+ ...(stoppedReason ? { stoppedReason } : {}),
195
+ },
196
+ };
197
+ }
198
+ export async function searchFiles(directory, pattern, excludePatterns, options, pathGuard) {
199
+ const maxResults = options.maxResults ?? 100;
200
+ const entries = globEntries({
201
+ cwd: directory,
202
+ pattern,
203
+ excludePatterns,
204
+ includeHidden: Boolean(options.includeHidden),
205
+ respectGitignore: Boolean(options.respectGitignore),
206
+ maxDepth: options.maxDepth ?? 100,
207
+ suppressErrors: true,
208
+ });
209
+ const results = [];
210
+ let filesScanned = 0;
211
+ const counters = { skippedInaccessible: 0, stoppedByAbort: false };
212
+ for await (const entry of guardedEntries(entries, pathGuard, options.signal, counters)) {
213
+ if (results.length >= maxResults)
214
+ break;
215
+ filesScanned++;
216
+ results.push({ path: entry.path });
217
+ }
218
+ // Sorting — only name / path are supported; size / modified were removed
219
+ // (the glob never collected stats, so they were always undefined).
220
+ if (options.sortBy === 'name') {
221
+ results.sort((a, b) => {
222
+ const aName = a.path.split(/[/\\]/).pop() ?? '';
223
+ const bName = b.path.split(/[/\\]/).pop() ?? '';
224
+ return aName.localeCompare(bName);
225
+ });
226
+ }
227
+ else {
228
+ results.sort((a, b) => a.path.localeCompare(b.path));
229
+ }
230
+ const tracker = new StopReasonTracker();
231
+ if (results.length >= maxResults)
232
+ tracker.hitMaxResults();
233
+ if (counters.stoppedByAbort)
234
+ tracker.hitAbort();
235
+ const stoppedReason = tracker.resolve();
236
+ return {
237
+ basePath: directory,
238
+ results,
239
+ summary: {
240
+ matched: results.length,
241
+ filesScanned,
242
+ truncated: tracker.truncated,
243
+ skippedInaccessible: counters.skippedInaccessible,
244
+ ...(stoppedReason ? { stoppedReason } : {}),
245
+ },
246
+ };
247
+ }
248
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAExC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,WAAW,EAAkB,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAc/C,+EAA+E;AAC/E,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,UAA+B,EAAE;IAC7E,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACnD,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,WAAW,CAAC;YAAE,MAAM,KAAK,CAAC;QACjD,MAAM,IAAI,WAAW,CACnB,GAAG,KAAK,CAAC,OAAO,iEAAiE,EACjF,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAwB;IAChD,MAAM,MAAM,GAAI,KAAsE,EAAE,OAAO,CAAC;IAChG,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,KAAY,EAAE,IAAY;IAClD,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;IACpB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,KAA0B,CAAC;IAC/B,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3C,KAAK,EAAE,CAAC;QACR,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,SAAS;YAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,iCAAiC;QACzF,IAAI,KAAK,IAAI,oBAAoB;YAAE,MAAM;IAC3C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAoCD,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,SAAiB,EACjB,OAAe,EACf,OAA6B,EAC7B,SAAoB;IAEpB,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,YAAY,CAAC,YAAY,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IAC5F,IAAI,CAAC;QACH,OAAO,MAAM,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IACjE,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,SAAS,CAAC,CAAC,cAAc,CAC5B,OAAiC,EACjC,SAAoB,EACpB,MAA+B,EAC/B,QAAkE;IAElE,6EAA6E;IAC7E,yEAAyE;IACzE,4DAA4D;IAC5D,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAClC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,QAAQ,CAAC,cAAc,GAAG,IAAI,CAAC;YAC/B,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,mBAAmB,EAAE,CAAC;YAC/B,SAAS;QACX,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,SAAiB,EACjB,OAA6B,EAC7B,SAAoB,EACpB,KAAY;IAEZ,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;IAC7C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,EAAE,CAAC;IAEhE,MAAM,OAAO,GAAG,WAAW,CAAC;QAC1B,GAAG,EAAE,SAAS;QACd,OAAO,EAAE,OAAO,CAAC,WAAW,IAAI,MAAM;QACtC,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,EAAE;QAC9C,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7C,gBAAgB,EAAE,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC;QACnD,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG;QACjC,cAAc,EAAE,IAAI;KACrB,CAAC,CAAC;IAEH,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,QAAQ,GAAG,EAAE,mBAAmB,EAAE,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;IAEnE,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACvF,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU;YAAE,MAAM;QAExC,2EAA2E;QAC3E,0EAA0E;QAC1E,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,IAAI,GAAG,WAAW,EAAE,CAAC;gBAC7B,eAAe,EAAE,CAAC;gBAClB,SAAS;YACX,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,mBAAmB,EAAE,CAAC;YAC/B,SAAS;QACX,CAAC;QAED,YAAY,EAAE,CAAC;QAEf,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1F,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,WAAW,GAAG,KAAK,CAAC;YACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACtB,IAAI,IAAI,KAAK,SAAS;oBAAE,SAAS;gBACjC,qEAAqE;gBACrE,8CAA8C;gBAC9C,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBAClD,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;oBACpB,WAAW,GAAG,IAAI,CAAC;oBACnB,aAAa,EAAE,CAAC;oBAChB,OAAO,CAAC,IAAI,CAAC;wBACX,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,IAAI,EAAE,CAAC,GAAG,CAAC;wBACX,OAAO,EAAE,IAAI;wBACb,UAAU,EAAE,WAAW;qBACxB,CAAC,CAAC;oBACH,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU;wBAAE,MAAM;gBAC1C,CAAC;YACH,CAAC;YACD,IAAI,WAAW;gBAAE,YAAY,EAAE,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;YAClE,sEAAsE;YACtE,uCAAuC;YACvC,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC5B,QAAQ,CAAC,cAAc,GAAG,IAAI,CAAC;gBAC/B,MAAM;YACR,CAAC;YACD,yCAAyC;QAC3C,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAE,CAAC;IACxC,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU;QAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAC1D,IAAI,QAAQ,CAAC,cAAc;QAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IAChD,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAExC,OAAO;QACL,QAAQ,EAAE,SAAS;QACnB,OAAO;QACP,OAAO,EAAE;YACP,aAAa;YACb,YAAY;YACZ,YAAY;YACZ,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,mBAAmB,EAAE,QAAQ,CAAC,mBAAmB;YACjD,eAAe;YACf,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAAiB,EACjB,OAAe,EACf,eAAyB,EACzB,OAOC,EACD,SAAoB;IAYpB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;IAC7C,MAAM,OAAO,GAAG,WAAW,CAAC;QAC1B,GAAG,EAAE,SAAS;QACd,OAAO;QACP,eAAe;QACf,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7C,gBAAgB,EAAE,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC;QACnD,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG;QACjC,cAAc,EAAE,IAAI;KACrB,CAAC,CAAC;IACH,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,MAAM,QAAQ,GAAG,EAAE,mBAAmB,EAAE,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;IAEnE,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACvF,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU;YAAE,MAAM;QACxC,YAAY,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,yEAAyE;IACzE,mEAAmE;IACnE,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACpB,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAChD,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAE,CAAC;IACxC,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU;QAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAC1D,IAAI,QAAQ,CAAC,cAAc;QAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IAChD,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAExC,OAAO;QACL,QAAQ,EAAE,SAAS;QACnB,OAAO;QACP,OAAO,EAAE;YACP,OAAO,EAAE,OAAO,CAAC,MAAM;YACvB,YAAY;YACZ,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,mBAAmB,EAAE,QAAQ,CAAC,mBAAmB;YACjD,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { stat as fsStat, readFile } from 'node:fs/promises';\n\nimport type { RE2ExecArray } from '@adguard/re2-wasm';\nimport { RE2 } from '@adguard/re2-wasm';\n\nimport { StopReasonTracker } from './concurrency.js';\nimport type { StoppedReason } from './concurrency.js';\nimport { globEntries, type GlobEntry } from './glob.js';\nimport type { PathGuard } from './path.js';\nimport { escapeRegexLiteral } from './primitives.js';\nimport { getMaxTextFileSize } from './util.js';\n\ninterface SearchResult {\n file: string;\n line: number;\n content: string;\n matchCount?: number;\n}\n\nexport type Regex = RE2;\nexport interface RegexCompileOptions {\n caseSensitive?: boolean;\n}\n\n/** Occurrence-counting bound, so one pathological line cannot spin forever. */\nconst MAX_MATCHES_PER_LINE = 100_000;\n\n/**\n * Compile a pattern on RE2 rather than on V8's irregexp.\n *\n * Patterns arrive from the MCP client, so a backtracking engine would let one\n * request pin the event loop with no way out: an abort signal cannot preempt a\n * synchronous `exec`, and on stdio that wedges the whole server. RE2 matches in\n * time linear in the input and cannot backtrack at all, so the hazard is gone\n * rather than bounded.\n *\n * RE2 rejects the constructs the tool schema documents as unsupported —\n * lookahead, lookbehind, backreferences — with its own {@link SyntaxError},\n * which the tool layer turns into a normal tool error. It always matches in\n * Unicode mode and requires the `u` flag to say so.\n *\n * Every compiled pattern owns memory in re2-wasm's fixed 16 MB heap, which\n * `ALLOW_MEMORY_GROWTH` is off for. re2-wasm never frees it and a\n * FinalizationRegistry does not keep up (V8 sees no pressure from the wasm\n * heap), so exhaustion is an emscripten `abort()` that kills regex search for\n * the rest of the process. Every caller MUST pass the result to\n * {@link freeRegex} when it is done with it.\n */\nexport function compileRegex(pattern: string, options: RegexCompileOptions = {}): Regex {\n const flags = options.caseSensitive ? 'gu' : 'giu';\n try {\n return new RE2(pattern, flags);\n } catch (error) {\n if (!(error instanceof SyntaxError)) throw error;\n throw new SyntaxError(\n `${error.message} — lookahead, lookbehind, and backreferences are not supported.`,\n { cause: error },\n );\n }\n}\n\n/**\n * Release a compiled pattern's wasm memory. re2-wasm exposes no disposal of its\n * own, so this reaches the embind handle it holds privately; a version that\n * renames the field degrades to the pre-existing leak rather than throwing.\n * Idempotent — a second call on an already-freed handle is swallowed. Using a\n * {@link Regex} after freeing it is undefined behaviour in the wasm heap, so\n * free only in a `finally` that owns the compile.\n */\nexport function freeRegex(regex: Regex | undefined): void {\n const handle = (regex as unknown as { wrapper?: { delete?: () => void } } | undefined)?.wrapper;\n try {\n handle?.delete?.();\n } catch {\n // already deleted, or a re2-wasm build without embind disposal\n }\n}\n\n/**\n * Count non-overlapping occurrences of a global regex in a single line. Guards\n * zero-length matches (e.g. `a*`) so they cannot loop forever.\n */\nfunction countLineMatches(regex: Regex, line: string): number {\n regex.lastIndex = 0;\n let count = 0;\n let match: RE2ExecArray | null;\n while ((match = regex.exec(line)) !== null) {\n count++;\n if (match.index === regex.lastIndex) regex.lastIndex++; // advance past zero-length match\n if (count >= MAX_MATCHES_PER_LINE) break;\n }\n return count;\n}\n\nexport interface SearchContentOptions {\n caseSensitive?: boolean;\n isRegex?: boolean;\n maxResults?: number;\n filePattern?: string;\n excludePatterns?: string[];\n respectGitignore?: boolean;\n includeHidden?: boolean;\n maxDepth?: number;\n maxFileSize?: number;\n signal?: AbortSignal;\n}\n\nexport interface SearchContentOutcome {\n basePath: string;\n matches: SearchResult[];\n summary: {\n /**\n * Matching *lines*, one per entry in `matches` — not pattern occurrences.\n * A line with three occurrences counts once here and reports 3 in its own\n * `SearchResult.matchCount`.\n */\n matchingLines: number;\n filesScanned: number;\n filesMatched: number;\n truncated: boolean;\n /** Files the guard rejected or that could not be stat'd. */\n skippedInaccessible: number;\n /** Files skipped unread because they exceed maxFileSize. */\n skippedTooLarge: number;\n stoppedReason?: StoppedReason;\n };\n}\n\n/** Owns the compiled pattern's lifetime; the scan itself is `scanContent`. */\nexport async function searchContent(\n directory: string,\n pattern: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n): Promise<SearchContentOutcome> {\n const regexPattern = options.isRegex ? pattern || '' : escapeRegexLiteral(pattern || '');\n const regex = compileRegex(regexPattern, { caseSensitive: Boolean(options.caseSensitive) });\n try {\n return await scanContent(directory, options, pathGuard, regex);\n } finally {\n freeRegex(regex);\n }\n}\n\nasync function* guardedEntries(\n entries: AsyncIterable<GlobEntry>,\n pathGuard: PathGuard,\n signal: AbortSignal | undefined,\n counters: { skippedInaccessible: number; stoppedByAbort: boolean },\n): AsyncGenerator<GlobEntry> {\n // The signal carries both client cancellation and the tool's search timeout,\n // so an abort means \"return what we have, marked incomplete\" rather than\n // throw — but it must never be reported as a finished scan.\n for await (const entry of entries) {\n if (signal?.aborted) {\n counters.stoppedByAbort = true;\n return;\n }\n try {\n await pathGuard.validateExistingPath(entry.path);\n } catch {\n counters.skippedInaccessible++;\n continue;\n }\n yield entry;\n }\n}\n\nasync function scanContent(\n directory: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n regex: Regex,\n): Promise<SearchContentOutcome> {\n const matches: SearchResult[] = [];\n const maxResults = options.maxResults ?? 100;\n const maxFileSize = options.maxFileSize ?? getMaxTextFileSize();\n\n const entries = globEntries({\n cwd: directory,\n pattern: options.filePattern ?? '**/*',\n excludePatterns: options.excludePatterns ?? [],\n includeHidden: Boolean(options.includeHidden),\n respectGitignore: Boolean(options.respectGitignore),\n maxDepth: options.maxDepth ?? 100,\n suppressErrors: true,\n });\n\n let filesScanned = 0;\n let filesMatched = 0;\n let matchingLines = 0;\n let skippedTooLarge = 0;\n const counters = { skippedInaccessible: 0, stoppedByAbort: false };\n\n for await (const entry of guardedEntries(entries, pathGuard, options.signal, counters)) {\n if (matches.length >= maxResults) break;\n\n // Skip oversized files before reading to avoid unbounded memory use. Count\n // them: \"no matches\" for a reason other than the pattern must be visible.\n try {\n const stats = await fsStat(entry.path);\n if (stats.size > maxFileSize) {\n skippedTooLarge++;\n continue;\n }\n } catch {\n counters.skippedInaccessible++;\n continue;\n }\n\n filesScanned++;\n\n try {\n const content = await readFile(entry.path, { encoding: 'utf-8', signal: options.signal });\n const lines = content.split('\\n');\n let matchedFile = false;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n // One scan per line: countLineMatches resets lastIndex itself, so it\n // doubles as the \"does this line match\" test.\n const occurrences = countLineMatches(regex, line);\n if (occurrences > 0) {\n matchedFile = true;\n matchingLines++;\n matches.push({\n file: entry.path,\n line: i + 1,\n content: line,\n matchCount: occurrences,\n });\n if (matches.length >= maxResults) break;\n }\n }\n if (matchedFile) filesMatched++;\n } catch {\n // A read failure while the signal is aborted IS the abort, not an\n // unreadable file — stop rather than spend another iteration and then\n // report a cut-short scan as complete.\n if (options.signal?.aborted) {\n counters.stoppedByAbort = true;\n break;\n }\n // ignore read errors (e.g. binary files)\n }\n }\n\n const tracker = new StopReasonTracker();\n if (matches.length >= maxResults) tracker.hitMaxResults();\n if (counters.stoppedByAbort) tracker.hitAbort();\n const stoppedReason = tracker.resolve();\n\n return {\n basePath: directory,\n matches,\n summary: {\n matchingLines,\n filesScanned,\n filesMatched,\n truncated: tracker.truncated,\n skippedInaccessible: counters.skippedInaccessible,\n skippedTooLarge,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n}\n\nexport async function searchFiles(\n directory: string,\n pattern: string,\n excludePatterns: string[],\n options: {\n maxResults?: number;\n includeHidden?: boolean;\n sortBy?: 'name' | 'path';\n respectGitignore?: boolean;\n maxDepth?: number;\n signal?: AbortSignal;\n },\n pathGuard: PathGuard,\n): Promise<{\n basePath: string;\n results: { path: string }[];\n summary: {\n matched: number;\n filesScanned: number;\n truncated: boolean;\n skippedInaccessible: number;\n stoppedReason?: StoppedReason;\n };\n}> {\n const maxResults = options.maxResults ?? 100;\n const entries = globEntries({\n cwd: directory,\n pattern,\n excludePatterns,\n includeHidden: Boolean(options.includeHidden),\n respectGitignore: Boolean(options.respectGitignore),\n maxDepth: options.maxDepth ?? 100,\n suppressErrors: true,\n });\n const results: { path: string }[] = [];\n let filesScanned = 0;\n const counters = { skippedInaccessible: 0, stoppedByAbort: false };\n\n for await (const entry of guardedEntries(entries, pathGuard, options.signal, counters)) {\n if (results.length >= maxResults) break;\n filesScanned++;\n results.push({ path: entry.path });\n }\n\n // Sorting — only name / path are supported; size / modified were removed\n // (the glob never collected stats, so they were always undefined).\n if (options.sortBy === 'name') {\n results.sort((a, b) => {\n const aName = a.path.split(/[/\\\\]/).pop() ?? '';\n const bName = b.path.split(/[/\\\\]/).pop() ?? '';\n return aName.localeCompare(bName);\n });\n } else {\n results.sort((a, b) => a.path.localeCompare(b.path));\n }\n\n const tracker = new StopReasonTracker();\n if (results.length >= maxResults) tracker.hitMaxResults();\n if (counters.stoppedByAbort) tracker.hitAbort();\n const stoppedReason = tracker.resolve();\n\n return {\n basePath: directory,\n results,\n summary: {\n matched: results.length,\n filesScanned,\n truncated: tracker.truncated,\n skippedInaccessible: counters.skippedInaccessible,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export declare class SensitiveMatcher {
2
+ private readonly patterns;
3
+ constructor(patterns?: readonly string[]);
4
+ isSensitive(filePath: string): boolean;
5
+ }
6
+ //# sourceMappingURL=sensitive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive.d.ts","sourceRoot":"","sources":["../../src/core/sensitive.ts"],"names":[],"mappings":"AA+JA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,QAAQ,GAAE,SAAS,MAAM,EAA6B;IAIlE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;CAWvC"}
@@ -0,0 +1,142 @@
1
+ // Sensitive-file denylist policy: the compiled pattern set, the path
2
+ // normalization that feeds it, and the NTFS alternate-data-stream stripping
3
+ // Windows needs. Split out of path.ts so the denylist has its own home — it
4
+ // shares only isAlpha / toPosixPath / IS_WINDOWS with the primitives in
5
+ // primitives.ts, not the allowed-directory assembly. path-completer.ts and
6
+ // glob.ts reach it through PathGuard.isSensitive, which delegates here.
7
+ import { normalize, posix, sep } from 'node:path';
8
+ import { cli } from './config.js';
9
+ import { IS_WINDOWS, isAlpha, parseTrueEnvFlag, toPosixPath } from './primitives.js';
10
+ const CHAR_COLON = 58;
11
+ const CHAR_FORWARD_SLASH = 47;
12
+ function normalizeForMatch(input) {
13
+ // Always lowercase for case-insensitive denylist matching on all platforms.
14
+ return toPosixPath(normalize(input)).toLowerCase();
15
+ }
16
+ function isWindowsAbsolutePosixPath(normalizedPattern) {
17
+ return (normalizedPattern.length >= 3 &&
18
+ normalizedPattern.charCodeAt(1) === CHAR_COLON &&
19
+ normalizedPattern.charCodeAt(2) === CHAR_FORWARD_SLASH &&
20
+ isAlpha(normalizedPattern.charCodeAt(0)));
21
+ }
22
+ function compilePatternGlobs(normalizedPattern) {
23
+ const globs = new Set([normalizedPattern]);
24
+ const isRooted = normalizedPattern.startsWith('**/') || isWindowsAbsolutePosixPath(normalizedPattern);
25
+ if (!isRooted) {
26
+ const withoutRoot = normalizedPattern.replace(/^\/+/, '');
27
+ if (withoutRoot) {
28
+ globs.add(`**/${withoutRoot}`);
29
+ }
30
+ }
31
+ return Array.from(globs);
32
+ }
33
+ function compilePatterns(patterns) {
34
+ const deduped = Array.from(new Set(patterns.map((p) => p.trim()).filter((p) => p.length > 0)));
35
+ return deduped.map((pattern) => {
36
+ const normalized = normalizeForMatch(pattern);
37
+ const matchesPath = normalized.includes('/');
38
+ return {
39
+ globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
40
+ matchesPath,
41
+ };
42
+ });
43
+ }
44
+ function toPatternSet(patterns) {
45
+ const pathGlobs = new Set();
46
+ const nameGlobs = new Set();
47
+ for (const pattern of patterns) {
48
+ const target = pattern.matchesPath ? pathGlobs : nameGlobs;
49
+ for (const glob of pattern.globs) {
50
+ target.add(glob);
51
+ }
52
+ }
53
+ return {
54
+ pathGlobs: [...pathGlobs],
55
+ nameGlobs: [...nameGlobs],
56
+ };
57
+ }
58
+ function matchesAnyGlob(globs, candidate) {
59
+ if (globs.length === 0)
60
+ return false;
61
+ for (const glob of globs) {
62
+ if (posix.matchesGlob(candidate, glob))
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ // Strip NTFS alternate-data-stream suffixes (the ":stream" after a segment)
68
+ // before denylist matching, so `.env:secret` is caught as `.env`. Preserves the
69
+ // drive-letter colon in the first segment of an absolute path. Also trims
70
+ // trailing dots/spaces: Win32 strips them at the syscall boundary (a request
71
+ // to create `.env ` actually creates `.env`), so without trimming them here the
72
+ // exact-name patterns (`.env`, `.npmrc`) are bypassed on Windows. Mirrors the
73
+ // precedent in getReservedDeviceName (path.ts) which trims for the same reason.
74
+ function stripAlternateDataStreams(filePath) {
75
+ const parts = filePath.split(/[\\/]/);
76
+ const stripped = parts.map((segment, i) => {
77
+ if (i === 0 &&
78
+ segment.length === 2 &&
79
+ isAlpha(segment.charCodeAt(0)) &&
80
+ segment.charCodeAt(1) === CHAR_COLON) {
81
+ return segment;
82
+ }
83
+ const colonIdx = segment.indexOf(':');
84
+ const withoutStream = colonIdx !== -1 ? segment.slice(0, colonIdx) : segment;
85
+ // Win32 strips trailing dots and spaces at the syscall boundary, so a
86
+ // request for ".env " creates ".env". Strip them before denylist matching
87
+ // or the exact-name patterns (".env", ".npmrc") are bypassed on Windows.
88
+ return trimTrailingDotsAndSpaces(withoutStream);
89
+ });
90
+ return stripped.join(sep);
91
+ }
92
+ const trimTrailingDotsAndSpaces = (segment) => segment.replace(/[. ]+$/, '');
93
+ const DEFAULT_SENSITIVE_PATTERNS = [
94
+ '.env',
95
+ '.env.*',
96
+ '.npmrc',
97
+ '.pypirc',
98
+ '.aws/credentials',
99
+ '.aws/config',
100
+ '.mcpregistry_*_token',
101
+ '*.pem',
102
+ '*.key',
103
+ '*.p12',
104
+ '*.pfx',
105
+ '*.crt',
106
+ '*.cer',
107
+ '*id_rsa*',
108
+ '*id_dsa*',
109
+ ];
110
+ function buildSensitivePatterns() {
111
+ const allowSensitive = cli.allowSensitive ?? parseTrueEnvFlag(process.env['FS_ALLOW_SENSITIVE'], 'FS_ALLOW_SENSITIVE');
112
+ const envValue = process.env['FS_DENYLIST'];
113
+ const envDenylist = envValue
114
+ ? envValue
115
+ .split(/[,\n]/u)
116
+ .map((t) => t.trim())
117
+ .filter((t) => t.length > 0)
118
+ : [];
119
+ const flagDenylist = cli.denyPatterns ?? [];
120
+ // FS_ALLOW_SENSITIVE suppresses built-ins only; deny entries (env and --deny)
121
+ // always apply. Set-dedupe so a pattern in both sources matches once.
122
+ return [
123
+ ...(allowSensitive ? [] : DEFAULT_SENSITIVE_PATTERNS),
124
+ ...new Set([...envDenylist, ...flagDenylist]),
125
+ ];
126
+ }
127
+ export class SensitiveMatcher {
128
+ patterns;
129
+ constructor(patterns = buildSensitivePatterns()) {
130
+ this.patterns = toPatternSet(compilePatterns(patterns));
131
+ }
132
+ isSensitive(filePath) {
133
+ if (this.patterns.pathGlobs.length === 0 && this.patterns.nameGlobs.length === 0) {
134
+ return false;
135
+ }
136
+ const pathToCheck = IS_WINDOWS ? stripAlternateDataStreams(filePath) : filePath;
137
+ const normalizedPath = normalizeForMatch(pathToCheck);
138
+ return (matchesAnyGlob(this.patterns.pathGlobs, normalizedPath) ||
139
+ matchesAnyGlob(this.patterns.nameGlobs, posix.basename(normalizedPath)));
140
+ }
141
+ }
142
+ //# sourceMappingURL=sensitive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitive.js","sourceRoot":"","sources":["../../src/core/sensitive.ts"],"names":[],"mappings":"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","sourcesContent":["// Sensitive-file denylist policy: the compiled pattern set, the path\n// normalization that feeds it, and the NTFS alternate-data-stream stripping\n// Windows needs. Split out of path.ts so the denylist has its own home — it\n// shares only isAlpha / toPosixPath / IS_WINDOWS with the primitives in\n// primitives.ts, not the allowed-directory assembly. path-completer.ts and\n// glob.ts reach it through PathGuard.isSensitive, which delegates here.\nimport { normalize, posix, sep } from 'node:path';\n\nimport { cli } from './config.js';\nimport { IS_WINDOWS, isAlpha, parseTrueEnvFlag, toPosixPath } from './primitives.js';\n\nconst CHAR_COLON = 58;\nconst CHAR_FORWARD_SLASH = 47;\n\nfunction normalizeForMatch(input: string): string {\n // Always lowercase for case-insensitive denylist matching on all platforms.\n return toPosixPath(normalize(input)).toLowerCase();\n}\n\ninterface CompiledPattern {\n globs: readonly string[];\n matchesPath: boolean;\n}\n\ninterface CompiledPatternSet {\n pathGlobs: readonly string[];\n nameGlobs: readonly string[];\n}\n\nfunction isWindowsAbsolutePosixPath(normalizedPattern: string): boolean {\n return (\n normalizedPattern.length >= 3 &&\n normalizedPattern.charCodeAt(1) === CHAR_COLON &&\n normalizedPattern.charCodeAt(2) === CHAR_FORWARD_SLASH &&\n isAlpha(normalizedPattern.charCodeAt(0))\n );\n}\n\nfunction compilePatternGlobs(normalizedPattern: string): readonly string[] {\n const globs = new Set<string>([normalizedPattern]);\n\n const isRooted =\n normalizedPattern.startsWith('**/') || isWindowsAbsolutePosixPath(normalizedPattern);\n if (!isRooted) {\n const withoutRoot = normalizedPattern.replace(/^\\/+/, '');\n if (withoutRoot) {\n globs.add(`**/${withoutRoot}`);\n }\n }\n\n return Array.from(globs);\n}\n\nfunction compilePatterns(patterns: readonly string[]): CompiledPattern[] {\n const deduped = Array.from(new Set(patterns.map((p) => p.trim()).filter((p) => p.length > 0)));\n return deduped.map((pattern) => {\n const normalized = normalizeForMatch(pattern);\n const matchesPath = normalized.includes('/');\n return {\n globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],\n matchesPath,\n };\n });\n}\n\nfunction toPatternSet(patterns: readonly CompiledPattern[]): CompiledPatternSet {\n const pathGlobs = new Set<string>();\n const nameGlobs = new Set<string>();\n\n for (const pattern of patterns) {\n const target = pattern.matchesPath ? pathGlobs : nameGlobs;\n for (const glob of pattern.globs) {\n target.add(glob);\n }\n }\n\n return {\n pathGlobs: [...pathGlobs],\n nameGlobs: [...nameGlobs],\n };\n}\n\nfunction matchesAnyGlob(globs: readonly string[], candidate: string): boolean {\n if (globs.length === 0) return false;\n\n for (const glob of globs) {\n if (posix.matchesGlob(candidate, glob)) return true;\n }\n\n return false;\n}\n\n// Strip NTFS alternate-data-stream suffixes (the \":stream\" after a segment)\n// before denylist matching, so `.env:secret` is caught as `.env`. Preserves the\n// drive-letter colon in the first segment of an absolute path. Also trims\n// trailing dots/spaces: Win32 strips them at the syscall boundary (a request\n// to create `.env ` actually creates `.env`), so without trimming them here the\n// exact-name patterns (`.env`, `.npmrc`) are bypassed on Windows. Mirrors the\n// precedent in getReservedDeviceName (path.ts) which trims for the same reason.\nfunction stripAlternateDataStreams(filePath: string): string {\n const parts = filePath.split(/[\\\\/]/);\n const stripped = parts.map((segment, i) => {\n if (\n i === 0 &&\n segment.length === 2 &&\n isAlpha(segment.charCodeAt(0)) &&\n segment.charCodeAt(1) === CHAR_COLON\n ) {\n return segment;\n }\n const colonIdx = segment.indexOf(':');\n const withoutStream = colonIdx !== -1 ? segment.slice(0, colonIdx) : segment;\n // Win32 strips trailing dots and spaces at the syscall boundary, so a\n // request for \".env \" creates \".env\". Strip them before denylist matching\n // or the exact-name patterns (\".env\", \".npmrc\") are bypassed on Windows.\n return trimTrailingDotsAndSpaces(withoutStream);\n });\n return stripped.join(sep);\n}\n\nconst trimTrailingDotsAndSpaces = (segment: string): string => segment.replace(/[. ]+$/, '');\n\nconst DEFAULT_SENSITIVE_PATTERNS = [\n '.env',\n '.env.*',\n '.npmrc',\n '.pypirc',\n '.aws/credentials',\n '.aws/config',\n '.mcpregistry_*_token',\n '*.pem',\n '*.key',\n '*.p12',\n '*.pfx',\n '*.crt',\n '*.cer',\n '*id_rsa*',\n '*id_dsa*',\n] as const;\n\nfunction buildSensitivePatterns(): readonly string[] {\n const allowSensitive =\n cli.allowSensitive ?? parseTrueEnvFlag(process.env['FS_ALLOW_SENSITIVE'], 'FS_ALLOW_SENSITIVE');\n const envValue = process.env['FS_DENYLIST'];\n const envDenylist = envValue\n ? envValue\n .split(/[,\\n]/u)\n .map((t) => t.trim())\n .filter((t) => t.length > 0)\n : [];\n const flagDenylist = cli.denyPatterns ?? [];\n // FS_ALLOW_SENSITIVE suppresses built-ins only; deny entries (env and --deny)\n // always apply. Set-dedupe so a pattern in both sources matches once.\n return [\n ...(allowSensitive ? [] : DEFAULT_SENSITIVE_PATTERNS),\n ...new Set([...envDenylist, ...flagDenylist]),\n ];\n}\n\nexport class SensitiveMatcher {\n private readonly patterns: CompiledPatternSet;\n\n constructor(patterns: readonly string[] = buildSensitivePatterns()) {\n this.patterns = toPatternSet(compilePatterns(patterns));\n }\n\n isSensitive(filePath: string): boolean {\n if (this.patterns.pathGlobs.length === 0 && this.patterns.nameGlobs.length === 0) {\n return false;\n }\n const pathToCheck = IS_WINDOWS ? stripAlternateDataStreams(filePath) : filePath;\n const normalizedPath = normalizeForMatch(pathToCheck);\n return (\n matchesAnyGlob(this.patterns.pathGlobs, normalizedPath) ||\n matchesAnyGlob(this.patterns.nameGlobs, posix.basename(normalizedPath))\n );\n }\n}\n"]}
@@ -0,0 +1,57 @@
1
+ import type { ContentBlock } from '@modelcontextprotocol/server';
2
+ interface ResourceEntry {
3
+ uri: string;
4
+ name: string;
5
+ mimeType: string;
6
+ size: number;
7
+ storedAt: string;
8
+ expiresAt: string;
9
+ text: string;
10
+ }
11
+ export declare class ResourceStore {
12
+ private readonly byUri;
13
+ private _totalBytes;
14
+ private readonly onListChanged;
15
+ constructor(onListChanged?: () => void);
16
+ private emitListChanged;
17
+ private bumpLru;
18
+ private removeEntry;
19
+ private evictOldest;
20
+ private pruneExpiredEntries;
21
+ private enforceLimits;
22
+ private getExisting;
23
+ private checkBeforePut;
24
+ putText(params: {
25
+ name: string;
26
+ mimeType?: string;
27
+ text: string;
28
+ }): ResourceEntry;
29
+ getEntry(uri: string): ResourceEntry;
30
+ /**
31
+ * Returns live URIs currently in the store.
32
+ * Side effect: prunes expired entries before returning.
33
+ *
34
+ * Deliberately silent: this is the read path `resources/list` runs through,
35
+ * and the caller is already receiving the post-prune list. Emitting here
36
+ * would tell the client its list changed while handing it that same list.
37
+ */
38
+ keys(): string[];
39
+ }
40
+ export interface JsonResourceResult {
41
+ entry: {
42
+ uri: string;
43
+ size: number;
44
+ mimeType: string;
45
+ expiresAt: string;
46
+ };
47
+ link: ContentBlock;
48
+ }
49
+ /**
50
+ * Owner of the externalize-a-payload rule: a tool whose inline response is
51
+ * truncated publishes the full value to the resource store as pretty-printed
52
+ * JSON and hands back the URI to reach it by, plus the link block that offers
53
+ * it to the user.
54
+ */
55
+ export declare function putJsonResource(store: ResourceStore, name: string, value: Record<string, unknown> | readonly unknown[]): JsonResourceResult;
56
+ export {};
57
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAQjE,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAeD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;gBAE7C,aAAa,CAAC,EAAE,MAAM,IAAI;IAItC,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa;IAqBjF,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa;IAIpC;;;;;;;OAOG;IACH,IAAI,IAAI,MAAM,EAAE;CAIjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE,YAAY,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EAIZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,GAClD,kBAAkB,CAuBpB"}