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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.js +1241 -0
- package/dist/index.js.map +1 -0
- package/hooks/agenttop-guard.py +120 -0
- package/package.json +44 -0
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
|