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/LICENSE +21 -0
- package/README.md +351 -0
- package/dist/chunk-4I4UZNKS.js +1019 -0
- package/dist/chunk-4I4UZNKS.js.map +1 -0
- package/dist/index.js +1390 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.js +14 -0
- package/dist/mcp-server.js.map +1 -0
- package/hooks/agenttop-guard.py +120 -0
- package/package.json +62 -0
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
|