@wrongstack/plugins 0.281.3 → 0.282.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +29 -3
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-i18n-extractor.d.ts +36 -0
  9. package/dist/auto-i18n-extractor.js +335 -0
  10. package/dist/checkpoint.js +18 -0
  11. package/dist/code-metrics.d.ts +31 -0
  12. package/dist/code-metrics.js +338 -0
  13. package/dist/commit-validator.js +67 -12
  14. package/dist/cost-tracker.js +58 -16
  15. package/dist/dead-code-detector.d.ts +34 -0
  16. package/dist/dead-code-detector.js +354 -0
  17. package/dist/dep-guard.js +47 -6
  18. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  19. package/dist/dependency-vulnerability-gate.js +308 -0
  20. package/dist/diff-summary.js +97 -10
  21. package/dist/doc-sync-guard.d.ts +33 -0
  22. package/dist/doc-sync-guard.js +223 -0
  23. package/dist/duplicate-code-detector.d.ts +33 -0
  24. package/dist/duplicate-code-detector.js +384 -0
  25. package/dist/feature-flag-tracker.d.ts +38 -0
  26. package/dist/feature-flag-tracker.js +316 -0
  27. package/dist/file-watcher.js +85 -36
  28. package/dist/format-on-save.js +76 -10
  29. package/dist/import-organizer.js +73 -14
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.js +14067 -5054
  32. package/dist/interface-contract-guard.d.ts +37 -0
  33. package/dist/interface-contract-guard.js +302 -0
  34. package/dist/knowledge-graph.d.ts +45 -0
  35. package/dist/knowledge-graph.js +325 -0
  36. package/dist/license-audit-gate.d.ts +34 -0
  37. package/dist/license-audit-gate.js +260 -0
  38. package/dist/llm-cache.js +5 -0
  39. package/dist/loop-breaker.d.ts +0 -38
  40. package/dist/loop-breaker.js +209 -8
  41. package/dist/migration-planner.d.ts +30 -0
  42. package/dist/migration-planner.js +349 -0
  43. package/dist/model-router.js +5 -0
  44. package/dist/performance-regression-gate.d.ts +33 -0
  45. package/dist/performance-regression-gate.js +315 -0
  46. package/dist/plugin-stack-observer.d.ts +35 -0
  47. package/dist/plugin-stack-observer.js +125 -0
  48. package/dist/pr-drafter.d.ts +35 -0
  49. package/dist/pr-drafter.js +334 -0
  50. package/dist/prompt-firewall.js +5 -0
  51. package/dist/refactor-suggester.d.ts +38 -0
  52. package/dist/refactor-suggester.js +382 -0
  53. package/dist/release-notes-generator.d.ts +27 -0
  54. package/dist/release-notes-generator.js +209 -0
  55. package/dist/schema-evolution-guard.d.ts +42 -0
  56. package/dist/schema-evolution-guard.js +319 -0
  57. package/dist/security-hotspot-scanner.d.ts +30 -0
  58. package/dist/security-hotspot-scanner.js +402 -0
  59. package/dist/semantic-search-indexer.d.ts +38 -0
  60. package/dist/semantic-search-indexer.js +436 -0
  61. package/dist/shell-check.js +38 -3
  62. package/dist/smart-rename.d.ts +26 -0
  63. package/dist/smart-rename.js +170 -0
  64. package/dist/spec-linker.js +273 -133
  65. package/dist/test-coverage-gate.d.ts +37 -0
  66. package/dist/test-coverage-gate.js +263 -0
  67. package/dist/test-flake-detector.d.ts +27 -0
  68. package/dist/test-flake-detector.js +274 -0
  69. package/dist/test-generator.d.ts +32 -0
  70. package/dist/test-generator.js +243 -0
  71. package/dist/test-runner-gate.js +154 -22
  72. package/dist/todo-listener.d.ts +2 -2
  73. package/dist/todo-listener.js +5 -5
  74. package/dist/token-throttle.js +5 -0
  75. package/dist/type-gate.d.ts +37 -0
  76. package/dist/type-gate.js +311 -0
  77. package/package.json +112 -4
  78. package/LICENSE +0 -21
@@ -0,0 +1,338 @@
1
+ import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
2
+ import { resolve, isAbsolute, relative, extname } from 'path';
3
+
4
+ // src/code-metrics/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ measureCount: 0,
8
+ fileCount: 0,
9
+ hookInvocationCount: 0,
10
+ errorCount: 0,
11
+ hookUnregister: null
12
+ };
13
+ var DEFAULTS = {
14
+ enabled: false,
15
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
16
+ maxFiles: 50
17
+ };
18
+ function readConfig(raw) {
19
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
20
+ const r = raw;
21
+ return {
22
+ enabled: r["enabled"] !== false,
23
+ extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
24
+ maxFiles: typeof r["maxFiles"] === "number" && r["maxFiles"] >= 1 && r["maxFiles"] <= 500 ? r["maxFiles"] : DEFAULTS.maxFiles
25
+ };
26
+ }
27
+ function withinProject(p) {
28
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
29
+ const root = process.cwd();
30
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
31
+ const rel = relative(root, resolved);
32
+ if (rel === "" || rel === ".") return true;
33
+ if (rel.startsWith("..")) return false;
34
+ if (isAbsolute(rel)) return false;
35
+ return true;
36
+ }
37
+ function normalizeExtensions(exts) {
38
+ return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
39
+ }
40
+ function matchesExtension(p, exts) {
41
+ return exts.includes(extname(p).toLowerCase());
42
+ }
43
+ function collectSourceFiles(root, exts) {
44
+ const files = [];
45
+ if (!existsSync(root)) return files;
46
+ const s = statSync(root);
47
+ if (s.isFile()) {
48
+ if (matchesExtension(root, exts)) files.push(root);
49
+ return files;
50
+ }
51
+ if (!s.isDirectory()) return files;
52
+ function walk(dir) {
53
+ let entries;
54
+ try {
55
+ entries = readdirSync(dir);
56
+ } catch {
57
+ return;
58
+ }
59
+ for (const entry of entries) {
60
+ if (entry === "node_modules" || entry === "dist" || entry === ".git" || entry === "coverage") continue;
61
+ const full = resolve(dir, entry);
62
+ let st;
63
+ try {
64
+ st = statSync(full);
65
+ } catch {
66
+ continue;
67
+ }
68
+ if (st.isDirectory()) {
69
+ walk(full);
70
+ } else if (st.isFile() && matchesExtension(full, exts)) {
71
+ files.push(full);
72
+ }
73
+ }
74
+ }
75
+ walk(root);
76
+ return files;
77
+ }
78
+ function toPosix(p) {
79
+ return p.replace(/\\/g, "/");
80
+ }
81
+ function relativePath(p) {
82
+ return toPosix(relative(process.cwd(), p));
83
+ }
84
+ function countFunctions(content) {
85
+ let count = 0;
86
+ const declRe = /\bfunction\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(/g;
87
+ const methodRe = /\b[A-Za-z_$][A-Za-z0-9_$]*\s*\([^)]*\)\s*\{/g;
88
+ const arrowRe = /=>\s*(?:\{|\(|[^\s;{}])/g;
89
+ let m;
90
+ declRe.lastIndex = 0;
91
+ while ((m = declRe.exec(content)) !== null) count++;
92
+ methodRe.lastIndex = 0;
93
+ while ((m = methodRe.exec(content)) !== null) {
94
+ const prefix = content.slice(0, m.index).trimEnd();
95
+ if (/\bfunction\s*$/.test(prefix)) continue;
96
+ count++;
97
+ }
98
+ arrowRe.lastIndex = 0;
99
+ while ((m = arrowRe.exec(content)) !== null) count++;
100
+ return count;
101
+ }
102
+ function countComplexity(content) {
103
+ const controlRe = /\b(if|else\s+if|for|while|switch|catch)\b/g;
104
+ const operatorRe = /[?&|]/g;
105
+ let complexity = 0;
106
+ let m;
107
+ controlRe.lastIndex = 0;
108
+ while ((m = controlRe.exec(content)) !== null) complexity++;
109
+ operatorRe.lastIndex = 0;
110
+ while ((m = operatorRe.exec(content)) !== null) {
111
+ const ch = m[0];
112
+ if (ch === "?") {
113
+ complexity++;
114
+ } else {
115
+ const next = content[m.index + 1];
116
+ if (next === ch) complexity++;
117
+ }
118
+ }
119
+ return complexity;
120
+ }
121
+ function analyzeFile(filePath, content) {
122
+ const lines = content.split(/\r?\n/);
123
+ let codeLines = 0;
124
+ let commentLines = 0;
125
+ let blankLines = 0;
126
+ let inBlockComment = false;
127
+ for (const rawLine of lines) {
128
+ const line = rawLine.trim();
129
+ if (line.length === 0) {
130
+ blankLines++;
131
+ continue;
132
+ }
133
+ if (inBlockComment) {
134
+ commentLines++;
135
+ if (line.includes("*/")) inBlockComment = false;
136
+ continue;
137
+ }
138
+ if (line.startsWith("//")) {
139
+ commentLines++;
140
+ continue;
141
+ }
142
+ if (line.startsWith("/*")) {
143
+ commentLines++;
144
+ if (!line.includes("*/")) inBlockComment = true;
145
+ continue;
146
+ }
147
+ codeLines++;
148
+ }
149
+ return {
150
+ file: relativePath(filePath),
151
+ lines: lines.length,
152
+ codeLines,
153
+ commentLines,
154
+ blankLines,
155
+ functionCount: countFunctions(content),
156
+ complexity: countComplexity(content)
157
+ };
158
+ }
159
+ function measurePath(rawPath, cfg) {
160
+ const root = process.cwd();
161
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
162
+ const exts = normalizeExtensions(cfg.extensions);
163
+ const allFiles = collectSourceFiles(resolved, exts);
164
+ const files = allFiles.slice(0, cfg.maxFiles);
165
+ const metrics = [];
166
+ for (const p of files) {
167
+ try {
168
+ const content = readFileSync(p, "utf-8");
169
+ metrics.push(analyzeFile(p, content));
170
+ } catch {
171
+ }
172
+ }
173
+ return { files: metrics, totalFiles: allFiles.length };
174
+ }
175
+ var plugin = {
176
+ name: "code-metrics",
177
+ version: "0.1.0",
178
+ description: "Computes per-file line counts, function counts, and cyclomatic-complexity-like scores",
179
+ apiVersion: API_VERSION,
180
+ capabilities: { tools: true, hooks: true },
181
+ defaultConfig: { ...DEFAULTS },
182
+ configSchema: {
183
+ type: "object",
184
+ properties: {
185
+ enabled: { type: "boolean", default: true, description: "Master switch." },
186
+ extensions: {
187
+ type: "array",
188
+ items: { type: "string" },
189
+ default: [".ts", ".tsx", ".js", ".jsx"],
190
+ description: "File extensions to measure."
191
+ },
192
+ maxFiles: {
193
+ type: "number",
194
+ minimum: 1,
195
+ maximum: 500,
196
+ default: 50,
197
+ description: "Maximum files measured in a directory scan."
198
+ }
199
+ }
200
+ },
201
+ setup(api) {
202
+ state.measureCount = 0;
203
+ state.fileCount = 0;
204
+ state.hookInvocationCount = 0;
205
+ state.errorCount = 0;
206
+ if (state.hookUnregister) {
207
+ try {
208
+ state.hookUnregister();
209
+ } catch {
210
+ }
211
+ state.hookUnregister = null;
212
+ }
213
+ const cfg = readConfig(api.config.extensions?.["code-metrics"]);
214
+ const hook = (input) => {
215
+ if (!cfg.enabled) return;
216
+ if (input.toolResult?.isError) return;
217
+ const inp = input.toolInput ?? {};
218
+ const sourcePath = inp["path"];
219
+ if (!sourcePath || typeof sourcePath !== "string") return;
220
+ if (!withinProject(sourcePath)) return;
221
+ const exts = normalizeExtensions(cfg.extensions);
222
+ if (!matchesExtension(sourcePath, exts)) return;
223
+ state.hookInvocationCount += 1;
224
+ const resolved = resolve(process.cwd(), sourcePath);
225
+ let content;
226
+ try {
227
+ content = readFileSync(resolved, "utf-8");
228
+ } catch {
229
+ state.errorCount += 1;
230
+ return;
231
+ }
232
+ const metrics = analyzeFile(resolved, content);
233
+ return {
234
+ additionalContext: `
235
+ \u{1F4CA} code-metrics: ${relativePath(resolved)} \u2014 ${metrics.lines} lines (${metrics.codeLines} code, ${metrics.commentLines} comments, ${metrics.blankLines} blank), ${metrics.functionCount} function(s), complexity ${metrics.complexity}.`
236
+ };
237
+ };
238
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
239
+ api.tools.register({
240
+ name: "measure_code_metrics",
241
+ description: "Measure lines, comments, blank lines, function count, and cyclomatic-complexity-like score for a source file or directory.",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ path: { type: "string", default: ".", description: "File or directory path to measure." }
246
+ }
247
+ },
248
+ permission: "auto",
249
+ category: "Diagnostics",
250
+ mutating: false,
251
+ async execute(input) {
252
+ if (!cfg.enabled) return { ok: false, error: "code-metrics is disabled" };
253
+ const rawPath = typeof input.path === "string" ? input.path : ".";
254
+ if (!withinProject(rawPath)) {
255
+ return { ok: false, error: "path is outside the project root" };
256
+ }
257
+ state.measureCount += 1;
258
+ let result;
259
+ try {
260
+ result = measurePath(rawPath, cfg);
261
+ } catch (err) {
262
+ state.errorCount += 1;
263
+ return { ok: false, error: String(err) };
264
+ }
265
+ state.fileCount += result.files.length;
266
+ return {
267
+ ok: true,
268
+ path: relativePath(resolve(process.cwd(), rawPath)),
269
+ files: result.files,
270
+ totalFiles: result.totalFiles,
271
+ capped: result.totalFiles > cfg.maxFiles
272
+ };
273
+ }
274
+ });
275
+ api.tools.register({
276
+ name: "metrics_status",
277
+ description: "Reports code-metrics state: config + counters.",
278
+ inputSchema: { type: "object", properties: {} },
279
+ permission: "auto",
280
+ category: "Diagnostics",
281
+ mutating: false,
282
+ async execute() {
283
+ return {
284
+ ok: true,
285
+ enabled: cfg.enabled,
286
+ extensions: cfg.extensions,
287
+ maxFiles: cfg.maxFiles,
288
+ counters: {
289
+ measures: state.measureCount,
290
+ files: state.fileCount,
291
+ hookInvocations: state.hookInvocationCount,
292
+ errors: state.errorCount
293
+ }
294
+ };
295
+ }
296
+ });
297
+ api.log.info("code-metrics plugin loaded", {
298
+ version: "0.1.0",
299
+ extensions: cfg.extensions,
300
+ maxFiles: cfg.maxFiles
301
+ });
302
+ },
303
+ teardown(api) {
304
+ if (state.hookUnregister) {
305
+ try {
306
+ state.hookUnregister();
307
+ } catch {
308
+ }
309
+ state.hookUnregister = null;
310
+ }
311
+ const final = {
312
+ measures: state.measureCount,
313
+ files: state.fileCount,
314
+ hookInvocations: state.hookInvocationCount,
315
+ errors: state.errorCount
316
+ };
317
+ state.measureCount = 0;
318
+ state.fileCount = 0;
319
+ state.hookInvocationCount = 0;
320
+ state.errorCount = 0;
321
+ api.log.info("code-metrics: teardown complete", { final });
322
+ },
323
+ async health() {
324
+ return {
325
+ ok: state.errorCount === 0,
326
+ message: state.errorCount ? `code-metrics: ${state.errorCount} error(s)` : `code-metrics: ${state.measureCount} measurement(s), ${state.fileCount} file(s)`,
327
+ counters: {
328
+ measures: state.measureCount,
329
+ files: state.fileCount,
330
+ hookInvocations: state.hookInvocationCount,
331
+ errors: state.errorCount
332
+ }
333
+ };
334
+ }
335
+ };
336
+ var code_metrics_default = plugin;
337
+
338
+ export { code_metrics_default as default };
@@ -4,6 +4,10 @@ var state = {
4
4
  invocationCount: 0,
5
5
  validCount: 0,
6
6
  invalidCount: 0,
7
+ /** Times the LLM successfully produced a subject suggestion. */
8
+ suggestFixCount: 0,
9
+ /** Times the LLM call failed or was skipped (api.llm absent, etc.). */
10
+ suggestFixErrors: 0,
7
11
  hookUnregister: null,
8
12
  lastValidation: null
9
13
  };
@@ -13,7 +17,8 @@ var DEFAULTS = {
13
17
  allowedTypes: [],
14
18
  maxSubjectLength: 72,
15
19
  bodyRequired: false,
16
- minBodyLength: 10
20
+ minBodyLength: 10,
21
+ suggestFix: false
17
22
  };
18
23
  function readConfig(raw) {
19
24
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
@@ -24,7 +29,8 @@ function readConfig(raw) {
24
29
  allowedTypes: Array.isArray(r["allowedTypes"]) ? r["allowedTypes"].filter((x) => typeof x === "string") : [],
25
30
  maxSubjectLength: typeof r["maxSubjectLength"] === "number" && r["maxSubjectLength"] > 0 ? r["maxSubjectLength"] : DEFAULTS.maxSubjectLength,
26
31
  bodyRequired: r["bodyRequired"] === true,
27
- minBodyLength: typeof r["minBodyLength"] === "number" && r["minBodyLength"] > 0 ? r["minBodyLength"] : DEFAULTS.minBodyLength
32
+ minBodyLength: typeof r["minBodyLength"] === "number" && r["minBodyLength"] > 0 ? r["minBodyLength"] : DEFAULTS.minBodyLength,
33
+ suggestFix: r["suggestFix"] === true
28
34
  };
29
35
  }
30
36
  var STANDARD_TYPES = [
@@ -44,7 +50,14 @@ function parseCommitMessage(message, cfg) {
44
50
  const errors = [];
45
51
  const firstLine = message.trim().split("\n")[0] ?? "";
46
52
  if (!firstLine) {
47
- return { valid: false, type: "", scope: "", subject: "", breaking: false, errors: ["empty commit message"] };
53
+ return {
54
+ valid: false,
55
+ type: "",
56
+ scope: "",
57
+ subject: "",
58
+ breaking: false,
59
+ errors: ["empty commit message"]
60
+ };
48
61
  }
49
62
  const match = firstLine.match(/^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
50
63
  if (!match) {
@@ -84,7 +97,9 @@ function parseCommitMessage(message, cfg) {
84
97
  const bodyStart = lines.findIndex((line, i) => i > 0 && line.trim() === "");
85
98
  const body = bodyStart >= 0 ? lines.slice(bodyStart + 1).join("\n").trim() : "";
86
99
  if (!body) {
87
- errors.push("A commit body is required. Add a blank line after the subject, then the description.");
100
+ errors.push(
101
+ "A commit body is required. Add a blank line after the subject, then the description."
102
+ );
88
103
  } else if (body.length < cfg.minBodyLength) {
89
104
  errors.push(
90
105
  `Body is ${body.length} characters \u2014 minimum is ${cfg.minBodyLength}. Add more context about what changed and why.`
@@ -152,6 +167,11 @@ var plugin = {
152
167
  minimum: 1,
153
168
  default: 10,
154
169
  description: "Minimum body length in characters (when bodyRequired is true)."
170
+ },
171
+ suggestFix: {
172
+ type: "boolean",
173
+ default: false,
174
+ description: "When true and mode=warn, ask the host LLM for a corrected conventional-commit subject and include it in the warn context. Off by default (LLM calls aren't free). Ignored in block mode."
155
175
  }
156
176
  }
157
177
  },
@@ -159,10 +179,12 @@ var plugin = {
159
179
  state.invocationCount = 0;
160
180
  state.validCount = 0;
161
181
  state.invalidCount = 0;
182
+ state.suggestFixCount = 0;
183
+ state.suggestFixErrors = 0;
162
184
  state.hookUnregister = null;
163
185
  state.lastValidation = null;
164
186
  const cfg = readConfig(api.config.extensions?.["commit-validator"]);
165
- const hook = (input) => {
187
+ const hook = async (input) => {
166
188
  const toolName = input.toolName ?? "";
167
189
  const inp = input.toolInput ?? {};
168
190
  let message = null;
@@ -237,12 +259,37 @@ Examples:
237
259
  ${example}`
238
260
  };
239
261
  }
262
+ let baseContext = `
263
+ ?? commit-validator: commit message has ${parsed.errors.length} issue(s):
264
+ ${errorList}
265
+ Expected: <type>[(scope)][!]: <description>`;
266
+ if (cfg.suggestFix && api.llm) {
267
+ try {
268
+ const suggest = await api.llm.complete(
269
+ `The user wrote a conventional-commit message that fails validation:
270
+ Original: ${message}
271
+ Errors: ${parsed.errors.join("; ")}
272
+ Reply with ONE corrected conventional-commit subject line (and optional body) and nothing else.`,
273
+ {
274
+ system: "You rewrite commit subjects to follow the conventional-commits format. Reply tersely, no preamble, no quotes.",
275
+ maxTokens: 120
276
+ }
277
+ );
278
+ const text = suggest.text.trim();
279
+ if (text) {
280
+ state.suggestFixCount += 1;
281
+ api.metrics.counter("suggest_fix");
282
+ baseContext += `
283
+ Suggested rewrite (${suggest.model}):
284
+ ${text.split("\n").join("\n ")}`;
285
+ }
286
+ } catch {
287
+ state.suggestFixErrors += 1;
288
+ }
289
+ }
240
290
  return {
241
291
  decision: "allow",
242
- additionalContext: `
243
- \u26A0\uFE0F commit-validator: commit message has ${parsed.errors.length} issue(s):
244
- ${errorList}
245
- Expected: <type>[(scope)][!]: <description>`
292
+ additionalContext: baseContext
246
293
  };
247
294
  };
248
295
  state.hookUnregister = api.registerHook("PreToolUse", "bash|git_autocommit", hook);
@@ -266,7 +313,9 @@ Expected: <type>[(scope)][!]: <description>`
266
313
  counters: {
267
314
  invocations: state.invocationCount,
268
315
  valid: state.validCount,
269
- invalid: state.invalidCount
316
+ invalid: state.invalidCount,
317
+ suggestFix: state.suggestFixCount,
318
+ suggestFixErrors: state.suggestFixErrors
270
319
  },
271
320
  lastValidation: state.lastValidation
272
321
  };
@@ -289,11 +338,15 @@ Expected: <type>[(scope)][!]: <description>`
289
338
  const final = {
290
339
  invocations: state.invocationCount,
291
340
  valid: state.validCount,
292
- invalid: state.invalidCount
341
+ invalid: state.invalidCount,
342
+ suggestFix: state.suggestFixCount,
343
+ suggestFixErrors: state.suggestFixErrors
293
344
  };
294
345
  state.invocationCount = 0;
295
346
  state.validCount = 0;
296
347
  state.invalidCount = 0;
348
+ state.suggestFixCount = 0;
349
+ state.suggestFixErrors = 0;
297
350
  state.lastValidation = null;
298
351
  api.log.info("commit-validator: teardown complete", { final });
299
352
  },
@@ -304,7 +357,9 @@ Expected: <type>[(scope)][!]: <description>`
304
357
  counters: {
305
358
  invocations: state.invocationCount,
306
359
  valid: state.validCount,
307
- invalid: state.invalidCount
360
+ invalid: state.invalidCount,
361
+ suggestFix: state.suggestFixCount,
362
+ suggestFixErrors: state.suggestFixErrors
308
363
  },
309
364
  lastValidation: state.lastValidation
310
365
  };
@@ -11,16 +11,23 @@ var PRICING = {
11
11
  "claude-3-opus": { input: 15, output: 75 },
12
12
  "gemini-1.5-pro": { input: 3.5, output: 10.5 },
13
13
  "gemini-1.5-flash": { input: 0.075, output: 0.3 },
14
- "default": { input: 5, output: 15 }
14
+ default: { input: 5, output: 15 }
15
15
  };
16
16
  var DEFAULT_PRICING = { input: 5, output: 15 };
17
17
  var pricingOverrides = {};
18
18
  var bundledFromRegistry = {};
19
19
  var lastCost = { usd: 0, model: null, at: null };
20
+ var digestCounters = {
21
+ totalRequests: 0,
22
+ mailboxDigestsSent: 0,
23
+ mailboxDigestErrors: 0
24
+ };
20
25
  function readCostTrackerConfig(raw) {
21
26
  return {
22
27
  budgetLimit: typeof raw?.["budgetLimit"] === "number" ? raw["budgetLimit"] : 0,
23
- warningThreshold: typeof raw?.["warningThreshold"] === "number" ? raw["warningThreshold"] : 80
28
+ warningThreshold: typeof raw?.["warningThreshold"] === "number" ? raw["warningThreshold"] : 80,
29
+ mailboxDigestEveryN: typeof raw?.["mailboxDigestEveryN"] === "number" && raw?.["mailboxDigestEveryN"] >= 0 ? Math.floor(raw?.["mailboxDigestEveryN"]) : 0,
30
+ mailboxDigestTo: typeof raw?.["mailboxDigestTo"] === "string" && (raw?.["mailboxDigestTo"]).length > 0 ? raw?.["mailboxDigestTo"] : "cost-tracker"
24
31
  };
25
32
  }
26
33
  function estimateCost(model, promptTokens, completionTokens) {
@@ -48,8 +55,16 @@ var plugin = {
48
55
  properties: {
49
56
  trackPerModel: { type: "boolean", default: true },
50
57
  trackPerUser: { type: "boolean", default: false },
51
- budgetLimit: { type: "number", default: 0, description: "Budget limit in USD (0 = no limit)" },
52
- warningThreshold: { type: "number", default: 80, description: "Warning threshold as percentage of budget" },
58
+ budgetLimit: {
59
+ type: "number",
60
+ default: 0,
61
+ description: "Budget limit in USD (0 = no limit)"
62
+ },
63
+ warningThreshold: {
64
+ type: "number",
65
+ default: 80,
66
+ description: "Warning threshold as percentage of budget"
67
+ },
53
68
  pricingOverrides: {
54
69
  type: "object",
55
70
  description: "Per-model pricing overrides in USD per 1M tokens. Keys are lowercased model names; values are { input, output }. Takes precedence over the bundled PRICING table.",
@@ -76,7 +91,11 @@ var plugin = {
76
91
  lastCost.usd = 0;
77
92
  lastCost.model = null;
78
93
  lastCost.at = null;
94
+ digestCounters.totalRequests = 0;
95
+ digestCounters.mailboxDigestsSent = 0;
96
+ digestCounters.mailboxDigestErrors = 0;
79
97
  const rawConfig = api.config.extensions?.["cost-tracker"];
98
+ const cfg = readCostTrackerConfig(rawConfig);
80
99
  const userOverrides = rawConfig?.["pricingOverrides"];
81
100
  if (userOverrides && typeof userOverrides === "object") {
82
101
  for (const [model, value] of Object.entries(userOverrides)) {
@@ -124,7 +143,7 @@ var plugin = {
124
143
  totalCostUsd: 0,
125
144
  byModel: {}
126
145
  };
127
- api.onEvent("provider.response", (payload) => {
146
+ api.onEvent("provider.response", async (payload) => {
128
147
  const usage = payload.usage;
129
148
  const model = payload.ctx?.model ?? "unknown";
130
149
  const promptTokens = usage.input ?? 0;
@@ -153,6 +172,23 @@ var plugin = {
153
172
  slot.requests += 1;
154
173
  api.metrics.counter("tokens_total", totalTokens, { model });
155
174
  api.metrics.histogram("cost_usd", costUsd, { model });
175
+ digestCounters.totalRequests += 1;
176
+ if (cfg.mailboxDigestEveryN > 0 && digestCounters.totalRequests % cfg.mailboxDigestEveryN === 0 && api.mailbox) {
177
+ const top = Object.entries(sessionCost.byModel).map(([m, v]) => `${m}:${v.tokens}t/${v.costUsd.toFixed(4)}`).join(", ");
178
+ try {
179
+ await api.mailbox.send({
180
+ to: cfg.mailboxDigestTo,
181
+ from: "cost-tracker",
182
+ type: "note",
183
+ subject: `cost-tracker digest @ #${digestCounters.totalRequests} requests`,
184
+ body: `running totals: ${sessionCost.totalTokens} tokens, ~${sessionCost.totalCostUsd.toFixed(4)} USD, ${sessionCost.requests.length} reqs
185
+ by model: ${top || "(none)"}`
186
+ });
187
+ digestCounters.mailboxDigestsSent += 1;
188
+ } catch {
189
+ digestCounters.mailboxDigestErrors += 1;
190
+ }
191
+ }
156
192
  lastCost.usd = costUsd;
157
193
  lastCost.model = model;
158
194
  lastCost.at = (/* @__PURE__ */ new Date()).toISOString();
@@ -165,9 +201,7 @@ var plugin = {
165
201
  category: "Meta",
166
202
  mutating: false,
167
203
  async execute() {
168
- const { budgetLimit, warningThreshold } = readCostTrackerConfig(
169
- api.config.extensions?.["cost-tracker"]
170
- );
204
+ const { budgetLimit, warningThreshold } = cfg;
171
205
  const usage = {
172
206
  totalRequests: sessionCost.requests.length,
173
207
  totalPromptTokens: sessionCost.totalPromptTokens,
@@ -263,13 +297,15 @@ var plugin = {
263
297
  totalRequests: sessionCost.requests.length,
264
298
  byModel: sessionCost.byModel
265
299
  },
266
- requests: includeModel ? sessionCost.requests : sessionCost.requests.map(({ promptTokens, completionTokens, totalTokens, costUsd, timestamp }) => ({
267
- promptTokens,
268
- completionTokens,
269
- totalTokens,
270
- costUsd,
271
- timestamp
272
- }))
300
+ requests: includeModel ? sessionCost.requests : sessionCost.requests.map(
301
+ ({ promptTokens, completionTokens, totalTokens, costUsd, timestamp }) => ({
302
+ promptTokens,
303
+ completionTokens,
304
+ totalTokens,
305
+ costUsd,
306
+ timestamp
307
+ })
308
+ )
273
309
  }
274
310
  };
275
311
  }
@@ -301,6 +337,9 @@ var plugin = {
301
337
  lastCost.usd = 0;
302
338
  lastCost.model = null;
303
339
  lastCost.at = null;
340
+ digestCounters.totalRequests = 0;
341
+ digestCounters.mailboxDigestsSent = 0;
342
+ digestCounters.mailboxDigestErrors = 0;
304
343
  api.log.info("cost-tracker: teardown complete", {
305
344
  overrideCount,
306
345
  registryCount,
@@ -315,7 +354,10 @@ var plugin = {
315
354
  registryCount: Object.keys(bundledFromRegistry).length,
316
355
  lastCostUsd: lastCost.usd,
317
356
  lastCostModel: lastCost.model,
318
- lastCostAt: lastCost.at
357
+ lastCostAt: lastCost.at,
358
+ mailboxDigestsSent: digestCounters.mailboxDigestsSent,
359
+ mailboxDigestErrors: digestCounters.mailboxDigestErrors,
360
+ totalRequests: digestCounters.totalRequests
319
361
  };
320
362
  }
321
363
  };
@@ -0,0 +1,34 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * dead-code-detector plugin — lightweight regex-based scan for exported
5
+ * identifiers that appear unused anywhere in the project.
6
+ *
7
+ * The plugin registers one tool:
8
+ * - `dead_code_scan` — scan a path for suspicious exported symbols.
9
+ *
10
+ * And one hook:
11
+ * - `PostToolUse` matcher `write|edit` — auto-scan changed source files and
12
+ * inject a short warning when newly exported symbols look unused.
13
+ *
14
+ * No external AST parser is used; all detection is regex-based and
15
+ * deterministic. Expect false positives on re-exports, public API surface,
16
+ * and test-only exports.
17
+ *
18
+ * Config (`config.extensions['dead-code-detector']`):
19
+ *
20
+ * ```jsonc
21
+ * {
22
+ * "enabled": true,
23
+ * "extensions": [".ts", ".tsx", ".js", ".jsx"],
24
+ * "defaultDepth": 3,
25
+ * "excludeDirs": ["node_modules", "dist", ".git", "coverage"]
26
+ * }
27
+ * ```
28
+ *
29
+ * @public
30
+ */
31
+
32
+ declare const plugin: Plugin;
33
+
34
+ export { plugin as default };