@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.
- package/README.md +30 -4
- 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-escalate.d.ts +1 -1
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/branch-guard.d.ts +6 -5
- package/dist/branch-guard.js +54 -4
- 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/context-pins.js +4 -4
- package/dist/cost-tracker.d.ts +1 -1
- package/dist/cost-tracker.js +58 -19
- package/dist/cron.js +18 -8
- 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 +99 -18
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +11205 -2106
- 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 +138 -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,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",
|
package/dist/loop-breaker.d.ts
CHANGED
|
@@ -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 };
|