@wrongstack/plugins 0.281.1 → 0.282.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 (78) hide show
  1. package/README.md +29 -3
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-i18n-extractor.d.ts +36 -0
  9. package/dist/auto-i18n-extractor.js +335 -0
  10. package/dist/checkpoint.js +18 -0
  11. package/dist/code-metrics.d.ts +31 -0
  12. package/dist/code-metrics.js +338 -0
  13. package/dist/commit-validator.js +67 -12
  14. package/dist/cost-tracker.js +58 -16
  15. package/dist/dead-code-detector.d.ts +34 -0
  16. package/dist/dead-code-detector.js +354 -0
  17. package/dist/dep-guard.js +47 -6
  18. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  19. package/dist/dependency-vulnerability-gate.js +308 -0
  20. package/dist/diff-summary.js +97 -10
  21. package/dist/doc-sync-guard.d.ts +33 -0
  22. package/dist/doc-sync-guard.js +223 -0
  23. package/dist/duplicate-code-detector.d.ts +33 -0
  24. package/dist/duplicate-code-detector.js +384 -0
  25. package/dist/feature-flag-tracker.d.ts +38 -0
  26. package/dist/feature-flag-tracker.js +316 -0
  27. package/dist/file-watcher.js +85 -36
  28. package/dist/format-on-save.js +76 -10
  29. package/dist/import-organizer.js +73 -14
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.js +14067 -5054
  32. package/dist/interface-contract-guard.d.ts +37 -0
  33. package/dist/interface-contract-guard.js +302 -0
  34. package/dist/knowledge-graph.d.ts +45 -0
  35. package/dist/knowledge-graph.js +325 -0
  36. package/dist/license-audit-gate.d.ts +34 -0
  37. package/dist/license-audit-gate.js +260 -0
  38. package/dist/llm-cache.js +5 -0
  39. package/dist/loop-breaker.d.ts +0 -38
  40. package/dist/loop-breaker.js +209 -8
  41. package/dist/migration-planner.d.ts +30 -0
  42. package/dist/migration-planner.js +349 -0
  43. package/dist/model-router.js +5 -0
  44. package/dist/performance-regression-gate.d.ts +33 -0
  45. package/dist/performance-regression-gate.js +315 -0
  46. package/dist/plugin-stack-observer.d.ts +35 -0
  47. package/dist/plugin-stack-observer.js +125 -0
  48. package/dist/pr-drafter.d.ts +35 -0
  49. package/dist/pr-drafter.js +334 -0
  50. package/dist/prompt-firewall.js +5 -0
  51. package/dist/refactor-suggester.d.ts +38 -0
  52. package/dist/refactor-suggester.js +382 -0
  53. package/dist/release-notes-generator.d.ts +27 -0
  54. package/dist/release-notes-generator.js +209 -0
  55. package/dist/schema-evolution-guard.d.ts +42 -0
  56. package/dist/schema-evolution-guard.js +319 -0
  57. package/dist/security-hotspot-scanner.d.ts +30 -0
  58. package/dist/security-hotspot-scanner.js +402 -0
  59. package/dist/semantic-search-indexer.d.ts +38 -0
  60. package/dist/semantic-search-indexer.js +436 -0
  61. package/dist/shell-check.js +38 -3
  62. package/dist/smart-rename.d.ts +26 -0
  63. package/dist/smart-rename.js +170 -0
  64. package/dist/spec-linker.js +273 -133
  65. package/dist/test-coverage-gate.d.ts +37 -0
  66. package/dist/test-coverage-gate.js +263 -0
  67. package/dist/test-flake-detector.d.ts +27 -0
  68. package/dist/test-flake-detector.js +274 -0
  69. package/dist/test-generator.d.ts +32 -0
  70. package/dist/test-generator.js +243 -0
  71. package/dist/test-runner-gate.js +154 -22
  72. package/dist/todo-listener.d.ts +2 -2
  73. package/dist/todo-listener.js +5 -5
  74. package/dist/token-throttle.js +5 -0
  75. package/dist/type-gate.d.ts +37 -0
  76. package/dist/type-gate.js +311 -0
  77. package/package.json +112 -4
  78. package/LICENSE +0 -21
@@ -1,20 +1,38 @@
1
+ import { execFileSync } from 'child_process';
2
+
1
3
  // src/loop-breaker/index.ts
2
4
  var state = {
3
5
  lastFingerprint: null,
4
6
  streak: 0,
5
7
  recent: [],
8
+ pendingBlockReason: null,
9
+ lastDiffFingerprint: null,
10
+ noDiffStreak: 0,
11
+ lastErrorFingerprint: null,
12
+ repeatedErrorStreak: 0,
6
13
  invocations: 0,
14
+ postInvocations: 0,
7
15
  warnings: 0,
8
16
  blocks: 0,
9
17
  oscillationsDetected: 0,
18
+ stepBudgetBlocks: 0,
19
+ noDiffWarnings: 0,
20
+ noDiffBlocks: 0,
21
+ repeatedErrorWarnings: 0,
22
+ repeatedErrorBlocks: 0,
10
23
  hookUnregister: null
11
24
  };
12
25
  var DEFAULTS = {
13
26
  enabled: true,
14
- mode: "block",
27
+ mode: "warn",
15
28
  warnAfter: 3,
16
29
  blockAfter: 5,
17
30
  oscillationWindow: 8,
31
+ maxSteps: 200,
32
+ noDiffWarnAfter: 6,
33
+ noDiffBlockAfter: 10,
34
+ repeatedErrorWarnAfter: 2,
35
+ repeatedErrorBlockAfter: 3,
18
36
  ignoreTools: []
19
37
  };
20
38
  function readConfig(raw) {
@@ -28,6 +46,11 @@ function readConfig(raw) {
28
46
  warnAfter,
29
47
  blockAfter,
30
48
  oscillationWindow: typeof r["oscillationWindow"] === "number" && r["oscillationWindow"] >= 4 ? r["oscillationWindow"] : DEFAULTS.oscillationWindow,
49
+ maxSteps: typeof r["maxSteps"] === "number" && r["maxSteps"] >= 0 ? Math.floor(r["maxSteps"]) : DEFAULTS.maxSteps,
50
+ noDiffWarnAfter: typeof r["noDiffWarnAfter"] === "number" && r["noDiffWarnAfter"] >= 1 ? Math.floor(r["noDiffWarnAfter"]) : DEFAULTS.noDiffWarnAfter,
51
+ noDiffBlockAfter: typeof r["noDiffBlockAfter"] === "number" && r["noDiffBlockAfter"] >= 0 ? Math.floor(r["noDiffBlockAfter"]) : DEFAULTS.noDiffBlockAfter,
52
+ repeatedErrorWarnAfter: typeof r["repeatedErrorWarnAfter"] === "number" && r["repeatedErrorWarnAfter"] >= 1 ? Math.floor(r["repeatedErrorWarnAfter"]) : DEFAULTS.repeatedErrorWarnAfter,
53
+ repeatedErrorBlockAfter: typeof r["repeatedErrorBlockAfter"] === "number" && r["repeatedErrorBlockAfter"] >= 0 ? Math.floor(r["repeatedErrorBlockAfter"]) : DEFAULTS.repeatedErrorBlockAfter,
31
54
  ignoreTools: Array.isArray(r["ignoreTools"]) ? r["ignoreTools"].filter((t) => typeof t === "string") : []
32
55
  };
33
56
  }
@@ -62,6 +85,32 @@ function isOscillating(recent, windowSize) {
62
85
  }
63
86
  return window[0] !== window[1];
64
87
  }
88
+ var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
89
+ function hashString(value) {
90
+ let h = 5381;
91
+ const cap = Math.min(value.length, 1e6);
92
+ for (let i = 0; i < cap; i++) {
93
+ h = (h << 5) + h + value.charCodeAt(i) | 0;
94
+ }
95
+ return String(h >>> 0);
96
+ }
97
+ function gitDiffFingerprint(cwd) {
98
+ try {
99
+ const diff = execFileSync("git", ["diff", "--no-ext-diff", "--"], {
100
+ cwd,
101
+ encoding: "utf8",
102
+ timeout: 1e3,
103
+ maxBuffer: 1e6,
104
+ stdio: ["ignore", "pipe", "ignore"]
105
+ });
106
+ return diff.length === 0 ? "" : hashString(diff);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ function normalizeError(content) {
112
+ return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 12).join("\n").replace(/\b\d{2,}\b/g, "<n>").slice(0, 1e3);
113
+ }
65
114
  var plugin = {
66
115
  name: "loop-breaker",
67
116
  version: "0.1.0",
@@ -75,8 +124,8 @@ var plugin = {
75
124
  enabled: { type: "boolean", default: true, description: "Master switch." },
76
125
  mode: {
77
126
  type: "string",
78
- enum: ["block", "warn"],
79
- default: "block",
127
+ enum: ["warn", "block"],
128
+ default: "warn",
80
129
  description: "block = refuse the repeated call; warn = only inject context."
81
130
  },
82
131
  warnAfter: {
@@ -97,6 +146,36 @@ var plugin = {
97
146
  default: 8,
98
147
  description: "Recent-call window length used for A-B-A-B oscillation detection."
99
148
  },
149
+ maxSteps: {
150
+ type: "number",
151
+ minimum: 0,
152
+ default: 200,
153
+ description: "Maximum tool steps before blocking; 0 disables the step budget."
154
+ },
155
+ noDiffWarnAfter: {
156
+ type: "number",
157
+ minimum: 1,
158
+ default: 6,
159
+ description: "Successful edit/write steps without a changed git diff before warning."
160
+ },
161
+ noDiffBlockAfter: {
162
+ type: "number",
163
+ minimum: 0,
164
+ default: 10,
165
+ description: "Successful edit/write steps without a changed git diff before blocking next step; 0 disables."
166
+ },
167
+ repeatedErrorWarnAfter: {
168
+ type: "number",
169
+ minimum: 1,
170
+ default: 2,
171
+ description: "Consecutive identical tool errors before warning."
172
+ },
173
+ repeatedErrorBlockAfter: {
174
+ type: "number",
175
+ minimum: 0,
176
+ default: 3,
177
+ description: "Consecutive identical tool errors before blocking next step; 0 disables."
178
+ },
100
179
  ignoreTools: {
101
180
  type: "array",
102
181
  items: { type: "string" },
@@ -109,10 +188,21 @@ var plugin = {
109
188
  state.lastFingerprint = null;
110
189
  state.streak = 0;
111
190
  state.recent = [];
191
+ state.pendingBlockReason = null;
192
+ state.lastDiffFingerprint = null;
193
+ state.noDiffStreak = 0;
194
+ state.lastErrorFingerprint = null;
195
+ state.repeatedErrorStreak = 0;
112
196
  state.invocations = 0;
197
+ state.postInvocations = 0;
113
198
  state.warnings = 0;
114
199
  state.blocks = 0;
115
200
  state.oscillationsDetected = 0;
201
+ state.stepBudgetBlocks = 0;
202
+ state.noDiffWarnings = 0;
203
+ state.noDiffBlocks = 0;
204
+ state.repeatedErrorWarnings = 0;
205
+ state.repeatedErrorBlocks = 0;
116
206
  if (state.hookUnregister) {
117
207
  try {
118
208
  state.hookUnregister();
@@ -126,6 +216,23 @@ var plugin = {
126
216
  const toolName = input.toolName ?? "unknown";
127
217
  if (cfg.ignoreTools.includes(toolName)) return;
128
218
  state.invocations += 1;
219
+ if (state.pendingBlockReason && cfg.mode === "block") {
220
+ const reason = state.pendingBlockReason;
221
+ state.pendingBlockReason = null;
222
+ state.blocks += 1;
223
+ api.metrics.counter("blocks");
224
+ return { decision: "block", reason };
225
+ }
226
+ if (cfg.maxSteps > 0 && state.invocations > cfg.maxSteps && cfg.mode === "block") {
227
+ state.blocks += 1;
228
+ state.stepBudgetBlocks += 1;
229
+ api.metrics.counter("blocks");
230
+ api.metrics.counter("step_budget_blocks");
231
+ return {
232
+ decision: "block",
233
+ reason: `loop-breaker: step budget exceeded (${state.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
234
+ };
235
+ }
129
236
  const fp = fingerprint(toolName, input.toolInput);
130
237
  if (fp === state.lastFingerprint) {
131
238
  state.streak += 1;
@@ -165,7 +272,65 @@ var plugin = {
165
272
  }
166
273
  return;
167
274
  };
168
- state.hookUnregister = api.registerHook("PreToolUse", "*", hook);
275
+ const postHook = (input) => {
276
+ if (!cfg.enabled) return;
277
+ const toolName = input.toolName ?? "unknown";
278
+ if (cfg.ignoreTools.includes(toolName)) return;
279
+ state.postInvocations += 1;
280
+ if (input.toolResult?.isError) {
281
+ const errorFingerprint = normalizeError(input.toolResult.content);
282
+ if (errorFingerprint && errorFingerprint === state.lastErrorFingerprint) {
283
+ state.repeatedErrorStreak += 1;
284
+ } else {
285
+ state.lastErrorFingerprint = errorFingerprint;
286
+ state.repeatedErrorStreak = errorFingerprint ? 1 : 0;
287
+ }
288
+ state.noDiffStreak = 0;
289
+ if (cfg.repeatedErrorBlockAfter > 0 && state.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
290
+ state.repeatedErrorBlocks += 1;
291
+ state.pendingBlockReason = `loop-breaker: the same tool error repeated ${state.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
292
+ }
293
+ if (state.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
294
+ state.repeatedErrorWarnings += 1;
295
+ api.metrics.counter("repeated_error_warnings");
296
+ return {
297
+ decision: "allow",
298
+ additionalContext: `loop-breaker: same error repeated ${state.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
299
+ };
300
+ }
301
+ return;
302
+ }
303
+ state.lastErrorFingerprint = null;
304
+ state.repeatedErrorStreak = 0;
305
+ if (!MUTATING_TOOLS.has(toolName)) return;
306
+ const diffFingerprint = gitDiffFingerprint(input.cwd ?? process.cwd());
307
+ if (diffFingerprint === null) return;
308
+ if (diffFingerprint === state.lastDiffFingerprint) {
309
+ state.noDiffStreak += 1;
310
+ } else {
311
+ state.lastDiffFingerprint = diffFingerprint;
312
+ state.noDiffStreak = 0;
313
+ }
314
+ if (cfg.noDiffBlockAfter > 0 && state.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
315
+ state.noDiffBlocks += 1;
316
+ state.pendingBlockReason = `loop-breaker: no diff was produced in the last ${state.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
317
+ }
318
+ if (state.noDiffStreak >= cfg.noDiffWarnAfter) {
319
+ state.noDiffWarnings += 1;
320
+ api.metrics.counter("no_diff_warnings");
321
+ return {
322
+ decision: "allow",
323
+ additionalContext: `loop-breaker: no diff has changed for ${state.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
324
+ };
325
+ }
326
+ return;
327
+ };
328
+ const unregisterPre = api.registerHook("PreToolUse", "*", hook);
329
+ const unregisterPost = api.registerHook("PostToolUse", "*", postHook);
330
+ state.hookUnregister = () => {
331
+ unregisterPre();
332
+ unregisterPost();
333
+ };
169
334
  api.tools.register({
170
335
  name: "loop_breaker_status",
171
336
  description: "Reports loop-breaker state: config, current repeat streak, and counters (warnings, blocks, oscillations).",
@@ -181,13 +346,26 @@ var plugin = {
181
346
  warnAfter: cfg.warnAfter,
182
347
  blockAfter: cfg.blockAfter,
183
348
  oscillationWindow: cfg.oscillationWindow,
349
+ maxSteps: cfg.maxSteps,
350
+ noDiffWarnAfter: cfg.noDiffWarnAfter,
351
+ noDiffBlockAfter: cfg.noDiffBlockAfter,
352
+ repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
353
+ repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
184
354
  ignoreTools: cfg.ignoreTools,
185
355
  currentStreak: state.streak,
356
+ noDiffStreak: state.noDiffStreak,
357
+ repeatedErrorStreak: state.repeatedErrorStreak,
186
358
  counters: {
187
359
  invocations: state.invocations,
360
+ postInvocations: state.postInvocations,
188
361
  warnings: state.warnings,
189
362
  blocks: state.blocks,
190
- oscillationsDetected: state.oscillationsDetected
363
+ oscillationsDetected: state.oscillationsDetected,
364
+ stepBudgetBlocks: state.stepBudgetBlocks,
365
+ noDiffWarnings: state.noDiffWarnings,
366
+ noDiffBlocks: state.noDiffBlocks,
367
+ repeatedErrorWarnings: state.repeatedErrorWarnings,
368
+ repeatedErrorBlocks: state.repeatedErrorBlocks
191
369
  }
192
370
  };
193
371
  }
@@ -210,28 +388,51 @@ var plugin = {
210
388
  }
211
389
  const final = {
212
390
  invocations: state.invocations,
391
+ postInvocations: state.postInvocations,
213
392
  warnings: state.warnings,
214
393
  blocks: state.blocks,
215
- oscillationsDetected: state.oscillationsDetected
394
+ oscillationsDetected: state.oscillationsDetected,
395
+ stepBudgetBlocks: state.stepBudgetBlocks,
396
+ noDiffWarnings: state.noDiffWarnings,
397
+ noDiffBlocks: state.noDiffBlocks,
398
+ repeatedErrorWarnings: state.repeatedErrorWarnings,
399
+ repeatedErrorBlocks: state.repeatedErrorBlocks
216
400
  };
217
401
  state.lastFingerprint = null;
218
402
  state.streak = 0;
219
403
  state.recent = [];
404
+ state.pendingBlockReason = null;
405
+ state.lastDiffFingerprint = null;
406
+ state.noDiffStreak = 0;
407
+ state.lastErrorFingerprint = null;
408
+ state.repeatedErrorStreak = 0;
220
409
  state.invocations = 0;
410
+ state.postInvocations = 0;
221
411
  state.warnings = 0;
222
412
  state.blocks = 0;
223
413
  state.oscillationsDetected = 0;
414
+ state.stepBudgetBlocks = 0;
415
+ state.noDiffWarnings = 0;
416
+ state.noDiffBlocks = 0;
417
+ state.repeatedErrorWarnings = 0;
418
+ state.repeatedErrorBlocks = 0;
224
419
  api.log.info("loop-breaker: teardown complete", { final });
225
420
  },
226
421
  async health() {
227
422
  return {
228
423
  ok: true,
229
- message: `loop-breaker: ${state.invocations} call(s) observed, ${state.warnings} warning(s), ${state.blocks} block(s), ${state.oscillationsDetected} oscillation(s)`,
424
+ message: `loop-breaker: ${state.invocations} call(s) observed, ${state.warnings + state.noDiffWarnings + state.repeatedErrorWarnings} warning(s), ${state.blocks} block(s), ${state.oscillationsDetected} oscillation(s)`,
230
425
  counters: {
231
426
  invocations: state.invocations,
427
+ postInvocations: state.postInvocations,
232
428
  warnings: state.warnings,
233
429
  blocks: state.blocks,
234
- oscillationsDetected: state.oscillationsDetected
430
+ oscillationsDetected: state.oscillationsDetected,
431
+ stepBudgetBlocks: state.stepBudgetBlocks,
432
+ noDiffWarnings: state.noDiffWarnings,
433
+ noDiffBlocks: state.noDiffBlocks,
434
+ repeatedErrorWarnings: state.repeatedErrorWarnings,
435
+ repeatedErrorBlocks: state.repeatedErrorBlocks
235
436
  }
236
437
  };
237
438
  }
@@ -0,0 +1,30 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * migration-planner plugin — helps plan dependency or framework migrations.
5
+ *
6
+ * The plugin reads a package CHANGELOG (project root or node_modules) and
7
+ * produces a migration checklist with breaking changes and recommended steps
8
+ * between two versions. If no changelog is available it returns a generic
9
+ * migration guide.
10
+ *
11
+ * Tools registered:
12
+ * - migration_plan — produce a migration checklist for a package/version range
13
+ * - migration_status — report plugin state and counters
14
+ *
15
+ * Config (`config.extensions['migration-planner']`):
16
+ *
17
+ * ```jsonc
18
+ * {
19
+ * "enabled": true,
20
+ * "changelogPaths": ["CHANGELOG.md"],
21
+ * "maxChars": 100_000
22
+ * }
23
+ * ```
24
+ *
25
+ * @public
26
+ */
27
+
28
+ declare const plugin: Plugin;
29
+
30
+ export { plugin as default };
@@ -0,0 +1,349 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { isAbsolute, resolve, relative } from 'path';
3
+
4
+ // src/migration-planner/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ plansGenerated: 0,
8
+ statusQueries: 0,
9
+ fallbackCount: 0,
10
+ lastPlan: null,
11
+ hookUnregister: null
12
+ };
13
+ var DEFAULTS = {
14
+ enabled: true,
15
+ changelogPaths: ["CHANGELOG.md"],
16
+ maxChars: 1e5
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
+ changelogPaths: Array.isArray(r["changelogPaths"]) ? r["changelogPaths"].filter((x) => typeof x === "string") : DEFAULTS.changelogPaths,
24
+ maxChars: typeof r["maxChars"] === "number" && r["maxChars"] >= 1e3 && r["maxChars"] <= 1e6 ? r["maxChars"] : DEFAULTS.maxChars
25
+ };
26
+ }
27
+ function withinProject(p) {
28
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
29
+ const root = process.cwd();
30
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
31
+ const rel = relative(root, resolved);
32
+ if (rel === "" || rel === ".") return true;
33
+ if (rel.startsWith("..")) return false;
34
+ if (isAbsolute(rel)) return false;
35
+ return true;
36
+ }
37
+ function normalizeVersion(v) {
38
+ return v.replace(/^v/i, "").trim();
39
+ }
40
+ function readChangelog(packageName, cfg) {
41
+ const candidates = cfg.changelogPaths.map((p) => p.replace(/<package>/g, packageName));
42
+ candidates.push(`node_modules/${packageName}/CHANGELOG.md`);
43
+ candidates.push(`node_modules/${packageName}/changelog.md`);
44
+ for (const candidate of candidates) {
45
+ if (!withinProject(candidate)) continue;
46
+ if (existsSync(candidate)) {
47
+ try {
48
+ const content = readFileSync(candidate, "utf-8");
49
+ return { source: candidate, content: content.slice(0, cfg.maxChars) };
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function extractVersionSections(changelog, fromVersion, toVersion) {
57
+ const fromNv = normalizeVersion(fromVersion);
58
+ const toNv = normalizeVersion(toVersion);
59
+ const sections = [];
60
+ let current = null;
61
+ for (const line of changelog.split(/\r?\n/)) {
62
+ const match = line.match(/^#{2,3}\s+(\[?v?(\d+\.\d+\.\d+[^[\]\s]*)\]?)\s*(.*)$/);
63
+ if (match) {
64
+ if (current) sections.push(current);
65
+ current = { version: normalizeVersion(match[2]), header: match[1], body: "" };
66
+ } else if (current) {
67
+ current.body += `${line}
68
+ `;
69
+ }
70
+ }
71
+ if (current) sections.push(current);
72
+ if (sections.length === 0) return [changelog];
73
+ const relevant = [];
74
+ for (const section of sections) {
75
+ if (section.version === toNv) {
76
+ relevant.push(`## ${section.header}
77
+ ${section.body}`);
78
+ } else if (section.version === fromNv) {
79
+ break;
80
+ } else if (relevant.length > 0) {
81
+ relevant.push(`## ${section.header}
82
+ ${section.body}`);
83
+ }
84
+ }
85
+ return relevant.length > 0 ? relevant : [changelog];
86
+ }
87
+ function extractBreakingChanges(sectionText) {
88
+ const breaking = [];
89
+ let inBreakingSection = false;
90
+ for (const rawLine of sectionText.split(/\r?\n/)) {
91
+ const line = rawLine.trim();
92
+ if (!line) {
93
+ inBreakingSection = false;
94
+ continue;
95
+ }
96
+ if (/^#{3,4}\s+(?:BREAKING\s+CHANGES?|Breaking\s+Changes?|Breaking)/i.test(line)) {
97
+ inBreakingSection = true;
98
+ continue;
99
+ }
100
+ if (/^#{1,4}\s+/.test(line)) {
101
+ inBreakingSection = false;
102
+ continue;
103
+ }
104
+ if (inBreakingSection) {
105
+ const item = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
106
+ if (item) breaking.push(item);
107
+ } else if (/^\s*[-*]\s+.*(?:BREAKING|breaking|removed|deprecated|no longer supported)/i.test(rawLine)) {
108
+ breaking.push(line.replace(/^[-*]\s+/, ""));
109
+ }
110
+ }
111
+ return breaking;
112
+ }
113
+ function extractRecommendedSteps(sectionText) {
114
+ const steps = [];
115
+ let inMigrationSection = false;
116
+ for (const rawLine of sectionText.split(/\r?\n/)) {
117
+ const line = rawLine.trim();
118
+ if (!line) {
119
+ inMigrationSection = false;
120
+ continue;
121
+ }
122
+ if (/^#{3,4}\s+(?:Migration|Upgrade|How to|Steps|Recommended)/i.test(line)) {
123
+ inMigrationSection = true;
124
+ continue;
125
+ }
126
+ if (/^#{1,4}\s+/.test(line)) {
127
+ inMigrationSection = false;
128
+ continue;
129
+ }
130
+ if (inMigrationSection) {
131
+ const item = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
132
+ if (item) steps.push(item);
133
+ }
134
+ }
135
+ if (steps.length === 0) {
136
+ steps.push("Review the changelog for deprecated APIs.");
137
+ steps.push("Update imports and call sites to new API signatures.");
138
+ steps.push("Run the test suite and fix regressions.");
139
+ steps.push("Verify type-checking passes with the new version.");
140
+ }
141
+ return steps;
142
+ }
143
+ function buildGenericGuide(packageName, fromVersion, toVersion, scope) {
144
+ return {
145
+ breakingChanges: [
146
+ `No changelog found for ${packageName}. Unknown breaking changes between ${fromVersion} and ${toVersion}.`
147
+ ],
148
+ recommendedSteps: [
149
+ `Visit ${packageName} release notes or GitHub releases for ${toVersion}.`,
150
+ scope ? `Review ${scope} usage of ${packageName} for API changes.` : `Search the codebase for direct ${packageName} usage.`,
151
+ `Update ${packageName} from ${fromVersion} to ${toVersion} in package.json.`,
152
+ "Run install and the full test suite.",
153
+ "Fix type errors and runtime regressions."
154
+ ]
155
+ };
156
+ }
157
+ var plugin = {
158
+ name: "migration-planner",
159
+ version: "0.1.0",
160
+ description: "Helps plan dependency or framework migrations by reading changelogs and producing checklists",
161
+ apiVersion: API_VERSION,
162
+ capabilities: { tools: true, hooks: true },
163
+ defaultConfig: { ...DEFAULTS },
164
+ configSchema: {
165
+ type: "object",
166
+ properties: {
167
+ enabled: {
168
+ type: "boolean",
169
+ default: true,
170
+ description: "Master switch."
171
+ },
172
+ changelogPaths: {
173
+ type: "array",
174
+ items: { type: "string" },
175
+ default: ["CHANGELOG.md"],
176
+ description: "Changelog paths to try; <package> is replaced with the package name."
177
+ },
178
+ maxChars: {
179
+ type: "number",
180
+ minimum: 1e3,
181
+ maximum: 1e6,
182
+ default: 1e5,
183
+ description: "Maximum changelog characters to scan."
184
+ }
185
+ }
186
+ },
187
+ setup(api) {
188
+ state.plansGenerated = 0;
189
+ state.statusQueries = 0;
190
+ state.fallbackCount = 0;
191
+ state.lastPlan = null;
192
+ state.hookUnregister = null;
193
+ const cfg = readConfig(api.config.extensions?.["migration-planner"]);
194
+ const hook = (input) => {
195
+ if (!cfg.enabled) return;
196
+ if (input.toolResult?.isError) return;
197
+ const inp = input.toolInput ?? {};
198
+ const path = inp["path"];
199
+ if (!path) return;
200
+ const basename = path.split(/[/\\]/).pop() ?? "";
201
+ if (!/^(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i.test(
202
+ basename
203
+ )) {
204
+ return;
205
+ }
206
+ return {
207
+ additionalContext: `Manifest file ${basename} changed. Consider running migration_plan if a dependency version was updated.`
208
+ };
209
+ };
210
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
211
+ api.tools.register({
212
+ name: "migration_plan",
213
+ description: "Read a package CHANGELOG and produce a migration checklist with breaking changes and recommended steps between two versions.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ packageName: {
218
+ type: "string",
219
+ description: "Name of the npm package or framework."
220
+ },
221
+ fromVersion: {
222
+ type: "string",
223
+ description: 'Current version, e.g. "1.2.3".'
224
+ },
225
+ toVersion: {
226
+ type: "string",
227
+ description: 'Target version, e.g. "2.0.0".'
228
+ },
229
+ scope: {
230
+ type: "string",
231
+ description: "Optional scope describing which parts of the project use the package."
232
+ }
233
+ },
234
+ required: ["packageName", "fromVersion", "toVersion"]
235
+ },
236
+ permission: "auto",
237
+ category: "Planning",
238
+ mutating: false,
239
+ async execute(input) {
240
+ if (!cfg.enabled) return { ok: false, error: "migration-planner is disabled" };
241
+ const packageName = String(input.packageName ?? "").trim();
242
+ const fromVersion = String(input.fromVersion ?? "").trim();
243
+ const toVersion = String(input.toVersion ?? "").trim();
244
+ if (!packageName || !fromVersion || !toVersion) {
245
+ return { ok: false, error: "packageName, fromVersion, and toVersion are required" };
246
+ }
247
+ const changelog = readChangelog(packageName, cfg);
248
+ let breakingChanges;
249
+ let recommendedSteps;
250
+ let source;
251
+ if (changelog) {
252
+ source = changelog.source;
253
+ const sections = extractVersionSections(changelog.content, fromVersion, toVersion);
254
+ const combined = sections.join("\n\n");
255
+ breakingChanges = extractBreakingChanges(combined);
256
+ recommendedSteps = extractRecommendedSteps(combined);
257
+ } else {
258
+ state.fallbackCount += 1;
259
+ source = null;
260
+ const guide = buildGenericGuide(packageName, fromVersion, toVersion, input.scope);
261
+ breakingChanges = guide.breakingChanges;
262
+ recommendedSteps = guide.recommendedSteps;
263
+ }
264
+ state.plansGenerated += 1;
265
+ state.lastPlan = {
266
+ packageName,
267
+ fromVersion,
268
+ toVersion,
269
+ scope: input.scope,
270
+ breakingChanges,
271
+ recommendedSteps,
272
+ changelogSource: source
273
+ };
274
+ return {
275
+ ok: true,
276
+ packageName,
277
+ fromVersion,
278
+ toVersion,
279
+ scope: input.scope,
280
+ changelogSource: source,
281
+ fallback: source === null,
282
+ breakingChanges,
283
+ recommendedSteps
284
+ };
285
+ }
286
+ });
287
+ api.tools.register({
288
+ name: "migration_status",
289
+ description: "Reports migration-planner state: counters, config, and last plan.",
290
+ inputSchema: { type: "object", properties: {} },
291
+ permission: "auto",
292
+ category: "Diagnostics",
293
+ mutating: false,
294
+ async execute() {
295
+ state.statusQueries += 1;
296
+ return {
297
+ ok: true,
298
+ enabled: cfg.enabled,
299
+ changelogPaths: cfg.changelogPaths,
300
+ maxChars: cfg.maxChars,
301
+ counters: {
302
+ plansGenerated: state.plansGenerated,
303
+ statusQueries: state.statusQueries,
304
+ fallbackCount: state.fallbackCount
305
+ },
306
+ lastPlan: state.lastPlan
307
+ };
308
+ }
309
+ });
310
+ api.log.info("migration-planner plugin loaded", {
311
+ version: "0.1.0",
312
+ changelogPaths: cfg.changelogPaths
313
+ });
314
+ },
315
+ teardown(api) {
316
+ if (state.hookUnregister) {
317
+ try {
318
+ state.hookUnregister();
319
+ } catch {
320
+ }
321
+ state.hookUnregister = null;
322
+ }
323
+ const final = {
324
+ plansGenerated: state.plansGenerated,
325
+ statusQueries: state.statusQueries,
326
+ fallbackCount: state.fallbackCount
327
+ };
328
+ state.plansGenerated = 0;
329
+ state.statusQueries = 0;
330
+ state.fallbackCount = 0;
331
+ state.lastPlan = null;
332
+ api.log.info("migration-planner: teardown complete", { final });
333
+ },
334
+ async health() {
335
+ return {
336
+ ok: true,
337
+ message: `migration-planner: ${state.plansGenerated} plan(s), ${state.fallbackCount} fallback(s)`,
338
+ counters: {
339
+ plansGenerated: state.plansGenerated,
340
+ statusQueries: state.statusQueries,
341
+ fallbackCount: state.fallbackCount
342
+ },
343
+ lastPlan: state.lastPlan
344
+ };
345
+ }
346
+ };
347
+ var migration_planner_default = plugin;
348
+
349
+ export { migration_planner_default as default };