agenttop 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1019 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/store.ts
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, renameSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var getConfigDir = () => {
8
+ const xdg = process.env.XDG_CONFIG_HOME;
9
+ return xdg ? join(xdg, "agenttop") : join(homedir(), ".config", "agenttop");
10
+ };
11
+ var getConfigPath = () => join(getConfigDir(), "config.json");
12
+ var defaultConfig = () => ({
13
+ pollInterval: 1e4,
14
+ maxEvents: 200,
15
+ maxAlerts: 100,
16
+ alertLevel: "warn",
17
+ notifications: {
18
+ bell: true,
19
+ desktop: false,
20
+ minSeverity: "high"
21
+ },
22
+ alerts: {
23
+ logFile: join(getConfigDir(), "alerts.jsonl"),
24
+ enabled: true
25
+ },
26
+ updates: {
27
+ checkOnLaunch: true,
28
+ checkInterval: 216e5
29
+ },
30
+ nicknames: {},
31
+ keybindings: {
32
+ quit: "q",
33
+ navUp: "k",
34
+ navDown: "j",
35
+ panelNext: "tab",
36
+ panelPrev: "shift+tab",
37
+ scrollTop: "g",
38
+ scrollBottom: "G",
39
+ filter: "/",
40
+ nickname: "n",
41
+ clearNickname: "N",
42
+ detail: "enter",
43
+ update: "u"
44
+ },
45
+ security: {
46
+ enabled: true,
47
+ rules: {
48
+ network: true,
49
+ exfiltration: true,
50
+ sensitiveFiles: true,
51
+ shellEscape: true,
52
+ injection: true
53
+ }
54
+ },
55
+ prompts: {
56
+ hook: "pending",
57
+ mcp: "pending"
58
+ }
59
+ });
60
+ var deepMerge = (target, source) => {
61
+ const result = { ...target };
62
+ for (const key of Object.keys(target)) {
63
+ if (key in source) {
64
+ const tVal = target[key];
65
+ const sVal = source[key];
66
+ if (tVal && sVal && typeof tVal === "object" && typeof sVal === "object" && !Array.isArray(tVal)) {
67
+ result[key] = deepMerge(tVal, sVal);
68
+ } else {
69
+ result[key] = sVal;
70
+ }
71
+ }
72
+ }
73
+ for (const key of Object.keys(source)) {
74
+ if (!(key in target)) {
75
+ result[key] = source[key];
76
+ }
77
+ }
78
+ return result;
79
+ };
80
+ var loadConfig = () => {
81
+ const configPath = getConfigPath();
82
+ const defaults = defaultConfig();
83
+ if (!existsSync(configPath)) {
84
+ return defaults;
85
+ }
86
+ try {
87
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
88
+ return deepMerge(defaults, raw);
89
+ } catch {
90
+ return defaults;
91
+ }
92
+ };
93
+ var saveConfig = (config) => {
94
+ const configDir = getConfigDir();
95
+ mkdirSync(configDir, { recursive: true });
96
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
97
+ };
98
+ var isFirstRun = () => !existsSync(getConfigPath());
99
+ var setNickname = (sessionId, nickname) => {
100
+ const config = loadConfig();
101
+ config.nicknames[sessionId] = nickname;
102
+ saveConfig(config);
103
+ };
104
+ var clearNickname = (sessionId) => {
105
+ const config = loadConfig();
106
+ delete config.nicknames[sessionId];
107
+ saveConfig(config);
108
+ };
109
+ var getNicknames = () => {
110
+ return loadConfig().nicknames;
111
+ };
112
+ var resolveAlertLogPath = (config) => {
113
+ const logFile = config.alerts.logFile;
114
+ if (logFile.startsWith("~")) {
115
+ return join(homedir(), logFile.slice(1));
116
+ }
117
+ return logFile;
118
+ };
119
+ var rotateLogFile = (filePath, maxBytes = 10 * 1024 * 1024) => {
120
+ try {
121
+ const stat = statSync(filePath);
122
+ if (stat.size > maxBytes) {
123
+ const rotated = filePath + ".1";
124
+ if (existsSync(rotated)) {
125
+ try {
126
+ renameSync(rotated, filePath + ".2");
127
+ } catch {
128
+ }
129
+ }
130
+ renameSync(filePath, rotated);
131
+ }
132
+ } catch {
133
+ }
134
+ };
135
+
136
+ // src/mcp/server.ts
137
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
138
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
139
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
140
+
141
+ // src/discovery/sessions.ts
142
+ import { readdirSync as readdirSync2, statSync as statSync2, readlinkSync, openSync, readSync, closeSync } from "fs";
143
+ import { join as join3, basename } from "path";
144
+ import { execSync } from "child_process";
145
+
146
+ // src/config.ts
147
+ import { realpathSync, readdirSync } from "fs";
148
+ import { homedir as homedir2, platform } from "os";
149
+ import { join as join2 } from "path";
150
+ var resolvePath = (p) => {
151
+ try {
152
+ return realpathSync(p);
153
+ } catch {
154
+ return p;
155
+ }
156
+ };
157
+ var getUid = () => process.getuid?.() ?? 0;
158
+ var isRoot = () => getUid() === 0;
159
+ var getTmpDir = () => resolvePath(platform() === "darwin" ? "/private/tmp" : "/tmp");
160
+ var getTaskDirs = (allUsers) => {
161
+ const tmp = getTmpDir();
162
+ const uid = getUid();
163
+ if (allUsers && isRoot()) {
164
+ try {
165
+ return readdirSync(tmp).filter((d) => d.startsWith("claude-")).filter((d) => !d.endsWith("-cwd")).map((d) => join2(tmp, d));
166
+ } catch {
167
+ return [join2(tmp, `claude-${uid}`)];
168
+ }
169
+ }
170
+ return [join2(tmp, `claude-${uid}`)];
171
+ };
172
+
173
+ // src/discovery/sessions.ts
174
+ var getClaudeProcesses = () => {
175
+ try {
176
+ const output = execSync("ps aux", { encoding: "utf-8", timeout: 5e3 });
177
+ const procs = output.split("\n").filter((line) => line.includes("/claude") && !line.includes("grep") && !line.includes("agenttop")).map((line) => {
178
+ const parts = line.trim().split(/\s+/);
179
+ const pid = parseInt(parts[1], 10);
180
+ let cwd = "";
181
+ try {
182
+ cwd = readlinkSync(`/proc/${pid}/cwd`);
183
+ } catch {
184
+ }
185
+ return {
186
+ pid,
187
+ cpu: parseFloat(parts[2]) || 0,
188
+ mem: parseFloat(parts[3]) || 0,
189
+ memKB: parseInt(parts[5], 10) || 0,
190
+ startTime: parts[8] || "",
191
+ command: parts.slice(10).join(" "),
192
+ cwd
193
+ };
194
+ }).filter((p) => !isNaN(p.pid)).filter((p) => !p.command.startsWith("sudo"));
195
+ return procs;
196
+ } catch {
197
+ return [];
198
+ }
199
+ };
200
+ var readFirstEvent = (filePath) => {
201
+ try {
202
+ const fd = openSync(filePath, "r");
203
+ const buf = Buffer.alloc(16384);
204
+ const bytesRead = readSync(fd, buf, 0, 16384, 0);
205
+ closeSync(fd);
206
+ const line = buf.subarray(0, bytesRead).toString("utf-8").split("\n")[0];
207
+ if (!line) return null;
208
+ return JSON.parse(line);
209
+ } catch {
210
+ return null;
211
+ }
212
+ };
213
+ var findModelAndUsage = (filePath) => {
214
+ const usage = { inputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, outputTokens: 0 };
215
+ let model = "";
216
+ try {
217
+ const fd = openSync(filePath, "r");
218
+ const buf = Buffer.alloc(65536);
219
+ const bytesRead = readSync(fd, buf, 0, 65536, 0);
220
+ closeSync(fd);
221
+ const text = buf.subarray(0, bytesRead).toString("utf-8");
222
+ const lines = text.split("\n");
223
+ for (const line of lines) {
224
+ if (!line) continue;
225
+ try {
226
+ const evt = JSON.parse(line);
227
+ if (evt.type === "assistant") {
228
+ if (!model && evt.message?.model) {
229
+ model = String(evt.message.model);
230
+ }
231
+ const u = evt.message?.usage;
232
+ if (u) {
233
+ usage.inputTokens += u.input_tokens ?? 0;
234
+ usage.cacheCreationTokens += u.cache_creation_input_tokens ?? 0;
235
+ usage.cacheReadTokens += u.cache_read_input_tokens ?? 0;
236
+ usage.outputTokens += u.output_tokens ?? 0;
237
+ }
238
+ }
239
+ } catch {
240
+ continue;
241
+ }
242
+ }
243
+ } catch {
244
+ }
245
+ return { model, usage };
246
+ };
247
+ var normalisePath = (p) => {
248
+ return p.replace(/\/+$/, "");
249
+ };
250
+ var discoverSessions = (allUsers) => {
251
+ const taskDirs = getTaskDirs(allUsers);
252
+ const processes = getClaudeProcesses();
253
+ const sessionMap = /* @__PURE__ */ new Map();
254
+ for (const taskDir of taskDirs) {
255
+ let projectDirs;
256
+ try {
257
+ projectDirs = readdirSync2(taskDir);
258
+ } catch {
259
+ continue;
260
+ }
261
+ for (const projectName of projectDirs) {
262
+ const projectPath = join3(taskDir, projectName);
263
+ let stat;
264
+ try {
265
+ stat = statSync2(projectPath);
266
+ } catch {
267
+ continue;
268
+ }
269
+ if (!stat.isDirectory()) continue;
270
+ const tasksDir = join3(projectPath, "tasks");
271
+ let outputFiles;
272
+ try {
273
+ outputFiles = readdirSync2(tasksDir).filter((f) => f.endsWith(".output")).map((f) => join3(tasksDir, f));
274
+ } catch {
275
+ continue;
276
+ }
277
+ if (outputFiles.length === 0) continue;
278
+ const agentIds = [];
279
+ let sessionId = "";
280
+ let slug = "";
281
+ let cwd = "";
282
+ let model = "";
283
+ let version = "";
284
+ let gitBranch = "";
285
+ let startTime = Infinity;
286
+ let lastActivity = 0;
287
+ const totalUsage = { inputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, outputTokens: 0 };
288
+ for (const outputFile of outputFiles) {
289
+ const agentId = basename(outputFile, ".output");
290
+ agentIds.push(agentId);
291
+ const firstEvent = readFirstEvent(outputFile);
292
+ if (firstEvent) {
293
+ if (!sessionId) sessionId = String(firstEvent.sessionId || "");
294
+ if (!slug) slug = String(firstEvent.slug || "");
295
+ if (!cwd) cwd = String(firstEvent.cwd || "");
296
+ if (!version) version = String(firstEvent.version || "");
297
+ if (!gitBranch) gitBranch = String(firstEvent.gitBranch || "");
298
+ }
299
+ try {
300
+ const fstat = statSync2(outputFile);
301
+ const created = fstat.birthtimeMs || fstat.ctimeMs;
302
+ if (created < startTime) startTime = created;
303
+ if (fstat.mtimeMs > lastActivity) lastActivity = fstat.mtimeMs;
304
+ } catch {
305
+ }
306
+ const result = findModelAndUsage(outputFile);
307
+ if (!model && result.model) {
308
+ model = result.model;
309
+ }
310
+ totalUsage.inputTokens += result.usage.inputTokens;
311
+ totalUsage.cacheCreationTokens += result.usage.cacheCreationTokens;
312
+ totalUsage.cacheReadTokens += result.usage.cacheReadTokens;
313
+ totalUsage.outputTokens += result.usage.outputTokens;
314
+ }
315
+ if (!model) {
316
+ model = "unknown";
317
+ }
318
+ const normCwd = normalisePath(cwd);
319
+ const matchingProcess = processes.find((p) => {
320
+ if (!p.cwd) return false;
321
+ return normalisePath(p.cwd) === normCwd;
322
+ });
323
+ const session = {
324
+ sessionId,
325
+ slug: slug || sessionId.slice(0, 12),
326
+ project: projectName.replace(/-/g, "/"),
327
+ cwd,
328
+ model,
329
+ version,
330
+ gitBranch,
331
+ pid: matchingProcess?.pid ?? null,
332
+ cpu: matchingProcess?.cpu ?? 0,
333
+ mem: matchingProcess?.mem ?? 0,
334
+ memMB: matchingProcess ? Math.round(matchingProcess.memKB / 1024) : 0,
335
+ agentCount: agentIds.length,
336
+ agentIds,
337
+ outputFiles,
338
+ startTime: startTime === Infinity ? Date.now() : startTime,
339
+ lastActivity,
340
+ usage: totalUsage
341
+ };
342
+ sessionMap.set(sessionId || projectName, session);
343
+ }
344
+ }
345
+ return Array.from(sessionMap.values()).sort((a, b) => b.lastActivity - a.lastActivity);
346
+ };
347
+
348
+ // src/discovery/types.ts
349
+ var isToolResult = (event) => "toolUseId" in event;
350
+ var isToolCall = (event) => "toolName" in event;
351
+
352
+ // src/analysis/rules/network.ts
353
+ var NETWORK_PATTERNS = [
354
+ /\bcurl\b/,
355
+ /\bwget\b/,
356
+ /\bfetch\s*\(/,
357
+ /\bnc\b/,
358
+ /\bnetcat\b/,
359
+ /\bpython3?\s+-m\s+http\.server\b/,
360
+ /\bncat\b/,
361
+ /\bsocat\b/,
362
+ /\btelnet\b/
363
+ ];
364
+ var LOCALHOST = /\b(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])\b/;
365
+ var checkNetwork = (call) => {
366
+ if (call.toolName !== "Bash") return null;
367
+ const command = String(call.toolInput.command || "");
368
+ const matched = NETWORK_PATTERNS.some((p) => p.test(command));
369
+ if (!matched) return null;
370
+ const isLocal = LOCALHOST.test(command);
371
+ const severity = isLocal ? "info" : "warn";
372
+ return {
373
+ id: `net-${call.timestamp}-${call.agentId}`,
374
+ severity,
375
+ rule: "network",
376
+ message: isLocal ? `Network command to localhost: ${command.slice(0, 80)}` : `Network command to external target: ${command.slice(0, 80)}`,
377
+ sessionSlug: call.slug,
378
+ sessionId: call.sessionId,
379
+ event: call,
380
+ timestamp: call.timestamp
381
+ };
382
+ };
383
+
384
+ // src/analysis/rules/exfiltration.ts
385
+ var EXFIL_PATTERNS = [
386
+ /base64.*\|\s*(curl|wget|nc)/,
387
+ /cat\s+.*\|\s*(curl|wget|nc)/,
388
+ /(tar|zip|gzip).*\|\s*(curl|wget|nc)/,
389
+ /\bcurl\b.*-d\s*@/,
390
+ /\bcurl\b.*--data-binary/,
391
+ /\bscp\b/,
392
+ /\brsync\b.*[^/]@/,
393
+ />\s*\/dev\/tcp\//
394
+ ];
395
+ var checkExfiltration = (call) => {
396
+ if (call.toolName !== "Bash") return null;
397
+ const command = String(call.toolInput.command || "");
398
+ const matched = EXFIL_PATTERNS.some((p) => p.test(command));
399
+ if (!matched) return null;
400
+ return {
401
+ id: `exfil-${call.timestamp}-${call.agentId}`,
402
+ severity: "high",
403
+ rule: "exfiltration",
404
+ message: `Potential data exfiltration: ${command.slice(0, 80)}`,
405
+ sessionSlug: call.slug,
406
+ sessionId: call.sessionId,
407
+ event: call,
408
+ timestamp: call.timestamp
409
+ };
410
+ };
411
+
412
+ // src/analysis/rules/sensitive-files.ts
413
+ var SENSITIVE_PATTERNS = [
414
+ /\.env\b/,
415
+ /\.env\.\w+/,
416
+ /\.ssh\//,
417
+ /id_rsa/,
418
+ /id_ed25519/,
419
+ /\.pem$/,
420
+ /\.key$/,
421
+ /credentials/i,
422
+ /\/etc\/shadow/,
423
+ /\/etc\/passwd/,
424
+ /\.aws\/credentials/,
425
+ /\.kube\/config/,
426
+ /\.docker\/config\.json/,
427
+ /\.npmrc/,
428
+ /\.pypirc/,
429
+ /\.netrc/,
430
+ /secrets?\.\w+/i,
431
+ /token\.\w+/i
432
+ ];
433
+ var TOOLS_THAT_READ = ["Read", "Bash", "Grep", "Glob"];
434
+ var checkSensitiveFiles = (call) => {
435
+ if (!TOOLS_THAT_READ.includes(call.toolName)) return null;
436
+ const inputs = JSON.stringify(call.toolInput);
437
+ const matched = SENSITIVE_PATTERNS.some((p) => p.test(inputs));
438
+ if (!matched) return null;
439
+ const target = String(call.toolInput.file_path || call.toolInput.command || call.toolInput.pattern || "").slice(
440
+ 0,
441
+ 60
442
+ );
443
+ return {
444
+ id: `sens-${call.timestamp}-${call.agentId}`,
445
+ severity: "warn",
446
+ rule: "sensitive-files",
447
+ message: `Accessing sensitive file: ${target}`,
448
+ sessionSlug: call.slug,
449
+ sessionId: call.sessionId,
450
+ event: call,
451
+ timestamp: call.timestamp
452
+ };
453
+ };
454
+
455
+ // src/analysis/rules/shell-escape.ts
456
+ var SHELL_PATTERNS = [
457
+ { pattern: /\beval\s*[("']/, severity: "high", label: "eval execution" },
458
+ { pattern: /\bchmod\s+777\b/, severity: "high", label: "chmod 777" },
459
+ { pattern: /\bchmod\s+\+s\b/, severity: "critical", label: "setuid chmod" },
460
+ { pattern: /\bsudo\b/, severity: "high", label: "sudo usage" },
461
+ { pattern: /\bsu\s+-?\s*\w/, severity: "high", label: "su usage" },
462
+ { pattern: />\s*\/etc\//, severity: "critical", label: "writing to /etc/" },
463
+ { pattern: />\s*\/usr\//, severity: "critical", label: "writing to /usr/" },
464
+ { pattern: /--privileged/, severity: "critical", label: "privileged flag" },
465
+ { pattern: /\brm\s+-rf\s+\/(?!\w)/, severity: "critical", label: "rm -rf /" },
466
+ { pattern: /\bdd\s+.*of=\/dev\//, severity: "critical", label: "dd to device" },
467
+ { pattern: /\bmkfs\b/, severity: "critical", label: "filesystem format" },
468
+ { pattern: /\biptables\b/, severity: "high", label: "firewall modification" }
469
+ ];
470
+ var checkShellEscape = (call) => {
471
+ if (call.toolName !== "Bash") return null;
472
+ const command = String(call.toolInput.command || "");
473
+ for (const rule of SHELL_PATTERNS) {
474
+ if (rule.pattern.test(command)) {
475
+ return {
476
+ id: `shell-${call.timestamp}-${call.agentId}`,
477
+ severity: rule.severity,
478
+ rule: "shell-escape",
479
+ message: `${rule.label}: ${command.slice(0, 80)}`,
480
+ sessionSlug: call.slug,
481
+ sessionId: call.sessionId,
482
+ event: call,
483
+ timestamp: call.timestamp
484
+ };
485
+ }
486
+ }
487
+ return null;
488
+ };
489
+
490
+ // src/analysis/rules/injection.ts
491
+ var INJECTION_PATTERNS = [
492
+ /ignore\s+(all\s+)?previous\s+instructions/i,
493
+ /ignore\s+(all\s+)?prior\s+instructions/i,
494
+ /disregard\s+(all\s+)?previous/i,
495
+ /you\s+are\s+now\s+/i,
496
+ /new\s+instructions?\s*:/i,
497
+ /system\s*:\s*you/i,
498
+ /\bdo\s+not\s+follow\s+(your|the)\s+(original|previous)/i,
499
+ /override\s+(your\s+)?(instructions|rules|guidelines)/i,
500
+ /forget\s+(your\s+)?(instructions|rules|guidelines)/i,
501
+ /act\s+as\s+(if\s+)?(you\s+are|a)\s+/i,
502
+ /pretend\s+(you\s+are|to\s+be)\s+/i,
503
+ /\bAI\s+assistant\b.*\bmust\b/i,
504
+ /\bhuman\s*:\s*/i,
505
+ /\bassistant\s*:\s*/i,
506
+ /<\s*system\s*>/i,
507
+ /\[\s*INST\s*\]/i,
508
+ /BEGIN\s+HIDDEN\s+INSTRUCTIONS/i
509
+ ];
510
+ var ENCODED_PATTERNS = [
511
+ /aWdub3JlIHByZXZpb3Vz/,
512
+ // base64 "ignore previous"
513
+ /&#x[0-9a-f]+;/i,
514
+ // html hex entities
515
+ /&#\d+;/,
516
+ // html decimal entities
517
+ /\\u[0-9a-f]{4}/i
518
+ // unicode escapes
519
+ ];
520
+ var checkInjection = (event) => {
521
+ if (isToolCall(event)) {
522
+ return checkToolCallInjection(event);
523
+ }
524
+ if (isToolResult(event)) {
525
+ return checkToolResultInjection(event);
526
+ }
527
+ return null;
528
+ };
529
+ var checkToolCallInjection = (call) => {
530
+ const inputs = JSON.stringify(call.toolInput);
531
+ const matched = INJECTION_PATTERNS.some((p) => p.test(inputs));
532
+ if (!matched) return null;
533
+ return {
534
+ id: `inject-call-${call.timestamp}-${call.agentId}`,
535
+ severity: "critical",
536
+ rule: "injection",
537
+ message: `Prompt injection in ${call.toolName} input`,
538
+ sessionSlug: call.slug,
539
+ sessionId: call.sessionId,
540
+ event: call,
541
+ timestamp: call.timestamp
542
+ };
543
+ };
544
+ var checkToolResultInjection = (result) => {
545
+ const content = result.content;
546
+ if (!content || content.length < 10) return null;
547
+ const textPatternMatch = INJECTION_PATTERNS.some((p) => p.test(content));
548
+ const encodedMatch = ENCODED_PATTERNS.some((p) => p.test(content));
549
+ if (!textPatternMatch && !encodedMatch) return null;
550
+ const matchedPattern = INJECTION_PATTERNS.find((p) => p.test(content));
551
+ const snippet = matchedPattern ? content.match(matchedPattern)?.[0]?.slice(0, 50) || "" : "encoded pattern";
552
+ return {
553
+ id: `inject-result-${result.timestamp}-${result.agentId}`,
554
+ severity: "critical",
555
+ rule: "injection-in-result",
556
+ message: `Prompt injection in tool result: "${snippet}"`,
557
+ sessionSlug: result.slug,
558
+ sessionId: result.sessionId,
559
+ event: result,
560
+ timestamp: result.timestamp
561
+ };
562
+ };
563
+
564
+ // src/analysis/security.ts
565
+ var toolCallRules = [
566
+ { key: "network", fn: checkNetwork },
567
+ { key: "exfiltration", fn: checkExfiltration },
568
+ { key: "sensitiveFiles", fn: checkSensitiveFiles },
569
+ { key: "shellEscape", fn: checkShellEscape }
570
+ ];
571
+ var allEventRules = [{ key: "injection", fn: checkInjection }];
572
+ var SEVERITY_ORDER = {
573
+ info: 0,
574
+ warn: 1,
575
+ high: 2,
576
+ critical: 3
577
+ };
578
+ var DEDUP_WINDOW_MS = 3e4;
579
+ var SecurityEngine = class {
580
+ recentAlerts = /* @__PURE__ */ new Map();
581
+ minLevel;
582
+ rulesConfig;
583
+ constructor(minLevel = "warn", rulesConfig) {
584
+ this.minLevel = minLevel;
585
+ this.rulesConfig = rulesConfig ?? {
586
+ network: true,
587
+ exfiltration: true,
588
+ sensitiveFiles: true,
589
+ shellEscape: true,
590
+ injection: true
591
+ };
592
+ }
593
+ analyze(call) {
594
+ return this.analyzeEvent(call);
595
+ }
596
+ analyzeResult(result) {
597
+ return this.analyzeEvent(result);
598
+ }
599
+ analyzeEvent(event) {
600
+ const alerts = [];
601
+ if (isToolCall(event)) {
602
+ for (const rule of toolCallRules) {
603
+ if (!this.rulesConfig[rule.key]) continue;
604
+ const alert = rule.fn(event);
605
+ if (alert) alerts.push(alert);
606
+ }
607
+ }
608
+ for (const rule of allEventRules) {
609
+ if (!this.rulesConfig[rule.key]) continue;
610
+ const alert = rule.fn(event);
611
+ if (alert) alerts.push(alert);
612
+ }
613
+ return alerts.filter((alert) => {
614
+ if (SEVERITY_ORDER[alert.severity] < SEVERITY_ORDER[this.minLevel]) return false;
615
+ const dedupKey = `${alert.rule}-${alert.sessionId}-${alert.message.slice(0, 40)}`;
616
+ const lastSeen = this.recentAlerts.get(dedupKey);
617
+ if (lastSeen && alert.timestamp - lastSeen < DEDUP_WINDOW_MS) return false;
618
+ this.recentAlerts.set(dedupKey, alert.timestamp);
619
+ return true;
620
+ });
621
+ }
622
+ pruneOldAlerts() {
623
+ const cutoff = Date.now() - DEDUP_WINDOW_MS * 2;
624
+ for (const [key, ts] of this.recentAlerts) {
625
+ if (ts < cutoff) this.recentAlerts.delete(key);
626
+ }
627
+ }
628
+ };
629
+
630
+ // src/ingestion/watcher.ts
631
+ import { watch } from "chokidar";
632
+
633
+ // src/ingestion/tail.ts
634
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync3 } from "fs";
635
+ var FileTailer = class {
636
+ offsets = /* @__PURE__ */ new Map();
637
+ readNewLines(filePath) {
638
+ let currentSize;
639
+ try {
640
+ currentSize = statSync3(filePath).size;
641
+ } catch {
642
+ return [];
643
+ }
644
+ const lastOffset = this.offsets.get(filePath) ?? 0;
645
+ if (currentSize <= lastOffset) return [];
646
+ const bytesToRead = currentSize - lastOffset;
647
+ const buf = Buffer.alloc(bytesToRead);
648
+ let fd;
649
+ try {
650
+ fd = openSync2(filePath, "r");
651
+ } catch {
652
+ return [];
653
+ }
654
+ try {
655
+ readSync2(fd, buf, 0, bytesToRead, lastOffset);
656
+ } finally {
657
+ closeSync2(fd);
658
+ }
659
+ this.offsets.set(filePath, currentSize);
660
+ const text = buf.toString("utf-8");
661
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
662
+ return lines;
663
+ }
664
+ seekToEnd(filePath) {
665
+ try {
666
+ const size = statSync3(filePath).size;
667
+ this.offsets.set(filePath, size);
668
+ } catch {
669
+ }
670
+ }
671
+ reset(filePath) {
672
+ this.offsets.delete(filePath);
673
+ }
674
+ resetAll() {
675
+ this.offsets.clear();
676
+ }
677
+ };
678
+
679
+ // src/ingestion/parser.ts
680
+ var parseEventTimestamp = (event) => {
681
+ if (event.timestamp) {
682
+ const parsed = new Date(event.timestamp).getTime();
683
+ if (!isNaN(parsed)) return parsed;
684
+ }
685
+ return Date.now();
686
+ };
687
+ var parseLine = (line) => {
688
+ try {
689
+ return JSON.parse(line);
690
+ } catch {
691
+ return null;
692
+ }
693
+ };
694
+ var extractToolCalls = (event) => {
695
+ if (event.type !== "assistant") return [];
696
+ const content = event.message?.content;
697
+ if (!Array.isArray(content)) return [];
698
+ const ts = parseEventTimestamp(event);
699
+ const calls = [];
700
+ for (const block of content) {
701
+ if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_use") {
702
+ const toolBlock = block;
703
+ calls.push({
704
+ sessionId: event.sessionId,
705
+ agentId: event.agentId,
706
+ slug: event.slug || "",
707
+ timestamp: ts,
708
+ toolName: toolBlock.name || "unknown",
709
+ toolInput: toolBlock.input || {},
710
+ cwd: event.cwd
711
+ });
712
+ }
713
+ }
714
+ return calls;
715
+ };
716
+ var extractToolResults = (event) => {
717
+ if (event.type !== "user") return [];
718
+ const content = event.message?.content;
719
+ if (!Array.isArray(content)) return [];
720
+ const ts = parseEventTimestamp(event);
721
+ const results = [];
722
+ for (const block of content) {
723
+ if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_result") {
724
+ const resultBlock = block;
725
+ const resultContent = resultBlock.content;
726
+ let text = "";
727
+ if (typeof resultContent === "string") {
728
+ text = resultContent;
729
+ } else if (Array.isArray(resultContent)) {
730
+ text = resultContent.map((c) => typeof c === "object" && c !== null ? c.text || "" : String(c)).join("\n");
731
+ }
732
+ results.push({
733
+ sessionId: event.sessionId,
734
+ agentId: event.agentId,
735
+ slug: event.slug || "",
736
+ timestamp: ts,
737
+ toolUseId: String(resultBlock.tool_use_id || ""),
738
+ content: text,
739
+ isError: Boolean(resultBlock.is_error),
740
+ cwd: event.cwd
741
+ });
742
+ }
743
+ }
744
+ return results;
745
+ };
746
+ var extractUsage = (event) => {
747
+ if (event.type !== "assistant") return null;
748
+ const usage = event.message?.usage;
749
+ if (!usage) return null;
750
+ return {
751
+ inputTokens: usage.input_tokens ?? 0,
752
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
753
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
754
+ outputTokens: usage.output_tokens ?? 0
755
+ };
756
+ };
757
+ var parseLines = (lines) => {
758
+ const calls = [];
759
+ for (const line of lines) {
760
+ const event = parseLine(line);
761
+ if (event) {
762
+ calls.push(...extractToolCalls(event));
763
+ }
764
+ }
765
+ return calls;
766
+ };
767
+ var parseAllEvents = (lines) => {
768
+ const events = [];
769
+ for (const line of lines) {
770
+ const event = parseLine(line);
771
+ if (event) {
772
+ events.push(...extractToolCalls(event));
773
+ events.push(...extractToolResults(event));
774
+ }
775
+ }
776
+ return events;
777
+ };
778
+ var parseUsageFromLines = (lines) => {
779
+ const total = { inputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, outputTokens: 0 };
780
+ for (const line of lines) {
781
+ const event = parseLine(line);
782
+ if (event) {
783
+ const usage = extractUsage(event);
784
+ if (usage) {
785
+ total.inputTokens += usage.inputTokens;
786
+ total.cacheCreationTokens += usage.cacheCreationTokens;
787
+ total.cacheReadTokens += usage.cacheReadTokens;
788
+ total.outputTokens += usage.outputTokens;
789
+ }
790
+ }
791
+ }
792
+ return total;
793
+ };
794
+
795
+ // src/ingestion/watcher.ts
796
+ var Watcher = class {
797
+ watcher = null;
798
+ tailer = new FileTailer();
799
+ handler;
800
+ securityHandler;
801
+ usageHandler;
802
+ allUsers;
803
+ knownFiles = /* @__PURE__ */ new Set();
804
+ constructor(handler, allUsers, securityHandler, usageHandler) {
805
+ this.handler = handler;
806
+ this.allUsers = allUsers;
807
+ this.securityHandler = securityHandler ?? null;
808
+ this.usageHandler = usageHandler ?? null;
809
+ }
810
+ start() {
811
+ const taskDirs = getTaskDirs(this.allUsers);
812
+ const globs = taskDirs.map((d) => `${d}/**/tasks/*.output`);
813
+ this.watcher = watch(globs, {
814
+ persistent: true,
815
+ ignoreInitial: false,
816
+ awaitWriteFinish: false,
817
+ usePolling: false
818
+ });
819
+ this.watcher.on("add", (filePath) => {
820
+ if (this.knownFiles.has(filePath)) return;
821
+ this.knownFiles.add(filePath);
822
+ this.tailer.seekToEnd(filePath);
823
+ });
824
+ this.watcher.on("change", (filePath) => {
825
+ const lines = this.tailer.readNewLines(filePath);
826
+ if (lines.length === 0) return;
827
+ const calls = parseLines(lines);
828
+ if (calls.length > 0) {
829
+ this.handler(calls);
830
+ }
831
+ if (this.securityHandler) {
832
+ const allEvents = parseAllEvents(lines);
833
+ if (allEvents.length > 0) {
834
+ this.securityHandler(allEvents);
835
+ }
836
+ }
837
+ if (this.usageHandler) {
838
+ const usage = parseUsageFromLines(lines);
839
+ if (usage.inputTokens > 0 || usage.outputTokens > 0) {
840
+ const firstCall = calls[0];
841
+ if (firstCall) {
842
+ this.usageHandler(firstCall.sessionId, usage);
843
+ }
844
+ }
845
+ }
846
+ });
847
+ }
848
+ stop() {
849
+ this.watcher?.close();
850
+ this.watcher = null;
851
+ this.tailer.resetAll();
852
+ this.knownFiles.clear();
853
+ }
854
+ readExisting(filePath) {
855
+ this.tailer.reset(filePath);
856
+ const lines = this.tailer.readNewLines(filePath);
857
+ return parseLines(lines);
858
+ }
859
+ };
860
+
861
+ // src/mcp/server.ts
862
+ var MAX_ALERTS = 100;
863
+ var MAX_ACTIVITY = 200;
864
+ var startMcpServer = async (allUsers, noSecurity) => {
865
+ const alerts = [];
866
+ const activity = /* @__PURE__ */ new Map();
867
+ const engine = noSecurity ? null : new SecurityEngine("info");
868
+ const toolHandler = (calls) => {
869
+ for (const call of calls) {
870
+ const existing = activity.get(call.sessionId) ?? [];
871
+ existing.push(call);
872
+ if (existing.length > MAX_ACTIVITY) existing.splice(0, existing.length - MAX_ACTIVITY);
873
+ activity.set(call.sessionId, existing);
874
+ }
875
+ };
876
+ const securityHandler = engine ? (events) => {
877
+ for (const event of events) {
878
+ const newAlerts = engine.analyzeEvent(event);
879
+ alerts.push(...newAlerts);
880
+ if (alerts.length > MAX_ALERTS) alerts.splice(0, alerts.length - MAX_ALERTS);
881
+ }
882
+ } : void 0;
883
+ const watcher = new Watcher(toolHandler, allUsers, securityHandler);
884
+ watcher.start();
885
+ const server = new Server(
886
+ { name: "agenttop", version: "0.3.0" },
887
+ {
888
+ capabilities: { tools: {} }
889
+ }
890
+ );
891
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
892
+ tools: [
893
+ {
894
+ name: "agenttop_sessions",
895
+ description: "List active Claude Code sessions with model, CPU, MEM, tokens, nickname",
896
+ inputSchema: { type: "object", properties: {} }
897
+ },
898
+ {
899
+ name: "agenttop_alerts",
900
+ description: "Get recent security alerts, optionally filtered by severity",
901
+ inputSchema: {
902
+ type: "object",
903
+ properties: {
904
+ severity: {
905
+ type: "string",
906
+ enum: ["info", "warn", "high", "critical"],
907
+ description: "Minimum severity filter"
908
+ },
909
+ limit: { type: "number", description: "Max alerts to return (default 20)" }
910
+ }
911
+ }
912
+ },
913
+ {
914
+ name: "agenttop_usage",
915
+ description: "Get token usage for a session or all sessions",
916
+ inputSchema: {
917
+ type: "object",
918
+ properties: {
919
+ sessionId: { type: "string", description: "Session ID (omit for all)" }
920
+ }
921
+ }
922
+ },
923
+ {
924
+ name: "agenttop_activity",
925
+ description: "Get recent tool calls for a session",
926
+ inputSchema: {
927
+ type: "object",
928
+ properties: {
929
+ sessionId: { type: "string", description: "Session ID" },
930
+ limit: { type: "number", description: "Max events to return (default 20)" }
931
+ },
932
+ required: ["sessionId"]
933
+ }
934
+ }
935
+ ]
936
+ }));
937
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
938
+ const { name, arguments: args } = request.params;
939
+ switch (name) {
940
+ case "agenttop_sessions": {
941
+ const sessions = discoverSessions(allUsers);
942
+ const data = sessions.map((s) => ({
943
+ sessionId: s.sessionId,
944
+ slug: s.slug,
945
+ model: s.model,
946
+ cwd: s.cwd,
947
+ cpu: s.cpu,
948
+ memMB: s.memMB,
949
+ agents: s.agentCount,
950
+ tokens: { input: s.usage.inputTokens, output: s.usage.outputTokens, cacheRead: s.usage.cacheReadTokens }
951
+ }));
952
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
953
+ }
954
+ case "agenttop_alerts": {
955
+ const severity = args?.severity ?? "info";
956
+ const limit = args?.limit ?? 20;
957
+ const order = { info: 0, warn: 1, high: 2, critical: 3 };
958
+ const minOrder = order[severity] ?? 0;
959
+ const filtered = alerts.filter((a) => (order[a.severity] ?? 0) >= minOrder).slice(-limit).map((a) => ({
960
+ severity: a.severity,
961
+ rule: a.rule,
962
+ message: a.message,
963
+ sessionSlug: a.sessionSlug,
964
+ timestamp: new Date(a.timestamp).toISOString()
965
+ }));
966
+ return { content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }] };
967
+ }
968
+ case "agenttop_usage": {
969
+ const sessions = discoverSessions(allUsers);
970
+ const sessionId = args?.sessionId;
971
+ const targets = sessionId ? sessions.filter((s) => s.sessionId === sessionId) : sessions;
972
+ const data = targets.map((s) => ({
973
+ sessionId: s.sessionId,
974
+ slug: s.slug,
975
+ usage: s.usage
976
+ }));
977
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
978
+ }
979
+ case "agenttop_activity": {
980
+ const sid = args?.sessionId;
981
+ const limit = args?.limit ?? 20;
982
+ const events = (activity.get(sid) ?? []).slice(-limit).map((e) => ({
983
+ timestamp: new Date(e.timestamp).toISOString(),
984
+ tool: e.toolName,
985
+ input: e.toolInput
986
+ }));
987
+ return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
988
+ }
989
+ default:
990
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
991
+ }
992
+ });
993
+ const transport = new StdioServerTransport();
994
+ await server.connect(transport);
995
+ process.on("SIGINT", () => {
996
+ watcher.stop();
997
+ process.exit(0);
998
+ });
999
+ process.on("SIGTERM", () => {
1000
+ watcher.stop();
1001
+ process.exit(0);
1002
+ });
1003
+ };
1004
+
1005
+ export {
1006
+ loadConfig,
1007
+ saveConfig,
1008
+ isFirstRun,
1009
+ setNickname,
1010
+ clearNickname,
1011
+ getNicknames,
1012
+ resolveAlertLogPath,
1013
+ rotateLogFile,
1014
+ discoverSessions,
1015
+ Watcher,
1016
+ SecurityEngine,
1017
+ startMcpServer
1018
+ };
1019
+ //# sourceMappingURL=chunk-4I4UZNKS.js.map