@wrongstack/plugins 1.0.7 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/accessibility-auditor/index.d.ts +1 -1
  2. package/dist/accessibility-auditor.js +15 -3
  3. package/dist/agent-handoff.js +6 -6
  4. package/dist/auto-doc/index.d.ts +1 -1
  5. package/dist/auto-doc.js +31 -20
  6. package/dist/auto-i18n-extractor/index.d.ts +1 -1
  7. package/dist/auto-i18n-extractor.js +12 -8
  8. package/dist/branch-guard.js +3 -3
  9. package/dist/changelog-writer/index.d.ts +1 -1
  10. package/dist/changelog-writer.js +19 -10
  11. package/dist/checkpoint/index.d.ts +1 -1
  12. package/dist/checkpoint.js +38 -24
  13. package/dist/code-metrics/index.d.ts +1 -1
  14. package/dist/code-metrics.js +12 -4
  15. package/dist/commit-validator.js +5 -5
  16. package/dist/context-pins/index.d.ts +1 -1
  17. package/dist/context-pins.js +12 -9
  18. package/dist/cost-tracker.js +21 -9
  19. package/dist/cron/index.d.ts +1 -1
  20. package/dist/cron.js +18 -5
  21. package/dist/dead-code-detector/index.d.ts +1 -1
  22. package/dist/dead-code-detector.js +14 -12
  23. package/dist/duplicate-code-detector/index.d.ts +1 -1
  24. package/dist/duplicate-code-detector.js +11 -3
  25. package/dist/feature-flag-tracker/index.d.ts +1 -1
  26. package/dist/feature-flag-tracker.js +12 -4
  27. package/dist/file-watcher/index.d.ts +1 -1
  28. package/dist/file-watcher.js +37 -38
  29. package/dist/git-autocommit/index.d.ts +1 -1
  30. package/dist/git-autocommit.js +44 -39
  31. package/dist/gitignore-guard/index.d.ts +1 -1
  32. package/dist/gitignore-guard.js +12 -6
  33. package/dist/index.js +1534 -1080
  34. package/dist/interface-contract-guard/index.d.ts +1 -1
  35. package/dist/interface-contract-guard.js +12 -4
  36. package/dist/knowledge-graph/index.d.ts +1 -1
  37. package/dist/knowledge-graph.js +11 -9
  38. package/dist/migration-planner/index.d.ts +1 -1
  39. package/dist/migration-planner.js +6 -2
  40. package/dist/notify-hub/index.d.ts +1 -1
  41. package/dist/notify-hub.js +12 -11
  42. package/dist/performance-regression-gate/index.d.ts +1 -1
  43. package/dist/performance-regression-gate.js +33 -13
  44. package/dist/pr-drafter/index.d.ts +10 -1
  45. package/dist/pr-drafter.js +57 -26
  46. package/dist/refactor-suggester/index.d.ts +1 -1
  47. package/dist/refactor-suggester.js +17 -4
  48. package/dist/release-notes-generator.js +2 -2
  49. package/dist/secret-scanner/index.d.ts +1 -1
  50. package/dist/secret-scanner.js +11 -3
  51. package/dist/security-hotspot-scanner/index.d.ts +1 -1
  52. package/dist/security-hotspot-scanner.js +6 -2
  53. package/dist/semantic-search-indexer/index.d.ts +1 -1
  54. package/dist/semantic-search-indexer.js +19 -3
  55. package/dist/semver-bump/index.d.ts +1 -1
  56. package/dist/semver-bump.js +57 -37
  57. package/dist/session-recap.js +4 -2
  58. package/dist/shell-check/index.d.ts +1 -1
  59. package/dist/shell-check.js +30 -39
  60. package/dist/smart-rename/index.d.ts +1 -1
  61. package/dist/smart-rename.js +23 -10
  62. package/dist/template-engine/index.d.ts +1 -1
  63. package/dist/template-engine.js +51 -38
  64. package/dist/test-flake-detector/index.d.ts +1 -1
  65. package/dist/test-flake-detector.js +11 -5
  66. package/dist/test-generator/index.d.ts +1 -1
  67. package/dist/test-generator.js +12 -8
  68. package/dist/todo-tracker/index.d.ts +68 -1
  69. package/dist/todo-tracker.js +579 -395
  70. package/dist/token-budget.js +7 -4
  71. package/dist/token-throttle.js +6 -3
  72. package/package.json +7 -7
@@ -30,7 +30,7 @@
30
30
  *
31
31
  * @public
32
32
  */
33
- import type { Plugin } from '@wrongstack/core/types';
33
+ import { type Plugin } from '@wrongstack/core/types';
34
34
  interface SemanticSearchConfig {
35
35
  enabled: boolean;
36
36
  includeExtensions: string[];
@@ -1,6 +1,7 @@
1
1
  // src/semantic-search-indexer/index.ts
2
2
  import * as fs from "node:fs/promises";
3
3
  import { isAbsolute, relative, resolve } from "node:path";
4
+ import { ToolValidationError } from "@wrongstack/core/types";
4
5
  import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
5
6
  var API_VERSION = "^0.1.10";
6
7
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -510,16 +511,31 @@ var plugin = {
510
511
  icon: "search",
511
512
  async execute(input) {
512
513
  if (!cfg.enabled) {
513
- return { ok: false, error: "semantic-search-indexer is disabled" };
514
+ throw new Error("semantic-search-indexer is disabled");
514
515
  }
515
516
  const rawPath = input.path ?? input["directory"] ?? input["dir"] ?? input["SearchDirectory"] ?? input["SearchPath"] ?? input["TargetFile"] ?? input["targetFile"] ?? input["filePath"] ?? input["file"];
516
517
  const resolved = resolveProjectPath(typeof rawPath === "string" ? rawPath : void 0);
517
518
  if (!resolved) {
518
- return { ok: false, error: "path outside project root" };
519
+ throw new ToolValidationError({ message: "path outside project root", field: "path" });
519
520
  }
520
- await ensureIndex(resolved, cfg);
521
521
  const rawQuery = input.query ?? input["q"] ?? input["text"] ?? input["keyword"] ?? input["keywords"] ?? input["search"] ?? "";
522
522
  const query = String(rawQuery);
523
+ if (tokenize(query, cfg.minTokenLength).length === 0) {
524
+ throw new ToolValidationError({
525
+ message: `query must contain at least one keyword of ${cfg.minTokenLength}+ characters`,
526
+ field: "query"
527
+ });
528
+ }
529
+ try {
530
+ await fs.stat(resolved);
531
+ } catch (err) {
532
+ throw new ToolValidationError({
533
+ message: `path does not exist or cannot be read: ${typeof rawPath === "string" ? rawPath : resolved}`,
534
+ field: "path",
535
+ cause: err
536
+ });
537
+ }
538
+ await ensureIndex(resolved, cfg);
523
539
  const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : cfg.defaultLimit;
524
540
  const results = runQuery(query, limit, cfg);
525
541
  const queryTokens = [...new Set(tokenize(query, cfg.minTokenLength))];
@@ -6,7 +6,7 @@
6
6
  * - semver_current: Show the current version from package.json
7
7
  * - semver_changelog: Generate a changelog between two versions
8
8
  */
9
- import type { Plugin } from '@wrongstack/core/types';
9
+ import { type Plugin } from '@wrongstack/core/types';
10
10
  type BumpType = 'major' | 'minor' | 'patch' | 'auto';
11
11
  interface ConventionalCommit {
12
12
  hash: string;
@@ -1,10 +1,26 @@
1
1
  // src/semver-bump/index.ts
2
2
  import { expectDefined } from "@wrongstack/core/utils";
3
3
  import { toErrorMessage } from "@wrongstack/core/utils";
4
+ import { ToolValidationError } from "@wrongstack/core/types";
4
5
  import { execFile } from "node:child_process";
5
6
  import { access, readFile, readdir, writeFile } from "node:fs/promises";
6
7
  import { isAbsolute, join, relative, resolve } from "node:path";
7
8
  var API_VERSION = "^0.1.10";
9
+ function requireProjectRoot(rawCwd) {
10
+ const safeCwd = resolveProjectRoot(rawCwd);
11
+ if (!safeCwd) {
12
+ throw new ToolValidationError({
13
+ message: "cwd must stay within the current project directory",
14
+ field: "cwd"
15
+ });
16
+ }
17
+ return safeCwd;
18
+ }
19
+ function requireGitRef(field, ref) {
20
+ if (ref !== void 0 && (typeof ref !== "string" || ref.startsWith("-"))) {
21
+ throw new ToolValidationError({ message: `${field} is not a valid git ref`, field });
22
+ }
23
+ }
8
24
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
9
25
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
10
26
  const base = resolve(root);
@@ -250,14 +266,10 @@ var plugin = {
250
266
  defaultPart = readDefaultPart(next);
251
267
  });
252
268
  async function performBump(part, dryRun, cwd) {
253
- const safeCwd = resolveProjectRoot(cwd);
254
- if (!safeCwd) {
255
- return { ok: false, error: "cwd must stay within the current project directory" };
256
- }
257
- cwd = safeCwd;
269
+ cwd = requireProjectRoot(cwd);
258
270
  const pkg = await getPackageJson(cwd);
259
271
  if (!pkg) {
260
- return { ok: false, error: "No package.json found" };
272
+ throw new Error("No package.json found");
261
273
  }
262
274
  const currentVersion = pkg.version;
263
275
  let bumpPart = part;
@@ -272,8 +284,7 @@ var plugin = {
272
284
  try {
273
285
  commits = await getRecentCommits(lastTag, cwd);
274
286
  } catch (err) {
275
- const msg = toErrorMessage(err);
276
- return { ok: false, error: `Git error: ${msg}`, bumpPart: "patch" };
287
+ throw new Error(`Git error: ${toErrorMessage(err)}`, { cause: err });
277
288
  }
278
289
  bumpPart = determineBump(commits);
279
290
  } else {
@@ -305,8 +316,7 @@ var plugin = {
305
316
  try {
306
317
  await runCommand(process.execPath, [bumpScript, "set", newVersion], root);
307
318
  } catch (err) {
308
- const msg = toErrorMessage(err);
309
- return { ok: false, error: `bump script failed: ${msg}` };
319
+ throw new Error(`bump script failed: ${toErrorMessage(err)}`, { cause: err });
310
320
  }
311
321
  for (const rel of ["package.json", "package-lock.json", "src/lib/utils.ts", "index.html"]) {
312
322
  const p = join(root, "website", rel);
@@ -323,16 +333,15 @@ var plugin = {
323
333
  try {
324
334
  pkgData = JSON.parse(await readFile(manifest, "utf-8"));
325
335
  } catch (err) {
326
- return {
327
- ok: false,
328
- error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
329
- };
336
+ throw new Error(
337
+ `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`,
338
+ { cause: err }
339
+ );
330
340
  }
331
341
  if (!pkgData || typeof pkgData !== "object") {
332
- return {
333
- ok: false,
334
- error: `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
335
- };
342
+ throw new Error(
343
+ `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
344
+ );
336
345
  }
337
346
  pkgData.version = newVersion;
338
347
  pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
@@ -342,16 +351,20 @@ var plugin = {
342
351
  await writeFile(path, contents, "utf-8");
343
352
  }
344
353
  }
354
+ let commitError;
345
355
  try {
346
356
  await runGit(["add", "--", ...changed], cwd);
347
357
  await runGit(["commit", "-m", `chore: bump version to ${newVersion}`], cwd);
348
- } catch {
358
+ } catch (err) {
359
+ commitError = toErrorMessage(err);
349
360
  }
361
+ let tagError;
350
362
  if (autoTag) {
351
363
  try {
352
364
  const msg = tagMessage.replace("{{version}}", newVersion);
353
365
  await runGit(["tag", "-a", `${tagPrefix}${newVersion}`, "-m", msg], cwd);
354
- } catch {
366
+ } catch (err) {
367
+ tagError = toErrorMessage(err);
355
368
  }
356
369
  }
357
370
  api.log.info("semver-bump: bumped", { from: currentVersion, to: newVersion, bump: bumpPart });
@@ -371,13 +384,22 @@ var plugin = {
371
384
  commitCount: commits.length,
372
385
  breakingCount: commits.filter((c) => c.breaking).length
373
386
  };
387
+ const tagged = autoTag && tagError === void 0;
388
+ const warnings = [
389
+ ...commitError ? [`commit failed: ${commitError}`] : [],
390
+ ...tagError ? [`tag failed: ${tagError}`] : []
391
+ ];
374
392
  return {
375
393
  ok: true,
376
394
  currentVersion,
377
395
  newVersion,
378
396
  bump: bumpPart,
379
- tag: `${tagPrefix}${newVersion}`,
380
- message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
397
+ // Only name the tag when it was actually created.
398
+ tag: tagged ? `${tagPrefix}${newVersion}` : null,
399
+ committed: commitError === void 0,
400
+ tagged,
401
+ ...warnings.length > 0 ? { warnings } : {},
402
+ message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})` + (warnings.length > 0 ? ` \u2014 ${warnings.join("; ")}` : "")
381
403
  };
382
404
  }
383
405
  api.tools.register({
@@ -463,8 +485,12 @@ var plugin = {
463
485
  if (!safeCwd) {
464
486
  return { message: "cwd must stay within the current project directory" };
465
487
  }
466
- const result = await performBump(mode, dry, safeCwd);
467
- return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
488
+ try {
489
+ const result = await performBump(mode, dry, safeCwd);
490
+ return { message: String(result["message"] ?? JSON.stringify(result)) };
491
+ } catch (err) {
492
+ return { message: toErrorMessage(err) };
493
+ }
468
494
  }
469
495
  });
470
496
  api.tools.register({
@@ -481,11 +507,7 @@ var plugin = {
481
507
  async execute(input) {
482
508
  state.invocationCount += 1;
483
509
  state.perTool["semver_current"] = (state.perTool["semver_current"] ?? 0) + 1;
484
- const cwdInput = input["cwd"];
485
- const safeCwd = resolveProjectRoot(cwdInput);
486
- if (!safeCwd) {
487
- return { ok: false, error: "cwd must stay within the current project directory" };
488
- }
510
+ const safeCwd = requireProjectRoot(input["cwd"]);
489
511
  const pkg = await getPackageJson(safeCwd);
490
512
  const currentVersion = pkg?.version ?? "unknown";
491
513
  let latestTag = null;
@@ -531,22 +553,20 @@ var plugin = {
531
553
  state.perTool["semver_changelog"] = (state.perTool["semver_changelog"] ?? 0) + 1;
532
554
  const from = input["from"];
533
555
  const to = input["to"] ?? "HEAD";
534
- const cwd = input["cwd"];
535
- const safeCwd = resolveProjectRoot(cwd);
536
- if (!safeCwd) {
537
- return { ok: false, error: "cwd must stay within the current project directory" };
538
- }
556
+ requireGitRef("from", from);
557
+ requireGitRef("to", to);
558
+ const safeCwd = requireProjectRoot(input["cwd"]);
539
559
  const format = input["format"] ?? "markdown";
540
- const range = from ? `${from}..${to}` : to;
560
+ const rangeArgs = from ? [`${from}..${to}`] : ["-30", to];
541
561
  let commits;
542
562
  try {
543
563
  const output = await runGit(
544
- ["log", range === to ? "-30" : range, "--format=%H%x1f%s%x1f%b%x1e"],
564
+ ["log", ...rangeArgs, "--format=%H%x1f%s%x1f%b%x1e"],
545
565
  safeCwd
546
566
  );
547
567
  commits = parseGitLogOutput(output);
548
568
  } catch (err) {
549
- return { ok: false, error: `Failed to get git log: ${err}` };
569
+ throw new Error(`Failed to get git log: ${toErrorMessage(err)}`, { cause: err });
550
570
  }
551
571
  if (format === "json") {
552
572
  return {
@@ -238,8 +238,10 @@ var plugin = {
238
238
  const offTool = api.onPattern("tool.*", (eventName, payload) => {
239
239
  touchActivity();
240
240
  const p = payload;
241
- const toolName = p?.tool ?? p?.name ?? eventName;
242
- if (typeof toolName === "string") bumpToolCount(toolName);
241
+ const rawTool = p?.tool;
242
+ const nameOf = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.name === "string" ? v.name : void 0;
243
+ const toolName = nameOf(rawTool) ?? nameOf(p?.name) ?? eventName;
244
+ bumpToolCount(toolName);
243
245
  if (toolName === "git_autocommit" || toolName.startsWith("git ")) {
244
246
  }
245
247
  });
@@ -8,7 +8,7 @@
8
8
  * via the `directory` + `pattern` parameters. Pass `files` for specific
9
9
  * files, or `directory` (optionally with `pattern`) for recursive scanning.
10
10
  */
11
- import type { Plugin } from '@wrongstack/core/types';
11
+ import { type Plugin } from '@wrongstack/core/types';
12
12
  declare const plugin: Plugin;
13
13
  export default plugin;
14
14
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  // src/shell-check/index.ts
2
+ import { ToolValidationError } from "@wrongstack/core/types";
2
3
  import { execFile } from "node:child_process";
3
4
  import { readdir } from "node:fs/promises";
4
5
  import { isAbsolute, join, relative, resolve } from "node:path";
@@ -72,8 +73,11 @@ async function runShellCheck(files, severity, cwd) {
72
73
  }
73
74
  );
74
75
  });
75
- } catch {
76
- return [];
76
+ } catch (err) {
77
+ throw new Error(
78
+ `shellcheck failed without output: ${err instanceof Error ? err.message : String(err)}`,
79
+ { cause: err }
80
+ );
77
81
  }
78
82
  if (!raw.trim()) return [];
79
83
  try {
@@ -86,22 +90,31 @@ async function runShellCheck(files, severity, cwd) {
86
90
  code: item.code,
87
91
  message: item.message
88
92
  }));
89
- } catch {
90
- return [];
93
+ } catch (err) {
94
+ throw new Error(`shellcheck returned unparseable output: ${raw.trim().slice(0, 500)}`, {
95
+ cause: err
96
+ });
91
97
  }
92
98
  }
93
- async function findShellFiles(dir, pattern) {
99
+ async function findShellFiles(dir, pattern, isRoot = true) {
94
100
  const results = [];
95
101
  let entries;
96
102
  try {
97
103
  entries = await readdir(dir, { withFileTypes: true });
98
- } catch {
104
+ } catch (err) {
105
+ if (isRoot) {
106
+ throw new ToolValidationError({
107
+ message: `directory does not exist or cannot be read: ${dir}`,
108
+ field: "directory",
109
+ cause: err
110
+ });
111
+ }
99
112
  return results;
100
113
  }
101
114
  for (const entry of entries) {
102
115
  const full = join(dir, entry.name);
103
116
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
104
- results.push(...await findShellFiles(full, pattern));
117
+ results.push(...await findShellFiles(full, pattern, false));
105
118
  } else if (entry.isFile() && (entry.name.endsWith(".sh") || entry.name.endsWith(".bash") || entry.name.endsWith(".zsh") || entry.name === "Dockerfile" || entry.name === ".bashrc" || entry.name === ".zshrc")) {
106
119
  if (!pattern || entry.name.includes(pattern)) {
107
120
  results.push(full);
@@ -171,12 +184,8 @@ var plugin = {
171
184
  enum: ["error", "warning", "info", "style"],
172
185
  default: "warning",
173
186
  description: "Minimum severity level to report"
174
- },
175
- fix: {
176
- type: "boolean",
177
- default: false,
178
- description: "Apply safe automatic fixes where possible"
179
187
  }
188
+ // `fix` was declared ("apply safe automatic fixes") but never implemented.
180
189
  }
181
190
  },
182
191
  permission: "auto",
@@ -202,22 +211,16 @@ var plugin = {
202
211
  state.invocationCount += 1;
203
212
  const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject(p);
204
213
  if (!pathIsSafe(directory)) {
205
- return {
206
- ok: false,
207
- error: `directory path is outside the project root: ${directory}`,
208
- issues: [],
209
- filesScanned: 0,
210
- rejectedOutsideProject: true
211
- };
214
+ throw new ToolValidationError({
215
+ message: `directory path is outside the project root: ${directory}`,
216
+ field: "directory"
217
+ });
212
218
  }
213
219
  if (files?.some((f) => !pathIsSafe(f))) {
214
- return {
215
- ok: false,
216
- error: "one or more file paths are outside the project root",
217
- issues: [],
218
- filesScanned: 0,
219
- rejectedOutsideProject: true
220
- };
220
+ throw new ToolValidationError({
221
+ message: "one or more file paths are outside the project root",
222
+ field: "files"
223
+ });
221
224
  }
222
225
  let checkFiles;
223
226
  let scannedDirectories = false;
@@ -243,19 +246,7 @@ var plugin = {
243
246
  mode: scannedDirectories ? "directory" : "files"
244
247
  };
245
248
  }
246
- let issues;
247
- try {
248
- issues = await runShellCheck(checkFiles, severity);
249
- } catch (err) {
250
- const msg = err instanceof Error ? err.message : String(err);
251
- return {
252
- ok: false,
253
- error: msg,
254
- issues: [],
255
- filesScanned: 0,
256
- mode: scannedDirectories ? "directory" : "files"
257
- };
258
- }
249
+ const issues = await runShellCheck(checkFiles, severity);
259
250
  const byFile = {};
260
251
  for (const issue of issues) {
261
252
  if (byFile[issue.file] === void 0) {
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * @public
20
20
  */
21
- import type { Plugin } from '@wrongstack/core/types';
21
+ import { type Plugin } from '@wrongstack/core/types';
22
22
  /** True when `name` is something this tool may safely substitute. */
23
23
  export declare function isIdentifier(name: string): boolean;
24
24
  declare const plugin: Plugin;
@@ -1,6 +1,7 @@
1
1
  // src/smart-rename/index.ts
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { extname, isAbsolute, relative, resolve } from "node:path";
4
+ import { ToolValidationError } from "@wrongstack/core/types";
4
5
  var NEW_API_VERSION = "^0.1.10";
5
6
  var state = {
6
7
  renameCount: 0,
@@ -106,32 +107,44 @@ var plugin = {
106
107
  mutating: true,
107
108
  capabilities: ["fs.write"],
108
109
  async execute(input) {
109
- if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
110
+ if (!cfg.enabled) throw new Error("smart-rename is disabled");
110
111
  const inp = input ?? {};
111
112
  const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"] ?? inp["TargetFile"] ?? inp["targetFile"] ?? inp["file"];
112
113
  const oldName = inp["oldName"] ?? inp["old_name"] ?? inp["from"];
113
114
  const newName = inp["newName"] ?? inp["new_name"] ?? inp["to"];
114
115
  if (!rawPath || typeof rawPath !== "string") {
115
- return { ok: false, error: "path is required" };
116
+ throw new ToolValidationError({ message: "path is required", field: "path" });
116
117
  }
117
118
  if (!oldName || typeof oldName !== "string" || oldName.length === 0) {
118
- return { ok: false, error: "oldName is required" };
119
+ throw new ToolValidationError({ message: "oldName is required", field: "oldName" });
119
120
  }
120
121
  if (!newName || typeof newName !== "string" || newName.length === 0) {
121
- return { ok: false, error: "newName is required" };
122
+ throw new ToolValidationError({ message: "newName is required", field: "newName" });
122
123
  }
123
124
  if (!isIdentifier(oldName)) {
124
- return { ok: false, error: `oldName "${oldName}" is not a valid identifier` };
125
+ throw new ToolValidationError({
126
+ message: `oldName "${oldName}" is not a valid identifier`,
127
+ field: "oldName"
128
+ });
125
129
  }
126
130
  if (!isIdentifier(newName)) {
127
- return { ok: false, error: `newName "${newName}" is not a valid identifier` };
131
+ throw new ToolValidationError({
132
+ message: `newName "${newName}" is not a valid identifier`,
133
+ field: "newName"
134
+ });
128
135
  }
129
136
  if (!withinProject(rawPath)) {
130
- return { ok: false, error: "path is outside the project root" };
137
+ throw new ToolValidationError({
138
+ message: "path is outside the project root",
139
+ field: "path"
140
+ });
131
141
  }
132
142
  const ext = extname(rawPath).toLowerCase();
133
143
  if (!cfg.extensions.includes(ext)) {
134
- return { ok: false, error: `extension ${ext} is not allowed for rename` };
144
+ throw new ToolValidationError({
145
+ message: `extension ${ext} is not allowed for rename`,
146
+ field: "path"
147
+ });
135
148
  }
136
149
  const resolved = resolve(process.cwd(), rawPath);
137
150
  let content;
@@ -139,7 +152,7 @@ var plugin = {
139
152
  content = readFileSync(resolved, "utf-8");
140
153
  } catch (err) {
141
154
  state.errorCount += 1;
142
- return { ok: false, error: String(err) };
155
+ throw new Error(`Could not read ${rawPath}: ${String(err)}`, { cause: err });
143
156
  }
144
157
  const { preview, replacements } = renameInContent(content, oldName, newName);
145
158
  state.renameCount += 1;
@@ -152,7 +165,7 @@ var plugin = {
152
165
  writeFileSync(resolved, preview, "utf-8");
153
166
  } catch (err) {
154
167
  state.errorCount += 1;
155
- return { ok: false, error: String(err) };
168
+ throw new Error(`Could not write ${rawPath}: ${String(err)}`, { cause: err });
156
169
  }
157
170
  }
158
171
  return {
@@ -7,7 +7,7 @@
7
7
  * - template_create: Save a named template to the plugin store
8
8
  * - template_list: List all saved templates
9
9
  */
10
- import type { Plugin } from '@wrongstack/core/types';
10
+ import { type Plugin } from '@wrongstack/core/types';
11
11
  declare const plugin: Plugin;
12
12
  export default plugin;
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -15,6 +15,7 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
15
15
  // src/template-engine/index.ts
16
16
  import { readFile, writeFile } from "node:fs/promises";
17
17
  import { isAbsolute } from "node:path";
18
+ import { ToolValidationError } from "@wrongstack/core/types";
18
19
 
19
20
  // src/runtime/index.ts
20
21
  var runtime_exports = {};
@@ -183,24 +184,26 @@ var plugin = {
183
184
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
184
185
  const raw = input["raw"] ?? false;
185
186
  if (!template || typeof template !== "string") {
186
- return { ok: false, error: "template is required and must be a string" };
187
+ throw new ToolValidationError({
188
+ message: "template is required and must be a string",
189
+ field: "template"
190
+ });
187
191
  }
188
192
  if (!variables || typeof variables !== "object") {
189
- return { ok: false, error: "variables is required and must be an object" };
190
- }
191
- let result;
192
- try {
193
- result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables, autoEscapeHtml);
194
- } catch (err) {
195
- return { ok: false, error: String(err) };
193
+ throw new ToolValidationError({
194
+ message: "variables is required and must be an object",
195
+ field: "variables"
196
+ });
196
197
  }
198
+ const result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables, autoEscapeHtml);
197
199
  if (output_path) {
198
200
  const pathError = validateWritableTemplateTarget("output_path", output_path);
199
- if (pathError) return { ok: false, error: pathError };
201
+ if (pathError)
202
+ throw new ToolValidationError({ message: pathError, field: "output_path" });
200
203
  try {
201
204
  await writeFile(output_path, result, "utf-8");
202
205
  } catch (err) {
203
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
206
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
204
207
  }
205
208
  return {
206
209
  ok: true,
@@ -251,32 +254,36 @@ var plugin = {
251
254
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
252
255
  const raw = input["raw"] ?? false;
253
256
  if (!template_path || typeof template_path !== "string") {
254
- return { ok: false, error: "template_path is required and must be a string" };
257
+ throw new ToolValidationError({
258
+ message: "template_path is required and must be a string",
259
+ field: "template_path"
260
+ });
255
261
  }
256
262
  const templatePathError = validateRelativeTemplatePath("template_path", template_path);
257
- if (templatePathError) return { ok: false, error: templatePathError };
263
+ if (templatePathError) {
264
+ throw new ToolValidationError({ message: templatePathError, field: "template_path" });
265
+ }
258
266
  if (!variables || typeof variables !== "object") {
259
- return { ok: false, error: "variables is required and must be an object" };
267
+ throw new ToolValidationError({
268
+ message: "variables is required and must be an object",
269
+ field: "variables"
270
+ });
260
271
  }
261
272
  let content;
262
273
  try {
263
274
  content = await readFile(template_path, "utf-8");
264
275
  } catch (err) {
265
- return { ok: false, error: `Could not read template file: ${err}` };
266
- }
267
- let result;
268
- try {
269
- result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables, autoEscapeHtml);
270
- } catch (err) {
271
- return { ok: false, error: `Template rendering failed: ${err}` };
276
+ throw new Error(`Could not read template file: ${String(err)}`, { cause: err });
272
277
  }
278
+ const result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables, autoEscapeHtml);
273
279
  if (output_path) {
274
280
  const pathError = validateWritableTemplateTarget("output_path", output_path);
275
- if (pathError) return { ok: false, error: pathError };
281
+ if (pathError)
282
+ throw new ToolValidationError({ message: pathError, field: "output_path" });
276
283
  try {
277
284
  await writeFile(output_path, result, "utf-8");
278
285
  } catch (err) {
279
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
286
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
280
287
  }
281
288
  return {
282
289
  ok: true,
@@ -327,30 +334,39 @@ var plugin = {
327
334
  const rawDesc = input["description"] ?? input["desc"] ?? input["summary"];
328
335
  const description = typeof rawDesc === "string" ? rawDesc : void 0;
329
336
  if (!name || typeof name !== "string" || name.trim() === "") {
330
- return { ok: false, error: "name is required and must be a non-empty string" };
337
+ throw new ToolValidationError({
338
+ message: "name is required and must be a non-empty string",
339
+ field: "name"
340
+ });
331
341
  }
332
342
  if (!content || typeof content !== "string") {
333
- return { ok: false, error: "content is required and must be a string" };
343
+ throw new ToolValidationError({
344
+ message: "content is required and must be a string",
345
+ field: "content"
346
+ });
334
347
  }
335
348
  if (name.length > MAX_TEMPLATE_NAME_CHARS) {
336
- return { ok: false, error: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters` };
349
+ throw new ToolValidationError({
350
+ message: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters`,
351
+ field: "name"
352
+ });
337
353
  }
338
354
  if (content.length > MAX_TEMPLATE_CONTENT_CHARS) {
339
- return {
340
- ok: false,
341
- error: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`
342
- };
355
+ throw new ToolValidationError({
356
+ message: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`,
357
+ field: "content"
358
+ });
343
359
  }
344
360
  if (description && description.length > MAX_TEMPLATE_DESCRIPTION_CHARS) {
345
- return {
346
- ok: false,
347
- error: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`
348
- };
361
+ throw new ToolValidationError({
362
+ message: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`,
363
+ field: "description"
364
+ });
349
365
  }
350
366
  const now = (/* @__PURE__ */ new Date()).toISOString();
351
367
  const existing = templates.get(name);
352
368
  if (!existing && templates.size >= MAX_TEMPLATES) {
353
- return { ok: false, error: `template limit reached (${MAX_TEMPLATES})` };
369
+ throw new Error(`template limit reached (${MAX_TEMPLATES})`);
354
370
  }
355
371
  const tmpl = {
356
372
  name,
@@ -363,10 +379,7 @@ var plugin = {
363
379
  for (const stored of templates.values()) retainedChars += templateChars(stored);
364
380
  const nextChars = retainedChars - (existing ? templateChars(existing) : 0) + templateChars(tmpl);
365
381
  if (nextChars > MAX_TOTAL_TEMPLATE_CHARS) {
366
- return {
367
- ok: false,
368
- error: `template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`
369
- };
382
+ throw new Error(`template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`);
370
383
  }
371
384
  templates.set(name, tmpl);
372
385
  api.metrics.gauge("template_count", templates.size);