@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.
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,228 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ // src/branch-guard/index.ts
4
+ var API_VERSION = "^0.1.10";
5
+ var state = {
6
+ invocationCount: 0,
7
+ blockCount: 0,
8
+ warnCount: 0,
9
+ hookUnregister: null,
10
+ lastBlock: null
11
+ };
12
+ var DEFAULTS = {
13
+ branches: ["main", "master"],
14
+ mode: "block",
15
+ blockCommit: true,
16
+ blockPush: true,
17
+ blockMerge: true
18
+ };
19
+ function readConfig(raw) {
20
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
21
+ const r = raw;
22
+ const branches = Array.isArray(r["branches"]) ? r["branches"].filter((b) => typeof b === "string") : DEFAULTS.branches;
23
+ return {
24
+ branches: branches.length > 0 ? branches : DEFAULTS.branches,
25
+ mode: r["mode"] === "warn" ? "warn" : "block",
26
+ blockCommit: r["blockCommit"] !== false,
27
+ blockPush: r["blockPush"] !== false,
28
+ blockMerge: r["blockMerge"] !== false
29
+ };
30
+ }
31
+ function getCurrentBranch(cwd) {
32
+ try {
33
+ const branch = execSync("git branch --show-current", {
34
+ encoding: "utf-8",
35
+ timeout: 3e3,
36
+ cwd,
37
+ stdio: ["pipe", "pipe", "pipe"]
38
+ }).trim();
39
+ return branch || null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function detectUncommittedChanges(cwd) {
45
+ try {
46
+ const output = execSync("git status --porcelain", {
47
+ encoding: "utf-8",
48
+ timeout: 3e3,
49
+ cwd,
50
+ stdio: ["pipe", "pipe", "pipe"]
51
+ }).trim();
52
+ return output.length > 0;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+ function detectGitCommand(command) {
58
+ const cmd = command.trim();
59
+ if (/\bgit\s+commit\b/.test(cmd)) {
60
+ return { type: "commit", snippet: cmd.slice(0, 120) };
61
+ }
62
+ if (/\bgit\s+push\b/.test(cmd)) {
63
+ return { type: "push", snippet: cmd.slice(0, 120) };
64
+ }
65
+ if (/\bgit\s+merge\s/.test(cmd)) {
66
+ return { type: "merge", snippet: cmd.slice(0, 120) };
67
+ }
68
+ return null;
69
+ }
70
+ function shouldBlock(op, cfg) {
71
+ if (op === "commit") return cfg.blockCommit;
72
+ if (op === "push") return cfg.blockPush;
73
+ if (op === "merge") return cfg.blockMerge;
74
+ return false;
75
+ }
76
+ var plugin = {
77
+ name: "branch-guard",
78
+ version: "0.1.0",
79
+ description: "Pre-tool hook that blocks commits, pushes, and merges to protected branches (default: main, master)",
80
+ apiVersion: API_VERSION,
81
+ capabilities: { tools: true, hooks: true },
82
+ defaultConfig: { ...DEFAULTS },
83
+ configSchema: {
84
+ type: "object",
85
+ properties: {
86
+ branches: {
87
+ type: "array",
88
+ items: { type: "string" },
89
+ default: ["main", "master"],
90
+ description: "Branch names that are protected."
91
+ },
92
+ mode: {
93
+ type: "string",
94
+ enum: ["block", "warn"],
95
+ default: "block",
96
+ description: '"block" refuses the call; "warn" injects context but lets it through.'
97
+ },
98
+ blockCommit: { type: "boolean", default: true, description: "Block commits on protected branches." },
99
+ blockPush: { type: "boolean", default: true, description: "Block pushes from protected branches." },
100
+ blockMerge: { type: "boolean", default: true, description: "Block merges into protected branches." }
101
+ }
102
+ },
103
+ setup(api) {
104
+ state.invocationCount = 0;
105
+ state.blockCount = 0;
106
+ state.warnCount = 0;
107
+ state.hookUnregister = null;
108
+ state.lastBlock = null;
109
+ const cfg = readConfig(api.config.extensions?.["branch-guard"]);
110
+ const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
111
+ const protectedSet = new Set(cfg.branches);
112
+ const hook = (input) => {
113
+ const toolName = input.toolName ?? "";
114
+ const inp = input.toolInput ?? {};
115
+ state.invocationCount += 1;
116
+ let gitOp = null;
117
+ if (toolName === "git_autocommit") {
118
+ gitOp = { type: "commit", snippet: "git_autocommit" };
119
+ } else if (toolName === "bash") {
120
+ const command = inp["command"];
121
+ if (typeof command !== "string") return;
122
+ gitOp = detectGitCommand(command);
123
+ }
124
+ if (!gitOp) return;
125
+ if (!shouldBlock(gitOp.type, cfg)) return;
126
+ const branch = getCurrentBranch(cwd);
127
+ if (!branch) return;
128
+ if (!protectedSet.has(branch)) return;
129
+ const when = (/* @__PURE__ */ new Date()).toISOString();
130
+ const opVerb = gitOp.type === "commit" ? "committing to" : gitOp.type === "push" ? "pushing from" : "merging into";
131
+ const hasUncommitted = detectUncommittedChanges(cwd);
132
+ const suggestionParts = [];
133
+ if (hasUncommitted) {
134
+ suggestionParts.push("git stash");
135
+ }
136
+ suggestionParts.push("git checkout -b feat/my-change");
137
+ if (hasUncommitted) {
138
+ suggestionParts.push("git stash pop");
139
+ }
140
+ suggestionParts.push(`git ${gitOp.type} ...`);
141
+ const suggestion = suggestionParts.join(" \u2192 ");
142
+ const reason = `branch-guard: refused to ${gitOp.type} on protected branch '${branch}'. You're on a protected branch. Use a feature branch instead.
143
+ ` + (hasUncommitted ? `You have uncommitted changes. Safe workflow:
144
+ ${suggestion}
145
+ ` : `Safe workflow:
146
+ ${suggestion}
147
+ `) + `Protected branches: ${cfg.branches.join(", ")}.`;
148
+ state.lastBlock = { tool: toolName, branch, command: gitOp.snippet, when };
149
+ if (cfg.mode === "block") {
150
+ state.blockCount += 1;
151
+ return {
152
+ decision: "block",
153
+ reason
154
+ };
155
+ }
156
+ state.warnCount += 1;
157
+ return {
158
+ decision: "allow",
159
+ additionalContext: `
160
+ \u26A0\uFE0F branch-guard: you are ${opVerb} protected branch '${branch}'. ` + (hasUncommitted ? `You have uncommitted changes \u2014 consider \`git stash\` before switching branches. ` : "") + `Use a feature branch instead. Protected: ${cfg.branches.join(", ")}.`
161
+ };
162
+ };
163
+ state.hookUnregister = api.registerHook("PreToolUse", "bash|git_autocommit", hook);
164
+ api.tools.register({
165
+ name: "branch_guard_status",
166
+ description: "Reports branch-guard state: protected branches, mode, and per-session invocation/block/warn counters.",
167
+ inputSchema: { type: "object", properties: {} },
168
+ permission: "auto",
169
+ category: "Git",
170
+ mutating: false,
171
+ async execute() {
172
+ return {
173
+ ok: true,
174
+ branches: cfg.branches,
175
+ mode: cfg.mode,
176
+ blockCommit: cfg.blockCommit,
177
+ blockPush: cfg.blockPush,
178
+ blockMerge: cfg.blockMerge,
179
+ counters: {
180
+ invocations: state.invocationCount,
181
+ blocks: state.blockCount,
182
+ warns: state.warnCount
183
+ },
184
+ lastBlock: state.lastBlock
185
+ };
186
+ }
187
+ });
188
+ api.log.info("branch-guard plugin loaded", {
189
+ version: "0.1.0",
190
+ branches: cfg.branches,
191
+ mode: cfg.mode
192
+ });
193
+ },
194
+ teardown(api) {
195
+ if (state.hookUnregister) {
196
+ try {
197
+ state.hookUnregister();
198
+ } catch {
199
+ }
200
+ state.hookUnregister = null;
201
+ }
202
+ const final = {
203
+ invocations: state.invocationCount,
204
+ blocks: state.blockCount,
205
+ warns: state.warnCount
206
+ };
207
+ state.invocationCount = 0;
208
+ state.blockCount = 0;
209
+ state.warnCount = 0;
210
+ state.lastBlock = null;
211
+ api.log.info("branch-guard: teardown complete", { final });
212
+ },
213
+ async health() {
214
+ return {
215
+ ok: true,
216
+ message: state.lastBlock === null ? `branch-guard: ${state.invocationCount} invocation(s), ${state.blockCount} block(s)` : `branch-guard: last block on '${state.lastBlock.branch}' (${state.lastBlock.command}) at ${state.lastBlock.when}`,
217
+ counters: {
218
+ invocations: state.invocationCount,
219
+ blocks: state.blockCount,
220
+ warns: state.warnCount
221
+ },
222
+ lastBlock: state.lastBlock
223
+ };
224
+ }
225
+ };
226
+ var branch_guard_default = plugin;
227
+
228
+ export { branch_guard_default as default };
@@ -0,0 +1,73 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * changelog-writer plugin — turns session activity into
5
+ * Keep-a-Changelog entries.
6
+ *
7
+ * While the session runs, the plugin passively collects work signals
8
+ * from the event bus: files touched by `write`/`edit`, and commit
9
+ * subjects observed from `git_autocommit` / `bash git commit`
10
+ * invocations. Commit subjects in conventional-commit format are
11
+ * mapped to Keep-a-Changelog sections:
12
+ *
13
+ * feat → Added · fix → Fixed · perf/refactor → Changed ·
14
+ * docs → Documentation · chore/build/ci → Maintenance ·
15
+ * revert/remove → Removed · sec/security → Security
16
+ *
17
+ * Tools:
18
+ * - `changelog_add` — record an entry manually ("Added dark mode")
19
+ * - `changelog_preview` — render the pending Unreleased block
20
+ * - `changelog_write` — merge the pending entries into
21
+ * `CHANGELOG.md` under `## [Unreleased]` (creates the file with a
22
+ * Keep-a-Changelog header when missing); pending entries are
23
+ * cleared after a successful write
24
+ *
25
+ * Both `changelog_preview` and `changelog_write` accept `polish: true`
26
+ * to rewrite the block into user-facing release-notes wording via the
27
+ * host LLM (`api.llm`) — provider/model follow the plugin's
28
+ * `config.extensions['changelog-writer'].llm` override, then the
29
+ * session default. Polish is best-effort: on any LLM failure (or when
30
+ * the response stops looking like the block) the raw entries are used.
31
+ *
32
+ * Config (`config.extensions['changelog-writer']`):
33
+ *
34
+ * ```jsonc
35
+ * {
36
+ * "enabled": true,
37
+ * "filePath": "CHANGELOG.md",
38
+ * "collectCommits": true,
39
+ * "maxEntries": 200
40
+ * }
41
+ * ```
42
+ *
43
+ * Toggle off with `{ "name": "changelog-writer", "enabled": false }`
44
+ * in `config.plugins`, or `"enabled": false` in the options above.
45
+ *
46
+ * @public
47
+ */
48
+
49
+ type Section = 'Added' | 'Changed' | 'Fixed' | 'Removed' | 'Security' | 'Documentation' | 'Maintenance';
50
+ interface ChangelogEntry {
51
+ section: Section;
52
+ text: string;
53
+ origin: 'commit' | 'manual';
54
+ when: string;
55
+ }
56
+ /** Parse "feat(scope): add dark mode" → { section: Added, text: "add dark mode (scope)" }. */
57
+ declare function commitToEntry(subject: string): {
58
+ section: Section;
59
+ text: string;
60
+ };
61
+ /** Extract the commit subject from a `git commit -m "..."` command, if any. */
62
+ declare function commitSubjectFromCommand(command: string): string | null;
63
+ declare function renderUnreleasedBlock(entries: ChangelogEntry[]): string;
64
+ /**
65
+ * Merge a rendered Unreleased block into existing changelog content:
66
+ * inserted directly under `## [Unreleased]` (existing unreleased
67
+ * content is preserved below the new lines). Creates the standard
68
+ * header when the file has no Unreleased heading.
69
+ */
70
+ declare function mergeIntoChangelog(existing: string | null, block: string): string;
71
+ declare const plugin: Plugin;
72
+
73
+ export { type ChangelogEntry, commitSubjectFromCommand, commitToEntry, plugin as default, mergeIntoChangelog, renderUnreleasedBlock };
@@ -0,0 +1,369 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+
3
+ // src/changelog-writer/index.ts
4
+ var state = {
5
+ entries: [],
6
+ filesTouched: /* @__PURE__ */ new Set(),
7
+ commitsSeen: 0,
8
+ writes: 0,
9
+ polishes: 0,
10
+ polishErrors: 0,
11
+ eventUnsubscribers: []
12
+ };
13
+ var DEFAULTS = {
14
+ enabled: true,
15
+ filePath: "CHANGELOG.md",
16
+ collectCommits: true,
17
+ maxEntries: 200
18
+ };
19
+ function readConfig(raw) {
20
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
21
+ const r = raw;
22
+ return {
23
+ enabled: r["enabled"] !== false,
24
+ filePath: typeof r["filePath"] === "string" && r["filePath"].length > 0 ? r["filePath"] : DEFAULTS.filePath,
25
+ collectCommits: r["collectCommits"] !== false,
26
+ maxEntries: typeof r["maxEntries"] === "number" && r["maxEntries"] >= 10 ? r["maxEntries"] : DEFAULTS.maxEntries
27
+ };
28
+ }
29
+ var TYPE_TO_SECTION = {
30
+ feat: "Added",
31
+ feature: "Added",
32
+ fix: "Fixed",
33
+ bugfix: "Fixed",
34
+ perf: "Changed",
35
+ refactor: "Changed",
36
+ style: "Changed",
37
+ docs: "Documentation",
38
+ doc: "Documentation",
39
+ chore: "Maintenance",
40
+ build: "Maintenance",
41
+ ci: "Maintenance",
42
+ test: "Maintenance",
43
+ revert: "Removed",
44
+ remove: "Removed",
45
+ sec: "Security",
46
+ security: "Security"
47
+ };
48
+ var SECTION_ORDER = [
49
+ "Added",
50
+ "Changed",
51
+ "Fixed",
52
+ "Removed",
53
+ "Security",
54
+ "Documentation",
55
+ "Maintenance"
56
+ ];
57
+ function commitToEntry(subject) {
58
+ const m = /^(\w+)(?:\(([^)]*)\))?!?:\s*(.+)$/.exec(subject.trim());
59
+ if (m?.[1] && m[3]) {
60
+ const section = TYPE_TO_SECTION[m[1].toLowerCase()] ?? "Changed";
61
+ const scope = m[2] ? ` (${m[2]})` : "";
62
+ return { section, text: `${m[3].trim()}${scope}` };
63
+ }
64
+ return { section: "Changed", text: subject.trim() };
65
+ }
66
+ function commitSubjectFromCommand(command) {
67
+ if (!/\bgit\s+commit\b/.test(command)) return null;
68
+ const m = /-m\s+(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(command);
69
+ const subject = m?.[1] ?? m?.[2] ?? m?.[3] ?? null;
70
+ if (!subject) return null;
71
+ return subject.split("\n")[0]?.trim() || null;
72
+ }
73
+ var CHANGELOG_HEADER = `# Changelog
74
+
75
+ All notable changes to this project will be documented in this file.
76
+
77
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
78
+
79
+ ## [Unreleased]
80
+ `;
81
+ function renderUnreleasedBlock(entries) {
82
+ const bySection = /* @__PURE__ */ new Map();
83
+ for (const e of entries) {
84
+ const list = bySection.get(e.section) ?? [];
85
+ if (!list.includes(e.text)) list.push(e.text);
86
+ bySection.set(e.section, list);
87
+ }
88
+ const parts = [];
89
+ for (const section of SECTION_ORDER) {
90
+ const items = bySection.get(section);
91
+ if (!items || items.length === 0) continue;
92
+ parts.push(`### ${section}`);
93
+ parts.push(...items.map((t) => `- ${t}`));
94
+ parts.push("");
95
+ }
96
+ return parts.join("\n").trimEnd();
97
+ }
98
+ function mergeIntoChangelog(existing, block) {
99
+ if (!existing?.trim()) {
100
+ return `${CHANGELOG_HEADER}
101
+ ${block}
102
+ `;
103
+ }
104
+ const unreleasedRe = /^##\s*\[?unreleased\]?.*$/im;
105
+ const m = unreleasedRe.exec(existing);
106
+ if (!m || m.index === void 0) {
107
+ const h1 = /^#\s.*$/m.exec(existing);
108
+ if (h1 && h1.index !== void 0) {
109
+ const insertAt2 = h1.index + h1[0].length;
110
+ return `${existing.slice(0, insertAt2)}
111
+
112
+ ## [Unreleased]
113
+
114
+ ${block}
115
+ ${existing.slice(insertAt2)}`;
116
+ }
117
+ return `## [Unreleased]
118
+
119
+ ${block}
120
+
121
+ ${existing}`;
122
+ }
123
+ const insertAt = m.index + m[0].length;
124
+ return `${existing.slice(0, insertAt)}
125
+
126
+ ${block}
127
+ ${existing.slice(insertAt)}`;
128
+ }
129
+ var plugin = {
130
+ name: "changelog-writer",
131
+ version: "0.1.0",
132
+ description: "Collects session work (commits, edits, manual notes) and writes Keep-a-Changelog entries under [Unreleased] on demand",
133
+ apiVersion: "^0.1.10",
134
+ capabilities: { tools: true },
135
+ defaultConfig: { ...DEFAULTS },
136
+ configSchema: {
137
+ type: "object",
138
+ properties: {
139
+ enabled: { type: "boolean", default: true, description: "Master switch." },
140
+ filePath: {
141
+ type: "string",
142
+ default: "CHANGELOG.md",
143
+ description: "Changelog file path (relative to the session cwd or absolute)."
144
+ },
145
+ collectCommits: {
146
+ type: "boolean",
147
+ default: true,
148
+ description: "Auto-collect entries from conventional commit subjects seen this session."
149
+ },
150
+ maxEntries: {
151
+ type: "number",
152
+ minimum: 10,
153
+ default: 200,
154
+ description: "Cap on pending entries held in memory."
155
+ }
156
+ }
157
+ },
158
+ setup(api) {
159
+ state.entries = [];
160
+ state.filesTouched = /* @__PURE__ */ new Set();
161
+ state.commitsSeen = 0;
162
+ state.writes = 0;
163
+ for (const off of state.eventUnsubscribers) {
164
+ try {
165
+ off();
166
+ } catch {
167
+ }
168
+ }
169
+ state.eventUnsubscribers = [];
170
+ const cfg = readConfig(api.config.extensions?.["changelog-writer"]);
171
+ const addEntry = (entry) => {
172
+ state.entries.push(entry);
173
+ if (state.entries.length > cfg.maxEntries) {
174
+ state.entries.splice(0, state.entries.length - cfg.maxEntries);
175
+ }
176
+ };
177
+ const maybePolish = async (block, polish) => {
178
+ if (!polish || !block || !api.llm) return block;
179
+ try {
180
+ const result = await api.llm.complete(
181
+ 'Rewrite this Keep-a-Changelog block into concise, user-facing release-notes wording. Keep the EXACT markdown structure: the same "### Section" headings, one "- " bullet per entry, no entries added or removed. Output ONLY the markdown block.\n\n' + block,
182
+ { system: "You are a precise technical release-notes editor.", maxTokens: 1500 }
183
+ );
184
+ const text = result.text.trim();
185
+ if (!text.startsWith("###")) return block;
186
+ state.polishes += 1;
187
+ api.metrics.counter("polishes");
188
+ return text;
189
+ } catch {
190
+ state.polishErrors += 1;
191
+ return block;
192
+ }
193
+ };
194
+ if (cfg.enabled && cfg.collectCommits) {
195
+ const off = api.onPattern("tool.*", (eventName, payload) => {
196
+ if (!/completed|result|executed/.test(eventName)) return;
197
+ const p = payload;
198
+ if (p?.isError) return;
199
+ const toolName = p?.tool ?? p?.name ?? "";
200
+ const input = p?.input ?? {};
201
+ if (toolName === "write" || toolName === "edit") {
202
+ const raw = input["path"] ?? input["file_path"] ?? input["filePath"];
203
+ if (typeof raw === "string" && raw) state.filesTouched.add(raw);
204
+ return;
205
+ }
206
+ if (toolName === "bash" || toolName === "exec") {
207
+ const command = typeof input["command"] === "string" ? input["command"] : "";
208
+ const subject = command ? commitSubjectFromCommand(command) : null;
209
+ if (subject) {
210
+ state.commitsSeen += 1;
211
+ api.metrics.counter("commits_seen");
212
+ const { section, text } = commitToEntry(subject);
213
+ addEntry({ section, text, origin: "commit", when: (/* @__PURE__ */ new Date()).toISOString() });
214
+ }
215
+ }
216
+ });
217
+ state.eventUnsubscribers.push(off);
218
+ }
219
+ api.tools.register({
220
+ name: "changelog_add",
221
+ description: "Record a changelog entry for the pending [Unreleased] block (written later via changelog_write).",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ text: { type: "string", description: "The entry text, user-facing wording." },
226
+ section: {
227
+ type: "string",
228
+ enum: SECTION_ORDER,
229
+ description: "Keep-a-Changelog section (default Changed)."
230
+ }
231
+ },
232
+ required: ["text"]
233
+ },
234
+ permission: "auto",
235
+ category: "Docs",
236
+ mutating: false,
237
+ async execute(input) {
238
+ if (!cfg.enabled) return { ok: false, error: "changelog-writer is disabled" };
239
+ const text = String(input.text ?? "").trim();
240
+ if (!text) return { ok: false, error: "entry text must not be empty" };
241
+ const section = SECTION_ORDER.includes(input.section) ? input.section : "Changed";
242
+ addEntry({ section, text, origin: "manual", when: (/* @__PURE__ */ new Date()).toISOString() });
243
+ return { ok: true, pendingEntries: state.entries.length };
244
+ }
245
+ });
246
+ api.tools.register({
247
+ name: "changelog_preview",
248
+ description: "Render the pending [Unreleased] changelog block collected this session without writing anything. Pass polish:true to rewrite entries into user-facing wording via the LLM.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ polish: {
253
+ type: "boolean",
254
+ description: "Rewrite entries into user-facing wording via the LLM (api.llm)."
255
+ }
256
+ }
257
+ },
258
+ permission: "auto",
259
+ category: "Docs",
260
+ mutating: false,
261
+ async execute(input) {
262
+ const raw = renderUnreleasedBlock(state.entries);
263
+ const markdown = raw ? await maybePolish(raw, input.polish === true) : "";
264
+ return {
265
+ ok: true,
266
+ enabled: cfg.enabled,
267
+ filePath: cfg.filePath,
268
+ pendingEntries: state.entries.length,
269
+ filesTouched: [...state.filesTouched].slice(0, 50),
270
+ commitsSeen: state.commitsSeen,
271
+ polished: input.polish === true && markdown !== raw,
272
+ llmAvailable: Boolean(api.llm),
273
+ markdown: markdown || "(no pending entries)"
274
+ };
275
+ }
276
+ });
277
+ api.tools.register({
278
+ name: "changelog_write",
279
+ description: "Merge the pending entries into the changelog file under ## [Unreleased] (creates the file when missing). Clears pending entries on success. Pass polish:true to rewrite entries into user-facing wording via the LLM first.",
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: {
283
+ polish: {
284
+ type: "boolean",
285
+ description: "Rewrite entries into user-facing wording via the LLM (api.llm) before writing."
286
+ }
287
+ }
288
+ },
289
+ permission: "confirm",
290
+ category: "Docs",
291
+ mutating: true,
292
+ async execute(input) {
293
+ if (!cfg.enabled) return { ok: false, error: "changelog-writer is disabled" };
294
+ if (state.entries.length === 0) {
295
+ return { ok: false, error: "no pending entries \u2014 add some with changelog_add first" };
296
+ }
297
+ const block = await maybePolish(
298
+ renderUnreleasedBlock(state.entries),
299
+ input.polish === true
300
+ );
301
+ let existing = null;
302
+ try {
303
+ existing = readFileSync(cfg.filePath, "utf-8");
304
+ } catch {
305
+ existing = null;
306
+ }
307
+ try {
308
+ writeFileSync(cfg.filePath, mergeIntoChangelog(existing, block));
309
+ } catch (err) {
310
+ return {
311
+ ok: false,
312
+ error: `failed to write ${cfg.filePath}: ${err instanceof Error ? err.message : String(err)}`
313
+ };
314
+ }
315
+ const written = state.entries.length;
316
+ state.entries = [];
317
+ state.writes += 1;
318
+ api.metrics.counter("writes");
319
+ return {
320
+ ok: true,
321
+ filePath: cfg.filePath,
322
+ entriesWritten: written,
323
+ created: existing === null
324
+ };
325
+ }
326
+ });
327
+ api.log.info("changelog-writer plugin loaded", {
328
+ version: "0.1.0",
329
+ enabled: cfg.enabled,
330
+ filePath: cfg.filePath,
331
+ collectCommits: cfg.collectCommits
332
+ });
333
+ },
334
+ teardown(api) {
335
+ for (const off of state.eventUnsubscribers) {
336
+ try {
337
+ off();
338
+ } catch {
339
+ }
340
+ }
341
+ state.eventUnsubscribers = [];
342
+ const final = {
343
+ pendingEntries: state.entries.length,
344
+ commitsSeen: state.commitsSeen,
345
+ writes: state.writes,
346
+ filesTouched: state.filesTouched.size
347
+ };
348
+ state.entries = [];
349
+ state.filesTouched = /* @__PURE__ */ new Set();
350
+ state.commitsSeen = 0;
351
+ state.writes = 0;
352
+ api.log.info("changelog-writer: teardown complete", { final });
353
+ },
354
+ async health() {
355
+ return {
356
+ ok: true,
357
+ message: `changelog-writer: ${state.entries.length} pending entr(ies), ${state.commitsSeen} commit(s) collected, ${state.writes} write(s)`,
358
+ counters: {
359
+ pendingEntries: state.entries.length,
360
+ commitsSeen: state.commitsSeen,
361
+ writes: state.writes,
362
+ filesTouched: state.filesTouched.size
363
+ }
364
+ };
365
+ }
366
+ };
367
+ var changelog_writer_default = plugin;
368
+
369
+ export { commitSubjectFromCommand, commitToEntry, changelog_writer_default as default, mergeIntoChangelog, renderUnreleasedBlock };