agenttop 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1390 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SecurityEngine,
4
+ Watcher,
5
+ clearNickname,
6
+ discoverSessions,
7
+ getNicknames,
8
+ isFirstRun,
9
+ loadConfig,
10
+ resolveAlertLogPath,
11
+ rotateLogFile,
12
+ saveConfig,
13
+ setNickname,
14
+ startMcpServer
15
+ } from "./chunk-4I4UZNKS.js";
16
+
17
+ // src/index.tsx
18
+ import { readFileSync as readFileSync4 } from "fs";
19
+ import { join as join4, dirname as dirname4 } from "path";
20
+ import { fileURLToPath as fileURLToPath3 } from "url";
21
+ import React9 from "react";
22
+ import { render } from "ink";
23
+
24
+ // src/ui/App.tsx
25
+ import { useState as useState7, useEffect as useEffect5, useCallback as useCallback3 } from "react";
26
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput2, useStdout } from "ink";
27
+
28
+ // src/hooks/installer.ts
29
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, chmodSync } from "fs";
30
+ import { join, dirname } from "path";
31
+ import { homedir } from "os";
32
+ import { fileURLToPath } from "url";
33
+ var HOOK_FILENAME = "agenttop-guard.py";
34
+ var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
35
+ var getHookSource = () => {
36
+ const thisFile = fileURLToPath(import.meta.url);
37
+ const srcHooksDir = join(dirname(thisFile), "..", "src", "hooks");
38
+ const distHooksDir = join(dirname(thisFile), "hooks");
39
+ for (const dir of [distHooksDir, srcHooksDir]) {
40
+ const path = join(dir, HOOK_FILENAME);
41
+ if (existsSync(path)) return path;
42
+ }
43
+ const npmGlobalPath = join(dirname(thisFile), "..", "hooks", HOOK_FILENAME);
44
+ if (existsSync(npmGlobalPath)) return npmGlobalPath;
45
+ throw new Error(`cannot find ${HOOK_FILENAME} \u2014 is agenttop installed correctly?`);
46
+ };
47
+ var getHookTarget = () => {
48
+ const claudeHooksDir = join(homedir(), ".claude", "hooks");
49
+ mkdirSync(claudeHooksDir, { recursive: true });
50
+ return join(claudeHooksDir, HOOK_FILENAME);
51
+ };
52
+ var readSettings = () => {
53
+ if (!existsSync(SETTINGS_PATH)) {
54
+ return {};
55
+ }
56
+ try {
57
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
58
+ } catch {
59
+ return {};
60
+ }
61
+ };
62
+ var writeSettings = (settings) => {
63
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
64
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
65
+ };
66
+ var installHooks = () => {
67
+ const source = getHookSource();
68
+ const target = getHookTarget();
69
+ copyFileSync(source, target);
70
+ chmodSync(target, 493);
71
+ const settings = readSettings();
72
+ const hooks = settings.hooks ?? {};
73
+ const postToolUse = hooks.PostToolUse ?? [];
74
+ const hookCommand = target;
75
+ const allToolsMatcher = postToolUse.find(
76
+ (entry) => entry.matcher === "Bash|Read|Grep|Glob|WebFetch|WebSearch"
77
+ );
78
+ if (allToolsMatcher) {
79
+ const alreadyInstalled = allToolsMatcher.hooks.some((h) => h.command.includes("agenttop-guard"));
80
+ if (alreadyInstalled) {
81
+ process.stdout.write("agenttop hooks already installed\n");
82
+ return;
83
+ }
84
+ allToolsMatcher.hooks.push({ type: "command", command: hookCommand });
85
+ } else {
86
+ postToolUse.push({
87
+ matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch",
88
+ hooks: [{ type: "command", command: hookCommand }]
89
+ });
90
+ }
91
+ hooks.PostToolUse = postToolUse;
92
+ settings.hooks = hooks;
93
+ writeSettings(settings);
94
+ process.stdout.write(`agenttop hooks installed:
95
+ `);
96
+ process.stdout.write(` hook: ${target}
97
+ `);
98
+ process.stdout.write(` settings: ${SETTINGS_PATH}
99
+ `);
100
+ process.stdout.write(` matcher: PostToolUse (Bash|Read|Grep|Glob|WebFetch|WebSearch)
101
+ `);
102
+ };
103
+ var uninstallHooks = () => {
104
+ const settings = readSettings();
105
+ const hooks = settings.hooks ?? {};
106
+ const postToolUse = hooks.PostToolUse ?? [];
107
+ let removed = false;
108
+ for (const entry of postToolUse) {
109
+ const before = entry.hooks.length;
110
+ entry.hooks = entry.hooks.filter((h) => !h.command.includes("agenttop-guard"));
111
+ if (entry.hooks.length < before) removed = true;
112
+ }
113
+ hooks.PostToolUse = postToolUse.filter((e) => e.hooks.length > 0);
114
+ settings.hooks = hooks;
115
+ writeSettings(settings);
116
+ if (removed) {
117
+ process.stdout.write("agenttop hooks removed from Claude Code settings\n");
118
+ } else {
119
+ process.stdout.write("agenttop hooks were not installed\n");
120
+ }
121
+ };
122
+
123
+ // src/install-mcp.ts
124
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
125
+ import { join as join2 } from "path";
126
+ import { homedir as homedir2 } from "os";
127
+ var installMcpConfig = () => {
128
+ const settingsPath = join2(homedir2(), ".claude", "settings.json");
129
+ let settings = {};
130
+ try {
131
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
132
+ } catch {
133
+ }
134
+ const mcpServers = settings.mcpServers ?? {};
135
+ mcpServers.agenttop = { command: "agenttop", args: ["--mcp"] };
136
+ settings.mcpServers = mcpServers;
137
+ mkdirSync2(join2(homedir2(), ".claude"), { recursive: true });
138
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
139
+ process.stdout.write("agenttop MCP server registered in Claude Code settings\n");
140
+ process.stdout.write(` settings: ${settingsPath}
141
+ `);
142
+ };
143
+
144
+ // src/updates.ts
145
+ import { execSync, exec } from "child_process";
146
+ import { readFileSync as readFileSync3 } from "fs";
147
+ import { join as join3, dirname as dirname2 } from "path";
148
+ import { fileURLToPath as fileURLToPath2 } from "url";
149
+ var getPackageVersion = () => {
150
+ try {
151
+ const thisFile = fileURLToPath2(import.meta.url);
152
+ const pkgPath = join3(dirname2(thisFile), "..", "package.json");
153
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
154
+ return pkg.version || "0.0.0";
155
+ } catch {
156
+ return "0.0.0";
157
+ }
158
+ };
159
+ var checkForUpdate = () => {
160
+ const current = getPackageVersion();
161
+ try {
162
+ const latest = execSync("npm view agenttop version", { encoding: "utf-8", timeout: 5e3 }).trim();
163
+ return {
164
+ current,
165
+ latest,
166
+ available: latest !== current && compareVersions(latest, current) > 0
167
+ };
168
+ } catch {
169
+ return { current, latest: current, available: false };
170
+ }
171
+ };
172
+ var installUpdate = () => {
173
+ return new Promise((resolve, reject) => {
174
+ exec("npm install -g agenttop@latest", { timeout: 6e4 }, (err, stdout) => {
175
+ if (err) {
176
+ reject(err);
177
+ } else {
178
+ resolve(stdout.trim());
179
+ }
180
+ });
181
+ });
182
+ };
183
+ var compareVersions = (a, b) => {
184
+ const pa = a.split(".").map(Number);
185
+ const pb = b.split(".").map(Number);
186
+ for (let i = 0; i < 3; i++) {
187
+ const da = pa[i] ?? 0;
188
+ const db = pb[i] ?? 0;
189
+ if (da > db) return 1;
190
+ if (da < db) return -1;
191
+ }
192
+ return 0;
193
+ };
194
+
195
+ // src/ui/components/StatusBar.tsx
196
+ import React, { useState, useEffect } from "react";
197
+ import { Box, Text } from "ink";
198
+
199
+ // src/ui/theme.ts
200
+ var colors = {
201
+ primary: "#61AFEF",
202
+ secondary: "#98C379",
203
+ accent: "#C678DD",
204
+ warning: "#E5C07B",
205
+ error: "#E06C75",
206
+ critical: "#FF0000",
207
+ muted: "#5C6370",
208
+ text: "#ABB2BF",
209
+ bright: "#FFFFFF",
210
+ border: "#3E4451",
211
+ selected: "#2C313A",
212
+ header: "#61AFEF"
213
+ };
214
+ var severityColors = {
215
+ info: colors.muted,
216
+ warn: colors.warning,
217
+ high: colors.error,
218
+ critical: colors.critical
219
+ };
220
+ var toolColors = {
221
+ Bash: colors.error,
222
+ Read: colors.secondary,
223
+ Write: colors.accent,
224
+ Edit: colors.accent,
225
+ Grep: colors.primary,
226
+ Glob: colors.primary,
227
+ Task: colors.warning,
228
+ WebFetch: colors.warning,
229
+ WebSearch: colors.warning
230
+ };
231
+ var getToolColor = (toolName) => toolColors[toolName] || colors.text;
232
+
233
+ // src/ui/components/StatusBar.tsx
234
+ import { jsx, jsxs } from "react/jsx-runtime";
235
+ var StatusBar = React.memo(({ sessionCount, alertCount, version, updateInfo }) => {
236
+ const [time, setTime] = useState(/* @__PURE__ */ new Date());
237
+ useEffect(() => {
238
+ const interval = setInterval(() => setTime(/* @__PURE__ */ new Date()), 1e3);
239
+ return () => clearInterval(interval);
240
+ }, []);
241
+ const timeStr = time.toLocaleTimeString("en-GB", { hour12: false });
242
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: colors.border, paddingX: 1, justifyContent: "space-between", children: [
243
+ /* @__PURE__ */ jsxs(Text, { color: colors.header, bold: true, children: [
244
+ "agenttop v",
245
+ version
246
+ ] }),
247
+ /* @__PURE__ */ jsxs(Text, { color: colors.text, children: [
248
+ sessionCount,
249
+ " session",
250
+ sessionCount !== 1 ? "s" : ""
251
+ ] }),
252
+ alertCount > 0 && /* @__PURE__ */ jsxs(Text, { color: colors.error, bold: true, children: [
253
+ alertCount,
254
+ " alert",
255
+ alertCount !== 1 ? "s" : ""
256
+ ] }),
257
+ updateInfo?.available && /* @__PURE__ */ jsxs(Text, { color: colors.secondary, children: [
258
+ "v",
259
+ updateInfo.latest,
260
+ " available (u)"
261
+ ] }),
262
+ /* @__PURE__ */ jsx(Text, { color: colors.muted, children: timeStr })
263
+ ] });
264
+ });
265
+
266
+ // src/ui/components/SessionList.tsx
267
+ import React2 from "react";
268
+ import { Box as Box2, Text as Text2 } from "ink";
269
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
270
+ var formatModel = (model) => {
271
+ if (model.includes("opus")) return "opus";
272
+ if (model.includes("sonnet")) return "sonnet";
273
+ if (model.includes("haiku")) return "haiku";
274
+ return model.slice(0, 8);
275
+ };
276
+ var formatProject = (project) => {
277
+ const parts = project.split("/");
278
+ const last = parts[parts.length - 1] || project;
279
+ return last.length > 18 ? last.slice(0, 17) + "\u2026" : last;
280
+ };
281
+ var formatTokens = (n) => {
282
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
283
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
284
+ return String(n);
285
+ };
286
+ var SessionList = React2.memo(({ sessions, selectedIndex, focused, filter }) => {
287
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 28, borderStyle: "single", borderColor: focused ? colors.primary : colors.border, children: [
288
+ /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, children: [
289
+ /* @__PURE__ */ jsx2(Text2, { color: colors.header, bold: true, children: "SESSIONS" }),
290
+ filter && /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
291
+ " [",
292
+ filter,
293
+ "]"
294
+ ] })
295
+ ] }),
296
+ sessions.length === 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: colors.muted, italic: true, children: filter ? "No matching sessions" : "No active sessions" }) }),
297
+ sessions.map((session, i) => {
298
+ const isSelected = i === selectedIndex;
299
+ const indicator = isSelected ? ">" : " ";
300
+ const displayName = session.nickname || session.slug;
301
+ const totalIn = session.usage.inputTokens + session.usage.cacheReadTokens;
302
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
303
+ /* @__PURE__ */ jsxs2(
304
+ Text2,
305
+ {
306
+ color: isSelected ? colors.bright : colors.text,
307
+ bold: isSelected,
308
+ backgroundColor: isSelected ? colors.selected : void 0,
309
+ children: [
310
+ indicator,
311
+ " ",
312
+ displayName
313
+ ]
314
+ }
315
+ ),
316
+ session.nickname && /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
317
+ " ",
318
+ session.slug
319
+ ] }),
320
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
321
+ " ",
322
+ formatProject(session.project),
323
+ " | ",
324
+ formatModel(session.model)
325
+ ] }),
326
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
327
+ " ",
328
+ "CPU ",
329
+ session.cpu,
330
+ "% | ",
331
+ session.memMB,
332
+ "MB | ",
333
+ session.agentCount,
334
+ " ag"
335
+ ] }),
336
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
337
+ " ",
338
+ formatTokens(totalIn),
339
+ " in | ",
340
+ formatTokens(session.usage.outputTokens),
341
+ " out"
342
+ ] })
343
+ ] }, session.sessionId);
344
+ })
345
+ ] });
346
+ });
347
+
348
+ // src/ui/components/ActivityFeed.tsx
349
+ import React3 from "react";
350
+ import { Box as Box3, Text as Text3 } from "ink";
351
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
352
+ var formatTime = (ts) => {
353
+ const d = new Date(ts);
354
+ return d.toLocaleTimeString("en-GB", { hour12: false });
355
+ };
356
+ var summarizeInput = (call) => {
357
+ const input = call.toolInput;
358
+ switch (call.toolName) {
359
+ case "Bash":
360
+ return String(input.command || "").slice(0, 50);
361
+ case "Read":
362
+ case "Write":
363
+ case "Edit":
364
+ return String(input.file_path || "").split("/").slice(-2).join("/");
365
+ case "Grep":
366
+ return `pattern="${String(input.pattern || "").slice(0, 30)}"`;
367
+ case "Glob":
368
+ return String(input.pattern || "").slice(0, 40);
369
+ case "Task":
370
+ return String(input.description || "").slice(0, 40);
371
+ case "WebFetch":
372
+ case "WebSearch":
373
+ return String(input.url || input.query || "").slice(0, 40);
374
+ default:
375
+ return JSON.stringify(input).slice(0, 40);
376
+ }
377
+ };
378
+ var ActivityFeed = React3.memo(
379
+ ({ events, sessionSlug, focused, height, scrollOffset }) => {
380
+ const viewportRows = height - 2;
381
+ const totalEvents = events.length;
382
+ const start = Math.max(0, totalEvents - viewportRows - scrollOffset);
383
+ const end = start + viewportRows;
384
+ const visible = events.slice(start, end);
385
+ const isAtBottom = scrollOffset === 0;
386
+ const isAtTop = start === 0;
387
+ const canScroll = totalEvents > viewportRows;
388
+ return /* @__PURE__ */ jsxs3(
389
+ Box3,
390
+ {
391
+ flexDirection: "column",
392
+ flexGrow: 1,
393
+ borderStyle: "single",
394
+ borderColor: focused ? colors.primary : colors.border,
395
+ children: [
396
+ /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, justifyContent: "space-between", children: [
397
+ /* @__PURE__ */ jsxs3(Box3, { children: [
398
+ /* @__PURE__ */ jsx3(Text3, { color: colors.header, bold: true, children: "ACTIVITY" }),
399
+ sessionSlug && /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
400
+ " (",
401
+ sessionSlug,
402
+ ")"
403
+ ] })
404
+ ] }),
405
+ focused && canScroll && !isAtBottom && /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
406
+ "[",
407
+ totalEvents - end + viewportRows,
408
+ "/",
409
+ totalEvents,
410
+ "]"
411
+ ] })
412
+ ] }),
413
+ visible.length === 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, italic: true, children: sessionSlug ? "Waiting for activity..." : "Select a session" }) }),
414
+ visible.map((event, i) => /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
415
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
416
+ formatTime(event.timestamp),
417
+ " "
418
+ ] }),
419
+ /* @__PURE__ */ jsx3(Text3, { color: getToolColor(event.toolName), bold: true, children: event.toolName.padEnd(8) }),
420
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
421
+ " ",
422
+ summarizeInput(event)
423
+ ] })
424
+ ] }, `${event.timestamp}-${i}`)),
425
+ focused && canScroll && !isAtTop && visible.length > 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, children: isAtBottom ? "" : "G:bottom " }) })
426
+ ]
427
+ }
428
+ );
429
+ }
430
+ );
431
+
432
+ // src/ui/components/AlertBar.tsx
433
+ import React4 from "react";
434
+ import { Box as Box4, Text as Text4 } from "ink";
435
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
436
+ var formatTime2 = (ts) => {
437
+ const d = new Date(ts);
438
+ return d.toLocaleTimeString("en-GB", { hour12: false });
439
+ };
440
+ var severityIcon = {
441
+ info: "i",
442
+ warn: "!",
443
+ high: "!!",
444
+ critical: "!!!"
445
+ };
446
+ var AlertBar = React4.memo(({ alerts, maxVisible = 4 }) => {
447
+ const visible = alerts.slice(-maxVisible);
448
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "single", borderColor: alerts.length > 0 ? colors.error : colors.border, children: [
449
+ /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
450
+ /* @__PURE__ */ jsx4(Text4, { color: colors.error, bold: true, children: "ALERTS" }),
451
+ alerts.length === 0 && /* @__PURE__ */ jsx4(Text4, { color: colors.muted, children: " (none)" })
452
+ ] }),
453
+ visible.map((alert, i) => /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
454
+ /* @__PURE__ */ jsxs4(Text4, { color: severityColors[alert.severity] || colors.text, children: [
455
+ "[",
456
+ severityIcon[alert.severity] || "?",
457
+ "]"
458
+ ] }),
459
+ /* @__PURE__ */ jsxs4(Text4, { color: colors.muted, children: [
460
+ " ",
461
+ formatTime2(alert.timestamp),
462
+ " "
463
+ ] }),
464
+ /* @__PURE__ */ jsxs4(Text4, { color: colors.warning, children: [
465
+ alert.sessionSlug,
466
+ ": "
467
+ ] }),
468
+ /* @__PURE__ */ jsx4(Text4, { color: colors.text, children: alert.message.slice(0, 60) })
469
+ ] }, alert.id || i))
470
+ ] });
471
+ });
472
+
473
+ // src/ui/components/SessionDetail.tsx
474
+ import React5 from "react";
475
+ import { Box as Box5, Text as Text5 } from "ink";
476
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
477
+ var formatTokens2 = (n) => {
478
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
479
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
480
+ return String(n);
481
+ };
482
+ var formatUptime = (startTime) => {
483
+ const ms = Date.now() - startTime;
484
+ const secs = Math.floor(ms / 1e3);
485
+ const mins = Math.floor(secs / 60);
486
+ const hours = Math.floor(mins / 60);
487
+ if (hours > 0) return `${hours}h ${mins % 60}m`;
488
+ if (mins > 0) return `${mins}m ${secs % 60}s`;
489
+ return `${secs}s`;
490
+ };
491
+ var formatModel2 = (model) => {
492
+ if (model.includes("opus")) return "opus";
493
+ if (model.includes("sonnet")) return "sonnet";
494
+ if (model.includes("haiku")) return "haiku";
495
+ return model.slice(0, 20);
496
+ };
497
+ var SessionDetail = React5.memo(({ session, focused }) => {
498
+ const totalInput = session.usage.inputTokens + session.usage.cacheCreationTokens + session.usage.cacheReadTokens;
499
+ const totalTokens = totalInput + session.usage.outputTokens;
500
+ const cacheHitRate = totalInput > 0 ? (session.usage.cacheReadTokens / totalInput * 100).toFixed(0) : "0";
501
+ return /* @__PURE__ */ jsxs5(
502
+ Box5,
503
+ {
504
+ flexDirection: "column",
505
+ flexGrow: 1,
506
+ borderStyle: "single",
507
+ borderColor: focused ? colors.primary : colors.border,
508
+ children: [
509
+ /* @__PURE__ */ jsxs5(Box5, { paddingX: 1, children: [
510
+ /* @__PURE__ */ jsx5(Text5, { color: colors.header, bold: true, children: "SESSION DETAIL" }),
511
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " (Esc to return)" })
512
+ ] }),
513
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
514
+ /* @__PURE__ */ jsxs5(Box5, { children: [
515
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "slug: " }),
516
+ /* @__PURE__ */ jsx5(Text5, { color: colors.bright, bold: true, children: session.slug })
517
+ ] }),
518
+ session.nickname && /* @__PURE__ */ jsxs5(Box5, { children: [
519
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "nickname: " }),
520
+ /* @__PURE__ */ jsx5(Text5, { color: colors.secondary, children: session.nickname })
521
+ ] }),
522
+ /* @__PURE__ */ jsxs5(Box5, { children: [
523
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "model: " }),
524
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatModel2(session.model) })
525
+ ] }),
526
+ /* @__PURE__ */ jsxs5(Box5, { children: [
527
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "cwd: " }),
528
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: session.cwd })
529
+ ] }),
530
+ session.gitBranch && /* @__PURE__ */ jsxs5(Box5, { children: [
531
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "branch: " }),
532
+ /* @__PURE__ */ jsx5(Text5, { color: colors.secondary, children: session.gitBranch })
533
+ ] }),
534
+ /* @__PURE__ */ jsxs5(Box5, { children: [
535
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "version: " }),
536
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: session.version || "unknown" })
537
+ ] }),
538
+ /* @__PURE__ */ jsxs5(Box5, { children: [
539
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "pid: " }),
540
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: session.pid ?? "not running" })
541
+ ] }),
542
+ /* @__PURE__ */ jsxs5(Box5, { children: [
543
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "cpu: " }),
544
+ /* @__PURE__ */ jsxs5(Text5, { color: colors.text, children: [
545
+ session.cpu,
546
+ "%"
547
+ ] })
548
+ ] }),
549
+ /* @__PURE__ */ jsxs5(Box5, { children: [
550
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "memory: " }),
551
+ /* @__PURE__ */ jsxs5(Text5, { color: colors.text, children: [
552
+ session.memMB,
553
+ "MB"
554
+ ] })
555
+ ] }),
556
+ /* @__PURE__ */ jsxs5(Box5, { children: [
557
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "uptime: " }),
558
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatUptime(session.startTime) })
559
+ ] }),
560
+ /* @__PURE__ */ jsxs5(Box5, { children: [
561
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: "agents: " }),
562
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: session.agentCount })
563
+ ] }),
564
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: colors.header, bold: true, children: "Token usage" }) }),
565
+ /* @__PURE__ */ jsxs5(Box5, { children: [
566
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " input: " }),
567
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatTokens2(session.usage.inputTokens) })
568
+ ] }),
569
+ /* @__PURE__ */ jsxs5(Box5, { children: [
570
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " output: " }),
571
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatTokens2(session.usage.outputTokens) })
572
+ ] }),
573
+ /* @__PURE__ */ jsxs5(Box5, { children: [
574
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " cache write: " }),
575
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatTokens2(session.usage.cacheCreationTokens) })
576
+ ] }),
577
+ /* @__PURE__ */ jsxs5(Box5, { children: [
578
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " cache read: " }),
579
+ /* @__PURE__ */ jsx5(Text5, { color: colors.text, children: formatTokens2(session.usage.cacheReadTokens) })
580
+ ] }),
581
+ /* @__PURE__ */ jsxs5(Box5, { children: [
582
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " cache hit: " }),
583
+ /* @__PURE__ */ jsxs5(Text5, { color: session.usage.cacheReadTokens > 0 ? colors.secondary : colors.text, children: [
584
+ cacheHitRate,
585
+ "%"
586
+ ] })
587
+ ] }),
588
+ /* @__PURE__ */ jsxs5(Box5, { children: [
589
+ /* @__PURE__ */ jsx5(Text5, { color: colors.muted, children: " total: " }),
590
+ /* @__PURE__ */ jsx5(Text5, { color: colors.bright, bold: true, children: formatTokens2(totalTokens) })
591
+ ] })
592
+ ] })
593
+ ]
594
+ }
595
+ );
596
+ });
597
+
598
+ // src/ui/components/SetupModal.tsx
599
+ import React6, { useState as useState2 } from "react";
600
+ import { Box as Box6, Text as Text6, useInput } from "ink";
601
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
602
+ var OPTIONS = [
603
+ { label: "Yes", value: "yes" },
604
+ { label: "Not now", value: "not_now" },
605
+ { label: "Don't ask again", value: "dismiss" }
606
+ ];
607
+ var SetupModal = React6.memo(({ steps, onComplete }) => {
608
+ const [stepIndex, setStepIndex] = useState2(0);
609
+ const [selectedOption, setSelectedOption] = useState2(0);
610
+ const [results, setResults] = useState2([]);
611
+ const step = steps[stepIndex];
612
+ useInput((_input, key) => {
613
+ if (key.upArrow) {
614
+ setSelectedOption((i) => Math.max(0, i - 1));
615
+ }
616
+ if (key.downArrow) {
617
+ setSelectedOption((i) => Math.min(OPTIONS.length - 1, i + 1));
618
+ }
619
+ if (key.return) {
620
+ const choice = OPTIONS[selectedOption].value;
621
+ const newResults = [...results, choice];
622
+ if (stepIndex + 1 >= steps.length) {
623
+ onComplete(newResults);
624
+ } else {
625
+ setResults(newResults);
626
+ setStepIndex(stepIndex + 1);
627
+ setSelectedOption(0);
628
+ }
629
+ }
630
+ });
631
+ if (!step) return null;
632
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", paddingX: 4, paddingY: 2, children: /* @__PURE__ */ jsxs6(Box6, { borderStyle: "round", borderColor: colors.primary, flexDirection: "column", paddingX: 3, paddingY: 1, children: [
633
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: colors.header, bold: true, children: [
634
+ "agenttop setup (",
635
+ stepIndex + 1,
636
+ "/",
637
+ steps.length,
638
+ ")"
639
+ ] }) }),
640
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: colors.bright, bold: true, children: step.title }) }),
641
+ /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: colors.text, children: step.description }) }),
642
+ OPTIONS.map((opt, i) => /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: i === selectedOption ? colors.primary : colors.muted, children: [
643
+ i === selectedOption ? "> " : " ",
644
+ opt.label
645
+ ] }) }, opt.value)),
646
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: colors.muted, children: "Use arrow keys + Enter to select" }) })
647
+ ] }) });
648
+ });
649
+
650
+ // src/ui/components/FooterBar.tsx
651
+ import React7 from "react";
652
+ import { Box as Box7, Text as Text7 } from "ink";
653
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
654
+ var label = (key) => {
655
+ if (key === "tab") return "tab";
656
+ if (key === "shift+tab") return "S-tab";
657
+ if (key === "enter") return "enter";
658
+ return key;
659
+ };
660
+ var FooterBar = React7.memo(({ keybindings, updateStatus }) => /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
661
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
662
+ label(keybindings.quit),
663
+ ":quit"
664
+ ] }) }),
665
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
666
+ label(keybindings.navDown),
667
+ "/",
668
+ label(keybindings.navUp),
669
+ ":nav"
670
+ ] }) }),
671
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
672
+ label(keybindings.panelNext),
673
+ ":panel"
674
+ ] }) }),
675
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
676
+ label(keybindings.filter),
677
+ ":filter"
678
+ ] }) }),
679
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
680
+ label(keybindings.nickname),
681
+ ":name"
682
+ ] }) }),
683
+ /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: "#5C6370", children: [
684
+ label(keybindings.detail),
685
+ ":detail"
686
+ ] }) }),
687
+ updateStatus && /* @__PURE__ */ jsx7(Box7, { marginRight: 2, children: /* @__PURE__ */ jsx7(Text7, { color: colors.secondary, children: updateStatus }) })
688
+ ] }));
689
+
690
+ // src/ui/hooks/useSessions.ts
691
+ import { useState as useState3, useEffect as useEffect2, useCallback, useRef } from "react";
692
+ var ACTIVE_POLL_MS = 1e4;
693
+ var IDLE_POLL_MS = 3e4;
694
+ var useSessions = (allUsers, filter) => {
695
+ const [sessions, setSessions] = useState3([]);
696
+ const [selectedIndex, setSelectedIndex] = useState3(0);
697
+ const usageOverrides = useRef(/* @__PURE__ */ new Map());
698
+ const refresh = useCallback(() => {
699
+ const found = discoverSessions(allUsers);
700
+ const nicknames = getNicknames();
701
+ const enriched = found.map((s) => {
702
+ const override = usageOverrides.current.get(s.sessionId);
703
+ return {
704
+ ...s,
705
+ nickname: nicknames[s.sessionId],
706
+ usage: override ? {
707
+ inputTokens: s.usage.inputTokens + override.inputTokens,
708
+ cacheCreationTokens: s.usage.cacheCreationTokens + override.cacheCreationTokens,
709
+ cacheReadTokens: s.usage.cacheReadTokens + override.cacheReadTokens,
710
+ outputTokens: s.usage.outputTokens + override.outputTokens
711
+ } : s.usage
712
+ };
713
+ });
714
+ let filtered = enriched;
715
+ if (filter) {
716
+ const lower = filter.toLowerCase();
717
+ filtered = enriched.filter(
718
+ (s) => s.slug.toLowerCase().includes(lower) || s.nickname?.toLowerCase().includes(lower) || s.project.toLowerCase().includes(lower) || s.model.toLowerCase().includes(lower)
719
+ );
720
+ }
721
+ setSessions(filtered);
722
+ }, [allUsers, filter]);
723
+ useEffect2(() => {
724
+ refresh();
725
+ const pollMs = sessions.length > 0 ? ACTIVE_POLL_MS : IDLE_POLL_MS;
726
+ const interval = setInterval(refresh, pollMs);
727
+ return () => clearInterval(interval);
728
+ }, [refresh, sessions.length > 0]);
729
+ const selectedSession = sessions[selectedIndex] ?? null;
730
+ const selectNext = useCallback(() => {
731
+ setSelectedIndex((i) => Math.min(i + 1, Math.max(0, sessions.length - 1)));
732
+ }, [sessions.length]);
733
+ const selectPrev = useCallback(() => {
734
+ setSelectedIndex((i) => Math.max(i - 1, 0));
735
+ }, []);
736
+ const selectIndex = useCallback((i) => {
737
+ setSelectedIndex(i);
738
+ }, []);
739
+ const addUsage = useCallback((sessionId, usage) => {
740
+ const existing = usageOverrides.current.get(sessionId);
741
+ if (existing) {
742
+ usageOverrides.current.set(sessionId, {
743
+ inputTokens: existing.inputTokens + usage.inputTokens,
744
+ cacheCreationTokens: existing.cacheCreationTokens + usage.cacheCreationTokens,
745
+ cacheReadTokens: existing.cacheReadTokens + usage.cacheReadTokens,
746
+ outputTokens: existing.outputTokens + usage.outputTokens
747
+ });
748
+ } else {
749
+ usageOverrides.current.set(sessionId, usage);
750
+ }
751
+ }, []);
752
+ return { sessions, selectedSession, selectedIndex, selectNext, selectPrev, selectIndex, refresh, addUsage };
753
+ };
754
+
755
+ // src/ui/hooks/useActivityStream.ts
756
+ import { useState as useState4, useEffect as useEffect3, useRef as useRef2 } from "react";
757
+ var MAX_EVENTS = 200;
758
+ var useActivityStream = (session, allUsers) => {
759
+ const [events, setEvents] = useState4([]);
760
+ const watcherRef = useRef2(null);
761
+ useEffect3(() => {
762
+ setEvents([]);
763
+ if (!session) return;
764
+ const existingCalls = [];
765
+ const tempWatcher = new Watcher(() => {
766
+ }, allUsers);
767
+ for (const file of session.outputFiles) {
768
+ existingCalls.push(...tempWatcher.readExisting(file));
769
+ }
770
+ setEvents(existingCalls.slice(-MAX_EVENTS));
771
+ const handler = (calls) => {
772
+ const sessionCalls = calls.filter((c) => c.sessionId === session.sessionId);
773
+ if (sessionCalls.length === 0) return;
774
+ setEvents((prev) => [...prev, ...sessionCalls].slice(-MAX_EVENTS));
775
+ };
776
+ const watcher = new Watcher(handler, allUsers);
777
+ watcherRef.current = watcher;
778
+ watcher.start();
779
+ return () => {
780
+ watcher.stop();
781
+ watcherRef.current = null;
782
+ };
783
+ }, [session?.sessionId, allUsers]);
784
+ return events;
785
+ };
786
+
787
+ // src/ui/hooks/useAlerts.ts
788
+ import { useState as useState5, useEffect as useEffect4, useRef as useRef3 } from "react";
789
+
790
+ // src/notifications.ts
791
+ import { exec as exec2 } from "child_process";
792
+ import { platform } from "os";
793
+ var SEVERITY_ORDER = {
794
+ info: 0,
795
+ warn: 1,
796
+ high: 2,
797
+ critical: 3
798
+ };
799
+ var RATE_LIMIT_MS = 3e4;
800
+ var lastDesktopNotification = 0;
801
+ var notify = (alert, config) => {
802
+ if (SEVERITY_ORDER[alert.severity] < SEVERITY_ORDER[config.minSeverity]) return;
803
+ if (config.bell) {
804
+ process.stdout.write("\x07");
805
+ }
806
+ if (config.desktop) {
807
+ const now = Date.now();
808
+ if (now - lastDesktopNotification < RATE_LIMIT_MS) return;
809
+ lastDesktopNotification = now;
810
+ const title = `agenttop: ${alert.severity} alert`;
811
+ const body = `${alert.sessionSlug}: ${alert.message.slice(0, 100)}`;
812
+ const os = platform();
813
+ if (os === "linux") {
814
+ exec2(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, { timeout: 3e3 });
815
+ } else if (os === "darwin") {
816
+ exec2(`osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`, {
817
+ timeout: 3e3
818
+ });
819
+ }
820
+ }
821
+ };
822
+
823
+ // src/alerts/logger.ts
824
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
825
+ import { dirname as dirname3 } from "path";
826
+ var AlertLogger = class {
827
+ logPath;
828
+ writeCount = 0;
829
+ constructor(config) {
830
+ this.logPath = resolveAlertLogPath(config);
831
+ mkdirSync3(dirname3(this.logPath), { recursive: true });
832
+ }
833
+ log(alert) {
834
+ const entry = {
835
+ timestamp: new Date(alert.timestamp).toISOString(),
836
+ severity: alert.severity,
837
+ rule: alert.rule,
838
+ message: alert.message,
839
+ sessionSlug: alert.sessionSlug,
840
+ sessionId: alert.sessionId
841
+ };
842
+ try {
843
+ appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
844
+ this.writeCount++;
845
+ if (this.writeCount % 100 === 0) {
846
+ rotateLogFile(this.logPath);
847
+ }
848
+ } catch {
849
+ }
850
+ }
851
+ };
852
+
853
+ // src/ui/hooks/useAlerts.ts
854
+ var MAX_ALERTS = 100;
855
+ var useAlerts = (enabled, alertLevel, allUsers, config) => {
856
+ const [alerts, setAlerts] = useState5([]);
857
+ const engineRef = useRef3(new SecurityEngine(alertLevel));
858
+ const watcherRef = useRef3(null);
859
+ const loggerRef = useRef3(null);
860
+ useEffect4(() => {
861
+ if (!enabled) return;
862
+ engineRef.current = new SecurityEngine(alertLevel);
863
+ if (config?.alerts.enabled) {
864
+ loggerRef.current = new AlertLogger(config);
865
+ }
866
+ const securityHandler = (events) => {
867
+ const newAlerts = [];
868
+ for (const event of events) {
869
+ newAlerts.push(...engineRef.current.analyzeEvent(event));
870
+ }
871
+ if (newAlerts.length > 0) {
872
+ for (const alert of newAlerts) {
873
+ if (config) {
874
+ notify(alert, config.notifications);
875
+ }
876
+ loggerRef.current?.log(alert);
877
+ }
878
+ setAlerts((prev) => [...prev, ...newAlerts].slice(-MAX_ALERTS));
879
+ }
880
+ };
881
+ const watcher = new Watcher(() => {
882
+ }, allUsers, securityHandler);
883
+ watcherRef.current = watcher;
884
+ watcher.start();
885
+ return () => {
886
+ watcher.stop();
887
+ watcherRef.current = null;
888
+ loggerRef.current = null;
889
+ };
890
+ }, [enabled, alertLevel, allUsers]);
891
+ const clearAlerts = () => setAlerts([]);
892
+ return { alerts, clearAlerts };
893
+ };
894
+
895
+ // src/ui/hooks/useTextInput.ts
896
+ import { useState as useState6, useCallback as useCallback2 } from "react";
897
+ var useTextInput = (onConfirm) => {
898
+ const [value, setValue] = useState6("");
899
+ const [isActive, setIsActive] = useState6(false);
900
+ const start = useCallback2((initial = "") => {
901
+ setValue(initial);
902
+ setIsActive(true);
903
+ }, []);
904
+ const cancel = useCallback2(() => {
905
+ setValue("");
906
+ setIsActive(false);
907
+ }, []);
908
+ const confirm = useCallback2(() => {
909
+ const result = value;
910
+ setIsActive(false);
911
+ setValue("");
912
+ onConfirm?.(result);
913
+ return result;
914
+ }, [value, onConfirm]);
915
+ const handleInput = useCallback2(
916
+ (input, key) => {
917
+ if (!isActive) return false;
918
+ if (key.escape) {
919
+ cancel();
920
+ return true;
921
+ }
922
+ if (key.return) {
923
+ confirm();
924
+ return true;
925
+ }
926
+ if (key.backspace || key.delete) {
927
+ setValue((v) => v.slice(0, -1));
928
+ return true;
929
+ }
930
+ if (input && input.length === 1 && input >= " ") {
931
+ setValue((v) => v + input);
932
+ return true;
933
+ }
934
+ return true;
935
+ },
936
+ [isActive, cancel, confirm]
937
+ );
938
+ return { value, isActive, start, cancel, confirm, handleInput };
939
+ };
940
+
941
+ // src/ui/App.tsx
942
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
943
+ var matchKey = (binding, input, key) => {
944
+ if (binding === "tab") return Boolean(key.tab);
945
+ if (binding === "shift+tab") return Boolean(key.shift && key.tab);
946
+ if (binding === "enter") return Boolean(key.return);
947
+ return input === binding;
948
+ };
949
+ var App = ({ options, config, version, firstRun }) => {
950
+ const { exit } = useApp();
951
+ const { stdout } = useStdout();
952
+ const termHeight = stdout?.rows ?? 40;
953
+ const kb = config.keybindings;
954
+ const [activePanel, setActivePanel] = useState7("sessions");
955
+ const [activityScroll, setActivityScroll] = useState7(0);
956
+ const [inputMode, setInputMode] = useState7("normal");
957
+ const [showSetup, setShowSetup] = useState7(firstRun);
958
+ const [filter, setFilter] = useState7("");
959
+ const [updateInfo, setUpdateInfo] = useState7(null);
960
+ const [updateStatus, setUpdateStatus] = useState7("");
961
+ const [showDetail, setShowDetail] = useState7(false);
962
+ const { sessions, selectedSession, selectedIndex, selectNext, selectPrev } = useSessions(
963
+ options.allUsers,
964
+ filter || void 0
965
+ );
966
+ const events = useActivityStream(selectedSession, options.allUsers);
967
+ const { alerts } = useAlerts(!options.noSecurity, options.alertLevel, options.allUsers, config);
968
+ const nicknameInput = useTextInput((value) => {
969
+ if (selectedSession && value.trim()) setNickname(selectedSession.sessionId, value.trim());
970
+ setInputMode("normal");
971
+ });
972
+ const filterInput = useTextInput((value) => {
973
+ setFilter(value);
974
+ setInputMode("normal");
975
+ });
976
+ useEffect5(() => {
977
+ if (options.noUpdates || !config.updates.checkOnLaunch) return;
978
+ try {
979
+ const i = checkForUpdate();
980
+ if (i.available) setUpdateInfo(i);
981
+ } catch {
982
+ }
983
+ const iv = setInterval(() => {
984
+ try {
985
+ const i = checkForUpdate();
986
+ if (i.available) setUpdateInfo(i);
987
+ } catch {
988
+ }
989
+ }, config.updates.checkInterval);
990
+ return () => clearInterval(iv);
991
+ }, []);
992
+ const alertHeight = options.noSecurity ? 0 : 6;
993
+ const mainHeight = termHeight - 3 - alertHeight - 1 - (inputMode !== "normal" ? 1 : 0);
994
+ const viewportRows = mainHeight - 2;
995
+ const maxScroll = Math.max(0, events.length - viewportRows);
996
+ useEffect5(() => {
997
+ setActivityScroll(0);
998
+ }, [selectedSession?.sessionId]);
999
+ const handleSetupComplete = useCallback3(
1000
+ (results) => {
1001
+ const nc = { ...config };
1002
+ const [hc, mc] = results;
1003
+ if (hc === "yes") {
1004
+ try {
1005
+ installHooks();
1006
+ } catch {
1007
+ }
1008
+ nc.prompts.hook = "installed";
1009
+ } else if (hc === "dismiss") nc.prompts.hook = "dismissed";
1010
+ if (mc === "yes") {
1011
+ try {
1012
+ installMcpConfig();
1013
+ } catch {
1014
+ }
1015
+ nc.prompts.mcp = "installed";
1016
+ } else if (mc === "dismiss") nc.prompts.mcp = "dismissed";
1017
+ saveConfig(nc);
1018
+ setShowSetup(false);
1019
+ },
1020
+ [config]
1021
+ );
1022
+ const switchPanel = useCallback3((_dir) => {
1023
+ setActivePanel((p) => p === "sessions" ? "activity" : "sessions");
1024
+ }, []);
1025
+ useInput2((input, key) => {
1026
+ if (showSetup) return;
1027
+ if (inputMode === "nickname") {
1028
+ nicknameInput.handleInput(input, key);
1029
+ return;
1030
+ }
1031
+ if (inputMode === "filter") {
1032
+ if (key.escape) {
1033
+ setFilter("");
1034
+ setInputMode("normal");
1035
+ filterInput.cancel();
1036
+ return;
1037
+ }
1038
+ filterInput.handleInput(input, key);
1039
+ return;
1040
+ }
1041
+ if (matchKey(kb.quit, input, key)) {
1042
+ exit();
1043
+ return;
1044
+ }
1045
+ if (showDetail) {
1046
+ if (key.escape || key.return || key.leftArrow) {
1047
+ setShowDetail(false);
1048
+ }
1049
+ return;
1050
+ }
1051
+ if (matchKey(kb.detail, input, key) && selectedSession && activePanel === "sessions") {
1052
+ setShowDetail(true);
1053
+ return;
1054
+ }
1055
+ if (matchKey(kb.panelNext, input, key) || key.rightArrow) {
1056
+ switchPanel("next");
1057
+ return;
1058
+ }
1059
+ if (matchKey(kb.panelPrev, input, key) || key.leftArrow) {
1060
+ switchPanel("prev");
1061
+ return;
1062
+ }
1063
+ if (matchKey(kb.nickname, input, key) && selectedSession) {
1064
+ setInputMode("nickname");
1065
+ nicknameInput.start(selectedSession.nickname || "");
1066
+ return;
1067
+ }
1068
+ if (matchKey(kb.clearNickname, input, key) && selectedSession) {
1069
+ clearNickname(selectedSession.sessionId);
1070
+ return;
1071
+ }
1072
+ if (matchKey(kb.filter, input, key)) {
1073
+ setInputMode("filter");
1074
+ filterInput.start(filter);
1075
+ return;
1076
+ }
1077
+ if (key.escape && filter) {
1078
+ setFilter("");
1079
+ return;
1080
+ }
1081
+ if (matchKey(kb.update, input, key) && updateInfo?.available) {
1082
+ setUpdateStatus("updating...");
1083
+ installUpdate().then(() => setUpdateStatus(`updated to v${updateInfo.latest} \u2014 restart to apply`)).catch(() => setUpdateStatus("update failed"));
1084
+ return;
1085
+ }
1086
+ if (activePanel === "sessions") {
1087
+ if (matchKey(kb.navDown, input, key) || key.downArrow) selectNext();
1088
+ if (matchKey(kb.navUp, input, key) || key.upArrow) selectPrev();
1089
+ }
1090
+ if (activePanel === "activity") {
1091
+ if (matchKey(kb.navUp, input, key) || key.upArrow) setActivityScroll((s) => Math.min(s + 1, maxScroll));
1092
+ if (matchKey(kb.navDown, input, key) || key.downArrow) setActivityScroll((s) => Math.max(s - 1, 0));
1093
+ if (matchKey(kb.scrollBottom, input, key) || key.end) setActivityScroll(0);
1094
+ if (matchKey(kb.scrollTop, input, key) || key.home) setActivityScroll(maxScroll);
1095
+ }
1096
+ });
1097
+ if (showSetup) {
1098
+ const steps = [];
1099
+ if (config.prompts.hook === "pending")
1100
+ steps.push({
1101
+ title: "Install Claude Code hook?",
1102
+ description: "Adds a PostToolUse hook that blocks prompt injection attempts in real-time."
1103
+ });
1104
+ if (config.prompts.mcp === "pending")
1105
+ steps.push({
1106
+ title: "Install MCP server?",
1107
+ description: "Registers agenttop as an MCP server so Claude Code can query session status and alerts."
1108
+ });
1109
+ if (steps.length === 0) {
1110
+ setShowSetup(false);
1111
+ return null;
1112
+ }
1113
+ return /* @__PURE__ */ jsx8(SetupModal, { steps, onComplete: handleSetupComplete });
1114
+ }
1115
+ const rightPanel = showDetail && selectedSession ? /* @__PURE__ */ jsx8(SessionDetail, { session: selectedSession, focused: activePanel === "activity", height: mainHeight }) : /* @__PURE__ */ jsx8(
1116
+ ActivityFeed,
1117
+ {
1118
+ events,
1119
+ sessionSlug: selectedSession?.slug ?? null,
1120
+ focused: activePanel === "activity",
1121
+ height: mainHeight,
1122
+ scrollOffset: activityScroll
1123
+ }
1124
+ );
1125
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", height: termHeight, children: [
1126
+ /* @__PURE__ */ jsx8(StatusBar, { sessionCount: sessions.length, alertCount: alerts.length, version, updateInfo }),
1127
+ /* @__PURE__ */ jsxs8(Box8, { flexGrow: 1, height: mainHeight, children: [
1128
+ /* @__PURE__ */ jsx8(
1129
+ SessionList,
1130
+ {
1131
+ sessions,
1132
+ selectedIndex,
1133
+ focused: activePanel === "sessions",
1134
+ filter: filter || void 0
1135
+ }
1136
+ ),
1137
+ rightPanel
1138
+ ] }),
1139
+ !options.noSecurity && /* @__PURE__ */ jsx8(AlertBar, { alerts }),
1140
+ inputMode === "nickname" && /* @__PURE__ */ jsxs8(Box8, { paddingX: 1, children: [
1141
+ /* @__PURE__ */ jsx8(Text8, { color: colors.primary, children: "nickname: " }),
1142
+ /* @__PURE__ */ jsx8(Text8, { color: colors.bright, children: nicknameInput.value }),
1143
+ /* @__PURE__ */ jsx8(Text8, { color: colors.muted, children: "_" })
1144
+ ] }),
1145
+ inputMode === "filter" && /* @__PURE__ */ jsxs8(Box8, { paddingX: 1, children: [
1146
+ /* @__PURE__ */ jsx8(Text8, { color: colors.primary, children: "/" }),
1147
+ /* @__PURE__ */ jsx8(Text8, { color: colors.bright, children: filterInput.value }),
1148
+ /* @__PURE__ */ jsx8(Text8, { color: colors.muted, children: "_" })
1149
+ ] }),
1150
+ inputMode === "normal" && /* @__PURE__ */ jsx8(FooterBar, { keybindings: kb, updateStatus })
1151
+ ] });
1152
+ };
1153
+
1154
+ // src/stream.ts
1155
+ var write = (msg) => {
1156
+ process.stdout.write(msg + "\n");
1157
+ };
1158
+ var formatTokens3 = (n) => {
1159
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
1160
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
1161
+ return String(n);
1162
+ };
1163
+ var runStreamMode = (options, isJson) => {
1164
+ const engine = options.noSecurity ? null : new SecurityEngine(options.alertLevel);
1165
+ const sessions = discoverSessions(options.allUsers);
1166
+ if (isJson) {
1167
+ write(JSON.stringify({ type: "sessions", data: sessions }));
1168
+ } else {
1169
+ for (const s of sessions) {
1170
+ write(
1171
+ `SESSION ${s.slug} | ${s.model} | ${s.cwd} | CPU ${s.cpu}% | ${s.memMB}MB | ${formatTokens3(s.usage.inputTokens)} in / ${formatTokens3(s.usage.outputTokens)} out`
1172
+ );
1173
+ }
1174
+ }
1175
+ const handler = (calls) => {
1176
+ for (const call of calls) {
1177
+ if (isJson) {
1178
+ write(JSON.stringify({ type: "tool_call", data: call }));
1179
+ } else {
1180
+ const ts = new Date(call.timestamp).toLocaleTimeString("en-GB", { hour12: false });
1181
+ const input = call.toolName === "Bash" ? String(call.toolInput.command || "").slice(0, 80) : JSON.stringify(call.toolInput).slice(0, 80);
1182
+ write(`${ts} ${call.slug} ${call.toolName.padEnd(8)} ${input}`);
1183
+ }
1184
+ }
1185
+ };
1186
+ const securityHandler = engine ? (events) => {
1187
+ for (const event of events) {
1188
+ const alerts = engine.analyzeEvent(event);
1189
+ for (const alert of alerts) {
1190
+ if (isJson) {
1191
+ write(JSON.stringify({ type: "alert", data: alert }));
1192
+ } else {
1193
+ const ts = new Date(alert.timestamp).toLocaleTimeString("en-GB", { hour12: false });
1194
+ write(`ALERT ${ts} [${alert.severity}] ${alert.sessionSlug}: ${alert.message}`);
1195
+ }
1196
+ }
1197
+ }
1198
+ } : void 0;
1199
+ const usageHandler = (sessionId, usage) => {
1200
+ if (isJson) {
1201
+ write(JSON.stringify({ type: "usage", data: { sessionId, usage } }));
1202
+ } else {
1203
+ write(
1204
+ `USAGE ${sessionId.slice(0, 12)} +${formatTokens3(usage.inputTokens)} in / +${formatTokens3(usage.outputTokens)} out`
1205
+ );
1206
+ }
1207
+ };
1208
+ const watcher = new Watcher(handler, options.allUsers, securityHandler, usageHandler);
1209
+ watcher.start();
1210
+ process.on("SIGINT", () => {
1211
+ watcher.stop();
1212
+ process.exit(0);
1213
+ });
1214
+ process.on("SIGTERM", () => {
1215
+ watcher.stop();
1216
+ process.exit(0);
1217
+ });
1218
+ };
1219
+
1220
+ // src/index.tsx
1221
+ var getVersion = () => {
1222
+ try {
1223
+ const thisFile = fileURLToPath3(import.meta.url);
1224
+ const pkgPath = join4(dirname4(thisFile), "..", "package.json");
1225
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1226
+ return pkg.version || "0.0.0";
1227
+ } catch {
1228
+ return "0.0.0";
1229
+ }
1230
+ };
1231
+ var VERSION = getVersion();
1232
+ var HELP = `agenttop v${VERSION} -- Real-time dashboard for AI coding agent sessions
1233
+
1234
+ Usage: agenttop [options]
1235
+
1236
+ Options:
1237
+ --all-users Monitor all users (root only)
1238
+ --no-security Disable security analysis
1239
+ --json Stream events as JSON lines (no TUI)
1240
+ --plain Stream events as plain text (no TUI)
1241
+ --alert-level <l> Minimum: info|warn|high|critical (default: warn)
1242
+ --no-notify Disable all notifications
1243
+ --no-alert-log Disable alert file logging
1244
+ --no-updates Disable update checks
1245
+ --poll-interval <ms> Session discovery interval (default: 10000)
1246
+ --mcp Start as MCP server (for Claude Code integration)
1247
+ --install-mcp Register agenttop as MCP server in Claude Code
1248
+ --install-hooks Install Claude Code PostToolUse hook for active protection
1249
+ --uninstall-hooks Remove agenttop hooks from Claude Code
1250
+ --version Show version
1251
+ --help Show this help
1252
+
1253
+ Streaming modes (--json / --plain):
1254
+ Stream session events to stdout for piping into other tools.
1255
+ --json outputs one JSON object per line (JSONL format).
1256
+ --plain outputs human-readable lines.
1257
+
1258
+ Examples:
1259
+ agenttop --json | jq 'select(.type == "alert")'
1260
+ agenttop --plain | grep ALERT
1261
+ agenttop --json --no-security # stream tool calls only
1262
+ `;
1263
+ var write2 = (msg) => {
1264
+ process.stdout.write(msg + "\n");
1265
+ };
1266
+ var parseArgs = (argv) => {
1267
+ const args = argv.slice(2);
1268
+ const options = {
1269
+ allUsers: false,
1270
+ noSecurity: false,
1271
+ json: false,
1272
+ plain: false,
1273
+ alertLevel: "warn",
1274
+ installHooks: false,
1275
+ uninstallHooks: false,
1276
+ help: false,
1277
+ version: false,
1278
+ noNotify: false,
1279
+ noAlertLog: false,
1280
+ noUpdates: false,
1281
+ pollInterval: 1e4,
1282
+ mcp: false,
1283
+ installMcp: false
1284
+ };
1285
+ for (let i = 0; i < args.length; i++) {
1286
+ switch (args[i]) {
1287
+ case "--all-users":
1288
+ options.allUsers = true;
1289
+ break;
1290
+ case "--no-security":
1291
+ options.noSecurity = true;
1292
+ break;
1293
+ case "--json":
1294
+ options.json = true;
1295
+ break;
1296
+ case "--plain":
1297
+ options.plain = true;
1298
+ break;
1299
+ case "--alert-level":
1300
+ i++;
1301
+ if (["info", "warn", "high", "critical"].includes(args[i])) {
1302
+ options.alertLevel = args[i];
1303
+ }
1304
+ break;
1305
+ case "--no-notify":
1306
+ options.noNotify = true;
1307
+ break;
1308
+ case "--no-alert-log":
1309
+ options.noAlertLog = true;
1310
+ break;
1311
+ case "--no-updates":
1312
+ options.noUpdates = true;
1313
+ break;
1314
+ case "--poll-interval":
1315
+ i++;
1316
+ {
1317
+ const n = parseInt(args[i], 10);
1318
+ if (n > 0) options.pollInterval = n;
1319
+ }
1320
+ break;
1321
+ case "--mcp":
1322
+ options.mcp = true;
1323
+ break;
1324
+ case "--install-mcp":
1325
+ options.installMcp = true;
1326
+ break;
1327
+ case "--install-hooks":
1328
+ options.installHooks = true;
1329
+ break;
1330
+ case "--uninstall-hooks":
1331
+ options.uninstallHooks = true;
1332
+ break;
1333
+ case "--version":
1334
+ options.version = true;
1335
+ break;
1336
+ case "--help":
1337
+ options.help = true;
1338
+ break;
1339
+ }
1340
+ }
1341
+ return options;
1342
+ };
1343
+ var main = () => {
1344
+ const options = parseArgs(process.argv);
1345
+ if (options.version) {
1346
+ write2(`agenttop v${VERSION}`);
1347
+ process.exit(0);
1348
+ }
1349
+ if (options.help) {
1350
+ write2(HELP);
1351
+ process.exit(0);
1352
+ }
1353
+ if (options.installHooks) {
1354
+ installHooks();
1355
+ process.exit(0);
1356
+ }
1357
+ if (options.uninstallHooks) {
1358
+ uninstallHooks();
1359
+ process.exit(0);
1360
+ }
1361
+ if (options.installMcp) {
1362
+ installMcpConfig();
1363
+ process.exit(0);
1364
+ }
1365
+ if (options.mcp) {
1366
+ startMcpServer(options.allUsers, options.noSecurity).catch((err) => {
1367
+ process.stderr.write(`mcp server error: ${err}
1368
+ `);
1369
+ process.exit(1);
1370
+ });
1371
+ return;
1372
+ }
1373
+ if (options.json || options.plain) {
1374
+ runStreamMode(options, options.json);
1375
+ return;
1376
+ }
1377
+ const config = loadConfig();
1378
+ const firstRun = isFirstRun();
1379
+ if (options.noNotify) {
1380
+ config.notifications.bell = false;
1381
+ config.notifications.desktop = false;
1382
+ }
1383
+ if (options.noAlertLog) config.alerts.enabled = false;
1384
+ if (options.noUpdates) config.updates.checkOnLaunch = false;
1385
+ if (options.noSecurity) config.security.enabled = false;
1386
+ if (firstRun) saveConfig(config);
1387
+ render(React9.createElement(App, { options, config, version: VERSION, firstRun }));
1388
+ };
1389
+ main();
1390
+ //# sourceMappingURL=index.js.map