@wrongstack/plugins 0.313.0 → 0.316.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 (48) hide show
  1. package/dist/accessibility-auditor.js +3 -2
  2. package/dist/agent-handoff.js +13 -3
  3. package/dist/auto-doc.js +5 -2
  4. package/dist/auto-i18n-extractor.js +2 -1
  5. package/dist/branch-guard.js +1 -1
  6. package/dist/changelog-writer.js +16 -4
  7. package/dist/checkpoint.js +2 -1
  8. package/dist/commit-validator.js +2 -2
  9. package/dist/config-validator.js +26 -1
  10. package/dist/cost-tracker.js +2 -1
  11. package/dist/cron.js +1 -1
  12. package/dist/dependency-vulnerability-gate.js +12 -0
  13. package/dist/diff-summary.js +1 -1
  14. package/dist/doc-sync-guard.js +8 -1
  15. package/dist/duplicate-code-detector.js +4 -3
  16. package/dist/error-lens.js +3 -3
  17. package/dist/git-autocommit.js +5 -2
  18. package/dist/gitignore-guard.js +6 -2
  19. package/dist/import-organizer.js +10 -4
  20. package/dist/index.js +304 -186
  21. package/dist/interface-contract-guard.js +12 -3
  22. package/dist/knowledge-graph.js +5 -1
  23. package/dist/llm-cache.js +3 -2
  24. package/dist/loop-breaker.js +1 -1
  25. package/dist/migration-planner.js +0 -2
  26. package/dist/model-router.js +2 -0
  27. package/dist/notify-hub.js +5 -1
  28. package/dist/path-guard.js +2 -1
  29. package/dist/performance-regression-gate.js +2 -0
  30. package/dist/plugin-stack-observer.js +1 -0
  31. package/dist/pr-drafter.js +2 -2
  32. package/dist/process-guard.js +1 -2
  33. package/dist/prompt-firewall.js +3 -2
  34. package/dist/refactor-suggester.js +11 -0
  35. package/dist/security-hotspot-scanner.js +5 -5
  36. package/dist/semver-bump.js +3 -1
  37. package/dist/smart-rename.js +2 -2
  38. package/dist/spec-linker.js +1 -1
  39. package/dist/template-engine.js +4 -4
  40. package/dist/test-coverage-gate.js +5 -4
  41. package/dist/test-flake-detector.js +22 -8
  42. package/dist/test-generator.js +2 -2
  43. package/dist/todo-listener.js +3 -2
  44. package/dist/todo-tracker.js +2 -1
  45. package/package.json +5 -5
  46. package/dist/runtime/bounded-map.d.ts +0 -3
  47. package/dist/runtime/local-bin.d.ts +0 -3
  48. package/dist/runtime/safe-json.d.ts +0 -3
@@ -300,8 +300,9 @@ var plugin = {
300
300
  if (!cfg.enabled || !cfg.onWriteEdit) return;
301
301
  if (input.toolResult?.isError) return;
302
302
  const inp = input.toolInput ?? {};
303
- const sourcePath = inp["path"];
304
- if (!sourcePath || typeof sourcePath !== "string") return;
303
+ const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"];
304
+ const sourcePath = typeof rawPath === "string" ? rawPath : void 0;
305
+ if (!sourcePath) return;
305
306
  if (!(0, runtime_exports.withinProject)(sourcePath)) return;
306
307
  const exts = normalizeExtensions(cfg.includeExtensions);
307
308
  if (!(0, runtime_exports.matchesExtension)(sourcePath, exts)) return;
@@ -95,12 +95,22 @@ function buildBody(payload, cfg) {
95
95
  return truncate(lines.join("\n"), cfg.maxBodyChars);
96
96
  }
97
97
  async function sendHandoff(cfg, mailbox, payload) {
98
- const body = buildBody(payload, cfg);
99
- const hash = hashString(body);
98
+ const hash = hashString(
99
+ (0, runtime_exports.safeJsonStringify)({
100
+ agentId: payload.agentId,
101
+ agentName: payload.agentName,
102
+ task: payload.task,
103
+ status: payload.status,
104
+ summary: payload.summary,
105
+ result: payload.result,
106
+ todos: payload.todos
107
+ })
108
+ );
100
109
  if (hash === state.lastPayloadHash) {
101
110
  state.skippedCount += 1;
102
111
  return;
103
112
  }
113
+ const body = buildBody(payload, cfg);
104
114
  const subject = `${cfg.subjectPrefix}${payload.agentName ?? payload.agentId ?? "subagent"} \u2014 ${payload.status ?? "done"}`.slice(
105
115
  0,
106
116
  200
@@ -115,7 +125,7 @@ async function sendHandoff(cfg, mailbox, payload) {
115
125
  };
116
126
  const result = await mailbox.send(sendInput);
117
127
  state.sentCount += 1;
118
- state.lastMessageId = result.id ?? null;
128
+ state.lastMessageId = result?.id ?? null;
119
129
  state.lastPayloadHash = hash;
120
130
  }
121
131
  var plugin = {
package/dist/auto-doc.js CHANGED
@@ -146,8 +146,11 @@ function needsDocComment(content, entity) {
146
146
  const lines = content.split("\n");
147
147
  const lineIdx = entity.startLine - 1;
148
148
  if (lineIdx < 1) return true;
149
- const prevLine = lines[lineIdx - 1] ?? "";
150
- return !/^\s*\/\*\*\s*$/.test(prevLine.trim());
149
+ const prevLine = lines[lineIdx - 1]?.trim() ?? "";
150
+ if (/^\s*\*\/\s*$/.test(prevLine) || /^\s*\/\*\*.*?\*\/\s*$/.test(prevLine) || /^\s*\/\*\*/.test(prevLine)) {
151
+ return false;
152
+ }
153
+ return true;
151
154
  }
152
155
  function injectDocComment(content, entity, doc) {
153
156
  const lines = content.split("\n");
@@ -81,7 +81,7 @@ function looksLikeUserText(value, minLength) {
81
81
  function isExcludedContext(line, quoteIndex, excludeAttributes) {
82
82
  const before = line.slice(0, quoteIndex);
83
83
  for (const attr of excludeAttributes) {
84
- const re = new RegExp(`\\b${attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*["'{]?$`);
84
+ const re = new RegExp(`\\b${attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*(?:\\{\\s*)?["']?$`);
85
85
  if (re.test(before)) return true;
86
86
  }
87
87
  return false;
@@ -92,6 +92,7 @@ function extractStrings(content, cfg) {
92
92
  const stringRegex = /(["'`])((?:\\.|(?!\1)[^\\])*?)\1/g;
93
93
  for (let i = 0; i < lines.length; i += 1) {
94
94
  const line = lines[i];
95
+ if (/^\s*(?:import\b|export\b.*?from\b|const\s+.*=\s*require\()/.test(line)) continue;
95
96
  stringRegex.lastIndex = 0;
96
97
  for (const match of line.matchAll(stringRegex)) {
97
98
  const value = match[2] ?? "";
@@ -72,7 +72,7 @@ function hasDisabledPluginEntry(raw) {
72
72
  return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
73
73
  });
74
74
  }
75
- var branchCache = new runtime_exports.BoundedMap({ max: 64, ttlMs: 3e4 });
75
+ var branchCache = new runtime_exports.BoundedMap({ max: 64, ttlMs: 2e3 });
76
76
  function runGit(args, cwd, signal) {
77
77
  return new Promise((resolve, reject) => {
78
78
  execFile(
@@ -93,10 +93,10 @@ function commitToEntry(subject) {
93
93
  }
94
94
  function commitSubjectFromCommand(command) {
95
95
  if (!/\bgit\s+commit\b/.test(command)) return null;
96
- const m = /-m\s+(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(command);
97
- const subject = m?.[1] ?? m?.[2] ?? m?.[3] ?? null;
98
- if (!subject) return null;
99
- return subject.split("\n")[0]?.trim() || null;
96
+ const m = /(?:-m|--message)\s+(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|(\S+))/.exec(command);
97
+ const rawSubject = m?.[1] ? m[1].replace(/\\"/g, '"') : m?.[2] ?? m?.[3] ?? null;
98
+ if (!rawSubject) return null;
99
+ return rawSubject.split("\n")[0]?.trim() || null;
100
100
  }
101
101
  var CHANGELOG_HEADER = `# Changelog
102
102
 
@@ -235,6 +235,18 @@ var plugin = {
235
235
  if (typeof raw === "string" && raw) state.filesTouched.add(raw);
236
236
  return;
237
237
  }
238
+ if (toolName === "git_autocommit") {
239
+ const type = typeof input["type"] === "string" ? input["type"] : "";
240
+ const msg = typeof input["message"] === "string" ? input["message"] : "";
241
+ const subject = msg ? type && !msg.startsWith(type) ? `${type}: ${msg}` : msg : null;
242
+ if (subject) {
243
+ state.commitsSeen += 1;
244
+ api.metrics.counter("commits_seen");
245
+ const { section, text } = commitToEntry(subject);
246
+ addEntry({ section, text, origin: "commit", when: (/* @__PURE__ */ new Date()).toISOString() });
247
+ }
248
+ return;
249
+ }
238
250
  if (toolName === "bash" || toolName === "exec") {
239
251
  const command = typeof input["command"] === "string" ? input["command"] : "";
240
252
  const subject = command ? commitSubjectFromCommand(command) : null;
@@ -302,7 +302,8 @@ var plugin = {
302
302
  error: input.id ? `no snapshot with id "${input.id}"` : "no snapshots captured yet"
303
303
  };
304
304
  }
305
- const targets = input.path ? snapshot.files.filter((f) => f.path === input.path) : snapshot.files;
305
+ const targetPath = input.path ? resolveProjectPath(input.path) ?? input.path : null;
306
+ const targets = targetPath ? snapshot.files.filter((f) => f.path === targetPath || f.path === input.path) : snapshot.files;
306
307
  if (targets.length === 0) {
307
308
  return { ok: false, error: `snapshot ${snapshot.id} has no entry for "${input.path}"` };
308
309
  }
@@ -152,7 +152,7 @@ function extractMessageFromBash(command) {
152
152
  if (value !== void 0) parts.push(value);
153
153
  }
154
154
  if (parts.length === 0) return null;
155
- return parts.join("\n");
155
+ return parts.join("\n\n");
156
156
  }
157
157
  var plugin = {
158
158
  name: "commit-validator",
@@ -290,7 +290,7 @@ Examples:
290
290
  };
291
291
  }
292
292
  let baseContext = `
293
- ?? commit-validator: commit message has ${parsed.errors.length} issue(s):
293
+ \u26A0\uFE0F commit-validator: commit message has ${parsed.errors.length} issue(s):
294
294
  ${errorList}
295
295
  Expected: <type>[(scope)][!]: <description>`;
296
296
  if (cfg.suggestFix && api.llm) {
@@ -90,9 +90,34 @@ function stripJsonc(text) {
90
90
  i += 1;
91
91
  continue;
92
92
  }
93
+ if (ch === ",") {
94
+ let j = i + 1;
95
+ let isTrailing = false;
96
+ while (j < text.length) {
97
+ const c = text[j];
98
+ if (c === " " || c === " " || c === "\r" || c === "\n") {
99
+ j++;
100
+ } else if (c === "/" && text[j + 1] === "/") {
101
+ j += 2;
102
+ while (j < text.length && text[j] !== "\n") j++;
103
+ } else if (c === "/" && text[j + 1] === "*") {
104
+ j += 2;
105
+ while (j < text.length && !(text[j] === "*" && text[j + 1] === "/")) j++;
106
+ j += 2;
107
+ } else if (c === "}" || c === "]") {
108
+ isTrailing = true;
109
+ break;
110
+ } else {
111
+ break;
112
+ }
113
+ }
114
+ if (isTrailing) {
115
+ continue;
116
+ }
117
+ }
93
118
  out += ch;
94
119
  }
95
- return out.replace(/,\s*([}\]])/g, "$1");
120
+ return out;
96
121
  }
97
122
  function positionToLineCol(text, pos) {
98
123
  const upTo = text.slice(0, pos);
@@ -56,7 +56,8 @@ function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
56
56
  key = model.toLowerCase();
57
57
  modelKeyCache.set(model, key);
58
58
  }
59
- const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
59
+ const unnamespaced = key.includes("/") ? key.split("/").pop() : key;
60
+ const pricing = pricingOverrides[key] ?? pricingOverrides[unnamespaced] ?? bundledFromRegistry[key] ?? bundledFromRegistry[unnamespaced] ?? PRICING[key] ?? PRICING[unnamespaced] ?? DEFAULT_PRICING;
60
61
  const inputCost = freshTokens / 1e6 * pricing.input + cachedTokens / 1e6 * (pricing.cacheRead ?? pricing.input);
61
62
  const outputCost = completionTokens / 1e6 * pricing.output;
62
63
  return inputCost + outputCost;
package/dist/cron.js CHANGED
@@ -8,7 +8,7 @@ var state = {
8
8
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
9
9
  };
10
10
  function formatNextRun(intervalMs) {
11
- const ms = Number.isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
11
+ const ms = Number.isNaN(intervalMs) || !Number.isFinite(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
12
12
  return new Date(Date.now() + ms).toISOString();
13
13
  }
14
14
  function buildSnapshot(s, maxConcurrent) {
@@ -464,6 +464,18 @@ function parseAuditJson(jsonString) {
464
464
  }
465
465
  }
466
466
  }
467
+ const advisories = parsed["advisories"];
468
+ if (advisories && typeof advisories === "object" && !Array.isArray(advisories)) {
469
+ recognizedShape = true;
470
+ for (const entry of Object.values(advisories)) {
471
+ if (entry && typeof entry === "object") {
472
+ const severity = entry["severity"];
473
+ if (typeof severity === "string") {
474
+ collectSeverities(out, severity);
475
+ }
476
+ }
477
+ }
478
+ }
467
479
  if (out.total === 0) {
468
480
  const metadata = parsed["metadata"];
469
481
  const metaVulns = metadata && typeof metadata === "object" ? metadata["vulnerabilities"] : void 0;
@@ -197,7 +197,7 @@ var plugin = {
197
197
  state.fallbackCount += 1;
198
198
  return;
199
199
  }
200
- const toolInputForHash = toolName === "edit" ? inp["new_string"] ?? "" : toolName === "write" ? inp["content"] ?? "" : "";
200
+ const toolInputForHash = toolName === "edit" ? `${String(inp["old_string"] ?? "")}:::${String(inp["new_string"] ?? "")}` : toolName === "write" ? inp["content"] ?? "" : "";
201
201
  const now = Date.now();
202
202
  const memo = pathMemo.get(filePath);
203
203
  if (memo) {
@@ -13,6 +13,7 @@ var __copyProps = (to, from, except, desc) => {
13
13
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
14
14
 
15
15
  // src/doc-sync-guard/index.ts
16
+ import { existsSync, readFileSync } from "node:fs";
16
17
  import { basename, extname } from "node:path";
17
18
 
18
19
  // src/runtime/index.ts
@@ -153,7 +154,13 @@ var plugin = {
153
154
  }
154
155
  if (isDocFile(path, cfg.docNames)) {
155
156
  state.docWrites += 1;
156
- const content = extractDocContent(input.toolInput);
157
+ let content = extractDocContent(input.toolInput);
158
+ if (!content) {
159
+ try {
160
+ if (existsSync(path)) content = readFileSync(path, "utf-8");
161
+ } catch {
162
+ }
163
+ }
157
164
  if (!content || state.changedFiles.length === 0) return;
158
165
  const missing = state.changedFiles.filter((changedPath) => !isReferenced(changedPath, content));
159
166
  if (missing.length === 0) return;
@@ -14,7 +14,7 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
14
14
 
15
15
  // src/duplicate-code-detector/index.ts
16
16
  import { readFile, realpath, stat } from "node:fs/promises";
17
- import { isAbsolute, relative, resolve, sep } from "node:path";
17
+ import { extname, isAbsolute, relative, resolve, sep } from "node:path";
18
18
 
19
19
  // src/runtime/index.ts
20
20
  var runtime_exports = {};
@@ -316,7 +316,7 @@ var plugin = {
316
316
  return;
317
317
  }
318
318
  if (!isWithinRoot(projectRoot, changedFile)) return;
319
- const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
319
+ const ext = extname(sourcePath).toLowerCase();
320
320
  if (!extensionsSet.has(ext)) return;
321
321
  state.hookInvocationCount += 1;
322
322
  const now = Date.now();
@@ -339,8 +339,9 @@ var plugin = {
339
339
  return;
340
340
  }
341
341
  const matched = /* @__PURE__ */ new Set();
342
+ const resolvedChanged = resolve(changedFile).toLowerCase();
342
343
  for (const p of otherFilePaths) {
343
- if (resolve(p) === resolve(changedFile)) continue;
344
+ if (resolve(p).toLowerCase() === resolvedChanged) continue;
344
345
  const otherFps = await readCachedFingerprints(p, cfg.minLines);
345
346
  if (otherFps === null || otherFps.size === 0) continue;
346
347
  for (const fp of changedFps) {
@@ -42,13 +42,13 @@ var ERROR_LINE_PATTERNS = [
42
42
  ];
43
43
  var FRAME_PATTERNS = [
44
44
  // Node/V8: "at fn (path/file.ts:12:5)" or "at path/file.ts:12:5"
45
- /\bat\s+(?:[^(\n]+\()?([^()\n:]+:\d+)(?::\d+)?\)?/g,
45
+ /\bat\s+(?:[^(\n]+\()?((?:[A-Za-z]:[\\/])?[^()\n:]+:\d+)(?::\d+)?\)?/g,
46
46
  // Python: File "path/file.py", line 12
47
47
  /File "([^"]+)", line (\d+)/g,
48
48
  // tsc/vitest/eslint bare: "src/foo.ts:12:5" (require a path-ish prefix)
49
- /(?:^|[ \t(])([A-Za-z0-9_./\\-]+\.[a-z]{1,4}:\d+)(?::\d+)?/gm,
49
+ /(?:^|[ \t(])((?:[A-Za-z]:[\\/])?[A-Za-z0-9_./\\-]+\.[a-z]{1,4}:\d+)(?::\d+)?/gm,
50
50
  // Rust: "--> src/main.rs:4:5"
51
- /-->\s+([^\s:]+:\d+)/g
51
+ /-->\s+((?:[A-Za-z]:[\\/])?[^\s:]+:\d+)/g
52
52
  ];
53
53
  function extractErrorLine(output) {
54
54
  for (const re of ERROR_LINE_PATTERNS) {
@@ -1,6 +1,7 @@
1
1
  // src/git-autocommit/index.ts
2
2
  import { execFile } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
+ import { resolve } from "node:path";
4
5
  var API_VERSION = "^0.1.10";
5
6
  var commitCount = { value: 0 };
6
7
  var lastCommit = { hash: null, at: null };
@@ -108,12 +109,14 @@ async function stageFiles(files, cwd) {
108
109
  if (!hasPattern) {
109
110
  const existing = files.filter((f) => {
110
111
  try {
111
- return existsSync(f);
112
+ return existsSync(cwd ? resolve(cwd, f) : f);
112
113
  } catch {
113
114
  return false;
114
115
  }
115
116
  });
116
- if (existing.length === 0) throw new Error("No files exist to stage");
117
+ if (existing.length === 0) {
118
+ throw new Error("Failed to stage files: none of the specified files exist on disk");
119
+ }
117
120
  await runGit(["add", "--", ...existing], cwd);
118
121
  return;
119
122
  }
@@ -36,9 +36,13 @@ function globToRegExp(glob) {
36
36
  return new RegExp(`^${globToSource(glob)}$`);
37
37
  }
38
38
  function matchGitignorePattern(relPath, pattern) {
39
- const rel = toForwardSlashes(relPath);
40
- const pat = pattern.trim();
39
+ const rel = toForwardSlashes(relPath).replace(/^\//, "");
40
+ let pat = pattern.trim();
41
41
  if (pat.length === 0 || pat.startsWith("#")) return false;
42
+ if (pat.startsWith("/")) {
43
+ pat = pat.slice(1);
44
+ if (pat.length === 0) return false;
45
+ }
42
46
  const segments = rel.split("/");
43
47
  if (pat.endsWith("/")) {
44
48
  const dir = pat.slice(0, -1);
@@ -194,8 +194,11 @@ async function organizeImports(filePath, cfg, cwd) {
194
194
  if (!(0, runtime_exports.withinProject)(filePath)) return null;
195
195
  if (!existsSync(filePath)) return null;
196
196
  let bytesBefore;
197
+ let mtimeBefore;
197
198
  try {
198
- bytesBefore = statSync(filePath).size;
199
+ const st = statSync(filePath);
200
+ bytesBefore = st.size;
201
+ mtimeBefore = st.mtimeMs;
199
202
  } catch {
200
203
  return null;
201
204
  }
@@ -215,13 +218,16 @@ async function organizeImports(filePath, cfg, cwd) {
215
218
  if (result.timedOut || result.code === null) return null;
216
219
  if (result.code === 127) return null;
217
220
  let bytesAfter;
221
+ let mtimeAfter;
218
222
  try {
219
- bytesAfter = statSync(filePath).size;
223
+ const st = statSync(filePath);
224
+ bytesAfter = st.size;
225
+ mtimeAfter = st.mtimeMs;
220
226
  } catch {
221
227
  return null;
222
228
  }
223
229
  return {
224
- changed: bytesAfter !== bytesBefore,
230
+ changed: bytesAfter !== bytesBefore || mtimeAfter > mtimeBefore,
225
231
  bytesBefore,
226
232
  bytesAfter,
227
233
  command: usedCommand,
@@ -284,7 +290,7 @@ var plugin = {
284
290
  const filePath = inp["path"];
285
291
  if (!filePath || typeof filePath !== "string") return;
286
292
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : "";
287
- if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"].includes(ext)) return;
293
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts"].includes(ext)) return;
288
294
  state.invocationCount += 1;
289
295
  const result = await organizeImports(filePath, cfg, process.cwd());
290
296
  if (!result) {