@wrongstack/plugins 0.277.2 → 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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// src/diff-summary/index.ts
|
|
4
|
+
var API_VERSION = "^0.1.10";
|
|
5
|
+
var state = {
|
|
6
|
+
invocationCount: 0,
|
|
7
|
+
/** Times a diff was successfully generated and injected. */
|
|
8
|
+
injectedCount: 0,
|
|
9
|
+
/** Times git diff failed (not a repo, untracked, etc.). */
|
|
10
|
+
fallbackCount: 0,
|
|
11
|
+
/** Hook handle for teardown. */
|
|
12
|
+
hookUnregister: null,
|
|
13
|
+
/** Last diff summary — surfaced by health() + status tool. */
|
|
14
|
+
lastSummary: null
|
|
15
|
+
};
|
|
16
|
+
var DEFAULTS = {
|
|
17
|
+
maxLines: 50,
|
|
18
|
+
showStat: true,
|
|
19
|
+
mode: "diff",
|
|
20
|
+
includeContext: 3
|
|
21
|
+
};
|
|
22
|
+
function readConfig(raw) {
|
|
23
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
24
|
+
const r = raw;
|
|
25
|
+
return {
|
|
26
|
+
maxLines: typeof r["maxLines"] === "number" && r["maxLines"] > 0 ? r["maxLines"] : DEFAULTS.maxLines,
|
|
27
|
+
showStat: r["showStat"] !== false,
|
|
28
|
+
mode: r["mode"] === "stat" ? "stat" : r["mode"] === "off" ? "off" : "diff",
|
|
29
|
+
includeContext: typeof r["includeContext"] === "number" && r["includeContext"] >= 0 ? r["includeContext"] : DEFAULTS.includeContext
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function getGitDiff(filePath, contextLines, cwd) {
|
|
33
|
+
const opts = {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
timeout: 3e3,
|
|
36
|
+
cwd,
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
38
|
+
};
|
|
39
|
+
let isTracked = false;
|
|
40
|
+
try {
|
|
41
|
+
execSync(`git ls-files --error-unmatch "${filePath}"`, opts);
|
|
42
|
+
isTracked = true;
|
|
43
|
+
} catch {
|
|
44
|
+
isTracked = false;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
let rawDiff;
|
|
48
|
+
const contextFlag = `-U${contextLines}`;
|
|
49
|
+
if (isTracked) {
|
|
50
|
+
rawDiff = execSync(`git diff ${contextFlag} -- "${filePath}"`, opts);
|
|
51
|
+
} else {
|
|
52
|
+
try {
|
|
53
|
+
rawDiff = execSync(`git diff --no-index ${contextFlag} /dev/null "${filePath}"`, opts);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const e = err;
|
|
56
|
+
rawDiff = e.stdout ?? "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!rawDiff.trim()) {
|
|
60
|
+
return { diff: "", added: 0, removed: 0, isNewFile: !isTracked };
|
|
61
|
+
}
|
|
62
|
+
const lines = rawDiff.split("\n");
|
|
63
|
+
let added = 0;
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added++;
|
|
67
|
+
else if (line.startsWith("-") && !line.startsWith("---")) removed++;
|
|
68
|
+
}
|
|
69
|
+
return { diff: rawDiff, added, removed, isNewFile: !isTracked };
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function buildStatSummary(filePath, result) {
|
|
75
|
+
if (result.diff === "") return `${filePath}: no changes`;
|
|
76
|
+
const tag = result.isNewFile ? " (new file)" : "";
|
|
77
|
+
return `${filePath}${tag}: +${result.added} -${result.removed}`;
|
|
78
|
+
}
|
|
79
|
+
function buildDiffSummary(filePath, result, maxLines) {
|
|
80
|
+
if (result.diff === "") return `${filePath}: no changes`;
|
|
81
|
+
const lines = result.diff.split("\n");
|
|
82
|
+
const tag = result.isNewFile ? " (new file)" : "";
|
|
83
|
+
if (lines.length <= maxLines) {
|
|
84
|
+
return `${filePath}${tag}: +${result.added} -${result.removed}
|
|
85
|
+
${result.diff}`;
|
|
86
|
+
}
|
|
87
|
+
const truncated = lines.slice(0, maxLines).join("\n");
|
|
88
|
+
return `${filePath}${tag}: +${result.added} -${result.removed}
|
|
89
|
+
${truncated}
|
|
90
|
+
... (${lines.length - maxLines} more lines truncated)`;
|
|
91
|
+
}
|
|
92
|
+
var plugin = {
|
|
93
|
+
name: "diff-summary",
|
|
94
|
+
version: "0.1.0",
|
|
95
|
+
description: "PostToolUse hook that injects a compact git diff into the LLM context after every write or edit",
|
|
96
|
+
apiVersion: API_VERSION,
|
|
97
|
+
capabilities: { tools: true, hooks: true },
|
|
98
|
+
defaultConfig: { ...DEFAULTS },
|
|
99
|
+
configSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
maxLines: {
|
|
103
|
+
type: "number",
|
|
104
|
+
minimum: 5,
|
|
105
|
+
default: 50,
|
|
106
|
+
description: "Cap diff context at N lines to avoid blowing up the context window."
|
|
107
|
+
},
|
|
108
|
+
showStat: {
|
|
109
|
+
type: "boolean",
|
|
110
|
+
default: true,
|
|
111
|
+
description: 'Include "+N -M" summary line.'
|
|
112
|
+
},
|
|
113
|
+
mode: {
|
|
114
|
+
type: "string",
|
|
115
|
+
enum: ["diff", "stat", "off"],
|
|
116
|
+
default: "diff",
|
|
117
|
+
description: '"diff" injects unified diff; "stat" injects only +N -M counts; "off" disables the hook.'
|
|
118
|
+
},
|
|
119
|
+
includeContext: {
|
|
120
|
+
type: "number",
|
|
121
|
+
minimum: 0,
|
|
122
|
+
default: 3,
|
|
123
|
+
description: "Context lines around each change (git -U<N>). 0 = compact (no surrounding lines), 3 = git default, higher = more orientation."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
setup(api) {
|
|
128
|
+
state.invocationCount = 0;
|
|
129
|
+
state.injectedCount = 0;
|
|
130
|
+
state.fallbackCount = 0;
|
|
131
|
+
state.hookUnregister = null;
|
|
132
|
+
state.lastSummary = null;
|
|
133
|
+
const cfg = readConfig(api.config.extensions?.["diff-summary"]);
|
|
134
|
+
const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
|
|
135
|
+
const hook = (input) => {
|
|
136
|
+
if (cfg.mode === "off") return;
|
|
137
|
+
if (input.toolResult?.isError) return;
|
|
138
|
+
const toolName = input.toolName ?? "";
|
|
139
|
+
const inp = input.toolInput ?? {};
|
|
140
|
+
const filePath = inp["path"];
|
|
141
|
+
if (!filePath || typeof filePath !== "string") return;
|
|
142
|
+
state.invocationCount += 1;
|
|
143
|
+
const result = getGitDiff(filePath, cfg.includeContext, cwd);
|
|
144
|
+
if (!result) {
|
|
145
|
+
state.fallbackCount += 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (result.diff === "" && result.added === 0 && result.removed === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
state.injectedCount += 1;
|
|
152
|
+
state.lastSummary = {
|
|
153
|
+
path: filePath,
|
|
154
|
+
tool: toolName,
|
|
155
|
+
added: result.added,
|
|
156
|
+
removed: result.removed,
|
|
157
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
158
|
+
};
|
|
159
|
+
let summary;
|
|
160
|
+
if (cfg.mode === "stat") {
|
|
161
|
+
summary = buildStatSummary(filePath, result);
|
|
162
|
+
} else {
|
|
163
|
+
summary = buildDiffSummary(filePath, result, cfg.maxLines);
|
|
164
|
+
}
|
|
165
|
+
const header = cfg.showStat ? `
|
|
166
|
+
\u{1F4DD} diff-summary (${toolName}): ` : "\n\u{1F4DD} diff-summary: ";
|
|
167
|
+
return {
|
|
168
|
+
additionalContext: header + summary
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
172
|
+
api.tools.register({
|
|
173
|
+
name: "diff_summary_status",
|
|
174
|
+
description: "Reports diff-summary state: mode, maxLines, and per-session invocation/injected/fallback counters.",
|
|
175
|
+
inputSchema: { type: "object", properties: {} },
|
|
176
|
+
permission: "auto",
|
|
177
|
+
category: "Meta",
|
|
178
|
+
mutating: false,
|
|
179
|
+
async execute() {
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
mode: cfg.mode,
|
|
183
|
+
maxLines: cfg.maxLines,
|
|
184
|
+
showStat: cfg.showStat,
|
|
185
|
+
includeContext: cfg.includeContext,
|
|
186
|
+
counters: {
|
|
187
|
+
invocations: state.invocationCount,
|
|
188
|
+
injected: state.injectedCount,
|
|
189
|
+
fallbacks: state.fallbackCount
|
|
190
|
+
},
|
|
191
|
+
lastSummary: state.lastSummary
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
api.log.info("diff-summary plugin loaded", {
|
|
196
|
+
version: "0.1.0",
|
|
197
|
+
mode: cfg.mode,
|
|
198
|
+
maxLines: cfg.maxLines
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
teardown(api) {
|
|
202
|
+
if (state.hookUnregister) {
|
|
203
|
+
try {
|
|
204
|
+
state.hookUnregister();
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
state.hookUnregister = null;
|
|
208
|
+
}
|
|
209
|
+
const final = {
|
|
210
|
+
invocations: state.invocationCount,
|
|
211
|
+
injected: state.injectedCount,
|
|
212
|
+
fallbacks: state.fallbackCount
|
|
213
|
+
};
|
|
214
|
+
state.invocationCount = 0;
|
|
215
|
+
state.injectedCount = 0;
|
|
216
|
+
state.fallbackCount = 0;
|
|
217
|
+
state.lastSummary = null;
|
|
218
|
+
api.log.info("diff-summary: teardown complete", { final });
|
|
219
|
+
},
|
|
220
|
+
async health() {
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
message: state.lastSummary === null ? `diff-summary: ${state.invocationCount} invocation(s), ${state.injectedCount} injected` : `diff-summary: last ${state.lastSummary.tool} on ${state.lastSummary.path} (+${state.lastSummary.added} -${state.lastSummary.removed}) at ${state.lastSummary.when}`,
|
|
224
|
+
counters: {
|
|
225
|
+
invocations: state.invocationCount,
|
|
226
|
+
injected: state.injectedCount,
|
|
227
|
+
fallbacks: state.fallbackCount
|
|
228
|
+
},
|
|
229
|
+
lastSummary: state.lastSummary
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var diff_summary_default = plugin;
|
|
234
|
+
|
|
235
|
+
export { diff_summary_default as default };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* error-lens plugin — turns noisy failure output into a compact,
|
|
5
|
+
* actionable digest.
|
|
6
|
+
*
|
|
7
|
+
* When a shell command fails, the model gets a wall of stderr: full
|
|
8
|
+
* stack traces, npm noise, repeated frames. A `PostToolUse` hook on
|
|
9
|
+
* `bash|exec` parses the failure and injects a short digest via
|
|
10
|
+
* `additionalContext`:
|
|
11
|
+
*
|
|
12
|
+
* - the error type + message line (TypeError, ENOENT, AssertionError…)
|
|
13
|
+
* - up to `maxFrames` source locations (`file:line`), *project frames
|
|
14
|
+
* first* (node_modules/internal frames are de-prioritized)
|
|
15
|
+
* - a repeat marker when the same failure was already seen, so the
|
|
16
|
+
* model notices "this is the SAME error as before"
|
|
17
|
+
*
|
|
18
|
+
* Recognized trace formats: Node/V8 (`at fn (file:line:col)`), Python
|
|
19
|
+
* (`File "x.py", line 12`), tsc/vitest (`file.ts:12:5`), Rust
|
|
20
|
+
* (`--> src/main.rs:4:5`), Go (`file.go:12 +0x…`).
|
|
21
|
+
*
|
|
22
|
+
* A ring of recent failures is queryable via `error_lens_history` —
|
|
23
|
+
* useful for "what have we broken so far this session?".
|
|
24
|
+
*
|
|
25
|
+
* Config (`config.extensions['error-lens']`):
|
|
26
|
+
*
|
|
27
|
+
* ```jsonc
|
|
28
|
+
* {
|
|
29
|
+
* "enabled": true,
|
|
30
|
+
* "maxFrames": 5, // source locations per digest
|
|
31
|
+
* "historySize": 20, // failures kept for error_lens_history
|
|
32
|
+
* "minOutputChars": 200, // don't digest tiny outputs (already readable)
|
|
33
|
+
* "aiHints": false, // ask the LLM (api.llm) for a one-line fix hint
|
|
34
|
+
* "llm": { "provider": "", "model": "" } // optional override for the hint model
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* With `aiHints: true` and a host that wires `api.llm`, each NEW failure
|
|
39
|
+
* digest is enriched with a one-line fix hint from the configured LLM
|
|
40
|
+
* (per-plugin `llm.provider`/`llm.model` override the session default).
|
|
41
|
+
* Hint failures are swallowed — the digest always lands.
|
|
42
|
+
*
|
|
43
|
+
* Toggle off with `{ "name": "error-lens", "enabled": false }` in
|
|
44
|
+
* `config.plugins`, or `"enabled": false` in the options above.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
interface FailureRecord {
|
|
50
|
+
when: string;
|
|
51
|
+
tool: string;
|
|
52
|
+
command: string | null;
|
|
53
|
+
errorLine: string | null;
|
|
54
|
+
frames: string[];
|
|
55
|
+
repeats: number;
|
|
56
|
+
}
|
|
57
|
+
/** Extract the first recognizable error line from tool output. */
|
|
58
|
+
declare function extractErrorLine(output: string): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Extract source locations (`file:line`) from tool output.
|
|
61
|
+
* Project frames (not node_modules / internal) are ordered first,
|
|
62
|
+
* duplicates removed, capped at `maxFrames`.
|
|
63
|
+
*/
|
|
64
|
+
declare function extractFrames(output: string, maxFrames: number): string[];
|
|
65
|
+
declare const plugin: Plugin;
|
|
66
|
+
|
|
67
|
+
export { type FailureRecord, plugin as default, extractErrorLine, extractFrames };
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/error-lens/index.ts
|
|
2
|
+
var state = {
|
|
3
|
+
history: [],
|
|
4
|
+
invocations: 0,
|
|
5
|
+
digestsInjected: 0,
|
|
6
|
+
repeatsDetected: 0,
|
|
7
|
+
aiHintsProvided: 0,
|
|
8
|
+
aiHintErrors: 0,
|
|
9
|
+
hookUnregister: null
|
|
10
|
+
};
|
|
11
|
+
var DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
maxFrames: 5,
|
|
14
|
+
historySize: 20,
|
|
15
|
+
minOutputChars: 200,
|
|
16
|
+
aiHints: false
|
|
17
|
+
};
|
|
18
|
+
function readConfig(raw) {
|
|
19
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
20
|
+
const r = raw;
|
|
21
|
+
return {
|
|
22
|
+
enabled: r["enabled"] !== false,
|
|
23
|
+
maxFrames: typeof r["maxFrames"] === "number" && r["maxFrames"] >= 1 && r["maxFrames"] <= 20 ? r["maxFrames"] : DEFAULTS.maxFrames,
|
|
24
|
+
historySize: typeof r["historySize"] === "number" && r["historySize"] >= 1 && r["historySize"] <= 200 ? r["historySize"] : DEFAULTS.historySize,
|
|
25
|
+
minOutputChars: typeof r["minOutputChars"] === "number" && r["minOutputChars"] >= 0 ? r["minOutputChars"] : DEFAULTS.minOutputChars,
|
|
26
|
+
aiHints: r["aiHints"] === true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
var ERROR_LINE_PATTERNS = [
|
|
30
|
+
// JS/TS: "TypeError: x is not a function", "Error: ENOENT: no such file"
|
|
31
|
+
/^[ \t]*(?:Uncaught )?((?:[A-Z][a-zA-Z]*)?(?:Error|Exception|AssertionError)[^\n]*)/m,
|
|
32
|
+
// Node errno style: "ENOENT: no such file or directory"
|
|
33
|
+
/^[ \t]*((?:ENOENT|EACCES|EADDRINUSE|ECONNREFUSED|ETIMEDOUT|EPERM|ENOTEMPTY)[^\n]*)/m,
|
|
34
|
+
// Python: "ValueError: invalid literal ..."
|
|
35
|
+
/^([A-Z][a-zA-Z]*(?:Error|Exception|Warning): [^\n]*)/m,
|
|
36
|
+
// Rust panic: "thread 'main' panicked at ..."
|
|
37
|
+
/^(thread '[^']*' panicked at [^\n]*)/m,
|
|
38
|
+
// Go panic: "panic: runtime error: ..."
|
|
39
|
+
/^(panic: [^\n]*)/m,
|
|
40
|
+
// Generic FAIL lines from test runners.
|
|
41
|
+
/^[ \t]*((?:FAIL|✗|×)[ \t]+[^\n]{5,})/m
|
|
42
|
+
];
|
|
43
|
+
var FRAME_PATTERNS = [
|
|
44
|
+
// Node/V8: "at fn (path/file.ts:12:5)" or "at path/file.ts:12:5"
|
|
45
|
+
/\bat\s+(?:[^(\n]+\()?([^()\n:]+:\d+)(?::\d+)?\)?/g,
|
|
46
|
+
// Python: File "path/file.py", line 12
|
|
47
|
+
/File "([^"]+)", line (\d+)/g,
|
|
48
|
+
// tsc/vitest/eslint bare: "src/foo.ts:12:5" (require a path-ish prefix)
|
|
49
|
+
/(?:^|[ \t(])([A-Za-z0-9_./\\-]+\.[a-z]{1,4}:\d+)(?::\d+)?/gm,
|
|
50
|
+
// Rust: "--> src/main.rs:4:5"
|
|
51
|
+
/-->\s+([^\s:]+:\d+)/g
|
|
52
|
+
];
|
|
53
|
+
function extractErrorLine(output) {
|
|
54
|
+
for (const re of ERROR_LINE_PATTERNS) {
|
|
55
|
+
const m = re.exec(output);
|
|
56
|
+
if (m?.[1]) return m[1].trim().slice(0, 300);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function extractFrames(output, maxFrames) {
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62
|
+
const project = [];
|
|
63
|
+
const vendor = [];
|
|
64
|
+
for (const re of FRAME_PATTERNS) {
|
|
65
|
+
re.lastIndex = 0;
|
|
66
|
+
let m = re.exec(output);
|
|
67
|
+
while (m !== null) {
|
|
68
|
+
const frame = m[2] !== void 0 ? `${m[1]}:${m[2]}` : m[1] ?? "";
|
|
69
|
+
const cleaned = frame.replace(/\\/g, "/").trim();
|
|
70
|
+
if (cleaned && !seen.has(cleaned)) {
|
|
71
|
+
seen.add(cleaned);
|
|
72
|
+
const isVendor = cleaned.includes("node_modules/") || cleaned.startsWith("node:") || cleaned.includes("internal/");
|
|
73
|
+
(isVendor ? vendor : project).push(cleaned);
|
|
74
|
+
}
|
|
75
|
+
m = re.exec(output);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...project, ...vendor].slice(0, maxFrames);
|
|
79
|
+
}
|
|
80
|
+
function failureKey(errorLine, frames) {
|
|
81
|
+
return `${errorLine ?? ""}|${frames[0] ?? ""}`;
|
|
82
|
+
}
|
|
83
|
+
var plugin = {
|
|
84
|
+
name: "error-lens",
|
|
85
|
+
version: "0.1.0",
|
|
86
|
+
description: "Distills failed command output into a compact digest (error line + project stack frames) and flags repeated failures",
|
|
87
|
+
apiVersion: "^0.1.10",
|
|
88
|
+
capabilities: { tools: true, hooks: true },
|
|
89
|
+
defaultConfig: { ...DEFAULTS },
|
|
90
|
+
configSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
94
|
+
maxFrames: {
|
|
95
|
+
type: "number",
|
|
96
|
+
minimum: 1,
|
|
97
|
+
maximum: 20,
|
|
98
|
+
default: 5,
|
|
99
|
+
description: "Source locations included per digest."
|
|
100
|
+
},
|
|
101
|
+
historySize: {
|
|
102
|
+
type: "number",
|
|
103
|
+
minimum: 1,
|
|
104
|
+
maximum: 200,
|
|
105
|
+
default: 20,
|
|
106
|
+
description: "Failures kept for error_lens_history."
|
|
107
|
+
},
|
|
108
|
+
minOutputChars: {
|
|
109
|
+
type: "number",
|
|
110
|
+
minimum: 0,
|
|
111
|
+
default: 200,
|
|
112
|
+
description: "Outputs shorter than this are not digested (already readable)."
|
|
113
|
+
},
|
|
114
|
+
aiHints: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
default: false,
|
|
117
|
+
description: 'Enrich each NEW failure digest with a one-line fix hint from the LLM (api.llm). Provider/model follow config.extensions["error-lens"].llm, then the session default.'
|
|
118
|
+
},
|
|
119
|
+
llm: {
|
|
120
|
+
type: "object",
|
|
121
|
+
description: "Optional { provider, model } override for AI hints."
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
setup(api) {
|
|
126
|
+
state.history = [];
|
|
127
|
+
state.invocations = 0;
|
|
128
|
+
state.digestsInjected = 0;
|
|
129
|
+
state.repeatsDetected = 0;
|
|
130
|
+
state.aiHintsProvided = 0;
|
|
131
|
+
state.aiHintErrors = 0;
|
|
132
|
+
if (state.hookUnregister) {
|
|
133
|
+
try {
|
|
134
|
+
state.hookUnregister();
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
state.hookUnregister = null;
|
|
138
|
+
}
|
|
139
|
+
const cfg = readConfig(api.config.extensions?.["error-lens"]);
|
|
140
|
+
const hook = async (input) => {
|
|
141
|
+
if (!cfg.enabled) return;
|
|
142
|
+
state.invocations += 1;
|
|
143
|
+
const result = input.toolResult;
|
|
144
|
+
if (!result) return;
|
|
145
|
+
const output = result.content ?? "";
|
|
146
|
+
if (!result.isError) return;
|
|
147
|
+
if (output.length < cfg.minOutputChars) return;
|
|
148
|
+
const errorLine = extractErrorLine(output);
|
|
149
|
+
const frames = extractFrames(output, cfg.maxFrames);
|
|
150
|
+
if (!errorLine && frames.length === 0) return;
|
|
151
|
+
const key = failureKey(errorLine, frames);
|
|
152
|
+
const previous = state.history.find((h) => failureKey(h.errorLine, h.frames) === key);
|
|
153
|
+
let repeats = 1;
|
|
154
|
+
let isNewFailure = false;
|
|
155
|
+
if (previous) {
|
|
156
|
+
previous.repeats += 1;
|
|
157
|
+
previous.when = (/* @__PURE__ */ new Date()).toISOString();
|
|
158
|
+
repeats = previous.repeats;
|
|
159
|
+
state.repeatsDetected += 1;
|
|
160
|
+
api.metrics.counter("repeats");
|
|
161
|
+
} else {
|
|
162
|
+
isNewFailure = true;
|
|
163
|
+
const ti = input.toolInput ?? {};
|
|
164
|
+
state.history.push({
|
|
165
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
166
|
+
tool: input.toolName ?? "unknown",
|
|
167
|
+
command: typeof ti["command"] === "string" ? ti["command"].slice(0, 200) : null,
|
|
168
|
+
errorLine,
|
|
169
|
+
frames,
|
|
170
|
+
repeats: 1
|
|
171
|
+
});
|
|
172
|
+
if (state.history.length > cfg.historySize) {
|
|
173
|
+
state.history.splice(0, state.history.length - cfg.historySize);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
state.digestsInjected += 1;
|
|
177
|
+
api.metrics.counter("digests");
|
|
178
|
+
const parts = ["error-lens digest:"];
|
|
179
|
+
if (errorLine) parts.push(` error: ${errorLine}`);
|
|
180
|
+
if (frames.length > 0) parts.push(` frames: ${frames.join(" ")}`);
|
|
181
|
+
if (repeats > 1) {
|
|
182
|
+
parts.push(
|
|
183
|
+
` NOTE: this is the SAME failure as ${repeats - 1} earlier attempt(s) \u2014 the previous fix did not work; try a different approach.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (cfg.aiHints && isNewFailure && api.llm) {
|
|
187
|
+
try {
|
|
188
|
+
const hint = await api.llm.complete(
|
|
189
|
+
`A command failed with this error:
|
|
190
|
+
${errorLine ?? "(no error line)"}
|
|
191
|
+
` + (frames.length > 0 ? `Top stack frames: ${frames.join(", ")}
|
|
192
|
+
` : "") + "Reply with ONE short sentence suggesting the most likely fix. No preamble.",
|
|
193
|
+
{ system: "You are a terse debugging assistant.", maxTokens: 100 }
|
|
194
|
+
);
|
|
195
|
+
const text = hint.text.trim();
|
|
196
|
+
if (text) {
|
|
197
|
+
state.aiHintsProvided += 1;
|
|
198
|
+
api.metrics.counter("ai_hints");
|
|
199
|
+
parts.push(` hint (${hint.model}): ${text.slice(0, 300)}`);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
state.aiHintErrors += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { additionalContext: parts.join("\n") };
|
|
206
|
+
};
|
|
207
|
+
state.hookUnregister = api.registerHook("PostToolUse", "bash|exec", hook);
|
|
208
|
+
api.tools.register({
|
|
209
|
+
name: "error_lens_history",
|
|
210
|
+
description: "Lists failures observed this session (error line, source frames, repeat counts). Useful to review what has been breaking.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
limit: { type: "number", description: "Max entries to return (default 10)." }
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
permission: "auto",
|
|
218
|
+
category: "Diagnostics",
|
|
219
|
+
mutating: false,
|
|
220
|
+
async execute(input) {
|
|
221
|
+
const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : 10;
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
enabled: cfg.enabled,
|
|
225
|
+
aiHints: cfg.aiHints,
|
|
226
|
+
llmAvailable: Boolean(api.llm),
|
|
227
|
+
failures: [...state.history].reverse().slice(0, limit),
|
|
228
|
+
counters: {
|
|
229
|
+
invocations: state.invocations,
|
|
230
|
+
digestsInjected: state.digestsInjected,
|
|
231
|
+
repeatsDetected: state.repeatsDetected,
|
|
232
|
+
aiHintsProvided: state.aiHintsProvided,
|
|
233
|
+
aiHintErrors: state.aiHintErrors
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
api.log.info("error-lens plugin loaded", {
|
|
239
|
+
version: "0.1.0",
|
|
240
|
+
enabled: cfg.enabled,
|
|
241
|
+
maxFrames: cfg.maxFrames
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
teardown(api) {
|
|
245
|
+
if (state.hookUnregister) {
|
|
246
|
+
try {
|
|
247
|
+
state.hookUnregister();
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
state.hookUnregister = null;
|
|
251
|
+
}
|
|
252
|
+
const final = {
|
|
253
|
+
invocations: state.invocations,
|
|
254
|
+
digestsInjected: state.digestsInjected,
|
|
255
|
+
repeatsDetected: state.repeatsDetected,
|
|
256
|
+
aiHintsProvided: state.aiHintsProvided
|
|
257
|
+
};
|
|
258
|
+
state.history = [];
|
|
259
|
+
state.invocations = 0;
|
|
260
|
+
state.digestsInjected = 0;
|
|
261
|
+
state.repeatsDetected = 0;
|
|
262
|
+
state.aiHintsProvided = 0;
|
|
263
|
+
state.aiHintErrors = 0;
|
|
264
|
+
api.log.info("error-lens: teardown complete", { final });
|
|
265
|
+
},
|
|
266
|
+
async health() {
|
|
267
|
+
return {
|
|
268
|
+
ok: true,
|
|
269
|
+
message: `error-lens: ${state.digestsInjected} digest(s) injected, ${state.repeatsDetected} repeat(s) flagged, ${state.history.length} failure(s) in history`,
|
|
270
|
+
counters: {
|
|
271
|
+
invocations: state.invocations,
|
|
272
|
+
digestsInjected: state.digestsInjected,
|
|
273
|
+
repeatsDetected: state.repeatsDetected
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
var error_lens_default = plugin;
|
|
279
|
+
|
|
280
|
+
export { error_lens_default as default, extractErrorLine, extractFrames };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto-format-on-save plugin — PostToolUse hook that runs biome
|
|
5
|
+
* `format --write` on the file after every `write` or `edit`.
|
|
6
|
+
*
|
|
7
|
+
* Unlike lint-gate (which lints BEFORE the tool runs and can block),
|
|
8
|
+
* this plugin formats AFTER the write/edit commits — ensuring the
|
|
9
|
+
* file on disk always matches the project's formatting rules. No
|
|
10
|
+
* blocking, no warnings — just silently formats in-place.
|
|
11
|
+
*
|
|
12
|
+
* Tools registered:
|
|
13
|
+
* - format_on_save_status : Show config + per-session counters.
|
|
14
|
+
*
|
|
15
|
+
* Hooks registered:
|
|
16
|
+
* - PostToolUse with matcher `write|edit`. After the tool completes,
|
|
17
|
+
* runs `biome format --write <path>` on the actual file on disk.
|
|
18
|
+
* If the file changed (formatting was applied), logs the diff size.
|
|
19
|
+
* If biome fails or the file doesn't exist, silent fallback.
|
|
20
|
+
*
|
|
21
|
+
* Config (`config.extensions['format-on-save']`):
|
|
22
|
+
*
|
|
23
|
+
* ```jsonc
|
|
24
|
+
* {
|
|
25
|
+
* "enabled": true, // master switch
|
|
26
|
+
* "timeoutMs": 5000 // biome process timeout
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const plugin: Plugin;
|
|
34
|
+
|
|
35
|
+
export { plugin as default };
|