@wrongstack/plugins 0.295.0 → 0.295.1

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 (90) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +19 -2
  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 +103 -1
  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 +17 -7
  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.js +5 -0
  25. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  26. package/dist/dependency-vulnerability-gate.js +43 -8
  27. package/dist/diff-summary/index.d.ts.map +1 -1
  28. package/dist/diff-summary.js +100 -1
  29. package/dist/doc-sync-guard.js +5 -0
  30. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  31. package/dist/duplicate-code-detector.js +102 -2
  32. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  33. package/dist/feature-flag-tracker.js +30 -4
  34. package/dist/file-watcher/index.d.ts.map +1 -1
  35. package/dist/file-watcher.js +6 -7
  36. package/dist/format-on-save/index.d.ts +2 -0
  37. package/dist/format-on-save/index.d.ts.map +1 -1
  38. package/dist/format-on-save.js +288 -59
  39. package/dist/import-organizer/index.d.ts.map +1 -1
  40. package/dist/import-organizer.js +140 -20
  41. package/dist/index.js +1265 -544
  42. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  43. package/dist/interface-contract-guard.js +60 -26
  44. package/dist/knowledge-graph/index.d.ts.map +1 -1
  45. package/dist/knowledge-graph.js +7 -6
  46. package/dist/lint-gate/index.d.ts.map +1 -1
  47. package/dist/lint-gate.js +103 -1
  48. package/dist/loop-breaker/index.d.ts.map +1 -1
  49. package/dist/loop-breaker.js +15 -5
  50. package/dist/migration-planner.js +3 -0
  51. package/dist/notify-hub/index.d.ts.map +1 -1
  52. package/dist/notify-hub.js +41 -4
  53. package/dist/prompt-firewall/index.d.ts.map +1 -1
  54. package/dist/prompt-firewall.js +140 -11
  55. package/dist/refactor-suggester/index.d.ts.map +1 -1
  56. package/dist/refactor-suggester.js +119 -4
  57. package/dist/runtime/bounded-map.d.ts +86 -0
  58. package/dist/runtime/bounded-map.d.ts.map +1 -0
  59. package/dist/runtime/credential-patterns.d.ts +42 -0
  60. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  61. package/dist/runtime/index.d.ts +3 -0
  62. package/dist/runtime/index.d.ts.map +1 -1
  63. package/dist/runtime/local-bin.d.ts +120 -0
  64. package/dist/runtime/local-bin.d.ts.map +1 -0
  65. package/dist/runtime/safe-json.d.ts +25 -0
  66. package/dist/runtime/safe-json.d.ts.map +1 -0
  67. package/dist/runtime.js +291 -22
  68. package/dist/schema-evolution-guard.js +5 -0
  69. package/dist/secret-scanner/index.d.ts.map +1 -1
  70. package/dist/secret-scanner.js +175 -48
  71. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  72. package/dist/security-hotspot-scanner.js +132 -1
  73. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  74. package/dist/semantic-search-indexer.js +7 -1
  75. package/dist/semver-bump/index.d.ts.map +1 -1
  76. package/dist/semver-bump.js +21 -2
  77. package/dist/template-engine/index.d.ts.map +1 -1
  78. package/dist/template-engine.js +30 -2
  79. package/dist/test-coverage-gate.js +5 -0
  80. package/dist/test-runner-gate/index.d.ts.map +1 -1
  81. package/dist/test-runner-gate.js +130 -8
  82. package/dist/todo-listener/index.d.ts.map +1 -1
  83. package/dist/todo-listener.js +30 -1
  84. package/dist/todo-tracker/index.d.ts.map +1 -1
  85. package/dist/todo-tracker.js +3 -4
  86. package/dist/token-throttle/index.d.ts.map +1 -1
  87. package/dist/token-throttle.js +17 -2
  88. package/dist/type-gate/index.d.ts.map +1 -1
  89. package/dist/type-gate.js +96 -16
  90. 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,249 @@ 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
+
55
298
  // src/runtime/index.ts
56
299
  var META_CHARS = /["'`;&|<>\r\n]/;
57
300
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
@@ -66,9 +309,9 @@ function safeSplit(command) {
66
309
  function withinProjectPath(projectRoot, candidate) {
67
310
  if (candidate.length === 0 || candidate.length > 4096) return false;
68
311
  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);
312
+ const resolved = isAbsolute2(candidate) ? resolve2(candidate) : resolve2(projectRoot, candidate);
313
+ const rel = relative2(projectRoot, resolved);
314
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
72
315
  }
73
316
  function everyFlagAllowed(allowed, args) {
74
317
  for (const arg of args) {
@@ -80,14 +323,14 @@ function everyFlagAllowed(allowed, args) {
80
323
  }
81
324
  function sanitizeRunnerPath(value, options = {}) {
82
325
  if (!value || hasLeadingDash(value)) return null;
83
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
326
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
84
327
  if (!withinProjectPath(projectRoot, value)) return null;
85
- return isAbsolute(value) ? resolve(value) : resolve(projectRoot, value);
328
+ return isAbsolute2(value) ? resolve2(value) : resolve2(projectRoot, value);
86
329
  }
87
330
  function resolveRunnerCommand(runtime, command, options = {}) {
88
331
  const tokens = safeSplit(command);
89
332
  if (!tokens || tokens.length === 0) return null;
90
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
333
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
91
334
  const launcher = runtime.packageManager;
92
335
  const [head, second, ...rest] = tokens;
93
336
  if (!head) return null;
@@ -118,7 +361,7 @@ function resolveRunnerCommand(runtime, command, options = {}) {
118
361
  return { cmd: head, args: [second, exe, ...tail], display };
119
362
  }
120
363
  }
121
- if (isAbsolute(head)) {
364
+ if (isAbsolute2(head)) {
122
365
  if (!withinProjectPath(projectRoot, head)) return null;
123
366
  const base = basename(head);
124
367
  if (base !== runtime.executable && base !== launcher) return null;
@@ -139,7 +382,7 @@ function runRunnerCommand(argv, options) {
139
382
  });
140
383
  }
141
384
  return new Promise((resolvePromise) => {
142
- const projectRoot = resolve(options.projectRoot ?? process.cwd());
385
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
143
386
  const trimmedCwd = options.cwd.trim();
144
387
  if (!withinProjectPath(projectRoot, trimmedCwd)) {
145
388
  resolvePromise({
@@ -157,21 +400,37 @@ function runRunnerCommand(argv, options) {
157
400
  const stderrChunks = [];
158
401
  let stdoutBytes = 0;
159
402
  let stderrBytes = 0;
160
- options.signal?.addEventListener("abort", () => {
403
+ const onAbort = () => {
161
404
  timedOut = true;
162
- }, { once: true });
405
+ };
406
+ let invocation;
407
+ try {
408
+ invocation = resolveExecInvocation(argv[0], argv.slice(1));
409
+ } catch (err) {
410
+ resolvePromise({
411
+ code: null,
412
+ stdout: "",
413
+ stderr: `runtime helper: ${err instanceof Error ? err.message : String(err)}`,
414
+ timedOut: false,
415
+ spawnError: true
416
+ });
417
+ return;
418
+ }
419
+ options.signal?.addEventListener("abort", onAbort, { once: true });
163
420
  const child = execFile(
164
- argv[0],
165
- argv.slice(1),
421
+ invocation.cmd,
422
+ invocation.args,
166
423
  {
167
424
  cwd: trimmedCwd,
168
425
  timeout: options.timeoutMs,
169
426
  signal: options.signal,
170
427
  maxBuffer: MAX_BUFFER_BYTES,
171
428
  windowsHide: true,
172
- shell: false
429
+ shell: false,
430
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
173
431
  },
174
432
  (err) => {
433
+ options.signal?.removeEventListener("abort", onAbort);
175
434
  if (timedOut) {
176
435
  resolvePromise({
177
436
  code: null,
@@ -235,7 +494,7 @@ function runRunnerCommand(argv, options) {
235
494
  }
236
495
  function withinProject(p) {
237
496
  const cwd = process.cwd();
238
- return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
497
+ return withinProjectPath(cwd, p) || relative2(cwd, p) === ".";
239
498
  }
240
499
  var DEFAULT_EXCLUDE_DIRS = ["node_modules", "dist", ".git", "coverage"];
241
500
  async function collectSourceFilesAsync(root, opts) {
@@ -264,7 +523,7 @@ async function collectSourceFilesAsync(root, opts) {
264
523
  entries.sort((a, b) => a.name.localeCompare(b.name));
265
524
  for (const entry of entries) {
266
525
  if (excludeSet.has(entry.name)) continue;
267
- const full = resolve(dir, entry.name);
526
+ const full = resolve2(dir, entry.name);
268
527
  if (entry.isDirectory()) {
269
528
  await walk(full, depth + 1);
270
529
  } else if (entry.isFile() && matchesExtension(full, opts.extensions)) {
@@ -276,7 +535,7 @@ async function collectSourceFilesAsync(root, opts) {
276
535
  return files;
277
536
  }
278
537
  function matchesExtension(p, exts) {
279
- return exts.includes(extname(p).toLowerCase());
538
+ return exts.includes(extname2(p).toLowerCase());
280
539
  }
281
540
 
282
541
  // src/accessibility-auditor/index.ts
@@ -339,7 +598,7 @@ async function auditFile(filePath, projectRoot) {
339
598
  const idsByValue = /* @__PURE__ */ new Map();
340
599
  function add(line, rule, severity, message) {
341
600
  findings.push({
342
- file: relative2(projectRoot, filePath),
601
+ file: relative3(projectRoot, filePath),
343
602
  line,
344
603
  rule,
345
604
  severity,
@@ -418,19 +677,27 @@ async function auditFile(filePath, projectRoot) {
418
677
  }
419
678
  async function auditPath(rawPath, cfg) {
420
679
  const root = process.cwd();
421
- const resolved = isAbsolute2(rawPath) ? resolve2(rawPath) : resolve2(root, rawPath);
680
+ const resolved = isAbsolute3(rawPath) ? resolve3(rawPath) : resolve3(root, rawPath);
422
681
  const exts = normalizeExtensions(cfg.includeExtensions);
423
682
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
424
683
  const findings = [];
684
+ let scannedFiles = 0;
685
+ let truncated = false;
425
686
  for (const file of files) {
426
687
  const fileFindings = await auditFile(file, root);
688
+ scannedFiles += 1;
427
689
  findings.push(...fileFindings);
428
- if (findings.length >= cfg.maxFindings) break;
690
+ if (findings.length >= cfg.maxFindings) {
691
+ truncated = scannedFiles < files.length;
692
+ break;
693
+ }
429
694
  }
430
695
  return {
431
- path: relative2(root, resolved),
696
+ path: relative3(root, resolved),
432
697
  findings: findings.slice(0, cfg.maxFindings),
433
- fileCount: files.length
698
+ fileCount: files.length,
699
+ scannedFiles,
700
+ truncated
434
701
  };
435
702
  }
436
703
  function formatSummary(result) {
@@ -559,6 +826,10 @@ var plugin = {
559
826
  ok: true,
560
827
  path: result.path,
561
828
  fileCount: result.fileCount,
829
+ scannedFiles: result.scannedFiles,
830
+ // Say so when the cap stopped the walk early: a partial scan
831
+ // that reports few findings must not read as a clean result.
832
+ truncated: result.truncated,
562
833
  findingCount: result.findings.length,
563
834
  findings: result.findings
564
835
  };
@@ -689,7 +960,7 @@ function buildBody(payload, cfg) {
689
960
  if (cfg.includeResult && payload.result !== void 0) {
690
961
  lines.push("## Result");
691
962
  lines.push("```json");
692
- lines.push(JSON.stringify(payload.result, null, 2));
963
+ lines.push(safeJsonStringify(payload.result, 2));
693
964
  lines.push("```");
694
965
  lines.push("");
695
966
  }
@@ -931,9 +1202,9 @@ var agent_handoff_default = plugin2;
931
1202
 
932
1203
  // src/api-compatibility-gate/index.ts
933
1204
  import { execFile as execFile2 } from "node:child_process";
934
- import { existsSync, readFileSync } from "node:fs";
1205
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
935
1206
  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";
1207
+ import { basename as basename2, dirname as dirname2, extname as extname3, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
937
1208
  var API_VERSION3 = "^0.1.10";
938
1209
  var state3 = {
939
1210
  invocationCount: 0,
@@ -997,25 +1268,25 @@ function matchesAnyPattern(filePath, patterns) {
997
1268
  return patterns.some((p) => patternToRegex(p).test(normalized));
998
1269
  }
999
1270
  function resolveToProjectRoot(filePath) {
1000
- if (isAbsolute3(filePath)) return resolve3(filePath);
1001
- return resolve3(process.cwd(), filePath);
1271
+ if (isAbsolute4(filePath)) return resolve4(filePath);
1272
+ return resolve4(process.cwd(), filePath);
1002
1273
  }
1003
1274
  function withinProject2(filePath) {
1004
1275
  if (typeof filePath !== "string" || filePath.length === 0 || filePath.length > 4096) return false;
1005
1276
  const resolved = resolveToProjectRoot(filePath);
1006
- const rel = relative3(process.cwd(), resolved);
1277
+ const rel = relative4(process.cwd(), resolved);
1007
1278
  if (rel === "" || rel === ".") return true;
1008
1279
  if (rel.startsWith("..")) return false;
1009
- if (isAbsolute3(rel)) return false;
1280
+ if (isAbsolute4(rel)) return false;
1010
1281
  return true;
1011
1282
  }
1012
1283
  function isPackageEntryPoint(filePath) {
1013
1284
  const resolved = resolveToProjectRoot(filePath);
1014
- const dir = dirname(resolved);
1015
- const pkgPath = resolve3(dir, "package.json");
1285
+ const dir = dirname2(resolved);
1286
+ const pkgPath = resolve4(dir, "package.json");
1016
1287
  if (!existsSync(pkgPath)) return false;
1017
1288
  try {
1018
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1289
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1019
1290
  const entries = [];
1020
1291
  if (pkg.main) entries.push(pkg.main);
1021
1292
  if (pkg.module) entries.push(pkg.module);
@@ -1033,9 +1304,9 @@ function isPackageEntryPoint(filePath) {
1033
1304
  }
1034
1305
  }
1035
1306
  for (const entry of entries) {
1036
- const entryResolved = resolve3(dir, entry);
1307
+ const entryResolved = resolve4(dir, entry);
1037
1308
  if (entryResolved === resolved) return true;
1038
- if (basename2(dirname(entryResolved)) === basename2(dirname(resolved)) && basename2(entryResolved, extname2(entryResolved)) === basename2(resolved, extname2(resolved))) {
1309
+ if (basename2(dirname2(entryResolved)) === basename2(dirname2(resolved)) && basename2(entryResolved, extname3(entryResolved)) === basename2(resolved, extname3(resolved))) {
1039
1310
  return true;
1040
1311
  }
1041
1312
  }
@@ -1086,7 +1357,7 @@ function extractExports(source) {
1086
1357
  }
1087
1358
  function readGitVersion(filePath) {
1088
1359
  const resolved = resolveToProjectRoot(filePath);
1089
- const relPath = normalizePath(relative3(process.cwd(), resolved));
1360
+ const relPath = normalizePath(relative4(process.cwd(), resolved));
1090
1361
  return new Promise((resolveVersion) => {
1091
1362
  execFile2(
1092
1363
  "git",
@@ -1290,14 +1561,14 @@ var plugin3 = {
1290
1561
  var api_compatibility_gate_default = plugin3;
1291
1562
 
1292
1563
  // src/auto-doc/index.ts
1293
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
1564
+ import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "node:path";
1294
1565
  var AUTO_DOC_API_VERSION = "^0.1.10";
1295
1566
  function resolveProjectPath(rawPath, cwd = process.cwd()) {
1296
1567
  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;
1568
+ const root = resolve5(cwd);
1569
+ const resolved = isAbsolute5(rawPath) ? resolve5(rawPath) : resolve5(root, rawPath);
1570
+ const rel = relative5(root, resolved);
1571
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute5(rel)) return resolved;
1301
1572
  return null;
1302
1573
  }
1303
1574
  var state4 = {
@@ -1477,16 +1748,17 @@ async function runAutoDoc(input, api) {
1477
1748
  continue;
1478
1749
  }
1479
1750
  try {
1480
- const { readFileSync: readFileSync16, writeFileSync: writeFileSync5 } = await import("node:fs");
1751
+ const { readFileSync: readFileSync17, writeFileSync: writeFileSync3 } = await import("node:fs");
1481
1752
  let content;
1482
1753
  try {
1483
- content = readFileSync16(safeFile, "utf-8");
1754
+ content = readFileSync17(safeFile, "utf-8");
1484
1755
  } catch {
1485
1756
  api.log.warn(`auto-doc: could not read file ${safeFile}`);
1486
1757
  continue;
1487
1758
  }
1488
1759
  const entities = parseSource(content);
1489
1760
  let modified = content;
1761
+ const beforeCount = results.length;
1490
1762
  for (const entity of entities) {
1491
1763
  if (!input.force && !needsDocComment(modified, entity)) continue;
1492
1764
  let doc = null;
@@ -1512,8 +1784,9 @@ async function runAutoDoc(input, api) {
1512
1784
  modified = injectDocComment(modified, entity, doc);
1513
1785
  results.push({ file: safeFile, entity: entity.name, source });
1514
1786
  }
1515
- if (!input.dry_run && results.length > 0) {
1516
- writeFileSync5(safeFile, modified, "utf-8");
1787
+ const changedThisFile = results.length > beforeCount && modified !== content;
1788
+ if (!input.dry_run && changedThisFile) {
1789
+ writeFileSync3(safeFile, modified, "utf-8");
1517
1790
  api.log.info(`auto-doc: updated ${safeFile}`);
1518
1791
  }
1519
1792
  } catch (err) {
@@ -1828,7 +2101,7 @@ var plugin5 = {
1828
2101
  var auto_escalate_default = plugin5;
1829
2102
 
1830
2103
  // src/auto-i18n-extractor/index.ts
1831
- import { readFileSync as readFileSync2 } from "node:fs";
2104
+ import { readFileSync as readFileSync3 } from "node:fs";
1832
2105
  var API_VERSION4 = "^0.1.10";
1833
2106
  var state6 = {
1834
2107
  filesScanned: 0,
@@ -1913,7 +2186,7 @@ function extractStrings(content, cfg) {
1913
2186
  }
1914
2187
  function readSourceFile(filePath) {
1915
2188
  try {
1916
- return readFileSync2(filePath, "utf-8");
2189
+ return readFileSync3(filePath, "utf-8");
1917
2190
  } catch {
1918
2191
  return null;
1919
2192
  }
@@ -2201,16 +2474,16 @@ function hasDisabledPluginEntry(raw) {
2201
2474
  return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
2202
2475
  });
2203
2476
  }
2204
- var branchCache = /* @__PURE__ */ new Map();
2477
+ var branchCache = new BoundedMap({ max: 64, ttlMs: 3e4 });
2205
2478
  function runGit(args, cwd, signal) {
2206
- return new Promise((resolve27, reject) => {
2479
+ return new Promise((resolve28, reject) => {
2207
2480
  execFile3(
2208
2481
  "git",
2209
2482
  args,
2210
2483
  { encoding: "utf-8", timeout: 3e3, cwd, windowsHide: true, signal },
2211
2484
  (error, stdout) => {
2212
2485
  if (error) reject(error);
2213
- else resolve27(stdout);
2486
+ else resolve28(stdout);
2214
2487
  }
2215
2488
  );
2216
2489
  });
@@ -2458,11 +2731,11 @@ var plugin7 = {
2458
2731
  var branch_guard_default = plugin7;
2459
2732
 
2460
2733
  // 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";
2734
+ import { readFileSync as readFileSync4, writeFileSync } from "node:fs";
2735
+ import { isAbsolute as isAbsolute6, relative as relative6, resolve as resolve6 } from "node:path";
2463
2736
  var state8 = {
2464
2737
  entries: [],
2465
- filesTouched: /* @__PURE__ */ new Set(),
2738
+ filesTouched: new BoundedSet({ max: 2e3 }),
2466
2739
  commitsSeen: 0,
2467
2740
  writes: 0,
2468
2741
  polishes: 0,
@@ -2476,10 +2749,10 @@ var DEFAULTS6 = {
2476
2749
  maxEntries: 200
2477
2750
  };
2478
2751
  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;
2752
+ const root = resolve6(cwd);
2753
+ const resolved = isAbsolute6(rawPath) ? resolve6(rawPath) : resolve6(root, rawPath);
2754
+ const rel = relative6(root, resolved);
2755
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute6(rel)) return resolved;
2483
2756
  return null;
2484
2757
  }
2485
2758
  function readConfig7(raw) {
@@ -2623,7 +2896,7 @@ var plugin8 = {
2623
2896
  },
2624
2897
  setup(api) {
2625
2898
  state8.entries = [];
2626
- state8.filesTouched = /* @__PURE__ */ new Set();
2899
+ state8.filesTouched = new BoundedSet({ max: 2e3 });
2627
2900
  state8.commitsSeen = 0;
2628
2901
  state8.writes = 0;
2629
2902
  for (const off of state8.eventUnsubscribers) {
@@ -2769,7 +3042,7 @@ var plugin8 = {
2769
3042
  );
2770
3043
  let existing = null;
2771
3044
  try {
2772
- existing = readFileSync3(cfg.filePath, "utf-8");
3045
+ existing = readFileSync4(cfg.filePath, "utf-8");
2773
3046
  } catch {
2774
3047
  existing = null;
2775
3048
  }
@@ -2815,7 +3088,7 @@ var plugin8 = {
2815
3088
  filesTouched: state8.filesTouched.size
2816
3089
  };
2817
3090
  state8.entries = [];
2818
- state8.filesTouched = /* @__PURE__ */ new Set();
3091
+ state8.filesTouched = new BoundedSet({ max: 2e3 });
2819
3092
  state8.commitsSeen = 0;
2820
3093
  state8.writes = 0;
2821
3094
  api.log.info("changelog-writer: teardown complete", { final });
@@ -2837,20 +3110,22 @@ var changelog_writer_default = plugin8;
2837
3110
 
2838
3111
  // src/checkpoint/index.ts
2839
3112
  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";
3113
+ import { dirname as dirname3, isAbsolute as isAbsolute7, relative as relative7, resolve as resolve7 } from "node:path";
2841
3114
  var state9 = {
2842
3115
  snapshots: [],
2843
3116
  nextId: 1,
2844
3117
  captures: 0,
2845
3118
  restores: 0,
2846
3119
  skippedLarge: 0,
3120
+ evictedForBytes: 0,
2847
3121
  hookUnregister: null
2848
3122
  };
2849
3123
  var DEFAULTS7 = {
2850
3124
  enabled: true,
2851
3125
  autoCapture: true,
2852
3126
  maxSnapshots: 50,
2853
- maxFileBytes: 1048576
3127
+ maxFileBytes: 1048576,
3128
+ maxTotalBytes: 64 * 1048576
2854
3129
  };
2855
3130
  function readConfig8(raw) {
2856
3131
  if (!raw || typeof raw !== "object") return { ...DEFAULTS7 };
@@ -2859,14 +3134,15 @@ function readConfig8(raw) {
2859
3134
  enabled: r["enabled"] !== false,
2860
3135
  autoCapture: r["autoCapture"] !== false,
2861
3136
  maxSnapshots: typeof r["maxSnapshots"] === "number" && r["maxSnapshots"] >= 1 && r["maxSnapshots"] <= 500 ? r["maxSnapshots"] : DEFAULTS7.maxSnapshots,
3137
+ maxTotalBytes: typeof r["maxTotalBytes"] === "number" && r["maxTotalBytes"] >= 1024 ? r["maxTotalBytes"] : DEFAULTS7.maxTotalBytes,
2862
3138
  maxFileBytes: typeof r["maxFileBytes"] === "number" && r["maxFileBytes"] >= 1024 ? r["maxFileBytes"] : DEFAULTS7.maxFileBytes
2863
3139
  };
2864
3140
  }
2865
3141
  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;
3142
+ const root = resolve7(cwd);
3143
+ const resolved = isAbsolute7(rawPath) ? resolve7(rawPath) : resolve7(root, rawPath);
3144
+ const rel = relative7(root, resolved);
3145
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute7(rel)) return resolved;
2870
3146
  return null;
2871
3147
  }
2872
3148
  function hashContent(s) {
@@ -2900,11 +3176,22 @@ async function captureFileForHook(path, maxBytes, signal) {
2900
3176
  return { path, content: null, bytes: 0 };
2901
3177
  }
2902
3178
  }
2903
- function pushSnapshot(snapshot, maxSnapshots) {
3179
+ function retainedSnapshotBytes() {
3180
+ let total = 0;
3181
+ for (const snap of state9.snapshots) {
3182
+ for (const f of snap.files) total += f.content === null ? 0 : f.content.length;
3183
+ }
3184
+ return total;
3185
+ }
3186
+ function pushSnapshot(snapshot, maxSnapshots, maxTotalBytes) {
2904
3187
  state9.snapshots.push(snapshot);
2905
3188
  if (state9.snapshots.length > maxSnapshots) {
2906
3189
  state9.snapshots.splice(0, state9.snapshots.length - maxSnapshots);
2907
3190
  }
3191
+ while (state9.snapshots.length > 1 && retainedSnapshotBytes() > maxTotalBytes) {
3192
+ state9.snapshots.shift();
3193
+ state9.evictedForBytes += 1;
3194
+ }
2908
3195
  }
2909
3196
  var plugin9 = {
2910
3197
  name: "checkpoint",
@@ -2943,6 +3230,7 @@ var plugin9 = {
2943
3230
  state9.captures = 0;
2944
3231
  state9.restores = 0;
2945
3232
  state9.skippedLarge = 0;
3233
+ state9.evictedForBytes = 0;
2946
3234
  if (state9.hookUnregister) {
2947
3235
  try {
2948
3236
  state9.hookUnregister();
@@ -2970,7 +3258,8 @@ var plugin9 = {
2970
3258
  origin: `auto:${input.toolName ?? "unknown"}`,
2971
3259
  files: [captured]
2972
3260
  },
2973
- cfg.maxSnapshots
3261
+ cfg.maxSnapshots,
3262
+ cfg.maxTotalBytes
2974
3263
  );
2975
3264
  state9.captures += 1;
2976
3265
  api.metrics.counter("captures");
@@ -3048,7 +3337,7 @@ var plugin9 = {
3048
3337
  origin: input.label?.trim() ? `manual:${input.label.trim()}` : "manual",
3049
3338
  files
3050
3339
  };
3051
- pushSnapshot(snapshot, cfg.maxSnapshots);
3340
+ pushSnapshot(snapshot, cfg.maxSnapshots, cfg.maxTotalBytes);
3052
3341
  state9.captures += 1;
3053
3342
  api.metrics.counter("captures");
3054
3343
  return {
@@ -3091,7 +3380,9 @@ var plugin9 = {
3091
3380
  counters: {
3092
3381
  captures: state9.captures,
3093
3382
  restores: state9.restores,
3094
- skippedLarge: state9.skippedLarge
3383
+ skippedLarge: state9.skippedLarge,
3384
+ retainedBytes: retainedSnapshotBytes(),
3385
+ evictedForBytes: state9.evictedForBytes
3095
3386
  }
3096
3387
  };
3097
3388
  }
@@ -3134,7 +3425,7 @@ var plugin9 = {
3134
3425
  continue;
3135
3426
  }
3136
3427
  try {
3137
- await mkdir(dirname2(f.path), { recursive: true });
3428
+ await mkdir(dirname3(f.path), { recursive: true });
3138
3429
  await writeFile(f.path, f.content);
3139
3430
  restored.push(f.path);
3140
3431
  } catch (err) {
@@ -3173,13 +3464,16 @@ var plugin9 = {
3173
3464
  captures: state9.captures,
3174
3465
  restores: state9.restores,
3175
3466
  skippedLarge: state9.skippedLarge,
3176
- snapshotsHeld: state9.snapshots.length
3467
+ snapshotsHeld: state9.snapshots.length,
3468
+ retainedBytes: retainedSnapshotBytes(),
3469
+ evictedForBytes: state9.evictedForBytes
3177
3470
  };
3178
3471
  state9.snapshots = [];
3179
3472
  state9.nextId = 1;
3180
3473
  state9.captures = 0;
3181
3474
  state9.restores = 0;
3182
3475
  state9.skippedLarge = 0;
3476
+ state9.evictedForBytes = 0;
3183
3477
  api.log.info("checkpoint: teardown complete", { final });
3184
3478
  },
3185
3479
  async health() {
@@ -3190,7 +3484,11 @@ var plugin9 = {
3190
3484
  snapshotsHeld: state9.snapshots.length,
3191
3485
  captures: state9.captures,
3192
3486
  restores: state9.restores,
3193
- skippedLarge: state9.skippedLarge
3487
+ skippedLarge: state9.skippedLarge,
3488
+ // Retained snapshot bytes: the count-based ring alone does not
3489
+ // bound memory, so surface the number the byte budget acts on.
3490
+ retainedBytes: retainedSnapshotBytes(),
3491
+ evictedForBytes: state9.evictedForBytes
3194
3492
  }
3195
3493
  };
3196
3494
  }
@@ -3199,7 +3497,7 @@ var checkpoint_default = plugin9;
3199
3497
 
3200
3498
  // src/code-metrics/index.ts
3201
3499
  import { readFile as readFile4 } from "node:fs/promises";
3202
- import { isAbsolute as isAbsolute7, relative as relative7, resolve as resolve7 } from "node:path";
3500
+ import { isAbsolute as isAbsolute8, relative as relative8, resolve as resolve8 } from "node:path";
3203
3501
  var API_VERSION6 = "^0.1.10";
3204
3502
  var state10 = {
3205
3503
  measureCount: 0,
@@ -3229,7 +3527,7 @@ function toPosix(p) {
3229
3527
  return p.replace(/\\/g, "/");
3230
3528
  }
3231
3529
  function relativePath(p) {
3232
- return toPosix(relative7(process.cwd(), p));
3530
+ return toPosix(relative8(process.cwd(), p));
3233
3531
  }
3234
3532
  function countFunctions(content) {
3235
3533
  let count = 0;
@@ -3306,7 +3604,7 @@ function analyzeFile(filePath, content) {
3306
3604
  }
3307
3605
  async function measurePath(rawPath, cfg) {
3308
3606
  const root = process.cwd();
3309
- const resolved = isAbsolute7(rawPath) ? resolve7(rawPath) : resolve7(root, rawPath);
3607
+ const resolved = isAbsolute8(rawPath) ? resolve8(rawPath) : resolve8(root, rawPath);
3310
3608
  const exts = normalizeExtensions2(cfg.extensions);
3311
3609
  const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
3312
3610
  const files = allFiles.slice(0, cfg.maxFiles);
@@ -3369,7 +3667,7 @@ var plugin10 = {
3369
3667
  const exts = normalizeExtensions2(cfg.extensions);
3370
3668
  if (!matchesExtension(sourcePath, exts)) return;
3371
3669
  state10.hookInvocationCount += 1;
3372
- const resolved = resolve7(process.cwd(), sourcePath);
3670
+ const resolved = resolve8(process.cwd(), sourcePath);
3373
3671
  let content;
3374
3672
  try {
3375
3673
  content = await readFile4(resolved, "utf-8");
@@ -3413,7 +3711,7 @@ var plugin10 = {
3413
3711
  state10.fileCount += result.files.length;
3414
3712
  return {
3415
3713
  ok: true,
3416
- path: relativePath(resolve7(process.cwd(), rawPath)),
3714
+ path: relativePath(resolve8(process.cwd(), rawPath)),
3417
3715
  files: result.files,
3418
3716
  totalFiles: result.totalFiles,
3419
3717
  capped: result.totalFiles > cfg.maxFiles
@@ -3601,14 +3899,24 @@ function parseCommitMessage(message, cfg) {
3601
3899
  errors
3602
3900
  };
3603
3901
  }
3902
+ var GIT_MESSAGE_FLAG_RE = new RegExp(
3903
+ [
3904
+ // -m "…" | -m '…' | -m=… ; --message "…" | --message='…' | --message=…
3905
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)"([^"]*)"`,
3906
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)'([^']*)'`,
3907
+ // Bare value: stops at whitespace or a shell separator.
3908
+ String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)([^\s;&|"']+)`
3909
+ ].join("|"),
3910
+ "g"
3911
+ );
3604
3912
  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");
3913
+ const parts = [];
3914
+ for (const m of command.matchAll(GIT_MESSAGE_FLAG_RE)) {
3915
+ const value = m[1] ?? m[2] ?? m[3];
3916
+ if (value !== void 0) parts.push(value);
3917
+ }
3918
+ if (parts.length === 0) return null;
3919
+ return parts.join("\n");
3612
3920
  }
3613
3921
  var plugin11 = {
3614
3922
  name: "commit-validator",
@@ -3859,7 +4167,7 @@ Suggested rewrite (${suggest.model}):
3859
4167
  var commit_validator_default = plugin11;
3860
4168
 
3861
4169
  // src/config-validator/index.ts
3862
- import { readFileSync as readFileSync4, statSync } from "node:fs";
4170
+ import { readFileSync as readFileSync5, statSync } from "node:fs";
3863
4171
  var state12 = {
3864
4172
  invocations: 0,
3865
4173
  filesChecked: 0,
@@ -4121,7 +4429,7 @@ var plugin12 = {
4121
4429
  let text;
4122
4430
  try {
4123
4431
  if (statSync(raw).size > cfg.maxFileBytes) return;
4124
- text = readFileSync4(raw, "utf-8");
4432
+ text = readFileSync5(raw, "utf-8");
4125
4433
  } catch {
4126
4434
  return;
4127
4435
  }
@@ -4205,7 +4513,8 @@ var config_validator_default = plugin12;
4205
4513
 
4206
4514
  // src/context-pins/index.ts
4207
4515
  import * as fs from "node:fs";
4208
- import { dirname as dirname3, isAbsolute as isAbsolute8, relative as relative8, resolve as resolve8 } from "node:path";
4516
+ import { dirname as dirname4, isAbsolute as isAbsolute9, relative as relative9, resolve as resolve9 } from "node:path";
4517
+ import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
4209
4518
  var state13 = {
4210
4519
  pins: [],
4211
4520
  nextId: 1,
@@ -4222,10 +4531,10 @@ var DEFAULTS11 = {
4222
4531
  };
4223
4532
  function resolveProjectPath4(rawPath, cwd = process.cwd()) {
4224
4533
  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;
4534
+ const root = resolve9(cwd);
4535
+ const resolved = isAbsolute9(rawPath) ? resolve9(rawPath) : resolve9(root, rawPath);
4536
+ const rel = relative9(root, resolved);
4537
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute9(rel)) return resolved;
4229
4538
  return null;
4230
4539
  }
4231
4540
  function readConfig12(raw) {
@@ -4252,11 +4561,11 @@ function loadPins(filePath) {
4252
4561
  return { pins: [], nextId: 1 };
4253
4562
  }
4254
4563
  }
4255
- function persistPins(filePath) {
4564
+ async function persistPins(filePath) {
4256
4565
  if (!filePath) return true;
4257
4566
  try {
4258
- fs.mkdirSync(dirname3(filePath), { recursive: true });
4259
- fs.writeFileSync(filePath, JSON.stringify({ pins: state13.pins, nextId: state13.nextId }, null, 2));
4567
+ await ensureDir(dirname4(filePath));
4568
+ await atomicWrite(filePath, JSON.stringify({ pins: state13.pins, nextId: state13.nextId }, null, 2));
4260
4569
  return true;
4261
4570
  } catch {
4262
4571
  state13.persistErrors += 1;
@@ -4354,7 +4663,7 @@ var plugin13 = {
4354
4663
  state13.pins.push(pin);
4355
4664
  state13.adds += 1;
4356
4665
  api.metrics.counter("adds");
4357
- const persisted = persistPins(cfg.filePath);
4666
+ const persisted = await persistPins(cfg.filePath);
4358
4667
  return { ok: true, pin, persisted, totalPins: state13.pins.length };
4359
4668
  }
4360
4669
  });
@@ -4380,7 +4689,7 @@ var plugin13 = {
4380
4689
  if (removed === 0) return { ok: false, error: `no pin matches "${key}"` };
4381
4690
  state13.removals += removed;
4382
4691
  api.metrics.counter("removals", removed);
4383
- const persisted = persistPins(cfg.filePath);
4692
+ const persisted = await persistPins(cfg.filePath);
4384
4693
  return { ok: true, removed, persisted, totalPins: state13.pins.length };
4385
4694
  }
4386
4695
  });
@@ -4480,7 +4789,7 @@ function readCostTrackerConfig(raw) {
4480
4789
  mailboxDigestTo: typeof digestTo === "string" && digestTo.length > 0 ? digestTo : "cost-tracker"
4481
4790
  };
4482
4791
  }
4483
- var modelKeyCache = /* @__PURE__ */ new Map();
4792
+ var modelKeyCache = new BoundedMap({ max: 256 });
4484
4793
  function estimateCost(model, promptTokens, completionTokens) {
4485
4794
  let key = modelKeyCache.get(model);
4486
4795
  if (!key) {
@@ -4904,6 +5213,7 @@ var plugin15 = {
4904
5213
  api.metrics.histogram("cron_job_interval_ms", job.intervalMs, { job: name });
4905
5214
  scheduleNextRun(name);
4906
5215
  }, delay);
5216
+ timer.unref?.();
4907
5217
  state14.timers.set(name, timer);
4908
5218
  }
4909
5219
  function cancelJob(name) {
@@ -5089,7 +5399,7 @@ var cron_default = plugin15;
5089
5399
 
5090
5400
  // src/dead-code-detector/index.ts
5091
5401
  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";
5402
+ import { extname as extname4, join as join2, relative as relative10, resolve as resolve10 } from "node:path";
5093
5403
  var API_VERSION10 = "^0.1.10";
5094
5404
  var state15 = {
5095
5405
  scanCount: 0,
@@ -5132,17 +5442,17 @@ async function gatherFiles(root, depth, cfg) {
5132
5442
  for (const entry of entries) {
5133
5443
  if (entry.isDirectory()) {
5134
5444
  if (remaining > 0 && !excludeSet.has(entry.name)) {
5135
- await walk(join(dir, entry.name), remaining - 1);
5445
+ await walk(join2(dir, entry.name), remaining - 1);
5136
5446
  }
5137
5447
  } else if (entry.isFile()) {
5138
- const ext = extname3(entry.name).toLowerCase();
5448
+ const ext = extname4(entry.name).toLowerCase();
5139
5449
  if (cfg.extensions.includes(ext)) {
5140
- files.push(join(dir, entry.name));
5450
+ files.push(join2(dir, entry.name));
5141
5451
  }
5142
5452
  }
5143
5453
  }
5144
5454
  }
5145
- await walk(resolve9(root), depth);
5455
+ await walk(resolve10(root), depth);
5146
5456
  return files;
5147
5457
  }
5148
5458
  function stripNoise(content) {
@@ -5231,11 +5541,11 @@ async function scan(root, depth, cfg) {
5231
5541
  return { findings: findUnusedSymbols(files), scannedFiles: files.length };
5232
5542
  }
5233
5543
  async function resolveScanRoot(rawPath) {
5234
- const resolved = resolve9(process.cwd(), rawPath);
5544
+ const resolved = resolve10(process.cwd(), rawPath);
5235
5545
  try {
5236
5546
  const stats = await stat2(resolved);
5237
5547
  if (!stats.isDirectory()) {
5238
- return resolve9(resolved, "..");
5548
+ return resolve10(resolved, "..");
5239
5549
  }
5240
5550
  } catch {
5241
5551
  }
@@ -5245,7 +5555,7 @@ function toPosix2(p) {
5245
5555
  return p.replace(/\\/g, "/");
5246
5556
  }
5247
5557
  function relativePath2(p) {
5248
- return toPosix2(relative9(process.cwd(), p));
5558
+ return toPosix2(relative10(process.cwd(), p));
5249
5559
  }
5250
5560
  var plugin16 = {
5251
5561
  name: "dead-code-detector",
@@ -5316,8 +5626,8 @@ var plugin16 = {
5316
5626
  state15.errorCount += 1;
5317
5627
  return;
5318
5628
  }
5319
- const changedFile = resolve9(process.cwd(), sourcePath);
5320
- const relevant = result.findings.filter((f) => resolve9(f.file) === changedFile);
5629
+ const changedFile = resolve10(process.cwd(), sourcePath);
5630
+ const relevant = result.findings.filter((f) => resolve10(f.file) === changedFile);
5321
5631
  if (relevant.length === 0) {
5322
5632
  state15.missCount += 1;
5323
5633
  return;
@@ -5910,34 +6220,51 @@ function isInstallCommand(input) {
5910
6220
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
5911
6221
  return INSTALL_RE2.test(command);
5912
6222
  }
6223
+ var LOCKFILE_MANAGERS = [
6224
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
6225
+ { file: "bun.lockb", manager: "bun" },
6226
+ { file: "bun.lock", manager: "bun" },
6227
+ { file: "yarn.lock", manager: "yarn" },
6228
+ { file: "package-lock.json", manager: "npm" }
6229
+ ];
5913
6230
  async function detectManager() {
5914
- try {
5915
- await access("pnpm-lock.yaml");
5916
- return "pnpm";
5917
- } catch {
5918
- return "npm";
6231
+ for (const { file, manager } of LOCKFILE_MANAGERS) {
6232
+ try {
6233
+ await access(file);
6234
+ return manager;
6235
+ } catch {
6236
+ }
5919
6237
  }
6238
+ return "npm";
5920
6239
  }
5921
6240
  async function runAudit(cfg) {
5922
6241
  const manager = await detectManager();
5923
6242
  const start = Date.now();
5924
- const stdout = await new Promise((resolve27) => {
6243
+ let invocation;
6244
+ try {
6245
+ invocation = resolveExecInvocation(manager, ["audit", "--json"]);
6246
+ } catch {
6247
+ return null;
6248
+ }
6249
+ const stdout = await new Promise((resolve28) => {
5925
6250
  execFile4(
5926
- manager,
5927
- ["audit", "--json"],
6251
+ invocation.cmd,
6252
+ invocation.args,
5928
6253
  {
5929
6254
  encoding: "utf8",
5930
6255
  timeout: cfg.timeoutMs,
5931
6256
  cwd: process.cwd(),
5932
6257
  windowsHide: true,
5933
- maxBuffer: 10 * 1024 * 1024
6258
+ maxBuffer: 10 * 1024 * 1024,
6259
+ shell: false,
6260
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
5934
6261
  },
5935
6262
  (error, output) => {
5936
6263
  if (error && error.killed) {
5937
- resolve27(null);
6264
+ resolve28(null);
5938
6265
  return;
5939
6266
  }
5940
- resolve27(output);
6267
+ resolve28(output);
5941
6268
  }
5942
6269
  );
5943
6270
  });
@@ -6160,7 +6487,7 @@ function contentHash(s) {
6160
6487
  }
6161
6488
  return h >>> 0;
6162
6489
  }
6163
- var pathMemo = /* @__PURE__ */ new Map();
6490
+ var pathMemo = new BoundedMap({ max: 512 });
6164
6491
  function runGit2(args, cwd) {
6165
6492
  return new Promise((resolveCommand) => {
6166
6493
  execFile5(
@@ -6422,7 +6749,7 @@ var plugin19 = {
6422
6749
  var diff_summary_default = plugin19;
6423
6750
 
6424
6751
  // src/doc-sync-guard/index.ts
6425
- import { basename as basename3, extname as extname4 } from "node:path";
6752
+ import { basename as basename3, extname as extname5 } from "node:path";
6426
6753
  var API_VERSION13 = "^0.1.10";
6427
6754
  var state19 = {
6428
6755
  changedFiles: [],
@@ -6451,7 +6778,7 @@ function normalizeSlashes(p) {
6451
6778
  return p.replace(/\\/g, "/");
6452
6779
  }
6453
6780
  function isSourceFile(p, extensions) {
6454
- const ext = extname4(p).toLowerCase();
6781
+ const ext = extname5(p).toLowerCase();
6455
6782
  return extensions.includes(ext);
6456
6783
  }
6457
6784
  function isPublicSource(p, extensions) {
@@ -6634,8 +6961,9 @@ var doc_sync_guard_default = plugin20;
6634
6961
 
6635
6962
  // src/duplicate-code-detector/index.ts
6636
6963
  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";
6964
+ import { isAbsolute as isAbsolute10, relative as relative11, resolve as resolve11, sep as sep2 } from "node:path";
6638
6965
  var API_VERSION14 = "^0.1.10";
6966
+ var HOOK_WARNING_COOLDOWN_MS = 6e4;
6639
6967
  var state20 = {
6640
6968
  scanCount: 0,
6641
6969
  findingCount: 0,
@@ -6643,7 +6971,7 @@ var state20 = {
6643
6971
  warningCount: 0,
6644
6972
  errorCount: 0,
6645
6973
  hookUnregister: null,
6646
- lastHookWarning: /* @__PURE__ */ new Map(),
6974
+ lastHookWarning: new BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS }),
6647
6975
  fileIndex: /* @__PURE__ */ new Map(),
6648
6976
  inFlightFingerprintReads: /* @__PURE__ */ new Map(),
6649
6977
  indexFingerprintCount: 0,
@@ -6690,14 +7018,14 @@ function readConfig18(raw) {
6690
7018
  };
6691
7019
  }
6692
7020
  function isWithinRoot(projectRoot, candidate) {
6693
- const rel = relative10(projectRoot, candidate);
6694
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
7021
+ const rel = relative11(projectRoot, candidate);
7022
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
6695
7023
  }
6696
7024
  function toPosix3(p) {
6697
7025
  return p.replace(/\\/g, "/");
6698
7026
  }
6699
7027
  function relativePath3(p) {
6700
- return toPosix3(relative10(process.cwd(), p));
7028
+ return toPosix3(relative11(process.cwd(), p));
6701
7029
  }
6702
7030
  function removeInlineComments(line) {
6703
7031
  return line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//, "");
@@ -6789,7 +7117,7 @@ function findDuplicates(files, minLines, maxFindings) {
6789
7117
  }
6790
7118
  async function scanPath(rawPath, cfg) {
6791
7119
  const root = process.cwd();
6792
- const resolved = isAbsolute9(rawPath) ? resolve10(rawPath) : resolve10(root, rawPath);
7120
+ const resolved = isAbsolute10(rawPath) ? resolve11(rawPath) : resolve11(root, rawPath);
6793
7121
  const filePaths = await collectSourceFilesAsync(resolved, {
6794
7122
  extensions: cfg.extensions,
6795
7123
  excludeDirs: cfg.excludeDirs
@@ -6917,8 +7245,8 @@ var plugin21 = {
6917
7245
  const inp = input.toolInput ?? {};
6918
7246
  const sourcePath = inp["path"];
6919
7247
  if (!sourcePath || typeof sourcePath !== "string") return;
6920
- const projectRoot = resolve10(process.cwd());
6921
- const resolvedFile = isAbsolute9(sourcePath) ? resolve10(sourcePath) : resolve10(projectRoot, sourcePath);
7248
+ const projectRoot = resolve11(process.cwd());
7249
+ const resolvedFile = isAbsolute10(sourcePath) ? resolve11(sourcePath) : resolve11(projectRoot, sourcePath);
6922
7250
  if (!isWithinRoot(projectRoot, resolvedFile)) return;
6923
7251
  let changedFile;
6924
7252
  try {
@@ -6933,7 +7261,7 @@ var plugin21 = {
6933
7261
  state20.hookInvocationCount += 1;
6934
7262
  const now = Date.now();
6935
7263
  const lastWarning = state20.lastHookWarning.get(changedFile);
6936
- if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
7264
+ if (lastWarning !== void 0 && now - lastWarning < HOOK_WARNING_COOLDOWN_MS) return;
6937
7265
  const changedFps = await readCachedFingerprints(changedFile, cfg.minLines);
6938
7266
  if (changedFps === null) {
6939
7267
  state20.errorCount += 1;
@@ -6952,7 +7280,7 @@ var plugin21 = {
6952
7280
  }
6953
7281
  const matched = /* @__PURE__ */ new Set();
6954
7282
  for (const p of otherFilePaths) {
6955
- if (resolve10(p) === resolve10(changedFile)) continue;
7283
+ if (resolve11(p) === resolve11(changedFile)) continue;
6956
7284
  const otherFps = await readCachedFingerprints(p, cfg.minLines);
6957
7285
  if (otherFps === null || otherFps.size === 0) continue;
6958
7286
  for (const fp of changedFps) {
@@ -6998,7 +7326,7 @@ var plugin21 = {
6998
7326
  state20.findingCount += result.findings.length;
6999
7327
  return {
7000
7328
  ok: true,
7001
- path: relativePath3(resolve10(process.cwd(), rawPath)),
7329
+ path: relativePath3(resolve11(process.cwd(), rawPath)),
7002
7330
  scannedFiles: result.scannedFiles,
7003
7331
  minLines: cfg.minLines,
7004
7332
  findings: result.findings
@@ -7373,7 +7701,7 @@ var error_lens_default = plugin22;
7373
7701
 
7374
7702
  // src/feature-flag-tracker/index.ts
7375
7703
  import { readFile as readFile7 } from "node:fs/promises";
7376
- import { isAbsolute as isAbsolute10, relative as relative11, resolve as resolve11 } from "node:path";
7704
+ import { isAbsolute as isAbsolute11, relative as relative12, resolve as resolve12 } from "node:path";
7377
7705
  var API_VERSION15 = "^0.1.10";
7378
7706
  var state22 = {
7379
7707
  scanCount: 0,
@@ -7412,11 +7740,17 @@ function toPosix4(p) {
7412
7740
  return p.replace(/\\/g, "/");
7413
7741
  }
7414
7742
  function relativePath4(p) {
7415
- return toPosix4(relative11(process.cwd(), p));
7743
+ return toPosix4(relative12(process.cwd(), p));
7416
7744
  }
7417
7745
  function compilePatterns(patterns) {
7418
- const sources = [...DEFAULT_PATTERNS2, ...patterns];
7419
- return sources.map((src) => new RegExp(src, "g"));
7746
+ const out = [];
7747
+ for (const src of [...DEFAULT_PATTERNS2, ...patterns]) {
7748
+ try {
7749
+ out.push(new RegExp(src, "g"));
7750
+ } catch {
7751
+ }
7752
+ }
7753
+ return out;
7420
7754
  }
7421
7755
  function scanFile(filePath, content, patterns, maxFindings) {
7422
7756
  const usages = [];
@@ -7442,20 +7776,31 @@ function scanFile(filePath, content, patterns, maxFindings) {
7442
7776
  }
7443
7777
  async function scanPath2(rawPath, cfg) {
7444
7778
  const root = process.cwd();
7445
- const resolved = isAbsolute10(rawPath) ? resolve11(rawPath) : resolve11(root, rawPath);
7779
+ const resolved = isAbsolute11(rawPath) ? resolve12(rawPath) : resolve12(root, rawPath);
7446
7780
  const exts = normalizeExtensions3(cfg.extensions);
7447
7781
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
7448
7782
  const patterns = compilePatterns(cfg.patterns);
7449
7783
  const usages = [];
7784
+ let scannedFiles = 0;
7785
+ let truncated = false;
7450
7786
  for (const p of files) {
7451
7787
  try {
7452
7788
  const content = await readFile7(p, "utf-8");
7789
+ scannedFiles += 1;
7453
7790
  usages.push(...scanFile(p, content, patterns, cfg.maxFindings));
7454
- if (usages.length >= cfg.maxFindings) break;
7791
+ if (usages.length >= cfg.maxFindings) {
7792
+ truncated = scannedFiles < files.length;
7793
+ break;
7794
+ }
7455
7795
  } catch {
7456
7796
  }
7457
7797
  }
7458
- return { usages: usages.slice(0, cfg.maxFindings), scannedFiles: files.length };
7798
+ return {
7799
+ usages: usages.slice(0, cfg.maxFindings),
7800
+ scannedFiles,
7801
+ discoveredFiles: files.length,
7802
+ truncated
7803
+ };
7459
7804
  }
7460
7805
  var plugin23 = {
7461
7806
  name: "feature-flag-tracker",
@@ -7513,7 +7858,7 @@ var plugin23 = {
7513
7858
  const exts = normalizeExtensions3(cfg.extensions);
7514
7859
  if (!matchesExtension(sourcePath, exts)) return;
7515
7860
  state22.hookInvocationCount += 1;
7516
- const resolved = resolve11(process.cwd(), sourcePath);
7861
+ const resolved = resolve12(process.cwd(), sourcePath);
7517
7862
  let content;
7518
7863
  try {
7519
7864
  content = await readFile7(resolved, "utf-8");
@@ -7562,8 +7907,12 @@ Make sure flag behavior is intentional and consider updating flag inventory/docs
7562
7907
  state22.flagCount += result.usages.length;
7563
7908
  return {
7564
7909
  ok: true,
7565
- path: relativePath4(resolve11(process.cwd(), rawPath)),
7910
+ path: relativePath4(resolve12(process.cwd(), rawPath)),
7566
7911
  scannedFiles: result.scannedFiles,
7912
+ discoveredFiles: result.discoveredFiles,
7913
+ // Say so when the cap stopped the walk early: a partial scan
7914
+ // that reports few findings must not read as a clean result.
7915
+ truncated: result.truncated,
7567
7916
  usages: result.usages
7568
7917
  };
7569
7918
  }
@@ -7638,16 +7987,16 @@ var feature_flag_tracker_default = plugin23;
7638
7987
 
7639
7988
  // src/file-watcher/index.ts
7640
7989
  import { watch as fsWatch } from "node:fs";
7641
- import { isAbsolute as isAbsolute11, join as join2, relative as relative12, resolve as resolve12 } from "node:path";
7990
+ import { isAbsolute as isAbsolute12, join as join3, relative as relative13, resolve as resolve13 } from "node:path";
7642
7991
  var API_VERSION16 = "^0.1.10";
7643
7992
  function withinProject3(p) {
7644
7993
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
7645
7994
  const root = process.cwd();
7646
- const resolved = isAbsolute11(p) ? resolve12(p) : resolve12(root, p);
7647
- const rel = relative12(root, resolved);
7995
+ const resolved = isAbsolute12(p) ? resolve13(p) : resolve13(root, p);
7996
+ const rel = relative13(root, resolved);
7648
7997
  if (rel === "" || rel === ".") return true;
7649
7998
  if (rel.startsWith("..")) return false;
7650
- if (isAbsolute11(rel)) return false;
7999
+ if (isAbsolute12(rel)) return false;
7651
8000
  return true;
7652
8001
  }
7653
8002
  var watch_idCounter = 0;
@@ -7718,13 +8067,12 @@ var plugin24 = {
7718
8067
  function debounceEvent(key, fn, ms) {
7719
8068
  const existing = debounceTimers.get(key);
7720
8069
  if (existing) clearTimeout(existing);
7721
- debounceTimers.set(
7722
- key,
7723
- setTimeout(() => {
7724
- debounceTimers.delete(key);
7725
- fn();
7726
- }, ms)
7727
- );
8070
+ const timer = setTimeout(() => {
8071
+ debounceTimers.delete(key);
8072
+ fn();
8073
+ }, ms);
8074
+ timer.unref?.();
8075
+ debounceTimers.set(key, timer);
7728
8076
  }
7729
8077
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
7730
8078
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
@@ -7750,7 +8098,7 @@ var plugin24 = {
7750
8098
  if (handle.events.length > 0 && !(eventType === "change" ? handle.events.includes("change") : handle.events.includes("add") || handle.events.includes("delete"))) {
7751
8099
  return;
7752
8100
  }
7753
- const rawPath = filename.startsWith(dirPath) ? filename : join2(dirPath, filename);
8101
+ const rawPath = filename.startsWith(dirPath) ? filename : join3(dirPath, filename);
7754
8102
  const fullPath = rawPath.replace(/\\/g, "/");
7755
8103
  const key = `${handle.id}:${fullPath}:${eventType}`;
7756
8104
  debounceEvent(
@@ -7970,7 +8318,8 @@ var file_watcher_default = plugin24;
7970
8318
 
7971
8319
  // src/format-on-save/index.ts
7972
8320
  import { execFile as execFile6 } from "node:child_process";
7973
- import { access as access2, stat as stat4 } from "node:fs/promises";
8321
+ import { createHash } from "node:crypto";
8322
+ import { access as access2, readFile as readFile8, stat as stat4 } from "node:fs/promises";
7974
8323
  var API_VERSION17 = "^0.1.10";
7975
8324
  var state23 = {
7976
8325
  invocationCount: 0,
@@ -8006,7 +8355,7 @@ function readConfig21(raw) {
8006
8355
  skipTtlMs: typeof r["skipTtlMs"] === "number" && r["skipTtlMs"] >= 0 ? r["skipTtlMs"] : DEFAULTS20.skipTtlMs
8007
8356
  };
8008
8357
  }
8009
- var recentlyCovered = /* @__PURE__ */ new Map();
8358
+ var recentlyCovered = new BoundedMap({ max: 256 });
8010
8359
  function clearRegistrations() {
8011
8360
  if (state23.hookUnregister) {
8012
8361
  try {
@@ -8029,6 +8378,55 @@ function evictExpired(ttlMs) {
8029
8378
  if (ts < cutoff) recentlyCovered.delete(path);
8030
8379
  }
8031
8380
  }
8381
+ var FORMATTERS = [
8382
+ {
8383
+ packageName: "@biomejs/biome",
8384
+ binName: "biome",
8385
+ writeArgs: ["format", "--write"],
8386
+ label: "biome"
8387
+ },
8388
+ { packageName: "prettier", binName: "prettier", writeArgs: ["--write"], label: "prettier" },
8389
+ { packageName: "dprint", binName: "dprint", writeArgs: ["fmt"], label: "dprint" }
8390
+ ];
8391
+ var activeFormatter;
8392
+ function resetFormatter() {
8393
+ activeFormatter = void 0;
8394
+ clearLocalBinCache();
8395
+ }
8396
+ function resolveFormatter() {
8397
+ if (activeFormatter !== void 0) return activeFormatter;
8398
+ const cwd = process.cwd();
8399
+ for (const f of FORMATTERS) {
8400
+ const local = resolveNodeBinFor(f, cwd);
8401
+ if (local) {
8402
+ activeFormatter = local;
8403
+ return local;
8404
+ }
8405
+ }
8406
+ for (const f of FORMATTERS) {
8407
+ const onPath = findOnPath(f.binName);
8408
+ if (!onPath) continue;
8409
+ activeFormatter = { cmd: onPath, args: [...f.writeArgs], label: f.label };
8410
+ return activeFormatter;
8411
+ }
8412
+ activeFormatter = null;
8413
+ return null;
8414
+ }
8415
+ function resolveNodeBinFor(f, cwd) {
8416
+ const hit = resolveFirstNodeBin([{ packageName: f.packageName, binName: f.binName }], cwd);
8417
+ if (!hit) return null;
8418
+ return { cmd: hit.cmd, args: [...hit.args, ...f.writeArgs], label: f.label };
8419
+ }
8420
+ function activeFormatterLabel() {
8421
+ return activeFormatter?.label ?? null;
8422
+ }
8423
+ async function sha256File(filePath) {
8424
+ try {
8425
+ return createHash("sha256").update(await readFile8(filePath)).digest("hex");
8426
+ } catch {
8427
+ return null;
8428
+ }
8429
+ }
8032
8430
  async function formatFile(filePath, timeoutMs) {
8033
8431
  if (!withinProject(filePath)) return null;
8034
8432
  try {
@@ -8036,30 +8434,40 @@ async function formatFile(filePath, timeoutMs) {
8036
8434
  } catch {
8037
8435
  return null;
8038
8436
  }
8437
+ const formatter = resolveFormatter();
8438
+ if (!formatter) return null;
8039
8439
  let bytesBefore;
8040
8440
  try {
8041
8441
  bytesBefore = (await stat4(filePath)).size;
8042
8442
  } catch {
8043
8443
  return null;
8044
8444
  }
8445
+ const hashBefore = await sha256File(filePath);
8446
+ let invocation;
8045
8447
  try {
8046
- await new Promise((resolve27, reject) => {
8448
+ invocation = resolveExecInvocation(formatter.cmd, [...formatter.args, filePath]);
8449
+ } catch {
8450
+ return null;
8451
+ }
8452
+ try {
8453
+ await new Promise((resolve28, reject) => {
8047
8454
  execFile6(
8048
- "npx",
8049
- ["biome", "format", "--write", filePath],
8455
+ invocation.cmd,
8456
+ invocation.args,
8050
8457
  {
8051
8458
  encoding: "utf-8",
8052
8459
  timeout: timeoutMs,
8053
8460
  cwd: process.cwd(),
8054
8461
  windowsHide: true,
8055
- maxBuffer: 16 * 1024 * 1024
8462
+ maxBuffer: 16 * 1024 * 1024,
8463
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
8056
8464
  },
8057
8465
  (err) => {
8058
8466
  if (err) {
8059
8467
  const e = err;
8060
8468
  if (e.killed) return reject(err);
8061
8469
  }
8062
- resolve27();
8470
+ resolve28();
8063
8471
  }
8064
8472
  );
8065
8473
  });
@@ -8075,28 +8483,11 @@ async function formatFile(filePath, timeoutMs) {
8075
8483
  if (bytesAfter !== bytesBefore) {
8076
8484
  return { changed: true, bytesBefore, bytesAfter };
8077
8485
  }
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
- });
8486
+ const hashAfter = await sha256File(filePath);
8487
+ if (hashBefore === null || hashAfter === null) {
8096
8488
  return { changed: false, bytesBefore, bytesAfter };
8097
- } catch {
8098
- return { changed: true, bytesBefore, bytesAfter };
8099
8489
  }
8490
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
8100
8491
  }
8101
8492
  var plugin25 = {
8102
8493
  name: "format-on-save",
@@ -8142,32 +8533,18 @@ var plugin25 = {
8142
8533
  state23.lastResult = null;
8143
8534
  recentlyCovered.clear();
8144
8535
  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");
8536
+ resetFormatter();
8537
+ const formatter = resolveFormatter();
8538
+ const formatterAvailable = formatter !== null;
8539
+ if (formatter) {
8540
+ api.log.info(`format-on-save: ${formatter.label} detected`);
8541
+ } else {
8542
+ api.log.warn(
8543
+ "format-on-save: no formatter found (biome, prettier, dprint) \u2014 hook will be a no-op"
8544
+ );
8168
8545
  }
8169
8546
  const hook = async (input) => {
8170
- if (!cfg.enabled || !biomeAvailable) return;
8547
+ if (!cfg.enabled || !formatterAvailable) return;
8171
8548
  if (input.toolResult?.isError) return;
8172
8549
  const toolName = input.toolName ?? "";
8173
8550
  const inp = input.toolInput ?? {};
@@ -8228,7 +8605,7 @@ var plugin25 = {
8228
8605
  );
8229
8606
  api.tools.register({
8230
8607
  name: "format_on_save_status",
8231
- description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
8608
+ description: "Reports format-on-save state: which formatter is active, and per-session formatted/clean/error/skipped counters.",
8232
8609
  inputSchema: { type: "object", properties: {} },
8233
8610
  permission: "auto",
8234
8611
  category: "Code Quality",
@@ -8237,7 +8614,14 @@ var plugin25 = {
8237
8614
  return {
8238
8615
  ok: true,
8239
8616
  enabled: cfg.enabled,
8240
- biomeAvailable,
8617
+ formatterAvailable,
8618
+ formatter: activeFormatterLabel(),
8619
+ /**
8620
+ * Back-compat alias for the pre-multi-formatter status shape.
8621
+ * Now accurate rather than assumed: true only when the resolved
8622
+ * formatter really is biome.
8623
+ */
8624
+ biomeAvailable: activeFormatterLabel() === "biome",
8241
8625
  timeoutMs: cfg.timeoutMs,
8242
8626
  skipWhenCoveredBy: cfg.skipWhenCoveredBy,
8243
8627
  skipTtlMs: cfg.skipTtlMs,
@@ -8255,11 +8639,13 @@ var plugin25 = {
8255
8639
  api.log.info("format-on-save plugin loaded", {
8256
8640
  version: "0.1.0",
8257
8641
  enabled: cfg.enabled,
8258
- biomeAvailable
8642
+ formatterAvailable,
8643
+ formatter: activeFormatterLabel()
8259
8644
  });
8260
8645
  },
8261
8646
  teardown(api) {
8262
8647
  clearRegistrations();
8648
+ resetFormatter();
8263
8649
  const final = {
8264
8650
  invocations: state23.invocationCount,
8265
8651
  formatted: state23.formattedCount,
@@ -8768,7 +9154,7 @@ var git_autocommit_default = plugin26;
8768
9154
  // src/import-organizer/index.ts
8769
9155
  import { spawn } from "node:child_process";
8770
9156
  import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
8771
- import { basename as basename4, isAbsolute as isAbsolute12 } from "node:path";
9157
+ import { basename as basename4, isAbsolute as isAbsolute13 } from "node:path";
8772
9158
  var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
8773
9159
  "npx",
8774
9160
  "pnpm",
@@ -8777,23 +9163,51 @@ var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
8777
9163
  "biome",
8778
9164
  "@biomejs/biome",
8779
9165
  "eslint",
9166
+ "oxlint",
8780
9167
  "node"
8781
9168
  // Recognise a fully-qualified project-local path like
8782
9169
  // "./node_modules/.bin/biome"; basename check kicks in below.
8783
9170
  ]);
9171
+ var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "npm", "yarn"]);
9172
+ var LOCAL_BIN_PACKAGES = {
9173
+ biome: { packageName: "@biomejs/biome", binName: "biome" },
9174
+ "@biomejs/biome": { packageName: "@biomejs/biome", binName: "biome" },
9175
+ eslint: { packageName: "eslint", binName: "eslint" },
9176
+ oxlint: { packageName: "oxlint", binName: "oxlint" }
9177
+ };
9178
+ function resolveLocalToolCommand(tokens) {
9179
+ if (tokens.length === 0) return null;
9180
+ let i = 0;
9181
+ if (PACKAGE_RUNNERS.has(tokens[i])) {
9182
+ i++;
9183
+ while (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx" || tokens[i] === "run")) {
9184
+ i++;
9185
+ }
9186
+ }
9187
+ const toolToken = tokens[i];
9188
+ if (!toolToken) return null;
9189
+ const rest = tokens.slice(i + 1);
9190
+ const pkg = LOCAL_BIN_PACKAGES[toolToken];
9191
+ if (!pkg) return null;
9192
+ const local = resolveNodeBin(pkg.packageName, pkg.binName, process.cwd(), rest);
9193
+ if (!local) return null;
9194
+ return { cmd: local.cmd, args: local.args };
9195
+ }
8784
9196
  function resolveAllowedCommand(command) {
8785
9197
  const tokens = command.split(/\s+/).filter(Boolean);
8786
9198
  if (tokens.length === 0) return null;
8787
9199
  const head = tokens[0];
9200
+ const local = ALLOWED_FIRST_TOKENS.has(head) ? resolveLocalToolCommand(tokens) : null;
9201
+ if (local) return local;
8788
9202
  if (ALLOWED_FIRST_TOKENS.has(head)) {
8789
9203
  return { cmd: head, args: tokens.slice(1) };
8790
9204
  }
8791
- if (isAbsolute12(head)) {
9205
+ if (isAbsolute13(head)) {
8792
9206
  if (!withinProject(head)) return null;
8793
9207
  const base = basename4(head);
8794
9208
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
8795
9209
  }
8796
- if (!isAbsolute12(head) && withinProject(head)) {
9210
+ if (!isAbsolute13(head) && withinProject(head)) {
8797
9211
  const base = basename4(head);
8798
9212
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
8799
9213
  }
@@ -8828,40 +9242,70 @@ function readConfig22(raw) {
8828
9242
  notifyFormatOnSave: r["notifyFormatOnSave"] !== false
8829
9243
  };
8830
9244
  }
9245
+ var MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
8831
9246
  function runCommand(command, args, timeoutMs, cwd) {
8832
- return new Promise((resolve27) => {
9247
+ return new Promise((resolve28) => {
8833
9248
  let timedOut = false;
9249
+ let settled = false;
9250
+ const settle = (r) => {
9251
+ if (settled) return;
9252
+ settled = true;
9253
+ resolve28(r);
9254
+ };
8834
9255
  const stdoutChunks = [];
8835
9256
  const stderrChunks = [];
9257
+ let stdoutBytes = 0;
9258
+ let stderrBytes = 0;
9259
+ const signal = AbortSignal.timeout(timeoutMs);
9260
+ const onAbort = () => {
9261
+ timedOut = true;
9262
+ settle({ code: null, stdout: "", stderr: "", timedOut: true });
9263
+ };
9264
+ signal.addEventListener("abort", onAbort, { once: true });
8836
9265
  let child;
9266
+ let invocation;
9267
+ try {
9268
+ invocation = resolveExecInvocation(command, args);
9269
+ } catch {
9270
+ signal.removeEventListener("abort", onAbort);
9271
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
9272
+ return;
9273
+ }
8837
9274
  try {
8838
- child = spawn(command, args, {
9275
+ child = spawn(invocation.cmd, invocation.args, {
8839
9276
  cwd,
8840
9277
  stdio: ["ignore", "pipe", "pipe"],
8841
- signal: AbortSignal.timeout(timeoutMs)
9278
+ signal,
9279
+ windowsHide: true,
9280
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
8842
9281
  });
8843
9282
  } catch {
8844
- resolve27({ code: 127, stdout: "", stderr: "", timedOut: false });
9283
+ signal.removeEventListener("abort", onAbort);
9284
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
8845
9285
  return;
8846
9286
  }
8847
- child.stdout?.on("data", (c) => stdoutChunks.push(c));
8848
- child.stderr?.on("data", (c) => stderrChunks.push(c));
9287
+ child.stdout?.on("data", (c) => {
9288
+ stdoutBytes += c.length;
9289
+ if (stdoutBytes <= MAX_CAPTURE_BYTES) stdoutChunks.push(c);
9290
+ });
9291
+ child.stderr?.on("data", (c) => {
9292
+ stderrBytes += c.length;
9293
+ if (stderrBytes <= MAX_CAPTURE_BYTES) stderrChunks.push(c);
9294
+ });
8849
9295
  child.on("error", () => {
8850
- resolve27({ code: 127, stdout: "", stderr: "", timedOut: false });
9296
+ signal.removeEventListener("abort", onAbort);
9297
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
8851
9298
  });
8852
9299
  child.on("close", (code) => {
9300
+ signal.removeEventListener("abort", onAbort);
8853
9301
  if (timedOut) return;
8854
- resolve27({
9302
+ settle({
8855
9303
  code,
8856
9304
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
8857
9305
  stderr: Buffer.concat(stderrChunks).toString("utf-8"),
8858
9306
  timedOut: false
8859
9307
  });
8860
9308
  });
8861
- child.on("abort", () => {
8862
- timedOut = true;
8863
- resolve27({ code: null, stdout: "", stderr: "", timedOut: true });
8864
- });
8865
9309
  });
8866
9310
  }
8867
9311
  async function organizeImports(filePath, cfg, cwd) {
@@ -9292,8 +9736,8 @@ var plugin28 = {
9292
9736
  var injection_shield_default = plugin28;
9293
9737
 
9294
9738
  // 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";
9739
+ import { readFile as readFile9 } from "node:fs/promises";
9740
+ import { isAbsolute as isAbsolute14, relative as relative14, resolve as resolve14 } from "node:path";
9297
9741
  var API_VERSION20 = "^0.1.10";
9298
9742
  var state26 = {
9299
9743
  scanCount: 0,
@@ -9306,7 +9750,8 @@ var state26 = {
9306
9750
  var DEFAULTS23 = {
9307
9751
  enabled: false,
9308
9752
  extensions: [".ts", ".tsx"],
9309
- maxFindings: 50
9753
+ maxFindings: 50,
9754
+ maxFiles: 2e3
9310
9755
  };
9311
9756
  function readConfig24(raw) {
9312
9757
  if (!raw || typeof raw !== "object") return { ...DEFAULTS23 };
@@ -9314,7 +9759,8 @@ function readConfig24(raw) {
9314
9759
  return {
9315
9760
  enabled: r["enabled"] !== false,
9316
9761
  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
9762
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS23.maxFindings,
9763
+ maxFiles: typeof r["maxFiles"] === "number" && r["maxFiles"] >= 1 && r["maxFiles"] <= 5e4 ? Math.floor(r["maxFiles"]) : DEFAULTS23.maxFiles
9318
9764
  };
9319
9765
  }
9320
9766
  function normalizeExtensions4(exts) {
@@ -9324,53 +9770,66 @@ function toPosix5(p) {
9324
9770
  return p.replace(/\\/g, "/");
9325
9771
  }
9326
9772
  function relativePath5(p) {
9327
- return toPosix5(relative13(process.cwd(), p));
9773
+ return toPosix5(relative14(process.cwd(), p));
9328
9774
  }
9329
9775
  var INTERFACE_RE = /(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
9330
9776
  function extractInterfaceNames(content) {
9331
9777
  const names = [];
9332
9778
  INTERFACE_RE.lastIndex = 0;
9779
+ let searchedUpTo = 0;
9780
+ let line = 1;
9333
9781
  for (const m of content.matchAll(INTERFACE_RE)) {
9334
- names.push({ name: m[1], line: content.slice(0, m.index).split(/\r?\n/).length });
9782
+ const index = m.index ?? 0;
9783
+ for (let i = searchedUpTo; i < index; i++) {
9784
+ if (content.charCodeAt(i) === 10) line += 1;
9785
+ }
9786
+ searchedUpTo = index;
9787
+ names.push({ name: m[1], line });
9335
9788
  }
9336
9789
  return names;
9337
9790
  }
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;
9791
+ var IMPLEMENTER_RE = /(?:implements|satisfies|\bas)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
9792
+ function collectImplementedNames(content, into) {
9793
+ IMPLEMENTER_RE.lastIndex = 0;
9794
+ for (const m of content.matchAll(IMPLEMENTER_RE)) {
9795
+ const name = m[1];
9796
+ if (name) into.add(name);
9342
9797
  }
9343
- return false;
9344
9798
  }
9345
9799
  async function scanPath3(rawPath, cfg) {
9346
9800
  const root = process.cwd();
9347
- const resolved = isAbsolute13(rawPath) ? resolve13(rawPath) : resolve13(root, rawPath);
9801
+ const resolved = isAbsolute14(rawPath) ? resolve14(rawPath) : resolve14(root, rawPath);
9348
9802
  const exts = normalizeExtensions4(cfg.extensions);
9349
- const files = await collectSourceFilesAsync(resolved, { extensions: exts });
9350
- const contents = [];
9803
+ const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
9804
+ const files = allFiles.slice(0, cfg.maxFiles);
9805
+ const implemented = /* @__PURE__ */ new Set();
9806
+ const declarations = [];
9807
+ let scannedFiles = 0;
9351
9808
  for (const p of files) {
9809
+ let content;
9352
9810
  try {
9353
- contents.push({ path: p, content: await readFile8(p, "utf-8") });
9811
+ content = await readFile9(p, "utf-8");
9354
9812
  } catch {
9813
+ continue;
9355
9814
  }
9356
- }
9357
- const allContentStrings = contents.map((c) => c.content);
9358
- const findings = [];
9359
- for (const { path, content } of contents) {
9815
+ scannedFiles += 1;
9816
+ collectImplementedNames(content, implemented);
9360
9817
  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
- }
9818
+ declarations.push({ name, line, file: p });
9370
9819
  }
9820
+ }
9821
+ const findings = [];
9822
+ for (const { name, line, file } of declarations) {
9823
+ if (implemented.has(name)) continue;
9824
+ findings.push({
9825
+ interfaceName: name,
9826
+ file: relativePath5(file),
9827
+ line,
9828
+ message: `interface "${name}" is declared but has no visible implementer`
9829
+ });
9371
9830
  if (findings.length >= cfg.maxFindings) break;
9372
9831
  }
9373
- return { findings, scannedFiles: contents.length };
9832
+ return { findings, scannedFiles, truncated: allFiles.length > files.length };
9374
9833
  }
9375
9834
  var plugin29 = {
9376
9835
  name: "interface-contract-guard",
@@ -9389,6 +9848,13 @@ var plugin29 = {
9389
9848
  default: [".ts", ".tsx"],
9390
9849
  description: "File extensions to scan."
9391
9850
  },
9851
+ maxFiles: {
9852
+ type: "number",
9853
+ minimum: 1,
9854
+ maximum: 5e4,
9855
+ default: 2e3,
9856
+ description: "Upper bound on files read per scan."
9857
+ },
9392
9858
  maxFindings: {
9393
9859
  type: "number",
9394
9860
  minimum: 1,
@@ -9422,10 +9888,10 @@ var plugin29 = {
9422
9888
  const exts = normalizeExtensions4(cfg.extensions);
9423
9889
  if (!matchesExtension(sourcePath, exts)) return;
9424
9890
  state26.hookInvocationCount += 1;
9425
- const resolved = resolve13(process.cwd(), sourcePath);
9891
+ const resolved = resolve14(process.cwd(), sourcePath);
9426
9892
  let content;
9427
9893
  try {
9428
- content = await readFile8(resolved, "utf-8");
9894
+ content = await readFile9(resolved, "utf-8");
9429
9895
  } catch {
9430
9896
  state26.errorCount += 1;
9431
9897
  return;
@@ -9470,9 +9936,16 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
9470
9936
  state26.findingCount += result.findings.length;
9471
9937
  return {
9472
9938
  ok: true,
9473
- path: relativePath5(resolve13(process.cwd(), rawPath)),
9939
+ path: relativePath5(resolve14(process.cwd(), rawPath)),
9474
9940
  scannedFiles: result.scannedFiles,
9475
- findings: result.findings
9941
+ findings: result.findings,
9942
+ // Say so when the corpus was cut short. A partial scan that
9943
+ // reports "no findings" is not the same as a clean result, and
9944
+ // the caller has no other way to tell the difference.
9945
+ truncated: result.truncated,
9946
+ ...result.truncated ? {
9947
+ note: `Scan stopped at the maxFiles limit (${cfg.maxFiles}); results are partial. Raise \`maxFiles\` or scan a narrower path for full coverage.`
9948
+ } : {}
9476
9949
  };
9477
9950
  }
9478
9951
  });
@@ -9543,8 +10016,9 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
9543
10016
  var interface_contract_guard_default = plugin29;
9544
10017
 
9545
10018
  // 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";
10019
+ import { readFileSync as readFileSync7 } from "node:fs";
10020
+ import { dirname as dirname5, isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10021
+ import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
9548
10022
  var API_VERSION21 = "^0.1.10";
9549
10023
  var state27 = {
9550
10024
  facts: [],
@@ -9565,10 +10039,10 @@ var DEFAULTS24 = {
9565
10039
  };
9566
10040
  function resolveProjectPath5(rawPath, cwd = process.cwd()) {
9567
10041
  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;
10042
+ const root = resolve15(cwd);
10043
+ const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10044
+ const rel = relative15(root, resolved);
10045
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute15(rel)) return resolved;
9572
10046
  return null;
9573
10047
  }
9574
10048
  function readConfig25(raw) {
@@ -9586,7 +10060,7 @@ function readConfig25(raw) {
9586
10060
  function loadFacts(filePath) {
9587
10061
  if (!filePath) return { facts: [], nextId: 1 };
9588
10062
  try {
9589
- const raw = JSON.parse(readFileSync6(filePath, "utf-8"));
10063
+ const raw = JSON.parse(readFileSync7(filePath, "utf-8"));
9590
10064
  const facts = Array.isArray(raw.facts) ? raw.facts.filter(
9591
10065
  (f) => !!f && typeof f === "object" && typeof f.id === "string" && typeof f.subject === "string" && typeof f.relation === "string" && typeof f.object === "string"
9592
10066
  ) : [];
@@ -9596,11 +10070,11 @@ function loadFacts(filePath) {
9596
10070
  return { facts: [], nextId: 1 };
9597
10071
  }
9598
10072
  }
9599
- function persistFacts(filePath) {
10073
+ async function persistFacts(filePath) {
9600
10074
  if (!filePath) return true;
9601
10075
  try {
9602
- mkdirSync2(dirname4(filePath), { recursive: true });
9603
- writeFileSync3(
10076
+ await ensureDir2(dirname5(filePath));
10077
+ await atomicWrite2(
9604
10078
  filePath,
9605
10079
  JSON.stringify({ facts: state27.facts, nextId: state27.nextId }, null, 2)
9606
10080
  );
@@ -9731,7 +10205,7 @@ var plugin30 = {
9731
10205
  state27.facts.push(fact);
9732
10206
  state27.adds += 1;
9733
10207
  api.metrics.counter("adds");
9734
- const persisted = persistFacts(resolved);
10208
+ const persisted = await persistFacts(resolved);
9735
10209
  return { ok: true, fact, persisted, totalFacts: state27.facts.length };
9736
10210
  }
9737
10211
  });
@@ -9791,7 +10265,7 @@ var plugin30 = {
9791
10265
  if (removed === 0) return { ok: false, error: `no fact matches "${input.id}"` };
9792
10266
  state27.removals += removed;
9793
10267
  api.metrics.counter("removals", removed);
9794
- const persisted = persistFacts(resolved);
10268
+ const persisted = await persistFacts(resolved);
9795
10269
  return { ok: true, removed, persisted, totalFacts: state27.facts.length };
9796
10270
  }
9797
10271
  });
@@ -9866,8 +10340,8 @@ var plugin30 = {
9866
10340
  var knowledge_graph_default = plugin30;
9867
10341
 
9868
10342
  // src/license-audit-gate/index.ts
9869
- import { readFileSync as readFileSync7 } from "node:fs";
9870
- import { resolve as resolve15 } from "node:path";
10343
+ import { readFileSync as readFileSync8 } from "node:fs";
10344
+ import { resolve as resolve16 } from "node:path";
9871
10345
  var API_VERSION22 = "^0.1.10";
9872
10346
  var state28 = {
9873
10347
  invocations: 0,
@@ -9943,8 +10417,8 @@ function auditPackages(names, allowedLicenses) {
9943
10417
  for (const name of names) {
9944
10418
  let licenses = [];
9945
10419
  try {
9946
- const pkgPath = resolve15("node_modules", name, "package.json");
9947
- const raw = JSON.parse(readFileSync7(pkgPath, "utf-8"));
10420
+ const pkgPath = resolve16("node_modules", name, "package.json");
10421
+ const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
9948
10422
  licenses = extractLicenseStrings(raw);
9949
10423
  } catch {
9950
10424
  errors.push(name);
@@ -10124,11 +10598,11 @@ var license_audit_gate_default = plugin31;
10124
10598
 
10125
10599
  // src/lint-gate/index.ts
10126
10600
  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";
10601
+ import { readFileSync as readFileSync9 } from "node:fs";
10602
+ import { mkdtemp, readFile as readFile10, rm, writeFile as writeFile2 } from "node:fs/promises";
10603
+ import { createRequire as createRequire2 } from "node:module";
10130
10604
  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";
10605
+ import { dirname as dirname6, isAbsolute as isAbsolute16, join as join4, relative as relative16, resolve as resolve17, sep as sep3 } from "node:path";
10132
10606
  var API_VERSION23 = "^0.1.10";
10133
10607
  var state29 = {
10134
10608
  /** Total PreToolUse invocations. */
@@ -10166,22 +10640,22 @@ var LINTER_PACKAGES = {
10166
10640
  biome: "@biomejs/biome",
10167
10641
  eslint: "eslint"
10168
10642
  };
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);
10643
+ var linterCache = new BoundedMap({ max: 32, ttlMs: 3e5 });
10644
+ function isInside2(parent, candidate) {
10645
+ const rel = relative16(parent, candidate);
10646
+ return rel === "" || !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute16(rel);
10173
10647
  }
10174
10648
  function resolveLocalLinter(name, cwd) {
10175
10649
  try {
10176
10650
  const packageName = LINTER_PACKAGES[name];
10177
- const requireFromProject = createRequire(resolve16(cwd, "package.json"));
10651
+ const requireFromProject = createRequire2(resolve17(cwd, "package.json"));
10178
10652
  const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
10179
- const packageJson = JSON.parse(readFileSync8(packagePath, "utf-8"));
10653
+ const packageJson = JSON.parse(readFileSync9(packagePath, "utf-8"));
10180
10654
  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;
10655
+ if (!relativeBin || isAbsolute16(relativeBin)) return null;
10656
+ const packageDir = dirname6(packagePath);
10657
+ const entry = resolve17(packageDir, relativeBin);
10658
+ if (!isInside2(packageDir, entry)) return null;
10185
10659
  return {
10186
10660
  cmd: process.execPath,
10187
10661
  args: name === "biome" ? [entry, "check", "--reporter=json"] : [entry, "--format=json"],
@@ -10232,8 +10706,8 @@ async function detectLinter(requested, cwd) {
10232
10706
  }
10233
10707
  async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
10234
10708
  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}`);
10709
+ const tmpDir = await mkdtemp(join4(tmpdir(), "lint-gate-"));
10710
+ const tmpFile = join4(tmpDir, `input${ext}`);
10237
10711
  try {
10238
10712
  await writeFile2(tmpFile, content, "utf-8");
10239
10713
  const fullArgs = [...linter.args, tmpFile];
@@ -10250,14 +10724,14 @@ async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
10250
10724
  }
10251
10725
  async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
10252
10726
  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}`);
10727
+ const tmpDir = await mkdtemp(join4(tmpdir(), "lint-gate-fix-"));
10728
+ const tmpFile = join4(tmpDir, `input${ext}`);
10255
10729
  try {
10256
10730
  await writeFile2(tmpFile, content, "utf-8");
10257
10731
  const fixArgs = linter.name === "biome" ? [linter.args[0], "check", "--write", tmpFile] : [linter.args[0], "--fix", tmpFile];
10258
10732
  await runCommand2(linter.cmd, fixArgs, timeoutMs, cwd, signal);
10259
10733
  if (signal.aborted) throw signal.reason;
10260
- return await readFile9(tmpFile, "utf-8");
10734
+ return await readFile10(tmpFile, "utf-8");
10261
10735
  } catch {
10262
10736
  if (signal.aborted) throw signal.reason;
10263
10737
  return content;
@@ -10357,7 +10831,7 @@ var plugin32 = {
10357
10831
  state29.lastResult = null;
10358
10832
  linterCache.clear();
10359
10833
  const cfg = readConfig27(api.config.extensions?.["lint-gate"]);
10360
- const cwd = resolve16(api.config.cwd ?? process.cwd());
10834
+ const cwd = resolve17(api.config.cwd ?? process.cwd());
10361
10835
  const linterReady = detectLinter(cfg.linter, cwd).then((linter) => {
10362
10836
  if (!linter) {
10363
10837
  api.log.warn("lint-gate: no linter found (biome or eslint) \u2014 hook will be a no-op", {
@@ -10386,7 +10860,7 @@ var plugin32 = {
10386
10860
  const newStr = inp["new_string"];
10387
10861
  if (typeof oldStr !== "string" || typeof newStr !== "string") return;
10388
10862
  try {
10389
- const current = await readFile9(filePath, "utf-8");
10863
+ const current = await readFile10(filePath, "utf-8");
10390
10864
  content = applyEdit(current, oldStr, newStr);
10391
10865
  } catch {
10392
10866
  return;
@@ -10591,7 +11065,7 @@ ${summary}${truncated}`
10591
11065
  var lint_gate_default = plugin32;
10592
11066
 
10593
11067
  // src/llm-cache/index.ts
10594
- import { createHash } from "node:crypto";
11068
+ import { createHash as createHash2 } from "node:crypto";
10595
11069
  var state30 = {
10596
11070
  cache: /* @__PURE__ */ new Map(),
10597
11071
  hits: 0,
@@ -10641,7 +11115,7 @@ function fingerprintRequest(request) {
10641
11115
  topK: request["topK"] ?? null,
10642
11116
  topP: request["topP"] ?? null
10643
11117
  };
10644
- const fingerprint2 = createHash("sha256").update(JSON.stringify(subset)).digest("hex");
11118
+ const fingerprint2 = createHash2("sha256").update(JSON.stringify(subset)).digest("hex");
10645
11119
  fingerprintCache.set(request, fingerprint2);
10646
11120
  return fingerprint2;
10647
11121
  }
@@ -10920,16 +11394,26 @@ function canonicalize(value) {
10920
11394
  return String(value);
10921
11395
  }
10922
11396
  }
10923
- function sortKeys(value) {
10924
- if (Array.isArray(value)) return value.map(sortKeys);
10925
- if (value && typeof value === "object") {
11397
+ var CANONICALIZE_MAX_DEPTH = 12;
11398
+ function sortKeys(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
11399
+ if (value === null || typeof value !== "object") return value;
11400
+ if (seen.has(value)) return "[circular]";
11401
+ if (depth >= CANONICALIZE_MAX_DEPTH) {
11402
+ return Array.isArray(value) ? `[array:${value.length}]` : "[deep-object]";
11403
+ }
11404
+ seen.add(value);
11405
+ try {
11406
+ if (Array.isArray(value)) {
11407
+ return value.map((v) => sortKeys(v, depth + 1, seen));
11408
+ }
10926
11409
  const out = {};
10927
11410
  for (const key of Object.keys(value).sort()) {
10928
- out[key] = sortKeys(value[key]);
11411
+ out[key] = sortKeys(value[key], depth + 1, seen);
10929
11412
  }
10930
11413
  return out;
11414
+ } finally {
11415
+ seen.delete(value);
10931
11416
  }
10932
- return value;
10933
11417
  }
10934
11418
  function fingerprint(toolName, toolInput) {
10935
11419
  return `${toolName}\0${canonicalize(toolInput)}`;
@@ -10955,7 +11439,7 @@ function hashString2(value) {
10955
11439
  }
10956
11440
  async function gitDiffFingerprint(cwd, signal) {
10957
11441
  try {
10958
- const diff = await new Promise((resolve27, reject) => {
11442
+ const diff = await new Promise((resolve28, reject) => {
10959
11443
  execFile9(
10960
11444
  "git",
10961
11445
  ["diff", "--no-ext-diff", "--"],
@@ -10972,7 +11456,7 @@ async function gitDiffFingerprint(cwd, signal) {
10972
11456
  },
10973
11457
  (error, stdout) => {
10974
11458
  if (error) reject(error);
10975
- else resolve27(stdout);
11459
+ else resolve28(stdout);
10976
11460
  }
10977
11461
  );
10978
11462
  });
@@ -11322,7 +11806,7 @@ var plugin34 = {
11322
11806
  var loop_breaker_default = plugin34;
11323
11807
 
11324
11808
  // src/migration-planner/index.ts
11325
- import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
11809
+ import { existsSync as existsSync4, readFileSync as readFileSync10 } from "node:fs";
11326
11810
  var API_VERSION24 = "^0.1.10";
11327
11811
  var state32 = {
11328
11812
  plansGenerated: 0,
@@ -11362,7 +11846,7 @@ function readChangelog(packageName, cfg) {
11362
11846
  if (!withinProject(candidate)) continue;
11363
11847
  if (existsSync4(candidate)) {
11364
11848
  try {
11365
- const content = readFileSync9(candidate, "utf-8");
11849
+ const content = readFileSync10(candidate, "utf-8");
11366
11850
  return { source: candidate, content: content.slice(0, cfg.maxChars) };
11367
11851
  } catch {
11368
11852
  }
@@ -12189,13 +12673,21 @@ async function deliver(event, payload) {
12189
12673
  }
12190
12674
  const result = await deliverViaChannel(ch, event, {
12191
12675
  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),
12676
+ body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : safeJsonStringify(payload),
12193
12677
  level: event === "tool.error" || event === "budget.threshold" ? "warning" : "info",
12194
12678
  source: event,
12195
12679
  metadata: payload
12196
12680
  });
12197
12681
  return result.ok;
12198
12682
  }
12683
+ function deliverDetached(event, payload) {
12684
+ deliver(event, payload).catch((err) => {
12685
+ _pluginLog?.warn("notify-hub: notification delivery failed", {
12686
+ event,
12687
+ error: err instanceof Error ? err.message : String(err)
12688
+ });
12689
+ });
12690
+ }
12199
12691
  async function deliverViaChannel(ch, event, msg) {
12200
12692
  const result = await ch.deliver(msg);
12201
12693
  if (result.ok) {
@@ -12304,7 +12796,7 @@ var plugin37 = {
12304
12796
  }
12305
12797
  if (active && cfg.events.includes("session.stop")) {
12306
12798
  const stopHook = (input) => {
12307
- void deliver("session.stop", {
12799
+ deliverDetached("session.stop", {
12308
12800
  sessionId: input.sessionId ?? null,
12309
12801
  cwd: input.cwd ?? null
12310
12802
  });
@@ -12315,7 +12807,7 @@ var plugin37 = {
12315
12807
  const off = api.onPattern("tool.*", (eventName, payload) => {
12316
12808
  if (!/error|failed/.test(eventName)) return;
12317
12809
  const p = payload;
12318
- void deliver("tool.error", {
12810
+ deliverDetached("tool.error", {
12319
12811
  tool: p?.tool ?? p?.name ?? "unknown",
12320
12812
  busEvent: eventName,
12321
12813
  error: truncateText(
@@ -12329,7 +12821,7 @@ var plugin37 = {
12329
12821
  if (active && cfg.events.includes("budget.threshold")) {
12330
12822
  const off = api.onPattern("budget.*", (eventName, payload) => {
12331
12823
  if (!eventName.includes("threshold")) return;
12332
- void deliver("budget.threshold", { busEvent: eventName, detail: payload });
12824
+ deliverDetached("budget.threshold", { busEvent: eventName, detail: payload });
12333
12825
  });
12334
12826
  state34.eventUnsubscribers.push(off);
12335
12827
  }
@@ -12692,8 +13184,8 @@ var plugin38 = {
12692
13184
  var path_guard_default = plugin38;
12693
13185
 
12694
13186
  // 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";
13187
+ import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
13188
+ import { isAbsolute as isAbsolute17, relative as relative17, resolve as resolve18 } from "node:path";
12697
13189
  var API_VERSION25 = "^0.1.10";
12698
13190
  var state36 = {
12699
13191
  invocationCount: 0,
@@ -12718,18 +13210,18 @@ function readConfig34(raw) {
12718
13210
  }
12719
13211
  function withinProject4(p, cwd = process.cwd()) {
12720
13212
  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);
13213
+ const root = resolve18(cwd);
13214
+ const resolved = isAbsolute17(p) ? resolve18(p) : resolve18(root, p);
13215
+ const rel = relative17(root, resolved);
12724
13216
  if (rel === "" || rel === ".") return true;
12725
13217
  if (rel.startsWith("..")) return false;
12726
- if (isAbsolute16(rel)) return false;
13218
+ if (isAbsolute17(rel)) return false;
12727
13219
  return true;
12728
13220
  }
12729
13221
  function resolveProjectPath6(rawPath, cwd = process.cwd()) {
12730
13222
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
12731
- const root = resolve17(cwd);
12732
- const resolved = isAbsolute16(rawPath) ? resolve17(rawPath) : resolve17(root, rawPath);
13223
+ const root = resolve18(cwd);
13224
+ const resolved = isAbsolute17(rawPath) ? resolve18(rawPath) : resolve18(root, rawPath);
12733
13225
  if (!withinProject4(resolved, cwd)) return null;
12734
13226
  return resolved;
12735
13227
  }
@@ -12760,7 +13252,7 @@ function flattenResults(results) {
12760
13252
  function loadResults(path) {
12761
13253
  if (!path || !existsSync5(path)) return null;
12762
13254
  try {
12763
- const raw = JSON.parse(readFileSync10(path, "utf-8"));
13255
+ const raw = JSON.parse(readFileSync11(path, "utf-8"));
12764
13256
  return raw;
12765
13257
  } catch {
12766
13258
  return null;
@@ -13144,7 +13636,7 @@ var plugin_stack_observer_default = PLUGIN;
13144
13636
  // src/pr-drafter/index.ts
13145
13637
  import { execFile as execFile10 } from "node:child_process";
13146
13638
  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";
13639
+ import { dirname as dirname7, isAbsolute as isAbsolute18, relative as relative18, resolve as resolve19 } from "node:path";
13148
13640
  var API_VERSION26 = "^0.1.10";
13149
13641
  var state38 = {
13150
13642
  commits: [],
@@ -13183,10 +13675,10 @@ function readConfig36(raw) {
13183
13675
  }
13184
13676
  function resolveProjectPath7(rawPath, cwd = process.cwd()) {
13185
13677
  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;
13678
+ const root = resolve19(cwd);
13679
+ const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
13680
+ const rel = relative18(root, resolved);
13681
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute18(rel)) return resolved;
13190
13682
  return null;
13191
13683
  }
13192
13684
  function runGit4(args, timeout) {
@@ -13272,7 +13764,7 @@ async function writeDraft(cfg, llm) {
13272
13764
  }
13273
13765
  const draft = await buildDraft(cfg, llm);
13274
13766
  try {
13275
- await mkdir2(dirname6(resolved), { recursive: true });
13767
+ await mkdir2(dirname7(resolved), { recursive: true });
13276
13768
  await writeFile3(resolved, draft.body);
13277
13769
  state38.draftsWritten += 1;
13278
13770
  } catch {
@@ -13399,7 +13891,7 @@ var plugin40 = {
13399
13891
  const resolved = resolveProjectPath7(cfg.outputPath);
13400
13892
  if (!resolved) return { ok: false, error: "outputPath resolves outside project" };
13401
13893
  try {
13402
- await mkdir2(dirname6(resolved), { recursive: true });
13894
+ await mkdir2(dirname7(resolved), { recursive: true });
13403
13895
  await writeFile3(resolved, draft.body);
13404
13896
  state38.draftsWritten += 1;
13405
13897
  return {
@@ -13620,26 +14112,155 @@ var plugin41 = {
13620
14112
  };
13621
14113
  var process_guard_default = plugin41;
13622
14114
 
14115
+ // src/runtime/credential-patterns.ts
14116
+ var CREDENTIAL_PATTERNS = [
14117
+ // LLM provider keys
14118
+ {
14119
+ type: "anthropic_key",
14120
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
14121
+ },
14122
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
14123
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
14124
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
14125
+ // refresh (`ghr_`) tokens grant the same or broader access and were
14126
+ // previously not detected at all.
14127
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
14128
+ {
14129
+ type: "github_oauth_token",
14130
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
14131
+ },
14132
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
14133
+ // GitLab
14134
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14135
+ {
14136
+ type: "gitlab_runner_token",
14137
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14138
+ },
14139
+ // npm — a leaked publish token is a supply-chain compromise.
14140
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
14141
+ // AWS
14142
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
14143
+ // GCP
14144
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
14145
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
14146
+ // both are as sensitive as the bot/user tokens already covered.
14147
+ {
14148
+ type: "slack_token",
14149
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
14150
+ },
14151
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
14152
+ {
14153
+ type: "slack_webhook",
14154
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
14155
+ },
14156
+ // Stripe
14157
+ {
14158
+ type: "stripe_key",
14159
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
14160
+ },
14161
+ // Twilio
14162
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
14163
+ // Telegram
14164
+ {
14165
+ type: "telegram_bot_token",
14166
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14167
+ },
14168
+ // JWT
14169
+ {
14170
+ type: "jwt",
14171
+ 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
14172
+ },
14173
+ // Private keys
14174
+ {
14175
+ type: "private_key",
14176
+ 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
14177
+ },
14178
+ // AI/ML provider tokens
14179
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
14180
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14181
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14182
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14183
+ // SaaS / infrastructure tokens — each grants API access on the user's
14184
+ // account, and each was previously invisible to this gate.
14185
+ { type: "sendgrid_key", regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14186
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
14187
+ { type: "doppler_token", regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14188
+ { type: "shopify_token", regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g },
14189
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14190
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
14191
+ { type: "atlassian_token", regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g },
14192
+ { type: "square_token", regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
14193
+ {
14194
+ type: "azure_storage_key",
14195
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
14196
+ },
14197
+ {
14198
+ type: "google_oauth_client_secret",
14199
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
14200
+ },
14201
+ // Bearer tokens
14202
+ {
14203
+ type: "bearer_token",
14204
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
14205
+ },
14206
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
14207
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
14208
+ {
14209
+ type: "postgres_uri",
14210
+ // Query parsers decode percent-encoded parameter names. Recognize each
14211
+ // encoded character in `password` so mixed forms such as `pass%77ord`
14212
+ // cannot bypass detection while keeping the scan strictly bounded.
14213
+ 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
14214
+ },
14215
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
14216
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g }
14217
+ ];
14218
+ function cloneCredentialPatterns() {
14219
+ return CREDENTIAL_PATTERNS.map((p) => ({
14220
+ type: p.type,
14221
+ regex: new RegExp(p.regex.source, p.regex.flags)
14222
+ }));
14223
+ }
14224
+
13623
14225
  // src/prompt-firewall/index.ts
13624
- var PATTERNS2 = [
13625
- { kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
14226
+ var KIND_ALIASES = {
14227
+ aws_access_key: "aws-access-key",
14228
+ private_key: "private-key-block",
14229
+ github_pat: "github-token",
14230
+ github_oauth_token: "github-token",
14231
+ openai_key: "openai-key",
14232
+ anthropic_key: "anthropic-key",
14233
+ slack_token: "slack-token",
14234
+ gcp_key: "google-api-key",
14235
+ bearer_token: "bearer-token"
14236
+ };
14237
+ var EXTRA_PATTERNS = [
13626
14238
  {
14239
+ // AWS secret access keys have no prefix — only a 40-char base64-ish
14240
+ // shape — so they need a nearby `aws`/`secret` mention to be worth
14241
+ // acting on.
14242
+ //
14243
+ // The previous spelling required the word "aws" to appear AFTER the
14244
+ // key and wrapped the run in `\b`. Both are wrong: the real layout is
14245
+ // `AWS_SECRET_ACCESS_KEY=<key>` (context first), and a `\b` cannot
14246
+ // hold next to the `+` or `/` that base64 keys routinely end with. The
14247
+ // pattern therefore never fired on a real key. Anchor on the context
14248
+ // token instead and let the key follow it.
13627
14249
  kind: "aws-secret-key",
13628
- re: /\b(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])\b(?=.*aws)/gi
14250
+ 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
14251
  },
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
14252
  {
13639
14253
  kind: "generic-secret-assignment",
13640
14254
  re: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*['"]?[A-Za-z0-9._/+-]{12,}['"]?/gi
13641
14255
  }
13642
14256
  ];
14257
+ var PATTERNS2 = [
14258
+ ...cloneCredentialPatterns().map((p) => ({
14259
+ kind: KIND_ALIASES[p.type] ?? p.type,
14260
+ re: p.regex
14261
+ })),
14262
+ ...EXTRA_PATTERNS
14263
+ ];
13643
14264
  function detectSecrets(text, allow) {
13644
14265
  const counts = /* @__PURE__ */ new Map();
13645
14266
  for (const p of PATTERNS2) {
@@ -13915,9 +14536,10 @@ var plugin42 = {
13915
14536
  var prompt_firewall_default = plugin42;
13916
14537
 
13917
14538
  // 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";
14539
+ import { readFile as readFile11 } from "node:fs/promises";
14540
+ import { isAbsolute as isAbsolute19, relative as relative19, resolve as resolve20 } from "node:path";
13920
14541
  var API_VERSION27 = "^0.1.10";
14542
+ var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
13921
14543
  var state41 = {
13922
14544
  scanCount: 0,
13923
14545
  suggestionCount: 0,
@@ -13925,7 +14547,7 @@ var state41 = {
13925
14547
  warningCount: 0,
13926
14548
  errorCount: 0,
13927
14549
  hookUnregister: null,
13928
- lastHookWarning: /* @__PURE__ */ new Map()
14550
+ lastHookWarning: new BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS2 })
13929
14551
  };
13930
14552
  var DEFAULTS36 = {
13931
14553
  enabled: false,
@@ -13959,7 +14581,7 @@ function toPosix6(p) {
13959
14581
  return p.replace(/\\/g, "/");
13960
14582
  }
13961
14583
  function relativePath6(p) {
13962
- return toPosix6(relative18(process.cwd(), p));
14584
+ return toPosix6(relative19(process.cwd(), p));
13963
14585
  }
13964
14586
  function leadingIndentLevel(line) {
13965
14587
  const leading = line.match(/^(\s*)/)?.[1] ?? "";
@@ -14046,19 +14668,30 @@ function detectSmells(filePath, content, rules) {
14046
14668
  }
14047
14669
  async function scanPath4(rawPath, cfg) {
14048
14670
  const root = process.cwd();
14049
- const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
14671
+ const resolved = isAbsolute19(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
14050
14672
  const exts = normalizeExtensions5(cfg.extensions);
14051
14673
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
14052
14674
  const suggestions = [];
14675
+ let scannedFiles = 0;
14676
+ let truncated = false;
14053
14677
  for (const p of files) {
14054
14678
  try {
14055
- const content = await readFile10(p, "utf-8");
14679
+ const content = await readFile11(p, "utf-8");
14680
+ scannedFiles += 1;
14056
14681
  suggestions.push(...detectSmells(p, content, cfg.rules));
14057
- if (suggestions.length >= cfg.maxSuggestions) break;
14682
+ if (suggestions.length >= cfg.maxSuggestions) {
14683
+ truncated = scannedFiles < files.length;
14684
+ break;
14685
+ }
14058
14686
  } catch {
14059
14687
  }
14060
14688
  }
14061
- return { suggestions: suggestions.slice(0, cfg.maxSuggestions), scannedFiles: files.length };
14689
+ return {
14690
+ suggestions: suggestions.slice(0, cfg.maxSuggestions),
14691
+ scannedFiles,
14692
+ discoveredFiles: files.length,
14693
+ truncated
14694
+ };
14062
14695
  }
14063
14696
  var plugin43 = {
14064
14697
  name: "refactor-suggester",
@@ -14121,11 +14754,11 @@ var plugin43 = {
14121
14754
  state41.hookInvocationCount += 1;
14122
14755
  const now = Date.now();
14123
14756
  const lastWarning = state41.lastHookWarning.get(sourcePath);
14124
- if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
14125
- const resolved = resolve19(process.cwd(), sourcePath);
14757
+ if (lastWarning !== void 0 && now - lastWarning < HOOK_WARNING_COOLDOWN_MS2) return;
14758
+ const resolved = resolve20(process.cwd(), sourcePath);
14126
14759
  let content;
14127
14760
  try {
14128
- content = await readFile10(resolved, "utf-8");
14761
+ content = await readFile11(resolved, "utf-8");
14129
14762
  } catch {
14130
14763
  state41.errorCount += 1;
14131
14764
  return;
@@ -14169,8 +14802,12 @@ var plugin43 = {
14169
14802
  state41.suggestionCount += result.suggestions.length;
14170
14803
  return {
14171
14804
  ok: true,
14172
- path: relativePath6(resolve19(process.cwd(), rawPath)),
14805
+ path: relativePath6(resolve20(process.cwd(), rawPath)),
14173
14806
  scannedFiles: result.scannedFiles,
14807
+ discoveredFiles: result.discoveredFiles,
14808
+ // Say so when the cap stopped the walk early: a partial scan
14809
+ // that reports few findings must not read as a clean result.
14810
+ truncated: result.truncated,
14174
14811
  suggestions: result.suggestions,
14175
14812
  rules: cfg.rules
14176
14813
  };
@@ -14579,7 +15216,7 @@ var plugin44 = {
14579
15216
  var release_notes_generator_default = plugin44;
14580
15217
 
14581
15218
  // src/schema-evolution-guard/index.ts
14582
- import { readFileSync as readFileSync11 } from "node:fs";
15219
+ import { readFileSync as readFileSync12 } from "node:fs";
14583
15220
  import { basename as basename5 } from "node:path";
14584
15221
  var API_VERSION29 = "^0.1.10";
14585
15222
  var state43 = {
@@ -14782,7 +15419,7 @@ var plugin45 = {
14782
15419
  content = toolInput["content"];
14783
15420
  } else if (withinProject(filePath)) {
14784
15421
  try {
14785
- content = readFileSync11(filePath, "utf-8");
15422
+ content = readFileSync12(filePath, "utf-8");
14786
15423
  } catch {
14787
15424
  content = void 0;
14788
15425
  }
@@ -14886,71 +15523,43 @@ ${body}${suffix}`;
14886
15523
  var schema_evolution_guard_default = plugin45;
14887
15524
 
14888
15525
  // 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
- ];
15526
+ var BASE_PATTERNS = cloneCredentialPatterns();
14943
15527
  var PATTERNS3 = [...BASE_PATTERNS];
14944
15528
  var patternCacheKey = "";
15529
+ var GROUP_INDEX_OF_PATTERN = [];
14945
15530
  var COMBINED_REGEX = buildCombinedRegex(PATTERNS3);
14946
- var RE_DOS_MAX_SCAN_LENGTH = 1e5;
15531
+ var SCAN_WINDOW_LENGTH = 1e5;
15532
+ var SCAN_WINDOW_OVERLAP = 4096;
15533
+ var MAX_TOTAL_SCAN_LENGTH = 8 * 1024 * 1024;
15534
+ var TOO_LARGE_MARKER = "unscannable_oversized_input";
14947
15535
  var RE_DOS_TIMEOUT_MS = 100;
15536
+ function countCaptureGroups(source) {
15537
+ try {
15538
+ return new RegExp(`${source}|`).exec("").length - 1;
15539
+ } catch {
15540
+ return 0;
15541
+ }
15542
+ }
15543
+ function patternTypeForGroups(groups) {
15544
+ for (let i = 0; i < PATTERNS3.length; i++) {
15545
+ const at = GROUP_INDEX_OF_PATTERN[i];
15546
+ if (at !== void 0 && groups[at] !== void 0) return PATTERNS3[i].type;
15547
+ }
15548
+ return void 0;
15549
+ }
14948
15550
  function buildCombinedRegex(patterns) {
14949
- const newCacheKey = JSON.stringify(patterns.map((p) => p.type));
15551
+ const newCacheKey = JSON.stringify(patterns.map((p) => [p.type, p.regex.source]));
14950
15552
  if (newCacheKey === patternCacheKey && COMBINED_REGEX) {
14951
15553
  return COMBINED_REGEX;
14952
15554
  }
14953
15555
  patternCacheKey = newCacheKey;
15556
+ const offsets = [];
15557
+ let cursor = 0;
15558
+ for (const p of patterns) {
15559
+ offsets.push(cursor);
15560
+ cursor += 1 + countCaptureGroups(p.regex.source);
15561
+ }
15562
+ GROUP_INDEX_OF_PATTERN = offsets;
14954
15563
  return new RegExp(patterns.map((p) => `(${p.regex.source})`).join("|"), "g");
14955
15564
  }
14956
15565
  var state44 = {
@@ -14968,29 +15577,37 @@ var state44 = {
14968
15577
  /** PostToolUse hook handle so teardown can unregister. */
14969
15578
  postHookUnregister: null
14970
15579
  };
14971
- function findMatches(text) {
14972
- if (!text) return [];
14973
- if (text.length > RE_DOS_MAX_SCAN_LENGTH) return [];
14974
- const found = /* @__PURE__ */ new Set();
15580
+ function scanWindow(window, found, startTime) {
14975
15581
  COMBINED_REGEX.lastIndex = 0;
14976
15582
  let m;
14977
- const startTime = performance.now();
14978
- while ((m = COMBINED_REGEX.exec(text)) !== null) {
15583
+ while ((m = COMBINED_REGEX.exec(window)) !== null) {
14979
15584
  if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
14980
15585
  throw new Error(
14981
- `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${text.length}-char input`
15586
+ `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${window.length}-char window`
14982
15587
  );
14983
15588
  }
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
- }
15589
+ const type = patternTypeForGroups(m.slice(1));
15590
+ if (type !== void 0) found.add(type);
14990
15591
  if (m.index === COMBINED_REGEX.lastIndex) {
14991
15592
  COMBINED_REGEX.lastIndex += 1;
14992
15593
  }
14993
15594
  }
15595
+ }
15596
+ function findMatches(text) {
15597
+ if (!text) return [];
15598
+ const found = /* @__PURE__ */ new Set();
15599
+ const startTime = performance.now();
15600
+ if (text.length <= SCAN_WINDOW_LENGTH) {
15601
+ scanWindow(text, found, startTime);
15602
+ return Array.from(found).sort();
15603
+ }
15604
+ if (text.length > MAX_TOTAL_SCAN_LENGTH) {
15605
+ return [TOO_LARGE_MARKER];
15606
+ }
15607
+ const stride = SCAN_WINDOW_LENGTH - SCAN_WINDOW_OVERLAP;
15608
+ for (let offset = 0; offset < text.length; offset += stride) {
15609
+ scanWindow(text.slice(offset, offset + SCAN_WINDOW_LENGTH), found, startTime);
15610
+ }
14994
15611
  return Array.from(found).sort();
14995
15612
  }
14996
15613
  function scanInput(input, depth = 0) {
@@ -15016,30 +15633,55 @@ function scanInput(input, depth = 0) {
15016
15633
  }
15017
15634
  return null;
15018
15635
  }
15636
+ function redactString(text) {
15637
+ if (text.length > SCAN_WINDOW_LENGTH) {
15638
+ return { ok: false, reason: "oversized_input" };
15639
+ }
15640
+ const startTime = performance.now();
15641
+ let cursor = 0;
15642
+ let output = "";
15643
+ COMBINED_REGEX.lastIndex = 0;
15644
+ let match;
15645
+ while ((match = COMBINED_REGEX.exec(text)) !== null) {
15646
+ if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
15647
+ return { ok: false, reason: "oversized_input" };
15648
+ }
15649
+ const type = patternTypeForGroups(match.slice(1));
15650
+ output += text.slice(cursor, match.index);
15651
+ output += `[REDACTED:${type ?? "unknown"}]`;
15652
+ cursor = COMBINED_REGEX.lastIndex;
15653
+ if (match.index === COMBINED_REGEX.lastIndex) {
15654
+ COMBINED_REGEX.lastIndex += 1;
15655
+ }
15656
+ }
15657
+ if (cursor === 0) return { ok: true, value: text };
15658
+ return { ok: true, value: output + text.slice(cursor) };
15659
+ }
15019
15660
  function redactInput(input, depth = 0) {
15020
- if (input === null || input === void 0) return input;
15021
- if (depth > 50) return input;
15661
+ if (input === null || input === void 0) return { ok: true, value: input };
15662
+ if (depth > 50) return { ok: false, reason: "maximum_depth_exceeded" };
15022
15663
  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
- });
15664
+ return redactString(input);
15031
15665
  }
15032
15666
  if (Array.isArray(input)) {
15033
- return input.map((item) => redactInput(item, depth + 1));
15667
+ const out = [];
15668
+ for (const item of input) {
15669
+ const redacted = redactInput(item, depth + 1);
15670
+ if (!redacted.ok) return redacted;
15671
+ out.push(redacted.value);
15672
+ }
15673
+ return { ok: true, value: out };
15034
15674
  }
15035
15675
  if (typeof input === "object") {
15036
15676
  const out = {};
15037
15677
  for (const [k, v] of Object.entries(input)) {
15038
- out[k] = redactInput(v, depth + 1);
15678
+ const redacted = redactInput(v, depth + 1);
15679
+ if (!redacted.ok) return redacted;
15680
+ out[k] = redacted.value;
15039
15681
  }
15040
- return out;
15682
+ return { ok: true, value: out };
15041
15683
  }
15042
- return input;
15684
+ return { ok: true, value: input };
15043
15685
  }
15044
15686
  var DEFAULTS39 = {
15045
15687
  matcher: "bash|write|edit",
@@ -15099,20 +15741,21 @@ function buildHook(cfg, log) {
15099
15741
  }
15100
15742
  if (cfg.mode === "redact") {
15101
15743
  const redacted = redactInput(input.toolInput);
15102
- if (redacted !== null && typeof redacted === "object" && !Array.isArray(redacted)) {
15744
+ if (redacted.ok && redacted.value !== null && typeof redacted.value === "object" && !Array.isArray(redacted.value)) {
15103
15745
  state44.redactCount += 1;
15104
15746
  log.info(`[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`);
15105
15747
  return {
15106
15748
  decision: "allow",
15107
- modifiedInput: redacted,
15749
+ modifiedInput: redacted.value,
15108
15750
  additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
15109
15751
  };
15110
15752
  }
15111
15753
  state44.blockCount += 1;
15112
15754
  state44.lastBlock = { toolName, matchedTypes: matched, when };
15755
+ 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
15756
  return {
15114
15757
  decision: "block",
15115
- reason: `secret-scanner: cannot safely redact '${toolName}' input (non-object shape); refusing to run.`
15758
+ reason: `secret-scanner: cannot safely redact '${toolName}' because ${detail}; refusing to run.`
15116
15759
  };
15117
15760
  }
15118
15761
  state44.allowCount += 1;
@@ -15130,10 +15773,21 @@ function buildPostHook(cfg, log) {
15130
15773
  const matched = findMatches(result.content);
15131
15774
  if (matched.length === 0) return;
15132
15775
  const toolName = input.toolName ?? "unknown";
15133
- const summary = matched.join(", ");
15776
+ const oversized = matched.includes(TOO_LARGE_MARKER);
15777
+ const credentialMatches = matched.filter((type) => type !== TOO_LARGE_MARKER);
15778
+ if (oversized && credentialMatches.length === 0) {
15779
+ log.warn(
15780
+ `[secret-scanner] POST-TOOL UNSCANNABLE: ${toolName} output exceeds ${MAX_TOTAL_SCAN_LENGTH} characters`
15781
+ );
15782
+ return {
15783
+ additionalContext: `
15784
+ \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.`
15785
+ };
15786
+ }
15787
+ const summary = credentialMatches.join(", ");
15134
15788
  const when = (/* @__PURE__ */ new Date()).toISOString();
15135
15789
  state44.leakCount += 1;
15136
- state44.lastLeak = { toolName, matchedTypes: matched, when };
15790
+ state44.lastLeak = { toolName, matchedTypes: credentialMatches, when };
15137
15791
  log.warn(`[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`);
15138
15792
  return {
15139
15793
  additionalContext: `
@@ -15326,8 +15980,8 @@ var plugin46 = {
15326
15980
  var secret_scanner_default = plugin46;
15327
15981
 
15328
15982
  // 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";
15983
+ import { readdir as readdir2, readFile as readFile12, stat as stat5 } from "node:fs/promises";
15984
+ import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
15331
15985
  var API_VERSION30 = "^0.1.10";
15332
15986
  var state45 = {
15333
15987
  scanCount: 0,
@@ -15337,7 +15991,7 @@ var state45 = {
15337
15991
  skippedCount: 0,
15338
15992
  blockedCount: 0,
15339
15993
  lastResult: null,
15340
- knownHotspots: /* @__PURE__ */ new Set(),
15994
+ knownHotspots: new BoundedSet({ max: 5e3 }),
15341
15995
  hookUnregister: null
15342
15996
  };
15343
15997
  var DEFAULTS40 = {
@@ -15418,7 +16072,7 @@ function isSourceFile2(filePath, extensions) {
15418
16072
  async function scanPath5(inputPath, cfg) {
15419
16073
  const start = Date.now();
15420
16074
  const root = process.cwd();
15421
- const resolved = isAbsolute19(inputPath) ? resolve20(inputPath) : resolve20(root, inputPath);
16075
+ const resolved = isAbsolute20(inputPath) ? resolve21(inputPath) : resolve21(root, inputPath);
15422
16076
  if (!withinProject(inputPath)) {
15423
16077
  return {
15424
16078
  path: inputPath,
@@ -15435,11 +16089,11 @@ async function scanPath5(inputPath, cfg) {
15435
16089
  const scanFile2 = async (filePath) => {
15436
16090
  if (!isSourceFile2(filePath, cfg.scanOnChange)) return;
15437
16091
  try {
15438
- const content = await readFile11(filePath, "utf-8");
16092
+ const content = await readFile12(filePath, "utf-8");
15439
16093
  filesScanned += 1;
15440
16094
  const findings = scanSource(content, maxPerFile);
15441
16095
  for (const f of findings) {
15442
- allFindings.push({ ...f, snippet: `${relative19(root, filePath)}:${f.line}: ${f.snippet}` });
16096
+ allFindings.push({ ...f, snippet: `${relative20(root, filePath)}:${f.line}: ${f.snippet}` });
15443
16097
  if (allFindings.length >= cfg.maxFindings) return;
15444
16098
  }
15445
16099
  } catch {
@@ -15454,7 +16108,7 @@ async function scanPath5(inputPath, cfg) {
15454
16108
  }
15455
16109
  for (const entry of entries) {
15456
16110
  if (allFindings.length >= cfg.maxFindings) return;
15457
- const full = resolve20(dir, entry);
16111
+ const full = resolve21(dir, entry);
15458
16112
  try {
15459
16113
  const st = await stat5(full);
15460
16114
  if (st.isDirectory()) {
@@ -15723,8 +16377,10 @@ var security_hotspot_scanner_default = plugin47;
15723
16377
 
15724
16378
  // src/semantic-search-indexer/index.ts
15725
16379
  import * as fs2 from "node:fs/promises";
15726
- import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
16380
+ import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve22 } from "node:path";
16381
+ import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
15727
16382
  var API_VERSION31 = "^0.1.10";
16383
+ var escapeRegex2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15728
16384
  var state46 = {
15729
16385
  index: null,
15730
16386
  cachedPath: null,
@@ -15768,7 +16424,11 @@ var DEFAULTS41 = {
15768
16424
  ".scss",
15769
16425
  ".html"
15770
16426
  ],
15771
- excludePatterns: ["node_modules", "\\.git", "\\.wrongstack", "\\.temp_files", "coverage", "dist"],
16427
+ excludePatterns: [
16428
+ ...DEFAULT_WALK_IGNORE_DIRS.map(escapeRegex2),
16429
+ "\\.wrongstack",
16430
+ "\\.temp_files"
16431
+ ],
15772
16432
  maxFileBytes: 1e6,
15773
16433
  defaultLimit: 10,
15774
16434
  minTokenLength: 2,
@@ -15798,18 +16458,18 @@ function normalizeSlashes2(p) {
15798
16458
  function withinProject5(p) {
15799
16459
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
15800
16460
  const root = normalizeSlashes2(process.cwd());
15801
- const resolved = normalizeSlashes2(isAbsolute20(p) ? resolve21(p) : resolve21(root, p));
15802
- const rel = normalizeSlashes2(relative20(root, resolved));
16461
+ const resolved = normalizeSlashes2(isAbsolute21(p) ? resolve22(p) : resolve22(root, p));
16462
+ const rel = normalizeSlashes2(relative21(root, resolved));
15803
16463
  if (rel === "" || rel === ".") return true;
15804
16464
  if (rel.startsWith("..")) return false;
15805
- if (isAbsolute20(rel)) return false;
16465
+ if (isAbsolute21(rel)) return false;
15806
16466
  return true;
15807
16467
  }
15808
16468
  function resolveProjectPath8(p) {
15809
16469
  const raw = typeof p === "string" && p.length > 0 ? p : ".";
15810
16470
  if (!withinProject5(raw)) return null;
15811
16471
  const root = normalizeSlashes2(process.cwd());
15812
- return normalizeSlashes2(isAbsolute20(raw) ? resolve21(raw) : resolve21(root, raw));
16472
+ return normalizeSlashes2(isAbsolute21(raw) ? resolve22(raw) : resolve22(root, raw));
15813
16473
  }
15814
16474
  function tokenize(text, minLength) {
15815
16475
  const tokens = [];
@@ -15839,7 +16499,7 @@ function shouldIndexFile(filePath, cfg) {
15839
16499
  var INDEX_BATCH_SIZE = 32;
15840
16500
  var YIELD_EVERY_FILES = 64;
15841
16501
  function yieldEventLoop() {
15842
- return new Promise((resolve27) => setImmediate(resolve27));
16502
+ return new Promise((resolve28) => setImmediate(resolve28));
15843
16503
  }
15844
16504
  function addFileToIndex(relPath, content, size, cfg) {
15845
16505
  if (!state46.index || content.includes("\0")) return;
@@ -15893,8 +16553,8 @@ async function walkDirectory(absPath, cfg, excludes, fileBatch) {
15893
16553
  state46.truncated = true;
15894
16554
  return;
15895
16555
  }
15896
- const absChild = normalizeSlashes2(resolve21(absPath, ent.name));
15897
- const relChild = normalizeSlashes2(relative20(root, absChild));
16556
+ const absChild = normalizeSlashes2(resolve22(absPath, ent.name));
16557
+ const relChild = normalizeSlashes2(relative21(root, absChild));
15898
16558
  if (relChild === "" || relChild === ".") continue;
15899
16559
  if (excludes.some((re) => re.test(relChild))) continue;
15900
16560
  if (ent.isDirectory()) {
@@ -15936,7 +16596,7 @@ async function buildIndex(rootPath, cfg) {
15936
16596
  return;
15937
16597
  }
15938
16598
  if (rootStats.isFile()) {
15939
- const relPath = normalizeSlashes2(relative20(normalizeSlashes2(process.cwd()), rootPath));
16599
+ const relPath = normalizeSlashes2(relative21(normalizeSlashes2(process.cwd()), rootPath));
15940
16600
  await indexFileFromStats(rootPath, relPath === "" ? "." : relPath, rootStats, cfg);
15941
16601
  state46.fileCount = state46.index.files.size;
15942
16602
  } else if (rootStats.isDirectory()) {
@@ -16232,15 +16892,15 @@ var semantic_search_indexer_default = plugin48;
16232
16892
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
16233
16893
  import { toErrorMessage } from "@wrongstack/core/utils";
16234
16894
  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";
16895
+ import { access as access3, readFile as readFile14, readdir as readdir4, writeFile as writeFile4 } from "node:fs/promises";
16896
+ import { isAbsolute as isAbsolute22, join as join5, relative as relative22, resolve as resolve23 } from "node:path";
16237
16897
  var API_VERSION32 = "^0.1.10";
16238
16898
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
16239
16899
  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;
16900
+ const base = resolve23(root);
16901
+ const resolved = isAbsolute22(rawCwd) ? resolve23(rawCwd) : resolve23(base, rawCwd);
16902
+ const rel = relative22(base, resolved);
16903
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute22(rel)) return resolved;
16244
16904
  return null;
16245
16905
  }
16246
16906
  var state47 = {
@@ -16252,7 +16912,7 @@ var state47 = {
16252
16912
  lastBump: null
16253
16913
  };
16254
16914
  function runCommand3(command, args, cwd) {
16255
- return new Promise((resolve27, reject) => {
16915
+ return new Promise((resolve28, reject) => {
16256
16916
  execFile12(command, args, {
16257
16917
  encoding: "utf-8",
16258
16918
  cwd,
@@ -16260,7 +16920,7 @@ function runCommand3(command, args, cwd) {
16260
16920
  windowsHide: true
16261
16921
  }, (err, stdout, stderr) => {
16262
16922
  if (!err) {
16263
- resolve27(stdout.trim());
16923
+ resolve28(stdout.trim());
16264
16924
  return;
16265
16925
  }
16266
16926
  const failure = err;
@@ -16281,21 +16941,21 @@ async function getPackageJson(cwd) {
16281
16941
  const path = cwd ? `${cwd}/package.json` : "package.json";
16282
16942
  try {
16283
16943
  await access3(path);
16284
- return JSON.parse(await readFile13(path, "utf-8"));
16944
+ return JSON.parse(await readFile14(path, "utf-8"));
16285
16945
  } catch {
16286
16946
  return null;
16287
16947
  }
16288
16948
  }
16289
16949
  async function collectManifests(root) {
16290
16950
  const paths = [];
16291
- const rootPkg = join4(root, "package.json");
16951
+ const rootPkg = join5(root, "package.json");
16292
16952
  try {
16293
16953
  await access3(rootPkg);
16294
16954
  paths.push(rootPkg);
16295
16955
  } catch {
16296
16956
  }
16297
16957
  for (const group of ["packages", "apps"]) {
16298
- const groupDir = join4(root, group);
16958
+ const groupDir = join5(root, group);
16299
16959
  let entries;
16300
16960
  try {
16301
16961
  entries = await readdir4(groupDir, { withFileTypes: true });
@@ -16304,7 +16964,7 @@ async function collectManifests(root) {
16304
16964
  }
16305
16965
  for (const entry of entries) {
16306
16966
  if (!entry.isDirectory()) continue;
16307
- const candidate = join4(groupDir, entry.name, "package.json");
16967
+ const candidate = join5(groupDir, entry.name, "package.json");
16308
16968
  try {
16309
16969
  await access3(candidate);
16310
16970
  paths.push(candidate);
@@ -16499,7 +17159,7 @@ var plugin49 = {
16499
17159
  };
16500
17160
  }
16501
17161
  const root = cwd ?? process.cwd();
16502
- const bumpScript = join4(root, "scripts", "bump-version.mjs");
17162
+ const bumpScript = join5(root, "scripts", "bump-version.mjs");
16503
17163
  const changed = await collectManifests(root);
16504
17164
  let hasBumpScript = true;
16505
17165
  try {
@@ -16515,7 +17175,7 @@ var plugin49 = {
16515
17175
  return { ok: false, error: `bump script failed: ${msg}` };
16516
17176
  }
16517
17177
  for (const rel of ["package.json", "package-lock.json", "src/lib/utils.ts", "index.html"]) {
16518
- const p = join4(root, "website", rel);
17178
+ const p = join5(root, "website", rel);
16519
17179
  try {
16520
17180
  await access3(p);
16521
17181
  changed.push(p);
@@ -16523,10 +17183,29 @@ var plugin49 = {
16523
17183
  }
16524
17184
  }
16525
17185
  } else {
17186
+ const pending = [];
16526
17187
  for (const manifest of changed) {
16527
- const pkgData = JSON.parse(await readFile13(manifest, "utf-8"));
17188
+ let pkgData;
17189
+ try {
17190
+ pkgData = JSON.parse(await readFile14(manifest, "utf-8"));
17191
+ } catch (err) {
17192
+ return {
17193
+ ok: false,
17194
+ error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
17195
+ };
17196
+ }
17197
+ if (!pkgData || typeof pkgData !== "object") {
17198
+ return {
17199
+ ok: false,
17200
+ error: `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
17201
+ };
17202
+ }
16528
17203
  pkgData.version = newVersion;
16529
- await writeFile4(manifest, JSON.stringify(pkgData, null, 2) + "\n", "utf-8");
17204
+ pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
17205
+ ` });
17206
+ }
17207
+ for (const { path, contents } of pending) {
17208
+ await writeFile4(path, contents, "utf-8");
16530
17209
  }
16531
17210
  }
16532
17211
  try {
@@ -16864,8 +17543,8 @@ async function readTranscriptTail(transcriptPath, n) {
16864
17543
  if (!transcriptPath || n <= 0) return [];
16865
17544
  let raw;
16866
17545
  try {
16867
- const { readFile: readFile17 } = await import("node:fs/promises");
16868
- raw = await readFile17(transcriptPath, "utf-8");
17546
+ const { readFile: readFile18 } = await import("node:fs/promises");
17547
+ raw = await readFile18(transcriptPath, "utf-8");
16869
17548
  } catch {
16870
17549
  return [];
16871
17550
  }
@@ -17206,15 +17885,15 @@ var session_recap_default = plugin50;
17206
17885
  // src/shell-check/index.ts
17207
17886
  import { execFile as execFile13 } from "node:child_process";
17208
17887
  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";
17888
+ import { isAbsolute as isAbsolute23, join as join6, relative as relative23, resolve as resolve24 } from "node:path";
17210
17889
  var API_VERSION33 = "^0.1.10";
17211
17890
  function withinProject6(p) {
17212
17891
  const root = process.cwd();
17213
- const resolved = isAbsolute22(p) ? resolve23(p) : resolve23(root, p);
17214
- const rel = relative22(root, resolved);
17892
+ const resolved = isAbsolute23(p) ? resolve24(p) : resolve24(root, p);
17893
+ const rel = relative23(root, resolved);
17215
17894
  if (rel === "" || rel === ".") return true;
17216
17895
  if (rel.startsWith("..")) return false;
17217
- if (isAbsolute22(rel)) return false;
17896
+ if (isAbsolute23(rel)) return false;
17218
17897
  return true;
17219
17898
  }
17220
17899
  var MAX_PATH_LEN = 4096;
@@ -17307,7 +17986,7 @@ async function findShellFiles(dir, pattern) {
17307
17986
  return results;
17308
17987
  }
17309
17988
  for (const entry of entries) {
17310
- const full = join5(dir, entry.name);
17989
+ const full = join6(dir, entry.name);
17311
17990
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
17312
17991
  results.push(...await findShellFiles(full, pattern));
17313
17992
  } else if (entry.isFile() && (entry.name.endsWith(".sh") || entry.name === "Dockerfile")) {
@@ -17506,8 +18185,8 @@ var plugin51 = {
17506
18185
  var shell_check_default = plugin51;
17507
18186
 
17508
18187
  // 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";
18188
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
18189
+ import { extname as extname6, isAbsolute as isAbsolute24, relative as relative24, resolve as resolve25 } from "node:path";
17511
18190
  var API_VERSION34 = "^0.1.10";
17512
18191
  var state50 = {
17513
18192
  renameCount: 0,
@@ -17529,24 +18208,24 @@ function readConfig46(raw) {
17529
18208
  function withinProject7(p) {
17530
18209
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
17531
18210
  const root = process.cwd();
17532
- const resolved = isAbsolute23(p) ? resolve24(p) : resolve24(root, p);
17533
- const rel = relative23(root, resolved);
18211
+ const resolved = isAbsolute24(p) ? resolve25(p) : resolve25(root, p);
18212
+ const rel = relative24(root, resolved);
17534
18213
  if (rel === "" || rel === ".") return true;
17535
18214
  if (rel.startsWith("..")) return false;
17536
- if (isAbsolute23(rel)) return false;
18215
+ if (isAbsolute24(rel)) return false;
17537
18216
  return true;
17538
18217
  }
17539
18218
  function toPosix7(p) {
17540
18219
  return p.replace(/\\/g, "/");
17541
18220
  }
17542
18221
  function relativePath7(p) {
17543
- return toPosix7(relative23(process.cwd(), p));
18222
+ return toPosix7(relative24(process.cwd(), p));
17544
18223
  }
17545
- function escapeRegex2(s) {
18224
+ function escapeRegex3(s) {
17546
18225
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17547
18226
  }
17548
18227
  function renameInContent(content, oldName, newName) {
17549
- const re = new RegExp(`\\b${escapeRegex2(oldName)}\\b`, "g");
18228
+ const re = new RegExp(`\\b${escapeRegex3(oldName)}\\b`, "g");
17550
18229
  let replacements = 0;
17551
18230
  const preview = content.replace(re, () => {
17552
18231
  replacements++;
@@ -17611,14 +18290,14 @@ var plugin52 = {
17611
18290
  if (!withinProject7(rawPath)) {
17612
18291
  return { ok: false, error: "path is outside the project root" };
17613
18292
  }
17614
- const ext = extname5(rawPath).toLowerCase();
18293
+ const ext = extname6(rawPath).toLowerCase();
17615
18294
  if (!cfg.extensions.includes(ext)) {
17616
18295
  return { ok: false, error: `extension ${ext} is not allowed for rename` };
17617
18296
  }
17618
- const resolved = resolve24(process.cwd(), rawPath);
18297
+ const resolved = resolve25(process.cwd(), rawPath);
17619
18298
  let content;
17620
18299
  try {
17621
- content = readFileSync12(resolved, "utf-8");
18300
+ content = readFileSync13(resolved, "utf-8");
17622
18301
  } catch (err) {
17623
18302
  state50.errorCount += 1;
17624
18303
  return { ok: false, error: String(err) };
@@ -17628,7 +18307,7 @@ var plugin52 = {
17628
18307
  state50.replacementCount += replacements;
17629
18308
  if (input.apply) {
17630
18309
  try {
17631
- writeFileSync4(resolved, preview, "utf-8");
18310
+ writeFileSync2(resolved, preview, "utf-8");
17632
18311
  } catch (err) {
17633
18312
  state50.errorCount += 1;
17634
18313
  return { ok: false, error: String(err) };
@@ -18159,8 +18838,8 @@ ${lines}${overflowNote}`
18159
18838
  var spec_linker_default = plugin53;
18160
18839
 
18161
18840
  // 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";
18841
+ import { readFile as readFile16, writeFile as writeFile5 } from "node:fs/promises";
18842
+ import { isAbsolute as isAbsolute25 } from "node:path";
18164
18843
  var API_VERSION35 = "^0.1.10";
18165
18844
  var templates = /* @__PURE__ */ new Map();
18166
18845
  function expandTemplate(template, variables) {
@@ -18207,9 +18886,12 @@ function renderTemplateRaw(template, variables) {
18207
18886
  return result;
18208
18887
  }
18209
18888
  function validateRelativeTemplatePath(field, value) {
18210
- if (isAbsolute24(value) || value.split(/[\\/]+/).includes("..")) {
18889
+ if (isAbsolute25(value) || value.split(/[\\/]+/).includes("..")) {
18211
18890
  return `${field} must be a relative path without ".." components`;
18212
18891
  }
18892
+ if (!withinProject(value)) {
18893
+ return `${field} must resolve inside the project directory`;
18894
+ }
18213
18895
  return null;
18214
18896
  }
18215
18897
  var plugin54 = {
@@ -18333,7 +19015,7 @@ var plugin54 = {
18333
19015
  }
18334
19016
  let content;
18335
19017
  try {
18336
- content = await readFile15(template_path, "utf-8");
19018
+ content = await readFile16(template_path, "utf-8");
18337
19019
  } catch (err) {
18338
19020
  return { ok: false, error: `Could not read template file: ${err}` };
18339
19021
  }
@@ -18465,7 +19147,7 @@ var plugin54 = {
18465
19147
  var template_engine_default = plugin54;
18466
19148
 
18467
19149
  // src/test-coverage-gate/index.ts
18468
- import { readFileSync as readFileSync13 } from "node:fs";
19150
+ import { readFileSync as readFileSync14 } from "node:fs";
18469
19151
  var API_VERSION36 = "^0.1.10";
18470
19152
  var state52 = {
18471
19153
  invocationCount: 0,
@@ -18506,7 +19188,7 @@ function readConfig48(raw) {
18506
19188
  }
18507
19189
  function readCoverageSummary(coveragePath) {
18508
19190
  try {
18509
- const raw = readFileSync13(coveragePath, "utf-8");
19191
+ const raw = readFileSync14(coveragePath, "utf-8");
18510
19192
  return JSON.parse(raw);
18511
19193
  } catch {
18512
19194
  return null;
@@ -18722,9 +19404,9 @@ var test_coverage_gate_default = plugin55;
18722
19404
 
18723
19405
  // src/test-flake-detector/index.ts
18724
19406
  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";
19407
+ import { readFileSync as readFileSync15 } from "node:fs";
19408
+ import { createRequire as createRequire3 } from "node:module";
19409
+ import { dirname as dirname8, isAbsolute as isAbsolute26, relative as relative25, resolve as resolve26 } from "node:path";
18728
19410
  var API_VERSION37 = "^0.1.10";
18729
19411
  var state53 = {
18730
19412
  invocationCount: 0,
@@ -18795,15 +19477,15 @@ var ALLOWED_RUNNER_FLAGS = /* @__PURE__ */ new Set([
18795
19477
  ]);
18796
19478
  function withinProject8(p) {
18797
19479
  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);
19480
+ const root = resolve26(process.cwd());
19481
+ const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
19482
+ const rel = relative25(root, resolved);
19483
+ return rel === "" || !rel.startsWith("..") && !isAbsolute26(rel);
18802
19484
  }
18803
- function isInside2(parent, child) {
19485
+ function isInside3(parent, child) {
18804
19486
  if (parent === child) return true;
18805
- const rel = relative24(parent, child);
18806
- return rel !== "" && !rel.startsWith("..") && !isAbsolute25(rel);
19487
+ const rel = relative25(parent, child);
19488
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute26(rel);
18807
19489
  }
18808
19490
  function tokenizeCommand(command) {
18809
19491
  const trimmed = command.trim();
@@ -18840,14 +19522,14 @@ function resolveTestCommand(baseCommand, testPattern) {
18840
19522
  if (runnerArgs.some((arg) => !ALLOWED_RUNNER_FLAGS.has(arg))) return null;
18841
19523
  let resolvedEntry;
18842
19524
  try {
18843
- const requireFromProject = createRequire2(resolve25(process.cwd(), "package.json"));
19525
+ const requireFromProject = createRequire3(resolve26(process.cwd(), "package.json"));
18844
19526
  const packagePath = requireFromProject.resolve(`${runner}/package.json`);
18845
- const packageJson = JSON.parse(readFileSync14(packagePath, "utf8"));
19527
+ const packageJson = JSON.parse(readFileSync15(packagePath, "utf8"));
18846
19528
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[runner] ?? Object.values(packageJson.bin ?? {})[0];
18847
19529
  if (!relativeBin) return null;
18848
- const packageDir = dirname7(packagePath);
18849
- const candidate = resolve25(packageDir, relativeBin);
18850
- if (isAbsolute25(relativeBin) || !isInside2(packageDir, candidate)) {
19530
+ const packageDir = dirname8(packagePath);
19531
+ const candidate = resolve26(packageDir, relativeBin);
19532
+ if (isAbsolute26(relativeBin) || !isInside3(packageDir, candidate)) {
18851
19533
  return null;
18852
19534
  }
18853
19535
  resolvedEntry = candidate;
@@ -19089,8 +19771,8 @@ var plugin56 = {
19089
19771
  var test_flake_detector_default = plugin56;
19090
19772
 
19091
19773
  // 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";
19774
+ import { readFileSync as readFileSync16 } from "node:fs";
19775
+ import { isAbsolute as isAbsolute27, relative as relative26, resolve as resolve27 } from "node:path";
19094
19776
  var API_VERSION38 = "^0.1.10";
19095
19777
  var state54 = {
19096
19778
  generateCount: 0,
@@ -19125,18 +19807,18 @@ function readConfig50(raw) {
19125
19807
  function withinProject9(p) {
19126
19808
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
19127
19809
  const root = process.cwd();
19128
- const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
19129
- const rel = relative25(root, resolved);
19810
+ const resolved = isAbsolute27(p) ? resolve27(p) : resolve27(root, p);
19811
+ const rel = relative26(root, resolved);
19130
19812
  if (rel === "" || rel === ".") return true;
19131
19813
  if (rel.startsWith("..")) return false;
19132
- if (isAbsolute26(rel)) return false;
19814
+ if (isAbsolute27(rel)) return false;
19133
19815
  return true;
19134
19816
  }
19135
19817
  function toPosix8(p) {
19136
19818
  return p.replace(/\\/g, "/");
19137
19819
  }
19138
19820
  function relativePath8(p) {
19139
- return toPosix8(relative25(process.cwd(), p));
19821
+ return toPosix8(relative26(process.cwd(), p));
19140
19822
  }
19141
19823
  function detectExports(content) {
19142
19824
  const exports = [];
@@ -19230,7 +19912,7 @@ function generateTestContent(sourcePath, sourceModule, detected, cfg) {
19230
19912
  return lines.join("\n");
19231
19913
  }
19232
19914
  function generateForFile(filePath, cfg) {
19233
- const sourceContent = readFileSync15(filePath, "utf-8");
19915
+ const sourceContent = readFileSync16(filePath, "utf-8");
19234
19916
  const detected = detectExports(sourceContent);
19235
19917
  const sourceFile = relativePath8(filePath);
19236
19918
  const sourceParts = sourceFile.split("/");
@@ -19349,7 +20031,7 @@ var plugin57 = {
19349
20031
  if (!withinProject9(rawPath)) {
19350
20032
  return { ok: false, error: "path is outside the project root" };
19351
20033
  }
19352
- const resolved = resolve26(process.cwd(), rawPath);
20034
+ const resolved = resolve27(process.cwd(), rawPath);
19353
20035
  state54.generateCount += 1;
19354
20036
  let result;
19355
20037
  try {
@@ -19435,7 +20117,17 @@ var test_generator_default = plugin57;
19435
20117
  // src/test-runner-gate/index.ts
19436
20118
  import { execFile as execFile15 } from "node:child_process";
19437
20119
  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";
20120
+ import { basename as basename6, dirname as dirname9, isAbsolute as isAbsolute28, join as join7 } from "node:path";
20121
+ import { buildWin32CmdShimInvocation as buildWin32CmdShimInvocation2, resolveWin32Command as resolveWin32Command2 } from "@wrongstack/tools/win32";
20122
+ function resolveExec(command, args) {
20123
+ const resolved = resolveWin32Command2(command);
20124
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
20125
+ if (needsShell) {
20126
+ const shim = buildWin32CmdShimInvocation2(resolved, args);
20127
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
20128
+ }
20129
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
20130
+ }
19439
20131
  var API_VERSION39 = "^0.1.10";
19440
20132
  var state55 = {
19441
20133
  invocationCount: 0,
@@ -19500,7 +20192,7 @@ function pathContentHash(content) {
19500
20192
  }
19501
20193
  return h >>> 0;
19502
20194
  }
19503
- var lastPassedHash = /* @__PURE__ */ new Map();
20195
+ var lastPassedHash = new BoundedMap({ max: 1024 });
19504
20196
  var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
19505
20197
  "npx",
19506
20198
  "pnpm",
@@ -19518,7 +20210,7 @@ var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
19518
20210
  function resolveTestFiles(sourcePath, patterns) {
19519
20211
  const name = basename6(sourcePath).replace(/\.[^.]+$/, "");
19520
20212
  const pathNoExt = sourcePath.replace(/\.[^.]+$/, "");
19521
- const dir = dirname8(sourcePath);
20213
+ const dir = dirname9(sourcePath);
19522
20214
  const candidates = [];
19523
20215
  for (const pattern of patterns) {
19524
20216
  const candidate = pattern.replace(/\{name\}/g, name).replace(/\{path\}/g, pathNoExt).replace(/\{dir\}/g, dir);
@@ -19527,7 +20219,7 @@ function resolveTestFiles(sourcePath, patterns) {
19527
20219
  candidates.push(candidate);
19528
20220
  } else {
19529
20221
  candidates.push(candidate);
19530
- candidates.push(join6(dir, candidate));
20222
+ candidates.push(join7(dir, candidate));
19531
20223
  }
19532
20224
  }
19533
20225
  }
@@ -19554,12 +20246,15 @@ async function detectRunner(requested) {
19554
20246
  const match = candidates.find((c) => c.name === requested);
19555
20247
  if (!match) return null;
19556
20248
  try {
19557
- await new Promise((resolve27, reject) => {
19558
- execFile15("npx", [`${match.name}`, "--version"], {
20249
+ await new Promise((resolve28, reject) => {
20250
+ const ex = resolveExec("npx", [`${match.name}`, "--version"]);
20251
+ execFile15(ex.cmd, ex.args, {
19559
20252
  encoding: "utf-8",
19560
20253
  timeout: 5e3,
19561
- cwd: process.cwd()
19562
- }, (err) => err ? reject(err) : resolve27());
20254
+ cwd: process.cwd(),
20255
+ windowsHide: true,
20256
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20257
+ }, (err) => err ? reject(err) : resolve28());
19563
20258
  });
19564
20259
  return match;
19565
20260
  } catch {
@@ -19568,12 +20263,15 @@ async function detectRunner(requested) {
19568
20263
  }
19569
20264
  for (const candidate of candidates) {
19570
20265
  try {
19571
- await new Promise((resolve27, reject) => {
19572
- execFile15("npx", [`${candidate.name}`, "--version"], {
20266
+ await new Promise((resolve28, reject) => {
20267
+ const ex = resolveExec("npx", [`${candidate.name}`, "--version"]);
20268
+ execFile15(ex.cmd, ex.args, {
19573
20269
  encoding: "utf-8",
19574
20270
  timeout: 5e3,
19575
- cwd: process.cwd()
19576
- }, (err) => err ? reject(err) : resolve27());
20271
+ cwd: process.cwd(),
20272
+ windowsHide: true,
20273
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20274
+ }, (err) => err ? reject(err) : resolve28());
19577
20275
  });
19578
20276
  return candidate;
19579
20277
  } catch {
@@ -19588,7 +20286,7 @@ function resolveAllowedCommand2(customCommand) {
19588
20286
  if (ALLOWED_COMMAND_TOKENS.has(head)) {
19589
20287
  return { cmd: head, args: tokens.slice(1) };
19590
20288
  }
19591
- if (isAbsolute27(head)) {
20289
+ if (isAbsolute28(head)) {
19592
20290
  if (!withinProject(head)) return null;
19593
20291
  const base = basename6(head);
19594
20292
  if (ALLOWED_COMMAND_TOKENS.has(base)) {
@@ -19619,14 +20317,21 @@ async function runTests(testFile, runner, customCommand, timeoutMs) {
19619
20317
  let stdout = "";
19620
20318
  try {
19621
20319
  const { stdout: out } = await new Promise(
19622
- (resolve27, reject) => {
20320
+ (resolve28, reject) => {
20321
+ const ex = resolveExec(cmd, fullArgs);
19623
20322
  execFile15(
19624
- cmd,
19625
- fullArgs,
19626
- { encoding: "utf-8", timeout: timeoutMs, cwd: process.cwd() },
20323
+ ex.cmd,
20324
+ ex.args,
20325
+ {
20326
+ encoding: "utf-8",
20327
+ timeout: timeoutMs,
20328
+ cwd: process.cwd(),
20329
+ windowsHide: true,
20330
+ ...ex.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
20331
+ },
19627
20332
  (err, out2, stderr) => {
19628
20333
  if (err) reject(Object.assign(err, { stdout: out2, stderr }));
19629
- else resolve27({ stdout: out2, stderr });
20334
+ else resolve28({ stdout: out2, stderr });
19630
20335
  }
19631
20336
  );
19632
20337
  }
@@ -20035,7 +20740,7 @@ var plugin59 = {
20035
20740
  0,
20036
20741
  200
20037
20742
  );
20038
- const body = JSON.stringify(payload, null, 2);
20743
+ const body = safeJsonStringify(payload, 2);
20039
20744
  const sendInput = {
20040
20745
  from: `plugin:todo-listener`,
20041
20746
  to: "*",
@@ -20139,6 +20844,7 @@ var todo_listener_default = plugin59;
20139
20844
  // src/todo-tracker/index.ts
20140
20845
  import * as fsp from "node:fs/promises";
20141
20846
  import { randomUUID } from "node:crypto";
20847
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core/utils";
20142
20848
  function deriveFilePath(api) {
20143
20849
  const raw = api.config.extensions?.["todo-tracker"];
20144
20850
  const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
@@ -20168,10 +20874,8 @@ async function loadFile(filePath) {
20168
20874
  }
20169
20875
  }
20170
20876
  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);
20877
+ await ensureDir3(filePath.replace(/[/\\][^/\\]+$/, ""));
20878
+ await atomicWrite3(filePath, JSON.stringify(file, null, 2), { mode: 384 });
20175
20879
  }
20176
20880
  var state57 = {
20177
20881
  filePath: null,
@@ -20890,7 +21594,21 @@ function estimateRequestTokens(request, charsPerToken) {
20890
21594
  walk(request["messages"]);
20891
21595
  return Math.ceil(chars / charsPerToken);
20892
21596
  }
20893
- var sleep = (ms) => new Promise((resolve27) => setTimeout(resolve27, ms));
21597
+ function sleep(ms, signal) {
21598
+ if (signal?.aborted) return Promise.resolve();
21599
+ return new Promise((resolve28) => {
21600
+ const timer = setTimeout(() => {
21601
+ signal?.removeEventListener("abort", onAbort);
21602
+ resolve28();
21603
+ }, ms);
21604
+ timer.unref?.();
21605
+ function onAbort() {
21606
+ clearTimeout(timer);
21607
+ resolve28();
21608
+ }
21609
+ signal?.addEventListener("abort", onAbort, { once: true });
21610
+ });
21611
+ }
20894
21612
  var plugin62 = {
20895
21613
  name: "token-throttle",
20896
21614
  version: "0.1.0",
@@ -20949,6 +21667,7 @@ var plugin62 = {
20949
21667
  name: "token-throttle",
20950
21668
  owner: "token-throttle",
20951
21669
  async wrapProviderRunner(_ctx, request, inner) {
21670
+ const signal = _ctx?.signal;
20952
21671
  const req = request ?? {};
20953
21672
  state59.invocations += 1;
20954
21673
  const now = Date.now();
@@ -20962,7 +21681,7 @@ var plugin62 = {
20962
21681
  api.metrics.counter("throttled");
20963
21682
  api.metrics.histogram("delay_ms", delay);
20964
21683
  api.log.info("token-throttle: delaying provider call", { delayMs: delay, projected });
20965
- await sleep(delay);
21684
+ await sleep(delay, signal);
20966
21685
  }
20967
21686
  const response = await inner(_ctx, request);
20968
21687
  const used = (response?.usage?.input ?? 0) + (response?.usage?.output ?? 0) || projected;
@@ -21110,7 +21829,9 @@ async function runTypeCheck(cfg) {
21110
21829
  if (!resolved) return null;
21111
21830
  argv = [resolved.cmd, ...resolved.args];
21112
21831
  } else {
21113
- argv = ["npx", "tsc", "--noEmit", "--incremental"];
21832
+ const tscFlags = ["--noEmit", "--incremental"];
21833
+ const localTsc = resolveNodeBin("typescript", "tsc", process.cwd(), tscFlags);
21834
+ argv = localTsc ? [localTsc.cmd, ...localTsc.args] : ["npx", "tsc", ...tscFlags];
21114
21835
  if (tsConfig && tsConfig !== "tsconfig.json") {
21115
21836
  argv = [...argv, "-p", tsConfig];
21116
21837
  }