neuralos 2.8.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,229 @@
1
+ /**
2
+ * netdata-rterm plugin — Netdata integration for RTerm.
3
+ *
4
+ * Ingests Netdata alert webhooks (per the Netdata Cloud webhook schema),
5
+ * correlates anomalies with RTerm's metrics ledger + incident history for
6
+ * agent RCA, and fires triggers for auto-remediation playbooks.
7
+ *
8
+ * Netdata webhook alert payload fields (from Netdata docs):
9
+ * message, alert, info, chart, context, space, rooms, family, class,
10
+ * severity (warning|critical|clear), date (ISO8601), duration,
11
+ * additional_active_critical_alerts, additional_active_warning_alerts,
12
+ * alert_url
13
+ *
14
+ * Netdata webhook reachability payload fields:
15
+ * message, node, space, rooms, status (up|down), date, duration
16
+ */
17
+
18
+ // --- Pure: parse a Netdata webhook alert payload ---
19
+ export function parseNetdataAlert(payload) {
20
+ if (!payload || typeof payload !== 'object') return null
21
+
22
+ // Alert notification
23
+ if (payload.alert && payload.severity) {
24
+ return {
25
+ kind: 'alert',
26
+ alert: String(payload.alert),
27
+ message: String(payload.message ?? ''),
28
+ info: String(payload.info ?? ''),
29
+ chart: String(payload.chart ?? ''),
30
+ context: String(payload.context ?? ''),
31
+ space: String(payload.space ?? ''),
32
+ family: String(payload.family ?? ''),
33
+ class: String(payload.class ?? ''),
34
+ severity: String(payload.severity), // warning | critical | clear
35
+ date: String(payload.date ?? ''),
36
+ duration: String(payload.duration ?? ''),
37
+ alertUrl: String(payload.alert_url ?? ''),
38
+ additionalCritical: Number(payload.additional_active_critical_alerts ?? 0),
39
+ additionalWarning: Number(payload.additional_active_warning_alerts ?? 0),
40
+ // host is inferred from space/node; Netdata Cloud sends space name.
41
+ host: String(payload.space ?? 'unknown'),
42
+ }
43
+ }
44
+
45
+ // Reachability notification
46
+ if (payload.node && payload.status) {
47
+ return {
48
+ kind: 'reachability',
49
+ message: String(payload.message ?? ''),
50
+ node: String(payload.node),
51
+ space: String(payload.space ?? ''),
52
+ status: String(payload.status), // up | down
53
+ date: String(payload.date ?? ''),
54
+ duration: String(payload.duration ?? ''),
55
+ host: String(payload.node ?? 'unknown'),
56
+ }
57
+ }
58
+
59
+ return null
60
+ }
61
+
62
+ // --- Pure: map Netdata severity to RTerm alert severity ---
63
+ export function mapSeverity(netdataSeverity) {
64
+ const s = String(netdataSeverity).toLowerCase()
65
+ if (s === 'critical') return 'critical'
66
+ if (s === 'warning') return 'warning'
67
+ if (s === 'clear') return 'info' // alert cleared
68
+ return 'info'
69
+ }
70
+
71
+ // --- Pure: build an RTerm alert fingerprint from a parsed Netdata alert ---
72
+ export function buildFingerprint(parsed) {
73
+ if (!parsed) return ''
74
+ if (parsed.kind === 'reachability') {
75
+ return `netdata:reachability:${parsed.host}:${parsed.status}`
76
+ }
77
+ return `netdata:${parsed.host}:${parsed.alert}:${parsed.severity}`
78
+ }
79
+
80
+ // --- Pure: build a trigger event from a parsed Netdata alert ---
81
+ export function toTriggerEvent(parsed) {
82
+ if (!parsed) return null
83
+ const severity = parsed.kind === 'reachability'
84
+ ? (parsed.status === 'down' ? 'critical' : 'info')
85
+ : mapSeverity(parsed.severity)
86
+
87
+ return {
88
+ source: 'netdata',
89
+ fingerprint: buildFingerprint(parsed),
90
+ title: parsed.kind === 'reachability'
91
+ ? `${parsed.host} ${parsed.status === 'down' ? 'is DOWN' : 'is UP'}`
92
+ : `[Netdata] ${parsed.alert} on ${parsed.host}`,
93
+ severity,
94
+ detail: parsed.message || parsed.info || '',
95
+ labels: {
96
+ host: parsed.host,
97
+ alert: parsed.alert ?? '',
98
+ chart: parsed.chart ?? '',
99
+ context: parsed.context ?? '',
100
+ netdata_severity: parsed.severity ?? parsed.status ?? '',
101
+ netdata_space: parsed.space ?? '',
102
+ },
103
+ at: parsed.date ? new Date(parsed.date).getTime() : Date.now(),
104
+ }
105
+ }
106
+
107
+ // --- Pure: correlate a Netdata alert with RTerm's metrics + incidents ---
108
+ export function correlateWithRterm(parsed, metricsLedger, incidentLedger) {
109
+ if (!parsed) return { recentMetrics: null, openIncidents: [], correlation: '' }
110
+
111
+ const host = parsed.host
112
+
113
+ // Pull recent metrics for the host from the ledger.
114
+ let recentMetrics = null
115
+ if (metricsLedger && typeof metricsLedger.snapshot === 'function') {
116
+ try {
117
+ recentMetrics = metricsLedger.snapshot(host)
118
+ } catch { /* best-effort */ }
119
+ }
120
+
121
+ // Pull open incidents for the host.
122
+ let openIncidents = []
123
+ if (incidentLedger && typeof incidentLedger.list === 'function') {
124
+ try {
125
+ const all = incidentLedger.list()
126
+ openIncidents = all.filter(
127
+ (inc) => Array.isArray(inc.affected) && inc.affected.includes(host) && inc.status !== 'resolved'
128
+ )
129
+ } catch { /* best-effort */ }
130
+ }
131
+
132
+ // Build a correlation summary for the agent.
133
+ const parts = []
134
+ if (recentMetrics) {
135
+ parts.push(`Recent metrics for ${host}: ${JSON.stringify(recentMetrics).slice(0, 500)}`)
136
+ }
137
+ if (openIncidents.length > 0) {
138
+ parts.push(`${openIncidents.length} open incident(s) for ${host}: ${openIncidents.map((i) => i.title).join('; ')}`)
139
+ }
140
+ if (parsed.kind === 'alert' && parsed.additionalCritical > 0) {
141
+ parts.push(`${parsed.additionalCritical} additional critical alert(s) on the same node`)
142
+ }
143
+
144
+ const correlation = parts.length > 0
145
+ ? `Netdata alert "${parsed.alert ?? parsed.message}" on ${host} correlates with: ${parts.join('. ')}.`
146
+ : `No prior RTerm context for ${host}.`
147
+
148
+ return { recentMetrics, openIncidents, correlation }
149
+ }
150
+
151
+ // --- Plugin entry: register tools, triggers, panel ---
152
+ export function register(ctx) {
153
+ const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
154
+
155
+ // Tool: netdata_alert_summary — summarize recent Netdata alerts for a host.
156
+ registerTool({
157
+ name: 'netdata_alert_summary',
158
+ description: 'Summarize recent Netdata alerts for a host. Returns parsed alert details including severity, chart, context, and duration.',
159
+ params: { host: { type: 'string', description: 'Host name to summarize alerts for' } },
160
+ handler: async (params) => {
161
+ const host = params?.host ?? 'unknown'
162
+ log(`[netdata] alert summary requested for ${host}`)
163
+ return { host, summary: `No cached Netdata alerts for ${host}. Configure Netdata Cloud to send webhooks to RTerm's gateway.` }
164
+ },
165
+ })
166
+
167
+ // Tool: netdata_correlate — correlate a Netdata alert with RTerm's metrics + incidents.
168
+ registerTool({
169
+ name: 'netdata_correlate',
170
+ description: 'Correlate a Netdata alert with RTerm metrics ledger and incident history for root-cause analysis. Returns recent metrics, open incidents, and a correlation summary.',
171
+ params: {
172
+ alert: { type: 'object', description: 'Parsed Netdata alert payload (from webhook)' },
173
+ },
174
+ handler: async (params) => {
175
+ const parsed = parseNetdataAlert(params?.alert)
176
+ if (!parsed) return { error: 'Invalid Netdata alert payload' }
177
+
178
+ const metrics = readLedger ? readLedger('metrics') : null
179
+ const incidents = readLedger ? readLedger('incidents') : null
180
+
181
+ // readLedger returns ledger-like objects or plain data.
182
+ const metricsLedger = metrics && typeof metrics === 'object' && typeof metrics.snapshot === 'function' ? metrics : null
183
+ const incidentLedger = incidents && typeof incidents === 'object' && typeof incidents.list === 'function' ? incidents : null
184
+
185
+ const result = correlateWithRterm(parsed, metricsLedger, incidentLedger)
186
+ log(`[netdata] correlated alert "${parsed.alert ?? parsed.message}" on ${parsed.host}: ${result.correlation}`)
187
+ return { parsed, ...result }
188
+ },
189
+ })
190
+
191
+ // Trigger: netdata_critical_alert — fires when a Netdata webhook delivers a critical alert.
192
+ registerTrigger({
193
+ name: 'netdata_critical_alert',
194
+ description: 'Fires when Netdata sends a critical-severity alert via webhook. Use for auto-remediation playbooks.',
195
+ match: (event) => {
196
+ if (event?.source !== 'netdata') return false
197
+ return event.severity === 'critical'
198
+ },
199
+ action: 'run-playbook', // operator assigns a remediation playbook
200
+ })
201
+
202
+ // Trigger: netdata_warning_alert — fires when a Netdata webhook delivers a warning alert.
203
+ registerTrigger({
204
+ name: 'netdata_warning_alert',
205
+ description: 'Fires when Netdata sends a warning-severity alert via webhook. Use for investigation/diagnosis playbooks.',
206
+ match: (event) => {
207
+ if (event?.source !== 'netdata') return false
208
+ return event.severity === 'warning'
209
+ },
210
+ action: 'propose-change', // propose a MOP change for investigation
211
+ })
212
+
213
+ // Panel: netdata-alert-feed — dashboard panel showing recent Netdata alerts.
214
+ registerPanel({
215
+ name: 'netdata-alert-feed',
216
+ title: 'Netdata Alert Feed',
217
+ render: (data) => {
218
+ const alerts = Array.isArray(data) ? data : []
219
+ const rows = alerts.map((a) =>
220
+ `<tr><td>${a.host ?? ''}</td><td>${a.alert ?? a.message ?? ''}</td><td>${a.severity ?? a.status ?? ''}</td><td>${a.date ?? ''}</td></tr>`
221
+ ).join('')
222
+ return `<div class="netdata-alert-feed"><h3>Netdata Alerts</h3><table><thead><tr><th>Host</th><th>Alert</th><th>Severity</th><th>Date</th></tr></thead><tbody>${rows}</tbody></table></div>`
223
+ },
224
+ })
225
+
226
+ log('[netdata] netdata-rterm plugin registered: 2 tools, 2 triggers, 1 panel')
227
+ }
228
+
229
+ export default { register, parseNetdataAlert, mapSeverity, buildFingerprint, toTriggerEvent, correlateWithRterm }
@@ -0,0 +1,252 @@
1
+ import {
2
+ parseNetdataAlert, mapSeverity, buildFingerprint, toTriggerEvent, correlateWithRterm, register,
3
+ } from './index.mjs'
4
+
5
+ const cases: Array<{ name: string; run: () => void | Promise<void> }> = []
6
+ function test(n: string, r: () => void | Promise<void>) { cases.push({ name: n, run: r }) }
7
+
8
+ // ---- parseNetdataAlert ----
9
+ test('parse: alert notification with all fields', () => {
10
+ const payload = {
11
+ message: 'CPU usage is 95%', alert: 'cpu_usage', info: 'CPU utilization too high',
12
+ chart: 'system.cpu', context: 'system.cpu', space: 'prod-cluster', family: 'cpu',
13
+ class: 'Error', severity: 'critical', date: '2026-07-22T10:00:00Z', duration: '5m',
14
+ additional_active_critical_alerts: 2, additional_active_warning_alerts: 1,
15
+ alert_url: 'https://app.netdata.cloud/alert/123',
16
+ }
17
+ const p = parseNetdataAlert(payload)
18
+ if (!p || p.kind !== 'alert') throw new Error('should parse alert')
19
+ if (p.alert !== 'cpu_usage') throw new Error('alert name')
20
+ if (p.severity !== 'critical') throw new Error('severity')
21
+ if (p.chart !== 'system.cpu') throw new Error('chart')
22
+ if (p.additionalCritical !== 2) throw new Error('additional critical')
23
+ if (p.host !== 'prod-cluster') throw new Error('host from space')
24
+ if (p.alertUrl !== 'https://app.netdata.cloud/alert/123') throw new Error('alert url')
25
+ })
26
+
27
+ test('parse: warning severity', () => {
28
+ const p = parseNetdataAlert({ alert: 'disk_space', severity: 'warning', message: 'Disk 80% full', space: 'web-01' })
29
+ if (!p || p.severity !== 'warning') throw new Error('should be warning')
30
+ })
31
+
32
+ test('parse: clear severity (alert resolved)', () => {
33
+ const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'clear', message: 'CPU back to normal', space: 'web-01' })
34
+ if (!p || p.severity !== 'clear') throw new Error('should be clear')
35
+ })
36
+
37
+ test('parse: reachability notification (node down)', () => {
38
+ const p = parseNetdataAlert({ message: 'Node unreachable', node: 'web-02', space: 'prod', status: 'down', date: '2026-07-22T10:00:00Z', duration: '2m' })
39
+ if (!p || p.kind !== 'reachability') throw new Error('should parse reachability')
40
+ if (p.status !== 'down') throw new Error('status')
41
+ if (p.host !== 'web-02') throw new Error('host from node')
42
+ })
43
+
44
+ test('parse: reachability notification (node up)', () => {
45
+ const p = parseNetdataAlert({ node: 'web-02', status: 'up', date: '2026-07-22T10:05:00Z' })
46
+ if (!p || p.status !== 'up') throw new Error('should be up')
47
+ })
48
+
49
+ test('parse: null for invalid payload', () => {
50
+ if (parseNetdataAlert(null) !== null) throw new Error('null payload')
51
+ if (parseNetdataAlert({}) !== null) throw new Error('empty object')
52
+ if (parseNetdataAlert('not an object') !== null) throw new Error('string')
53
+ if (parseNetdataAlert({ foo: 'bar' }) !== null) throw new Error('missing required fields')
54
+ })
55
+
56
+ // ---- mapSeverity ----
57
+ test('mapSeverity: critical -> critical', () => {
58
+ if (mapSeverity('critical') !== 'critical') throw new Error('critical')
59
+ })
60
+ test('mapSeverity: warning -> warning', () => {
61
+ if (mapSeverity('warning') !== 'warning') throw new Error('warning')
62
+ })
63
+ test('mapSeverity: clear -> info', () => {
64
+ if (mapSeverity('clear') !== 'info') throw new Error('clear should map to info')
65
+ })
66
+ test('mapSeverity: unknown -> info', () => {
67
+ if (mapSeverity('unknown') !== 'info') throw new Error('unknown')
68
+ })
69
+
70
+ // ---- buildFingerprint ----
71
+ test('buildFingerprint: alert fingerprint', () => {
72
+ const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'critical', space: 'web-01' })
73
+ const fp = buildFingerprint(p)
74
+ if (fp !== 'netdata:web-01:cpu_usage:critical') throw new Error(`got ${fp}`)
75
+ })
76
+ test('buildFingerprint: reachability fingerprint', () => {
77
+ const p = parseNetdataAlert({ node: 'web-02', status: 'down' })
78
+ const fp = buildFingerprint(p)
79
+ if (fp !== 'netdata:reachability:web-02:down') throw new Error(`got ${fp}`)
80
+ })
81
+ test('buildFingerprint: empty for null', () => {
82
+ if (buildFingerprint(null) !== '') throw new Error('should be empty')
83
+ })
84
+
85
+ // ---- toTriggerEvent ----
86
+ test('toTriggerEvent: alert -> trigger event with correct severity', () => {
87
+ const p = parseNetdataAlert({ alert: 'disk_full', severity: 'critical', space: 'db-01', message: 'Disk 95%', date: '2026-07-22T10:00:00Z' })
88
+ const evt = toTriggerEvent(p)
89
+ if (!evt) throw new Error('should produce event')
90
+ if (evt.source !== 'netdata') throw new Error('source')
91
+ if (evt.severity !== 'critical') throw new Error('severity')
92
+ if (!evt.title.includes('disk_full')) throw new Error('title')
93
+ if (!evt.title.includes('db-01')) throw new Error('title host')
94
+ if (evt.labels.host !== 'db-01') throw new Error('labels host')
95
+ if (evt.labels.alert !== 'disk_full') throw new Error('labels alert')
96
+ })
97
+
98
+ test('toTriggerEvent: reachability down -> critical', () => {
99
+ const p = parseNetdataAlert({ node: 'web-03', status: 'down', date: '2026-07-22T10:00:00Z' })
100
+ const evt = toTriggerEvent(p)
101
+ if (!evt || evt.severity !== 'critical') throw new Error('down should be critical')
102
+ if (!evt.title.includes('DOWN')) throw new Error('title')
103
+ })
104
+
105
+ test('toTriggerEvent: reachability up -> info', () => {
106
+ const p = parseNetdataAlert({ node: 'web-03', status: 'up', date: '2026-07-22T10:00:00Z' })
107
+ const evt = toTriggerEvent(p)
108
+ if (!evt || evt.severity !== 'info') throw new Error('up should be info')
109
+ })
110
+
111
+ test('toTriggerEvent: null parsed -> null event', () => {
112
+ if (toTriggerEvent(null) !== null) throw new Error('should be null')
113
+ })
114
+
115
+ // ---- correlateWithRterm ----
116
+ test('correlate: with metrics + incidents', () => {
117
+ const p = parseNetdataAlert({ alert: 'cpu_usage', severity: 'critical', space: 'web-01', additional_active_critical_alerts: 3 })
118
+ const mockMetrics = { snapshot: (host: string) => ({ host, cpuUsagePercent: 95, memoryUsagePercent: 70 }) }
119
+ const mockIncidents = { list: () => [
120
+ { title: 'web-01 disk full', affected: ['web-01'], status: 'open' },
121
+ { title: 'web-02 network issue', affected: ['web-02'], status: 'open' },
122
+ { title: 'web-01 resolved issue', affected: ['web-01'], status: 'resolved' },
123
+ ] }
124
+ const result = correlateWithRterm(p, mockMetrics as any, mockIncidents as any)
125
+ if (!result.recentMetrics || result.recentMetrics.cpuUsagePercent !== 95) throw new Error('metrics')
126
+ if (result.openIncidents.length !== 1) throw new Error(`expected 1 open incident, got ${result.openIncidents.length}`)
127
+ if (!result.correlation.includes('cpu_usage')) throw new Error('correlation should mention alert')
128
+ if (!result.correlation.includes('disk full')) throw new Error('correlation should mention incident')
129
+ if (!result.correlation.includes('3 additional critical')) throw new Error('correlation should mention additional alerts')
130
+ })
131
+
132
+ test('correlate: no prior context', () => {
133
+ const p = parseNetdataAlert({ alert: 'mem_usage', severity: 'warning', space: 'new-host' })
134
+ const result = correlateWithRterm(p, null, null)
135
+ if (result.recentMetrics !== null) throw new Error('no metrics')
136
+ if (result.openIncidents.length !== 0) throw new Error('no incidents')
137
+ if (!result.correlation.includes('No prior RTerm context')) throw new Error('should say no context')
138
+ })
139
+
140
+ test('correlate: null parsed -> empty', () => {
141
+ const result = correlateWithRterm(null, null, null)
142
+ if (result.recentMetrics !== null || result.openIncidents.length !== 0) throw new Error('should be empty')
143
+ })
144
+
145
+ // ---- register (plugin lifecycle) ----
146
+ test('register: registers 2 tools, 2 triggers, 1 panel', () => {
147
+ const tools: any[] = [], triggers: any[] = [], panels: any[] = [], logs: string[] = []
148
+ register({
149
+ registerTool: (t) => tools.push(t),
150
+ registerTrigger: (t) => triggers.push(t),
151
+ registerPanel: (p) => panels.push(p),
152
+ exec: async () => '',
153
+ readLedger: () => ({}),
154
+ log: (line: string) => logs.push(line),
155
+ } as any)
156
+ if (tools.length !== 2) throw new Error(`expected 2 tools, got ${tools.length}`)
157
+ if (triggers.length !== 2) throw new Error(`expected 2 triggers, got ${triggers.length}`)
158
+ if (panels.length !== 1) throw new Error(`expected 1 panel, got ${panels.length}`)
159
+ if (!tools.some((t) => t.name === 'netdata_alert_summary')) throw new Error('missing alert_summary tool')
160
+ if (!tools.some((t) => t.name === 'netdata_correlate')) throw new Error('missing correlate tool')
161
+ if (!triggers.some((t) => t.name === 'netdata_critical_alert')) throw new Error('missing critical trigger')
162
+ if (!triggers.some((t) => t.name === 'netdata_warning_alert')) throw new Error('missing warning trigger')
163
+ if (!panels.some((p) => p.name === 'netdata-alert-feed')) throw new Error('missing alert feed panel')
164
+ if (!logs.some((l) => l.includes('registered'))) throw new Error('should log registration')
165
+ })
166
+
167
+ test('register: critical trigger matches critical events only', () => {
168
+ const triggers: any[] = []
169
+ register({
170
+ registerTool: () => {}, registerTrigger: (t) => triggers.push(t), registerPanel: () => {},
171
+ exec: async () => '', readLedger: () => ({}), log: () => {},
172
+ } as any)
173
+ const critTrigger = triggers.find((t) => t.name === 'netdata_critical_alert')
174
+ if (!critTrigger) throw new Error('missing critical trigger')
175
+ if (!critTrigger.match({ source: 'netdata', severity: 'critical' })) throw new Error('should match critical')
176
+ if (critTrigger.match({ source: 'netdata', severity: 'warning' })) throw new Error('should NOT match warning')
177
+ if (critTrigger.match({ source: 'other', severity: 'critical' })) throw new Error('should NOT match non-netdata')
178
+ if (critTrigger.match({})) throw new Error('should NOT match empty')
179
+ })
180
+
181
+ test('register: warning trigger matches warning events only', () => {
182
+ const triggers: any[] = []
183
+ register({
184
+ registerTool: () => {}, registerTrigger: (t) => triggers.push(t), registerPanel: () => {},
185
+ exec: async () => '', readLedger: () => ({}), log: () => {},
186
+ } as any)
187
+ const warnTrigger = triggers.find((t) => t.name === 'netdata_warning_alert')
188
+ if (!warnTrigger) throw new Error('missing warning trigger')
189
+ if (!warnTrigger.match({ source: 'netdata', severity: 'warning' })) throw new Error('should match warning')
190
+ if (warnTrigger.match({ source: 'netdata', severity: 'critical' })) throw new Error('should NOT match critical')
191
+ })
192
+
193
+ test('register: panel renders alert rows', () => {
194
+ const panels: any[] = []
195
+ register({
196
+ registerTool: () => {}, registerTrigger: () => {}, registerPanel: (p) => panels.push(p),
197
+ exec: async () => '', readLedger: () => ({}), log: () => {},
198
+ } as any)
199
+ const panel = panels[0]
200
+ const html = panel.render([
201
+ { host: 'web-01', alert: 'cpu_high', severity: 'critical', date: '2026-07-22' },
202
+ { host: 'web-02', alert: 'disk_full', severity: 'warning', date: '2026-07-22' },
203
+ ])
204
+ if (!html.includes('cpu_high') || !html.includes('disk_full')) throw new Error('should contain alert names')
205
+ if (!html.includes('<table>')) throw new Error('should render table')
206
+ })
207
+
208
+ test('register: panel renders empty feed', () => {
209
+ const panels: any[] = []
210
+ register({
211
+ registerTool: () => {}, registerTrigger: () => {}, registerPanel: (p) => panels.push(p),
212
+ exec: async () => '', readLedger: () => ({}), log: () => {},
213
+ } as any)
214
+ const html = panels[0].render(null)
215
+ if (!html.includes('Netdata Alerts')) throw new Error('should have title even when empty')
216
+ })
217
+
218
+ test('register: netdata_correlate tool handles invalid payload', async () => {
219
+ const tools: any[] = []
220
+ register({
221
+ registerTool: (t) => tools.push(t), registerTrigger: () => {}, registerPanel: () => {},
222
+ exec: async () => '', readLedger: () => ({}), log: () => {},
223
+ } as any)
224
+ const correlateTool = tools.find((t) => t.name === 'netdata_correlate')
225
+ const result = await correlateTool.handler({ alert: { foo: 'bar' } })
226
+ if (!result.error) throw new Error('should return error for invalid payload')
227
+ })
228
+
229
+ test('register: netdata_correlate tool correlates valid payload', async () => {
230
+ const tools: any[] = []
231
+ register({
232
+ registerTool: (t) => tools.push(t), registerTrigger: () => {}, registerPanel: () => {},
233
+ exec: async () => '', readLedger: () => null, log: () => {},
234
+ } as any)
235
+ const correlateTool = tools.find((t) => t.name === 'netdata_correlate')
236
+ const result = await correlateTool.handler({
237
+ alert: { alert: 'cpu_usage', severity: 'critical', space: 'web-01', message: 'CPU 95%' },
238
+ })
239
+ if (!result.parsed) throw new Error('should return parsed alert')
240
+ if (result.parsed.alert !== 'cpu_usage') throw new Error('alert name')
241
+ })
242
+
243
+ async function main() {
244
+ let pass = 0, fail = 0
245
+ for (const c of cases) {
246
+ try { await c.run(); pass++; console.log(`PASS ${c.name}`) }
247
+ catch (e: any) { fail++; console.log(`FAIL ${c.name}: ${e?.message ?? e}`) }
248
+ }
249
+ console.log(`\n${pass}/${cases.length} passed, ${fail} failed`)
250
+ if (fail > 0) process.exit(1)
251
+ }
252
+ void main()
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "netdata-rterm",
3
+ "version": "1.0.0",
4
+ "description": "Netdata integration for RTerm — ingests Netdata alert webhooks as triggers, correlates anomalies with changes/incidents for agent RCA + auto-remediation. Bridges Netdata's per-second monitoring + ML anomaly detection with RTerm's AI agent and SRE pillar.",
5
+ "entry": "index.mjs",
6
+ "tools": [
7
+ "netdata_alert_summary",
8
+ "netdata_correlate"
9
+ ],
10
+ "triggers": [
11
+ "netdata_critical_alert",
12
+ "netdata_warning_alert"
13
+ ],
14
+ "panels": [
15
+ "netdata-alert-feed"
16
+ ],
17
+ "permissions": [
18
+ "exec",
19
+ "readLedger:metrics",
20
+ "readLedger:incidents"
21
+ ]
22
+ }
@@ -0,0 +1,10 @@
1
+ // patch-manager plugin type declarations
2
+ export function register(ctx: any): void
3
+ export function buildPatchStatusCommand(os: string): string
4
+ export function buildPatchApplyCommand(os: string, opts?: { severity?: string; dryRun?: boolean }): string
5
+ export function buildPrePatchCheckCommand(os: string): string
6
+ export function buildPostPatchCheckCommand(os: string): string
7
+ export function parsePatchStatus(output: string | null | undefined, os: string | null | undefined): { patches: Array<{ id: string; title: string; severity: string; os: string }>; summary: { total: number; critical: number; security: number; recommended: number } }
8
+ export function buildPatchPlan(host: string, os: string, patchStatus: any, opts?: { severity?: string }): any
9
+ export function buildComplianceReport(hostStatuses: Record<string, any> | null | undefined): any
10
+ export default any