@wrongstack/plugins 0.277.2 → 0.280.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
@@ -0,0 +1,541 @@
1
+ import { existsSync, statSync } from 'fs';
2
+ import * as fsp from 'fs/promises';
3
+ import 'child_process';
4
+ import 'path';
5
+ import '@wrongstack/core';
6
+ import 'os';
7
+ import 'crypto';
8
+ import '@wrongstack/core/utils';
9
+
10
+ // src/spec-linker/index.ts
11
+ var plugin = {
12
+ name: "auto-doc"};
13
+ var auto_doc_default = plugin;
14
+ var plugin2 = {
15
+ name: "auto-escalate"};
16
+ var auto_escalate_default = plugin2;
17
+ var plugin3 = {
18
+ name: "branch-guard"};
19
+ var branch_guard_default = plugin3;
20
+ var plugin4 = {
21
+ name: "changelog-writer"};
22
+ var changelog_writer_default = plugin4;
23
+ var plugin5 = {
24
+ name: "checkpoint"};
25
+ var checkpoint_default = plugin5;
26
+ var plugin6 = {
27
+ name: "commit-validator"};
28
+ var commit_validator_default = plugin6;
29
+ var plugin7 = {
30
+ name: "config-validator"};
31
+ var config_validator_default = plugin7;
32
+ var plugin8 = {
33
+ name: "context-pins"};
34
+ var context_pins_default = plugin8;
35
+ var plugin9 = {
36
+ name: "cost-tracker"};
37
+ var cost_tracker_default = plugin9;
38
+ ({
39
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
40
+ });
41
+ var plugin10 = {
42
+ name: "cron"};
43
+ var cron_default = plugin10;
44
+ var plugin11 = {
45
+ name: "dep-guard"};
46
+ var dep_guard_default = plugin11;
47
+ var plugin12 = {
48
+ name: "diff-summary"};
49
+ var diff_summary_default = plugin12;
50
+ var plugin13 = {
51
+ name: "error-lens"};
52
+ var error_lens_default = plugin13;
53
+ var plugin14 = {
54
+ name: "file-watcher"};
55
+ var file_watcher_default = plugin14;
56
+ var plugin15 = {
57
+ name: "format-on-save"};
58
+ var format_on_save_default = plugin15;
59
+ var plugin16 = {
60
+ name: "git-autocommit"};
61
+ var git_autocommit_default = plugin16;
62
+ var plugin17 = {
63
+ name: "import-organizer"};
64
+ var import_organizer_default = plugin17;
65
+ var plugin18 = {
66
+ name: "injection-shield"};
67
+ var injection_shield_default = plugin18;
68
+ var plugin19 = {
69
+ name: "lint-gate"};
70
+ var lint_gate_default = plugin19;
71
+ var plugin20 = {
72
+ name: "llm-cache"};
73
+ var llm_cache_default = plugin20;
74
+ var plugin21 = {
75
+ name: "loop-breaker"};
76
+ var loop_breaker_default = plugin21;
77
+ var plugin22 = {
78
+ name: "model-router"};
79
+ var model_router_default = plugin22;
80
+ var plugin23 = {
81
+ name: "notify-hub"};
82
+ var notify_hub_default = plugin23;
83
+ var plugin24 = {
84
+ name: "path-guard"};
85
+ var path_guard_default = plugin24;
86
+ var plugin25 = {
87
+ name: "prompt-firewall"};
88
+ var prompt_firewall_default = plugin25;
89
+
90
+ // src/secret-scanner/index.ts
91
+ var BASE_PATTERNS = [
92
+ // LLM provider keys
93
+ { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
94
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
95
+ // GitHub
96
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
97
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
98
+ // AWS
99
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
100
+ // GCP
101
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
102
+ // Slack
103
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
104
+ // Stripe
105
+ { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
106
+ // Twilio
107
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
108
+ // Telegram
109
+ {
110
+ type: "telegram_bot_token",
111
+ regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
112
+ },
113
+ // JWT
114
+ {
115
+ type: "jwt",
116
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
117
+ },
118
+ // Private keys
119
+ {
120
+ type: "private_key",
121
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----(?!\S)/g
122
+ },
123
+ // AI/ML provider tokens
124
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
125
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
126
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
127
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
128
+ // Bearer tokens
129
+ {
130
+ type: "bearer_token",
131
+ regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
132
+ },
133
+ // Database URIs
134
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
135
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
136
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
137
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g }
138
+ ];
139
+ var PATTERNS3 = [...BASE_PATTERNS];
140
+ buildCombinedRegex(PATTERNS3);
141
+ function buildCombinedRegex(patterns) {
142
+ return new RegExp(
143
+ patterns.map((p) => `(${p.regex.source})`).join("|"),
144
+ "g"
145
+ );
146
+ }
147
+ var plugin26 = {
148
+ name: "secret-scanner"};
149
+ var secret_scanner_default = plugin26;
150
+ var plugin27 = {
151
+ name: "semver-bump"};
152
+ var semver_bump_default = plugin27;
153
+ var plugin28 = {
154
+ name: "session-recap"};
155
+ var session_recap_default = plugin28;
156
+ var plugin29 = {
157
+ name: "shell-check"};
158
+ var shell_check_default = plugin29;
159
+ var plugin30 = {
160
+ name: "template-engine"};
161
+ var template_engine_default = plugin30;
162
+ var plugin31 = {
163
+ name: "test-runner-gate"};
164
+ var test_runner_gate_default = plugin31;
165
+ var plugin32 = {
166
+ name: "todo-listener"};
167
+ var todo_listener_default = plugin32;
168
+ var plugin33 = {
169
+ name: "todo-tracker"};
170
+ var todo_tracker_default = plugin33;
171
+ var plugin34 = {
172
+ name: "token-budget"};
173
+ var token_budget_default = plugin34;
174
+ var plugin35 = {
175
+ name: "token-throttle"};
176
+ var token_throttle_default = plugin35;
177
+
178
+ // src/catalog.ts
179
+ var ENTRIES = [
180
+ { name: auto_doc_default.name, path: "./src/auto-doc" },
181
+ { name: git_autocommit_default.name, path: "./src/git-autocommit" },
182
+ { name: shell_check_default.name, path: "./src/shell-check" },
183
+ { name: cost_tracker_default.name, path: "./src/cost-tracker" },
184
+ { name: file_watcher_default.name, path: "./src/file-watcher" },
185
+ { name: cron_default.name, path: "./src/cron" },
186
+ { name: template_engine_default.name, path: "./src/template-engine" },
187
+ { name: semver_bump_default.name, path: "./src/semver-bump" },
188
+ { name: secret_scanner_default.name, path: "./src/secret-scanner" },
189
+ { name: todo_tracker_default.name, path: "./src/todo-tracker" },
190
+ { name: token_budget_default.name, path: "./src/token-budget" },
191
+ { name: lint_gate_default.name, path: "./src/lint-gate" },
192
+ { name: branch_guard_default.name, path: "./src/branch-guard" },
193
+ { name: diff_summary_default.name, path: "./src/diff-summary" },
194
+ { name: commit_validator_default.name, path: "./src/commit-validator" },
195
+ { name: format_on_save_default.name, path: "./src/format-on-save" },
196
+ { name: test_runner_gate_default.name, path: "./src/test-runner-gate" },
197
+ { name: import_organizer_default.name, path: "./src/import-organizer" },
198
+ { name: todo_listener_default.name, path: "./src/todo-listener" },
199
+ { name: session_recap_default.name, path: "./src/session-recap" },
200
+ { name: "spec-linker", path: "./src/spec-linker" },
201
+ { name: loop_breaker_default.name, path: "./src/loop-breaker" },
202
+ { name: path_guard_default.name, path: "./src/path-guard" },
203
+ { name: context_pins_default.name, path: "./src/context-pins" },
204
+ { name: checkpoint_default.name, path: "./src/checkpoint" },
205
+ { name: error_lens_default.name, path: "./src/error-lens" },
206
+ { name: dep_guard_default.name, path: "./src/dep-guard" },
207
+ { name: config_validator_default.name, path: "./src/config-validator" },
208
+ { name: notify_hub_default.name, path: "./src/notify-hub" },
209
+ { name: changelog_writer_default.name, path: "./src/changelog-writer" },
210
+ { name: injection_shield_default.name, path: "./src/injection-shield" },
211
+ { name: llm_cache_default.name, path: "./src/llm-cache" },
212
+ { name: model_router_default.name, path: "./src/model-router" },
213
+ { name: prompt_firewall_default.name, path: "./src/prompt-firewall" },
214
+ { name: auto_escalate_default.name, path: "./src/auto-escalate" },
215
+ { name: token_throttle_default.name, path: "./src/token-throttle" }
216
+ ];
217
+ function assertValidCatalog(entries) {
218
+ for (const e of entries) {
219
+ if (typeof e.name !== "string" || e.name.length === 0) {
220
+ throw new Error(`plugin catalog: entry has invalid name: ${JSON.stringify(e)}`);
221
+ }
222
+ if (!/^[a-z0-9-]+$/.test(e.name)) {
223
+ throw new Error(`plugin catalog: name "${e.name}" is not kebab-case`);
224
+ }
225
+ }
226
+ const seen = /* @__PURE__ */ new Set();
227
+ for (const e of entries) {
228
+ if (seen.has(e.name)) {
229
+ throw new Error(`plugin catalog: duplicate entry for "${e.name}"`);
230
+ }
231
+ seen.add(e.name);
232
+ }
233
+ }
234
+ assertValidCatalog(ENTRIES);
235
+ var PLUGIN_CATALOG = (() => {
236
+ const m = /* @__PURE__ */ new Map();
237
+ for (const e of ENTRIES) m.set(e.name, e.path);
238
+ return m;
239
+ })();
240
+ var PLUGIN_CATALOG_ENTRIES = Object.freeze(
241
+ ENTRIES.map((e) => Object.freeze({ ...e }))
242
+ );
243
+ var PLUGIN_NAMES = PLUGIN_CATALOG_ENTRIES.map((e) => e.name);
244
+
245
+ // src/spec-linker/index.ts
246
+ var state32 = {
247
+ postInvocations: 0,
248
+ preInvocations: 0,
249
+ unlinkedCount: 0,
250
+ cleanCount: 0,
251
+ skippedNonMd: 0,
252
+ readErrorCount: 0,
253
+ autoFixApplied: 0,
254
+ postHookUnregister: null,
255
+ preHookUnregister: null
256
+ };
257
+ var DEFAULTS25 = {
258
+ enabled: true,
259
+ fileGlobs: ["**/*.md", "**/*.mdx"],
260
+ maxReferences: 8,
261
+ autoFix: false
262
+ };
263
+ function readConfig27(raw) {
264
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS25 };
265
+ const r = raw;
266
+ return {
267
+ enabled: r["enabled"] !== false,
268
+ fileGlobs: Array.isArray(r["fileGlobs"]) ? r["fileGlobs"].filter((g) => typeof g === "string") : DEFAULTS25.fileGlobs,
269
+ maxReferences: typeof r["maxReferences"] === "number" && r["maxReferences"] > 0 ? r["maxReferences"] : DEFAULTS25.maxReferences,
270
+ autoFix: r["autoFix"] === true
271
+ };
272
+ }
273
+ function fileMatchesGlobs(filePath, globs) {
274
+ const lower = filePath.toLowerCase();
275
+ return globs.some((g) => {
276
+ const normalized = g.replace(/\\/g, "/").toLowerCase();
277
+ const pattern = normalized.replace(/^\*\*\//, "");
278
+ if (pattern.startsWith("*.")) {
279
+ return lower.endsWith(pattern.slice(1));
280
+ }
281
+ return lower.includes(pattern);
282
+ });
283
+ }
284
+ function isWrappedAsLinkOrCode(line, name) {
285
+ const lower = line.toLowerCase();
286
+ const target = name.toLowerCase();
287
+ if (lower.includes(`[${target}](`)) return true;
288
+ if (lower.includes(`\`${target}\``)) return true;
289
+ if (lower.includes(`[\``) && lower.includes(`\`](`)) return true;
290
+ return false;
291
+ }
292
+ function escapeRegExp(s) {
293
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
294
+ }
295
+ function findUnlinkedReferences(lines, names) {
296
+ const found = /* @__PURE__ */ new Map();
297
+ for (const line of lines) {
298
+ if (line.length === 0) continue;
299
+ for (const name of names) {
300
+ const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
301
+ if (re.test(line) && !isWrappedAsLinkOrCode(line, name)) {
302
+ if (!found.has(name)) found.set(name, true);
303
+ }
304
+ }
305
+ }
306
+ return [...found.keys()];
307
+ }
308
+ function wrapUnlinkedReferences(content) {
309
+ const lines = content.split("\n");
310
+ let changed = false;
311
+ for (let i = 0; i < lines.length; i++) {
312
+ const line = lines[i];
313
+ if (line.length === 0) continue;
314
+ const newLine = wrapLineReferences(line);
315
+ if (newLine !== line) {
316
+ lines[i] = newLine;
317
+ changed = true;
318
+ }
319
+ }
320
+ return changed ? lines.join("\n") : content;
321
+ }
322
+ function wrapLineReferences(line) {
323
+ let out = "";
324
+ let cursor = 0;
325
+ const spans = [];
326
+ for (const name of PLUGIN_NAMES) {
327
+ const re = new RegExp(`(^|[^\\w-])(${escapeRegExp(name)})(?![\\w-])`, "gi");
328
+ let m;
329
+ re.lastIndex = 0;
330
+ while ((m = re.exec(line)) !== null) {
331
+ const leadingLen = m[1].length;
332
+ const nameStart = m.index + leadingLen;
333
+ const nameEnd = nameStart + m[2].length;
334
+ const originalName = line.slice(nameStart, nameEnd);
335
+ if (isWrappedAsLinkOrCode(line, originalName)) continue;
336
+ if (spans.some((s) => !(nameEnd <= s.start || nameStart >= s.end))) {
337
+ continue;
338
+ }
339
+ spans.push({ start: nameStart, end: nameEnd, name: originalName });
340
+ re.lastIndex = nameEnd;
341
+ }
342
+ }
343
+ if (spans.length === 0) return line;
344
+ spans.sort((a, b) => a.start - b.start);
345
+ for (const span of spans) {
346
+ out += line.slice(cursor, span.start);
347
+ const path2 = PLUGIN_CATALOG.get(span.name.toLowerCase()) ?? `./src/${span.name.toLowerCase()}`;
348
+ out += `[${span.name}](${path2})`;
349
+ cursor = span.end;
350
+ }
351
+ out += line.slice(cursor);
352
+ return out;
353
+ }
354
+ var plugin36 = {
355
+ name: "spec-linker",
356
+ version: "0.2.0",
357
+ description: "Markdown link auditor for plugin references. PostToolUse surfaces unlinked references; PreToolUse on `write` (autoFix) wraps them in markdown links via modifiedInput.",
358
+ apiVersion: "^0.1.10",
359
+ capabilities: { tools: true, hooks: true },
360
+ defaultConfig: { ...DEFAULTS25 },
361
+ configSchema: {
362
+ type: "object",
363
+ properties: {
364
+ enabled: { type: "boolean", default: true, description: "Master switch." },
365
+ fileGlobs: {
366
+ type: "array",
367
+ items: { type: "string" },
368
+ default: DEFAULTS25.fileGlobs,
369
+ description: "Glob patterns to match (markdown by default)."
370
+ },
371
+ maxReferences: {
372
+ type: "number",
373
+ minimum: 1,
374
+ maximum: 50,
375
+ default: 8,
376
+ description: "Hard cap on the number of unlinked references in the injected context."
377
+ },
378
+ autoFix: {
379
+ type: "boolean",
380
+ default: false,
381
+ description: "When true, the PreToolUse hook on `write` returns a `modifiedInput.content` where each unlinked plugin reference is wrapped in a markdown link. Default false (opt in)."
382
+ }
383
+ }
384
+ },
385
+ setup(api) {
386
+ state32.postInvocations = 0;
387
+ state32.preInvocations = 0;
388
+ state32.unlinkedCount = 0;
389
+ state32.cleanCount = 0;
390
+ state32.skippedNonMd = 0;
391
+ state32.readErrorCount = 0;
392
+ state32.autoFixApplied = 0;
393
+ state32.postHookUnregister = null;
394
+ state32.preHookUnregister = null;
395
+ const cfg = readConfig27(api.config.extensions?.["spec-linker"]);
396
+ const postHook = async (input) => {
397
+ if (!cfg.enabled) return;
398
+ if (input.toolResult?.isError) return;
399
+ const toolName = input.toolName ?? "";
400
+ if (toolName !== "write" && toolName !== "edit") return;
401
+ const inp = input.toolInput ?? {};
402
+ const filePath = inp.path;
403
+ if (!filePath || typeof filePath !== "string") return;
404
+ if (!fileMatchesGlobs(filePath, cfg.fileGlobs)) {
405
+ state32.skippedNonMd += 1;
406
+ return;
407
+ }
408
+ state32.postInvocations += 1;
409
+ if (!existsSync(filePath)) return;
410
+ let content;
411
+ try {
412
+ const stat = statSync(filePath);
413
+ if (!stat.isFile()) return;
414
+ content = await fsp.readFile(filePath, "utf-8");
415
+ } catch {
416
+ state32.readErrorCount += 1;
417
+ return;
418
+ }
419
+ const unlinked = findUnlinkedReferences(content.split("\n"), [...PLUGIN_NAMES]);
420
+ if (unlinked.length === 0) {
421
+ state32.cleanCount += 1;
422
+ return;
423
+ }
424
+ state32.unlinkedCount += 1;
425
+ const limited = unlinked.slice(0, cfg.maxReferences);
426
+ const overflow = unlinked.length - limited.length;
427
+ const lines = limited.map((name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``).join("\n");
428
+ const overflowNote = overflow > 0 ? `
429
+ - \u2026and ${overflow} more` : "";
430
+ return {
431
+ additionalContext: `
432
+ \u{1F517} spec-linker: ${unlinked.length} unlinked plugin reference(s) in '${filePath}'. Consider wrapping them in markdown links to keep the docs navigable:
433
+ ${lines}${overflowNote}`
434
+ };
435
+ };
436
+ state32.postHookUnregister = api.registerHook("PostToolUse", "write|edit", postHook);
437
+ if (cfg.autoFix) {
438
+ const preHook = async (input) => {
439
+ if (!cfg.enabled) return;
440
+ if (input.toolName !== "write") return;
441
+ const inp = input.toolInput ?? {};
442
+ const filePath = inp.path;
443
+ if (!filePath || typeof filePath !== "string") return;
444
+ if (!fileMatchesGlobs(filePath, cfg.fileGlobs)) return;
445
+ if (typeof inp.content !== "string" || inp.content.length === 0) return;
446
+ state32.preInvocations += 1;
447
+ const fixed = wrapUnlinkedReferences(inp.content);
448
+ if (fixed === inp.content) return;
449
+ state32.autoFixApplied += 1;
450
+ return {
451
+ decision: "allow",
452
+ modifiedInput: { ...inp, content: fixed, path: filePath },
453
+ additionalContext: `
454
+ \u{1F517} spec-linker (autoFix): wrapped unlinked plugin reference(s) in '${filePath}'.`
455
+ };
456
+ };
457
+ state32.preHookUnregister = api.registerHook("PreToolUse", "write", preHook);
458
+ }
459
+ api.tools.register({
460
+ name: "spec_linker_status",
461
+ description: "Reports spec-linker state: config, counters (post + pre hooks), and the canonical plugin catalog used for detection.",
462
+ inputSchema: { type: "object", properties: {} },
463
+ permission: "auto",
464
+ category: "Diagnostics",
465
+ mutating: false,
466
+ async execute() {
467
+ return {
468
+ ok: true,
469
+ enabled: cfg.enabled,
470
+ fileGlobs: cfg.fileGlobs,
471
+ maxReferences: cfg.maxReferences,
472
+ autoFix: cfg.autoFix,
473
+ counters: {
474
+ postInvocations: state32.postInvocations,
475
+ preInvocations: state32.preInvocations,
476
+ unlinked: state32.unlinkedCount,
477
+ clean: state32.cleanCount,
478
+ skippedNonMd: state32.skippedNonMd,
479
+ readErrors: state32.readErrorCount,
480
+ autoFixApplied: state32.autoFixApplied
481
+ },
482
+ catalogSize: PLUGIN_NAMES.length
483
+ };
484
+ }
485
+ });
486
+ api.log.info("spec-linker plugin loaded", {
487
+ version: "0.2.0",
488
+ enabled: cfg.enabled,
489
+ fileGlobs: cfg.fileGlobs,
490
+ autoFix: cfg.autoFix,
491
+ catalogSize: PLUGIN_NAMES.length
492
+ });
493
+ },
494
+ teardown(api) {
495
+ for (const off of [state32.postHookUnregister, state32.preHookUnregister]) {
496
+ if (off) {
497
+ try {
498
+ off();
499
+ } catch {
500
+ }
501
+ }
502
+ }
503
+ state32.postHookUnregister = null;
504
+ state32.preHookUnregister = null;
505
+ const final = {
506
+ postInvocations: state32.postInvocations,
507
+ preInvocations: state32.preInvocations,
508
+ unlinked: state32.unlinkedCount,
509
+ clean: state32.cleanCount,
510
+ skippedNonMd: state32.skippedNonMd,
511
+ readErrors: state32.readErrorCount,
512
+ autoFixApplied: state32.autoFixApplied
513
+ };
514
+ state32.postInvocations = 0;
515
+ state32.preInvocations = 0;
516
+ state32.unlinkedCount = 0;
517
+ state32.cleanCount = 0;
518
+ state32.skippedNonMd = 0;
519
+ state32.readErrorCount = 0;
520
+ state32.autoFixApplied = 0;
521
+ api.log.info("spec-linker: teardown complete", { final });
522
+ },
523
+ async health() {
524
+ return {
525
+ ok: true,
526
+ message: `spec-linker: post=${state32.postInvocations} pre=${state32.preInvocations}, unlinked=${state32.unlinkedCount}, autoFix=${state32.autoFixApplied}, clean=${state32.cleanCount}, non-md=${state32.skippedNonMd}`,
527
+ counters: {
528
+ postInvocations: state32.postInvocations,
529
+ preInvocations: state32.preInvocations,
530
+ unlinked: state32.unlinkedCount,
531
+ clean: state32.cleanCount,
532
+ skippedNonMd: state32.skippedNonMd,
533
+ readErrors: state32.readErrorCount,
534
+ autoFixApplied: state32.autoFixApplied
535
+ }
536
+ };
537
+ }
538
+ };
539
+ var spec_linker_default = plugin36;
540
+
541
+ export { spec_linker_default as default };
@@ -2,6 +2,7 @@ import { isAbsolute } from 'path';
2
2
 
3
3
  // src/template-engine/index.ts
4
4
  var API_VERSION = "^0.1.10";
5
+ var templates = /* @__PURE__ */ new Map();
5
6
  function expandTemplate(template, variables) {
6
7
  let result = template;
7
8
  result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
@@ -71,7 +72,7 @@ var plugin = {
71
72
  }
72
73
  },
73
74
  setup(api) {
74
- const templates = /* @__PURE__ */ new Map();
75
+ templates.clear();
75
76
  const autoEscapeHtml = api.config.extensions?.["template-engine"]?.["autoEscapeHtml"] ?? true;
76
77
  api.tools.register({
77
78
  name: "template_expand",
@@ -270,6 +271,23 @@ var plugin = {
270
271
  }
271
272
  ]);
272
273
  api.log.info("template-engine plugin loaded", { version: "0.1.0" });
274
+ },
275
+ teardown(api) {
276
+ const count = templates.size;
277
+ templates.clear();
278
+ api.log.info("template-engine: teardown complete", { cleared: count });
279
+ },
280
+ async health() {
281
+ let totalBytes = 0;
282
+ for (const t of templates.values()) {
283
+ totalBytes += t.content.length;
284
+ }
285
+ return {
286
+ ok: true,
287
+ message: `template-engine: ${templates.size} saved template(s), ${totalBytes} bytes total`,
288
+ count: templates.size,
289
+ totalBytes
290
+ };
273
291
  }
274
292
  };
275
293
  var template_engine_default = plugin;
@@ -0,0 +1,37 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * test-runner-gate plugin — PostToolUse hook that runs the relevant
5
+ * test file after every `write` or `edit` to a source file.
6
+ *
7
+ * Tools registered:
8
+ * - test_gate_status : Show config + per-session counters.
9
+ *
10
+ * Hooks registered:
11
+ * - PostToolUse with matcher `write|edit`. After the tool completes,
12
+ * maps the changed source file to its test file (using configurable
13
+ * patterns), runs `vitest run <test-file>` and injects the result
14
+ * as `additionalContext`.
15
+ *
16
+ * Config (`config.extensions['test-runner-gate']`):
17
+ *
18
+ * ```jsonc
19
+ * {
20
+ * "enabled": true,
21
+ * "command": "npx vitest run", // base test command
22
+ * "timeoutMs": 30000, // test process timeout
23
+ * "testFilePatterns": [ // how to derive test path from source
24
+ * "src/{path}.test.ts", // co-located: src/foo.ts → src/foo.test.ts
25
+ * "tests/{name}.test.ts", // mirror dir: src/foo.ts → tests/foo.test.ts
26
+ * "tests/{name}-exec.test.ts" // exec variant
27
+ * ],
28
+ * "injectOnPass": false // inject context when tests pass too?
29
+ * }
30
+ * ```
31
+ *
32
+ * @public
33
+ */
34
+
35
+ declare const plugin: Plugin;
36
+
37
+ export { plugin as default };