thuban 0.3.2 → 0.3.3

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.
@@ -1,398 +1,398 @@
1
- /**
2
- * Alert Manager
3
- *
4
- * @purpose Event-driven AlertManager component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system writes, File system reads, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel Alert Manager
15
- *
16
- * Handles alerting and reporting for the Sentinel system.
17
- * Outputs: Console, files, and future integrations (Slack, email, GitHub issues)
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const EventEmitter = require('events');
23
-
24
- class AlertManager extends EventEmitter {
25
- constructor(config = {}) {
26
- super();
27
-
28
- this.config = {
29
- rootPath: config.rootPath || process.cwd(),
30
- alertsDir: config.alertsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'alerts'),
31
- reportsDir: config.reportsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'reports'),
32
- consoleOutput: config.consoleOutput !== false,
33
- fileOutput: config.fileOutput !== false,
34
- maxAlertsPerFile: config.maxAlertsPerFile || 1000,
35
- slackWebhook: config.slackWebhook || null,
36
- emailConfig: config.emailConfig || null,
37
- ...config
38
- };
39
-
40
- // Ensure directories exist
41
- this._ensureDirectories();
42
-
43
- // Alert history for deduplication
44
- this.recentAlerts = new Map();
45
- this.alertDedupeWindowMs = config.alertDedupeWindowMs || 60000; // 1 minute
46
- }
47
-
48
- /**
49
- * Send an alert
50
- */
51
- async sendAlert(alert) {
52
- const normalizedAlert = this._normalizeAlert(alert);
53
-
54
- // Check for duplicate
55
- if (this._isDuplicate(normalizedAlert)) {
56
- return { sent: false, reason: 'duplicate' };
57
- }
58
-
59
- // Record alert
60
- this._recordAlert(normalizedAlert);
61
-
62
- // Output to configured channels
63
- const results = {
64
- console: false,
65
- file: false,
66
- slack: false,
67
- email: false
68
- };
69
-
70
- // Console output
71
- if (this.config.consoleOutput) {
72
- results.console = this._outputToConsole(normalizedAlert);
73
- }
74
-
75
- // File output
76
- if (this.config.fileOutput) {
77
- results.file = await this._outputToFile(normalizedAlert);
78
- }
79
-
80
- // Slack output (if configured and critical/error)
81
- if (this.config.slackWebhook && ['critical', 'error'].includes(normalizedAlert.level)) {
82
- results.slack = await this._outputToSlack(normalizedAlert);
83
- }
84
-
85
- this.emit('alertSent', { alert: normalizedAlert, results });
86
-
87
- return { sent: true, alert: normalizedAlert, results };
88
- }
89
-
90
- /**
91
- * Send daily digest
92
- */
93
- async sendDigest(digest) {
94
- console.log('\n' + '='.repeat(60));
95
- console.log('[SENTINEL] DAILY DIGEST - ' + digest.date);
96
- console.log('='.repeat(60));
97
-
98
- console.log(`\nSummary:`);
99
- console.log(` Issues Found: ${digest.summary.issuesFound}`);
100
- console.log(` Critical: ${digest.summary.criticalIssues}`);
101
- console.log(` Warnings: ${digest.summary.warningIssues}`);
102
- console.log(` Files Watched: ${digest.summary.filesWatched}`);
103
- console.log(` Last Scan: ${digest.summary.lastScan || 'N/A'}`);
104
-
105
- if (digest.topIssues && digest.topIssues.length > 0) {
106
- console.log(`\nTop Issues:`);
107
- for (const issue of digest.topIssues.slice(0, 5)) {
108
- console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
109
- if (issue.file) {
110
- console.log(` File: ${path.relative(this.config.rootPath, issue.file)}`);
111
- }
112
- }
113
- }
114
-
115
- if (digest.recommendations && digest.recommendations.length > 0) {
116
- console.log(`\nRecommendations:`);
117
- for (const rec of digest.recommendations) {
118
- console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
119
- }
120
- }
121
-
122
- console.log('\n' + '='.repeat(60) + '\n');
123
-
124
- // Save digest to file
125
- if (this.config.fileOutput) {
126
- const digestPath = path.join(
127
- this.config.reportsDir,
128
- `digest-${digest.date}.json`
129
- );
130
- fs.writeFileSync(digestPath, JSON.stringify(digest, null, 2));
131
- }
132
-
133
- this.emit('digestSent', digest);
134
- return { sent: true };
135
- }
136
-
137
- /**
138
- * Send weekly report
139
- */
140
- async sendWeeklyReport(report) {
141
- console.log('\n' + '='.repeat(70));
142
- console.log('[SENTINEL] WEEKLY REPORT - Week of ' + report.weekOf);
143
- console.log('='.repeat(70));
144
-
145
- console.log(`\nMetrics:`);
146
- console.log(` Total Scans: ${report.summary.totalScans}`);
147
- console.log(` Issues Detected: ${report.summary.issuesDetected}`);
148
- console.log(` Issues Resolved: ${report.summary.issuesResolved}`);
149
- console.log(` Avg Scan Time: ${report.summary.averageScanTime}ms`);
150
-
151
- console.log(`\nCode Quality:`);
152
- console.log(` Score: ${report.codeQuality.score}/100 (${report.codeQuality.grade})`);
153
- console.log(` Complexity: ${report.codeQuality.factors.complexity}`);
154
- console.log(` Maintainability: ${report.codeQuality.factors.maintainability}`);
155
- console.log(` Test Coverage: ${report.codeQuality.factors.testCoverage}%`);
156
-
157
- console.log(`\nTechnical Debt:`);
158
- console.log(` Estimated Hours: ${report.technicalDebt.estimatedHours}`);
159
- console.log(` Refactoring: ${report.technicalDebt.categories.refactoring}h`);
160
- console.log(` Documentation: ${report.technicalDebt.categories.documentation}h`);
161
- console.log(` Testing: ${report.technicalDebt.categories.testing}h`);
162
-
163
- if (report.recommendations && report.recommendations.length > 0) {
164
- console.log(`\nRecommendations:`);
165
- for (const rec of report.recommendations) {
166
- console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
167
- }
168
- }
169
-
170
- console.log('\n' + '='.repeat(70) + '\n');
171
-
172
- // Save report to file
173
- if (this.config.fileOutput) {
174
- const reportPath = path.join(
175
- this.config.reportsDir,
176
- `weekly-report-${report.weekOf}.json`
177
- );
178
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
179
- }
180
-
181
- this.emit('weeklyReportSent', report);
182
- return { sent: true };
183
- }
184
-
185
- /**
186
- * Get alert history
187
- */
188
- getAlertHistory(options = {}) {
189
- const { days = 7, level = null, category = null } = options;
190
-
191
- const cutoff = new Date();
192
- cutoff.setDate(cutoff.getDate() - days);
193
-
194
- // Read from alert files
195
- const alerts = [];
196
-
197
- try {
198
- if (fs.existsSync(this.config.alertsDir)) {
199
- const files = fs.readdirSync(this.config.alertsDir)
200
- .filter(f => f.startsWith('alerts-') && f.endsWith('.json'))
201
- .sort()
202
- .reverse();
203
-
204
- for (const file of files) {
205
- const fileDate = file.match(/alerts-(\d{4}-\d{2}-\d{2})/);
206
- if (fileDate && new Date(fileDate[1]) < cutoff) {
207
- break;
208
- }
209
-
210
- try {
211
- const content = JSON.parse(fs.readFileSync(
212
- path.join(this.config.alertsDir, file),
213
- 'utf8'
214
- ));
215
- alerts.push(...(content.alerts || []));
216
- } catch (e) {
217
- // Skip corrupted files
218
- }
219
- }
220
- }
221
- } catch (error) {
222
- console.error('[ALERT-MANAGER] Error reading alert history:', error.message);
223
- }
224
-
225
- // Filter
226
- let filtered = alerts;
227
- if (level) {
228
- filtered = filtered.filter(a => a.level === level);
229
- }
230
- if (category) {
231
- filtered = filtered.filter(a => a.category === category);
232
- }
233
-
234
- return filtered;
235
- }
236
-
237
- /**
238
- * Clear old alerts
239
- */
240
- async clearOldAlerts(daysToKeep = 30) {
241
- const cutoff = new Date();
242
- cutoff.setDate(cutoff.getDate() - daysToKeep);
243
-
244
- let deletedCount = 0;
245
-
246
- try {
247
- if (fs.existsSync(this.config.alertsDir)) {
248
- const files = fs.readdirSync(this.config.alertsDir);
249
-
250
- for (const file of files) {
251
- const fileDate = file.match(/(\d{4}-\d{2}-\d{2})/);
252
- if (fileDate && new Date(fileDate[1]) < cutoff) {
253
- fs.unlinkSync(path.join(this.config.alertsDir, file));
254
- deletedCount++;
255
- }
256
- }
257
- }
258
- } catch (error) {
259
- console.error('[ALERT-MANAGER] Error clearing old alerts:', error.message);
260
- }
261
-
262
- return { deleted: deletedCount };
263
- }
264
-
265
- // Private methods
266
-
267
- _ensureDirectories() {
268
- try {
269
- if (!fs.existsSync(this.config.alertsDir)) {
270
- fs.mkdirSync(this.config.alertsDir, { recursive: true });
271
- }
272
- if (!fs.existsSync(this.config.reportsDir)) {
273
- fs.mkdirSync(this.config.reportsDir, { recursive: true });
274
- }
275
- } catch (error) {
276
- console.warn('[ALERT-MANAGER] Could not create directories:', error.message);
277
- }
278
- }
279
-
280
- _normalizeAlert(alert) {
281
- return {
282
- id: alert.id || this._generateAlertId(),
283
- timestamp: alert.timestamp || new Date().toISOString(),
284
- level: alert.level || 'info',
285
- category: alert.category || 'general',
286
- title: alert.title || 'Alert',
287
- message: alert.message || '',
288
- file: alert.file || null,
289
- line: alert.line || null,
290
- details: alert.details || {}
291
- };
292
- }
293
-
294
- _generateAlertId() {
295
- return `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
296
- }
297
-
298
- _isDuplicate(alert) {
299
- const key = `${alert.level}:${alert.message}:${alert.file}`;
300
- const lastSeen = this.recentAlerts.get(key);
301
-
302
- if (lastSeen && (Date.now() - lastSeen) < this.alertDedupeWindowMs) {
303
- return true;
304
- }
305
-
306
- return false;
307
- }
308
-
309
- _recordAlert(alert) {
310
- const key = `${alert.level}:${alert.message}:${alert.file}`;
311
- this.recentAlerts.set(key, Date.now());
312
-
313
- // Cleanup old entries
314
- const cutoff = Date.now() - this.alertDedupeWindowMs;
315
- for (const [k, v] of this.recentAlerts) {
316
- if (v < cutoff) {
317
- this.recentAlerts.delete(k);
318
- }
319
- }
320
- }
321
-
322
- _outputToConsole(alert) {
323
- const levelColors = {
324
- critical: '\x1b[31m', // Red
325
- error: '\x1b[31m', // Red
326
- warning: '\x1b[33m', // Yellow
327
- info: '\x1b[36m' // Cyan
328
- };
329
-
330
- const reset = '\x1b[0m';
331
- const color = levelColors[alert.level] || '';
332
-
333
- console.log(`${color}[SENTINEL:${alert.level.toUpperCase()}]${reset} ${alert.title}`);
334
- console.log(` ${alert.message}`);
335
-
336
- if (alert.file) {
337
- const relPath = path.relative(this.config.rootPath, alert.file);
338
- console.log(` File: ${relPath}${alert.line ? ':' + alert.line : ''}`);
339
- }
340
-
341
- return true;
342
- }
343
-
344
- async _outputToFile(alert) {
345
- try {
346
- const dateStr = new Date().toISOString().split('T')[0];
347
- const filePath = path.join(this.config.alertsDir, `alerts-${dateStr}.json`);
348
-
349
- let fileData = { date: dateStr, alerts: [] };
350
-
351
- if (fs.existsSync(filePath)) {
352
- try {
353
- fileData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
354
- } catch (e) {
355
- // File corrupted, start fresh
356
- }
357
- }
358
-
359
- fileData.alerts.push(alert);
360
-
361
- // Limit alerts per file
362
- if (fileData.alerts.length > this.config.maxAlertsPerFile) {
363
- fileData.alerts = fileData.alerts.slice(-this.config.maxAlertsPerFile);
364
- }
365
-
366
- fs.writeFileSync(filePath, JSON.stringify(fileData, null, 2));
367
- return true;
368
- } catch (error) {
369
- console.error('[ALERT-MANAGER] File output failed:', error.message);
370
- return false;
371
- }
372
- }
373
-
374
- async _outputToSlack(alert) {
375
- if (!this.config.slackWebhook) {
376
- return false;
377
- }
378
-
379
- try {
380
- // Would use fetch or axios here
381
- // For now, just log that we would send to Slack
382
- console.log(`[ALERT-MANAGER] Would send to Slack: ${alert.title}`);
383
- return true;
384
- } catch (error) {
385
- console.error('[ALERT-MANAGER] Slack output failed:', error.message);
386
- return false;
387
- }
388
- }
389
-
390
- async _createGitHubIssue(alert) {
391
- // Future: Create GitHub issues for critical alerts
392
- // Would use GitHub API or gh CLI
393
- console.log(`[ALERT-MANAGER] Would create GitHub issue: ${alert.title}`);
394
- return false;
395
- }
396
- }
397
-
398
- module.exports = AlertManager;
1
+ /**
2
+ * Alert Manager
3
+ *
4
+ * @purpose Event-driven AlertManager component
5
+ * @module sentinel
6
+ * @layer application
7
+ * @imports-from fs, path, events
8
+ * @exports-to sentinel
9
+ * @side-effects File system writes, File system reads, Console output, Event emission
10
+ * @critical-for sentinel
11
+ */
12
+
13
+ /**
14
+ * Sentinel Alert Manager
15
+ *
16
+ * Handles alerting and reporting for the Sentinel system.
17
+ * Outputs: Console, files, and future integrations (Slack, email, GitHub issues)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const EventEmitter = require('events');
23
+
24
+ class AlertManager extends EventEmitter {
25
+ constructor(config = {}) {
26
+ super();
27
+
28
+ this.config = {
29
+ rootPath: config.rootPath || process.cwd(),
30
+ alertsDir: config.alertsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'alerts'),
31
+ reportsDir: config.reportsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'reports'),
32
+ consoleOutput: config.consoleOutput !== false,
33
+ fileOutput: config.fileOutput !== false,
34
+ maxAlertsPerFile: config.maxAlertsPerFile || 1000,
35
+ slackWebhook: config.slackWebhook || null,
36
+ emailConfig: config.emailConfig || null,
37
+ ...config
38
+ };
39
+
40
+ // Ensure directories exist
41
+ this._ensureDirectories();
42
+
43
+ // Alert history for deduplication
44
+ this.recentAlerts = new Map();
45
+ this.alertDedupeWindowMs = config.alertDedupeWindowMs || 60000; // 1 minute
46
+ }
47
+
48
+ /**
49
+ * Send an alert
50
+ */
51
+ async sendAlert(alert) {
52
+ const normalizedAlert = this._normalizeAlert(alert);
53
+
54
+ // Check for duplicate
55
+ if (this._isDuplicate(normalizedAlert)) {
56
+ return { sent: false, reason: 'duplicate' };
57
+ }
58
+
59
+ // Record alert
60
+ this._recordAlert(normalizedAlert);
61
+
62
+ // Output to configured channels
63
+ const results = {
64
+ console: false,
65
+ file: false,
66
+ slack: false,
67
+ email: false
68
+ };
69
+
70
+ // Console output
71
+ if (this.config.consoleOutput) {
72
+ results.console = this._outputToConsole(normalizedAlert);
73
+ }
74
+
75
+ // File output
76
+ if (this.config.fileOutput) {
77
+ results.file = await this._outputToFile(normalizedAlert);
78
+ }
79
+
80
+ // Slack output (if configured and critical/error)
81
+ if (this.config.slackWebhook && ['critical', 'error'].includes(normalizedAlert.level)) {
82
+ results.slack = await this._outputToSlack(normalizedAlert);
83
+ }
84
+
85
+ this.emit('alertSent', { alert: normalizedAlert, results });
86
+
87
+ return { sent: true, alert: normalizedAlert, results };
88
+ }
89
+
90
+ /**
91
+ * Send daily digest
92
+ */
93
+ async sendDigest(digest) {
94
+ console.log('\n' + '='.repeat(60));
95
+ console.log('[SENTINEL] DAILY DIGEST - ' + digest.date);
96
+ console.log('='.repeat(60));
97
+
98
+ console.log(`\nSummary:`);
99
+ console.log(` Issues Found: ${digest.summary.issuesFound}`);
100
+ console.log(` Critical: ${digest.summary.criticalIssues}`);
101
+ console.log(` Warnings: ${digest.summary.warningIssues}`);
102
+ console.log(` Files Watched: ${digest.summary.filesWatched}`);
103
+ console.log(` Last Scan: ${digest.summary.lastScan || 'N/A'}`);
104
+
105
+ if (digest.topIssues && digest.topIssues.length > 0) {
106
+ console.log(`\nTop Issues:`);
107
+ for (const issue of digest.topIssues.slice(0, 5)) {
108
+ console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
109
+ if (issue.file) {
110
+ console.log(` File: ${path.relative(this.config.rootPath, issue.file)}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ if (digest.recommendations && digest.recommendations.length > 0) {
116
+ console.log(`\nRecommendations:`);
117
+ for (const rec of digest.recommendations) {
118
+ console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
119
+ }
120
+ }
121
+
122
+ console.log('\n' + '='.repeat(60) + '\n');
123
+
124
+ // Save digest to file
125
+ if (this.config.fileOutput) {
126
+ const digestPath = path.join(
127
+ this.config.reportsDir,
128
+ `digest-${digest.date}.json`
129
+ );
130
+ fs.writeFileSync(digestPath, JSON.stringify(digest, null, 2));
131
+ }
132
+
133
+ this.emit('digestSent', digest);
134
+ return { sent: true };
135
+ }
136
+
137
+ /**
138
+ * Send weekly report
139
+ */
140
+ async sendWeeklyReport(report) {
141
+ console.log('\n' + '='.repeat(70));
142
+ console.log('[SENTINEL] WEEKLY REPORT - Week of ' + report.weekOf);
143
+ console.log('='.repeat(70));
144
+
145
+ console.log(`\nMetrics:`);
146
+ console.log(` Total Scans: ${report.summary.totalScans}`);
147
+ console.log(` Issues Detected: ${report.summary.issuesDetected}`);
148
+ console.log(` Issues Resolved: ${report.summary.issuesResolved}`);
149
+ console.log(` Avg Scan Time: ${report.summary.averageScanTime}ms`);
150
+
151
+ console.log(`\nCode Quality:`);
152
+ console.log(` Score: ${report.codeQuality.score}/100 (${report.codeQuality.grade})`);
153
+ console.log(` Complexity: ${report.codeQuality.factors.complexity}`);
154
+ console.log(` Maintainability: ${report.codeQuality.factors.maintainability}`);
155
+ console.log(` Test Coverage: ${report.codeQuality.factors.testCoverage}%`);
156
+
157
+ console.log(`\nTechnical Debt:`);
158
+ console.log(` Estimated Hours: ${report.technicalDebt.estimatedHours}`);
159
+ console.log(` Refactoring: ${report.technicalDebt.categories.refactoring}h`);
160
+ console.log(` Documentation: ${report.technicalDebt.categories.documentation}h`);
161
+ console.log(` Testing: ${report.technicalDebt.categories.testing}h`);
162
+
163
+ if (report.recommendations && report.recommendations.length > 0) {
164
+ console.log(`\nRecommendations:`);
165
+ for (const rec of report.recommendations) {
166
+ console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
167
+ }
168
+ }
169
+
170
+ console.log('\n' + '='.repeat(70) + '\n');
171
+
172
+ // Save report to file
173
+ if (this.config.fileOutput) {
174
+ const reportPath = path.join(
175
+ this.config.reportsDir,
176
+ `weekly-report-${report.weekOf}.json`
177
+ );
178
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
179
+ }
180
+
181
+ this.emit('weeklyReportSent', report);
182
+ return { sent: true };
183
+ }
184
+
185
+ /**
186
+ * Get alert history
187
+ */
188
+ getAlertHistory(options = {}) {
189
+ const { days = 7, level = null, category = null } = options;
190
+
191
+ const cutoff = new Date();
192
+ cutoff.setDate(cutoff.getDate() - days);
193
+
194
+ // Read from alert files
195
+ const alerts = [];
196
+
197
+ try {
198
+ if (fs.existsSync(this.config.alertsDir)) {
199
+ const files = fs.readdirSync(this.config.alertsDir)
200
+ .filter(f => f.startsWith('alerts-') && f.endsWith('.json'))
201
+ .sort()
202
+ .reverse();
203
+
204
+ for (const file of files) {
205
+ const fileDate = file.match(/alerts-(\d{4}-\d{2}-\d{2})/);
206
+ if (fileDate && new Date(fileDate[1]) < cutoff) {
207
+ break;
208
+ }
209
+
210
+ try {
211
+ const content = JSON.parse(fs.readFileSync(
212
+ path.join(this.config.alertsDir, file),
213
+ 'utf8'
214
+ ));
215
+ alerts.push(...(content.alerts || []));
216
+ } catch (e) {
217
+ // Skip corrupted files
218
+ }
219
+ }
220
+ }
221
+ } catch (error) {
222
+ console.error('[ALERT-MANAGER] Error reading alert history:', error.message);
223
+ }
224
+
225
+ // Filter
226
+ let filtered = alerts;
227
+ if (level) {
228
+ filtered = filtered.filter(a => a.level === level);
229
+ }
230
+ if (category) {
231
+ filtered = filtered.filter(a => a.category === category);
232
+ }
233
+
234
+ return filtered;
235
+ }
236
+
237
+ /**
238
+ * Clear old alerts
239
+ */
240
+ async clearOldAlerts(daysToKeep = 30) {
241
+ const cutoff = new Date();
242
+ cutoff.setDate(cutoff.getDate() - daysToKeep);
243
+
244
+ let deletedCount = 0;
245
+
246
+ try {
247
+ if (fs.existsSync(this.config.alertsDir)) {
248
+ const files = fs.readdirSync(this.config.alertsDir);
249
+
250
+ for (const file of files) {
251
+ const fileDate = file.match(/(\d{4}-\d{2}-\d{2})/);
252
+ if (fileDate && new Date(fileDate[1]) < cutoff) {
253
+ fs.unlinkSync(path.join(this.config.alertsDir, file));
254
+ deletedCount++;
255
+ }
256
+ }
257
+ }
258
+ } catch (error) {
259
+ console.error('[ALERT-MANAGER] Error clearing old alerts:', error.message);
260
+ }
261
+
262
+ return { deleted: deletedCount };
263
+ }
264
+
265
+ // Private methods
266
+
267
+ _ensureDirectories() {
268
+ try {
269
+ if (!fs.existsSync(this.config.alertsDir)) {
270
+ fs.mkdirSync(this.config.alertsDir, { recursive: true });
271
+ }
272
+ if (!fs.existsSync(this.config.reportsDir)) {
273
+ fs.mkdirSync(this.config.reportsDir, { recursive: true });
274
+ }
275
+ } catch (error) {
276
+ console.warn('[ALERT-MANAGER] Could not create directories:', error.message);
277
+ }
278
+ }
279
+
280
+ _normalizeAlert(alert) {
281
+ return {
282
+ id: alert.id || this._generateAlertId(),
283
+ timestamp: alert.timestamp || new Date().toISOString(),
284
+ level: alert.level || 'info',
285
+ category: alert.category || 'general',
286
+ title: alert.title || 'Alert',
287
+ message: alert.message || '',
288
+ file: alert.file || null,
289
+ line: alert.line || null,
290
+ details: alert.details || {}
291
+ };
292
+ }
293
+
294
+ _generateAlertId() {
295
+ return `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
296
+ }
297
+
298
+ _isDuplicate(alert) {
299
+ const key = `${alert.level}:${alert.message}:${alert.file}`;
300
+ const lastSeen = this.recentAlerts.get(key);
301
+
302
+ if (lastSeen && (Date.now() - lastSeen) < this.alertDedupeWindowMs) {
303
+ return true;
304
+ }
305
+
306
+ return false;
307
+ }
308
+
309
+ _recordAlert(alert) {
310
+ const key = `${alert.level}:${alert.message}:${alert.file}`;
311
+ this.recentAlerts.set(key, Date.now());
312
+
313
+ // Cleanup old entries
314
+ const cutoff = Date.now() - this.alertDedupeWindowMs;
315
+ for (const [k, v] of this.recentAlerts) {
316
+ if (v < cutoff) {
317
+ this.recentAlerts.delete(k);
318
+ }
319
+ }
320
+ }
321
+
322
+ _outputToConsole(alert) {
323
+ const levelColors = {
324
+ critical: '\x1b[31m', // Red
325
+ error: '\x1b[31m', // Red
326
+ warning: '\x1b[33m', // Yellow
327
+ info: '\x1b[36m' // Cyan
328
+ };
329
+
330
+ const reset = '\x1b[0m';
331
+ const color = levelColors[alert.level] || '';
332
+
333
+ console.log(`${color}[SENTINEL:${alert.level.toUpperCase()}]${reset} ${alert.title}`);
334
+ console.log(` ${alert.message}`);
335
+
336
+ if (alert.file) {
337
+ const relPath = path.relative(this.config.rootPath, alert.file);
338
+ console.log(` File: ${relPath}${alert.line ? ':' + alert.line : ''}`);
339
+ }
340
+
341
+ return true;
342
+ }
343
+
344
+ async _outputToFile(alert) {
345
+ try {
346
+ const dateStr = new Date().toISOString().split('T')[0];
347
+ const filePath = path.join(this.config.alertsDir, `alerts-${dateStr}.json`);
348
+
349
+ let fileData = { date: dateStr, alerts: [] };
350
+
351
+ if (fs.existsSync(filePath)) {
352
+ try {
353
+ fileData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
354
+ } catch (e) {
355
+ // File corrupted, start fresh
356
+ }
357
+ }
358
+
359
+ fileData.alerts.push(alert);
360
+
361
+ // Limit alerts per file
362
+ if (fileData.alerts.length > this.config.maxAlertsPerFile) {
363
+ fileData.alerts = fileData.alerts.slice(-this.config.maxAlertsPerFile);
364
+ }
365
+
366
+ fs.writeFileSync(filePath, JSON.stringify(fileData, null, 2));
367
+ return true;
368
+ } catch (error) {
369
+ console.error('[ALERT-MANAGER] File output failed:', error.message);
370
+ return false;
371
+ }
372
+ }
373
+
374
+ async _outputToSlack(alert) {
375
+ if (!this.config.slackWebhook) {
376
+ return false;
377
+ }
378
+
379
+ try {
380
+ // Would use fetch or axios here
381
+ // For now, just log that we would send to Slack
382
+ console.log(`[ALERT-MANAGER] Would send to Slack: ${alert.title}`);
383
+ return true;
384
+ } catch (error) {
385
+ console.error('[ALERT-MANAGER] Slack output failed:', error.message);
386
+ return false;
387
+ }
388
+ }
389
+
390
+ async _createGitHubIssue(alert) {
391
+ // Future: Create GitHub issues for critical alerts
392
+ // Would use GitHub API or gh CLI
393
+ console.log(`[ALERT-MANAGER] Would create GitHub issue: ${alert.title}`);
394
+ return false;
395
+ }
396
+ }
397
+
398
+ module.exports = AlertManager;