@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,325 @@
1
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { resolve, isAbsolute, relative, dirname } from 'path';
3
+
4
+ // src/knowledge-graph/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ facts: [],
8
+ nextId: 1,
9
+ adds: 0,
10
+ removals: 0,
11
+ queries: 0,
12
+ persistErrors: 0,
13
+ contributorUnregister: null
14
+ };
15
+ var DEFAULTS = {
16
+ enabled: true,
17
+ filePath: ".wrongstack/knowledge-graph.json",
18
+ maxFacts: 200,
19
+ maxFactChars: 300,
20
+ contributeToSystemPrompt: true,
21
+ contributeMaxChars: 1500
22
+ };
23
+ function resolveProjectPath(rawPath, cwd = process.cwd()) {
24
+ if (typeof rawPath !== "string" || rawPath.length === 0) return null;
25
+ const root = resolve(cwd);
26
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
27
+ const rel = relative(root, resolved);
28
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
29
+ return null;
30
+ }
31
+ function readConfig(raw) {
32
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
33
+ const r = raw;
34
+ return {
35
+ enabled: r["enabled"] !== false,
36
+ filePath: typeof r["filePath"] === "string" ? r["filePath"] : DEFAULTS.filePath,
37
+ maxFacts: typeof r["maxFacts"] === "number" && r["maxFacts"] >= 1 && r["maxFacts"] <= 2e3 ? r["maxFacts"] : DEFAULTS.maxFacts,
38
+ maxFactChars: typeof r["maxFactChars"] === "number" && r["maxFactChars"] >= 20 ? r["maxFactChars"] : DEFAULTS.maxFactChars,
39
+ contributeToSystemPrompt: r["contributeToSystemPrompt"] !== false,
40
+ contributeMaxChars: typeof r["contributeMaxChars"] === "number" && r["contributeMaxChars"] >= 100 ? r["contributeMaxChars"] : DEFAULTS.contributeMaxChars
41
+ };
42
+ }
43
+ function loadFacts(filePath) {
44
+ if (!filePath) return { facts: [], nextId: 1 };
45
+ try {
46
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
47
+ const facts = Array.isArray(raw.facts) ? raw.facts.filter(
48
+ (f) => !!f && typeof f === "object" && typeof f.id === "string" && typeof f.subject === "string" && typeof f.relation === "string" && typeof f.object === "string"
49
+ ) : [];
50
+ const nextId = typeof raw.nextId === "number" && raw.nextId >= 1 ? raw.nextId : facts.length + 1;
51
+ return { facts, nextId };
52
+ } catch {
53
+ return { facts: [], nextId: 1 };
54
+ }
55
+ }
56
+ function persistFacts(filePath) {
57
+ if (!filePath) return true;
58
+ try {
59
+ mkdirSync(dirname(filePath), { recursive: true });
60
+ writeFileSync(
61
+ filePath,
62
+ JSON.stringify({ facts: state.facts, nextId: state.nextId }, null, 2)
63
+ );
64
+ return true;
65
+ } catch {
66
+ state.persistErrors += 1;
67
+ return false;
68
+ }
69
+ }
70
+ var plugin = {
71
+ name: "knowledge-graph",
72
+ version: "0.1.0",
73
+ description: "Accumulates structured (subject, relation, object) facts about the project and queries them across sessions",
74
+ apiVersion: API_VERSION,
75
+ capabilities: { tools: true },
76
+ defaultConfig: { ...DEFAULTS },
77
+ configSchema: {
78
+ type: "object",
79
+ properties: {
80
+ enabled: { type: "boolean", default: true, description: "Master switch." },
81
+ filePath: {
82
+ type: "string",
83
+ default: ".wrongstack/knowledge-graph.json",
84
+ description: "Project-local JSON file where facts persist."
85
+ },
86
+ maxFacts: {
87
+ type: "number",
88
+ minimum: 1,
89
+ maximum: 2e3,
90
+ default: 200,
91
+ description: "Maximum number of facts stored."
92
+ },
93
+ maxFactChars: {
94
+ type: "number",
95
+ minimum: 20,
96
+ default: 300,
97
+ description: "Per-field length cap (subject, relation, object)."
98
+ },
99
+ contributeToSystemPrompt: {
100
+ type: "boolean",
101
+ default: true,
102
+ description: "Inject a compact fact summary into the system prompt."
103
+ },
104
+ contributeMaxChars: {
105
+ type: "number",
106
+ minimum: 100,
107
+ default: 1500,
108
+ description: "Maximum chars contributed to the system prompt."
109
+ }
110
+ }
111
+ },
112
+ setup(api) {
113
+ state.adds = 0;
114
+ state.removals = 0;
115
+ state.queries = 0;
116
+ state.persistErrors = 0;
117
+ if (state.contributorUnregister) {
118
+ try {
119
+ state.contributorUnregister();
120
+ } catch {
121
+ }
122
+ state.contributorUnregister = null;
123
+ }
124
+ const cfg = readConfig(api.config.extensions?.["knowledge-graph"]);
125
+ const resolved = resolveProjectPath(cfg.filePath) ?? "";
126
+ const loaded = resolved ? loadFacts(resolved) : { facts: [], nextId: 1 };
127
+ state.facts = loaded.facts.slice(0, cfg.maxFacts);
128
+ state.nextId = loaded.nextId;
129
+ if (cfg.enabled && cfg.contributeToSystemPrompt) {
130
+ state.contributorUnregister = api.registerSystemPromptContributor(async () => {
131
+ if (state.facts.length === 0) return [];
132
+ const recent = [...state.facts].filter((f) => f.confidence !== "low").sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)).slice(0, 20);
133
+ if (recent.length === 0) return [];
134
+ const lines = recent.map((f) => `- ${f.subject} ${f.relation} ${f.object}`);
135
+ const text = "[project_knowledge]\nRecorded facts about this project (most recent, medium/high confidence):\n" + lines.join("\n");
136
+ const truncated = text.length > cfg.contributeMaxChars ? text.slice(0, cfg.contributeMaxChars) + "\n[truncated]" : text;
137
+ return [{ type: "text", text: truncated }];
138
+ });
139
+ }
140
+ api.tools.register({
141
+ name: "kg_add_fact",
142
+ description: "Add a structured fact to the project knowledge graph. Facts persist across sessions.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ subject: { type: "string", description: "The entity the fact is about." },
147
+ relation: { type: "string", description: 'Relationship, e.g. "depends_on", "owned_by".' },
148
+ object: { type: "string", description: "The related entity or value." },
149
+ source: {
150
+ type: "string",
151
+ description: "Where this fact came from (file path, conversation, tool result)."
152
+ },
153
+ confidence: {
154
+ type: "string",
155
+ enum: ["low", "medium", "high"],
156
+ description: "How sure the agent is about this fact."
157
+ }
158
+ },
159
+ required: ["subject", "relation", "object"]
160
+ },
161
+ permission: "auto",
162
+ category: "Memory",
163
+ mutating: true,
164
+ async execute(input) {
165
+ if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
166
+ if (state.facts.length >= cfg.maxFacts) {
167
+ return {
168
+ ok: false,
169
+ error: `fact limit reached (${cfg.maxFacts}). Remove old facts first.`
170
+ };
171
+ }
172
+ const trim = (s) => String(s ?? "").trim().slice(0, cfg.maxFactChars);
173
+ const subject = trim(input.subject);
174
+ const relation = trim(input.relation);
175
+ const object = trim(input.object);
176
+ if (!subject || !relation || !object) {
177
+ return { ok: false, error: "subject, relation, and object are required" };
178
+ }
179
+ const fact = {
180
+ id: `kg-${state.nextId++}`,
181
+ subject,
182
+ relation,
183
+ object,
184
+ source: input.source ? String(input.source).slice(0, cfg.maxFactChars) : null,
185
+ confidence: input.confidence ?? "medium",
186
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
187
+ };
188
+ state.facts.push(fact);
189
+ state.adds += 1;
190
+ api.metrics.counter("adds");
191
+ const persisted = persistFacts(resolved);
192
+ return { ok: true, fact, persisted, totalFacts: state.facts.length };
193
+ }
194
+ });
195
+ api.tools.register({
196
+ name: "kg_query",
197
+ description: "Query the knowledge graph by subject, relation, object, or confidence. Returns matching facts.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ subject: { type: "string" },
202
+ relation: { type: "string" },
203
+ object: { type: "string" },
204
+ confidence: { type: "string", enum: ["low", "medium", "high"] },
205
+ limit: { type: "number", description: "Max results (default 20)." }
206
+ }
207
+ },
208
+ permission: "auto",
209
+ category: "Memory",
210
+ mutating: false,
211
+ async execute(input) {
212
+ if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
213
+ state.queries += 1;
214
+ const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : 20;
215
+ const matches = state.facts.filter((f) => {
216
+ if (input.subject && !f.subject.toLowerCase().includes(input.subject.toLowerCase())) return false;
217
+ if (input.relation && !f.relation.toLowerCase().includes(input.relation.toLowerCase())) return false;
218
+ if (input.object && !f.object.toLowerCase().includes(input.object.toLowerCase())) return false;
219
+ if (input.confidence && f.confidence !== input.confidence) return false;
220
+ return true;
221
+ });
222
+ return {
223
+ ok: true,
224
+ totalFacts: state.facts.length,
225
+ returned: Math.min(matches.length, limit),
226
+ facts: matches.slice(-limit)
227
+ };
228
+ }
229
+ });
230
+ api.tools.register({
231
+ name: "kg_remove_fact",
232
+ description: "Remove a fact from the knowledge graph by its id (kg-N).",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ id: { type: "string", description: "Fact id to remove." }
237
+ },
238
+ required: ["id"]
239
+ },
240
+ permission: "auto",
241
+ category: "Memory",
242
+ mutating: true,
243
+ async execute(input) {
244
+ if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
245
+ const before = state.facts.length;
246
+ state.facts = state.facts.filter((f) => f.id !== input.id);
247
+ const removed = before - state.facts.length;
248
+ if (removed === 0) return { ok: false, error: `no fact matches "${input.id}"` };
249
+ state.removals += removed;
250
+ api.metrics.counter("removals", removed);
251
+ const persisted = persistFacts(resolved);
252
+ return { ok: true, removed, persisted, totalFacts: state.facts.length };
253
+ }
254
+ });
255
+ api.tools.register({
256
+ name: "kg_status",
257
+ description: "Reports knowledge-graph state: fact count, persisted path, and counters.",
258
+ inputSchema: { type: "object", properties: {} },
259
+ permission: "auto",
260
+ category: "Memory",
261
+ mutating: false,
262
+ async execute() {
263
+ return {
264
+ ok: true,
265
+ enabled: cfg.enabled,
266
+ totalFacts: state.facts.length,
267
+ maxFacts: cfg.maxFacts,
268
+ filePath: cfg.filePath,
269
+ resolvedPath: resolved || null,
270
+ contributeToSystemPrompt: cfg.contributeToSystemPrompt,
271
+ counters: {
272
+ adds: state.adds,
273
+ removals: state.removals,
274
+ queries: state.queries,
275
+ persistErrors: state.persistErrors
276
+ }
277
+ };
278
+ }
279
+ });
280
+ api.log.info("knowledge-graph plugin loaded", {
281
+ version: "0.1.0",
282
+ factsLoaded: state.facts.length,
283
+ filePath: cfg.filePath
284
+ });
285
+ },
286
+ teardown(api) {
287
+ if (state.contributorUnregister) {
288
+ try {
289
+ state.contributorUnregister();
290
+ } catch {
291
+ }
292
+ state.contributorUnregister = null;
293
+ }
294
+ const final = {
295
+ facts: state.facts.length,
296
+ adds: state.adds,
297
+ removals: state.removals,
298
+ queries: state.queries,
299
+ persistErrors: state.persistErrors
300
+ };
301
+ state.facts = [];
302
+ state.nextId = 1;
303
+ state.adds = 0;
304
+ state.removals = 0;
305
+ state.queries = 0;
306
+ state.persistErrors = 0;
307
+ api.log.info("knowledge-graph: teardown complete", { final });
308
+ },
309
+ async health() {
310
+ return {
311
+ ok: state.persistErrors === 0,
312
+ message: `knowledge-graph: ${state.facts.length} fact(s), ${state.adds} add(s), ${state.queries} query(ies), ${state.persistErrors} persist error(s)`,
313
+ counters: {
314
+ facts: state.facts.length,
315
+ adds: state.adds,
316
+ removals: state.removals,
317
+ queries: state.queries,
318
+ persistErrors: state.persistErrors
319
+ }
320
+ };
321
+ }
322
+ };
323
+ var knowledge_graph_default = plugin;
324
+
325
+ export { knowledge_graph_default as default };
@@ -0,0 +1,34 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * license-audit-gate plugin — audits dependency licenses at install time.
5
+ *
6
+ * After every `bash` or `exec` tool call that looks like a package-manager
7
+ * install/add command (`npm i`, `pnpm add`, `yarn add`, `bun add`), the
8
+ * plugin reads `node_modules/<package>/package.json` for each newly added
9
+ * dependency, extracts its license field(s), and checks them against an
10
+ * allowlist. If any package's license is not on the allowlist, the hook
11
+ * either blocks the install (default) or injects a warning note.
12
+ *
13
+ * Tools registered:
14
+ * - license_audit_status : Show config + per-session counters.
15
+ *
16
+ * Hooks registered:
17
+ * - PostToolUse with matcher `bash|exec`.
18
+ *
19
+ * Config (`config.extensions['license-audit-gate']`):
20
+ *
21
+ * ```jsonc
22
+ * {
23
+ * "enabled": true,
24
+ * "allowedLicenses": ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "0BSD", "Unlicense"],
25
+ * "block": true
26
+ * }
27
+ * ```
28
+ *
29
+ * @public
30
+ */
31
+
32
+ declare const plugin: Plugin;
33
+
34
+ export { plugin as default };
@@ -0,0 +1,260 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+
4
+ // src/license-audit-gate/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ invocations: 0,
8
+ installsSeen: 0,
9
+ packagesAudited: 0,
10
+ allowedCount: 0,
11
+ deniedCount: 0,
12
+ blockedCount: 0,
13
+ errorCount: 0,
14
+ lastResult: null,
15
+ hookUnregister: null
16
+ };
17
+ var DEFAULTS = {
18
+ enabled: true,
19
+ allowedLicenses: ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "0BSD", "Unlicense"],
20
+ block: true
21
+ };
22
+ function normalizeStrings(v) {
23
+ if (!Array.isArray(v)) return [];
24
+ return v.filter((s) => typeof s === "string" && s.length > 0).map((s) => s.trim());
25
+ }
26
+ function readConfig(raw) {
27
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
28
+ const r = raw;
29
+ const allowed = normalizeStrings(r["allowedLicenses"]);
30
+ return {
31
+ enabled: r["enabled"] !== false,
32
+ allowedLicenses: allowed.length > 0 ? allowed : [...DEFAULTS.allowedLicenses],
33
+ block: r["block"] !== false
34
+ };
35
+ }
36
+ function extractLicenseStrings(pkg) {
37
+ if (!pkg || typeof pkg !== "object") return [];
38
+ const p = pkg;
39
+ const out = [];
40
+ const push = (v) => {
41
+ if (typeof v === "string" && v.trim()) {
42
+ out.push(v.trim());
43
+ } else if (v && typeof v === "object" && typeof v.type === "string") {
44
+ const t = v.type;
45
+ if (t.trim()) out.push(t.trim());
46
+ }
47
+ };
48
+ push(p["license"]);
49
+ if (Array.isArray(p["licenses"])) {
50
+ for (const entry of p["licenses"]) push(entry);
51
+ }
52
+ return [...new Set(out)];
53
+ }
54
+ var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/gi;
55
+ function parsePackageNames(command) {
56
+ const names = [];
57
+ INSTALL_RE.lastIndex = 0;
58
+ let m;
59
+ while ((m = INSTALL_RE.exec(command)) !== null) {
60
+ const argString = m[2] ?? "";
61
+ for (const token of argString.split(/\s+/)) {
62
+ if (!token || token.startsWith("-")) continue;
63
+ if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
64
+ const cleaned = token.replace(/^['"]|['"]$/g, "");
65
+ if (!cleaned) continue;
66
+ let name = cleaned;
67
+ const at = cleaned.lastIndexOf("@");
68
+ if (at > 0) name = cleaned.slice(0, at);
69
+ if (name) names.push(name);
70
+ }
71
+ }
72
+ return [...new Set(names)];
73
+ }
74
+ function auditPackages(names, allowedLicenses) {
75
+ const results = [];
76
+ const errors = [];
77
+ const normalizedAllowed = new Set(allowedLicenses.map((l) => l.toLowerCase()));
78
+ for (const name of names) {
79
+ let licenses = [];
80
+ try {
81
+ const pkgPath = resolve("node_modules", name, "package.json");
82
+ const raw = JSON.parse(readFileSync(pkgPath, "utf-8"));
83
+ licenses = extractLicenseStrings(raw);
84
+ } catch {
85
+ errors.push(name);
86
+ }
87
+ const allowed = licenses.length > 0 && licenses.every((l) => normalizedAllowed.has(l.toLowerCase()));
88
+ results.push({ name, licenses, allowed });
89
+ }
90
+ const ok = errors.length === 0 && results.every((r) => r.allowed);
91
+ return { ok, results, errors };
92
+ }
93
+ var plugin = {
94
+ name: "license-audit-gate",
95
+ version: "0.1.0",
96
+ description: "PostToolUse hook that audits dependency licenses after package-manager install/add commands and blocks disallowed licenses",
97
+ apiVersion: API_VERSION,
98
+ capabilities: { tools: true, hooks: true },
99
+ defaultConfig: { ...DEFAULTS },
100
+ configSchema: {
101
+ type: "object",
102
+ properties: {
103
+ enabled: {
104
+ type: "boolean",
105
+ default: true,
106
+ description: "Master switch."
107
+ },
108
+ allowedLicenses: {
109
+ type: "array",
110
+ items: { type: "string" },
111
+ default: DEFAULTS.allowedLicenses,
112
+ description: "List of allowed SPDX/license identifiers."
113
+ },
114
+ block: {
115
+ type: "boolean",
116
+ default: true,
117
+ description: "true = block the install when a disallowed license is found; false = inject a warning note."
118
+ }
119
+ }
120
+ },
121
+ setup(api) {
122
+ state.invocations = 0;
123
+ state.installsSeen = 0;
124
+ state.packagesAudited = 0;
125
+ state.allowedCount = 0;
126
+ state.deniedCount = 0;
127
+ state.blockedCount = 0;
128
+ state.errorCount = 0;
129
+ state.lastResult = null;
130
+ if (state.hookUnregister) {
131
+ try {
132
+ state.hookUnregister();
133
+ } catch {
134
+ }
135
+ state.hookUnregister = null;
136
+ }
137
+ const cfg = readConfig(api.config.extensions?.["license-audit-gate"]);
138
+ const hook = (input) => {
139
+ if (!cfg.enabled) return;
140
+ if (input.toolResult?.isError) return;
141
+ const ti = input.toolInput ?? {};
142
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
143
+ if (!command) return;
144
+ state.invocations += 1;
145
+ const names = parsePackageNames(command);
146
+ if (names.length === 0) return;
147
+ state.installsSeen += 1;
148
+ const audit = auditPackages(names, cfg.allowedLicenses);
149
+ state.packagesAudited += names.length;
150
+ const denied = audit.results.filter((r) => !r.allowed).map((r) => r.name);
151
+ state.allowedCount += audit.results.filter((r) => r.allowed).length;
152
+ state.deniedCount += denied.length;
153
+ state.errorCount += audit.errors.length;
154
+ state.lastResult = {
155
+ passed: audit.ok,
156
+ denied,
157
+ when: (/* @__PURE__ */ new Date()).toISOString()
158
+ };
159
+ if (audit.ok) return;
160
+ const lines = audit.results.map((r) => {
161
+ const licenseText = r.licenses.length > 0 ? r.licenses.join(", ") : "no license found";
162
+ if (r.allowed) return ` \u2705 ${r.name}: ${licenseText}`;
163
+ return ` \u274C ${r.name}: ${licenseText} (not in allowlist)`;
164
+ });
165
+ if (audit.errors.length > 0) {
166
+ lines.push(` \u26A0\uFE0F could not read package.json for: ${audit.errors.join(", ")}`);
167
+ }
168
+ const message = `license-audit-gate: dependency license check failed for newly added packages.
169
+ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
170
+ ` + lines.join("\n");
171
+ if (cfg.block) {
172
+ state.blockedCount += 1;
173
+ return {
174
+ decision: "block",
175
+ reason: message,
176
+ additionalContext: message
177
+ };
178
+ }
179
+ return { additionalContext: message };
180
+ };
181
+ state.hookUnregister = api.registerHook("PostToolUse", "bash|exec", hook);
182
+ api.tools.register({
183
+ name: "license_audit_status",
184
+ description: "Reports license-audit-gate state: allowlist, block mode, and per-session audit counters.",
185
+ inputSchema: { type: "object", properties: {} },
186
+ permission: "auto",
187
+ category: "Diagnostics",
188
+ mutating: false,
189
+ async execute() {
190
+ return {
191
+ ok: true,
192
+ enabled: cfg.enabled,
193
+ allowedLicenses: cfg.allowedLicenses,
194
+ block: cfg.block,
195
+ counters: {
196
+ invocations: state.invocations,
197
+ installsSeen: state.installsSeen,
198
+ packagesAudited: state.packagesAudited,
199
+ allowed: state.allowedCount,
200
+ denied: state.deniedCount,
201
+ blocked: state.blockedCount,
202
+ errors: state.errorCount
203
+ },
204
+ lastResult: state.lastResult
205
+ };
206
+ }
207
+ });
208
+ api.log.info("license-audit-gate plugin loaded", {
209
+ version: "0.1.0",
210
+ allowedLicensesCount: cfg.allowedLicenses.length,
211
+ block: cfg.block
212
+ });
213
+ },
214
+ teardown(api) {
215
+ if (state.hookUnregister) {
216
+ try {
217
+ state.hookUnregister();
218
+ } catch {
219
+ }
220
+ state.hookUnregister = null;
221
+ }
222
+ const final = {
223
+ invocations: state.invocations,
224
+ installsSeen: state.installsSeen,
225
+ packagesAudited: state.packagesAudited,
226
+ allowed: state.allowedCount,
227
+ denied: state.deniedCount,
228
+ blocked: state.blockedCount,
229
+ errors: state.errorCount
230
+ };
231
+ state.invocations = 0;
232
+ state.installsSeen = 0;
233
+ state.packagesAudited = 0;
234
+ state.allowedCount = 0;
235
+ state.deniedCount = 0;
236
+ state.blockedCount = 0;
237
+ state.errorCount = 0;
238
+ state.lastResult = null;
239
+ api.log.info("license-audit-gate: teardown complete", { final });
240
+ },
241
+ async health() {
242
+ return {
243
+ ok: true,
244
+ message: state.lastResult ? `license-audit-gate: ${state.installsSeen} install command(s) seen, ${state.deniedCount} denied, ${state.blockedCount} blocked` : `license-audit-gate: ${state.invocations} invocation(s), ${state.packagesAudited} package(s) audited`,
245
+ counters: {
246
+ invocations: state.invocations,
247
+ installsSeen: state.installsSeen,
248
+ packagesAudited: state.packagesAudited,
249
+ allowed: state.allowedCount,
250
+ denied: state.deniedCount,
251
+ blocked: state.blockedCount,
252
+ errors: state.errorCount
253
+ },
254
+ lastResult: state.lastResult
255
+ };
256
+ }
257
+ };
258
+ var license_audit_gate_default = plugin;
259
+
260
+ export { license_audit_gate_default as default };
package/dist/llm-cache.js CHANGED
@@ -125,6 +125,11 @@ var plugin = {
125
125
  }
126
126
  const cfg = readConfig(api.config.extensions?.["llm-cache"]);
127
127
  if (cfg.enabled) {
128
+ api.emitCustom?.("provider.wrap:loaded", {
129
+ plugin: "llm-cache",
130
+ kind: "cache",
131
+ wraps: ["request"]
132
+ });
128
133
  state.extensionUnregister = api.extensions.register({
129
134
  name: "llm-cache",
130
135
  owner: "llm-cache",
@@ -1,43 +1,5 @@
1
1
  import { Plugin } from '@wrongstack/core';
2
2
 
3
- /**
4
- * loop-breaker plugin — detects runaway tool-call loops and breaks them.
5
- *
6
- * Agents occasionally get stuck re-issuing the same tool call with the
7
- * same input (a failing bash command retried forever, re-reading the
8
- * same file, re-writing identical content). Each repeat burns tokens
9
- * and wall-clock without making progress. This plugin fingerprints
10
- * every tool call (`toolName` + canonicalized input JSON) via a
11
- * `PreToolUse '*'` hook and tracks consecutive repeats:
12
- *
13
- * - at `warnAfter` repeats → inject `additionalContext` telling the
14
- * model it is looping and should change approach
15
- * - at `blockAfter` repeats → block the call outright with a clear
16
- * reason (unless `mode: 'warn'`)
17
- *
18
- * Any *different* call resets the streak, so normal workflows (many
19
- * distinct reads/edits) are never touched. A small LRU of recent
20
- * fingerprints also catches A-B-A-B oscillation loops.
21
- *
22
- * Config (`config.extensions['loop-breaker']`):
23
- *
24
- * ```jsonc
25
- * {
26
- * "enabled": true,
27
- * "mode": "block", // "block" | "warn"
28
- * "warnAfter": 3, // consecutive identical calls before warning
29
- * "blockAfter": 5, // consecutive identical calls before blocking
30
- * "oscillationWindow": 8, // recent-call window for A-B-A-B detection
31
- * "ignoreTools": [] // tool names exempt from loop detection
32
- * }
33
- * ```
34
- *
35
- * Toggle off entirely with `{ "name": "loop-breaker", "enabled": false }`
36
- * in `config.plugins`, or `"enabled": false` in the options above.
37
- *
38
- * @public
39
- */
40
-
41
3
  declare const plugin: Plugin;
42
4
 
43
5
  export { plugin as default };