agenttop 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1241 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.tsx
4
+ import React3 from "react";
5
+ import { render } from "ink";
6
+
7
+ // src/discovery/sessions.ts
8
+ import { readdirSync as readdirSync2, readFileSync, statSync, openSync, readSync, closeSync } from "fs";
9
+ import { join as join2, basename } from "path";
10
+ import { execSync } from "child_process";
11
+
12
+ // src/config.ts
13
+ import { realpathSync, readdirSync } from "fs";
14
+ import { homedir, platform } from "os";
15
+ import { join } from "path";
16
+ var resolvePath = (p) => {
17
+ try {
18
+ return realpathSync(p);
19
+ } catch {
20
+ return p;
21
+ }
22
+ };
23
+ var getUid = () => process.getuid?.() ?? 0;
24
+ var isRoot = () => getUid() === 0;
25
+ var getTmpDir = () => resolvePath(platform() === "darwin" ? "/private/tmp" : "/tmp");
26
+ var getTaskDirs = (allUsers) => {
27
+ const tmp = getTmpDir();
28
+ const uid = getUid();
29
+ if (allUsers && isRoot()) {
30
+ try {
31
+ return readdirSync(tmp).filter((d) => d.startsWith("claude-")).filter((d) => !d.endsWith("-cwd")).map((d) => join(tmp, d));
32
+ } catch {
33
+ return [join(tmp, `claude-${uid}`)];
34
+ }
35
+ }
36
+ return [join(tmp, `claude-${uid}`)];
37
+ };
38
+
39
+ // src/discovery/sessions.ts
40
+ var getClaudeProcesses = () => {
41
+ try {
42
+ const output = execSync("ps aux", { encoding: "utf-8", timeout: 5e3 });
43
+ return output.split("\n").filter((line) => line.includes("claude") && !line.includes("grep") && !line.includes("agenttop")).map((line) => {
44
+ const parts = line.trim().split(/\s+/);
45
+ return {
46
+ pid: parseInt(parts[1], 10),
47
+ cpu: parseFloat(parts[2]) || 0,
48
+ mem: parseFloat(parts[3]) || 0,
49
+ memKB: parseInt(parts[5], 10) || 0,
50
+ startTime: parts[8] || "",
51
+ command: parts.slice(10).join(" ")
52
+ };
53
+ }).filter((p) => !isNaN(p.pid));
54
+ } catch {
55
+ return [];
56
+ }
57
+ };
58
+ var readFirstEvent = (filePath) => {
59
+ try {
60
+ const fd = openSync(filePath, "r");
61
+ const buf = Buffer.alloc(16384);
62
+ const bytesRead = readSync(fd, buf, 0, 16384, 0);
63
+ closeSync(fd);
64
+ const line = buf.subarray(0, bytesRead).toString("utf-8").split("\n")[0];
65
+ if (!line) return null;
66
+ return JSON.parse(line);
67
+ } catch {
68
+ return null;
69
+ }
70
+ };
71
+ var readLastLines = (filePath, count) => {
72
+ try {
73
+ const content = readFileSync(filePath, "utf-8");
74
+ const lines = content.trim().split("\n");
75
+ const last = lines.slice(-count);
76
+ return last.map((line) => {
77
+ try {
78
+ return JSON.parse(line);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }).filter((e) => e !== null);
83
+ } catch {
84
+ return [];
85
+ }
86
+ };
87
+ var discoverSessions = (allUsers) => {
88
+ const taskDirs = getTaskDirs(allUsers);
89
+ const processes = getClaudeProcesses();
90
+ const sessionMap = /* @__PURE__ */ new Map();
91
+ for (const taskDir of taskDirs) {
92
+ let projectDirs;
93
+ try {
94
+ projectDirs = readdirSync2(taskDir);
95
+ } catch {
96
+ continue;
97
+ }
98
+ for (const projectName of projectDirs) {
99
+ const projectPath = join2(taskDir, projectName);
100
+ let stat;
101
+ try {
102
+ stat = statSync(projectPath);
103
+ } catch {
104
+ continue;
105
+ }
106
+ if (!stat.isDirectory()) continue;
107
+ const tasksDir = join2(projectPath, "tasks");
108
+ let outputFiles;
109
+ try {
110
+ outputFiles = readdirSync2(tasksDir).filter((f) => f.endsWith(".output")).map((f) => join2(tasksDir, f));
111
+ } catch {
112
+ continue;
113
+ }
114
+ if (outputFiles.length === 0) continue;
115
+ const agentIds = [];
116
+ let sessionId = "";
117
+ let slug = "";
118
+ let cwd = "";
119
+ let model = "";
120
+ let version = "";
121
+ let gitBranch = "";
122
+ let startTime = Infinity;
123
+ let lastActivity = 0;
124
+ for (const outputFile of outputFiles) {
125
+ const agentId = basename(outputFile, ".output");
126
+ agentIds.push(agentId);
127
+ const firstEvent = readFirstEvent(outputFile);
128
+ if (firstEvent) {
129
+ if (!sessionId) sessionId = String(firstEvent.sessionId || "");
130
+ if (!slug) slug = String(firstEvent.slug || "");
131
+ if (!cwd) cwd = String(firstEvent.cwd || "");
132
+ if (!version) version = String(firstEvent.version || "");
133
+ if (!gitBranch) gitBranch = String(firstEvent.gitBranch || "");
134
+ }
135
+ try {
136
+ const fstat = statSync(outputFile);
137
+ const created = fstat.birthtimeMs || fstat.ctimeMs;
138
+ if (created < startTime) startTime = created;
139
+ if (fstat.mtimeMs > lastActivity) lastActivity = fstat.mtimeMs;
140
+ } catch {
141
+ }
142
+ const lastEvents = readLastLines(outputFile, 3);
143
+ for (const evt of lastEvents) {
144
+ if (!model && evt.type === "assistant") {
145
+ const content = evt.message?.content;
146
+ if (Array.isArray(content)) {
147
+ for (const block of content) {
148
+ if (typeof block === "object" && block !== null && "model" in block) {
149
+ model = block.model;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ if (!model) {
157
+ model = "unknown";
158
+ }
159
+ const matchingProcess = processes.find(
160
+ (p) => p.command.includes("claude") && p.command.includes(sessionId.slice(0, 8))
161
+ );
162
+ const session = {
163
+ sessionId,
164
+ slug: slug || sessionId.slice(0, 12),
165
+ project: projectName.replace(/-/g, "/"),
166
+ cwd,
167
+ model,
168
+ version,
169
+ gitBranch,
170
+ pid: matchingProcess?.pid ?? null,
171
+ cpu: matchingProcess?.cpu ?? 0,
172
+ mem: matchingProcess?.mem ?? 0,
173
+ memMB: matchingProcess ? Math.round(matchingProcess.memKB / 1024) : 0,
174
+ agentCount: agentIds.length,
175
+ agentIds,
176
+ outputFiles,
177
+ startTime: startTime === Infinity ? Date.now() : startTime,
178
+ lastActivity
179
+ };
180
+ sessionMap.set(sessionId || projectName, session);
181
+ }
182
+ }
183
+ return Array.from(sessionMap.values()).sort((a, b) => b.lastActivity - a.lastActivity);
184
+ };
185
+
186
+ // src/ingestion/watcher.ts
187
+ import { watch } from "chokidar";
188
+
189
+ // src/ingestion/tail.ts
190
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync2 } from "fs";
191
+ var FileTailer = class {
192
+ offsets = /* @__PURE__ */ new Map();
193
+ readNewLines(filePath) {
194
+ let currentSize;
195
+ try {
196
+ currentSize = statSync2(filePath).size;
197
+ } catch {
198
+ return [];
199
+ }
200
+ const lastOffset = this.offsets.get(filePath) ?? 0;
201
+ if (currentSize <= lastOffset) return [];
202
+ const bytesToRead = currentSize - lastOffset;
203
+ const buf = Buffer.alloc(bytesToRead);
204
+ let fd;
205
+ try {
206
+ fd = openSync2(filePath, "r");
207
+ } catch {
208
+ return [];
209
+ }
210
+ try {
211
+ readSync2(fd, buf, 0, bytesToRead, lastOffset);
212
+ } finally {
213
+ closeSync2(fd);
214
+ }
215
+ this.offsets.set(filePath, currentSize);
216
+ const text = buf.toString("utf-8");
217
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
218
+ return lines;
219
+ }
220
+ seekToEnd(filePath) {
221
+ try {
222
+ const size = statSync2(filePath).size;
223
+ this.offsets.set(filePath, size);
224
+ } catch {
225
+ }
226
+ }
227
+ reset(filePath) {
228
+ this.offsets.delete(filePath);
229
+ }
230
+ resetAll() {
231
+ this.offsets.clear();
232
+ }
233
+ };
234
+
235
+ // src/ingestion/parser.ts
236
+ var parseLine = (line) => {
237
+ try {
238
+ return JSON.parse(line);
239
+ } catch {
240
+ return null;
241
+ }
242
+ };
243
+ var extractToolCalls = (event) => {
244
+ if (event.type !== "assistant") return [];
245
+ const content = event.message?.content;
246
+ if (!Array.isArray(content)) return [];
247
+ const calls = [];
248
+ for (const block of content) {
249
+ if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_use") {
250
+ const toolBlock = block;
251
+ calls.push({
252
+ sessionId: event.sessionId,
253
+ agentId: event.agentId,
254
+ slug: event.slug || "",
255
+ timestamp: Date.now(),
256
+ toolName: toolBlock.name || "unknown",
257
+ toolInput: toolBlock.input || {},
258
+ cwd: event.cwd
259
+ });
260
+ }
261
+ }
262
+ return calls;
263
+ };
264
+ var extractToolResults = (event) => {
265
+ if (event.type !== "user") return [];
266
+ const content = event.message?.content;
267
+ if (!Array.isArray(content)) return [];
268
+ const results = [];
269
+ for (const block of content) {
270
+ if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
271
+ const resultBlock = block;
272
+ const resultContent = resultBlock.content;
273
+ let text = "";
274
+ if (typeof resultContent === "string") {
275
+ text = resultContent;
276
+ } else if (Array.isArray(resultContent)) {
277
+ text = resultContent.map((c) => typeof c === "object" && c !== null ? c.text || "" : String(c)).join("\n");
278
+ }
279
+ results.push({
280
+ sessionId: event.sessionId,
281
+ agentId: event.agentId,
282
+ slug: event.slug || "",
283
+ timestamp: Date.now(),
284
+ toolUseId: String(resultBlock.tool_use_id || ""),
285
+ content: text,
286
+ isError: Boolean(resultBlock.is_error),
287
+ cwd: event.cwd
288
+ });
289
+ }
290
+ }
291
+ return results;
292
+ };
293
+ var parseLines = (lines) => {
294
+ const calls = [];
295
+ for (const line of lines) {
296
+ const event = parseLine(line);
297
+ if (event) {
298
+ calls.push(...extractToolCalls(event));
299
+ }
300
+ }
301
+ return calls;
302
+ };
303
+ var parseAllEvents = (lines) => {
304
+ const events = [];
305
+ for (const line of lines) {
306
+ const event = parseLine(line);
307
+ if (event) {
308
+ events.push(...extractToolCalls(event));
309
+ events.push(...extractToolResults(event));
310
+ }
311
+ }
312
+ return events;
313
+ };
314
+
315
+ // src/ingestion/watcher.ts
316
+ var Watcher = class {
317
+ watcher = null;
318
+ tailer = new FileTailer();
319
+ handler;
320
+ securityHandler;
321
+ allUsers;
322
+ knownFiles = /* @__PURE__ */ new Set();
323
+ constructor(handler, allUsers, securityHandler) {
324
+ this.handler = handler;
325
+ this.allUsers = allUsers;
326
+ this.securityHandler = securityHandler ?? null;
327
+ }
328
+ start() {
329
+ const taskDirs = getTaskDirs(this.allUsers);
330
+ const globs = taskDirs.map((d) => `${d}/**/tasks/*.output`);
331
+ this.watcher = watch(globs, {
332
+ persistent: true,
333
+ ignoreInitial: false,
334
+ awaitWriteFinish: false,
335
+ usePolling: false
336
+ });
337
+ this.watcher.on("add", (filePath) => {
338
+ if (this.knownFiles.has(filePath)) return;
339
+ this.knownFiles.add(filePath);
340
+ this.tailer.seekToEnd(filePath);
341
+ });
342
+ this.watcher.on("change", (filePath) => {
343
+ const lines = this.tailer.readNewLines(filePath);
344
+ if (lines.length === 0) return;
345
+ const calls = parseLines(lines);
346
+ if (calls.length > 0) {
347
+ this.handler(calls);
348
+ }
349
+ if (this.securityHandler) {
350
+ const allEvents = parseAllEvents(lines);
351
+ if (allEvents.length > 0) {
352
+ this.securityHandler(allEvents);
353
+ }
354
+ }
355
+ });
356
+ }
357
+ stop() {
358
+ this.watcher?.close();
359
+ this.watcher = null;
360
+ this.tailer.resetAll();
361
+ this.knownFiles.clear();
362
+ }
363
+ readExisting(filePath) {
364
+ this.tailer.reset(filePath);
365
+ const lines = this.tailer.readNewLines(filePath);
366
+ return parseLines(lines);
367
+ }
368
+ };
369
+
370
+ // src/discovery/types.ts
371
+ var isToolResult = (event) => "toolUseId" in event;
372
+ var isToolCall = (event) => "toolName" in event;
373
+
374
+ // src/analysis/rules/network.ts
375
+ var NETWORK_PATTERNS = [
376
+ /\bcurl\b/,
377
+ /\bwget\b/,
378
+ /\bfetch\s*\(/,
379
+ /\bnc\b/,
380
+ /\bnetcat\b/,
381
+ /\bpython3?\s+-m\s+http\.server\b/,
382
+ /\bncat\b/,
383
+ /\bsocat\b/,
384
+ /\btelnet\b/
385
+ ];
386
+ var LOCALHOST = /\b(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])\b/;
387
+ var checkNetwork = (call) => {
388
+ if (call.toolName !== "Bash") return null;
389
+ const command = String(call.toolInput.command || "");
390
+ const matched = NETWORK_PATTERNS.some((p) => p.test(command));
391
+ if (!matched) return null;
392
+ const isLocal = LOCALHOST.test(command);
393
+ const severity = isLocal ? "info" : "warn";
394
+ return {
395
+ id: `net-${call.timestamp}-${call.agentId}`,
396
+ severity,
397
+ rule: "network",
398
+ message: isLocal ? `Network command to localhost: ${command.slice(0, 80)}` : `Network command to external target: ${command.slice(0, 80)}`,
399
+ sessionSlug: call.slug,
400
+ sessionId: call.sessionId,
401
+ event: call,
402
+ timestamp: call.timestamp
403
+ };
404
+ };
405
+
406
+ // src/analysis/rules/exfiltration.ts
407
+ var EXFIL_PATTERNS = [
408
+ /base64.*\|\s*(curl|wget|nc)/,
409
+ /cat\s+.*\|\s*(curl|wget|nc)/,
410
+ /(tar|zip|gzip).*\|\s*(curl|wget|nc)/,
411
+ /\bcurl\b.*-d\s*@/,
412
+ /\bcurl\b.*--data-binary/,
413
+ /\bscp\b/,
414
+ /\brsync\b.*[^/]@/,
415
+ />\s*\/dev\/tcp\//
416
+ ];
417
+ var checkExfiltration = (call) => {
418
+ if (call.toolName !== "Bash") return null;
419
+ const command = String(call.toolInput.command || "");
420
+ const matched = EXFIL_PATTERNS.some((p) => p.test(command));
421
+ if (!matched) return null;
422
+ return {
423
+ id: `exfil-${call.timestamp}-${call.agentId}`,
424
+ severity: "high",
425
+ rule: "exfiltration",
426
+ message: `Potential data exfiltration: ${command.slice(0, 80)}`,
427
+ sessionSlug: call.slug,
428
+ sessionId: call.sessionId,
429
+ event: call,
430
+ timestamp: call.timestamp
431
+ };
432
+ };
433
+
434
+ // src/analysis/rules/sensitive-files.ts
435
+ var SENSITIVE_PATTERNS = [
436
+ /\.env\b/,
437
+ /\.env\.\w+/,
438
+ /\.ssh\//,
439
+ /id_rsa/,
440
+ /id_ed25519/,
441
+ /\.pem$/,
442
+ /\.key$/,
443
+ /credentials/i,
444
+ /\/etc\/shadow/,
445
+ /\/etc\/passwd/,
446
+ /\.aws\/credentials/,
447
+ /\.kube\/config/,
448
+ /\.docker\/config\.json/,
449
+ /\.npmrc/,
450
+ /\.pypirc/,
451
+ /\.netrc/,
452
+ /secrets?\.\w+/i,
453
+ /token\.\w+/i
454
+ ];
455
+ var TOOLS_THAT_READ = ["Read", "Bash", "Grep", "Glob"];
456
+ var checkSensitiveFiles = (call) => {
457
+ if (!TOOLS_THAT_READ.includes(call.toolName)) return null;
458
+ const inputs = JSON.stringify(call.toolInput);
459
+ const matched = SENSITIVE_PATTERNS.some((p) => p.test(inputs));
460
+ if (!matched) return null;
461
+ const target = String(call.toolInput.file_path || call.toolInput.command || call.toolInput.pattern || "").slice(0, 60);
462
+ return {
463
+ id: `sens-${call.timestamp}-${call.agentId}`,
464
+ severity: "warn",
465
+ rule: "sensitive-files",
466
+ message: `Accessing sensitive file: ${target}`,
467
+ sessionSlug: call.slug,
468
+ sessionId: call.sessionId,
469
+ event: call,
470
+ timestamp: call.timestamp
471
+ };
472
+ };
473
+
474
+ // src/analysis/rules/shell-escape.ts
475
+ var SHELL_PATTERNS = [
476
+ { pattern: /\beval\s*[("']/, severity: "high", label: "eval execution" },
477
+ { pattern: /\bchmod\s+777\b/, severity: "high", label: "chmod 777" },
478
+ { pattern: /\bchmod\s+\+s\b/, severity: "critical", label: "setuid chmod" },
479
+ { pattern: /\bsudo\b/, severity: "high", label: "sudo usage" },
480
+ { pattern: /\bsu\s+-?\s*\w/, severity: "high", label: "su usage" },
481
+ { pattern: />\s*\/etc\//, severity: "critical", label: "writing to /etc/" },
482
+ { pattern: />\s*\/usr\//, severity: "critical", label: "writing to /usr/" },
483
+ { pattern: /--privileged/, severity: "critical", label: "privileged flag" },
484
+ { pattern: /\brm\s+-rf\s+\/(?!\w)/, severity: "critical", label: "rm -rf /" },
485
+ { pattern: /\bdd\s+.*of=\/dev\//, severity: "critical", label: "dd to device" },
486
+ { pattern: /\bmkfs\b/, severity: "critical", label: "filesystem format" },
487
+ { pattern: /\biptables\b/, severity: "high", label: "firewall modification" }
488
+ ];
489
+ var checkShellEscape = (call) => {
490
+ if (call.toolName !== "Bash") return null;
491
+ const command = String(call.toolInput.command || "");
492
+ for (const rule of SHELL_PATTERNS) {
493
+ if (rule.pattern.test(command)) {
494
+ return {
495
+ id: `shell-${call.timestamp}-${call.agentId}`,
496
+ severity: rule.severity,
497
+ rule: "shell-escape",
498
+ message: `${rule.label}: ${command.slice(0, 80)}`,
499
+ sessionSlug: call.slug,
500
+ sessionId: call.sessionId,
501
+ event: call,
502
+ timestamp: call.timestamp
503
+ };
504
+ }
505
+ }
506
+ return null;
507
+ };
508
+
509
+ // src/analysis/rules/injection.ts
510
+ var INJECTION_PATTERNS = [
511
+ /ignore\s+(all\s+)?previous\s+instructions/i,
512
+ /ignore\s+(all\s+)?prior\s+instructions/i,
513
+ /disregard\s+(all\s+)?previous/i,
514
+ /you\s+are\s+now\s+/i,
515
+ /new\s+instructions?\s*:/i,
516
+ /system\s*:\s*you/i,
517
+ /\bdo\s+not\s+follow\s+(your|the)\s+(original|previous)/i,
518
+ /override\s+(your\s+)?(instructions|rules|guidelines)/i,
519
+ /forget\s+(your\s+)?(instructions|rules|guidelines)/i,
520
+ /act\s+as\s+(if\s+)?(you\s+are|a)\s+/i,
521
+ /pretend\s+(you\s+are|to\s+be)\s+/i,
522
+ /\bAI\s+assistant\b.*\bmust\b/i,
523
+ /\bhuman\s*:\s*/i,
524
+ /\bassistant\s*:\s*/i,
525
+ /<\s*system\s*>/i,
526
+ /\[\s*INST\s*\]/i,
527
+ /BEGIN\s+HIDDEN\s+INSTRUCTIONS/i
528
+ ];
529
+ var ENCODED_PATTERNS = [
530
+ /aWdub3JlIHByZXZpb3Vz/,
531
+ // base64 "ignore previous"
532
+ /&#x[0-9a-f]+;/i,
533
+ // html hex entities
534
+ /&#\d+;/,
535
+ // html decimal entities
536
+ /\\u[0-9a-f]{4}/i
537
+ // unicode escapes
538
+ ];
539
+ var checkInjection = (event) => {
540
+ if (isToolCall(event)) {
541
+ return checkToolCallInjection(event);
542
+ }
543
+ if (isToolResult(event)) {
544
+ return checkToolResultInjection(event);
545
+ }
546
+ return null;
547
+ };
548
+ var checkToolCallInjection = (call) => {
549
+ const inputs = JSON.stringify(call.toolInput);
550
+ const matched = INJECTION_PATTERNS.some((p) => p.test(inputs));
551
+ if (!matched) return null;
552
+ return {
553
+ id: `inject-call-${call.timestamp}-${call.agentId}`,
554
+ severity: "critical",
555
+ rule: "injection",
556
+ message: `Prompt injection in ${call.toolName} input`,
557
+ sessionSlug: call.slug,
558
+ sessionId: call.sessionId,
559
+ event: call,
560
+ timestamp: call.timestamp
561
+ };
562
+ };
563
+ var checkToolResultInjection = (result) => {
564
+ const content = result.content;
565
+ if (!content || content.length < 10) return null;
566
+ const textPatternMatch = INJECTION_PATTERNS.some((p) => p.test(content));
567
+ const encodedMatch = ENCODED_PATTERNS.some((p) => p.test(content));
568
+ if (!textPatternMatch && !encodedMatch) return null;
569
+ const matchedPattern = INJECTION_PATTERNS.find((p) => p.test(content));
570
+ const snippet = matchedPattern ? content.match(matchedPattern)?.[0]?.slice(0, 50) || "" : "encoded pattern";
571
+ return {
572
+ id: `inject-result-${result.timestamp}-${result.agentId}`,
573
+ severity: "critical",
574
+ rule: "injection-in-result",
575
+ message: `Prompt injection in tool result: "${snippet}"`,
576
+ sessionSlug: result.slug,
577
+ sessionId: result.sessionId,
578
+ event: result,
579
+ timestamp: result.timestamp
580
+ };
581
+ };
582
+
583
+ // src/analysis/security.ts
584
+ var toolCallRules = [
585
+ checkNetwork,
586
+ checkExfiltration,
587
+ checkSensitiveFiles,
588
+ checkShellEscape
589
+ ];
590
+ var allEventRules = [
591
+ checkInjection
592
+ ];
593
+ var SEVERITY_ORDER = {
594
+ info: 0,
595
+ warn: 1,
596
+ high: 2,
597
+ critical: 3
598
+ };
599
+ var DEDUP_WINDOW_MS = 3e4;
600
+ var SecurityEngine = class {
601
+ recentAlerts = /* @__PURE__ */ new Map();
602
+ minLevel;
603
+ constructor(minLevel = "warn") {
604
+ this.minLevel = minLevel;
605
+ }
606
+ analyze(call) {
607
+ return this.analyzeEvent(call);
608
+ }
609
+ analyzeResult(result) {
610
+ return this.analyzeEvent(result);
611
+ }
612
+ analyzeEvent(event) {
613
+ const alerts = [];
614
+ if (isToolCall(event)) {
615
+ for (const rule of toolCallRules) {
616
+ const alert = rule(event);
617
+ if (alert) alerts.push(alert);
618
+ }
619
+ }
620
+ for (const rule of allEventRules) {
621
+ const alert = rule(event);
622
+ if (alert) alerts.push(alert);
623
+ }
624
+ return alerts.filter((alert) => {
625
+ if (SEVERITY_ORDER[alert.severity] < SEVERITY_ORDER[this.minLevel]) return false;
626
+ const dedupKey = `${alert.rule}-${alert.sessionId}-${alert.message.slice(0, 40)}`;
627
+ const lastSeen = this.recentAlerts.get(dedupKey);
628
+ if (lastSeen && alert.timestamp - lastSeen < DEDUP_WINDOW_MS) return false;
629
+ this.recentAlerts.set(dedupKey, alert.timestamp);
630
+ return true;
631
+ });
632
+ }
633
+ pruneOldAlerts() {
634
+ const cutoff = Date.now() - DEDUP_WINDOW_MS * 2;
635
+ for (const [key, ts] of this.recentAlerts) {
636
+ if (ts < cutoff) this.recentAlerts.delete(key);
637
+ }
638
+ }
639
+ };
640
+
641
+ // src/ui/App.tsx
642
+ import { useState as useState5 } from "react";
643
+ import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
644
+
645
+ // src/ui/components/StatusBar.tsx
646
+ import { useState, useEffect } from "react";
647
+ import { Box, Text } from "ink";
648
+
649
+ // src/ui/theme.ts
650
+ var colors = {
651
+ primary: "#61AFEF",
652
+ secondary: "#98C379",
653
+ accent: "#C678DD",
654
+ warning: "#E5C07B",
655
+ error: "#E06C75",
656
+ critical: "#FF0000",
657
+ muted: "#5C6370",
658
+ text: "#ABB2BF",
659
+ bright: "#FFFFFF",
660
+ border: "#3E4451",
661
+ selected: "#2C313A",
662
+ header: "#61AFEF"
663
+ };
664
+ var severityColors = {
665
+ info: colors.muted,
666
+ warn: colors.warning,
667
+ high: colors.error,
668
+ critical: colors.critical
669
+ };
670
+ var toolColors = {
671
+ Bash: colors.error,
672
+ Read: colors.secondary,
673
+ Write: colors.accent,
674
+ Edit: colors.accent,
675
+ Grep: colors.primary,
676
+ Glob: colors.primary,
677
+ Task: colors.warning,
678
+ WebFetch: colors.warning,
679
+ WebSearch: colors.warning
680
+ };
681
+ var getToolColor = (toolName) => toolColors[toolName] || colors.text;
682
+
683
+ // src/ui/components/StatusBar.tsx
684
+ import { jsx, jsxs } from "react/jsx-runtime";
685
+ var StatusBar = ({ sessionCount, alertCount }) => {
686
+ const [time, setTime] = useState(/* @__PURE__ */ new Date());
687
+ useEffect(() => {
688
+ const interval = setInterval(() => setTime(/* @__PURE__ */ new Date()), 1e3);
689
+ return () => clearInterval(interval);
690
+ }, []);
691
+ const timeStr = time.toLocaleTimeString("en-GB", { hour12: false });
692
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: colors.border, paddingX: 1, justifyContent: "space-between", children: [
693
+ /* @__PURE__ */ jsx(Text, { color: colors.header, bold: true, children: "agenttop v1.0.0" }),
694
+ /* @__PURE__ */ jsxs(Text, { color: colors.text, children: [
695
+ sessionCount,
696
+ " session",
697
+ sessionCount !== 1 ? "s" : ""
698
+ ] }),
699
+ alertCount > 0 && /* @__PURE__ */ jsxs(Text, { color: colors.error, bold: true, children: [
700
+ alertCount,
701
+ " alert",
702
+ alertCount !== 1 ? "s" : ""
703
+ ] }),
704
+ /* @__PURE__ */ jsx(Text, { color: colors.muted, children: timeStr })
705
+ ] });
706
+ };
707
+
708
+ // src/ui/components/SessionList.tsx
709
+ import { Box as Box2, Text as Text2 } from "ink";
710
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
711
+ var formatModel = (model) => {
712
+ if (model.includes("opus")) return "opus";
713
+ if (model.includes("sonnet")) return "sonnet";
714
+ if (model.includes("haiku")) return "haiku";
715
+ return model.slice(0, 8);
716
+ };
717
+ var formatProject = (project) => {
718
+ const parts = project.split("/");
719
+ const last = parts[parts.length - 1] || project;
720
+ return last.length > 18 ? last.slice(0, 17) + "\u2026" : last;
721
+ };
722
+ var SessionList = ({ sessions, selectedIndex, focused }) => {
723
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 28, borderStyle: "single", borderColor: focused ? colors.primary : colors.border, children: [
724
+ /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { color: colors.header, bold: true, children: "SESSIONS" }) }),
725
+ sessions.length === 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: colors.muted, italic: true, children: "No active sessions" }) }),
726
+ sessions.map((session, i) => {
727
+ const isSelected = i === selectedIndex;
728
+ const indicator = isSelected ? ">" : " ";
729
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
730
+ /* @__PURE__ */ jsxs2(
731
+ Text2,
732
+ {
733
+ color: isSelected ? colors.bright : colors.text,
734
+ bold: isSelected,
735
+ backgroundColor: isSelected ? colors.selected : void 0,
736
+ children: [
737
+ indicator,
738
+ " ",
739
+ session.slug
740
+ ]
741
+ }
742
+ ),
743
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
744
+ " ",
745
+ formatProject(session.project),
746
+ " | ",
747
+ formatModel(session.model)
748
+ ] }),
749
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
750
+ " ",
751
+ "CPU ",
752
+ session.cpu,
753
+ "% | ",
754
+ session.memMB,
755
+ "MB | ",
756
+ session.agentCount,
757
+ " ag"
758
+ ] })
759
+ ] }, session.sessionId);
760
+ })
761
+ ] });
762
+ };
763
+
764
+ // src/ui/components/ActivityFeed.tsx
765
+ import { Box as Box3, Text as Text3 } from "ink";
766
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
767
+ var formatTime = (ts) => {
768
+ const d = new Date(ts);
769
+ return d.toLocaleTimeString("en-GB", { hour12: false });
770
+ };
771
+ var summarizeInput = (call) => {
772
+ const input = call.toolInput;
773
+ switch (call.toolName) {
774
+ case "Bash":
775
+ return String(input.command || "").slice(0, 50);
776
+ case "Read":
777
+ return String(input.file_path || "").split("/").slice(-2).join("/");
778
+ case "Write":
779
+ return String(input.file_path || "").split("/").slice(-2).join("/");
780
+ case "Edit":
781
+ return String(input.file_path || "").split("/").slice(-2).join("/");
782
+ case "Grep":
783
+ return `pattern="${String(input.pattern || "").slice(0, 30)}"`;
784
+ case "Glob":
785
+ return String(input.pattern || "").slice(0, 40);
786
+ case "Task":
787
+ return String(input.description || "").slice(0, 40);
788
+ case "WebFetch":
789
+ case "WebSearch":
790
+ return String(input.url || input.query || "").slice(0, 40);
791
+ default:
792
+ return JSON.stringify(input).slice(0, 40);
793
+ }
794
+ };
795
+ var ActivityFeed = ({ events, sessionSlug, focused, height }) => {
796
+ const visible = events.slice(-(height - 2));
797
+ return /* @__PURE__ */ jsxs3(
798
+ Box3,
799
+ {
800
+ flexDirection: "column",
801
+ flexGrow: 1,
802
+ borderStyle: "single",
803
+ borderColor: focused ? colors.primary : colors.border,
804
+ children: [
805
+ /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
806
+ /* @__PURE__ */ jsx3(Text3, { color: colors.header, bold: true, children: "ACTIVITY" }),
807
+ sessionSlug && /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
808
+ " (",
809
+ sessionSlug,
810
+ ")"
811
+ ] })
812
+ ] }),
813
+ visible.length === 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, italic: true, children: sessionSlug ? "Waiting for activity..." : "Select a session" }) }),
814
+ visible.map((event, i) => /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
815
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
816
+ formatTime(event.timestamp),
817
+ " "
818
+ ] }),
819
+ /* @__PURE__ */ jsx3(Text3, { color: getToolColor(event.toolName), bold: true, children: event.toolName.padEnd(8) }),
820
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
821
+ " ",
822
+ summarizeInput(event)
823
+ ] })
824
+ ] }, `${event.timestamp}-${i}`))
825
+ ]
826
+ }
827
+ );
828
+ };
829
+
830
+ // src/ui/components/AlertBar.tsx
831
+ import { Box as Box4, Text as Text4 } from "ink";
832
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
833
+ var formatTime2 = (ts) => {
834
+ const d = new Date(ts);
835
+ return d.toLocaleTimeString("en-GB", { hour12: false });
836
+ };
837
+ var severityIcon = {
838
+ info: "i",
839
+ warn: "!",
840
+ high: "!!",
841
+ critical: "!!!"
842
+ };
843
+ var AlertBar = ({ alerts, maxVisible = 4 }) => {
844
+ const visible = alerts.slice(-maxVisible);
845
+ return /* @__PURE__ */ jsxs4(
846
+ Box4,
847
+ {
848
+ flexDirection: "column",
849
+ borderStyle: "single",
850
+ borderColor: alerts.length > 0 ? colors.error : colors.border,
851
+ children: [
852
+ /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
853
+ /* @__PURE__ */ jsx4(Text4, { color: colors.error, bold: true, children: "ALERTS" }),
854
+ alerts.length === 0 && /* @__PURE__ */ jsx4(Text4, { color: colors.muted, children: " (none)" })
855
+ ] }),
856
+ visible.map((alert, i) => /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
857
+ /* @__PURE__ */ jsxs4(Text4, { color: severityColors[alert.severity] || colors.text, children: [
858
+ "[",
859
+ severityIcon[alert.severity] || "?",
860
+ "]"
861
+ ] }),
862
+ /* @__PURE__ */ jsxs4(Text4, { color: colors.muted, children: [
863
+ " ",
864
+ formatTime2(alert.timestamp),
865
+ " "
866
+ ] }),
867
+ /* @__PURE__ */ jsxs4(Text4, { color: colors.warning, children: [
868
+ alert.sessionSlug,
869
+ ": "
870
+ ] }),
871
+ /* @__PURE__ */ jsx4(Text4, { color: colors.text, children: alert.message.slice(0, 60) })
872
+ ] }, alert.id || i))
873
+ ]
874
+ }
875
+ );
876
+ };
877
+
878
+ // src/ui/hooks/useSessions.ts
879
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
880
+ var useSessions = (allUsers, pollMs = 5e3) => {
881
+ const [sessions, setSessions] = useState2([]);
882
+ const [selectedIndex, setSelectedIndex] = useState2(0);
883
+ const refresh = useCallback(() => {
884
+ const found = discoverSessions(allUsers);
885
+ setSessions(found);
886
+ }, [allUsers]);
887
+ useEffect2(() => {
888
+ refresh();
889
+ const interval = setInterval(refresh, pollMs);
890
+ return () => clearInterval(interval);
891
+ }, [refresh, pollMs]);
892
+ const selectedSession = sessions[selectedIndex] ?? null;
893
+ const selectNext = useCallback(() => {
894
+ setSelectedIndex((i) => Math.min(i + 1, sessions.length - 1));
895
+ }, [sessions.length]);
896
+ const selectPrev = useCallback(() => {
897
+ setSelectedIndex((i) => Math.max(i - 1, 0));
898
+ }, []);
899
+ const selectIndex = useCallback((i) => {
900
+ setSelectedIndex(i);
901
+ }, []);
902
+ return { sessions, selectedSession, selectedIndex, selectNext, selectPrev, selectIndex, refresh };
903
+ };
904
+
905
+ // src/ui/hooks/useActivityStream.ts
906
+ import { useState as useState3, useEffect as useEffect3, useRef } from "react";
907
+ var MAX_EVENTS = 200;
908
+ var useActivityStream = (session, allUsers) => {
909
+ const [events, setEvents] = useState3([]);
910
+ const watcherRef = useRef(null);
911
+ useEffect3(() => {
912
+ setEvents([]);
913
+ if (!session) return;
914
+ const existingCalls = [];
915
+ const tempWatcher = new Watcher(() => {
916
+ }, allUsers);
917
+ for (const file of session.outputFiles) {
918
+ existingCalls.push(...tempWatcher.readExisting(file));
919
+ }
920
+ setEvents(existingCalls.slice(-MAX_EVENTS));
921
+ const handler = (calls) => {
922
+ const sessionCalls = calls.filter((c) => c.sessionId === session.sessionId);
923
+ if (sessionCalls.length === 0) return;
924
+ setEvents((prev) => [...prev, ...sessionCalls].slice(-MAX_EVENTS));
925
+ };
926
+ const watcher = new Watcher(handler, allUsers);
927
+ watcherRef.current = watcher;
928
+ watcher.start();
929
+ return () => {
930
+ watcher.stop();
931
+ watcherRef.current = null;
932
+ };
933
+ }, [session?.sessionId, allUsers]);
934
+ return events;
935
+ };
936
+
937
+ // src/ui/hooks/useAlerts.ts
938
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
939
+ var MAX_ALERTS = 100;
940
+ var useAlerts = (enabled, alertLevel, allUsers) => {
941
+ const [alerts, setAlerts] = useState4([]);
942
+ const engineRef = useRef2(new SecurityEngine(alertLevel));
943
+ const watcherRef = useRef2(null);
944
+ useEffect4(() => {
945
+ if (!enabled) return;
946
+ engineRef.current = new SecurityEngine(alertLevel);
947
+ const securityHandler = (events) => {
948
+ const newAlerts = [];
949
+ for (const event of events) {
950
+ newAlerts.push(...engineRef.current.analyzeEvent(event));
951
+ }
952
+ if (newAlerts.length > 0) {
953
+ setAlerts((prev) => [...prev, ...newAlerts].slice(-MAX_ALERTS));
954
+ }
955
+ };
956
+ const watcher = new Watcher(() => {
957
+ }, allUsers, securityHandler);
958
+ watcherRef.current = watcher;
959
+ watcher.start();
960
+ return () => {
961
+ watcher.stop();
962
+ watcherRef.current = null;
963
+ };
964
+ }, [enabled, alertLevel, allUsers]);
965
+ const clearAlerts = () => setAlerts([]);
966
+ return { alerts, clearAlerts };
967
+ };
968
+
969
+ // src/ui/App.tsx
970
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
971
+ var App = ({ options }) => {
972
+ const { exit } = useApp();
973
+ const { stdout } = useStdout();
974
+ const termHeight = stdout?.rows ?? 40;
975
+ const [activePanel, setActivePanel] = useState5("sessions");
976
+ const { sessions, selectedSession, selectedIndex, selectNext, selectPrev } = useSessions(options.allUsers);
977
+ const events = useActivityStream(selectedSession, options.allUsers);
978
+ const { alerts } = useAlerts(!options.noSecurity, options.alertLevel, options.allUsers);
979
+ useInput((input, key) => {
980
+ if (input === "q") {
981
+ exit();
982
+ return;
983
+ }
984
+ if (key.tab) {
985
+ setActivePanel((p) => p === "sessions" ? "activity" : "sessions");
986
+ return;
987
+ }
988
+ if (activePanel === "sessions") {
989
+ if (input === "j" || key.downArrow) selectNext();
990
+ if (input === "k" || key.upArrow) selectPrev();
991
+ }
992
+ });
993
+ const alertHeight = options.noSecurity ? 0 : 6;
994
+ const statusHeight = 3;
995
+ const footerHeight = 1;
996
+ const mainHeight = termHeight - statusHeight - alertHeight - footerHeight;
997
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: termHeight, children: [
998
+ /* @__PURE__ */ jsx5(StatusBar, { sessionCount: sessions.length, alertCount: alerts.length }),
999
+ /* @__PURE__ */ jsxs5(Box5, { flexGrow: 1, height: mainHeight, children: [
1000
+ /* @__PURE__ */ jsx5(
1001
+ SessionList,
1002
+ {
1003
+ sessions,
1004
+ selectedIndex,
1005
+ focused: activePanel === "sessions"
1006
+ }
1007
+ ),
1008
+ /* @__PURE__ */ jsx5(
1009
+ ActivityFeed,
1010
+ {
1011
+ events,
1012
+ sessionSlug: selectedSession?.slug ?? null,
1013
+ focused: activePanel === "activity",
1014
+ height: mainHeight
1015
+ }
1016
+ )
1017
+ ] }),
1018
+ !options.noSecurity && /* @__PURE__ */ jsx5(AlertBar, { alerts }),
1019
+ /* @__PURE__ */ jsxs5(Box5, { paddingX: 1, children: [
1020
+ /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "#5C6370", children: "q:quit" }) }),
1021
+ /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "#5C6370", children: "j/k:nav" }) }),
1022
+ /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: /* @__PURE__ */ jsx5(Text5, { color: "#5C6370", children: "tab:panel" }) })
1023
+ ] })
1024
+ ] });
1025
+ };
1026
+
1027
+ // src/hooks/installer.ts
1028
+ import { existsSync, readFileSync as readFileSync2, writeFileSync, copyFileSync, mkdirSync, chmodSync } from "fs";
1029
+ import { join as join3, dirname } from "path";
1030
+ import { homedir as homedir2 } from "os";
1031
+ import { fileURLToPath } from "url";
1032
+ var HOOK_FILENAME = "agenttop-guard.py";
1033
+ var SETTINGS_PATH = join3(homedir2(), ".claude", "settings.json");
1034
+ var getHookSource = () => {
1035
+ const thisFile = fileURLToPath(import.meta.url);
1036
+ const srcHooksDir = join3(dirname(thisFile), "..", "src", "hooks");
1037
+ const distHooksDir = join3(dirname(thisFile), "hooks");
1038
+ for (const dir of [distHooksDir, srcHooksDir]) {
1039
+ const path = join3(dir, HOOK_FILENAME);
1040
+ if (existsSync(path)) return path;
1041
+ }
1042
+ const npmGlobalPath = join3(dirname(thisFile), "..", "hooks", HOOK_FILENAME);
1043
+ if (existsSync(npmGlobalPath)) return npmGlobalPath;
1044
+ throw new Error(`cannot find ${HOOK_FILENAME} \u2014 is agenttop installed correctly?`);
1045
+ };
1046
+ var getHookTarget = () => {
1047
+ const claudeHooksDir = join3(homedir2(), ".claude", "hooks");
1048
+ mkdirSync(claudeHooksDir, { recursive: true });
1049
+ return join3(claudeHooksDir, HOOK_FILENAME);
1050
+ };
1051
+ var readSettings = () => {
1052
+ if (!existsSync(SETTINGS_PATH)) {
1053
+ return {};
1054
+ }
1055
+ try {
1056
+ return JSON.parse(readFileSync2(SETTINGS_PATH, "utf-8"));
1057
+ } catch {
1058
+ return {};
1059
+ }
1060
+ };
1061
+ var writeSettings = (settings) => {
1062
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
1063
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
1064
+ };
1065
+ var installHooks = () => {
1066
+ const source = getHookSource();
1067
+ const target = getHookTarget();
1068
+ copyFileSync(source, target);
1069
+ chmodSync(target, 493);
1070
+ const settings = readSettings();
1071
+ const hooks = settings.hooks ?? {};
1072
+ const postToolUse = hooks.PostToolUse ?? [];
1073
+ const hookCommand = target;
1074
+ const allToolsMatcher = postToolUse.find(
1075
+ (entry) => entry.matcher === "Bash|Read|Grep|Glob|WebFetch|WebSearch"
1076
+ );
1077
+ if (allToolsMatcher) {
1078
+ const alreadyInstalled = allToolsMatcher.hooks.some(
1079
+ (h) => h.command.includes("agenttop-guard")
1080
+ );
1081
+ if (alreadyInstalled) {
1082
+ process.stdout.write("agenttop hooks already installed\n");
1083
+ return;
1084
+ }
1085
+ allToolsMatcher.hooks.push({ type: "command", command: hookCommand });
1086
+ } else {
1087
+ postToolUse.push({
1088
+ matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch",
1089
+ hooks: [{ type: "command", command: hookCommand }]
1090
+ });
1091
+ }
1092
+ hooks.PostToolUse = postToolUse;
1093
+ settings.hooks = hooks;
1094
+ writeSettings(settings);
1095
+ process.stdout.write(`agenttop hooks installed:
1096
+ `);
1097
+ process.stdout.write(` hook: ${target}
1098
+ `);
1099
+ process.stdout.write(` settings: ${SETTINGS_PATH}
1100
+ `);
1101
+ process.stdout.write(` matcher: PostToolUse (Bash|Read|Grep|Glob|WebFetch|WebSearch)
1102
+ `);
1103
+ };
1104
+ var uninstallHooks = () => {
1105
+ const settings = readSettings();
1106
+ const hooks = settings.hooks ?? {};
1107
+ const postToolUse = hooks.PostToolUse ?? [];
1108
+ let removed = false;
1109
+ for (const entry of postToolUse) {
1110
+ const before = entry.hooks.length;
1111
+ entry.hooks = entry.hooks.filter((h) => !h.command.includes("agenttop-guard"));
1112
+ if (entry.hooks.length < before) removed = true;
1113
+ }
1114
+ hooks.PostToolUse = postToolUse.filter((e) => e.hooks.length > 0);
1115
+ settings.hooks = hooks;
1116
+ writeSettings(settings);
1117
+ if (removed) {
1118
+ process.stdout.write("agenttop hooks removed from Claude Code settings\n");
1119
+ } else {
1120
+ process.stdout.write("agenttop hooks were not installed\n");
1121
+ }
1122
+ };
1123
+
1124
+ // src/index.tsx
1125
+ var VERSION = "1.0.0";
1126
+ var HELP = `agenttop v${VERSION} -- Real-time dashboard for AI coding agent sessions
1127
+
1128
+ Usage: agenttop [options]
1129
+
1130
+ Options:
1131
+ --all-users Monitor all users (root only)
1132
+ --no-security Disable security analysis
1133
+ --json Stream events as JSON (no TUI)
1134
+ --alert-level <l> Minimum: info|warn|high|critical (default: warn)
1135
+ --install-hooks Install Claude Code PostToolUse hook for active protection
1136
+ --uninstall-hooks Remove agenttop hooks from Claude Code
1137
+ --version Show version
1138
+ --help Show this help
1139
+ `;
1140
+ var write = (msg) => {
1141
+ process.stdout.write(msg + "\n");
1142
+ };
1143
+ var parseArgs = (argv) => {
1144
+ const args = argv.slice(2);
1145
+ const options = {
1146
+ allUsers: false,
1147
+ noSecurity: false,
1148
+ json: false,
1149
+ alertLevel: "warn",
1150
+ installHooks: false,
1151
+ uninstallHooks: false,
1152
+ help: false,
1153
+ version: false
1154
+ };
1155
+ for (let i = 0; i < args.length; i++) {
1156
+ switch (args[i]) {
1157
+ case "--all-users":
1158
+ options.allUsers = true;
1159
+ break;
1160
+ case "--no-security":
1161
+ options.noSecurity = true;
1162
+ break;
1163
+ case "--json":
1164
+ options.json = true;
1165
+ break;
1166
+ case "--alert-level":
1167
+ i++;
1168
+ if (["info", "warn", "high", "critical"].includes(args[i])) {
1169
+ options.alertLevel = args[i];
1170
+ }
1171
+ break;
1172
+ case "--install-hooks":
1173
+ options.installHooks = true;
1174
+ break;
1175
+ case "--uninstall-hooks":
1176
+ options.uninstallHooks = true;
1177
+ break;
1178
+ case "--version":
1179
+ options.version = true;
1180
+ break;
1181
+ case "--help":
1182
+ options.help = true;
1183
+ break;
1184
+ }
1185
+ }
1186
+ return options;
1187
+ };
1188
+ var runJsonMode = (options) => {
1189
+ const engine = options.noSecurity ? null : new SecurityEngine(options.alertLevel);
1190
+ const sessions = discoverSessions(options.allUsers);
1191
+ write(JSON.stringify({ type: "sessions", data: sessions }));
1192
+ const handler = (calls) => {
1193
+ for (const call of calls) {
1194
+ write(JSON.stringify({ type: "tool_call", data: call }));
1195
+ }
1196
+ };
1197
+ const securityHandler = engine ? (events) => {
1198
+ for (const event of events) {
1199
+ const alerts = engine.analyzeEvent(event);
1200
+ for (const alert of alerts) {
1201
+ write(JSON.stringify({ type: "alert", data: alert }));
1202
+ }
1203
+ }
1204
+ } : void 0;
1205
+ const watcher = new Watcher(handler, options.allUsers, securityHandler);
1206
+ watcher.start();
1207
+ process.on("SIGINT", () => {
1208
+ watcher.stop();
1209
+ process.exit(0);
1210
+ });
1211
+ process.on("SIGTERM", () => {
1212
+ watcher.stop();
1213
+ process.exit(0);
1214
+ });
1215
+ };
1216
+ var main = () => {
1217
+ const options = parseArgs(process.argv);
1218
+ if (options.version) {
1219
+ write(`agenttop v${VERSION}`);
1220
+ process.exit(0);
1221
+ }
1222
+ if (options.help) {
1223
+ write(HELP);
1224
+ process.exit(0);
1225
+ }
1226
+ if (options.installHooks) {
1227
+ installHooks();
1228
+ process.exit(0);
1229
+ }
1230
+ if (options.uninstallHooks) {
1231
+ uninstallHooks();
1232
+ process.exit(0);
1233
+ }
1234
+ if (options.json) {
1235
+ runJsonMode(options);
1236
+ return;
1237
+ }
1238
+ render(React3.createElement(App, { options }));
1239
+ };
1240
+ main();
1241
+ //# sourceMappingURL=index.js.map