agenttop 0.10.7 → 0.11.1
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-CXXCDFJ5.js → chunk-27WRQSJY.js} +571 -54
- package/dist/chunk-27WRQSJY.js.map +1 -0
- package/dist/index.js +1149 -341
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-CXXCDFJ5.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,
|
|
@@ -74,7 +83,8 @@ var defaultConfig = () => ({
|
|
|
74
83
|
archiveExpiryDays: 0,
|
|
75
84
|
sidebarWidth: 30,
|
|
76
85
|
theme: "one-dark",
|
|
77
|
-
customThemes: {}
|
|
86
|
+
customThemes: {},
|
|
87
|
+
pinnedSessions: []
|
|
78
88
|
});
|
|
79
89
|
var deepMerge = (target, source) => {
|
|
80
90
|
const result = { ...target };
|
|
@@ -185,21 +195,46 @@ var deleteSessionFiles = (outputFiles) => {
|
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
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
|
+
};
|
|
188
220
|
|
|
189
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";
|
|
190
225
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
191
226
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
192
227
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
193
228
|
|
|
194
229
|
// src/discovery/sessions.ts
|
|
195
|
-
import { readdirSync as readdirSync2, statSync as statSync2,
|
|
230
|
+
import { readdirSync as readdirSync2, statSync as statSync2, openSync, readSync, closeSync } from "fs";
|
|
196
231
|
import { join as join3, basename } from "path";
|
|
197
|
-
import { execSync } from "child_process";
|
|
198
232
|
|
|
199
233
|
// src/config.ts
|
|
200
234
|
import { existsSync as existsSync2, realpathSync, readdirSync } from "fs";
|
|
201
|
-
import { homedir as homedir2, platform } from "os";
|
|
235
|
+
import { homedir as homedir2, platform, tmpdir, userInfo } from "os";
|
|
202
236
|
import { join as join2 } from "path";
|
|
237
|
+
var os = platform();
|
|
203
238
|
var resolvePath = (p) => {
|
|
204
239
|
try {
|
|
205
240
|
return realpathSync(p);
|
|
@@ -207,13 +242,25 @@ var resolvePath = (p) => {
|
|
|
207
242
|
return p;
|
|
208
243
|
}
|
|
209
244
|
};
|
|
210
|
-
var getUid = () =>
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
};
|
|
213
260
|
var getTaskDirs = (allUsers) => {
|
|
214
261
|
const tmp = getTmpDir();
|
|
215
262
|
const uid = getUid();
|
|
216
|
-
if (allUsers) {
|
|
263
|
+
if (allUsers && os !== "win32") {
|
|
217
264
|
try {
|
|
218
265
|
const dirs2 = readdirSync(tmp).filter((d) => d.startsWith("claude-")).filter((d) => !d.endsWith("-cwd")).map((d) => join2(tmp, d));
|
|
219
266
|
if (dirs2.length > 0) return dirs2;
|
|
@@ -243,50 +290,213 @@ var getProjectsDirs = (allUsers) => {
|
|
|
243
290
|
addDir(join2("/root", ".claude", "projects"));
|
|
244
291
|
const sudoUser = process.env["SUDO_USER"];
|
|
245
292
|
if (sudoUser) {
|
|
246
|
-
const homeBase =
|
|
293
|
+
const homeBase = os === "darwin" ? "/Users" : "/home";
|
|
247
294
|
addDir(join2(homeBase, sudoUser, ".claude", "projects"));
|
|
248
295
|
}
|
|
249
296
|
}
|
|
250
|
-
if (allUsers) {
|
|
297
|
+
if (allUsers && os !== "win32") {
|
|
251
298
|
try {
|
|
252
|
-
const homeBase =
|
|
299
|
+
const homeBase = os === "darwin" ? "/Users" : "/home";
|
|
253
300
|
for (const user of readdirSync(homeBase)) {
|
|
254
301
|
addDir(join2(homeBase, user, ".claude", "projects"));
|
|
255
302
|
}
|
|
256
303
|
} catch {
|
|
257
304
|
}
|
|
258
|
-
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
|
+
}
|
|
259
314
|
}
|
|
260
315
|
return dirs;
|
|
261
316
|
};
|
|
262
317
|
|
|
263
|
-
// 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
|
+
};
|
|
264
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 [];
|
|
265
472
|
try {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
cpu: parseFloat(parts[2]) || 0,
|
|
278
|
-
mem: parseFloat(parts[3]) || 0,
|
|
279
|
-
memKB: parseInt(parts[5], 10) || 0,
|
|
280
|
-
startTime: parts[8] || "",
|
|
281
|
-
command: parts.slice(10).join(" "),
|
|
282
|
-
cwd
|
|
283
|
-
};
|
|
284
|
-
}).filter((p) => !isNaN(p.pid)).filter((p) => !p.command.startsWith("sudo"));
|
|
285
|
-
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
|
+
}));
|
|
286
484
|
} catch {
|
|
287
485
|
return [];
|
|
288
486
|
}
|
|
289
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
|
|
290
500
|
var readFirstLines = (filePath, bytes) => {
|
|
291
501
|
try {
|
|
292
502
|
const fd = openSync(filePath, "r");
|
|
@@ -298,6 +508,44 @@ var readFirstLines = (filePath, bytes) => {
|
|
|
298
508
|
return [];
|
|
299
509
|
}
|
|
300
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
|
+
};
|
|
301
549
|
var readFirstEvent = (filePath) => {
|
|
302
550
|
const lines = readFirstLines(filePath, 16384);
|
|
303
551
|
if (lines.length === 0) return null;
|
|
@@ -363,7 +611,7 @@ var extractSessionMeta = (filePath) => {
|
|
|
363
611
|
if (!sessionId) return null;
|
|
364
612
|
return { sessionId, cwd, version, gitBranch, model, usage };
|
|
365
613
|
};
|
|
366
|
-
var discoverFromProjects = (allUsers, processes, sessionMap) => {
|
|
614
|
+
var discoverFromProjects = (allUsers, processes, sessionMap, staleTimeout, pinnedOrder) => {
|
|
367
615
|
const projectsDirs = getProjectsDirs(allUsers);
|
|
368
616
|
for (const projectsDir of projectsDirs) {
|
|
369
617
|
let projectNames;
|
|
@@ -416,14 +664,16 @@ var discoverFromProjects = (allUsers, processes, sessionMap) => {
|
|
|
416
664
|
outputFiles: [filePath],
|
|
417
665
|
startTime: fstat.birthtimeMs || fstat.ctimeMs,
|
|
418
666
|
lastActivity: fstat.mtimeMs,
|
|
419
|
-
usage: meta.usage
|
|
667
|
+
usage: meta.usage,
|
|
668
|
+
status: detectStatus(filePath, matchingProcess !== void 0, fstat.mtimeMs, staleTimeout),
|
|
669
|
+
pinned: pinnedOrder.includes(meta.sessionId)
|
|
420
670
|
};
|
|
421
671
|
sessionMap.set(meta.sessionId, session);
|
|
422
672
|
}
|
|
423
673
|
}
|
|
424
674
|
}
|
|
425
675
|
};
|
|
426
|
-
var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
676
|
+
var discoverFromTmp = (allUsers, processes, sessionMap, staleTimeout, pinnedOrder) => {
|
|
427
677
|
const taskDirs = getTaskDirs(allUsers);
|
|
428
678
|
for (const taskDir of taskDirs) {
|
|
429
679
|
let projectDirs;
|
|
@@ -511,6 +761,13 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
511
761
|
if (sessionMap.has(sessionId || projectName)) continue;
|
|
512
762
|
const normCwd = normalisePath(cwd);
|
|
513
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
|
+
});
|
|
514
771
|
const session = {
|
|
515
772
|
sessionId,
|
|
516
773
|
slug: slug || sessionId.slice(0, 12),
|
|
@@ -528,7 +785,9 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
528
785
|
outputFiles,
|
|
529
786
|
startTime: startTime === Infinity ? Date.now() : startTime,
|
|
530
787
|
lastActivity,
|
|
531
|
-
usage: totalUsage
|
|
788
|
+
usage: totalUsage,
|
|
789
|
+
status: detectStatus(latestFile, matchingProcess !== void 0, lastActivity, staleTimeout),
|
|
790
|
+
pinned: pinnedOrder.includes(sessionId)
|
|
532
791
|
};
|
|
533
792
|
sessionMap.set(sessionId || projectName, session);
|
|
534
793
|
}
|
|
@@ -536,22 +795,28 @@ var discoverFromTmp = (allUsers, processes, sessionMap) => {
|
|
|
536
795
|
}
|
|
537
796
|
};
|
|
538
797
|
var discoverSessions = (allUsers) => {
|
|
798
|
+
const config = loadConfig();
|
|
799
|
+
const staleTimeout = config.alerts.staleTimeout ?? 60;
|
|
800
|
+
const pinnedOrder = config.pinnedSessions ?? [];
|
|
539
801
|
const processes = getClaudeProcesses();
|
|
540
802
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
541
|
-
discoverFromProjects(allUsers, processes, sessionMap);
|
|
542
|
-
discoverFromTmp(allUsers, processes, sessionMap);
|
|
803
|
+
discoverFromProjects(allUsers, processes, sessionMap, staleTimeout, pinnedOrder);
|
|
804
|
+
discoverFromTmp(allUsers, processes, sessionMap, staleTimeout, pinnedOrder);
|
|
543
805
|
return Array.from(sessionMap.values()).sort((a, b) => {
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
|
|
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;
|
|
547
816
|
return b.lastActivity - a.lastActivity;
|
|
548
817
|
});
|
|
549
818
|
};
|
|
550
819
|
|
|
551
|
-
// src/discovery/types.ts
|
|
552
|
-
var isToolResult = (event) => "toolUseId" in event;
|
|
553
|
-
var isToolCall = (event) => "toolName" in event;
|
|
554
|
-
|
|
555
820
|
// src/analysis/rules/network.ts
|
|
556
821
|
var NETWORK_PATTERNS = [
|
|
557
822
|
/\bcurl\b/,
|
|
@@ -764,6 +1029,51 @@ var checkToolResultInjection = (result) => {
|
|
|
764
1029
|
};
|
|
765
1030
|
};
|
|
766
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
|
+
|
|
767
1077
|
// src/analysis/security.ts
|
|
768
1078
|
var toolCallRules = [
|
|
769
1079
|
{ key: "network", fn: checkNetwork },
|
|
@@ -783,7 +1093,8 @@ var SecurityEngine = class {
|
|
|
783
1093
|
recentAlerts = /* @__PURE__ */ new Map();
|
|
784
1094
|
minLevel;
|
|
785
1095
|
rulesConfig;
|
|
786
|
-
|
|
1096
|
+
customEventRules = [];
|
|
1097
|
+
constructor(minLevel = "warn", rulesConfig, customRules) {
|
|
787
1098
|
this.minLevel = minLevel;
|
|
788
1099
|
this.rulesConfig = rulesConfig ?? {
|
|
789
1100
|
network: true,
|
|
@@ -792,6 +1103,9 @@ var SecurityEngine = class {
|
|
|
792
1103
|
shellEscape: true,
|
|
793
1104
|
injection: true
|
|
794
1105
|
};
|
|
1106
|
+
if (customRules?.length) {
|
|
1107
|
+
this.customEventRules = createCustomRules(customRules);
|
|
1108
|
+
}
|
|
795
1109
|
}
|
|
796
1110
|
analyze(call) {
|
|
797
1111
|
return this.analyzeEvent(call);
|
|
@@ -813,6 +1127,10 @@ var SecurityEngine = class {
|
|
|
813
1127
|
const alert = rule.fn(event);
|
|
814
1128
|
if (alert) alerts.push(alert);
|
|
815
1129
|
}
|
|
1130
|
+
for (const fn of this.customEventRules) {
|
|
1131
|
+
const alert = fn(event);
|
|
1132
|
+
if (alert) alerts.push(alert);
|
|
1133
|
+
}
|
|
816
1134
|
return alerts.filter((alert) => {
|
|
817
1135
|
if (SEVERITY_ORDER[alert.severity] < SEVERITY_ORDER[this.minLevel]) return false;
|
|
818
1136
|
const dedupKey = `${alert.rule}-${alert.sessionId}-${alert.message.slice(0, 40)}`;
|
|
@@ -1068,7 +1386,7 @@ var Watcher = class {
|
|
|
1068
1386
|
this.watcher = watch(globs, {
|
|
1069
1387
|
persistent: true,
|
|
1070
1388
|
ignoreInitial: false,
|
|
1071
|
-
awaitWriteFinish:
|
|
1389
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
1072
1390
|
usePolling: false
|
|
1073
1391
|
});
|
|
1074
1392
|
this.watcher.on("add", (filePath) => {
|
|
@@ -1132,6 +1450,16 @@ var Watcher = class {
|
|
|
1132
1450
|
// src/mcp/server.ts
|
|
1133
1451
|
var MAX_ALERTS = 100;
|
|
1134
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
|
+
};
|
|
1135
1463
|
var startMcpServer = async (allUsers, noSecurity) => {
|
|
1136
1464
|
const alerts = [];
|
|
1137
1465
|
const activity = /* @__PURE__ */ new Map();
|
|
@@ -1154,7 +1482,7 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1154
1482
|
const watcher = new Watcher(toolHandler, allUsers, securityHandler);
|
|
1155
1483
|
watcher.start();
|
|
1156
1484
|
const server = new Server(
|
|
1157
|
-
{ name: "agenttop", version:
|
|
1485
|
+
{ name: "agenttop", version: getVersion() },
|
|
1158
1486
|
{
|
|
1159
1487
|
capabilities: { tools: {} }
|
|
1160
1488
|
}
|
|
@@ -1202,6 +1530,79 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1202
1530
|
},
|
|
1203
1531
|
required: ["sessionId"]
|
|
1204
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
|
+
}
|
|
1205
1606
|
}
|
|
1206
1607
|
]
|
|
1207
1608
|
}));
|
|
@@ -1218,7 +1619,9 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1218
1619
|
cpu: s.cpu,
|
|
1219
1620
|
memMB: s.memMB,
|
|
1220
1621
|
agents: s.agentCount,
|
|
1221
|
-
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
|
|
1222
1625
|
}));
|
|
1223
1626
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1224
1627
|
}
|
|
@@ -1257,6 +1660,115 @@ var startMcpServer = async (allUsers, noSecurity) => {
|
|
|
1257
1660
|
}));
|
|
1258
1661
|
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
|
|
1259
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
|
+
}
|
|
1260
1772
|
default:
|
|
1261
1773
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1262
1774
|
}
|
|
@@ -1287,11 +1799,16 @@ export {
|
|
|
1287
1799
|
getArchived,
|
|
1288
1800
|
purgeExpiredArchives,
|
|
1289
1801
|
deleteSessionFiles,
|
|
1802
|
+
pinSession,
|
|
1803
|
+
unpinSession,
|
|
1804
|
+
movePinned,
|
|
1290
1805
|
getTaskDirs,
|
|
1291
1806
|
getProjectsDirs,
|
|
1807
|
+
STATUS_PRIORITY,
|
|
1808
|
+
getClaudeProcessesAsync,
|
|
1292
1809
|
Watcher,
|
|
1293
1810
|
SecurityEngine,
|
|
1294
1811
|
discoverSessions,
|
|
1295
1812
|
startMcpServer
|
|
1296
1813
|
};
|
|
1297
|
-
//# sourceMappingURL=chunk-
|
|
1814
|
+
//# sourceMappingURL=chunk-27WRQSJY.js.map
|