@j0hanz/filesystem-mcp 2.1.8 → 2.3.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 (149) hide show
  1. package/README.md +41 -29
  2. package/dist/cli-help.d.ts.map +1 -1
  3. package/dist/cli-help.js +10 -2
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +10 -5
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/concurrency.d.ts.map +1 -1
  9. package/dist/core/concurrency.js +21 -6
  10. package/dist/core/concurrency.js.map +1 -1
  11. package/dist/core/config.d.ts +2 -0
  12. package/dist/core/config.d.ts.map +1 -1
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/cursor.d.ts +11 -0
  15. package/dist/core/cursor.d.ts.map +1 -1
  16. package/dist/core/cursor.js +10 -0
  17. package/dist/core/cursor.js.map +1 -1
  18. package/dist/core/errors.d.ts +6 -9
  19. package/dist/core/errors.d.ts.map +1 -1
  20. package/dist/core/errors.js +58 -102
  21. package/dist/core/errors.js.map +1 -1
  22. package/dist/core/file-uri.d.ts +5 -1
  23. package/dist/core/file-uri.d.ts.map +1 -1
  24. package/dist/core/file-uri.js +6 -2
  25. package/dist/core/file-uri.js.map +1 -1
  26. package/dist/core/fmt.d.ts +0 -1
  27. package/dist/core/fmt.d.ts.map +1 -1
  28. package/dist/core/fmt.js +0 -1
  29. package/dist/core/fmt.js.map +1 -1
  30. package/dist/core/fs.d.ts +31 -1
  31. package/dist/core/fs.d.ts.map +1 -1
  32. package/dist/core/fs.js +142 -2
  33. package/dist/core/fs.js.map +1 -1
  34. package/dist/core/glob.d.ts +4 -13
  35. package/dist/core/glob.d.ts.map +1 -1
  36. package/dist/core/glob.js +14 -14
  37. package/dist/core/glob.js.map +1 -1
  38. package/dist/core/input-required.d.ts +16 -17
  39. package/dist/core/input-required.d.ts.map +1 -1
  40. package/dist/core/input-required.js +42 -47
  41. package/dist/core/input-required.js.map +1 -1
  42. package/dist/core/observability.d.ts +9 -0
  43. package/dist/core/observability.d.ts.map +1 -1
  44. package/dist/core/observability.js +13 -1
  45. package/dist/core/observability.js.map +1 -1
  46. package/dist/core/page-store.d.ts +4 -3
  47. package/dist/core/page-store.d.ts.map +1 -1
  48. package/dist/core/page-store.js +4 -3
  49. package/dist/core/page-store.js.map +1 -1
  50. package/dist/core/primitives.d.ts +7 -0
  51. package/dist/core/primitives.d.ts.map +1 -1
  52. package/dist/core/primitives.js +15 -8
  53. package/dist/core/primitives.js.map +1 -1
  54. package/dist/core/search.d.ts +8 -4
  55. package/dist/core/search.d.ts.map +1 -1
  56. package/dist/core/search.js +23 -24
  57. package/dist/core/search.js.map +1 -1
  58. package/dist/core/sensitive.d.ts +4 -2
  59. package/dist/core/sensitive.d.ts.map +1 -1
  60. package/dist/core/sensitive.js +261 -43
  61. package/dist/core/sensitive.js.map +1 -1
  62. package/dist/core/util.d.ts.map +1 -1
  63. package/dist/core/util.js +2 -11
  64. package/dist/core/util.js.map +1 -1
  65. package/dist/core/watcher-registry.d.ts.map +1 -1
  66. package/dist/core/watcher-registry.js +5 -1
  67. package/dist/core/watcher-registry.js.map +1 -1
  68. package/dist/instructions.js +11 -11
  69. package/dist/instructions.js.map +1 -1
  70. package/dist/resources.d.ts.map +1 -1
  71. package/dist/resources.js +13 -4
  72. package/dist/resources.js.map +1 -1
  73. package/dist/server.d.ts +1 -9
  74. package/dist/server.d.ts.map +1 -1
  75. package/dist/server.js +22 -27
  76. package/dist/server.js.map +1 -1
  77. package/dist/tools/create.d.ts.map +1 -1
  78. package/dist/tools/create.js +163 -14
  79. package/dist/tools/create.js.map +1 -1
  80. package/dist/tools/define.d.ts +6 -1
  81. package/dist/tools/define.d.ts.map +1 -1
  82. package/dist/tools/define.js +29 -9
  83. package/dist/tools/define.js.map +1 -1
  84. package/dist/tools/delete.d.ts +2 -0
  85. package/dist/tools/delete.d.ts.map +1 -0
  86. package/dist/tools/{delete-file.js → delete.js} +12 -15
  87. package/dist/tools/delete.js.map +1 -0
  88. package/dist/tools/edit.d.ts.map +1 -1
  89. package/dist/tools/edit.js +77 -69
  90. package/dist/tools/edit.js.map +1 -1
  91. package/dist/tools/find-files.d.ts +2 -0
  92. package/dist/tools/find-files.d.ts.map +1 -0
  93. package/dist/tools/{search-files.js → find-files.js} +14 -19
  94. package/dist/tools/find-files.js.map +1 -0
  95. package/dist/tools/index.d.ts +7 -6
  96. package/dist/tools/index.d.ts.map +1 -1
  97. package/dist/tools/index.js +16 -15
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/list-roots.d.ts +2 -0
  100. package/dist/tools/list-roots.d.ts.map +1 -0
  101. package/dist/tools/{roots.js → list-roots.js} +2 -2
  102. package/dist/tools/list-roots.js.map +1 -0
  103. package/dist/tools/list.d.ts.map +1 -1
  104. package/dist/tools/list.js +28 -32
  105. package/dist/tools/list.js.map +1 -1
  106. package/dist/tools/move.d.ts.map +1 -1
  107. package/dist/tools/move.js +17 -36
  108. package/dist/tools/move.js.map +1 -1
  109. package/dist/tools/patch.js +3 -3
  110. package/dist/tools/patch.js.map +1 -1
  111. package/dist/tools/read.d.ts +1 -1
  112. package/dist/tools/read.d.ts.map +1 -1
  113. package/dist/tools/read.js +1 -1
  114. package/dist/tools/read.js.map +1 -1
  115. package/dist/tools/replace-text.d.ts +2 -0
  116. package/dist/tools/replace-text.d.ts.map +1 -0
  117. package/dist/tools/{replace-in-files.js → replace-text.js} +5 -5
  118. package/dist/tools/replace-text.js.map +1 -0
  119. package/dist/tools/search-text.d.ts +2 -0
  120. package/dist/tools/search-text.d.ts.map +1 -0
  121. package/dist/tools/{search-content.js → search-text.js} +71 -13
  122. package/dist/tools/search-text.js.map +1 -0
  123. package/dist/tools/stat.d.ts +1 -1
  124. package/dist/tools/stat.d.ts.map +1 -1
  125. package/dist/tools/stat.js +1 -1
  126. package/dist/tools/stat.js.map +1 -1
  127. package/dist/transport/http.d.ts.map +1 -1
  128. package/dist/transport/http.js +5 -1
  129. package/dist/transport/http.js.map +1 -1
  130. package/dist/transport/stdio.d.ts +1 -0
  131. package/dist/transport/stdio.d.ts.map +1 -1
  132. package/dist/transport/stdio.js +1 -0
  133. package/dist/transport/stdio.js.map +1 -1
  134. package/package.json +6 -9
  135. package/dist/tools/delete-file.d.ts +0 -2
  136. package/dist/tools/delete-file.d.ts.map +0 -1
  137. package/dist/tools/delete-file.js.map +0 -1
  138. package/dist/tools/replace-in-files.d.ts +0 -2
  139. package/dist/tools/replace-in-files.d.ts.map +0 -1
  140. package/dist/tools/replace-in-files.js.map +0 -1
  141. package/dist/tools/roots.d.ts +0 -2
  142. package/dist/tools/roots.d.ts.map +0 -1
  143. package/dist/tools/roots.js.map +0 -1
  144. package/dist/tools/search-content.d.ts +0 -2
  145. package/dist/tools/search-content.d.ts.map +0 -1
  146. package/dist/tools/search-content.js.map +0 -1
  147. package/dist/tools/search-files.d.ts +0 -2
  148. package/dist/tools/search-files.d.ts.map +0 -1
  149. package/dist/tools/search-files.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import { stat as fsStat, readFile } from 'node:fs/promises';
2
2
  import { basename } from 'node:path';
3
3
  import { RE2 } from '@adguard/re2-wasm';
4
- import { StopReasonTracker } from './concurrency.js';
5
4
  import { globEntries } from './glob.js';
6
5
  import { escapeRegexLiteral } from './primitives.js';
7
6
  import { getMaxTextFileSize } from './util.js';
@@ -134,12 +133,13 @@ export async function searchContent(directory, pattern, options, pathGuard) {
134
133
  const matches = [];
135
134
  const maxResults = options.maxResults ?? 100;
136
135
  const maxFileSize = getMaxTextFileSize();
136
+ const context = options.context ?? 0;
137
137
  const entries = globEntries({
138
138
  cwd: directory,
139
139
  pattern: options.filePattern ?? '**/*',
140
- excludePatterns: options.excludePatterns ?? [],
141
140
  includeHidden: Boolean(options.includeHidden),
142
- respectGitignore: Boolean(options.respectGitignore),
141
+ skipIgnored: Boolean(options.skipIgnored),
142
+ ...(options.signal ? { signal: options.signal } : {}),
143
143
  maxDepth: options.maxDepth ?? 100,
144
144
  suppressErrors: true,
145
145
  });
@@ -168,6 +168,9 @@ export async function searchContent(directory, pattern, options, pathGuard) {
168
168
  try {
169
169
  const content = await readFile(entry.path, { encoding: 'utf-8', signal: options.signal });
170
170
  const lines = content.split('\n');
171
+ // A trailing newline splits into a phantom empty last element; context
172
+ // must not report it as a line the file has.
173
+ const lineCount = content.endsWith('\n') ? lines.length - 1 : lines.length;
171
174
  let matchedFile = false;
172
175
  for (let i = 0; i < lines.length; i++) {
173
176
  const line = lines[i];
@@ -185,6 +188,12 @@ export async function searchContent(directory, pattern, options, pathGuard) {
185
188
  column: found.column,
186
189
  content: line,
187
190
  matchCount: found.count,
191
+ ...(context > 0
192
+ ? {
193
+ before: lines.slice(Math.max(0, i - context), i),
194
+ after: lines.slice(i + 1, Math.min(lineCount, i + 1 + context)),
195
+ }
196
+ : {}),
188
197
  });
189
198
  if (matches.length >= maxResults)
190
199
  break;
@@ -204,14 +213,10 @@ export async function searchContent(directory, pattern, options, pathGuard) {
204
213
  // ignore read errors (e.g. binary files)
205
214
  }
206
215
  }
207
- const tracker = new StopReasonTracker();
208
- if (matches.length >= maxResults)
209
- tracker.hitMaxResults();
210
- if (counters.stoppedByAbort)
211
- tracker.hitAbort();
212
- // resolve() is StoppedReason | undefined, but this scan only records
213
- // maxResults/timeout stops (see the summary type's narrowing note).
214
- const stoppedReason = tracker.resolve();
216
+ // The result cap is the definite cause even when the abort fired on the same
217
+ // iteration, matching StopReasonTracker's precedence for the two stops this
218
+ // scan can record (see the summary type's narrowing note).
219
+ const stoppedReason = matches.length >= maxResults ? 'maxResults' : counters.stoppedByAbort ? 'timeout' : undefined;
215
220
  return {
216
221
  basePath: directory,
217
222
  matches,
@@ -219,7 +224,7 @@ export async function searchContent(directory, pattern, options, pathGuard) {
219
224
  matchingLines,
220
225
  filesScanned,
221
226
  filesMatched,
222
- truncated: tracker.truncated,
227
+ truncated: stoppedReason !== undefined,
223
228
  skippedInaccessible: counters.skippedInaccessible,
224
229
  skippedTooLarge,
225
230
  ...(stoppedReason ? { stoppedReason } : {}),
@@ -249,14 +254,14 @@ async function* guardedEntries(entries, pathGuard, signal, counters) {
249
254
  yield entry;
250
255
  }
251
256
  }
252
- export async function searchFiles(directory, pattern, excludePatterns, options, pathGuard) {
257
+ export async function searchFiles(directory, pattern, options, pathGuard) {
253
258
  const maxResults = options.maxResults ?? 100;
254
259
  const entries = globEntries({
255
260
  cwd: directory,
256
261
  pattern,
257
- excludePatterns,
258
262
  includeHidden: Boolean(options.includeHidden),
259
- respectGitignore: Boolean(options.respectGitignore),
263
+ skipIgnored: Boolean(options.skipIgnored),
264
+ ...(options.signal ? { signal: options.signal } : {}),
260
265
  maxDepth: options.maxDepth ?? 100,
261
266
  suppressErrors: true,
262
267
  });
@@ -277,21 +282,15 @@ export async function searchFiles(directory, pattern, excludePatterns, options,
277
282
  else {
278
283
  results.sort((a, b) => a.path.localeCompare(b.path));
279
284
  }
280
- const tracker = new StopReasonTracker();
281
- if (results.length >= maxResults)
282
- tracker.hitMaxResults();
283
- if (counters.stoppedByAbort)
284
- tracker.hitAbort();
285
- // resolve() is StoppedReason | undefined, but this scan only records
286
- // maxResults/timeout stops (see the summary type's narrowing note).
287
- const stoppedReason = tracker.resolve();
285
+ // Same precedence as above: the result cap wins over a same-iteration abort.
286
+ const stoppedReason = results.length >= maxResults ? 'maxResults' : counters.stoppedByAbort ? 'timeout' : undefined;
288
287
  return {
289
288
  basePath: directory,
290
289
  results,
291
290
  summary: {
292
291
  matched: results.length,
293
292
  filesScanned,
294
- truncated: tracker.truncated,
293
+ truncated: stoppedReason !== undefined,
295
294
  skippedInaccessible: counters.skippedInaccessible,
296
295
  ...(stoppedReason ? { stoppedReason } : {}),
297
296
  },
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat as fsStat, readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport type { RE2ExecArray } from '@adguard/re2-wasm';\nimport { RE2 } from '@adguard/re2-wasm';\n\nimport { StopReasonTracker } 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 /** 0-indexed column of the first occurrence on the line. */\n column: 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\nexport interface RegexMatch {\n /** The matched text. */\n text: string;\n /** Capture groups, 1-indexed in the pattern, 0-indexed here. */\n groups: (string | undefined)[];\n /** Named capture groups, when the pattern declares any. */\n named: Record<string, string> | undefined;\n /** Start offset in UTF-16 code units — usable with `String.prototype.slice`. */\n start: number;\n /** End offset in UTF-16 code units. */\n end: number;\n}\n\n/** Code-point length of `text`, i.e. its UTF-16 length minus its surrogate pairs. */\nfunction codePointLength(text: string): number {\n let length = 0;\n for (let i = 0; i < text.length; i++) {\n if ((text.charCodeAt(i) & 0xfc00) === 0xd800 && (text.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {\n i++;\n }\n length++;\n }\n return length;\n}\n\n/**\n * Iterate a global pattern's non-overlapping matches with offsets JS can use.\n *\n * re2-wasm reports `index` and `lastIndex` in **code points**, while every JS\n * string operation indexes in UTF-16 code units. The two agree only until the\n * first astral character (most emoji), past which every offset is short by one\n * per surrogate pair — slicing on the raw index cuts into the middle of\n * neighbouring text. Its `lastIndex` bookkeeping is worse still: it adds the\n * match's UTF-16 length to a code-point index, so an astral character inside a\n * match can skip the next one. This is the single place that corrects both; no\n * caller should read `match.index` or `regex.lastIndex` directly.\n *\n * Zero-length matches (e.g. `a*`) advance by one code point so they cannot loop\n * forever. The regex is global and shared across calls, so `lastIndex` is reset\n * on entry.\n */\nexport function* execMatches(regex: Regex, text: string): Generator<RegexMatch, undefined> {\n regex.lastIndex = 0;\n // Cursor into `text`, carried forward across matches: RE2 reports them in\n // ascending order, so the walk is O(text) in total rather than per match.\n let cursorCp = 0;\n let cursorU16 = 0;\n let match: RE2ExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n while (cursorCp < match.index && cursorU16 < text.length) {\n const isPair =\n (text.charCodeAt(cursorU16) & 0xfc00) === 0xd800 &&\n (text.charCodeAt(cursorU16 + 1) & 0xfc00) === 0xdc00;\n cursorU16 += isPair ? 2 : 1;\n cursorCp++;\n }\n const matched = match[0] ?? '';\n const start = cursorU16;\n yield {\n text: matched,\n groups: match.slice(1),\n named: match.groups,\n start,\n end: start + matched.length,\n };\n // Own the advance rather than trusting the wrapper's mixed-unit arithmetic.\n regex.lastIndex = match.index + (matched.length === 0 ? 1 : codePointLength(matched));\n }\n}\n\n/**\n * Find non-overlapping occurrences of a global regex in a single line, reporting\n * the first match's column alongside the count.\n */\nfunction findLineMatches(\n regex: Regex,\n line: string,\n): { count: number; column: number } | undefined {\n let count = 0;\n let column = -1;\n for (const match of execMatches(regex, line)) {\n if (column === -1) column = match.start;\n count++;\n if (count >= MAX_MATCHES_PER_LINE) break;\n }\n return count > 0 ? { count, column } : undefined;\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 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 /**\n * `StoppedReason` narrowed to the stops these scans can produce: both call\n * only `hitMaxResults`/`hitAbort` (see concurrency.ts) — never\n * `hitMaxFiles`, which belongs to `replace_text`'s per-file cap.\n */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}\n\nexport async function searchContent(\n directory: string,\n pattern: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n): Promise<SearchContentOutcome> {\n const regex = compileRegex(options.isRegex ? pattern || '' : escapeRegexLiteral(pattern || ''), {\n caseSensitive: Boolean(options.caseSensitive),\n });\n try {\n const matches: SearchResult[] = [];\n const maxResults = options.maxResults ?? 100;\n const 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: findLineMatches resets lastIndex itself, so it\n // doubles as the \"does this line match\" test.\n const found = findLineMatches(regex, line);\n if (found) {\n matchedFile = true;\n matchingLines++;\n matches.push({\n file: entry.path,\n line: i + 1,\n column: found.column,\n content: line,\n matchCount: found.count,\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 // resolve() is StoppedReason | undefined, but this scan only records\n // maxResults/timeout stops (see the summary type's narrowing note).\n const stoppedReason = tracker.resolve() as 'maxResults' | 'timeout' | undefined;\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 } 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\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 /** Narrowed like SearchContentOutcome's — scans never hit `hitMaxFiles`. */\n stoppedReason?: 'maxResults' | 'timeout';\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) => basename(a.path).localeCompare(basename(b.path)));\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 // resolve() is StoppedReason | undefined, but this scan only records\n // maxResults/timeout stops (see the summary type's narrowing note).\n const stoppedReason = tracker.resolve() as 'maxResults' | 'timeout' | undefined;\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"]}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat as fsStat, readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport type { RE2ExecArray } from '@adguard/re2-wasm';\nimport { RE2 } from '@adguard/re2-wasm';\n\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 /** 0-indexed column of the first occurrence on the line. */\n column: number;\n content: string;\n matchCount?: number;\n /** Up to `context` lines either side of the match; absent when context is 0. */\n before?: string[];\n after?: string[];\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\nexport interface RegexMatch {\n /** The matched text. */\n text: string;\n /** Capture groups, 1-indexed in the pattern, 0-indexed here. */\n groups: (string | undefined)[];\n /** Named capture groups, when the pattern declares any. */\n named: Record<string, string> | undefined;\n /** Start offset in UTF-16 code units — usable with `String.prototype.slice`. */\n start: number;\n /** End offset in UTF-16 code units. */\n end: number;\n}\n\n/** Code-point length of `text`, i.e. its UTF-16 length minus its surrogate pairs. */\nfunction codePointLength(text: string): number {\n let length = 0;\n for (let i = 0; i < text.length; i++) {\n if ((text.charCodeAt(i) & 0xfc00) === 0xd800 && (text.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {\n i++;\n }\n length++;\n }\n return length;\n}\n\n/**\n * Iterate a global pattern's non-overlapping matches with offsets JS can use.\n *\n * re2-wasm reports `index` and `lastIndex` in **code points**, while every JS\n * string operation indexes in UTF-16 code units. The two agree only until the\n * first astral character (most emoji), past which every offset is short by one\n * per surrogate pair — slicing on the raw index cuts into the middle of\n * neighbouring text. Its `lastIndex` bookkeeping is worse still: it adds the\n * match's UTF-16 length to a code-point index, so an astral character inside a\n * match can skip the next one. This is the single place that corrects both; no\n * caller should read `match.index` or `regex.lastIndex` directly.\n *\n * Zero-length matches (e.g. `a*`) advance by one code point so they cannot loop\n * forever. The regex is global and shared across calls, so `lastIndex` is reset\n * on entry.\n */\nexport function* execMatches(regex: Regex, text: string): Generator<RegexMatch, undefined> {\n regex.lastIndex = 0;\n // Cursor into `text`, carried forward across matches: RE2 reports them in\n // ascending order, so the walk is O(text) in total rather than per match.\n let cursorCp = 0;\n let cursorU16 = 0;\n let match: RE2ExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n while (cursorCp < match.index && cursorU16 < text.length) {\n const isPair =\n (text.charCodeAt(cursorU16) & 0xfc00) === 0xd800 &&\n (text.charCodeAt(cursorU16 + 1) & 0xfc00) === 0xdc00;\n cursorU16 += isPair ? 2 : 1;\n cursorCp++;\n }\n const matched = match[0] ?? '';\n const start = cursorU16;\n yield {\n text: matched,\n groups: match.slice(1),\n named: match.groups,\n start,\n end: start + matched.length,\n };\n // Own the advance rather than trusting the wrapper's mixed-unit arithmetic.\n regex.lastIndex = match.index + (matched.length === 0 ? 1 : codePointLength(matched));\n }\n}\n\n/**\n * Find non-overlapping occurrences of a global regex in a single line, reporting\n * the first match's column alongside the count.\n */\nfunction findLineMatches(\n regex: Regex,\n line: string,\n): { count: number; column: number } | undefined {\n let count = 0;\n let column = -1;\n for (const match of execMatches(regex, line)) {\n if (column === -1) column = match.start;\n count++;\n if (count >= MAX_MATCHES_PER_LINE) break;\n }\n return count > 0 ? { count, column } : undefined;\n}\n\nexport interface SearchContentOptions {\n caseSensitive?: boolean;\n isRegex?: boolean;\n maxResults?: number;\n filePattern?: string;\n skipIgnored?: boolean;\n includeHidden?: boolean;\n maxDepth?: number;\n /** Lines of context to carry either side of each match; 0 (default) carries none. */\n context?: 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 /**\n * `StoppedReason` narrowed to the stops these scans can produce: both call\n * only `hitMaxResults`/`hitAbort` (see concurrency.ts) — never\n * `hitMaxFiles`, which belongs to `replace_text`'s per-file cap.\n */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}\n\nexport async function searchContent(\n directory: string,\n pattern: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n): Promise<SearchContentOutcome> {\n const regex = compileRegex(options.isRegex ? pattern || '' : escapeRegexLiteral(pattern || ''), {\n caseSensitive: Boolean(options.caseSensitive),\n });\n try {\n const matches: SearchResult[] = [];\n const maxResults = options.maxResults ?? 100;\n const maxFileSize = getMaxTextFileSize();\n const context = options.context ?? 0;\n\n const entries = globEntries({\n cwd: directory,\n pattern: options.filePattern ?? '**/*',\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(options.signal ? { signal: options.signal } : {}),\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 // A trailing newline splits into a phantom empty last element; context\n // must not report it as a line the file has.\n const lineCount = content.endsWith('\\n') ? lines.length - 1 : lines.length;\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: findLineMatches resets lastIndex itself, so it\n // doubles as the \"does this line match\" test.\n const found = findLineMatches(regex, line);\n if (found) {\n matchedFile = true;\n matchingLines++;\n matches.push({\n file: entry.path,\n line: i + 1,\n column: found.column,\n content: line,\n matchCount: found.count,\n ...(context > 0\n ? {\n before: lines.slice(Math.max(0, i - context), i),\n after: lines.slice(i + 1, Math.min(lineCount, i + 1 + context)),\n }\n : {}),\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 // The result cap is the definite cause even when the abort fired on the same\n // iteration, matching StopReasonTracker's precedence for the two stops this\n // scan can record (see the summary type's narrowing note).\n const stoppedReason =\n matches.length >= maxResults ? 'maxResults' : counters.stoppedByAbort ? 'timeout' : undefined;\n\n return {\n basePath: directory,\n matches,\n summary: {\n matchingLines,\n filesScanned,\n filesMatched,\n truncated: stoppedReason !== undefined,\n skippedInaccessible: counters.skippedInaccessible,\n skippedTooLarge,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\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\nexport async function searchFiles(\n directory: string,\n pattern: string,\n options: {\n maxResults?: number;\n includeHidden?: boolean;\n sortBy?: 'name' | 'path';\n skipIgnored?: 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 /** Narrowed like SearchContentOutcome's — scans never hit `hitMaxFiles`. */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}> {\n const maxResults = options.maxResults ?? 100;\n const entries = globEntries({\n cwd: directory,\n pattern,\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(options.signal ? { signal: options.signal } : {}),\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) => basename(a.path).localeCompare(basename(b.path)));\n } else {\n results.sort((a, b) => a.path.localeCompare(b.path));\n }\n\n // Same precedence as above: the result cap wins over a same-iteration abort.\n const stoppedReason =\n results.length >= maxResults ? 'maxResults' : counters.stoppedByAbort ? 'timeout' : undefined;\n\n return {\n basePath: directory,\n results,\n summary: {\n matched: results.length,\n filesScanned,\n truncated: stoppedReason !== undefined,\n skippedInaccessible: counters.skippedInaccessible,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  export declare class SensitiveMatcher {
2
- private readonly patterns;
3
- constructor(patterns?: readonly string[]);
2
+ private readonly builtin;
3
+ private readonly operator;
4
+ private readonly allow;
5
+ constructor(patterns?: readonly string[], allow?: readonly string[]);
4
6
  isSensitive(filePath: string): boolean;
5
7
  }
6
8
  //# sourceMappingURL=sensitive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sensitive.d.ts","sourceRoot":"","sources":["../../src/core/sensitive.ts"],"names":[],"mappings":"AAiJA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,QAAQ,GAAE,SAAS,MAAM,EAA6B;IAIlE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;CAWvC"}
1
+ {"version":3,"file":"sensitive.d.ts","sourceRoot":"","sources":["../../src/core/sensitive.ts"],"names":[],"mappings":"AAoUA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;gBAK/B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE;IAanE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;CAcvC"}
@@ -8,27 +8,178 @@ import { normalize, posix, sep } from 'node:path';
8
8
  import { cli } from './config.js';
9
9
  import { IS_WINDOWS, isAlpha, parseTrueEnvFlag, toPosixPath } from './primitives.js';
10
10
  const CHAR_COLON = 58;
11
- const CHAR_FORWARD_SLASH = 47;
12
11
  function normalizeForMatch(input) {
13
12
  // Always lowercase for case-insensitive denylist matching on all platforms.
14
- return toPosixPath(normalize(input)).toLowerCase();
13
+ // normalize() preserves the double leading slash of UNC paths
14
+ // (//server/share/...) on Windows; collapse it to one so the compiled
15
+ // patterns — which carry no leading slash — still match their segments.
16
+ return toPosixPath(normalize(input)).toLowerCase().replace(/^\/+/u, '/');
15
17
  }
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)));
18
+ // posix.matchesGlob runs with dot:false semantics and does not honor a dot
19
+ // option at all (verified on Node 24): '*' and '**' never match dot-leading
20
+ // path segments, so a deny of `secrets/**` silently let `secrets/.env`
21
+ // through — fail-open against exactly the hidden files this list exists to
22
+ // guard. Compile our own matcher instead. Supported metacharacters: '*'
23
+ // (any run within a segment), '**' (any run of whole segments), '?' (one
24
+ // char), '[...]' classes, and '{a,b}' alternation; everything else is
25
+ // literal, and dot-leading segments match like any other.
26
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
+ // Character classes can carry '{', '}' and ',' as members, so every scanner
28
+ // below must skip a class's interior. classEnd returns the index of the ']'
29
+ // closing the class opened at openIdx, or -1 when unclosed (then '[' is a
30
+ // literal, matching globToRegExp's fallback). A ']' right after '[' or the
31
+ // '!' negation is a literal member, per glob syntax.
32
+ function classEnd(glob, openIdx) {
33
+ let i = openIdx + 1;
34
+ if (glob[i] === '!')
35
+ i++;
36
+ if (glob[i] === ']')
37
+ i++;
38
+ while (i < glob.length && glob[i] !== ']')
39
+ i++;
40
+ return i < glob.length ? i : -1;
21
41
  }
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}`);
42
+ function expandBraces(glob) {
43
+ // First '{' outside any [...] class.
44
+ let open = -1;
45
+ for (let i = 0; i < glob.length; i++) {
46
+ const char = glob[i] ?? '';
47
+ if (char === '[') {
48
+ const end = classEnd(glob, i);
49
+ if (end !== -1)
50
+ i = end;
51
+ }
52
+ else if (char === '{') {
53
+ open = i;
54
+ break;
55
+ }
56
+ }
57
+ if (open === -1)
58
+ return [glob];
59
+ // Pair the outer '{' with ITS matching '}' — indexOf would grab the inner
60
+ // group's closer and produce garbage like 'data/x}/secret'. Then split the
61
+ // body on depth-0 commas so nested groups stay intact for the recursion.
62
+ let depth = 0;
63
+ let close = -1;
64
+ for (let i = open; i < glob.length; i++) {
65
+ const char = glob[i] ?? '';
66
+ if (char === '[') {
67
+ const end = classEnd(glob, i);
68
+ if (end !== -1) {
69
+ i = end;
70
+ continue;
71
+ }
72
+ }
73
+ if (char === '{')
74
+ depth++;
75
+ else if (char === '}') {
76
+ depth--;
77
+ if (depth === 0) {
78
+ close = i;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ if (close === -1)
84
+ return [glob];
85
+ const prefix = glob.slice(0, open);
86
+ const suffix = glob.slice(close + 1);
87
+ const body = glob.slice(open + 1, close);
88
+ const parts = [];
89
+ let start = 0;
90
+ depth = 0;
91
+ for (let i = 0; i < body.length; i++) {
92
+ const char = body[i] ?? '';
93
+ if (char === '[') {
94
+ const end = classEnd(body, i);
95
+ if (end !== -1) {
96
+ i = end;
97
+ continue;
98
+ }
99
+ }
100
+ if (char === '{')
101
+ depth++;
102
+ else if (char === '}')
103
+ depth--;
104
+ else if (char === ',' && depth === 0) {
105
+ parts.push(body.slice(start, i));
106
+ start = i + 1;
29
107
  }
30
108
  }
31
- return Array.from(globs);
109
+ parts.push(body.slice(start));
110
+ return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`));
111
+ }
112
+ function globToRegExp(glob) {
113
+ const segments = glob.split('/');
114
+ let source = '';
115
+ for (let i = 0; i < segments.length; i++) {
116
+ const segment = segments[i] ?? '';
117
+ const last = i === segments.length - 1;
118
+ if (segment === '**') {
119
+ source += last ? '(?:.*)?' : '(?:[^/]+/)*';
120
+ continue;
121
+ }
122
+ for (let j = 0; j < segment.length; j++) {
123
+ const char = segment[j] ?? '';
124
+ if (char === '*') {
125
+ while (segment[j + 1] === '*')
126
+ j++;
127
+ source += '[^/]*';
128
+ }
129
+ else if (char === '?') {
130
+ source += '[^/]';
131
+ }
132
+ else if (char === '[') {
133
+ // Same classEnd rule the scanners use: a ']' right after '[' (or
134
+ // after the '!' negation) is a literal member, not the closer.
135
+ const close = classEnd(segment, j);
136
+ if (close !== -1) {
137
+ // Pass the class through, flipping glob '!' negation to '^' and
138
+ // escaping backslashes so no body can make new RegExp throw. A
139
+ // leading ']' must be escaped or the 'u' flag parses '[]a]' as an
140
+ // empty class followed by stray literals; a leading '^' is a
141
+ // literal member (glob negation is '!' only), unescaped it would
142
+ // negate the class and relieve more than the operator named.
143
+ let body = segment.slice(j + 1, close).replace(/\\/gu, '\\\\');
144
+ const negated = body.startsWith('!');
145
+ if (negated)
146
+ body = body.slice(1);
147
+ if (body.startsWith(']') || body.startsWith('^'))
148
+ body = `\\${body}`;
149
+ source += `[${negated ? '^' : ''}${body}]`;
150
+ j = close;
151
+ }
152
+ else {
153
+ source += escapeRegExp(char);
154
+ }
155
+ }
156
+ else {
157
+ source += escapeRegExp(char);
158
+ }
159
+ }
160
+ if (!last)
161
+ source += '/';
162
+ }
163
+ // Candidates are absolute paths but patterns carry no leading '/', so the
164
+ // anchor optionally eats the root slash — `**/` then reaches from any
165
+ // parent, exactly as matchesGlob's globstar did. '$' is strict in
166
+ // JavaScript (it matches end-of-input only, unlike PCRE's, which also
167
+ // matches before a final newline), so an exact-name pattern can never
168
+ // match a '.env\n' lookalike. The 's' flag makes the terminal-`**` `.`
169
+ // match newlines too: filenames can contain them, and a deny ending in
170
+ // '/**' must not fail open on 'secrets/plan\nnotes'.
171
+ return new RegExp(`^/?${source}$`, 'su');
172
+ }
173
+ function compilePatternGlobs(normalizedPattern) {
174
+ // A leading '/' (POSIX-absolute) is rooted like a Windows drive path:
175
+ // aliasing '/abs/secret' as '**/abs/secret' would also match
176
+ // 'other/abs/secret', and on the allow side that is over-relief. The
177
+ // drive-letter regex is safe on [a-z] because normalizeForMatch lowercased.
178
+ const isRooted = normalizedPattern.startsWith('/') ||
179
+ normalizedPattern.startsWith('**/') ||
180
+ /^[a-z]:\//u.test(normalizedPattern);
181
+ const withoutRoot = isRooted ? '' : normalizedPattern.replace(/^\/+/, '');
182
+ return withoutRoot ? [normalizedPattern, `**/${withoutRoot}`] : [normalizedPattern];
32
183
  }
33
184
  function toPatternSet(patterns) {
34
185
  const pathGlobs = new Set();
@@ -38,8 +189,11 @@ function toPatternSet(patterns) {
38
189
  const normalized = normalizeForMatch(pattern);
39
190
  const matchesPath = normalized.includes('/');
40
191
  const target = matchesPath ? pathGlobs : nameGlobs;
41
- for (const glob of matchesPath ? compilePatternGlobs(normalized) : [normalized]) {
42
- target.add(glob);
192
+ const globs = matchesPath ? compilePatternGlobs(normalized) : [normalized];
193
+ for (const glob of globs) {
194
+ for (const expanded of expandBraces(glob)) {
195
+ target.add(globToRegExp(expanded));
196
+ }
43
197
  }
44
198
  }
45
199
  return {
@@ -48,13 +202,7 @@ function toPatternSet(patterns) {
48
202
  };
49
203
  }
50
204
  function matchesAnyGlob(globs, candidate) {
51
- if (globs.length === 0)
52
- return false;
53
- for (const glob of globs) {
54
- if (posix.matchesGlob(candidate, glob))
55
- return true;
56
- }
57
- return false;
205
+ return globs.some((glob) => glob.test(candidate));
58
206
  }
59
207
  // Strip NTFS alternate-data-stream suffixes (the ":stream" after a segment)
60
208
  // before denylist matching, so `.env:secret` is caught as `.env`. Preserves the
@@ -99,36 +247,106 @@ const DEFAULT_SENSITIVE_PATTERNS = [
99
247
  '*id_rsa*',
100
248
  '*id_dsa*',
101
249
  ];
102
- function buildSensitivePatterns() {
250
+ // Built-ins and operator-supplied deny entries live in separate tiers because
251
+ // they answer to different relief switches: an allow entry (FS_ALLOWLIST /
252
+ // --allow) can lift a built-in hit, but never an explicit deny.
253
+ // Split an env pattern list on commas and newlines at brace depth 0 only and
254
+ // outside any [...] class, so documented syntax like '*.{pem,key}' or
255
+ // 'x[1,2].env' survives as one pattern instead of tearing apart.
256
+ function splitPatternList(value) {
257
+ const naive = () => value
258
+ .split(/,|\n/u)
259
+ .map((t) => t.trim())
260
+ .filter((t) => t.length > 0);
261
+ const parts = [];
262
+ let depth = 0;
263
+ let start = 0;
264
+ let unbalanced = false;
265
+ for (let i = 0; i < value.length; i++) {
266
+ const char = value[i] ?? '';
267
+ if (char === '[') {
268
+ const end = classEnd(value, i);
269
+ if (end !== -1) {
270
+ i = end;
271
+ continue;
272
+ }
273
+ }
274
+ if (char === '{')
275
+ depth++;
276
+ else if (char === '}') {
277
+ if (depth === 0)
278
+ unbalanced = true;
279
+ else
280
+ depth--;
281
+ }
282
+ else if (depth === 0 && (char === ',' || char === '\n')) {
283
+ parts.push(value.slice(start, i));
284
+ start = i + 1;
285
+ }
286
+ }
287
+ // An unbalanced brace would swallow every separator behind it into one
288
+ // bogus literal pattern and silently drop the real entries — '{bad,
289
+ // secrets/**' must not lose 'secrets/**'. A typo'd brace costs the brace
290
+ // pattern only: fall back to the naive split.
291
+ if (unbalanced || depth !== 0)
292
+ return naive();
293
+ parts.push(value.slice(start));
294
+ return parts.map((t) => t.trim()).filter((t) => t.length > 0);
295
+ }
296
+ function buildDenyTiers() {
103
297
  const allowSensitive = cli.allowSensitive ?? parseTrueEnvFlag(process.env['FS_ALLOW_SENSITIVE'], 'FS_ALLOW_SENSITIVE');
104
- const envValue = process.env['FS_DENYLIST'];
105
- const envDenylist = envValue
106
- ? envValue
107
- .split(/[,\n]/u)
108
- .map((t) => t.trim())
109
- .filter((t) => t.length > 0)
298
+ const envDenylist = process.env['FS_DENYLIST']
299
+ ? splitPatternList(process.env['FS_DENYLIST'])
110
300
  : [];
111
301
  const flagDenylist = cli.denyPatterns ?? [];
112
302
  // FS_ALLOW_SENSITIVE suppresses built-ins only; deny entries (env and --deny)
113
303
  // always apply. Set-dedupe so a pattern in both sources matches once.
114
- return [
115
- ...(allowSensitive ? [] : DEFAULT_SENSITIVE_PATTERNS),
116
- ...new Set([...envDenylist, ...flagDenylist]),
117
- ];
304
+ return {
305
+ builtin: allowSensitive ? [] : DEFAULT_SENSITIVE_PATTERNS,
306
+ operator: [...new Set([...envDenylist, ...flagDenylist])],
307
+ };
118
308
  }
309
+ function buildAllowPatterns() {
310
+ const envAllowlist = process.env['FS_ALLOWLIST']
311
+ ? splitPatternList(process.env['FS_ALLOWLIST'])
312
+ : [];
313
+ const flagAllowlist = cli.allowPatterns ?? [];
314
+ return [...new Set([...envAllowlist, ...flagAllowlist])];
315
+ }
316
+ const EMPTY_PATTERN_SET = { pathGlobs: [], nameGlobs: [] };
119
317
  export class SensitiveMatcher {
120
- patterns;
121
- constructor(patterns = buildSensitivePatterns()) {
122
- this.patterns = toPatternSet(patterns);
318
+ builtin;
319
+ operator;
320
+ allow;
321
+ // An explicit pattern list (tests, custom guards) is one relievable tier;
322
+ // the default construction splits built-ins from operator-supplied
323
+ // FS_DENYLIST/--deny entries, which allow entries can never lift.
324
+ constructor(patterns, allow) {
325
+ if (patterns !== undefined || allow !== undefined) {
326
+ this.builtin = toPatternSet(patterns ?? []);
327
+ this.operator = EMPTY_PATTERN_SET;
328
+ this.allow = toPatternSet(allow ?? []);
329
+ }
330
+ else {
331
+ const tiers = buildDenyTiers();
332
+ this.builtin = toPatternSet(tiers.builtin);
333
+ this.operator = toPatternSet(tiers.operator);
334
+ this.allow = toPatternSet(buildAllowPatterns());
335
+ }
123
336
  }
124
337
  isSensitive(filePath) {
125
- if (this.patterns.pathGlobs.length === 0 && this.patterns.nameGlobs.length === 0) {
126
- return false;
127
- }
128
338
  const pathToCheck = IS_WINDOWS ? stripAlternateDataStreams(filePath) : filePath;
129
339
  const normalizedPath = normalizeForMatch(pathToCheck);
130
- return (matchesAnyGlob(this.patterns.pathGlobs, normalizedPath) ||
131
- matchesAnyGlob(this.patterns.nameGlobs, posix.basename(normalizedPath)));
340
+ const name = posix.basename(normalizedPath);
341
+ const matches = (set) => matchesAnyGlob(set.pathGlobs, normalizedPath) || matchesAnyGlob(set.nameGlobs, name);
342
+ // Explicit deny entries (FS_DENYLIST/--deny) always apply.
343
+ if (matches(this.operator))
344
+ return true;
345
+ if (!matches(this.builtin))
346
+ return false;
347
+ // A built-in hit is lifted only by an allow entry that matches the same
348
+ // normalized candidate — a pattern that matches nothing relieves nothing.
349
+ return !matches(this.allow);
132
350
  }
133
351
  }
134
352
  //# sourceMappingURL=sensitive.js.map