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