prismalens 0.2.0 → 0.4.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.
- package/README.md +29 -13
- package/dist/bin/prismalens.js +39 -50
- package/dist/chunk-2Z7QZSIS.js +20 -0
- package/dist/chunk-5BT3XQOG.js +2990 -0
- package/dist/chunk-6N4PHBII.js +30 -0
- package/dist/chunk-CTKCK7QT.js +387 -0
- package/dist/chunk-CVSS5JPC.js +130 -0
- package/dist/chunk-HPE5CTO4.js +22 -0
- package/dist/chunk-PTBCAI34.js +34 -0
- package/dist/chunk-PVQVFRIV.js +452 -0
- package/dist/chunk-QDK5PAI3.js +155 -0
- package/dist/chunk-SSEIMBQ2.js +204 -0
- package/dist/chunk-TGP6QTFO.js +156 -0
- package/dist/chunk-V6M6ISKI.js +161 -0
- package/dist/chunk-XH6DIKTA.js +83 -0
- package/dist/src/cli/auth.js +112 -101
- package/dist/src/cli/doctor.js +184 -187
- package/dist/src/cli/flags.js +6 -29
- package/dist/src/cli/grouping.js +16 -227
- package/dist/src/cli/init.js +58 -52
- package/dist/src/cli/investigate.js +23 -465
- package/dist/src/cli/listen.js +550 -286
- package/dist/src/cli/report.js +70 -67
- package/dist/src/cli/serve.js +260 -30
- package/dist/src/cli/status.js +87 -86
- package/package.json +16 -7
- package/dist/bin/prismalens.d.ts +0 -3
- package/dist/bin/prismalens.d.ts.map +0 -1
- package/dist/bin/prismalens.js.map +0 -1
- package/dist/src/cli/auth.d.ts +0 -3
- package/dist/src/cli/auth.d.ts.map +0 -1
- package/dist/src/cli/auth.js.map +0 -1
- package/dist/src/cli/doctor.d.ts +0 -18
- package/dist/src/cli/doctor.d.ts.map +0 -1
- package/dist/src/cli/doctor.js.map +0 -1
- package/dist/src/cli/flags.d.ts +0 -2
- package/dist/src/cli/flags.d.ts.map +0 -1
- package/dist/src/cli/flags.js.map +0 -1
- package/dist/src/cli/grouping.d.ts +0 -18
- package/dist/src/cli/grouping.d.ts.map +0 -1
- package/dist/src/cli/grouping.js.map +0 -1
- package/dist/src/cli/init.d.ts +0 -3
- package/dist/src/cli/init.d.ts.map +0 -1
- package/dist/src/cli/init.js.map +0 -1
- package/dist/src/cli/investigate.d.ts +0 -115
- package/dist/src/cli/investigate.d.ts.map +0 -1
- package/dist/src/cli/investigate.js.map +0 -1
- package/dist/src/cli/listen.d.ts +0 -56
- package/dist/src/cli/listen.d.ts.map +0 -1
- package/dist/src/cli/listen.js.map +0 -1
- package/dist/src/cli/report.d.ts +0 -24
- package/dist/src/cli/report.d.ts.map +0 -1
- package/dist/src/cli/report.js.map +0 -1
- package/dist/src/cli/serve.d.ts +0 -3
- package/dist/src/cli/serve.d.ts.map +0 -1
- package/dist/src/cli/serve.js.map +0 -1
- package/dist/src/cli/status.d.ts +0 -19
- package/dist/src/cli/status.d.ts.map +0 -1
- package/dist/src/cli/status.js.map +0 -1
- package/dist/src/config/loader.d.ts +0 -22
- package/dist/src/config/loader.d.ts.map +0 -1
- package/dist/src/config/loader.js +0 -193
- package/dist/src/config/loader.js.map +0 -1
- package/dist/src/config/schema.d.ts +0 -463
- package/dist/src/config/schema.d.ts.map +0 -1
- package/dist/src/config/schema.js +0 -217
- package/dist/src/config/schema.js.map +0 -1
- package/dist/src/core/caps.d.ts +0 -36
- package/dist/src/core/caps.d.ts.map +0 -1
- package/dist/src/core/caps.js +0 -36
- package/dist/src/core/caps.js.map +0 -1
- package/dist/src/core/db.d.ts +0 -3
- package/dist/src/core/db.d.ts.map +0 -1
- package/dist/src/core/db.js +0 -105
- package/dist/src/core/db.js.map +0 -1
- package/dist/src/core/detect-repo.d.ts +0 -4
- package/dist/src/core/detect-repo.d.ts.map +0 -1
- package/dist/src/core/detect-repo.js +0 -28
- package/dist/src/core/detect-repo.js.map +0 -1
- package/dist/src/core/file-session-store.d.ts +0 -10
- package/dist/src/core/file-session-store.d.ts.map +0 -1
- package/dist/src/core/file-session-store.js +0 -34
- package/dist/src/core/file-session-store.js.map +0 -1
- package/dist/src/core/parse-stdin.d.ts +0 -2
- package/dist/src/core/parse-stdin.d.ts.map +0 -1
- package/dist/src/core/parse-stdin.js +0 -21
- package/dist/src/core/parse-stdin.js.map +0 -1
- package/dist/src/core/run-investigation.d.ts +0 -54
- package/dist/src/core/run-investigation.d.ts.map +0 -1
- package/dist/src/core/run-investigation.js +0 -200
- package/dist/src/core/run-investigation.js.map +0 -1
- package/dist/src/core/session.d.ts +0 -63
- package/dist/src/core/session.d.ts.map +0 -1
- package/dist/src/core/session.js +0 -45
- package/dist/src/core/session.js.map +0 -1
- package/dist/src/core/slack-notify.d.ts +0 -9
- package/dist/src/core/slack-notify.d.ts.map +0 -1
- package/dist/src/core/slack-notify.js +0 -57
- package/dist/src/core/slack-notify.js.map +0 -1
- package/dist/src/core/sqlite-session-store.d.ts +0 -39
- package/dist/src/core/sqlite-session-store.d.ts.map +0 -1
- package/dist/src/core/sqlite-session-store.js +0 -239
- package/dist/src/core/sqlite-session-store.js.map +0 -1
- package/dist/src/http/server.d.ts +0 -31
- package/dist/src/http/server.d.ts.map +0 -1
- package/dist/src/http/server.js +0 -164
- package/dist/src/http/server.js.map +0 -1
- package/dist/src/jsonrpc/server.d.ts +0 -15
- package/dist/src/jsonrpc/server.d.ts.map +0 -1
- package/dist/src/jsonrpc/server.js +0 -244
- package/dist/src/jsonrpc/server.js.map +0 -1
- package/dist/src/jsonrpc/types.d.ts +0 -12
- package/dist/src/jsonrpc/types.d.ts.map +0 -1
- package/dist/src/jsonrpc/types.js +0 -4
- package/dist/src/jsonrpc/types.js.map +0 -1
- package/dist/src/version.d.ts +0 -8
- package/dist/src/version.d.ts.map +0 -1
- package/dist/src/version.js +0 -27
- package/dist/src/version.js.map +0 -1
package/dist/src/cli/listen.js
CHANGED
|
@@ -1,305 +1,569 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import {
|
|
2
|
+
createGroupingLayer,
|
|
3
|
+
deriveDedupeKey,
|
|
4
|
+
deriveGroupKey
|
|
5
|
+
} from "../../chunk-SSEIMBQ2.js";
|
|
6
|
+
import {
|
|
7
|
+
createFileSessionStore,
|
|
8
|
+
liveTimelineEntry,
|
|
9
|
+
resolveRepoSlug,
|
|
10
|
+
resolveRunSandbox
|
|
11
|
+
} from "../../chunk-PVQVFRIV.js";
|
|
12
|
+
import {
|
|
13
|
+
loadConfig
|
|
14
|
+
} from "../../chunk-V6M6ISKI.js";
|
|
15
|
+
import {
|
|
16
|
+
createSessionManager
|
|
17
|
+
} from "../../chunk-CTKCK7QT.js";
|
|
18
|
+
import {
|
|
19
|
+
isSynthConfigured,
|
|
20
|
+
pickServiceLabel,
|
|
21
|
+
resolveInvestigation
|
|
22
|
+
} from "../../chunk-QDK5PAI3.js";
|
|
23
|
+
import "../../chunk-CVSS5JPC.js";
|
|
24
|
+
import "../../chunk-TGP6QTFO.js";
|
|
25
|
+
import {
|
|
26
|
+
assertKnownFlags
|
|
27
|
+
} from "../../chunk-PTBCAI34.js";
|
|
28
|
+
import {
|
|
29
|
+
SeveritySchema,
|
|
30
|
+
conductRun
|
|
31
|
+
} from "../../chunk-5BT3XQOG.js";
|
|
32
|
+
import "../../chunk-6N4PHBII.js";
|
|
33
|
+
import "../../chunk-XH6DIKTA.js";
|
|
34
|
+
import "../../chunk-2Z7QZSIS.js";
|
|
35
|
+
|
|
36
|
+
// src/cli/listen.ts
|
|
12
37
|
import { existsSync } from "node:fs";
|
|
13
38
|
import { resolve } from "node:path";
|
|
14
|
-
|
|
39
|
+
|
|
40
|
+
// ../@prismalens/contracts/dist/schemas/webhook.js
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
var PrometheusAlertSchema = z.object({
|
|
43
|
+
status: z.enum(["firing", "resolved"]),
|
|
44
|
+
labels: z.record(z.string()),
|
|
45
|
+
annotations: z.record(z.string()).optional(),
|
|
46
|
+
startsAt: z.string(),
|
|
47
|
+
endsAt: z.string().optional(),
|
|
48
|
+
generatorURL: z.string().optional(),
|
|
49
|
+
fingerprint: z.string().optional()
|
|
50
|
+
});
|
|
51
|
+
var PrometheusWebhookSchema = z.object({
|
|
52
|
+
version: z.string().optional(),
|
|
53
|
+
groupKey: z.string().optional(),
|
|
54
|
+
truncatedAlerts: z.number().optional(),
|
|
55
|
+
status: z.enum(["firing", "resolved"]),
|
|
56
|
+
receiver: z.string().optional(),
|
|
57
|
+
groupLabels: z.record(z.string()).optional(),
|
|
58
|
+
commonLabels: z.record(z.string()).optional(),
|
|
59
|
+
commonAnnotations: z.record(z.string()).optional(),
|
|
60
|
+
externalURL: z.string().optional(),
|
|
61
|
+
alerts: z.array(PrometheusAlertSchema)
|
|
62
|
+
});
|
|
63
|
+
var PrometheusWebhookResponseSchema = z.object({
|
|
64
|
+
received: z.number().int(),
|
|
65
|
+
processed: z.number().int(),
|
|
66
|
+
alertIds: z.array(z.string().uuid())
|
|
67
|
+
});
|
|
68
|
+
var GenericWebhookSchema = z.object({
|
|
69
|
+
title: z.string().min(1),
|
|
70
|
+
description: z.string().optional(),
|
|
71
|
+
severity: SeveritySchema.optional(),
|
|
72
|
+
source: z.string().optional(),
|
|
73
|
+
sourceAlertId: z.string().optional(),
|
|
74
|
+
sourceUrl: z.string().optional(),
|
|
75
|
+
serviceId: z.string().uuid().optional(),
|
|
76
|
+
serviceName: z.string().optional(),
|
|
77
|
+
// Will be matched to service by name
|
|
78
|
+
tags: z.array(z.string()).optional(),
|
|
79
|
+
labels: z.record(z.string()).optional(),
|
|
80
|
+
rawPayload: z.record(z.unknown()).optional()
|
|
81
|
+
});
|
|
82
|
+
var GenericWebhookResponseSchema = z.object({
|
|
83
|
+
alertId: z.string().uuid(),
|
|
84
|
+
incidentId: z.string().uuid().optional(),
|
|
85
|
+
isNewIncident: z.boolean()
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/cli/listen.ts
|
|
15
89
|
import { defineCommand } from "citty";
|
|
16
90
|
import { consola } from "consola";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* manual `pl investigate` run from that directory (single-player default).
|
|
45
|
-
*/
|
|
46
|
-
export function resolveRepoPath(alert, config) {
|
|
47
|
-
const name = pickServiceLabel(alert);
|
|
48
|
-
if (!name) {
|
|
49
|
-
throw new Error(`Listen dispatch refused: alert is missing a service label (cannot resolve an investigation workspace).`);
|
|
50
|
-
}
|
|
51
|
-
const repoRef = config.services[name]?.repo;
|
|
52
|
-
if (!repoRef) {
|
|
53
|
-
throw new Error(`Listen dispatch refused: service "${name}" has no mapped repo in config (cannot resolve an investigation workspace).`);
|
|
91
|
+
|
|
92
|
+
// src/core/caps.ts
|
|
93
|
+
var HOUR_MS = 36e5;
|
|
94
|
+
function createCapsGate() {
|
|
95
|
+
let active = 0;
|
|
96
|
+
const startsMs = [];
|
|
97
|
+
return {
|
|
98
|
+
get activeCount() {
|
|
99
|
+
return active;
|
|
100
|
+
},
|
|
101
|
+
tryDispatch(caps, nowMs) {
|
|
102
|
+
if (caps.maxConcurrent !== void 0 && active >= caps.maxConcurrent) {
|
|
103
|
+
return { allow: false, reason: "concurrency" };
|
|
104
|
+
}
|
|
105
|
+
const cutoff = nowMs - HOUR_MS;
|
|
106
|
+
while (startsMs.length > 0 && startsMs[0] <= cutoff) {
|
|
107
|
+
startsMs.shift();
|
|
108
|
+
}
|
|
109
|
+
if (caps.maxPerHour !== void 0 && startsMs.length >= caps.maxPerHour) {
|
|
110
|
+
return { allow: false, reason: "per_hour" };
|
|
111
|
+
}
|
|
112
|
+
active++;
|
|
113
|
+
startsMs.push(nowMs);
|
|
114
|
+
return { allow: true };
|
|
115
|
+
},
|
|
116
|
+
release() {
|
|
117
|
+
if (active > 0) active--;
|
|
54
118
|
}
|
|
55
|
-
|
|
56
|
-
if (localPath)
|
|
57
|
-
return resolve(localPath);
|
|
58
|
-
if (existsSync(repoRef))
|
|
59
|
-
return resolve(repoRef);
|
|
60
|
-
throw new Error(`Listen dispatch refused: repo "${repoRef}" (mapped from service "${name}") does not exist locally (cannot resolve an investigation workspace).`);
|
|
119
|
+
};
|
|
61
120
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
const alertname = (alerts[0]?.labels?.alertname ?? undefined);
|
|
78
|
-
// AC5: the checkout under investigation follows the ALERT (service label →
|
|
79
|
-
// catalog), not the directory the server happened to start in.
|
|
80
|
-
// MUST be resolved before consuming caps so unmapped alerts do not exhaust budget.
|
|
81
|
-
let repoPath;
|
|
82
|
-
try {
|
|
83
|
-
repoPath = resolveRepoPath(alerts[0], config);
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
87
|
-
console.error(JSON.stringify({
|
|
88
|
-
event: "dispatch_refused",
|
|
89
|
-
runId,
|
|
90
|
-
reason,
|
|
91
|
-
...(alertname ? { alertname } : {}),
|
|
92
|
-
}));
|
|
93
|
-
options.log(`Refused run ${runId} (${reason})`);
|
|
94
|
-
const refusedSessions = createSessionManager(config.workspace.base_dir);
|
|
95
|
-
try {
|
|
96
|
-
await refusedSessions.recordSuppressed({
|
|
97
|
-
runId,
|
|
98
|
-
reason,
|
|
99
|
-
...(alertname ? { alertname } : {}),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
refusedSessions.close?.();
|
|
104
|
-
}
|
|
105
|
-
return; // refused, never consume caps
|
|
106
|
-
}
|
|
107
|
-
const dispatchCaps = {
|
|
108
|
-
maxConcurrent: config.listen.caps.max_concurrent,
|
|
109
|
-
maxPerHour: config.listen.caps.max_per_hour,
|
|
110
|
-
};
|
|
111
|
-
const decision = caps.tryDispatch(dispatchCaps, Date.now());
|
|
112
|
-
if (!decision.allow) {
|
|
113
|
-
// Structured suppression log — match the repo convention
|
|
114
|
-
// (slack-notify.ts: console.error(JSON.stringify({ event, ... }))).
|
|
115
|
-
console.error(JSON.stringify({
|
|
116
|
-
event: "dispatch_suppressed",
|
|
117
|
-
runId,
|
|
118
|
-
reason: decision.reason,
|
|
119
|
-
...(alertname ? { alertname } : {}),
|
|
120
|
-
maxConcurrent: dispatchCaps.maxConcurrent,
|
|
121
|
-
maxPerHour: dispatchCaps.maxPerHour,
|
|
122
|
-
}));
|
|
123
|
-
options.log(`Suppressed run ${runId} (${decision.reason} cap)`);
|
|
124
|
-
const suppressedSessions = createSessionManager(config.workspace.base_dir);
|
|
125
|
-
try {
|
|
126
|
-
await suppressedSessions.recordSuppressed({
|
|
127
|
-
runId,
|
|
128
|
-
reason: decision.reason,
|
|
129
|
-
...(alertname ? { alertname } : {}),
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
suppressedSessions.close?.();
|
|
134
|
-
}
|
|
135
|
-
return; // never arm a sandbox / call conductRun for a suppressed run
|
|
136
|
-
}
|
|
137
|
-
const harnessName = config.agent.default;
|
|
138
|
-
const { selection } = await resolveRunSandbox(harnessName, config.agent.sandbox, config);
|
|
139
|
-
let sessions;
|
|
140
|
-
try {
|
|
141
|
-
const resolved = resolveInvestigation({
|
|
142
|
-
alerts,
|
|
143
|
-
repo: repoPath,
|
|
144
|
-
...(selection
|
|
145
|
-
? {
|
|
146
|
-
sandbox: selection.sandbox,
|
|
147
|
-
requestedSandbox: selection.requested,
|
|
148
|
-
}
|
|
149
|
-
: {}),
|
|
150
|
-
...(config.listen.caps.max_turns !== undefined
|
|
151
|
-
? { maxTurns: config.listen.caps.max_turns }
|
|
152
|
-
: {}),
|
|
153
|
-
isolateSettings: true,
|
|
154
|
-
}, config);
|
|
155
|
-
const { context, harness, synth, fidelity, maxBranches } = resolved;
|
|
156
|
-
const primaryAlert = context.alerts[0];
|
|
157
|
-
const repoSlug = await resolveRepoSlug(config.repo, repoPath);
|
|
158
|
-
sessions = createSessionManager(config.workspace.base_dir);
|
|
159
|
-
const fileSession = createFileSessionStore(sessions, {
|
|
160
|
-
runId,
|
|
161
|
-
alertname: primaryAlert.alertname,
|
|
162
|
-
agent: resolved.harnessName,
|
|
163
|
-
...(repoSlug ? { repo: repoSlug } : {}),
|
|
164
|
-
});
|
|
165
|
-
options.log(`Investigating "${primaryAlert.alertname}" with ${resolved.harnessName} in ${repoPath} (run ${runId})`);
|
|
166
|
-
const outcome = await conductRun({
|
|
167
|
-
context,
|
|
168
|
-
harness,
|
|
169
|
-
synth,
|
|
170
|
-
fidelity,
|
|
171
|
-
runId,
|
|
172
|
-
...(maxBranches !== undefined ? { maxBranches } : {}),
|
|
173
|
-
}, {
|
|
174
|
-
sink: (event) => {
|
|
175
|
-
const entry = liveTimelineEntry(event);
|
|
176
|
-
if (entry)
|
|
177
|
-
options.log(entry);
|
|
178
|
-
},
|
|
179
|
-
store: fileSession.store,
|
|
180
|
-
});
|
|
181
|
-
if (config.listen.slack_webhook_url) {
|
|
182
|
-
try {
|
|
183
|
-
await notify(config.listen.slack_webhook_url, primaryAlert.alertname, outcome);
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
// Fallback for an injected notify that violates the contract.
|
|
187
|
-
// The real notifyRun never rejects.
|
|
188
|
-
options.log(`Slack notification failed with error: ${err}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (!outcome.report) {
|
|
192
|
-
throw new Error(outcome.error ?? "investigation produced no evidence");
|
|
193
|
-
}
|
|
194
|
-
options.log(`Report saved for run ${runId}`);
|
|
195
|
-
}
|
|
196
|
-
finally {
|
|
197
|
-
caps.release();
|
|
198
|
-
// Close this run's sqlite connection — the runner opens one PER alert,
|
|
199
|
-
// so leaving it open would leak a handle on every webhook.
|
|
200
|
-
sessions?.close?.();
|
|
201
|
-
// The listen path owns the boundary's lifecycle (ADR-0020) — torn down
|
|
202
|
-
// whichever way the run exited (report, no-evidence, error, or throw).
|
|
203
|
-
if (selection)
|
|
204
|
-
await selection.sandbox.destroy();
|
|
205
|
-
}
|
|
206
|
-
};
|
|
121
|
+
|
|
122
|
+
// src/http/server.ts
|
|
123
|
+
import { timingSafeEqual } from "node:crypto";
|
|
124
|
+
import {
|
|
125
|
+
createServer
|
|
126
|
+
} from "node:http";
|
|
127
|
+
var DEFAULT_MAX_PENDING = 8;
|
|
128
|
+
var WEBHOOK_PATH = "/webhooks/alertmanager";
|
|
129
|
+
function isAuthorized(req, token) {
|
|
130
|
+
const header = req.headers.authorization;
|
|
131
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
132
|
+
const presented = Buffer.from(header.slice("Bearer ".length));
|
|
133
|
+
const expected = Buffer.from(token);
|
|
134
|
+
return presented.length === expected.length && timingSafeEqual(presented, expected);
|
|
207
135
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
136
|
+
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
137
|
+
async function readBody(req) {
|
|
138
|
+
const chunks = [];
|
|
139
|
+
let size = 0;
|
|
140
|
+
for await (const chunk of req) {
|
|
141
|
+
size += chunk.length;
|
|
142
|
+
if (size > MAX_BODY_BYTES) return null;
|
|
143
|
+
chunks.push(chunk);
|
|
144
|
+
}
|
|
145
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
146
|
+
}
|
|
147
|
+
async function startListenServer(options) {
|
|
148
|
+
const maxPending = options.maxPending ?? DEFAULT_MAX_PENDING;
|
|
149
|
+
const reply = (res, status, body) => {
|
|
150
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
151
|
+
res.end(JSON.stringify(body));
|
|
152
|
+
};
|
|
153
|
+
const handle = async (req, res) => {
|
|
154
|
+
const path = (req.url ?? "").split("?", 1)[0];
|
|
155
|
+
if (path !== WEBHOOK_PATH) {
|
|
156
|
+
reply(res, 404, { error: "not found" });
|
|
157
|
+
return;
|
|
222
158
|
}
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
159
|
+
if (req.method !== "POST") {
|
|
160
|
+
res.setHeader("allow", "POST");
|
|
161
|
+
reply(res, 405, { error: "method not allowed" });
|
|
162
|
+
return;
|
|
226
163
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const staleRuns = await sessions.list({ status: ["running"] });
|
|
231
|
-
// Liveness guard: only reap runs whose updatedAt is older than the max possible
|
|
232
|
-
// run duration (wall-clock cap if configured, else 1 hour) plus 5 minutes slack.
|
|
233
|
-
const runWallClockMs = config.agent.limits.wall_clock_ms || 60 * 60 * 1000;
|
|
234
|
-
const staleThresholdMs = runWallClockMs + 5 * 60 * 1000;
|
|
235
|
-
const now = Date.now();
|
|
236
|
-
for (const run of staleRuns) {
|
|
237
|
-
const updatedTime = new Date(run.updatedAt).getTime();
|
|
238
|
-
if (now - updatedTime > staleThresholdMs) {
|
|
239
|
-
await sessions.update(run.runId, {
|
|
240
|
-
status: "errored",
|
|
241
|
-
error: "listener process died mid-investigation (interrupted)",
|
|
242
|
-
});
|
|
243
|
-
options.log(`Reaped stale run ${run.runId} from a previous listener`);
|
|
244
|
-
}
|
|
164
|
+
if (!isAuthorized(req, options.token)) {
|
|
165
|
+
reply(res, 401, { error: "unauthorized" });
|
|
166
|
+
return;
|
|
245
167
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
168
|
+
if (options.grouping.isShuttingDown()) {
|
|
169
|
+
reply(res, 503, { error: "server is shutting down" });
|
|
170
|
+
req.destroy();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const raw = await readBody(req);
|
|
174
|
+
if (raw === null) {
|
|
175
|
+
reply(res, 413, { error: "request body too large" });
|
|
176
|
+
req.destroy();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let payloadObj;
|
|
180
|
+
try {
|
|
181
|
+
payloadObj = JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
reply(res, 400, { error: "request body is not valid JSON" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const parsed = PrometheusWebhookSchema.safeParse(payloadObj);
|
|
187
|
+
if (!parsed.success) {
|
|
188
|
+
const reason = parsed.error.issues.map((i) => `${i.path.join(".") || "payload"}: ${i.message}`).join("; ");
|
|
189
|
+
reply(res, 400, { error: `invalid Alertmanager payload \u2014 ${reason}` });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const firing = parsed.data.alerts.filter(
|
|
193
|
+
(a) => a.status === "firing"
|
|
194
|
+
);
|
|
195
|
+
const payload = payloadObj;
|
|
196
|
+
const pendingGroups = options.grouping.pendingGroups();
|
|
197
|
+
const newGroupCount = options.grouping.newGroupCount(firing, payload);
|
|
198
|
+
if (newGroupCount > 0 && pendingGroups > 0 && pendingGroups + newGroupCount > maxPending) {
|
|
199
|
+
reply(res, 503, {
|
|
200
|
+
error: `investigation queue full (${pendingGroups} pending, max ${maxPending}) \u2014 retry later`
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
reply(res, 202, {
|
|
205
|
+
received: parsed.data.alerts.length,
|
|
206
|
+
accepted: firing.length
|
|
251
207
|
});
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
208
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
209
|
+
const fingerprint = firing.length > 0 ? deriveDedupeKey(firing[0]) : "none";
|
|
210
|
+
const groupKey = firing.length > 0 ? deriveGroupKey(firing[0], payload) : "none";
|
|
211
|
+
console.error(
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
event: "intake_accepted",
|
|
214
|
+
timestamp,
|
|
215
|
+
fingerprint,
|
|
216
|
+
groupKey,
|
|
217
|
+
alertCount: parsed.data.alerts.length,
|
|
218
|
+
acceptedCount: firing.length
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
options.grouping.admit(firing, payload);
|
|
222
|
+
};
|
|
223
|
+
const server = createServer((req, res) => {
|
|
224
|
+
handle(req, res).catch(() => {
|
|
225
|
+
if (!res.headersSent) reply(res, 500, { error: "internal error" });
|
|
226
|
+
else res.end();
|
|
259
227
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
228
|
+
});
|
|
229
|
+
await new Promise((resolve2, reject) => {
|
|
230
|
+
server.once("error", reject);
|
|
231
|
+
server.listen(options.port, options.host ?? "127.0.0.1", () => resolve2());
|
|
232
|
+
});
|
|
233
|
+
const address = server.address();
|
|
234
|
+
if (address === null || typeof address === "string") {
|
|
235
|
+
throw new Error("listen server bound to a non-TCP address");
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
port: address.port,
|
|
239
|
+
host: address.address,
|
|
240
|
+
close: () => {
|
|
241
|
+
options.grouping.shutdown();
|
|
242
|
+
return new Promise((resolve2, reject) => {
|
|
243
|
+
server.close((err) => err ? reject(err) : resolve2());
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
};
|
|
269
247
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
248
|
+
|
|
249
|
+
// src/core/slack-notify.ts
|
|
250
|
+
function buildSlackMessage(input) {
|
|
251
|
+
const { alertname, runId, outcome } = input;
|
|
252
|
+
if (outcome.failureKind === "cancelled") {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
if (outcome.failureKind === "error") {
|
|
256
|
+
return `${alertname}
|
|
257
|
+
Investigation errored: ${outcome.error ?? "unknown error"}
|
|
258
|
+
Run ${runId} \u2014 pl report ${runId}`;
|
|
259
|
+
}
|
|
260
|
+
if (outcome.failureKind === "no-evidence") {
|
|
261
|
+
return `${alertname}
|
|
262
|
+
Investigated \u2014 insufficient evidence to determine a cause.
|
|
263
|
+
Run ${runId} \u2014 pl report ${runId}`;
|
|
264
|
+
}
|
|
265
|
+
const report = outcome.report;
|
|
266
|
+
if (!report) {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
let message = `${alertname}
|
|
270
|
+
${report.summary}
|
|
271
|
+
Root cause: ${report.rootCause ?? "not determined"}`;
|
|
272
|
+
if (report.hypotheses.length > 0) {
|
|
273
|
+
const topHypothesis = report.hypotheses[0];
|
|
274
|
+
message += `
|
|
275
|
+
Top hypothesis: ${topHypothesis.statement} (${topHypothesis.evidence.length} evidence)`;
|
|
276
|
+
}
|
|
277
|
+
message += `
|
|
278
|
+
Run ${runId} \u2014 pl report ${runId}`;
|
|
279
|
+
return message;
|
|
280
|
+
}
|
|
281
|
+
async function postToSlack(webhookUrl, text) {
|
|
282
|
+
const res = await fetch(webhookUrl, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: { "Content-Type": "application/json" },
|
|
285
|
+
body: JSON.stringify({ text }),
|
|
286
|
+
signal: AbortSignal.timeout(5e3)
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
throw new Error(`Slack webhook returned ${res.status}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function notifyRun(webhookUrl, alertname, outcome) {
|
|
293
|
+
try {
|
|
294
|
+
const text = buildSlackMessage({
|
|
295
|
+
alertname,
|
|
296
|
+
runId: outcome.runId,
|
|
297
|
+
outcome
|
|
298
|
+
});
|
|
299
|
+
if (!text) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
await postToSlack(webhookUrl, text);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error(
|
|
305
|
+
JSON.stringify({
|
|
306
|
+
event: "slack_delivery_failed",
|
|
307
|
+
runId: outcome.runId,
|
|
308
|
+
error: err instanceof Error ? err.message : String(err)
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/cli/listen.ts
|
|
315
|
+
function resolveRepoPath(alert, config) {
|
|
316
|
+
const name = pickServiceLabel(alert);
|
|
317
|
+
if (!name) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Listen dispatch refused: alert is missing a service label (cannot resolve an investigation workspace).`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const repoRef = config.services[name]?.repo;
|
|
323
|
+
if (!repoRef) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Listen dispatch refused: service "${name}" has no mapped repo in config (cannot resolve an investigation workspace).`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const localPath = config.repos[repoRef]?.local_path;
|
|
329
|
+
if (localPath) return resolve(localPath);
|
|
330
|
+
if (existsSync(repoRef)) return resolve(repoRef);
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Listen dispatch refused: repo "${repoRef}" (mapped from service "${name}") does not exist locally (cannot resolve an investigation workspace).`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
function createInvestigationRunner(options, deps = {}) {
|
|
336
|
+
const conductRun2 = deps.conductRun ?? conductRun;
|
|
337
|
+
const resolveRunSandbox2 = deps.resolveRunSandbox ?? resolveRunSandbox;
|
|
338
|
+
const notify = deps.notify ?? notifyRun;
|
|
339
|
+
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
340
|
+
const caps = deps.capsGate ?? createCapsGate();
|
|
341
|
+
return async (runId, alerts) => {
|
|
342
|
+
const config = await loadConfig({
|
|
343
|
+
cwd,
|
|
344
|
+
...options.configPath ? { configPath: options.configPath } : {}
|
|
345
|
+
});
|
|
346
|
+
const alertname = alerts[0]?.labels?.alertname ?? void 0;
|
|
347
|
+
let repoPath;
|
|
348
|
+
try {
|
|
349
|
+
repoPath = resolveRepoPath(alerts[0], config);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
352
|
+
console.error(
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
event: "dispatch_refused",
|
|
355
|
+
runId,
|
|
356
|
+
reason,
|
|
357
|
+
...alertname ? { alertname } : {}
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
options.log(`Refused run ${runId} (${reason})`);
|
|
361
|
+
const refusedSessions = createSessionManager(config.workspace.dir);
|
|
362
|
+
try {
|
|
363
|
+
await refusedSessions.recordSuppressed({
|
|
364
|
+
runId,
|
|
365
|
+
reason,
|
|
366
|
+
...alertname ? { alertname } : {}
|
|
367
|
+
});
|
|
368
|
+
} finally {
|
|
369
|
+
refusedSessions.close?.();
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const dispatchCaps = {
|
|
374
|
+
maxConcurrent: config.listen.caps.max_concurrent,
|
|
375
|
+
maxPerHour: config.listen.caps.max_per_hour
|
|
376
|
+
};
|
|
377
|
+
const decision = caps.tryDispatch(dispatchCaps, Date.now());
|
|
378
|
+
if (!decision.allow) {
|
|
379
|
+
console.error(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
event: "dispatch_suppressed",
|
|
382
|
+
runId,
|
|
383
|
+
reason: decision.reason,
|
|
384
|
+
...alertname ? { alertname } : {},
|
|
385
|
+
maxConcurrent: dispatchCaps.maxConcurrent,
|
|
386
|
+
maxPerHour: dispatchCaps.maxPerHour
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
options.log(`Suppressed run ${runId} (${decision.reason} cap)`);
|
|
390
|
+
const suppressedSessions = createSessionManager(config.workspace.dir);
|
|
391
|
+
try {
|
|
392
|
+
await suppressedSessions.recordSuppressed({
|
|
393
|
+
runId,
|
|
394
|
+
reason: decision.reason,
|
|
395
|
+
...alertname ? { alertname } : {}
|
|
396
|
+
});
|
|
397
|
+
} finally {
|
|
398
|
+
suppressedSessions.close?.();
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const harnessName = config.agent.default;
|
|
403
|
+
const { selection } = await resolveRunSandbox2(
|
|
404
|
+
harnessName,
|
|
405
|
+
config.agent.sandbox,
|
|
406
|
+
config
|
|
407
|
+
);
|
|
408
|
+
let sessions;
|
|
409
|
+
try {
|
|
410
|
+
const resolved = resolveInvestigation(
|
|
411
|
+
{
|
|
412
|
+
alerts,
|
|
413
|
+
repo: repoPath,
|
|
414
|
+
...selection ? {
|
|
415
|
+
sandbox: selection.sandbox,
|
|
416
|
+
requestedSandbox: selection.requested
|
|
417
|
+
} : {},
|
|
418
|
+
...config.listen.caps.max_turns !== void 0 ? { maxTurns: config.listen.caps.max_turns } : {},
|
|
419
|
+
isolateSettings: true
|
|
279
420
|
},
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
421
|
+
config
|
|
422
|
+
);
|
|
423
|
+
const { context, harness, synth, fidelity, maxBranches } = resolved;
|
|
424
|
+
const primaryAlert = context.alerts[0];
|
|
425
|
+
const repoSlug = await resolveRepoSlug(config.repo, repoPath);
|
|
426
|
+
sessions = createSessionManager(config.workspace.dir);
|
|
427
|
+
const fileSession = createFileSessionStore(sessions, {
|
|
428
|
+
runId,
|
|
429
|
+
alertname: primaryAlert.alertname,
|
|
430
|
+
agent: resolved.harnessName,
|
|
431
|
+
...repoSlug ? { repo: repoSlug } : {}
|
|
432
|
+
});
|
|
433
|
+
options.log(
|
|
434
|
+
`Investigating "${primaryAlert.alertname}" with ${resolved.harnessName} in ${repoPath} (run ${runId})`
|
|
435
|
+
);
|
|
436
|
+
const outcome = await conductRun2(
|
|
437
|
+
{
|
|
438
|
+
context,
|
|
439
|
+
harness,
|
|
440
|
+
synth,
|
|
441
|
+
fidelity,
|
|
442
|
+
runId,
|
|
443
|
+
...maxBranches !== void 0 ? { maxBranches } : {}
|
|
283
444
|
},
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const server = await startListenFromConfig({
|
|
291
|
-
cwd: process.cwd(),
|
|
292
|
-
...(args.config ? { configPath: args.config } : {}),
|
|
293
|
-
...(args.host ? { host: args.host } : {}),
|
|
294
|
-
log: (line) => consola.log(line),
|
|
295
|
-
});
|
|
296
|
-
consola.success(`Listening for Alertmanager webhooks on http://${server.host}:${server.port}${WEBHOOK_PATH}`);
|
|
445
|
+
{
|
|
446
|
+
sink: (event) => {
|
|
447
|
+
const entry = liveTimelineEntry(event);
|
|
448
|
+
if (entry) options.log(entry);
|
|
449
|
+
},
|
|
450
|
+
store: fileSession.store
|
|
297
451
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
452
|
+
);
|
|
453
|
+
if (config.listen.slack_webhook_url) {
|
|
454
|
+
try {
|
|
455
|
+
await notify(
|
|
456
|
+
config.listen.slack_webhook_url,
|
|
457
|
+
primaryAlert.alertname,
|
|
458
|
+
outcome
|
|
459
|
+
);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
options.log(`Slack notification failed with error: ${err}`);
|
|
301
462
|
}
|
|
463
|
+
}
|
|
464
|
+
if (!outcome.report) {
|
|
465
|
+
throw new Error(outcome.error ?? "investigation produced no evidence");
|
|
466
|
+
}
|
|
467
|
+
options.log(`Report saved for run ${runId}`);
|
|
468
|
+
} finally {
|
|
469
|
+
caps.release();
|
|
470
|
+
sessions?.close?.();
|
|
471
|
+
if (selection) await selection.sandbox.destroy();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function startListenFromConfig(options, deps = {}) {
|
|
476
|
+
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
477
|
+
const config = await loadConfig({
|
|
478
|
+
cwd,
|
|
479
|
+
...options.configPath ? { configPath: options.configPath } : {}
|
|
480
|
+
});
|
|
481
|
+
if (!isSynthConfigured(config)) {
|
|
482
|
+
options.log(
|
|
483
|
+
"No Tier-1 provider configured (checked env + _FILE for: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY, OLLAMA_API_KEY) \u2014 reports will be RAW harness pass-through (un-synthesized). This is supported."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (!config.listen.token) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
"config is missing listen.token (required \u2014 intake auth). Add listen.token to prismalens.config.yaml."
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const sessions = createSessionManager(config.workspace.dir);
|
|
492
|
+
const staleRuns = await sessions.list({ status: ["running"] });
|
|
493
|
+
const runWallClockMs = config.agent.limits.wall_clock_ms || 60 * 60 * 1e3;
|
|
494
|
+
const staleThresholdMs = runWallClockMs + 5 * 60 * 1e3;
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
for (const run of staleRuns) {
|
|
497
|
+
const updatedTime = new Date(run.updatedAt).getTime();
|
|
498
|
+
if (now - updatedTime > staleThresholdMs) {
|
|
499
|
+
await sessions.update(run.runId, {
|
|
500
|
+
status: "errored",
|
|
501
|
+
error: "listener process died mid-investigation (interrupted)"
|
|
502
|
+
});
|
|
503
|
+
options.log(`Reaped stale run ${run.runId} from a previous listener`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const grouping = createGroupingLayer({
|
|
507
|
+
windowMs: config.listen.grouping_window_ms,
|
|
508
|
+
sessions,
|
|
509
|
+
runInvestigation: createInvestigationRunner(options, deps),
|
|
510
|
+
log: options.log
|
|
511
|
+
});
|
|
512
|
+
const server = await startListenServer({
|
|
513
|
+
host: options.host ?? config.listen.host,
|
|
514
|
+
port: config.listen.port,
|
|
515
|
+
token: config.listen.token,
|
|
516
|
+
maxPending: config.listen.max_pending,
|
|
517
|
+
grouping,
|
|
518
|
+
log: options.log
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
...server,
|
|
522
|
+
async close() {
|
|
523
|
+
await server.close();
|
|
524
|
+
sessions.close?.();
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
var listen_default = defineCommand({
|
|
529
|
+
meta: {
|
|
530
|
+
name: "listen",
|
|
531
|
+
description: "Start a token-authed local HTTP listener for Alertmanager webhooks; each firing alert triggers an investigation (Phase 1 R1).\n\nPort and token are configured in the `listen:` block of prismalens.config.yaml (or via PRISMALENS_LISTEN_TOKEN env var).\n\nExamples:\n $ pl listen\n $ pl listen --config my-stack.yaml"
|
|
532
|
+
},
|
|
533
|
+
args: {
|
|
534
|
+
config: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "Path to prismalens.config.yaml (else global\u2192project hierarchy)."
|
|
302
537
|
},
|
|
303
|
-
|
|
538
|
+
host: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: "Bind host (default 127.0.0.1)."
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
/* v8 ignore start — process-global glue (cwd + consola binding); the body it
|
|
544
|
+
delegates to is fully covered via startListenFromConfig tests */
|
|
545
|
+
async run({ args, cmd }) {
|
|
546
|
+
try {
|
|
547
|
+
assertKnownFlags(args, cmd);
|
|
548
|
+
const server = await startListenFromConfig({
|
|
549
|
+
cwd: process.cwd(),
|
|
550
|
+
...args.config ? { configPath: args.config } : {},
|
|
551
|
+
...args.host ? { host: args.host } : {},
|
|
552
|
+
log: (line) => consola.log(line)
|
|
553
|
+
});
|
|
554
|
+
consola.success(
|
|
555
|
+
`Listening for Alertmanager webhooks on http://${server.host}:${server.port}${WEBHOOK_PATH}`
|
|
556
|
+
);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
consola.error(err instanceof Error ? err.message : String(err));
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/* v8 ignore stop */
|
|
304
563
|
});
|
|
305
|
-
|
|
564
|
+
export {
|
|
565
|
+
createInvestigationRunner,
|
|
566
|
+
listen_default as default,
|
|
567
|
+
resolveRepoPath,
|
|
568
|
+
startListenFromConfig
|
|
569
|
+
};
|