@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 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
- "plugins": {
47
- "entries": {
48
- "openalerts": {
49
- "enabled": true,
50
- "config": {
51
- "alertChannel": "telegram", // telegram | discord | slack | whatsapp | signal
52
- "alertTo": "YOUR_CHAT_ID"
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 | Watches for | Severity | Threshold (default) |
90
- |---|---|---|---|
91
- | `llm-errors` | LLM/agent failures in 1 min window | ERROR | `1` error |
92
- | `infra-errors` | Infrastructure errors in 1 min window | ERROR | `1` error |
93
- | `gateway-down` | No heartbeat received | CRITICAL | `30000` ms (30s) |
94
- | `session-stuck` | Session idle too long | WARN | `120000` ms (2 min) |
95
- | `high-error-rate` | Message failure rate over last 20 | ERROR | `50`% |
96
- | `queue-depth` | Queued items piling up | WARN | `10` items |
97
- | `tool-errors` | Tool failures in 1 min window | WARN | `1` error |
98
- | `heartbeat-fail` | Consecutive heartbeat failures | ERROR | `3` failures |
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
- "plugins": {
109
- "entries": {
110
- "openalerts": {
111
- "config": {
112
- "cooldownMinutes": 10,
113
- "rules": {
114
- "llm-errors": { "threshold": 5 },
115
- "infra-errors": { "cooldownMinutes": 30 },
116
- "high-error-rate": { "enabled": false },
117
- "gateway-down": { "threshold": 60000 }
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
- "plugins": {
135
- "entries": {
136
- "openalerts": {
137
- "config": {
138
- "llmEnriched": true
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 | What it does |
165
- |---|---|
166
- | `/health` | System health snapshot — uptime, active alerts, stats |
167
- | `/alerts` | Recent alert history with severity and timestamps |
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",
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": ["openalerts", "openclaw", "monitoring", "alerting", "plugin"],
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
  },