envseed 0.1.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/Dockerfile.simulation +18 -0
- package/README.md +498 -0
- package/bin/dashboard.mjs +706 -0
- package/bin/propensity-monitor.mjs +897 -0
- package/commands/log-incident.md +20 -0
- package/entrypoint.sh +93 -0
- package/lib/background-analyzer.mjs +113 -0
- package/lib/container-replicator.mjs +690 -0
- package/lib/hook-handler.mjs +109 -0
- package/lib/llm-analyzer.mjs +247 -0
- package/lib/log-incident.mjs +320 -0
- package/lib/logger.mjs +42 -0
- package/lib/personas.mjs +176 -0
- package/lib/redaction-review.mjs +255 -0
- package/lib/risk-analyzer.mjs +477 -0
- package/lib/s3.mjs +191 -0
- package/lib/session-tracker.mjs +132 -0
- package/lib/simulation-orchestrator.mjs +492 -0
- package/lib/utils.mjs +33 -0
- package/package.json +28 -0
- package/postinstall.mjs +165 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import https from 'node:https';
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = path.join(process.env.HOME, '.propensity-monitor', 'data');
|
|
8
|
+
const INSTALL_DIR = path.join(process.env.HOME, '.propensity-monitor');
|
|
9
|
+
const CLAUDE_SETTINGS = path.join(process.env.HOME, '.claude', 'settings.json');
|
|
10
|
+
|
|
11
|
+
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const C = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
bold: '\x1b[1m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
cyan: '\x1b[36m',
|
|
21
|
+
gray: '\x1b[90m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function colorForCategory(cat) {
|
|
25
|
+
if (cat === 'critical') return C.red;
|
|
26
|
+
if (cat === 'high') return C.yellow;
|
|
27
|
+
if (cat === 'medium') return C.cyan;
|
|
28
|
+
return C.dim;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function today() {
|
|
34
|
+
return new Date().toISOString().split('T')[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJsonl(filePath) {
|
|
38
|
+
if (!fs.existsSync(filePath)) return [];
|
|
39
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter(line => line.trim())
|
|
42
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(filePath) {
|
|
47
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArgs(args) {
|
|
51
|
+
const opts = {};
|
|
52
|
+
for (let i = 0; i < args.length; i++) {
|
|
53
|
+
if (args[i].startsWith('--')) {
|
|
54
|
+
const key = args[i].replace(/^--/, '');
|
|
55
|
+
const next = args[i + 1];
|
|
56
|
+
if (next && !next.startsWith('--')) {
|
|
57
|
+
opts[key] = next;
|
|
58
|
+
i++;
|
|
59
|
+
} else {
|
|
60
|
+
opts[key] = true;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
opts._positional = opts._positional || [];
|
|
64
|
+
opts._positional.push(args[i]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return opts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shortTime(ts) {
|
|
71
|
+
return ts.replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shortSession(id) {
|
|
75
|
+
return id ? id.substring(0, 12) : 'unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function showAlerts(args) {
|
|
81
|
+
const opts = parseArgs(args);
|
|
82
|
+
const date = opts.date || today();
|
|
83
|
+
const last = parseInt(opts.last || '20', 10);
|
|
84
|
+
|
|
85
|
+
const file = path.join(DATA_DIR, 'alerts', `${date}.jsonl`);
|
|
86
|
+
const events = readJsonl(file);
|
|
87
|
+
|
|
88
|
+
if (events.length === 0) {
|
|
89
|
+
console.log(`No alerts for ${date}.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const shown = events.slice(-last);
|
|
94
|
+
console.log(`${C.bold}Alerts for ${date}${C.reset} (showing ${shown.length} of ${events.length}):\n`);
|
|
95
|
+
|
|
96
|
+
for (const e of shown) {
|
|
97
|
+
const color = colorForCategory(e.risk?.category);
|
|
98
|
+
const cat = (e.risk?.category || 'none').toUpperCase().padEnd(8);
|
|
99
|
+
const time = shortTime(e.timestamp);
|
|
100
|
+
const sid = shortSession(e.session_id);
|
|
101
|
+
const tool = e.tool_name || e.hook_event_name || '';
|
|
102
|
+
|
|
103
|
+
console.log(`${color}${C.bold}${cat}${C.reset} ${C.dim}${time}${C.reset} ${C.cyan}${sid}${C.reset} ${tool}`);
|
|
104
|
+
|
|
105
|
+
for (const f of (e.risk?.factors || [])) {
|
|
106
|
+
if (f.score >= 2) {
|
|
107
|
+
console.log(` ${color}${f.name}${C.reset}: ${f.detail}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (e.session_state) {
|
|
112
|
+
const ss = e.session_state;
|
|
113
|
+
console.log(` ${C.dim}Session: ${ss.toolCallCount} tools, ${ss.toolCallsSinceLastPrompt} since last prompt${C.reset}`);
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function showEvents(args) {
|
|
120
|
+
const opts = parseArgs(args);
|
|
121
|
+
const date = opts.date || today();
|
|
122
|
+
const last = parseInt(opts.last || '50', 10);
|
|
123
|
+
const minRisk = parseInt(opts['min-risk'] || '0', 10);
|
|
124
|
+
|
|
125
|
+
const file = path.join(DATA_DIR, 'events', `${date}.jsonl`);
|
|
126
|
+
let events = readJsonl(file);
|
|
127
|
+
|
|
128
|
+
if (minRisk > 0) {
|
|
129
|
+
events = events.filter(e => (e.risk?.score || 0) >= minRisk);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (events.length === 0) {
|
|
133
|
+
console.log(`No events for ${date}${minRisk > 0 ? ` with risk >= ${minRisk}` : ''}.`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const shown = events.slice(-last);
|
|
138
|
+
console.log(`${C.bold}Events for ${date}${C.reset} (${shown.length} of ${events.length}):\n`);
|
|
139
|
+
|
|
140
|
+
for (const e of shown) {
|
|
141
|
+
const color = colorForCategory(e.risk?.category);
|
|
142
|
+
const score = e.risk?.score || 0;
|
|
143
|
+
const scoreStr = score > 0 ? `${color}[${score}]${C.reset} ` : `${C.dim}[0]${C.reset} `;
|
|
144
|
+
const time = shortTime(e.timestamp);
|
|
145
|
+
const tool = e.tool_name || e.hook_event_name || '';
|
|
146
|
+
const sid = shortSession(e.session_id);
|
|
147
|
+
|
|
148
|
+
let detail = '';
|
|
149
|
+
if (e.risk?.factors?.length > 0) {
|
|
150
|
+
detail = ` ${C.dim}(${e.risk.factors.map(f => f.name).join(', ')})${C.reset}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`${scoreStr}${C.dim}${time}${C.reset} ${C.cyan}${sid}${C.reset} ${tool}${detail}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function showSessions(args) {
|
|
158
|
+
const opts = parseArgs(args);
|
|
159
|
+
const minRisk = parseInt(opts['min-risk'] || '0', 10);
|
|
160
|
+
|
|
161
|
+
const index = readJson(path.join(DATA_DIR, 'index.json')) || {};
|
|
162
|
+
let entries = Object.entries(index);
|
|
163
|
+
|
|
164
|
+
if (minRisk > 0) {
|
|
165
|
+
entries = entries.filter(([, v]) => (v.maxRisk || 0) >= minRisk);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
entries.sort((a, b) => (b[1].date || '').localeCompare(a[1].date || ''));
|
|
169
|
+
|
|
170
|
+
if (entries.length === 0) {
|
|
171
|
+
console.log('No sessions recorded yet.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`${C.bold}${'SESSION'.padEnd(14)} ${'DATE'.padEnd(12)} ${'RISK'.padEnd(6)} ${'TOOLS'.padEnd(7)} ${'ALERTS'.padEnd(8)} CWD${C.reset}`);
|
|
176
|
+
|
|
177
|
+
for (const [id, v] of entries) {
|
|
178
|
+
const color = v.maxRisk >= 3 ? C.red : v.maxRisk >= 2 ? C.yellow : v.maxRisk >= 1 ? C.cyan : C.dim;
|
|
179
|
+
const sid = shortSession(id);
|
|
180
|
+
const cwdShort = (v.cwd || '').replace(process.env.HOME, '~');
|
|
181
|
+
|
|
182
|
+
console.log(
|
|
183
|
+
`${C.cyan}${sid}${C.reset} ${C.dim}${(v.date || '').padEnd(12)}${C.reset} ` +
|
|
184
|
+
`${color}${String(v.maxRisk || 0).padEnd(6)}${C.reset} ` +
|
|
185
|
+
`${String(v.toolCalls || 0).padEnd(7)} ` +
|
|
186
|
+
`${v.alerts > 0 ? C.red : C.dim}${String(v.alerts || 0).padEnd(8)}${C.reset} ` +
|
|
187
|
+
`${C.dim}${cwdShort}${C.reset}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function showSession(args) {
|
|
193
|
+
const opts = parseArgs(args);
|
|
194
|
+
const sessionId = opts._positional?.[0];
|
|
195
|
+
|
|
196
|
+
if (!sessionId) {
|
|
197
|
+
console.error('Usage: propensity-monitor session <session-id>');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find full session ID from prefix
|
|
202
|
+
const index = readJson(path.join(DATA_DIR, 'index.json')) || {};
|
|
203
|
+
const fullId = Object.keys(index).find(k => k.startsWith(sessionId));
|
|
204
|
+
|
|
205
|
+
if (!fullId) {
|
|
206
|
+
console.error(`Session not found: ${sessionId}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sessionData = readJson(path.join(DATA_DIR, 'sessions', `${fullId}.json`));
|
|
211
|
+
|
|
212
|
+
if (sessionData) {
|
|
213
|
+
console.log(`${C.bold}Session: ${fullId}${C.reset}`);
|
|
214
|
+
console.log(` Started: ${sessionData.started_at || 'unknown'}`);
|
|
215
|
+
console.log(` CWD: ${sessionData.cwd || 'unknown'}`);
|
|
216
|
+
console.log(` Tool calls: ${sessionData.toolCallCount}`);
|
|
217
|
+
console.log(` Prompts: ${sessionData.userPromptCount}`);
|
|
218
|
+
console.log(` Max risk: ${sessionData.maxRiskScore}`);
|
|
219
|
+
console.log(` Alerts: ${sessionData.alertCount}`);
|
|
220
|
+
|
|
221
|
+
if (sessionData.riskFactorCounts && Object.keys(sessionData.riskFactorCounts).length > 0) {
|
|
222
|
+
console.log(`\n ${C.bold}Risk factors:${C.reset}`);
|
|
223
|
+
const sorted = Object.entries(sessionData.riskFactorCounts).sort((a, b) => b[1] - a[1]);
|
|
224
|
+
for (const [name, count] of sorted) {
|
|
225
|
+
console.log(` ${name}: ${count}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (sessionData.toolNameCounts && Object.keys(sessionData.toolNameCounts).length > 0) {
|
|
230
|
+
console.log(`\n ${C.bold}Tool usage:${C.reset}`);
|
|
231
|
+
const sorted = Object.entries(sessionData.toolNameCounts).sort((a, b) => b[1] - a[1]);
|
|
232
|
+
for (const [name, count] of sorted) {
|
|
233
|
+
console.log(` ${name}: ${count}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Show events timeline
|
|
239
|
+
const date = index[fullId]?.date;
|
|
240
|
+
if (date) {
|
|
241
|
+
const file = path.join(DATA_DIR, 'events', `${date}.jsonl`);
|
|
242
|
+
const events = readJsonl(file).filter(e => e.session_id === fullId);
|
|
243
|
+
|
|
244
|
+
if (events.length > 0) {
|
|
245
|
+
console.log(`\n ${C.bold}Timeline (${events.length} events):${C.reset}\n`);
|
|
246
|
+
for (const e of events) {
|
|
247
|
+
const color = colorForCategory(e.risk?.category);
|
|
248
|
+
const score = e.risk?.score || 0;
|
|
249
|
+
const time = shortTime(e.timestamp).split(' ')[1] || '';
|
|
250
|
+
const tool = e.tool_name || e.hook_event_name || '';
|
|
251
|
+
const factors = (e.risk?.factors || []).map(f => f.name).join(', ');
|
|
252
|
+
|
|
253
|
+
console.log(
|
|
254
|
+
` ${C.dim}${time}${C.reset} ${score > 0 ? color : C.dim}[${score}]${C.reset} ${tool}` +
|
|
255
|
+
(factors ? ` ${C.dim}(${factors})${C.reset}` : '')
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function tailEvents(args) {
|
|
263
|
+
const opts = parseArgs(args);
|
|
264
|
+
const minRisk = parseInt(opts['min-risk'] || '0', 10);
|
|
265
|
+
const file = path.join(DATA_DIR, 'events', `${today()}.jsonl`);
|
|
266
|
+
|
|
267
|
+
console.log(`${C.bold}Tailing events...${C.reset} (Ctrl+C to stop)\n`);
|
|
268
|
+
|
|
269
|
+
let lastSize = 0;
|
|
270
|
+
try { lastSize = fs.statSync(file).size; } catch {}
|
|
271
|
+
|
|
272
|
+
const check = () => {
|
|
273
|
+
try {
|
|
274
|
+
const stat = fs.statSync(file);
|
|
275
|
+
if (stat.size > lastSize) {
|
|
276
|
+
const buf = Buffer.alloc(stat.size - lastSize);
|
|
277
|
+
const fd = fs.openSync(file, 'r');
|
|
278
|
+
fs.readSync(fd, buf, 0, buf.length, lastSize);
|
|
279
|
+
fs.closeSync(fd);
|
|
280
|
+
lastSize = stat.size;
|
|
281
|
+
|
|
282
|
+
const lines = buf.toString('utf8').split('\n').filter(l => l.trim());
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
try {
|
|
285
|
+
const e = JSON.parse(line);
|
|
286
|
+
if ((e.risk?.score || 0) < minRisk) continue;
|
|
287
|
+
|
|
288
|
+
const color = colorForCategory(e.risk?.category);
|
|
289
|
+
const score = e.risk?.score || 0;
|
|
290
|
+
const time = shortTime(e.timestamp);
|
|
291
|
+
const tool = e.tool_name || e.hook_event_name || '';
|
|
292
|
+
const sid = shortSession(e.session_id);
|
|
293
|
+
const factors = (e.risk?.factors || []).map(f => f.name).join(', ');
|
|
294
|
+
|
|
295
|
+
console.log(
|
|
296
|
+
`${score > 0 ? color : C.dim}[${score}]${C.reset} ${C.dim}${time}${C.reset} ` +
|
|
297
|
+
`${C.cyan}${sid}${C.reset} ${tool}` +
|
|
298
|
+
(factors ? ` ${C.dim}(${factors})${C.reset}` : '')
|
|
299
|
+
);
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
setInterval(check, 500);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function showStats(args) {
|
|
310
|
+
const opts = parseArgs(args);
|
|
311
|
+
const date = opts.date;
|
|
312
|
+
|
|
313
|
+
// Collect events
|
|
314
|
+
const eventsDir = path.join(DATA_DIR, 'events');
|
|
315
|
+
let allEvents = [];
|
|
316
|
+
|
|
317
|
+
if (date) {
|
|
318
|
+
allEvents = readJsonl(path.join(eventsDir, `${date}.jsonl`));
|
|
319
|
+
} else {
|
|
320
|
+
// All dates
|
|
321
|
+
try {
|
|
322
|
+
const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl')).sort();
|
|
323
|
+
for (const f of files) {
|
|
324
|
+
allEvents.push(...readJsonl(path.join(eventsDir, f)));
|
|
325
|
+
}
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (allEvents.length === 0) {
|
|
330
|
+
console.log('No events recorded yet.');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Compute stats
|
|
335
|
+
const sessions = new Set(allEvents.map(e => e.session_id).filter(Boolean));
|
|
336
|
+
const tools = {};
|
|
337
|
+
const riskDist = { 0: 0, 1: 0, 2: 0, 3: 0 };
|
|
338
|
+
const factorCounts = {};
|
|
339
|
+
|
|
340
|
+
for (const e of allEvents) {
|
|
341
|
+
if (e.tool_name) tools[e.tool_name] = (tools[e.tool_name] || 0) + 1;
|
|
342
|
+
const score = e.risk?.score || 0;
|
|
343
|
+
riskDist[score] = (riskDist[score] || 0) + 1;
|
|
344
|
+
for (const f of (e.risk?.factors || [])) {
|
|
345
|
+
factorCounts[f.name] = (factorCounts[f.name] || 0) + 1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log(`${C.bold}Statistics${date ? ` for ${date}` : ' (all time)'}${C.reset}\n`);
|
|
350
|
+
console.log(` Total events: ${allEvents.length}`);
|
|
351
|
+
console.log(` Total sessions: ${sessions.size}`);
|
|
352
|
+
|
|
353
|
+
console.log(`\n ${C.bold}Risk distribution:${C.reset}`);
|
|
354
|
+
console.log(` ${C.red}critical (3): ${riskDist[3] || 0}${C.reset} (${((riskDist[3] || 0) / allEvents.length * 100).toFixed(1)}%)`);
|
|
355
|
+
console.log(` ${C.yellow}high (2): ${riskDist[2] || 0}${C.reset} (${((riskDist[2] || 0) / allEvents.length * 100).toFixed(1)}%)`);
|
|
356
|
+
console.log(` ${C.cyan}medium (1): ${riskDist[1] || 0}${C.reset} (${((riskDist[1] || 0) / allEvents.length * 100).toFixed(1)}%)`);
|
|
357
|
+
console.log(` ${C.dim}none (0): ${riskDist[0] || 0}${C.reset} (${((riskDist[0] || 0) / allEvents.length * 100).toFixed(1)}%)`);
|
|
358
|
+
|
|
359
|
+
if (Object.keys(factorCounts).length > 0) {
|
|
360
|
+
console.log(`\n ${C.bold}Top risk factors:${C.reset}`);
|
|
361
|
+
const sorted = Object.entries(factorCounts).sort((a, b) => b[1] - a[1]);
|
|
362
|
+
for (const [name, count] of sorted.slice(0, 15)) {
|
|
363
|
+
console.log(` ${name}: ${count}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (Object.keys(tools).length > 0) {
|
|
368
|
+
console.log(`\n ${C.bold}Tool usage:${C.reset}`);
|
|
369
|
+
const sorted = Object.entries(tools).sort((a, b) => b[1] - a[1]);
|
|
370
|
+
for (const [name, count] of sorted.slice(0, 10)) {
|
|
371
|
+
console.log(` ${name}: ${count}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function searchEvents(args) {
|
|
377
|
+
const opts = parseArgs(args);
|
|
378
|
+
const pattern = opts._positional?.[0];
|
|
379
|
+
const date = opts.date;
|
|
380
|
+
const last = parseInt(opts.last || '30', 10);
|
|
381
|
+
|
|
382
|
+
if (!pattern) {
|
|
383
|
+
console.error('Usage: propensity-monitor search <pattern> [--date YYYY-MM-DD]');
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const re = new RegExp(pattern, 'i');
|
|
388
|
+
const eventsDir = path.join(DATA_DIR, 'events');
|
|
389
|
+
const matches = [];
|
|
390
|
+
|
|
391
|
+
const files = date
|
|
392
|
+
? [`${date}.jsonl`]
|
|
393
|
+
: (fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl')).sort());
|
|
394
|
+
|
|
395
|
+
for (const f of files) {
|
|
396
|
+
const events = readJsonl(path.join(eventsDir, f));
|
|
397
|
+
for (const e of events) {
|
|
398
|
+
const str = JSON.stringify(e);
|
|
399
|
+
if (re.test(str)) matches.push(e);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (matches.length === 0) {
|
|
404
|
+
console.log(`No events matching: ${pattern}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const shown = matches.slice(-last);
|
|
409
|
+
console.log(`${C.bold}Search: ${pattern}${C.reset} (${shown.length} of ${matches.length} matches):\n`);
|
|
410
|
+
|
|
411
|
+
for (const e of shown) {
|
|
412
|
+
const color = colorForCategory(e.risk?.category);
|
|
413
|
+
const score = e.risk?.score || 0;
|
|
414
|
+
const time = shortTime(e.timestamp);
|
|
415
|
+
const tool = e.tool_name || e.hook_event_name || '';
|
|
416
|
+
console.log(`${score > 0 ? color : C.dim}[${score}]${C.reset} ${C.dim}${time}${C.reset} ${tool}`);
|
|
417
|
+
|
|
418
|
+
// Show matching content
|
|
419
|
+
if (e.tool_input_summary?.command) {
|
|
420
|
+
console.log(` ${C.dim}cmd: ${e.tool_input_summary.command.substring(0, 100)}${C.reset}`);
|
|
421
|
+
}
|
|
422
|
+
if (e.tool_input_summary?.file_path) {
|
|
423
|
+
console.log(` ${C.dim}file: ${e.tool_input_summary.file_path}${C.reset}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function exportData(args) {
|
|
429
|
+
const opts = parseArgs(args);
|
|
430
|
+
const date = opts.date;
|
|
431
|
+
const minRisk = parseInt(opts['min-risk'] || '0', 10);
|
|
432
|
+
const format = opts.format || 'jsonl';
|
|
433
|
+
|
|
434
|
+
const eventsDir = path.join(DATA_DIR, 'events');
|
|
435
|
+
let allEvents = [];
|
|
436
|
+
|
|
437
|
+
if (date) {
|
|
438
|
+
allEvents = readJsonl(path.join(eventsDir, `${date}.jsonl`));
|
|
439
|
+
} else {
|
|
440
|
+
try {
|
|
441
|
+
const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl')).sort();
|
|
442
|
+
for (const f of files) {
|
|
443
|
+
allEvents.push(...readJsonl(path.join(eventsDir, f)));
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (minRisk > 0) {
|
|
449
|
+
allEvents = allEvents.filter(e => (e.risk?.score || 0) >= minRisk);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (format === 'json') {
|
|
453
|
+
process.stdout.write(JSON.stringify(allEvents, null, 2) + '\n');
|
|
454
|
+
} else {
|
|
455
|
+
for (const e of allEvents) {
|
|
456
|
+
process.stdout.write(JSON.stringify(e) + '\n');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function showStatus() {
|
|
462
|
+
console.log(`${C.bold}propensity-monitor status${C.reset}\n`);
|
|
463
|
+
|
|
464
|
+
// Check install dir
|
|
465
|
+
const dirExists = fs.existsSync(INSTALL_DIR);
|
|
466
|
+
console.log(` Install dir: ${dirExists ? C.green + 'OK' : C.red + 'MISSING'}${C.reset} ${INSTALL_DIR}`);
|
|
467
|
+
|
|
468
|
+
// Check data dir
|
|
469
|
+
const dataExists = fs.existsSync(DATA_DIR);
|
|
470
|
+
console.log(` Data dir: ${dataExists ? C.green + 'OK' : C.red + 'MISSING'}${C.reset} ${DATA_DIR}`);
|
|
471
|
+
|
|
472
|
+
// Check hooks in settings
|
|
473
|
+
let hooksOk = false;
|
|
474
|
+
try {
|
|
475
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf8'));
|
|
476
|
+
const events = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'Stop'];
|
|
477
|
+
const registered = events.filter(e => {
|
|
478
|
+
const hooks = settings.hooks?.[e] || [];
|
|
479
|
+
return hooks.some(h => {
|
|
480
|
+
if (h.command?.includes('propensity-monitor')) return true;
|
|
481
|
+
if (h.hooks) return h.hooks.some(hh => hh.command?.includes('propensity-monitor'));
|
|
482
|
+
return false;
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
hooksOk = registered.length === events.length;
|
|
486
|
+
console.log(` Hooks: ${hooksOk ? C.green + 'OK' : C.yellow + 'PARTIAL'}${C.reset} (${registered.length}/${events.length} events)`);
|
|
487
|
+
if (!hooksOk) {
|
|
488
|
+
const missing = events.filter(e => !registered.includes(e));
|
|
489
|
+
console.log(` Missing: ${missing.join(', ')}`);
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
console.log(` Hooks: ${C.red}ERROR${C.reset} (cannot read ${CLAUDE_SETTINGS})`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check event count
|
|
496
|
+
try {
|
|
497
|
+
const eventsDir = path.join(DATA_DIR, 'events');
|
|
498
|
+
const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl'));
|
|
499
|
+
let total = 0;
|
|
500
|
+
for (const f of files) {
|
|
501
|
+
const content = fs.readFileSync(path.join(eventsDir, f), 'utf8');
|
|
502
|
+
total += content.split('\n').filter(l => l.trim()).length;
|
|
503
|
+
}
|
|
504
|
+
console.log(` Events: ${total} across ${files.length} day(s)`);
|
|
505
|
+
} catch {
|
|
506
|
+
console.log(` Events: ${C.dim}none yet${C.reset}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check alerts
|
|
510
|
+
try {
|
|
511
|
+
const alertsDir = path.join(DATA_DIR, 'alerts');
|
|
512
|
+
const files = fs.readdirSync(alertsDir).filter(f => f.endsWith('.jsonl'));
|
|
513
|
+
let total = 0;
|
|
514
|
+
for (const f of files) {
|
|
515
|
+
const content = fs.readFileSync(path.join(alertsDir, f), 'utf8');
|
|
516
|
+
total += content.split('\n').filter(l => l.trim()).length;
|
|
517
|
+
}
|
|
518
|
+
console.log(` Alerts: ${total > 0 ? C.red : C.dim}${total}${C.reset}`);
|
|
519
|
+
} catch {
|
|
520
|
+
console.log(` Alerts: ${C.dim}none${C.reset}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── Incident Commands ───────────────────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
const INCIDENTS_DIR = path.join(DATA_DIR, 'incidents');
|
|
527
|
+
|
|
528
|
+
function showIncidents(args) {
|
|
529
|
+
const opts = parseArgs(args);
|
|
530
|
+
const last = parseInt(opts.last || '20', 10);
|
|
531
|
+
|
|
532
|
+
if (!fs.existsSync(INCIDENTS_DIR)) {
|
|
533
|
+
console.log('No incidents logged yet.');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const entries = fs.readdirSync(INCIDENTS_DIR)
|
|
538
|
+
.filter(f => fs.statSync(path.join(INCIDENTS_DIR, f)).isDirectory())
|
|
539
|
+
.sort()
|
|
540
|
+
.reverse()
|
|
541
|
+
.slice(0, last);
|
|
542
|
+
|
|
543
|
+
if (entries.length === 0) {
|
|
544
|
+
console.log('No incidents logged yet.');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
console.log(`${C.bold}${'INCIDENT'.padEnd(28)} ${'STATUS'.padEnd(12)} ${'SIMS'.padEnd(10)} CWD${C.reset}`);
|
|
549
|
+
|
|
550
|
+
for (const id of entries) {
|
|
551
|
+
const meta = readJson(path.join(INCIDENTS_DIR, id, 'metadata.json'));
|
|
552
|
+
const status = readJson(path.join(INCIDENTS_DIR, id, 'status.json'));
|
|
553
|
+
|
|
554
|
+
const cwdShort = (meta?.cwd || '').replace(process.env.HOME, '~');
|
|
555
|
+
const totalPlanned = status?.totalPlanned || 0;
|
|
556
|
+
const completed = status?.completed || 0;
|
|
557
|
+
const failed = status?.failed || 0;
|
|
558
|
+
|
|
559
|
+
let statusStr, statusColor;
|
|
560
|
+
if (status?.finished) {
|
|
561
|
+
statusStr = 'done';
|
|
562
|
+
statusColor = C.green;
|
|
563
|
+
} else if (status?.shuttingDown) {
|
|
564
|
+
statusStr = 'stopping';
|
|
565
|
+
statusColor = C.yellow;
|
|
566
|
+
} else if (status?.simulationsStarted) {
|
|
567
|
+
statusStr = 'running';
|
|
568
|
+
statusColor = C.cyan;
|
|
569
|
+
} else if (status?.error) {
|
|
570
|
+
statusStr = 'error';
|
|
571
|
+
statusColor = C.red;
|
|
572
|
+
} else {
|
|
573
|
+
statusStr = 'archived';
|
|
574
|
+
statusColor = C.dim;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const simStr = totalPlanned > 0 ? `${completed}/${totalPlanned}${failed > 0 ? ` (${C.red}${failed}err${C.reset})` : ''}` : '-';
|
|
578
|
+
|
|
579
|
+
console.log(
|
|
580
|
+
`${C.cyan}${id.padEnd(28)}${C.reset} ${statusColor}${statusStr.padEnd(12)}${C.reset} ${simStr.padEnd(10)} ${C.dim}${cwdShort}${C.reset}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function showIncident(args) {
|
|
586
|
+
const opts = parseArgs(args);
|
|
587
|
+
const incidentId = opts._positional?.[0];
|
|
588
|
+
const subCmd = opts._positional?.[1];
|
|
589
|
+
|
|
590
|
+
if (!incidentId) {
|
|
591
|
+
console.error('Usage: propensity-monitor incident <id> [simulations|upload]');
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Find incident (support prefix match)
|
|
596
|
+
let fullId = incidentId;
|
|
597
|
+
if (!fs.existsSync(path.join(INCIDENTS_DIR, incidentId))) {
|
|
598
|
+
try {
|
|
599
|
+
const match = fs.readdirSync(INCIDENTS_DIR).find(d => d.startsWith(incidentId));
|
|
600
|
+
if (match) fullId = match;
|
|
601
|
+
else { console.error(`Incident not found: ${incidentId}`); process.exit(1); }
|
|
602
|
+
} catch { console.error(`Incident not found: ${incidentId}`); process.exit(1); }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const incidentDir = path.join(INCIDENTS_DIR, fullId);
|
|
606
|
+
const meta = readJson(path.join(incidentDir, 'metadata.json'));
|
|
607
|
+
const status = readJson(path.join(incidentDir, 'status.json'));
|
|
608
|
+
|
|
609
|
+
if (subCmd === 'simulations') {
|
|
610
|
+
return showIncidentSimulations(incidentDir, fullId, status);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (subCmd === 'upload') {
|
|
614
|
+
console.log('Re-uploading is handled by: node ~/.propensity-monitor/lib/log-incident.mjs');
|
|
615
|
+
console.log(`Incident dir: ${incidentDir}`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
console.log(`${C.bold}Incident: ${fullId}${C.reset}\n`);
|
|
620
|
+
|
|
621
|
+
if (meta) {
|
|
622
|
+
console.log(` Session: ${meta.sessionId || 'unknown'}`);
|
|
623
|
+
console.log(` CWD: ${meta.cwd || 'unknown'}`);
|
|
624
|
+
console.log(` Timestamp: ${meta.timestamp || 'unknown'}`);
|
|
625
|
+
console.log(` Notes: ${meta.userNotes || '(none)'}`);
|
|
626
|
+
console.log(` Transcripts: ${(meta.transcriptFiles || []).length} file(s)`);
|
|
627
|
+
console.log(` Assessments: ${meta.assessmentCount || 0}`);
|
|
628
|
+
console.log(` Dir snapshot: ${meta.dirSnapshotSizeMB ? meta.dirSnapshotSizeMB + ' MB' : 'none'}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (status) {
|
|
632
|
+
console.log(`\n ${C.bold}Simulation Status:${C.reset}`);
|
|
633
|
+
console.log(` Started: ${status.started || 'unknown'}`);
|
|
634
|
+
console.log(` Finished: ${status.finished || 'in progress'}`);
|
|
635
|
+
console.log(` Planned: ${status.totalPlanned || 0}`);
|
|
636
|
+
console.log(` Completed: ${C.green}${status.completed || 0}${C.reset}`);
|
|
637
|
+
console.log(` Failed: ${(status.failed || 0) > 0 ? C.red : C.dim}${status.failed || 0}${C.reset}`);
|
|
638
|
+
console.log(` Running: ${status.running || 0}`);
|
|
639
|
+
if (status.s3Uploaded) console.log(` S3: ${C.green}uploaded${C.reset}`);
|
|
640
|
+
if (status.error) console.log(` Error: ${C.red}${status.error}${C.reset}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
console.log(`\n ${C.dim}View simulations: propensity-monitor incident ${fullId} simulations${C.reset}`);
|
|
644
|
+
console.log(` ${C.dim}Local path: ${incidentDir}${C.reset}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function showIncidentSimulations(incidentDir, fullId, status) {
|
|
648
|
+
const simsDir = path.join(incidentDir, 'simulations');
|
|
649
|
+
|
|
650
|
+
if (!status?.simulations || status.simulations.length === 0) {
|
|
651
|
+
console.log('No simulations recorded yet.');
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
console.log(`${C.bold}Simulations for ${fullId}${C.reset}\n`);
|
|
656
|
+
console.log(`${C.bold}${'ID'.padEnd(10)} ${'PERSONA'.padEnd(22)} ${'STATUS'.padEnd(12)} ${'TURNS'.padEnd(7)} DURATION${C.reset}`);
|
|
657
|
+
|
|
658
|
+
for (const sim of status.simulations) {
|
|
659
|
+
const statusColor = sim.status === 'completed' ? C.green
|
|
660
|
+
: sim.status === 'running' ? C.cyan
|
|
661
|
+
: sim.status === 'failed' ? C.red
|
|
662
|
+
: C.dim;
|
|
663
|
+
|
|
664
|
+
const dur = sim.duration ? `${sim.duration}s` : '-';
|
|
665
|
+
console.log(
|
|
666
|
+
`${C.cyan}${(sim.id || '').padEnd(10)}${C.reset} ` +
|
|
667
|
+
`${(sim.persona || '').padEnd(22)} ` +
|
|
668
|
+
`${statusColor}${(sim.status || 'pending').padEnd(12)}${C.reset} ` +
|
|
669
|
+
`${String(sim.turns || 0).padEnd(7)} ` +
|
|
670
|
+
`${C.dim}${dur}${C.reset}`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Show detailed results if available
|
|
675
|
+
if (fs.existsSync(simsDir)) {
|
|
676
|
+
const simDirs = fs.readdirSync(simsDir).filter(d => {
|
|
677
|
+
const resultPath = path.join(simsDir, d, 'result.json');
|
|
678
|
+
return fs.existsSync(resultPath);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const failedSims = simDirs.filter(d => {
|
|
682
|
+
const result = readJson(path.join(simsDir, d, 'result.json'));
|
|
683
|
+
return result && result.exitReason !== 'completed';
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
if (failedSims.length > 0) {
|
|
687
|
+
console.log(`\n${C.bold}${C.red}Failed simulations:${C.reset}`);
|
|
688
|
+
for (const d of failedSims) {
|
|
689
|
+
const result = readJson(path.join(simsDir, d, 'result.json'));
|
|
690
|
+
console.log(` ${C.cyan}${d}${C.reset}: ${result?.error || result?.exitReason || 'unknown'}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function toggleEnabled(enable) {
|
|
697
|
+
const configPath = path.join(INSTALL_DIR, 'config.json');
|
|
698
|
+
let config = {};
|
|
699
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
|
|
700
|
+
config.enabled = enable;
|
|
701
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
702
|
+
console.log(`propensity-monitor ${enable ? C.green + 'enabled' : C.red + 'disabled'}${C.reset}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function turnOn() { toggleEnabled(true); }
|
|
706
|
+
function turnOff() { toggleEnabled(false); }
|
|
707
|
+
|
|
708
|
+
async function startDashboard(args) {
|
|
709
|
+
const opts = parseArgs(args);
|
|
710
|
+
const port = opts.port || '3456';
|
|
711
|
+
const dashboardScript = path.join(INSTALL_DIR, 'bin', 'dashboard.mjs');
|
|
712
|
+
if (!fs.existsSync(dashboardScript)) {
|
|
713
|
+
console.error('Dashboard not installed. Run install.sh to update.');
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
const { spawnSync } = await import('child_process');
|
|
717
|
+
spawnSync('node', [dashboardScript, '--port', port], { stdio: 'inherit' });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// GitHub Device Flow client ID — set after creating the OAuth App
|
|
721
|
+
const GITHUB_CLIENT_ID = process.env.ENVSEED_GITHUB_CLIENT_ID || 'Ov23lid2fKxyN7lOd9qv';
|
|
722
|
+
|
|
723
|
+
function httpsRequest(options, body) {
|
|
724
|
+
return new Promise((resolve, reject) => {
|
|
725
|
+
const req = https.request(options, (res) => {
|
|
726
|
+
let data = '';
|
|
727
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
728
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
|
729
|
+
});
|
|
730
|
+
req.on('error', reject);
|
|
731
|
+
if (body) req.write(body);
|
|
732
|
+
req.end();
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
737
|
+
|
|
738
|
+
async function registerCommand() {
|
|
739
|
+
const config = readJson(path.join(INSTALL_DIR, 'config.json')) || {};
|
|
740
|
+
|
|
741
|
+
if (config.apiKey) {
|
|
742
|
+
console.log(`Already registered with API key: ${config.apiKey.substring(0, 8)}...`);
|
|
743
|
+
console.log(`To re-register, remove apiKey from ${INSTALL_DIR}/config.json`);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const clientId = config.githubClientId || GITHUB_CLIENT_ID;
|
|
748
|
+
if (!clientId) {
|
|
749
|
+
console.error('No GitHub client ID configured.');
|
|
750
|
+
console.error(`Set githubClientId in ${INSTALL_DIR}/config.json`);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const uploadEndpoint = config.uploadEndpoint;
|
|
755
|
+
if (!uploadEndpoint) {
|
|
756
|
+
console.error('No upload endpoint configured.');
|
|
757
|
+
console.error(`Set uploadEndpoint in ${INSTALL_DIR}/config.json`);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Step 1: Request device code
|
|
762
|
+
console.log('Starting GitHub authentication...');
|
|
763
|
+
const codeRes = await httpsRequest({
|
|
764
|
+
hostname: 'github.com',
|
|
765
|
+
path: '/login/device/code',
|
|
766
|
+
method: 'POST',
|
|
767
|
+
headers: {
|
|
768
|
+
'Content-Type': 'application/json',
|
|
769
|
+
Accept: 'application/json',
|
|
770
|
+
},
|
|
771
|
+
}, JSON.stringify({ client_id: clientId, scope: 'read:user' }));
|
|
772
|
+
|
|
773
|
+
const codeData = JSON.parse(codeRes.body);
|
|
774
|
+
if (!codeData.device_code) {
|
|
775
|
+
console.error('Failed to start device flow:', codeData);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log('');
|
|
780
|
+
console.log(` Visit: ${C.bold}${codeData.verification_uri}${C.reset}`);
|
|
781
|
+
console.log(` Enter code: ${C.bold}${C.green}${codeData.user_code}${C.reset}`);
|
|
782
|
+
console.log('');
|
|
783
|
+
console.log('Waiting for authorization...');
|
|
784
|
+
|
|
785
|
+
// Step 2: Poll for access token
|
|
786
|
+
const interval = (codeData.interval || 5) * 1000;
|
|
787
|
+
let githubToken = null;
|
|
788
|
+
|
|
789
|
+
for (let i = 0; i < 60; i++) {
|
|
790
|
+
await sleep(interval);
|
|
791
|
+
|
|
792
|
+
const tokenRes = await httpsRequest({
|
|
793
|
+
hostname: 'github.com',
|
|
794
|
+
path: '/login/oauth/access_token',
|
|
795
|
+
method: 'POST',
|
|
796
|
+
headers: {
|
|
797
|
+
'Content-Type': 'application/json',
|
|
798
|
+
Accept: 'application/json',
|
|
799
|
+
},
|
|
800
|
+
}, JSON.stringify({
|
|
801
|
+
client_id: clientId,
|
|
802
|
+
device_code: codeData.device_code,
|
|
803
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
804
|
+
}));
|
|
805
|
+
|
|
806
|
+
const tokenData = JSON.parse(tokenRes.body);
|
|
807
|
+
|
|
808
|
+
if (tokenData.access_token) {
|
|
809
|
+
githubToken = tokenData.access_token;
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
if (tokenData.error === 'authorization_pending') continue;
|
|
813
|
+
if (tokenData.error === 'slow_down') {
|
|
814
|
+
await sleep(5000);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (tokenData.error === 'expired_token') {
|
|
818
|
+
console.error('Authorization timed out. Please try again.');
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
if (tokenData.error) {
|
|
822
|
+
console.error(`GitHub error: ${tokenData.error_description || tokenData.error}`);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!githubToken) {
|
|
828
|
+
console.error('Timed out waiting for authorization.');
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Step 3: Exchange GitHub token for envseed API key
|
|
833
|
+
console.log('Registering with envseed...');
|
|
834
|
+
const regRes = await httpsRequest({
|
|
835
|
+
hostname: new URL(uploadEndpoint).hostname,
|
|
836
|
+
path: '/register',
|
|
837
|
+
method: 'POST',
|
|
838
|
+
headers: {
|
|
839
|
+
'Content-Type': 'application/json',
|
|
840
|
+
},
|
|
841
|
+
}, JSON.stringify({ githubToken }));
|
|
842
|
+
|
|
843
|
+
if (regRes.statusCode !== 200) {
|
|
844
|
+
console.error(`Registration failed: ${regRes.body}`);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const regData = JSON.parse(regRes.body);
|
|
849
|
+
config.apiKey = regData.apiKey;
|
|
850
|
+
fs.writeFileSync(path.join(INSTALL_DIR, 'config.json'), JSON.stringify(config, null, 2) + '\n');
|
|
851
|
+
|
|
852
|
+
console.log('');
|
|
853
|
+
console.log(`${C.green}${C.bold}Registered as @${regData.githubUser}. Your garden is ready.${C.reset}`);
|
|
854
|
+
console.log(`API key saved to ${INSTALL_DIR}/config.json`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function showHelp() {
|
|
858
|
+
console.log(`${C.bold}envseed${C.reset} (propensity-monitor) — cultivate AI safety evals from real Claude Code sessions
|
|
859
|
+
|
|
860
|
+
${C.bold}Usage:${C.reset}
|
|
861
|
+
envseed <command> [options]
|
|
862
|
+
|
|
863
|
+
${C.bold}Setup:${C.reset}
|
|
864
|
+
register Authenticate via GitHub and get API key
|
|
865
|
+
status Check installation health
|
|
866
|
+
|
|
867
|
+
${C.bold}Commands:${C.reset}
|
|
868
|
+
on Enable monitoring
|
|
869
|
+
off Disable monitoring
|
|
870
|
+
alerts [--date YYYY-MM-DD] [--last N] Show critical events
|
|
871
|
+
events [--date YYYY-MM-DD] [--last N] [--min-risk N] Show all events
|
|
872
|
+
sessions [--min-risk N] List sessions with risk summary
|
|
873
|
+
session <session-id> Detailed session view
|
|
874
|
+
tail [--min-risk N] Live-tail events
|
|
875
|
+
stats [--date YYYY-MM-DD] Aggregate statistics
|
|
876
|
+
search <pattern> [--date YYYY-MM-DD] [--last N] Search events by pattern
|
|
877
|
+
export [--date YYYY-MM-DD] [--min-risk N] [--format jsonl|json] Export for eval pipeline
|
|
878
|
+
dashboard [--port 3456] Open web dashboard
|
|
879
|
+
incidents [--last N] List logged incidents
|
|
880
|
+
incident <id> [simulations|upload] View incident detail or simulations
|
|
881
|
+
help Show this help
|
|
882
|
+
`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
886
|
+
|
|
887
|
+
const COMMANDS = { on: turnOn, off: turnOff, dashboard: startDashboard, alerts: showAlerts, events: showEvents, sessions: showSessions, session: showSession, tail: tailEvents, stats: showStats, search: searchEvents, export: exportData, incidents: showIncidents, incident: showIncident, status: showStatus, register: registerCommand, help: showHelp };
|
|
888
|
+
|
|
889
|
+
const [command, ...args] = process.argv.slice(2);
|
|
890
|
+
// Default: show status if installed, help if not
|
|
891
|
+
const effectiveCommand = command || (fs.existsSync(INSTALL_DIR) ? 'status' : 'help');
|
|
892
|
+
const handler = COMMANDS[effectiveCommand];
|
|
893
|
+
if (!handler) {
|
|
894
|
+
console.error(`Unknown command: ${command}. Run 'propensity-monitor help' for usage.`);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
Promise.resolve(handler(args)).catch(err => { console.error(err.message); process.exit(1); });
|