@wrongstack/plugins 0.295.0 → 0.296.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 (110) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +31 -3
  3. package/dist/agent-handoff/index.d.ts.map +1 -1
  4. package/dist/agent-handoff.js +30 -1
  5. package/dist/auto-doc/index.d.ts.map +1 -1
  6. package/dist/auto-doc.js +3 -1
  7. package/dist/auto-i18n-extractor.js +5 -0
  8. package/dist/branch-guard/index.d.ts.map +1 -1
  9. package/dist/branch-guard.js +116 -3
  10. package/dist/changelog-writer/index.d.ts.map +1 -1
  11. package/dist/changelog-writer.js +137 -3
  12. package/dist/checkpoint/index.d.ts +2 -0
  13. package/dist/checkpoint/index.d.ts.map +1 -1
  14. package/dist/checkpoint.js +34 -8
  15. package/dist/code-metrics.js +5 -0
  16. package/dist/commit-validator/index.d.ts.map +1 -1
  17. package/dist/commit-validator.js +35 -8
  18. package/dist/context-pins/index.d.ts.map +1 -1
  19. package/dist/context-pins.js +6 -5
  20. package/dist/cost-tracker/index.d.ts.map +1 -1
  21. package/dist/cost-tracker.js +101 -1
  22. package/dist/cron/index.d.ts.map +1 -1
  23. package/dist/cron.js +1 -0
  24. package/dist/dead-code-detector/index.d.ts.map +1 -1
  25. package/dist/dead-code-detector.js +9 -1
  26. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  27. package/dist/dependency-vulnerability-gate.js +43 -8
  28. package/dist/diff-summary/index.d.ts.map +1 -1
  29. package/dist/diff-summary.js +112 -2
  30. package/dist/doc-sync-guard.js +17 -1
  31. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  32. package/dist/duplicate-code-detector.js +102 -2
  33. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  34. package/dist/feature-flag-tracker.js +30 -4
  35. package/dist/file-watcher/index.d.ts.map +1 -1
  36. package/dist/file-watcher.js +6 -7
  37. package/dist/format-on-save/index.d.ts +2 -0
  38. package/dist/format-on-save/index.d.ts.map +1 -1
  39. package/dist/format-on-save.js +288 -59
  40. package/dist/git-autocommit/index.d.ts +12 -0
  41. package/dist/git-autocommit/index.d.ts.map +1 -1
  42. package/dist/git-autocommit.js +58 -4
  43. package/dist/import-organizer/index.d.ts.map +1 -1
  44. package/dist/import-organizer.js +152 -21
  45. package/dist/index.js +1413 -590
  46. package/dist/injection-shield/index.d.ts +16 -0
  47. package/dist/injection-shield/index.d.ts.map +1 -1
  48. package/dist/injection-shield.js +12 -4
  49. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  50. package/dist/interface-contract-guard.js +60 -26
  51. package/dist/knowledge-graph/index.d.ts.map +1 -1
  52. package/dist/knowledge-graph.js +7 -6
  53. package/dist/lint-gate/index.d.ts.map +1 -1
  54. package/dist/lint-gate.js +115 -2
  55. package/dist/loop-breaker/index.d.ts.map +1 -1
  56. package/dist/loop-breaker.js +15 -5
  57. package/dist/migration-planner.js +15 -1
  58. package/dist/notify-hub/index.d.ts.map +1 -1
  59. package/dist/notify-hub.js +41 -4
  60. package/dist/pr-drafter/index.d.ts.map +1 -1
  61. package/dist/pr-drafter.js +20 -1
  62. package/dist/prompt-firewall/index.d.ts.map +1 -1
  63. package/dist/prompt-firewall.js +140 -11
  64. package/dist/refactor-suggester/index.d.ts.map +1 -1
  65. package/dist/refactor-suggester.js +119 -4
  66. package/dist/runtime/bounded-map.d.ts +86 -0
  67. package/dist/runtime/bounded-map.d.ts.map +1 -0
  68. package/dist/runtime/credential-patterns.d.ts +42 -0
  69. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  70. package/dist/runtime/handles.d.ts +46 -0
  71. package/dist/runtime/handles.d.ts.map +1 -0
  72. package/dist/runtime/index.d.ts +4 -0
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/local-bin.d.ts +120 -0
  75. package/dist/runtime/local-bin.d.ts.map +1 -0
  76. package/dist/runtime/safe-json.d.ts +25 -0
  77. package/dist/runtime/safe-json.d.ts.map +1 -0
  78. package/dist/runtime.js +309 -22
  79. package/dist/schema-evolution-guard.js +5 -0
  80. package/dist/secret-scanner/index.d.ts.map +1 -1
  81. package/dist/secret-scanner.js +194 -50
  82. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  83. package/dist/security-hotspot-scanner.js +132 -1
  84. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  85. package/dist/semantic-search-indexer.js +7 -1
  86. package/dist/semver-bump/index.d.ts.map +1 -1
  87. package/dist/semver-bump.js +21 -2
  88. package/dist/session-recap/index.d.ts.map +1 -1
  89. package/dist/session-recap.js +18 -1
  90. package/dist/smart-rename/index.d.ts +2 -0
  91. package/dist/smart-rename/index.d.ts.map +1 -1
  92. package/dist/smart-rename.js +16 -2
  93. package/dist/spec-linker/index.d.ts.map +1 -1
  94. package/dist/spec-linker.js +19 -2
  95. package/dist/template-engine/index.d.ts.map +1 -1
  96. package/dist/template-engine.js +45 -3
  97. package/dist/test-coverage-gate.js +17 -1
  98. package/dist/test-runner-gate/index.d.ts.map +1 -1
  99. package/dist/test-runner-gate.js +142 -9
  100. package/dist/todo-listener/index.d.ts.map +1 -1
  101. package/dist/todo-listener.js +42 -2
  102. package/dist/todo-tracker/index.d.ts.map +1 -1
  103. package/dist/todo-tracker.js +3 -4
  104. package/dist/token-budget/index.d.ts.map +1 -1
  105. package/dist/token-budget.js +31 -19
  106. package/dist/token-throttle/index.d.ts.map +1 -1
  107. package/dist/token-throttle.js +17 -2
  108. package/dist/type-gate/index.d.ts.map +1 -1
  109. package/dist/type-gate.js +108 -17
  110. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/accessibility-auditor/index.ts
2
2
  import { readFile } from "node:fs/promises";
3
- import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
3
+ import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
4
4
 
5
5
  // src/runtime/index.ts
6
6
  import { execFile } from "node:child_process";
7
- import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
+ import { basename, extname as extname2, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
8
8
 
9
9
  // src/runtime/llm.ts
10
10
  function stripOuterMarkdownFence(text) {
@@ -52,6 +52,260 @@ async function runOptionalPluginLlm(request) {
52
52
  }
53
53
  }
54
54
 
55
+ // src/runtime/local-bin.ts
56
+ import { createRequire } from "node:module";
57
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
58
+ import { accessSync, constants, readFileSync } from "node:fs";
59
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
60
+ function resolveExecInvocation(command, args = []) {
61
+ const resolved = resolveWin32Command(command);
62
+ const normalizedResolved = resolved.toLowerCase();
63
+ const needsShell = process.platform === "win32" && (normalizedResolved.endsWith(".cmd") || normalizedResolved.endsWith(".bat"));
64
+ if (needsShell) {
65
+ const shim = buildWin32CmdShimInvocation(resolved, args);
66
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
67
+ }
68
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
69
+ }
70
+ function findOnPath(cmd) {
71
+ if (!cmd) return null;
72
+ const exists = (p) => {
73
+ try {
74
+ accessSync(p, constants.X_OK);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+ if (cmd.includes("/") || cmd.includes("\\")) {
81
+ return exists(cmd) ? resolve(cmd) : null;
82
+ }
83
+ const suffixes = process.platform === "win32" && extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
84
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
85
+ if (!dir) continue;
86
+ const base = join(dir, cmd);
87
+ for (const suffix of suffixes) {
88
+ const candidate = `${base}${suffix}`;
89
+ if (exists(candidate)) return candidate;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function isInside(parent, candidate) {
95
+ const rel = relative(parent, candidate);
96
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
97
+ }
98
+ var binCache = /* @__PURE__ */ new Map();
99
+ var BIN_CACHE_MAX = 64;
100
+ var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
101
+ function cachePut(key, value) {
102
+ while (binCache.size >= BIN_CACHE_MAX) {
103
+ const oldest = binCache.keys().next().value;
104
+ if (oldest === void 0) break;
105
+ binCache.delete(oldest);
106
+ }
107
+ binCache.set(key, { value, cachedAt: Date.now() });
108
+ return value;
109
+ }
110
+ function clearLocalBinCache() {
111
+ binCache.clear();
112
+ }
113
+ function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
114
+ const key = `${packageName}|${binName}|${cwd}`;
115
+ const cached = binCache.get(key);
116
+ if (cached !== void 0) {
117
+ if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
118
+ return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
119
+ }
120
+ binCache.delete(key);
121
+ }
122
+ let resolved = null;
123
+ try {
124
+ const requireFromProject = createRequire(resolve(cwd, "package.json"));
125
+ const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
126
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
127
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
128
+ if (relativeBin && !isAbsolute(relativeBin)) {
129
+ const packageDir = dirname(packagePath);
130
+ const entry = resolve(packageDir, relativeBin);
131
+ if (isInside(packageDir, entry)) {
132
+ resolved = { cmd: process.execPath, args: [entry], entry };
133
+ }
134
+ }
135
+ } catch {
136
+ resolved = null;
137
+ }
138
+ cachePut(key, resolved);
139
+ return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
140
+ }
141
+ function resolveFirstNodeBin(candidates, cwd) {
142
+ for (const c of candidates) {
143
+ const hit = resolveNodeBin(c.packageName, c.binName, cwd, c.args ?? []);
144
+ if (hit) return { ...hit, packageName: c.packageName, binName: c.binName };
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // src/runtime/bounded-map.ts
150
+ var BoundedMap = class {
151
+ map = /* @__PURE__ */ new Map();
152
+ max;
153
+ ttlMs;
154
+ now;
155
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
156
+ evictions = 0;
157
+ constructor(options) {
158
+ const normalizedMax = Math.floor(options.max);
159
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
160
+ this.ttlMs = options.ttlMs;
161
+ this.now = options.now ?? Date.now;
162
+ }
163
+ expired(entry) {
164
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
165
+ }
166
+ get(key) {
167
+ const entry = this.map.get(key);
168
+ if (entry === void 0) return void 0;
169
+ if (this.expired(entry)) {
170
+ this.map.delete(key);
171
+ return void 0;
172
+ }
173
+ this.map.delete(key);
174
+ this.map.set(key, entry);
175
+ return entry.value;
176
+ }
177
+ /**
178
+ * Read without promoting the key to most-recently-used. Use for
179
+ * diagnostics that must not perturb the eviction order.
180
+ */
181
+ peek(key) {
182
+ const entry = this.map.get(key);
183
+ if (entry === void 0 || this.expired(entry)) return void 0;
184
+ return entry.value;
185
+ }
186
+ has(key) {
187
+ const entry = this.map.get(key);
188
+ if (entry === void 0) return false;
189
+ if (this.expired(entry)) {
190
+ this.map.delete(key);
191
+ return false;
192
+ }
193
+ return true;
194
+ }
195
+ set(key, value) {
196
+ this.map.delete(key);
197
+ this.map.set(key, { value, storedAt: this.now() });
198
+ while (this.map.size > this.max) {
199
+ const coldest = this.map.keys().next().value;
200
+ if (coldest === void 0) break;
201
+ this.map.delete(coldest);
202
+ this.evictions += 1;
203
+ }
204
+ return this;
205
+ }
206
+ delete(key) {
207
+ return this.map.delete(key);
208
+ }
209
+ clear() {
210
+ this.map.clear();
211
+ this.evictions = 0;
212
+ }
213
+ get size() {
214
+ return this.map.size;
215
+ }
216
+ /** How many entries have been dropped to respect `max`, since the last clear. */
217
+ get evictionCount() {
218
+ return this.evictions;
219
+ }
220
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
221
+ prune() {
222
+ if (this.ttlMs === void 0) return 0;
223
+ let removed = 0;
224
+ for (const [key, entry] of this.map) {
225
+ if (this.expired(entry)) {
226
+ this.map.delete(key);
227
+ removed += 1;
228
+ }
229
+ }
230
+ return removed;
231
+ }
232
+ /** Live (non-expired) entries, coldest first. */
233
+ *entries() {
234
+ for (const [key, entry] of this.map) {
235
+ if (!this.expired(entry)) yield [key, entry.value];
236
+ }
237
+ }
238
+ [Symbol.iterator]() {
239
+ return this.entries();
240
+ }
241
+ };
242
+ var BoundedSet = class {
243
+ inner;
244
+ constructor(options) {
245
+ this.inner = new BoundedMap(options);
246
+ }
247
+ has(value) {
248
+ return this.inner.has(value);
249
+ }
250
+ add(value) {
251
+ this.inner.set(value, true);
252
+ return this;
253
+ }
254
+ delete(value) {
255
+ return this.inner.delete(value);
256
+ }
257
+ clear() {
258
+ this.inner.clear();
259
+ }
260
+ get size() {
261
+ return this.inner.size;
262
+ }
263
+ /** How many entries have been dropped to respect `max`, since the last clear. */
264
+ get evictionCount() {
265
+ return this.inner.evictionCount;
266
+ }
267
+ *values() {
268
+ for (const [key] of this.inner) yield key;
269
+ }
270
+ [Symbol.iterator]() {
271
+ return this.values();
272
+ }
273
+ };
274
+
275
+ // src/runtime/safe-json.ts
276
+ var UNSERIALIZABLE = "[unserializable]";
277
+ function safeJsonStringify(value, indent) {
278
+ try {
279
+ const stack = [];
280
+ const out = JSON.stringify(
281
+ value,
282
+ function replacer(_key, val) {
283
+ if (typeof val === "bigint") return `${val.toString()}n`;
284
+ if (val === null || typeof val !== "object") return val;
285
+ while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
286
+ if (stack.includes(val)) return "[circular]";
287
+ stack.push(val);
288
+ return val;
289
+ },
290
+ indent
291
+ );
292
+ return out ?? String(value);
293
+ } catch {
294
+ return UNSERIALIZABLE;
295
+ }
296
+ }
297
+
298
+ // src/runtime/handles.ts
299
+ function releaseHandle(off) {
300
+ if (off) {
301
+ try {
302
+ off();
303
+ } catch {
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+
55
309
  // src/runtime/index.ts
56
310
  var META_CHARS = /["'`;&|<>\r\n]/;
57
311
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
@@ -66,9 +320,9 @@ function safeSplit(command) {
66
320
  function withinProjectPath(projectRoot, candidate) {
67
321
  if (candidate.length === 0 || candidate.length > 4096) return false;
68
322
  if (hasLeadingDash(candidate)) return false;
69
- const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
70
- const rel = relative(projectRoot, resolved);
71
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
323
+ const resolved = isAbsolute2(candidate) ? resolve2(candidate) : resolve2(projectRoot, candidate);
324
+ const rel = relative2(projectRoot, resolved);
325
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
72
326
  }
73
327
  function everyFlagAllowed(allowed, args) {
74
328
  for (const arg of args) {
@@ -80,14 +334,14 @@ function everyFlagAllowed(allowed, args) {
80
334
  }
81
335
  function sanitizeRunnerPath(value, options = {}) {
82
336
  if (!value || hasLeadingDash(value)) return null;
83
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
337
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
84
338
  if (!withinProjectPath(projectRoot, value)) return null;
85
- return isAbsolute(value) ? resolve(value) : resolve(projectRoot, value);
339
+ return isAbsolute2(value) ? resolve2(value) : resolve2(projectRoot, value);
86
340
  }
87
341
  function resolveRunnerCommand(runtime, command, options = {}) {
88
342
  const tokens = safeSplit(command);
89
343
  if (!tokens || tokens.length === 0) return null;
90
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
344
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
91
345
  const launcher = runtime.packageManager;
92
346
  const [head, second, ...rest] = tokens;
93
347
  if (!head) return null;
@@ -118,7 +372,7 @@ function resolveRunnerCommand(runtime, command, options = {}) {
118
372
  return { cmd: head, args: [second, exe, ...tail], display };
119
373
  }
120
374
  }
121
- if (isAbsolute(head)) {
375
+ if (isAbsolute2(head)) {
122
376
  if (!withinProjectPath(projectRoot, head)) return null;
123
377
  const base = basename(head);
124
378
  if (base !== runtime.executable && base !== launcher) return null;
@@ -139,7 +393,7 @@ function runRunnerCommand(argv, options) {
139
393
  });
140
394
  }
141
395
  return new Promise((resolvePromise) => {
142
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
396
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
143
397
  const trimmedCwd = options.cwd.trim();
144
398
  if (!withinProjectPath(projectRoot, trimmedCwd)) {
145
399
  resolvePromise({
@@ -157,21 +411,37 @@ function runRunnerCommand(argv, options) {
157
411
  const stderrChunks = [];
158
412
  let stdoutBytes = 0;
159
413
  let stderrBytes = 0;
160
- options.signal?.addEventListener("abort", () => {
414
+ const onAbort = () => {
161
415
  timedOut = true;
162
- }, { once: true });
416
+ };
417
+ let invocation;
418
+ try {
419
+ invocation = resolveExecInvocation(argv[0], argv.slice(1));
420
+ } catch (err) {
421
+ resolvePromise({
422
+ code: null,
423
+ stdout: "",
424
+ stderr: `runtime helper: ${err instanceof Error ? err.message : String(err)}`,
425
+ timedOut: false,
426
+ spawnError: true
427
+ });
428
+ return;
429
+ }
430
+ options.signal?.addEventListener("abort", onAbort, { once: true });
163
431
  const child = execFile(
164
- argv[0],
165
- argv.slice(1),
432
+ invocation.cmd,
433
+ invocation.args,
166
434
  {
167
435
  cwd: trimmedCwd,
168
436
  timeout: options.timeoutMs,
169
437
  signal: options.signal,
170
438
  maxBuffer: MAX_BUFFER_BYTES,
171
439
  windowsHide: true,
172
- shell: false
440
+ shell: false,
441
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
173
442
  },
174
443
  (err) => {
444
+ options.signal?.removeEventListener("abort", onAbort);
175
445
  if (timedOut) {
176
446
  resolvePromise({
177
447
  code: null,
@@ -235,7 +505,7 @@ function runRunnerCommand(argv, options) {
235
505
  }
236
506
  function withinProject(p) {
237
507
  const cwd = process.cwd();
238
- return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
508
+ return withinProjectPath(cwd, p) || relative2(cwd, p) === ".";
239
509
  }
240
510
  var DEFAULT_EXCLUDE_DIRS = ["node_modules", "dist", ".git", "coverage"];
241
511
  async function collectSourceFilesAsync(root, opts) {
@@ -264,7 +534,7 @@ async function collectSourceFilesAsync(root, opts) {
264
534
  entries.sort((a, b) => a.name.localeCompare(b.name));
265
535
  for (const entry of entries) {
266
536
  if (excludeSet.has(entry.name)) continue;
267
- const full = resolve(dir, entry.name);
537
+ const full = resolve2(dir, entry.name);
268
538
  if (entry.isDirectory()) {
269
539
  await walk(full, depth + 1);
270
540
  } else if (entry.isFile() && matchesExtension(full, opts.extensions)) {
@@ -276,7 +546,7 @@ async function collectSourceFilesAsync(root, opts) {
276
546
  return files;
277
547
  }
278
548
  function matchesExtension(p, exts) {
279
- return exts.includes(extname(p).toLowerCase());
549
+ return exts.includes(extname2(p).toLowerCase());
280
550
  }
281
551
 
282
552
  // src/accessibility-auditor/index.ts
@@ -339,7 +609,7 @@ async function auditFile(filePath, projectRoot) {
339
609
  const idsByValue = /* @__PURE__ */ new Map();
340
610
  function add(line, rule, severity, message) {
341
611
  findings.push({
342
- file: relative2(projectRoot, filePath),
612
+ file: relative3(projectRoot, filePath),
343
613
  line,
344
614
  rule,
345
615
  severity,
@@ -418,19 +688,27 @@ async function auditFile(filePath, projectRoot) {
418
688
  }
419
689
  async function auditPath(rawPath, cfg) {
420
690
  const root = process.cwd();
421
- const resolved = isAbsolute2(rawPath) ? resolve2(rawPath) : resolve2(root, rawPath);
691
+ const resolved = isAbsolute3(rawPath) ? resolve3(rawPath) : resolve3(root, rawPath);
422
692
  const exts = normalizeExtensions(cfg.includeExtensions);
423
693
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
424
694
  const findings = [];
695
+ let scannedFiles = 0;
696
+ let truncated = false;
425
697
  for (const file of files) {
426
698
  const fileFindings = await auditFile(file, root);
699
+ scannedFiles += 1;
427
700
  findings.push(...fileFindings);
428
- if (findings.length >= cfg.maxFindings) break;
701
+ if (findings.length >= cfg.maxFindings) {
702
+ truncated = scannedFiles < files.length;
703
+ break;
704
+ }
429
705
  }
430
706
  return {
431
- path: relative2(root, resolved),
707
+ path: relative3(root, resolved),
432
708
  findings: findings.slice(0, cfg.maxFindings),
433
- fileCount: files.length
709
+ fileCount: files.length,
710
+ scannedFiles,
711
+ truncated
434
712
  };
435
713
  }
436
714
  function formatSummary(result) {
@@ -490,7 +768,7 @@ var plugin = {
490
768
  state.findingCount = 0;
491
769
  state.hookInvocationCount = 0;
492
770
  state.lastResult = null;
493
- state.hookUnregister = null;
771
+ state.hookUnregister = releaseHandle(state.hookUnregister);
494
772
  const cfg = readConfig(api.config.extensions?.["accessibility-auditor"]);
495
773
  const hook = async (input) => {
496
774
  if (!cfg.enabled || !cfg.onWriteEdit) return;
@@ -559,6 +837,10 @@ var plugin = {
559
837
  ok: true,
560
838
  path: result.path,
561
839
  fileCount: result.fileCount,
840
+ scannedFiles: result.scannedFiles,
841
+ // Say so when the cap stopped the walk early: a partial scan
842
+ // that reports few findings must not read as a clean result.
843
+ truncated: result.truncated,
562
844
  findingCount: result.findings.length,
563
845
  findings: result.findings
564
846
  };
@@ -689,7 +971,7 @@ function buildBody(payload, cfg) {
689
971
  if (cfg.includeResult && payload.result !== void 0) {
690
972
  lines.push("## Result");
691
973
  lines.push("```json");
692
- lines.push(JSON.stringify(payload.result, null, 2));
974
+ lines.push(safeJsonStringify(payload.result, 2));
693
975
  lines.push("```");
694
976
  lines.push("");
695
977
  }
@@ -931,9 +1213,9 @@ var agent_handoff_default = plugin2;
931
1213
 
932
1214
  // src/api-compatibility-gate/index.ts
933
1215
  import { execFile as execFile2 } from "node:child_process";
934
- import { existsSync, readFileSync } from "node:fs";
1216
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
935
1217
  import { readFile as readFile2 } from "node:fs/promises";
936
- import { basename as basename2, dirname, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
1218
+ import { basename as basename2, dirname as dirname2, extname as extname3, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
937
1219
  var API_VERSION3 = "^0.1.10";
938
1220
  var state3 = {
939
1221
  invocationCount: 0,
@@ -997,25 +1279,25 @@ function matchesAnyPattern(filePath, patterns) {
997
1279
  return patterns.some((p) => patternToRegex(p).test(normalized));
998
1280
  }
999
1281
  function resolveToProjectRoot(filePath) {
1000
- if (isAbsolute3(filePath)) return resolve3(filePath);
1001
- return resolve3(process.cwd(), filePath);
1282
+ if (isAbsolute4(filePath)) return resolve4(filePath);
1283
+ return resolve4(process.cwd(), filePath);
1002
1284
  }
1003
1285
  function withinProject2(filePath) {
1004
1286
  if (typeof filePath !== "string" || filePath.length === 0 || filePath.length > 4096) return false;
1005
1287
  const resolved = resolveToProjectRoot(filePath);
1006
- const rel = relative3(process.cwd(), resolved);
1288
+ const rel = relative4(process.cwd(), resolved);
1007
1289
  if (rel === "" || rel === ".") return true;
1008
1290
  if (rel.startsWith("..")) return false;
1009
- if (isAbsolute3(rel)) return false;
1291
+ if (isAbsolute4(rel)) return false;
1010
1292
  return true;
1011
1293
  }
1012
1294
  function isPackageEntryPoint(filePath) {
1013
1295
  const resolved = resolveToProjectRoot(filePath);
1014
- const dir = dirname(resolved);
1015
- const pkgPath = resolve3(dir, "package.json");
1296
+ const dir = dirname2(resolved);
1297
+ const pkgPath = resolve4(dir, "package.json");
1016
1298
  if (!existsSync(pkgPath)) return false;
1017
1299
  try {
1018
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1300
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1019
1301
  const entries = [];
1020
1302
  if (pkg.main) entries.push(pkg.main);
1021
1303
  if (pkg.module) entries.push(pkg.module);
@@ -1033,9 +1315,9 @@ function isPackageEntryPoint(filePath) {
1033
1315
  }
1034
1316
  }
1035
1317
  for (const entry of entries) {
1036
- const entryResolved = resolve3(dir, entry);
1318
+ const entryResolved = resolve4(dir, entry);
1037
1319
  if (entryResolved === resolved) return true;
1038
- if (basename2(dirname(entryResolved)) === basename2(dirname(resolved)) && basename2(entryResolved, extname2(entryResolved)) === basename2(resolved, extname2(resolved))) {
1320
+ if (basename2(dirname2(entryResolved)) === basename2(dirname2(resolved)) && basename2(entryResolved, extname3(entryResolved)) === basename2(resolved, extname3(resolved))) {
1039
1321
  return true;
1040
1322
  }
1041
1323
  }
@@ -1086,7 +1368,7 @@ function extractExports(source) {
1086
1368
  }
1087
1369
  function readGitVersion(filePath) {
1088
1370
  const resolved = resolveToProjectRoot(filePath);
1089
- const relPath = normalizePath(relative3(process.cwd(), resolved));
1371
+ const relPath = normalizePath(relative4(process.cwd(), resolved));
1090
1372
  return new Promise((resolveVersion) => {
1091
1373
  execFile2(
1092
1374
  "git",
@@ -1290,14 +1572,14 @@ var plugin3 = {
1290
1572
  var api_compatibility_gate_default = plugin3;
1291
1573
 
1292
1574
  // src/auto-doc/index.ts
1293
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
1575
+ import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "node:path";
1294
1576
  var AUTO_DOC_API_VERSION = "^0.1.10";
1295
1577
  function resolveProjectPath(rawPath, cwd = process.cwd()) {
1296
1578
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
1297
- const root = resolve4(cwd);
1298
- const resolved = isAbsolute4(rawPath) ? resolve4(rawPath) : resolve4(root, rawPath);
1299
- const rel = relative4(root, resolved);
1300
- if (rel === "" || !rel.startsWith("..") && !isAbsolute4(rel)) return resolved;
1579
+ const root = resolve5(cwd);
1580
+ const resolved = isAbsolute5(rawPath) ? resolve5(rawPath) : resolve5(root, rawPath);
1581
+ const rel = relative5(root, resolved);
1582
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute5(rel)) return resolved;
1301
1583
  return null;
1302
1584
  }
1303
1585
  var state4 = {
@@ -1477,16 +1759,17 @@ async function runAutoDoc(input, api) {
1477
1759
  continue;
1478
1760
  }
1479
1761
  try {
1480
- const { readFileSync: readFileSync16, writeFileSync: writeFileSync5 } = await import("node:fs");
1762
+ const { readFileSync: readFileSync17, writeFileSync: writeFileSync3 } = await import("node:fs");
1481
1763
  let content;
1482
1764
  try {
1483
- content = readFileSync16(safeFile, "utf-8");
1765
+ content = readFileSync17(safeFile, "utf-8");
1484
1766
  } catch {
1485
1767
  api.log.warn(`auto-doc: could not read file ${safeFile}`);
1486
1768
  continue;
1487
1769
  }
1488
1770
  const entities = parseSource(content);
1489
1771
  let modified = content;
1772
+ const beforeCount = results.length;
1490
1773
  for (const entity of entities) {
1491
1774
  if (!input.force && !needsDocComment(modified, entity)) continue;
1492
1775
  let doc = null;
@@ -1512,8 +1795,9 @@ async function runAutoDoc(input, api) {
1512
1795
  modified = injectDocComment(modified, entity, doc);
1513
1796
  results.push({ file: safeFile, entity: entity.name, source });
1514
1797
  }
1515
- if (!input.dry_run && results.length > 0) {
1516
- writeFileSync5(safeFile, modified, "utf-8");
1798
+ const changedThisFile = results.length > beforeCount && modified !== content;
1799
+ if (!input.dry_run && changedThisFile) {
1800
+ writeFileSync3(safeFile, modified, "utf-8");
1517
1801
  api.log.info(`auto-doc: updated ${safeFile}`);
1518
1802
  }
1519
1803
  } catch (err) {
@@ -1828,7 +2112,7 @@ var plugin5 = {
1828
2112
  var auto_escalate_default = plugin5;
1829
2113
 
1830
2114
  // src/auto-i18n-extractor/index.ts
1831
- import { readFileSync as readFileSync2 } from "node:fs";
2115
+ import { readFileSync as readFileSync3 } from "node:fs";
1832
2116
  var API_VERSION4 = "^0.1.10";
1833
2117
  var state6 = {
1834
2118
  filesScanned: 0,
@@ -1913,7 +2197,7 @@ function extractStrings(content, cfg) {
1913
2197
  }
1914
2198
  function readSourceFile(filePath) {
1915
2199
  try {
1916
- return readFileSync2(filePath, "utf-8");
2200
+ return readFileSync3(filePath, "utf-8");
1917
2201
  } catch {
1918
2202
  return null;
1919
2203
  }
@@ -2201,16 +2485,16 @@ function hasDisabledPluginEntry(raw) {
2201
2485
  return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
2202
2486
  });
2203
2487
  }
2204
- var branchCache = /* @__PURE__ */ new Map();
2488
+ var branchCache = new BoundedMap({ max: 64, ttlMs: 3e4 });
2205
2489
  function runGit(args, cwd, signal) {
2206
- return new Promise((resolve27, reject) => {
2490
+ return new Promise((resolve28, reject) => {
2207
2491
  execFile3(
2208
2492
  "git",
2209
2493
  args,
2210
2494
  { encoding: "utf-8", timeout: 3e3, cwd, windowsHide: true, signal },
2211
2495
  (error, stdout) => {
2212
2496
  if (error) reject(error);
2213
- else resolve27(stdout);
2497
+ else resolve28(stdout);
2214
2498
  }
2215
2499
  );
2216
2500
  });
@@ -2311,8 +2595,8 @@ var plugin7 = {
2311
2595
  state7.invocationCount = 0;
2312
2596
  state7.blockCount = 0;
2313
2597
  state7.warnCount = 0;
2314
- state7.hookUnregister = null;
2315
- state7.configUnregister = null;
2598
+ state7.hookUnregister = releaseHandle(state7.hookUnregister);
2599
+ state7.configUnregister = releaseHandle(state7.configUnregister);
2316
2600
  state7.lastBlock = null;
2317
2601
  branchCache.clear();
2318
2602
  let cfg = readHostConfig(api.config);
@@ -2458,11 +2742,11 @@ var plugin7 = {
2458
2742
  var branch_guard_default = plugin7;
2459
2743
 
2460
2744
  // src/changelog-writer/index.ts
2461
- import { readFileSync as readFileSync3, writeFileSync } from "node:fs";
2462
- import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "node:path";
2745
+ import { readFileSync as readFileSync4, writeFileSync } from "node:fs";
2746
+ import { isAbsolute as isAbsolute6, relative as relative6, resolve as resolve6 } from "node:path";
2463
2747
  var state8 = {
2464
2748
  entries: [],
2465
- filesTouched: /* @__PURE__ */ new Set(),
2749
+ filesTouched: new BoundedSet({ max: 2e3 }),
2466
2750
  commitsSeen: 0,
2467
2751
  writes: 0,
2468
2752
  polishes: 0,
@@ -2476,10 +2760,10 @@ var DEFAULTS6 = {
2476
2760
  maxEntries: 200
2477
2761
  };
2478
2762
  function resolveProjectPath2(rawPath, cwd = process.cwd()) {
2479
- const root = resolve5(cwd);
2480
- const resolved = isAbsolute5(rawPath) ? resolve5(rawPath) : resolve5(root, rawPath);
2481
- const rel = relative5(root, resolved);
2482
- if (rel === "" || !rel.startsWith("..") && !isAbsolute5(rel)) return resolved;
2763
+ const root = resolve6(cwd);
2764
+ const resolved = isAbsolute6(rawPath) ? resolve6(rawPath) : resolve6(root, rawPath);
2765
+ const rel = relative6(root, resolved);
2766
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute6(rel)) return resolved;
2483
2767
  return null;
2484
2768
  }
2485
2769
  function readConfig7(raw) {
@@ -2623,7 +2907,7 @@ var plugin8 = {
2623
2907
  },
2624
2908
  setup(api) {
2625
2909
  state8.entries = [];
2626
- state8.filesTouched = /* @__PURE__ */ new Set();
2910
+ state8.filesTouched = new BoundedSet({ max: 2e3 });
2627
2911
  state8.commitsSeen = 0;
2628
2912
  state8.writes = 0;
2629
2913
  for (const off of state8.eventUnsubscribers) {
@@ -2769,7 +3053,7 @@ var plugin8 = {
2769
3053
  );
2770
3054
  let existing = null;
2771
3055
  try {
2772
- existing = readFileSync3(cfg.filePath, "utf-8");
3056
+ existing = readFileSync4(cfg.filePath, "utf-8");
2773
3057
  } catch {
2774
3058
  existing = null;
2775
3059
  }
@@ -2815,7 +3099,7 @@ var plugin8 = {
2815
3099
  filesTouched: state8.filesTouched.size
2816
3100
  };
2817
3101
  state8.entries = [];
2818
- state8.filesTouched = /* @__PURE__ */ new Set();
3102
+ state8.filesTouched = new BoundedSet({ max: 2e3 });
2819
3103
  state8.commitsSeen = 0;
2820
3104
  state8.writes = 0;
2821
3105
  api.log.info("changelog-writer: teardown complete", { final });
@@ -2837,20 +3121,22 @@ var changelog_writer_default = plugin8;
2837
3121
 
2838
3122
  // src/checkpoint/index.ts
2839
3123
  import { mkdir, readFile as readFile3, stat, writeFile } from "node:fs/promises";
2840
- import { dirname as dirname2, isAbsolute as isAbsolute6, relative as relative6, resolve as resolve6 } from "node:path";
3124
+ import { dirname as dirname3, isAbsolute as isAbsolute7, relative as relative7, resolve as resolve7 } from "node:path";
2841
3125
  var state9 = {
2842
3126
  snapshots: [],
2843
3127
  nextId: 1,
2844
3128
  captures: 0,
2845
3129
  restores: 0,
2846
3130
  skippedLarge: 0,
3131
+ evictedForBytes: 0,
2847
3132
  hookUnregister: null
2848
3133
  };
2849
3134
  var DEFAULTS7 = {
2850
3135
  enabled: true,
2851
3136
  autoCapture: true,
2852
3137
  maxSnapshots: 50,
2853
- maxFileBytes: 1048576
3138
+ maxFileBytes: 1048576,
3139
+ maxTotalBytes: 64 * 1048576
2854
3140
  };
2855
3141
  function readConfig8(raw) {
2856
3142
  if (!raw || typeof raw !== "object") return { ...DEFAULTS7 };
@@ -2859,14 +3145,15 @@ function readConfig8(raw) {
2859
3145
  enabled: r["enabled"] !== false,
2860
3146
  autoCapture: r["autoCapture"] !== false,
2861
3147
  maxSnapshots: typeof r["maxSnapshots"] === "number" && r["maxSnapshots"] >= 1 && r["maxSnapshots"] <= 500 ? r["maxSnapshots"] : DEFAULTS7.maxSnapshots,
3148
+ maxTotalBytes: typeof r["maxTotalBytes"] === "number" && r["maxTotalBytes"] >= 1024 ? r["maxTotalBytes"] : DEFAULTS7.maxTotalBytes,
2862
3149
  maxFileBytes: typeof r["maxFileBytes"] === "number" && r["maxFileBytes"] >= 1024 ? r["maxFileBytes"] : DEFAULTS7.maxFileBytes
2863
3150
  };
2864
3151
  }
2865
3152
  function resolveProjectPath3(rawPath, cwd = process.cwd()) {
2866
- const root = resolve6(cwd);
2867
- const resolved = isAbsolute6(rawPath) ? resolve6(rawPath) : resolve6(root, rawPath);
2868
- const rel = relative6(root, resolved);
2869
- if (rel === "" || !rel.startsWith("..") && !isAbsolute6(rel)) return resolved;
3153
+ const root = resolve7(cwd);
3154
+ const resolved = isAbsolute7(rawPath) ? resolve7(rawPath) : resolve7(root, rawPath);
3155
+ const rel = relative7(root, resolved);
3156
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute7(rel)) return resolved;
2870
3157
  return null;
2871
3158
  }
2872
3159
  function hashContent(s) {
@@ -2900,11 +3187,22 @@ async function captureFileForHook(path, maxBytes, signal) {
2900
3187
  return { path, content: null, bytes: 0 };
2901
3188
  }
2902
3189
  }
2903
- function pushSnapshot(snapshot, maxSnapshots) {
3190
+ function retainedSnapshotBytes() {
3191
+ let total = 0;
3192
+ for (const snap of state9.snapshots) {
3193
+ for (const f of snap.files) total += f.content === null ? 0 : f.content.length;
3194
+ }
3195
+ return total;
3196
+ }
3197
+ function pushSnapshot(snapshot, maxSnapshots, maxTotalBytes) {
2904
3198
  state9.snapshots.push(snapshot);
2905
3199
  if (state9.snapshots.length > maxSnapshots) {
2906
3200
  state9.snapshots.splice(0, state9.snapshots.length - maxSnapshots);
2907
3201
  }
3202
+ while (state9.snapshots.length > 1 && retainedSnapshotBytes() > maxTotalBytes) {
3203
+ state9.snapshots.shift();
3204
+ state9.evictedForBytes += 1;
3205
+ }
2908
3206
  }
2909
3207
  var plugin9 = {
2910
3208
  name: "checkpoint",
@@ -2943,6 +3241,7 @@ var plugin9 = {
2943
3241
  state9.captures = 0;
2944
3242
  state9.restores = 0;
2945
3243
  state9.skippedLarge = 0;
3244
+ state9.evictedForBytes = 0;
2946
3245
  if (state9.hookUnregister) {
2947
3246
  try {
2948
3247
  state9.hookUnregister();
@@ -2970,7 +3269,8 @@ var plugin9 = {
2970
3269
  origin: `auto:${input.toolName ?? "unknown"}`,
2971
3270
  files: [captured]
2972
3271
  },
2973
- cfg.maxSnapshots
3272
+ cfg.maxSnapshots,
3273
+ cfg.maxTotalBytes
2974
3274
  );
2975
3275
  state9.captures += 1;
2976
3276
  api.metrics.counter("captures");
@@ -3048,7 +3348,7 @@ var plugin9 = {
3048
3348
  origin: input.label?.trim() ? `manual:${input.label.trim()}` : "manual",
3049
3349
  files
3050
3350
  };
3051
- pushSnapshot(snapshot, cfg.maxSnapshots);
3351
+ pushSnapshot(snapshot, cfg.maxSnapshots, cfg.maxTotalBytes);
3052
3352
  state9.captures += 1;
3053
3353
  api.metrics.counter("captures");
3054
3354
  return {
@@ -3091,7 +3391,9 @@ var plugin9 = {
3091
3391
  counters: {
3092
3392
  captures: state9.captures,
3093
3393
  restores: state9.restores,
3094
- skippedLarge: state9.skippedLarge
3394
+ skippedLarge: state9.skippedLarge,
3395
+ retainedBytes: retainedSnapshotBytes(),
3396
+ evictedForBytes: state9.evictedForBytes
3095
3397
  }
3096
3398
  };
3097
3399
  }
@@ -3134,7 +3436,7 @@ var plugin9 = {
3134
3436
  continue;
3135
3437
  }
3136
3438
  try {
3137
- await mkdir(dirname2(f.path), { recursive: true });
3439
+ await mkdir(dirname3(f.path), { recursive: true });
3138
3440
  await writeFile(f.path, f.content);
3139
3441
  restored.push(f.path);
3140
3442
  } catch (err) {
@@ -3173,13 +3475,16 @@ var plugin9 = {
3173
3475
  captures: state9.captures,
3174
3476
  restores: state9.restores,
3175
3477
  skippedLarge: state9.skippedLarge,
3176
- snapshotsHeld: state9.snapshots.length
3478
+ snapshotsHeld: state9.snapshots.length,
3479
+ retainedBytes: retainedSnapshotBytes(),
3480
+ evictedForBytes: state9.evictedForBytes
3177
3481
  };
3178
3482
  state9.snapshots = [];
3179
3483
  state9.nextId = 1;
3180
3484
  state9.captures = 0;
3181
3485
  state9.restores = 0;
3182
3486
  state9.skippedLarge = 0;
3487
+ state9.evictedForBytes = 0;
3183
3488
  api.log.info("checkpoint: teardown complete", { final });
3184
3489
  },
3185
3490
  async health() {
@@ -3190,7 +3495,11 @@ var plugin9 = {
3190
3495
  snapshotsHeld: state9.snapshots.length,
3191
3496
  captures: state9.captures,
3192
3497
  restores: state9.restores,
3193
- skippedLarge: state9.skippedLarge
3498
+ skippedLarge: state9.skippedLarge,
3499
+ // Retained snapshot bytes: the count-based ring alone does not
3500
+ // bound memory, so surface the number the byte budget acts on.
3501
+ retainedBytes: retainedSnapshotBytes(),
3502
+ evictedForBytes: state9.evictedForBytes
3194
3503
  }
3195
3504
  };
3196
3505
  }
@@ -3199,7 +3508,7 @@ var checkpoint_default = plugin9;
3199
3508
 
3200
3509
  // src/code-metrics/index.ts
3201
3510
  import { readFile as readFile4 } from "node:fs/promises";
3202
- import { isAbsolute as isAbsolute7, relative as relative7, resolve as resolve7 } from "node:path";
3511
+ import { isAbsolute as isAbsolute8, relative as relative8, resolve as resolve8 } from "node:path";
3203
3512
  var API_VERSION6 = "^0.1.10";
3204
3513
  var state10 = {
3205
3514
  measureCount: 0,
@@ -3229,7 +3538,7 @@ function toPosix(p) {
3229
3538
  return p.replace(/\\/g, "/");
3230
3539
  }
3231
3540
  function relativePath(p) {
3232
- return toPosix(relative7(process.cwd(), p));
3541
+ return toPosix(relative8(process.cwd(), p));
3233
3542
  }
3234
3543
  function countFunctions(content) {
3235
3544
  let count = 0;
@@ -3306,7 +3615,7 @@ function analyzeFile(filePath, content) {
3306
3615
  }
3307
3616
  async function measurePath(rawPath, cfg) {
3308
3617
  const root = process.cwd();
3309
- const resolved = isAbsolute7(rawPath) ? resolve7(rawPath) : resolve7(root, rawPath);
3618
+ const resolved = isAbsolute8(rawPath) ? resolve8(rawPath) : resolve8(root, rawPath);
3310
3619
  const exts = normalizeExtensions2(cfg.extensions);
3311
3620
  const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
3312
3621
  const files = allFiles.slice(0, cfg.maxFiles);
@@ -3369,7 +3678,7 @@ var plugin10 = {
3369
3678
  const exts = normalizeExtensions2(cfg.extensions);
3370
3679
  if (!matchesExtension(sourcePath, exts)) return;
3371
3680
  state10.hookInvocationCount += 1;
3372
- const resolved = resolve7(process.cwd(), sourcePath);
3681
+ const resolved = resolve8(process.cwd(), sourcePath);
3373
3682
  let content;
3374
3683
  try {
3375
3684
  content = await readFile4(resolved, "utf-8");
@@ -3413,7 +3722,7 @@ var plugin10 = {
3413
3722
  state10.fileCount += result.files.length;
3414
3723
  return {
3415
3724
  ok: true,
3416
- path: relativePath(resolve7(process.cwd(), rawPath)),
3725
+ path: relativePath(resolve8(process.cwd(), rawPath)),
3417
3726
  files: result.files,
3418
3727
  totalFiles: result.totalFiles,
3419
3728
  capped: result.totalFiles > cfg.maxFiles
@@ -3601,14 +3910,24 @@ function parseCommitMessage(message, cfg) {
3601
3910
  errors
3602
3911
  };
3603
3912
  }
3913
+ var GIT_MESSAGE_FLAG_RE = new RegExp(
3914
+ [
3915
+ // -m "…" | -m '…' | -m=… ; --message "…" | --message='…' | --message=…
3916
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)"([^"]*)"`,
3917
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)'([^']*)'`,
3918
+ // Bare value: stops at whitespace or a shell separator.
3919
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)([^\s;&|"']+)`
3920
+ ].join("|"),
3921
+ "g"
3922
+ );
3604
3923
  function extractMessageFromBash(command) {
3605
- const flags = [];
3606
- const doubleQuoted = command.matchAll(/-m\s+"([^"]*)"/g);
3607
- const singleQuoted = command.matchAll(/-m\s+'([^']*)'/g);
3608
- for (const m of doubleQuoted) flags.push(m[1] ?? "");
3609
- for (const m of singleQuoted) flags.push(m[1] ?? "");
3610
- if (flags.length === 0) return null;
3611
- return flags.join("\n");
3924
+ const parts = [];
3925
+ for (const m of command.matchAll(GIT_MESSAGE_FLAG_RE)) {
3926
+ const value = m[1] ?? m[2] ?? m[3];
3927
+ if (value !== void 0) parts.push(value);
3928
+ }
3929
+ if (parts.length === 0) return null;
3930
+ return parts.join("\n");
3612
3931
  }
3613
3932
  var plugin11 = {
3614
3933
  name: "commit-validator",
@@ -3667,7 +3986,7 @@ var plugin11 = {
3667
3986
  state11.invalidCount = 0;
3668
3987
  state11.suggestFixCount = 0;
3669
3988
  state11.suggestFixErrors = 0;
3670
- state11.hookUnregister = null;
3989
+ state11.hookUnregister = releaseHandle(state11.hookUnregister);
3671
3990
  state11.lastValidation = null;
3672
3991
  const cfg = readConfig10(api.config.extensions?.["commit-validator"]);
3673
3992
  const hook = async (input) => {
@@ -3859,7 +4178,7 @@ Suggested rewrite (${suggest.model}):
3859
4178
  var commit_validator_default = plugin11;
3860
4179
 
3861
4180
  // src/config-validator/index.ts
3862
- import { readFileSync as readFileSync4, statSync } from "node:fs";
4181
+ import { readFileSync as readFileSync5, statSync } from "node:fs";
3863
4182
  var state12 = {
3864
4183
  invocations: 0,
3865
4184
  filesChecked: 0,
@@ -4121,7 +4440,7 @@ var plugin12 = {
4121
4440
  let text;
4122
4441
  try {
4123
4442
  if (statSync(raw).size > cfg.maxFileBytes) return;
4124
- text = readFileSync4(raw, "utf-8");
4443
+ text = readFileSync5(raw, "utf-8");
4125
4444
  } catch {
4126
4445
  return;
4127
4446
  }
@@ -4205,7 +4524,8 @@ var config_validator_default = plugin12;
4205
4524
 
4206
4525
  // src/context-pins/index.ts
4207
4526
  import * as fs from "node:fs";
4208
- import { dirname as dirname3, isAbsolute as isAbsolute8, relative as relative8, resolve as resolve8 } from "node:path";
4527
+ import { dirname as dirname4, isAbsolute as isAbsolute9, relative as relative9, resolve as resolve9 } from "node:path";
4528
+ import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
4209
4529
  var state13 = {
4210
4530
  pins: [],
4211
4531
  nextId: 1,
@@ -4222,10 +4542,10 @@ var DEFAULTS11 = {
4222
4542
  };
4223
4543
  function resolveProjectPath4(rawPath, cwd = process.cwd()) {
4224
4544
  if (typeof rawPath !== "string" || rawPath.length === 0) return "";
4225
- const root = resolve8(cwd);
4226
- const resolved = isAbsolute8(rawPath) ? resolve8(rawPath) : resolve8(root, rawPath);
4227
- const rel = relative8(root, resolved);
4228
- if (rel === "" || !rel.startsWith("..") && !isAbsolute8(rel)) return resolved;
4545
+ const root = resolve9(cwd);
4546
+ const resolved = isAbsolute9(rawPath) ? resolve9(rawPath) : resolve9(root, rawPath);
4547
+ const rel = relative9(root, resolved);
4548
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute9(rel)) return resolved;
4229
4549
  return null;
4230
4550
  }
4231
4551
  function readConfig12(raw) {
@@ -4252,11 +4572,11 @@ function loadPins(filePath) {
4252
4572
  return { pins: [], nextId: 1 };
4253
4573
  }
4254
4574
  }
4255
- function persistPins(filePath) {
4575
+ async function persistPins(filePath) {
4256
4576
  if (!filePath) return true;
4257
4577
  try {
4258
- fs.mkdirSync(dirname3(filePath), { recursive: true });
4259
- fs.writeFileSync(filePath, JSON.stringify({ pins: state13.pins, nextId: state13.nextId }, null, 2));
4578
+ await ensureDir(dirname4(filePath));
4579
+ await atomicWrite(filePath, JSON.stringify({ pins: state13.pins, nextId: state13.nextId }, null, 2));
4260
4580
  return true;
4261
4581
  } catch {
4262
4582
  state13.persistErrors += 1;
@@ -4354,7 +4674,7 @@ var plugin13 = {
4354
4674
  state13.pins.push(pin);
4355
4675
  state13.adds += 1;
4356
4676
  api.metrics.counter("adds");
4357
- const persisted = persistPins(cfg.filePath);
4677
+ const persisted = await persistPins(cfg.filePath);
4358
4678
  return { ok: true, pin, persisted, totalPins: state13.pins.length };
4359
4679
  }
4360
4680
  });
@@ -4380,7 +4700,7 @@ var plugin13 = {
4380
4700
  if (removed === 0) return { ok: false, error: `no pin matches "${key}"` };
4381
4701
  state13.removals += removed;
4382
4702
  api.metrics.counter("removals", removed);
4383
- const persisted = persistPins(cfg.filePath);
4703
+ const persisted = await persistPins(cfg.filePath);
4384
4704
  return { ok: true, removed, persisted, totalPins: state13.pins.length };
4385
4705
  }
4386
4706
  });
@@ -4480,7 +4800,7 @@ function readCostTrackerConfig(raw) {
4480
4800
  mailboxDigestTo: typeof digestTo === "string" && digestTo.length > 0 ? digestTo : "cost-tracker"
4481
4801
  };
4482
4802
  }
4483
- var modelKeyCache = /* @__PURE__ */ new Map();
4803
+ var modelKeyCache = new BoundedMap({ max: 256 });
4484
4804
  function estimateCost(model, promptTokens, completionTokens) {
4485
4805
  let key = modelKeyCache.get(model);
4486
4806
  if (!key) {
@@ -4904,6 +5224,7 @@ var plugin15 = {
4904
5224
  api.metrics.histogram("cron_job_interval_ms", job.intervalMs, { job: name });
4905
5225
  scheduleNextRun(name);
4906
5226
  }, delay);
5227
+ timer.unref?.();
4907
5228
  state14.timers.set(name, timer);
4908
5229
  }
4909
5230
  function cancelJob(name) {
@@ -5089,7 +5410,7 @@ var cron_default = plugin15;
5089
5410
 
5090
5411
  // src/dead-code-detector/index.ts
5091
5412
  import { readdir, readFile as readFile5, stat as stat2 } from "node:fs/promises";
5092
- import { extname as extname3, join, relative as relative9, resolve as resolve9 } from "node:path";
5413
+ import { extname as extname4, join as join2, relative as relative10, resolve as resolve10 } from "node:path";
5093
5414
  var API_VERSION10 = "^0.1.10";
5094
5415
  var state15 = {
5095
5416
  scanCount: 0,
@@ -5132,17 +5453,17 @@ async function gatherFiles(root, depth, cfg) {
5132
5453
  for (const entry of entries) {
5133
5454
  if (entry.isDirectory()) {
5134
5455
  if (remaining > 0 && !excludeSet.has(entry.name)) {
5135
- await walk(join(dir, entry.name), remaining - 1);
5456
+ await walk(join2(dir, entry.name), remaining - 1);
5136
5457
  }
5137
5458
  } else if (entry.isFile()) {
5138
- const ext = extname3(entry.name).toLowerCase();
5459
+ const ext = extname4(entry.name).toLowerCase();
5139
5460
  if (cfg.extensions.includes(ext)) {
5140
- files.push(join(dir, entry.name));
5461
+ files.push(join2(dir, entry.name));
5141
5462
  }
5142
5463
  }
5143
5464
  }
5144
5465
  }
5145
- await walk(resolve9(root), depth);
5466
+ await walk(resolve10(root), depth);
5146
5467
  return files;
5147
5468
  }
5148
5469
  function stripNoise(content) {
@@ -5197,7 +5518,10 @@ function findUnusedSymbols(files) {
5197
5518
  for (const file of files) {
5198
5519
  const fileExports = extractExports2(file.content, file.path);
5199
5520
  for (const exp of fileExports) {
5200
- const pattern = new RegExp(`\\b${escapeRegex(exp.identifier)}\\b`, "g");
5521
+ const pattern = new RegExp(
5522
+ `(?<![A-Za-z0-9_$])${escapeRegex(exp.identifier)}(?![A-Za-z0-9_$])`,
5523
+ "g"
5524
+ );
5201
5525
  let usedElsewhere = false;
5202
5526
  for (const other of files) {
5203
5527
  if (other.path === file.path) continue;
@@ -5231,11 +5555,11 @@ async function scan(root, depth, cfg) {
5231
5555
  return { findings: findUnusedSymbols(files), scannedFiles: files.length };
5232
5556
  }
5233
5557
  async function resolveScanRoot(rawPath) {
5234
- const resolved = resolve9(process.cwd(), rawPath);
5558
+ const resolved = resolve10(process.cwd(), rawPath);
5235
5559
  try {
5236
5560
  const stats = await stat2(resolved);
5237
5561
  if (!stats.isDirectory()) {
5238
- return resolve9(resolved, "..");
5562
+ return resolve10(resolved, "..");
5239
5563
  }
5240
5564
  } catch {
5241
5565
  }
@@ -5245,7 +5569,7 @@ function toPosix2(p) {
5245
5569
  return p.replace(/\\/g, "/");
5246
5570
  }
5247
5571
  function relativePath2(p) {
5248
- return toPosix2(relative9(process.cwd(), p));
5572
+ return toPosix2(relative10(process.cwd(), p));
5249
5573
  }
5250
5574
  var plugin16 = {
5251
5575
  name: "dead-code-detector",
@@ -5316,8 +5640,8 @@ var plugin16 = {
5316
5640
  state15.errorCount += 1;
5317
5641
  return;
5318
5642
  }
5319
- const changedFile = resolve9(process.cwd(), sourcePath);
5320
- const relevant = result.findings.filter((f) => resolve9(f.file) === changedFile);
5643
+ const changedFile = resolve10(process.cwd(), sourcePath);
5644
+ const relevant = result.findings.filter((f) => resolve10(f.file) === changedFile);
5321
5645
  if (relevant.length === 0) {
5322
5646
  state15.missCount += 1;
5323
5647
  return;
@@ -5910,34 +6234,51 @@ function isInstallCommand(input) {
5910
6234
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
5911
6235
  return INSTALL_RE2.test(command);
5912
6236
  }
6237
+ var LOCKFILE_MANAGERS = [
6238
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
6239
+ { file: "bun.lockb", manager: "bun" },
6240
+ { file: "bun.lock", manager: "bun" },
6241
+ { file: "yarn.lock", manager: "yarn" },
6242
+ { file: "package-lock.json", manager: "npm" }
6243
+ ];
5913
6244
  async function detectManager() {
5914
- try {
5915
- await access("pnpm-lock.yaml");
5916
- return "pnpm";
5917
- } catch {
5918
- return "npm";
6245
+ for (const { file, manager } of LOCKFILE_MANAGERS) {
6246
+ try {
6247
+ await access(file);
6248
+ return manager;
6249
+ } catch {
6250
+ }
5919
6251
  }
6252
+ return "npm";
5920
6253
  }
5921
6254
  async function runAudit(cfg) {
5922
6255
  const manager = await detectManager();
5923
6256
  const start = Date.now();
5924
- const stdout = await new Promise((resolve27) => {
6257
+ let invocation;
6258
+ try {
6259
+ invocation = resolveExecInvocation(manager, ["audit", "--json"]);
6260
+ } catch {
6261
+ return null;
6262
+ }
6263
+ const stdout = await new Promise((resolve28) => {
5925
6264
  execFile4(
5926
- manager,
5927
- ["audit", "--json"],
6265
+ invocation.cmd,
6266
+ invocation.args,
5928
6267
  {
5929
6268
  encoding: "utf8",
5930
6269
  timeout: cfg.timeoutMs,
5931
6270
  cwd: process.cwd(),
5932
6271
  windowsHide: true,
5933
- maxBuffer: 10 * 1024 * 1024
6272
+ maxBuffer: 10 * 1024 * 1024,
6273
+ shell: false,
6274
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
5934
6275
  },
5935
6276
  (error, output) => {
5936
6277
  if (error && error.killed) {
5937
- resolve27(null);
6278
+ resolve28(null);
5938
6279
  return;
5939
6280
  }
5940
- resolve27(output);
6281
+ resolve28(output);
5941
6282
  }
5942
6283
  );
5943
6284
  });
@@ -6160,7 +6501,7 @@ function contentHash(s) {
6160
6501
  }
6161
6502
  return h >>> 0;
6162
6503
  }
6163
- var pathMemo = /* @__PURE__ */ new Map();
6504
+ var pathMemo = new BoundedMap({ max: 512 });
6164
6505
  function runGit2(args, cwd) {
6165
6506
  return new Promise((resolveCommand) => {
6166
6507
  execFile5(
@@ -6275,7 +6616,7 @@ var plugin19 = {
6275
6616
  state18.fallbackCount = 0;
6276
6617
  state18.throttledCount = 0;
6277
6618
  state18.duplicateContentCount = 0;
6278
- state18.hookUnregister = null;
6619
+ state18.hookUnregister = releaseHandle(state18.hookUnregister);
6279
6620
  state18.lastSummary = null;
6280
6621
  pathMemo.clear();
6281
6622
  const cfg = readConfig16(api.config.extensions?.["diff-summary"]);
@@ -6422,7 +6763,7 @@ var plugin19 = {
6422
6763
  var diff_summary_default = plugin19;
6423
6764
 
6424
6765
  // src/doc-sync-guard/index.ts
6425
- import { basename as basename3, extname as extname4 } from "node:path";
6766
+ import { basename as basename3, extname as extname5 } from "node:path";
6426
6767
  var API_VERSION13 = "^0.1.10";
6427
6768
  var state19 = {
6428
6769
  changedFiles: [],
@@ -6451,7 +6792,7 @@ function normalizeSlashes(p) {
6451
6792
  return p.replace(/\\/g, "/");
6452
6793
  }
6453
6794
  function isSourceFile(p, extensions) {
6454
- const ext = extname4(p).toLowerCase();
6795
+ const ext = extname5(p).toLowerCase();
6455
6796
  return extensions.includes(ext);
6456
6797
  }
6457
6798
  function isPublicSource(p, extensions) {
@@ -6541,7 +6882,7 @@ var plugin20 = {
6541
6882
  state19.sourceWrites = 0;
6542
6883
  state19.docWrites = 0;
6543
6884
  state19.warningsIssued = 0;
6544
- state19.hookUnregister = null;
6885
+ state19.hookUnregister = releaseHandle(state19.hookUnregister);
6545
6886
  const cfg = readConfig17(api.config.extensions?.["doc-sync-guard"]);
6546
6887
  const hook = (input) => {
6547
6888
  if (!cfg.enabled) return;
@@ -6634,8 +6975,9 @@ var doc_sync_guard_default = plugin20;
6634
6975
 
6635
6976
  // src/duplicate-code-detector/index.ts
6636
6977
  import { readFile as readFile6, realpath, stat as stat3 } from "node:fs/promises";
6637
- import { isAbsolute as isAbsolute9, relative as relative10, resolve as resolve10, sep } from "node:path";
6978
+ import { isAbsolute as isAbsolute10, relative as relative11, resolve as resolve11, sep as sep2 } from "node:path";
6638
6979
  var API_VERSION14 = "^0.1.10";
6980
+ var HOOK_WARNING_COOLDOWN_MS = 6e4;
6639
6981
  var state20 = {
6640
6982
  scanCount: 0,
6641
6983
  findingCount: 0,
@@ -6643,7 +6985,7 @@ var state20 = {
6643
6985
  warningCount: 0,
6644
6986
  errorCount: 0,
6645
6987
  hookUnregister: null,
6646
- lastHookWarning: /* @__PURE__ */ new Map(),
6988
+ lastHookWarning: new BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS }),
6647
6989
  fileIndex: /* @__PURE__ */ new Map(),
6648
6990
  inFlightFingerprintReads: /* @__PURE__ */ new Map(),
6649
6991
  indexFingerprintCount: 0,
@@ -6690,14 +7032,14 @@ function readConfig18(raw) {
6690
7032
  };
6691
7033
  }
6692
7034
  function isWithinRoot(projectRoot, candidate) {
6693
- const rel = relative10(projectRoot, candidate);
6694
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
7035
+ const rel = relative11(projectRoot, candidate);
7036
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
6695
7037
  }
6696
7038
  function toPosix3(p) {
6697
7039
  return p.replace(/\\/g, "/");
6698
7040
  }
6699
7041
  function relativePath3(p) {
6700
- return toPosix3(relative10(process.cwd(), p));
7042
+ return toPosix3(relative11(process.cwd(), p));
6701
7043
  }
6702
7044
  function removeInlineComments(line) {
6703
7045
  return line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//, "");
@@ -6789,7 +7131,7 @@ function findDuplicates(files, minLines, maxFindings) {
6789
7131
  }
6790
7132
  async function scanPath(rawPath, cfg) {
6791
7133
  const root = process.cwd();
6792
- const resolved = isAbsolute9(rawPath) ? resolve10(rawPath) : resolve10(root, rawPath);
7134
+ const resolved = isAbsolute10(rawPath) ? resolve11(rawPath) : resolve11(root, rawPath);
6793
7135
  const filePaths = await collectSourceFilesAsync(resolved, {
6794
7136
  extensions: cfg.extensions,
6795
7137
  excludeDirs: cfg.excludeDirs
@@ -6917,8 +7259,8 @@ var plugin21 = {
6917
7259
  const inp = input.toolInput ?? {};
6918
7260
  const sourcePath = inp["path"];
6919
7261
  if (!sourcePath || typeof sourcePath !== "string") return;
6920
- const projectRoot = resolve10(process.cwd());
6921
- const resolvedFile = isAbsolute9(sourcePath) ? resolve10(sourcePath) : resolve10(projectRoot, sourcePath);
7262
+ const projectRoot = resolve11(process.cwd());
7263
+ const resolvedFile = isAbsolute10(sourcePath) ? resolve11(sourcePath) : resolve11(projectRoot, sourcePath);
6922
7264
  if (!isWithinRoot(projectRoot, resolvedFile)) return;
6923
7265
  let changedFile;
6924
7266
  try {
@@ -6933,7 +7275,7 @@ var plugin21 = {
6933
7275
  state20.hookInvocationCount += 1;
6934
7276
  const now = Date.now();
6935
7277
  const lastWarning = state20.lastHookWarning.get(changedFile);
6936
- if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
7278
+ if (lastWarning !== void 0 && now - lastWarning < HOOK_WARNING_COOLDOWN_MS) return;
6937
7279
  const changedFps = await readCachedFingerprints(changedFile, cfg.minLines);
6938
7280
  if (changedFps === null) {
6939
7281
  state20.errorCount += 1;
@@ -6952,7 +7294,7 @@ var plugin21 = {
6952
7294
  }
6953
7295
  const matched = /* @__PURE__ */ new Set();
6954
7296
  for (const p of otherFilePaths) {
6955
- if (resolve10(p) === resolve10(changedFile)) continue;
7297
+ if (resolve11(p) === resolve11(changedFile)) continue;
6956
7298
  const otherFps = await readCachedFingerprints(p, cfg.minLines);
6957
7299
  if (otherFps === null || otherFps.size === 0) continue;
6958
7300
  for (const fp of changedFps) {
@@ -6998,7 +7340,7 @@ var plugin21 = {
6998
7340
  state20.findingCount += result.findings.length;
6999
7341
  return {
7000
7342
  ok: true,
7001
- path: relativePath3(resolve10(process.cwd(), rawPath)),
7343
+ path: relativePath3(resolve11(process.cwd(), rawPath)),
7002
7344
  scannedFiles: result.scannedFiles,
7003
7345
  minLines: cfg.minLines,
7004
7346
  findings: result.findings
@@ -7373,7 +7715,7 @@ var error_lens_default = plugin22;
7373
7715
 
7374
7716
  // src/feature-flag-tracker/index.ts
7375
7717
  import { readFile as readFile7 } from "node:fs/promises";
7376
- import { isAbsolute as isAbsolute10, relative as relative11, resolve as resolve11 } from "node:path";
7718
+ import { isAbsolute as isAbsolute11, relative as relative12, resolve as resolve12 } from "node:path";
7377
7719
  var API_VERSION15 = "^0.1.10";
7378
7720
  var state22 = {
7379
7721
  scanCount: 0,
@@ -7412,11 +7754,17 @@ function toPosix4(p) {
7412
7754
  return p.replace(/\\/g, "/");
7413
7755
  }
7414
7756
  function relativePath4(p) {
7415
- return toPosix4(relative11(process.cwd(), p));
7757
+ return toPosix4(relative12(process.cwd(), p));
7416
7758
  }
7417
7759
  function compilePatterns(patterns) {
7418
- const sources = [...DEFAULT_PATTERNS2, ...patterns];
7419
- return sources.map((src) => new RegExp(src, "g"));
7760
+ const out = [];
7761
+ for (const src of [...DEFAULT_PATTERNS2, ...patterns]) {
7762
+ try {
7763
+ out.push(new RegExp(src, "g"));
7764
+ } catch {
7765
+ }
7766
+ }
7767
+ return out;
7420
7768
  }
7421
7769
  function scanFile(filePath, content, patterns, maxFindings) {
7422
7770
  const usages = [];
@@ -7442,20 +7790,31 @@ function scanFile(filePath, content, patterns, maxFindings) {
7442
7790
  }
7443
7791
  async function scanPath2(rawPath, cfg) {
7444
7792
  const root = process.cwd();
7445
- const resolved = isAbsolute10(rawPath) ? resolve11(rawPath) : resolve11(root, rawPath);
7793
+ const resolved = isAbsolute11(rawPath) ? resolve12(rawPath) : resolve12(root, rawPath);
7446
7794
  const exts = normalizeExtensions3(cfg.extensions);
7447
7795
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
7448
7796
  const patterns = compilePatterns(cfg.patterns);
7449
7797
  const usages = [];
7798
+ let scannedFiles = 0;
7799
+ let truncated = false;
7450
7800
  for (const p of files) {
7451
7801
  try {
7452
7802
  const content = await readFile7(p, "utf-8");
7803
+ scannedFiles += 1;
7453
7804
  usages.push(...scanFile(p, content, patterns, cfg.maxFindings));
7454
- if (usages.length >= cfg.maxFindings) break;
7805
+ if (usages.length >= cfg.maxFindings) {
7806
+ truncated = scannedFiles < files.length;
7807
+ break;
7808
+ }
7455
7809
  } catch {
7456
7810
  }
7457
7811
  }
7458
- return { usages: usages.slice(0, cfg.maxFindings), scannedFiles: files.length };
7812
+ return {
7813
+ usages: usages.slice(0, cfg.maxFindings),
7814
+ scannedFiles,
7815
+ discoveredFiles: files.length,
7816
+ truncated
7817
+ };
7459
7818
  }
7460
7819
  var plugin23 = {
7461
7820
  name: "feature-flag-tracker",
@@ -7513,7 +7872,7 @@ var plugin23 = {
7513
7872
  const exts = normalizeExtensions3(cfg.extensions);
7514
7873
  if (!matchesExtension(sourcePath, exts)) return;
7515
7874
  state22.hookInvocationCount += 1;
7516
- const resolved = resolve11(process.cwd(), sourcePath);
7875
+ const resolved = resolve12(process.cwd(), sourcePath);
7517
7876
  let content;
7518
7877
  try {
7519
7878
  content = await readFile7(resolved, "utf-8");
@@ -7562,8 +7921,12 @@ Make sure flag behavior is intentional and consider updating flag inventory/docs
7562
7921
  state22.flagCount += result.usages.length;
7563
7922
  return {
7564
7923
  ok: true,
7565
- path: relativePath4(resolve11(process.cwd(), rawPath)),
7924
+ path: relativePath4(resolve12(process.cwd(), rawPath)),
7566
7925
  scannedFiles: result.scannedFiles,
7926
+ discoveredFiles: result.discoveredFiles,
7927
+ // Say so when the cap stopped the walk early: a partial scan
7928
+ // that reports few findings must not read as a clean result.
7929
+ truncated: result.truncated,
7567
7930
  usages: result.usages
7568
7931
  };
7569
7932
  }
@@ -7638,16 +8001,16 @@ var feature_flag_tracker_default = plugin23;
7638
8001
 
7639
8002
  // src/file-watcher/index.ts
7640
8003
  import { watch as fsWatch } from "node:fs";
7641
- import { isAbsolute as isAbsolute11, join as join2, relative as relative12, resolve as resolve12 } from "node:path";
8004
+ import { isAbsolute as isAbsolute12, join as join3, relative as relative13, resolve as resolve13 } from "node:path";
7642
8005
  var API_VERSION16 = "^0.1.10";
7643
8006
  function withinProject3(p) {
7644
8007
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
7645
8008
  const root = process.cwd();
7646
- const resolved = isAbsolute11(p) ? resolve12(p) : resolve12(root, p);
7647
- const rel = relative12(root, resolved);
8009
+ const resolved = isAbsolute12(p) ? resolve13(p) : resolve13(root, p);
8010
+ const rel = relative13(root, resolved);
7648
8011
  if (rel === "" || rel === ".") return true;
7649
8012
  if (rel.startsWith("..")) return false;
7650
- if (isAbsolute11(rel)) return false;
8013
+ if (isAbsolute12(rel)) return false;
7651
8014
  return true;
7652
8015
  }
7653
8016
  var watch_idCounter = 0;
@@ -7718,13 +8081,12 @@ var plugin24 = {
7718
8081
  function debounceEvent(key, fn, ms) {
7719
8082
  const existing = debounceTimers.get(key);
7720
8083
  if (existing) clearTimeout(existing);
7721
- debounceTimers.set(
7722
- key,
7723
- setTimeout(() => {
7724
- debounceTimers.delete(key);
7725
- fn();
7726
- }, ms)
7727
- );
8084
+ const timer = setTimeout(() => {
8085
+ debounceTimers.delete(key);
8086
+ fn();
8087
+ }, ms);
8088
+ timer.unref?.();
8089
+ debounceTimers.set(key, timer);
7728
8090
  }
7729
8091
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
7730
8092
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
@@ -7750,7 +8112,7 @@ var plugin24 = {
7750
8112
  if (handle.events.length > 0 && !(eventType === "change" ? handle.events.includes("change") : handle.events.includes("add") || handle.events.includes("delete"))) {
7751
8113
  return;
7752
8114
  }
7753
- const rawPath = filename.startsWith(dirPath) ? filename : join2(dirPath, filename);
8115
+ const rawPath = filename.startsWith(dirPath) ? filename : join3(dirPath, filename);
7754
8116
  const fullPath = rawPath.replace(/\\/g, "/");
7755
8117
  const key = `${handle.id}:${fullPath}:${eventType}`;
7756
8118
  debounceEvent(
@@ -7970,7 +8332,8 @@ var file_watcher_default = plugin24;
7970
8332
 
7971
8333
  // src/format-on-save/index.ts
7972
8334
  import { execFile as execFile6 } from "node:child_process";
7973
- import { access as access2, stat as stat4 } from "node:fs/promises";
8335
+ import { createHash } from "node:crypto";
8336
+ import { access as access2, readFile as readFile8, stat as stat4 } from "node:fs/promises";
7974
8337
  var API_VERSION17 = "^0.1.10";
7975
8338
  var state23 = {
7976
8339
  invocationCount: 0,
@@ -8006,7 +8369,7 @@ function readConfig21(raw) {
8006
8369
  skipTtlMs: typeof r["skipTtlMs"] === "number" && r["skipTtlMs"] >= 0 ? r["skipTtlMs"] : DEFAULTS20.skipTtlMs
8007
8370
  };
8008
8371
  }
8009
- var recentlyCovered = /* @__PURE__ */ new Map();
8372
+ var recentlyCovered = new BoundedMap({ max: 256 });
8010
8373
  function clearRegistrations() {
8011
8374
  if (state23.hookUnregister) {
8012
8375
  try {
@@ -8029,6 +8392,55 @@ function evictExpired(ttlMs) {
8029
8392
  if (ts < cutoff) recentlyCovered.delete(path);
8030
8393
  }
8031
8394
  }
8395
+ var FORMATTERS = [
8396
+ {
8397
+ packageName: "@biomejs/biome",
8398
+ binName: "biome",
8399
+ writeArgs: ["format", "--write"],
8400
+ label: "biome"
8401
+ },
8402
+ { packageName: "prettier", binName: "prettier", writeArgs: ["--write"], label: "prettier" },
8403
+ { packageName: "dprint", binName: "dprint", writeArgs: ["fmt"], label: "dprint" }
8404
+ ];
8405
+ var activeFormatter;
8406
+ function resetFormatter() {
8407
+ activeFormatter = void 0;
8408
+ clearLocalBinCache();
8409
+ }
8410
+ function resolveFormatter() {
8411
+ if (activeFormatter !== void 0) return activeFormatter;
8412
+ const cwd = process.cwd();
8413
+ for (const f of FORMATTERS) {
8414
+ const local = resolveNodeBinFor(f, cwd);
8415
+ if (local) {
8416
+ activeFormatter = local;
8417
+ return local;
8418
+ }
8419
+ }
8420
+ for (const f of FORMATTERS) {
8421
+ const onPath = findOnPath(f.binName);
8422
+ if (!onPath) continue;
8423
+ activeFormatter = { cmd: onPath, args: [...f.writeArgs], label: f.label };
8424
+ return activeFormatter;
8425
+ }
8426
+ activeFormatter = null;
8427
+ return null;
8428
+ }
8429
+ function resolveNodeBinFor(f, cwd) {
8430
+ const hit = resolveFirstNodeBin([{ packageName: f.packageName, binName: f.binName }], cwd);
8431
+ if (!hit) return null;
8432
+ return { cmd: hit.cmd, args: [...hit.args, ...f.writeArgs], label: f.label };
8433
+ }
8434
+ function activeFormatterLabel() {
8435
+ return activeFormatter?.label ?? null;
8436
+ }
8437
+ async function sha256File(filePath) {
8438
+ try {
8439
+ return createHash("sha256").update(await readFile8(filePath)).digest("hex");
8440
+ } catch {
8441
+ return null;
8442
+ }
8443
+ }
8032
8444
  async function formatFile(filePath, timeoutMs) {
8033
8445
  if (!withinProject(filePath)) return null;
8034
8446
  try {
@@ -8036,30 +8448,40 @@ async function formatFile(filePath, timeoutMs) {
8036
8448
  } catch {
8037
8449
  return null;
8038
8450
  }
8451
+ const formatter = resolveFormatter();
8452
+ if (!formatter) return null;
8039
8453
  let bytesBefore;
8040
8454
  try {
8041
8455
  bytesBefore = (await stat4(filePath)).size;
8042
8456
  } catch {
8043
8457
  return null;
8044
8458
  }
8459
+ const hashBefore = await sha256File(filePath);
8460
+ let invocation;
8461
+ try {
8462
+ invocation = resolveExecInvocation(formatter.cmd, [...formatter.args, filePath]);
8463
+ } catch {
8464
+ return null;
8465
+ }
8045
8466
  try {
8046
- await new Promise((resolve27, reject) => {
8467
+ await new Promise((resolve28, reject) => {
8047
8468
  execFile6(
8048
- "npx",
8049
- ["biome", "format", "--write", filePath],
8469
+ invocation.cmd,
8470
+ invocation.args,
8050
8471
  {
8051
8472
  encoding: "utf-8",
8052
8473
  timeout: timeoutMs,
8053
8474
  cwd: process.cwd(),
8054
8475
  windowsHide: true,
8055
- maxBuffer: 16 * 1024 * 1024
8476
+ maxBuffer: 16 * 1024 * 1024,
8477
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
8056
8478
  },
8057
8479
  (err) => {
8058
8480
  if (err) {
8059
8481
  const e = err;
8060
8482
  if (e.killed) return reject(err);
8061
8483
  }
8062
- resolve27();
8484
+ resolve28();
8063
8485
  }
8064
8486
  );
8065
8487
  });
@@ -8075,28 +8497,11 @@ async function formatFile(filePath, timeoutMs) {
8075
8497
  if (bytesAfter !== bytesBefore) {
8076
8498
  return { changed: true, bytesBefore, bytesAfter };
8077
8499
  }
8078
- try {
8079
- await new Promise((resolve27, reject) => {
8080
- execFile6(
8081
- "npx",
8082
- ["biome", "format", filePath],
8083
- {
8084
- encoding: "utf-8",
8085
- timeout: timeoutMs,
8086
- cwd: process.cwd(),
8087
- windowsHide: true,
8088
- maxBuffer: 16 * 1024 * 1024
8089
- },
8090
- (err) => {
8091
- if (err) return reject(err);
8092
- resolve27();
8093
- }
8094
- );
8095
- });
8500
+ const hashAfter = await sha256File(filePath);
8501
+ if (hashBefore === null || hashAfter === null) {
8096
8502
  return { changed: false, bytesBefore, bytesAfter };
8097
- } catch {
8098
- return { changed: true, bytesBefore, bytesAfter };
8099
8503
  }
8504
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
8100
8505
  }
8101
8506
  var plugin25 = {
8102
8507
  name: "format-on-save",
@@ -8142,32 +8547,18 @@ var plugin25 = {
8142
8547
  state23.lastResult = null;
8143
8548
  recentlyCovered.clear();
8144
8549
  const cfg = readConfig21(api.config.extensions?.["format-on-save"]);
8145
- let biomeAvailable = false;
8146
- try {
8147
- await new Promise((resolve27, reject) => {
8148
- execFile6(
8149
- "npx",
8150
- ["biome", "--version"],
8151
- {
8152
- encoding: "utf-8",
8153
- timeout: 5e3,
8154
- cwd: process.cwd(),
8155
- windowsHide: true
8156
- },
8157
- (err) => {
8158
- if (err) reject(err);
8159
- else resolve27();
8160
- }
8161
- );
8162
- });
8163
- biomeAvailable = true;
8164
- api.log.info("format-on-save: biome detected");
8165
- } catch {
8166
- biomeAvailable = false;
8167
- api.log.warn("format-on-save: biome not found \u2014 hook will be a no-op");
8550
+ resetFormatter();
8551
+ const formatter = resolveFormatter();
8552
+ const formatterAvailable = formatter !== null;
8553
+ if (formatter) {
8554
+ api.log.info(`format-on-save: ${formatter.label} detected`);
8555
+ } else {
8556
+ api.log.warn(
8557
+ "format-on-save: no formatter found (biome, prettier, dprint) \u2014 hook will be a no-op"
8558
+ );
8168
8559
  }
8169
8560
  const hook = async (input) => {
8170
- if (!cfg.enabled || !biomeAvailable) return;
8561
+ if (!cfg.enabled || !formatterAvailable) return;
8171
8562
  if (input.toolResult?.isError) return;
8172
8563
  const toolName = input.toolName ?? "";
8173
8564
  const inp = input.toolInput ?? {};
@@ -8228,7 +8619,7 @@ var plugin25 = {
8228
8619
  );
8229
8620
  api.tools.register({
8230
8621
  name: "format_on_save_status",
8231
- description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
8622
+ description: "Reports format-on-save state: which formatter is active, and per-session formatted/clean/error/skipped counters.",
8232
8623
  inputSchema: { type: "object", properties: {} },
8233
8624
  permission: "auto",
8234
8625
  category: "Code Quality",
@@ -8237,7 +8628,14 @@ var plugin25 = {
8237
8628
  return {
8238
8629
  ok: true,
8239
8630
  enabled: cfg.enabled,
8240
- biomeAvailable,
8631
+ formatterAvailable,
8632
+ formatter: activeFormatterLabel(),
8633
+ /**
8634
+ * Back-compat alias for the pre-multi-formatter status shape.
8635
+ * Now accurate rather than assumed: true only when the resolved
8636
+ * formatter really is biome.
8637
+ */
8638
+ biomeAvailable: activeFormatterLabel() === "biome",
8241
8639
  timeoutMs: cfg.timeoutMs,
8242
8640
  skipWhenCoveredBy: cfg.skipWhenCoveredBy,
8243
8641
  skipTtlMs: cfg.skipTtlMs,
@@ -8255,11 +8653,13 @@ var plugin25 = {
8255
8653
  api.log.info("format-on-save plugin loaded", {
8256
8654
  version: "0.1.0",
8257
8655
  enabled: cfg.enabled,
8258
- biomeAvailable
8656
+ formatterAvailable,
8657
+ formatter: activeFormatterLabel()
8259
8658
  });
8260
8659
  },
8261
8660
  teardown(api) {
8262
8661
  clearRegistrations();
8662
+ resetFormatter();
8263
8663
  const final = {
8264
8664
  invocations: state23.invocationCount,
8265
8665
  formatted: state23.formattedCount,
@@ -8329,10 +8729,63 @@ async function runGit3(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
8329
8729
  );
8330
8730
  });
8331
8731
  }
8732
+ function unquotePorcelainPath(raw) {
8733
+ if (!raw.startsWith('"') || !raw.endsWith('"') || raw.length < 2) return raw;
8734
+ const inner = raw.slice(1, -1);
8735
+ const bytes = [];
8736
+ for (let i = 0; i < inner.length; i++) {
8737
+ const ch = inner[i];
8738
+ if (ch !== "\\") {
8739
+ for (const b of Buffer.from(ch, "utf8")) bytes.push(b);
8740
+ continue;
8741
+ }
8742
+ const next = inner[i + 1];
8743
+ if (next === void 0) {
8744
+ bytes.push(92);
8745
+ break;
8746
+ }
8747
+ const simple = {
8748
+ n: 10,
8749
+ t: 9,
8750
+ r: 13,
8751
+ a: 7,
8752
+ b: 8,
8753
+ f: 12,
8754
+ v: 11,
8755
+ '"': 34,
8756
+ "\\": 92
8757
+ };
8758
+ const mapped = simple[next];
8759
+ if (mapped !== void 0) {
8760
+ bytes.push(mapped);
8761
+ i += 1;
8762
+ continue;
8763
+ }
8764
+ const octal = /^[0-7]{1,3}/.exec(inner.slice(i + 1));
8765
+ if (octal) {
8766
+ bytes.push(Number.parseInt(octal[0], 8) & 255);
8767
+ i += octal[0].length;
8768
+ continue;
8769
+ }
8770
+ for (const b of Buffer.from(next, "utf8")) bytes.push(b);
8771
+ i += 1;
8772
+ }
8773
+ return Buffer.from(bytes).toString("utf8");
8774
+ }
8775
+ function parsePorcelainLine(line) {
8776
+ const body = line.slice(3);
8777
+ if (!body) return null;
8778
+ const status = line.slice(0, 2);
8779
+ if (status.includes("R") || status.includes("C")) {
8780
+ const arrow = body.lastIndexOf(" -> ");
8781
+ if (arrow !== -1) return unquotePorcelainPath(body.slice(arrow + 4).trim());
8782
+ }
8783
+ return unquotePorcelainPath(body.trim());
8784
+ }
8332
8785
  async function getChangedFiles(cwd) {
8333
8786
  const output = await runGit3(["status", "--porcelain"], cwd);
8334
8787
  if (!output) return [];
8335
- return output.split("\n").filter((l) => l.trim()).map((l) => l.slice(3).trim());
8788
+ return output.split("\n").filter((l) => l.trim()).map((l) => parsePorcelainLine(l)).filter((p) => p !== null && p.length > 0);
8336
8789
  }
8337
8790
  async function getStagedFiles(cwd) {
8338
8791
  const output = await runGit3(["diff", "--cached", "--name-only"], cwd);
@@ -8348,7 +8801,7 @@ async function stageFiles(files, cwd) {
8348
8801
  }
8349
8802
  });
8350
8803
  if (existing.length === 0) throw new Error("No files exist to stage");
8351
- await runGit3(["add", ...existing], cwd);
8804
+ await runGit3(["add", "--", ...existing], cwd);
8352
8805
  }
8353
8806
  async function commitWithMessage(message, cwd) {
8354
8807
  return await runGit3(["commit", "-m", message], cwd, GIT_COMMIT_TIMEOUT_MS);
@@ -8401,7 +8854,7 @@ async function externalChangesSinceStage(cwd) {
8401
8854
  const unstaged = out.split("\n").filter((l) => l.trim()).filter((l) => {
8402
8855
  const idx = l[0] ?? " ";
8403
8856
  return idx === " " || idx === "?";
8404
- }).map((l) => l.slice(3).trim());
8857
+ }).map((l) => parsePorcelainLine(l)).filter((p) => p !== null && p.length > 0);
8405
8858
  return unstaged.length > 0 ? unstaged : null;
8406
8859
  } catch {
8407
8860
  return null;
@@ -8768,7 +9221,7 @@ var git_autocommit_default = plugin26;
8768
9221
  // src/import-organizer/index.ts
8769
9222
  import { spawn } from "node:child_process";
8770
9223
  import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
8771
- import { basename as basename4, isAbsolute as isAbsolute12 } from "node:path";
9224
+ import { basename as basename4, isAbsolute as isAbsolute13 } from "node:path";
8772
9225
  var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
8773
9226
  "npx",
8774
9227
  "pnpm",
@@ -8777,23 +9230,51 @@ var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
8777
9230
  "biome",
8778
9231
  "@biomejs/biome",
8779
9232
  "eslint",
9233
+ "oxlint",
8780
9234
  "node"
8781
9235
  // Recognise a fully-qualified project-local path like
8782
9236
  // "./node_modules/.bin/biome"; basename check kicks in below.
8783
9237
  ]);
9238
+ var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "npm", "yarn"]);
9239
+ var LOCAL_BIN_PACKAGES = {
9240
+ biome: { packageName: "@biomejs/biome", binName: "biome" },
9241
+ "@biomejs/biome": { packageName: "@biomejs/biome", binName: "biome" },
9242
+ eslint: { packageName: "eslint", binName: "eslint" },
9243
+ oxlint: { packageName: "oxlint", binName: "oxlint" }
9244
+ };
9245
+ function resolveLocalToolCommand(tokens) {
9246
+ if (tokens.length === 0) return null;
9247
+ let i = 0;
9248
+ if (PACKAGE_RUNNERS.has(tokens[i])) {
9249
+ i++;
9250
+ while (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx" || tokens[i] === "run")) {
9251
+ i++;
9252
+ }
9253
+ }
9254
+ const toolToken = tokens[i];
9255
+ if (!toolToken) return null;
9256
+ const rest = tokens.slice(i + 1);
9257
+ const pkg = LOCAL_BIN_PACKAGES[toolToken];
9258
+ if (!pkg) return null;
9259
+ const local = resolveNodeBin(pkg.packageName, pkg.binName, process.cwd(), rest);
9260
+ if (!local) return null;
9261
+ return { cmd: local.cmd, args: local.args };
9262
+ }
8784
9263
  function resolveAllowedCommand(command) {
8785
9264
  const tokens = command.split(/\s+/).filter(Boolean);
8786
9265
  if (tokens.length === 0) return null;
8787
9266
  const head = tokens[0];
9267
+ const local = ALLOWED_FIRST_TOKENS.has(head) ? resolveLocalToolCommand(tokens) : null;
9268
+ if (local) return local;
8788
9269
  if (ALLOWED_FIRST_TOKENS.has(head)) {
8789
9270
  return { cmd: head, args: tokens.slice(1) };
8790
9271
  }
8791
- if (isAbsolute12(head)) {
9272
+ if (isAbsolute13(head)) {
8792
9273
  if (!withinProject(head)) return null;
8793
9274
  const base = basename4(head);
8794
9275
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
8795
9276
  }
8796
- if (!isAbsolute12(head) && withinProject(head)) {
9277
+ if (!isAbsolute13(head) && withinProject(head)) {
8797
9278
  const base = basename4(head);
8798
9279
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
8799
9280
  }
@@ -8828,40 +9309,70 @@ function readConfig22(raw) {
8828
9309
  notifyFormatOnSave: r["notifyFormatOnSave"] !== false
8829
9310
  };
8830
9311
  }
9312
+ var MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
8831
9313
  function runCommand(command, args, timeoutMs, cwd) {
8832
- return new Promise((resolve27) => {
9314
+ return new Promise((resolve28) => {
8833
9315
  let timedOut = false;
9316
+ let settled = false;
9317
+ const settle = (r) => {
9318
+ if (settled) return;
9319
+ settled = true;
9320
+ resolve28(r);
9321
+ };
8834
9322
  const stdoutChunks = [];
8835
9323
  const stderrChunks = [];
9324
+ let stdoutBytes = 0;
9325
+ let stderrBytes = 0;
9326
+ const signal = AbortSignal.timeout(timeoutMs);
9327
+ const onAbort = () => {
9328
+ timedOut = true;
9329
+ settle({ code: null, stdout: "", stderr: "", timedOut: true });
9330
+ };
9331
+ signal.addEventListener("abort", onAbort, { once: true });
8836
9332
  let child;
9333
+ let invocation;
8837
9334
  try {
8838
- child = spawn(command, args, {
9335
+ invocation = resolveExecInvocation(command, args);
9336
+ } catch {
9337
+ signal.removeEventListener("abort", onAbort);
9338
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
9339
+ return;
9340
+ }
9341
+ try {
9342
+ child = spawn(invocation.cmd, invocation.args, {
8839
9343
  cwd,
8840
9344
  stdio: ["ignore", "pipe", "pipe"],
8841
- signal: AbortSignal.timeout(timeoutMs)
9345
+ signal,
9346
+ windowsHide: true,
9347
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
8842
9348
  });
8843
9349
  } catch {
8844
- resolve27({ code: 127, stdout: "", stderr: "", timedOut: false });
9350
+ signal.removeEventListener("abort", onAbort);
9351
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
8845
9352
  return;
8846
9353
  }
8847
- child.stdout?.on("data", (c) => stdoutChunks.push(c));
8848
- child.stderr?.on("data", (c) => stderrChunks.push(c));
9354
+ child.stdout?.on("data", (c) => {
9355
+ stdoutBytes += c.length;
9356
+ if (stdoutBytes <= MAX_CAPTURE_BYTES) stdoutChunks.push(c);
9357
+ });
9358
+ child.stderr?.on("data", (c) => {
9359
+ stderrBytes += c.length;
9360
+ if (stderrBytes <= MAX_CAPTURE_BYTES) stderrChunks.push(c);
9361
+ });
8849
9362
  child.on("error", () => {
8850
- resolve27({ code: 127, stdout: "", stderr: "", timedOut: false });
9363
+ signal.removeEventListener("abort", onAbort);
9364
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
8851
9365
  });
8852
9366
  child.on("close", (code) => {
9367
+ signal.removeEventListener("abort", onAbort);
8853
9368
  if (timedOut) return;
8854
- resolve27({
9369
+ settle({
8855
9370
  code,
8856
9371
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
8857
9372
  stderr: Buffer.concat(stderrChunks).toString("utf-8"),
8858
9373
  timedOut: false
8859
9374
  });
8860
9375
  });
8861
- child.on("abort", () => {
8862
- timedOut = true;
8863
- resolve27({ code: null, stdout: "", stderr: "", timedOut: true });
8864
- });
8865
9376
  });
8866
9377
  }
8867
9378
  async function organizeImports(filePath, cfg, cwd) {
@@ -8945,7 +9456,7 @@ var plugin27 = {
8945
9456
  state24.organizedCount = 0;
8946
9457
  state24.cleanCount = 0;
8947
9458
  state24.errorCount = 0;
8948
- state24.hookUnregister = null;
9459
+ state24.hookUnregister = releaseHandle(state24.hookUnregister);
8949
9460
  state24.lastResult = null;
8950
9461
  state24.probeComplete = false;
8951
9462
  state24.linterAvailable = false;
@@ -9152,12 +9663,19 @@ var PATTERNS = [
9152
9663
  re: /(?:\u200B|\u200C|\u200D|\u2060|\uFEFF){20,}/
9153
9664
  }
9154
9665
  ];
9666
+ var INVISIBLE_CHARS = /[\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/g;
9667
+ function stripInvisible(text) {
9668
+ INVISIBLE_CHARS.lastIndex = 0;
9669
+ return text.replace(INVISIBLE_CHARS, "");
9670
+ }
9155
9671
  function scanForInjection(text) {
9156
- const hits = [];
9672
+ const hits = /* @__PURE__ */ new Set();
9673
+ const cleaned = stripInvisible(text);
9157
9674
  for (const p of PATTERNS) {
9158
- if (p.re.test(text)) hits.push(p.name);
9675
+ if (p.re.test(text)) hits.add(p.name);
9676
+ else if (cleaned !== text && p.re.test(cleaned)) hits.add(p.name);
9159
9677
  }
9160
- return hits;
9678
+ return [...hits];
9161
9679
  }
9162
9680
  var plugin28 = {
9163
9681
  name: "injection-shield",
@@ -9292,8 +9810,8 @@ var plugin28 = {
9292
9810
  var injection_shield_default = plugin28;
9293
9811
 
9294
9812
  // src/interface-contract-guard/index.ts
9295
- import { readFile as readFile8 } from "node:fs/promises";
9296
- import { isAbsolute as isAbsolute13, relative as relative13, resolve as resolve13 } from "node:path";
9813
+ import { readFile as readFile9 } from "node:fs/promises";
9814
+ import { isAbsolute as isAbsolute14, relative as relative14, resolve as resolve14 } from "node:path";
9297
9815
  var API_VERSION20 = "^0.1.10";
9298
9816
  var state26 = {
9299
9817
  scanCount: 0,
@@ -9306,7 +9824,8 @@ var state26 = {
9306
9824
  var DEFAULTS23 = {
9307
9825
  enabled: false,
9308
9826
  extensions: [".ts", ".tsx"],
9309
- maxFindings: 50
9827
+ maxFindings: 50,
9828
+ maxFiles: 2e3
9310
9829
  };
9311
9830
  function readConfig24(raw) {
9312
9831
  if (!raw || typeof raw !== "object") return { ...DEFAULTS23 };
@@ -9314,7 +9833,8 @@ function readConfig24(raw) {
9314
9833
  return {
9315
9834
  enabled: r["enabled"] !== false,
9316
9835
  extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS23.extensions,
9317
- maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS23.maxFindings
9836
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS23.maxFindings,
9837
+ maxFiles: typeof r["maxFiles"] === "number" && r["maxFiles"] >= 1 && r["maxFiles"] <= 5e4 ? Math.floor(r["maxFiles"]) : DEFAULTS23.maxFiles
9318
9838
  };
9319
9839
  }
9320
9840
  function normalizeExtensions4(exts) {
@@ -9324,53 +9844,66 @@ function toPosix5(p) {
9324
9844
  return p.replace(/\\/g, "/");
9325
9845
  }
9326
9846
  function relativePath5(p) {
9327
- return toPosix5(relative13(process.cwd(), p));
9847
+ return toPosix5(relative14(process.cwd(), p));
9328
9848
  }
9329
9849
  var INTERFACE_RE = /(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
9330
9850
  function extractInterfaceNames(content) {
9331
9851
  const names = [];
9332
9852
  INTERFACE_RE.lastIndex = 0;
9853
+ let searchedUpTo = 0;
9854
+ let line = 1;
9333
9855
  for (const m of content.matchAll(INTERFACE_RE)) {
9334
- names.push({ name: m[1], line: content.slice(0, m.index).split(/\r?\n/).length });
9856
+ const index = m.index ?? 0;
9857
+ for (let i = searchedUpTo; i < index; i++) {
9858
+ if (content.charCodeAt(i) === 10) line += 1;
9859
+ }
9860
+ searchedUpTo = index;
9861
+ names.push({ name: m[1], line });
9335
9862
  }
9336
9863
  return names;
9337
9864
  }
9338
- function hasImplementer(name, contents) {
9339
- const implRe = new RegExp(`(?:implements|satisfies|\\bas)\\s+${name}\\b`, "g");
9340
- for (const content of contents) {
9341
- if (implRe.test(content)) return true;
9865
+ var IMPLEMENTER_RE = /(?:implements|satisfies|\bas)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
9866
+ function collectImplementedNames(content, into) {
9867
+ IMPLEMENTER_RE.lastIndex = 0;
9868
+ for (const m of content.matchAll(IMPLEMENTER_RE)) {
9869
+ const name = m[1];
9870
+ if (name) into.add(name);
9342
9871
  }
9343
- return false;
9344
9872
  }
9345
9873
  async function scanPath3(rawPath, cfg) {
9346
9874
  const root = process.cwd();
9347
- const resolved = isAbsolute13(rawPath) ? resolve13(rawPath) : resolve13(root, rawPath);
9875
+ const resolved = isAbsolute14(rawPath) ? resolve14(rawPath) : resolve14(root, rawPath);
9348
9876
  const exts = normalizeExtensions4(cfg.extensions);
9349
- const files = await collectSourceFilesAsync(resolved, { extensions: exts });
9350
- const contents = [];
9877
+ const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
9878
+ const files = allFiles.slice(0, cfg.maxFiles);
9879
+ const implemented = /* @__PURE__ */ new Set();
9880
+ const declarations = [];
9881
+ let scannedFiles = 0;
9351
9882
  for (const p of files) {
9883
+ let content;
9352
9884
  try {
9353
- contents.push({ path: p, content: await readFile8(p, "utf-8") });
9885
+ content = await readFile9(p, "utf-8");
9354
9886
  } catch {
9887
+ continue;
9355
9888
  }
9356
- }
9357
- const allContentStrings = contents.map((c) => c.content);
9358
- const findings = [];
9359
- for (const { path, content } of contents) {
9889
+ scannedFiles += 1;
9890
+ collectImplementedNames(content, implemented);
9360
9891
  for (const { name, line } of extractInterfaceNames(content)) {
9361
- if (!hasImplementer(name, allContentStrings)) {
9362
- findings.push({
9363
- interfaceName: name,
9364
- file: relativePath5(path),
9365
- line,
9366
- message: `interface "${name}" is declared but has no visible implementer`
9367
- });
9368
- if (findings.length >= cfg.maxFindings) break;
9369
- }
9892
+ declarations.push({ name, line, file: p });
9370
9893
  }
9894
+ }
9895
+ const findings = [];
9896
+ for (const { name, line, file } of declarations) {
9897
+ if (implemented.has(name)) continue;
9898
+ findings.push({
9899
+ interfaceName: name,
9900
+ file: relativePath5(file),
9901
+ line,
9902
+ message: `interface "${name}" is declared but has no visible implementer`
9903
+ });
9371
9904
  if (findings.length >= cfg.maxFindings) break;
9372
9905
  }
9373
- return { findings, scannedFiles: contents.length };
9906
+ return { findings, scannedFiles, truncated: allFiles.length > files.length };
9374
9907
  }
9375
9908
  var plugin29 = {
9376
9909
  name: "interface-contract-guard",
@@ -9389,6 +9922,13 @@ var plugin29 = {
9389
9922
  default: [".ts", ".tsx"],
9390
9923
  description: "File extensions to scan."
9391
9924
  },
9925
+ maxFiles: {
9926
+ type: "number",
9927
+ minimum: 1,
9928
+ maximum: 5e4,
9929
+ default: 2e3,
9930
+ description: "Upper bound on files read per scan."
9931
+ },
9392
9932
  maxFindings: {
9393
9933
  type: "number",
9394
9934
  minimum: 1,
@@ -9422,10 +9962,10 @@ var plugin29 = {
9422
9962
  const exts = normalizeExtensions4(cfg.extensions);
9423
9963
  if (!matchesExtension(sourcePath, exts)) return;
9424
9964
  state26.hookInvocationCount += 1;
9425
- const resolved = resolve13(process.cwd(), sourcePath);
9965
+ const resolved = resolve14(process.cwd(), sourcePath);
9426
9966
  let content;
9427
9967
  try {
9428
- content = await readFile8(resolved, "utf-8");
9968
+ content = await readFile9(resolved, "utf-8");
9429
9969
  } catch {
9430
9970
  state26.errorCount += 1;
9431
9971
  return;
@@ -9470,9 +10010,16 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
9470
10010
  state26.findingCount += result.findings.length;
9471
10011
  return {
9472
10012
  ok: true,
9473
- path: relativePath5(resolve13(process.cwd(), rawPath)),
10013
+ path: relativePath5(resolve14(process.cwd(), rawPath)),
9474
10014
  scannedFiles: result.scannedFiles,
9475
- findings: result.findings
10015
+ findings: result.findings,
10016
+ // Say so when the corpus was cut short. A partial scan that
10017
+ // reports "no findings" is not the same as a clean result, and
10018
+ // the caller has no other way to tell the difference.
10019
+ truncated: result.truncated,
10020
+ ...result.truncated ? {
10021
+ note: `Scan stopped at the maxFiles limit (${cfg.maxFiles}); results are partial. Raise \`maxFiles\` or scan a narrower path for full coverage.`
10022
+ } : {}
9476
10023
  };
9477
10024
  }
9478
10025
  });
@@ -9543,8 +10090,9 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
9543
10090
  var interface_contract_guard_default = plugin29;
9544
10091
 
9545
10092
  // src/knowledge-graph/index.ts
9546
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
9547
- import { dirname as dirname4, isAbsolute as isAbsolute14, relative as relative14, resolve as resolve14 } from "node:path";
10093
+ import { readFileSync as readFileSync7 } from "node:fs";
10094
+ import { dirname as dirname5, isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10095
+ import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
9548
10096
  var API_VERSION21 = "^0.1.10";
9549
10097
  var state27 = {
9550
10098
  facts: [],
@@ -9565,10 +10113,10 @@ var DEFAULTS24 = {
9565
10113
  };
9566
10114
  function resolveProjectPath5(rawPath, cwd = process.cwd()) {
9567
10115
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
9568
- const root = resolve14(cwd);
9569
- const resolved = isAbsolute14(rawPath) ? resolve14(rawPath) : resolve14(root, rawPath);
9570
- const rel = relative14(root, resolved);
9571
- if (rel === "" || !rel.startsWith("..") && !isAbsolute14(rel)) return resolved;
10116
+ const root = resolve15(cwd);
10117
+ const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10118
+ const rel = relative15(root, resolved);
10119
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute15(rel)) return resolved;
9572
10120
  return null;
9573
10121
  }
9574
10122
  function readConfig25(raw) {
@@ -9586,7 +10134,7 @@ function readConfig25(raw) {
9586
10134
  function loadFacts(filePath) {
9587
10135
  if (!filePath) return { facts: [], nextId: 1 };
9588
10136
  try {
9589
- const raw = JSON.parse(readFileSync6(filePath, "utf-8"));
10137
+ const raw = JSON.parse(readFileSync7(filePath, "utf-8"));
9590
10138
  const facts = Array.isArray(raw.facts) ? raw.facts.filter(
9591
10139
  (f) => !!f && typeof f === "object" && typeof f.id === "string" && typeof f.subject === "string" && typeof f.relation === "string" && typeof f.object === "string"
9592
10140
  ) : [];
@@ -9596,11 +10144,11 @@ function loadFacts(filePath) {
9596
10144
  return { facts: [], nextId: 1 };
9597
10145
  }
9598
10146
  }
9599
- function persistFacts(filePath) {
10147
+ async function persistFacts(filePath) {
9600
10148
  if (!filePath) return true;
9601
10149
  try {
9602
- mkdirSync2(dirname4(filePath), { recursive: true });
9603
- writeFileSync3(
10150
+ await ensureDir2(dirname5(filePath));
10151
+ await atomicWrite2(
9604
10152
  filePath,
9605
10153
  JSON.stringify({ facts: state27.facts, nextId: state27.nextId }, null, 2)
9606
10154
  );
@@ -9731,7 +10279,7 @@ var plugin30 = {
9731
10279
  state27.facts.push(fact);
9732
10280
  state27.adds += 1;
9733
10281
  api.metrics.counter("adds");
9734
- const persisted = persistFacts(resolved);
10282
+ const persisted = await persistFacts(resolved);
9735
10283
  return { ok: true, fact, persisted, totalFacts: state27.facts.length };
9736
10284
  }
9737
10285
  });
@@ -9791,7 +10339,7 @@ var plugin30 = {
9791
10339
  if (removed === 0) return { ok: false, error: `no fact matches "${input.id}"` };
9792
10340
  state27.removals += removed;
9793
10341
  api.metrics.counter("removals", removed);
9794
- const persisted = persistFacts(resolved);
10342
+ const persisted = await persistFacts(resolved);
9795
10343
  return { ok: true, removed, persisted, totalFacts: state27.facts.length };
9796
10344
  }
9797
10345
  });
@@ -9866,8 +10414,8 @@ var plugin30 = {
9866
10414
  var knowledge_graph_default = plugin30;
9867
10415
 
9868
10416
  // src/license-audit-gate/index.ts
9869
- import { readFileSync as readFileSync7 } from "node:fs";
9870
- import { resolve as resolve15 } from "node:path";
10417
+ import { readFileSync as readFileSync8 } from "node:fs";
10418
+ import { resolve as resolve16 } from "node:path";
9871
10419
  var API_VERSION22 = "^0.1.10";
9872
10420
  var state28 = {
9873
10421
  invocations: 0,
@@ -9943,8 +10491,8 @@ function auditPackages(names, allowedLicenses) {
9943
10491
  for (const name of names) {
9944
10492
  let licenses = [];
9945
10493
  try {
9946
- const pkgPath = resolve15("node_modules", name, "package.json");
9947
- const raw = JSON.parse(readFileSync7(pkgPath, "utf-8"));
10494
+ const pkgPath = resolve16("node_modules", name, "package.json");
10495
+ const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
9948
10496
  licenses = extractLicenseStrings(raw);
9949
10497
  } catch {
9950
10498
  errors.push(name);
@@ -10124,11 +10672,11 @@ var license_audit_gate_default = plugin31;
10124
10672
 
10125
10673
  // src/lint-gate/index.ts
10126
10674
  import { execFile as execFile8 } from "node:child_process";
10127
- import { readFileSync as readFileSync8 } from "node:fs";
10128
- import { mkdtemp, readFile as readFile9, rm, writeFile as writeFile2 } from "node:fs/promises";
10129
- import { createRequire } from "node:module";
10675
+ import { readFileSync as readFileSync9 } from "node:fs";
10676
+ import { mkdtemp, readFile as readFile10, rm, writeFile as writeFile2 } from "node:fs/promises";
10677
+ import { createRequire as createRequire2 } from "node:module";
10130
10678
  import { tmpdir } from "node:os";
10131
- import { dirname as dirname5, isAbsolute as isAbsolute15, join as join3, relative as relative15, resolve as resolve16, sep as sep2 } from "node:path";
10679
+ import { dirname as dirname6, isAbsolute as isAbsolute16, join as join4, relative as relative16, resolve as resolve17, sep as sep3 } from "node:path";
10132
10680
  var API_VERSION23 = "^0.1.10";
10133
10681
  var state29 = {
10134
10682
  /** Total PreToolUse invocations. */
@@ -10166,22 +10714,22 @@ var LINTER_PACKAGES = {
10166
10714
  biome: "@biomejs/biome",
10167
10715
  eslint: "eslint"
10168
10716
  };
10169
- var linterCache = /* @__PURE__ */ new Map();
10170
- function isInside(parent, candidate) {
10171
- const rel = relative15(parent, candidate);
10172
- return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute15(rel);
10717
+ var linterCache = new BoundedMap({ max: 32, ttlMs: 3e5 });
10718
+ function isInside2(parent, candidate) {
10719
+ const rel = relative16(parent, candidate);
10720
+ return rel === "" || !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute16(rel);
10173
10721
  }
10174
10722
  function resolveLocalLinter(name, cwd) {
10175
10723
  try {
10176
10724
  const packageName = LINTER_PACKAGES[name];
10177
- const requireFromProject = createRequire(resolve16(cwd, "package.json"));
10725
+ const requireFromProject = createRequire2(resolve17(cwd, "package.json"));
10178
10726
  const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
10179
- const packageJson = JSON.parse(readFileSync8(packagePath, "utf-8"));
10727
+ const packageJson = JSON.parse(readFileSync9(packagePath, "utf-8"));
10180
10728
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[name] ?? Object.values(packageJson.bin ?? {})[0];
10181
- if (!relativeBin || isAbsolute15(relativeBin)) return null;
10182
- const packageDir = dirname5(packagePath);
10183
- const entry = resolve16(packageDir, relativeBin);
10184
- if (!isInside(packageDir, entry)) return null;
10729
+ if (!relativeBin || isAbsolute16(relativeBin)) return null;
10730
+ const packageDir = dirname6(packagePath);
10731
+ const entry = resolve17(packageDir, relativeBin);
10732
+ if (!isInside2(packageDir, entry)) return null;
10185
10733
  return {
10186
10734
  cmd: process.execPath,
10187
10735
  args: name === "biome" ? [entry, "check", "--reporter=json"] : [entry, "--format=json"],
@@ -10232,8 +10780,8 @@ async function detectLinter(requested, cwd) {
10232
10780
  }
10233
10781
  async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
10234
10782
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
10235
- const tmpDir = await mkdtemp(join3(tmpdir(), "lint-gate-"));
10236
- const tmpFile = join3(tmpDir, `input${ext}`);
10783
+ const tmpDir = await mkdtemp(join4(tmpdir(), "lint-gate-"));
10784
+ const tmpFile = join4(tmpDir, `input${ext}`);
10237
10785
  try {
10238
10786
  await writeFile2(tmpFile, content, "utf-8");
10239
10787
  const fullArgs = [...linter.args, tmpFile];
@@ -10250,14 +10798,14 @@ async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
10250
10798
  }
10251
10799
  async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
10252
10800
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
10253
- const tmpDir = await mkdtemp(join3(tmpdir(), "lint-gate-fix-"));
10254
- const tmpFile = join3(tmpDir, `input${ext}`);
10801
+ const tmpDir = await mkdtemp(join4(tmpdir(), "lint-gate-fix-"));
10802
+ const tmpFile = join4(tmpDir, `input${ext}`);
10255
10803
  try {
10256
10804
  await writeFile2(tmpFile, content, "utf-8");
10257
10805
  const fixArgs = linter.name === "biome" ? [linter.args[0], "check", "--write", tmpFile] : [linter.args[0], "--fix", tmpFile];
10258
10806
  await runCommand2(linter.cmd, fixArgs, timeoutMs, cwd, signal);
10259
10807
  if (signal.aborted) throw signal.reason;
10260
- return await readFile9(tmpFile, "utf-8");
10808
+ return await readFile10(tmpFile, "utf-8");
10261
10809
  } catch {
10262
10810
  if (signal.aborted) throw signal.reason;
10263
10811
  return content;
@@ -10353,11 +10901,11 @@ var plugin32 = {
10353
10901
  state29.hitCount = 0;
10354
10902
  state29.fixCount = 0;
10355
10903
  state29.linterErrorCount = 0;
10356
- state29.hookUnregister = null;
10904
+ state29.hookUnregister = releaseHandle(state29.hookUnregister);
10357
10905
  state29.lastResult = null;
10358
10906
  linterCache.clear();
10359
10907
  const cfg = readConfig27(api.config.extensions?.["lint-gate"]);
10360
- const cwd = resolve16(api.config.cwd ?? process.cwd());
10908
+ const cwd = resolve17(api.config.cwd ?? process.cwd());
10361
10909
  const linterReady = detectLinter(cfg.linter, cwd).then((linter) => {
10362
10910
  if (!linter) {
10363
10911
  api.log.warn("lint-gate: no linter found (biome or eslint) \u2014 hook will be a no-op", {
@@ -10386,7 +10934,7 @@ var plugin32 = {
10386
10934
  const newStr = inp["new_string"];
10387
10935
  if (typeof oldStr !== "string" || typeof newStr !== "string") return;
10388
10936
  try {
10389
- const current = await readFile9(filePath, "utf-8");
10937
+ const current = await readFile10(filePath, "utf-8");
10390
10938
  content = applyEdit(current, oldStr, newStr);
10391
10939
  } catch {
10392
10940
  return;
@@ -10591,7 +11139,7 @@ ${summary}${truncated}`
10591
11139
  var lint_gate_default = plugin32;
10592
11140
 
10593
11141
  // src/llm-cache/index.ts
10594
- import { createHash } from "node:crypto";
11142
+ import { createHash as createHash2 } from "node:crypto";
10595
11143
  var state30 = {
10596
11144
  cache: /* @__PURE__ */ new Map(),
10597
11145
  hits: 0,
@@ -10641,7 +11189,7 @@ function fingerprintRequest(request) {
10641
11189
  topK: request["topK"] ?? null,
10642
11190
  topP: request["topP"] ?? null
10643
11191
  };
10644
- const fingerprint2 = createHash("sha256").update(JSON.stringify(subset)).digest("hex");
11192
+ const fingerprint2 = createHash2("sha256").update(JSON.stringify(subset)).digest("hex");
10645
11193
  fingerprintCache.set(request, fingerprint2);
10646
11194
  return fingerprint2;
10647
11195
  }
@@ -10920,16 +11468,26 @@ function canonicalize(value) {
10920
11468
  return String(value);
10921
11469
  }
10922
11470
  }
10923
- function sortKeys(value) {
10924
- if (Array.isArray(value)) return value.map(sortKeys);
10925
- if (value && typeof value === "object") {
11471
+ var CANONICALIZE_MAX_DEPTH = 12;
11472
+ function sortKeys(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
11473
+ if (value === null || typeof value !== "object") return value;
11474
+ if (seen.has(value)) return "[circular]";
11475
+ if (depth >= CANONICALIZE_MAX_DEPTH) {
11476
+ return Array.isArray(value) ? `[array:${value.length}]` : "[deep-object]";
11477
+ }
11478
+ seen.add(value);
11479
+ try {
11480
+ if (Array.isArray(value)) {
11481
+ return value.map((v) => sortKeys(v, depth + 1, seen));
11482
+ }
10926
11483
  const out = {};
10927
11484
  for (const key of Object.keys(value).sort()) {
10928
- out[key] = sortKeys(value[key]);
11485
+ out[key] = sortKeys(value[key], depth + 1, seen);
10929
11486
  }
10930
11487
  return out;
11488
+ } finally {
11489
+ seen.delete(value);
10931
11490
  }
10932
- return value;
10933
11491
  }
10934
11492
  function fingerprint(toolName, toolInput) {
10935
11493
  return `${toolName}\0${canonicalize(toolInput)}`;
@@ -10955,7 +11513,7 @@ function hashString2(value) {
10955
11513
  }
10956
11514
  async function gitDiffFingerprint(cwd, signal) {
10957
11515
  try {
10958
- const diff = await new Promise((resolve27, reject) => {
11516
+ const diff = await new Promise((resolve28, reject) => {
10959
11517
  execFile9(
10960
11518
  "git",
10961
11519
  ["diff", "--no-ext-diff", "--"],
@@ -10972,7 +11530,7 @@ async function gitDiffFingerprint(cwd, signal) {
10972
11530
  },
10973
11531
  (error, stdout) => {
10974
11532
  if (error) reject(error);
10975
- else resolve27(stdout);
11533
+ else resolve28(stdout);
10976
11534
  }
10977
11535
  );
10978
11536
  });
@@ -11322,7 +11880,7 @@ var plugin34 = {
11322
11880
  var loop_breaker_default = plugin34;
11323
11881
 
11324
11882
  // src/migration-planner/index.ts
11325
- import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
11883
+ import { existsSync as existsSync4, readFileSync as readFileSync10 } from "node:fs";
11326
11884
  var API_VERSION24 = "^0.1.10";
11327
11885
  var state32 = {
11328
11886
  plansGenerated: 0,
@@ -11362,7 +11920,7 @@ function readChangelog(packageName, cfg) {
11362
11920
  if (!withinProject(candidate)) continue;
11363
11921
  if (existsSync4(candidate)) {
11364
11922
  try {
11365
- const content = readFileSync9(candidate, "utf-8");
11923
+ const content = readFileSync10(candidate, "utf-8");
11366
11924
  return { source: candidate, content: content.slice(0, cfg.maxChars) };
11367
11925
  } catch {
11368
11926
  }
@@ -11570,7 +12128,7 @@ var plugin35 = {
11570
12128
  state32.llmAnalysisCount = 0;
11571
12129
  state32.llmFallbackCount = 0;
11572
12130
  state32.lastPlan = null;
11573
- state32.hookUnregister = null;
12131
+ state32.hookUnregister = releaseHandle(state32.hookUnregister);
11574
12132
  const cfg = readConfig30(api.config.extensions?.["migration-planner"]);
11575
12133
  const hook = (input) => {
11576
12134
  if (!cfg.enabled) return;
@@ -12189,13 +12747,21 @@ async function deliver(event, payload) {
12189
12747
  }
12190
12748
  const result = await deliverViaChannel(ch, event, {
12191
12749
  title: typeof payload.title === "string" ? payload.title : event,
12192
- body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : JSON.stringify(payload),
12750
+ body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : safeJsonStringify(payload),
12193
12751
  level: event === "tool.error" || event === "budget.threshold" ? "warning" : "info",
12194
12752
  source: event,
12195
12753
  metadata: payload
12196
12754
  });
12197
12755
  return result.ok;
12198
12756
  }
12757
+ function deliverDetached(event, payload) {
12758
+ deliver(event, payload).catch((err) => {
12759
+ _pluginLog?.warn("notify-hub: notification delivery failed", {
12760
+ event,
12761
+ error: err instanceof Error ? err.message : String(err)
12762
+ });
12763
+ });
12764
+ }
12199
12765
  async function deliverViaChannel(ch, event, msg) {
12200
12766
  const result = await ch.deliver(msg);
12201
12767
  if (result.ok) {
@@ -12304,7 +12870,7 @@ var plugin37 = {
12304
12870
  }
12305
12871
  if (active && cfg.events.includes("session.stop")) {
12306
12872
  const stopHook = (input) => {
12307
- void deliver("session.stop", {
12873
+ deliverDetached("session.stop", {
12308
12874
  sessionId: input.sessionId ?? null,
12309
12875
  cwd: input.cwd ?? null
12310
12876
  });
@@ -12315,7 +12881,7 @@ var plugin37 = {
12315
12881
  const off = api.onPattern("tool.*", (eventName, payload) => {
12316
12882
  if (!/error|failed/.test(eventName)) return;
12317
12883
  const p = payload;
12318
- void deliver("tool.error", {
12884
+ deliverDetached("tool.error", {
12319
12885
  tool: p?.tool ?? p?.name ?? "unknown",
12320
12886
  busEvent: eventName,
12321
12887
  error: truncateText(
@@ -12329,7 +12895,7 @@ var plugin37 = {
12329
12895
  if (active && cfg.events.includes("budget.threshold")) {
12330
12896
  const off = api.onPattern("budget.*", (eventName, payload) => {
12331
12897
  if (!eventName.includes("threshold")) return;
12332
- void deliver("budget.threshold", { busEvent: eventName, detail: payload });
12898
+ deliverDetached("budget.threshold", { busEvent: eventName, detail: payload });
12333
12899
  });
12334
12900
  state34.eventUnsubscribers.push(off);
12335
12901
  }
@@ -12692,8 +13258,8 @@ var plugin38 = {
12692
13258
  var path_guard_default = plugin38;
12693
13259
 
12694
13260
  // src/performance-regression-gate/index.ts
12695
- import { existsSync as existsSync5, readFileSync as readFileSync10 } from "node:fs";
12696
- import { isAbsolute as isAbsolute16, relative as relative16, resolve as resolve17 } from "node:path";
13261
+ import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
13262
+ import { isAbsolute as isAbsolute17, relative as relative17, resolve as resolve18 } from "node:path";
12697
13263
  var API_VERSION25 = "^0.1.10";
12698
13264
  var state36 = {
12699
13265
  invocationCount: 0,
@@ -12718,18 +13284,18 @@ function readConfig34(raw) {
12718
13284
  }
12719
13285
  function withinProject4(p, cwd = process.cwd()) {
12720
13286
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
12721
- const root = resolve17(cwd);
12722
- const resolved = isAbsolute16(p) ? resolve17(p) : resolve17(root, p);
12723
- const rel = relative16(root, resolved);
13287
+ const root = resolve18(cwd);
13288
+ const resolved = isAbsolute17(p) ? resolve18(p) : resolve18(root, p);
13289
+ const rel = relative17(root, resolved);
12724
13290
  if (rel === "" || rel === ".") return true;
12725
13291
  if (rel.startsWith("..")) return false;
12726
- if (isAbsolute16(rel)) return false;
13292
+ if (isAbsolute17(rel)) return false;
12727
13293
  return true;
12728
13294
  }
12729
13295
  function resolveProjectPath6(rawPath, cwd = process.cwd()) {
12730
13296
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
12731
- const root = resolve17(cwd);
12732
- const resolved = isAbsolute16(rawPath) ? resolve17(rawPath) : resolve17(root, rawPath);
13297
+ const root = resolve18(cwd);
13298
+ const resolved = isAbsolute17(rawPath) ? resolve18(rawPath) : resolve18(root, rawPath);
12733
13299
  if (!withinProject4(resolved, cwd)) return null;
12734
13300
  return resolved;
12735
13301
  }
@@ -12760,7 +13326,7 @@ function flattenResults(results) {
12760
13326
  function loadResults(path) {
12761
13327
  if (!path || !existsSync5(path)) return null;
12762
13328
  try {
12763
- const raw = JSON.parse(readFileSync10(path, "utf-8"));
13329
+ const raw = JSON.parse(readFileSync11(path, "utf-8"));
12764
13330
  return raw;
12765
13331
  } catch {
12766
13332
  return null;
@@ -13144,7 +13710,7 @@ var plugin_stack_observer_default = PLUGIN;
13144
13710
  // src/pr-drafter/index.ts
13145
13711
  import { execFile as execFile10 } from "node:child_process";
13146
13712
  import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
13147
- import { dirname as dirname6, isAbsolute as isAbsolute17, relative as relative17, resolve as resolve18 } from "node:path";
13713
+ import { dirname as dirname7, isAbsolute as isAbsolute18, relative as relative18, resolve as resolve19 } from "node:path";
13148
13714
  var API_VERSION26 = "^0.1.10";
13149
13715
  var state38 = {
13150
13716
  commits: [],
@@ -13183,10 +13749,10 @@ function readConfig36(raw) {
13183
13749
  }
13184
13750
  function resolveProjectPath7(rawPath, cwd = process.cwd()) {
13185
13751
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
13186
- const root = resolve18(cwd);
13187
- const resolved = isAbsolute17(rawPath) ? resolve18(rawPath) : resolve18(root, rawPath);
13188
- const rel = relative17(root, resolved);
13189
- if (rel === "" || !rel.startsWith("..") && !isAbsolute17(rel)) return resolved;
13752
+ const root = resolve19(cwd);
13753
+ const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
13754
+ const rel = relative18(root, resolved);
13755
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute18(rel)) return resolved;
13190
13756
  return null;
13191
13757
  }
13192
13758
  function runGit4(args, timeout) {
@@ -13272,7 +13838,7 @@ async function writeDraft(cfg, llm) {
13272
13838
  }
13273
13839
  const draft = await buildDraft(cfg, llm);
13274
13840
  try {
13275
- await mkdir2(dirname6(resolved), { recursive: true });
13841
+ await mkdir2(dirname7(resolved), { recursive: true });
13276
13842
  await writeFile3(resolved, draft.body);
13277
13843
  state38.draftsWritten += 1;
13278
13844
  } catch {
@@ -13336,7 +13902,7 @@ var plugin40 = {
13336
13902
  state38.draftsWritten = 0;
13337
13903
  state38.draftErrors = 0;
13338
13904
  state38.stopInvocations = 0;
13339
- state38.stopHookUnregister = null;
13905
+ state38.stopHookUnregister = releaseHandle(state38.stopHookUnregister);
13340
13906
  for (const off of state38.eventUnsubscribers) {
13341
13907
  try {
13342
13908
  off();
@@ -13399,7 +13965,7 @@ var plugin40 = {
13399
13965
  const resolved = resolveProjectPath7(cfg.outputPath);
13400
13966
  if (!resolved) return { ok: false, error: "outputPath resolves outside project" };
13401
13967
  try {
13402
- await mkdir2(dirname6(resolved), { recursive: true });
13968
+ await mkdir2(dirname7(resolved), { recursive: true });
13403
13969
  await writeFile3(resolved, draft.body);
13404
13970
  state38.draftsWritten += 1;
13405
13971
  return {
@@ -13620,26 +14186,155 @@ var plugin41 = {
13620
14186
  };
13621
14187
  var process_guard_default = plugin41;
13622
14188
 
14189
+ // src/runtime/credential-patterns.ts
14190
+ var CREDENTIAL_PATTERNS = [
14191
+ // LLM provider keys
14192
+ {
14193
+ type: "anthropic_key",
14194
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
14195
+ },
14196
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
14197
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
14198
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
14199
+ // refresh (`ghr_`) tokens grant the same or broader access and were
14200
+ // previously not detected at all.
14201
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
14202
+ {
14203
+ type: "github_oauth_token",
14204
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
14205
+ },
14206
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
14207
+ // GitLab
14208
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14209
+ {
14210
+ type: "gitlab_runner_token",
14211
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14212
+ },
14213
+ // npm — a leaked publish token is a supply-chain compromise.
14214
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
14215
+ // AWS
14216
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
14217
+ // GCP
14218
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
14219
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
14220
+ // both are as sensitive as the bot/user tokens already covered.
14221
+ {
14222
+ type: "slack_token",
14223
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
14224
+ },
14225
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
14226
+ {
14227
+ type: "slack_webhook",
14228
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
14229
+ },
14230
+ // Stripe
14231
+ {
14232
+ type: "stripe_key",
14233
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
14234
+ },
14235
+ // Twilio
14236
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
14237
+ // Telegram
14238
+ {
14239
+ type: "telegram_bot_token",
14240
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14241
+ },
14242
+ // JWT
14243
+ {
14244
+ type: "jwt",
14245
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
14246
+ },
14247
+ // Private keys
14248
+ {
14249
+ type: "private_key",
14250
+ regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
14251
+ },
14252
+ // AI/ML provider tokens
14253
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
14254
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14255
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14256
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14257
+ // SaaS / infrastructure tokens — each grants API access on the user's
14258
+ // account, and each was previously invisible to this gate.
14259
+ { type: "sendgrid_key", regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14260
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
14261
+ { type: "doppler_token", regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14262
+ { type: "shopify_token", regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g },
14263
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14264
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14265
+ { type: "atlassian_token", regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g },
14266
+ { type: "square_token", regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14267
+ {
14268
+ type: "azure_storage_key",
14269
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
14270
+ },
14271
+ {
14272
+ type: "google_oauth_client_secret",
14273
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14274
+ },
14275
+ // Bearer tokens
14276
+ {
14277
+ type: "bearer_token",
14278
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
14279
+ },
14280
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
14281
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
14282
+ {
14283
+ type: "postgres_uri",
14284
+ // Query parsers decode percent-encoded parameter names. Recognize each
14285
+ // encoded character in `password` so mixed forms such as `pass%77ord`
14286
+ // cannot bypass detection while keeping the scan strictly bounded.
14287
+ regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
14288
+ },
14289
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
14290
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g }
14291
+ ];
14292
+ function cloneCredentialPatterns() {
14293
+ return CREDENTIAL_PATTERNS.map((p) => ({
14294
+ type: p.type,
14295
+ regex: new RegExp(p.regex.source, p.regex.flags)
14296
+ }));
14297
+ }
14298
+
13623
14299
  // src/prompt-firewall/index.ts
13624
- var PATTERNS2 = [
13625
- { kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
14300
+ var KIND_ALIASES = {
14301
+ aws_access_key: "aws-access-key",
14302
+ private_key: "private-key-block",
14303
+ github_pat: "github-token",
14304
+ github_oauth_token: "github-token",
14305
+ openai_key: "openai-key",
14306
+ anthropic_key: "anthropic-key",
14307
+ slack_token: "slack-token",
14308
+ gcp_key: "google-api-key",
14309
+ bearer_token: "bearer-token"
14310
+ };
14311
+ var EXTRA_PATTERNS = [
13626
14312
  {
14313
+ // AWS secret access keys have no prefix — only a 40-char base64-ish
14314
+ // shape — so they need a nearby `aws`/`secret` mention to be worth
14315
+ // acting on.
14316
+ //
14317
+ // The previous spelling required the word "aws" to appear AFTER the
14318
+ // key and wrapped the run in `\b`. Both are wrong: the real layout is
14319
+ // `AWS_SECRET_ACCESS_KEY=<key>` (context first), and a `\b` cannot
14320
+ // hold next to the `+` or `/` that base64 keys routinely end with. The
14321
+ // pattern therefore never fired on a real key. Anchor on the context
14322
+ // token instead and let the key follow it.
13627
14323
  kind: "aws-secret-key",
13628
- re: /\b(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])\b(?=.*aws)/gi
14324
+ re: /(?:aws|amazon)[A-Za-z0-9_-]*(?:secret|key)[A-Za-z0-9_-]*\s*[:=]\s*['"]?([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/gi
13629
14325
  },
13630
- { kind: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
13631
- { kind: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
13632
- { kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
13633
- { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
13634
- { kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
13635
- { kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
13636
- { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
13637
- { kind: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/g },
13638
14326
  {
13639
14327
  kind: "generic-secret-assignment",
13640
14328
  re: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*['"]?[A-Za-z0-9._/+-]{12,}['"]?/gi
13641
14329
  }
13642
14330
  ];
14331
+ var PATTERNS2 = [
14332
+ ...cloneCredentialPatterns().map((p) => ({
14333
+ kind: KIND_ALIASES[p.type] ?? p.type,
14334
+ re: p.regex
14335
+ })),
14336
+ ...EXTRA_PATTERNS
14337
+ ];
13643
14338
  function detectSecrets(text, allow) {
13644
14339
  const counts = /* @__PURE__ */ new Map();
13645
14340
  for (const p of PATTERNS2) {
@@ -13915,9 +14610,10 @@ var plugin42 = {
13915
14610
  var prompt_firewall_default = plugin42;
13916
14611
 
13917
14612
  // src/refactor-suggester/index.ts
13918
- import { readFile as readFile10 } from "node:fs/promises";
13919
- import { isAbsolute as isAbsolute18, relative as relative18, resolve as resolve19 } from "node:path";
14613
+ import { readFile as readFile11 } from "node:fs/promises";
14614
+ import { isAbsolute as isAbsolute19, relative as relative19, resolve as resolve20 } from "node:path";
13920
14615
  var API_VERSION27 = "^0.1.10";
14616
+ var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
13921
14617
  var state41 = {
13922
14618
  scanCount: 0,
13923
14619
  suggestionCount: 0,
@@ -13925,7 +14621,7 @@ var state41 = {
13925
14621
  warningCount: 0,
13926
14622
  errorCount: 0,
13927
14623
  hookUnregister: null,
13928
- lastHookWarning: /* @__PURE__ */ new Map()
14624
+ lastHookWarning: new BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS2 })
13929
14625
  };
13930
14626
  var DEFAULTS36 = {
13931
14627
  enabled: false,
@@ -13959,7 +14655,7 @@ function toPosix6(p) {
13959
14655
  return p.replace(/\\/g, "/");
13960
14656
  }
13961
14657
  function relativePath6(p) {
13962
- return toPosix6(relative18(process.cwd(), p));
14658
+ return toPosix6(relative19(process.cwd(), p));
13963
14659
  }
13964
14660
  function leadingIndentLevel(line) {
13965
14661
  const leading = line.match(/^(\s*)/)?.[1] ?? "";
@@ -14046,19 +14742,30 @@ function detectSmells(filePath, content, rules) {
14046
14742
  }
14047
14743
  async function scanPath4(rawPath, cfg) {
14048
14744
  const root = process.cwd();
14049
- const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
14745
+ const resolved = isAbsolute19(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
14050
14746
  const exts = normalizeExtensions5(cfg.extensions);
14051
14747
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
14052
14748
  const suggestions = [];
14749
+ let scannedFiles = 0;
14750
+ let truncated = false;
14053
14751
  for (const p of files) {
14054
14752
  try {
14055
- const content = await readFile10(p, "utf-8");
14753
+ const content = await readFile11(p, "utf-8");
14754
+ scannedFiles += 1;
14056
14755
  suggestions.push(...detectSmells(p, content, cfg.rules));
14057
- if (suggestions.length >= cfg.maxSuggestions) break;
14756
+ if (suggestions.length >= cfg.maxSuggestions) {
14757
+ truncated = scannedFiles < files.length;
14758
+ break;
14759
+ }
14058
14760
  } catch {
14059
14761
  }
14060
14762
  }
14061
- return { suggestions: suggestions.slice(0, cfg.maxSuggestions), scannedFiles: files.length };
14763
+ return {
14764
+ suggestions: suggestions.slice(0, cfg.maxSuggestions),
14765
+ scannedFiles,
14766
+ discoveredFiles: files.length,
14767
+ truncated
14768
+ };
14062
14769
  }
14063
14770
  var plugin43 = {
14064
14771
  name: "refactor-suggester",
@@ -14121,11 +14828,11 @@ var plugin43 = {
14121
14828
  state41.hookInvocationCount += 1;
14122
14829
  const now = Date.now();
14123
14830
  const lastWarning = state41.lastHookWarning.get(sourcePath);
14124
- if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
14125
- const resolved = resolve19(process.cwd(), sourcePath);
14831
+ if (lastWarning !== void 0 && now - lastWarning < HOOK_WARNING_COOLDOWN_MS2) return;
14832
+ const resolved = resolve20(process.cwd(), sourcePath);
14126
14833
  let content;
14127
14834
  try {
14128
- content = await readFile10(resolved, "utf-8");
14835
+ content = await readFile11(resolved, "utf-8");
14129
14836
  } catch {
14130
14837
  state41.errorCount += 1;
14131
14838
  return;
@@ -14169,8 +14876,12 @@ var plugin43 = {
14169
14876
  state41.suggestionCount += result.suggestions.length;
14170
14877
  return {
14171
14878
  ok: true,
14172
- path: relativePath6(resolve19(process.cwd(), rawPath)),
14879
+ path: relativePath6(resolve20(process.cwd(), rawPath)),
14173
14880
  scannedFiles: result.scannedFiles,
14881
+ discoveredFiles: result.discoveredFiles,
14882
+ // Say so when the cap stopped the walk early: a partial scan
14883
+ // that reports few findings must not read as a clean result.
14884
+ truncated: result.truncated,
14174
14885
  suggestions: result.suggestions,
14175
14886
  rules: cfg.rules
14176
14887
  };
@@ -14579,7 +15290,7 @@ var plugin44 = {
14579
15290
  var release_notes_generator_default = plugin44;
14580
15291
 
14581
15292
  // src/schema-evolution-guard/index.ts
14582
- import { readFileSync as readFileSync11 } from "node:fs";
15293
+ import { readFileSync as readFileSync12 } from "node:fs";
14583
15294
  import { basename as basename5 } from "node:path";
14584
15295
  var API_VERSION29 = "^0.1.10";
14585
15296
  var state43 = {
@@ -14782,7 +15493,7 @@ var plugin45 = {
14782
15493
  content = toolInput["content"];
14783
15494
  } else if (withinProject(filePath)) {
14784
15495
  try {
14785
- content = readFileSync11(filePath, "utf-8");
15496
+ content = readFileSync12(filePath, "utf-8");
14786
15497
  } catch {
14787
15498
  content = void 0;
14788
15499
  }
@@ -14886,71 +15597,43 @@ ${body}${suffix}`;
14886
15597
  var schema_evolution_guard_default = plugin45;
14887
15598
 
14888
15599
  // src/secret-scanner/index.ts
14889
- var BASE_PATTERNS = [
14890
- // LLM provider keys
14891
- {
14892
- type: "anthropic_key",
14893
- regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
14894
- },
14895
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
14896
- // GitHub
14897
- { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
14898
- { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
14899
- // AWS
14900
- { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
14901
- // GCP
14902
- { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
14903
- // Slack
14904
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
14905
- // Stripe
14906
- {
14907
- type: "stripe_key",
14908
- regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
14909
- },
14910
- // Twilio
14911
- { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
14912
- // Telegram
14913
- {
14914
- type: "telegram_bot_token",
14915
- regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14916
- },
14917
- // JWT
14918
- {
14919
- type: "jwt",
14920
- regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
14921
- },
14922
- // Private keys
14923
- {
14924
- type: "private_key",
14925
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----(?!\S)/g
14926
- },
14927
- // AI/ML provider tokens
14928
- { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
14929
- { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14930
- { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14931
- { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14932
- // Bearer tokens
14933
- {
14934
- type: "bearer_token",
14935
- regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
14936
- },
14937
- // Database URIs
14938
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
14939
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
14940
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
14941
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g }
14942
- ];
15600
+ var BASE_PATTERNS = cloneCredentialPatterns();
14943
15601
  var PATTERNS3 = [...BASE_PATTERNS];
14944
15602
  var patternCacheKey = "";
15603
+ var GROUP_INDEX_OF_PATTERN = [];
14945
15604
  var COMBINED_REGEX = buildCombinedRegex(PATTERNS3);
14946
- var RE_DOS_MAX_SCAN_LENGTH = 1e5;
15605
+ var SCAN_WINDOW_LENGTH = 1e5;
15606
+ var SCAN_WINDOW_OVERLAP = 4096;
15607
+ var MAX_TOTAL_SCAN_LENGTH = 8 * 1024 * 1024;
15608
+ var TOO_LARGE_MARKER = "unscannable_oversized_input";
14947
15609
  var RE_DOS_TIMEOUT_MS = 100;
15610
+ function countCaptureGroups(source) {
15611
+ try {
15612
+ return new RegExp(`${source}|`).exec("").length - 1;
15613
+ } catch {
15614
+ return 0;
15615
+ }
15616
+ }
15617
+ function patternTypeForGroups(groups) {
15618
+ for (let i = 0; i < PATTERNS3.length; i++) {
15619
+ const at = GROUP_INDEX_OF_PATTERN[i];
15620
+ if (at !== void 0 && groups[at] !== void 0) return PATTERNS3[i].type;
15621
+ }
15622
+ return void 0;
15623
+ }
14948
15624
  function buildCombinedRegex(patterns) {
14949
- const newCacheKey = JSON.stringify(patterns.map((p) => p.type));
15625
+ const newCacheKey = JSON.stringify(patterns.map((p) => [p.type, p.regex.source]));
14950
15626
  if (newCacheKey === patternCacheKey && COMBINED_REGEX) {
14951
15627
  return COMBINED_REGEX;
14952
15628
  }
14953
15629
  patternCacheKey = newCacheKey;
15630
+ const offsets = [];
15631
+ let cursor = 0;
15632
+ for (const p of patterns) {
15633
+ offsets.push(cursor);
15634
+ cursor += 1 + countCaptureGroups(p.regex.source);
15635
+ }
15636
+ GROUP_INDEX_OF_PATTERN = offsets;
14954
15637
  return new RegExp(patterns.map((p) => `(${p.regex.source})`).join("|"), "g");
14955
15638
  }
14956
15639
  var state44 = {
@@ -14968,29 +15651,37 @@ var state44 = {
14968
15651
  /** PostToolUse hook handle so teardown can unregister. */
14969
15652
  postHookUnregister: null
14970
15653
  };
14971
- function findMatches(text) {
14972
- if (!text) return [];
14973
- if (text.length > RE_DOS_MAX_SCAN_LENGTH) return [];
14974
- const found = /* @__PURE__ */ new Set();
15654
+ function scanWindow(window, found, startTime) {
14975
15655
  COMBINED_REGEX.lastIndex = 0;
14976
15656
  let m;
14977
- const startTime = performance.now();
14978
- while ((m = COMBINED_REGEX.exec(text)) !== null) {
15657
+ while ((m = COMBINED_REGEX.exec(window)) !== null) {
14979
15658
  if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
14980
15659
  throw new Error(
14981
- `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${text.length}-char input`
15660
+ `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${window.length}-char window`
14982
15661
  );
14983
15662
  }
14984
- for (let i = 0; i < PATTERNS3.length; i++) {
14985
- if (m[i + 1] !== void 0) {
14986
- found.add(PATTERNS3[i].type);
14987
- break;
14988
- }
14989
- }
15663
+ const type = patternTypeForGroups(m.slice(1));
15664
+ if (type !== void 0) found.add(type);
14990
15665
  if (m.index === COMBINED_REGEX.lastIndex) {
14991
15666
  COMBINED_REGEX.lastIndex += 1;
14992
15667
  }
14993
15668
  }
15669
+ }
15670
+ function findMatches(text) {
15671
+ if (!text) return [];
15672
+ const found = /* @__PURE__ */ new Set();
15673
+ const startTime = performance.now();
15674
+ if (text.length <= SCAN_WINDOW_LENGTH) {
15675
+ scanWindow(text, found, startTime);
15676
+ return Array.from(found).sort();
15677
+ }
15678
+ if (text.length > MAX_TOTAL_SCAN_LENGTH) {
15679
+ return [TOO_LARGE_MARKER];
15680
+ }
15681
+ const stride = SCAN_WINDOW_LENGTH - SCAN_WINDOW_OVERLAP;
15682
+ for (let offset = 0; offset < text.length; offset += stride) {
15683
+ scanWindow(text.slice(offset, offset + SCAN_WINDOW_LENGTH), found, startTime);
15684
+ }
14994
15685
  return Array.from(found).sort();
14995
15686
  }
14996
15687
  function scanInput(input, depth = 0) {
@@ -15016,30 +15707,55 @@ function scanInput(input, depth = 0) {
15016
15707
  }
15017
15708
  return null;
15018
15709
  }
15710
+ function redactString(text) {
15711
+ if (text.length > SCAN_WINDOW_LENGTH) {
15712
+ return { ok: false, reason: "oversized_input" };
15713
+ }
15714
+ const startTime = performance.now();
15715
+ let cursor = 0;
15716
+ let output = "";
15717
+ COMBINED_REGEX.lastIndex = 0;
15718
+ let match;
15719
+ while ((match = COMBINED_REGEX.exec(text)) !== null) {
15720
+ if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
15721
+ return { ok: false, reason: "oversized_input" };
15722
+ }
15723
+ const type = patternTypeForGroups(match.slice(1));
15724
+ output += text.slice(cursor, match.index);
15725
+ output += `[REDACTED:${type ?? "unknown"}]`;
15726
+ cursor = COMBINED_REGEX.lastIndex;
15727
+ if (match.index === COMBINED_REGEX.lastIndex) {
15728
+ COMBINED_REGEX.lastIndex += 1;
15729
+ }
15730
+ }
15731
+ if (cursor === 0) return { ok: true, value: text };
15732
+ return { ok: true, value: output + text.slice(cursor) };
15733
+ }
15019
15734
  function redactInput(input, depth = 0) {
15020
- if (input === null || input === void 0) return input;
15021
- if (depth > 50) return input;
15735
+ if (input === null || input === void 0) return { ok: true, value: input };
15736
+ if (depth > 50) return { ok: false, reason: "maximum_depth_exceeded" };
15022
15737
  if (typeof input === "string") {
15023
- return input.replace(COMBINED_REGEX, (_match, ...groups) => {
15024
- for (let i = 0; i < PATTERNS3.length; i++) {
15025
- if (groups[i] !== void 0) {
15026
- return `[REDACTED:${PATTERNS3[i].type}]`;
15027
- }
15028
- }
15029
- return "[REDACTED:unknown]";
15030
- });
15738
+ return redactString(input);
15031
15739
  }
15032
15740
  if (Array.isArray(input)) {
15033
- return input.map((item) => redactInput(item, depth + 1));
15741
+ const out = [];
15742
+ for (const item of input) {
15743
+ const redacted = redactInput(item, depth + 1);
15744
+ if (!redacted.ok) return redacted;
15745
+ out.push(redacted.value);
15746
+ }
15747
+ return { ok: true, value: out };
15034
15748
  }
15035
15749
  if (typeof input === "object") {
15036
15750
  const out = {};
15037
15751
  for (const [k, v] of Object.entries(input)) {
15038
- out[k] = redactInput(v, depth + 1);
15752
+ const redacted = redactInput(v, depth + 1);
15753
+ if (!redacted.ok) return redacted;
15754
+ out[k] = redacted.value;
15039
15755
  }
15040
- return out;
15756
+ return { ok: true, value: out };
15041
15757
  }
15042
- return input;
15758
+ return { ok: true, value: input };
15043
15759
  }
15044
15760
  var DEFAULTS39 = {
15045
15761
  matcher: "bash|write|edit",
@@ -15099,20 +15815,21 @@ function buildHook(cfg, log) {
15099
15815
  }
15100
15816
  if (cfg.mode === "redact") {
15101
15817
  const redacted = redactInput(input.toolInput);
15102
- if (redacted !== null && typeof redacted === "object" && !Array.isArray(redacted)) {
15818
+ if (redacted.ok && redacted.value !== null && typeof redacted.value === "object" && !Array.isArray(redacted.value)) {
15103
15819
  state44.redactCount += 1;
15104
15820
  log.info(`[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`);
15105
15821
  return {
15106
15822
  decision: "allow",
15107
- modifiedInput: redacted,
15823
+ modifiedInput: redacted.value,
15108
15824
  additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
15109
15825
  };
15110
15826
  }
15111
15827
  state44.blockCount += 1;
15112
15828
  state44.lastBlock = { toolName, matchedTypes: matched, when };
15829
+ const detail = !redacted.ok ? redacted.reason === "oversized_input" ? "an input field exceeds the safe scan limit" : "the input exceeds the safe nesting depth" : "the input has a non-object shape";
15113
15830
  return {
15114
15831
  decision: "block",
15115
- reason: `secret-scanner: cannot safely redact '${toolName}' input (non-object shape); refusing to run.`
15832
+ reason: `secret-scanner: cannot safely redact '${toolName}' because ${detail}; refusing to run.`
15116
15833
  };
15117
15834
  }
15118
15835
  state44.allowCount += 1;
@@ -15130,10 +15847,21 @@ function buildPostHook(cfg, log) {
15130
15847
  const matched = findMatches(result.content);
15131
15848
  if (matched.length === 0) return;
15132
15849
  const toolName = input.toolName ?? "unknown";
15133
- const summary = matched.join(", ");
15850
+ const oversized = matched.includes(TOO_LARGE_MARKER);
15851
+ const credentialMatches = matched.filter((type) => type !== TOO_LARGE_MARKER);
15852
+ if (oversized && credentialMatches.length === 0) {
15853
+ log.warn(
15854
+ `[secret-scanner] POST-TOOL UNSCANNABLE: ${toolName} output exceeds ${MAX_TOTAL_SCAN_LENGTH} characters`
15855
+ );
15856
+ return {
15857
+ additionalContext: `
15858
+ \u26A0\uFE0F secret-scanner: the output of '${toolName}' exceeds the safe scan limit and could not be inspected for plaintext credentials. Do not treat this output as verified clean; avoid echoing, storing, committing, or transmitting it until it can be reviewed in smaller bounded sections.`
15859
+ };
15860
+ }
15861
+ const summary = credentialMatches.join(", ");
15134
15862
  const when = (/* @__PURE__ */ new Date()).toISOString();
15135
15863
  state44.leakCount += 1;
15136
- state44.lastLeak = { toolName, matchedTypes: matched, when };
15864
+ state44.lastLeak = { toolName, matchedTypes: credentialMatches, when };
15137
15865
  log.warn(`[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`);
15138
15866
  return {
15139
15867
  additionalContext: `
@@ -15195,8 +15923,8 @@ var plugin46 = {
15195
15923
  state44.leakCount = 0;
15196
15924
  state44.lastBlock = null;
15197
15925
  state44.lastLeak = null;
15198
- state44.hookUnregister = null;
15199
- state44.postHookUnregister = null;
15926
+ state44.hookUnregister = releaseHandle(state44.hookUnregister);
15927
+ state44.postHookUnregister = releaseHandle(state44.postHookUnregister);
15200
15928
  const cfg = readConfig42(api.config.extensions?.["secret-scanner"]);
15201
15929
  PATTERNS3 = [...BASE_PATTERNS];
15202
15930
  for (const cp of cfg.customPatterns) {
@@ -15326,8 +16054,8 @@ var plugin46 = {
15326
16054
  var secret_scanner_default = plugin46;
15327
16055
 
15328
16056
  // src/security-hotspot-scanner/index.ts
15329
- import { readdir as readdir2, readFile as readFile11, stat as stat5 } from "node:fs/promises";
15330
- import { isAbsolute as isAbsolute19, relative as relative19, resolve as resolve20 } from "node:path";
16057
+ import { readdir as readdir2, readFile as readFile12, stat as stat5 } from "node:fs/promises";
16058
+ import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
15331
16059
  var API_VERSION30 = "^0.1.10";
15332
16060
  var state45 = {
15333
16061
  scanCount: 0,
@@ -15337,7 +16065,7 @@ var state45 = {
15337
16065
  skippedCount: 0,
15338
16066
  blockedCount: 0,
15339
16067
  lastResult: null,
15340
- knownHotspots: /* @__PURE__ */ new Set(),
16068
+ knownHotspots: new BoundedSet({ max: 5e3 }),
15341
16069
  hookUnregister: null
15342
16070
  };
15343
16071
  var DEFAULTS40 = {
@@ -15418,7 +16146,7 @@ function isSourceFile2(filePath, extensions) {
15418
16146
  async function scanPath5(inputPath, cfg) {
15419
16147
  const start = Date.now();
15420
16148
  const root = process.cwd();
15421
- const resolved = isAbsolute19(inputPath) ? resolve20(inputPath) : resolve20(root, inputPath);
16149
+ const resolved = isAbsolute20(inputPath) ? resolve21(inputPath) : resolve21(root, inputPath);
15422
16150
  if (!withinProject(inputPath)) {
15423
16151
  return {
15424
16152
  path: inputPath,
@@ -15435,11 +16163,11 @@ async function scanPath5(inputPath, cfg) {
15435
16163
  const scanFile2 = async (filePath) => {
15436
16164
  if (!isSourceFile2(filePath, cfg.scanOnChange)) return;
15437
16165
  try {
15438
- const content = await readFile11(filePath, "utf-8");
16166
+ const content = await readFile12(filePath, "utf-8");
15439
16167
  filesScanned += 1;
15440
16168
  const findings = scanSource(content, maxPerFile);
15441
16169
  for (const f of findings) {
15442
- allFindings.push({ ...f, snippet: `${relative19(root, filePath)}:${f.line}: ${f.snippet}` });
16170
+ allFindings.push({ ...f, snippet: `${relative20(root, filePath)}:${f.line}: ${f.snippet}` });
15443
16171
  if (allFindings.length >= cfg.maxFindings) return;
15444
16172
  }
15445
16173
  } catch {
@@ -15454,7 +16182,7 @@ async function scanPath5(inputPath, cfg) {
15454
16182
  }
15455
16183
  for (const entry of entries) {
15456
16184
  if (allFindings.length >= cfg.maxFindings) return;
15457
- const full = resolve20(dir, entry);
16185
+ const full = resolve21(dir, entry);
15458
16186
  try {
15459
16187
  const st = await stat5(full);
15460
16188
  if (st.isDirectory()) {
@@ -15723,8 +16451,10 @@ var security_hotspot_scanner_default = plugin47;
15723
16451
 
15724
16452
  // src/semantic-search-indexer/index.ts
15725
16453
  import * as fs2 from "node:fs/promises";
15726
- import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
16454
+ import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve22 } from "node:path";
16455
+ import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
15727
16456
  var API_VERSION31 = "^0.1.10";
16457
+ var escapeRegex2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15728
16458
  var state46 = {
15729
16459
  index: null,
15730
16460
  cachedPath: null,
@@ -15768,7 +16498,11 @@ var DEFAULTS41 = {
15768
16498
  ".scss",
15769
16499
  ".html"
15770
16500
  ],
15771
- excludePatterns: ["node_modules", "\\.git", "\\.wrongstack", "\\.temp_files", "coverage", "dist"],
16501
+ excludePatterns: [
16502
+ ...DEFAULT_WALK_IGNORE_DIRS.map(escapeRegex2),
16503
+ "\\.wrongstack",
16504
+ "\\.temp_files"
16505
+ ],
15772
16506
  maxFileBytes: 1e6,
15773
16507
  defaultLimit: 10,
15774
16508
  minTokenLength: 2,
@@ -15798,18 +16532,18 @@ function normalizeSlashes2(p) {
15798
16532
  function withinProject5(p) {
15799
16533
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
15800
16534
  const root = normalizeSlashes2(process.cwd());
15801
- const resolved = normalizeSlashes2(isAbsolute20(p) ? resolve21(p) : resolve21(root, p));
15802
- const rel = normalizeSlashes2(relative20(root, resolved));
16535
+ const resolved = normalizeSlashes2(isAbsolute21(p) ? resolve22(p) : resolve22(root, p));
16536
+ const rel = normalizeSlashes2(relative21(root, resolved));
15803
16537
  if (rel === "" || rel === ".") return true;
15804
16538
  if (rel.startsWith("..")) return false;
15805
- if (isAbsolute20(rel)) return false;
16539
+ if (isAbsolute21(rel)) return false;
15806
16540
  return true;
15807
16541
  }
15808
16542
  function resolveProjectPath8(p) {
15809
16543
  const raw = typeof p === "string" && p.length > 0 ? p : ".";
15810
16544
  if (!withinProject5(raw)) return null;
15811
16545
  const root = normalizeSlashes2(process.cwd());
15812
- return normalizeSlashes2(isAbsolute20(raw) ? resolve21(raw) : resolve21(root, raw));
16546
+ return normalizeSlashes2(isAbsolute21(raw) ? resolve22(raw) : resolve22(root, raw));
15813
16547
  }
15814
16548
  function tokenize(text, minLength) {
15815
16549
  const tokens = [];
@@ -15839,7 +16573,7 @@ function shouldIndexFile(filePath, cfg) {
15839
16573
  var INDEX_BATCH_SIZE = 32;
15840
16574
  var YIELD_EVERY_FILES = 64;
15841
16575
  function yieldEventLoop() {
15842
- return new Promise((resolve27) => setImmediate(resolve27));
16576
+ return new Promise((resolve28) => setImmediate(resolve28));
15843
16577
  }
15844
16578
  function addFileToIndex(relPath, content, size, cfg) {
15845
16579
  if (!state46.index || content.includes("\0")) return;
@@ -15893,8 +16627,8 @@ async function walkDirectory(absPath, cfg, excludes, fileBatch) {
15893
16627
  state46.truncated = true;
15894
16628
  return;
15895
16629
  }
15896
- const absChild = normalizeSlashes2(resolve21(absPath, ent.name));
15897
- const relChild = normalizeSlashes2(relative20(root, absChild));
16630
+ const absChild = normalizeSlashes2(resolve22(absPath, ent.name));
16631
+ const relChild = normalizeSlashes2(relative21(root, absChild));
15898
16632
  if (relChild === "" || relChild === ".") continue;
15899
16633
  if (excludes.some((re) => re.test(relChild))) continue;
15900
16634
  if (ent.isDirectory()) {
@@ -15936,7 +16670,7 @@ async function buildIndex(rootPath, cfg) {
15936
16670
  return;
15937
16671
  }
15938
16672
  if (rootStats.isFile()) {
15939
- const relPath = normalizeSlashes2(relative20(normalizeSlashes2(process.cwd()), rootPath));
16673
+ const relPath = normalizeSlashes2(relative21(normalizeSlashes2(process.cwd()), rootPath));
15940
16674
  await indexFileFromStats(rootPath, relPath === "" ? "." : relPath, rootStats, cfg);
15941
16675
  state46.fileCount = state46.index.files.size;
15942
16676
  } else if (rootStats.isDirectory()) {
@@ -16232,15 +16966,15 @@ var semantic_search_indexer_default = plugin48;
16232
16966
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
16233
16967
  import { toErrorMessage } from "@wrongstack/core/utils";
16234
16968
  import { execFile as execFile12 } from "node:child_process";
16235
- import { access as access3, readFile as readFile13, readdir as readdir4, writeFile as writeFile4 } from "node:fs/promises";
16236
- import { isAbsolute as isAbsolute21, join as join4, relative as relative21, resolve as resolve22 } from "node:path";
16969
+ import { access as access3, readFile as readFile14, readdir as readdir4, writeFile as writeFile4 } from "node:fs/promises";
16970
+ import { isAbsolute as isAbsolute22, join as join5, relative as relative22, resolve as resolve23 } from "node:path";
16237
16971
  var API_VERSION32 = "^0.1.10";
16238
16972
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
16239
16973
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
16240
- const base = resolve22(root);
16241
- const resolved = isAbsolute21(rawCwd) ? resolve22(rawCwd) : resolve22(base, rawCwd);
16242
- const rel = relative21(base, resolved);
16243
- if (rel === "" || !rel.startsWith("..") && !isAbsolute21(rel)) return resolved;
16974
+ const base = resolve23(root);
16975
+ const resolved = isAbsolute22(rawCwd) ? resolve23(rawCwd) : resolve23(base, rawCwd);
16976
+ const rel = relative22(base, resolved);
16977
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute22(rel)) return resolved;
16244
16978
  return null;
16245
16979
  }
16246
16980
  var state47 = {
@@ -16252,7 +16986,7 @@ var state47 = {
16252
16986
  lastBump: null
16253
16987
  };
16254
16988
  function runCommand3(command, args, cwd) {
16255
- return new Promise((resolve27, reject) => {
16989
+ return new Promise((resolve28, reject) => {
16256
16990
  execFile12(command, args, {
16257
16991
  encoding: "utf-8",
16258
16992
  cwd,
@@ -16260,7 +16994,7 @@ function runCommand3(command, args, cwd) {
16260
16994
  windowsHide: true
16261
16995
  }, (err, stdout, stderr) => {
16262
16996
  if (!err) {
16263
- resolve27(stdout.trim());
16997
+ resolve28(stdout.trim());
16264
16998
  return;
16265
16999
  }
16266
17000
  const failure = err;
@@ -16281,21 +17015,21 @@ async function getPackageJson(cwd) {
16281
17015
  const path = cwd ? `${cwd}/package.json` : "package.json";
16282
17016
  try {
16283
17017
  await access3(path);
16284
- return JSON.parse(await readFile13(path, "utf-8"));
17018
+ return JSON.parse(await readFile14(path, "utf-8"));
16285
17019
  } catch {
16286
17020
  return null;
16287
17021
  }
16288
17022
  }
16289
17023
  async function collectManifests(root) {
16290
17024
  const paths = [];
16291
- const rootPkg = join4(root, "package.json");
17025
+ const rootPkg = join5(root, "package.json");
16292
17026
  try {
16293
17027
  await access3(rootPkg);
16294
17028
  paths.push(rootPkg);
16295
17029
  } catch {
16296
17030
  }
16297
17031
  for (const group of ["packages", "apps"]) {
16298
- const groupDir = join4(root, group);
17032
+ const groupDir = join5(root, group);
16299
17033
  let entries;
16300
17034
  try {
16301
17035
  entries = await readdir4(groupDir, { withFileTypes: true });
@@ -16304,7 +17038,7 @@ async function collectManifests(root) {
16304
17038
  }
16305
17039
  for (const entry of entries) {
16306
17040
  if (!entry.isDirectory()) continue;
16307
- const candidate = join4(groupDir, entry.name, "package.json");
17041
+ const candidate = join5(groupDir, entry.name, "package.json");
16308
17042
  try {
16309
17043
  await access3(candidate);
16310
17044
  paths.push(candidate);
@@ -16499,7 +17233,7 @@ var plugin49 = {
16499
17233
  };
16500
17234
  }
16501
17235
  const root = cwd ?? process.cwd();
16502
- const bumpScript = join4(root, "scripts", "bump-version.mjs");
17236
+ const bumpScript = join5(root, "scripts", "bump-version.mjs");
16503
17237
  const changed = await collectManifests(root);
16504
17238
  let hasBumpScript = true;
16505
17239
  try {
@@ -16515,7 +17249,7 @@ var plugin49 = {
16515
17249
  return { ok: false, error: `bump script failed: ${msg}` };
16516
17250
  }
16517
17251
  for (const rel of ["package.json", "package-lock.json", "src/lib/utils.ts", "index.html"]) {
16518
- const p = join4(root, "website", rel);
17252
+ const p = join5(root, "website", rel);
16519
17253
  try {
16520
17254
  await access3(p);
16521
17255
  changed.push(p);
@@ -16523,10 +17257,29 @@ var plugin49 = {
16523
17257
  }
16524
17258
  }
16525
17259
  } else {
17260
+ const pending = [];
16526
17261
  for (const manifest of changed) {
16527
- const pkgData = JSON.parse(await readFile13(manifest, "utf-8"));
17262
+ let pkgData;
17263
+ try {
17264
+ pkgData = JSON.parse(await readFile14(manifest, "utf-8"));
17265
+ } catch (err) {
17266
+ return {
17267
+ ok: false,
17268
+ error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
17269
+ };
17270
+ }
17271
+ if (!pkgData || typeof pkgData !== "object") {
17272
+ return {
17273
+ ok: false,
17274
+ error: `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
17275
+ };
17276
+ }
16528
17277
  pkgData.version = newVersion;
16529
- await writeFile4(manifest, JSON.stringify(pkgData, null, 2) + "\n", "utf-8");
17278
+ pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
17279
+ ` });
17280
+ }
17281
+ for (const { path, contents } of pending) {
17282
+ await writeFile4(path, contents, "utf-8");
16530
17283
  }
16531
17284
  }
16532
17285
  try {
@@ -16864,8 +17617,8 @@ async function readTranscriptTail(transcriptPath, n) {
16864
17617
  if (!transcriptPath || n <= 0) return [];
16865
17618
  let raw;
16866
17619
  try {
16867
- const { readFile: readFile17 } = await import("node:fs/promises");
16868
- raw = await readFile17(transcriptPath, "utf-8");
17620
+ const { readFile: readFile18 } = await import("node:fs/promises");
17621
+ raw = await readFile18(transcriptPath, "utf-8");
16869
17622
  } catch {
16870
17623
  return [];
16871
17624
  }
@@ -16939,7 +17692,7 @@ var plugin50 = {
16939
17692
  state48.commitCount = 0;
16940
17693
  state48.startedAt = null;
16941
17694
  state48.lastActivityAt = null;
16942
- state48.stopHookUnregister = null;
17695
+ state48.stopHookUnregister = releaseHandle(state48.stopHookUnregister);
16943
17696
  for (const off of state48.eventUnsubscribers) {
16944
17697
  try {
16945
17698
  off();
@@ -17206,15 +17959,15 @@ var session_recap_default = plugin50;
17206
17959
  // src/shell-check/index.ts
17207
17960
  import { execFile as execFile13 } from "node:child_process";
17208
17961
  import { readdir as readdir5 } from "node:fs/promises";
17209
- import { isAbsolute as isAbsolute22, join as join5, relative as relative22, resolve as resolve23 } from "node:path";
17962
+ import { isAbsolute as isAbsolute23, join as join6, relative as relative23, resolve as resolve24 } from "node:path";
17210
17963
  var API_VERSION33 = "^0.1.10";
17211
17964
  function withinProject6(p) {
17212
17965
  const root = process.cwd();
17213
- const resolved = isAbsolute22(p) ? resolve23(p) : resolve23(root, p);
17214
- const rel = relative22(root, resolved);
17966
+ const resolved = isAbsolute23(p) ? resolve24(p) : resolve24(root, p);
17967
+ const rel = relative23(root, resolved);
17215
17968
  if (rel === "" || rel === ".") return true;
17216
17969
  if (rel.startsWith("..")) return false;
17217
- if (isAbsolute22(rel)) return false;
17970
+ if (isAbsolute23(rel)) return false;
17218
17971
  return true;
17219
17972
  }
17220
17973
  var MAX_PATH_LEN = 4096;
@@ -17307,7 +18060,7 @@ async function findShellFiles(dir, pattern) {
17307
18060
  return results;
17308
18061
  }
17309
18062
  for (const entry of entries) {
17310
- const full = join5(dir, entry.name);
18063
+ const full = join6(dir, entry.name);
17311
18064
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
17312
18065
  results.push(...await findShellFiles(full, pattern));
17313
18066
  } else if (entry.isFile() && (entry.name.endsWith(".sh") || entry.name === "Dockerfile")) {
@@ -17506,8 +18259,8 @@ var plugin51 = {
17506
18259
  var shell_check_default = plugin51;
17507
18260
 
17508
18261
  // src/smart-rename/index.ts
17509
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
17510
- import { extname as extname5, isAbsolute as isAbsolute23, relative as relative23, resolve as resolve24 } from "node:path";
18262
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
18263
+ import { extname as extname6, isAbsolute as isAbsolute24, relative as relative24, resolve as resolve25 } from "node:path";
17511
18264
  var API_VERSION34 = "^0.1.10";
17512
18265
  var state50 = {
17513
18266
  renameCount: 0,
@@ -17529,24 +18282,31 @@ function readConfig46(raw) {
17529
18282
  function withinProject7(p) {
17530
18283
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
17531
18284
  const root = process.cwd();
17532
- const resolved = isAbsolute23(p) ? resolve24(p) : resolve24(root, p);
17533
- const rel = relative23(root, resolved);
18285
+ const resolved = isAbsolute24(p) ? resolve25(p) : resolve25(root, p);
18286
+ const rel = relative24(root, resolved);
17534
18287
  if (rel === "" || rel === ".") return true;
17535
18288
  if (rel.startsWith("..")) return false;
17536
- if (isAbsolute23(rel)) return false;
18289
+ if (isAbsolute24(rel)) return false;
17537
18290
  return true;
17538
18291
  }
17539
18292
  function toPosix7(p) {
17540
18293
  return p.replace(/\\/g, "/");
17541
18294
  }
17542
18295
  function relativePath7(p) {
17543
- return toPosix7(relative23(process.cwd(), p));
18296
+ return toPosix7(relative24(process.cwd(), p));
17544
18297
  }
17545
- function escapeRegex2(s) {
18298
+ function escapeRegex3(s) {
17546
18299
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17547
18300
  }
18301
+ var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
18302
+ function isIdentifier(name) {
18303
+ return IDENTIFIER_RE.test(name);
18304
+ }
17548
18305
  function renameInContent(content, oldName, newName) {
17549
- const re = new RegExp(`\\b${escapeRegex2(oldName)}\\b`, "g");
18306
+ const re = new RegExp(
18307
+ `(?<![A-Za-z0-9_$])${escapeRegex3(oldName)}(?![A-Za-z0-9_$])`,
18308
+ "g"
18309
+ );
17550
18310
  let replacements = 0;
17551
18311
  const preview = content.replace(re, () => {
17552
18312
  replacements++;
@@ -17608,17 +18368,23 @@ var plugin52 = {
17608
18368
  if (!newName || typeof newName !== "string" || newName.length === 0) {
17609
18369
  return { ok: false, error: "newName is required" };
17610
18370
  }
18371
+ if (!isIdentifier(oldName)) {
18372
+ return { ok: false, error: `oldName "${oldName}" is not a valid identifier` };
18373
+ }
18374
+ if (!isIdentifier(newName)) {
18375
+ return { ok: false, error: `newName "${newName}" is not a valid identifier` };
18376
+ }
17611
18377
  if (!withinProject7(rawPath)) {
17612
18378
  return { ok: false, error: "path is outside the project root" };
17613
18379
  }
17614
- const ext = extname5(rawPath).toLowerCase();
18380
+ const ext = extname6(rawPath).toLowerCase();
17615
18381
  if (!cfg.extensions.includes(ext)) {
17616
18382
  return { ok: false, error: `extension ${ext} is not allowed for rename` };
17617
18383
  }
17618
- const resolved = resolve24(process.cwd(), rawPath);
18384
+ const resolved = resolve25(process.cwd(), rawPath);
17619
18385
  let content;
17620
18386
  try {
17621
- content = readFileSync12(resolved, "utf-8");
18387
+ content = readFileSync13(resolved, "utf-8");
17622
18388
  } catch (err) {
17623
18389
  state50.errorCount += 1;
17624
18390
  return { ok: false, error: String(err) };
@@ -17628,7 +18394,7 @@ var plugin52 = {
17628
18394
  state50.replacementCount += replacements;
17629
18395
  if (input.apply) {
17630
18396
  try {
17631
- writeFileSync4(resolved, preview, "utf-8");
18397
+ writeFileSync2(resolved, preview, "utf-8");
17632
18398
  } catch (err) {
17633
18399
  state50.errorCount += 1;
17634
18400
  return { ok: false, error: String(err) };
@@ -18005,8 +18771,8 @@ var plugin53 = {
18005
18771
  state51.skippedNonMd = 0;
18006
18772
  state51.readErrorCount = 0;
18007
18773
  state51.autoFixApplied = 0;
18008
- state51.postHookUnregister = null;
18009
- state51.preHookUnregister = null;
18774
+ state51.postHookUnregister = releaseHandle(state51.postHookUnregister);
18775
+ state51.preHookUnregister = releaseHandle(state51.preHookUnregister);
18010
18776
  const cfg = readConfig47(api.config.extensions?.["spec-linker"]);
18011
18777
  const postHook = async (input) => {
18012
18778
  if (!cfg.enabled) return;
@@ -18159,10 +18925,11 @@ ${lines}${overflowNote}`
18159
18925
  var spec_linker_default = plugin53;
18160
18926
 
18161
18927
  // src/template-engine/index.ts
18162
- import { readFile as readFile15, writeFile as writeFile5 } from "node:fs/promises";
18163
- import { isAbsolute as isAbsolute24 } from "node:path";
18928
+ import { readFile as readFile16, writeFile as writeFile5 } from "node:fs/promises";
18929
+ import { isAbsolute as isAbsolute25 } from "node:path";
18164
18930
  var API_VERSION35 = "^0.1.10";
18165
18931
  var templates = /* @__PURE__ */ new Map();
18932
+ var contributorUnregister = null;
18166
18933
  function expandTemplate(template, variables) {
18167
18934
  let result = template;
18168
18935
  result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
@@ -18207,9 +18974,12 @@ function renderTemplateRaw(template, variables) {
18207
18974
  return result;
18208
18975
  }
18209
18976
  function validateRelativeTemplatePath(field, value) {
18210
- if (isAbsolute24(value) || value.split(/[\\/]+/).includes("..")) {
18977
+ if (isAbsolute25(value) || value.split(/[\\/]+/).includes("..")) {
18211
18978
  return `${field} must be a relative path without ".." components`;
18212
18979
  }
18980
+ if (!withinProject(value)) {
18981
+ return `${field} must resolve inside the project directory`;
18982
+ }
18213
18983
  return null;
18214
18984
  }
18215
18985
  var plugin54 = {
@@ -18333,7 +19103,7 @@ var plugin54 = {
18333
19103
  }
18334
19104
  let content;
18335
19105
  try {
18336
- content = await readFile15(template_path, "utf-8");
19106
+ content = await readFile16(template_path, "utf-8");
18337
19107
  } catch (err) {
18338
19108
  return { ok: false, error: `Could not read template file: ${err}` };
18339
19109
  }
@@ -18432,7 +19202,8 @@ var plugin54 = {
18432
19202
  };
18433
19203
  }
18434
19204
  });
18435
- api.registerSystemPromptContributor(async () => [
19205
+ contributorUnregister = releaseHandle(contributorUnregister);
19206
+ contributorUnregister = api.registerSystemPromptContributor(async () => [
18436
19207
  {
18437
19208
  type: "text",
18438
19209
  text: `Template engine available:
@@ -18447,6 +19218,7 @@ var plugin54 = {
18447
19218
  teardown(api) {
18448
19219
  const count = templates.size;
18449
19220
  templates.clear();
19221
+ contributorUnregister = releaseHandle(contributorUnregister);
18450
19222
  api.log.info("template-engine: teardown complete", { cleared: count });
18451
19223
  },
18452
19224
  async health() {
@@ -18465,7 +19237,7 @@ var plugin54 = {
18465
19237
  var template_engine_default = plugin54;
18466
19238
 
18467
19239
  // src/test-coverage-gate/index.ts
18468
- import { readFileSync as readFileSync13 } from "node:fs";
19240
+ import { readFileSync as readFileSync14 } from "node:fs";
18469
19241
  var API_VERSION36 = "^0.1.10";
18470
19242
  var state52 = {
18471
19243
  invocationCount: 0,
@@ -18506,7 +19278,7 @@ function readConfig48(raw) {
18506
19278
  }
18507
19279
  function readCoverageSummary(coveragePath) {
18508
19280
  try {
18509
- const raw = readFileSync13(coveragePath, "utf-8");
19281
+ const raw = readFileSync14(coveragePath, "utf-8");
18510
19282
  return JSON.parse(raw);
18511
19283
  } catch {
18512
19284
  return null;
@@ -18576,7 +19348,7 @@ var plugin55 = {
18576
19348
  state52.skippedCount = 0;
18577
19349
  state52.lastOverallPct = null;
18578
19350
  state52.lastResult = null;
18579
- state52.hookUnregister = null;
19351
+ state52.hookUnregister = releaseHandle(state52.hookUnregister);
18580
19352
  const cfg = readConfig48(api.config.extensions?.["test-coverage-gate"]);
18581
19353
  const hook = (input) => {
18582
19354
  if (!cfg.enabled) return;
@@ -18722,9 +19494,9 @@ var test_coverage_gate_default = plugin55;
18722
19494
 
18723
19495
  // src/test-flake-detector/index.ts
18724
19496
  import { execFile as execFile14 } from "node:child_process";
18725
- import { readFileSync as readFileSync14 } from "node:fs";
18726
- import { createRequire as createRequire2 } from "node:module";
18727
- import { dirname as dirname7, isAbsolute as isAbsolute25, relative as relative24, resolve as resolve25 } from "node:path";
19497
+ import { readFileSync as readFileSync15 } from "node:fs";
19498
+ import { createRequire as createRequire3 } from "node:module";
19499
+ import { dirname as dirname8, isAbsolute as isAbsolute26, relative as relative25, resolve as resolve26 } from "node:path";
18728
19500
  var API_VERSION37 = "^0.1.10";
18729
19501
  var state53 = {
18730
19502
  invocationCount: 0,
@@ -18795,15 +19567,15 @@ var ALLOWED_RUNNER_FLAGS = /* @__PURE__ */ new Set([
18795
19567
  ]);
18796
19568
  function withinProject8(p) {
18797
19569
  if (p.length === 0 || p.length > 4096 || p.startsWith("-")) return false;
18798
- const root = resolve25(process.cwd());
18799
- const resolved = isAbsolute25(p) ? resolve25(p) : resolve25(root, p);
18800
- const rel = relative24(root, resolved);
18801
- return rel === "" || !rel.startsWith("..") && !isAbsolute25(rel);
19570
+ const root = resolve26(process.cwd());
19571
+ const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
19572
+ const rel = relative25(root, resolved);
19573
+ return rel === "" || !rel.startsWith("..") && !isAbsolute26(rel);
18802
19574
  }
18803
- function isInside2(parent, child) {
19575
+ function isInside3(parent, child) {
18804
19576
  if (parent === child) return true;
18805
- const rel = relative24(parent, child);
18806
- return rel !== "" && !rel.startsWith("..") && !isAbsolute25(rel);
19577
+ const rel = relative25(parent, child);
19578
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute26(rel);
18807
19579
  }
18808
19580
  function tokenizeCommand(command) {
18809
19581
  const trimmed = command.trim();
@@ -18840,14 +19612,14 @@ function resolveTestCommand(baseCommand, testPattern) {
18840
19612
  if (runnerArgs.some((arg) => !ALLOWED_RUNNER_FLAGS.has(arg))) return null;
18841
19613
  let resolvedEntry;
18842
19614
  try {
18843
- const requireFromProject = createRequire2(resolve25(process.cwd(), "package.json"));
19615
+ const requireFromProject = createRequire3(resolve26(process.cwd(), "package.json"));
18844
19616
  const packagePath = requireFromProject.resolve(`${runner}/package.json`);
18845
- const packageJson = JSON.parse(readFileSync14(packagePath, "utf8"));
19617
+ const packageJson = JSON.parse(readFileSync15(packagePath, "utf8"));
18846
19618
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[runner] ?? Object.values(packageJson.bin ?? {})[0];
18847
19619
  if (!relativeBin) return null;
18848
- const packageDir = dirname7(packagePath);
18849
- const candidate = resolve25(packageDir, relativeBin);
18850
- if (isAbsolute25(relativeBin) || !isInside2(packageDir, candidate)) {
19620
+ const packageDir = dirname8(packagePath);
19621
+ const candidate = resolve26(packageDir, relativeBin);
19622
+ if (isAbsolute26(relativeBin) || !isInside3(packageDir, candidate)) {
18851
19623
  return null;
18852
19624
  }
18853
19625
  resolvedEntry = candidate;
@@ -19089,8 +19861,8 @@ var plugin56 = {
19089
19861
  var test_flake_detector_default = plugin56;
19090
19862
 
19091
19863
  // src/test-generator/index.ts
19092
- import { readFileSync as readFileSync15 } from "node:fs";
19093
- import { isAbsolute as isAbsolute26, relative as relative25, resolve as resolve26 } from "node:path";
19864
+ import { readFileSync as readFileSync16 } from "node:fs";
19865
+ import { isAbsolute as isAbsolute27, relative as relative26, resolve as resolve27 } from "node:path";
19094
19866
  var API_VERSION38 = "^0.1.10";
19095
19867
  var state54 = {
19096
19868
  generateCount: 0,
@@ -19125,18 +19897,18 @@ function readConfig50(raw) {
19125
19897
  function withinProject9(p) {
19126
19898
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
19127
19899
  const root = process.cwd();
19128
- const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
19129
- const rel = relative25(root, resolved);
19900
+ const resolved = isAbsolute27(p) ? resolve27(p) : resolve27(root, p);
19901
+ const rel = relative26(root, resolved);
19130
19902
  if (rel === "" || rel === ".") return true;
19131
19903
  if (rel.startsWith("..")) return false;
19132
- if (isAbsolute26(rel)) return false;
19904
+ if (isAbsolute27(rel)) return false;
19133
19905
  return true;
19134
19906
  }
19135
19907
  function toPosix8(p) {
19136
19908
  return p.replace(/\\/g, "/");
19137
19909
  }
19138
19910
  function relativePath8(p) {
19139
- return toPosix8(relative25(process.cwd(), p));
19911
+ return toPosix8(relative26(process.cwd(), p));
19140
19912
  }
19141
19913
  function detectExports(content) {
19142
19914
  const exports = [];
@@ -19230,7 +20002,7 @@ function generateTestContent(sourcePath, sourceModule, detected, cfg) {
19230
20002
  return lines.join("\n");
19231
20003
  }
19232
20004
  function generateForFile(filePath, cfg) {
19233
- const sourceContent = readFileSync15(filePath, "utf-8");
20005
+ const sourceContent = readFileSync16(filePath, "utf-8");
19234
20006
  const detected = detectExports(sourceContent);
19235
20007
  const sourceFile = relativePath8(filePath);
19236
20008
  const sourceParts = sourceFile.split("/");
@@ -19349,7 +20121,7 @@ var plugin57 = {
19349
20121
  if (!withinProject9(rawPath)) {
19350
20122
  return { ok: false, error: "path is outside the project root" };
19351
20123
  }
19352
- const resolved = resolve26(process.cwd(), rawPath);
20124
+ const resolved = resolve27(process.cwd(), rawPath);
19353
20125
  state54.generateCount += 1;
19354
20126
  let result;
19355
20127
  try {
@@ -19435,7 +20207,17 @@ var test_generator_default = plugin57;
19435
20207
  // src/test-runner-gate/index.ts
19436
20208
  import { execFile as execFile15 } from "node:child_process";
19437
20209
  import { access as access4 } from "node:fs/promises";
19438
- import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute27, join as join6 } from "node:path";
20210
+ import { basename as basename6, dirname as dirname9, isAbsolute as isAbsolute28, join as join7 } from "node:path";
20211
+ import { buildWin32CmdShimInvocation as buildWin32CmdShimInvocation2, resolveWin32Command as resolveWin32Command2 } from "@wrongstack/tools/win32";
20212
+ function resolveExec(command, args) {
20213
+ const resolved = resolveWin32Command2(command);
20214
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
20215
+ if (needsShell) {
20216
+ const shim = buildWin32CmdShimInvocation2(resolved, args);
20217
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
20218
+ }
20219
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
20220
+ }
19439
20221
  var API_VERSION39 = "^0.1.10";
19440
20222
  var state55 = {
19441
20223
  invocationCount: 0,
@@ -19500,7 +20282,7 @@ function pathContentHash(content) {
19500
20282
  }
19501
20283
  return h >>> 0;
19502
20284
  }
19503
- var lastPassedHash = /* @__PURE__ */ new Map();
20285
+ var lastPassedHash = new BoundedMap({ max: 1024 });
19504
20286
  var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
19505
20287
  "npx",
19506
20288
  "pnpm",
@@ -19518,7 +20300,7 @@ var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
19518
20300
  function resolveTestFiles(sourcePath, patterns) {
19519
20301
  const name = basename6(sourcePath).replace(/\.[^.]+$/, "");
19520
20302
  const pathNoExt = sourcePath.replace(/\.[^.]+$/, "");
19521
- const dir = dirname8(sourcePath);
20303
+ const dir = dirname9(sourcePath);
19522
20304
  const candidates = [];
19523
20305
  for (const pattern of patterns) {
19524
20306
  const candidate = pattern.replace(/\{name\}/g, name).replace(/\{path\}/g, pathNoExt).replace(/\{dir\}/g, dir);
@@ -19527,7 +20309,7 @@ function resolveTestFiles(sourcePath, patterns) {
19527
20309
  candidates.push(candidate);
19528
20310
  } else {
19529
20311
  candidates.push(candidate);
19530
- candidates.push(join6(dir, candidate));
20312
+ candidates.push(join7(dir, candidate));
19531
20313
  }
19532
20314
  }
19533
20315
  }
@@ -19554,12 +20336,15 @@ async function detectRunner(requested) {
19554
20336
  const match = candidates.find((c) => c.name === requested);
19555
20337
  if (!match) return null;
19556
20338
  try {
19557
- await new Promise((resolve27, reject) => {
19558
- execFile15("npx", [`${match.name}`, "--version"], {
20339
+ await new Promise((resolve28, reject) => {
20340
+ const ex = resolveExec("npx", [`${match.name}`, "--version"]);
20341
+ execFile15(ex.cmd, ex.args, {
19559
20342
  encoding: "utf-8",
19560
20343
  timeout: 5e3,
19561
- cwd: process.cwd()
19562
- }, (err) => err ? reject(err) : resolve27());
20344
+ cwd: process.cwd(),
20345
+ windowsHide: true,
20346
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20347
+ }, (err) => err ? reject(err) : resolve28());
19563
20348
  });
19564
20349
  return match;
19565
20350
  } catch {
@@ -19568,12 +20353,15 @@ async function detectRunner(requested) {
19568
20353
  }
19569
20354
  for (const candidate of candidates) {
19570
20355
  try {
19571
- await new Promise((resolve27, reject) => {
19572
- execFile15("npx", [`${candidate.name}`, "--version"], {
20356
+ await new Promise((resolve28, reject) => {
20357
+ const ex = resolveExec("npx", [`${candidate.name}`, "--version"]);
20358
+ execFile15(ex.cmd, ex.args, {
19573
20359
  encoding: "utf-8",
19574
20360
  timeout: 5e3,
19575
- cwd: process.cwd()
19576
- }, (err) => err ? reject(err) : resolve27());
20361
+ cwd: process.cwd(),
20362
+ windowsHide: true,
20363
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20364
+ }, (err) => err ? reject(err) : resolve28());
19577
20365
  });
19578
20366
  return candidate;
19579
20367
  } catch {
@@ -19588,7 +20376,7 @@ function resolveAllowedCommand2(customCommand) {
19588
20376
  if (ALLOWED_COMMAND_TOKENS.has(head)) {
19589
20377
  return { cmd: head, args: tokens.slice(1) };
19590
20378
  }
19591
- if (isAbsolute27(head)) {
20379
+ if (isAbsolute28(head)) {
19592
20380
  if (!withinProject(head)) return null;
19593
20381
  const base = basename6(head);
19594
20382
  if (ALLOWED_COMMAND_TOKENS.has(base)) {
@@ -19619,14 +20407,21 @@ async function runTests(testFile, runner, customCommand, timeoutMs) {
19619
20407
  let stdout = "";
19620
20408
  try {
19621
20409
  const { stdout: out } = await new Promise(
19622
- (resolve27, reject) => {
20410
+ (resolve28, reject) => {
20411
+ const ex = resolveExec(cmd, fullArgs);
19623
20412
  execFile15(
19624
- cmd,
19625
- fullArgs,
19626
- { encoding: "utf-8", timeout: timeoutMs, cwd: process.cwd() },
20413
+ ex.cmd,
20414
+ ex.args,
20415
+ {
20416
+ encoding: "utf-8",
20417
+ timeout: timeoutMs,
20418
+ cwd: process.cwd(),
20419
+ windowsHide: true,
20420
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20421
+ },
19627
20422
  (err, out2, stderr) => {
19628
20423
  if (err) reject(Object.assign(err, { stdout: out2, stderr }));
19629
- else resolve27({ stdout: out2, stderr });
20424
+ else resolve28({ stdout: out2, stderr });
19630
20425
  }
19631
20426
  );
19632
20427
  }
@@ -19743,7 +20538,7 @@ var plugin58 = {
19743
20538
  state55.errorCount = 0;
19744
20539
  state55.extensionSkippedCount = 0;
19745
20540
  state55.cachedSkipCount = 0;
19746
- state55.hookUnregister = null;
20541
+ state55.hookUnregister = releaseHandle(state55.hookUnregister);
19747
20542
  state55.lastResult = null;
19748
20543
  lastPassedHash.clear();
19749
20544
  const cfg = readConfig51(api.config.extensions?.["test-runner-gate"]);
@@ -19994,7 +20789,7 @@ var plugin59 = {
19994
20789
  state56.lastMessageId = null;
19995
20790
  state56.lastPayloadHash = "";
19996
20791
  state56.lastBroadcastAt = 0;
19997
- state56.hookUnregister = null;
20792
+ state56.hookUnregister = releaseHandle(state56.hookUnregister);
19998
20793
  const cfg = readConfig52(api.config.extensions?.["todo-listener"]);
19999
20794
  const mailbox = api.mailbox;
20000
20795
  const hook = async (input) => {
@@ -20035,7 +20830,7 @@ var plugin59 = {
20035
20830
  0,
20036
20831
  200
20037
20832
  );
20038
- const body = JSON.stringify(payload, null, 2);
20833
+ const body = safeJsonStringify(payload, 2);
20039
20834
  const sendInput = {
20040
20835
  from: `plugin:todo-listener`,
20041
20836
  to: "*",
@@ -20139,6 +20934,7 @@ var todo_listener_default = plugin59;
20139
20934
  // src/todo-tracker/index.ts
20140
20935
  import * as fsp from "node:fs/promises";
20141
20936
  import { randomUUID } from "node:crypto";
20937
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core/utils";
20142
20938
  function deriveFilePath(api) {
20143
20939
  const raw = api.config.extensions?.["todo-tracker"];
20144
20940
  const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
@@ -20168,10 +20964,8 @@ async function loadFile(filePath) {
20168
20964
  }
20169
20965
  }
20170
20966
  async function saveFile(filePath, file) {
20171
- const tmp = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
20172
- await fsp.mkdir(filePath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
20173
- await fsp.writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", mode: 384 });
20174
- await fsp.rename(tmp, filePath);
20967
+ await ensureDir3(filePath.replace(/[/\\][^/\\]+$/, ""));
20968
+ await atomicWrite3(filePath, JSON.stringify(file, null, 2), { mode: 384 });
20175
20969
  }
20176
20970
  var state57 = {
20177
20971
  filePath: null,
@@ -20611,16 +21405,36 @@ function modelMatches(pattern, model) {
20611
21405
  }
20612
21406
  return model.toLowerCase() === pattern.toLowerCase();
20613
21407
  }
21408
+ function readBoundedNumber(value, min, max, fallback) {
21409
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
21410
+ if (value < min || value > max) return fallback;
21411
+ return value;
21412
+ }
20614
21413
  function readConfig53(raw) {
20615
21414
  if (!raw || typeof raw !== "object") return { ...DEFAULTS50 };
20616
21415
  const r = raw;
21416
+ const warnPercent = readBoundedNumber(r["warnPercent"], 1, 100, DEFAULTS50.warnPercent);
21417
+ const stopPercent = readBoundedNumber(r["stopPercent"], 1, 100, DEFAULTS50.stopPercent);
20617
21418
  return {
20618
- limit: typeof r["limit"] === "number" ? r["limit"] : DEFAULTS50.limit,
20619
- warnPercent: typeof r["warnPercent"] === "number" ? r["warnPercent"] : DEFAULTS50.warnPercent,
20620
- stopPercent: typeof r["stopPercent"] === "number" ? r["stopPercent"] : DEFAULTS50.stopPercent,
21419
+ limit: readBoundedNumber(r["limit"], 0, Number.MAX_SAFE_INTEGER, DEFAULTS50.limit),
21420
+ warnPercent,
21421
+ // A warn threshold above the stop threshold can never fire — the run
21422
+ // stops first. Clamp so the pair always describes a reachable window.
21423
+ stopPercent: Math.max(stopPercent, warnPercent),
20621
21424
  model: typeof r["model"] === "string" ? r["model"] : ""
20622
21425
  };
20623
21426
  }
21427
+ function clearRegistrations2() {
21428
+ for (const key of ["hookUnregister", "postHookUnregister"]) {
21429
+ const off = state58[key];
21430
+ if (!off) continue;
21431
+ try {
21432
+ off();
21433
+ } catch {
21434
+ }
21435
+ state58[key] = null;
21436
+ }
21437
+ }
20624
21438
  var plugin61 = {
20625
21439
  name: "token-budget",
20626
21440
  version: "0.1.0",
@@ -20667,9 +21481,8 @@ var plugin61 = {
20667
21481
  state58.stopFired = false;
20668
21482
  state58.warnContextInjected = false;
20669
21483
  state58.stopContextInjected = false;
20670
- state58.hookUnregister = null;
20671
- state58.postHookUnregister = null;
20672
21484
  state58.lastRequest = null;
21485
+ clearRegistrations2();
20673
21486
  const cfg = readConfig53(api.config.extensions?.["token-budget"]);
20674
21487
  api.onEvent("provider.response", (payload) => {
20675
21488
  const p = payload;
@@ -20766,6 +21579,12 @@ var plugin61 = {
20766
21579
  remaining,
20767
21580
  percent,
20768
21581
  requestCount: state58.requestCount,
21582
+ // The EFFECTIVE thresholds, after out-of-range values fall back to
21583
+ // the defaults and warn/stop are ordered. Without these the user
21584
+ // cannot tell that a rejected or clamped setting is not in force.
21585
+ warnPercent: cfg.warnPercent,
21586
+ stopPercent: cfg.stopPercent,
21587
+ model: cfg.model === "" ? null : cfg.model,
20769
21588
  breakdown: {
20770
21589
  prompt: state58.totalPromptTokens,
20771
21590
  completion: state58.totalCompletionTokens
@@ -20784,20 +21603,7 @@ var plugin61 = {
20784
21603
  });
20785
21604
  },
20786
21605
  teardown(api) {
20787
- if (state58.hookUnregister) {
20788
- try {
20789
- state58.hookUnregister();
20790
- } catch {
20791
- }
20792
- state58.hookUnregister = null;
20793
- }
20794
- if (state58.postHookUnregister) {
20795
- try {
20796
- state58.postHookUnregister();
20797
- } catch {
20798
- }
20799
- state58.postHookUnregister = null;
20800
- }
21606
+ clearRegistrations2();
20801
21607
  const final = {
20802
21608
  totalTokens: state58.totalTokens,
20803
21609
  requestCount: state58.requestCount,
@@ -20890,7 +21696,21 @@ function estimateRequestTokens(request, charsPerToken) {
20890
21696
  walk(request["messages"]);
20891
21697
  return Math.ceil(chars / charsPerToken);
20892
21698
  }
20893
- var sleep = (ms) => new Promise((resolve27) => setTimeout(resolve27, ms));
21699
+ function sleep(ms, signal) {
21700
+ if (signal?.aborted) return Promise.resolve();
21701
+ return new Promise((resolve28) => {
21702
+ const timer = setTimeout(() => {
21703
+ signal?.removeEventListener("abort", onAbort);
21704
+ resolve28();
21705
+ }, ms);
21706
+ timer.unref?.();
21707
+ function onAbort() {
21708
+ clearTimeout(timer);
21709
+ resolve28();
21710
+ }
21711
+ signal?.addEventListener("abort", onAbort, { once: true });
21712
+ });
21713
+ }
20894
21714
  var plugin62 = {
20895
21715
  name: "token-throttle",
20896
21716
  version: "0.1.0",
@@ -20949,6 +21769,7 @@ var plugin62 = {
20949
21769
  name: "token-throttle",
20950
21770
  owner: "token-throttle",
20951
21771
  async wrapProviderRunner(_ctx, request, inner) {
21772
+ const signal = _ctx?.signal;
20952
21773
  const req = request ?? {};
20953
21774
  state59.invocations += 1;
20954
21775
  const now = Date.now();
@@ -20962,7 +21783,7 @@ var plugin62 = {
20962
21783
  api.metrics.counter("throttled");
20963
21784
  api.metrics.histogram("delay_ms", delay);
20964
21785
  api.log.info("token-throttle: delaying provider call", { delayMs: delay, projected });
20965
- await sleep(delay);
21786
+ await sleep(delay, signal);
20966
21787
  }
20967
21788
  const response = await inner(_ctx, request);
20968
21789
  const used = (response?.usage?.input ?? 0) + (response?.usage?.output ?? 0) || projected;
@@ -21110,7 +21931,9 @@ async function runTypeCheck(cfg) {
21110
21931
  if (!resolved) return null;
21111
21932
  argv = [resolved.cmd, ...resolved.args];
21112
21933
  } else {
21113
- argv = ["npx", "tsc", "--noEmit", "--incremental"];
21934
+ const tscFlags = ["--noEmit", "--incremental"];
21935
+ const localTsc = resolveNodeBin("typescript", "tsc", process.cwd(), tscFlags);
21936
+ argv = localTsc ? [localTsc.cmd, ...localTsc.args] : ["npx", "tsc", ...tscFlags];
21114
21937
  if (tsConfig && tsConfig !== "tsconfig.json") {
21115
21938
  argv = [...argv, "-p", tsConfig];
21116
21939
  }
@@ -21206,7 +22029,7 @@ var plugin63 = {
21206
22029
  state60.errorCount = 0;
21207
22030
  state60.skippedCount = 0;
21208
22031
  state60.lastResult = null;
21209
- state60.hookUnregister = null;
22032
+ state60.hookUnregister = releaseHandle(state60.hookUnregister);
21210
22033
  const cfg = readConfig55(api.config.extensions?.["type-gate"]);
21211
22034
  const hook = async (input) => {
21212
22035
  if (!cfg.enabled) return;