@wrongstack/plugins 0.281.1 → 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,354 @@
|
|
|
1
|
+
import { statSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { resolve, isAbsolute, relative, join, extname } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/dead-code-detector/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
scanCount: 0,
|
|
8
|
+
hitCount: 0,
|
|
9
|
+
missCount: 0,
|
|
10
|
+
hookInvocationCount: 0,
|
|
11
|
+
warningCount: 0,
|
|
12
|
+
errorCount: 0,
|
|
13
|
+
hookUnregister: null
|
|
14
|
+
};
|
|
15
|
+
var DEFAULTS = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
18
|
+
defaultDepth: 3,
|
|
19
|
+
maxDepth: 10,
|
|
20
|
+
excludeDirs: ["node_modules", "dist", ".git", "coverage"]
|
|
21
|
+
};
|
|
22
|
+
function readConfig(raw) {
|
|
23
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
24
|
+
const r = raw;
|
|
25
|
+
return {
|
|
26
|
+
enabled: r["enabled"] === true,
|
|
27
|
+
extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
|
|
28
|
+
defaultDepth: typeof r["defaultDepth"] === "number" && r["defaultDepth"] >= 0 && r["defaultDepth"] <= DEFAULTS.maxDepth ? r["defaultDepth"] : DEFAULTS.defaultDepth,
|
|
29
|
+
maxDepth: DEFAULTS.maxDepth,
|
|
30
|
+
excludeDirs: Array.isArray(r["excludeDirs"]) ? r["excludeDirs"].filter((x) => typeof x === "string") : DEFAULTS.excludeDirs
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function gatherFiles(root, depth, cfg) {
|
|
34
|
+
const files = [];
|
|
35
|
+
function walk(dir, remaining) {
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (remaining > 0 && !cfg.excludeDirs.includes(entry.name)) {
|
|
45
|
+
walk(join(dir, entry.name), remaining - 1);
|
|
46
|
+
}
|
|
47
|
+
} else if (entry.isFile()) {
|
|
48
|
+
const ext = extname(entry.name).toLowerCase();
|
|
49
|
+
if (cfg.extensions.includes(ext)) {
|
|
50
|
+
files.push(join(dir, entry.name));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
walk(resolve(root), depth);
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
function stripNoise(content) {
|
|
59
|
+
let stripped = content.replace(/\/\/.*$/gm, " ");
|
|
60
|
+
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
61
|
+
stripped = stripped.replace(
|
|
62
|
+
/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/g,
|
|
63
|
+
" "
|
|
64
|
+
);
|
|
65
|
+
return stripped;
|
|
66
|
+
}
|
|
67
|
+
function lineNumber(content, index) {
|
|
68
|
+
return content.slice(0, index).split(/\r?\n/).length;
|
|
69
|
+
}
|
|
70
|
+
function escapeRegex(s) {
|
|
71
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}
|
|
73
|
+
var DECLARATION_EXPORT_RE = /export\s+(?:async\s+|abstract\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
74
|
+
var NAMED_EXPORT_RE = /export\s*\{([^}]+)\}/g;
|
|
75
|
+
function extractExports(content, filePath) {
|
|
76
|
+
const exports = [];
|
|
77
|
+
let match;
|
|
78
|
+
DECLARATION_EXPORT_RE.lastIndex = 0;
|
|
79
|
+
while ((match = DECLARATION_EXPORT_RE.exec(content)) !== null) {
|
|
80
|
+
exports.push({
|
|
81
|
+
identifier: match[1],
|
|
82
|
+
file: filePath,
|
|
83
|
+
line: lineNumber(content, match.index),
|
|
84
|
+
exportType: "declaration"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
NAMED_EXPORT_RE.lastIndex = 0;
|
|
88
|
+
while ((match = NAMED_EXPORT_RE.exec(content)) !== null) {
|
|
89
|
+
const line = lineNumber(content, match.index);
|
|
90
|
+
const names = match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const parts = name.split(/\s+as\s+/);
|
|
93
|
+
const exportedName = parts.length > 1 ? parts[parts.length - 1].trim() : name;
|
|
94
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportedName)) {
|
|
95
|
+
exports.push({
|
|
96
|
+
identifier: exportedName,
|
|
97
|
+
file: filePath,
|
|
98
|
+
line,
|
|
99
|
+
exportType: "named"
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return exports;
|
|
105
|
+
}
|
|
106
|
+
function findUnusedSymbols(files) {
|
|
107
|
+
const suspicious = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const fileExports = extractExports(file.content, file.path);
|
|
110
|
+
for (const exp of fileExports) {
|
|
111
|
+
const pattern = new RegExp(`\\b${escapeRegex(exp.identifier)}\\b`, "g");
|
|
112
|
+
let usedElsewhere = false;
|
|
113
|
+
for (const other of files) {
|
|
114
|
+
if (other.path === file.path) continue;
|
|
115
|
+
if (pattern.test(other.stripped)) {
|
|
116
|
+
usedElsewhere = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!usedElsewhere) {
|
|
121
|
+
suspicious.push(exp);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return suspicious;
|
|
126
|
+
}
|
|
127
|
+
function scan(root, depth, cfg) {
|
|
128
|
+
const filePaths = gatherFiles(root, depth, cfg);
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const p of filePaths) {
|
|
131
|
+
try {
|
|
132
|
+
const content = readFileSync(p, "utf-8");
|
|
133
|
+
files.push({ path: p, content, stripped: stripNoise(content) });
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { findings: findUnusedSymbols(files), scannedFiles: files.length };
|
|
138
|
+
}
|
|
139
|
+
function withinProject(p) {
|
|
140
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
141
|
+
const root = process.cwd();
|
|
142
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
143
|
+
const rel = relative(root, resolved);
|
|
144
|
+
if (rel === "" || rel === ".") return true;
|
|
145
|
+
if (rel.startsWith("..")) return false;
|
|
146
|
+
if (isAbsolute(rel)) return false;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
function resolveScanRoot(rawPath) {
|
|
150
|
+
const resolved = resolve(process.cwd(), rawPath);
|
|
151
|
+
try {
|
|
152
|
+
const stats = statSync(resolved);
|
|
153
|
+
if (!stats.isDirectory()) {
|
|
154
|
+
return resolve(resolved, "..");
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return resolved;
|
|
159
|
+
}
|
|
160
|
+
function toPosix(p) {
|
|
161
|
+
return p.replace(/\\/g, "/");
|
|
162
|
+
}
|
|
163
|
+
function relativePath(p) {
|
|
164
|
+
return toPosix(relative(process.cwd(), p));
|
|
165
|
+
}
|
|
166
|
+
var plugin = {
|
|
167
|
+
name: "dead-code-detector",
|
|
168
|
+
version: "0.1.0",
|
|
169
|
+
description: "Lightweight regex-based scan for exported identifiers that appear unused anywhere in the project",
|
|
170
|
+
apiVersion: API_VERSION,
|
|
171
|
+
capabilities: { tools: true, hooks: true },
|
|
172
|
+
defaultConfig: { ...DEFAULTS },
|
|
173
|
+
configSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
enabled: {
|
|
177
|
+
type: "boolean",
|
|
178
|
+
default: false,
|
|
179
|
+
description: "Master switch."
|
|
180
|
+
},
|
|
181
|
+
extensions: {
|
|
182
|
+
type: "array",
|
|
183
|
+
items: { type: "string" },
|
|
184
|
+
default: [".ts", ".tsx", ".js", ".jsx"],
|
|
185
|
+
description: "File extensions to scan."
|
|
186
|
+
},
|
|
187
|
+
defaultDepth: {
|
|
188
|
+
type: "number",
|
|
189
|
+
minimum: 0,
|
|
190
|
+
maximum: 10,
|
|
191
|
+
default: 3,
|
|
192
|
+
description: "Default recursion depth for scans."
|
|
193
|
+
},
|
|
194
|
+
excludeDirs: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "string" },
|
|
197
|
+
default: ["node_modules", "dist", ".git", "coverage"],
|
|
198
|
+
description: "Directory names to skip while scanning."
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
setup(api) {
|
|
203
|
+
state.scanCount = 0;
|
|
204
|
+
state.hitCount = 0;
|
|
205
|
+
state.missCount = 0;
|
|
206
|
+
state.hookInvocationCount = 0;
|
|
207
|
+
state.warningCount = 0;
|
|
208
|
+
state.errorCount = 0;
|
|
209
|
+
if (state.hookUnregister) {
|
|
210
|
+
try {
|
|
211
|
+
state.hookUnregister();
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
state.hookUnregister = null;
|
|
215
|
+
}
|
|
216
|
+
const cfg = readConfig(api.config.extensions?.["dead-code-detector"]);
|
|
217
|
+
const hook = (input) => {
|
|
218
|
+
if (!cfg.enabled) return;
|
|
219
|
+
if (input.toolResult?.isError) return;
|
|
220
|
+
const inp = input.toolInput ?? {};
|
|
221
|
+
const sourcePath = inp["path"];
|
|
222
|
+
if (!sourcePath || typeof sourcePath !== "string") return;
|
|
223
|
+
if (!withinProject(sourcePath)) return;
|
|
224
|
+
const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
|
|
225
|
+
if (!cfg.extensions.includes(ext)) return;
|
|
226
|
+
state.hookInvocationCount += 1;
|
|
227
|
+
const scanRoot = resolveScanRoot(sourcePath);
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = scan(scanRoot, 1, cfg);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
state.errorCount += 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const changedFile = resolve(process.cwd(), sourcePath);
|
|
236
|
+
const relevant = result.findings.filter((f) => resolve(f.file) === changedFile);
|
|
237
|
+
if (relevant.length === 0) {
|
|
238
|
+
state.missCount += 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
state.hitCount += 1;
|
|
242
|
+
state.warningCount += relevant.length;
|
|
243
|
+
const list = relevant.map(
|
|
244
|
+
(f) => ` - ${f.identifier} (${f.exportType}) at ${relativePath(f.file)}:${f.line}`
|
|
245
|
+
).join("\n");
|
|
246
|
+
const message = `
|
|
247
|
+
\u26A0\uFE0F dead-code-detector: ${relevant.length} exported symbol(s) in ${sourcePath} look unused:
|
|
248
|
+
${list}
|
|
249
|
+
Consider removing the export if it is not part of the public API.`;
|
|
250
|
+
return { additionalContext: message };
|
|
251
|
+
};
|
|
252
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
253
|
+
api.tools.register({
|
|
254
|
+
name: "dead_code_scan",
|
|
255
|
+
description: "Scan TypeScript/JavaScript source files for exported symbols that appear unused anywhere in the project.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
path: {
|
|
260
|
+
type: "string",
|
|
261
|
+
default: ".",
|
|
262
|
+
description: "Directory or file path to scan."
|
|
263
|
+
},
|
|
264
|
+
depth: {
|
|
265
|
+
type: "number",
|
|
266
|
+
default: 3,
|
|
267
|
+
minimum: 0,
|
|
268
|
+
maximum: 10,
|
|
269
|
+
description: "Maximum recursion depth."
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
permission: "auto",
|
|
274
|
+
category: "Diagnostics",
|
|
275
|
+
mutating: false,
|
|
276
|
+
async execute(input) {
|
|
277
|
+
if (!cfg.enabled) return { ok: false, error: "dead-code-detector is disabled" };
|
|
278
|
+
const rawPath = typeof input.path === "string" ? input.path : ".";
|
|
279
|
+
const rawDepth = typeof input.depth === "number" ? input.depth : cfg.defaultDepth;
|
|
280
|
+
const depth = Math.max(0, Math.min(Math.floor(rawDepth), cfg.maxDepth));
|
|
281
|
+
if (!withinProject(rawPath)) {
|
|
282
|
+
return { ok: false, error: "scan path is outside the project root" };
|
|
283
|
+
}
|
|
284
|
+
state.scanCount += 1;
|
|
285
|
+
const scanRoot = resolveScanRoot(rawPath);
|
|
286
|
+
let result;
|
|
287
|
+
try {
|
|
288
|
+
result = scan(scanRoot, depth, cfg);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
state.errorCount += 1;
|
|
291
|
+
return { ok: false, error: String(err) };
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
scanRoot: relativePath(scanRoot),
|
|
296
|
+
depth,
|
|
297
|
+
scannedFiles: result.scannedFiles,
|
|
298
|
+
findings: result.findings.map((f) => ({
|
|
299
|
+
identifier: f.identifier,
|
|
300
|
+
file: relativePath(f.file),
|
|
301
|
+
line: f.line,
|
|
302
|
+
exportType: f.exportType
|
|
303
|
+
}))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
api.log.info("dead-code-detector plugin loaded", {
|
|
308
|
+
version: "0.1.0",
|
|
309
|
+
extensions: cfg.extensions,
|
|
310
|
+
defaultDepth: cfg.defaultDepth
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
teardown(api) {
|
|
314
|
+
if (state.hookUnregister) {
|
|
315
|
+
try {
|
|
316
|
+
state.hookUnregister();
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
state.hookUnregister = null;
|
|
320
|
+
}
|
|
321
|
+
const final = {
|
|
322
|
+
scans: state.scanCount,
|
|
323
|
+
hits: state.hitCount,
|
|
324
|
+
misses: state.missCount,
|
|
325
|
+
hookInvocations: state.hookInvocationCount,
|
|
326
|
+
warnings: state.warningCount,
|
|
327
|
+
errors: state.errorCount
|
|
328
|
+
};
|
|
329
|
+
state.scanCount = 0;
|
|
330
|
+
state.hitCount = 0;
|
|
331
|
+
state.missCount = 0;
|
|
332
|
+
state.hookInvocationCount = 0;
|
|
333
|
+
state.warningCount = 0;
|
|
334
|
+
state.errorCount = 0;
|
|
335
|
+
api.log.info("dead-code-detector: teardown complete", { final });
|
|
336
|
+
},
|
|
337
|
+
async health() {
|
|
338
|
+
return {
|
|
339
|
+
ok: state.errorCount === 0,
|
|
340
|
+
message: state.errorCount ? `dead-code-detector: ${state.errorCount} error(s)` : `dead-code-detector: ${state.scanCount} scan(s), ${state.hitCount} finding(s), ${state.warningCount} warning(s)`,
|
|
341
|
+
counters: {
|
|
342
|
+
scans: state.scanCount,
|
|
343
|
+
hits: state.hitCount,
|
|
344
|
+
misses: state.missCount,
|
|
345
|
+
hookInvocations: state.hookInvocationCount,
|
|
346
|
+
warnings: state.warningCount,
|
|
347
|
+
errors: state.errorCount
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var dead_code_detector_default = plugin;
|
|
353
|
+
|
|
354
|
+
export { dead_code_detector_default as default };
|
package/dist/dep-guard.js
CHANGED
|
@@ -4,6 +4,8 @@ var state = {
|
|
|
4
4
|
installsSeen: 0,
|
|
5
5
|
blocks: 0,
|
|
6
6
|
warns: 0,
|
|
7
|
+
llmConfirmCount: 0,
|
|
8
|
+
llmConfirmErrors: 0,
|
|
7
9
|
lastBlock: null,
|
|
8
10
|
hookUnregister: null
|
|
9
11
|
};
|
|
@@ -13,7 +15,8 @@ var DEFAULTS = {
|
|
|
13
15
|
deny: [],
|
|
14
16
|
allow: [],
|
|
15
17
|
warnOnUnpinned: false,
|
|
16
|
-
typosquatCheck: true
|
|
18
|
+
typosquatCheck: true,
|
|
19
|
+
confirmTyposquatsWithLlm: false
|
|
17
20
|
};
|
|
18
21
|
function readConfig(raw) {
|
|
19
22
|
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
@@ -25,7 +28,8 @@ function readConfig(raw) {
|
|
|
25
28
|
deny: strings(r["deny"]),
|
|
26
29
|
allow: strings(r["allow"]),
|
|
27
30
|
warnOnUnpinned: r["warnOnUnpinned"] === true,
|
|
28
|
-
typosquatCheck: r["typosquatCheck"] !== false
|
|
31
|
+
typosquatCheck: r["typosquatCheck"] !== false,
|
|
32
|
+
confirmTyposquatsWithLlm: r["confirmTyposquatsWithLlm"] === true
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)|(?:^|[;&|]\s*)(pip3?|uv)\s+(?:pip\s+)?install\s+([^;&|]+)|(?:^|[;&|]\s*)(cargo)\s+add\s+([^;&|]+)/gi;
|
|
@@ -167,6 +171,11 @@ var plugin = {
|
|
|
167
171
|
type: "boolean",
|
|
168
172
|
default: true,
|
|
169
173
|
description: "Warn when a package name is one edit away from a well-known package."
|
|
174
|
+
},
|
|
175
|
+
confirmTyposquatsWithLlm: {
|
|
176
|
+
type: "boolean",
|
|
177
|
+
default: false,
|
|
178
|
+
description: "Ask the host LLM to confirm whether a flagged typosquat is real-but-obscure or genuinely a typo. Appended to the warn context, never escalates to a block. Off by default."
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
181
|
},
|
|
@@ -175,6 +184,8 @@ var plugin = {
|
|
|
175
184
|
state.installsSeen = 0;
|
|
176
185
|
state.blocks = 0;
|
|
177
186
|
state.warns = 0;
|
|
187
|
+
state.llmConfirmCount = 0;
|
|
188
|
+
state.llmConfirmErrors = 0;
|
|
178
189
|
state.lastBlock = null;
|
|
179
190
|
if (state.hookUnregister) {
|
|
180
191
|
try {
|
|
@@ -184,7 +195,7 @@ var plugin = {
|
|
|
184
195
|
state.hookUnregister = null;
|
|
185
196
|
}
|
|
186
197
|
const cfg = readConfig(api.config.extensions?.["dep-guard"]);
|
|
187
|
-
const hook = (input) => {
|
|
198
|
+
const hook = async (input) => {
|
|
188
199
|
if (!cfg.enabled) return;
|
|
189
200
|
state.invocations += 1;
|
|
190
201
|
const ti = input.toolInput ?? {};
|
|
@@ -220,9 +231,31 @@ var plugin = {
|
|
|
220
231
|
if (cfg.typosquatCheck) {
|
|
221
232
|
const lookalike = typosquatOf(pkg.name);
|
|
222
233
|
if (lookalike) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
const baseNote = `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`;
|
|
235
|
+
if (cfg.confirmTyposquatsWithLlm && api.llm) {
|
|
236
|
+
try {
|
|
237
|
+
const verdict = await api.llm.complete(
|
|
238
|
+
`A user is installing the npm package "${pkg.name}" which is 1 edit away from the well-known "${lookalike}". Is "${pkg.name}" likely a typo of "${lookalike}" or a real-but-obscure package? Reply with ONE sentence starting with "TYPO:" or "REAL:".`,
|
|
239
|
+
{
|
|
240
|
+
system: "You are a supply-chain security assistant. Reply tersely with a single TYPO: or REAL: verdict.",
|
|
241
|
+
maxTokens: 80
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
const t = verdict.text.trim();
|
|
245
|
+
if (t) {
|
|
246
|
+
state.llmConfirmCount += 1;
|
|
247
|
+
api.metrics.counter("llm_confirm");
|
|
248
|
+
notes.push(`${baseNote} LLM verdict: ${t.slice(0, 300)}`);
|
|
249
|
+
} else {
|
|
250
|
+
notes.push(baseNote);
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
state.llmConfirmErrors += 1;
|
|
254
|
+
notes.push(baseNote);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
notes.push(baseNote);
|
|
258
|
+
}
|
|
226
259
|
}
|
|
227
260
|
}
|
|
228
261
|
if (cfg.warnOnUnpinned && pkg.version === null) {
|
|
@@ -263,6 +296,8 @@ ${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
|
263
296
|
counters: {
|
|
264
297
|
invocations: state.invocations,
|
|
265
298
|
installsSeen: state.installsSeen,
|
|
299
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
300
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
266
301
|
blocks: state.blocks,
|
|
267
302
|
warns: state.warns
|
|
268
303
|
},
|
|
@@ -288,6 +323,8 @@ ${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
|
288
323
|
const final = {
|
|
289
324
|
invocations: state.invocations,
|
|
290
325
|
installsSeen: state.installsSeen,
|
|
326
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
327
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
291
328
|
blocks: state.blocks,
|
|
292
329
|
warns: state.warns
|
|
293
330
|
};
|
|
@@ -295,6 +332,8 @@ ${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
|
295
332
|
state.installsSeen = 0;
|
|
296
333
|
state.blocks = 0;
|
|
297
334
|
state.warns = 0;
|
|
335
|
+
state.llmConfirmCount = 0;
|
|
336
|
+
state.llmConfirmErrors = 0;
|
|
298
337
|
state.lastBlock = null;
|
|
299
338
|
api.log.info("dep-guard: teardown complete", { final });
|
|
300
339
|
},
|
|
@@ -305,6 +344,8 @@ ${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
|
305
344
|
counters: {
|
|
306
345
|
invocations: state.invocations,
|
|
307
346
|
installsSeen: state.installsSeen,
|
|
347
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
348
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
308
349
|
blocks: state.blocks,
|
|
309
350
|
warns: state.warns
|
|
310
351
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dependency-vulnerability-gate plugin — PostToolUse hook that audits
|
|
5
|
+
* dependencies after install commands.
|
|
6
|
+
*
|
|
7
|
+
* Runs `npm audit --json` (or `pnpm audit --json` when pnpm-lock.yaml is
|
|
8
|
+
* present) after the `install` tool or bash/exec commands containing
|
|
9
|
+
* `npm install`/`pnpm add`/`yarn add`. Parses the audit report and blocks
|
|
10
|
+
* or warns when any vulnerability severity meets or exceeds the configured
|
|
11
|
+
* threshold.
|
|
12
|
+
*
|
|
13
|
+
* Tool registered:
|
|
14
|
+
* - dependency_audit_status : Show config + per-session counters.
|
|
15
|
+
*
|
|
16
|
+
* Hooks registered:
|
|
17
|
+
* - PostToolUse with matcher `install|bash|exec`.
|
|
18
|
+
*
|
|
19
|
+
* Config (`config.extensions['dependency-vulnerability-gate']`):
|
|
20
|
+
*
|
|
21
|
+
* ```jsonc
|
|
22
|
+
* {
|
|
23
|
+
* "enabled": true,
|
|
24
|
+
* "severityThreshold": "high", // "low" | "moderate" | "high" | "critical"
|
|
25
|
+
* "block": true, // true = block, false = warn
|
|
26
|
+
* "timeoutMs": 120000
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const plugin: Plugin;
|
|
34
|
+
|
|
35
|
+
export { plugin as default };
|