openclaw-aegis 1.0.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.
@@ -0,0 +1,2063 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/index.ts
27
+ var import_commander5 = require("commander");
28
+
29
+ // src/cli/commands/status.ts
30
+ var import_commander = require("commander");
31
+
32
+ // src/config/loader.ts
33
+ var fs = __toESM(require("fs"));
34
+ var path = __toESM(require("path"));
35
+ var os = __toESM(require("os"));
36
+ var import_toml = __toESM(require("@iarna/toml"));
37
+
38
+ // src/config/schema.ts
39
+ var import_zod = require("zod");
40
+ var alertChannelSchema = import_zod.z.discriminatedUnion("type", [
41
+ import_zod.z.object({
42
+ type: import_zod.z.literal("ntfy"),
43
+ url: import_zod.z.string().url().default("https://ntfy.sh"),
44
+ topic: import_zod.z.string().min(1),
45
+ priority: import_zod.z.number().int().min(1).max(5).default(4)
46
+ }),
47
+ import_zod.z.object({
48
+ type: import_zod.z.literal("webhook"),
49
+ url: import_zod.z.string().url(),
50
+ secret: import_zod.z.string().min(1).optional()
51
+ }),
52
+ import_zod.z.object({
53
+ type: import_zod.z.literal("telegram"),
54
+ botToken: import_zod.z.string().min(1),
55
+ chatId: import_zod.z.string().min(1)
56
+ }),
57
+ import_zod.z.object({
58
+ type: import_zod.z.literal("whatsapp"),
59
+ phoneNumberId: import_zod.z.string().min(1),
60
+ accessToken: import_zod.z.string().min(1),
61
+ recipientNumber: import_zod.z.string().min(1)
62
+ }),
63
+ import_zod.z.object({
64
+ type: import_zod.z.literal("slack"),
65
+ webhookUrl: import_zod.z.string().url(),
66
+ channel: import_zod.z.string().optional()
67
+ }),
68
+ import_zod.z.object({
69
+ type: import_zod.z.literal("discord"),
70
+ webhookUrl: import_zod.z.string().url(),
71
+ username: import_zod.z.string().optional()
72
+ }),
73
+ import_zod.z.object({
74
+ type: import_zod.z.literal("email"),
75
+ host: import_zod.z.string().min(1),
76
+ port: import_zod.z.number().int().min(1).max(65535).default(587),
77
+ secure: import_zod.z.boolean().default(false),
78
+ username: import_zod.z.string().min(1),
79
+ password: import_zod.z.string().min(1),
80
+ from: import_zod.z.string().min(1),
81
+ to: import_zod.z.string().min(1)
82
+ }),
83
+ import_zod.z.object({
84
+ type: import_zod.z.literal("pushover"),
85
+ apiToken: import_zod.z.string().min(1),
86
+ userKey: import_zod.z.string().min(1),
87
+ device: import_zod.z.string().optional()
88
+ })
89
+ ]);
90
+ var aegisConfigSchema = import_zod.z.object({
91
+ gateway: import_zod.z.object({
92
+ configPath: import_zod.z.string().default("~/.openclaw/openclaw.json"),
93
+ pidFile: import_zod.z.string().default("openclaw-gateway.service"),
94
+ port: import_zod.z.number().int().min(1).max(65535).default(18789),
95
+ logPath: import_zod.z.string().default("~/.openclaw/logs/gateway.log"),
96
+ healthEndpoint: import_zod.z.string().default("/health")
97
+ }).default({}),
98
+ monitoring: import_zod.z.object({
99
+ intervalMs: import_zod.z.number().int().min(1e3).default(1e4),
100
+ probeTimeoutMs: import_zod.z.number().int().min(500).default(5e3),
101
+ configPollIntervalMs: import_zod.z.number().int().min(500).default(2e3),
102
+ degradedConfirmationCount: import_zod.z.number().int().min(1).default(2)
103
+ }).default({}),
104
+ health: import_zod.z.object({
105
+ healthyMin: import_zod.z.number().int().min(0).default(7),
106
+ degradedMin: import_zod.z.number().int().min(0).default(4),
107
+ memoryThresholdMb: import_zod.z.number().int().min(0).default(512),
108
+ cpuThresholdPercent: import_zod.z.number().int().min(0).max(100).default(90),
109
+ diskThresholdMb: import_zod.z.number().int().min(0).default(100)
110
+ }).default({}),
111
+ recovery: import_zod.z.object({
112
+ l1MaxAttempts: import_zod.z.number().int().min(1).default(3),
113
+ l1BackoffBaseMs: import_zod.z.number().int().min(1e3).default(5e3),
114
+ l1BackoffMultiplier: import_zod.z.number().min(1).default(3),
115
+ l2MaxAttempts: import_zod.z.number().int().min(1).default(2),
116
+ l2CooldownMs: import_zod.z.number().int().min(1e3).default(6e4),
117
+ circuitBreakerMaxCycles: import_zod.z.number().int().min(1).default(3),
118
+ circuitBreakerWindowMs: import_zod.z.number().int().min(6e4).default(36e5),
119
+ antiFlap: import_zod.z.object({
120
+ maxRestarts: import_zod.z.number().int().min(1).default(5),
121
+ windowMs: import_zod.z.number().int().min(6e4).default(9e5),
122
+ cooldownMs: import_zod.z.number().int().min(6e4).default(6e5),
123
+ decayMs: import_zod.z.number().int().min(6e4).default(216e5)
124
+ }).default({})
125
+ }).default({}),
126
+ backup: import_zod.z.object({
127
+ maxChronological: import_zod.z.number().int().min(1).default(20),
128
+ maxKnownGood: import_zod.z.number().int().min(1).default(3),
129
+ knownGoodStabilityMs: import_zod.z.number().int().min(1e4).default(6e4),
130
+ basePath: import_zod.z.string().default("~/.openclaw/aegis/backups")
131
+ }).default({}),
132
+ deadManSwitch: import_zod.z.object({
133
+ countdownMs: import_zod.z.number().int().min(5e3).default(3e4),
134
+ enabled: import_zod.z.boolean().default(true)
135
+ }).default({}),
136
+ alerts: import_zod.z.object({
137
+ channels: import_zod.z.array(alertChannelSchema).default([]),
138
+ retryAttempts: import_zod.z.number().int().min(0).default(3),
139
+ retryBackoffMs: import_zod.z.array(import_zod.z.number().int()).default([5e3, 15e3, 45e3])
140
+ }).default({}),
141
+ platform: import_zod.z.object({
142
+ type: import_zod.z.enum(["systemd", "launchd"]).default("systemd"),
143
+ watchdogSec: import_zod.z.number().int().min(10).default(30)
144
+ }).default({})
145
+ });
146
+
147
+ // src/config/loader.ts
148
+ function expandHome(filepath) {
149
+ if (filepath.startsWith("~/")) {
150
+ return path.join(os.homedir(), filepath.slice(2));
151
+ }
152
+ return filepath;
153
+ }
154
+ function resolveConfigPaths(config) {
155
+ return {
156
+ ...config,
157
+ gateway: {
158
+ ...config.gateway,
159
+ configPath: expandHome(config.gateway.configPath),
160
+ pidFile: expandHome(config.gateway.pidFile),
161
+ logPath: expandHome(config.gateway.logPath)
162
+ },
163
+ backup: {
164
+ ...config.backup,
165
+ basePath: expandHome(config.backup.basePath)
166
+ }
167
+ };
168
+ }
169
+ function loadConfig(configPath) {
170
+ const resolvedPath = expandHome(configPath);
171
+ if (!fs.existsSync(resolvedPath)) {
172
+ const defaults = aegisConfigSchema.parse({});
173
+ return resolveConfigPaths(defaults);
174
+ }
175
+ const raw = fs.readFileSync(resolvedPath, "utf-8");
176
+ const parsed = import_toml.default.parse(raw);
177
+ const validated = aegisConfigSchema.parse(parsed);
178
+ return resolveConfigPaths(validated);
179
+ }
180
+ var DEFAULT_CONFIG_PATH = "~/.openclaw/aegis/config.toml";
181
+ function getConfigDir() {
182
+ return expandHome("~/.openclaw/aegis");
183
+ }
184
+ function ensureConfigDir() {
185
+ const dir = getConfigDir();
186
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
187
+ }
188
+
189
+ // src/health/monitor.ts
190
+ var import_node_events = require("events");
191
+
192
+ // src/types/index.ts
193
+ var PROBE_WEIGHTS = {
194
+ process: 2,
195
+ port: 2,
196
+ http: 2,
197
+ config: 2,
198
+ websocket: 1,
199
+ tun: 1,
200
+ memory: 1,
201
+ cpu: 1,
202
+ disk: 1,
203
+ logTail: 1
204
+ };
205
+ var MAX_HEALTH_SCORE = Object.values(PROBE_WEIGHTS).reduce((a, b) => a + b, 0) * 2;
206
+
207
+ // src/health/scoring.ts
208
+ var DEFAULT_THRESHOLDS = {
209
+ healthyMin: 7,
210
+ degradedMin: 4
211
+ };
212
+ function computeHealthScore(results, thresholds = DEFAULT_THRESHOLDS) {
213
+ let rawTotal = 0;
214
+ for (const result of results) {
215
+ const weight = PROBE_WEIGHTS[result.name] ?? 1;
216
+ rawTotal += result.score * weight;
217
+ }
218
+ const normalized = MAX_HEALTH_SCORE > 0 ? Math.round(rawTotal / MAX_HEALTH_SCORE * 10) : 0;
219
+ const band = classifyBand(normalized, thresholds);
220
+ return { total: normalized, band, probeResults: results };
221
+ }
222
+ function classifyBand(score, thresholds) {
223
+ if (score >= thresholds.healthyMin) return "healthy";
224
+ if (score >= thresholds.degradedMin) return "degraded";
225
+ return "critical";
226
+ }
227
+ var DegradedConfirmation = class {
228
+ consecutiveDegradedCount = 0;
229
+ requiredCount;
230
+ constructor(requiredCount = 2) {
231
+ this.requiredCount = requiredCount;
232
+ }
233
+ update(band) {
234
+ if (band === "degraded") {
235
+ this.consecutiveDegradedCount++;
236
+ return this.consecutiveDegradedCount >= this.requiredCount;
237
+ }
238
+ if (band === "critical") {
239
+ this.consecutiveDegradedCount = 0;
240
+ return true;
241
+ }
242
+ this.consecutiveDegradedCount = 0;
243
+ return false;
244
+ }
245
+ reset() {
246
+ this.consecutiveDegradedCount = 0;
247
+ }
248
+ getCount() {
249
+ return this.consecutiveDegradedCount;
250
+ }
251
+ };
252
+
253
+ // src/health/probes/resolve-pid.ts
254
+ var fs2 = __toESM(require("fs"));
255
+ var import_node_child_process = require("child_process");
256
+ function resolvePid(pidSource) {
257
+ if (pidSource.endsWith(".service") || !pidSource.includes("/")) {
258
+ const pid = resolveFromSystemd(pidSource);
259
+ if (pid !== null) return pid;
260
+ }
261
+ return resolveFromFile(pidSource);
262
+ }
263
+ function resolveFromSystemd(unit) {
264
+ try {
265
+ const output = (0, import_node_child_process.execFileSync)(
266
+ "systemctl",
267
+ ["--user", "show", "-p", "MainPID", "--value", unit],
268
+ { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
269
+ ).trim();
270
+ const pid = parseInt(output, 10);
271
+ if (!isNaN(pid) && pid > 0) return pid;
272
+ return null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ function resolveFromFile(pidFile) {
278
+ try {
279
+ if (!fs2.existsSync(pidFile)) return null;
280
+ const pidStr = fs2.readFileSync(pidFile, "utf-8").trim();
281
+ const pid = parseInt(pidStr, 10);
282
+ if (!isNaN(pid) && pid > 0) return pid;
283
+ return null;
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ // src/health/probes/process.ts
290
+ async function processProbe(_target, pidSource) {
291
+ const start = Date.now();
292
+ try {
293
+ const pid = resolvePid(pidSource);
294
+ if (pid === null) {
295
+ return {
296
+ name: "process",
297
+ healthy: false,
298
+ score: 0,
299
+ message: "Gateway process not found (checked systemd unit and PID file)",
300
+ latencyMs: Date.now() - start
301
+ };
302
+ }
303
+ try {
304
+ process.kill(pid, 0);
305
+ return {
306
+ name: "process",
307
+ healthy: true,
308
+ score: 2,
309
+ latencyMs: Date.now() - start
310
+ };
311
+ } catch {
312
+ return {
313
+ name: "process",
314
+ healthy: false,
315
+ score: 0,
316
+ message: `Process ${pid} not running (stale PID)`,
317
+ latencyMs: Date.now() - start
318
+ };
319
+ }
320
+ } catch (err) {
321
+ return {
322
+ name: "process",
323
+ healthy: false,
324
+ score: 0,
325
+ message: `Process probe error: ${err instanceof Error ? err.message : String(err)}`,
326
+ latencyMs: Date.now() - start
327
+ };
328
+ }
329
+ }
330
+
331
+ // src/health/probes/port.ts
332
+ var net = __toESM(require("net"));
333
+ async function portProbe(target, port, timeoutMs = 5e3) {
334
+ const start = Date.now();
335
+ const host = target.type === "remote" ? target.host : "127.0.0.1";
336
+ return new Promise((resolve) => {
337
+ const socket = new net.Socket();
338
+ let resolved = false;
339
+ const finish = (result) => {
340
+ if (!resolved) {
341
+ resolved = true;
342
+ socket.destroy();
343
+ resolve(result);
344
+ }
345
+ };
346
+ socket.setTimeout(timeoutMs);
347
+ socket.connect(port, host, () => {
348
+ finish({
349
+ name: "port",
350
+ healthy: true,
351
+ score: 2,
352
+ latencyMs: Date.now() - start
353
+ });
354
+ });
355
+ socket.on("error", (err) => {
356
+ finish({
357
+ name: "port",
358
+ healthy: false,
359
+ score: 0,
360
+ message: `Port ${port} unreachable: ${err.message}`,
361
+ latencyMs: Date.now() - start
362
+ });
363
+ });
364
+ socket.on("timeout", () => {
365
+ finish({
366
+ name: "port",
367
+ healthy: false,
368
+ score: 0,
369
+ message: `Port ${port} connection timed out after ${timeoutMs}ms`,
370
+ latencyMs: Date.now() - start
371
+ });
372
+ });
373
+ });
374
+ }
375
+
376
+ // src/health/probes/http.ts
377
+ var http = __toESM(require("http"));
378
+ async function httpHealthProbe(target, port, endpoint = "/health", timeoutMs = 5e3) {
379
+ const start = Date.now();
380
+ const host = target.type === "remote" ? target.host : "127.0.0.1";
381
+ return new Promise((resolve) => {
382
+ const req = http.get(
383
+ {
384
+ hostname: host,
385
+ port,
386
+ path: endpoint,
387
+ timeout: timeoutMs
388
+ },
389
+ (res) => {
390
+ const statusCode = res.statusCode ?? 0;
391
+ res.resume();
392
+ if (statusCode >= 200 && statusCode < 300) {
393
+ resolve({
394
+ name: "http",
395
+ healthy: true,
396
+ score: 2,
397
+ latencyMs: Date.now() - start
398
+ });
399
+ } else {
400
+ resolve({
401
+ name: "http",
402
+ healthy: false,
403
+ score: 0,
404
+ message: `HTTP health returned status ${statusCode}`,
405
+ latencyMs: Date.now() - start
406
+ });
407
+ }
408
+ }
409
+ );
410
+ req.on("error", (err) => {
411
+ resolve({
412
+ name: "http",
413
+ healthy: false,
414
+ score: 0,
415
+ message: `HTTP health probe failed: ${err.message}`,
416
+ latencyMs: Date.now() - start
417
+ });
418
+ });
419
+ req.on("timeout", () => {
420
+ req.destroy();
421
+ resolve({
422
+ name: "http",
423
+ healthy: false,
424
+ score: 0,
425
+ message: `HTTP health probe timed out after ${timeoutMs}ms`,
426
+ latencyMs: Date.now() - start
427
+ });
428
+ });
429
+ });
430
+ }
431
+
432
+ // src/health/probes/config.ts
433
+ var fs3 = __toESM(require("fs"));
434
+ var REQUIRED_CONFIG_PATHS = [
435
+ { path: ["gateway", "port"], label: "gateway.port" }
436
+ ];
437
+ var POISON_KEYS = ["autoAck", "autoAckMessage"];
438
+ async function configProbe(_target, configPath) {
439
+ const start = Date.now();
440
+ try {
441
+ if (!fs3.existsSync(configPath)) {
442
+ return {
443
+ name: "config",
444
+ healthy: false,
445
+ score: 0,
446
+ message: "Gateway config file not found",
447
+ latencyMs: Date.now() - start
448
+ };
449
+ }
450
+ const raw = fs3.readFileSync(configPath, "utf-8");
451
+ let parsed;
452
+ try {
453
+ parsed = JSON.parse(raw);
454
+ } catch {
455
+ return {
456
+ name: "config",
457
+ healthy: false,
458
+ score: 0,
459
+ message: "Gateway config is not valid JSON",
460
+ latencyMs: Date.now() - start
461
+ };
462
+ }
463
+ const missingPaths = REQUIRED_CONFIG_PATHS.filter(({ path: path3 }) => {
464
+ let obj = parsed;
465
+ for (const key of path3) {
466
+ if (obj === null || typeof obj !== "object" || !(key in obj)) return true;
467
+ obj = obj[key];
468
+ }
469
+ return false;
470
+ });
471
+ if (missingPaths.length > 0) {
472
+ return {
473
+ name: "config",
474
+ healthy: false,
475
+ score: 0,
476
+ message: `Missing required config keys: ${missingPaths.map((p) => p.label).join(", ")}`,
477
+ latencyMs: Date.now() - start
478
+ };
479
+ }
480
+ const foundPoison = POISON_KEYS.filter((key) => key in parsed);
481
+ if (foundPoison.length > 0) {
482
+ return {
483
+ name: "config",
484
+ healthy: false,
485
+ score: 0,
486
+ message: `Poison keys detected: ${foundPoison.join(", ")}`,
487
+ latencyMs: Date.now() - start
488
+ };
489
+ }
490
+ return {
491
+ name: "config",
492
+ healthy: true,
493
+ score: 2,
494
+ latencyMs: Date.now() - start
495
+ };
496
+ } catch (err) {
497
+ return {
498
+ name: "config",
499
+ healthy: false,
500
+ score: 0,
501
+ message: `Config probe error: ${err instanceof Error ? err.message : String(err)}`,
502
+ latencyMs: Date.now() - start
503
+ };
504
+ }
505
+ }
506
+
507
+ // src/health/probes/tun.ts
508
+ var fs4 = __toESM(require("fs"));
509
+ var import_node_child_process2 = require("child_process");
510
+ var import_node_util = require("util");
511
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
512
+ async function tunProbe(_target) {
513
+ const start = Date.now();
514
+ try {
515
+ const tunDevices = fs4.readdirSync("/sys/class/net").filter((dev) => {
516
+ try {
517
+ const type = fs4.readFileSync(`/sys/class/net/${dev}/type`, "utf-8").trim();
518
+ return type === "65534";
519
+ } catch {
520
+ return false;
521
+ }
522
+ });
523
+ if (tunDevices.length === 0) {
524
+ return {
525
+ name: "tun",
526
+ healthy: true,
527
+ score: 2,
528
+ message: "No TUN device configured (not required)",
529
+ latencyMs: Date.now() - start
530
+ };
531
+ }
532
+ const tunDev = tunDevices[0];
533
+ if (!tunDev) {
534
+ return {
535
+ name: "tun",
536
+ healthy: true,
537
+ score: 2,
538
+ message: "No TUN device configured (not required)",
539
+ latencyMs: Date.now() - start
540
+ };
541
+ }
542
+ const { stdout } = await execFileAsync("ip", ["link", "show", tunDev]);
543
+ const isUp = stdout.includes("state UP") || stdout.includes(",UP");
544
+ return {
545
+ name: "tun",
546
+ healthy: isUp,
547
+ score: isUp ? 2 : 0,
548
+ message: isUp ? void 0 : `TUN device ${tunDev} is DOWN`,
549
+ latencyMs: Date.now() - start
550
+ };
551
+ } catch {
552
+ return {
553
+ name: "tun",
554
+ healthy: true,
555
+ score: 2,
556
+ message: "TUN probe skipped (not available on this platform)",
557
+ latencyMs: Date.now() - start
558
+ };
559
+ }
560
+ }
561
+
562
+ // src/health/probes/memory.ts
563
+ var fs5 = __toESM(require("fs"));
564
+ async function memoryProbe(_target, pidSource, thresholdMb = 512) {
565
+ const start = Date.now();
566
+ try {
567
+ const pid = resolvePid(pidSource);
568
+ if (pid === null) {
569
+ return {
570
+ name: "memory",
571
+ healthy: false,
572
+ score: 0,
573
+ message: "Gateway process not found \u2014 cannot check memory",
574
+ latencyMs: Date.now() - start
575
+ };
576
+ }
577
+ const statusPath = `/proc/${pid}/status`;
578
+ if (!fs5.existsSync(statusPath)) {
579
+ return {
580
+ name: "memory",
581
+ healthy: false,
582
+ score: 0,
583
+ message: `Process ${pid} not found in /proc`,
584
+ latencyMs: Date.now() - start
585
+ };
586
+ }
587
+ const status = fs5.readFileSync(statusPath, "utf-8");
588
+ const rssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
589
+ if (!rssMatch) {
590
+ return {
591
+ name: "memory",
592
+ healthy: true,
593
+ score: 1,
594
+ message: "Could not parse RSS from /proc status",
595
+ latencyMs: Date.now() - start
596
+ };
597
+ }
598
+ const rssKb = parseInt(rssMatch[1] ?? "0", 10);
599
+ const rssMb = rssKb / 1024;
600
+ const healthy = rssMb < thresholdMb;
601
+ return {
602
+ name: "memory",
603
+ healthy,
604
+ score: healthy ? 2 : 0,
605
+ message: healthy ? void 0 : `RSS ${rssMb.toFixed(0)}MB exceeds threshold ${thresholdMb}MB`,
606
+ latencyMs: Date.now() - start
607
+ };
608
+ } catch (err) {
609
+ return {
610
+ name: "memory",
611
+ healthy: false,
612
+ score: 0,
613
+ message: `Memory probe error: ${err instanceof Error ? err.message : String(err)}`,
614
+ latencyMs: Date.now() - start
615
+ };
616
+ }
617
+ }
618
+
619
+ // src/health/probes/cpu.ts
620
+ var fs6 = __toESM(require("fs"));
621
+ async function cpuProbe(_target, pidSource, thresholdPercent = 90, sampleMs = 1e3) {
622
+ const start = Date.now();
623
+ try {
624
+ const pid = resolvePid(pidSource);
625
+ if (pid === null) {
626
+ return {
627
+ name: "cpu",
628
+ healthy: false,
629
+ score: 0,
630
+ message: "Gateway process not found \u2014 cannot check CPU",
631
+ latencyMs: Date.now() - start
632
+ };
633
+ }
634
+ const statPath = `/proc/${pid}/stat`;
635
+ if (!fs6.existsSync(statPath)) {
636
+ return {
637
+ name: "cpu",
638
+ healthy: false,
639
+ score: 0,
640
+ message: `Process ${pid} not found in /proc`,
641
+ latencyMs: Date.now() - start
642
+ };
643
+ }
644
+ const readCpuTime = () => {
645
+ const stat = fs6.readFileSync(statPath, "utf-8");
646
+ const fields = stat.split(" ");
647
+ const utime = parseInt(fields[13] ?? "0", 10) || 0;
648
+ const stime = parseInt(fields[14] ?? "0", 10) || 0;
649
+ return utime + stime;
650
+ };
651
+ const cpuTime1 = readCpuTime();
652
+ await new Promise((r) => setTimeout(r, sampleMs));
653
+ const cpuTime2 = readCpuTime();
654
+ const clockTicks = 100;
655
+ const cpuDelta = (cpuTime2 - cpuTime1) / clockTicks;
656
+ const cpuPercent = cpuDelta / (sampleMs / 1e3) * 100;
657
+ const healthy = cpuPercent < thresholdPercent;
658
+ return {
659
+ name: "cpu",
660
+ healthy,
661
+ score: healthy ? 2 : 0,
662
+ message: healthy ? void 0 : `CPU ${cpuPercent.toFixed(1)}% exceeds threshold ${thresholdPercent}%`,
663
+ latencyMs: Date.now() - start
664
+ };
665
+ } catch (err) {
666
+ return {
667
+ name: "cpu",
668
+ healthy: false,
669
+ score: 0,
670
+ message: `CPU probe error: ${err instanceof Error ? err.message : String(err)}`,
671
+ latencyMs: Date.now() - start
672
+ };
673
+ }
674
+ }
675
+
676
+ // src/health/probes/disk.ts
677
+ var import_node_child_process3 = require("child_process");
678
+ var import_node_util2 = require("util");
679
+ var path2 = __toESM(require("path"));
680
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
681
+ async function diskProbe(_target, configPath, thresholdMb = 100) {
682
+ const start = Date.now();
683
+ try {
684
+ const dir = path2.dirname(configPath);
685
+ const { stdout } = await execFileAsync2("df", ["-BM", "--output=avail", dir]);
686
+ const lines = stdout.trim().split("\n");
687
+ const lastLine = lines[lines.length - 1];
688
+ if (!lastLine) {
689
+ return {
690
+ name: "disk",
691
+ healthy: true,
692
+ score: 1,
693
+ message: "Could not parse df output",
694
+ latencyMs: Date.now() - start
695
+ };
696
+ }
697
+ const availStr = lastLine.trim().replace("M", "");
698
+ const availMb = parseInt(availStr, 10);
699
+ if (isNaN(availMb)) {
700
+ return {
701
+ name: "disk",
702
+ healthy: true,
703
+ score: 1,
704
+ message: "Could not parse disk space",
705
+ latencyMs: Date.now() - start
706
+ };
707
+ }
708
+ const healthy = availMb >= thresholdMb;
709
+ return {
710
+ name: "disk",
711
+ healthy,
712
+ score: healthy ? 2 : 0,
713
+ message: healthy ? void 0 : `Only ${availMb}MB available (threshold: ${thresholdMb}MB)`,
714
+ latencyMs: Date.now() - start
715
+ };
716
+ } catch (err) {
717
+ return {
718
+ name: "disk",
719
+ healthy: true,
720
+ score: 1,
721
+ message: `Disk probe fallback: ${err instanceof Error ? err.message : String(err)}`,
722
+ latencyMs: Date.now() - start
723
+ };
724
+ }
725
+ }
726
+
727
+ // src/health/probes/log-tail.ts
728
+ var fs7 = __toESM(require("fs"));
729
+ var ERROR_PATTERNS = [
730
+ /ECONNRESET/,
731
+ /SIGTERM/,
732
+ /SIGKILL/,
733
+ /ENOMEM/,
734
+ /fatal\s+error/i,
735
+ /uncaught\s+exception/i,
736
+ /unhandled\s+rejection/i,
737
+ /out\s+of\s+memory/i,
738
+ /EACCES/,
739
+ /ENOSPC/
740
+ ];
741
+ async function logTailProbe(_target, logPath, tailLines = 50) {
742
+ const start = Date.now();
743
+ try {
744
+ if (!fs7.existsSync(logPath)) {
745
+ return {
746
+ name: "logTail",
747
+ healthy: true,
748
+ score: 2,
749
+ message: "Log file not found (may not exist yet)",
750
+ latencyMs: Date.now() - start
751
+ };
752
+ }
753
+ const content = fs7.readFileSync(logPath, "utf-8");
754
+ const lines = content.split("\n").slice(-tailLines);
755
+ const recentText = lines.join("\n");
756
+ const matchedPatterns = ERROR_PATTERNS.filter((p) => p.test(recentText));
757
+ if (matchedPatterns.length === 0) {
758
+ return {
759
+ name: "logTail",
760
+ healthy: true,
761
+ score: 2,
762
+ latencyMs: Date.now() - start
763
+ };
764
+ }
765
+ const score = matchedPatterns.length >= 3 ? 0 : 1;
766
+ return {
767
+ name: "logTail",
768
+ healthy: false,
769
+ score,
770
+ message: `Error patterns in recent logs: ${matchedPatterns.map((p) => p.source).join(", ")}`,
771
+ latencyMs: Date.now() - start
772
+ };
773
+ } catch (err) {
774
+ return {
775
+ name: "logTail",
776
+ healthy: true,
777
+ score: 1,
778
+ message: `Log tail probe fallback: ${err instanceof Error ? err.message : String(err)}`,
779
+ latencyMs: Date.now() - start
780
+ };
781
+ }
782
+ }
783
+
784
+ // src/health/probes/websocket.ts
785
+ var import_ws = __toESM(require("ws"));
786
+ async function websocketProbe(target, port, timeoutMs = 5e3) {
787
+ const start = Date.now();
788
+ const host = target.type === "remote" ? target.host : "127.0.0.1";
789
+ const url = `ws://${host}:${port}`;
790
+ return new Promise((resolve) => {
791
+ let resolved = false;
792
+ const finish = (result) => {
793
+ if (!resolved) {
794
+ resolved = true;
795
+ resolve(result);
796
+ }
797
+ };
798
+ const timer = setTimeout(() => {
799
+ ws.terminate();
800
+ finish({
801
+ name: "websocket",
802
+ healthy: false,
803
+ score: 0,
804
+ message: `WebSocket handshake timed out after ${timeoutMs}ms`,
805
+ latencyMs: Date.now() - start
806
+ });
807
+ }, timeoutMs);
808
+ const ws = new import_ws.default(url, { handshakeTimeout: timeoutMs });
809
+ ws.on("open", () => {
810
+ clearTimeout(timer);
811
+ ws.close();
812
+ finish({
813
+ name: "websocket",
814
+ healthy: true,
815
+ score: 2,
816
+ latencyMs: Date.now() - start
817
+ });
818
+ });
819
+ ws.on("error", (err) => {
820
+ clearTimeout(timer);
821
+ ws.terminate();
822
+ finish({
823
+ name: "websocket",
824
+ healthy: false,
825
+ score: 0,
826
+ message: `WebSocket probe failed: ${err.message}`,
827
+ latencyMs: Date.now() - start
828
+ });
829
+ });
830
+ });
831
+ }
832
+
833
+ // src/health/monitor.ts
834
+ var HealthMonitor = class extends import_node_events.EventEmitter {
835
+ constructor(config) {
836
+ super();
837
+ this.config = config;
838
+ this.degradedConfirmation = new DegradedConfirmation(
839
+ config.monitoring.degradedConfirmationCount
840
+ );
841
+ }
842
+ interval = null;
843
+ degradedConfirmation;
844
+ lastScore = null;
845
+ async runAllProbes() {
846
+ const target = { type: "local" };
847
+ const timeout = this.config.monitoring.probeTimeoutMs;
848
+ const withTimeout = async (fn, name) => {
849
+ try {
850
+ return await Promise.race([
851
+ fn(),
852
+ new Promise(
853
+ (resolve) => setTimeout(
854
+ () => resolve({
855
+ name,
856
+ healthy: false,
857
+ score: 0,
858
+ message: `Probe timed out after ${timeout}ms`,
859
+ latencyMs: timeout
860
+ }),
861
+ timeout
862
+ )
863
+ )
864
+ ]);
865
+ } catch (err) {
866
+ return {
867
+ name,
868
+ healthy: false,
869
+ score: 0,
870
+ message: `Probe error: ${err instanceof Error ? err.message : String(err)}`,
871
+ latencyMs: 0
872
+ };
873
+ }
874
+ };
875
+ const results = await Promise.allSettled([
876
+ withTimeout(() => processProbe(target, this.config.gateway.pidFile), "process"),
877
+ withTimeout(() => portProbe(target, this.config.gateway.port, timeout), "port"),
878
+ withTimeout(
879
+ () => httpHealthProbe(
880
+ target,
881
+ this.config.gateway.port,
882
+ this.config.gateway.healthEndpoint,
883
+ timeout
884
+ ),
885
+ "http"
886
+ ),
887
+ withTimeout(() => configProbe(target, this.config.gateway.configPath), "config"),
888
+ withTimeout(() => tunProbe(target), "tun"),
889
+ withTimeout(
890
+ () => memoryProbe(target, this.config.gateway.pidFile, this.config.health.memoryThresholdMb),
891
+ "memory"
892
+ ),
893
+ withTimeout(
894
+ () => cpuProbe(target, this.config.gateway.pidFile, this.config.health.cpuThresholdPercent),
895
+ "cpu"
896
+ ),
897
+ withTimeout(
898
+ () => diskProbe(target, this.config.gateway.configPath, this.config.health.diskThresholdMb),
899
+ "disk"
900
+ ),
901
+ withTimeout(() => logTailProbe(target, this.config.gateway.logPath), "logTail"),
902
+ withTimeout(
903
+ () => websocketProbe(target, this.config.gateway.port, timeout),
904
+ "websocket"
905
+ )
906
+ ]);
907
+ const probeResults = results.map((r, i) => {
908
+ if (r.status === "fulfilled") return r.value;
909
+ const names = [
910
+ "process",
911
+ "port",
912
+ "http",
913
+ "config",
914
+ "tun",
915
+ "memory",
916
+ "cpu",
917
+ "disk",
918
+ "logTail",
919
+ "websocket"
920
+ ];
921
+ return {
922
+ name: names[i] ?? "unknown",
923
+ healthy: false,
924
+ score: 0,
925
+ message: `Probe rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`,
926
+ latencyMs: 0
927
+ };
928
+ });
929
+ const score = computeHealthScore(probeResults, {
930
+ healthyMin: this.config.health.healthyMin,
931
+ degradedMin: this.config.health.degradedMin
932
+ });
933
+ this.lastScore = score;
934
+ const shouldEscalate = this.degradedConfirmation.update(score.band);
935
+ this.emit("check", score);
936
+ if (shouldEscalate && score.band !== "healthy") {
937
+ this.emit("escalate", score);
938
+ }
939
+ return score;
940
+ }
941
+ start() {
942
+ this.interval = setInterval(() => {
943
+ void this.runAllProbes();
944
+ }, this.config.monitoring.intervalMs);
945
+ }
946
+ stop() {
947
+ if (this.interval) {
948
+ clearInterval(this.interval);
949
+ this.interval = null;
950
+ }
951
+ }
952
+ getLastScore() {
953
+ return this.lastScore;
954
+ }
955
+ };
956
+
957
+ // src/cli/commands/status.ts
958
+ var statusCommand = new import_commander.Command("status").description("Show current health status and recovery stats").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).action(async (opts) => {
959
+ const config = loadConfig(opts.config);
960
+ const monitor = new HealthMonitor(config);
961
+ const score = await monitor.runAllProbes();
962
+ const bandColors = {
963
+ healthy: "\x1B[32m",
964
+ degraded: "\x1B[33m",
965
+ critical: "\x1B[31m"
966
+ };
967
+ const reset = "\x1B[0m";
968
+ const color = bandColors[score.band] ?? "";
969
+ process.stdout.write(`
970
+ Health: ${color}${score.band.toUpperCase()}${reset} (score: ${score.total})
971
+
972
+ `);
973
+ for (const probe of score.probeResults) {
974
+ const icon = probe.healthy ? "\x1B[32m+\x1B[0m" : "\x1B[31m-\x1B[0m";
975
+ const msg = probe.message ? ` \u2014 ${probe.message}` : "";
976
+ process.stdout.write(` ${icon} ${probe.name} (${probe.latencyMs}ms)${msg}
977
+ `);
978
+ }
979
+ if (config.alerts.channels.length === 0) {
980
+ process.stdout.write(
981
+ `
982
+ \x1B[33mWARNING: No alert channels configured. Aegis cannot notify you during incidents. Run 'aegis init' to add alerts.\x1B[0m
983
+ `
984
+ );
985
+ }
986
+ process.stdout.write("\n");
987
+ });
988
+
989
+ // src/cli/commands/check.ts
990
+ var import_commander2 = require("commander");
991
+ var checkCommand = new import_commander2.Command("check").description("Run all health probes once and exit").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).option("--json", "Output as JSON").action(async (opts) => {
992
+ const config = loadConfig(opts.config);
993
+ const monitor = new HealthMonitor(config);
994
+ const score = await monitor.runAllProbes();
995
+ if (opts.json) {
996
+ process.stdout.write(JSON.stringify(score, null, 2) + "\n");
997
+ } else {
998
+ const failed = score.probeResults.filter((p) => !p.healthy);
999
+ process.stdout.write(`Health: ${score.band.toUpperCase()} (score: ${score.total})
1000
+ `);
1001
+ process.stdout.write(`Probes: ${score.probeResults.length - failed.length} passed, ${failed.length} failed
1002
+ `);
1003
+ if (failed.length > 0) {
1004
+ process.stdout.write("\nFailed probes:\n");
1005
+ for (const probe of failed) {
1006
+ process.stdout.write(` - ${probe.name}: ${probe.message ?? "failed"}
1007
+ `);
1008
+ }
1009
+ }
1010
+ }
1011
+ process.exit(score.band === "healthy" ? 0 : 1);
1012
+ });
1013
+
1014
+ // src/cli/commands/init.ts
1015
+ var import_commander3 = require("commander");
1016
+ var fs8 = __toESM(require("fs"));
1017
+ var readline = __toESM(require("readline"));
1018
+ function ask(rl, question, defaultValue) {
1019
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
1020
+ return new Promise((resolve) => {
1021
+ rl.question(`${question}${suffix}: `, (answer) => {
1022
+ resolve(answer.trim() || defaultValue || "");
1023
+ });
1024
+ });
1025
+ }
1026
+ function detectPort() {
1027
+ try {
1028
+ const configPath = expandHome("~/.openclaw/openclaw.json");
1029
+ if (fs8.existsSync(configPath)) {
1030
+ const raw = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
1031
+ if (raw?.gateway?.port) return raw.gateway.port;
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ return 3e3;
1036
+ }
1037
+ function generateToml(opts) {
1038
+ let toml = `# OpenClaw Aegis Configuration
1039
+ # Generated by 'aegis init'
1040
+
1041
+ [gateway]
1042
+ configPath = "~/.openclaw/openclaw.json"
1043
+ pidFile = "openclaw-gateway.service"
1044
+ port = ${opts.port}
1045
+ logPath = "~/.openclaw/logs/gateway.log"
1046
+ healthEndpoint = "/health"
1047
+
1048
+ [monitoring]
1049
+ intervalMs = 10000
1050
+ probeTimeoutMs = 5000
1051
+
1052
+ [health]
1053
+ memoryThresholdMb = ${opts.memoryThresholdMb}
1054
+ cpuThresholdPercent = 90
1055
+ diskThresholdMb = 100
1056
+
1057
+ [recovery]
1058
+ l1MaxAttempts = 3
1059
+ l2MaxAttempts = 2
1060
+
1061
+ [backup]
1062
+ maxKnownGood = 5
1063
+ knownGoodStabilityMs = 60000
1064
+
1065
+ [deadManSwitch]
1066
+ enabled = true
1067
+ countdownMs = 30000
1068
+
1069
+ [platform]
1070
+ type = "systemd"
1071
+ `;
1072
+ if (opts.channels.length > 0) {
1073
+ toml += "\n[alerts]\n";
1074
+ for (const ch of opts.channels) {
1075
+ toml += "\n[[alerts.channels]]\n";
1076
+ for (const [key, val] of Object.entries(ch)) {
1077
+ if (typeof val === "number") {
1078
+ toml += `${key} = ${val}
1079
+ `;
1080
+ } else {
1081
+ toml += `${key} = "${val}"
1082
+ `;
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ return toml;
1088
+ }
1089
+ var initCommand = new import_commander3.Command("init").description("Interactive setup wizard \u2014 configure Aegis for your gateway").option("--auto", "Auto-detect everything, no prompts").action(async (opts) => {
1090
+ const configDir = getConfigDir();
1091
+ const configFile = expandHome(DEFAULT_CONFIG_PATH);
1092
+ if (fs8.existsSync(configFile) && !opts.auto) {
1093
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1094
+ const overwrite = await ask(rl, "Config already exists. Overwrite? (y/N)", "n");
1095
+ if (overwrite.toLowerCase() !== "y") {
1096
+ console.log("Aborted.");
1097
+ rl.close();
1098
+ return;
1099
+ }
1100
+ rl.close();
1101
+ }
1102
+ const detectedPort = detectPort();
1103
+ const channels = [];
1104
+ if (opts.auto) {
1105
+ console.log("Auto-detecting configuration...");
1106
+ console.log(` Gateway port: ${detectedPort}`);
1107
+ console.log(` PID source: openclaw-gateway.service (systemd)`);
1108
+ console.log(` Memory threshold: 768MB`);
1109
+ } else {
1110
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1111
+ console.log("\n OpenClaw Aegis \u2014 Setup Wizard\n");
1112
+ console.log(" Detected gateway config at ~/.openclaw/openclaw.json");
1113
+ console.log(` Detected gateway port: ${detectedPort}
1114
+ `);
1115
+ const portStr = await ask(rl, "Gateway port", String(detectedPort));
1116
+ const port = parseInt(portStr, 10) || detectedPort;
1117
+ const memStr = await ask(rl, "Memory threshold (MB)", "768");
1118
+ const memThreshold = parseInt(memStr, 10) || 768;
1119
+ console.log("\n Alert Channels (out-of-band \u2014 never through the gateway)\n");
1120
+ console.log(" Supported: ntfy, telegram, whatsapp, slack, discord, email, pushover, webhook\n");
1121
+ let addMore = true;
1122
+ while (addMore) {
1123
+ const channelType = await ask(rl, "Add alert channel? (ntfy/telegram/whatsapp/slack/discord/email/pushover/webhook/skip)", "skip");
1124
+ if (channelType === "skip" || channelType === "") {
1125
+ addMore = false;
1126
+ continue;
1127
+ }
1128
+ switch (channelType) {
1129
+ case "ntfy": {
1130
+ const topic = await ask(rl, " ntfy topic", "aegis-alerts");
1131
+ const url = await ask(rl, " ntfy server URL", "https://ntfy.sh");
1132
+ channels.push({ type: "ntfy", topic, url, priority: 4 });
1133
+ console.log(` Added ntfy channel (${url}/${topic})
1134
+ `);
1135
+ break;
1136
+ }
1137
+ case "telegram": {
1138
+ const botToken = await ask(rl, " Telegram bot token");
1139
+ const chatId = await ask(rl, " Telegram chat ID");
1140
+ if (botToken && chatId) {
1141
+ channels.push({ type: "telegram", botToken, chatId });
1142
+ console.log(" Added Telegram channel\n");
1143
+ } else {
1144
+ console.log(" Skipped \u2014 bot token and chat ID required\n");
1145
+ }
1146
+ break;
1147
+ }
1148
+ case "whatsapp": {
1149
+ const phoneNumberId = await ask(rl, " WhatsApp Business phone number ID");
1150
+ const accessToken = await ask(rl, " WhatsApp Cloud API access token");
1151
+ const recipientNumber = await ask(rl, " Recipient phone number (with country code, e.g. 61412345678)");
1152
+ if (phoneNumberId && accessToken && recipientNumber) {
1153
+ channels.push({ type: "whatsapp", phoneNumberId, accessToken, recipientNumber });
1154
+ console.log(" Added WhatsApp channel\n");
1155
+ } else {
1156
+ console.log(" Skipped \u2014 all three fields required\n");
1157
+ }
1158
+ break;
1159
+ }
1160
+ case "slack": {
1161
+ const webhookUrl = await ask(rl, " Slack Incoming Webhook URL");
1162
+ if (webhookUrl) {
1163
+ const channel = await ask(rl, " Slack channel override (optional, e.g. #alerts)");
1164
+ const ch = { type: "slack", webhookUrl };
1165
+ if (channel) ch.channel = channel;
1166
+ channels.push(ch);
1167
+ console.log(" Added Slack channel\n");
1168
+ } else {
1169
+ console.log(" Skipped \u2014 webhook URL required\n");
1170
+ }
1171
+ break;
1172
+ }
1173
+ case "discord": {
1174
+ const webhookUrl = await ask(rl, " Discord Webhook URL");
1175
+ if (webhookUrl) {
1176
+ const username = await ask(rl, " Bot display name (optional)", "Aegis");
1177
+ const ch = { type: "discord", webhookUrl };
1178
+ if (username && username !== "Aegis") ch.username = username;
1179
+ channels.push(ch);
1180
+ console.log(" Added Discord channel\n");
1181
+ } else {
1182
+ console.log(" Skipped \u2014 webhook URL required\n");
1183
+ }
1184
+ break;
1185
+ }
1186
+ case "email": {
1187
+ const host = await ask(rl, " SMTP host (e.g. smtp.gmail.com)");
1188
+ const portStr2 = await ask(rl, " SMTP port", "587");
1189
+ const username = await ask(rl, " SMTP username");
1190
+ const password = await ask(rl, " SMTP password");
1191
+ const from = await ask(rl, " From address");
1192
+ const to = await ask(rl, " To address");
1193
+ if (host && username && password && from && to) {
1194
+ channels.push({
1195
+ type: "email",
1196
+ host,
1197
+ port: parseInt(portStr2, 10) || 587,
1198
+ secure: 0,
1199
+ username,
1200
+ password,
1201
+ from,
1202
+ to
1203
+ });
1204
+ console.log(" Added Email channel\n");
1205
+ } else {
1206
+ console.log(" Skipped \u2014 all fields required\n");
1207
+ }
1208
+ break;
1209
+ }
1210
+ case "pushover": {
1211
+ const apiToken = await ask(rl, " Pushover API token (from your app)");
1212
+ const userKey = await ask(rl, " Pushover user key");
1213
+ if (apiToken && userKey) {
1214
+ const device = await ask(rl, " Device name (optional)");
1215
+ const ch = { type: "pushover", apiToken, userKey };
1216
+ if (device) ch.device = device;
1217
+ channels.push(ch);
1218
+ console.log(" Added Pushover channel\n");
1219
+ } else {
1220
+ console.log(" Skipped \u2014 API token and user key required\n");
1221
+ }
1222
+ break;
1223
+ }
1224
+ case "webhook": {
1225
+ const url = await ask(rl, " Webhook URL");
1226
+ if (url) {
1227
+ const secret = await ask(rl, " Webhook secret (optional)");
1228
+ const ch = { type: "webhook", url };
1229
+ if (secret) ch.secret = secret;
1230
+ channels.push(ch);
1231
+ console.log(` Added webhook channel (${url})
1232
+ `);
1233
+ } else {
1234
+ console.log(" Skipped \u2014 URL required\n");
1235
+ }
1236
+ break;
1237
+ }
1238
+ default:
1239
+ console.log(` Unknown channel type: ${channelType}
1240
+ `);
1241
+ }
1242
+ }
1243
+ rl.close();
1244
+ const toml2 = generateToml({ port, memoryThresholdMb: memThreshold, channels });
1245
+ ensureConfigDir();
1246
+ fs8.writeFileSync(configFile, toml2, { mode: 384 });
1247
+ console.log(`
1248
+ Config written to ${configFile}`);
1249
+ console.log("\n Running health check...\n");
1250
+ const config2 = loadConfig(configFile);
1251
+ const monitor2 = new HealthMonitor(config2);
1252
+ const score2 = await monitor2.runAllProbes();
1253
+ const passed2 = score2.probeResults.filter((p) => p.healthy).length;
1254
+ const failed = score2.probeResults.filter((p) => !p.healthy);
1255
+ console.log(` Health: ${score2.band.toUpperCase()} (${passed2}/${score2.probeResults.length} probes passed)`);
1256
+ if (failed.length > 0) {
1257
+ for (const probe of failed) {
1258
+ console.log(` - ${probe.name}: ${probe.message ?? "failed"}`);
1259
+ }
1260
+ }
1261
+ if (channels.length === 0) {
1262
+ console.log("\n No alert channels configured. You can add them later by editing:");
1263
+ console.log(` ${configFile}`);
1264
+ }
1265
+ console.log("\n Setup complete. Run 'aegis check' to verify anytime.\n");
1266
+ return;
1267
+ }
1268
+ const toml = generateToml({ port: detectedPort, memoryThresholdMb: 768, channels });
1269
+ ensureConfigDir();
1270
+ fs8.writeFileSync(configFile, toml, { mode: 384 });
1271
+ console.log(`Config written to ${configFile}`);
1272
+ const config = loadConfig(configFile);
1273
+ const monitor = new HealthMonitor(config);
1274
+ const score = await monitor.runAllProbes();
1275
+ const passed = score.probeResults.filter((p) => p.healthy).length;
1276
+ console.log(`Health: ${score.band.toUpperCase()} (${passed}/${score.probeResults.length} probes passed)`);
1277
+ console.log("\nSetup complete.");
1278
+ });
1279
+
1280
+ // src/cli/commands/test-alert.ts
1281
+ var import_commander4 = require("commander");
1282
+
1283
+ // src/alerts/dispatcher.ts
1284
+ var AlertDispatcher = class {
1285
+ providers = [];
1286
+ retryBackoffMs;
1287
+ retryAttempts;
1288
+ constructor(retryAttempts = 3, retryBackoffMs = [5e3, 15e3, 45e3]) {
1289
+ this.retryAttempts = retryAttempts;
1290
+ this.retryBackoffMs = retryBackoffMs;
1291
+ }
1292
+ addProvider(provider) {
1293
+ this.providers.push(provider);
1294
+ }
1295
+ getProviders() {
1296
+ return [...this.providers];
1297
+ }
1298
+ hasProviders() {
1299
+ return this.providers.length > 0;
1300
+ }
1301
+ async dispatch(alert) {
1302
+ if (this.providers.length === 0) {
1303
+ return { sent: false, results: [], allFailed: true };
1304
+ }
1305
+ const scrubbed = scrubSensitiveData(alert);
1306
+ const results = [];
1307
+ for (const provider of this.providers) {
1308
+ const result = await this.sendWithRetry(provider, scrubbed);
1309
+ results.push(result);
1310
+ }
1311
+ const anySuccess = results.some((r) => r.success);
1312
+ return { sent: anySuccess, results, allFailed: !anySuccess };
1313
+ }
1314
+ async testAll() {
1315
+ const results = /* @__PURE__ */ new Map();
1316
+ for (const provider of this.providers) {
1317
+ try {
1318
+ const ok = await provider.test();
1319
+ results.set(provider.name, ok);
1320
+ } catch {
1321
+ results.set(provider.name, false);
1322
+ }
1323
+ }
1324
+ return results;
1325
+ }
1326
+ async sendWithRetry(provider, alert) {
1327
+ let lastResult = {
1328
+ provider: provider.name,
1329
+ success: false,
1330
+ error: "No attempts made",
1331
+ durationMs: 0
1332
+ };
1333
+ for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
1334
+ try {
1335
+ lastResult = await provider.send(alert);
1336
+ if (lastResult.success) return lastResult;
1337
+ } catch (err) {
1338
+ lastResult = {
1339
+ provider: provider.name,
1340
+ success: false,
1341
+ error: err instanceof Error ? err.message : String(err),
1342
+ durationMs: 0
1343
+ };
1344
+ }
1345
+ if (attempt < this.retryAttempts) {
1346
+ const delay = this.retryBackoffMs[attempt] ?? this.retryBackoffMs[this.retryBackoffMs.length - 1] ?? 5e3;
1347
+ await new Promise((r) => setTimeout(r, delay));
1348
+ }
1349
+ }
1350
+ return lastResult;
1351
+ }
1352
+ };
1353
+ function scrubSensitiveData(alert) {
1354
+ return {
1355
+ ...alert,
1356
+ body: scrubString(alert.body),
1357
+ title: scrubString(alert.title)
1358
+ };
1359
+ }
1360
+ function scrubString(input) {
1361
+ return input.replace(
1362
+ /("[^"]*(?:key|secret|token|password|credential|auth)[^"]*"\s*:\s*)"[^"]*"/gi,
1363
+ '$1"[REDACTED]"'
1364
+ );
1365
+ }
1366
+
1367
+ // src/alerts/providers/ntfy.ts
1368
+ var NtfyProvider = class {
1369
+ name = "ntfy";
1370
+ config;
1371
+ constructor(config) {
1372
+ this.config = config;
1373
+ }
1374
+ async send(alert) {
1375
+ const start = Date.now();
1376
+ const url = `${this.config.url}/${this.config.topic}`;
1377
+ try {
1378
+ const response = await fetch(url, {
1379
+ method: "POST",
1380
+ headers: {
1381
+ "Title": alert.title,
1382
+ "Priority": String(this.config.priority),
1383
+ "Tags": alertSeverityToTag(alert.severity)
1384
+ },
1385
+ body: alert.body
1386
+ });
1387
+ return {
1388
+ provider: this.name,
1389
+ success: response.ok,
1390
+ error: response.ok ? void 0 : `HTTP ${response.status}`,
1391
+ durationMs: Date.now() - start
1392
+ };
1393
+ } catch (err) {
1394
+ return {
1395
+ provider: this.name,
1396
+ success: false,
1397
+ error: err instanceof Error ? err.message : String(err),
1398
+ durationMs: Date.now() - start
1399
+ };
1400
+ }
1401
+ }
1402
+ async test() {
1403
+ try {
1404
+ const result = await this.send({
1405
+ severity: "info",
1406
+ title: "Aegis Alert Test",
1407
+ body: "This is a test alert from OpenClaw Aegis.",
1408
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1409
+ });
1410
+ return result.success;
1411
+ } catch {
1412
+ return false;
1413
+ }
1414
+ }
1415
+ };
1416
+ function alertSeverityToTag(severity) {
1417
+ switch (severity) {
1418
+ case "critical":
1419
+ return "rotating_light";
1420
+ case "warning":
1421
+ return "warning";
1422
+ default:
1423
+ return "information_source";
1424
+ }
1425
+ }
1426
+
1427
+ // src/alerts/providers/telegram.ts
1428
+ var TelegramProvider = class {
1429
+ name = "telegram";
1430
+ config;
1431
+ constructor(config) {
1432
+ this.config = config;
1433
+ }
1434
+ async send(alert) {
1435
+ const start = Date.now();
1436
+ const url = `https://api.telegram.org/bot${this.config.botToken}/sendMessage`;
1437
+ const icon = alert.severity === "critical" ? "\u{1F6A8}" : alert.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
1438
+ const text = `${icon} *${escapeMarkdown(alert.title)}*
1439
+
1440
+ ${escapeMarkdown(alert.body)}`;
1441
+ try {
1442
+ const response = await fetch(url, {
1443
+ method: "POST",
1444
+ headers: { "Content-Type": "application/json" },
1445
+ body: JSON.stringify({
1446
+ chat_id: this.config.chatId,
1447
+ text,
1448
+ parse_mode: "MarkdownV2"
1449
+ })
1450
+ });
1451
+ const data = await response.json();
1452
+ return {
1453
+ provider: this.name,
1454
+ success: data.ok,
1455
+ error: data.ok ? void 0 : data.description,
1456
+ durationMs: Date.now() - start
1457
+ };
1458
+ } catch (err) {
1459
+ return {
1460
+ provider: this.name,
1461
+ success: false,
1462
+ error: err instanceof Error ? err.message : String(err),
1463
+ durationMs: Date.now() - start
1464
+ };
1465
+ }
1466
+ }
1467
+ async test() {
1468
+ try {
1469
+ const result = await this.send({
1470
+ severity: "info",
1471
+ title: "Aegis Alert Test",
1472
+ body: "This is a test alert from OpenClaw Aegis.",
1473
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1474
+ });
1475
+ return result.success;
1476
+ } catch {
1477
+ return false;
1478
+ }
1479
+ }
1480
+ };
1481
+ function escapeMarkdown(text) {
1482
+ return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
1483
+ }
1484
+
1485
+ // src/alerts/providers/whatsapp.ts
1486
+ var WhatsAppProvider = class {
1487
+ name = "whatsapp";
1488
+ config;
1489
+ constructor(config) {
1490
+ this.config = config;
1491
+ }
1492
+ async send(alert) {
1493
+ const start = Date.now();
1494
+ const url = `https://graph.facebook.com/v21.0/${this.config.phoneNumberId}/messages`;
1495
+ const icon = alert.severity === "critical" ? "\u{1F6A8}" : alert.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
1496
+ const text = `${icon} *${alert.title}*
1497
+
1498
+ ${alert.body}`;
1499
+ try {
1500
+ const response = await fetch(url, {
1501
+ method: "POST",
1502
+ headers: {
1503
+ "Authorization": `Bearer ${this.config.accessToken}`,
1504
+ "Content-Type": "application/json"
1505
+ },
1506
+ body: JSON.stringify({
1507
+ messaging_product: "whatsapp",
1508
+ to: this.config.recipientNumber,
1509
+ type: "text",
1510
+ text: { body: text }
1511
+ })
1512
+ });
1513
+ const data = await response.json();
1514
+ const success = response.ok && !!data.messages?.length;
1515
+ return {
1516
+ provider: this.name,
1517
+ success,
1518
+ error: success ? void 0 : data.error?.message ?? `HTTP ${response.status}`,
1519
+ durationMs: Date.now() - start
1520
+ };
1521
+ } catch (err) {
1522
+ return {
1523
+ provider: this.name,
1524
+ success: false,
1525
+ error: err instanceof Error ? err.message : String(err),
1526
+ durationMs: Date.now() - start
1527
+ };
1528
+ }
1529
+ }
1530
+ async test() {
1531
+ try {
1532
+ const result = await this.send({
1533
+ severity: "info",
1534
+ title: "Aegis Alert Test",
1535
+ body: "This is a test alert from OpenClaw Aegis.",
1536
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1537
+ });
1538
+ return result.success;
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1543
+ };
1544
+
1545
+ // src/alerts/providers/webhook.ts
1546
+ var crypto = __toESM(require("crypto"));
1547
+ var WebhookProvider = class {
1548
+ name = "webhook";
1549
+ config;
1550
+ constructor(config) {
1551
+ this.config = config;
1552
+ }
1553
+ async send(alert) {
1554
+ const start = Date.now();
1555
+ const body = JSON.stringify(alert);
1556
+ const headers = { "Content-Type": "application/json" };
1557
+ if (this.config.secret) {
1558
+ const signature = crypto.createHmac("sha256", this.config.secret).update(body).digest("hex");
1559
+ headers["X-Aegis-Signature"] = `sha256=${signature}`;
1560
+ }
1561
+ try {
1562
+ const response = await fetch(this.config.url, {
1563
+ method: "POST",
1564
+ headers,
1565
+ body
1566
+ });
1567
+ return {
1568
+ provider: this.name,
1569
+ success: response.ok,
1570
+ error: response.ok ? void 0 : `HTTP ${response.status}`,
1571
+ durationMs: Date.now() - start
1572
+ };
1573
+ } catch (err) {
1574
+ return {
1575
+ provider: this.name,
1576
+ success: false,
1577
+ error: err instanceof Error ? err.message : String(err),
1578
+ durationMs: Date.now() - start
1579
+ };
1580
+ }
1581
+ }
1582
+ async test() {
1583
+ try {
1584
+ const result = await this.send({
1585
+ severity: "info",
1586
+ title: "Aegis Webhook Test",
1587
+ body: "This is a test alert from OpenClaw Aegis.",
1588
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1589
+ });
1590
+ return result.success;
1591
+ } catch {
1592
+ return false;
1593
+ }
1594
+ }
1595
+ };
1596
+
1597
+ // src/alerts/providers/slack.ts
1598
+ var SlackProvider = class {
1599
+ name = "slack";
1600
+ config;
1601
+ constructor(config) {
1602
+ this.config = config;
1603
+ }
1604
+ async send(alert) {
1605
+ const start = Date.now();
1606
+ const icon = alert.severity === "critical" ? ":rotating_light:" : alert.severity === "warning" ? ":warning:" : ":information_source:";
1607
+ const payload = {
1608
+ text: `${icon} *${alert.title}*
1609
+
1610
+ ${alert.body}`
1611
+ };
1612
+ if (this.config.channel) {
1613
+ payload.channel = this.config.channel;
1614
+ }
1615
+ try {
1616
+ const response = await fetch(this.config.webhookUrl, {
1617
+ method: "POST",
1618
+ headers: { "Content-Type": "application/json" },
1619
+ body: JSON.stringify(payload)
1620
+ });
1621
+ const ok = response.ok;
1622
+ return {
1623
+ provider: this.name,
1624
+ success: ok,
1625
+ error: ok ? void 0 : `HTTP ${response.status}`,
1626
+ durationMs: Date.now() - start
1627
+ };
1628
+ } catch (err) {
1629
+ return {
1630
+ provider: this.name,
1631
+ success: false,
1632
+ error: err instanceof Error ? err.message : String(err),
1633
+ durationMs: Date.now() - start
1634
+ };
1635
+ }
1636
+ }
1637
+ async test() {
1638
+ try {
1639
+ const result = await this.send({
1640
+ severity: "info",
1641
+ title: "Aegis Alert Test",
1642
+ body: "This is a test alert from OpenClaw Aegis.",
1643
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1644
+ });
1645
+ return result.success;
1646
+ } catch {
1647
+ return false;
1648
+ }
1649
+ }
1650
+ };
1651
+
1652
+ // src/alerts/providers/discord.ts
1653
+ var DiscordProvider = class {
1654
+ name = "discord";
1655
+ config;
1656
+ constructor(config) {
1657
+ this.config = config;
1658
+ }
1659
+ async send(alert) {
1660
+ const start = Date.now();
1661
+ const color = alert.severity === "critical" ? 15548997 : alert.severity === "warning" ? 16705372 : 5763719;
1662
+ const payload = {
1663
+ embeds: [{
1664
+ title: alert.title,
1665
+ description: alert.body,
1666
+ color,
1667
+ timestamp: alert.timestamp,
1668
+ footer: { text: "OpenClaw Aegis" }
1669
+ }]
1670
+ };
1671
+ if (this.config.username) {
1672
+ payload.username = this.config.username;
1673
+ }
1674
+ try {
1675
+ const response = await fetch(this.config.webhookUrl, {
1676
+ method: "POST",
1677
+ headers: { "Content-Type": "application/json" },
1678
+ body: JSON.stringify(payload)
1679
+ });
1680
+ const ok = response.status === 204 || response.ok;
1681
+ return {
1682
+ provider: this.name,
1683
+ success: ok,
1684
+ error: ok ? void 0 : `HTTP ${response.status}`,
1685
+ durationMs: Date.now() - start
1686
+ };
1687
+ } catch (err) {
1688
+ return {
1689
+ provider: this.name,
1690
+ success: false,
1691
+ error: err instanceof Error ? err.message : String(err),
1692
+ durationMs: Date.now() - start
1693
+ };
1694
+ }
1695
+ }
1696
+ async test() {
1697
+ try {
1698
+ const result = await this.send({
1699
+ severity: "info",
1700
+ title: "Aegis Alert Test",
1701
+ body: "This is a test alert from OpenClaw Aegis.",
1702
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1703
+ });
1704
+ return result.success;
1705
+ } catch {
1706
+ return false;
1707
+ }
1708
+ }
1709
+ };
1710
+
1711
+ // src/alerts/providers/email.ts
1712
+ var net2 = __toESM(require("net"));
1713
+ var tls = __toESM(require("tls"));
1714
+ var EmailProvider = class {
1715
+ name = "email";
1716
+ config;
1717
+ constructor(config) {
1718
+ this.config = config;
1719
+ }
1720
+ async send(alert) {
1721
+ const start = Date.now();
1722
+ const icon = alert.severity === "critical" ? "[CRITICAL]" : alert.severity === "warning" ? "[WARNING]" : "[INFO]";
1723
+ const subject = `${icon} ${alert.title}`;
1724
+ try {
1725
+ await this.sendSmtp(subject, alert.body);
1726
+ return {
1727
+ provider: this.name,
1728
+ success: true,
1729
+ durationMs: Date.now() - start
1730
+ };
1731
+ } catch (err) {
1732
+ return {
1733
+ provider: this.name,
1734
+ success: false,
1735
+ error: err instanceof Error ? err.message : String(err),
1736
+ durationMs: Date.now() - start
1737
+ };
1738
+ }
1739
+ }
1740
+ async test() {
1741
+ try {
1742
+ const result = await this.send({
1743
+ severity: "info",
1744
+ title: "Aegis Alert Test",
1745
+ body: "This is a test alert from OpenClaw Aegis.",
1746
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1747
+ });
1748
+ return result.success;
1749
+ } catch {
1750
+ return false;
1751
+ }
1752
+ }
1753
+ sendSmtp(subject, body) {
1754
+ return new Promise((resolve, reject) => {
1755
+ const timeout = 15e3;
1756
+ let socket;
1757
+ const connect2 = () => {
1758
+ if (this.config.secure) {
1759
+ socket = tls.connect({
1760
+ host: this.config.host,
1761
+ port: this.config.port,
1762
+ rejectUnauthorized: true
1763
+ });
1764
+ } else {
1765
+ socket = net2.createConnection({
1766
+ host: this.config.host,
1767
+ port: this.config.port
1768
+ });
1769
+ }
1770
+ socket.setTimeout(timeout);
1771
+ let buffer = "";
1772
+ let step = 0;
1773
+ const commands = [
1774
+ `EHLO aegis\r
1775
+ `,
1776
+ `AUTH LOGIN\r
1777
+ `,
1778
+ `${Buffer.from(this.config.username).toString("base64")}\r
1779
+ `,
1780
+ `${Buffer.from(this.config.password).toString("base64")}\r
1781
+ `,
1782
+ `MAIL FROM:<${this.config.from}>\r
1783
+ `,
1784
+ `RCPT TO:<${this.config.to}>\r
1785
+ `,
1786
+ `DATA\r
1787
+ `,
1788
+ `From: ${this.config.from}\r
1789
+ To: ${this.config.to}\r
1790
+ Subject: ${subject}\r
1791
+ Content-Type: text/plain; charset=utf-8\r
1792
+ X-Mailer: OpenClaw-Aegis\r
1793
+ \r
1794
+ ${body}\r
1795
+ .\r
1796
+ `,
1797
+ `QUIT\r
1798
+ `
1799
+ ];
1800
+ socket.on("data", (data) => {
1801
+ buffer += data.toString();
1802
+ const lines = buffer.split("\r\n");
1803
+ buffer = lines.pop() ?? "";
1804
+ for (const line of lines) {
1805
+ if (!line) continue;
1806
+ const code = parseInt(line.substring(0, 3), 10);
1807
+ if (line.charAt(3) === "-") continue;
1808
+ if (code >= 400) {
1809
+ socket.destroy();
1810
+ reject(new Error(`SMTP error: ${line}`));
1811
+ return;
1812
+ }
1813
+ if (step < commands.length) {
1814
+ socket.write(commands[step]);
1815
+ step++;
1816
+ }
1817
+ if (step >= commands.length && code === 221) {
1818
+ socket.end();
1819
+ resolve();
1820
+ return;
1821
+ }
1822
+ }
1823
+ });
1824
+ socket.on("timeout", () => {
1825
+ socket.destroy();
1826
+ reject(new Error("SMTP connection timed out"));
1827
+ });
1828
+ socket.on("error", (err) => {
1829
+ reject(new Error(`SMTP error: ${err.message}`));
1830
+ });
1831
+ };
1832
+ if (!this.config.secure && this.config.port === 587) {
1833
+ const plain = net2.createConnection({
1834
+ host: this.config.host,
1835
+ port: this.config.port
1836
+ });
1837
+ plain.setTimeout(timeout);
1838
+ let buf = "";
1839
+ let startTlsSent = false;
1840
+ let ehloSent = false;
1841
+ plain.on("data", (data) => {
1842
+ buf += data.toString();
1843
+ const lines = buf.split("\r\n");
1844
+ buf = lines.pop() ?? "";
1845
+ for (const line of lines) {
1846
+ if (!line) continue;
1847
+ const code = parseInt(line.substring(0, 3), 10);
1848
+ if (line.charAt(3) === "-") continue;
1849
+ if (code >= 400) {
1850
+ plain.destroy();
1851
+ reject(new Error(`SMTP error: ${line}`));
1852
+ return;
1853
+ }
1854
+ if (!ehloSent) {
1855
+ plain.write("EHLO aegis\r\n");
1856
+ ehloSent = true;
1857
+ } else if (!startTlsSent) {
1858
+ plain.write("STARTTLS\r\n");
1859
+ startTlsSent = true;
1860
+ } else if (code === 220 && startTlsSent) {
1861
+ socket = tls.connect({
1862
+ socket: plain,
1863
+ host: this.config.host,
1864
+ rejectUnauthorized: true
1865
+ }, () => {
1866
+ let step2 = 0;
1867
+ let buffer2 = "";
1868
+ const cmds = [
1869
+ `EHLO aegis\r
1870
+ `,
1871
+ `AUTH LOGIN\r
1872
+ `,
1873
+ `${Buffer.from(this.config.username).toString("base64")}\r
1874
+ `,
1875
+ `${Buffer.from(this.config.password).toString("base64")}\r
1876
+ `,
1877
+ `MAIL FROM:<${this.config.from}>\r
1878
+ `,
1879
+ `RCPT TO:<${this.config.to}>\r
1880
+ `,
1881
+ `DATA\r
1882
+ `,
1883
+ `From: ${this.config.from}\r
1884
+ To: ${this.config.to}\r
1885
+ Subject: ${subject}\r
1886
+ Content-Type: text/plain; charset=utf-8\r
1887
+ X-Mailer: OpenClaw-Aegis\r
1888
+ \r
1889
+ ${body}\r
1890
+ .\r
1891
+ `,
1892
+ `QUIT\r
1893
+ `
1894
+ ];
1895
+ socket.write(cmds[0]);
1896
+ step2 = 1;
1897
+ socket.on("data", (d) => {
1898
+ buffer2 += d.toString();
1899
+ const ls = buffer2.split("\r\n");
1900
+ buffer2 = ls.pop() ?? "";
1901
+ for (const l of ls) {
1902
+ if (!l) continue;
1903
+ const c = parseInt(l.substring(0, 3), 10);
1904
+ if (l.charAt(3) === "-") continue;
1905
+ if (c >= 400) {
1906
+ socket.destroy();
1907
+ reject(new Error(`SMTP error: ${l}`));
1908
+ return;
1909
+ }
1910
+ if (step2 < cmds.length) {
1911
+ socket.write(cmds[step2]);
1912
+ step2++;
1913
+ }
1914
+ if (step2 >= cmds.length && c === 221) {
1915
+ socket.end();
1916
+ resolve();
1917
+ return;
1918
+ }
1919
+ }
1920
+ });
1921
+ });
1922
+ return;
1923
+ }
1924
+ }
1925
+ });
1926
+ plain.on("timeout", () => {
1927
+ plain.destroy();
1928
+ reject(new Error("SMTP connection timed out"));
1929
+ });
1930
+ plain.on("error", (err) => {
1931
+ reject(new Error(`SMTP error: ${err.message}`));
1932
+ });
1933
+ } else {
1934
+ connect2();
1935
+ }
1936
+ });
1937
+ }
1938
+ };
1939
+
1940
+ // src/alerts/providers/pushover.ts
1941
+ var PushoverProvider = class {
1942
+ name = "pushover";
1943
+ config;
1944
+ constructor(config) {
1945
+ this.config = config;
1946
+ }
1947
+ async send(alert) {
1948
+ const start = Date.now();
1949
+ const priority = alert.severity === "critical" ? 1 : alert.severity === "warning" ? 0 : -1;
1950
+ const params = {
1951
+ token: this.config.apiToken,
1952
+ user: this.config.userKey,
1953
+ title: alert.title,
1954
+ message: alert.body,
1955
+ priority: String(priority),
1956
+ timestamp: String(Math.floor(new Date(alert.timestamp).getTime() / 1e3))
1957
+ };
1958
+ if (this.config.device) {
1959
+ params.device = this.config.device;
1960
+ }
1961
+ try {
1962
+ const response = await fetch("https://api.pushover.net/1/messages.json", {
1963
+ method: "POST",
1964
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1965
+ body: new URLSearchParams(params).toString()
1966
+ });
1967
+ const data = await response.json();
1968
+ const ok = data.status === 1;
1969
+ return {
1970
+ provider: this.name,
1971
+ success: ok,
1972
+ error: ok ? void 0 : data.errors?.join(", ") ?? `HTTP ${response.status}`,
1973
+ durationMs: Date.now() - start
1974
+ };
1975
+ } catch (err) {
1976
+ return {
1977
+ provider: this.name,
1978
+ success: false,
1979
+ error: err instanceof Error ? err.message : String(err),
1980
+ durationMs: Date.now() - start
1981
+ };
1982
+ }
1983
+ }
1984
+ async test() {
1985
+ try {
1986
+ const result = await this.send({
1987
+ severity: "info",
1988
+ title: "Aegis Alert Test",
1989
+ body: "This is a test alert from OpenClaw Aegis.",
1990
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1991
+ });
1992
+ return result.success;
1993
+ } catch {
1994
+ return false;
1995
+ }
1996
+ }
1997
+ };
1998
+
1999
+ // src/cli/commands/test-alert.ts
2000
+ var testAlertCommand = new import_commander4.Command("test-alert").description("Send a test alert to all configured channels").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).action(async (opts) => {
2001
+ const config = loadConfig(opts.config);
2002
+ if (config.alerts.channels.length === 0) {
2003
+ console.log("No alert channels configured. Run 'aegis init' to add one.");
2004
+ process.exit(1);
2005
+ }
2006
+ const dispatcher = new AlertDispatcher(1, [1e3]);
2007
+ for (const ch of config.alerts.channels) {
2008
+ switch (ch.type) {
2009
+ case "ntfy":
2010
+ dispatcher.addProvider(new NtfyProvider(ch));
2011
+ break;
2012
+ case "telegram":
2013
+ dispatcher.addProvider(new TelegramProvider(ch));
2014
+ break;
2015
+ case "whatsapp":
2016
+ dispatcher.addProvider(new WhatsAppProvider(ch));
2017
+ break;
2018
+ case "webhook":
2019
+ dispatcher.addProvider(new WebhookProvider(ch));
2020
+ break;
2021
+ case "slack":
2022
+ dispatcher.addProvider(new SlackProvider(ch));
2023
+ break;
2024
+ case "discord":
2025
+ dispatcher.addProvider(new DiscordProvider(ch));
2026
+ break;
2027
+ case "email":
2028
+ dispatcher.addProvider(new EmailProvider(ch));
2029
+ break;
2030
+ case "pushover":
2031
+ dispatcher.addProvider(new PushoverProvider(ch));
2032
+ break;
2033
+ }
2034
+ }
2035
+ const alert = {
2036
+ severity: "info",
2037
+ title: "Aegis Test Alert",
2038
+ body: "This is a test notification from OpenClaw Aegis. If you see this, alerts are working.",
2039
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2040
+ };
2041
+ console.log(`Sending test alert to ${dispatcher.getProviders().length} channel(s)...
2042
+ `);
2043
+ const result = await dispatcher.dispatch(alert);
2044
+ for (const r of result.results) {
2045
+ if (r.success) {
2046
+ console.log(` + ${r.provider}: sent (${r.durationMs}ms)`);
2047
+ } else {
2048
+ console.log(` - ${r.provider}: failed \u2014 ${r.error}`);
2049
+ }
2050
+ }
2051
+ console.log(result.sent ? "\nTest alert sent successfully." : "\nAll channels failed.");
2052
+ process.exit(result.sent ? 0 : 1);
2053
+ });
2054
+
2055
+ // src/cli/index.ts
2056
+ var program = new import_commander5.Command();
2057
+ program.name("aegis").description("OpenClaw Aegis \u2014 self-healing sidecar for the OpenClaw gateway").version("1.0.0");
2058
+ program.addCommand(initCommand);
2059
+ program.addCommand(statusCommand);
2060
+ program.addCommand(checkCommand);
2061
+ program.addCommand(testAlertCommand);
2062
+ program.parse(process.argv);
2063
+ //# sourceMappingURL=index.js.map