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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pickServiceLabel
|
|
3
|
+
} from "./chunk-QDK5PAI3.js";
|
|
4
|
+
|
|
5
|
+
// src/cli/grouping.ts
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
function deriveGroupKey(alert, payload) {
|
|
8
|
+
if (typeof payload.groupKey === "string" && payload.groupKey) {
|
|
9
|
+
return payload.groupKey;
|
|
10
|
+
}
|
|
11
|
+
if (payload.groupLabels && typeof payload.groupLabels === "object") {
|
|
12
|
+
const labels = payload.groupLabels;
|
|
13
|
+
const keys = Object.keys(labels);
|
|
14
|
+
if (keys.length > 0) {
|
|
15
|
+
const sorted = keys.sort().map((k) => `${k}=${labels[k]}`).join(",");
|
|
16
|
+
return createHash("sha256").update(sorted).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const serviceLabel = pickServiceLabel(alert);
|
|
20
|
+
const alertname = alert.labels?.alertname || "";
|
|
21
|
+
if (serviceLabel) {
|
|
22
|
+
return `${alertname}\0${serviceLabel}`;
|
|
23
|
+
}
|
|
24
|
+
const alertLabels = alert.labels;
|
|
25
|
+
if (alertLabels && Object.keys(alertLabels).length > 0) {
|
|
26
|
+
const sorted = Object.keys(alertLabels).sort().map((k) => `${k}=${alertLabels[k]}`).join(",");
|
|
27
|
+
return createHash("sha256").update(sorted).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
return "default";
|
|
30
|
+
}
|
|
31
|
+
function deriveDedupeKey(alert) {
|
|
32
|
+
if (typeof alert.fingerprint === "string" && alert.fingerprint) {
|
|
33
|
+
return alert.fingerprint;
|
|
34
|
+
}
|
|
35
|
+
const alertLabels = alert.labels;
|
|
36
|
+
const alertname = alertLabels?.alertname || "";
|
|
37
|
+
const sorted = alertLabels ? Object.keys(alertLabels).sort().map((k) => `${k}=${alertLabels[k]}`).join(",") : "";
|
|
38
|
+
const hash = createHash("sha256").update(sorted).digest("hex");
|
|
39
|
+
const startsAt = alert.startsAt || "";
|
|
40
|
+
return `${alertname}${hash}${startsAt}`;
|
|
41
|
+
}
|
|
42
|
+
function createGroupingLayer(options) {
|
|
43
|
+
let shuttingDown = false;
|
|
44
|
+
const windows = /* @__PURE__ */ new Map();
|
|
45
|
+
const running = /* @__PURE__ */ new Map();
|
|
46
|
+
const activeAlerts = /* @__PURE__ */ new Map();
|
|
47
|
+
return {
|
|
48
|
+
isShuttingDown() {
|
|
49
|
+
return shuttingDown;
|
|
50
|
+
},
|
|
51
|
+
newGroupCount(firing, payload) {
|
|
52
|
+
const keys = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const alert of firing) {
|
|
54
|
+
const key = deriveGroupKey(alert, payload);
|
|
55
|
+
if (!windows.has(key) && !running.has(key)) {
|
|
56
|
+
keys.add(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return keys.size;
|
|
60
|
+
},
|
|
61
|
+
pendingGroups() {
|
|
62
|
+
return windows.size + running.size;
|
|
63
|
+
},
|
|
64
|
+
admit(firing, payload) {
|
|
65
|
+
if (shuttingDown) {
|
|
66
|
+
options.log("Dropped alert: grouping layer is shutting down");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const alert of firing) {
|
|
70
|
+
const groupKey = deriveGroupKey(alert, payload);
|
|
71
|
+
const dedupeKey = deriveDedupeKey(alert);
|
|
72
|
+
if (activeAlerts.has(dedupeKey)) {
|
|
73
|
+
const active = activeAlerts.get(dedupeKey);
|
|
74
|
+
if (active && active.phase === "running" && active.runId) {
|
|
75
|
+
const state2 = running.get(active.groupKey);
|
|
76
|
+
if (state2) {
|
|
77
|
+
state2.writeQueue = state2.writeQueue.then(
|
|
78
|
+
() => options.sessions.appendGroupAlert(state2.runId, alert)
|
|
79
|
+
).catch((err) => {
|
|
80
|
+
options.log(
|
|
81
|
+
`Failed to attach re-page to group ${active.groupKey} (run ${state2.runId}): ${err}`
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
options.log(
|
|
86
|
+
`Suppression lookup hit but run vanished for dedupeKey ${dedupeKey}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (running.has(groupKey)) {
|
|
93
|
+
const state2 = running.get(groupKey);
|
|
94
|
+
if (state2) {
|
|
95
|
+
activeAlerts.set(dedupeKey, {
|
|
96
|
+
phase: "running",
|
|
97
|
+
groupKey,
|
|
98
|
+
runId: state2.runId
|
|
99
|
+
});
|
|
100
|
+
state2.dedupeKeys.add(dedupeKey);
|
|
101
|
+
state2.writeQueue = state2.writeQueue.then(() => options.sessions.appendGroupAlert(state2.runId, alert)).catch((err) => {
|
|
102
|
+
options.log(
|
|
103
|
+
`Failed to attach late alert to group ${groupKey} (run ${state2.runId}): ${err}`
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (windows.has(groupKey)) {
|
|
110
|
+
const state2 = windows.get(groupKey);
|
|
111
|
+
if (state2) {
|
|
112
|
+
activeAlerts.set(dedupeKey, { phase: "window", groupKey });
|
|
113
|
+
state2.alerts.push(alert);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const state = {
|
|
118
|
+
alerts: [alert],
|
|
119
|
+
timer: setTimeout(() => {
|
|
120
|
+
const alertsToRun = state.alerts;
|
|
121
|
+
windows.delete(groupKey);
|
|
122
|
+
const runId = randomUUID();
|
|
123
|
+
const dedupeKeys = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const a of alertsToRun) {
|
|
125
|
+
const dkey = deriveDedupeKey(a);
|
|
126
|
+
activeAlerts.set(dkey, {
|
|
127
|
+
phase: "running",
|
|
128
|
+
groupKey,
|
|
129
|
+
runId
|
|
130
|
+
});
|
|
131
|
+
dedupeKeys.add(dkey);
|
|
132
|
+
}
|
|
133
|
+
const rec = {
|
|
134
|
+
groupKey,
|
|
135
|
+
formedBy: "window",
|
|
136
|
+
alerts: alertsToRun,
|
|
137
|
+
lateAlerts: []
|
|
138
|
+
};
|
|
139
|
+
const writePromise = Promise.resolve().then(() => options.sessions.writeGroupRecord(runId, rec)).catch((err) => {
|
|
140
|
+
options.log(
|
|
141
|
+
`Failed to write group record for group ${groupKey} (run ${runId}): ${err} \u2014 investigation will proceed without it`
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
running.set(groupKey, {
|
|
145
|
+
runId,
|
|
146
|
+
writeQueue: writePromise,
|
|
147
|
+
dedupeKeys
|
|
148
|
+
});
|
|
149
|
+
writePromise.then(async () => {
|
|
150
|
+
if (shuttingDown) {
|
|
151
|
+
running.delete(groupKey);
|
|
152
|
+
for (const key of dedupeKeys) {
|
|
153
|
+
activeAlerts.delete(key);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await options.runInvestigation(runId, alertsToRun);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
options.log(
|
|
161
|
+
`Investigation for group ${groupKey} failed: ${err}`
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
running.delete(groupKey);
|
|
165
|
+
for (const key of dedupeKeys) {
|
|
166
|
+
activeAlerts.delete(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}).catch((err) => {
|
|
170
|
+
options.log(
|
|
171
|
+
`Investigation for group ${groupKey} failed: ${err}`
|
|
172
|
+
);
|
|
173
|
+
running.delete(groupKey);
|
|
174
|
+
for (const key of dedupeKeys) {
|
|
175
|
+
activeAlerts.delete(key);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}, options.windowMs)
|
|
179
|
+
};
|
|
180
|
+
activeAlerts.set(dedupeKey, { phase: "window", groupKey });
|
|
181
|
+
windows.set(groupKey, state);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
shutdown() {
|
|
185
|
+
shuttingDown = true;
|
|
186
|
+
let dropped = 0;
|
|
187
|
+
for (const state of windows.values()) {
|
|
188
|
+
clearTimeout(state.timer);
|
|
189
|
+
dropped++;
|
|
190
|
+
}
|
|
191
|
+
windows.clear();
|
|
192
|
+
activeAlerts.clear();
|
|
193
|
+
if (dropped > 0) {
|
|
194
|
+
options.log(`Shutdown: dropped ${dropped} pending grouping windows`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
deriveGroupKey,
|
|
202
|
+
deriveDedupeKey,
|
|
203
|
+
createGroupingLayer
|
|
204
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HARNESS_IDS,
|
|
3
|
+
PERMISSION_MODES,
|
|
4
|
+
SANDBOX_MODES
|
|
5
|
+
} from "./chunk-5BT3XQOG.js";
|
|
6
|
+
import {
|
|
7
|
+
llmProviderIdSchema
|
|
8
|
+
} from "./chunk-XH6DIKTA.js";
|
|
9
|
+
|
|
10
|
+
// src/config/schema.ts
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var PermissionConfigSchema = z.object({
|
|
13
|
+
mode: z.enum(PERMISSION_MODES).default("read-only")
|
|
14
|
+
});
|
|
15
|
+
var HarnessNativeConfigSchema = z.object({
|
|
16
|
+
native: z.record(z.string(), z.unknown()).optional()
|
|
17
|
+
});
|
|
18
|
+
var AgentLimitsConfigSchema = z.object({
|
|
19
|
+
/** Wall-clock deadline (ms) — enforced by every sandbox provider. */
|
|
20
|
+
wall_clock_ms: z.number().int().positive().optional(),
|
|
21
|
+
/** Memory ceiling (MB) — best-effort (srt+systemd-run only). */
|
|
22
|
+
memory_mb: z.number().int().positive().optional(),
|
|
23
|
+
/** CPU quota (whole/fractional cores) — best-effort (srt+systemd-run only). */
|
|
24
|
+
cpu_cores: z.number().positive().optional()
|
|
25
|
+
});
|
|
26
|
+
var AgentConfigSchema = z.object({
|
|
27
|
+
default: z.enum(HARNESS_IDS).default("claude-code"),
|
|
28
|
+
/**
|
|
29
|
+
* BARE model id (e.g. "gpt-oss:120b") for the Tier-2 HARNESS ONLY — the chosen
|
|
30
|
+
* harness applies its own provider prefix (ADR-0017: deepagents prepends
|
|
31
|
+
* "openai:"; Claude Code takes it bare). Omit to let the harness pick its own
|
|
32
|
+
* default. The Tier-1 reduce model is a SEPARATE knob — `synth.model` (ADR-0013/
|
|
33
|
+
* 0016): the two calls go to different providers, so one key must never feed both.
|
|
34
|
+
*/
|
|
35
|
+
model: z.string().optional(),
|
|
36
|
+
/** The posture dial (ADR-0017). Defaults to `read-only`. */
|
|
37
|
+
permissions: optionalWithDefaults(PermissionConfigSchema),
|
|
38
|
+
/**
|
|
39
|
+
* The isolation boundary the harness runs in (ADR-0020): `process` (the always-on
|
|
40
|
+
* cooperative floor), `srt` (enforced OS boundary), or `auto` (srt WHEN its egress
|
|
41
|
+
* bridge is healthy — the self-check, B.1.1 — else the floor; the degrade is honest,
|
|
42
|
+
* never silent). Defaults to `auto`: honest precisely BECAUSE of the self-check, which
|
|
43
|
+
* is what makes it safe to prefer the enforced boundary by default.
|
|
44
|
+
*/
|
|
45
|
+
sandbox: z.enum(SANDBOX_MODES).default("auto"),
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort resource caps for the sandboxed harness (ADR-0020). Optional; an
|
|
48
|
+
* omitted section means "no caps" (the object defaults to `{}` after parse).
|
|
49
|
+
*/
|
|
50
|
+
limits: optionalWithDefaults(AgentLimitsConfigSchema),
|
|
51
|
+
/**
|
|
52
|
+
* Per-alert fan-out cap (ADR-0016 decision 2): the max branches the supervisor
|
|
53
|
+
* runs when a context carries multiple alerts. Optional — omitted ⇒ the engine
|
|
54
|
+
* default (3). A single-alert run always uses one branch regardless.
|
|
55
|
+
*/
|
|
56
|
+
max_branches: z.number().int().positive().optional(),
|
|
57
|
+
max_turns: z.number().int().positive().optional()
|
|
58
|
+
});
|
|
59
|
+
var WorkspaceConfigSchema = z.object({
|
|
60
|
+
dir: z.string().default("~/.prismalens")
|
|
61
|
+
});
|
|
62
|
+
var ListenCapsConfigSchema = z.object({
|
|
63
|
+
/** Max investigations running at once. Default 2. */
|
|
64
|
+
max_concurrent: z.number().int().positive().default(2),
|
|
65
|
+
/** Max investigations started per trailing hour. Default 10. */
|
|
66
|
+
max_per_hour: z.number().int().positive().default(10),
|
|
67
|
+
/** Per-run turn ceiling for the default harness. Unset ⇒ the harness default. */
|
|
68
|
+
max_turns: z.number().int().positive().optional()
|
|
69
|
+
});
|
|
70
|
+
var ListenConfigSchema = z.object({
|
|
71
|
+
/** Local intake host. Default 127.0.0.1. */
|
|
72
|
+
host: z.string().default("127.0.0.1"),
|
|
73
|
+
/** Local intake port (4181: clear of 9090/9093/3000/8080 defaults). 0 = ephemeral. */
|
|
74
|
+
port: z.number().int().min(0).max(65535).default(4181),
|
|
75
|
+
/** Shared bearer token Alertmanager presents; unset ⇒ `pl listen` refuses to start. */
|
|
76
|
+
token: z.string().optional(),
|
|
77
|
+
/** Max alerts waiting or running at once; default 8. */
|
|
78
|
+
max_pending: z.number().int().positive().default(8),
|
|
79
|
+
/** Grouping window in milliseconds to debounce alerts into one investigation. */
|
|
80
|
+
grouping_window_ms: z.number().int().min(1e3).max(12e4).default(6e4),
|
|
81
|
+
/**
|
|
82
|
+
* Slack incoming-webhook URL for report delivery (issue #61). Optional — unset ⇒
|
|
83
|
+
* delivery disabled. Set via env interpolation (slack_webhook_url:
|
|
84
|
+
* "${PRISMALENS_SLACK_WEBHOOK_URL}") so the secret stays in env (ADR-0006).
|
|
85
|
+
*/
|
|
86
|
+
slack_webhook_url: z.string().url().optional(),
|
|
87
|
+
/** Run-budget guardrails (issue #62). Always present with defaults after parse. */
|
|
88
|
+
caps: optionalWithDefaults(ListenCapsConfigSchema)
|
|
89
|
+
});
|
|
90
|
+
var TelemetryConfigSchema = z.object({
|
|
91
|
+
prometheus_url: z.string().optional(),
|
|
92
|
+
alertmanager_url: z.string().optional(),
|
|
93
|
+
api_url: z.string().optional()
|
|
94
|
+
});
|
|
95
|
+
var ServiceConfigSchema = z.object({
|
|
96
|
+
tier: z.string().optional(),
|
|
97
|
+
/** owner/name slug or a local path the harness reads. */
|
|
98
|
+
repo: z.string().optional(),
|
|
99
|
+
/** Direct dependency service names (blast-radius seed). */
|
|
100
|
+
depends_on: z.array(z.string()).default([])
|
|
101
|
+
});
|
|
102
|
+
var LogSourceConfigSchema = z.object({
|
|
103
|
+
kind: z.string().optional(),
|
|
104
|
+
url: z.string().optional()
|
|
105
|
+
});
|
|
106
|
+
var AlertSourceConfigSchema = z.record(
|
|
107
|
+
z.string(),
|
|
108
|
+
z.record(z.string(), z.string())
|
|
109
|
+
);
|
|
110
|
+
var RepoConfigSchema = z.object({
|
|
111
|
+
repo: z.string(),
|
|
112
|
+
local_path: z.string().nullable().default(null),
|
|
113
|
+
alert_sources: AlertSourceConfigSchema.optional()
|
|
114
|
+
});
|
|
115
|
+
function optionalWithDefaults(schema) {
|
|
116
|
+
return z.optional(z.record(z.string(), z.unknown())).transform((val, ctx) => {
|
|
117
|
+
const res = schema.safeParse(val ?? {});
|
|
118
|
+
if (!res.success) {
|
|
119
|
+
for (const issue of res.error.issues) {
|
|
120
|
+
ctx.addIssue(issue);
|
|
121
|
+
}
|
|
122
|
+
return z.NEVER;
|
|
123
|
+
}
|
|
124
|
+
return res.data;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
var SynthConfigSchema = z.object({
|
|
128
|
+
provider: llmProviderIdSchema.optional(),
|
|
129
|
+
model: z.string().optional(),
|
|
130
|
+
base_url: z.string().url().optional()
|
|
131
|
+
});
|
|
132
|
+
var PlConfigSchema = z.object({
|
|
133
|
+
$schema: z.string().optional(),
|
|
134
|
+
/** Convenience single-repo default (owner/name); `repos` is the multi-repo map. */
|
|
135
|
+
repo: z.string().optional(),
|
|
136
|
+
repos: z.record(z.string(), RepoConfigSchema).optional().transform((val) => val ?? {}),
|
|
137
|
+
/** Service catalog (ADR-0015) — keyed by service name; maps into the context. */
|
|
138
|
+
services: z.record(z.string(), ServiceConfigSchema).optional().transform((val) => val ?? {}),
|
|
139
|
+
/** Read-only log-query system the harness may query. */
|
|
140
|
+
logs: optionalWithDefaults(LogSourceConfigSchema),
|
|
141
|
+
agent: optionalWithDefaults(AgentConfigSchema),
|
|
142
|
+
synth: optionalWithDefaults(SynthConfigSchema),
|
|
143
|
+
workspace: optionalWithDefaults(WorkspaceConfigSchema),
|
|
144
|
+
listen: optionalWithDefaults(ListenConfigSchema),
|
|
145
|
+
telemetry: optionalWithDefaults(TelemetryConfigSchema),
|
|
146
|
+
alert_sources: AlertSourceConfigSchema.optional().transform(
|
|
147
|
+
(val) => val ?? {}
|
|
148
|
+
),
|
|
149
|
+
/** Per-harness native passthrough (ADR-0017), keyed by harness id. */
|
|
150
|
+
harnesses: z.record(z.string(), HarnessNativeConfigSchema).optional().transform((val) => val ?? {})
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
AgentConfigSchema,
|
|
155
|
+
PlConfigSchema
|
|
156
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlConfigSchema
|
|
3
|
+
} from "./chunk-TGP6QTFO.js";
|
|
4
|
+
|
|
5
|
+
// src/config/loader.ts
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join, parse as parsePath, resolve } from "node:path";
|
|
9
|
+
import consola from "consola";
|
|
10
|
+
import envPaths from "env-paths";
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
import { ZodError } from "zod";
|
|
13
|
+
var PROJECT_CONFIG_FILENAME = "prismalens.config.yaml";
|
|
14
|
+
var PROJECT_LOCAL_CONFIG_FILENAME = "prismalens.config.local.yaml";
|
|
15
|
+
var USER_CONFIG_FILENAME = "config.yaml";
|
|
16
|
+
function findConfigFile(cwd = process.cwd()) {
|
|
17
|
+
let dir = resolve(cwd);
|
|
18
|
+
const { root } = parsePath(dir);
|
|
19
|
+
for (; ; ) {
|
|
20
|
+
const candidate = join(dir, PROJECT_CONFIG_FILENAME);
|
|
21
|
+
if (existsSync(candidate)) return candidate;
|
|
22
|
+
if (dir === root) return void 0;
|
|
23
|
+
dir = dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function userConfigFile() {
|
|
27
|
+
const candidate = join(
|
|
28
|
+
envPaths("prismalens", { suffix: "" }).config,
|
|
29
|
+
USER_CONFIG_FILENAME
|
|
30
|
+
);
|
|
31
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
32
|
+
}
|
|
33
|
+
function projectLocalConfigFile(projectFile, cwd) {
|
|
34
|
+
const dir = projectFile ? dirname(projectFile) : resolve(cwd);
|
|
35
|
+
const candidate = join(dir, PROJECT_LOCAL_CONFIG_FILENAME);
|
|
36
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
37
|
+
}
|
|
38
|
+
async function readYamlFile(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(filePath, "utf-8");
|
|
41
|
+
return parseYaml(content) ?? {};
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === "ENOENT") return {};
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
|
|
48
|
+
function interpolateDeep(value) {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
return value.replace(ENV_VAR_PATTERN, (_, name) => {
|
|
51
|
+
const v = process.env[name];
|
|
52
|
+
if (v === void 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Config references unset environment variable "${name}" (as \${${name}})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) return value.map(interpolateDeep);
|
|
61
|
+
if (value !== null && typeof value === "object") {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, v] of Object.entries(value)) out[k] = interpolateDeep(v);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function isObject(v) {
|
|
69
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
70
|
+
}
|
|
71
|
+
function deepMerge(target, source) {
|
|
72
|
+
const out = { ...target };
|
|
73
|
+
for (const [k, sv] of Object.entries(source)) {
|
|
74
|
+
const tv = out[k];
|
|
75
|
+
out[k] = isObject(sv) && isObject(tv) ? deepMerge(tv, sv) : sv;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function applyCliOverrides(config, o) {
|
|
80
|
+
const patch = {};
|
|
81
|
+
const agent = {};
|
|
82
|
+
if (o.agent !== void 0) agent.default = o.agent;
|
|
83
|
+
if (o.model !== void 0) agent.model = o.model;
|
|
84
|
+
if (Object.keys(agent).length > 0) patch.agent = agent;
|
|
85
|
+
const telemetry = {};
|
|
86
|
+
if (o.prometheusUrl !== void 0) telemetry.prometheus_url = o.prometheusUrl;
|
|
87
|
+
if (o.alertmanagerUrl !== void 0)
|
|
88
|
+
telemetry.alertmanager_url = o.alertmanagerUrl;
|
|
89
|
+
if (o.apiUrl !== void 0) telemetry.api_url = o.apiUrl;
|
|
90
|
+
if (Object.keys(telemetry).length > 0) patch.telemetry = telemetry;
|
|
91
|
+
return deepMerge(config, patch);
|
|
92
|
+
}
|
|
93
|
+
function warnUnknownKeys(input, output, path = []) {
|
|
94
|
+
for (const key in input) {
|
|
95
|
+
if (!Object.hasOwn(output, key)) {
|
|
96
|
+
consola.warn(`Unknown config key ignored: ${[...path, key].join(".")}`);
|
|
97
|
+
} else if (isObject(input[key]) && isObject(output[key])) {
|
|
98
|
+
warnUnknownKeys(
|
|
99
|
+
input[key],
|
|
100
|
+
output[key],
|
|
101
|
+
[...path, key]
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function loadConfig(options = {}) {
|
|
107
|
+
const cwd = options.cwd ?? process.cwd();
|
|
108
|
+
if (options.configPath) {
|
|
109
|
+
const resolvedPath = resolve(options.configPath);
|
|
110
|
+
if (!existsSync(resolvedPath)) {
|
|
111
|
+
throw new Error(`Configuration file not found: ${resolvedPath}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const userFile = userConfigFile();
|
|
115
|
+
const projectFile = options.configPath ? resolve(options.configPath) : findConfigFile(cwd);
|
|
116
|
+
const projectLocalFile = projectLocalConfigFile(projectFile, cwd);
|
|
117
|
+
let merged = {};
|
|
118
|
+
const readFiles = [];
|
|
119
|
+
if (userFile) {
|
|
120
|
+
merged = deepMerge(merged, await readYamlFile(userFile));
|
|
121
|
+
readFiles.push(userFile);
|
|
122
|
+
}
|
|
123
|
+
if (projectFile) {
|
|
124
|
+
merged = deepMerge(merged, await readYamlFile(projectFile));
|
|
125
|
+
readFiles.push(projectFile);
|
|
126
|
+
}
|
|
127
|
+
if (projectLocalFile && projectLocalFile !== projectFile) {
|
|
128
|
+
merged = deepMerge(merged, await readYamlFile(projectLocalFile));
|
|
129
|
+
readFiles.push(projectLocalFile);
|
|
130
|
+
}
|
|
131
|
+
if (options.cliOverrides) {
|
|
132
|
+
merged = applyCliOverrides(merged, options.cliOverrides);
|
|
133
|
+
}
|
|
134
|
+
const interpolated = interpolateDeep(merged);
|
|
135
|
+
let config;
|
|
136
|
+
let attribution = "in config";
|
|
137
|
+
if (readFiles.length === 1) {
|
|
138
|
+
attribution = `in ${readFiles[0]}`;
|
|
139
|
+
} else if (readFiles.length > 1) {
|
|
140
|
+
attribution = `(merged from: ${readFiles.join(", ")})`;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
config = PlConfigSchema.parse(interpolated);
|
|
144
|
+
warnUnknownKeys(interpolated, config);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof ZodError) {
|
|
147
|
+
const issues = err.issues.map(
|
|
148
|
+
(i) => `${i.path.length > 0 ? i.path.join(".") : "<root>"}: ${i.message}`
|
|
149
|
+
).join("\n");
|
|
150
|
+
throw new Error(`Invalid configuration ${attribution}:
|
|
151
|
+
${issues}`);
|
|
152
|
+
}
|
|
153
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
154
|
+
throw new Error(`Invalid PrismaLens configuration: ${detail}`);
|
|
155
|
+
}
|
|
156
|
+
return Object.freeze(config);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
loadConfig
|
|
161
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ../@prismalens/config/dist/providers/llm.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var LLM_PROVIDERS = {
|
|
4
|
+
anthropic: {
|
|
5
|
+
id: "anthropic",
|
|
6
|
+
name: "Anthropic",
|
|
7
|
+
helpUrl: "https://console.anthropic.com/settings/keys",
|
|
8
|
+
defaultModel: "claude-sonnet-5",
|
|
9
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
10
|
+
logoUrl: "https://models.dev/logos/anthropic.svg",
|
|
11
|
+
allowedHosts: ["api.anthropic.com"]
|
|
12
|
+
},
|
|
13
|
+
openai: {
|
|
14
|
+
id: "openai",
|
|
15
|
+
name: "OpenAI",
|
|
16
|
+
helpUrl: "https://platform.openai.com/api-keys",
|
|
17
|
+
defaultModel: "gpt-5.4-mini",
|
|
18
|
+
envVar: "OPENAI_API_KEY",
|
|
19
|
+
logoUrl: "https://models.dev/logos/openai.svg",
|
|
20
|
+
allowedHosts: ["api.openai.com"]
|
|
21
|
+
},
|
|
22
|
+
google: {
|
|
23
|
+
id: "google",
|
|
24
|
+
name: "Google Gemini",
|
|
25
|
+
helpUrl: "https://aistudio.google.com",
|
|
26
|
+
defaultModel: "gemini-3.5-flash",
|
|
27
|
+
envVar: "GOOGLE_API_KEY",
|
|
28
|
+
logoUrl: "https://models.dev/logos/google.svg",
|
|
29
|
+
allowedHosts: ["generativelanguage.googleapis.com"]
|
|
30
|
+
},
|
|
31
|
+
ollama: {
|
|
32
|
+
id: "ollama",
|
|
33
|
+
name: "Ollama",
|
|
34
|
+
helpUrl: "https://ollama.ai",
|
|
35
|
+
defaultModel: "gpt-oss:20b-cloud",
|
|
36
|
+
envVar: "OLLAMA_API_KEY",
|
|
37
|
+
// Optional — for Ollama Cloud
|
|
38
|
+
baseUrlRequired: false,
|
|
39
|
+
defaultBaseUrl: "http://localhost:11434/v1",
|
|
40
|
+
cloudBaseUrl: "https://ollama.com",
|
|
41
|
+
logoUrl: "https://models.dev/logos/ollama.svg",
|
|
42
|
+
allowedHosts: ["localhost", "127.0.0.1", "::1", "ollama.com"]
|
|
43
|
+
},
|
|
44
|
+
groq: {
|
|
45
|
+
id: "groq",
|
|
46
|
+
name: "Groq",
|
|
47
|
+
helpUrl: "https://console.groq.com/keys",
|
|
48
|
+
defaultModel: "openai/gpt-oss-120b",
|
|
49
|
+
envVar: "GROQ_API_KEY",
|
|
50
|
+
free: true,
|
|
51
|
+
logoUrl: "https://models.dev/logos/groq.svg",
|
|
52
|
+
allowedHosts: ["api.groq.com"]
|
|
53
|
+
},
|
|
54
|
+
custom: {
|
|
55
|
+
id: "custom",
|
|
56
|
+
name: "Custom (OpenAI)",
|
|
57
|
+
helpUrl: "https://platform.openai.com/docs/api-reference",
|
|
58
|
+
defaultModel: null,
|
|
59
|
+
envVar: "CUSTOM_LLM_API_KEY",
|
|
60
|
+
baseUrlRequired: true,
|
|
61
|
+
defaultBaseUrl: "http://localhost:8000/v1",
|
|
62
|
+
logoUrl: null,
|
|
63
|
+
allowedHosts: null
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var LLM_PROVIDER_IDS = Object.keys(LLM_PROVIDERS);
|
|
67
|
+
var AUTO_SELECT_PROVIDER_IDS = LLM_PROVIDER_IDS.filter((id) => id !== "custom");
|
|
68
|
+
var LLM_CREDENTIAL_ENV_VARS = Object.values(LLM_PROVIDERS).map((provider) => provider.envVar);
|
|
69
|
+
var llmProviderIdSchema = z.enum(LLM_PROVIDER_IDS);
|
|
70
|
+
function getApiKeyEnvVar(providerId) {
|
|
71
|
+
return LLM_PROVIDERS[providerId].envVar;
|
|
72
|
+
}
|
|
73
|
+
function getDefaultModel(providerId) {
|
|
74
|
+
return LLM_PROVIDERS[providerId].defaultModel;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
LLM_PROVIDERS,
|
|
79
|
+
AUTO_SELECT_PROVIDER_IDS,
|
|
80
|
+
llmProviderIdSchema,
|
|
81
|
+
getApiKeyEnvVar,
|
|
82
|
+
getDefaultModel
|
|
83
|
+
};
|