@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.
- package/README.md +29 -3
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/cost-tracker.js +58 -16
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +125 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- 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 };
|
package/dist/commit-validator.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
};
|
package/dist/cost-tracker.js
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
52
|
-
|
|
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 } =
|
|
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(
|
|
267
|
-
promptTokens,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 };
|