@j0hanz/fs-context-mcp 2.0.0 → 2.0.2

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 (178) hide show
  1. package/README.md +45 -60
  2. package/dist/config.d.ts +117 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +31 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/instructions.md +32 -35
  9. package/dist/lib/constants.d.ts +3 -3
  10. package/dist/lib/constants.d.ts.map +1 -1
  11. package/dist/lib/constants.js +130 -4
  12. package/dist/lib/constants.js.map +1 -1
  13. package/dist/lib/errors.d.ts +1 -1
  14. package/dist/lib/errors.d.ts.map +1 -1
  15. package/dist/lib/errors.js +9 -9
  16. package/dist/lib/errors.js.map +1 -1
  17. package/dist/lib/file-operations/file-info.d.ts +7 -1
  18. package/dist/lib/file-operations/file-info.d.ts.map +1 -1
  19. package/dist/lib/file-operations/file-info.js +84 -8
  20. package/dist/lib/file-operations/file-info.js.map +1 -1
  21. package/dist/lib/file-operations/get-multiple-file-info.d.ts.map +1 -1
  22. package/dist/lib/file-operations/get-multiple-file-info.js +8 -4
  23. package/dist/lib/file-operations/get-multiple-file-info.js.map +1 -1
  24. package/dist/lib/file-operations/glob-engine.d.ts.map +1 -1
  25. package/dist/lib/file-operations/glob-engine.js +190 -26
  26. package/dist/lib/file-operations/glob-engine.js.map +1 -1
  27. package/dist/lib/file-operations/list-directory-entry.d.ts.map +1 -1
  28. package/dist/lib/file-operations/list-directory-entry.js +5 -3
  29. package/dist/lib/file-operations/list-directory-entry.js.map +1 -1
  30. package/dist/lib/file-operations/list-directory-helpers.d.ts.map +1 -1
  31. package/dist/lib/file-operations/list-directory-helpers.js +11 -4
  32. package/dist/lib/file-operations/list-directory-helpers.js.map +1 -1
  33. package/dist/lib/file-operations/list-directory.d.ts +12 -2
  34. package/dist/lib/file-operations/list-directory.d.ts.map +1 -1
  35. package/dist/lib/file-operations/list-directory.js +175 -3
  36. package/dist/lib/file-operations/list-directory.js.map +1 -1
  37. package/dist/lib/file-operations/read-multiple-files-helpers.d.ts.map +1 -1
  38. package/dist/lib/file-operations/read-multiple-files-helpers.js +40 -19
  39. package/dist/lib/file-operations/read-multiple-files-helpers.js.map +1 -1
  40. package/dist/lib/file-operations/read-multiple-files.d.ts +18 -1
  41. package/dist/lib/file-operations/read-multiple-files.d.ts.map +1 -1
  42. package/dist/lib/file-operations/read-multiple-files.js +107 -2
  43. package/dist/lib/file-operations/read-multiple-files.js.map +1 -1
  44. package/dist/lib/file-operations/search/engine.d.ts.map +1 -1
  45. package/dist/lib/file-operations/search/engine.js +7 -2
  46. package/dist/lib/file-operations/search/engine.js.map +1 -1
  47. package/dist/lib/file-operations/search/scan-file.d.ts.map +1 -1
  48. package/dist/lib/file-operations/search/scan-file.js +6 -3
  49. package/dist/lib/file-operations/search/scan-file.js.map +1 -1
  50. package/dist/lib/file-operations/search/scan-runner.d.ts.map +1 -1
  51. package/dist/lib/file-operations/search/scan-runner.js +21 -16
  52. package/dist/lib/file-operations/search/scan-runner.js.map +1 -1
  53. package/dist/lib/file-operations/search-content.d.ts +41 -17
  54. package/dist/lib/file-operations/search-content.d.ts.map +1 -1
  55. package/dist/lib/file-operations/search-content.js +855 -3
  56. package/dist/lib/file-operations/search-content.js.map +1 -1
  57. package/dist/lib/file-operations/search-files-collector.d.ts +2 -2
  58. package/dist/lib/file-operations/search-files-collector.d.ts.map +1 -1
  59. package/dist/lib/file-operations/search-files-collector.js +23 -11
  60. package/dist/lib/file-operations/search-files-collector.js.map +1 -1
  61. package/dist/lib/file-operations/search-files-helpers.d.ts.map +1 -1
  62. package/dist/lib/file-operations/search-files-helpers.js +5 -2
  63. package/dist/lib/file-operations/search-files-helpers.js.map +1 -1
  64. package/dist/lib/file-operations/search-files.d.ts +20 -2
  65. package/dist/lib/file-operations/search-files.d.ts.map +1 -1
  66. package/dist/lib/file-operations/search-files.js +192 -5
  67. package/dist/lib/file-operations/search-files.js.map +1 -1
  68. package/dist/lib/file-operations/search-worker.d.ts +30 -0
  69. package/dist/lib/file-operations/search-worker.d.ts.map +1 -0
  70. package/dist/lib/file-operations/search-worker.js +98 -0
  71. package/dist/lib/file-operations/search-worker.js.map +1 -0
  72. package/dist/lib/fs-helpers/readers/read-modes.d.ts.map +1 -1
  73. package/dist/lib/fs-helpers/readers/read-modes.js +6 -4
  74. package/dist/lib/fs-helpers/readers/read-modes.js.map +1 -1
  75. package/dist/lib/fs-helpers/readers/read-options.d.ts.map +1 -1
  76. package/dist/lib/fs-helpers/readers/read-options.js +8 -3
  77. package/dist/lib/fs-helpers/readers/read-options.js.map +1 -1
  78. package/dist/lib/fs-helpers.d.ts +42 -5
  79. package/dist/lib/fs-helpers.d.ts.map +1 -1
  80. package/dist/lib/fs-helpers.js +479 -5
  81. package/dist/lib/fs-helpers.js.map +1 -1
  82. package/dist/lib/observability/diagnostics-helpers.d.ts +3 -0
  83. package/dist/lib/observability/diagnostics-helpers.d.ts.map +1 -0
  84. package/dist/lib/observability/diagnostics-helpers.js +50 -0
  85. package/dist/lib/observability/diagnostics-helpers.js.map +1 -0
  86. package/dist/lib/observability/diagnostics.d.ts.map +1 -1
  87. package/dist/lib/observability/diagnostics.js +24 -59
  88. package/dist/lib/observability/diagnostics.js.map +1 -1
  89. package/dist/lib/observability.d.ts +15 -0
  90. package/dist/lib/observability.d.ts.map +1 -0
  91. package/dist/lib/observability.js +196 -0
  92. package/dist/lib/observability.js.map +1 -0
  93. package/dist/lib/path-validation/validate-existing.js +1 -1
  94. package/dist/lib/path-validation/validate-existing.js.map +1 -1
  95. package/dist/lib/path-validation.d.ts +18 -5
  96. package/dist/lib/path-validation.d.ts.map +1 -1
  97. package/dist/lib/path-validation.js +340 -5
  98. package/dist/lib/path-validation.js.map +1 -1
  99. package/dist/schemas/inputs/directory.d.ts +1 -1
  100. package/dist/schemas/inputs/directory.d.ts.map +1 -1
  101. package/dist/schemas/inputs/directory.js +1 -12
  102. package/dist/schemas/inputs/directory.js.map +1 -1
  103. package/dist/schemas/inputs/helpers.d.ts +2 -0
  104. package/dist/schemas/inputs/helpers.d.ts.map +1 -0
  105. package/dist/schemas/inputs/helpers.js +13 -0
  106. package/dist/schemas/inputs/helpers.js.map +1 -0
  107. package/dist/schemas/inputs/safe-glob.d.ts +2 -0
  108. package/dist/schemas/inputs/safe-glob.d.ts.map +1 -0
  109. package/dist/schemas/inputs/safe-glob.js +13 -0
  110. package/dist/schemas/inputs/safe-glob.js.map +1 -0
  111. package/dist/schemas/inputs/search.d.ts.map +1 -1
  112. package/dist/schemas/inputs/search.js +2 -13
  113. package/dist/schemas/inputs/search.js.map +1 -1
  114. package/dist/schemas/output-helpers.d.ts +6 -1
  115. package/dist/schemas/output-helpers.d.ts.map +1 -1
  116. package/dist/schemas/output-helpers.js +5 -0
  117. package/dist/schemas/output-helpers.js.map +1 -1
  118. package/dist/schemas/outputs/file-info.d.ts +2 -2
  119. package/dist/schemas/outputs/file-info.d.ts.map +1 -1
  120. package/dist/schemas/outputs/file-info.js +2 -8
  121. package/dist/schemas/outputs/file-info.js.map +1 -1
  122. package/dist/schemas/outputs/read.d.ts.map +1 -1
  123. package/dist/schemas/outputs/read.js +2 -7
  124. package/dist/schemas/outputs/read.js.map +1 -1
  125. package/dist/schemas.d.ts +195 -0
  126. package/dist/schemas.d.ts.map +1 -0
  127. package/dist/schemas.js +234 -0
  128. package/dist/schemas.js.map +1 -0
  129. package/dist/server/roots.d.ts.map +1 -1
  130. package/dist/server/roots.js +19 -15
  131. package/dist/server/roots.js.map +1 -1
  132. package/dist/server.d.ts +10 -2
  133. package/dist/server.d.ts.map +1 -1
  134. package/dist/server.js +208 -10
  135. package/dist/server.js.map +1 -1
  136. package/dist/tools/get-file-info.d.ts.map +1 -1
  137. package/dist/tools/get-file-info.js +5 -1
  138. package/dist/tools/get-file-info.js.map +1 -1
  139. package/dist/tools/get-multiple-file-info.d.ts.map +1 -1
  140. package/dist/tools/get-multiple-file-info.js +6 -3
  141. package/dist/tools/get-multiple-file-info.js.map +1 -1
  142. package/dist/tools/list-directory-formatting.d.ts.map +1 -1
  143. package/dist/tools/list-directory-formatting.js +7 -1
  144. package/dist/tools/list-directory-formatting.js.map +1 -1
  145. package/dist/tools/list-directory.d.ts.map +1 -1
  146. package/dist/tools/list-directory.js +5 -4
  147. package/dist/tools/list-directory.js.map +1 -1
  148. package/dist/tools/read-file.d.ts.map +1 -1
  149. package/dist/tools/read-file.js +9 -4
  150. package/dist/tools/read-file.js.map +1 -1
  151. package/dist/tools/read-multiple-files.d.ts.map +1 -1
  152. package/dist/tools/read-multiple-files.js +9 -4
  153. package/dist/tools/read-multiple-files.js.map +1 -1
  154. package/dist/tools/search-content.d.ts.map +1 -1
  155. package/dist/tools/search-content.js +19 -17
  156. package/dist/tools/search-content.js.map +1 -1
  157. package/dist/tools/search-files.d.ts.map +1 -1
  158. package/dist/tools/search-files.js +15 -12
  159. package/dist/tools/search-files.js.map +1 -1
  160. package/dist/tools/shared/file-info.d.ts +4 -2
  161. package/dist/tools/shared/file-info.d.ts.map +1 -1
  162. package/dist/tools/shared/file-info.js +4 -2
  163. package/dist/tools/shared/file-info.js.map +1 -1
  164. package/dist/tools/shared/resolve-path.d.ts +2 -0
  165. package/dist/tools/shared/resolve-path.d.ts.map +1 -0
  166. package/dist/tools/shared/resolve-path.js +11 -0
  167. package/dist/tools/shared/resolve-path.js.map +1 -0
  168. package/dist/tools/shared/search-formatting.d.ts.map +1 -1
  169. package/dist/tools/shared/search-formatting.js +58 -38
  170. package/dist/tools/shared/search-formatting.js.map +1 -1
  171. package/dist/tools/tool-response.d.ts.map +1 -1
  172. package/dist/tools/tool-response.js +11 -6
  173. package/dist/tools/tool-response.js.map +1 -1
  174. package/dist/tools.d.ts +34 -0
  175. package/dist/tools.d.ts.map +1 -0
  176. package/dist/tools.js +550 -0
  177. package/dist/tools.js.map +1 -0
  178. package/package.json +11 -6
@@ -1,5 +1,857 @@
1
- import { executeSearch } from './search/engine.js';
2
- export async function searchContent(basePath, searchPattern, options = {}) {
3
- return executeSearch(basePath, searchPattern, options, options.signal);
1
+ import * as fsp from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { Worker } from 'node:worker_threads';
6
+ import RE2 from 're2';
7
+ import safeRegex from 'safe-regex2';
8
+ import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
9
+ import { assertNotAborted, createTimedAbortSignal, isProbablyBinary, withAbort, } from '../fs-helpers.js';
10
+ import { publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, } from '../observability.js';
11
+ import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toAccessDeniedWithHint, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
12
+ import { globEntries } from './glob-engine.js';
13
+ const INTERNAL_MAX_RESULTS = 500;
14
+ const DEFAULTS = {
15
+ filePattern: '**/*',
16
+ excludePatterns: [],
17
+ caseSensitive: false,
18
+ maxResults: INTERNAL_MAX_RESULTS,
19
+ maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
20
+ maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
21
+ timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
22
+ skipBinary: true,
23
+ contextLines: 0,
24
+ wholeWord: false,
25
+ isLiteral: true,
26
+ includeHidden: false,
27
+ baseNameMatch: false,
28
+ caseSensitiveFileMatch: true,
29
+ };
30
+ function mergeOptions(partial) {
31
+ const rest = { ...partial };
32
+ delete rest.signal;
33
+ return { ...DEFAULTS, ...rest };
34
+ }
35
+ function validatePattern(pattern, options) {
36
+ if (options.isLiteral && !options.wholeWord) {
37
+ return;
38
+ }
39
+ const final = buildRegexPattern(pattern, options);
40
+ assertSafePattern(final, pattern);
41
+ }
42
+ function escapeLiteral(pattern) {
43
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ }
45
+ function buildRegexPattern(pattern, options) {
46
+ const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
47
+ return options.wholeWord ? `\\b${escaped}\\b` : escaped;
48
+ }
49
+ function assertSafePattern(final, original) {
50
+ if (!safeRegex(final)) {
51
+ throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${original}`);
52
+ }
53
+ }
54
+ function buildLiteralMatcher(pattern, options) {
55
+ const needle = options.caseSensitive ? pattern : pattern.toLowerCase();
56
+ return (line) => {
57
+ const hay = options.caseSensitive ? line : line.toLowerCase();
58
+ if (needle.length === 0 || hay.length === 0)
59
+ return 0;
60
+ let count = 0;
61
+ let pos = hay.indexOf(needle);
62
+ while (pos !== -1) {
63
+ count++;
64
+ pos = hay.indexOf(needle, pos + needle.length);
65
+ }
66
+ return count;
67
+ };
68
+ }
69
+ function buildRegexMatcher(final, caseSensitive) {
70
+ const regex = new RE2(final, caseSensitive ? 'g' : 'gi');
71
+ return (line) => {
72
+ regex.lastIndex = 0;
73
+ let count = 0;
74
+ let match;
75
+ while ((match = regex.exec(line)) !== null) {
76
+ count++;
77
+ if (match[0].length === 0) {
78
+ regex.lastIndex++;
79
+ }
80
+ }
81
+ return count;
82
+ };
83
+ }
84
+ export function buildMatcher(pattern, options) {
85
+ if (options.isLiteral && !options.wholeWord && !options.caseSensitive) {
86
+ if (pattern.length === 0) {
87
+ return () => 0;
88
+ }
89
+ return buildRegexMatcher(escapeLiteral(pattern), false);
90
+ }
91
+ if (options.isLiteral && !options.wholeWord) {
92
+ return buildLiteralMatcher(pattern, options);
93
+ }
94
+ const final = buildRegexPattern(pattern, options);
95
+ assertSafePattern(final, pattern);
96
+ return buildRegexMatcher(final, options.caseSensitive);
97
+ }
98
+ function makeContext() {
99
+ return { before: [], pendingAfter: [] };
100
+ }
101
+ function pushContext(ctx, line, max) {
102
+ if (max <= 0)
103
+ return;
104
+ ctx.before.push(line);
105
+ if (ctx.before.length > max)
106
+ ctx.before.shift();
107
+ for (const pending of ctx.pendingAfter) {
108
+ if (pending.left <= 0)
109
+ continue;
110
+ pending.buffer.push(line);
111
+ pending.left -= 1;
112
+ }
113
+ while (ctx.pendingAfter.length > 0 && ctx.pendingAfter[0]?.left === 0) {
114
+ ctx.pendingAfter.shift();
115
+ }
116
+ }
117
+ function trimContent(line) {
118
+ return line.trimEnd().slice(0, MAX_LINE_CONTENT_LENGTH);
119
+ }
120
+ function buildReadline(handle, signal) {
121
+ const input = handle.createReadStream({
122
+ encoding: 'utf-8',
123
+ autoClose: false,
124
+ });
125
+ const baseOptions = {
126
+ input,
127
+ crlfDelay: Infinity,
128
+ };
129
+ const options = signal ? { ...baseOptions, signal } : baseOptions;
130
+ const rl = readline.createInterface(options);
131
+ return { rl, input };
132
+ }
133
+ function updateContext(line, contextLines, ctx) {
134
+ if (contextLines <= 0)
135
+ return undefined;
136
+ const trimmedLine = trimContent(line);
137
+ pushContext(ctx, trimmedLine, contextLines);
138
+ return trimmedLine;
139
+ }
140
+ function appendMatch(matches, requestedPath, line, trimmedLine, lineNo, count, contextLines, ctx) {
141
+ const contextBefore = contextLines > 0 ? [...ctx.before] : undefined;
142
+ const contextAfterBuffer = contextLines > 0 ? [] : undefined;
143
+ const match = {
144
+ file: requestedPath,
145
+ line: lineNo,
146
+ content: trimmedLine ?? trimContent(line),
147
+ matchCount: count,
148
+ ...(contextBefore ? { contextBefore } : {}),
149
+ ...(contextAfterBuffer ? { contextAfter: contextAfterBuffer } : {}),
150
+ };
151
+ matches.push(match);
152
+ if (contextAfterBuffer) {
153
+ ctx.pendingAfter.push({
154
+ buffer: contextAfterBuffer,
155
+ left: contextLines,
156
+ });
157
+ }
158
+ }
159
+ function recordLineMatch(line, matcher, options, requestedPath, lineNo, matches, ctx) {
160
+ const trimmedLine = updateContext(line, options.contextLines, ctx);
161
+ const count = matcher(line);
162
+ if (count > 0) {
163
+ appendMatch(matches, requestedPath, line, trimmedLine, lineNo, count, options.contextLines, ctx);
164
+ }
165
+ }
166
+ async function readLoop(rl, matcher, options, requestedPath, maxMatches, isCancelled, matches, ctx) {
167
+ let lineNo = 0;
168
+ for await (const line of rl) {
169
+ if (isCancelled())
170
+ break;
171
+ lineNo++;
172
+ recordLineMatch(line, matcher, options, requestedPath, lineNo, matches, ctx);
173
+ if (matches.length >= maxMatches)
174
+ break;
175
+ }
176
+ }
177
+ function buildSkipResult(skippedTooLarge, skippedBinary) {
178
+ return {
179
+ matches: [],
180
+ matched: false,
181
+ skippedTooLarge,
182
+ skippedBinary,
183
+ };
184
+ }
185
+ function buildMatchResult(matches) {
186
+ return {
187
+ matches,
188
+ matched: matches.length > 0,
189
+ skippedTooLarge: false,
190
+ skippedBinary: false,
191
+ };
192
+ }
193
+ async function shouldSkipBinary(scanOptions, resolvedPath, handle, options) {
194
+ return (scanOptions.skipBinary &&
195
+ (await options.isProbablyBinary(resolvedPath, handle, options.signal)));
196
+ }
197
+ async function readMatches(handle, requestedPath, matcher, options, maxMatches, isCancelled, signal) {
198
+ const { rl, input } = buildReadline(handle, signal);
199
+ const ctx = makeContext();
200
+ const matches = [];
201
+ try {
202
+ await readLoop(rl, matcher, options, requestedPath, maxMatches, isCancelled, matches, ctx);
203
+ return matches;
204
+ }
205
+ finally {
206
+ rl.close();
207
+ input.destroy();
208
+ }
209
+ }
210
+ async function scanWithHandle(handle, resolvedPath, requestedPath, options) {
211
+ const scanOptions = options.options;
212
+ const stats = await withAbort(handle.stat(), options.signal);
213
+ if (stats.size > scanOptions.maxFileSize) {
214
+ return buildSkipResult(true, false);
215
+ }
216
+ if (await shouldSkipBinary(scanOptions, resolvedPath, handle, options)) {
217
+ return buildSkipResult(false, true);
218
+ }
219
+ const matches = await readMatches(handle, requestedPath, options.matcher, scanOptions, options.maxMatches, options.isCancelled, options.signal);
220
+ return buildMatchResult(matches);
221
+ }
222
+ async function scanFileWithMatcher(resolvedPath, requestedPath, options) {
223
+ assertNotAborted(options.signal);
224
+ const handle = await withAbort(fsp.open(resolvedPath, 'r'), options.signal);
225
+ try {
226
+ return await scanWithHandle(handle, resolvedPath, requestedPath, options);
227
+ }
228
+ finally {
229
+ await handle.close();
230
+ }
231
+ }
232
+ async function scanFileResolved(resolvedPath, requestedPath, matcher, options, signal, maxMatches = Number.POSITIVE_INFINITY) {
233
+ const scanOptions = {
234
+ matcher,
235
+ options,
236
+ maxMatches,
237
+ isCancelled: () => Boolean(signal?.aborted),
238
+ isProbablyBinary,
239
+ };
240
+ if (signal) {
241
+ scanOptions.signal = signal;
242
+ }
243
+ return scanFileWithMatcher(resolvedPath, requestedPath, scanOptions);
244
+ }
245
+ export async function scanFileInWorker(resolvedPath, requestedPath, matcher, options, maxMatches, isCancelled, isBinaryDetector) {
246
+ const result = await scanFileWithMatcher(resolvedPath, requestedPath, {
247
+ matcher,
248
+ options,
249
+ maxMatches,
250
+ isCancelled,
251
+ isProbablyBinary: isBinaryDetector,
252
+ });
253
+ return {
254
+ matches: result.matches,
255
+ matched: result.matched,
256
+ skippedTooLarge: result.skippedTooLarge,
257
+ skippedBinary: result.skippedBinary,
258
+ };
259
+ }
260
+ function resolveNonSymlinkPath(entryPath, allowedDirs) {
261
+ const normalized = normalizePath(entryPath);
262
+ if (!isPathWithinDirectories(normalized, allowedDirs)) {
263
+ throw toAccessDeniedWithHint(entryPath, normalized, normalized);
264
+ }
265
+ return { resolvedPath: normalized, requestedPath: normalized };
266
+ }
267
+ function createScanSummary() {
268
+ return {
269
+ filesScanned: 0,
270
+ filesMatched: 0,
271
+ skippedTooLarge: 0,
272
+ skippedBinary: 0,
273
+ skippedInaccessible: 0,
274
+ truncated: false,
275
+ stoppedReason: undefined,
276
+ };
277
+ }
278
+ function shouldStopCollecting(summary, maxFilesScanned, signal) {
279
+ if (signal.aborted) {
280
+ summary.truncated = true;
281
+ summary.stoppedReason = 'timeout';
282
+ return true;
283
+ }
284
+ if (summary.filesScanned >= maxFilesScanned) {
285
+ summary.truncated = true;
286
+ summary.stoppedReason = 'maxFiles';
287
+ return true;
288
+ }
289
+ return false;
290
+ }
291
+ async function resolveEntryPath(entry, allowedDirs, signal) {
292
+ try {
293
+ return entry.dirent.isSymbolicLink()
294
+ ? await validateExistingPathDetailed(entry.path, signal)
295
+ : resolveNonSymlinkPath(entry.path, allowedDirs);
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
301
+ async function* collectFromStream(stream, opts, allowedDirs, signal, summary) {
302
+ for await (const entry of stream) {
303
+ if (!entry.dirent.isFile())
304
+ continue;
305
+ if (shouldStopCollecting(summary, opts.maxFilesScanned, signal)) {
306
+ break;
307
+ }
308
+ const resolved = await resolveEntryPath(entry, allowedDirs, signal);
309
+ if (!resolved) {
310
+ summary.skippedInaccessible++;
311
+ continue;
312
+ }
313
+ summary.filesScanned++;
314
+ yield resolved;
315
+ }
316
+ }
317
+ function collectFilesStream(root, opts, allowedDirs, signal) {
318
+ const summary = createScanSummary();
319
+ const stream = globEntries({
320
+ cwd: root,
321
+ pattern: opts.filePattern,
322
+ excludePatterns: opts.excludePatterns,
323
+ includeHidden: opts.includeHidden,
324
+ baseNameMatch: opts.baseNameMatch,
325
+ caseSensitiveMatch: opts.caseSensitiveFileMatch,
326
+ followSymbolicLinks: false,
327
+ onlyFiles: true,
328
+ stats: false,
329
+ suppressErrors: true,
330
+ });
331
+ return {
332
+ stream: collectFromStream(stream, opts, allowedDirs, signal, summary),
333
+ summary,
334
+ };
335
+ }
336
+ function shouldStopOnSignalOrLimit(signal, matchesCount, maxResults, summary) {
337
+ if (signal.aborted) {
338
+ summary.truncated = true;
339
+ summary.stoppedReason = 'timeout';
340
+ return true;
341
+ }
342
+ if (matchesCount >= maxResults) {
343
+ summary.truncated = true;
344
+ summary.stoppedReason = 'maxResults';
345
+ return true;
346
+ }
347
+ return false;
348
+ }
349
+ function applyScanResult(result, matches, summary, remaining) {
350
+ if (result.skippedTooLarge)
351
+ summary.skippedTooLarge++;
352
+ if (result.skippedBinary)
353
+ summary.skippedBinary++;
354
+ if (result.matched)
355
+ summary.filesMatched++;
356
+ if (result.matches.length > 0 && remaining > 0) {
357
+ matches.push(...result.matches.slice(0, remaining));
358
+ }
359
+ }
360
+ async function scanSequentialFile(file, matcher, scanOptions, signal, maxResults, matches, summary) {
361
+ try {
362
+ const remaining = maxResults - matches.length;
363
+ const result = await scanFileResolved(file.resolvedPath, file.requestedPath, matcher, scanOptions, signal, remaining);
364
+ applyScanResult(result, matches, summary, remaining);
365
+ }
366
+ catch {
367
+ summary.skippedInaccessible++;
368
+ }
369
+ }
370
+ async function collectSequentialMatches(files, matcher, scanOptions, maxResults, signal, summary) {
371
+ const matches = [];
372
+ for await (const file of files) {
373
+ if (shouldStopOnSignalOrLimit(signal, matches.length, maxResults, summary)) {
374
+ break;
375
+ }
376
+ await scanSequentialFile(file, matcher, scanOptions, signal, maxResults, matches, summary);
377
+ }
378
+ return matches;
379
+ }
380
+ async function scanFilesSequential(files, pattern, matcherOptions, scanOptions, maxResults, signal, summary) {
381
+ const matcher = buildMatcher(pattern, matcherOptions);
382
+ return await collectSequentialMatches(files, matcher, scanOptions, maxResults, signal, summary);
383
+ }
384
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
385
+ const currentFile = fileURLToPath(import.meta.url);
386
+ const isSourceContext = currentFile.endsWith('.ts');
387
+ const WORKER_SCRIPT_PATH = path.join(currentDir, isSourceContext ? 'search-worker.ts' : 'search-worker.js');
388
+ const WORKER_SCRIPT_URL = pathToFileURL(WORKER_SCRIPT_PATH);
389
+ const MAX_RESPAWNS = 3;
390
+ function handleWorkerMessage(slot, message, log) {
391
+ const pending = slot.pending.get(message.id);
392
+ if (!pending) {
393
+ log(`Received message for unknown request ${String(message.id)}`);
394
+ return;
395
+ }
396
+ slot.pending.delete(message.id);
397
+ if (message.type === 'result') {
398
+ pending.resolve(message.result);
399
+ }
400
+ else {
401
+ pending.reject(new Error(message.error));
402
+ }
403
+ }
404
+ function handleWorkerError(slot, error, log) {
405
+ log(`Worker ${String(slot.index)} error: ${error.message}`);
406
+ for (const [, pending] of slot.pending) {
407
+ pending.reject(new Error(`Worker error: ${error.message}`));
408
+ }
409
+ slot.pending.clear();
410
+ slot.worker?.terminate().catch(() => { });
411
+ slot.worker = null;
412
+ }
413
+ function handleWorkerExit(slot, code, isClosed, maxRespawns, log) {
414
+ log(`Worker ${String(slot.index)} exited with code ${String(code)}`);
415
+ if (isClosed) {
416
+ return;
417
+ }
418
+ if (slot.pending.size > 0) {
419
+ const error = new Error(`Worker exited unexpectedly with code ${String(code)}`);
420
+ for (const [, pending] of slot.pending) {
421
+ pending.reject(error);
422
+ }
423
+ slot.pending.clear();
424
+ }
425
+ slot.worker = null;
426
+ if (code !== 0 && slot.respawnCount < maxRespawns) {
427
+ slot.respawnCount++;
428
+ log(`Worker ${String(slot.index)} will be respawned on next request (attempt ${String(slot.respawnCount)}/${String(maxRespawns)})`);
429
+ }
430
+ else if (slot.respawnCount >= maxRespawns) {
431
+ log(`Worker ${String(slot.index)} exceeded max respawns, slot disabled`);
432
+ }
433
+ }
434
+ function selectSlot(slots, nextSlotIndex, maxRespawns) {
435
+ let bestSlot = null;
436
+ let bestNextSlotIndex = nextSlotIndex;
437
+ let bestPending = Number.POSITIVE_INFINITY;
438
+ for (let offset = 0; offset < slots.length; offset++) {
439
+ const index = (nextSlotIndex + offset) % slots.length;
440
+ const slot = slots[index];
441
+ if (!slot)
442
+ continue;
443
+ if (!slot.worker && slot.respawnCount >= maxRespawns)
444
+ continue;
445
+ const pendingSize = slot.pending.size;
446
+ if (pendingSize < bestPending) {
447
+ bestSlot = slot;
448
+ bestPending = pendingSize;
449
+ bestNextSlotIndex = (index + 1) % slots.length;
450
+ if (bestPending === 0)
451
+ break;
452
+ }
453
+ }
454
+ return { slot: bestSlot, nextSlotIndex: bestNextSlotIndex };
455
+ }
456
+ function attachWorkerHandlers(worker, slot, getClosed, maxRespawns, log) {
457
+ worker.on('message', (message) => {
458
+ handleWorkerMessage(slot, message, log);
459
+ });
460
+ worker.on('error', (error) => {
461
+ handleWorkerError(slot, error, log);
462
+ });
463
+ worker.on('exit', (code) => {
464
+ handleWorkerExit(slot, code, getClosed(), maxRespawns, log);
465
+ });
466
+ }
467
+ class SearchWorkerPool {
468
+ slots;
469
+ debug;
470
+ nextRequestId = 0;
471
+ nextSlotIndex = 0;
472
+ closed = false;
473
+ constructor(options) {
474
+ this.debug = options.debug ?? false;
475
+ this.slots = [];
476
+ for (let i = 0; i < options.size; i++) {
477
+ this.slots.push({
478
+ worker: null,
479
+ pending: new Map(),
480
+ respawnCount: 0,
481
+ index: i,
482
+ });
483
+ }
484
+ }
485
+ log(message) {
486
+ if (this.debug) {
487
+ console.error(`[SearchWorkerPool] ${message}`);
488
+ }
489
+ }
490
+ spawnWorker(slot) {
491
+ this.log(`Spawning worker for slot ${String(slot.index)}`);
492
+ const workerOptions = {
493
+ workerData: {
494
+ debug: this.debug,
495
+ threadId: slot.index,
496
+ },
497
+ type: 'module',
498
+ execArgv: isSourceContext ? ['--import', 'tsx/esm'] : undefined,
499
+ };
500
+ const worker = new Worker(WORKER_SCRIPT_URL, workerOptions);
501
+ worker.unref();
502
+ const logEntry = (entry) => {
503
+ this.log(entry);
504
+ };
505
+ attachWorkerHandlers(worker, slot, () => this.closed, MAX_RESPAWNS, logEntry);
506
+ return worker;
507
+ }
508
+ getWorker(slot) {
509
+ slot.worker ??= this.spawnWorker(slot);
510
+ return slot.worker;
511
+ }
512
+ ensureOpen() {
513
+ if (this.closed) {
514
+ throw new Error('Worker pool is closed');
515
+ }
516
+ }
517
+ selectAvailableSlot() {
518
+ const selection = selectSlot(this.slots, this.nextSlotIndex, MAX_RESPAWNS);
519
+ this.nextSlotIndex = selection.nextSlotIndex;
520
+ if (!selection.slot) {
521
+ throw new Error('All worker slots are disabled');
522
+ }
523
+ return selection.slot;
524
+ }
525
+ buildScanRequest(id, request) {
526
+ return {
527
+ type: 'scan',
528
+ id,
529
+ ...request,
530
+ };
531
+ }
532
+ createScanPromise(slot, worker, scanRequest) {
533
+ let settled = false;
534
+ return new Promise((resolve, reject) => {
535
+ const safeResolve = (result) => {
536
+ if (settled)
537
+ return;
538
+ settled = true;
539
+ resolve(result);
540
+ };
541
+ const safeReject = (error) => {
542
+ if (settled)
543
+ return;
544
+ settled = true;
545
+ reject(error);
546
+ };
547
+ slot.pending.set(scanRequest.id, {
548
+ resolve: safeResolve,
549
+ reject: safeReject,
550
+ request: scanRequest,
551
+ });
552
+ worker.postMessage(scanRequest);
553
+ });
554
+ }
555
+ createCancel(slot, worker, id) {
556
+ return () => {
557
+ const pending = slot.pending.get(id);
558
+ if (!pending)
559
+ return;
560
+ slot.pending.delete(id);
561
+ worker.postMessage({ type: 'cancel', id });
562
+ pending.reject(new Error('Scan cancelled'));
563
+ };
564
+ }
565
+ scan(request) {
566
+ this.ensureOpen();
567
+ const slot = this.selectAvailableSlot();
568
+ const worker = this.getWorker(slot);
569
+ const id = this.nextRequestId++;
570
+ const scanRequest = this.buildScanRequest(id, request);
571
+ const promise = this.createScanPromise(slot, worker, scanRequest);
572
+ const cancel = this.createCancel(slot, worker, id);
573
+ const outcome = promise.then((result) => ({ task, result }), (error) => ({
574
+ task,
575
+ error: error instanceof Error ? error : new Error(String(error)),
576
+ }));
577
+ const task = { id, promise, cancel, outcome };
578
+ return task;
579
+ }
580
+ async close() {
581
+ if (this.closed)
582
+ return;
583
+ this.closed = true;
584
+ this.log('Closing worker pool');
585
+ for (const slot of this.slots) {
586
+ for (const [, pending] of slot.pending) {
587
+ pending.reject(new Error('Worker pool closed'));
588
+ }
589
+ slot.pending.clear();
590
+ }
591
+ const terminatePromises = [];
592
+ for (const slot of this.slots) {
593
+ if (slot.worker) {
594
+ slot.worker.postMessage({ type: 'shutdown' });
595
+ terminatePromises.push(slot.worker.terminate());
596
+ slot.worker = null;
597
+ }
598
+ }
599
+ await Promise.allSettled(terminatePromises);
600
+ this.log('Worker pool closed');
601
+ }
602
+ }
603
+ function isWorkerPoolAvailable() {
604
+ return !isSourceContext;
605
+ }
606
+ let poolInstance = null;
607
+ let poolSize = 0;
608
+ let poolDebug = false;
609
+ function getSearchWorkerPool(size, debug = false) {
610
+ if (size <= 0) {
611
+ throw new Error('Pool size must be positive');
612
+ }
613
+ if (poolInstance && poolSize === size && poolDebug === debug) {
614
+ return poolInstance;
615
+ }
616
+ if (poolInstance) {
617
+ void poolInstance.close();
618
+ }
619
+ poolInstance = new SearchWorkerPool({ size, debug });
620
+ poolSize = size;
621
+ poolDebug = debug;
622
+ return poolInstance;
623
+ }
624
+ function createParallelScanConfig(pattern, matcherOptions, scanOptions, maxResults, signal) {
625
+ const debug = process.env.FS_CONTEXT_SEARCH_WORKERS_DEBUG === '1';
626
+ return {
627
+ pool: getSearchWorkerPool(SEARCH_WORKERS, debug),
628
+ pattern,
629
+ matcherOptions,
630
+ scanOptions,
631
+ maxResults,
632
+ maxInFlight: Math.max(1, SEARCH_WORKERS),
633
+ signal,
634
+ };
635
+ }
636
+ function createParallelScanState(files, summary) {
637
+ return {
638
+ matches: [],
639
+ summary,
640
+ inFlight: new Set(),
641
+ iterator: files[Symbol.asyncIterator](),
642
+ done: false,
643
+ stoppedEarly: false,
644
+ };
645
+ }
646
+ function markTruncated(summary, reason) {
647
+ summary.truncated = true;
648
+ summary.stoppedReason = reason;
649
+ }
650
+ function cancelInFlight(inFlight) {
651
+ for (const task of inFlight) {
652
+ task.cancel();
653
+ void task.promise.catch(() => { });
654
+ }
655
+ inFlight.clear();
656
+ }
657
+ function stopIfSignaledOrLimited(config, state) {
658
+ if (!shouldStopOnSignalOrLimit(config.signal, state.matches.length, config.maxResults, state.summary)) {
659
+ return false;
660
+ }
661
+ state.stoppedEarly = true;
662
+ state.done = true;
663
+ cancelInFlight(state.inFlight);
664
+ return true;
665
+ }
666
+ async function awaitNextOutcome(inFlight) {
667
+ function* outcomes() {
668
+ for (const task of inFlight) {
669
+ yield task.outcome;
670
+ }
671
+ }
672
+ return await Promise.race(outcomes());
673
+ }
674
+ function handleWorkerOutcome(outcome, config, state) {
675
+ state.inFlight.delete(outcome.task);
676
+ if (outcome.error) {
677
+ if (outcome.error.message !== 'Scan cancelled') {
678
+ state.summary.skippedInaccessible++;
679
+ }
680
+ return;
681
+ }
682
+ const { result } = outcome;
683
+ if (!result)
684
+ return;
685
+ const remaining = config.maxResults - state.matches.length;
686
+ if (remaining <= 0) {
687
+ markTruncated(state.summary, 'maxResults');
688
+ return;
689
+ }
690
+ applyScanResult(result, state.matches, state.summary, remaining);
691
+ if (state.matches.length >= config.maxResults) {
692
+ markTruncated(state.summary, 'maxResults');
693
+ }
694
+ }
695
+ async function enqueueNextTask(config, state) {
696
+ if (stopIfSignaledOrLimited(config, state))
697
+ return;
698
+ const next = await state.iterator.next();
699
+ if (next.done) {
700
+ state.done = true;
701
+ return;
702
+ }
703
+ const remaining = Math.max(1, config.maxResults - state.matches.length);
704
+ const task = config.pool.scan({
705
+ resolvedPath: next.value.resolvedPath,
706
+ requestedPath: next.value.requestedPath,
707
+ pattern: config.pattern,
708
+ matcherOptions: config.matcherOptions,
709
+ scanOptions: config.scanOptions,
710
+ maxMatches: remaining,
711
+ });
712
+ state.inFlight.add(task);
713
+ }
714
+ async function fillInFlight(config, state) {
715
+ while (!state.done && state.inFlight.size < config.maxInFlight) {
716
+ await enqueueNextTask(config, state);
717
+ }
718
+ }
719
+ async function drainInFlight(config, state) {
720
+ await fillInFlight(config, state);
721
+ while (state.inFlight.size > 0) {
722
+ if (stopIfSignaledOrLimited(config, state))
723
+ break;
724
+ handleWorkerOutcome(await awaitNextOutcome(state.inFlight), config, state);
725
+ if (stopIfSignaledOrLimited(config, state))
726
+ break;
727
+ await fillInFlight(config, state);
728
+ }
729
+ }
730
+ async function finalizeParallelScan(state) {
731
+ if (!state.stoppedEarly)
732
+ return;
733
+ cancelInFlight(state.inFlight);
734
+ await state.iterator.return?.();
735
+ }
736
+ function attachAbortHandler(config, state) {
737
+ const onAbort = () => {
738
+ state.stoppedEarly = true;
739
+ markTruncated(state.summary, 'timeout');
740
+ cancelInFlight(state.inFlight);
741
+ };
742
+ config.signal.addEventListener('abort', onAbort, { once: true });
743
+ return onAbort;
744
+ }
745
+ async function scanFilesParallel(files, pattern, matcherOptions, scanOptions, maxResults, signal, summary) {
746
+ const config = createParallelScanConfig(pattern, matcherOptions, scanOptions, maxResults, signal);
747
+ const state = createParallelScanState(files, summary);
748
+ const onAbort = attachAbortHandler(config, state);
749
+ try {
750
+ await drainInFlight(config, state);
751
+ await finalizeParallelScan(state);
752
+ }
753
+ finally {
754
+ signal.removeEventListener('abort', onAbort);
755
+ }
756
+ return state.matches;
757
+ }
758
+ function buildMatcherOptions(opts) {
759
+ return {
760
+ caseSensitive: opts.caseSensitive,
761
+ wholeWord: opts.wholeWord,
762
+ isLiteral: opts.isLiteral,
763
+ };
764
+ }
765
+ function buildScanOptions(opts) {
766
+ return {
767
+ maxFileSize: opts.maxFileSize,
768
+ skipBinary: opts.skipBinary,
769
+ contextLines: opts.contextLines,
770
+ };
771
+ }
772
+ function shouldUseWorkers() {
773
+ // Multi-CPU only: require at least 2 workers to justify thread overhead.
774
+ return isWorkerPoolAvailable() && SEARCH_WORKERS >= 2;
775
+ }
776
+ function buildTraceContext(opts) {
777
+ if (!shouldPublishOpsTrace())
778
+ return undefined;
779
+ return {
780
+ op: 'searchContent',
781
+ engine: shouldUseWorkers() ? 'workers' : 'sequential',
782
+ maxResults: opts.maxResults,
783
+ };
784
+ }
785
+ async function withOpsTrace(context, run) {
786
+ if (!context) {
787
+ return await run();
788
+ }
789
+ publishOpsTraceStart(context);
790
+ try {
791
+ return await run();
792
+ }
793
+ catch (error) {
794
+ publishOpsTraceError(context, error);
795
+ throw error;
796
+ }
797
+ finally {
798
+ publishOpsTraceEnd(context);
799
+ }
800
+ }
801
+ async function scanMatches(files, pattern, matcherOptions, scanOptions, maxResults, signal, summary) {
802
+ if (shouldUseWorkers()) {
803
+ return await scanFilesParallel(files, pattern, matcherOptions, scanOptions, maxResults, signal, summary);
804
+ }
805
+ return await scanFilesSequential(files, pattern, matcherOptions, scanOptions, maxResults, signal, summary);
806
+ }
807
+ function buildSummary(summary, matches) {
808
+ const baseSummary = {
809
+ filesScanned: summary.filesScanned,
810
+ filesMatched: summary.filesMatched,
811
+ matches: matches.length,
812
+ truncated: summary.truncated,
813
+ skippedTooLarge: summary.skippedTooLarge,
814
+ skippedBinary: summary.skippedBinary,
815
+ skippedInaccessible: summary.skippedInaccessible,
816
+ linesSkippedDueToRegexTimeout: 0,
817
+ };
818
+ return {
819
+ ...baseSummary,
820
+ ...(summary.stoppedReason !== undefined
821
+ ? { stoppedReason: summary.stoppedReason }
822
+ : {}),
823
+ };
824
+ }
825
+ function buildSearchResult(root, pattern, filePattern, matches, summary) {
826
+ return {
827
+ basePath: root,
828
+ pattern,
829
+ filePattern,
830
+ matches,
831
+ summary: buildSummary(summary, matches),
832
+ };
833
+ }
834
+ async function executeSearch(root, pattern, opts, allowedDirs, signal) {
835
+ const matcherOptions = buildMatcherOptions(opts);
836
+ const scanOptions = buildScanOptions(opts);
837
+ const traceContext = buildTraceContext(opts);
838
+ return await withOpsTrace(traceContext, async () => {
839
+ validatePattern(pattern, matcherOptions);
840
+ const { stream, summary } = collectFilesStream(root, opts, allowedDirs, signal);
841
+ const matches = await scanMatches(stream, pattern, matcherOptions, scanOptions, opts.maxResults, signal, summary);
842
+ return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
843
+ });
844
+ }
845
+ export async function searchContent(basePath, pattern, options = {}) {
846
+ const opts = mergeOptions(options);
847
+ const root = await validateExistingDirectory(basePath, options.signal);
848
+ const { signal, cleanup } = createTimedAbortSignal(options.signal, opts.timeoutMs);
849
+ const allowedDirs = getAllowedDirectories();
850
+ try {
851
+ return await executeSearch(root, pattern, opts, allowedDirs, signal);
852
+ }
853
+ finally {
854
+ cleanup();
855
+ }
4
856
  }
5
857
  //# sourceMappingURL=search-content.js.map