@wrongstack/plugins 0.277.2 → 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,198 @@
1
+ // src/model-router/index.ts
2
+ var DEFAULTS = {
3
+ enabled: false,
4
+ dryRun: true,
5
+ rules: []
6
+ };
7
+ function readConfig(raw) {
8
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS, rules: [] };
9
+ const r = raw;
10
+ const rules = Array.isArray(r["rules"]) ? r["rules"].filter(
11
+ (x) => !!x && typeof x === "object" && typeof x.model === "string"
12
+ ).map((x) => ({
13
+ model: x["model"],
14
+ ...typeof x["maxChars"] === "number" ? { maxChars: x["maxChars"] } : {},
15
+ ...typeof x["minChars"] === "number" ? { minChars: x["minChars"] } : {},
16
+ ...typeof x["hasTools"] === "boolean" ? { hasTools: x["hasTools"] } : {}
17
+ })) : [];
18
+ return {
19
+ enabled: r["enabled"] === true,
20
+ dryRun: r["dryRun"] !== false,
21
+ rules
22
+ };
23
+ }
24
+ var state = {
25
+ invocations: 0,
26
+ routed: 0,
27
+ passthrough: 0,
28
+ routes: /* @__PURE__ */ new Map(),
29
+ extensionUnregister: null
30
+ };
31
+ function requestCharSize(request) {
32
+ let size = 0;
33
+ const walk = (v) => {
34
+ if (typeof v === "string") {
35
+ size += v.length;
36
+ } else if (Array.isArray(v)) {
37
+ for (const item of v) walk(item);
38
+ } else if (v && typeof v === "object") {
39
+ const o = v;
40
+ if (typeof o["text"] === "string") size += o["text"].length;
41
+ if (o["content"] !== void 0) walk(o["content"]);
42
+ }
43
+ };
44
+ walk(request["system"]);
45
+ walk(request["messages"]);
46
+ return size;
47
+ }
48
+ function pickRule(rules, size, hasTools) {
49
+ for (const rule of rules) {
50
+ if (rule.maxChars !== void 0 && size > rule.maxChars) continue;
51
+ if (rule.minChars !== void 0 && size < rule.minChars) continue;
52
+ if (rule.hasTools !== void 0 && rule.hasTools !== hasTools) continue;
53
+ return rule;
54
+ }
55
+ return null;
56
+ }
57
+ var plugin = {
58
+ name: "model-router",
59
+ version: "0.1.0",
60
+ description: "Routes each provider call to a different model by declarative size/tool rules (wrapProviderRunner). Opt-in; dry-run by default; routes within the active provider only.",
61
+ apiVersion: "^0.1.10",
62
+ capabilities: { tools: true },
63
+ defaultConfig: { ...DEFAULTS },
64
+ configSchema: {
65
+ type: "object",
66
+ properties: {
67
+ enabled: {
68
+ type: "boolean",
69
+ default: false,
70
+ description: "Master switch. OFF by default because rerouting changes which model serves each turn."
71
+ },
72
+ dryRun: {
73
+ type: "boolean",
74
+ default: true,
75
+ description: "Observe and count routing decisions without actually rewriting request.model."
76
+ },
77
+ rules: {
78
+ type: "array",
79
+ description: "Ordered rules. Each has an optional maxChars/minChars/hasTools condition and a required target model. First all-conditions-match wins.",
80
+ items: {
81
+ type: "object",
82
+ properties: {
83
+ maxChars: { type: "number", description: "Match when request char size <= this." },
84
+ minChars: { type: "number", description: "Match when request char size >= this." },
85
+ hasTools: { type: "boolean", description: "Match on tools present/absent." },
86
+ model: {
87
+ type: "string",
88
+ description: "Target model id (must be served by the active provider)."
89
+ }
90
+ },
91
+ required: ["model"]
92
+ }
93
+ }
94
+ }
95
+ },
96
+ setup(api) {
97
+ state.invocations = 0;
98
+ state.routed = 0;
99
+ state.passthrough = 0;
100
+ state.routes.clear();
101
+ if (state.extensionUnregister) {
102
+ try {
103
+ state.extensionUnregister();
104
+ } catch {
105
+ }
106
+ state.extensionUnregister = null;
107
+ }
108
+ const cfg = readConfig(api.config.extensions?.["model-router"]);
109
+ if (cfg.enabled && cfg.rules.length > 0) {
110
+ state.extensionUnregister = api.extensions.register({
111
+ name: "model-router",
112
+ owner: "model-router",
113
+ async wrapProviderRunner(_ctx, request, inner) {
114
+ const req = request ?? {};
115
+ state.invocations += 1;
116
+ const size = requestCharSize(req);
117
+ const hasTools = Array.isArray(req["tools"]) && req["tools"].length > 0;
118
+ const rule = pickRule(cfg.rules, size, hasTools);
119
+ const from = typeof req["model"] === "string" ? req["model"] : "unknown";
120
+ if (!rule || rule.model === from) {
121
+ state.passthrough += 1;
122
+ return inner(_ctx, request);
123
+ }
124
+ const routeKey = `${from}\u2192${rule.model}`;
125
+ state.routes.set(routeKey, (state.routes.get(routeKey) ?? 0) + 1);
126
+ state.routed += 1;
127
+ api.metrics.counter("routed", 1, { from, to: rule.model });
128
+ if (cfg.dryRun) {
129
+ return inner(_ctx, request);
130
+ }
131
+ return inner(_ctx, { ...req, model: rule.model });
132
+ }
133
+ });
134
+ }
135
+ api.tools.register({
136
+ name: "model_router_status",
137
+ description: "Reports model-router state: enabled, dryRun, rules, and per-route counts (from\u2192to).",
138
+ inputSchema: { type: "object", properties: {} },
139
+ permission: "auto",
140
+ category: "Diagnostics",
141
+ mutating: false,
142
+ async execute() {
143
+ return {
144
+ ok: true,
145
+ enabled: cfg.enabled,
146
+ dryRun: cfg.dryRun,
147
+ ruleCount: cfg.rules.length,
148
+ rules: cfg.rules,
149
+ counters: {
150
+ invocations: state.invocations,
151
+ routed: state.routed,
152
+ passthrough: state.passthrough
153
+ },
154
+ routes: Object.fromEntries(state.routes)
155
+ };
156
+ }
157
+ });
158
+ api.log.info("model-router plugin loaded", {
159
+ version: "0.1.0",
160
+ enabled: cfg.enabled,
161
+ dryRun: cfg.dryRun,
162
+ rules: cfg.rules.length
163
+ });
164
+ },
165
+ teardown(api) {
166
+ if (state.extensionUnregister) {
167
+ try {
168
+ state.extensionUnregister();
169
+ } catch {
170
+ }
171
+ state.extensionUnregister = null;
172
+ }
173
+ const final = {
174
+ invocations: state.invocations,
175
+ routed: state.routed,
176
+ passthrough: state.passthrough
177
+ };
178
+ state.invocations = 0;
179
+ state.routed = 0;
180
+ state.passthrough = 0;
181
+ state.routes.clear();
182
+ api.log.info("model-router: teardown complete", { final });
183
+ },
184
+ async health() {
185
+ return {
186
+ ok: true,
187
+ message: `model-router: ${state.routed} routed / ${state.passthrough} passthrough of ${state.invocations} call(s)`,
188
+ counters: {
189
+ invocations: state.invocations,
190
+ routed: state.routed,
191
+ passthrough: state.passthrough
192
+ }
193
+ };
194
+ }
195
+ };
196
+ var model_router_default = plugin;
197
+
198
+ export { model_router_default as default, pickRule, requestCharSize };
@@ -0,0 +1,45 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * notify-hub plugin — pushes session events to a webhook.
5
+ *
6
+ * Long-running autonomous sessions need a way to reach the user when
7
+ * something noteworthy happens and nobody is watching the terminal.
8
+ * notify-hub POSTs compact JSON payloads to a configurable webhook
9
+ * URL (Slack/Discord-compatible via generic JSON, n8n, ntfy, or any
10
+ * HTTP endpoint) for a configurable set of events:
11
+ *
12
+ * - `session.stop` — the agent loop ended (Stop hook)
13
+ * - `tool.error` — a tool invocation failed
14
+ * - `budget.threshold` — a budget threshold event fired on the bus
15
+ *
16
+ * Deliveries are fire-and-forget with a timeout — a dead webhook can
17
+ * never stall the agent. Failures are counted and reported via
18
+ * `notify_hub_status` / `health()`, and delivery stops trying after
19
+ * `maxConsecutiveFailures` (circuit breaker) until setup runs again.
20
+ *
21
+ * The agent can also send an ad-hoc notification with `notify_send`
22
+ * ("tell the user the migration finished").
23
+ *
24
+ * Config (`config.extensions['notify-hub']`):
25
+ *
26
+ * ```jsonc
27
+ * {
28
+ * "enabled": true,
29
+ * "webhookUrl": "", // empty = plugin idles
30
+ * "events": ["session.stop", "tool.error"],
31
+ * "headers": {}, // extra HTTP headers (auth…)
32
+ * "timeoutMs": 5000,
33
+ * "maxConsecutiveFailures": 5
34
+ * }
35
+ * ```
36
+ *
37
+ * Toggle off with `{ "name": "notify-hub", "enabled": false }` in
38
+ * `config.plugins`, or `"enabled": false` in the options above.
39
+ *
40
+ * @public
41
+ */
42
+
43
+ declare const plugin: Plugin;
44
+
45
+ export { plugin as default };
@@ -0,0 +1,304 @@
1
+ // src/notify-hub/index.ts
2
+ var state = {
3
+ sent: 0,
4
+ failed: 0,
5
+ suppressed: 0,
6
+ consecutiveFailures: 0,
7
+ circuitOpen: false,
8
+ lastDelivery: null,
9
+ stopHookUnregister: null,
10
+ eventUnsubscribers: []
11
+ };
12
+ var KNOWN_EVENTS = ["session.stop", "tool.error", "budget.threshold"];
13
+ var DEFAULTS = {
14
+ enabled: true,
15
+ webhookUrl: "",
16
+ events: ["session.stop", "tool.error"],
17
+ headers: {},
18
+ timeoutMs: 5e3,
19
+ maxConsecutiveFailures: 5
20
+ };
21
+ function readConfig(raw) {
22
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS, events: [...DEFAULTS.events] };
23
+ const r = raw;
24
+ const headers = {};
25
+ if (r["headers"] && typeof r["headers"] === "object" && !Array.isArray(r["headers"])) {
26
+ for (const [k, v] of Object.entries(r["headers"])) {
27
+ if (typeof v === "string") headers[k] = v;
28
+ }
29
+ }
30
+ return {
31
+ enabled: r["enabled"] !== false,
32
+ webhookUrl: typeof r["webhookUrl"] === "string" ? r["webhookUrl"] : "",
33
+ events: Array.isArray(r["events"]) ? r["events"].filter((e) => KNOWN_EVENTS.includes(e)) : [...DEFAULTS.events],
34
+ headers,
35
+ timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] >= 500 && r["timeoutMs"] <= 6e4 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
36
+ maxConsecutiveFailures: typeof r["maxConsecutiveFailures"] === "number" && r["maxConsecutiveFailures"] >= 1 ? r["maxConsecutiveFailures"] : DEFAULTS.maxConsecutiveFailures
37
+ };
38
+ }
39
+ async function deliver(cfg, event, payload, log) {
40
+ if (!cfg.webhookUrl) return false;
41
+ if (state.circuitOpen) {
42
+ state.suppressed += 1;
43
+ return false;
44
+ }
45
+ const body = JSON.stringify({
46
+ source: "wrongstack/notify-hub",
47
+ event,
48
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
49
+ ...payload
50
+ });
51
+ try {
52
+ const controller = new AbortController();
53
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
54
+ try {
55
+ const res = await fetch(cfg.webhookUrl, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json", ...cfg.headers },
58
+ body,
59
+ signal: controller.signal
60
+ });
61
+ if (!res.ok) throw new Error(`webhook responded ${res.status}`);
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ state.sent += 1;
66
+ state.consecutiveFailures = 0;
67
+ state.lastDelivery = { event, ok: true, when: (/* @__PURE__ */ new Date()).toISOString() };
68
+ return true;
69
+ } catch (err) {
70
+ state.failed += 1;
71
+ state.consecutiveFailures += 1;
72
+ state.lastDelivery = { event, ok: false, when: (/* @__PURE__ */ new Date()).toISOString() };
73
+ if (state.consecutiveFailures >= cfg.maxConsecutiveFailures) {
74
+ state.circuitOpen = true;
75
+ log.warn(
76
+ `notify-hub: ${state.consecutiveFailures} consecutive delivery failures \u2014 circuit opened, further notifications suppressed`,
77
+ { error: err instanceof Error ? err.message : String(err) }
78
+ );
79
+ }
80
+ return false;
81
+ }
82
+ }
83
+ function truncateText(s, max) {
84
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
85
+ }
86
+ var plugin = {
87
+ name: "notify-hub",
88
+ version: "0.1.0",
89
+ description: "POSTs session events (stop, tool errors, budget thresholds) and ad-hoc notify_send messages to a configurable webhook",
90
+ apiVersion: "^0.1.10",
91
+ capabilities: { tools: true, hooks: true },
92
+ defaultConfig: { ...DEFAULTS },
93
+ configSchema: {
94
+ type: "object",
95
+ properties: {
96
+ enabled: { type: "boolean", default: true, description: "Master switch." },
97
+ webhookUrl: {
98
+ type: "string",
99
+ default: "",
100
+ description: "HTTP(S) endpoint that receives JSON POSTs. Empty = plugin idles."
101
+ },
102
+ events: {
103
+ type: "array",
104
+ items: { type: "string", enum: KNOWN_EVENTS },
105
+ default: ["session.stop", "tool.error"],
106
+ description: "Which events trigger a webhook delivery."
107
+ },
108
+ headers: {
109
+ type: "object",
110
+ default: {},
111
+ description: "Extra HTTP headers sent with every delivery (e.g. Authorization)."
112
+ },
113
+ timeoutMs: {
114
+ type: "number",
115
+ minimum: 500,
116
+ maximum: 6e4,
117
+ default: 5e3,
118
+ description: "Per-delivery timeout."
119
+ },
120
+ maxConsecutiveFailures: {
121
+ type: "number",
122
+ minimum: 1,
123
+ default: 5,
124
+ description: "Circuit breaker: stop trying after this many consecutive failures."
125
+ }
126
+ }
127
+ },
128
+ setup(api) {
129
+ state.sent = 0;
130
+ state.failed = 0;
131
+ state.suppressed = 0;
132
+ state.consecutiveFailures = 0;
133
+ state.circuitOpen = false;
134
+ state.lastDelivery = null;
135
+ if (state.stopHookUnregister) {
136
+ try {
137
+ state.stopHookUnregister();
138
+ } catch {
139
+ }
140
+ state.stopHookUnregister = null;
141
+ }
142
+ for (const off of state.eventUnsubscribers) {
143
+ try {
144
+ off();
145
+ } catch {
146
+ }
147
+ }
148
+ state.eventUnsubscribers = [];
149
+ const cfg = readConfig(api.config.extensions?.["notify-hub"]);
150
+ const active = cfg.enabled && cfg.webhookUrl.length > 0;
151
+ if (active && cfg.events.includes("session.stop")) {
152
+ const stopHook = (input) => {
153
+ void deliver(
154
+ cfg,
155
+ "session.stop",
156
+ { sessionId: input.sessionId ?? null, cwd: input.cwd ?? null },
157
+ api.log
158
+ );
159
+ };
160
+ state.stopHookUnregister = api.registerHook("Stop", void 0, stopHook);
161
+ }
162
+ if (active && cfg.events.includes("tool.error")) {
163
+ const off = api.onPattern("tool.*", (eventName, payload) => {
164
+ if (!/error|failed/.test(eventName)) return;
165
+ const p = payload;
166
+ void deliver(
167
+ cfg,
168
+ "tool.error",
169
+ {
170
+ tool: p?.tool ?? p?.name ?? "unknown",
171
+ busEvent: eventName,
172
+ error: truncateText(
173
+ p?.error instanceof Error ? p.error.message : String(p?.error ?? ""),
174
+ 500
175
+ )
176
+ },
177
+ api.log
178
+ );
179
+ });
180
+ state.eventUnsubscribers.push(off);
181
+ }
182
+ if (active && cfg.events.includes("budget.threshold")) {
183
+ const off = api.onPattern("budget.*", (eventName, payload) => {
184
+ if (!eventName.includes("threshold")) return;
185
+ void deliver(cfg, "budget.threshold", { busEvent: eventName, detail: payload }, api.log);
186
+ });
187
+ state.eventUnsubscribers.push(off);
188
+ }
189
+ api.tools.register({
190
+ name: "notify_send",
191
+ description: 'Send an ad-hoc notification to the configured webhook (e.g. "migration finished", "need input"). No-op when notify-hub has no webhookUrl.',
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ title: { type: "string", description: "Short notification title." },
196
+ message: { type: "string", description: "Notification body." },
197
+ level: {
198
+ type: "string",
199
+ enum: ["info", "warning", "critical"],
200
+ description: "Severity hint for the receiver (default info)."
201
+ }
202
+ },
203
+ required: ["message"]
204
+ },
205
+ permission: "auto",
206
+ category: "Notifications",
207
+ mutating: true,
208
+ async execute(input) {
209
+ if (!cfg.enabled) return { ok: false, error: "notify-hub is disabled" };
210
+ if (!cfg.webhookUrl) {
211
+ return {
212
+ ok: false,
213
+ error: 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
214
+ };
215
+ }
216
+ const delivered = await deliver(
217
+ cfg,
218
+ "manual",
219
+ {
220
+ title: truncateText(String(input.title ?? "WrongStack notification"), 200),
221
+ message: truncateText(String(input.message ?? ""), 2e3),
222
+ level: input.level === "warning" || input.level === "critical" ? input.level : "info"
223
+ },
224
+ api.log
225
+ );
226
+ return {
227
+ ok: delivered,
228
+ circuitOpen: state.circuitOpen,
229
+ ...delivered ? {} : { error: "delivery failed (see notify_hub_status)" }
230
+ };
231
+ }
232
+ });
233
+ api.tools.register({
234
+ name: "notify_hub_status",
235
+ description: "Reports notify-hub state: webhook configuration (URL redacted), subscribed events, and delivery counters.",
236
+ inputSchema: { type: "object", properties: {} },
237
+ permission: "auto",
238
+ category: "Diagnostics",
239
+ mutating: false,
240
+ async execute() {
241
+ return {
242
+ ok: true,
243
+ enabled: cfg.enabled,
244
+ webhookConfigured: cfg.webhookUrl.length > 0,
245
+ events: cfg.events,
246
+ timeoutMs: cfg.timeoutMs,
247
+ circuitOpen: state.circuitOpen,
248
+ counters: {
249
+ sent: state.sent,
250
+ failed: state.failed,
251
+ suppressed: state.suppressed,
252
+ consecutiveFailures: state.consecutiveFailures
253
+ },
254
+ lastDelivery: state.lastDelivery
255
+ };
256
+ }
257
+ });
258
+ api.log.info("notify-hub plugin loaded", {
259
+ version: "0.1.0",
260
+ enabled: cfg.enabled,
261
+ webhookConfigured: cfg.webhookUrl.length > 0,
262
+ events: cfg.events
263
+ });
264
+ },
265
+ teardown(api) {
266
+ if (state.stopHookUnregister) {
267
+ try {
268
+ state.stopHookUnregister();
269
+ } catch {
270
+ }
271
+ state.stopHookUnregister = null;
272
+ }
273
+ for (const off of state.eventUnsubscribers) {
274
+ try {
275
+ off();
276
+ } catch {
277
+ }
278
+ }
279
+ state.eventUnsubscribers = [];
280
+ const final = { sent: state.sent, failed: state.failed, suppressed: state.suppressed };
281
+ state.sent = 0;
282
+ state.failed = 0;
283
+ state.suppressed = 0;
284
+ state.consecutiveFailures = 0;
285
+ state.circuitOpen = false;
286
+ state.lastDelivery = null;
287
+ api.log.info("notify-hub: teardown complete", { final });
288
+ },
289
+ async health() {
290
+ return {
291
+ ok: !state.circuitOpen,
292
+ message: state.circuitOpen ? `notify-hub: circuit OPEN after ${state.consecutiveFailures} consecutive failures \u2014 deliveries suppressed` : `notify-hub: ${state.sent} sent, ${state.failed} failed, ${state.suppressed} suppressed`,
293
+ counters: {
294
+ sent: state.sent,
295
+ failed: state.failed,
296
+ suppressed: state.suppressed,
297
+ consecutiveFailures: state.consecutiveFailures
298
+ }
299
+ };
300
+ }
301
+ };
302
+ var notify_hub_default = plugin;
303
+
304
+ export { notify_hub_default as default };
@@ -0,0 +1,54 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * path-guard plugin — blocks (or warns about) writes, edits, and
5
+ * destructive shell commands that touch protected paths.
6
+ *
7
+ * `branch-guard` protects git *branches*; this plugin protects
8
+ * *files and directories*. A `PreToolUse` hook on `write|edit|bash`
9
+ * matches the target path against a configurable glob list:
10
+ *
11
+ * - `write` / `edit` → the `path` / `file_path` input field
12
+ * - `bash` → destructive commands only (`rm`, `rmdir`,
13
+ * `mv`, `del`, output redirection `>` / `>>`, `truncate`) whose
14
+ * arguments mention a protected path
15
+ *
16
+ * Default protected set: lockfiles, `.env*` secrets, `.git/`
17
+ * internals, and production migration folders — files an agent
18
+ * should virtually never rewrite by hand.
19
+ *
20
+ * Config (`config.extensions['path-guard']`):
21
+ *
22
+ * ```jsonc
23
+ * {
24
+ * "enabled": true,
25
+ * "mode": "block", // "block" | "warn"
26
+ * "protect": ["pnpm-lock.yaml", ".env*", ".git/**", "**&#47;migrations/**"],
27
+ * "allow": [] // globs that override protect
28
+ * }
29
+ * ```
30
+ *
31
+ * Toggle off with `{ "name": "path-guard", "enabled": false }` in
32
+ * `config.plugins`, or `"enabled": false` in the options above.
33
+ *
34
+ * @public
35
+ */
36
+
37
+ /**
38
+ * Compile a glob pattern to a RegExp. Supports `**` (any depth),
39
+ * `*` (within one segment), and `?` (single char). Matching is done
40
+ * against forward-slash-normalized relative-ish paths, and a pattern
41
+ * without a slash matches the basename anywhere in the tree
42
+ * (`.env` matches `sub/dir/.env`).
43
+ */
44
+ declare function compilePathGlob(pattern: string): RegExp;
45
+ /**
46
+ * Extract candidate target paths from a shell command, but only when
47
+ * the command looks destructive. Non-destructive commands (cat, ls,
48
+ * grep …) never trigger the guard, even if they mention a protected
49
+ * path.
50
+ */
51
+ declare function destructiveTargets(command: string): string[];
52
+ declare const plugin: Plugin;
53
+
54
+ export { compilePathGlob, plugin as default, destructiveTargets };