@wrongstack/plugins 0.1.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.
@@ -0,0 +1,15 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * json-path plugin — JMESPath query, validate, and transform JSON/YAML.
5
+ *
6
+ * Tools registered:
7
+ * - jmespath_query: Execute JMESPath query on JSON/YAML data
8
+ * - json_validate: Validate data against a JSON Schema
9
+ * - json_transform: Apply JMESPath transforms to data
10
+ * - json_merge: Deep merge two JSON objects
11
+ */
12
+
13
+ declare const plugin: Plugin;
14
+
15
+ export { plugin as default };
@@ -0,0 +1,311 @@
1
+ // src/json-path/index.ts
2
+ var API_VERSION = "^0.1.10";
3
+ function jmespathSearch(data, query) {
4
+ if (!query || query === "@") return data;
5
+ if (query === "$") return data;
6
+ const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
7
+ if (dotMatch) {
8
+ const key = dotMatch[1];
9
+ const rest = dotMatch[2];
10
+ const val = data?.[key];
11
+ if (rest === void 0) return val;
12
+ return jmespathSearch(val, rest);
13
+ }
14
+ const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
15
+ if (arrMatch) {
16
+ const idx = parseInt(arrMatch[1], 10);
17
+ const rest = arrMatch[2];
18
+ const arr = data;
19
+ const val = arr?.[idx];
20
+ if (rest === void 0) return val;
21
+ return jmespathSearch(val, rest);
22
+ }
23
+ if (query === "[*]") {
24
+ if (Array.isArray(data)) {
25
+ return data;
26
+ }
27
+ return data;
28
+ }
29
+ const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
30
+ if (multiMatch) {
31
+ const key = multiMatch[1];
32
+ const rest = multiMatch[2];
33
+ const arr = data?.[key];
34
+ if (!Array.isArray(arr)) return [];
35
+ if (rest === void 0) return arr;
36
+ return arr.map((item) => jmespathSearch(item, rest));
37
+ }
38
+ const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
39
+ if (filterMatch) {
40
+ const field = filterMatch[1];
41
+ const op = filterMatch[2];
42
+ const rawVal = filterMatch[3];
43
+ const rest = filterMatch[4];
44
+ const cmpVal = JSON.parse(rawVal.slice(1, -1));
45
+ const arr = data;
46
+ if (!Array.isArray(arr)) return [];
47
+ const filtered = arr.filter((item) => {
48
+ const itemVal = item[field];
49
+ switch (op) {
50
+ case "==":
51
+ return itemVal == cmpVal;
52
+ case "!=":
53
+ return itemVal != cmpVal;
54
+ case ">":
55
+ return Number(itemVal) > Number(cmpVal);
56
+ case "<":
57
+ return Number(itemVal) < Number(cmpVal);
58
+ case ">=":
59
+ return Number(itemVal) >= Number(cmpVal);
60
+ case "<=":
61
+ return Number(itemVal) <= Number(cmpVal);
62
+ default:
63
+ return true;
64
+ }
65
+ });
66
+ if (rest === void 0) return filtered;
67
+ return filtered.map((item) => jmespathSearch(item, rest));
68
+ }
69
+ const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
70
+ if (fnMatch) {
71
+ const fn = fnMatch[1];
72
+ switch (fn) {
73
+ case "length":
74
+ if (Array.isArray(data)) return data.length;
75
+ if (typeof data === "string") return data.length;
76
+ if (typeof data === "object" && data !== null) return Object.keys(data).length;
77
+ return 0;
78
+ case "keys":
79
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
80
+ return [];
81
+ case "values":
82
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
83
+ return [];
84
+ case "type":
85
+ if (data === null) return "null";
86
+ if (Array.isArray(data)) return "array";
87
+ return typeof data;
88
+ default:
89
+ return null;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function validateJsonSchema(data, schema) {
95
+ const errors = [];
96
+ function check(value, s, path) {
97
+ if (s["type"]) {
98
+ const expectedType = s["type"];
99
+ const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
100
+ if (expectedType === "integer") {
101
+ if (!Number.isInteger(value)) errors.push(`${path}: expected integer, got ${actualType}`);
102
+ } else if (expectedType !== actualType) {
103
+ errors.push(`${path}: expected ${expectedType}, got ${actualType}`);
104
+ }
105
+ }
106
+ if (typeof value === "string" && s["format"] === "uri" && value) {
107
+ try {
108
+ new URL(value);
109
+ } catch {
110
+ errors.push(`${path}: not a valid URI`);
111
+ }
112
+ }
113
+ if (typeof value === "string" && s["pattern"]) {
114
+ const re = new RegExp(s["pattern"]);
115
+ if (!re.test(value)) errors.push(`${path}: does not match pattern ${s["pattern"]}`);
116
+ }
117
+ if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
118
+ errors.push(`${path}: string too short (min ${s["minLength"]})`);
119
+ }
120
+ if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
121
+ errors.push(`${path}: string too long (max ${s["maxLength"]})`);
122
+ }
123
+ if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
124
+ errors.push(`${path}: below minimum ${s["minimum"]}`);
125
+ }
126
+ if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
127
+ errors.push(`${path}: above maximum ${s["maximum"]}`);
128
+ }
129
+ if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
130
+ value.forEach((item, i) => check(item, s["items"], `${path}[${i}]`));
131
+ }
132
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
133
+ const props = s["properties"];
134
+ for (const [k, propSchema] of Object.entries(props)) {
135
+ check(value[k], propSchema, `${path}.${k}`);
136
+ }
137
+ }
138
+ }
139
+ check(data, schema, "$");
140
+ return { valid: errors.length === 0, errors };
141
+ }
142
+ function deepMerge(base, patch, conflictResolution = "prefer-patch") {
143
+ if (typeof base !== "object" || base === null || typeof patch !== "object" || patch === null) {
144
+ return conflictResolution === "prefer-patch" ? patch : base;
145
+ }
146
+ if (Array.isArray(base) && Array.isArray(patch)) {
147
+ return conflictResolution === "prefer-patch" ? patch : base;
148
+ }
149
+ const result = {};
150
+ const baseObj = base;
151
+ const patchObj = patch;
152
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(baseObj), ...Object.keys(patchObj)]);
153
+ for (const key of allKeys) {
154
+ const baseVal = baseObj[key];
155
+ const patchVal = patchObj[key];
156
+ if (key in baseObj && key in patchObj) {
157
+ if (typeof baseVal === "object" && baseVal !== null && typeof patchVal === "object" && patchVal !== null && !Array.isArray(baseVal) && !Array.isArray(patchVal)) {
158
+ result[key] = deepMerge(baseVal, patchVal, conflictResolution);
159
+ } else {
160
+ result[key] = conflictResolution === "prefer-patch" ? patchVal : baseVal;
161
+ }
162
+ } else if (key in baseObj) {
163
+ result[key] = baseVal;
164
+ } else {
165
+ result[key] = patchVal;
166
+ }
167
+ }
168
+ return result;
169
+ }
170
+ var plugin = {
171
+ name: "json-path",
172
+ version: "0.1.0",
173
+ description: "JMESPath query, JSON Schema validation, transformation, and deep merge for JSON/YAML",
174
+ apiVersion: API_VERSION,
175
+ capabilities: { tools: true },
176
+ defaultConfig: {
177
+ strictValidation: false,
178
+ maxDepth: 50,
179
+ allowLargeFiles: false
180
+ },
181
+ configSchema: {
182
+ type: "object",
183
+ properties: {
184
+ strictValidation: { type: "boolean", default: false },
185
+ maxDepth: { type: "number", default: 50 },
186
+ allowLargeFiles: { type: "boolean", default: false }
187
+ }
188
+ },
189
+ setup(api) {
190
+ api.tools.register({
191
+ name: "jmespath_query",
192
+ description: "Execute a JMESPath query on JSON or YAML data. Supports dot notation, array indexing, wildcards, filters, and functions.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ data: { description: "JSON/YAML data to query (object or array)" },
197
+ query: { type: "string", description: "JMESPath query expression" }
198
+ },
199
+ required: ["data", "query"]
200
+ },
201
+ permission: "auto",
202
+ mutating: false,
203
+ async execute(input) {
204
+ const data = input["data"];
205
+ const query = input["query"];
206
+ try {
207
+ const result = jmespathSearch(data, query);
208
+ return {
209
+ ok: true,
210
+ query,
211
+ result,
212
+ resultType: result === null ? "null" : Array.isArray(result) ? "array" : typeof result
213
+ };
214
+ } catch (err) {
215
+ return { ok: false, error: String(err), query };
216
+ }
217
+ }
218
+ });
219
+ api.tools.register({
220
+ name: "json_validate",
221
+ description: "Validate JSON/YAML data against a JSON Schema. Reports all validation errors found.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ data: { description: "JSON data to validate" },
226
+ schema: { description: "JSON Schema to validate against" }
227
+ },
228
+ required: ["data", "schema"]
229
+ },
230
+ permission: "auto",
231
+ mutating: false,
232
+ async execute(input) {
233
+ const data = input["data"];
234
+ const schema = input["schema"];
235
+ try {
236
+ const { valid, errors } = validateJsonSchema(data, schema);
237
+ return { ok: true, valid, errors, errorCount: errors.length };
238
+ } catch (err) {
239
+ return { ok: false, error: String(err) };
240
+ }
241
+ }
242
+ });
243
+ api.tools.register({
244
+ name: "json_transform",
245
+ description: "Apply a series of JMESPath transforms to data, passing the output of each as input to the next.",
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ data: { description: "Initial JSON data" },
250
+ transforms: {
251
+ type: "array",
252
+ items: { type: "string" },
253
+ description: "Array of JMESPath query strings to apply in sequence"
254
+ }
255
+ },
256
+ required: ["data", "transforms"]
257
+ },
258
+ permission: "auto",
259
+ mutating: false,
260
+ async execute(input) {
261
+ const data = input["data"];
262
+ const transforms = input["transforms"];
263
+ try {
264
+ let current = data;
265
+ const steps = [];
266
+ for (const t of transforms) {
267
+ current = jmespathSearch(current, t);
268
+ steps.push({ transform: t, result: current });
269
+ }
270
+ return { ok: true, finalResult: current, steps };
271
+ } catch (err) {
272
+ return { ok: false, error: String(err) };
273
+ }
274
+ }
275
+ });
276
+ api.tools.register({
277
+ name: "json_merge",
278
+ description: "Deep merge two JSON objects. Use conflictResolution to decide which value wins on collision.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ base: { description: "Base JSON object" },
283
+ patch: { description: "Patch JSON object to merge in" },
284
+ conflictResolution: {
285
+ type: "string",
286
+ enum: ["prefer-base", "prefer-patch"],
287
+ default: "prefer-patch"
288
+ }
289
+ },
290
+ required: ["base", "patch"]
291
+ },
292
+ permission: "auto",
293
+ mutating: false,
294
+ async execute(input) {
295
+ const base = input["base"];
296
+ const patch = input["patch"];
297
+ const conflictResolution = input["conflictResolution"] ?? "prefer-patch";
298
+ try {
299
+ const result = deepMerge(base, patch, conflictResolution);
300
+ return { ok: true, result };
301
+ } catch (err) {
302
+ return { ok: false, error: String(err) };
303
+ }
304
+ }
305
+ });
306
+ api.log.info("json-path plugin loaded", { version: "0.1.0" });
307
+ }
308
+ };
309
+ var json_path_default = plugin;
310
+
311
+ export { json_path_default as default };
@@ -0,0 +1,14 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * semver-bump plugin — Conventional-commit-driven semver version bumps.
5
+ *
6
+ * Tools registered:
7
+ * - semver_bump: Determine and apply the next version bump
8
+ * - semver_current: Show the current version from package.json
9
+ * - semver_changelog: Generate a changelog between two versions
10
+ */
11
+
12
+ declare const plugin: Plugin;
13
+
14
+ export { plugin as default };
@@ -0,0 +1,347 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+
4
+ // src/semver-bump/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ function runGit(args, cwd) {
7
+ try {
8
+ return execSync(`git ${args.join(" ")}`, {
9
+ encoding: "utf-8",
10
+ cwd,
11
+ stdio: ["pipe", "pipe", "pipe"],
12
+ timeout: 3e4
13
+ }).trim();
14
+ } catch (err) {
15
+ const e = err;
16
+ if (e.status === 128) throw new Error("Not a git repository");
17
+ throw new Error(`git failed: ${e.message ?? e.stderr ?? String(err)}`);
18
+ }
19
+ }
20
+ function getPackageJson(cwd) {
21
+ const path = cwd ? `${cwd}/package.json` : "package.json";
22
+ if (!existsSync(path)) return null;
23
+ try {
24
+ return JSON.parse(readFileSync(path, "utf-8"));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function parseVersion(v) {
30
+ const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
31
+ if (!m) return [0, 0, 0];
32
+ return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
33
+ }
34
+ function bumpVersion(version, part) {
35
+ let [major, minor, patch] = parseVersion(version);
36
+ if (part === "major") {
37
+ major++;
38
+ minor = 0;
39
+ patch = 0;
40
+ } else if (part === "minor") {
41
+ minor++;
42
+ patch = 0;
43
+ } else if (part === "patch") {
44
+ patch++;
45
+ } else {
46
+ return version;
47
+ }
48
+ return `${major}.${minor}.${patch}`;
49
+ }
50
+ function getRecentCommits(sinceTag, cwd) {
51
+ const range = sinceTag ? `${sinceTag}..HEAD` : "-30";
52
+ const output = runGit(["log", range, "--format=%H %s"], cwd);
53
+ if (!output) return [];
54
+ return output.split("\n").filter(Boolean).map((line) => {
55
+ const spaceIdx = line.indexOf(" ");
56
+ const hash = line.slice(0, spaceIdx);
57
+ const message = line.slice(spaceIdx + 1);
58
+ const m = message.match(/^(\w+)(!)?(?:\(([^)]+)\))?:\s(.+)/);
59
+ const type = m?.[1] ?? "chore";
60
+ const breaking = !!m?.[2];
61
+ const scope = m?.[2];
62
+ const msg = m?.[3] ?? message;
63
+ return { hash, type, scope, message: msg, breaking };
64
+ });
65
+ }
66
+ function determineBump(commits) {
67
+ for (const c of commits) {
68
+ if (c.breaking || c.type === "feat!:" || c.type === "fix!") {
69
+ return "major";
70
+ }
71
+ }
72
+ for (const c of commits) {
73
+ if (c.type === "feat" || c.type === "refactor" && c.scope) {
74
+ return "minor";
75
+ }
76
+ }
77
+ return "patch";
78
+ }
79
+ function generateChangelog(commits) {
80
+ const sections = {
81
+ breaking: [],
82
+ feat: [],
83
+ fix: [],
84
+ perf: [],
85
+ docs: [],
86
+ refactor: [],
87
+ test: [],
88
+ chore: [],
89
+ other: []
90
+ };
91
+ for (const c of commits) {
92
+ if (c.breaking) {
93
+ sections.breaking.push(c);
94
+ } else if (c.type in sections) {
95
+ sections[c.type].push(c);
96
+ } else {
97
+ sections.other.push(c);
98
+ }
99
+ }
100
+ const lines = ["# Changelog\n"];
101
+ if (sections.breaking.length > 0) {
102
+ lines.push("## \u26A0\uFE0F BREAKING CHANGES\n");
103
+ for (const c of sections.breaking) {
104
+ lines.push(`- **${c.hash.slice(0, 7)}** ${c.message} (${c.type})`);
105
+ }
106
+ lines.push("");
107
+ }
108
+ const ordered = ["feat", "fix", "perf", "docs", "refactor", "test", "chore", "other"];
109
+ const labels = {
110
+ breaking: "Breaking",
111
+ feat: "Features",
112
+ fix: "Bug Fixes",
113
+ perf: "Performance",
114
+ docs: "Documentation",
115
+ refactor: "Refactoring",
116
+ test: "Tests",
117
+ chore: "Chores",
118
+ other: "Other Changes"
119
+ };
120
+ for (const key of ordered) {
121
+ const items = sections[key];
122
+ if (items.length === 0) continue;
123
+ lines.push(`## ${labels[key]}
124
+ `);
125
+ for (const c of items) {
126
+ const scope = c.scope ? `**${c.scope}**: ` : "";
127
+ lines.push(`- **${c.hash.slice(0, 7)}** ${scope}${c.message}`);
128
+ }
129
+ lines.push("");
130
+ }
131
+ return lines.join("\n").trim();
132
+ }
133
+ var plugin = {
134
+ name: "semver-bump",
135
+ version: "0.1.0",
136
+ description: "Conventional-commit-driven semver version bumps with changelog generation",
137
+ apiVersion: API_VERSION,
138
+ capabilities: { tools: true },
139
+ defaultConfig: {
140
+ tagPrefix: "v",
141
+ changelogFile: "CHANGELOG.md",
142
+ autoTag: true,
143
+ tagMessage: "Release {{version}}"
144
+ },
145
+ configSchema: {
146
+ type: "object",
147
+ properties: {
148
+ tagPrefix: { type: "string", default: "v" },
149
+ changelogFile: { type: "string", default: "CHANGELOG.md" },
150
+ autoTag: { type: "boolean", default: true },
151
+ tagMessage: { type: "string", default: "Release {{version}}" }
152
+ }
153
+ },
154
+ setup(api) {
155
+ const tagPrefix = api.config.extensions?.["semver-bump"]?.["tagPrefix"] ?? "v";
156
+ const autoTag = api.config.extensions?.["semver-bump"]?.["autoTag"] ?? true;
157
+ api.config.extensions?.["semver-bump"]?.["changelogFile"] ?? "CHANGELOG.md";
158
+ api.tools.register({
159
+ name: "semver_bump",
160
+ description: "Determine the next version bump from conventional commits since the last tag, or force a specific bump. Creates a git tag.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ cwd: { type: "string", description: "Working directory (defaults to project root)" },
165
+ dryRun: { type: "boolean", default: false },
166
+ part: { type: "string", enum: ["major", "minor", "patch", "auto"], default: "auto", description: "Version part to bump (auto = infer from commits)" }
167
+ }
168
+ },
169
+ permission: "confirm",
170
+ mutating: true,
171
+ async execute(input) {
172
+ const cwd = input["cwd"];
173
+ const dryRun = input["dryRun"] ?? false;
174
+ const part = input["part"] ?? "auto";
175
+ const pkg = getPackageJson(cwd);
176
+ if (!pkg) {
177
+ return { ok: false, error: "No package.json found" };
178
+ }
179
+ const currentVersion = pkg.version;
180
+ let bumpPart = part;
181
+ let commits = [];
182
+ if (part === "auto") {
183
+ let lastTag;
184
+ try {
185
+ const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"], cwd);
186
+ lastTag = tagsOutput || void 0;
187
+ } catch {
188
+ }
189
+ try {
190
+ commits = getRecentCommits(lastTag, cwd);
191
+ } catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ return { ok: false, error: `Git error: ${msg}`, bumpPart: "patch" };
194
+ }
195
+ bumpPart = determineBump(commits);
196
+ } else {
197
+ bumpPart = part;
198
+ }
199
+ const newVersion = bumpVersion(currentVersion, bumpPart);
200
+ if (dryRun) {
201
+ return {
202
+ ok: true,
203
+ dryRun: true,
204
+ currentVersion,
205
+ suggestedBump: bumpPart,
206
+ newVersion,
207
+ commitCount: part === "auto" ? commits.length : void 0,
208
+ message: `Would bump ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
209
+ };
210
+ }
211
+ const fs = await import('fs');
212
+ const pkgPath = cwd ? `${cwd}/package.json` : "package.json";
213
+ const pkgData = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
214
+ pkgData.version = newVersion;
215
+ fs.writeFileSync(pkgPath, JSON.stringify(pkgData, null, 2) + "\n", "utf-8");
216
+ try {
217
+ runGit(["add", "package.json"], cwd);
218
+ runGit(["commit", "-m", `chore: bump version to ${newVersion}`], cwd);
219
+ } catch {
220
+ }
221
+ if (autoTag) {
222
+ try {
223
+ runGit(["tag", "-a", `${tagPrefix}${newVersion}`, "-m", `Release ${newVersion}`], cwd);
224
+ } catch {
225
+ }
226
+ }
227
+ api.log.info("semver-bump: bumped", { from: currentVersion, to: newVersion, bump: bumpPart });
228
+ api.metrics.counter("version_bump", 1, { bump: bumpPart });
229
+ await api.session.append({
230
+ type: "semver-bump:bumped",
231
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
232
+ from: currentVersion,
233
+ to: newVersion,
234
+ bump: bumpPart
235
+ });
236
+ return {
237
+ ok: true,
238
+ currentVersion,
239
+ newVersion,
240
+ bump: bumpPart,
241
+ tag: `${tagPrefix}${newVersion}`,
242
+ message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
243
+ };
244
+ }
245
+ });
246
+ api.tools.register({
247
+ name: "semver_current",
248
+ description: "Return the current version from package.json and the latest git tag.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ cwd: { type: "string", description: "Working directory" }
253
+ }
254
+ },
255
+ permission: "auto",
256
+ mutating: false,
257
+ async execute(input) {
258
+ const cwd = input["cwd"];
259
+ const pkg = getPackageJson(cwd);
260
+ const currentVersion = pkg?.version ?? "unknown";
261
+ let latestTag = null;
262
+ let commitsSinceTag = 0;
263
+ try {
264
+ const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"], cwd);
265
+ latestTag = tagsOutput || null;
266
+ if (latestTag) {
267
+ const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
268
+ commitsSinceTag = parseInt(countOutput) || 0;
269
+ }
270
+ } catch {
271
+ latestTag = null;
272
+ }
273
+ return {
274
+ ok: true,
275
+ currentVersion,
276
+ latestTag: latestTag ?? null,
277
+ tagPrefix,
278
+ commitsSinceTag
279
+ };
280
+ }
281
+ });
282
+ api.tools.register({
283
+ name: "semver_changelog",
284
+ description: "Generate a changelog (in markdown) between two version tags or from a tag to HEAD.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ from: { type: "string", description: "Starting tag (exclusive)" },
289
+ to: { type: "string", description: 'Ending tag or "HEAD"' },
290
+ cwd: { type: "string", description: "Working directory" },
291
+ format: { type: "string", enum: ["markdown", "json"], default: "markdown" }
292
+ }
293
+ },
294
+ permission: "auto",
295
+ mutating: false,
296
+ async execute(input) {
297
+ const from = input["from"];
298
+ const to = input["to"] ?? "HEAD";
299
+ const cwd = input["cwd"];
300
+ const format = input["format"] ?? "markdown";
301
+ const range = from ? `${from}..${to}` : to;
302
+ let commits;
303
+ try {
304
+ const output = runGit(["log", range === to ? "-30" : range, "--format=%H %s"], cwd);
305
+ commits = output.split("\n").filter(Boolean).map((line) => {
306
+ const spaceIdx = line.indexOf(" ");
307
+ const hash = line.slice(0, spaceIdx);
308
+ const message = line.slice(spaceIdx + 1);
309
+ const m = message.match(/^(\w+)(!)?(?:\(([^)]+)\))?:\s(.+)/);
310
+ const type = m?.[1] ?? "chore";
311
+ return {
312
+ hash,
313
+ type,
314
+ scope: m?.[2],
315
+ message: m?.[3] ?? message,
316
+ breaking: !!m?.[2]
317
+ };
318
+ });
319
+ } catch (err) {
320
+ return { ok: false, error: `Failed to get git log: ${err}` };
321
+ }
322
+ if (format === "json") {
323
+ return {
324
+ ok: true,
325
+ from,
326
+ to,
327
+ commits,
328
+ commitCount: commits.length
329
+ };
330
+ }
331
+ const changelog = generateChangelog(commits);
332
+ return {
333
+ ok: true,
334
+ from: from ?? "(beginning)",
335
+ to,
336
+ changelog,
337
+ commitCount: commits.length,
338
+ breakingCount: commits.filter((c) => c.breaking).length
339
+ };
340
+ }
341
+ });
342
+ api.log.info("semver-bump plugin loaded", { version: "0.1.0", tagPrefix, autoTag });
343
+ }
344
+ };
345
+ var semver_bump_default = plugin;
346
+
347
+ export { semver_bump_default as default };
@@ -0,0 +1,13 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * shell-check plugin — Runs shellcheck analysis on bash/shell scripts.
5
+ *
6
+ * Tools registered:
7
+ * - shellcheck: Run shellcheck on specific files
8
+ * - shellcheck_scan: Scan directory for shell script issues
9
+ */
10
+
11
+ declare const plugin: Plugin;
12
+
13
+ export { plugin as default };