@wrongstack/plugins 0.291.1 → 0.292.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/duplicate-code-detector/index.d.ts.map +1 -1
- package/dist/duplicate-code-detector.js +35 -18
- package/dist/format-on-save/index.d.ts.map +1 -1
- package/dist/format-on-save.js +65 -27
- package/dist/git-autocommit/index.d.ts.map +1 -1
- package/dist/git-autocommit.js +54 -42
- package/dist/index.js +529 -310
- package/dist/notify-hub/index.d.ts +0 -39
- package/dist/notify-hub/index.d.ts.map +1 -1
- package/dist/notify-hub/webhook-channel.d.ts +56 -0
- package/dist/notify-hub/webhook-channel.d.ts.map +1 -0
- package/dist/notify-hub.js +241 -102
- package/dist/shell-check/index.d.ts.map +1 -1
- package/dist/shell-check.js +60 -36
- package/dist/spec-linker.js +529 -310
- package/dist/type-gate/index.d.ts.map +1 -1
- package/dist/type-gate.js +12 -23
- package/package.json +3 -3
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* notify-hub plugin — pushes session events to a webhook.
|
|
3
|
-
*
|
|
4
|
-
* Long-running autonomous sessions need a way to reach the user when
|
|
5
|
-
* something noteworthy happens and nobody is watching the terminal.
|
|
6
|
-
* notify-hub POSTs compact JSON payloads to a configurable webhook
|
|
7
|
-
* URL (Slack/Discord-compatible via generic JSON, n8n, ntfy, or any
|
|
8
|
-
* HTTP endpoint) for a configurable set of events:
|
|
9
|
-
*
|
|
10
|
-
* - `session.stop` — the agent loop ended (Stop hook)
|
|
11
|
-
* - `tool.error` — a tool invocation failed
|
|
12
|
-
* - `budget.threshold` — a budget threshold event fired on the bus
|
|
13
|
-
*
|
|
14
|
-
* Deliveries are fire-and-forget with a timeout — a dead webhook can
|
|
15
|
-
* never stall the agent. Failures are counted and reported via
|
|
16
|
-
* `notify_hub_status` / `health()`, and delivery stops trying after
|
|
17
|
-
* `maxConsecutiveFailures` (circuit breaker) until setup runs again.
|
|
18
|
-
*
|
|
19
|
-
* The agent can also send an ad-hoc notification with `notify_send`
|
|
20
|
-
* ("tell the user the migration finished").
|
|
21
|
-
*
|
|
22
|
-
* Config (`config.extensions['notify-hub']`):
|
|
23
|
-
*
|
|
24
|
-
* ```jsonc
|
|
25
|
-
* {
|
|
26
|
-
* "enabled": true,
|
|
27
|
-
* "webhookUrl": "", // empty = plugin idles
|
|
28
|
-
* "events": ["session.stop", "tool.error"],
|
|
29
|
-
* "headers": {}, // extra HTTP headers (auth…)
|
|
30
|
-
* "timeoutMs": 5000,
|
|
31
|
-
* "maxConsecutiveFailures": 5
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* Toggle off with `{ "name": "notify-hub", "enabled": false }` in
|
|
36
|
-
* `config.plugins`, or `"enabled": false` in the options above.
|
|
37
|
-
*
|
|
38
|
-
* @public
|
|
39
|
-
*/
|
|
40
1
|
import type { Plugin } from '@wrongstack/core';
|
|
41
2
|
declare const plugin: Plugin;
|
|
42
3
|
export default plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify-hub/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify-hub/index.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAmD,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA8NhG,QAAA,MAAM,MAAM,EAAE,MAwRb,CAAC;eAEa,MAAM"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebhookNotificationChannel — NotificationChannel implementation for
|
|
3
|
+
* generic HTTP(S) webhook delivery.
|
|
4
|
+
*
|
|
5
|
+
* POSTs JSON payloads to a configurable webhook URL (Slack/Discord
|
|
6
|
+
* compatible via generic JSON, n8n, ntfy, or any HTTP endpoint). Includes
|
|
7
|
+
* a built-in circuit breaker that stops delivery after N consecutive
|
|
8
|
+
* failures to avoid hammering a dead endpoint.
|
|
9
|
+
*
|
|
10
|
+
* The channel is stateless beyond the circuit breaker state — each
|
|
11
|
+
* `deliver()` call is an independent HTTP POST. The caller (Notifier or
|
|
12
|
+
* direct user) is responsible for retry logic if needed.
|
|
13
|
+
*
|
|
14
|
+
* @module notify-hub
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
import type { NotificationChannel, NotificationMessage, NotificationResult } from '@wrongstack/core';
|
|
18
|
+
export interface WebhookChannelOptions {
|
|
19
|
+
/** HTTP(S) endpoint that receives JSON POSTs. */
|
|
20
|
+
readonly webhookUrl: string;
|
|
21
|
+
/** Extra HTTP headers sent with every delivery (e.g. Authorization). */
|
|
22
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
23
|
+
/** Per-delivery timeout in ms (default 5000). */
|
|
24
|
+
readonly timeoutMs?: number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Circuit breaker threshold — stop trying after this many consecutive
|
|
27
|
+
* failures. Default 5. Set to 0 to disable the circuit breaker.
|
|
28
|
+
*/
|
|
29
|
+
readonly maxConsecutiveFailures?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
export declare class WebhookNotificationChannel implements NotificationChannel {
|
|
32
|
+
#private;
|
|
33
|
+
readonly name: 'webhook';
|
|
34
|
+
readonly type: 'webhook';
|
|
35
|
+
constructor(opts: WebhookChannelOptions);
|
|
36
|
+
deliver(msg: NotificationMessage): Promise<NotificationResult>;
|
|
37
|
+
ping(): Promise<{
|
|
38
|
+
ok: boolean;
|
|
39
|
+
error?: string | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
/** Reset the circuit breaker (e.g. after config update). */
|
|
42
|
+
resetCircuit(): void;
|
|
43
|
+
/** Current circuit breaker state. */
|
|
44
|
+
circuitStatus(): {
|
|
45
|
+
open: boolean;
|
|
46
|
+
consecutiveFailures: number;
|
|
47
|
+
};
|
|
48
|
+
/** Lifetime delivery counters (never reset). */
|
|
49
|
+
counters(): {
|
|
50
|
+
attempted: number;
|
|
51
|
+
delivered: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
suppressed: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=webhook-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-channel.d.ts","sourceRoot":"","sources":["../../src/notify-hub/webhook-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAmBrG,MAAM,WAAW,qBAAqB;IACpC,iDAAiD;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAChE,iDAAiD;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtD;AAED,qBAAa,0BAA2B,YAAW,mBAAmB;;IACpE,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAU;IACnC,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAU;IAiBnC,YAAY,IAAI,EAAE,qBAAqB,EAMtC;IAMK,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA6DnE;IAEK,IAAI,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAYjE;IAMD,4DAA4D;IAC5D,YAAY,IAAI,IAAI,CAGnB;IAED,qCAAqC;IACrC,aAAa,IAAI;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE,CAE9D;IAED,gDAAgD;IAChD,QAAQ,IAAI;QACV,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAOA;CACF"}
|
package/dist/notify-hub.js
CHANGED
|
@@ -1,13 +1,132 @@
|
|
|
1
1
|
// src/notify-hub/index.ts
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
|
|
4
|
+
// src/notify-hub/webhook-channel.ts
|
|
5
|
+
function freshCircuit() {
|
|
6
|
+
return { open: false, consecutiveFailures: 0 };
|
|
7
|
+
}
|
|
8
|
+
var WebhookNotificationChannel = class {
|
|
9
|
+
name = "webhook";
|
|
10
|
+
type = "webhook";
|
|
11
|
+
#url;
|
|
12
|
+
#headers;
|
|
13
|
+
#timeoutMs;
|
|
14
|
+
#maxFailures;
|
|
15
|
+
#circuit;
|
|
16
|
+
/** Total deliveries attempted (across all resets). */
|
|
17
|
+
#totalAttempted = 0;
|
|
18
|
+
/** Total successful deliveries. */
|
|
19
|
+
#totalDelivered = 0;
|
|
20
|
+
/** Total failed deliveries. */
|
|
21
|
+
#totalFailed = 0;
|
|
22
|
+
/** Total suppressed by circuit breaker. */
|
|
23
|
+
#totalSuppressed = 0;
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.#url = opts.webhookUrl;
|
|
26
|
+
this.#headers = opts.headers ?? {};
|
|
27
|
+
this.#timeoutMs = opts.timeoutMs ?? 5e3;
|
|
28
|
+
this.#maxFailures = opts.maxConsecutiveFailures ?? 5;
|
|
29
|
+
this.#circuit = freshCircuit();
|
|
30
|
+
}
|
|
31
|
+
// -----------------------------------------------------------------------
|
|
32
|
+
// NotificationChannel
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
async deliver(msg) {
|
|
35
|
+
const deliveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
36
|
+
if (this.#circuit.open && this.#maxFailures > 0) {
|
|
37
|
+
this.#totalSuppressed += 1;
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
channel: this.name,
|
|
41
|
+
error: `circuit open after ${this.#circuit.consecutiveFailures} consecutive failures`,
|
|
42
|
+
deliveredAt
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
this.#totalAttempted += 1;
|
|
46
|
+
const body = JSON.stringify({
|
|
47
|
+
source: "wrongstack/notify-hub",
|
|
48
|
+
event: msg.source,
|
|
49
|
+
ts: deliveredAt,
|
|
50
|
+
title: msg.title,
|
|
51
|
+
message: msg.body,
|
|
52
|
+
level: msg.level,
|
|
53
|
+
...msg.metadata ?? {}
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(this.#url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json", ...this.#headers },
|
|
62
|
+
body,
|
|
63
|
+
signal: controller.signal
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) throw new Error(`webhook responded ${res.status}`);
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
this.#totalDelivered += 1;
|
|
70
|
+
this.#circuit.consecutiveFailures = 0;
|
|
71
|
+
this.#circuit.open = false;
|
|
72
|
+
return { ok: true, channel: this.name, deliveredAt };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.#totalFailed += 1;
|
|
75
|
+
this.#circuit.consecutiveFailures += 1;
|
|
76
|
+
if (this.#maxFailures > 0 && this.#circuit.consecutiveFailures >= this.#maxFailures) {
|
|
77
|
+
this.#circuit.open = true;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
channel: this.name,
|
|
82
|
+
error: err instanceof Error ? err.message : String(err),
|
|
83
|
+
deliveredAt
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async ping() {
|
|
88
|
+
if (!this.#url) {
|
|
89
|
+
return { ok: false, error: "no webhookUrl configured" };
|
|
90
|
+
}
|
|
91
|
+
if (this.#circuit.open) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `circuit open after ${this.#circuit.consecutiveFailures} consecutive failures`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
// Diagnostics (not part of NotificationChannel)
|
|
101
|
+
// -----------------------------------------------------------------------
|
|
102
|
+
/** Reset the circuit breaker (e.g. after config update). */
|
|
103
|
+
resetCircuit() {
|
|
104
|
+
this.#circuit.open = false;
|
|
105
|
+
this.#circuit.consecutiveFailures = 0;
|
|
106
|
+
}
|
|
107
|
+
/** Current circuit breaker state. */
|
|
108
|
+
circuitStatus() {
|
|
109
|
+
return { open: this.#circuit.open, consecutiveFailures: this.#circuit.consecutiveFailures };
|
|
110
|
+
}
|
|
111
|
+
/** Lifetime delivery counters (never reset). */
|
|
112
|
+
counters() {
|
|
113
|
+
return {
|
|
114
|
+
attempted: this.#totalAttempted,
|
|
115
|
+
delivered: this.#totalDelivered,
|
|
116
|
+
failed: this.#totalFailed,
|
|
117
|
+
suppressed: this.#totalSuppressed
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/notify-hub/index.ts
|
|
123
|
+
var _pluginLog = null;
|
|
2
124
|
var state = {
|
|
3
|
-
|
|
4
|
-
failed: 0,
|
|
5
|
-
suppressed: 0,
|
|
6
|
-
consecutiveFailures: 0,
|
|
7
|
-
circuitOpen: false,
|
|
125
|
+
channel: null,
|
|
8
126
|
lastDelivery: null,
|
|
9
127
|
stopHookUnregister: null,
|
|
10
|
-
eventUnsubscribers: []
|
|
128
|
+
eventUnsubscribers: [],
|
|
129
|
+
circuitWarned: false
|
|
11
130
|
};
|
|
12
131
|
var KNOWN_EVENTS = ["session.stop", "tool.error", "budget.threshold"];
|
|
13
132
|
var DEFAULTS = {
|
|
@@ -30,6 +149,21 @@ function isBlockedHostname(hostname) {
|
|
|
30
149
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
31
150
|
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "0:0:0:0:0:0:0:1" || h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:") || isPrivateIPv4(h);
|
|
32
151
|
}
|
|
152
|
+
async function hasPrivateResolvedIP(hostname) {
|
|
153
|
+
try {
|
|
154
|
+
const entries = await lookup(hostname, { all: true, verbatim: true });
|
|
155
|
+
for (const { address } of entries) {
|
|
156
|
+
if (isPrivateIPv4(address)) return true;
|
|
157
|
+
const lower = address.toLowerCase();
|
|
158
|
+
if (lower === "::1" || lower === "0:0:0:0:0:0:0:1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:")) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
33
167
|
function normalizeWebhookUrl(raw) {
|
|
34
168
|
if (typeof raw !== "string" || raw.trim().length === 0) return "";
|
|
35
169
|
try {
|
|
@@ -61,49 +195,37 @@ function readConfig(raw) {
|
|
|
61
195
|
maxConsecutiveFailures: typeof r["maxConsecutiveFailures"] === "number" && r["maxConsecutiveFailures"] >= 1 ? r["maxConsecutiveFailures"] : DEFAULTS.maxConsecutiveFailures
|
|
62
196
|
};
|
|
63
197
|
}
|
|
64
|
-
async function deliver(
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
state.suppressed += 1;
|
|
198
|
+
async function deliver(event, payload) {
|
|
199
|
+
const ch = state.channel;
|
|
200
|
+
if (!ch) {
|
|
68
201
|
return false;
|
|
69
202
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
203
|
+
const result = await deliverViaChannel(ch, event, {
|
|
204
|
+
title: typeof payload.title === "string" ? payload.title : event,
|
|
205
|
+
body: typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : JSON.stringify(payload),
|
|
206
|
+
level: event === "tool.error" || event === "budget.threshold" ? "warning" : "info",
|
|
207
|
+
source: event,
|
|
208
|
+
metadata: payload
|
|
75
209
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
headers: { "content-type": "application/json", ...cfg.headers },
|
|
83
|
-
body,
|
|
84
|
-
signal: controller.signal
|
|
85
|
-
});
|
|
86
|
-
if (!res.ok) throw new Error(`webhook responded ${res.status}`);
|
|
87
|
-
} finally {
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
}
|
|
90
|
-
state.sent += 1;
|
|
91
|
-
state.consecutiveFailures = 0;
|
|
210
|
+
return result.ok;
|
|
211
|
+
}
|
|
212
|
+
async function deliverViaChannel(ch, event, msg) {
|
|
213
|
+
const result = await ch.deliver(msg);
|
|
214
|
+
if (result.ok) {
|
|
215
|
+
state.circuitWarned = false;
|
|
92
216
|
state.lastDelivery = { event, ok: true, when: (/* @__PURE__ */ new Date()).toISOString() };
|
|
93
|
-
|
|
94
|
-
} catch (err) {
|
|
95
|
-
state.failed += 1;
|
|
96
|
-
state.consecutiveFailures += 1;
|
|
217
|
+
} else {
|
|
97
218
|
state.lastDelivery = { event, ok: false, when: (/* @__PURE__ */ new Date()).toISOString() };
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{
|
|
219
|
+
const cs = ch.circuitStatus();
|
|
220
|
+
if (cs.open && !state.circuitWarned) {
|
|
221
|
+
state.circuitWarned = true;
|
|
222
|
+
_pluginLog?.warn(
|
|
223
|
+
`notify-hub: ${cs.consecutiveFailures} consecutive delivery failures \u2014 circuit opened, further notifications suppressed`,
|
|
224
|
+
{ consecutiveFailures: cs.consecutiveFailures, event }
|
|
103
225
|
);
|
|
104
226
|
}
|
|
105
|
-
return false;
|
|
106
227
|
}
|
|
228
|
+
return result;
|
|
107
229
|
}
|
|
108
230
|
function truncateText(s, max) {
|
|
109
231
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
@@ -150,12 +272,10 @@ var plugin = {
|
|
|
150
272
|
}
|
|
151
273
|
}
|
|
152
274
|
},
|
|
153
|
-
setup(api) {
|
|
154
|
-
|
|
155
|
-
state.
|
|
156
|
-
state.
|
|
157
|
-
state.consecutiveFailures = 0;
|
|
158
|
-
state.circuitOpen = false;
|
|
275
|
+
async setup(api) {
|
|
276
|
+
_pluginLog = api.log;
|
|
277
|
+
state.circuitWarned = false;
|
|
278
|
+
state.channel = null;
|
|
159
279
|
state.lastDelivery = null;
|
|
160
280
|
if (state.stopHookUnregister) {
|
|
161
281
|
try {
|
|
@@ -172,15 +292,35 @@ var plugin = {
|
|
|
172
292
|
}
|
|
173
293
|
state.eventUnsubscribers = [];
|
|
174
294
|
const cfg = readConfig(api.config.extensions?.["notify-hub"]);
|
|
175
|
-
|
|
295
|
+
let active = cfg.enabled && cfg.webhookUrl.length > 0;
|
|
296
|
+
if (active) {
|
|
297
|
+
try {
|
|
298
|
+
const url = new URL(cfg.webhookUrl);
|
|
299
|
+
const hostnameBlocked = await hasPrivateResolvedIP(url.hostname);
|
|
300
|
+
if (hostnameBlocked) {
|
|
301
|
+
api.log.warn(
|
|
302
|
+
`notify-hub: webhook URL resolves to a private/local IP (${url.hostname}) \u2014 disabling plugin`
|
|
303
|
+
);
|
|
304
|
+
active = false;
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (active) {
|
|
310
|
+
state.channel = new WebhookNotificationChannel({
|
|
311
|
+
webhookUrl: cfg.webhookUrl,
|
|
312
|
+
headers: cfg.headers,
|
|
313
|
+
timeoutMs: cfg.timeoutMs,
|
|
314
|
+
maxConsecutiveFailures: cfg.maxConsecutiveFailures
|
|
315
|
+
});
|
|
316
|
+
api.notifier?.registerChannel(state.channel);
|
|
317
|
+
}
|
|
176
318
|
if (active && cfg.events.includes("session.stop")) {
|
|
177
319
|
const stopHook = (input) => {
|
|
178
|
-
void deliver(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
api.log
|
|
183
|
-
);
|
|
320
|
+
void deliver("session.stop", {
|
|
321
|
+
sessionId: input.sessionId ?? null,
|
|
322
|
+
cwd: input.cwd ?? null
|
|
323
|
+
});
|
|
184
324
|
};
|
|
185
325
|
state.stopHookUnregister = api.registerHook("Stop", void 0, stopHook);
|
|
186
326
|
}
|
|
@@ -188,26 +328,21 @@ var plugin = {
|
|
|
188
328
|
const off = api.onPattern("tool.*", (eventName, payload) => {
|
|
189
329
|
if (!/error|failed/.test(eventName)) return;
|
|
190
330
|
const p = payload;
|
|
191
|
-
void deliver(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
500
|
|
200
|
-
)
|
|
201
|
-
},
|
|
202
|
-
api.log
|
|
203
|
-
);
|
|
331
|
+
void deliver("tool.error", {
|
|
332
|
+
tool: p?.tool ?? p?.name ?? "unknown",
|
|
333
|
+
busEvent: eventName,
|
|
334
|
+
error: truncateText(
|
|
335
|
+
p?.error instanceof Error ? p.error.message : String(p?.error ?? ""),
|
|
336
|
+
500
|
|
337
|
+
)
|
|
338
|
+
});
|
|
204
339
|
});
|
|
205
340
|
state.eventUnsubscribers.push(off);
|
|
206
341
|
}
|
|
207
342
|
if (active && cfg.events.includes("budget.threshold")) {
|
|
208
343
|
const off = api.onPattern("budget.*", (eventName, payload) => {
|
|
209
344
|
if (!eventName.includes("threshold")) return;
|
|
210
|
-
void deliver(
|
|
345
|
+
void deliver("budget.threshold", { busEvent: eventName, detail: payload });
|
|
211
346
|
});
|
|
212
347
|
state.eventUnsubscribers.push(off);
|
|
213
348
|
}
|
|
@@ -232,26 +367,23 @@ var plugin = {
|
|
|
232
367
|
mutating: true,
|
|
233
368
|
async execute(input) {
|
|
234
369
|
if (!cfg.enabled) return { ok: false, error: "notify-hub is disabled" };
|
|
235
|
-
|
|
370
|
+
const ch = state.channel;
|
|
371
|
+
if (!ch) {
|
|
236
372
|
return {
|
|
237
373
|
ok: false,
|
|
238
374
|
error: 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
|
|
239
375
|
};
|
|
240
376
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
"
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
level: input.level === "warning" || input.level === "critical" ? input.level : "info"
|
|
248
|
-
},
|
|
249
|
-
api.log
|
|
250
|
-
);
|
|
377
|
+
const result = await deliverViaChannel(ch, "manual", {
|
|
378
|
+
title: truncateText(String(input.title ?? "WrongStack notification"), 200),
|
|
379
|
+
body: truncateText(String(input.message ?? ""), 2e3),
|
|
380
|
+
level: input.level === "warning" || input.level === "critical" ? input.level : "info",
|
|
381
|
+
source: "manual"
|
|
382
|
+
});
|
|
251
383
|
return {
|
|
252
|
-
ok:
|
|
253
|
-
circuitOpen:
|
|
254
|
-
...
|
|
384
|
+
ok: result.ok,
|
|
385
|
+
circuitOpen: ch.circuitStatus().open,
|
|
386
|
+
...result.ok ? {} : { error: result.error ?? "delivery failed" }
|
|
255
387
|
};
|
|
256
388
|
}
|
|
257
389
|
});
|
|
@@ -263,18 +395,26 @@ var plugin = {
|
|
|
263
395
|
category: "Diagnostics",
|
|
264
396
|
mutating: false,
|
|
265
397
|
async execute() {
|
|
398
|
+
const ch = state.channel;
|
|
399
|
+
const counters = ch?.counters() ?? {
|
|
400
|
+
attempted: 0,
|
|
401
|
+
delivered: 0,
|
|
402
|
+
failed: 0,
|
|
403
|
+
suppressed: 0
|
|
404
|
+
};
|
|
266
405
|
return {
|
|
267
406
|
ok: true,
|
|
268
407
|
enabled: cfg.enabled,
|
|
269
408
|
webhookConfigured: cfg.webhookUrl.length > 0,
|
|
270
409
|
events: cfg.events,
|
|
271
410
|
timeoutMs: cfg.timeoutMs,
|
|
272
|
-
circuitOpen:
|
|
411
|
+
circuitOpen: ch?.circuitStatus().open ?? false,
|
|
273
412
|
counters: {
|
|
274
|
-
sent:
|
|
275
|
-
failed:
|
|
276
|
-
suppressed:
|
|
277
|
-
consecutiveFailures:
|
|
413
|
+
sent: counters.delivered,
|
|
414
|
+
failed: counters.failed,
|
|
415
|
+
suppressed: counters.suppressed,
|
|
416
|
+
consecutiveFailures: ch?.circuitStatus().consecutiveFailures ?? 0,
|
|
417
|
+
totalAttempted: counters.attempted
|
|
278
418
|
},
|
|
279
419
|
lastDelivery: state.lastDelivery
|
|
280
420
|
};
|
|
@@ -288,6 +428,8 @@ var plugin = {
|
|
|
288
428
|
});
|
|
289
429
|
},
|
|
290
430
|
teardown(api) {
|
|
431
|
+
_pluginLog = null;
|
|
432
|
+
state.circuitWarned = false;
|
|
291
433
|
if (state.stopHookUnregister) {
|
|
292
434
|
try {
|
|
293
435
|
state.stopHookUnregister();
|
|
@@ -302,25 +444,22 @@ var plugin = {
|
|
|
302
444
|
}
|
|
303
445
|
}
|
|
304
446
|
state.eventUnsubscribers = [];
|
|
305
|
-
const
|
|
306
|
-
state.
|
|
307
|
-
state.failed = 0;
|
|
308
|
-
state.suppressed = 0;
|
|
309
|
-
state.consecutiveFailures = 0;
|
|
310
|
-
state.circuitOpen = false;
|
|
447
|
+
const channelCounters = state.channel?.counters();
|
|
448
|
+
state.channel = null;
|
|
311
449
|
state.lastDelivery = null;
|
|
312
|
-
api.log.info("notify-hub: teardown complete", {
|
|
450
|
+
api.log.info("notify-hub: teardown complete", { channelCounters });
|
|
313
451
|
},
|
|
314
452
|
async health() {
|
|
453
|
+
const ch = state.channel;
|
|
454
|
+
const cs = ch?.circuitStatus();
|
|
455
|
+
const channelCounters = ch?.counters();
|
|
456
|
+
const sent = channelCounters?.delivered ?? 0;
|
|
457
|
+
const failed = channelCounters?.failed ?? 0;
|
|
458
|
+
const suppressed = channelCounters?.suppressed ?? 0;
|
|
315
459
|
return {
|
|
316
|
-
ok: !
|
|
317
|
-
message:
|
|
318
|
-
counters: {
|
|
319
|
-
sent: state.sent,
|
|
320
|
-
failed: state.failed,
|
|
321
|
-
suppressed: state.suppressed,
|
|
322
|
-
consecutiveFailures: state.consecutiveFailures
|
|
323
|
-
}
|
|
460
|
+
ok: !cs?.open,
|
|
461
|
+
message: cs?.open ? `notify-hub: circuit OPEN after ${cs.consecutiveFailures} consecutive failures \u2014 deliveries suppressed` : ch ? `notify-hub: ${sent} sent, ${failed} failed, ${suppressed} suppressed` : `notify-hub: idle \u2014 no webhook channel configured`,
|
|
462
|
+
counters: { sent, failed, suppressed, consecutiveFailures: cs?.consecutiveFailures ?? 0 }
|
|
324
463
|
};
|
|
325
464
|
}
|
|
326
465
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell-check/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell-check/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA0M/C,QAAA,MAAM,MAAM,EAAE,MAgPb,CAAC;eAEa,MAAM"}
|