@wrongstack/plugins 0.277.1 → 0.280.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
@@ -0,0 +1,409 @@
1
+ // src/secret-scanner/index.ts
2
+ var BASE_PATTERNS = [
3
+ // LLM provider keys
4
+ { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
5
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
6
+ // GitHub
7
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
8
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
9
+ // AWS
10
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
11
+ // GCP
12
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
13
+ // Slack
14
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
15
+ // Stripe
16
+ { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
17
+ // Twilio
18
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
19
+ // Telegram
20
+ {
21
+ type: "telegram_bot_token",
22
+ regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
23
+ },
24
+ // JWT
25
+ {
26
+ type: "jwt",
27
+ 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
28
+ },
29
+ // Private keys
30
+ {
31
+ type: "private_key",
32
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----(?!\S)/g
33
+ },
34
+ // AI/ML provider tokens
35
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
36
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
37
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
38
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
39
+ // Bearer tokens
40
+ {
41
+ type: "bearer_token",
42
+ regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
43
+ },
44
+ // Database URIs
45
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
46
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
47
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
48
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g }
49
+ ];
50
+ var PATTERNS = [...BASE_PATTERNS];
51
+ var COMBINED_REGEX = buildCombinedRegex(PATTERNS);
52
+ function buildCombinedRegex(patterns) {
53
+ return new RegExp(
54
+ patterns.map((p) => `(${p.regex.source})`).join("|"),
55
+ "g"
56
+ );
57
+ }
58
+ var state = {
59
+ blockCount: 0,
60
+ redactCount: 0,
61
+ allowCount: 0,
62
+ /** PostToolUse: secrets detected in tool output. */
63
+ leakCount: 0,
64
+ /** Most recent PreToolUse block — surfaced by `secret_scanner_status`. */
65
+ lastBlock: null,
66
+ /** Most recent PostToolUse leak — surfaced by `secret_scanner_status`. */
67
+ lastLeak: null,
68
+ /** PreToolUse hook handle so teardown can unregister. */
69
+ hookUnregister: null,
70
+ /** PostToolUse hook handle so teardown can unregister. */
71
+ postHookUnregister: null
72
+ };
73
+ function findMatches(text) {
74
+ if (!text) return [];
75
+ const found = /* @__PURE__ */ new Set();
76
+ COMBINED_REGEX.lastIndex = 0;
77
+ let m;
78
+ while ((m = COMBINED_REGEX.exec(text)) !== null) {
79
+ for (let i = 0; i < PATTERNS.length; i++) {
80
+ if (m[i + 1] !== void 0) {
81
+ found.add(PATTERNS[i].type);
82
+ break;
83
+ }
84
+ }
85
+ if (m.index === COMBINED_REGEX.lastIndex) {
86
+ COMBINED_REGEX.lastIndex += 1;
87
+ }
88
+ }
89
+ return Array.from(found);
90
+ }
91
+ function scanInput(input) {
92
+ if (input === null || input === void 0) return null;
93
+ if (typeof input === "string") {
94
+ const found = findMatches(input);
95
+ return found.length > 0 ? found : null;
96
+ }
97
+ if (Array.isArray(input)) {
98
+ for (const item of input) {
99
+ const found = scanInput(item);
100
+ if (found) return found;
101
+ }
102
+ return null;
103
+ }
104
+ if (typeof input === "object") {
105
+ for (const value of Object.values(input)) {
106
+ const found = scanInput(value);
107
+ if (found) return found;
108
+ }
109
+ return null;
110
+ }
111
+ return null;
112
+ }
113
+ function redactInput(input) {
114
+ if (input === null || input === void 0) return input;
115
+ if (typeof input === "string") {
116
+ return input.replace(COMBINED_REGEX, (_match, ...groups) => {
117
+ for (let i = 0; i < PATTERNS.length; i++) {
118
+ if (groups[i] !== void 0) {
119
+ return `[REDACTED:${PATTERNS[i].type}]`;
120
+ }
121
+ }
122
+ return "[REDACTED:unknown]";
123
+ });
124
+ }
125
+ if (Array.isArray(input)) {
126
+ return input.map((item) => redactInput(item));
127
+ }
128
+ if (typeof input === "object") {
129
+ const out = {};
130
+ for (const [k, v] of Object.entries(input)) {
131
+ out[k] = redactInput(v);
132
+ }
133
+ return out;
134
+ }
135
+ return input;
136
+ }
137
+ var DEFAULTS = {
138
+ matcher: "bash|write|edit",
139
+ postToolUseMatcher: "*",
140
+ mode: "block",
141
+ enabled: true,
142
+ customPatterns: []
143
+ };
144
+ function readConfig(raw) {
145
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
146
+ const r = raw;
147
+ const mode = r["mode"] === "redact" || r["mode"] === "allow" ? r["mode"] : "block";
148
+ const customPatterns = [];
149
+ if (Array.isArray(r["customPatterns"])) {
150
+ for (const entry of r["customPatterns"]) {
151
+ if (!entry || typeof entry !== "object") continue;
152
+ const e = entry;
153
+ const type = e["type"];
154
+ const regex = e["regex"];
155
+ if (typeof type !== "string" || typeof regex !== "string") continue;
156
+ try {
157
+ new RegExp(regex, "g");
158
+ } catch {
159
+ continue;
160
+ }
161
+ customPatterns.push({ type, regex, description: typeof e["description"] === "string" ? e["description"] : void 0 });
162
+ }
163
+ }
164
+ return {
165
+ matcher: typeof r["matcher"] === "string" ? r["matcher"] : DEFAULTS.matcher,
166
+ postToolUseMatcher: typeof r["postToolUseMatcher"] === "string" ? r["postToolUseMatcher"] : DEFAULTS.postToolUseMatcher,
167
+ mode,
168
+ enabled: r["enabled"] !== false,
169
+ customPatterns
170
+ };
171
+ }
172
+ function buildHook(cfg, log) {
173
+ return (input) => {
174
+ if (!cfg.enabled) return;
175
+ const toolName = input.toolName ?? "unknown";
176
+ const matched = scanInput(input.toolInput);
177
+ if (!matched) return;
178
+ const summary = matched.join(", ");
179
+ const when = (/* @__PURE__ */ new Date()).toISOString();
180
+ if (cfg.mode === "block") {
181
+ state.blockCount += 1;
182
+ state.lastBlock = { toolName, matchedTypes: matched, when };
183
+ log.warn(
184
+ `[secret-scanner] blocked ${toolName} \u2014 matched: ${summary}`
185
+ );
186
+ return {
187
+ decision: "block",
188
+ reason: `secret-scanner: refused to run '${toolName}' because the arguments appear to contain plaintext credentials (${summary}). Move the secret to a secret manager, env var, or config file and re-issue the call.`
189
+ };
190
+ }
191
+ if (cfg.mode === "redact") {
192
+ const redacted = redactInput(input.toolInput);
193
+ if (redacted !== null && typeof redacted === "object" && !Array.isArray(redacted)) {
194
+ state.redactCount += 1;
195
+ log.info(
196
+ `[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`
197
+ );
198
+ return {
199
+ decision: "allow",
200
+ modifiedInput: redacted,
201
+ additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
202
+ };
203
+ }
204
+ state.blockCount += 1;
205
+ state.lastBlock = { toolName, matchedTypes: matched, when };
206
+ return {
207
+ decision: "block",
208
+ reason: `secret-scanner: cannot safely redact '${toolName}' input (non-object shape); refusing to run.`
209
+ };
210
+ }
211
+ state.allowCount += 1;
212
+ log.warn(
213
+ `[secret-scanner] allow-mode: ${toolName} matched ${summary} but mode='allow' lets it through.`
214
+ );
215
+ return void 0;
216
+ };
217
+ }
218
+ function buildPostHook(cfg, log) {
219
+ return (input) => {
220
+ if (!cfg.enabled) return;
221
+ const result = input.toolResult;
222
+ if (!result || typeof result.content !== "string") return;
223
+ const matched = findMatches(result.content);
224
+ if (matched.length === 0) return;
225
+ const toolName = input.toolName ?? "unknown";
226
+ const summary = matched.join(", ");
227
+ const when = (/* @__PURE__ */ new Date()).toISOString();
228
+ state.leakCount += 1;
229
+ state.lastLeak = { toolName, matchedTypes: matched, when };
230
+ log.warn(
231
+ `[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`
232
+ );
233
+ return {
234
+ additionalContext: `
235
+ \u26A0\uFE0F secret-scanner: the output of '${toolName}' contains what appears to be plaintext credential(s) (${summary}). Do NOT echo, store, commit, or transmit this value. Treat it as compromised and advise the user to rotate it.`
236
+ };
237
+ };
238
+ }
239
+ var plugin = {
240
+ name: "secret-scanner",
241
+ version: "0.1.0",
242
+ description: "Pre-tool hook that blocks (or optionally redacts) tools whose arguments contain plaintext credentials",
243
+ apiVersion: "^0.1.10",
244
+ capabilities: { tools: true, hooks: true },
245
+ defaultConfig: { ...DEFAULTS },
246
+ configSchema: {
247
+ type: "object",
248
+ properties: {
249
+ matcher: {
250
+ type: "string",
251
+ description: 'PreToolUse: Tool-name matcher (pipe-delimited case-insensitive, or "*")'
252
+ },
253
+ postToolUseMatcher: {
254
+ type: "string",
255
+ default: "*",
256
+ description: 'PostToolUse: Tool-name matcher for output leak detection. Default "*" scans all tool outputs.'
257
+ },
258
+ mode: {
259
+ type: "string",
260
+ enum: ["block", "redact", "allow"],
261
+ description: 'PreToolUse action on a match: "block" refuses the tool call, "redact" rewrites the input with [REDACTED:type], "allow" only logs'
262
+ },
263
+ enabled: { type: "boolean", default: true },
264
+ customPatterns: {
265
+ type: "array",
266
+ description: "User-supplied custom credential patterns. Each entry is { type: string, regex: string, description?: string }. Appended to the 20 built-in patterns at setup() time.",
267
+ items: {
268
+ type: "object",
269
+ properties: {
270
+ type: { type: "string", description: "Unique identifier (used in block reason + [REDACTED:type] label)" },
271
+ regex: { type: "string", description: "Regex source string (without /\u2026/g delimiters). Must be a valid JS regex." },
272
+ description: { type: "string", description: "Optional human-readable description" }
273
+ },
274
+ required: ["type", "regex"]
275
+ },
276
+ default: []
277
+ }
278
+ }
279
+ },
280
+ setup(api) {
281
+ state.blockCount = 0;
282
+ state.redactCount = 0;
283
+ state.allowCount = 0;
284
+ state.leakCount = 0;
285
+ state.lastBlock = null;
286
+ state.lastLeak = null;
287
+ state.hookUnregister = null;
288
+ state.postHookUnregister = null;
289
+ const cfg = readConfig(api.config.extensions?.["secret-scanner"]);
290
+ PATTERNS = [...BASE_PATTERNS];
291
+ for (const cp of cfg.customPatterns) {
292
+ try {
293
+ PATTERNS.push({ type: cp.type, regex: new RegExp(cp.regex, "g") });
294
+ } catch {
295
+ }
296
+ }
297
+ COMBINED_REGEX = buildCombinedRegex(PATTERNS);
298
+ const log = {
299
+ warn: (msg, ...rest) => api.log.warn(msg, ...rest),
300
+ info: (msg, ...rest) => api.log.info(msg, ...rest)
301
+ };
302
+ const hook = buildHook(cfg, log);
303
+ state.hookUnregister = api.registerHook("PreToolUse", cfg.matcher, hook);
304
+ const postHook = buildPostHook(cfg, log);
305
+ state.postHookUnregister = api.registerHook("PostToolUse", cfg.postToolUseMatcher, postHook);
306
+ api.tools.register({
307
+ name: "secret_scanner_status",
308
+ description: "Reports the current secret-scanner state: pattern count, last block (if any), and per-mode invocation counters.",
309
+ inputSchema: { type: "object", properties: {} },
310
+ permission: "auto",
311
+ mutating: false,
312
+ async execute() {
313
+ return {
314
+ ok: true,
315
+ enabled: cfg.enabled,
316
+ mode: cfg.mode,
317
+ matcher: cfg.matcher,
318
+ postToolUseMatcher: cfg.postToolUseMatcher,
319
+ patternCount: PATTERNS.length,
320
+ patternTypes: PATTERNS.map((p) => p.type),
321
+ counters: {
322
+ block: state.blockCount,
323
+ redact: state.redactCount,
324
+ allow: state.allowCount,
325
+ leak: state.leakCount
326
+ },
327
+ lastBlock: state.lastBlock,
328
+ lastLeak: state.lastLeak
329
+ };
330
+ }
331
+ });
332
+ api.tools.register({
333
+ name: "secret_scanner_test",
334
+ description: "Run the scanner against a user-supplied string and report which patterns matched. Useful for verifying config and tuning the matcher.",
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ text: { type: "string", description: "Text to scan for credential patterns" }
339
+ },
340
+ required: ["text"]
341
+ },
342
+ permission: "auto",
343
+ mutating: false,
344
+ async execute(input) {
345
+ const text = typeof input["text"] === "string" ? input["text"] : "";
346
+ const matched = findMatches(text);
347
+ return {
348
+ ok: true,
349
+ matched,
350
+ count: matched.length
351
+ };
352
+ }
353
+ });
354
+ api.log.info("secret-scanner plugin loaded", {
355
+ version: "0.1.0",
356
+ mode: cfg.mode,
357
+ matcher: cfg.matcher,
358
+ patterns: PATTERNS.length
359
+ });
360
+ },
361
+ teardown(api) {
362
+ if (state.hookUnregister) {
363
+ try {
364
+ state.hookUnregister();
365
+ } catch {
366
+ }
367
+ state.hookUnregister = null;
368
+ }
369
+ if (state.postHookUnregister) {
370
+ try {
371
+ state.postHookUnregister();
372
+ } catch {
373
+ }
374
+ state.postHookUnregister = null;
375
+ }
376
+ const finalCounters = {
377
+ block: state.blockCount,
378
+ redact: state.redactCount,
379
+ allow: state.allowCount,
380
+ leak: state.leakCount
381
+ };
382
+ state.blockCount = 0;
383
+ state.redactCount = 0;
384
+ state.allowCount = 0;
385
+ state.leakCount = 0;
386
+ state.lastBlock = null;
387
+ state.lastLeak = null;
388
+ PATTERNS = [...BASE_PATTERNS];
389
+ COMBINED_REGEX = buildCombinedRegex(PATTERNS);
390
+ api.log.info("secret-scanner: teardown complete", { counters: finalCounters });
391
+ },
392
+ async health() {
393
+ return {
394
+ ok: true,
395
+ message: state.lastLeak !== null ? `secret-scanner: last leak at ${state.lastLeak.when} on ${state.lastLeak.toolName} (${state.lastLeak.matchedTypes.join(", ")})` : state.lastBlock !== null ? `secret-scanner: last block at ${state.lastBlock.when} on ${state.lastBlock.toolName} (${state.lastBlock.matchedTypes.join(", ")})` : `secret-scanner: ${state.blockCount + state.redactCount + state.allowCount + state.leakCount} invocations, no blocks or leaks`,
396
+ counters: {
397
+ block: state.blockCount,
398
+ redact: state.redactCount,
399
+ allow: state.allowCount,
400
+ leak: state.leakCount
401
+ },
402
+ lastBlock: state.lastBlock,
403
+ lastLeak: state.lastLeak
404
+ };
405
+ }
406
+ };
407
+ var secret_scanner_default = plugin;
408
+
409
+ export { secret_scanner_default as default };
@@ -6,6 +6,14 @@ import { join } from 'path';
6
6
 
7
7
  // src/semver-bump/index.ts
8
8
  var API_VERSION = "^0.1.10";
9
+ var state = {
10
+ /** Total invocations across all three tools this session. */
11
+ invocationCount: 0,
12
+ /** Per-tool invocation counts so /diag can show "bumps: 2, current: 5". */
13
+ perTool: { semver_bump: 0, semver_current: 0, semver_changelog: 0 },
14
+ /** Most recent bump result, surfaced by health() (null until first call). */
15
+ lastBump: null
16
+ };
9
17
  function runGit(args, cwd) {
10
18
  try {
11
19
  return execFileSync("git", args, {
@@ -169,6 +177,9 @@ var plugin = {
169
177
  }
170
178
  },
171
179
  setup(api) {
180
+ state.invocationCount = 0;
181
+ state.perTool = { semver_bump: 0, semver_current: 0, semver_changelog: 0 };
182
+ state.lastBump = null;
172
183
  const tagPrefix = api.config.extensions?.["semver-bump"]?.["tagPrefix"] ?? "v";
173
184
  const autoTag = api.config.extensions?.["semver-bump"]?.["autoTag"] ?? true;
174
185
  const VALID_PARTS = ["major", "minor", "patch", "auto"];
@@ -263,6 +274,14 @@ var plugin = {
263
274
  to: newVersion,
264
275
  bump: bumpPart
265
276
  });
277
+ state.lastBump = {
278
+ when: (/* @__PURE__ */ new Date()).toISOString(),
279
+ from: currentVersion,
280
+ to: newVersion,
281
+ type: bumpPart,
282
+ commitCount: commits.length,
283
+ breakingCount: commits.filter((c) => c.breaking).length
284
+ };
266
285
  return {
267
286
  ok: true,
268
287
  currentVersion,
@@ -286,6 +305,8 @@ var plugin = {
286
305
  permission: "confirm",
287
306
  mutating: true,
288
307
  async execute(input) {
308
+ state.invocationCount += 1;
309
+ state.perTool["semver_bump"] = (state.perTool["semver_bump"] ?? 0) + 1;
289
310
  const cwd = input["cwd"];
290
311
  const dryRun = input["dry_run"] ?? false;
291
312
  const part = input["part"] ?? defaultPart;
@@ -358,6 +379,8 @@ var plugin = {
358
379
  permission: "auto",
359
380
  mutating: false,
360
381
  async execute(input) {
382
+ state.invocationCount += 1;
383
+ state.perTool["semver_current"] = (state.perTool["semver_current"] ?? 0) + 1;
361
384
  const cwd = input["cwd"];
362
385
  const pkg = getPackageJson(cwd);
363
386
  const currentVersion = pkg?.version ?? "unknown";
@@ -397,6 +420,8 @@ var plugin = {
397
420
  permission: "auto",
398
421
  mutating: false,
399
422
  async execute(input) {
423
+ state.invocationCount += 1;
424
+ state.perTool["semver_changelog"] = (state.perTool["semver_changelog"] ?? 0) + 1;
400
425
  const from = input["from"];
401
426
  const to = input["to"] ?? "HEAD";
402
427
  const cwd = input["cwd"];
@@ -435,6 +460,26 @@ var plugin = {
435
460
  }
436
461
  });
437
462
  api.log.info("semver-bump plugin loaded", { version: "0.1.0", tagPrefix, autoTag });
463
+ },
464
+ teardown(api) {
465
+ const finalTotal = state.invocationCount;
466
+ const finalPerTool = { ...state.perTool };
467
+ state.invocationCount = 0;
468
+ state.perTool = { semver_bump: 0, semver_current: 0, semver_changelog: 0 };
469
+ state.lastBump = null;
470
+ api.log.info("semver-bump: teardown complete", {
471
+ invocations: finalTotal,
472
+ perTool: finalPerTool
473
+ });
474
+ },
475
+ async health() {
476
+ return {
477
+ ok: true,
478
+ message: state.lastBump === null ? `semver-bump: ${state.invocationCount} call(s) this session` : `semver-bump: last bump ${state.lastBump.from} \u2192 ${state.lastBump.to} (${state.lastBump.type}) at ${state.lastBump.when}`,
479
+ invocationCount: state.invocationCount,
480
+ perTool: { ...state.perTool },
481
+ lastBump: state.lastBump
482
+ };
438
483
  }
439
484
  };
440
485
  var semver_bump_default = plugin;
@@ -0,0 +1,50 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * session-recap plugin — Stop hook that posts a one-page summary
5
+ * of the session to the project mailbox when the agent loop ends.
6
+ *
7
+ * As the session runs, the plugin accumulates lightweight metrics
8
+ * from the agent's EventBus:
9
+ *
10
+ * - Total input/output tokens (per model)
11
+ * - Tool-call counts (per tool name)
12
+ * - Git commits made during the session
13
+ * - Elapsed wall-clock time
14
+ *
15
+ * On `Stop`, the hook reads `api.session.transcriptPath` (the JSONL
16
+ * session log) for any extra detail the metrics stream didn't catch
17
+ * — last user prompt, last assistant output, final todo state —
18
+ * then composes a compact summary and posts it to `api.mailbox.send`
19
+ * with `type: 'status'` and a high-signal subject.
20
+ *
21
+ * Use cases:
22
+ * - End-of-day handoff — a second agent opens the mailbox and
23
+ * sees what the previous session finished
24
+ * - Audit — every session leaves a breadcrumb
25
+ * - Shadow agents can monitor the recap stream for anomalies
26
+ *
27
+ * Config (`config.extensions['session-recap']`):
28
+ *
29
+ * ```jsonc
30
+ * {
31
+ * "enabled": true,
32
+ * "subjectPrefix": "session recap: ",
33
+ * "includeTranscriptTail": 3,
34
+ * "maxBodyChars": 8000
35
+ * }
36
+ * ```
37
+ *
38
+ * Host requirements:
39
+ * - Requires `api.mailbox` (added in commit 31dde5ba). When absent
40
+ * the hook logs a one-shot warn and silently no-ops.
41
+ * - Requires `api.session.transcriptPath` to read the JSONL.
42
+ * Minimal hosts without a session writer skip the transcript tail
43
+ * but still post the metrics summary.
44
+ *
45
+ * @public
46
+ */
47
+
48
+ declare const plugin: Plugin;
49
+
50
+ export { plugin as default };