@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,219 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync, statSync } from 'fs';
3
+
4
+ // src/format-on-save/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ invocationCount: 0,
8
+ /** Times formatting was applied (file changed). */
9
+ formattedCount: 0,
10
+ /** Times the file was already formatted (no change). */
11
+ cleanCount: 0,
12
+ /** Times biome failed (not installed, timeout, parse error). */
13
+ errorCount: 0,
14
+ /** Hook handle for teardown. */
15
+ hookUnregister: null,
16
+ /** Last format result — surfaced by health() + status tool. */
17
+ lastResult: null
18
+ };
19
+ var DEFAULTS = {
20
+ enabled: true,
21
+ timeoutMs: 5e3
22
+ };
23
+ function readConfig(raw) {
24
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
25
+ const r = raw;
26
+ return {
27
+ enabled: r["enabled"] !== false,
28
+ timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs
29
+ };
30
+ }
31
+ function formatFile(filePath, timeoutMs) {
32
+ if (!existsSync(filePath)) return null;
33
+ let bytesBefore;
34
+ try {
35
+ bytesBefore = statSync(filePath).size;
36
+ } catch {
37
+ return null;
38
+ }
39
+ try {
40
+ execSync(`npx biome format --write "${filePath}"`, {
41
+ encoding: "utf-8",
42
+ timeout: timeoutMs,
43
+ cwd: process.cwd(),
44
+ stdio: ["pipe", "pipe", "pipe"]
45
+ });
46
+ } catch (err) {
47
+ const e = err;
48
+ if (e.killed) return null;
49
+ }
50
+ let bytesAfter;
51
+ try {
52
+ bytesAfter = statSync(filePath).size;
53
+ } catch {
54
+ return null;
55
+ }
56
+ if (bytesAfter !== bytesBefore) {
57
+ return { changed: true, bytesBefore, bytesAfter };
58
+ }
59
+ try {
60
+ execSync(`npx biome format "${filePath}"`, {
61
+ encoding: "utf-8",
62
+ timeout: timeoutMs,
63
+ cwd: process.cwd(),
64
+ stdio: ["pipe", "pipe", "pipe"]
65
+ });
66
+ return { changed: false, bytesBefore, bytesAfter };
67
+ } catch {
68
+ return { changed: true, bytesBefore, bytesAfter };
69
+ }
70
+ }
71
+ var plugin = {
72
+ name: "format-on-save",
73
+ version: "0.1.0",
74
+ description: "PostToolUse hook that runs biome format --write on the file after every write or edit",
75
+ apiVersion: API_VERSION,
76
+ capabilities: { tools: true, hooks: true },
77
+ defaultConfig: { ...DEFAULTS },
78
+ configSchema: {
79
+ type: "object",
80
+ properties: {
81
+ enabled: {
82
+ type: "boolean",
83
+ default: true,
84
+ description: "Master switch. When false, the hook is a no-op."
85
+ },
86
+ timeoutMs: {
87
+ type: "number",
88
+ minimum: 1e3,
89
+ default: 5e3,
90
+ description: "Biome format process timeout in milliseconds."
91
+ }
92
+ }
93
+ },
94
+ setup(api) {
95
+ state.invocationCount = 0;
96
+ state.formattedCount = 0;
97
+ state.cleanCount = 0;
98
+ state.errorCount = 0;
99
+ state.hookUnregister = null;
100
+ state.lastResult = null;
101
+ const cfg = readConfig(api.config.extensions?.["format-on-save"]);
102
+ let biomeAvailable = false;
103
+ try {
104
+ execSync("npx biome --version", {
105
+ encoding: "utf-8",
106
+ timeout: 5e3,
107
+ cwd: process.cwd(),
108
+ stdio: ["pipe", "pipe", "pipe"]
109
+ });
110
+ biomeAvailable = true;
111
+ api.log.info("format-on-save: biome detected");
112
+ } catch {
113
+ biomeAvailable = false;
114
+ api.log.warn("format-on-save: biome not found \u2014 hook will be a no-op");
115
+ }
116
+ const hook = (input) => {
117
+ if (!cfg.enabled || !biomeAvailable) return;
118
+ if (input.toolResult?.isError) return;
119
+ const toolName = input.toolName ?? "";
120
+ const inp = input.toolInput ?? {};
121
+ const filePath = inp["path"];
122
+ if (!filePath || typeof filePath !== "string") return;
123
+ state.invocationCount += 1;
124
+ const result = formatFile(filePath, cfg.timeoutMs);
125
+ if (!result) {
126
+ state.errorCount += 1;
127
+ return;
128
+ }
129
+ state.lastResult = {
130
+ path: filePath,
131
+ tool: toolName,
132
+ changed: result.changed,
133
+ bytesBefore: result.bytesBefore,
134
+ bytesAfter: result.bytesAfter,
135
+ when: (/* @__PURE__ */ new Date()).toISOString()
136
+ };
137
+ if (result.changed) {
138
+ state.formattedCount += 1;
139
+ const delta = result.bytesAfter - result.bytesBefore;
140
+ api.log.info(`format-on-save: formatted ${filePath}`, {
141
+ tool: toolName,
142
+ delta: `${delta >= 0 ? "+" : ""}${delta} bytes`
143
+ });
144
+ return {
145
+ additionalContext: `
146
+ \u{1F527} format-on-save: applied biome formatting to '${filePath}' after ${toolName}. The file on disk has been reformatted (${delta >= 0 ? "+" : ""}${delta} bytes).`
147
+ };
148
+ }
149
+ state.cleanCount += 1;
150
+ return;
151
+ };
152
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
153
+ api.tools.register({
154
+ name: "format_on_save_status",
155
+ description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error counters.",
156
+ inputSchema: { type: "object", properties: {} },
157
+ permission: "auto",
158
+ category: "Code Quality",
159
+ mutating: false,
160
+ async execute() {
161
+ return {
162
+ ok: true,
163
+ enabled: cfg.enabled,
164
+ biomeAvailable,
165
+ timeoutMs: cfg.timeoutMs,
166
+ counters: {
167
+ invocations: state.invocationCount,
168
+ formatted: state.formattedCount,
169
+ clean: state.cleanCount,
170
+ errors: state.errorCount
171
+ },
172
+ lastResult: state.lastResult
173
+ };
174
+ }
175
+ });
176
+ api.log.info("format-on-save plugin loaded", {
177
+ version: "0.1.0",
178
+ enabled: cfg.enabled,
179
+ biomeAvailable
180
+ });
181
+ },
182
+ teardown(api) {
183
+ if (state.hookUnregister) {
184
+ try {
185
+ state.hookUnregister();
186
+ } catch {
187
+ }
188
+ state.hookUnregister = null;
189
+ }
190
+ const final = {
191
+ invocations: state.invocationCount,
192
+ formatted: state.formattedCount,
193
+ clean: state.cleanCount,
194
+ errors: state.errorCount
195
+ };
196
+ state.invocationCount = 0;
197
+ state.formattedCount = 0;
198
+ state.cleanCount = 0;
199
+ state.errorCount = 0;
200
+ state.lastResult = null;
201
+ api.log.info("format-on-save: teardown complete", { final });
202
+ },
203
+ async health() {
204
+ return {
205
+ ok: true,
206
+ message: state.lastResult === null ? `format-on-save: ${state.invocationCount} invocation(s), ${state.formattedCount} formatted` : state.lastResult.changed ? `format-on-save: last formatted ${state.lastResult.path} (${state.lastResult.tool}) at ${state.lastResult.when}` : `format-on-save: last check on ${state.lastResult.path} was already clean`,
207
+ counters: {
208
+ invocations: state.invocationCount,
209
+ formatted: state.formattedCount,
210
+ clean: state.cleanCount,
211
+ errors: state.errorCount
212
+ },
213
+ lastResult: state.lastResult
214
+ };
215
+ }
216
+ };
217
+ var format_on_save_default = plugin;
218
+
219
+ export { format_on_save_default as default };
@@ -3,6 +3,9 @@ import { existsSync } from 'fs';
3
3
 
4
4
  // src/git-autocommit/index.ts
5
5
  var API_VERSION = "^0.1.10";
6
+ var commitCount = { value: 0 };
7
+ var lastCommit = { hash: null, at: null };
8
+ var llmGenerated = { value: 0 };
6
9
  function runGit(args, cwd) {
7
10
  try {
8
11
  return execFileSync("git", args, {
@@ -103,6 +106,54 @@ function generateCommitMessage(type, scope, summary, body) {
103
106
  ${body}` : "";
104
107
  return `${type}${scopePart}: ${summary}${footer}`;
105
108
  }
109
+ var VALID_TYPES = [
110
+ "feat",
111
+ "fix",
112
+ "docs",
113
+ "style",
114
+ "refactor",
115
+ "test",
116
+ "chore",
117
+ "perf",
118
+ "ci",
119
+ "build",
120
+ "revert"
121
+ ];
122
+ async function generateCommitFromDiff(api, stat, diff) {
123
+ if (!api.llm) return null;
124
+ try {
125
+ const result = await api.llm.complete(
126
+ `Write a Conventional Commits message for this staged git diff. Respond with ONLY a JSON object of the form {"type": string, "scope": string, "summary": string, "body": string}. type is one of: ${VALID_TYPES.join(", ")}. scope is a short area (empty string if unclear). summary is an imperative, lower-case, <=72-char subject with no trailing period. body is an optional short explanation (empty string if not needed). No prose outside the JSON.
127
+
128
+ Stat:
129
+ ${stat}
130
+
131
+ Diff:
132
+ ${diff}`,
133
+ {
134
+ system: "You are a precise release engineer writing Conventional Commits. Output only JSON.",
135
+ maxTokens: 400,
136
+ responseFormat: "json"
137
+ }
138
+ );
139
+ const parsed = JSON.parse(extractJsonObject(result.text));
140
+ const type = VALID_TYPES.includes(parsed.type) ? parsed.type : null;
141
+ const summary = typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : null;
142
+ if (!type || !summary) return null;
143
+ const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : void 0;
144
+ const body = typeof parsed.body === "string" && parsed.body.trim() ? parsed.body.trim() : void 0;
145
+ return { type, summary, ...scope ? { scope } : {}, ...body ? { body } : {} };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function extractJsonObject(text) {
151
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
152
+ const body = fenced?.[1] ?? text;
153
+ const start = body.indexOf("{");
154
+ const end = body.lastIndexOf("}");
155
+ return start >= 0 && end > start ? body.slice(start, end + 1) : body.trim();
156
+ }
106
157
  var plugin = {
107
158
  name: "git-autocommit",
108
159
  version: "0.2.0",
@@ -112,22 +163,40 @@ var plugin = {
112
163
  defaultConfig: {
113
164
  conventionalCommits: true,
114
165
  autoStage: false,
115
- defaultType: "feat"
166
+ defaultType: "feat",
167
+ useLlm: false
116
168
  },
117
169
  configSchema: {
118
170
  type: "object",
119
171
  properties: {
120
172
  conventionalCommits: { type: "boolean", default: true },
121
173
  autoStage: { type: "boolean", default: false },
122
- defaultType: { type: "string", default: "feat" }
174
+ defaultType: { type: "string", default: "feat" },
175
+ useLlm: {
176
+ type: "boolean",
177
+ default: false,
178
+ description: 'Auto-generate the commit message with the LLM (api.llm) when the caller supplies neither type nor message. Provider/model follow extensions["git-autocommit"].llm, then the session default.'
179
+ },
180
+ llm: {
181
+ type: "object",
182
+ description: "Optional { provider, model } override for LLM commit messages."
183
+ }
123
184
  }
124
185
  },
125
186
  setup(api) {
187
+ commitCount.value = 0;
188
+ llmGenerated.value = 0;
189
+ lastCommit.hash = null;
190
+ lastCommit.at = null;
126
191
  const extConfig = api.config.extensions?.["git-autocommit"];
127
192
  const opts = {
128
193
  conventionalCommits: extConfig?.["conventionalCommits"] ?? true,
129
194
  autoStage: extConfig?.["autoStage"] ?? false,
130
- defaultType: extConfig?.["defaultType"] ?? "feat"
195
+ defaultType: extConfig?.["defaultType"] ?? "feat",
196
+ // Opt-in: when true, git_autocommit writes the commit message with
197
+ // the LLM from the staged diff whenever the caller supplies neither
198
+ // `type` nor `message` (an explicit `generate: true` always asks).
199
+ useLlm: extConfig?.["useLlm"] ?? false
131
200
  };
132
201
  api.tools.register({
133
202
  name: "git_autocommit",
@@ -142,13 +211,33 @@ var plugin = {
142
211
  },
143
212
  type: {
144
213
  type: "string",
145
- enum: ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"],
214
+ enum: [
215
+ "feat",
216
+ "fix",
217
+ "docs",
218
+ "style",
219
+ "refactor",
220
+ "test",
221
+ "chore",
222
+ "perf",
223
+ "ci",
224
+ "build",
225
+ "revert"
226
+ ],
146
227
  description: "Conventional commit type"
147
228
  },
148
229
  scope: { type: "string", description: "Commit scope (e.g. auth, api, ui)" },
149
230
  message: { type: "string", description: "Commit summary message" },
150
231
  body: { type: "string", description: "Optional commit body/description" },
151
- dry_run: { type: "boolean", default: false, description: "Show what would be committed without committing" }
232
+ generate: {
233
+ type: "boolean",
234
+ description: "Write the conventional commit message with the LLM (api.llm) from the staged diff. Ignored when no LLM is wired."
235
+ },
236
+ dry_run: {
237
+ type: "boolean",
238
+ default: false,
239
+ description: "Show what would be committed without committing"
240
+ }
152
241
  }
153
242
  },
154
243
  permission: "confirm",
@@ -156,23 +245,14 @@ var plugin = {
156
245
  mutating: true,
157
246
  async execute(input, _ctx) {
158
247
  try {
159
- const type = input["type"] ?? opts.defaultType;
160
- const scope = input["scope"];
161
- const summary = input["message"] ?? "";
162
- const body = input["body"];
248
+ let type = input["type"];
249
+ let scope = input["scope"];
250
+ let summary = input["message"] ?? "";
251
+ let body = input["body"];
163
252
  const dryRun = input["dry_run"] ?? false;
164
- const validTypes = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"];
165
- if (!type || !validTypes.includes(type)) {
166
- if (dryRun) {
167
- return {
168
- ok: true,
169
- dry_run: true,
170
- message: `Would create: ${summary || "update code"}`
171
- };
172
- }
173
- return { ok: false, error: "type is required and must be a valid conventional commit type" };
174
- }
175
- const msg = generateCommitMessage(type, scope, summary || "update code", body);
253
+ const explicitAsk = input["generate"] === true;
254
+ const autoAsk = opts.useLlm && !input["type"] && !input["message"];
255
+ const wantGenerate = (explicitAsk || autoAsk) && Boolean(api.llm);
176
256
  let files;
177
257
  const rawFiles = input["files"];
178
258
  if (rawFiles !== void 0) {
@@ -185,7 +265,10 @@ var plugin = {
185
265
  try {
186
266
  stageFiles(files);
187
267
  } catch (err) {
188
- return { ok: false, error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}` };
268
+ return {
269
+ ok: false,
270
+ error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`
271
+ };
189
272
  }
190
273
  }
191
274
  let staged = [];
@@ -211,8 +294,51 @@ var plugin = {
211
294
  } catch {
212
295
  }
213
296
  }
297
+ const { stat, diff: stagedDiff } = getStagedDiff();
298
+ let generatedByLlm = false;
299
+ if (wantGenerate && staged.length > 0) {
300
+ const g = await generateCommitFromDiff(api, stat, stagedDiff);
301
+ if (g) {
302
+ type = g.type;
303
+ if (g.scope) scope = g.scope;
304
+ summary = g.summary;
305
+ if (g.body && !body) body = g.body;
306
+ generatedByLlm = true;
307
+ }
308
+ }
309
+ if (!type) type = opts.defaultType;
310
+ const validTypes = [
311
+ "feat",
312
+ "fix",
313
+ "docs",
314
+ "style",
315
+ "refactor",
316
+ "test",
317
+ "chore",
318
+ "perf",
319
+ "ci",
320
+ "build",
321
+ "revert"
322
+ ];
323
+ if (!type || !validTypes.includes(type)) {
324
+ if (dryRun) {
325
+ return {
326
+ ok: true,
327
+ dry_run: true,
328
+ message: `Would create: ${summary || "update code"}`
329
+ };
330
+ }
331
+ return {
332
+ ok: false,
333
+ error: "type is required and must be a valid conventional commit type"
334
+ };
335
+ }
336
+ const msg = generateCommitMessage(type, scope, summary || "update code", body);
214
337
  if (staged.length === 0) {
215
- return { ok: false, error: "Nothing staged. Add files with git add or provide files input." };
338
+ return {
339
+ ok: false,
340
+ error: "Nothing staged. Add files with git add or provide files input."
341
+ };
216
342
  }
217
343
  const worktreeWarn = simultaneousEditWarning();
218
344
  const externalChanges = externalChangesSinceStage();
@@ -223,7 +349,6 @@ var plugin = {
223
349
  externalWarning = `\u26A0 External changes detected since staging: ${preview}${suffix}. Another agent may be modifying files concurrently. These unstaged changes will NOT be included in this commit, but they indicate simultaneous edits. Review carefully.`;
224
350
  }
225
351
  const warning = [worktreeWarn, externalWarning].filter(Boolean).join("\n") || void 0;
226
- const { stat, diff: stagedDiff } = getStagedDiff();
227
352
  if (dryRun) {
228
353
  return {
229
354
  ok: true,
@@ -251,9 +376,16 @@ ${stagedDiff}
251
376
  try {
252
377
  hash = commitWithMessage(msg);
253
378
  } catch (err) {
254
- return { ok: false, error: `Failed to commit: ${err instanceof Error ? err.message : String(err)}` };
379
+ return {
380
+ ok: false,
381
+ error: `Failed to commit: ${err instanceof Error ? err.message : String(err)}`
382
+ };
255
383
  }
256
384
  api.log.info("git-autocommit: created commit", { hash, type, scope });
385
+ commitCount.value += 1;
386
+ if (generatedByLlm) llmGenerated.value += 1;
387
+ lastCommit.hash = String(hash);
388
+ lastCommit.at = (/* @__PURE__ */ new Date()).toISOString();
257
389
  try {
258
390
  await api.session.append({
259
391
  type: "git-autocommit:commit",
@@ -274,6 +406,7 @@ ${stagedDiff}
274
406
  stagedFiles: staged,
275
407
  type,
276
408
  scope: scope ?? null,
409
+ generatedByLlm,
277
410
  warning: warning ?? void 0,
278
411
  diff: `
279
412
  ## Staged diff
@@ -285,7 +418,10 @@ ${preCommitDiff}
285
418
  \`\`\``
286
419
  };
287
420
  } catch (err) {
288
- return { ok: false, error: `Uncaught error in git_autocommit: ${err instanceof Error ? err.message : String(err)}` };
421
+ return {
422
+ ok: false,
423
+ error: `Uncaught error in git_autocommit: ${err instanceof Error ? err.message : String(err)}`
424
+ };
289
425
  }
290
426
  }
291
427
  });
@@ -293,6 +429,30 @@ ${preCommitDiff}
293
429
  version: "0.2.0",
294
430
  conventionalCommits: opts.conventionalCommits
295
431
  });
432
+ },
433
+ teardown(api) {
434
+ const finalCount = commitCount.value;
435
+ const finalHash = lastCommit.hash;
436
+ const finalLlm = llmGenerated.value;
437
+ commitCount.value = 0;
438
+ llmGenerated.value = 0;
439
+ lastCommit.hash = null;
440
+ lastCommit.at = null;
441
+ api.log.info("git-autocommit: teardown complete", {
442
+ commits: finalCount,
443
+ llmGenerated: finalLlm,
444
+ lastHash: finalHash
445
+ });
446
+ },
447
+ async health() {
448
+ return {
449
+ ok: true,
450
+ message: commitCount.value === 0 ? "git-autocommit: no commits yet this session" : `git-autocommit: ${commitCount.value} commit(s) (${llmGenerated.value} LLM-written), last ${String(lastCommit.hash).slice(0, 8)} at ${lastCommit.at}`,
451
+ commits: commitCount.value,
452
+ llmGenerated: llmGenerated.value,
453
+ lastCommitHash: lastCommit.hash,
454
+ lastCommitAt: lastCommit.at
455
+ };
296
456
  }
297
457
  };
298
458
  var git_autocommit_default = plugin;
@@ -0,0 +1,52 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * import-organizer plugin — PostToolUse hook that re-sorts and
5
+ * de-duplicates imports in a file after every `write` or `edit`.
6
+ *
7
+ * This is a heavier, post-write step than `format-on-save` (which only
8
+ * handles whitespace/formatting). It runs `biome check --write --unsafe`
9
+ * (or `eslint --fix` as a fallback) on the saved file. The `--unsafe`
10
+ * flag enables import-organization rules:
11
+ * - Sort imports alphabetically within import groups
12
+ * - Group by source (builtin, external, internal, relative)
13
+ * - Remove unused imports
14
+ * - Merge duplicate imports from the same module
15
+ *
16
+ * Tools registered:
17
+ * - import_organizer_status : Show config + per-session counters
18
+ * (invocations / organized / clean / errors + lastResult).
19
+ *
20
+ * Hooks registered:
21
+ * - PostToolUse with matcher `write|edit`. After the tool completes,
22
+ * runs the configured command on the file on disk. The hook reads
23
+ * the file fresh from disk (so `edit` tool's post-edit state is
24
+ * captured) and detects whether the file changed via byte-count
25
+ * comparison. If the file was modified, returns `additionalContext`
26
+ * so the LLM sees that imports were reorganized.
27
+ *
28
+ * Linter detection is lazy: on the first hook invocation, the plugin
29
+ * tries `biome` first (since `--unsafe` is required for import
30
+ * organization), then falls back to `eslint --fix`. If neither
31
+ * succeeds, the hook logs a one-time warning and becomes a no-op for
32
+ * the rest of the session. Linter presence is re-checked on every
33
+ * setup() call so plugin reload can recover if a linter is installed
34
+ * mid-session.
35
+ *
36
+ * Config (`config.extensions['import-organizer']`):
37
+ *
38
+ * ```jsonc
39
+ * {
40
+ * "enabled": true,
41
+ * "command": "npx @biomejs/biome check --write --unsafe",
42
+ * "fallbackCommand": "npx eslint --fix",
43
+ * "timeoutMs": 10000
44
+ * }
45
+ * ```
46
+ *
47
+ * @public
48
+ */
49
+
50
+ declare const plugin: Plugin;
51
+
52
+ export { plugin as default };