@steadwing/openalerts 0.2.3 → 0.2.4
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/README.md +57 -51
- package/dist/plugin/gateway-client.d.ts +39 -0
- package/dist/plugin/gateway-client.js +193 -0
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<a href="https://www.npmjs.com/package/@steadwing/openalerts"><img src="https://img.shields.io/npm/v/@steadwing/openalerts?style=flat&color=blue" alt="npm"></a>
|
|
10
10
|
<a href="https://github.com/steadwing/openalerts/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green" alt="License"></a>
|
|
11
11
|
<a href="https://github.com/steadwing/openalerts/stargazers"><img src="https://img.shields.io/github/stars/steadwing/openalerts?style=flat" alt="GitHub stars"></a>
|
|
12
|
+
<a href="https://discord.gg/4rUP86tSXn"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat" alt="Discord"></a>
|
|
12
13
|
</p>
|
|
13
14
|
|
|
14
15
|
<p align="center">
|
|
@@ -28,6 +29,7 @@ OpenAlerts watches your agent in real-time and alerts you the moment something g
|
|
|
28
29
|
## Quickstart
|
|
29
30
|
|
|
30
31
|
> Currently supports OpenClaw. More framework adapters coming soon.
|
|
32
|
+
> This project is under revamp for the next few hours
|
|
31
33
|
|
|
32
34
|
### 1. Install
|
|
33
35
|
|
|
@@ -43,17 +45,17 @@ Otherwise, set it explicitly in `openclaw.json`:
|
|
|
43
45
|
|
|
44
46
|
```jsonc
|
|
45
47
|
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
"plugins": {
|
|
49
|
+
"entries": {
|
|
50
|
+
"openalerts": {
|
|
51
|
+
"enabled": true,
|
|
52
|
+
"config": {
|
|
53
|
+
"alertChannel": "telegram", // telegram | discord | slack | whatsapp | signal
|
|
54
|
+
"alertTo": "YOUR_CHAT_ID",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
57
59
|
}
|
|
58
60
|
```
|
|
59
61
|
|
|
@@ -65,11 +67,14 @@ Otherwise, set it explicitly in `openclaw.json`:
|
|
|
65
67
|
openclaw gateway stop && openclaw gateway run
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
69
70
|
Send `/health` to your bot. You should get a live status report back — zero LLM tokens consumed.
|
|
70
71
|
|
|
71
72
|
That's it. OpenAlerts is now watching your agent.
|
|
72
73
|
|
|
74
|
+
## Demo
|
|
75
|
+
|
|
76
|
+
https://github.com/user-attachments/assets/0b6ed26e-1eb0-47b2-ae4f-947516f024b4
|
|
77
|
+
|
|
73
78
|
## Dashboard
|
|
74
79
|
|
|
75
80
|
A real-time web dashboard is embedded in the gateway at:
|
|
@@ -86,18 +91,19 @@ http://127.0.0.1:18789/openalerts
|
|
|
86
91
|
|
|
87
92
|
Eight rules run against every event in real-time. All thresholds and cooldowns are configurable.
|
|
88
93
|
|
|
89
|
-
| Rule
|
|
90
|
-
|
|
91
|
-
| `llm-errors`
|
|
92
|
-
| `infra-errors`
|
|
93
|
-
| `gateway-down`
|
|
94
|
-
| `session-stuck`
|
|
95
|
-
| `high-error-rate` | Message failure rate over last 20
|
|
96
|
-
| `queue-depth`
|
|
97
|
-
| `tool-errors`
|
|
98
|
-
| `heartbeat-fail`
|
|
94
|
+
| Rule | Watches for | Severity | Threshold (default) |
|
|
95
|
+
| ----------------- | ------------------------------------- | -------- | ------------------- |
|
|
96
|
+
| `llm-errors` | LLM/agent failures in 1 min window | ERROR | `1` error |
|
|
97
|
+
| `infra-errors` | Infrastructure errors in 1 min window | ERROR | `1` error |
|
|
98
|
+
| `gateway-down` | No heartbeat received | CRITICAL | `30000` ms (30s) |
|
|
99
|
+
| `session-stuck` | Session idle too long | WARN | `120000` ms (2 min) |
|
|
100
|
+
| `high-error-rate` | Message failure rate over last 20 | ERROR | `50`% |
|
|
101
|
+
| `queue-depth` | Queued items piling up | WARN | `10` items |
|
|
102
|
+
| `tool-errors` | Tool failures in 1 min window | WARN | `1` error |
|
|
103
|
+
| `heartbeat-fail` | Consecutive heartbeat failures | ERROR | `3` failures |
|
|
99
104
|
|
|
100
105
|
Every rule also accepts:
|
|
106
|
+
|
|
101
107
|
- **`enabled`** — `false` to disable the rule (default: `true`)
|
|
102
108
|
- **`cooldownMinutes`** — minutes before the same rule can fire again (default: `15`)
|
|
103
109
|
|
|
@@ -105,21 +111,21 @@ To tune rules, add a `rules` object in your plugin config:
|
|
|
105
111
|
|
|
106
112
|
```jsonc
|
|
107
113
|
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
"plugins": {
|
|
115
|
+
"entries": {
|
|
116
|
+
"openalerts": {
|
|
117
|
+
"config": {
|
|
118
|
+
"cooldownMinutes": 10,
|
|
119
|
+
"rules": {
|
|
120
|
+
"llm-errors": { "threshold": 5 },
|
|
121
|
+
"infra-errors": { "cooldownMinutes": 30 },
|
|
122
|
+
"high-error-rate": { "enabled": false },
|
|
123
|
+
"gateway-down": { "threshold": 60000 },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
123
129
|
}
|
|
124
130
|
```
|
|
125
131
|
|
|
@@ -131,15 +137,15 @@ OpenAlerts can optionally use your configured LLM to enrich alerts with a human-
|
|
|
131
137
|
|
|
132
138
|
```jsonc
|
|
133
139
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
"plugins": {
|
|
141
|
+
"entries": {
|
|
142
|
+
"openalerts": {
|
|
143
|
+
"config": {
|
|
144
|
+
"llmEnriched": true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
143
149
|
}
|
|
144
150
|
```
|
|
145
151
|
|
|
@@ -161,11 +167,11 @@ Action: Update your API key in ~/.openclaw/.env with a valid key from platform.o
|
|
|
161
167
|
|
|
162
168
|
Zero-token chat commands available in any connected channel:
|
|
163
169
|
|
|
164
|
-
| Command
|
|
165
|
-
|
|
166
|
-
| `/health`
|
|
167
|
-
| `/alerts`
|
|
168
|
-
| `/dashboard` | Returns the dashboard URL
|
|
170
|
+
| Command | What it does |
|
|
171
|
+
| ------------ | ----------------------------------------------------- |
|
|
172
|
+
| `/health` | System health snapshot — uptime, active alerts, stats |
|
|
173
|
+
| `/alerts` | Recent alert history with severity and timestamps |
|
|
174
|
+
| `/dashboard` | Returns the dashboard URL |
|
|
169
175
|
|
|
170
176
|
## Roadmap
|
|
171
177
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
interface GatewayClientConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
token: string;
|
|
5
|
+
reconnectInterval: number;
|
|
6
|
+
maxRetries: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* WebSocket client for OpenClaw Gateway
|
|
10
|
+
* - Connects to ws://127.0.0.1:18789
|
|
11
|
+
* - Handles JSON-RPC requests/responses
|
|
12
|
+
* - Emits events: agent, health, cron, chat
|
|
13
|
+
* - Auto-reconnects on disconnect
|
|
14
|
+
*/
|
|
15
|
+
export declare class GatewayClient extends EventEmitter {
|
|
16
|
+
private ws;
|
|
17
|
+
private config;
|
|
18
|
+
private pending;
|
|
19
|
+
private backoffMs;
|
|
20
|
+
private closed;
|
|
21
|
+
private connectTimer;
|
|
22
|
+
private ready;
|
|
23
|
+
constructor(config?: Partial<GatewayClientConfig>);
|
|
24
|
+
start(): void;
|
|
25
|
+
stop(): void;
|
|
26
|
+
isReady(): boolean;
|
|
27
|
+
private doConnect;
|
|
28
|
+
private sendConnectHandshake;
|
|
29
|
+
private handleMessage;
|
|
30
|
+
/**
|
|
31
|
+
* Send RPC request to gateway
|
|
32
|
+
* @example
|
|
33
|
+
* const cost = await client.request("usage.cost", { period: "day" });
|
|
34
|
+
* const sessions = await client.request("sessions.list");
|
|
35
|
+
*/
|
|
36
|
+
request<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
37
|
+
private scheduleReconnect;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import EventEmitter from "eventemitter3";
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket client for OpenClaw Gateway
|
|
6
|
+
* - Connects to ws://127.0.0.1:18789
|
|
7
|
+
* - Handles JSON-RPC requests/responses
|
|
8
|
+
* - Emits events: agent, health, cron, chat
|
|
9
|
+
* - Auto-reconnects on disconnect
|
|
10
|
+
*/
|
|
11
|
+
export class GatewayClient extends EventEmitter {
|
|
12
|
+
ws = null;
|
|
13
|
+
config;
|
|
14
|
+
pending = new Map();
|
|
15
|
+
backoffMs = 1000;
|
|
16
|
+
closed = false;
|
|
17
|
+
connectTimer = null;
|
|
18
|
+
ready = false;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = {
|
|
22
|
+
url: config?.url ?? "ws://127.0.0.1:18789",
|
|
23
|
+
token: config?.token ?? "",
|
|
24
|
+
reconnectInterval: config?.reconnectInterval ?? 1000,
|
|
25
|
+
maxRetries: config?.maxRetries ?? 60,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
start() {
|
|
29
|
+
if (this.closed) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.doConnect();
|
|
33
|
+
}
|
|
34
|
+
stop() {
|
|
35
|
+
this.closed = true;
|
|
36
|
+
if (this.connectTimer) {
|
|
37
|
+
clearTimeout(this.connectTimer);
|
|
38
|
+
this.connectTimer = null;
|
|
39
|
+
}
|
|
40
|
+
if (this.ws) {
|
|
41
|
+
this.ws.close();
|
|
42
|
+
this.ws = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
isReady() {
|
|
46
|
+
return this.ready;
|
|
47
|
+
}
|
|
48
|
+
doConnect() {
|
|
49
|
+
if (this.closed || this.ws) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.ws = new WebSocket(this.config.url, {
|
|
53
|
+
maxPayload: 25 * 1024 * 1024,
|
|
54
|
+
});
|
|
55
|
+
this.ws.on("open", () => {
|
|
56
|
+
this.backoffMs = 1000;
|
|
57
|
+
// Delay connect handshake slightly to allow gateway to send challenge
|
|
58
|
+
setTimeout(() => this.sendConnectHandshake(), 800);
|
|
59
|
+
});
|
|
60
|
+
this.ws.on("message", (data) => {
|
|
61
|
+
this.handleMessage(data.toString());
|
|
62
|
+
});
|
|
63
|
+
this.ws.on("error", (err) => {
|
|
64
|
+
this.emit("error", err);
|
|
65
|
+
});
|
|
66
|
+
this.ws.on("close", () => {
|
|
67
|
+
this.ws = null;
|
|
68
|
+
this.ready = false;
|
|
69
|
+
this.emit("disconnected");
|
|
70
|
+
if (!this.closed) {
|
|
71
|
+
this.scheduleReconnect();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
sendConnectHandshake() {
|
|
76
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const id = randomUUID();
|
|
80
|
+
const frame = {
|
|
81
|
+
type: "req",
|
|
82
|
+
id,
|
|
83
|
+
method: "connect",
|
|
84
|
+
params: {
|
|
85
|
+
minProtocol: 3,
|
|
86
|
+
maxProtocol: 3,
|
|
87
|
+
client: {
|
|
88
|
+
id: "gateway-client",
|
|
89
|
+
displayName: "OpenAlerts Monitor",
|
|
90
|
+
version: "0.1.0",
|
|
91
|
+
platform: process.platform,
|
|
92
|
+
mode: "backend",
|
|
93
|
+
},
|
|
94
|
+
role: "operator",
|
|
95
|
+
scopes: ["operator.admin"],
|
|
96
|
+
auth: {
|
|
97
|
+
token: this.config.token,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
this.pending.set(id, {
|
|
102
|
+
resolve: (result) => {
|
|
103
|
+
this.ready = true;
|
|
104
|
+
this.emit("ready", result);
|
|
105
|
+
},
|
|
106
|
+
reject: (err) => {
|
|
107
|
+
this.emit("error", new Error(`Connect handshake failed: ${err.message}`));
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.ws.send(JSON.stringify(frame));
|
|
111
|
+
}
|
|
112
|
+
handleMessage(raw) {
|
|
113
|
+
try {
|
|
114
|
+
const frame = JSON.parse(raw);
|
|
115
|
+
if (frame.type === "res") {
|
|
116
|
+
const pending = this.pending.get(frame.id);
|
|
117
|
+
if (pending) {
|
|
118
|
+
if (frame.error || frame.ok === false) {
|
|
119
|
+
const errMsg = typeof frame.error === "string"
|
|
120
|
+
? frame.error
|
|
121
|
+
: typeof frame.payload === "object" &&
|
|
122
|
+
frame.payload &&
|
|
123
|
+
"message" in frame.payload
|
|
124
|
+
? String(frame.payload.message)
|
|
125
|
+
: JSON.stringify(frame.error ?? frame.payload);
|
|
126
|
+
pending.reject(new Error(errMsg));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
pending.resolve(frame.payload ?? frame.result);
|
|
130
|
+
}
|
|
131
|
+
this.pending.delete(frame.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (frame.type === "event") {
|
|
135
|
+
// Emit the event for subscribers: agent, health, cron, chat
|
|
136
|
+
this.emit(frame.event, frame.payload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
this.emit("error", new Error(`Failed to parse frame: ${err}`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send RPC request to gateway
|
|
145
|
+
* @example
|
|
146
|
+
* const cost = await client.request("usage.cost", { period: "day" });
|
|
147
|
+
* const sessions = await client.request("sessions.list");
|
|
148
|
+
*/
|
|
149
|
+
request(method, params) {
|
|
150
|
+
if (!this.ready || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
151
|
+
return Promise.reject(new Error("Gateway not ready"));
|
|
152
|
+
}
|
|
153
|
+
const id = randomUUID();
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const timeout = setTimeout(() => {
|
|
156
|
+
this.pending.delete(id);
|
|
157
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
158
|
+
}, 10000);
|
|
159
|
+
this.pending.set(id, {
|
|
160
|
+
resolve: (value) => {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
resolve(value);
|
|
163
|
+
},
|
|
164
|
+
reject: (err) => {
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
reject(err);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const frame = {
|
|
170
|
+
type: "req",
|
|
171
|
+
id,
|
|
172
|
+
method,
|
|
173
|
+
params,
|
|
174
|
+
};
|
|
175
|
+
if (!this.ws) {
|
|
176
|
+
this.pending.delete(id);
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
reject(new Error("WebSocket not connected"));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.ws.send(JSON.stringify(frame));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
scheduleReconnect() {
|
|
185
|
+
if (this.connectTimer) {
|
|
186
|
+
clearTimeout(this.connectTimer);
|
|
187
|
+
}
|
|
188
|
+
this.connectTimer = setTimeout(() => {
|
|
189
|
+
this.backoffMs = Math.min(this.backoffMs * 2, 60000);
|
|
190
|
+
this.doConnect();
|
|
191
|
+
}, this.backoffMs);
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steadwing/openalerts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenAlerts — An alerting layer for agentic frameworks",
|
|
6
6
|
"author": "Steadwing",
|
|
@@ -21,10 +21,15 @@
|
|
|
21
21
|
"publish": "npm publish",
|
|
22
22
|
"prepublishOnly": "npm run build"
|
|
23
23
|
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"eventemitter3": "^5.0.1",
|
|
26
|
+
"ws": "^8.18.0"
|
|
27
|
+
},
|
|
24
28
|
"peerDependencies": {
|
|
25
29
|
"openclaw": "*"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
32
|
+
"@types/ws": "^8.5.13",
|
|
28
33
|
"typescript": "^5.9.3"
|
|
29
34
|
},
|
|
30
35
|
"openclaw": {
|
|
@@ -36,7 +41,18 @@
|
|
|
36
41
|
},
|
|
37
42
|
"homepage": "https://github.com/steadwing/openalerts#readme",
|
|
38
43
|
"bugs": "https://github.com/steadwing/openalerts/issues",
|
|
39
|
-
"keywords": [
|
|
44
|
+
"keywords": [
|
|
45
|
+
"openclaw",
|
|
46
|
+
"openclaw-plugin",
|
|
47
|
+
"alerting",
|
|
48
|
+
"monitoring",
|
|
49
|
+
"observability",
|
|
50
|
+
"llm-monitoring",
|
|
51
|
+
"agent-monitoring",
|
|
52
|
+
"ai-agents",
|
|
53
|
+
"real-time-alerts",
|
|
54
|
+
"error-tracking"
|
|
55
|
+
],
|
|
40
56
|
"engines": {
|
|
41
57
|
"node": ">=18"
|
|
42
58
|
},
|