@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.
@@ -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
- /** Close all active SSE connections. Call on engine stop. */
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
- // Delay connect handshake slightly to allow gateway to send challenge
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: "gateway-client",
87
+ id: "cli",
89
88
  displayName: "OpenAlerts Monitor",
90
89
  version: "0.1.0",
91
90
  platform: process.platform,
92
- mode: "backend",
91
+ mode: "cli",
93
92
  },
94
93
  role: "operator",
95
- scopes: ["operator.admin"],
96
- auth: {
97
- token: this.config.token,
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) {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadwing/openalerts",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "OpenAlerts — An alerting layer for agentic frameworks",
6
6
  "author": "Steadwing",