@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.
- package/README.md +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- package/dist/web-search.js +0 -15
package/dist/cost-tracker.js
CHANGED
|
@@ -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
|
|
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 };
|