@wrongstack/plugins 0.277.1 → 0.280.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.
Files changed (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
@@ -14,6 +14,9 @@ var PRICING = {
14
14
  "default": { input: 5, output: 15 }
15
15
  };
16
16
  var DEFAULT_PRICING = { input: 5, output: 15 };
17
+ var pricingOverrides = {};
18
+ var bundledFromRegistry = {};
19
+ var lastCost = { usd: 0, model: null, at: null };
17
20
  function readCostTrackerConfig(raw) {
18
21
  return {
19
22
  budgetLimit: typeof raw?.["budgetLimit"] === "number" ? raw["budgetLimit"] : 0,
@@ -21,7 +24,8 @@ function readCostTrackerConfig(raw) {
21
24
  };
22
25
  }
23
26
  function estimateCost(model, promptTokens, completionTokens) {
24
- const pricing = PRICING[model.toLowerCase()] ?? DEFAULT_PRICING;
27
+ const key = model.toLowerCase();
28
+ const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
25
29
  const inputCost = promptTokens / 1e6 * pricing.input;
26
30
  const outputCost = completionTokens / 1e6 * pricing.output;
27
31
  return inputCost + outputCost;
@@ -36,7 +40,8 @@ var plugin = {
36
40
  trackPerModel: true,
37
41
  trackPerUser: false,
38
42
  budgetLimit: 0,
39
- warningThreshold: 80
43
+ warningThreshold: 80,
44
+ pricingOverrides: {}
40
45
  },
41
46
  configSchema: {
42
47
  type: "object",
@@ -44,10 +49,73 @@ var plugin = {
44
49
  trackPerModel: { type: "boolean", default: true },
45
50
  trackPerUser: { type: "boolean", default: false },
46
51
  budgetLimit: { type: "number", default: 0, description: "Budget limit in USD (0 = no limit)" },
47
- warningThreshold: { type: "number", default: 80, description: "Warning threshold as percentage of budget" }
52
+ warningThreshold: { type: "number", default: 80, description: "Warning threshold as percentage of budget" },
53
+ pricingOverrides: {
54
+ type: "object",
55
+ description: "Per-model pricing overrides in USD per 1M tokens. Keys are lowercased model names; values are { input, output }. Takes precedence over the bundled PRICING table.",
56
+ additionalProperties: {
57
+ type: "object",
58
+ properties: {
59
+ input: { type: "number", minimum: 0, description: "Cost per 1M input tokens in USD" },
60
+ output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" }
61
+ },
62
+ required: ["input", "output"],
63
+ additionalProperties: false
64
+ },
65
+ default: {}
66
+ }
48
67
  }
49
68
  },
50
- setup(api) {
69
+ async setup(api) {
70
+ for (const k of Object.keys(pricingOverrides)) {
71
+ delete pricingOverrides[k];
72
+ }
73
+ for (const k of Object.keys(bundledFromRegistry)) {
74
+ delete bundledFromRegistry[k];
75
+ }
76
+ lastCost.usd = 0;
77
+ lastCost.model = null;
78
+ lastCost.at = null;
79
+ const rawConfig = api.config.extensions?.["cost-tracker"];
80
+ const userOverrides = rawConfig?.["pricingOverrides"];
81
+ if (userOverrides && typeof userOverrides === "object") {
82
+ for (const [model, value] of Object.entries(userOverrides)) {
83
+ if (!value || typeof value !== "object") continue;
84
+ const v = value;
85
+ const input = v["input"];
86
+ const output = v["output"];
87
+ if (typeof input !== "number" || typeof output !== "number") continue;
88
+ pricingOverrides[model.toLowerCase()] = { input, output };
89
+ }
90
+ }
91
+ if (api.modelsRegistry) {
92
+ try {
93
+ const payload = await api.modelsRegistry.load();
94
+ let hydrated = 0;
95
+ for (const provider of Object.values(payload)) {
96
+ const providerModels = provider?.models;
97
+ if (!providerModels) continue;
98
+ for (const [modelId, model] of Object.entries(providerModels)) {
99
+ const cost = model?.cost;
100
+ if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
101
+ bundledFromRegistry[modelId.toLowerCase()] = {
102
+ input: cost.input,
103
+ output: cost.output
104
+ };
105
+ hydrated += 1;
106
+ }
107
+ }
108
+ }
109
+ api.log.info("cost-tracker: hydrated pricing from models registry", {
110
+ models: hydrated
111
+ });
112
+ } catch (err) {
113
+ api.log.warn(
114
+ "cost-tracker: failed to hydrate pricing from models registry \u2014 using bundled PRICING",
115
+ err
116
+ );
117
+ }
118
+ }
51
119
  const sessionCost = {
52
120
  requests: [],
53
121
  totalPromptTokens: 0,
@@ -85,6 +153,9 @@ var plugin = {
85
153
  slot.requests += 1;
86
154
  api.metrics.counter("tokens_total", totalTokens, { model });
87
155
  api.metrics.histogram("cost_usd", costUsd, { model });
156
+ lastCost.usd = costUsd;
157
+ lastCost.model = model;
158
+ lastCost.at = (/* @__PURE__ */ new Date()).toISOString();
88
159
  });
89
160
  api.tools.register({
90
161
  name: "cost_summary",
@@ -216,6 +287,36 @@ var plugin = {
216
287
  }
217
288
  });
218
289
  api.log.info("cost-tracker plugin loaded", { version: "0.1.0" });
290
+ },
291
+ teardown(api) {
292
+ const overrideCount = Object.keys(pricingOverrides).length;
293
+ const registryCount = Object.keys(bundledFromRegistry).length;
294
+ for (const k of Object.keys(pricingOverrides)) {
295
+ delete pricingOverrides[k];
296
+ }
297
+ for (const k of Object.keys(bundledFromRegistry)) {
298
+ delete bundledFromRegistry[k];
299
+ }
300
+ const finalLast = { ...lastCost };
301
+ lastCost.usd = 0;
302
+ lastCost.model = null;
303
+ lastCost.at = null;
304
+ api.log.info("cost-tracker: teardown complete", {
305
+ overrideCount,
306
+ registryCount,
307
+ lastModel: finalLast.model
308
+ });
309
+ },
310
+ async health() {
311
+ return {
312
+ ok: true,
313
+ message: lastCost.model === null ? "cost-tracker: no requests recorded yet this session" : `cost-tracker: last ${lastCost.model} cost=${lastCost.usd.toFixed(6)} at ${lastCost.at}`,
314
+ overrideCount: Object.keys(pricingOverrides).length,
315
+ registryCount: Object.keys(bundledFromRegistry).length,
316
+ lastCostUsd: lastCost.usd,
317
+ lastCostModel: lastCost.model,
318
+ lastCostAt: lastCost.at
319
+ };
219
320
  }
220
321
  };
221
322
  var cost_tracker_default = plugin;
@@ -0,0 +1,65 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * dep-guard plugin — supervises dependency-install commands.
5
+ *
6
+ * Supply-chain safety at the point of no return: a `PreToolUse` hook
7
+ * on `bash|exec` parses package-manager install commands
8
+ * (`npm i`, `pnpm add`, `yarn add`, `bun add`, `pip install`,
9
+ * `cargo add`) and extracts the package names being added:
10
+ *
11
+ * - packages on the `deny` list → blocked (or warned in
12
+ * warn mode) with the configured reason
13
+ * - typosquat lookalikes of `popular` → warned ("did you mean
14
+ * react? you typed raect") via Levenshtein distance 1
15
+ * - unpinned installs (no version) → optional warning when
16
+ * `warnOnUnpinned` is set
17
+ * - every install → a compact context note so
18
+ * the model consciously confirms new dependencies
19
+ *
20
+ * Non-install commands pass through untouched.
21
+ *
22
+ * Config (`config.extensions['dep-guard']`):
23
+ *
24
+ * ```jsonc
25
+ * {
26
+ * "enabled": true,
27
+ * "mode": "block", // "block" | "warn" (deny-list handling)
28
+ * "deny": [], // exact names or prefix globs ("left-pad", "@evil/*")
29
+ * "allow": [], // exemptions from deny
30
+ * "warnOnUnpinned": false, // warn when no version is specified
31
+ * "typosquatCheck": true // warn on 1-edit lookalikes of popular packages
32
+ * }
33
+ * ```
34
+ *
35
+ * Toggle off with `{ "name": "dep-guard", "enabled": false }` in
36
+ * `config.plugins`, or `"enabled": false` in the options above.
37
+ *
38
+ * @public
39
+ */
40
+
41
+ interface ParsedInstall {
42
+ manager: string;
43
+ packages: Array<{
44
+ name: string;
45
+ version: string | null;
46
+ }>;
47
+ }
48
+ /**
49
+ * Parse a shell command for dependency installs. Returns one entry
50
+ * per install segment found (compound commands can contain several).
51
+ * Bare `npm install` (restore from lockfile, no packages) yields an
52
+ * empty package list and is NOT treated as adding dependencies.
53
+ */
54
+ declare function parseInstallCommands(command: string): ParsedInstall[];
55
+ /**
56
+ * Optimal-string-alignment distance (Levenshtein + adjacent
57
+ * transposition). Transpositions count as ONE edit because they are
58
+ * the classic typosquat vector: `lodahs` → `lodash`, `raect` → `react`.
59
+ * Package names are short, so the full matrix is cheap.
60
+ */
61
+ declare function editDistance(a: string, b: string): number;
62
+ declare function typosquatOf(name: string): string | null;
63
+ declare const plugin: Plugin;
64
+
65
+ export { type ParsedInstall, plugin as default, editDistance, parseInstallCommands, typosquatOf };
@@ -0,0 +1,316 @@
1
+ // src/dep-guard/index.ts
2
+ var state = {
3
+ invocations: 0,
4
+ installsSeen: 0,
5
+ blocks: 0,
6
+ warns: 0,
7
+ lastBlock: null,
8
+ hookUnregister: null
9
+ };
10
+ var DEFAULTS = {
11
+ enabled: true,
12
+ mode: "block",
13
+ deny: [],
14
+ allow: [],
15
+ warnOnUnpinned: false,
16
+ typosquatCheck: true
17
+ };
18
+ function readConfig(raw) {
19
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
20
+ const r = raw;
21
+ const strings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === "string" && s.length > 0) : [];
22
+ return {
23
+ enabled: r["enabled"] !== false,
24
+ mode: r["mode"] === "warn" ? "warn" : "block",
25
+ deny: strings(r["deny"]),
26
+ allow: strings(r["allow"]),
27
+ warnOnUnpinned: r["warnOnUnpinned"] === true,
28
+ typosquatCheck: r["typosquatCheck"] !== false
29
+ };
30
+ }
31
+ 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;
32
+ function parseInstallCommands(command) {
33
+ const out = [];
34
+ INSTALL_RE.lastIndex = 0;
35
+ let m = INSTALL_RE.exec(command);
36
+ while (m !== null) {
37
+ const manager = (m[1] ?? m[3] ?? m[5] ?? "").toLowerCase();
38
+ const argString = m[2] ?? m[4] ?? m[6] ?? "";
39
+ const packages = [];
40
+ for (const token of argString.split(/\s+/)) {
41
+ if (!token || token.startsWith("-")) continue;
42
+ if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
43
+ const cleaned = token.replace(/^['"]|['"]$/g, "");
44
+ if (!cleaned) continue;
45
+ let name = cleaned;
46
+ let version = null;
47
+ const pipMatch = /^([A-Za-z0-9_.-]+)\s*(?:==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
48
+ if (pipMatch?.[1]) {
49
+ name = pipMatch[1];
50
+ version = pipMatch[2] ?? null;
51
+ } else {
52
+ const at = cleaned.lastIndexOf("@");
53
+ if (at > 0) {
54
+ name = cleaned.slice(0, at);
55
+ version = cleaned.slice(at + 1) || null;
56
+ }
57
+ }
58
+ if (name) packages.push({ name, version });
59
+ }
60
+ out.push({ manager, packages });
61
+ m = INSTALL_RE.exec(command);
62
+ }
63
+ return out;
64
+ }
65
+ function matchesPattern(name, pattern) {
66
+ if (pattern.endsWith("*"))
67
+ return name.toLowerCase().startsWith(pattern.slice(0, -1).toLowerCase());
68
+ return name.toLowerCase() === pattern.toLowerCase();
69
+ }
70
+ var POPULAR_PACKAGES = [
71
+ "react",
72
+ "react-dom",
73
+ "express",
74
+ "lodash",
75
+ "axios",
76
+ "typescript",
77
+ "vite",
78
+ "vitest",
79
+ "next",
80
+ "vue",
81
+ "svelte",
82
+ "zod",
83
+ "prettier",
84
+ "eslint",
85
+ "jest",
86
+ "webpack",
87
+ "commander",
88
+ "chalk",
89
+ "dotenv",
90
+ "requests",
91
+ "numpy",
92
+ "pandas",
93
+ "flask",
94
+ "django",
95
+ "serde",
96
+ "tokio"
97
+ ];
98
+ function editDistance(a, b) {
99
+ if (a === b) return 0;
100
+ if (Math.abs(a.length - b.length) > 2) return 3;
101
+ const d = Array.from({ length: a.length + 1 }, (_, i) => {
102
+ const row = new Array(b.length + 1).fill(0);
103
+ row[0] = i;
104
+ return row;
105
+ });
106
+ for (let j = 0; j <= b.length; j++) d[0][j] = j;
107
+ for (let i = 1; i <= a.length; i++) {
108
+ for (let j = 1; j <= b.length; j++) {
109
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
110
+ const row = d[i];
111
+ const prevRow = d[i - 1];
112
+ row[j] = Math.min(
113
+ prevRow[j] + 1,
114
+ row[j - 1] + 1,
115
+ prevRow[j - 1] + cost
116
+ );
117
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
118
+ row[j] = Math.min(row[j], d[i - 2][j - 2] + 1);
119
+ }
120
+ }
121
+ }
122
+ return d[a.length][b.length];
123
+ }
124
+ function typosquatOf(name) {
125
+ const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
126
+ if (POPULAR_PACKAGES.includes(lower)) return null;
127
+ for (const popular of POPULAR_PACKAGES) {
128
+ if (editDistance(lower, popular) === 1) return popular;
129
+ }
130
+ return null;
131
+ }
132
+ var plugin = {
133
+ name: "dep-guard",
134
+ version: "0.1.0",
135
+ description: "Supervises dependency installs: blocks deny-listed packages, flags typosquat lookalikes, and optionally warns on unpinned versions",
136
+ apiVersion: "^0.1.10",
137
+ capabilities: { tools: true, hooks: true },
138
+ defaultConfig: { ...DEFAULTS },
139
+ configSchema: {
140
+ type: "object",
141
+ properties: {
142
+ enabled: { type: "boolean", default: true, description: "Master switch." },
143
+ mode: {
144
+ type: "string",
145
+ enum: ["block", "warn"],
146
+ default: "block",
147
+ description: "How deny-list hits are handled."
148
+ },
149
+ deny: {
150
+ type: "array",
151
+ items: { type: "string" },
152
+ default: [],
153
+ description: 'Package names (exact) or prefix globs ("@evil/*") that must not be installed.'
154
+ },
155
+ allow: {
156
+ type: "array",
157
+ items: { type: "string" },
158
+ default: [],
159
+ description: "Exemptions that override deny."
160
+ },
161
+ warnOnUnpinned: {
162
+ type: "boolean",
163
+ default: false,
164
+ description: "Warn when a package is installed without an explicit version."
165
+ },
166
+ typosquatCheck: {
167
+ type: "boolean",
168
+ default: true,
169
+ description: "Warn when a package name is one edit away from a well-known package."
170
+ }
171
+ }
172
+ },
173
+ setup(api) {
174
+ state.invocations = 0;
175
+ state.installsSeen = 0;
176
+ state.blocks = 0;
177
+ state.warns = 0;
178
+ state.lastBlock = null;
179
+ if (state.hookUnregister) {
180
+ try {
181
+ state.hookUnregister();
182
+ } catch {
183
+ }
184
+ state.hookUnregister = null;
185
+ }
186
+ const cfg = readConfig(api.config.extensions?.["dep-guard"]);
187
+ const hook = (input) => {
188
+ if (!cfg.enabled) return;
189
+ state.invocations += 1;
190
+ const ti = input.toolInput ?? {};
191
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
192
+ if (!command) return;
193
+ const installs = parseInstallCommands(command);
194
+ const packages = installs.flatMap((i) => i.packages);
195
+ if (packages.length === 0) return;
196
+ state.installsSeen += 1;
197
+ api.metrics.counter("installs_seen");
198
+ const notes = [];
199
+ for (const pkg of packages) {
200
+ const allowed = cfg.allow.some((p) => matchesPattern(pkg.name, p));
201
+ const denied = !allowed && cfg.deny.some((p) => matchesPattern(pkg.name, p));
202
+ if (denied) {
203
+ if (cfg.mode === "block") {
204
+ state.blocks += 1;
205
+ state.lastBlock = {
206
+ pkg: pkg.name,
207
+ command: command.slice(0, 200),
208
+ when: (/* @__PURE__ */ new Date()).toISOString()
209
+ };
210
+ api.metrics.counter("blocks");
211
+ return {
212
+ decision: "block",
213
+ reason: `dep-guard: package "${pkg.name}" is on the deny list (config.extensions["dep-guard"].deny) \u2014 install refused. Ask the user before adding this dependency, or add an \`allow\` entry.`
214
+ };
215
+ }
216
+ notes.push(
217
+ `"${pkg.name}" is DENY-LISTED \u2014 do not add it without explicit user approval.`
218
+ );
219
+ }
220
+ if (cfg.typosquatCheck) {
221
+ const lookalike = typosquatOf(pkg.name);
222
+ if (lookalike) {
223
+ notes.push(
224
+ `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`
225
+ );
226
+ }
227
+ }
228
+ if (cfg.warnOnUnpinned && pkg.version === null) {
229
+ notes.push(`"${pkg.name}" has no pinned version \u2014 consider "${pkg.name}@<version>".`);
230
+ }
231
+ }
232
+ if (notes.length > 0) {
233
+ state.warns += 1;
234
+ api.metrics.counter("warns");
235
+ return {
236
+ decision: "allow",
237
+ additionalContext: `dep-guard:
238
+ ${notes.map((n) => ` - ${n}`).join("\n")}`
239
+ };
240
+ }
241
+ return {
242
+ decision: "allow",
243
+ additionalContext: `dep-guard: this command adds ${packages.length} dependenc${packages.length === 1 ? "y" : "ies"}: ${packages.map((p) => p.name).join(", ")}. Confirm each is intentional.`
244
+ };
245
+ };
246
+ state.hookUnregister = api.registerHook("PreToolUse", "bash|exec", hook);
247
+ api.tools.register({
248
+ name: "dep_guard_status",
249
+ description: "Reports dep-guard state: deny/allow lists, mode, and counters (installs seen, blocks, warns).",
250
+ inputSchema: { type: "object", properties: {} },
251
+ permission: "auto",
252
+ category: "Diagnostics",
253
+ mutating: false,
254
+ async execute() {
255
+ return {
256
+ ok: true,
257
+ enabled: cfg.enabled,
258
+ mode: cfg.mode,
259
+ deny: cfg.deny,
260
+ allow: cfg.allow,
261
+ warnOnUnpinned: cfg.warnOnUnpinned,
262
+ typosquatCheck: cfg.typosquatCheck,
263
+ counters: {
264
+ invocations: state.invocations,
265
+ installsSeen: state.installsSeen,
266
+ blocks: state.blocks,
267
+ warns: state.warns
268
+ },
269
+ lastBlock: state.lastBlock
270
+ };
271
+ }
272
+ });
273
+ api.log.info("dep-guard plugin loaded", {
274
+ version: "0.1.0",
275
+ enabled: cfg.enabled,
276
+ mode: cfg.mode,
277
+ denyCount: cfg.deny.length
278
+ });
279
+ },
280
+ teardown(api) {
281
+ if (state.hookUnregister) {
282
+ try {
283
+ state.hookUnregister();
284
+ } catch {
285
+ }
286
+ state.hookUnregister = null;
287
+ }
288
+ const final = {
289
+ invocations: state.invocations,
290
+ installsSeen: state.installsSeen,
291
+ blocks: state.blocks,
292
+ warns: state.warns
293
+ };
294
+ state.invocations = 0;
295
+ state.installsSeen = 0;
296
+ state.blocks = 0;
297
+ state.warns = 0;
298
+ state.lastBlock = null;
299
+ api.log.info("dep-guard: teardown complete", { final });
300
+ },
301
+ async health() {
302
+ return {
303
+ ok: true,
304
+ message: state.lastBlock === null ? `dep-guard: ${state.installsSeen} install command(s) seen, ${state.blocks} block(s), ${state.warns} warn(s)` : `dep-guard: last block on "${state.lastBlock.pkg}" at ${state.lastBlock.when}`,
305
+ counters: {
306
+ invocations: state.invocations,
307
+ installsSeen: state.installsSeen,
308
+ blocks: state.blocks,
309
+ warns: state.warns
310
+ }
311
+ };
312
+ }
313
+ };
314
+ var dep_guard_default = plugin;
315
+
316
+ export { dep_guard_default as default, editDistance, parseInstallCommands, typosquatOf };
@@ -0,0 +1,36 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * diff-summary plugin — PostToolUse hook that injects a compact diff
5
+ * into the LLM's context after every `write` or `edit`.
6
+ *
7
+ * Tools registered:
8
+ * - diff_summary_status : Show config + per-session counters.
9
+ *
10
+ * Hooks registered:
11
+ * - PostToolUse with matcher `write|edit`. After the tool completes,
12
+ * runs `git diff -- <path>` to capture what changed and injects a
13
+ * capped unified diff (or stat summary) as `additionalContext`.
14
+ *
15
+ * Config (`config.extensions['diff-summary']`):
16
+ *
17
+ * ```jsonc
18
+ * {
19
+ * "maxLines": 50, // cap diff context at N lines
20
+ * "showStat": true, // include "+N -M" summary line
21
+ * "mode": "diff" // "diff" (unified diff) | "stat" (counts only) | "off"
22
+ * }
23
+ * ```
24
+ *
25
+ * Why: The `write` tool's result doesn't include a diff. The `edit`
26
+ * tool shows the replacement but not the full file context. This
27
+ * plugin gives the LLM consistent, compact visibility into what its
28
+ * change actually did to the file — confirming the edit applied
29
+ * correctly and showing surrounding context.
30
+ *
31
+ * @public
32
+ */
33
+
34
+ declare const plugin: Plugin;
35
+
36
+ export { plugin as default };