@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
@@ -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 };