@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.
package/dist/index.js ADDED
@@ -0,0 +1,2503 @@
1
+ import { execSync } from 'child_process';
2
+ import { watch, existsSync, readdirSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ // src/auto-doc/index.ts
6
+ var AUTO_DOC_API_VERSION = "^0.1.10";
7
+ function parseSource(content) {
8
+ const entities = [];
9
+ const lines = content.split("\n");
10
+ const reFunction = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\((.*?)\)(?:\s*:\s*(.+?))?\s*\{/;
11
+ const reArrowFn = /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\((.*?)\)\s*(?::\s*(.+?))?\s*=>/;
12
+ const reClass = /^(?:export\s+)?class\s+(\w+)/;
13
+ const reType = /^(?:export\s+)?type\s+(\w+)\s*=\s*\{/;
14
+ const reInterface = /^(?:export\s+)?interface\s+(\w+)/;
15
+ for (let i = 0; i < lines.length; i++) {
16
+ const line = lines[i].trim();
17
+ let m = line.match(reFunction);
18
+ if (m) {
19
+ entities.push({
20
+ kind: "function",
21
+ name: m[1],
22
+ startLine: i + 1,
23
+ params: m[2] ? m[2].split(",").map((p) => p.trim().split(":")[0].trim()) : [],
24
+ returnType: m[3]?.trim()
25
+ });
26
+ continue;
27
+ }
28
+ m = line.match(reArrowFn);
29
+ if (m) {
30
+ entities.push({
31
+ kind: "function",
32
+ name: m[1],
33
+ startLine: i + 1,
34
+ params: m[2] ? m[2].split(",").map((p) => p.trim().split(":")[0].trim()) : [],
35
+ returnType: m[3]?.trim()
36
+ });
37
+ continue;
38
+ }
39
+ m = line.match(reClass);
40
+ if (m) {
41
+ entities.push({ kind: "class", name: m[1], startLine: i + 1 });
42
+ continue;
43
+ }
44
+ m = line.match(reType);
45
+ if (m) {
46
+ entities.push({ kind: "type", name: m[1], startLine: i + 1 });
47
+ continue;
48
+ }
49
+ m = line.match(reInterface);
50
+ if (m) {
51
+ entities.push({ kind: "interface", name: m[1], startLine: i + 1 });
52
+ continue;
53
+ }
54
+ }
55
+ return entities;
56
+ }
57
+ function generateJSDoc(entity, includeTypes) {
58
+ switch (entity.kind) {
59
+ case "function": {
60
+ const params = entity.params.map((p) => ` * @param ${p} - TODO: describe parameter`).join("\n");
61
+ const returns = entity.returnType ? `
62
+ * @returns ${includeTypes ? `{${entity.returnType}} ` : ""}TODO: describe return value` : "";
63
+ return `/**
64
+ * TODO: One-line description of ${entity.name}
65
+ ${params}${returns}
66
+ */`;
67
+ }
68
+ case "class":
69
+ return `/**
70
+ * TODO: Describe class ${entity.name}
71
+ */`;
72
+ case "type":
73
+ return `/**
74
+ * TODO: Describe type ${entity.name}
75
+ */`;
76
+ case "interface":
77
+ return `/**
78
+ * TODO: Describe interface ${entity.name}
79
+ */`;
80
+ }
81
+ }
82
+ function generateTSDoc(entity, includeTypes) {
83
+ switch (entity.kind) {
84
+ case "function": {
85
+ const params = entity.params.map((p) => ` * @param ${p} - TODO: describe parameter`).join("\n");
86
+ const returns = entity.returnType ? `
87
+ * @returns ${includeTypes ? `{${entity.returnType}} ` : ""}TODO: describe return value` : "";
88
+ return `/**
89
+ * TODO: One-line description of ${entity.name}
90
+ ${params}${returns}
91
+ */`;
92
+ }
93
+ case "class":
94
+ return `/**
95
+ * TODO: Describe class ${entity.name}
96
+ */`;
97
+ case "type":
98
+ return `/**
99
+ * TODO: Describe type ${entity.name}
100
+ */`;
101
+ case "interface":
102
+ return `/**
103
+ * TODO: Describe interface ${entity.name}
104
+ */`;
105
+ }
106
+ }
107
+ function needsDocComment(content, entity) {
108
+ const lines = content.split("\n");
109
+ const lineIdx = entity.startLine - 1;
110
+ if (lineIdx < 1) return true;
111
+ const prevLine = lines[lineIdx - 1] ?? "";
112
+ return !/^\s*\/\*\*\s*$/.test(prevLine.trim());
113
+ }
114
+ function injectDocComment(content, entity, doc) {
115
+ const lines = content.split("\n");
116
+ const idx = entity.startLine - 1;
117
+ const codeLine = lines[idx] ?? "";
118
+ const indent = codeLine.match(/^(\s*)/)?.[1] ?? "";
119
+ lines.splice(idx, 0, `${indent}${doc} ${codeLine.trim()}`);
120
+ return lines.join("\n");
121
+ }
122
+ async function runAutoDoc(input, api) {
123
+ if (!input.files || typeof input.files !== "object" || !Array.isArray(input.files)) {
124
+ return { ok: false, error: "input.files must be an array of file paths", filesProcessed: 0, changes: [] };
125
+ }
126
+ if (input.files.length === 0) {
127
+ return { ok: false, error: "input.files is empty \u2014 provide at least one file path", filesProcessed: 0, changes: [] };
128
+ }
129
+ const style = input.style ?? "tsdoc";
130
+ const includeTypes = api.config.extensions?.["auto-doc"]?.["includeTypes"] ?? false;
131
+ const results = [];
132
+ for (const file of input.files) {
133
+ try {
134
+ const { readFileSync: readFileSync3, writeFileSync } = await import('fs');
135
+ let content;
136
+ try {
137
+ content = readFileSync3(file, "utf-8");
138
+ } catch {
139
+ api.log.warn(`auto-doc: could not read file ${file}`);
140
+ continue;
141
+ }
142
+ const entities = parseSource(content);
143
+ let modified = content;
144
+ for (const entity of entities) {
145
+ if (!input.force && !needsDocComment(modified, entity)) continue;
146
+ const doc = style === "jsdoc" ? generateJSDoc(entity, includeTypes) : generateTSDoc(entity, includeTypes);
147
+ modified = injectDocComment(modified, entity, doc);
148
+ results.push({ file, entity: entity.name });
149
+ }
150
+ if (!input.dryRun && results.length > 0) {
151
+ writeFileSync(file, modified, "utf-8");
152
+ api.log.info(`auto-doc: updated ${file}`);
153
+ }
154
+ } catch (err) {
155
+ api.log.error(`auto-doc: error processing ${file}: ${err}`);
156
+ }
157
+ }
158
+ return { ok: true, filesProcessed: input.files.length, changes: results };
159
+ }
160
+ async function runAutoDocPreview(input, api) {
161
+ if (!input.files || typeof input.files !== "object" || !Array.isArray(input.files)) {
162
+ return { ok: false, error: "input.files must be an array of file paths", previews: [] };
163
+ }
164
+ if (input.files.length === 0) {
165
+ return { ok: false, error: "input.files is empty \u2014 provide at least one file path", previews: [] };
166
+ }
167
+ const style = input.style ?? "tsdoc";
168
+ const includeTypes = api.config.extensions?.["auto-doc"]?.["includeTypes"] ?? false;
169
+ const previews = [];
170
+ for (const file of input.files) {
171
+ try {
172
+ const { readFileSync: readFileSync3 } = await import('fs');
173
+ const content = readFileSync3(file, "utf-8");
174
+ const entities = parseSource(content);
175
+ const generated = entities.filter((e) => !needsDocComment(content, e)).map((e) => style === "jsdoc" ? generateJSDoc(e, includeTypes) : generateTSDoc(e, includeTypes));
176
+ previews.push({ file, entities: generated });
177
+ } catch {
178
+ api.log.warn(`auto-doc-preview: could not read file ${file}`);
179
+ }
180
+ }
181
+ return { ok: true, previews };
182
+ }
183
+ var plugin = {
184
+ name: "auto-doc",
185
+ version: "0.1.0",
186
+ description: "Auto-generates JSDoc/TSDoc comments for functions, classes, types, and interfaces",
187
+ apiVersion: AUTO_DOC_API_VERSION,
188
+ capabilities: { tools: true, pipelines: ["toolCall"] },
189
+ defaultConfig: { style: "tsdoc", includeTypes: false, dryRun: false },
190
+ configSchema: {
191
+ type: "object",
192
+ properties: {
193
+ style: { type: "string", enum: ["jsdoc", "tsdoc"], default: "tsdoc" },
194
+ includeTypes: { type: "boolean", default: false },
195
+ dryRun: { type: "boolean", default: false }
196
+ }
197
+ },
198
+ setup(api) {
199
+ api.tools.register({
200
+ name: "auto_doc",
201
+ description: "Auto-generate JSDoc/TSDoc comments for functions, classes, types, and interfaces in source files",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ files: { type: "array", items: { type: "string" }, description: "Source files to document" },
206
+ style: { type: "string", enum: ["jsdoc", "tsdoc"], default: "tsdoc", description: "Comment style" },
207
+ force: { type: "boolean", default: false, description: "Overwrite existing docstrings" },
208
+ dryRun: { type: "boolean", default: false, description: "Preview without writing" }
209
+ },
210
+ required: ["files"]
211
+ },
212
+ permission: "auto",
213
+ mutating: true,
214
+ async execute(input) {
215
+ return runAutoDoc(input, api);
216
+ }
217
+ });
218
+ api.tools.register({
219
+ name: "auto_doc_preview",
220
+ description: "Preview what JSDoc/TSDoc comments would be generated for files, without writing",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ files: { type: "array", items: { type: "string" }, description: "Source files to preview" },
225
+ style: { type: "string", enum: ["jsdoc", "tsdoc"], default: "tsdoc" }
226
+ },
227
+ required: ["files"]
228
+ },
229
+ permission: "auto",
230
+ mutating: false,
231
+ async execute(input) {
232
+ return runAutoDocPreview(input, api);
233
+ }
234
+ });
235
+ api.log.info("auto-doc plugin loaded", { version: "0.1.0", capabilities: ["auto_doc", "auto_doc_preview"] });
236
+ },
237
+ teardown(api) {
238
+ api.log.info("auto-doc plugin unloaded");
239
+ }
240
+ };
241
+ var auto_doc_default = plugin;
242
+ var API_VERSION = "^0.1.10";
243
+ function runGit(args, cwd) {
244
+ try {
245
+ return execSync(`git ${args.join(" ")}`, {
246
+ encoding: "utf-8",
247
+ cwd,
248
+ stdio: ["pipe", "pipe", "pipe"],
249
+ timeout: 3e4,
250
+ maxBuffer: 10 * 1024 * 1024
251
+ }).trim();
252
+ } catch (err) {
253
+ const e = err;
254
+ throw new Error(`git command failed: ${e.message ?? e.stderr ?? String(err)}`);
255
+ }
256
+ }
257
+ function getChangedFiles(cwd) {
258
+ const output = runGit(["status", "--porcelain"], cwd);
259
+ if (!output) return [];
260
+ return output.split("\n").filter((l) => l.trim()).map((l) => l.slice(3).trim());
261
+ }
262
+ function getStagedFiles(cwd) {
263
+ const output = runGit(["diff", "--cached", "--name-only"], cwd);
264
+ return output ? output.split("\n").filter(Boolean) : [];
265
+ }
266
+ function stageFiles(files, cwd) {
267
+ if (!files || !Array.isArray(files)) return;
268
+ const existing = files.filter((f) => {
269
+ try {
270
+ return existsSync(f);
271
+ } catch {
272
+ return false;
273
+ }
274
+ });
275
+ if (existing.length === 0) throw new Error("No files exist to stage");
276
+ runGit(["add", ...existing], cwd);
277
+ }
278
+ function commitWithMessage(message, cwd) {
279
+ return runGit(["commit", "-m", message], cwd);
280
+ }
281
+ function getCommitHistory(since, cwd) {
282
+ const range = `${since}..HEAD` ;
283
+ const output = runGit(["log", range, "--format=%H %s"], cwd);
284
+ if (!output) return [];
285
+ return output.split("\n").filter(Boolean).map((line) => {
286
+ const spaceIdx = line.indexOf(" ");
287
+ const hash = line.slice(0, spaceIdx);
288
+ const message = line.slice(spaceIdx + 1);
289
+ const typeMatch = message.match(/^(\w+)(!)?:\s/);
290
+ const type = typeMatch?.[1] ?? "chore";
291
+ return { hash, message, type };
292
+ });
293
+ }
294
+ function generateCommitMessage(type, scope, summary, body) {
295
+ const scopePart = scope ? `(${scope})` : "";
296
+ const footer = body ? `
297
+
298
+ ${body}` : "";
299
+ return `${type}${scopePart}: ${summary}${footer}`;
300
+ }
301
+ var plugin2 = {
302
+ name: "git-autocommit",
303
+ version: "0.1.0",
304
+ description: "AI-powered git staging and conventional commit message generation",
305
+ apiVersion: API_VERSION,
306
+ capabilities: { tools: true },
307
+ defaultConfig: {
308
+ conventionalCommits: true,
309
+ autoStage: false,
310
+ defaultType: "feat"
311
+ },
312
+ configSchema: {
313
+ type: "object",
314
+ properties: {
315
+ conventionalCommits: { type: "boolean", default: true },
316
+ autoStage: { type: "boolean", default: false },
317
+ defaultType: { type: "string", default: "feat" }
318
+ }
319
+ },
320
+ setup(api) {
321
+ const cwd = api.config.extensions?.["git-autocommit"];
322
+ const opts = {
323
+ conventionalCommits: cwd?.["conventionalCommits"] ?? true,
324
+ autoStage: cwd?.["autoStage"] ?? false,
325
+ defaultType: cwd?.["defaultType"] ?? "feat"
326
+ };
327
+ api.tools.register({
328
+ name: "git_autocommit",
329
+ description: "Stage files and create a git commit with an AI-generated conventional commit message. Pass files to stage specific ones, or leave empty to auto-detect all changed files.",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ files: {
334
+ type: "array",
335
+ items: { type: "string" },
336
+ description: "Specific files to stage. If empty, auto-detects all changed files."
337
+ },
338
+ type: {
339
+ type: "string",
340
+ enum: ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"],
341
+ description: "Conventional commit type"
342
+ },
343
+ scope: { type: "string", description: "Commit scope (e.g. auth, api, ui)" },
344
+ message: { type: "string", description: "Commit summary message" },
345
+ body: { type: "string", description: "Optional commit body/description" },
346
+ dryRun: { type: "boolean", default: false, description: "Show what would be committed without committing" }
347
+ }
348
+ },
349
+ permission: "confirm",
350
+ mutating: true,
351
+ async execute(input, _ctx) {
352
+ try {
353
+ const type = input["type"] ?? opts.defaultType;
354
+ const scope = input["scope"];
355
+ const summary = input["message"] ?? "";
356
+ const body = input["body"];
357
+ const dryRun = input["dryRun"] ?? false;
358
+ const validTypes = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"];
359
+ if (!type || !validTypes.includes(type)) {
360
+ if (dryRun) {
361
+ return {
362
+ ok: true,
363
+ dryRun: true,
364
+ message: `Would create: ${summary || "update code"}`
365
+ };
366
+ }
367
+ return { ok: false, error: "type is required and must be a valid conventional commit type" };
368
+ }
369
+ const msg = generateCommitMessage(type, scope, summary || "update code", body);
370
+ let files;
371
+ const rawFiles = input["files"];
372
+ if (rawFiles !== void 0) {
373
+ if (!Array.isArray(rawFiles)) {
374
+ return { ok: false, error: "files must be an array of file paths" };
375
+ }
376
+ files = rawFiles;
377
+ }
378
+ if (files && files.length > 0) {
379
+ try {
380
+ stageFiles(files);
381
+ } catch (err) {
382
+ return { ok: false, error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}` };
383
+ }
384
+ }
385
+ let staged = [];
386
+ try {
387
+ staged = getStagedFiles();
388
+ } catch {
389
+ staged = [];
390
+ }
391
+ if (staged.length === 0) {
392
+ try {
393
+ const changed = getChangedFiles();
394
+ if (changed.length > 0) {
395
+ try {
396
+ stageFiles(changed);
397
+ } catch {
398
+ }
399
+ try {
400
+ staged = getStagedFiles();
401
+ } catch {
402
+ staged = [];
403
+ }
404
+ }
405
+ } catch {
406
+ }
407
+ }
408
+ if (staged.length === 0) {
409
+ return { ok: false, error: "Nothing staged. Add files with git add or provide files input." };
410
+ }
411
+ let hash = "";
412
+ try {
413
+ hash = commitWithMessage(msg);
414
+ } catch (err) {
415
+ return { ok: false, error: `Failed to commit: ${err instanceof Error ? err.message : String(err)}` };
416
+ }
417
+ api.log.info("git-autocommit: created commit", { hash, type, scope });
418
+ try {
419
+ await api.session.append({
420
+ type: "git-autocommit:commit",
421
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
422
+ hash: String(hash),
423
+ commitType: type,
424
+ scope: String(scope ?? ""),
425
+ files: Array.isArray(staged) ? staged : []
426
+ });
427
+ } catch (_err) {
428
+ }
429
+ return {
430
+ ok: true,
431
+ hash,
432
+ message: msg,
433
+ stagedFiles: staged,
434
+ type,
435
+ scope: scope ?? null
436
+ };
437
+ } catch (err) {
438
+ return { ok: false, error: `Uncaught error in git_autocommit: ${err instanceof Error ? err.message : String(err)}` };
439
+ }
440
+ }
441
+ });
442
+ api.tools.register({
443
+ name: "git_stage",
444
+ description: "Stage specific files for commit. Shows what would be staged without staging if dryRun is true.",
445
+ inputSchema: {
446
+ type: "object",
447
+ properties: {
448
+ files: { type: "array", items: { type: "string" }, description: "Files to stage" },
449
+ dryRun: { type: "boolean", default: false }
450
+ },
451
+ required: ["files"]
452
+ },
453
+ permission: "confirm",
454
+ mutating: true,
455
+ async execute(input) {
456
+ try {
457
+ let files;
458
+ try {
459
+ files = input["files"] ?? [];
460
+ } catch {
461
+ files = [];
462
+ }
463
+ const dryRun = input["dryRun"] ?? false;
464
+ if (!Array.isArray(files) || files.length === 0) {
465
+ return { ok: false, error: "files must be a non-empty array of file paths" };
466
+ }
467
+ if (dryRun) {
468
+ return { ok: true, dryRun: true, files, message: `Would stage: ${files.join(", ")}` };
469
+ }
470
+ try {
471
+ stageFiles(files);
472
+ } catch (err) {
473
+ return { ok: false, error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}` };
474
+ }
475
+ let stillChanged = [];
476
+ try {
477
+ stillChanged = getChangedFiles();
478
+ } catch {
479
+ stillChanged = [];
480
+ }
481
+ return {
482
+ ok: true,
483
+ staged: files,
484
+ stillChanged,
485
+ message: `Staged ${files.length} file(s). ${stillChanged.length} file(s) still changed.`
486
+ };
487
+ } catch (err) {
488
+ return { ok: false, error: `git_stage error: ${err instanceof Error ? err.message : String(err)}` };
489
+ }
490
+ }
491
+ });
492
+ api.tools.register({
493
+ name: "git_status_summary",
494
+ description: "Returns a summary of the current git repository status: changed files, staged files, current branch, and recent commits.",
495
+ inputSchema: { type: "object", properties: {} },
496
+ permission: "auto",
497
+ mutating: false,
498
+ async execute() {
499
+ let branch = "";
500
+ let changed = [];
501
+ let staged = [];
502
+ let aheadBehind = "";
503
+ const recentCommits = [];
504
+ try {
505
+ branch = runGit(["branch", "--show-current"]);
506
+ } catch {
507
+ }
508
+ try {
509
+ changed = getChangedFiles();
510
+ } catch {
511
+ }
512
+ try {
513
+ staged = getStagedFiles();
514
+ } catch {
515
+ }
516
+ try {
517
+ aheadBehind = runGit(["status", "-sb"]).split("\n")[0] ?? "";
518
+ } catch {
519
+ }
520
+ try {
521
+ recentCommits.push(...getCommitHistory("-3", void 0).map((c) => ({ hash: c.hash.slice(0, 7), message: c.message })));
522
+ } catch {
523
+ }
524
+ return {
525
+ ok: true,
526
+ branch,
527
+ changedFiles: changed,
528
+ stagedFiles: staged,
529
+ aheadBehind,
530
+ recentCommits
531
+ };
532
+ }
533
+ });
534
+ api.log.info("git-autocommit plugin loaded", {
535
+ version: "0.1.0",
536
+ conventionalCommits: opts.conventionalCommits
537
+ });
538
+ }
539
+ };
540
+ var git_autocommit_default = plugin2;
541
+ var API_VERSION2 = "^0.1.10";
542
+ function runShellCheck(files, severity, cwd) {
543
+ if (!existsSync("shellcheck")) {
544
+ try {
545
+ execSync("shellcheck --version", { encoding: "utf-8", stdio: "ignore" });
546
+ } catch {
547
+ throw new Error("shellcheck is not installed. Install via: apt install shellcheck / brew install shellcheck");
548
+ }
549
+ }
550
+ const levelMap = {
551
+ error: "error",
552
+ warning: "warning",
553
+ info: "info",
554
+ style: "style"
555
+ };
556
+ const args = [
557
+ "-f",
558
+ "json",
559
+ "-S",
560
+ levelMap[severity] ?? "warning",
561
+ ...files
562
+ ];
563
+ let raw;
564
+ try {
565
+ raw = execSync(`shellcheck ${args.join(" ")}`, {
566
+ encoding: "utf-8",
567
+ cwd,
568
+ stdio: ["pipe", "pipe", "pipe"],
569
+ timeout: 6e4
570
+ });
571
+ } catch (err) {
572
+ const e = err;
573
+ if (e.stderr && !e.stderr.includes("shellcheck")) {
574
+ raw = e.stderr;
575
+ } else {
576
+ return [];
577
+ }
578
+ }
579
+ if (!raw.trim()) return [];
580
+ try {
581
+ const parsed = JSON.parse(raw);
582
+ return parsed.map((item) => ({
583
+ file: item.file,
584
+ line: item.line,
585
+ column: item.column,
586
+ level: item.level,
587
+ code: item.code,
588
+ message: item.message
589
+ }));
590
+ } catch {
591
+ return [];
592
+ }
593
+ }
594
+ function findShellFiles(dir, pattern) {
595
+ const results = [];
596
+ try {
597
+ const entries = readdirSync(dir, { withFileTypes: true });
598
+ for (const entry of entries) {
599
+ const full = join(dir, entry.name);
600
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
601
+ results.push(...findShellFiles(full, pattern));
602
+ } else if (entry.isFile() && (entry.name.endsWith(".sh") || entry.name === "Dockerfile")) {
603
+ if (!pattern || entry.name.includes(pattern)) {
604
+ results.push(full);
605
+ }
606
+ }
607
+ }
608
+ } catch {
609
+ }
610
+ return results;
611
+ }
612
+ var plugin3 = {
613
+ name: "shell-check",
614
+ version: "0.1.0",
615
+ description: "Runs shellcheck analysis on bash/shell scripts and surfaces issues with severity levels",
616
+ apiVersion: API_VERSION2,
617
+ capabilities: { tools: true, pipelines: ["toolCall"] },
618
+ defaultConfig: {
619
+ severity: "warning",
620
+ severityThreshold: "warning",
621
+ ignoredCodes: [],
622
+ autoScanOnBash: false
623
+ },
624
+ configSchema: {
625
+ type: "object",
626
+ properties: {
627
+ severity: { type: "string", enum: ["error", "warning", "info", "style"], default: "warning" },
628
+ severityThreshold: { type: "string", enum: ["error", "warning", "info", "style"], default: "warning" },
629
+ ignoredCodes: { type: "array", items: { type: "string" }, default: [] },
630
+ autoScanOnBash: { type: "boolean", default: false }
631
+ }
632
+ },
633
+ setup(api) {
634
+ api.tools.register({
635
+ name: "shellcheck",
636
+ description: "Run shellcheck analysis on shell script files. Returns issues with file, line, column, severity, code, and message.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ files: {
641
+ type: "array",
642
+ items: { type: "string" },
643
+ description: "Shell script files to check"
644
+ },
645
+ severity: {
646
+ type: "string",
647
+ enum: ["error", "warning", "info", "style"],
648
+ default: "warning",
649
+ description: "Minimum severity level to report"
650
+ },
651
+ fix: {
652
+ type: "boolean",
653
+ default: false,
654
+ description: "Apply safe automatic fixes where possible"
655
+ }
656
+ },
657
+ required: ["files"]
658
+ },
659
+ permission: "auto",
660
+ mutating: false,
661
+ async execute(input) {
662
+ const files = input["files"];
663
+ const severity = input["severity"] ?? "warning";
664
+ let issues;
665
+ try {
666
+ issues = runShellCheck(files, severity);
667
+ } catch (err) {
668
+ const msg = err instanceof Error ? err.message : String(err);
669
+ return { ok: false, error: msg, issues: [] };
670
+ }
671
+ const byFile = {};
672
+ for (const issue of issues) {
673
+ (byFile[issue.file] ??= []).push(issue);
674
+ }
675
+ const errorCount = issues.filter((i) => i.level === "error").length;
676
+ const warningCount = issues.filter((i) => i.level === "warning").length;
677
+ const infoCount = issues.filter((i) => i.level === "info").length;
678
+ const styleCount = issues.filter((i) => i.level === "style").length;
679
+ api.metrics.counter("issues_found", issues.length, { severity });
680
+ api.metrics.histogram("issues_per_file", issues.length / Math.max(files.length, 1));
681
+ return {
682
+ ok: true,
683
+ filesScanned: files.length,
684
+ issues,
685
+ summary: {
686
+ total: issues.length,
687
+ errors: errorCount,
688
+ warnings: warningCount,
689
+ info: infoCount,
690
+ style: styleCount
691
+ },
692
+ byFile,
693
+ recommendation: errorCount > 0 ? "Fix errors before deploying." : warningCount > 0 ? "Review and fix warnings for better script quality." : "No issues found."
694
+ };
695
+ }
696
+ });
697
+ api.tools.register({
698
+ name: "shellcheck_scan",
699
+ description: "Recursively scan a directory for shell scripts and run shellcheck on all found files.",
700
+ inputSchema: {
701
+ type: "object",
702
+ properties: {
703
+ directory: {
704
+ type: "string",
705
+ default: ".",
706
+ description: "Directory to scan"
707
+ },
708
+ pattern: {
709
+ type: "string",
710
+ default: "",
711
+ description: "Filename pattern to match (default: all .sh files)"
712
+ },
713
+ severity: {
714
+ type: "string",
715
+ enum: ["error", "warning", "info", "style"],
716
+ default: "warning"
717
+ }
718
+ }
719
+ },
720
+ permission: "auto",
721
+ mutating: false,
722
+ async execute(input) {
723
+ const dir = input["directory"] ?? ".";
724
+ const pattern = input["pattern"] ?? "";
725
+ const severity = input["severity"] ?? "warning";
726
+ const files = findShellFiles(dir, pattern);
727
+ if (files.length === 0) {
728
+ return { ok: true, filesScanned: 0, issues: [], summary: { total: 0 } };
729
+ }
730
+ let issues;
731
+ try {
732
+ issues = runShellCheck(files, severity);
733
+ } catch (err) {
734
+ const msg = err instanceof Error ? err.message : String(err);
735
+ return { ok: false, error: msg, issues: [], filesScanned: 0 };
736
+ }
737
+ const byFile = {};
738
+ for (const issue of issues) {
739
+ (byFile[issue.file] ??= []).push(issue);
740
+ }
741
+ return {
742
+ ok: true,
743
+ filesScanned: files.length,
744
+ filesWithIssues: Object.keys(byFile).length,
745
+ issues,
746
+ summary: {
747
+ total: issues.length,
748
+ errors: issues.filter((i) => i.level === "error").length,
749
+ warnings: issues.filter((i) => i.level === "warning").length
750
+ },
751
+ byFile
752
+ };
753
+ }
754
+ });
755
+ api.log.info("shell-check plugin loaded", { version: "0.1.0" });
756
+ }
757
+ };
758
+ var shell_check_default = plugin3;
759
+
760
+ // src/cost-tracker/index.ts
761
+ var API_VERSION3 = "^0.1.10";
762
+ var PRICING = {
763
+ "gpt-4o": { input: 5, output: 15 },
764
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
765
+ "gpt-4-turbo": { input: 10, output: 30 },
766
+ "claude-3-5-sonnet": { input: 3, output: 15 },
767
+ "claude-3-5-haiku": { input: 0.8, output: 4 },
768
+ "claude-3-opus": { input: 15, output: 75 },
769
+ "gemini-1.5-pro": { input: 3.5, output: 10.5 },
770
+ "gemini-1.5-flash": { input: 0.075, output: 0.3 },
771
+ "default": { input: 5, output: 15 }
772
+ };
773
+ var DEFAULT_PRICING = { input: 5, output: 15 };
774
+ function estimateCost(model, promptTokens, completionTokens) {
775
+ const pricing = PRICING[model.toLowerCase()] ?? DEFAULT_PRICING;
776
+ const inputCost = promptTokens / 1e6 * pricing.input;
777
+ const outputCost = completionTokens / 1e6 * pricing.output;
778
+ return inputCost + outputCost;
779
+ }
780
+ var plugin4 = {
781
+ name: "cost-tracker",
782
+ version: "0.1.0",
783
+ description: "Tracks LLM token usage and estimated cost per session with per-model breakdown",
784
+ apiVersion: API_VERSION3,
785
+ capabilities: { tools: true, pipelines: ["request", "response"] },
786
+ defaultConfig: {
787
+ trackPerModel: true,
788
+ trackPerUser: false,
789
+ budgetLimit: 0,
790
+ warningThreshold: 80
791
+ },
792
+ configSchema: {
793
+ type: "object",
794
+ properties: {
795
+ trackPerModel: { type: "boolean", default: true },
796
+ trackPerUser: { type: "boolean", default: false },
797
+ budgetLimit: { type: "number", default: 0, description: "Budget limit in USD (0 = no limit)" },
798
+ warningThreshold: { type: "number", default: 80, description: "Warning threshold as percentage of budget" }
799
+ }
800
+ },
801
+ setup(api) {
802
+ const sessionCost = {
803
+ requests: [],
804
+ totalPromptTokens: 0,
805
+ totalCompletionTokens: 0,
806
+ totalTokens: 0,
807
+ totalCostUsd: 0,
808
+ byModel: {}
809
+ };
810
+ api.onEvent("provider.response", (payload) => {
811
+ const usage = payload.usage;
812
+ const model = payload.ctx?.model ?? "unknown";
813
+ const promptTokens = usage.input ?? 0;
814
+ const completionTokens = usage.output ?? 0;
815
+ const totalTokens = promptTokens + completionTokens;
816
+ const costUsd = estimateCost(model, promptTokens, completionTokens);
817
+ const record = {
818
+ promptTokens,
819
+ completionTokens,
820
+ totalTokens,
821
+ model,
822
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
823
+ costUsd
824
+ };
825
+ sessionCost.requests.push(record);
826
+ sessionCost.totalPromptTokens += promptTokens;
827
+ sessionCost.totalCompletionTokens += completionTokens;
828
+ sessionCost.totalTokens += totalTokens;
829
+ sessionCost.totalCostUsd += costUsd;
830
+ const slot = sessionCost.byModel[model] ??= { tokens: 0, costUsd: 0, requests: 0 };
831
+ slot.tokens += totalTokens;
832
+ slot.costUsd += costUsd;
833
+ slot.requests += 1;
834
+ api.metrics.counter("tokens_total", totalTokens, { model });
835
+ api.metrics.histogram("cost_usd", costUsd, { model });
836
+ });
837
+ api.tools.register({
838
+ name: "cost_summary",
839
+ description: "Returns the current session's token usage breakdown by model, total cost estimate, and budget status.",
840
+ inputSchema: { type: "object", properties: {} },
841
+ permission: "auto",
842
+ mutating: false,
843
+ async execute() {
844
+ const budgetLimit = api.config.extensions?.["cost-tracker"]?.["budgetLimit"] ?? 0;
845
+ const warningThreshold = api.config.extensions?.["cost-tracker"]?.["warningThreshold"] ?? 80;
846
+ const usage = {
847
+ totalRequests: sessionCost.requests.length,
848
+ totalPromptTokens: sessionCost.totalPromptTokens,
849
+ totalCompletionTokens: sessionCost.totalCompletionTokens,
850
+ totalTokens: sessionCost.totalTokens,
851
+ totalCostUsd: Math.round(sessionCost.totalCostUsd * 1e6) / 1e6,
852
+ byModel: sessionCost.byModel,
853
+ recentRequests: sessionCost.requests.slice(-5).map((r) => ({
854
+ model: r.model,
855
+ tokens: r.totalTokens,
856
+ costUsd: r.costUsd,
857
+ ts: r.timestamp
858
+ }))
859
+ };
860
+ const budgetStatus = budgetLimit > 0 ? {
861
+ limit: budgetLimit,
862
+ spent: sessionCost.totalCostUsd,
863
+ percentUsed: Math.round(sessionCost.totalCostUsd / budgetLimit * 100),
864
+ warning: sessionCost.totalCostUsd / budgetLimit * 100 >= warningThreshold
865
+ } : null;
866
+ return {
867
+ ok: true,
868
+ usage,
869
+ budgetStatus
870
+ };
871
+ }
872
+ });
873
+ api.tools.register({
874
+ name: "cost_reset",
875
+ description: "Resets all token usage and cost counters for the current session.",
876
+ inputSchema: { type: "object", properties: {} },
877
+ permission: "auto",
878
+ mutating: false,
879
+ async execute() {
880
+ const prev = {
881
+ totalTokens: sessionCost.totalTokens,
882
+ totalCostUsd: sessionCost.totalCostUsd
883
+ };
884
+ sessionCost.requests = [];
885
+ sessionCost.totalPromptTokens = 0;
886
+ sessionCost.totalCompletionTokens = 0;
887
+ sessionCost.totalTokens = 0;
888
+ sessionCost.totalCostUsd = 0;
889
+ sessionCost.byModel = {};
890
+ return {
891
+ ok: true,
892
+ previousTotals: prev,
893
+ message: "Cost tracking counters have been reset."
894
+ };
895
+ }
896
+ });
897
+ api.tools.register({
898
+ name: "cost_export",
899
+ description: "Export the cost report as JSON or CSV.",
900
+ inputSchema: {
901
+ type: "object",
902
+ properties: {
903
+ format: { type: "string", enum: ["json", "csv"], default: "json" },
904
+ includeModel: { type: "boolean", default: true }
905
+ }
906
+ },
907
+ permission: "auto",
908
+ mutating: false,
909
+ async execute(input) {
910
+ const format = input["format"] ?? "json";
911
+ const includeModel = input["includeModel"] ?? true;
912
+ if (format === "csv") {
913
+ const header = includeModel ? "model,timestamp,prompt_tokens,completion_tokens,total_tokens,cost_usd" : "timestamp,prompt_tokens,completion_tokens,total_tokens,cost_usd";
914
+ const rows = sessionCost.requests.map(
915
+ (r) => includeModel ? `${r.model},${r.timestamp},${r.promptTokens},${r.completionTokens},${r.totalTokens},${r.costUsd ?? 0}` : `${r.timestamp},${r.promptTokens},${r.completionTokens},${r.totalTokens},${r.costUsd ?? 0}`
916
+ );
917
+ return {
918
+ ok: true,
919
+ format: "csv",
920
+ data: [header, ...rows].join("\n"),
921
+ summary: {
922
+ totalTokens: sessionCost.totalTokens,
923
+ totalCostUsd: sessionCost.totalCostUsd,
924
+ totalRequests: sessionCost.requests.length
925
+ }
926
+ };
927
+ }
928
+ return {
929
+ ok: true,
930
+ format: "json",
931
+ data: {
932
+ summary: {
933
+ totalTokens: sessionCost.totalTokens,
934
+ totalPromptTokens: sessionCost.totalPromptTokens,
935
+ totalCompletionTokens: sessionCost.totalCompletionTokens,
936
+ totalCostUsd: sessionCost.totalCostUsd,
937
+ totalRequests: sessionCost.requests.length,
938
+ byModel: sessionCost.byModel
939
+ },
940
+ requests: includeModel ? sessionCost.requests : sessionCost.requests.map(({ promptTokens, completionTokens, totalTokens, costUsd, timestamp }) => ({
941
+ promptTokens,
942
+ completionTokens,
943
+ totalTokens,
944
+ costUsd,
945
+ timestamp
946
+ }))
947
+ }
948
+ };
949
+ }
950
+ });
951
+ api.onEvent("session.close", async () => {
952
+ if (sessionCost.requests.length > 0) {
953
+ await api.session.append({
954
+ type: "cost-tracker:session_summary",
955
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
956
+ totalTokens: sessionCost.totalTokens,
957
+ totalCostUsd: sessionCost.totalCostUsd,
958
+ totalRequests: sessionCost.requests.length,
959
+ byModel: sessionCost.byModel
960
+ });
961
+ }
962
+ });
963
+ api.log.info("cost-tracker plugin loaded", { version: "0.1.0" });
964
+ }
965
+ };
966
+ var cost_tracker_default = plugin4;
967
+ var API_VERSION4 = "^0.1.10";
968
+ var watchIdCounter = 0;
969
+ function nextId() {
970
+ return `watch_${++watchIdCounter}_${Date.now().toString(36)}`;
971
+ }
972
+ var plugin5 = {
973
+ name: "file-watcher",
974
+ version: "0.1.0",
975
+ description: "Watches project files and emits events when changes occur (add, change, delete)",
976
+ apiVersion: API_VERSION4,
977
+ capabilities: { tools: true },
978
+ defaultConfig: {
979
+ debounceMs: 500,
980
+ watchOnStartup: [],
981
+ autoUnwatchOnExit: true
982
+ },
983
+ configSchema: {
984
+ type: "object",
985
+ properties: {
986
+ debounceMs: { type: "number", default: 500 },
987
+ watchOnStartup: { type: "array", items: { type: "string" }, default: [] },
988
+ autoUnwatchOnExit: { type: "boolean", default: true }
989
+ }
990
+ },
991
+ setup(api) {
992
+ const watches = /* @__PURE__ */ new Map();
993
+ let debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
994
+ const debounceTimers = /* @__PURE__ */ new Map();
995
+ function debounceEvent(key, fn, ms) {
996
+ const existing = debounceTimers.get(key);
997
+ if (existing) clearTimeout(existing);
998
+ debounceTimers.set(key, setTimeout(() => {
999
+ debounceTimers.delete(key);
1000
+ fn();
1001
+ }, ms));
1002
+ }
1003
+ function safeWatchDir(dirPath, recursive, handle) {
1004
+ try {
1005
+ const watcher = watch(dirPath, { recursive }, (eventType, filename) => {
1006
+ if (!filename) return;
1007
+ const fullPath = `${dirPath}/${filename}`;
1008
+ const key = `${handle.id}:${fullPath}:${eventType}`;
1009
+ debounceEvent(key, () => {
1010
+ api.emitCustom("file-watcher:changed", {
1011
+ watchId: handle.id,
1012
+ path: fullPath,
1013
+ event: eventType,
1014
+ filename,
1015
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1016
+ });
1017
+ api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
1018
+ api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
1019
+ }, debounceMs);
1020
+ });
1021
+ watcher.on("error", (err) => {
1022
+ api.log.warn(`file-watcher: error on ${dirPath}: ${err}`);
1023
+ });
1024
+ handle.watcher = watcher;
1025
+ } catch (err) {
1026
+ api.log.warn(`file-watcher: could not watch ${dirPath}: ${err}`);
1027
+ }
1028
+ }
1029
+ api.tools.register({
1030
+ name: "watch_start",
1031
+ description: "Start watching one or more file paths for changes (add, change, delete). Returns a watch ID for stopping the watch later.",
1032
+ inputSchema: {
1033
+ type: "object",
1034
+ properties: {
1035
+ paths: {
1036
+ type: "array",
1037
+ items: { type: "string" },
1038
+ description: "File or directory paths to watch"
1039
+ },
1040
+ events: {
1041
+ type: "array",
1042
+ items: { type: "string" },
1043
+ default: ["change", "add", "delete"],
1044
+ description: "Event types to watch for"
1045
+ },
1046
+ recursive: {
1047
+ type: "boolean",
1048
+ default: true,
1049
+ description: "Watch directories recursively"
1050
+ }
1051
+ },
1052
+ required: ["paths"]
1053
+ },
1054
+ permission: "confirm",
1055
+ mutating: false,
1056
+ async execute(input) {
1057
+ const rawPaths = input["paths"];
1058
+ if (!rawPaths || typeof rawPaths !== "object" || !Array.isArray(rawPaths)) {
1059
+ return { ok: false, error: "paths must be an array of file/directory paths", watchId: null };
1060
+ }
1061
+ const paths = rawPaths;
1062
+ if (paths.length === 0) {
1063
+ return { ok: false, error: "paths array is empty \u2014 provide at least one path", watchId: null };
1064
+ }
1065
+ const events = input["events"] ?? ["change", "add", "delete"];
1066
+ const recursive = input["recursive"] ?? true;
1067
+ const id = nextId();
1068
+ const handle = {
1069
+ id,
1070
+ paths,
1071
+ recursive,
1072
+ events,
1073
+ watcher: { close: () => {
1074
+ } },
1075
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1076
+ };
1077
+ for (const p of paths) {
1078
+ safeWatchDir(p, recursive, handle);
1079
+ }
1080
+ watches.set(id, handle);
1081
+ api.metrics.gauge("active_watches", watches.size);
1082
+ return {
1083
+ ok: true,
1084
+ watchId: id,
1085
+ paths,
1086
+ events,
1087
+ recursive,
1088
+ message: `Started watching ${paths.length} path(s). Use watch_stop to cancel.`
1089
+ };
1090
+ }
1091
+ });
1092
+ api.tools.register({
1093
+ name: "watch_stop",
1094
+ description: "Stop a file watch by its ID. Releases all resources.",
1095
+ inputSchema: {
1096
+ type: "object",
1097
+ properties: {
1098
+ watchId: { type: "string", description: "Watch ID returned by watch_start" }
1099
+ },
1100
+ required: ["watchId"]
1101
+ },
1102
+ permission: "auto",
1103
+ mutating: false,
1104
+ async execute(input) {
1105
+ const watchId = input["watchId"];
1106
+ const handle = watches.get(watchId);
1107
+ if (!handle) {
1108
+ return { ok: false, error: `No active watch with ID: ${watchId}` };
1109
+ }
1110
+ try {
1111
+ handle.watcher.close();
1112
+ } catch {
1113
+ }
1114
+ watches.delete(watchId);
1115
+ api.metrics.gauge("active_watches", watches.size);
1116
+ return {
1117
+ ok: true,
1118
+ watchId,
1119
+ message: `Stopped watch ${watchId}. ${watches.size} watch(es) remaining.`
1120
+ };
1121
+ }
1122
+ });
1123
+ api.tools.register({
1124
+ name: "watch_list",
1125
+ description: "List all currently active file watches with their IDs, paths, and creation times.",
1126
+ inputSchema: { type: "object", properties: {} },
1127
+ permission: "auto",
1128
+ mutating: false,
1129
+ async execute() {
1130
+ const list = Array.from(watches.values()).map((w) => ({
1131
+ id: w.id,
1132
+ paths: w.paths,
1133
+ events: w.events,
1134
+ recursive: w.recursive,
1135
+ createdAt: w.createdAt,
1136
+ age: `${Date.now() - new Date(w.createdAt).getTime()}ms`
1137
+ }));
1138
+ return {
1139
+ ok: true,
1140
+ count: list.length,
1141
+ watches: list
1142
+ };
1143
+ }
1144
+ });
1145
+ api.log.info("file-watcher plugin loaded", { version: "0.1.0" });
1146
+ },
1147
+ teardown(api) {
1148
+ {
1149
+ api.log.info("file-watcher: teardown complete");
1150
+ }
1151
+ }
1152
+ };
1153
+ var file_watcher_default = plugin5;
1154
+
1155
+ // src/web-search/index.ts
1156
+ var API_VERSION5 = "^0.1.10";
1157
+ async function duckduckgoSearch(query, numResults) {
1158
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
1159
+ const resp = await fetch(url, {
1160
+ headers: {
1161
+ "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)"
1162
+ }
1163
+ });
1164
+ if (!resp.ok) throw new Error(`DuckDuckGo search failed: ${resp.status}`);
1165
+ const html = await resp.text();
1166
+ const results = [];
1167
+ const resultRe = /<a class="result__a" href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
1168
+ let m;
1169
+ while ((m = resultRe.exec(html)) !== null && results.length < numResults) {
1170
+ const url2 = m[1];
1171
+ if (!url2) continue;
1172
+ const title = (m[2] ?? "").replace(/<[^>]+>/g, "").trim();
1173
+ const snippet = (m[3] ?? "").replace(/<[^>]+>/g, "").trim();
1174
+ results.push({
1175
+ url: url2,
1176
+ title,
1177
+ snippet,
1178
+ score: 1,
1179
+ source: "duckduckgo",
1180
+ cached: false
1181
+ });
1182
+ }
1183
+ return results;
1184
+ }
1185
+ async function fetchUrl(url, format) {
1186
+ const resp = await fetch(url, {
1187
+ headers: {
1188
+ "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
1189
+ "Accept": format === "text" ? "text/plain" : "text/html"
1190
+ }
1191
+ });
1192
+ if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
1193
+ if (format === "text") {
1194
+ return resp.text();
1195
+ }
1196
+ let html = await resp.text();
1197
+ html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
1198
+ html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
1199
+ html = html.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, (_, t) => `
1200
+ ## ${t.trim()}
1201
+ `);
1202
+ html = html.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, t) => `${t.trim()}
1203
+
1204
+ `);
1205
+ html = html.replace(/<br\s*\/?>/gi, "\n");
1206
+ html = html.replace(/<[^>]+>/g, "");
1207
+ html = html.replace(/&amp;/g, "&");
1208
+ html = html.replace(/&lt;/g, "<");
1209
+ html = html.replace(/&gt;/g, ">");
1210
+ html = html.replace(/&quot;/g, '"');
1211
+ html = html.replace(/&#39;/g, "'");
1212
+ html = html.replace(/\n{3,}/g, "\n\n");
1213
+ return html.trim().slice(0, 5e4);
1214
+ }
1215
+ function scoreResults(results, query) {
1216
+ const terms = query.toLowerCase().split(/\s+/);
1217
+ return results.map((r) => {
1218
+ const titleLower = r.title.toLowerCase();
1219
+ const snippetLower = r.snippet.toLowerCase();
1220
+ let score = r.score;
1221
+ for (const term of terms) {
1222
+ if (titleLower.includes(term)) score += 2;
1223
+ if (snippetLower.includes(term)) score += 1;
1224
+ }
1225
+ return { ...r, score };
1226
+ }).sort((a, b) => b.score - a.score);
1227
+ }
1228
+ var plugin6 = {
1229
+ name: "web-search",
1230
+ version: "0.1.0",
1231
+ description: "Cached web search with deduplication and relevance ranking",
1232
+ apiVersion: API_VERSION5,
1233
+ capabilities: { tools: true, pipelines: ["request"] },
1234
+ defaultConfig: {
1235
+ cacheTtlMs: 3e5,
1236
+ maxResults: 10,
1237
+ userAgent: "WrongStack/1.0"
1238
+ },
1239
+ configSchema: {
1240
+ type: "object",
1241
+ properties: {
1242
+ cacheTtlMs: { type: "number", default: 3e5 },
1243
+ maxResults: { type: "number", default: 10 },
1244
+ userAgent: { type: "string", default: "WrongStack/1.0" }
1245
+ }
1246
+ },
1247
+ setup(api) {
1248
+ const cache = /* @__PURE__ */ new Map();
1249
+ const cacheTtlMs = api.config.extensions?.["web-search"]?.["cacheTtlMs"] ?? 3e5;
1250
+ const maxResults = api.config.extensions?.["web-search"]?.["maxResults"] ?? 10;
1251
+ api.tools.register({
1252
+ name: "web_search",
1253
+ description: "Search the web using DuckDuckGo with automatic caching and deduplication. Results are cached for faster subsequent queries.",
1254
+ inputSchema: {
1255
+ type: "object",
1256
+ properties: {
1257
+ query: { type: "string", description: "Search query" },
1258
+ numResults: { type: "number", default: 10, description: "Maximum number of results" },
1259
+ source: { type: "string", enum: ["duckduckgo"], default: "duckduckgo", description: "Search engine" },
1260
+ skipCache: { type: "boolean", default: false, description: "Skip cache and force fresh search" }
1261
+ },
1262
+ required: ["query"]
1263
+ },
1264
+ permission: "auto",
1265
+ mutating: false,
1266
+ async execute(input) {
1267
+ const query = input["query"];
1268
+ if (!query || typeof query !== "string" || query.trim() === "") {
1269
+ return { ok: false, error: "query is required and must be a non-empty string", results: [] };
1270
+ }
1271
+ const numResults = input["numResults"] ?? maxResults;
1272
+ const skipCache = input["skipCache"] ?? false;
1273
+ if (!skipCache) {
1274
+ const cached = cache.get(query);
1275
+ if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
1276
+ const results = cached.results.map((r) => ({ ...r, cached: true }));
1277
+ api.metrics.counter("cache_hit", 1, { query: query.slice(0, 20) });
1278
+ return {
1279
+ ok: true,
1280
+ query,
1281
+ cached: true,
1282
+ results: results.slice(0, numResults),
1283
+ count: results.length
1284
+ };
1285
+ }
1286
+ }
1287
+ api.metrics.counter("cache_miss", 1, { query: query.slice(0, 20) });
1288
+ const seenUrls = /* @__PURE__ */ new Set();
1289
+ let rawResults;
1290
+ try {
1291
+ rawResults = await duckduckgoSearch(query, numResults * 2);
1292
+ } catch (err) {
1293
+ const msg = err instanceof Error ? err.message : String(err);
1294
+ return { ok: false, error: `Search failed: ${msg}`, results: [] };
1295
+ }
1296
+ const deduplicated = [];
1297
+ for (const r of rawResults) {
1298
+ const normalized = (r.url.split("?")[0] ?? r.url).split("#")[0] ?? r.url;
1299
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
1300
+ seenUrls.add(normalized);
1301
+ deduplicated.push(r);
1302
+ }
1303
+ }
1304
+ const ranked = scoreResults(deduplicated, query);
1305
+ cache.set(query, { results: ranked, timestamp: Date.now() });
1306
+ const now = Date.now();
1307
+ for (const [key, entry] of cache.entries()) {
1308
+ if (now - entry.timestamp > cacheTtlMs * 2) cache.delete(key);
1309
+ }
1310
+ return {
1311
+ ok: true,
1312
+ query,
1313
+ cached: false,
1314
+ results: ranked.slice(0, numResults),
1315
+ count: ranked.length
1316
+ };
1317
+ }
1318
+ });
1319
+ api.tools.register({
1320
+ name: "web_fetch",
1321
+ description: "Fetch a URL and return its content as markdown or plain text.",
1322
+ inputSchema: {
1323
+ type: "object",
1324
+ properties: {
1325
+ url: { type: "string", format: "uri", description: "URL to fetch" },
1326
+ format: { type: "string", enum: ["markdown", "text"], default: "markdown" }
1327
+ },
1328
+ required: ["url"]
1329
+ },
1330
+ permission: "confirm",
1331
+ mutating: false,
1332
+ async execute(input) {
1333
+ const rawUrl = input["url"];
1334
+ if (!rawUrl || typeof rawUrl !== "string") {
1335
+ return { ok: false, error: "url is required and must be a string" };
1336
+ }
1337
+ const url = rawUrl;
1338
+ const format = input["format"] ?? "markdown";
1339
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
1340
+ return { ok: false, error: "URL must start with http:// or https://" };
1341
+ }
1342
+ let content;
1343
+ try {
1344
+ content = await fetchUrl(url, format);
1345
+ } catch (err) {
1346
+ const msg = err instanceof Error ? err.message : String(err);
1347
+ return { ok: false, error: msg };
1348
+ }
1349
+ return {
1350
+ ok: true,
1351
+ url,
1352
+ format,
1353
+ contentLength: content.length,
1354
+ content: content.slice(0, 2e4),
1355
+ truncated: content.length > 2e4
1356
+ };
1357
+ }
1358
+ });
1359
+ api.log.info("web-search plugin loaded", { version: "0.1.0", cacheTtlMs });
1360
+ }
1361
+ };
1362
+ var web_search_default = plugin6;
1363
+
1364
+ // src/json-path/index.ts
1365
+ var API_VERSION6 = "^0.1.10";
1366
+ function jmespathSearch(data, query) {
1367
+ if (!query || query === "@") return data;
1368
+ if (query === "$") return data;
1369
+ const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
1370
+ if (dotMatch) {
1371
+ const key = dotMatch[1];
1372
+ const rest = dotMatch[2];
1373
+ const val = data?.[key];
1374
+ if (rest === void 0) return val;
1375
+ return jmespathSearch(val, rest);
1376
+ }
1377
+ const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
1378
+ if (arrMatch) {
1379
+ const idx = parseInt(arrMatch[1], 10);
1380
+ const rest = arrMatch[2];
1381
+ const arr = data;
1382
+ const val = arr?.[idx];
1383
+ if (rest === void 0) return val;
1384
+ return jmespathSearch(val, rest);
1385
+ }
1386
+ if (query === "[*]") {
1387
+ if (Array.isArray(data)) {
1388
+ return data;
1389
+ }
1390
+ return data;
1391
+ }
1392
+ const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
1393
+ if (multiMatch) {
1394
+ const key = multiMatch[1];
1395
+ const rest = multiMatch[2];
1396
+ const arr = data?.[key];
1397
+ if (!Array.isArray(arr)) return [];
1398
+ if (rest === void 0) return arr;
1399
+ return arr.map((item) => jmespathSearch(item, rest));
1400
+ }
1401
+ const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
1402
+ if (filterMatch) {
1403
+ const field = filterMatch[1];
1404
+ const op = filterMatch[2];
1405
+ const rawVal = filterMatch[3];
1406
+ const rest = filterMatch[4];
1407
+ const cmpVal = JSON.parse(rawVal.slice(1, -1));
1408
+ const arr = data;
1409
+ if (!Array.isArray(arr)) return [];
1410
+ const filtered = arr.filter((item) => {
1411
+ const itemVal = item[field];
1412
+ switch (op) {
1413
+ case "==":
1414
+ return itemVal == cmpVal;
1415
+ case "!=":
1416
+ return itemVal != cmpVal;
1417
+ case ">":
1418
+ return Number(itemVal) > Number(cmpVal);
1419
+ case "<":
1420
+ return Number(itemVal) < Number(cmpVal);
1421
+ case ">=":
1422
+ return Number(itemVal) >= Number(cmpVal);
1423
+ case "<=":
1424
+ return Number(itemVal) <= Number(cmpVal);
1425
+ default:
1426
+ return true;
1427
+ }
1428
+ });
1429
+ if (rest === void 0) return filtered;
1430
+ return filtered.map((item) => jmespathSearch(item, rest));
1431
+ }
1432
+ const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
1433
+ if (fnMatch) {
1434
+ const fn = fnMatch[1];
1435
+ switch (fn) {
1436
+ case "length":
1437
+ if (Array.isArray(data)) return data.length;
1438
+ if (typeof data === "string") return data.length;
1439
+ if (typeof data === "object" && data !== null) return Object.keys(data).length;
1440
+ return 0;
1441
+ case "keys":
1442
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
1443
+ return [];
1444
+ case "values":
1445
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
1446
+ return [];
1447
+ case "type":
1448
+ if (data === null) return "null";
1449
+ if (Array.isArray(data)) return "array";
1450
+ return typeof data;
1451
+ default:
1452
+ return null;
1453
+ }
1454
+ }
1455
+ return null;
1456
+ }
1457
+ function validateJsonSchema(data, schema) {
1458
+ const errors = [];
1459
+ function check(value, s, path) {
1460
+ if (s["type"]) {
1461
+ const expectedType = s["type"];
1462
+ const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
1463
+ if (expectedType === "integer") {
1464
+ if (!Number.isInteger(value)) errors.push(`${path}: expected integer, got ${actualType}`);
1465
+ } else if (expectedType !== actualType) {
1466
+ errors.push(`${path}: expected ${expectedType}, got ${actualType}`);
1467
+ }
1468
+ }
1469
+ if (typeof value === "string" && s["format"] === "uri" && value) {
1470
+ try {
1471
+ new URL(value);
1472
+ } catch {
1473
+ errors.push(`${path}: not a valid URI`);
1474
+ }
1475
+ }
1476
+ if (typeof value === "string" && s["pattern"]) {
1477
+ const re = new RegExp(s["pattern"]);
1478
+ if (!re.test(value)) errors.push(`${path}: does not match pattern ${s["pattern"]}`);
1479
+ }
1480
+ if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
1481
+ errors.push(`${path}: string too short (min ${s["minLength"]})`);
1482
+ }
1483
+ if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
1484
+ errors.push(`${path}: string too long (max ${s["maxLength"]})`);
1485
+ }
1486
+ if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
1487
+ errors.push(`${path}: below minimum ${s["minimum"]}`);
1488
+ }
1489
+ if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
1490
+ errors.push(`${path}: above maximum ${s["maximum"]}`);
1491
+ }
1492
+ if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
1493
+ value.forEach((item, i) => check(item, s["items"], `${path}[${i}]`));
1494
+ }
1495
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
1496
+ const props = s["properties"];
1497
+ for (const [k, propSchema] of Object.entries(props)) {
1498
+ check(value[k], propSchema, `${path}.${k}`);
1499
+ }
1500
+ }
1501
+ }
1502
+ check(data, schema, "$");
1503
+ return { valid: errors.length === 0, errors };
1504
+ }
1505
+ function deepMerge(base, patch, conflictResolution = "prefer-patch") {
1506
+ if (typeof base !== "object" || base === null || typeof patch !== "object" || patch === null) {
1507
+ return conflictResolution === "prefer-patch" ? patch : base;
1508
+ }
1509
+ if (Array.isArray(base) && Array.isArray(patch)) {
1510
+ return conflictResolution === "prefer-patch" ? patch : base;
1511
+ }
1512
+ const result = {};
1513
+ const baseObj = base;
1514
+ const patchObj = patch;
1515
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(baseObj), ...Object.keys(patchObj)]);
1516
+ for (const key of allKeys) {
1517
+ const baseVal = baseObj[key];
1518
+ const patchVal = patchObj[key];
1519
+ if (key in baseObj && key in patchObj) {
1520
+ if (typeof baseVal === "object" && baseVal !== null && typeof patchVal === "object" && patchVal !== null && !Array.isArray(baseVal) && !Array.isArray(patchVal)) {
1521
+ result[key] = deepMerge(baseVal, patchVal, conflictResolution);
1522
+ } else {
1523
+ result[key] = conflictResolution === "prefer-patch" ? patchVal : baseVal;
1524
+ }
1525
+ } else if (key in baseObj) {
1526
+ result[key] = baseVal;
1527
+ } else {
1528
+ result[key] = patchVal;
1529
+ }
1530
+ }
1531
+ return result;
1532
+ }
1533
+ var plugin7 = {
1534
+ name: "json-path",
1535
+ version: "0.1.0",
1536
+ description: "JMESPath query, JSON Schema validation, transformation, and deep merge for JSON/YAML",
1537
+ apiVersion: API_VERSION6,
1538
+ capabilities: { tools: true },
1539
+ defaultConfig: {
1540
+ strictValidation: false,
1541
+ maxDepth: 50,
1542
+ allowLargeFiles: false
1543
+ },
1544
+ configSchema: {
1545
+ type: "object",
1546
+ properties: {
1547
+ strictValidation: { type: "boolean", default: false },
1548
+ maxDepth: { type: "number", default: 50 },
1549
+ allowLargeFiles: { type: "boolean", default: false }
1550
+ }
1551
+ },
1552
+ setup(api) {
1553
+ api.tools.register({
1554
+ name: "jmespath_query",
1555
+ description: "Execute a JMESPath query on JSON or YAML data. Supports dot notation, array indexing, wildcards, filters, and functions.",
1556
+ inputSchema: {
1557
+ type: "object",
1558
+ properties: {
1559
+ data: { description: "JSON/YAML data to query (object or array)" },
1560
+ query: { type: "string", description: "JMESPath query expression" }
1561
+ },
1562
+ required: ["data", "query"]
1563
+ },
1564
+ permission: "auto",
1565
+ mutating: false,
1566
+ async execute(input) {
1567
+ const data = input["data"];
1568
+ const query = input["query"];
1569
+ try {
1570
+ const result = jmespathSearch(data, query);
1571
+ return {
1572
+ ok: true,
1573
+ query,
1574
+ result,
1575
+ resultType: result === null ? "null" : Array.isArray(result) ? "array" : typeof result
1576
+ };
1577
+ } catch (err) {
1578
+ return { ok: false, error: String(err), query };
1579
+ }
1580
+ }
1581
+ });
1582
+ api.tools.register({
1583
+ name: "json_validate",
1584
+ description: "Validate JSON/YAML data against a JSON Schema. Reports all validation errors found.",
1585
+ inputSchema: {
1586
+ type: "object",
1587
+ properties: {
1588
+ data: { description: "JSON data to validate" },
1589
+ schema: { description: "JSON Schema to validate against" }
1590
+ },
1591
+ required: ["data", "schema"]
1592
+ },
1593
+ permission: "auto",
1594
+ mutating: false,
1595
+ async execute(input) {
1596
+ const data = input["data"];
1597
+ const schema = input["schema"];
1598
+ try {
1599
+ const { valid, errors } = validateJsonSchema(data, schema);
1600
+ return { ok: true, valid, errors, errorCount: errors.length };
1601
+ } catch (err) {
1602
+ return { ok: false, error: String(err) };
1603
+ }
1604
+ }
1605
+ });
1606
+ api.tools.register({
1607
+ name: "json_transform",
1608
+ description: "Apply a series of JMESPath transforms to data, passing the output of each as input to the next.",
1609
+ inputSchema: {
1610
+ type: "object",
1611
+ properties: {
1612
+ data: { description: "Initial JSON data" },
1613
+ transforms: {
1614
+ type: "array",
1615
+ items: { type: "string" },
1616
+ description: "Array of JMESPath query strings to apply in sequence"
1617
+ }
1618
+ },
1619
+ required: ["data", "transforms"]
1620
+ },
1621
+ permission: "auto",
1622
+ mutating: false,
1623
+ async execute(input) {
1624
+ const data = input["data"];
1625
+ const transforms = input["transforms"];
1626
+ try {
1627
+ let current = data;
1628
+ const steps = [];
1629
+ for (const t of transforms) {
1630
+ current = jmespathSearch(current, t);
1631
+ steps.push({ transform: t, result: current });
1632
+ }
1633
+ return { ok: true, finalResult: current, steps };
1634
+ } catch (err) {
1635
+ return { ok: false, error: String(err) };
1636
+ }
1637
+ }
1638
+ });
1639
+ api.tools.register({
1640
+ name: "json_merge",
1641
+ description: "Deep merge two JSON objects. Use conflictResolution to decide which value wins on collision.",
1642
+ inputSchema: {
1643
+ type: "object",
1644
+ properties: {
1645
+ base: { description: "Base JSON object" },
1646
+ patch: { description: "Patch JSON object to merge in" },
1647
+ conflictResolution: {
1648
+ type: "string",
1649
+ enum: ["prefer-base", "prefer-patch"],
1650
+ default: "prefer-patch"
1651
+ }
1652
+ },
1653
+ required: ["base", "patch"]
1654
+ },
1655
+ permission: "auto",
1656
+ mutating: false,
1657
+ async execute(input) {
1658
+ const base = input["base"];
1659
+ const patch = input["patch"];
1660
+ const conflictResolution = input["conflictResolution"] ?? "prefer-patch";
1661
+ try {
1662
+ const result = deepMerge(base, patch, conflictResolution);
1663
+ return { ok: true, result };
1664
+ } catch (err) {
1665
+ return { ok: false, error: String(err) };
1666
+ }
1667
+ }
1668
+ });
1669
+ api.log.info("json-path plugin loaded", { version: "0.1.0" });
1670
+ }
1671
+ };
1672
+ var json_path_default = plugin7;
1673
+
1674
+ // src/cron/index.ts
1675
+ var API_VERSION7 = "^0.1.10";
1676
+ function formatNextRun(intervalMs) {
1677
+ const ms = isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
1678
+ return new Date(Date.now() + ms).toISOString();
1679
+ }
1680
+ var plugin8 = {
1681
+ name: "cron",
1682
+ version: "0.1.0",
1683
+ description: "Schedules recurring tasks using beforeIteration/afterIteration extension hooks",
1684
+ apiVersion: API_VERSION7,
1685
+ capabilities: { tools: true },
1686
+ defaultConfig: {
1687
+ maxConcurrentJobs: 5,
1688
+ timezone: "UTC",
1689
+ persistSchedules: false
1690
+ },
1691
+ configSchema: {
1692
+ type: "object",
1693
+ properties: {
1694
+ maxConcurrentJobs: { type: "number", default: 5 },
1695
+ timezone: { type: "string", default: "UTC" },
1696
+ persistSchedules: { type: "boolean", default: false }
1697
+ }
1698
+ },
1699
+ setup(api) {
1700
+ const state = {
1701
+ jobs: /* @__PURE__ */ new Map(),
1702
+ timers: /* @__PURE__ */ new Map(),
1703
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1704
+ };
1705
+ const maxConcurrent = api.config.extensions?.["cron"]?.["maxConcurrentJobs"] ?? 5;
1706
+ function scheduleNextRun(name) {
1707
+ const job = state.jobs.get(name);
1708
+ if (!job || !job.enabled) return;
1709
+ const existing = state.timers.get(name);
1710
+ if (existing) clearTimeout(existing);
1711
+ const delay = Math.max(0, new Date(job.nextRun).getTime() - Date.now());
1712
+ const timer = setTimeout(() => {
1713
+ job.runCount++;
1714
+ job.lastRun = (/* @__PURE__ */ new Date()).toISOString();
1715
+ job.nextRun = formatNextRun(job.intervalMs);
1716
+ api.emitCustom("cron:job_fired", {
1717
+ name,
1718
+ action: job.action,
1719
+ runCount: job.runCount,
1720
+ ts: (/* @__PURE__ */ new Date()).toISOString()
1721
+ });
1722
+ api.metrics.counter("cron_job_fired", 1, { job: name });
1723
+ api.metrics.histogram("cron_job_interval_ms", job.intervalMs, { job: name });
1724
+ scheduleNextRun(name);
1725
+ }, delay);
1726
+ state.timers.set(name, timer);
1727
+ }
1728
+ function cancelJob(name) {
1729
+ const timer = state.timers.get(name);
1730
+ if (timer) {
1731
+ clearTimeout(timer);
1732
+ state.timers.delete(name);
1733
+ }
1734
+ state.jobs.delete(name);
1735
+ }
1736
+ api.extensions.register({
1737
+ name: "cron-iteration-hooks",
1738
+ owner: "cron",
1739
+ beforeIteration: async (_ctx, _idx) => {
1740
+ const now = Date.now();
1741
+ let activeJobs = 0;
1742
+ const promises = [];
1743
+ for (const [name, job] of state.jobs) {
1744
+ if (!job.enabled) continue;
1745
+ if (activeJobs >= maxConcurrent) break;
1746
+ if (new Date(job.nextRun).getTime() <= now) {
1747
+ activeJobs++;
1748
+ promises.push(
1749
+ (async () => {
1750
+ await api.session.append({
1751
+ type: "cron:scheduled_trigger",
1752
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1753
+ jobName: name,
1754
+ action: job.action,
1755
+ runCount: job.runCount + 1
1756
+ });
1757
+ api.emitCustom("cron:job_due", {
1758
+ name,
1759
+ action: job.action,
1760
+ dueAt: (/* @__PURE__ */ new Date()).toISOString()
1761
+ });
1762
+ })()
1763
+ );
1764
+ }
1765
+ }
1766
+ await Promise.all(promises);
1767
+ },
1768
+ afterIteration: async (_ctx, _idx) => {
1769
+ for (const job of state.jobs.values()) {
1770
+ if (!job.enabled) continue;
1771
+ if (new Date(job.nextRun).getTime() <= Date.now()) {
1772
+ job.nextRun = formatNextRun(job.intervalMs);
1773
+ }
1774
+ }
1775
+ }
1776
+ });
1777
+ api.tools.register({
1778
+ name: "cron_schedule",
1779
+ description: "Schedule a recurring action to fire at a fixed interval (in milliseconds). The action is emitted as a custom event for downstream handlers.",
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {
1783
+ name: { type: "string", description: "Unique name for this cron job" },
1784
+ intervalMs: { type: "number", description: "Interval between runs in milliseconds (minimum 1000)" },
1785
+ action: { type: "string", description: "Action identifier or description of what to run" },
1786
+ enabled: { type: "boolean", default: true }
1787
+ },
1788
+ required: ["name", "intervalMs", "action"]
1789
+ },
1790
+ permission: "confirm",
1791
+ mutating: false,
1792
+ async execute(input) {
1793
+ const name = input["name"];
1794
+ const intervalMs = Math.max(1e3, Number(input["intervalMs"]));
1795
+ const action = input["action"];
1796
+ const enabled = input["enabled"] ?? true;
1797
+ if (!name || typeof name !== "string" || name.trim() === "") {
1798
+ return { ok: false, error: "name is required and must be a non-empty string" };
1799
+ }
1800
+ if (isNaN(intervalMs)) {
1801
+ return { ok: false, error: "intervalMs must be a number >= 1000" };
1802
+ }
1803
+ if (state.jobs.has(name)) {
1804
+ return { ok: false, error: `Cron job '${name}' already exists. Use cron_cancel first.` };
1805
+ }
1806
+ if (state.jobs.size >= maxConcurrent) {
1807
+ return { ok: false, error: `Maximum concurrent jobs (${maxConcurrent}) reached.` };
1808
+ }
1809
+ const job = {
1810
+ name,
1811
+ intervalMs,
1812
+ action,
1813
+ enabled,
1814
+ lastRun: null,
1815
+ nextRun: formatNextRun(intervalMs),
1816
+ runCount: 0
1817
+ };
1818
+ state.jobs.set(name, job);
1819
+ scheduleNextRun(name);
1820
+ api.metrics.gauge("cron_active_jobs", state.jobs.size);
1821
+ return {
1822
+ ok: true,
1823
+ name,
1824
+ intervalMs,
1825
+ nextRun: job.nextRun,
1826
+ message: `Scheduled '${name}' every ${intervalMs}ms.`
1827
+ };
1828
+ }
1829
+ });
1830
+ api.tools.register({
1831
+ name: "cron_list",
1832
+ description: "List all registered cron jobs with their intervals, next run times, and execution counts.",
1833
+ inputSchema: { type: "object", properties: {} },
1834
+ permission: "auto",
1835
+ mutating: false,
1836
+ async execute() {
1837
+ const jobs = Array.from(state.jobs.values()).map((j) => ({
1838
+ name: j.name,
1839
+ intervalMs: j.intervalMs,
1840
+ action: j.action,
1841
+ enabled: j.enabled,
1842
+ lastRun: j.lastRun,
1843
+ nextRun: j.nextRun,
1844
+ runCount: j.runCount,
1845
+ overdue: new Date(j.nextRun).getTime() < Date.now()
1846
+ }));
1847
+ return {
1848
+ ok: true,
1849
+ count: jobs.length,
1850
+ maxConcurrent,
1851
+ jobs
1852
+ };
1853
+ }
1854
+ });
1855
+ api.tools.register({
1856
+ name: "cron_cancel",
1857
+ description: "Cancel and remove a cron job by name.",
1858
+ inputSchema: {
1859
+ type: "object",
1860
+ properties: {
1861
+ name: { type: "string", description: "Name of the cron job to cancel" }
1862
+ },
1863
+ required: ["name"]
1864
+ },
1865
+ permission: "auto",
1866
+ mutating: false,
1867
+ async execute(input) {
1868
+ const name = input["name"];
1869
+ if (!state.jobs.has(name)) {
1870
+ return { ok: false, error: `No cron job named '${name}'` };
1871
+ }
1872
+ cancelJob(name);
1873
+ api.metrics.gauge("cron_active_jobs", state.jobs.size);
1874
+ return {
1875
+ ok: true,
1876
+ name,
1877
+ message: `Cancelled cron job '${name}'.`
1878
+ };
1879
+ }
1880
+ });
1881
+ api.log.info("cron plugin loaded", { version: "0.1.0", maxConcurrent });
1882
+ },
1883
+ teardown(api) {
1884
+ const { jobs, timers } = api._state ?? { jobs: /* @__PURE__ */ new Map(), timers: /* @__PURE__ */ new Map() };
1885
+ for (const name of jobs.keys()) {
1886
+ const timer = timers.get(name);
1887
+ if (timer) clearTimeout(timer);
1888
+ }
1889
+ jobs.clear();
1890
+ timers.clear();
1891
+ api.log.info("cron plugin unloaded");
1892
+ }
1893
+ };
1894
+ var cron_default = plugin8;
1895
+
1896
+ // src/template-engine/index.ts
1897
+ var API_VERSION8 = "^0.1.10";
1898
+ function expandTemplate(template, variables) {
1899
+ let result = template;
1900
+ result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1901
+ const value = variables[key];
1902
+ if (value !== void 0) return value;
1903
+ return match;
1904
+ });
1905
+ return result;
1906
+ }
1907
+ function expandConditionals(template, variables) {
1908
+ return template.replace(
1909
+ /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
1910
+ (_, key, content) => {
1911
+ const val = variables[key];
1912
+ return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
1913
+ }
1914
+ );
1915
+ }
1916
+ function expandLoops(template, variables) {
1917
+ return template.replace(
1918
+ /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
1919
+ (_, key, content) => {
1920
+ const val = variables[key];
1921
+ if (!val) return "";
1922
+ if (typeof val === "string" && val.includes(",")) {
1923
+ const items = val.split(",").map((s) => s.trim());
1924
+ return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
1925
+ }
1926
+ return expandTemplate(content, variables);
1927
+ }
1928
+ );
1929
+ }
1930
+ function renderTemplate(template, variables) {
1931
+ let result = template;
1932
+ result = expandConditionals(result, variables);
1933
+ result = expandLoops(result, variables);
1934
+ result = expandTemplate(result, variables);
1935
+ {
1936
+ result = result.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1937
+ }
1938
+ return result;
1939
+ }
1940
+ function renderTemplateRaw(template, variables) {
1941
+ let result = template;
1942
+ result = expandConditionals(result, variables);
1943
+ result = expandLoops(result, variables);
1944
+ result = expandTemplate(result, variables);
1945
+ return result;
1946
+ }
1947
+ var plugin9 = {
1948
+ name: "template-engine",
1949
+ version: "0.1.0",
1950
+ description: "Expands file templates with variable substitution, conditionals, and loops",
1951
+ apiVersion: API_VERSION8,
1952
+ capabilities: { tools: true },
1953
+ defaultConfig: {
1954
+ autoEscapeHtml: true,
1955
+ templateDir: "./templates",
1956
+ strictVariables: false
1957
+ },
1958
+ configSchema: {
1959
+ type: "object",
1960
+ properties: {
1961
+ autoEscapeHtml: { type: "boolean", default: true },
1962
+ templateDir: { type: "string", default: "./templates" },
1963
+ strictVariables: { type: "boolean", default: false }
1964
+ }
1965
+ },
1966
+ setup(api) {
1967
+ const templates = /* @__PURE__ */ new Map();
1968
+ api.tools.register({
1969
+ name: "template_expand",
1970
+ description: "Expand a template string with variable substitution. Supports {{variable}}, {{#if var}}...{{/if}} conditionals, and {{#each items}}...{{/each}} loops.",
1971
+ inputSchema: {
1972
+ type: "object",
1973
+ properties: {
1974
+ template: { type: "string", description: "Template string with {{variable}} placeholders" },
1975
+ variables: {
1976
+ type: "object",
1977
+ description: "Variables to substitute into the template",
1978
+ additionalProperties: { type: "string" }
1979
+ },
1980
+ outputPath: { type: "string", description: "Optional path to write the expanded result" },
1981
+ raw: { type: "boolean", default: false, description: "Disable HTML auto-escaping" }
1982
+ },
1983
+ required: ["template", "variables"]
1984
+ },
1985
+ permission: "auto",
1986
+ mutating: true,
1987
+ async execute(input) {
1988
+ const template = input["template"];
1989
+ const variables = input["variables"];
1990
+ const outputPath = input["outputPath"];
1991
+ const raw = input["raw"] ?? false;
1992
+ if (!template || typeof template !== "string") {
1993
+ return { ok: false, error: "template is required and must be a string" };
1994
+ }
1995
+ if (!variables || typeof variables !== "object") {
1996
+ return { ok: false, error: "variables is required and must be an object" };
1997
+ }
1998
+ let result;
1999
+ try {
2000
+ result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables);
2001
+ } catch (err) {
2002
+ return { ok: false, error: String(err) };
2003
+ }
2004
+ if (outputPath) {
2005
+ const { writeFileSync } = await import('fs');
2006
+ writeFileSync(outputPath, result, "utf-8");
2007
+ return {
2008
+ ok: true,
2009
+ outputPath,
2010
+ contentLength: result.length,
2011
+ message: `Wrote ${result.length} characters to ${outputPath}`
2012
+ };
2013
+ }
2014
+ return {
2015
+ ok: true,
2016
+ result,
2017
+ contentLength: result.length,
2018
+ variableCount: Object.keys(variables).length
2019
+ };
2020
+ }
2021
+ });
2022
+ api.tools.register({
2023
+ name: "template_render",
2024
+ description: "Read a template file from disk and expand it with the given variables.",
2025
+ inputSchema: {
2026
+ type: "object",
2027
+ properties: {
2028
+ templatePath: { type: "string", description: "Path to the template file" },
2029
+ variables: {
2030
+ type: "object",
2031
+ description: "Variables to substitute",
2032
+ additionalProperties: { type: "string" }
2033
+ },
2034
+ outputPath: { type: "string", description: "Optional path to write the rendered result" },
2035
+ raw: { type: "boolean", default: false }
2036
+ },
2037
+ required: ["templatePath", "variables"]
2038
+ },
2039
+ permission: "auto",
2040
+ mutating: true,
2041
+ async execute(input) {
2042
+ const templatePath = input["templatePath"];
2043
+ const variables = input["variables"];
2044
+ const outputPath = input["outputPath"];
2045
+ const raw = input["raw"] ?? false;
2046
+ if (!templatePath || typeof templatePath !== "string") {
2047
+ return { ok: false, error: "templatePath is required and must be a string" };
2048
+ }
2049
+ if (!variables || typeof variables !== "object") {
2050
+ return { ok: false, error: "variables is required and must be an object" };
2051
+ }
2052
+ let content;
2053
+ try {
2054
+ const { readFileSync: readFileSync3 } = await import('fs');
2055
+ content = readFileSync3(templatePath, "utf-8");
2056
+ } catch (err) {
2057
+ return { ok: false, error: `Could not read template file: ${err}` };
2058
+ }
2059
+ let result;
2060
+ try {
2061
+ result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables);
2062
+ } catch (err) {
2063
+ return { ok: false, error: `Template rendering failed: ${err}` };
2064
+ }
2065
+ if (outputPath) {
2066
+ const { writeFileSync } = await import('fs');
2067
+ writeFileSync(outputPath, result, "utf-8");
2068
+ return {
2069
+ ok: true,
2070
+ templatePath,
2071
+ outputPath,
2072
+ message: `Rendered and wrote ${result.length} chars to ${outputPath}`
2073
+ };
2074
+ }
2075
+ return {
2076
+ ok: true,
2077
+ templatePath,
2078
+ result,
2079
+ contentLength: result.length
2080
+ };
2081
+ }
2082
+ });
2083
+ api.tools.register({
2084
+ name: "template_create",
2085
+ description: "Save a named template to the plugin's template store for later use.",
2086
+ inputSchema: {
2087
+ type: "object",
2088
+ properties: {
2089
+ name: { type: "string", description: "Unique name for this template" },
2090
+ content: { type: "string", description: "Template content with {{variable}} placeholders" },
2091
+ description: { type: "string", description: "Optional description of what this template is for" }
2092
+ },
2093
+ required: ["name", "content"]
2094
+ },
2095
+ permission: "auto",
2096
+ mutating: false,
2097
+ async execute(input) {
2098
+ const name = input["name"];
2099
+ const content = input["content"];
2100
+ const description = input["description"];
2101
+ if (!name || typeof name !== "string" || name.trim() === "") {
2102
+ return { ok: false, error: "name is required and must be a non-empty string" };
2103
+ }
2104
+ if (!content || typeof content !== "string") {
2105
+ return { ok: false, error: "content is required and must be a string" };
2106
+ }
2107
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2108
+ const existing = templates.get(name);
2109
+ const tmpl = {
2110
+ name,
2111
+ content,
2112
+ description,
2113
+ createdAt: existing?.createdAt ?? now,
2114
+ updatedAt: now
2115
+ };
2116
+ templates.set(name, tmpl);
2117
+ api.metrics.gauge("template_count", templates.size);
2118
+ return {
2119
+ ok: true,
2120
+ name,
2121
+ message: existing ? `Updated template '${name}'.` : `Created template '${name}'.`,
2122
+ createdAt: tmpl.createdAt
2123
+ };
2124
+ }
2125
+ });
2126
+ api.tools.register({
2127
+ name: "template_list",
2128
+ description: "List all templates saved in the plugin's template store.",
2129
+ inputSchema: { type: "object", properties: {} },
2130
+ permission: "auto",
2131
+ mutating: false,
2132
+ async execute() {
2133
+ const list = Array.from(templates.values()).map((t) => ({
2134
+ name: t.name,
2135
+ description: t.description,
2136
+ contentLength: t.content.length,
2137
+ createdAt: t.createdAt,
2138
+ updatedAt: t.updatedAt
2139
+ }));
2140
+ return {
2141
+ ok: true,
2142
+ count: list.length,
2143
+ templates: list
2144
+ };
2145
+ }
2146
+ });
2147
+ api.registerSystemPromptContributor(async () => [
2148
+ {
2149
+ type: "text",
2150
+ text: `Template engine available:
2151
+ - template_expand: expand a template string with {{variable}}, {{#if}} conditionals, {{#each}} loops
2152
+ - template_render: render a template file with variables
2153
+ - template_create: save a named template
2154
+ - template_list: list saved templates`
2155
+ }
2156
+ ]);
2157
+ api.log.info("template-engine plugin loaded", { version: "0.1.0" });
2158
+ }
2159
+ };
2160
+ var template_engine_default = plugin9;
2161
+ var API_VERSION9 = "^0.1.10";
2162
+ function runGit2(args, cwd) {
2163
+ try {
2164
+ return execSync(`git ${args.join(" ")}`, {
2165
+ encoding: "utf-8",
2166
+ cwd,
2167
+ stdio: ["pipe", "pipe", "pipe"],
2168
+ timeout: 3e4
2169
+ }).trim();
2170
+ } catch (err) {
2171
+ const e = err;
2172
+ if (e.status === 128) throw new Error("Not a git repository");
2173
+ throw new Error(`git failed: ${e.message ?? e.stderr ?? String(err)}`);
2174
+ }
2175
+ }
2176
+ function getPackageJson(cwd) {
2177
+ const path = cwd ? `${cwd}/package.json` : "package.json";
2178
+ if (!existsSync(path)) return null;
2179
+ try {
2180
+ return JSON.parse(readFileSync(path, "utf-8"));
2181
+ } catch {
2182
+ return null;
2183
+ }
2184
+ }
2185
+ function parseVersion(v) {
2186
+ const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
2187
+ if (!m) return [0, 0, 0];
2188
+ return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
2189
+ }
2190
+ function bumpVersion(version, part) {
2191
+ let [major, minor, patch] = parseVersion(version);
2192
+ if (part === "major") {
2193
+ major++;
2194
+ minor = 0;
2195
+ patch = 0;
2196
+ } else if (part === "minor") {
2197
+ minor++;
2198
+ patch = 0;
2199
+ } else if (part === "patch") {
2200
+ patch++;
2201
+ } else {
2202
+ return version;
2203
+ }
2204
+ return `${major}.${minor}.${patch}`;
2205
+ }
2206
+ function getRecentCommits(sinceTag, cwd) {
2207
+ const range = sinceTag ? `${sinceTag}..HEAD` : "-30";
2208
+ const output = runGit2(["log", range, "--format=%H %s"], cwd);
2209
+ if (!output) return [];
2210
+ return output.split("\n").filter(Boolean).map((line) => {
2211
+ const spaceIdx = line.indexOf(" ");
2212
+ const hash = line.slice(0, spaceIdx);
2213
+ const message = line.slice(spaceIdx + 1);
2214
+ const m = message.match(/^(\w+)(!)?(?:\(([^)]+)\))?:\s(.+)/);
2215
+ const type = m?.[1] ?? "chore";
2216
+ const breaking = !!m?.[2];
2217
+ const scope = m?.[2];
2218
+ const msg = m?.[3] ?? message;
2219
+ return { hash, type, scope, message: msg, breaking };
2220
+ });
2221
+ }
2222
+ function determineBump(commits) {
2223
+ for (const c of commits) {
2224
+ if (c.breaking || c.type === "feat!:" || c.type === "fix!") {
2225
+ return "major";
2226
+ }
2227
+ }
2228
+ for (const c of commits) {
2229
+ if (c.type === "feat" || c.type === "refactor" && c.scope) {
2230
+ return "minor";
2231
+ }
2232
+ }
2233
+ return "patch";
2234
+ }
2235
+ function generateChangelog(commits) {
2236
+ const sections = {
2237
+ breaking: [],
2238
+ feat: [],
2239
+ fix: [],
2240
+ perf: [],
2241
+ docs: [],
2242
+ refactor: [],
2243
+ test: [],
2244
+ chore: [],
2245
+ other: []
2246
+ };
2247
+ for (const c of commits) {
2248
+ if (c.breaking) {
2249
+ sections.breaking.push(c);
2250
+ } else if (c.type in sections) {
2251
+ sections[c.type].push(c);
2252
+ } else {
2253
+ sections.other.push(c);
2254
+ }
2255
+ }
2256
+ const lines = ["# Changelog\n"];
2257
+ if (sections.breaking.length > 0) {
2258
+ lines.push("## \u26A0\uFE0F BREAKING CHANGES\n");
2259
+ for (const c of sections.breaking) {
2260
+ lines.push(`- **${c.hash.slice(0, 7)}** ${c.message} (${c.type})`);
2261
+ }
2262
+ lines.push("");
2263
+ }
2264
+ const ordered = ["feat", "fix", "perf", "docs", "refactor", "test", "chore", "other"];
2265
+ const labels = {
2266
+ breaking: "Breaking",
2267
+ feat: "Features",
2268
+ fix: "Bug Fixes",
2269
+ perf: "Performance",
2270
+ docs: "Documentation",
2271
+ refactor: "Refactoring",
2272
+ test: "Tests",
2273
+ chore: "Chores",
2274
+ other: "Other Changes"
2275
+ };
2276
+ for (const key of ordered) {
2277
+ const items = sections[key];
2278
+ if (items.length === 0) continue;
2279
+ lines.push(`## ${labels[key]}
2280
+ `);
2281
+ for (const c of items) {
2282
+ const scope = c.scope ? `**${c.scope}**: ` : "";
2283
+ lines.push(`- **${c.hash.slice(0, 7)}** ${scope}${c.message}`);
2284
+ }
2285
+ lines.push("");
2286
+ }
2287
+ return lines.join("\n").trim();
2288
+ }
2289
+ var plugin10 = {
2290
+ name: "semver-bump",
2291
+ version: "0.1.0",
2292
+ description: "Conventional-commit-driven semver version bumps with changelog generation",
2293
+ apiVersion: API_VERSION9,
2294
+ capabilities: { tools: true },
2295
+ defaultConfig: {
2296
+ tagPrefix: "v",
2297
+ changelogFile: "CHANGELOG.md",
2298
+ autoTag: true,
2299
+ tagMessage: "Release {{version}}"
2300
+ },
2301
+ configSchema: {
2302
+ type: "object",
2303
+ properties: {
2304
+ tagPrefix: { type: "string", default: "v" },
2305
+ changelogFile: { type: "string", default: "CHANGELOG.md" },
2306
+ autoTag: { type: "boolean", default: true },
2307
+ tagMessage: { type: "string", default: "Release {{version}}" }
2308
+ }
2309
+ },
2310
+ setup(api) {
2311
+ const tagPrefix = api.config.extensions?.["semver-bump"]?.["tagPrefix"] ?? "v";
2312
+ const autoTag = api.config.extensions?.["semver-bump"]?.["autoTag"] ?? true;
2313
+ api.config.extensions?.["semver-bump"]?.["changelogFile"] ?? "CHANGELOG.md";
2314
+ api.tools.register({
2315
+ name: "semver_bump",
2316
+ description: "Determine the next version bump from conventional commits since the last tag, or force a specific bump. Creates a git tag.",
2317
+ inputSchema: {
2318
+ type: "object",
2319
+ properties: {
2320
+ cwd: { type: "string", description: "Working directory (defaults to project root)" },
2321
+ dryRun: { type: "boolean", default: false },
2322
+ part: { type: "string", enum: ["major", "minor", "patch", "auto"], default: "auto", description: "Version part to bump (auto = infer from commits)" }
2323
+ }
2324
+ },
2325
+ permission: "confirm",
2326
+ mutating: true,
2327
+ async execute(input) {
2328
+ const cwd = input["cwd"];
2329
+ const dryRun = input["dryRun"] ?? false;
2330
+ const part = input["part"] ?? "auto";
2331
+ const pkg = getPackageJson(cwd);
2332
+ if (!pkg) {
2333
+ return { ok: false, error: "No package.json found" };
2334
+ }
2335
+ const currentVersion = pkg.version;
2336
+ let bumpPart = part;
2337
+ let commits = [];
2338
+ if (part === "auto") {
2339
+ let lastTag;
2340
+ try {
2341
+ const tagsOutput = runGit2(["describe", "--tags", "--abbrev=0"], cwd);
2342
+ lastTag = tagsOutput || void 0;
2343
+ } catch {
2344
+ }
2345
+ try {
2346
+ commits = getRecentCommits(lastTag, cwd);
2347
+ } catch (err) {
2348
+ const msg = err instanceof Error ? err.message : String(err);
2349
+ return { ok: false, error: `Git error: ${msg}`, bumpPart: "patch" };
2350
+ }
2351
+ bumpPart = determineBump(commits);
2352
+ } else {
2353
+ bumpPart = part;
2354
+ }
2355
+ const newVersion = bumpVersion(currentVersion, bumpPart);
2356
+ if (dryRun) {
2357
+ return {
2358
+ ok: true,
2359
+ dryRun: true,
2360
+ currentVersion,
2361
+ suggestedBump: bumpPart,
2362
+ newVersion,
2363
+ commitCount: part === "auto" ? commits.length : void 0,
2364
+ message: `Would bump ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
2365
+ };
2366
+ }
2367
+ const fs = await import('fs');
2368
+ const pkgPath = cwd ? `${cwd}/package.json` : "package.json";
2369
+ const pkgData = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
2370
+ pkgData.version = newVersion;
2371
+ fs.writeFileSync(pkgPath, JSON.stringify(pkgData, null, 2) + "\n", "utf-8");
2372
+ try {
2373
+ runGit2(["add", "package.json"], cwd);
2374
+ runGit2(["commit", "-m", `chore: bump version to ${newVersion}`], cwd);
2375
+ } catch {
2376
+ }
2377
+ if (autoTag) {
2378
+ try {
2379
+ runGit2(["tag", "-a", `${tagPrefix}${newVersion}`, "-m", `Release ${newVersion}`], cwd);
2380
+ } catch {
2381
+ }
2382
+ }
2383
+ api.log.info("semver-bump: bumped", { from: currentVersion, to: newVersion, bump: bumpPart });
2384
+ api.metrics.counter("version_bump", 1, { bump: bumpPart });
2385
+ await api.session.append({
2386
+ type: "semver-bump:bumped",
2387
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2388
+ from: currentVersion,
2389
+ to: newVersion,
2390
+ bump: bumpPart
2391
+ });
2392
+ return {
2393
+ ok: true,
2394
+ currentVersion,
2395
+ newVersion,
2396
+ bump: bumpPart,
2397
+ tag: `${tagPrefix}${newVersion}`,
2398
+ message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
2399
+ };
2400
+ }
2401
+ });
2402
+ api.tools.register({
2403
+ name: "semver_current",
2404
+ description: "Return the current version from package.json and the latest git tag.",
2405
+ inputSchema: {
2406
+ type: "object",
2407
+ properties: {
2408
+ cwd: { type: "string", description: "Working directory" }
2409
+ }
2410
+ },
2411
+ permission: "auto",
2412
+ mutating: false,
2413
+ async execute(input) {
2414
+ const cwd = input["cwd"];
2415
+ const pkg = getPackageJson(cwd);
2416
+ const currentVersion = pkg?.version ?? "unknown";
2417
+ let latestTag = null;
2418
+ let commitsSinceTag = 0;
2419
+ try {
2420
+ const tagsOutput = runGit2(["describe", "--tags", "--abbrev=0"], cwd);
2421
+ latestTag = tagsOutput || null;
2422
+ if (latestTag) {
2423
+ const countOutput = runGit2(["rev-list", "--count", `${latestTag}..HEAD`], cwd);
2424
+ commitsSinceTag = parseInt(countOutput) || 0;
2425
+ }
2426
+ } catch {
2427
+ latestTag = null;
2428
+ }
2429
+ return {
2430
+ ok: true,
2431
+ currentVersion,
2432
+ latestTag: latestTag ?? null,
2433
+ tagPrefix,
2434
+ commitsSinceTag
2435
+ };
2436
+ }
2437
+ });
2438
+ api.tools.register({
2439
+ name: "semver_changelog",
2440
+ description: "Generate a changelog (in markdown) between two version tags or from a tag to HEAD.",
2441
+ inputSchema: {
2442
+ type: "object",
2443
+ properties: {
2444
+ from: { type: "string", description: "Starting tag (exclusive)" },
2445
+ to: { type: "string", description: 'Ending tag or "HEAD"' },
2446
+ cwd: { type: "string", description: "Working directory" },
2447
+ format: { type: "string", enum: ["markdown", "json"], default: "markdown" }
2448
+ }
2449
+ },
2450
+ permission: "auto",
2451
+ mutating: false,
2452
+ async execute(input) {
2453
+ const from = input["from"];
2454
+ const to = input["to"] ?? "HEAD";
2455
+ const cwd = input["cwd"];
2456
+ const format = input["format"] ?? "markdown";
2457
+ const range = from ? `${from}..${to}` : to;
2458
+ let commits;
2459
+ try {
2460
+ const output = runGit2(["log", range === to ? "-30" : range, "--format=%H %s"], cwd);
2461
+ commits = output.split("\n").filter(Boolean).map((line) => {
2462
+ const spaceIdx = line.indexOf(" ");
2463
+ const hash = line.slice(0, spaceIdx);
2464
+ const message = line.slice(spaceIdx + 1);
2465
+ const m = message.match(/^(\w+)(!)?(?:\(([^)]+)\))?:\s(.+)/);
2466
+ const type = m?.[1] ?? "chore";
2467
+ return {
2468
+ hash,
2469
+ type,
2470
+ scope: m?.[2],
2471
+ message: m?.[3] ?? message,
2472
+ breaking: !!m?.[2]
2473
+ };
2474
+ });
2475
+ } catch (err) {
2476
+ return { ok: false, error: `Failed to get git log: ${err}` };
2477
+ }
2478
+ if (format === "json") {
2479
+ return {
2480
+ ok: true,
2481
+ from,
2482
+ to,
2483
+ commits,
2484
+ commitCount: commits.length
2485
+ };
2486
+ }
2487
+ const changelog = generateChangelog(commits);
2488
+ return {
2489
+ ok: true,
2490
+ from: from ?? "(beginning)",
2491
+ to,
2492
+ changelog,
2493
+ commitCount: commits.length,
2494
+ breakingCount: commits.filter((c) => c.breaking).length
2495
+ };
2496
+ }
2497
+ });
2498
+ api.log.info("semver-bump plugin loaded", { version: "0.1.0", tagPrefix, autoTag });
2499
+ }
2500
+ };
2501
+ var semver_bump_default = plugin10;
2502
+
2503
+ export { auto_doc_default as autoDocPlugin, cost_tracker_default as costTrackerPlugin, cron_default as cronPlugin, file_watcher_default as fileWatcherPlugin, git_autocommit_default as gitAutocommitPlugin, json_path_default as jsonPathPlugin, semver_bump_default as semverBumpPlugin, shell_check_default as shellCheckPlugin, template_engine_default as templateEnginePlugin, web_search_default as webSearchPlugin };