@steadwing/openalerts 0.2.4 → 0.2.5
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 +6 -2
- package/dist/collections/collection-manager.d.ts +50 -0
- package/dist/collections/collection-manager.js +583 -0
- package/dist/collections/event-parser.d.ts +27 -0
- package/dist/collections/event-parser.js +321 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.js +6 -0
- package/dist/collections/persistence.d.ts +25 -0
- package/dist/collections/persistence.js +213 -0
- package/dist/collections/types.d.ts +177 -0
- package/dist/collections/types.js +15 -0
- package/dist/core/engine.js +2 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/rules.js +97 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/index.js +410 -23
- package/dist/plugin/dashboard-html.js +33 -3
- package/dist/plugin/dashboard-routes.d.ts +7 -2
- package/dist/plugin/dashboard-routes.js +111 -3
- package/dist/plugin/gateway-client.js +15 -8
- package/openclaw.plugin.json +30 -0
- package/package.json +1 -1
|
@@ -2,10 +2,42 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DEFAULTS } from "../core/index.js";
|
|
4
4
|
import { getDashboardHtml } from "./dashboard-html.js";
|
|
5
|
-
// ─── SSE connection tracking ─────────────────────────────────────────────────
|
|
6
5
|
const sseConnections = new Set();
|
|
7
|
-
|
|
6
|
+
const monitorListeners = new Set();
|
|
7
|
+
const monitorConnections = new Set();
|
|
8
|
+
// Ring buffer — last 150 agent monitor events for history replay
|
|
9
|
+
const agentMonitorBuffer = [];
|
|
10
|
+
const AGENT_MONITOR_BUF_SIZE = 150;
|
|
11
|
+
export function emitAgentMonitorEvent(event) {
|
|
12
|
+
// Store in ring buffer
|
|
13
|
+
agentMonitorBuffer.push(event);
|
|
14
|
+
if (agentMonitorBuffer.length > AGENT_MONITOR_BUF_SIZE)
|
|
15
|
+
agentMonitorBuffer.shift();
|
|
16
|
+
for (const fn of monitorListeners) {
|
|
17
|
+
try {
|
|
18
|
+
fn(event);
|
|
19
|
+
}
|
|
20
|
+
catch { /* ignore */ }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ── Gateway event recorder ──────────────────────────────────────────────────
|
|
24
|
+
const recentGatewayEvents = [];
|
|
25
|
+
export function recordGatewayEvent(eventName, payload) {
|
|
26
|
+
recentGatewayEvents.unshift({ eventName, payload, ts: Date.now() });
|
|
27
|
+
while (recentGatewayEvents.length > 10) {
|
|
28
|
+
recentGatewayEvents.pop();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
8
31
|
export function closeDashboardConnections() {
|
|
32
|
+
for (const conn of monitorConnections) {
|
|
33
|
+
clearInterval(conn.heartbeat);
|
|
34
|
+
try {
|
|
35
|
+
conn.res.end();
|
|
36
|
+
}
|
|
37
|
+
catch { /* ignore */ }
|
|
38
|
+
}
|
|
39
|
+
monitorConnections.clear();
|
|
40
|
+
monitorListeners.clear();
|
|
9
41
|
for (const conn of sseConnections) {
|
|
10
42
|
clearInterval(conn.heartbeat);
|
|
11
43
|
clearInterval(conn.logTailer);
|
|
@@ -27,6 +59,8 @@ const RULE_IDS = [
|
|
|
27
59
|
"heartbeat-fail",
|
|
28
60
|
"queue-depth",
|
|
29
61
|
"high-error-rate",
|
|
62
|
+
"cost-hourly-spike",
|
|
63
|
+
"cost-daily-budget",
|
|
30
64
|
"tool-errors",
|
|
31
65
|
"gateway-down",
|
|
32
66
|
];
|
|
@@ -195,7 +229,7 @@ function createLogTailer(res) {
|
|
|
195
229
|
}, 2000);
|
|
196
230
|
}
|
|
197
231
|
// ─── HTTP Handler ────────────────────────────────────────────────────────────
|
|
198
|
-
export function createDashboardHandler(getEngine) {
|
|
232
|
+
export function createDashboardHandler(getEngine, getCollections) {
|
|
199
233
|
return async (req, res) => {
|
|
200
234
|
const url = req.url ?? "";
|
|
201
235
|
if (!url.startsWith("/openalerts"))
|
|
@@ -307,6 +341,27 @@ export function createDashboardHandler(getEngine) {
|
|
|
307
341
|
res.end(body);
|
|
308
342
|
return true;
|
|
309
343
|
}
|
|
344
|
+
// ── GET /openalerts/collections → Sessions, actions, execs from CollectionManager ────
|
|
345
|
+
if (url === "/openalerts/collections" && req.method === "GET") {
|
|
346
|
+
const collections = getCollections();
|
|
347
|
+
if (!collections) {
|
|
348
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
349
|
+
res.end(JSON.stringify({ error: "Collections not available" }));
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
// getActiveSessions filters out stale idle sessions (>2h inactive) from filesystem hydration
|
|
353
|
+
const sessions = collections.getActiveSessions();
|
|
354
|
+
const actions = collections.getActions();
|
|
355
|
+
const execs = collections.getExecs();
|
|
356
|
+
const stats = collections.getStats();
|
|
357
|
+
res.writeHead(200, {
|
|
358
|
+
"Content-Type": "application/json",
|
|
359
|
+
"Cache-Control": "no-cache",
|
|
360
|
+
"Access-Control-Allow-Origin": "*",
|
|
361
|
+
});
|
|
362
|
+
res.end(JSON.stringify({ sessions, actions, execs, stats }));
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
310
365
|
// ── GET /openalerts/logs → OpenClaw log entries (for Logs tab) ────
|
|
311
366
|
if (url.startsWith("/openalerts/logs") && req.method === "GET") {
|
|
312
367
|
const urlObj = new URL(url, "http://localhost");
|
|
@@ -328,6 +383,59 @@ export function createDashboardHandler(getEngine) {
|
|
|
328
383
|
}));
|
|
329
384
|
return true;
|
|
330
385
|
}
|
|
386
|
+
// ── GET /openalerts/agent-monitor/stream → Live agent activity SSE ────
|
|
387
|
+
// Receives events from plugin hooks (before_agent_start, agent_end, after_tool_call, message_received)
|
|
388
|
+
if (url === "/openalerts/agent-monitor/stream" && req.method === "GET") {
|
|
389
|
+
res.writeHead(200, {
|
|
390
|
+
"Content-Type": "text/event-stream",
|
|
391
|
+
"Cache-Control": "no-cache",
|
|
392
|
+
Connection: "keep-alive",
|
|
393
|
+
"Access-Control-Allow-Origin": "*",
|
|
394
|
+
});
|
|
395
|
+
res.flushHeaders();
|
|
396
|
+
res.write(`:ok\n\n`);
|
|
397
|
+
// Replay buffered events so client sees full current session history
|
|
398
|
+
if (agentMonitorBuffer.length > 0) {
|
|
399
|
+
try {
|
|
400
|
+
res.write(`event: history\ndata: ${JSON.stringify(agentMonitorBuffer)}\n\n`);
|
|
401
|
+
}
|
|
402
|
+
catch { /* closed before flush */ }
|
|
403
|
+
}
|
|
404
|
+
const listener = (event) => {
|
|
405
|
+
try {
|
|
406
|
+
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
407
|
+
}
|
|
408
|
+
catch { /* closed */ }
|
|
409
|
+
};
|
|
410
|
+
monitorListeners.add(listener);
|
|
411
|
+
const heartbeat = setInterval(() => {
|
|
412
|
+
try {
|
|
413
|
+
res.write(`:heartbeat\n\n`);
|
|
414
|
+
}
|
|
415
|
+
catch { /* closed */ }
|
|
416
|
+
}, 15_000);
|
|
417
|
+
const conn = { res, heartbeat };
|
|
418
|
+
monitorConnections.add(conn);
|
|
419
|
+
req.on("close", () => {
|
|
420
|
+
clearInterval(heartbeat);
|
|
421
|
+
monitorListeners.delete(listener);
|
|
422
|
+
monitorConnections.delete(conn);
|
|
423
|
+
});
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
// ── GET /openalerts/test/events → Last 10 gateway events ────
|
|
427
|
+
if (url === "/openalerts/test/events" && req.method === "GET") {
|
|
428
|
+
res.writeHead(200, {
|
|
429
|
+
"Content-Type": "application/json",
|
|
430
|
+
"Cache-Control": "no-cache",
|
|
431
|
+
"Access-Control-Allow-Origin": "*",
|
|
432
|
+
});
|
|
433
|
+
res.end(JSON.stringify({
|
|
434
|
+
count: recentGatewayEvents.length,
|
|
435
|
+
events: recentGatewayEvents,
|
|
436
|
+
}));
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
331
439
|
// Unknown /openalerts sub-route
|
|
332
440
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
333
441
|
res.end("Not found");
|
|
@@ -54,8 +54,7 @@ export class GatewayClient extends EventEmitter {
|
|
|
54
54
|
});
|
|
55
55
|
this.ws.on("open", () => {
|
|
56
56
|
this.backoffMs = 1000;
|
|
57
|
-
//
|
|
58
|
-
setTimeout(() => this.sendConnectHandshake(), 800);
|
|
57
|
+
// Gateway sends 'connect.challenge' event first
|
|
59
58
|
});
|
|
60
59
|
this.ws.on("message", (data) => {
|
|
61
60
|
this.handleMessage(data.toString());
|
|
@@ -85,17 +84,20 @@ export class GatewayClient extends EventEmitter {
|
|
|
85
84
|
minProtocol: 3,
|
|
86
85
|
maxProtocol: 3,
|
|
87
86
|
client: {
|
|
88
|
-
id: "
|
|
87
|
+
id: "cli",
|
|
89
88
|
displayName: "OpenAlerts Monitor",
|
|
90
89
|
version: "0.1.0",
|
|
91
90
|
platform: process.platform,
|
|
92
|
-
mode: "
|
|
91
|
+
mode: "cli",
|
|
93
92
|
},
|
|
94
93
|
role: "operator",
|
|
95
|
-
scopes: ["operator.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
94
|
+
scopes: ["operator.read"],
|
|
95
|
+
caps: [],
|
|
96
|
+
commands: [],
|
|
97
|
+
permissions: {},
|
|
98
|
+
locale: "en-US",
|
|
99
|
+
userAgent: "openalerts-monitor/0.1.0",
|
|
100
|
+
auth: this.config.token ? { token: this.config.token } : undefined,
|
|
99
101
|
},
|
|
100
102
|
};
|
|
101
103
|
this.pending.set(id, {
|
|
@@ -112,6 +114,11 @@ export class GatewayClient extends EventEmitter {
|
|
|
112
114
|
handleMessage(raw) {
|
|
113
115
|
try {
|
|
114
116
|
const frame = JSON.parse(raw);
|
|
117
|
+
// Handle challenge-response auth
|
|
118
|
+
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
119
|
+
this.sendConnectHandshake();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
115
122
|
if (frame.type === "res") {
|
|
116
123
|
const pending = this.pending.get(frame.id);
|
|
117
124
|
if (pending) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -14,6 +14,26 @@
|
|
|
14
14
|
"quiet": { "type": "boolean" },
|
|
15
15
|
"rules": {
|
|
16
16
|
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"cost-hourly-spike": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"enabled": { "type": "boolean" },
|
|
22
|
+
"threshold": { "type": "number", "minimum": 0 },
|
|
23
|
+
"cooldownMinutes": { "type": "number", "minimum": 1 }
|
|
24
|
+
},
|
|
25
|
+
"additionalProperties": false
|
|
26
|
+
},
|
|
27
|
+
"cost-daily-budget": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"enabled": { "type": "boolean" },
|
|
31
|
+
"threshold": { "type": "number", "minimum": 0 },
|
|
32
|
+
"cooldownMinutes": { "type": "number", "minimum": 1 }
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
17
37
|
"additionalProperties": {
|
|
18
38
|
"type": "object",
|
|
19
39
|
"properties": {
|
|
@@ -52,6 +72,16 @@
|
|
|
52
72
|
"quiet": {
|
|
53
73
|
"label": "Quiet Mode",
|
|
54
74
|
"help": "Log alerts to file only — no messages sent to your channels."
|
|
75
|
+
},
|
|
76
|
+
"rules.cost-hourly-spike.threshold": {
|
|
77
|
+
"label": "Hourly Cost Threshold (USD)",
|
|
78
|
+
"help": "Alert when total LLM spend in the last 60 minutes exceeds this amount. Default: 5.0.",
|
|
79
|
+
"advanced": true
|
|
80
|
+
},
|
|
81
|
+
"rules.cost-daily-budget.threshold": {
|
|
82
|
+
"label": "Daily Budget Threshold (USD)",
|
|
83
|
+
"help": "Alert when total LLM spend in the last 24 hours exceeds this amount. Default: 20.0.",
|
|
84
|
+
"advanced": true
|
|
55
85
|
}
|
|
56
86
|
}
|
|
57
87
|
}
|