agenttop 0.10.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -20
- package/dist/{chunk-24HX2MSZ.js → chunk-LPXME2WB.js} +572 -54
- package/dist/chunk-LPXME2WB.js.map +1 -0
- package/dist/index.js +1260 -365
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-24HX2MSZ.js.map +0 -1
|
@@ -6,7 +6,9 @@ import { join } from "path";
|
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
var getConfigDir = () => {
|
|
8
8
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
9
|
-
|
|
9
|
+
if (xdg) return join(xdg, "agenttop");
|
|
10
|
+
if (process.platform === "win32" && process.env.APPDATA) return join(process.env.APPDATA, "agenttop");
|
|
11
|
+
return join(homedir(), ".config", "agenttop");
|
|
10
12
|
};
|
|
11
13
|
var getConfigPath = () => join(getConfigDir(), "config.json");
|
|
12
14
|
var defaultConfig = () => ({
|
|
@@ -21,7 +23,10 @@ var defaultConfig = () => ({
|
|
|
21
23
|
},
|
|
22
24
|
alerts: {
|
|
23
25
|
logFile: join(getConfigDir(), "alerts.jsonl"),
|
|
24
|
-
enabled: true
|
|
26
|
+
enabled: true,
|
|
27
|
+
staleTimeout: 60,
|
|
28
|
+
staleAlertSeverity: "warn",
|
|
29
|
+
custom: []
|
|
25
30
|
},
|
|
26
31
|
updates: {
|
|
27
32
|
checkOnLaunch: true,
|
|
@@ -51,7 +56,11 @@ var defaultConfig = () => ({
|
|
|
51
56
|
swapPanels: "S",
|
|
52
57
|
closePanel: "X",
|
|
53
58
|
sidebarNarrower: "<",
|
|
54
|
-
sidebarWider: ">"
|
|
59
|
+
sidebarWider: ">",
|
|
60
|
+
alertRules: "r",
|
|
61
|
+
pin: "p",
|
|
62
|
+
pinMoveUp: "P",
|
|
63
|
+
pinMoveDown: "ctrl+p"
|
|
55
64
|
},
|
|
56
65
|
security: {
|
|
57
66
|
enabled: true,
|
|
@@ -67,13 +76,15 @@ var defaultConfig = () => ({
|
|
|
67
76
|
hook: "pending",
|
|
68
77
|
mcp: "pending",
|
|
69
78
|
theme: "pending",
|
|
70
|
-
tour: "pending"
|
|
79
|
+
tour: "pending",
|
|
80
|
+
autoUpdate: "pending"
|
|
71
81
|
},
|
|
72
82
|
archived: {},
|
|
73
83
|
archiveExpiryDays: 0,
|
|
74
84
|
sidebarWidth: 30,
|
|
75
85
|
theme: "one-dark",
|
|
76
|
-
customThemes: {}
|
|
86
|
+
customThemes: {},
|
|
87
|
+
pinnedSessions: []
|
|
77
88
|
});
|
|
78
89
|
var deepMerge = (target, source) => {
|
|
79
90
|
const result = { ...target };
|
|
@@ -184,21 +195,46 @@ var deleteSessionFiles = (outputFiles) => {
|
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
};
|
|
198
|
+
var pinSession = (sessionId) => {
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
if (!config.pinnedSessions.includes(sessionId)) {
|
|
201
|
+
config.pinnedSessions.push(sessionId);
|
|
202
|
+
saveConfig(config);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var unpinSession = (sessionId) => {
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
config.pinnedSessions = config.pinnedSessions.filter((id) => id !== sessionId);
|
|
208
|
+
saveConfig(config);
|
|
209
|
+
};
|
|
210
|
+
var movePinned = (sessionId, direction) => {
|
|
211
|
+
const config = loadConfig();
|
|
212
|
+
const idx = config.pinnedSessions.indexOf(sessionId);
|
|
213
|
+
if (idx === -1) return;
|
|
214
|
+
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
215
|
+
if (newIdx < 0 || newIdx >= config.pinnedSessions.length) return;
|
|
216
|
+
config.pinnedSessions.splice(idx, 1);
|
|
217
|
+
config.pinnedSessions.splice(newIdx, 0, sessionId);
|
|
218
|
+
saveConfig(config);
|
|
219
|
+
};
|
|
187
220
|
|
|
188
221
|
// src/mcp/server.ts
|
|
222
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
223
|
+
import { join as join4, dirname } from "path";
|
|
224
|
+
import { fileURLToPath } from "url";
|
|
189
225
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
190
226
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
191
227
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
192
228
|
|
|
193
229
|
// src/discovery/sessions.ts
|
|
194
|
-
import { readdirSync as readdirSync2, statSync as statSync2,
|
|
230
|
+
import { readdirSync as readdirSync2, statSync as statSync2, openSync, readSync, closeSync } from "fs";
|
|
195
231
|
import { join as join3, basename } from "path";
|
|
196
|
-
import { execSync } from "child_process";
|
|
197
232
|
|
|
198
233
|
// src/config.ts
|
|
199
234
|
import { existsSync as existsSync2, realpathSync, readdirSync } from "fs";
|
|
200
|
-
import { homedir as homedir2, platform } from "os";
|
|
235
|
+
import { homedir as homedir2, platform, tmpdir, userInfo } from "os";
|
|
201
236
|
import { join as join2 } from "path";
|
|
237
|
+
var os = platform();
|
|
202
238
|
var resolvePath = (p) => {
|
|
203
239
|
try {
|
|
204
240
|
return realpathSync(p);
|
|
@@ -206,13 +242,25 @@ var resolvePath = (p) => {
|
|
|
206
242
|
return p;
|
|
207
243
|
}
|
|
208
244
|
};
|
|
209
|
-
var getUid = () =>
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
var getUid = () => {
|
|
246
|
+
const uid = process.getuid?.();
|
|
247
|
+
if (uid !== void 0) return String(uid);
|
|
248
|
+
try {
|
|
249
|
+
return userInfo().username;
|
|
250
|
+
} catch {
|
|
251
|
+
return process.env["USERNAME"] || process.env["USER"] || "0";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var isRoot = () => process.getuid?.() === 0;
|
|
255
|
+
var getTmpDir = () => {
|
|
256
|
+
if (os === "darwin") return resolvePath("/private/tmp");
|
|
257
|
+
if (os === "win32") return tmpdir();
|
|
258
|
+
return resolvePath("/tmp");
|
|
259
|
+
};
|
|
212
260
|
var getTaskDirs = (allUsers) => {
|
|
213
261
|
const tmp = getTmpDir();
|
|
214
262
|
const uid = getUid();
|
|
215
|
-
if (allUsers) {
|
|
263
|
+
if (allUsers && os !== "win32") {
|
|
216
264
|
try {
|
|
217
265
|
const dirs2 = readdirSync(tmp).filter((d) => d.startsWith("claude-")).filter((d) => !d.endsWith("-cwd")).map((d) => join2(tmp, d));
|
|
218
266
|
if (dirs2.length > 0) return dirs2;
|
|
@@ -242,50 +290,213 @@ var getProjectsDirs = (allUsers) => {
|
|
|
242
290
|
addDir(join2("/root", ".claude", "projects"));
|
|
243
291
|
const sudoUser = process.env["SUDO_USER"];
|
|
244
292
|
if (sudoUser) {
|
|
245
|
-
const homeBase =
|
|
293
|
+
const homeBase = os === "darwin" ? "/Users" : "/home";
|
|
246
294
|
addDir(join2(homeBase, sudoUser, ".claude", "projects"));
|
|
247
295
|
}
|
|
248
296
|
}
|
|
249
|
-
if (allUsers) {
|
|
297
|
+
if (allUsers && os !== "win32") {
|
|
250
298
|
try {
|
|
251
|
-
const homeBase =
|
|
299
|
+
const homeBase = os === "darwin" ? "/Users" : "/home";
|
|
252
300
|
for (const user of readdirSync(homeBase)) {
|
|
253
301
|
addDir(join2(homeBase, user, ".claude", "projects"));
|
|
254
302
|
}
|
|
255
303
|
} catch {
|
|
256
304
|
}
|
|
257
|
-
addDir(join2("/root", ".claude", "projects"));
|
|
305
|
+
if (os !== "darwin") addDir(join2("/root", ".claude", "projects"));
|
|
306
|
+
}
|
|
307
|
+
if (allUsers && os === "win32") {
|
|
308
|
+
try {
|
|
309
|
+
for (const user of readdirSync("C:\\Users")) {
|
|
310
|
+
addDir(join2("C:\\Users", user, ".claude", "projects"));
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
258
314
|
}
|
|
259
315
|
return dirs;
|
|
260
316
|
};
|
|
261
317
|
|
|
262
|
-
// src/discovery/
|
|
318
|
+
// src/discovery/types.ts
|
|
319
|
+
var STATUS_PRIORITY = {
|
|
320
|
+
waiting: 0,
|
|
321
|
+
stale: 1,
|
|
322
|
+
active: 2,
|
|
323
|
+
inactive: 3
|
|
324
|
+
};
|
|
325
|
+
var isToolResult = (event) => "toolUseId" in event;
|
|
326
|
+
var isToolCall = (event) => "toolName" in event;
|
|
327
|
+
|
|
328
|
+
// src/discovery/platform.ts
|
|
329
|
+
import { execFileSync, execFile } from "child_process";
|
|
330
|
+
import { readlinkSync } from "fs";
|
|
331
|
+
import { readlink } from "fs/promises";
|
|
332
|
+
import { platform as platform2 } from "os";
|
|
333
|
+
var os2 = platform2();
|
|
334
|
+
var parsePsLine = (line) => {
|
|
335
|
+
const parts = line.trim().split(/\s+/);
|
|
336
|
+
const pid = parseInt(parts[1], 10);
|
|
337
|
+
if (isNaN(pid)) return null;
|
|
338
|
+
const command = parts.slice(10).join(" ");
|
|
339
|
+
if (command.startsWith("sudo")) return null;
|
|
340
|
+
return {
|
|
341
|
+
pid,
|
|
342
|
+
cpu: parseFloat(parts[2]) || 0,
|
|
343
|
+
mem: parseFloat(parts[3]) || 0,
|
|
344
|
+
memKB: parseInt(parts[5], 10) || 0,
|
|
345
|
+
startTime: parts[8] || "",
|
|
346
|
+
command,
|
|
347
|
+
cwd: ""
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
var isClaudeLine = (line) => (line.includes("/claude") || /\bclaude\b/.test(line)) && !line.includes("grep") && !line.includes("agenttop");
|
|
351
|
+
var getProcessCwd = (pid) => {
|
|
352
|
+
if (os2 === "linux") {
|
|
353
|
+
try {
|
|
354
|
+
return readlinkSync(`/proc/${pid}/cwd`);
|
|
355
|
+
} catch {
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (os2 === "darwin") {
|
|
360
|
+
try {
|
|
361
|
+
const out = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
362
|
+
encoding: "utf-8",
|
|
363
|
+
timeout: 3e3
|
|
364
|
+
});
|
|
365
|
+
for (const line of out.split("\n")) {
|
|
366
|
+
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
return "";
|
|
373
|
+
};
|
|
374
|
+
var getProcessCwdAsync = async (pid) => {
|
|
375
|
+
if (os2 === "linux") {
|
|
376
|
+
try {
|
|
377
|
+
return await readlink(`/proc/${pid}/cwd`);
|
|
378
|
+
} catch {
|
|
379
|
+
return "";
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (os2 === "darwin") {
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
execFile(
|
|
385
|
+
"lsof",
|
|
386
|
+
["-a", "-p", String(pid), "-d", "cwd", "-Fn"],
|
|
387
|
+
{ encoding: "utf-8", timeout: 3e3 },
|
|
388
|
+
(err, out) => {
|
|
389
|
+
if (err || !out) {
|
|
390
|
+
resolve("");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const line of out.split("\n")) {
|
|
394
|
+
if (line.startsWith("n") && line.length > 1) {
|
|
395
|
+
resolve(line.slice(1));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
resolve("");
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return "";
|
|
405
|
+
};
|
|
406
|
+
var getClaudeProcessesUnix = () => {
|
|
407
|
+
try {
|
|
408
|
+
const output = execFileSync("ps", ["aux"], { encoding: "utf-8", timeout: 5e3 });
|
|
409
|
+
return output.split("\n").filter(isClaudeLine).map(parsePsLine).filter((p) => p !== null);
|
|
410
|
+
} catch {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var getClaudeProcessesWin32 = () => {
|
|
415
|
+
try {
|
|
416
|
+
const output = execFileSync(
|
|
417
|
+
"powershell",
|
|
418
|
+
[
|
|
419
|
+
"-NoProfile",
|
|
420
|
+
"-Command",
|
|
421
|
+
"Get-Process | Where-Object { $_.ProcessName -match 'claude' } | Select-Object Id,CPU,WorkingSet64,Path,StartTime | ConvertTo-Json -Compress"
|
|
422
|
+
],
|
|
423
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
424
|
+
);
|
|
425
|
+
if (!output.trim()) return [];
|
|
426
|
+
const raw = JSON.parse(output.trim());
|
|
427
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
428
|
+
return items.filter((p) => p.Id).map((p) => ({
|
|
429
|
+
pid: Number(p.Id),
|
|
430
|
+
cpu: Number(p.CPU) || 0,
|
|
431
|
+
mem: 0,
|
|
432
|
+
memKB: Math.round((Number(p.WorkingSet64) || 0) / 1024),
|
|
433
|
+
startTime: String(p.StartTime || ""),
|
|
434
|
+
command: String(p.Path || ""),
|
|
435
|
+
cwd: ""
|
|
436
|
+
}));
|
|
437
|
+
} catch {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
};
|
|
263
441
|
var getClaudeProcesses = () => {
|
|
442
|
+
const procs = os2 === "win32" ? getClaudeProcessesWin32() : getClaudeProcessesUnix();
|
|
443
|
+
for (const p of procs) {
|
|
444
|
+
p.cwd = getProcessCwd(p.pid);
|
|
445
|
+
p.memMB = Math.round(p.memKB / 1024);
|
|
446
|
+
}
|
|
447
|
+
return procs;
|
|
448
|
+
};
|
|
449
|
+
var getClaudeProcessesUnixAsync = async () => {
|
|
450
|
+
const stdout = await new Promise((resolve) => {
|
|
451
|
+
execFile("ps", ["aux"], { encoding: "utf-8", timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, out) => {
|
|
452
|
+
resolve(err || !out ? "" : out);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
if (!stdout) return [];
|
|
456
|
+
return stdout.split("\n").filter(isClaudeLine).map(parsePsLine).filter((p) => p !== null);
|
|
457
|
+
};
|
|
458
|
+
var getClaudeProcessesWin32Async = async () => {
|
|
459
|
+
const stdout = await new Promise((resolve) => {
|
|
460
|
+
execFile(
|
|
461
|
+
"powershell",
|
|
462
|
+
[
|
|
463
|
+
"-NoProfile",
|
|
464
|
+
"-Command",
|
|
465
|
+
"Get-Process | Where-Object { $_.ProcessName -match 'claude' } | Select-Object Id,CPU,WorkingSet64,Path,StartTime | ConvertTo-Json -Compress"
|
|
466
|
+
],
|
|
467
|
+
{ encoding: "utf-8", timeout: 5e3 },
|
|
468
|
+
(err, out) => resolve(err || !out ? "" : out)
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
if (!stdout.trim()) return [];
|
|
264
472
|
try {
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
cpu: parseFloat(parts[2]) || 0,
|
|
277
|
-
mem: parseFloat(parts[3]) || 0,
|
|
278
|
-
memKB: parseInt(parts[5], 10) || 0,
|
|
279
|
-
startTime: parts[8] || "",
|
|
280
|
-
command: parts.slice(10).join(" "),
|
|
281
|
-
cwd
|
|
282
|
-
};
|
|
283
|
-
}).filter((p) => !isNaN(p.pid)).filter((p) => !p.command.startsWith("sudo"));
|
|
284
|
-
return procs;
|
|
473
|
+
const raw = JSON.parse(stdout.trim());
|
|
474
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
475
|
+
return items.filter((p) => p.Id).map((p) => ({
|
|
476
|
+
pid: Number(p.Id),
|
|
477
|
+
cpu: Number(p.CPU) || 0,
|
|
478
|
+
mem: 0,
|
|
479
|
+
memKB: Math.round((Number(p.WorkingSet64) || 0) / 1024),
|
|
480
|
+
startTime: String(p.StartTime || ""),
|
|
481
|
+
command: String(p.Path || ""),
|
|
482
|
+
cwd: ""
|
|
483
|
+
}));
|
|
285
484
|
} catch {
|
|
286
485
|
return [];
|
|
287
486
|
}
|
|
288
487
|
};
|
|
488
|
+
var getClaudeProcessesAsync = async () => {
|
|
489
|
+
const procs = os2 === "win32" ? await getClaudeProcessesWin32Async() : await getClaudeProcessesUnixAsync();
|
|
490
|
+
await Promise.all(
|
|
491
|
+
procs.map(async (p) => {
|
|
492
|
+
p.cwd = await getProcessCwdAsync(p.pid);
|
|
493
|
+
p.memMB = Math.round(p.memKB / 1024);
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
return procs;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/discovery/sessions.ts
|
|
289
500
|
var readFirstLines = (filePath, bytes) => {
|
|
290
501
|
try {
|
|
291
502
|
const fd = openSync(filePath, "r");
|
|
@@ -297,6 +508,44 @@ var readFirstLines = (filePath, bytes) => {
|
|
|
297
508
|
return [];
|
|
298
509
|
}
|
|
299
510
|
};
|
|
511
|
+
var readTailBytes = (filePath, bytes) => {
|
|
512
|
+
try {
|
|
513
|
+
const fd = openSync(filePath, "r");
|
|
514
|
+
const fstat = statSync2(filePath);
|
|
515
|
+
const start = Math.max(0, fstat.size - bytes);
|
|
516
|
+
const readSize = Math.min(bytes, fstat.size);
|
|
517
|
+
const buf = Buffer.alloc(readSize);
|
|
518
|
+
readSync(fd, buf, 0, readSize, start);
|
|
519
|
+
closeSync(fd);
|
|
520
|
+
return buf.toString("utf-8");
|
|
521
|
+
} catch {
|
|
522
|
+
return "";
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var detectStatus = (filePath, hasPid, lastActivity, staleTimeout) => {
|
|
526
|
+
if (!hasPid) return "inactive";
|
|
527
|
+
const tail = readTailBytes(filePath, 4096);
|
|
528
|
+
const lines = tail.split("\n").filter(Boolean);
|
|
529
|
+
if (lines.length > 0) {
|
|
530
|
+
try {
|
|
531
|
+
const lastEvent = JSON.parse(lines[lines.length - 1]);
|
|
532
|
+
if (lastEvent.type === "assistant") {
|
|
533
|
+
const content = lastEvent.message?.content;
|
|
534
|
+
if (Array.isArray(content)) {
|
|
535
|
+
const hasAskUser = content.some(
|
|
536
|
+
(b) => b.type === "tool_use" && b.name === "AskUserQuestion"
|
|
537
|
+
);
|
|
538
|
+
if (hasAskUser) return "waiting";
|
|
539
|
+
const hasToolUse = content.some((b) => b.type === "tool_use");
|
|
540
|
+
if (!hasToolUse) return "waiting";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (Date.now() - lastActivity > staleTimeout * 1e3) return "stale";
|
|
547
|
+
return "active";
|
|
548
|
+
};
|
|
300
549
|
var readFirstEvent = (filePath) => {
|
|
301
550
|
const lines = readFirstLines(filePath, 16384);
|
|
302
551
|
if (lines.length === 0) return null;
|
|
@@ -362,7 +611,7 @@ var extractSessionMeta = (filePath) => {
|
|
|
362
611
|
if (!sessionId) return null;
|
|
363
612
|
return { sessionId, cwd, version, gitBranch, model, usage };
|
|
364
613
|
};
|
|
365
|
-
var discoverFromProjects = (allUsers, processes, sessionMap) => {
|
|
614
|
+
var discoverFromProjects = (allUsers, processes, sessionMap, staleTimeout, pinnedOrder) => {
|
|
366
615
|
const projectsDirs = getProjectsDirs(allUsers);
|
|
367
616
|
for (const projectsDir of projectsDirs) {
|
|
368
617
|
let projectNames;
|
|
@@ -415,14 +664,16 @@ var discoverFromProjects = (allUsers, processes, sessionMap) => {
|
|
|
415
664
|
outputFiles: [filePath],
|
|
416
665
|
startTime: fstat.birthtimeMs || fstat.ctimeMs,
|
|
417
666
|
lastActivity: fstat.mtimeMs,
|
|
418
|
-
usage: meta.usage
|
|
667
|
+
usage: meta.usage,
|
|
668
|
+
status: detectStatus(filePath, matchingProcess !== void 0, fstat.mtimeMs, staleTimeout),
|
|
669
|
+
pinned: pinnedOrder.includes(meta.sessionId)
|
|
419
670
|
};
|
|
420
671
|
sessionMap.set(meta.sessionId, session);
|
|
421
672
|
}
|
|
422
673
|
}
|
|
423
674
|
}
|
|
424
675
|
};
|
|
425
|
-
var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
676
|
+
var discoverFromTmp = (allUsers, processes, sessionMap, staleTimeout, pinnedOrder) => {
|
|
426
677
|
const taskDirs = getTaskDirs(allUsers);
|
|
427
678
|
for (const taskDir of taskDirs) {
|
|
428
679
|
let projectDirs;
|
|
@@ -510,6 +761,13 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
510
761
|
if (sessionMap.has(sessionId || projectName)) continue;
|
|
511
762
|
const normCwd = normalisePath(cwd);
|
|
512
763
|
const matchingProcess = processes.find((p) => p.cwd && normalisePath(p.cwd) === normCwd);
|
|
764
|
+
const latestFile = outputFiles.reduce((a, b) => {
|
|
765
|
+
try {
|
|
766
|
+
return statSync2(a).mtimeMs > statSync2(b).mtimeMs ? a : b;
|
|
767
|
+
} catch {
|
|
768
|
+
return a;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
513
771
|
const session = {
|
|
514
772
|
sessionId,
|
|
515
773
|
slug: slug || sessionId.slice(0, 12),
|
|
@@ -527,7 +785,9 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
527
785
|
outputFiles,
|
|
528
786
|
startTime: startTime === Infinity ? Date.now() : startTime,
|
|
529
787
|
lastActivity,
|
|
530
|
-
usage: totalUsage
|
|
788
|
+
usage: totalUsage,
|
|
789
|
+
status: detectStatus(latestFile, matchingProcess !== void 0, lastActivity, staleTimeout),
|
|
790
|
+
pinned: pinnedOrder.includes(sessionId)
|
|
531
791
|
};
|
|
532
792
|
sessionMap.set(sessionId || projectName, session);
|
|
533
793
|
}
|
|
@@ -535,22 +795,28 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
535
795
|
}
|
|
536
796
|
};
|
|
537
797
|
var discoverSessions = (allUsers) => {
|
|
798
|
+
const config = loadConfig();
|
|
799
|
+
const staleTimeout = config.alerts.staleTimeout ?? 60;
|
|
800
|
+
const pinnedOrder = config.pinnedSessions ?? [];
|
|
538
801
|
const processes = getClaudeProcesses();
|
|
539
802
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
540
|
-
discoverFromProjects(allUsers, processes, sessionMap);
|
|
541
|
-
discoverFromTmp(allUsers, processes, sessionMap);
|
|
803
|
+
discoverFromProjects(allUsers, processes, sessionMap, staleTimeout, pinnedOrder);
|
|
804
|
+
discoverFromTmp(allUsers, processes, sessionMap, staleTimeout, pinnedOrder);
|
|
542
805
|
return Array.from(sessionMap.values()).sort((a, b) => {
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
806
|
+
const aPin = pinnedOrder.indexOf(a.sessionId);
|
|
807
|
+
const bPin = pinnedOrder.indexOf(b.sessionId);
|
|
808
|
+
const aIsPinned = aPin !== -1;
|
|
809
|
+
const bIsPinned = bPin !== -1;
|
|
810
|
+
if (aIsPinned && !bIsPinned) return -1;
|
|
811
|
+
if (!aIsPinned && bIsPinned) return 1;
|
|
812
|
+
if (aIsPinned && bIsPinned) return aPin - bPin;
|
|
813
|
+
const aPri = STATUS_PRIORITY[a.status];
|
|
814
|
+
const bPri = STATUS_PRIORITY[b.status];
|
|
815
|
+
if (aPri !== bPri) return aPri - bPri;
|
|
546
816
|
return b.lastActivity - a.lastActivity;
|
|
547
817
|
});
|
|
548
818
|
};
|
|
549
819
|
|
|
550
|
-
// src/discovery/types.ts
|
|
551
|
-
var isToolResult = (event) => "toolUseId" in event;
|
|
552
|
-
var isToolCall = (event) => "toolName" in event;
|
|
553
|
-
|
|
554
820
|
// src/analysis/rules/network.ts
|
|
555
821
|
var NETWORK_PATTERNS = [
|
|
556
822
|
/\bcurl\b/,
|
|
@@ -763,6 +1029,51 @@ var checkToolResultInjection = (result) => {
|
|
|
763
1029
|
};
|
|
764
1030
|
};
|
|
765
1031
|
|
|
1032
|
+
// src/analysis/rules/custom.ts
|
|
1033
|
+
var createCustomRules = (rules) => {
|
|
1034
|
+
const compiled = [];
|
|
1035
|
+
for (const rule of rules) {
|
|
1036
|
+
if (!rule.enabled) continue;
|
|
1037
|
+
let regex;
|
|
1038
|
+
try {
|
|
1039
|
+
regex = new RegExp(rule.pattern);
|
|
1040
|
+
} catch {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const fn = (event) => {
|
|
1044
|
+
let matched = false;
|
|
1045
|
+
if (isToolCall(event)) {
|
|
1046
|
+
if (rule.match === "output") return null;
|
|
1047
|
+
if (rule.match === "toolName" || rule.match === "all") {
|
|
1048
|
+
if (regex.test(event.toolName)) matched = true;
|
|
1049
|
+
}
|
|
1050
|
+
if (!matched && (rule.match === "input" || rule.match === "all")) {
|
|
1051
|
+
if (regex.test(JSON.stringify(event.toolInput))) matched = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (isToolResult(event)) {
|
|
1055
|
+
if (rule.match === "input" || rule.match === "toolName") return null;
|
|
1056
|
+
if (rule.match === "output" || rule.match === "all") {
|
|
1057
|
+
if (regex.test(event.content)) matched = true;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (!matched) return null;
|
|
1061
|
+
return {
|
|
1062
|
+
id: `custom-${rule.name}-${event.timestamp}`,
|
|
1063
|
+
severity: rule.severity,
|
|
1064
|
+
rule: `custom:${rule.name}`,
|
|
1065
|
+
message: rule.message || rule.name,
|
|
1066
|
+
sessionSlug: "slug" in event ? event.slug : "",
|
|
1067
|
+
sessionId: "sessionId" in event ? event.sessionId : "",
|
|
1068
|
+
event,
|
|
1069
|
+
timestamp: event.timestamp
|
|
1070
|
+
};
|
|
1071
|
+
};
|
|
1072
|
+
compiled.push(fn);
|
|
1073
|
+
}
|
|
1074
|
+
return compiled;
|
|
1075
|
+
};
|
|
1076
|
+
|
|
766
1077
|
// src/analysis/security.ts
|
|
767
1078
|
var toolCallRules = [
|
|
768
1079
|
{ key: "network", fn: checkNetwork },
|
|
@@ -782,7 +1093,8 @@ var SecurityEngine = class {
|
|
|
782
1093
|
recentAlerts = /* @__PURE__ */ new Map();
|
|
783
1094
|
minLevel;
|
|
784
1095
|
rulesConfig;
|
|
785
|
-
|
|
1096
|
+
customEventRules = [];
|
|
1097
|
+
constructor(minLevel = "warn", rulesConfig, customRules) {
|
|
786
1098
|
this.minLevel = minLevel;
|
|
787
1099
|
this.rulesConfig = rulesConfig ?? {
|
|
788
1100
|
network: true,
|
|
@@ -791,6 +1103,9 @@ var SecurityEngine = class {
|
|
|
791
1103
|
shellEscape: true,
|
|
792
1104
|
injection: true
|
|
793
1105
|
};
|
|
1106
|
+
if (customRules?.length) {
|
|
1107
|
+
this.customEventRules = createCustomRules(customRules);
|
|
1108
|
+
}
|
|
794
1109
|
}
|
|
795
1110
|
analyze(call) {
|
|
796
1111
|
return this.analyzeEvent(call);
|
|
@@ -812,6 +1127,10 @@ var SecurityEngine = class {
|
|
|
812
1127
|
const alert = rule.fn(event);
|
|
813
1128
|
if (alert) alerts.push(alert);
|
|
814
1129
|
}
|
|
1130
|
+
for (const fn of this.customEventRules) {
|
|
1131
|
+
const alert = fn(event);
|
|
1132
|
+
if (alert) alerts.push(alert);
|
|
1133
|
+
}
|
|
815
1134
|
return alerts.filter((alert) => {
|
|
816
1135
|
if (SEVERITY_ORDER[alert.severity] < SEVERITY_ORDER[this.minLevel]) return false;
|
|
817
1136
|
const dedupKey = `${alert.rule}-${alert.sessionId}-${alert.message.slice(0, 40)}`;
|
|
@@ -1131,6 +1450,16 @@ var Watcher = class {
|
|
|
1131
1450
|
// src/mcp/server.ts
|
|
1132
1451
|
var MAX_ALERTS = 100;
|
|
1133
1452
|
var MAX_ACTIVITY = 200;
|
|
1453
|
+
var getVersion = () => {
|
|
1454
|
+
try {
|
|
1455
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1456
|
+
const pkgPath = join4(dirname(thisFile), "..", "package.json");
|
|
1457
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1458
|
+
return pkg.version || "0.0.0";
|
|
1459
|
+
} catch {
|
|
1460
|
+
return "0.0.0";
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1134
1463
|
var startMcpServer = async (allUsers, noSecurity) => {
|
|
1135
1464
|
const alerts = [];
|
|
1136
1465
|
const activity = /* @__PURE__ */ new Map();
|
|
@@ -1153,7 +1482,7 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1153
1482
|
const watcher = new Watcher(toolHandler, allUsers, securityHandler);
|
|
1154
1483
|
watcher.start();
|
|
1155
1484
|
const server = new Server(
|
|
1156
|
-
{ name: "agenttop", version:
|
|
1485
|
+
{ name: "agenttop", version: getVersion() },
|
|
1157
1486
|
{
|
|
1158
1487
|
capabilities: { tools: {} }
|
|
1159
1488
|
}
|
|
@@ -1201,6 +1530,79 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1201
1530
|
},
|
|
1202
1531
|
required: ["sessionId"]
|
|
1203
1532
|
}
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
name: "agenttop_waiting_sessions",
|
|
1536
|
+
description: "List sessions in waiting or stale state",
|
|
1537
|
+
inputSchema: { type: "object", properties: {} }
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
name: "agenttop_session_status",
|
|
1541
|
+
description: "Get status for a session",
|
|
1542
|
+
inputSchema: {
|
|
1543
|
+
type: "object",
|
|
1544
|
+
properties: { sessionId: { type: "string" } },
|
|
1545
|
+
required: ["sessionId"]
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
name: "agenttop_custom_alerts",
|
|
1550
|
+
description: "List custom alert rules",
|
|
1551
|
+
inputSchema: { type: "object", properties: {} }
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
name: "agenttop_set_custom_alert",
|
|
1555
|
+
description: "Add or update a custom alert rule",
|
|
1556
|
+
inputSchema: {
|
|
1557
|
+
type: "object",
|
|
1558
|
+
properties: {
|
|
1559
|
+
name: { type: "string" },
|
|
1560
|
+
pattern: { type: "string" },
|
|
1561
|
+
match: { type: "string", enum: ["input", "output", "toolName", "all"] },
|
|
1562
|
+
severity: { type: "string", enum: ["info", "warn", "high", "critical"] },
|
|
1563
|
+
message: { type: "string" }
|
|
1564
|
+
},
|
|
1565
|
+
required: ["name", "pattern"]
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
name: "agenttop_delete_custom_alert",
|
|
1570
|
+
description: "Delete a custom alert rule",
|
|
1571
|
+
inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
name: "agenttop_alert_history",
|
|
1575
|
+
description: "Get recent alerts with severity filter",
|
|
1576
|
+
inputSchema: {
|
|
1577
|
+
type: "object",
|
|
1578
|
+
properties: {
|
|
1579
|
+
severity: { type: "string", enum: ["info", "warn", "high", "critical"] },
|
|
1580
|
+
limit: { type: "number" }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
name: "agenttop_set_stale_timeout",
|
|
1586
|
+
description: "Set stale timeout in seconds",
|
|
1587
|
+
inputSchema: { type: "object", properties: { seconds: { type: "number" } }, required: ["seconds"] }
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
name: "agenttop_pin_session",
|
|
1591
|
+
description: "Pin a session to top",
|
|
1592
|
+
inputSchema: {
|
|
1593
|
+
type: "object",
|
|
1594
|
+
properties: { sessionId: { type: "string" } },
|
|
1595
|
+
required: ["sessionId"]
|
|
1596
|
+
}
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
name: "agenttop_unpin_session",
|
|
1600
|
+
description: "Unpin a session",
|
|
1601
|
+
inputSchema: {
|
|
1602
|
+
type: "object",
|
|
1603
|
+
properties: { sessionId: { type: "string" } },
|
|
1604
|
+
required: ["sessionId"]
|
|
1605
|
+
}
|
|
1204
1606
|
}
|
|
1205
1607
|
]
|
|
1206
1608
|
}));
|
|
@@ -1217,7 +1619,9 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1217
1619
|
cpu: s.cpu,
|
|
1218
1620
|
memMB: s.memMB,
|
|
1219
1621
|
agents: s.agentCount,
|
|
1220
|
-
tokens: { input: s.usage.inputTokens, output: s.usage.outputTokens, cacheRead: s.usage.cacheReadTokens }
|
|
1622
|
+
tokens: { input: s.usage.inputTokens, output: s.usage.outputTokens, cacheRead: s.usage.cacheReadTokens },
|
|
1623
|
+
status: s.status,
|
|
1624
|
+
pinned: s.pinned
|
|
1221
1625
|
}));
|
|
1222
1626
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1223
1627
|
}
|
|
@@ -1256,6 +1660,115 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1256
1660
|
}));
|
|
1257
1661
|
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
|
|
1258
1662
|
}
|
|
1663
|
+
case "agenttop_waiting_sessions": {
|
|
1664
|
+
const sessions = discoverSessions(allUsers);
|
|
1665
|
+
const waiting = sessions.filter((s) => s.status === "waiting" || s.status === "stale").map((s) => ({
|
|
1666
|
+
sessionId: s.sessionId,
|
|
1667
|
+
slug: s.slug,
|
|
1668
|
+
status: s.status,
|
|
1669
|
+
cwd: s.cwd,
|
|
1670
|
+
model: s.model,
|
|
1671
|
+
idleSeconds: Math.round((Date.now() - s.lastActivity) / 1e3)
|
|
1672
|
+
}));
|
|
1673
|
+
return { content: [{ type: "text", text: JSON.stringify(waiting, null, 2) }] };
|
|
1674
|
+
}
|
|
1675
|
+
case "agenttop_session_status": {
|
|
1676
|
+
const sid = args?.sessionId;
|
|
1677
|
+
const sessions = discoverSessions(allUsers);
|
|
1678
|
+
const session = sessions.find((s) => s.sessionId === sid);
|
|
1679
|
+
if (!session) return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
|
|
1680
|
+
return { content: [{ type: "text", text: JSON.stringify({ sessionId: sid, status: session.status }) }] };
|
|
1681
|
+
}
|
|
1682
|
+
case "agenttop_custom_alerts": {
|
|
1683
|
+
const cfg = loadConfig();
|
|
1684
|
+
return { content: [{ type: "text", text: JSON.stringify(cfg.alerts.custom ?? [], null, 2) }] };
|
|
1685
|
+
}
|
|
1686
|
+
case "agenttop_set_custom_alert": {
|
|
1687
|
+
const name2 = args?.name;
|
|
1688
|
+
const pattern = args?.pattern;
|
|
1689
|
+
try {
|
|
1690
|
+
new RegExp(pattern);
|
|
1691
|
+
} catch {
|
|
1692
|
+
return { content: [{ type: "text", text: `Invalid regex: ${pattern}` }], isError: true };
|
|
1693
|
+
}
|
|
1694
|
+
const cfg = loadConfig();
|
|
1695
|
+
const custom = [...cfg.alerts.custom ?? []];
|
|
1696
|
+
const existing = custom.findIndex((r) => r.name === name2);
|
|
1697
|
+
const rule = {
|
|
1698
|
+
name: name2,
|
|
1699
|
+
pattern,
|
|
1700
|
+
match: args?.match ?? "all",
|
|
1701
|
+
severity: args?.severity ?? "warn",
|
|
1702
|
+
message: args?.message ?? name2,
|
|
1703
|
+
enabled: true
|
|
1704
|
+
};
|
|
1705
|
+
if (existing >= 0) custom[existing] = rule;
|
|
1706
|
+
else custom.push(rule);
|
|
1707
|
+
cfg.alerts.custom = custom;
|
|
1708
|
+
saveConfig(cfg);
|
|
1709
|
+
return { content: [{ type: "text", text: `Rule '${name2}' saved` }] };
|
|
1710
|
+
}
|
|
1711
|
+
case "agenttop_delete_custom_alert": {
|
|
1712
|
+
const name2 = args?.name;
|
|
1713
|
+
const cfg = loadConfig();
|
|
1714
|
+
cfg.alerts.custom = (cfg.alerts.custom ?? []).filter((r) => r.name !== name2);
|
|
1715
|
+
saveConfig(cfg);
|
|
1716
|
+
return { content: [{ type: "text", text: `Rule '${name2}' deleted` }] };
|
|
1717
|
+
}
|
|
1718
|
+
case "agenttop_alert_history": {
|
|
1719
|
+
const severity = args?.severity ?? "info";
|
|
1720
|
+
const limit = args?.limit ?? 50;
|
|
1721
|
+
const order = { info: 0, warn: 1, high: 2, critical: 3 };
|
|
1722
|
+
const minOrder = order[severity] ?? 0;
|
|
1723
|
+
const cfg = loadConfig();
|
|
1724
|
+
const logPath = resolveAlertLogPath(cfg);
|
|
1725
|
+
const logAlerts = [];
|
|
1726
|
+
try {
|
|
1727
|
+
const lines = readFileSync2(logPath, "utf-8").split("\n").filter(Boolean);
|
|
1728
|
+
for (const line of lines) {
|
|
1729
|
+
try {
|
|
1730
|
+
const a = JSON.parse(line);
|
|
1731
|
+
if ((order[a.severity] ?? 0) >= minOrder)
|
|
1732
|
+
logAlerts.push({
|
|
1733
|
+
severity: a.severity,
|
|
1734
|
+
rule: a.rule,
|
|
1735
|
+
message: a.message,
|
|
1736
|
+
sessionSlug: a.sessionSlug,
|
|
1737
|
+
timestamp: a.timestamp ? new Date(a.timestamp).toISOString() : "unknown"
|
|
1738
|
+
});
|
|
1739
|
+
} catch {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
for (const a of alerts) {
|
|
1746
|
+
if ((order[a.severity] ?? 0) >= minOrder)
|
|
1747
|
+
logAlerts.push({
|
|
1748
|
+
severity: a.severity,
|
|
1749
|
+
rule: a.rule,
|
|
1750
|
+
message: a.message,
|
|
1751
|
+
sessionSlug: a.sessionSlug,
|
|
1752
|
+
timestamp: new Date(a.timestamp).toISOString()
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
return { content: [{ type: "text", text: JSON.stringify(logAlerts.slice(-limit), null, 2) }] };
|
|
1756
|
+
}
|
|
1757
|
+
case "agenttop_set_stale_timeout": {
|
|
1758
|
+
const seconds = Math.max(15, args?.seconds ?? 60);
|
|
1759
|
+
const cfg = loadConfig();
|
|
1760
|
+
cfg.alerts.staleTimeout = seconds;
|
|
1761
|
+
saveConfig(cfg);
|
|
1762
|
+
return { content: [{ type: "text", text: `Stale timeout set to ${seconds}s` }] };
|
|
1763
|
+
}
|
|
1764
|
+
case "agenttop_pin_session": {
|
|
1765
|
+
pinSession(args?.sessionId);
|
|
1766
|
+
return { content: [{ type: "text", text: `Session pinned` }] };
|
|
1767
|
+
}
|
|
1768
|
+
case "agenttop_unpin_session": {
|
|
1769
|
+
unpinSession(args?.sessionId);
|
|
1770
|
+
return { content: [{ type: "text", text: `Session unpinned` }] };
|
|
1771
|
+
}
|
|
1259
1772
|
default:
|
|
1260
1773
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1261
1774
|
}
|
|
@@ -1286,11 +1799,16 @@ export {
|
|
|
1286
1799
|
getArchived,
|
|
1287
1800
|
purgeExpiredArchives,
|
|
1288
1801
|
deleteSessionFiles,
|
|
1802
|
+
pinSession,
|
|
1803
|
+
unpinSession,
|
|
1804
|
+
movePinned,
|
|
1289
1805
|
getTaskDirs,
|
|
1290
1806
|
getProjectsDirs,
|
|
1807
|
+
STATUS_PRIORITY,
|
|
1808
|
+
getClaudeProcessesAsync,
|
|
1291
1809
|
Watcher,
|
|
1292
1810
|
SecurityEngine,
|
|
1293
1811
|
discoverSessions,
|
|
1294
1812
|
startMcpServer
|
|
1295
1813
|
};
|
|
1296
|
-
//# sourceMappingURL=chunk-
|
|
1814
|
+
//# sourceMappingURL=chunk-LPXME2WB.js.map
|