@wrongstack/plugins 0.281.3 → 0.282.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 (84) hide show
  1. package/README.md +30 -4
  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-escalate.d.ts +1 -1
  9. package/dist/auto-i18n-extractor.d.ts +36 -0
  10. package/dist/auto-i18n-extractor.js +335 -0
  11. package/dist/branch-guard.d.ts +6 -5
  12. package/dist/branch-guard.js +54 -4
  13. package/dist/checkpoint.js +18 -0
  14. package/dist/code-metrics.d.ts +31 -0
  15. package/dist/code-metrics.js +338 -0
  16. package/dist/commit-validator.js +67 -12
  17. package/dist/context-pins.js +4 -4
  18. package/dist/cost-tracker.d.ts +1 -1
  19. package/dist/cost-tracker.js +58 -19
  20. package/dist/cron.js +18 -8
  21. package/dist/dead-code-detector.d.ts +34 -0
  22. package/dist/dead-code-detector.js +354 -0
  23. package/dist/dep-guard.js +47 -6
  24. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  25. package/dist/dependency-vulnerability-gate.js +308 -0
  26. package/dist/diff-summary.js +97 -10
  27. package/dist/doc-sync-guard.d.ts +33 -0
  28. package/dist/doc-sync-guard.js +223 -0
  29. package/dist/duplicate-code-detector.d.ts +33 -0
  30. package/dist/duplicate-code-detector.js +384 -0
  31. package/dist/feature-flag-tracker.d.ts +38 -0
  32. package/dist/feature-flag-tracker.js +316 -0
  33. package/dist/file-watcher.js +85 -36
  34. package/dist/format-on-save.js +99 -18
  35. package/dist/import-organizer.js +73 -14
  36. package/dist/index.d.ts +27 -0
  37. package/dist/index.js +11205 -2106
  38. package/dist/interface-contract-guard.d.ts +37 -0
  39. package/dist/interface-contract-guard.js +302 -0
  40. package/dist/knowledge-graph.d.ts +45 -0
  41. package/dist/knowledge-graph.js +325 -0
  42. package/dist/license-audit-gate.d.ts +34 -0
  43. package/dist/license-audit-gate.js +260 -0
  44. package/dist/llm-cache.js +5 -0
  45. package/dist/loop-breaker.d.ts +0 -38
  46. package/dist/loop-breaker.js +209 -8
  47. package/dist/migration-planner.d.ts +30 -0
  48. package/dist/migration-planner.js +349 -0
  49. package/dist/model-router.js +5 -0
  50. package/dist/performance-regression-gate.d.ts +33 -0
  51. package/dist/performance-regression-gate.js +315 -0
  52. package/dist/plugin-stack-observer.d.ts +35 -0
  53. package/dist/plugin-stack-observer.js +138 -0
  54. package/dist/pr-drafter.d.ts +35 -0
  55. package/dist/pr-drafter.js +334 -0
  56. package/dist/prompt-firewall.js +5 -0
  57. package/dist/refactor-suggester.d.ts +38 -0
  58. package/dist/refactor-suggester.js +382 -0
  59. package/dist/release-notes-generator.d.ts +27 -0
  60. package/dist/release-notes-generator.js +209 -0
  61. package/dist/schema-evolution-guard.d.ts +42 -0
  62. package/dist/schema-evolution-guard.js +319 -0
  63. package/dist/security-hotspot-scanner.d.ts +30 -0
  64. package/dist/security-hotspot-scanner.js +402 -0
  65. package/dist/semantic-search-indexer.d.ts +38 -0
  66. package/dist/semantic-search-indexer.js +436 -0
  67. package/dist/shell-check.js +38 -3
  68. package/dist/smart-rename.d.ts +26 -0
  69. package/dist/smart-rename.js +170 -0
  70. package/dist/spec-linker.js +273 -133
  71. package/dist/test-coverage-gate.d.ts +37 -0
  72. package/dist/test-coverage-gate.js +263 -0
  73. package/dist/test-flake-detector.d.ts +27 -0
  74. package/dist/test-flake-detector.js +274 -0
  75. package/dist/test-generator.d.ts +32 -0
  76. package/dist/test-generator.js +243 -0
  77. package/dist/test-runner-gate.js +154 -22
  78. package/dist/todo-listener.d.ts +2 -2
  79. package/dist/todo-listener.js +5 -5
  80. package/dist/token-throttle.js +5 -0
  81. package/dist/type-gate.d.ts +37 -0
  82. package/dist/type-gate.js +311 -0
  83. package/package.json +112 -4
  84. package/LICENSE +0 -21
@@ -0,0 +1,402 @@
1
+ import { statSync, readdirSync, readFileSync } from 'fs';
2
+ import { isAbsolute, resolve, relative } from 'path';
3
+
4
+ // src/security-hotspot-scanner/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ scanCount: 0,
8
+ fileScanCount: 0,
9
+ findingCount: 0,
10
+ hookInvocationCount: 0,
11
+ skippedCount: 0,
12
+ blockedCount: 0,
13
+ lastResult: null,
14
+ knownHotspots: /* @__PURE__ */ new Set(),
15
+ hookUnregister: null
16
+ };
17
+ var DEFAULTS = {
18
+ enabled: false,
19
+ severity: "warn",
20
+ maxFindings: 10,
21
+ scanOnChange: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".java", ".go", ".rb", ".php"]
22
+ };
23
+ function readConfig(raw) {
24
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
25
+ const r = raw;
26
+ return {
27
+ enabled: r["enabled"] === true,
28
+ severity: r["severity"] === "block" ? "block" : DEFAULTS.severity,
29
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 100 ? r["maxFindings"] : DEFAULTS.maxFindings,
30
+ scanOnChange: Array.isArray(r["scanOnChange"]) ? r["scanOnChange"].filter((x) => typeof x === "string") : DEFAULTS.scanOnChange
31
+ };
32
+ }
33
+ var PATTERNS = [
34
+ { type: "eval_call", severity: "high", regex: /\beval\s*\(/i },
35
+ { type: "function_constructor", severity: "high", regex: /\bnew\s+Function\s*\(|\bFunction\s*\(/i },
36
+ {
37
+ type: "unsafe_html",
38
+ severity: "high",
39
+ regex: /\binnerHTML\s*=|dangerouslySetInnerHTML/i
40
+ },
41
+ {
42
+ type: "sql_concatenation",
43
+ severity: "high",
44
+ regex: /\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b[^;]{0,120}(\+|\$\{|\.concat\s*\()/i
45
+ },
46
+ {
47
+ type: "hardcoded_http",
48
+ severity: "medium",
49
+ regex: /http:\/\/[a-zA-Z0-9][^\s'"\)\\]*/i
50
+ },
51
+ {
52
+ type: "console_log_credentials",
53
+ severity: "medium",
54
+ regex: /\bconsole\.log\s*\([^)]{0,120}(?:password|token|secret|key|credential|apikey|auth)/i
55
+ },
56
+ {
57
+ type: "exec_variable_command",
58
+ severity: "high",
59
+ regex: /\bexec\s*\(\s*(?:`[^`]*\$\{[^}]*\}[^`]*`|[a-zA-Z_$][\w$]*)/i
60
+ }
61
+ ];
62
+ function scanSource(content, maxFindings) {
63
+ const findings = [];
64
+ const lines = content.split(/\r?\n/);
65
+ for (let i = 0; i < lines.length; i += 1) {
66
+ const line = lines[i];
67
+ for (const pattern of PATTERNS) {
68
+ const match = pattern.regex.exec(line);
69
+ if (match) {
70
+ findings.push({
71
+ type: pattern.type,
72
+ severity: pattern.severity,
73
+ line: i + 1,
74
+ snippet: line.trim().slice(0, 160)
75
+ });
76
+ if (findings.length >= maxFindings) return findings;
77
+ }
78
+ }
79
+ }
80
+ return findings;
81
+ }
82
+ function isSourceFile(filePath, extensions) {
83
+ const lower = filePath.toLowerCase();
84
+ return extensions.some((ext) => lower.endsWith(ext));
85
+ }
86
+ function withinProject(p) {
87
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
88
+ const root = process.cwd();
89
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
90
+ const rel = relative(root, resolved);
91
+ if (rel === "" || rel === ".") return true;
92
+ if (rel.startsWith("..")) return false;
93
+ if (isAbsolute(rel)) return false;
94
+ return true;
95
+ }
96
+ function scanPath(inputPath, cfg) {
97
+ const start = Date.now();
98
+ const root = process.cwd();
99
+ const resolved = isAbsolute(inputPath) ? resolve(inputPath) : resolve(root, inputPath);
100
+ if (!withinProject(inputPath)) {
101
+ return {
102
+ path: inputPath,
103
+ scanned: false,
104
+ findings: [],
105
+ filesScanned: 0,
106
+ error: "path is outside the project directory",
107
+ durationMs: Date.now() - start
108
+ };
109
+ }
110
+ let filesScanned = 0;
111
+ const allFindings = [];
112
+ const maxPerFile = Math.max(1, Math.floor(cfg.maxFindings / 2));
113
+ const scanFile = (filePath) => {
114
+ if (!isSourceFile(filePath, cfg.scanOnChange)) return;
115
+ try {
116
+ const content = readFileSync(filePath, "utf-8");
117
+ filesScanned += 1;
118
+ const findings = scanSource(content, maxPerFile);
119
+ for (const f of findings) {
120
+ allFindings.push({ ...f, snippet: `${relative(root, filePath)}:${f.line}: ${f.snippet}` });
121
+ if (allFindings.length >= cfg.maxFindings) return;
122
+ }
123
+ } catch {
124
+ }
125
+ };
126
+ const walk = (dir) => {
127
+ let entries;
128
+ try {
129
+ entries = readdirSync(dir);
130
+ } catch {
131
+ return;
132
+ }
133
+ for (const entry of entries) {
134
+ if (allFindings.length >= cfg.maxFindings) return;
135
+ const full = resolve(dir, entry);
136
+ try {
137
+ const st = statSync(full);
138
+ if (st.isDirectory()) {
139
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "coverage") {
140
+ continue;
141
+ }
142
+ walk(full);
143
+ } else if (st.isFile()) {
144
+ scanFile(full);
145
+ }
146
+ } catch {
147
+ }
148
+ }
149
+ };
150
+ try {
151
+ const st = statSync(resolved);
152
+ if (st.isDirectory()) {
153
+ walk(resolved);
154
+ } else if (st.isFile()) {
155
+ scanFile(resolved);
156
+ } else {
157
+ return {
158
+ path: inputPath,
159
+ scanned: false,
160
+ findings: [],
161
+ filesScanned: 0,
162
+ error: "path is not a file or directory",
163
+ durationMs: Date.now() - start
164
+ };
165
+ }
166
+ } catch {
167
+ return {
168
+ path: inputPath,
169
+ scanned: false,
170
+ findings: [],
171
+ filesScanned: 0,
172
+ error: "path does not exist or cannot be read",
173
+ durationMs: Date.now() - start
174
+ };
175
+ }
176
+ return {
177
+ path: inputPath,
178
+ scanned: true,
179
+ findings: allFindings,
180
+ filesScanned,
181
+ durationMs: Date.now() - start
182
+ };
183
+ }
184
+ var plugin = {
185
+ name: "security-hotspot-scanner",
186
+ version: "0.1.0",
187
+ description: "Scans source code for security anti-patterns and warns after writes/edits that introduce new hotspots",
188
+ apiVersion: API_VERSION,
189
+ capabilities: { tools: true, hooks: true },
190
+ defaultConfig: { ...DEFAULTS },
191
+ configSchema: {
192
+ type: "object",
193
+ properties: {
194
+ enabled: { type: "boolean", default: false, description: "Master switch." },
195
+ severity: {
196
+ type: "string",
197
+ enum: ["warn", "block"],
198
+ default: "warn",
199
+ description: "warn = inject hotspots as additionalContext; block = refuse the mutating tool."
200
+ },
201
+ maxFindings: {
202
+ type: "number",
203
+ minimum: 1,
204
+ maximum: 100,
205
+ default: 10,
206
+ description: "Maximum hotspots reported per scan."
207
+ },
208
+ scanOnChange: {
209
+ type: "array",
210
+ items: { type: "string" },
211
+ default: DEFAULTS.scanOnChange,
212
+ description: "Only scan files with one of these extensions."
213
+ }
214
+ }
215
+ },
216
+ setup(api) {
217
+ state.scanCount = 0;
218
+ state.fileScanCount = 0;
219
+ state.findingCount = 0;
220
+ state.hookInvocationCount = 0;
221
+ state.skippedCount = 0;
222
+ state.blockedCount = 0;
223
+ state.lastResult = null;
224
+ state.knownHotspots.clear();
225
+ if (state.hookUnregister) {
226
+ try {
227
+ state.hookUnregister();
228
+ } catch {
229
+ }
230
+ state.hookUnregister = null;
231
+ }
232
+ const cfg = readConfig(api.config.extensions?.["security-hotspot-scanner"]);
233
+ const hook = (input) => {
234
+ if (!cfg.enabled) return;
235
+ if (input.toolResult?.isError) return;
236
+ const inp = input.toolInput ?? {};
237
+ const sourcePath = inp["path"];
238
+ if (!sourcePath || typeof sourcePath !== "string") return;
239
+ if (!withinProject(sourcePath)) return;
240
+ const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
241
+ if (!cfg.scanOnChange.includes(ext)) {
242
+ state.skippedCount += 1;
243
+ return;
244
+ }
245
+ state.hookInvocationCount += 1;
246
+ const result = scanPath(sourcePath, cfg);
247
+ state.scanCount += 1;
248
+ state.fileScanCount += result.filesScanned;
249
+ if (!result.scanned) {
250
+ return;
251
+ }
252
+ const newFindings = result.findings.filter((f) => {
253
+ const key = `${result.path}:${f.type}:${f.line}`;
254
+ if (state.knownHotspots.has(key)) return false;
255
+ state.knownHotspots.add(key);
256
+ return true;
257
+ });
258
+ if (newFindings.length === 0) return;
259
+ state.findingCount += newFindings.length;
260
+ state.lastResult = {
261
+ path: result.path,
262
+ findings: newFindings.length,
263
+ durationMs: result.durationMs,
264
+ when: (/* @__PURE__ */ new Date()).toISOString()
265
+ };
266
+ const lines = newFindings.map(
267
+ (f) => ` [${f.severity.toUpperCase()}] ${f.type} at line ${f.line}: ${f.snippet}`
268
+ );
269
+ const message = `
270
+ \u{1F6E1}\uFE0F security-hotspot-scanner: ${newFindings.length} new security hotspot(s) introduced by editing ${sourcePath}.
271
+ ${lines.join("\n")}` + (result.findings.length >= cfg.maxFindings ? `
272
+ \u2026 and possibly more (showing first ${cfg.maxFindings})` : "") + `
273
+ Review or remove the risky pattern(s).`;
274
+ if (cfg.severity === "block") {
275
+ state.blockedCount += 1;
276
+ return {
277
+ decision: "block",
278
+ reason: message
279
+ };
280
+ }
281
+ return { additionalContext: message };
282
+ };
283
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
284
+ api.tools.register({
285
+ name: "security_hotspot_scan",
286
+ description: "Scan a file or directory for security anti-patterns (eval, Function constructor, innerHTML, SQL concatenation, http URLs, credential logging, variable exec commands).",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ path: {
291
+ type: "string",
292
+ description: "File or directory path to scan (relative or absolute, must be inside the project)."
293
+ }
294
+ },
295
+ required: ["path"]
296
+ },
297
+ permission: "auto",
298
+ category: "Security",
299
+ mutating: false,
300
+ async execute(input) {
301
+ if (!cfg.enabled) return { ok: false, error: "security-hotspot-scanner is disabled" };
302
+ const result = scanPath(input.path, cfg);
303
+ state.scanCount += 1;
304
+ state.fileScanCount += result.filesScanned;
305
+ state.findingCount += result.findings.length;
306
+ if (!result.scanned) {
307
+ return { ok: false, error: result.error, path: input.path };
308
+ }
309
+ state.lastResult = {
310
+ path: result.path,
311
+ findings: result.findings.length,
312
+ durationMs: result.durationMs,
313
+ when: (/* @__PURE__ */ new Date()).toISOString()
314
+ };
315
+ return {
316
+ ok: true,
317
+ path: input.path,
318
+ filesScanned: result.filesScanned,
319
+ findings: result.findings,
320
+ findingCount: result.findings.length,
321
+ durationMs: result.durationMs
322
+ };
323
+ }
324
+ });
325
+ api.tools.register({
326
+ name: "security_hotspot_status",
327
+ description: "Reports security-hotspot-scanner state: severity, extensions, pattern types, and per-session counters.",
328
+ inputSchema: { type: "object", properties: {} },
329
+ permission: "auto",
330
+ category: "Security",
331
+ mutating: false,
332
+ async execute() {
333
+ return {
334
+ ok: true,
335
+ enabled: cfg.enabled,
336
+ severity: cfg.severity,
337
+ maxFindings: cfg.maxFindings,
338
+ scanOnChange: cfg.scanOnChange,
339
+ patternTypes: PATTERNS.map((p) => p.type),
340
+ counters: {
341
+ scans: state.scanCount,
342
+ filesScanned: state.fileScanCount,
343
+ findings: state.findingCount,
344
+ hookInvocations: state.hookInvocationCount,
345
+ skipped: state.skippedCount,
346
+ blocked: state.blockedCount
347
+ },
348
+ lastResult: state.lastResult
349
+ };
350
+ }
351
+ });
352
+ api.log.info("security-hotspot-scanner plugin loaded", {
353
+ version: "0.1.0",
354
+ severity: cfg.severity,
355
+ maxFindings: cfg.maxFindings
356
+ });
357
+ },
358
+ teardown(api) {
359
+ if (state.hookUnregister) {
360
+ try {
361
+ state.hookUnregister();
362
+ } catch {
363
+ }
364
+ state.hookUnregister = null;
365
+ }
366
+ const final = {
367
+ scans: state.scanCount,
368
+ filesScanned: state.fileScanCount,
369
+ findings: state.findingCount,
370
+ hookInvocations: state.hookInvocationCount,
371
+ skipped: state.skippedCount,
372
+ blocked: state.blockedCount
373
+ };
374
+ state.scanCount = 0;
375
+ state.fileScanCount = 0;
376
+ state.findingCount = 0;
377
+ state.hookInvocationCount = 0;
378
+ state.skippedCount = 0;
379
+ state.blockedCount = 0;
380
+ state.lastResult = null;
381
+ state.knownHotspots.clear();
382
+ api.log.info("security-hotspot-scanner: teardown complete", { final });
383
+ },
384
+ async health() {
385
+ return {
386
+ ok: true,
387
+ message: state.lastResult ? `security-hotspot-scanner: ${state.scanCount} scan(s), last scan found ${state.lastResult.findings} hotspot(s)` : `security-hotspot-scanner: ${state.scanCount} scan(s), ${state.findingCount} finding(s)`,
388
+ counters: {
389
+ scans: state.scanCount,
390
+ filesScanned: state.fileScanCount,
391
+ findings: state.findingCount,
392
+ hookInvocations: state.hookInvocationCount,
393
+ skipped: state.skippedCount,
394
+ blocked: state.blockedCount
395
+ },
396
+ lastResult: state.lastResult
397
+ };
398
+ }
399
+ };
400
+ var security_hotspot_scanner_default = plugin;
401
+
402
+ export { security_hotspot_scanner_default as default };
@@ -0,0 +1,38 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * semantic-search-indexer plugin — lightweight keyword-based search over
5
+ * project source files.
6
+ *
7
+ * The plugin builds an in-memory inverted index (no persistence, no
8
+ * embeddings) and exposes two tools:
9
+ *
10
+ * - `semantic_search` — ranked keyword search with TF scoring
11
+ * - `semantic_index_status` — index counters and configuration
12
+ *
13
+ * Files are tokenized into lowercase alphanumeric terms and indexed with
14
+ * per-document term frequencies. Queries are scored by summing the TF of
15
+ * each matched query token, then results are returned with the matching
16
+ * file path, score, and a handful of matched lines.
17
+ *
18
+ * Config (`config.extensions['semantic-search-indexer']`):
19
+ *
20
+ * ```jsonc
21
+ * {
22
+ * "enabled": true,
23
+ * "includeExtensions": [".ts", ".tsx", ".js", ".jsx"],
24
+ * "excludePatterns": ["node_modules", "\\.git", "dist"],
25
+ * "maxFileBytes": 1_000_000,
26
+ * "defaultLimit": 10,
27
+ * "minTokenLength": 2,
28
+ * "maxMatchesPerFile": 10,
29
+ * "maxFiles": 5000
30
+ * }
31
+ * ```
32
+ *
33
+ * @public
34
+ */
35
+
36
+ declare const plugin: Plugin;
37
+
38
+ export { plugin as default };