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.
@@ -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
- return xdg ? join(xdg, "agenttop") : join(homedir(), ".config", "agenttop");
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, readlinkSync, openSync, readSync, closeSync } from "fs";
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 = () => process.getuid?.() ?? 0;
210
- var isRoot = () => getUid() === 0;
211
- var getTmpDir = () => resolvePath(platform() === "darwin" ? "/private/tmp" : "/tmp");
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 = platform() === "darwin" ? "/Users" : "/home";
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 = platform() === "darwin" ? "/Users" : "/home";
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/sessions.ts
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 output = execSync("ps aux", { encoding: "utf-8", timeout: 5e3 });
266
- const procs = output.split("\n").filter((line) => line.includes("/claude") && !line.includes("grep") && !line.includes("agenttop")).map((line) => {
267
- const parts = line.trim().split(/\s+/);
268
- const pid = parseInt(parts[1], 10);
269
- let cwd = "";
270
- try {
271
- cwd = readlinkSync(`/proc/${pid}/cwd`);
272
- } catch {
273
- }
274
- return {
275
- pid,
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 aActive = a.pid !== null ? 1 : 0;
544
- const bActive = b.pid !== null ? 1 : 0;
545
- if (aActive !== bActive) return bActive - aActive;
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
- constructor(minLevel = "warn", rulesConfig) {
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: "0.3.0" },
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-24HX2MSZ.js.map
1814
+ //# sourceMappingURL=chunk-LPXME2WB.js.map