@annals/agent-mesh 0.12.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.
@@ -0,0 +1,936 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/list.ts
4
+ import { spawn as spawn2 } from "child_process";
5
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
6
+
7
+ // src/utils/config.ts
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ var CONFIG_DIR = join(homedir(), ".agent-mesh");
12
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
13
+ var PIDS_DIR = join(CONFIG_DIR, "pids");
14
+ var LOGS_DIR = join(CONFIG_DIR, "logs");
15
+ function ensureDir() {
16
+ for (const dir of [CONFIG_DIR, PIDS_DIR, LOGS_DIR]) {
17
+ if (!existsSync(dir)) {
18
+ mkdirSync(dir, { recursive: true, mode: 448 });
19
+ }
20
+ }
21
+ }
22
+ function loadConfig() {
23
+ try {
24
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
25
+ return JSON.parse(raw);
26
+ } catch {
27
+ return { agents: {} };
28
+ }
29
+ }
30
+ function saveConfig(config) {
31
+ ensureDir();
32
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
33
+ }
34
+ function updateConfig(partial) {
35
+ const existing = loadConfig();
36
+ saveConfig({ ...existing, ...partial });
37
+ }
38
+ function getConfigPath() {
39
+ return CONFIG_FILE;
40
+ }
41
+ function getPidsDir() {
42
+ ensureDir();
43
+ return PIDS_DIR;
44
+ }
45
+ function getLogsDir() {
46
+ ensureDir();
47
+ return LOGS_DIR;
48
+ }
49
+ function getAgent(name) {
50
+ return loadConfig().agents[name];
51
+ }
52
+ function addAgent(name, entry) {
53
+ const config = loadConfig();
54
+ config.agents[name] = entry;
55
+ saveConfig(config);
56
+ }
57
+ function removeAgent(name) {
58
+ const config = loadConfig();
59
+ delete config.agents[name];
60
+ saveConfig(config);
61
+ }
62
+ function saveAgentStartTime(name, ts) {
63
+ const config = loadConfig();
64
+ if (config.agents[name]) {
65
+ config.agents[name].startedAt = ts;
66
+ saveConfig(config);
67
+ }
68
+ }
69
+ function listAgents() {
70
+ return loadConfig().agents;
71
+ }
72
+ function findAgentByAgentId(agentId) {
73
+ const agents = loadConfig().agents;
74
+ for (const [name, entry] of Object.entries(agents)) {
75
+ if (entry.agentId === agentId) return { name, entry };
76
+ }
77
+ return void 0;
78
+ }
79
+ var AGENTS_DIR = join(CONFIG_DIR, "agents");
80
+ function getAgentWorkspaceDir(name) {
81
+ const dir = join(AGENTS_DIR, name);
82
+ if (!existsSync(dir)) {
83
+ mkdirSync(dir, { recursive: true });
84
+ }
85
+ return dir;
86
+ }
87
+ function slugify(name) {
88
+ return name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "") || "agent";
89
+ }
90
+ function uniqueSlug(base) {
91
+ const agents = loadConfig().agents;
92
+ const slug = slugify(base);
93
+ if (!(slug in agents)) return slug;
94
+ for (let i = 2; ; i++) {
95
+ const candidate = `${slug}-${i}`;
96
+ if (!(candidate in agents)) return candidate;
97
+ }
98
+ }
99
+
100
+ // src/utils/process-manager.ts
101
+ import { spawn, execSync } from "child_process";
102
+ import {
103
+ readFileSync as readFileSync2,
104
+ writeFileSync as writeFileSync2,
105
+ unlinkSync,
106
+ readdirSync,
107
+ statSync,
108
+ renameSync,
109
+ openSync,
110
+ closeSync
111
+ } from "fs";
112
+ import { join as join2 } from "path";
113
+ import { homedir as homedir2 } from "os";
114
+ var MAX_LOG_SIZE = 5 * 1024 * 1024;
115
+ var MAX_LOG_FILES = 3;
116
+ function writePid(name, pid) {
117
+ const pidPath = join2(getPidsDir(), `${name}.pid`);
118
+ writeFileSync2(pidPath, String(pid), { mode: 384 });
119
+ }
120
+ function readPid(name) {
121
+ try {
122
+ const raw = readFileSync2(join2(getPidsDir(), `${name}.pid`), "utf-8").trim();
123
+ const pid = parseInt(raw, 10);
124
+ return Number.isFinite(pid) ? pid : null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ function removePid(name) {
130
+ try {
131
+ unlinkSync(join2(getPidsDir(), `${name}.pid`));
132
+ } catch {
133
+ }
134
+ }
135
+ function isProcessAlive(pid) {
136
+ try {
137
+ process.kill(pid, 0);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function cleanStalePids() {
144
+ let files;
145
+ try {
146
+ files = readdirSync(getPidsDir());
147
+ } catch {
148
+ return;
149
+ }
150
+ for (const file of files) {
151
+ if (!file.endsWith(".pid")) continue;
152
+ const name = file.slice(0, -4);
153
+ const pid = readPid(name);
154
+ if (pid !== null && !isProcessAlive(pid)) {
155
+ removePid(name);
156
+ }
157
+ }
158
+ }
159
+ function getLogPath(name) {
160
+ return join2(getLogsDir(), `${name}.log`);
161
+ }
162
+ function rotateLogIfNeeded(name) {
163
+ const logPath = getLogPath(name);
164
+ try {
165
+ const stat = statSync(logPath);
166
+ if (stat.size <= MAX_LOG_SIZE) return;
167
+ try {
168
+ unlinkSync(`${logPath}.${MAX_LOG_FILES - 1}`);
169
+ } catch {
170
+ }
171
+ for (let i = MAX_LOG_FILES - 2; i >= 0; i--) {
172
+ const from = i === 0 ? logPath : `${logPath}.${i}`;
173
+ const to = `${logPath}.${i + 1}`;
174
+ try {
175
+ renameSync(from, to);
176
+ } catch {
177
+ }
178
+ }
179
+ writeFileSync2(logPath, "", { mode: 384 });
180
+ } catch {
181
+ }
182
+ }
183
+ var _loginEnvCache = null;
184
+ function getLoginShellEnv() {
185
+ if (_loginEnvCache) return _loginEnvCache;
186
+ try {
187
+ const shell = process.env.SHELL || "/bin/zsh";
188
+ const isZsh = shell.endsWith("/zsh");
189
+ const cmd = isZsh ? `${shell} -c 'source ~/.zshrc 2>/dev/null; env'` : `${shell} -li -c env`;
190
+ const output = execSync(cmd, {
191
+ encoding: "utf-8",
192
+ timeout: 5e3,
193
+ stdio: ["ignore", "pipe", "ignore"]
194
+ });
195
+ const env = {};
196
+ for (const line of output.split("\n")) {
197
+ const eq = line.indexOf("=");
198
+ if (eq > 0) env[line.slice(0, eq)] = line.slice(eq + 1);
199
+ }
200
+ _loginEnvCache = env;
201
+ return env;
202
+ } catch {
203
+ return {};
204
+ }
205
+ }
206
+ function spawnBackground(name, entry, platformToken) {
207
+ rotateLogIfNeeded(name);
208
+ const logPath = getLogPath(name);
209
+ const logFd = openSync(logPath, "a", 384);
210
+ const args = [
211
+ process.argv[1],
212
+ "connect",
213
+ entry.agentType,
214
+ "--agent-id",
215
+ entry.agentId,
216
+ "--bridge-url",
217
+ entry.bridgeUrl
218
+ ];
219
+ if (entry.gatewayUrl) args.push("--gateway-url", entry.gatewayUrl);
220
+ if (entry.gatewayToken) args.push("--gateway-token", entry.gatewayToken);
221
+ if (entry.projectPath) args.push("--project", entry.projectPath);
222
+ if (entry.sandbox) args.push("--sandbox");
223
+ const loginEnv = getLoginShellEnv();
224
+ const env = {};
225
+ for (const [k, v] of Object.entries(loginEnv)) {
226
+ if (v !== void 0) env[k] = v;
227
+ }
228
+ for (const [k, v] of Object.entries(process.env)) {
229
+ if (v !== void 0 && k !== "PATH") env[k] = v;
230
+ }
231
+ const tokenForChild = entry.bridgeToken || platformToken;
232
+ if (tokenForChild) env.AGENT_BRIDGE_TOKEN = tokenForChild;
233
+ const pathSet = /* @__PURE__ */ new Set();
234
+ for (const src of [loginEnv.PATH, process.env.PATH, "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"]) {
235
+ if (src) for (const p of src.split(":")) {
236
+ if (p) pathSet.add(p);
237
+ }
238
+ }
239
+ env.PATH = [...pathSet].join(":");
240
+ const agentWorkspaceDir = join2(homedir2(), ".agent-mesh", "agents", name);
241
+ const child = spawn(process.execPath, args, {
242
+ detached: true,
243
+ stdio: ["ignore", logFd, logFd],
244
+ cwd: entry.projectPath || agentWorkspaceDir,
245
+ env
246
+ });
247
+ const pid = child.pid;
248
+ child.unref();
249
+ closeSync(logFd);
250
+ writePid(name, pid);
251
+ saveAgentStartTime(name, Date.now());
252
+ return pid;
253
+ }
254
+ function sleep(ms) {
255
+ return new Promise((resolve) => setTimeout(resolve, ms));
256
+ }
257
+ async function stopProcess(name) {
258
+ const pid = readPid(name);
259
+ if (pid === null || !isProcessAlive(pid)) {
260
+ removePid(name);
261
+ return false;
262
+ }
263
+ process.kill(pid, "SIGTERM");
264
+ for (let i = 0; i < 30; i++) {
265
+ await sleep(100);
266
+ if (!isProcessAlive(pid)) {
267
+ removePid(name);
268
+ return true;
269
+ }
270
+ }
271
+ try {
272
+ process.kill(pid, "SIGKILL");
273
+ } catch {
274
+ }
275
+ removePid(name);
276
+ return true;
277
+ }
278
+
279
+ // src/utils/table.ts
280
+ var RESET = "\x1B[0m";
281
+ var RED = "\x1B[31m";
282
+ var GREEN = "\x1B[32m";
283
+ var YELLOW = "\x1B[33m";
284
+ var GRAY = "\x1B[90m";
285
+ var BOLD = "\x1B[1m";
286
+ function pad(text, width, align = "left") {
287
+ const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
288
+ const diff = width - plain.length;
289
+ if (diff <= 0) return text;
290
+ const padding = " ".repeat(diff);
291
+ return align === "right" ? padding + text : text + padding;
292
+ }
293
+ function renderTable(columns, rows) {
294
+ const lines = [];
295
+ const header = columns.map((col) => pad(col.label, col.width, col.align)).join("");
296
+ lines.push(` ${BOLD}${GRAY}${header}${RESET}`);
297
+ for (const row of rows) {
298
+ const cells = columns.map((col) => {
299
+ const raw = row[col.key] ?? "";
300
+ const formatted = col.format ? col.format(raw) : raw;
301
+ return pad(formatted, col.width, col.align);
302
+ });
303
+ lines.push(` ${cells.join("")}`);
304
+ }
305
+ return lines.join("\n");
306
+ }
307
+
308
+ // src/commands/list.ts
309
+ var ALT_ON = "\x1B[?1049h";
310
+ var ALT_OFF = "\x1B[?1049l";
311
+ var CUR_HIDE = "\x1B[?25l";
312
+ var CUR_SHOW = "\x1B[?25h";
313
+ var HOME = "\x1B[H";
314
+ var CLR = "\x1B[K";
315
+ var DIM = "\x1B[2m";
316
+ var SPLIT_MIN_WIDTH = 90;
317
+ function sleep2(ms) {
318
+ return new Promise((r) => setTimeout(r, ms));
319
+ }
320
+ function pad2(text, width) {
321
+ const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
322
+ const diff = width - plain.length;
323
+ return diff > 0 ? text + " ".repeat(diff) : text;
324
+ }
325
+ function truncate(text, maxLen) {
326
+ const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
327
+ if (plain.length <= maxLen) return text;
328
+ return plain.slice(0, maxLen - 1) + "\u2026";
329
+ }
330
+ async function fetchRemoteStatus(token) {
331
+ const map = {};
332
+ if (!token) return map;
333
+ try {
334
+ const res = await fetch("https://agents.hot/api/developer/agents", {
335
+ headers: { Authorization: `Bearer ${token}` }
336
+ });
337
+ if (res.ok) {
338
+ for (const a of await res.json()) {
339
+ map[a.id] = a.is_online;
340
+ }
341
+ }
342
+ } catch {
343
+ }
344
+ return map;
345
+ }
346
+ function buildRows(agents, remote) {
347
+ return Object.keys(agents).sort().map((name) => {
348
+ const entry = agents[name];
349
+ const pidNum = readPid(name);
350
+ const alive = pidNum !== null && isProcessAlive(pidNum);
351
+ const isOnline = remote[entry.agentId];
352
+ let status, statusColor;
353
+ if (alive && isOnline === true) {
354
+ status = "\u25CF online";
355
+ statusColor = GREEN;
356
+ } else if (alive) {
357
+ status = "\u25D0 running";
358
+ statusColor = YELLOW;
359
+ } else {
360
+ status = "\u25CB stopped";
361
+ statusColor = GRAY;
362
+ }
363
+ return {
364
+ name,
365
+ entry,
366
+ type: entry.agentType,
367
+ status,
368
+ statusColor,
369
+ pid: alive && pidNum !== null ? String(pidNum) : "\u2014",
370
+ alive,
371
+ url: `agents.hot/agents/${entry.agentId}`
372
+ };
373
+ });
374
+ }
375
+ function getFailReason(name) {
376
+ try {
377
+ const logPath = getLogPath(name);
378
+ if (!existsSync2(logPath)) return null;
379
+ const content = readFileSync3(logPath, "utf-8");
380
+ const lines = content.split("\n").slice(-20);
381
+ const text = lines.join("\n");
382
+ if (/token.*revoked|revoked.*token/i.test(text)) {
383
+ return "Token revoked \u2014 run `agent-mesh login` to get a new token";
384
+ }
385
+ if (/auth_failed|Not authenticated/i.test(text)) {
386
+ return "Authentication failed \u2014 check your token";
387
+ }
388
+ if (/ECONNREFUSED|Gateway unreachable/i.test(text)) {
389
+ return "Agent runtime unreachable \u2014 check if gateway is running";
390
+ }
391
+ return null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ function getRecentLogLines(name, count) {
397
+ try {
398
+ const logPath = getLogPath(name);
399
+ if (!existsSync2(logPath)) return [];
400
+ const content = readFileSync3(logPath, "utf-8");
401
+ return content.split("\n").filter((l) => l.trim().length > 0).slice(-count);
402
+ } catch {
403
+ return [];
404
+ }
405
+ }
406
+ function estimateActiveSessions(name) {
407
+ try {
408
+ const logPath = getLogPath(name);
409
+ if (!existsSync2(logPath)) return 0;
410
+ const content = readFileSync3(logPath, "utf-8");
411
+ const lines = content.split("\n").slice(-200);
412
+ let active = 0;
413
+ for (const line of lines) {
414
+ if (/Message received/.test(line)) active++;
415
+ if (/Request done|request.*error|Session cleaned/.test(line)) active = Math.max(0, active - 1);
416
+ }
417
+ return active;
418
+ } catch {
419
+ return 0;
420
+ }
421
+ }
422
+ function formatUptime(startedAt) {
423
+ if (!startedAt) return "\u2014";
424
+ const ms = Date.now() - startedAt;
425
+ const s = Math.floor(ms / 1e3);
426
+ if (s < 60) return `${s}s`;
427
+ const m = Math.floor(s / 60);
428
+ if (m < 60) return `${m}m`;
429
+ const h = Math.floor(m / 60);
430
+ const rem = m % 60;
431
+ return rem > 0 ? `${h}h ${rem}m` : `${h}h`;
432
+ }
433
+ function formatLogLine(line) {
434
+ return line.replace(/^\d{4}-\d{2}-\d{2} (\d{2}:\d{2}):\d{2}\s+/, "$1 ");
435
+ }
436
+ function colorizeLogLine(line) {
437
+ if (/\bERROR\b/.test(line)) return RED + line + RESET;
438
+ if (/\bWARN\b/.test(line)) return YELLOW + line + RESET;
439
+ if (/\bINFO\b/.test(line)) return line;
440
+ return GRAY + line + RESET;
441
+ }
442
+ var W_NAME = 20;
443
+ var W_TYPE = 12;
444
+ var W_STATUS = 14;
445
+ var W_PID = 8;
446
+ function renderSingleScreen(rows, sel, msg) {
447
+ const out = [];
448
+ const ln = (s = "") => out.push(s + CLR);
449
+ ln();
450
+ ln(` ${BOLD}AGENT BRIDGE${RESET}`);
451
+ ln();
452
+ if (rows.length === 0) {
453
+ ln(` No agents registered. Use ${BOLD}agent-mesh connect --setup <url>${RESET} to add one.`);
454
+ ln();
455
+ ln(` ${DIM}q quit${RESET}`);
456
+ return out.join("\n");
457
+ }
458
+ ln(`${BOLD}${GRAY} ${"NAME".padEnd(W_NAME)}${"TYPE".padEnd(W_TYPE)}${"STATUS".padEnd(W_STATUS)}${"PID".padStart(W_PID)} URL${RESET}`);
459
+ for (let i = 0; i < rows.length; i++) {
460
+ const r = rows[i];
461
+ const isSel = i === sel;
462
+ const mark = isSel ? `${GREEN}\u25B8${RESET}` : " ";
463
+ const nm = isSel ? `${BOLD}${r.name}${RESET}` : r.name;
464
+ ln(`${mark} ${pad2(nm, W_NAME)}${pad2(r.type, W_TYPE)}${pad2(`${r.statusColor}${r.status}${RESET}`, W_STATUS)}${r.pid.padStart(W_PID)} ${GRAY}${r.url}${RESET}`);
465
+ }
466
+ let on = 0, run = 0, off = 0;
467
+ for (const r of rows) {
468
+ if (r.status.includes("online")) on++;
469
+ else if (r.status.includes("running")) run++;
470
+ else off++;
471
+ }
472
+ const parts = [`${rows.length} agents`];
473
+ if (on) parts.push(`${on} online`);
474
+ if (run) parts.push(`${run} running`);
475
+ if (off) parts.push(`${off} stopped`);
476
+ ln();
477
+ ln(` ${GRAY}${parts.join(" \xB7 ")}${RESET}`);
478
+ ln();
479
+ ln(msg ? ` ${msg}` : "");
480
+ ln();
481
+ ln(` ${DIM}\u2191\u2193${RESET} navigate ${DIM}s${RESET} start ${DIM}x${RESET} stop ${DIM}r${RESET} restart ${DIM}l${RESET} logs ${DIM}o${RESET} open ${DIM}d${RESET} remove ${DIM}q${RESET} quit`);
482
+ return out.join("\n");
483
+ }
484
+ function renderSplitScreen(rows, sel, msg, rightFocus, logScroll) {
485
+ const tw = process.stdout.columns || 120;
486
+ const th = process.stdout.rows || 30;
487
+ const LEFT_W = Math.max(22, Math.min(30, Math.floor(tw * 0.3)));
488
+ const RIGHT_W = tw - LEFT_W - 1;
489
+ const BODY_H = Math.max(4, th - 6);
490
+ const selRow = rows[sel];
491
+ const leftLines = [];
492
+ if (rows.length === 0) {
493
+ leftLines.push(` ${GRAY}No agents${RESET}`);
494
+ leftLines.push(` ${DIM}connect --setup <url>${RESET}`);
495
+ } else {
496
+ for (let i = 0; i < rows.length; i++) {
497
+ const r = rows[i];
498
+ const isSel = i === sel;
499
+ const mark = isSel ? rightFocus ? `${DIM}\u25B8${RESET}` : `${GREEN}\u25B8${RESET}` : " ";
500
+ const nameW = LEFT_W - 4;
501
+ const nm = truncate(isSel ? `${BOLD}${r.name}${RESET}` : r.name, nameW);
502
+ leftLines.push(`${mark} ${pad2(nm, nameW)} ${r.statusColor}${r.status.slice(0, 2)}${RESET}`);
503
+ }
504
+ }
505
+ const rightLines = [];
506
+ const rw = RIGHT_W - 3;
507
+ if (!selRow) {
508
+ rightLines.push(`${GRAY}No agent selected${RESET}`);
509
+ } else {
510
+ const sessions = selRow.alive ? estimateActiveSessions(selRow.name) : 0;
511
+ const uptime = selRow.alive ? formatUptime(selRow.entry.startedAt) : "\u2014";
512
+ const pidStr = selRow.alive ? `(PID ${selRow.pid})` : "";
513
+ const statusStr = `${selRow.statusColor}${selRow.status}${RESET}${pidStr ? ` ${GRAY}${pidStr}${RESET}` : ""}`;
514
+ rightLines.push(`${BOLD}${truncate(selRow.name, rw)}${RESET}`);
515
+ rightLines.push("\u2500".repeat(Math.min(rw, 40)));
516
+ rightLines.push(`${GRAY}Type: ${RESET}${selRow.type}`);
517
+ rightLines.push(`${GRAY}Status: ${RESET}${statusStr}`);
518
+ rightLines.push(`${GRAY}Sessions:${RESET} ${sessions > 0 ? `${GREEN}${sessions} active${RESET}` : `${GRAY}\u2014${RESET}`}`);
519
+ rightLines.push(`${GRAY}Uptime: ${RESET}${uptime}`);
520
+ rightLines.push(`${GRAY}URL: ${RESET}${GRAY}${truncate(selRow.url, rw - 9)}${RESET}`);
521
+ rightLines.push("");
522
+ const logHeaderLine = `${DIM}\u2500\u2500 Recent Log ${"\u2500".repeat(Math.max(0, Math.min(rw - 14, 20)))}${RESET}`;
523
+ rightLines.push(logHeaderLine);
524
+ const logLines = getRecentLogLines(selRow.name, 40);
525
+ if (logLines.length === 0) {
526
+ rightLines.push(`${GRAY}No log yet${RESET}`);
527
+ } else {
528
+ const logAreaH = Math.max(3, BODY_H - rightLines.length - 1);
529
+ const maxScroll = Math.max(0, logLines.length - logAreaH);
530
+ const clampedScroll = Math.min(logScroll, maxScroll);
531
+ const visibleLines = logLines.slice(
532
+ Math.max(0, logLines.length - logAreaH - clampedScroll),
533
+ logLines.length - clampedScroll || void 0
534
+ );
535
+ for (const l of visibleLines) {
536
+ const formatted = colorizeLogLine(formatLogLine(l));
537
+ rightLines.push(truncate(formatted, rw));
538
+ }
539
+ if (maxScroll > 0) {
540
+ const scrollInfo = clampedScroll > 0 ? `${GRAY}\u2191\u2193 scroll (${clampedScroll}/${maxScroll})${RESET}` : `${GRAY}\u2191 scroll up for more${RESET}`;
541
+ rightLines.push(scrollInfo);
542
+ }
543
+ }
544
+ }
545
+ const out = [HOME];
546
+ out.push(`${"\u2500".repeat(LEFT_W)}\u252C${"\u2500".repeat(RIGHT_W)}${CLR}`);
547
+ const leftHeader = pad2(` ${BOLD}AGENT BRIDGE${RESET}`, LEFT_W);
548
+ const rightHeader = selRow ? ` ${GRAY}${selRow.type}${RESET}` : " ";
549
+ out.push(`${leftHeader}\u2502${rightHeader}${CLR}`);
550
+ for (let row = 0; row < BODY_H; row++) {
551
+ const leftCell = leftLines[row] ?? "";
552
+ const rightCell = rightLines[row] ?? "";
553
+ const leftPad = LEFT_W - leftCell.replace(/\x1b\[[0-9;]*m/g, "").length;
554
+ const leftFull = ` ${leftCell}${leftPad > 0 ? " ".repeat(Math.max(0, leftPad - 2)) : ""}`;
555
+ const rightFull = ` ${rightCell}`;
556
+ out.push(`${truncate(leftFull, LEFT_W)}\u2502${rightFull}${CLR}`);
557
+ }
558
+ let on = 0, run = 0, off = 0;
559
+ for (const r of rows) {
560
+ if (r.status.includes("online")) on++;
561
+ else if (r.status.includes("running")) run++;
562
+ else off++;
563
+ }
564
+ const summaryParts = [`${rows.length} agent${rows.length !== 1 ? "s" : ""}`];
565
+ if (on) summaryParts.push(`${GREEN}${on} online${RESET}`);
566
+ if (run) summaryParts.push(`${YELLOW}${run} running${RESET}`);
567
+ if (off) summaryParts.push(`${GRAY}${off} stopped${RESET}`);
568
+ const summary = ` ${summaryParts.join(" \xB7 ")}`;
569
+ out.push(`${"\u2500".repeat(LEFT_W)}\u2534${"\u2500".repeat(RIGHT_W)}${CLR}`);
570
+ out.push(`${summary}${CLR}`);
571
+ out.push(msg ? ` ${msg}${CLR}` : `${CLR}`);
572
+ const focusHint = rightFocus ? `${DIM}\u2191\u2193${RESET} scroll log ${DIM}\u2190/Esc${RESET} focus list ${DIM}l${RESET} full logs ${DIM}q${RESET} quit` : `${DIM}\u2191\u2193${RESET} navigate ${DIM}s${RESET} start ${DIM}x${RESET} stop ${DIM}r${RESET} restart ${DIM}l${RESET} logs ${DIM}o${RESET} open ${DIM}Tab/\u2192${RESET} detail ${DIM}q${RESET} quit`;
573
+ out.push(` ${focusHint}${CLR}`);
574
+ return out.join("\n");
575
+ }
576
+ var ListTUI = class {
577
+ rows = [];
578
+ sel = 0;
579
+ msg = "";
580
+ ok = true;
581
+ busy = false;
582
+ confirm = null;
583
+ refreshTimer = null;
584
+ rightRefreshTimer = null;
585
+ msgTimer = null;
586
+ token;
587
+ keyHandler = (k) => this.onKey(k);
588
+ resizeHandler = () => this.draw();
589
+ // Split layout state
590
+ rightFocus = false;
591
+ logScroll = 0;
592
+ get isSplit() {
593
+ return (process.stdout.columns || 80) >= SPLIT_MIN_WIDTH;
594
+ }
595
+ async run() {
596
+ if (!process.stdin.isTTY) {
597
+ await this.staticFallback();
598
+ return;
599
+ }
600
+ this.ok = true;
601
+ process.stdout.write(ALT_ON + CUR_HIDE);
602
+ process.stdin.setRawMode(true);
603
+ process.stdin.resume();
604
+ process.stdin.setEncoding("utf-8");
605
+ await this.refresh();
606
+ this.draw();
607
+ this.refreshTimer = setInterval(() => {
608
+ if (!this.ok || this.busy) return;
609
+ this.refresh().then(() => this.draw());
610
+ }, 5e3);
611
+ this.rightRefreshTimer = setInterval(() => {
612
+ if (!this.ok || this.busy) return;
613
+ this.draw();
614
+ }, 2e3);
615
+ process.stdin.on("data", this.keyHandler);
616
+ process.stdout.on("resize", this.resizeHandler);
617
+ }
618
+ async refresh() {
619
+ cleanStalePids();
620
+ const cfg = loadConfig();
621
+ this.token = cfg.token;
622
+ const remote = await fetchRemoteStatus(cfg.token);
623
+ this.rows = buildRows(cfg.agents, remote);
624
+ if (this.sel >= this.rows.length) this.sel = Math.max(0, this.rows.length - 1);
625
+ }
626
+ draw() {
627
+ if (!this.ok) return;
628
+ const screen = this.isSplit ? renderSplitScreen(this.rows, this.sel, this.msg, this.rightFocus, this.logScroll) : renderSingleScreen(this.rows, this.sel, this.msg);
629
+ process.stdout.write(HOME + screen);
630
+ }
631
+ flash(m, ms = 3e3) {
632
+ this.msg = m;
633
+ if (this.msgTimer) clearTimeout(this.msgTimer);
634
+ if (ms > 0) {
635
+ this.msgTimer = setTimeout(() => {
636
+ this.msg = "";
637
+ this.draw();
638
+ }, ms);
639
+ }
640
+ }
641
+ onKey(k) {
642
+ if (k === "") {
643
+ this.exit();
644
+ return;
645
+ }
646
+ if (!this.busy && !this.confirm && (k === "q" || k === "Q" || k === "\x1B")) {
647
+ if (this.rightFocus && k === "\x1B") {
648
+ this.rightFocus = false;
649
+ this.logScroll = 0;
650
+ this.draw();
651
+ return;
652
+ }
653
+ if (!this.rightFocus) {
654
+ this.exit();
655
+ return;
656
+ }
657
+ }
658
+ if (this.confirm) {
659
+ if (k === "y" || k === "Y") {
660
+ const name = this.confirm.name;
661
+ this.confirm = null;
662
+ this.doRemove(name);
663
+ } else {
664
+ this.confirm = null;
665
+ this.flash("", 0);
666
+ this.draw();
667
+ }
668
+ return;
669
+ }
670
+ if (this.busy) return;
671
+ if ((k === " " || k === "\x1B[C") && this.isSplit) {
672
+ this.rightFocus = true;
673
+ this.logScroll = 0;
674
+ this.draw();
675
+ return;
676
+ }
677
+ if (k === "\x1B[D" && this.rightFocus) {
678
+ this.rightFocus = false;
679
+ this.logScroll = 0;
680
+ this.draw();
681
+ return;
682
+ }
683
+ if (k === "\x1B[A" || k === "k") {
684
+ if (this.rightFocus) {
685
+ this.logScroll++;
686
+ this.draw();
687
+ } else if (this.sel > 0) {
688
+ this.sel--;
689
+ this.logScroll = 0;
690
+ this.draw();
691
+ }
692
+ return;
693
+ }
694
+ if (k === "\x1B[B" || k === "j") {
695
+ if (this.rightFocus) {
696
+ this.logScroll = Math.max(0, this.logScroll - 1);
697
+ this.draw();
698
+ } else if (this.sel < this.rows.length - 1) {
699
+ this.sel++;
700
+ this.logScroll = 0;
701
+ this.draw();
702
+ }
703
+ return;
704
+ }
705
+ const row = this.rows[this.sel];
706
+ if (!row) return;
707
+ switch (k.toLowerCase()) {
708
+ case "s":
709
+ this.doStart(row);
710
+ break;
711
+ case "x":
712
+ this.doStop(row);
713
+ break;
714
+ case "r":
715
+ this.doRestart(row);
716
+ break;
717
+ case "l":
718
+ this.doLogs(row);
719
+ break;
720
+ case "o":
721
+ this.doOpen(row);
722
+ break;
723
+ case "\r":
724
+ this.doOpen(row);
725
+ break;
726
+ case "d":
727
+ this.confirm = { name: row.name };
728
+ this.flash(`${YELLOW}Remove ${BOLD}${row.name}${RESET}${YELLOW}? Press y to confirm${RESET}`, 15e3);
729
+ this.draw();
730
+ break;
731
+ }
732
+ }
733
+ async doStart(row) {
734
+ if (row.alive) {
735
+ this.flash(`${YELLOW}\u2298${RESET} ${BOLD}${row.name}${RESET} already running`);
736
+ this.draw();
737
+ return;
738
+ }
739
+ this.busy = true;
740
+ this.flash(`Starting ${row.name}...`, 1e4);
741
+ this.draw();
742
+ const pid = spawnBackground(row.name, row.entry, this.token);
743
+ await sleep2(600);
744
+ await this.refresh();
745
+ this.busy = false;
746
+ if (!isProcessAlive(pid)) {
747
+ const reason = getFailReason(row.name);
748
+ this.flash(reason ? `${RED}\u2717${RESET} ${BOLD}${row.name}${RESET} \u2014 ${reason}` : `${RED}\u2717${RESET} ${BOLD}${row.name}${RESET} failed to start \u2014 press ${BOLD}l${RESET} for logs`);
749
+ } else {
750
+ this.flash(`${GREEN}\u2713${RESET} ${BOLD}${row.name}${RESET} started (PID: ${pid})`);
751
+ this.schedulePostStartCheck(row.name, pid);
752
+ }
753
+ this.draw();
754
+ }
755
+ async doStop(row) {
756
+ if (!row.alive) {
757
+ this.flash(`${YELLOW}\u2298${RESET} ${BOLD}${row.name}${RESET} not running`);
758
+ this.draw();
759
+ return;
760
+ }
761
+ this.busy = true;
762
+ this.flash(`Stopping ${row.name}...`, 1e4);
763
+ this.draw();
764
+ const ok = await stopProcess(row.name);
765
+ await this.refresh();
766
+ this.busy = false;
767
+ this.flash(ok ? `${GREEN}\u2713${RESET} ${BOLD}${row.name}${RESET} stopped` : `${YELLOW}\u2298${RESET} ${BOLD}${row.name}${RESET} was not running`);
768
+ this.draw();
769
+ }
770
+ async doRestart(row) {
771
+ this.busy = true;
772
+ this.flash(`Restarting ${row.name}...`, 1e4);
773
+ this.draw();
774
+ if (row.alive) await stopProcess(row.name);
775
+ await sleep2(500);
776
+ const pid = spawnBackground(row.name, row.entry, this.token);
777
+ await sleep2(600);
778
+ await this.refresh();
779
+ this.busy = false;
780
+ if (!isProcessAlive(pid)) {
781
+ const reason = getFailReason(row.name);
782
+ this.flash(reason ? `${RED}\u2717${RESET} ${BOLD}${row.name}${RESET} \u2014 ${reason}` : `${RED}\u2717${RESET} ${BOLD}${row.name}${RESET} failed to restart \u2014 press ${BOLD}l${RESET} for logs`);
783
+ } else {
784
+ this.flash(`${GREEN}\u2713${RESET} ${BOLD}${row.name}${RESET} restarted (PID: ${pid})`);
785
+ this.schedulePostStartCheck(row.name, pid);
786
+ }
787
+ this.draw();
788
+ }
789
+ schedulePostStartCheck(name, pid) {
790
+ setTimeout(async () => {
791
+ if (!this.ok || this.busy) return;
792
+ if (!isProcessAlive(pid)) {
793
+ await this.refresh();
794
+ const reason = getFailReason(name);
795
+ this.flash(reason ? `${RED}\u2717${RESET} ${BOLD}${name}${RESET} \u2014 ${reason}` : `${RED}\u2717${RESET} ${BOLD}${name}${RESET} exited shortly after start \u2014 press ${BOLD}l${RESET} for logs`, 8e3);
796
+ this.draw();
797
+ }
798
+ }, 3e3);
799
+ }
800
+ async doLogs(row) {
801
+ const logPath = getLogPath(row.name);
802
+ if (!existsSync2(logPath)) {
803
+ this.flash(`${YELLOW}No logs yet for ${BOLD}${row.name}${RESET}`);
804
+ this.draw();
805
+ return;
806
+ }
807
+ this.busy = true;
808
+ if (this.refreshTimer) {
809
+ clearInterval(this.refreshTimer);
810
+ this.refreshTimer = null;
811
+ }
812
+ if (this.rightRefreshTimer) {
813
+ clearInterval(this.rightRefreshTimer);
814
+ this.rightRefreshTimer = null;
815
+ }
816
+ process.stdin.removeListener("data", this.keyHandler);
817
+ process.stdout.removeListener("resize", this.resizeHandler);
818
+ process.stdin.setRawMode(false);
819
+ process.stdin.pause();
820
+ process.stdout.write(ALT_OFF + CUR_SHOW);
821
+ console.log(`
822
+ \u2500\u2500\u2500 ${BOLD}${row.name}${RESET} (${row.type}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
823
+ console.log(` ${GRAY}${logPath}${RESET}`);
824
+ console.log(` ${GRAY}Press Ctrl+C to return to list${RESET}
825
+ `);
826
+ const noop = () => {
827
+ };
828
+ process.on("SIGINT", noop);
829
+ const tail = spawn2("tail", ["-f", "-n", "50", logPath], { stdio: "inherit" });
830
+ await new Promise((resolve) => {
831
+ tail.on("close", resolve);
832
+ tail.on("error", resolve);
833
+ });
834
+ process.removeListener("SIGINT", noop);
835
+ process.stdout.write(ALT_ON + CUR_HIDE);
836
+ process.stdin.setRawMode(true);
837
+ process.stdin.resume();
838
+ process.stdin.setEncoding("utf-8");
839
+ process.stdin.on("data", this.keyHandler);
840
+ process.stdout.on("resize", this.resizeHandler);
841
+ await this.refresh();
842
+ this.busy = false;
843
+ this.draw();
844
+ this.refreshTimer = setInterval(() => {
845
+ if (!this.ok || this.busy) return;
846
+ this.refresh().then(() => this.draw());
847
+ }, 5e3);
848
+ this.rightRefreshTimer = setInterval(() => {
849
+ if (!this.ok || this.busy) return;
850
+ this.draw();
851
+ }, 2e3);
852
+ }
853
+ doOpen(row) {
854
+ const url = `https://agents.hot/agents/${row.entry.agentId}`;
855
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
856
+ const child = spawn2(cmd, [url], { detached: true, stdio: "ignore" });
857
+ child.unref();
858
+ this.flash(`${GREEN}\u2713${RESET} Opened ${GRAY}${url}${RESET}`);
859
+ this.draw();
860
+ }
861
+ async doRemove(name) {
862
+ this.busy = true;
863
+ this.flash(`Removing ${name}...`, 1e4);
864
+ this.draw();
865
+ await stopProcess(name);
866
+ removeAgent(name);
867
+ removePid(name);
868
+ await this.refresh();
869
+ this.busy = false;
870
+ this.flash(`${GREEN}\u2713${RESET} ${BOLD}${name}${RESET} removed`);
871
+ this.draw();
872
+ }
873
+ exit() {
874
+ if (!this.ok) return;
875
+ this.ok = false;
876
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
877
+ if (this.rightRefreshTimer) clearInterval(this.rightRefreshTimer);
878
+ if (this.msgTimer) clearTimeout(this.msgTimer);
879
+ process.stdout.removeListener("resize", this.resizeHandler);
880
+ process.stdout.write(ALT_OFF + CUR_SHOW);
881
+ process.stdin.setRawMode(false);
882
+ process.stdin.pause();
883
+ process.exit(0);
884
+ }
885
+ async staticFallback() {
886
+ cleanStalePids();
887
+ const cfg = loadConfig();
888
+ const names = Object.keys(cfg.agents);
889
+ if (names.length === 0) {
890
+ console.log("No agents registered.");
891
+ return;
892
+ }
893
+ const remote = await fetchRemoteStatus(cfg.token);
894
+ const rows = buildRows(cfg.agents, remote);
895
+ console.log("");
896
+ console.log(` ${BOLD}${GRAY}${"NAME".padEnd(W_NAME)}${"TYPE".padEnd(W_TYPE)}${"STATUS".padEnd(W_STATUS)}${"PID".padStart(W_PID)} URL${RESET}`);
897
+ for (const r of rows) {
898
+ console.log(` ${pad2(`${BOLD}${r.name}${RESET}`, W_NAME)}${pad2(r.type, W_TYPE)}${pad2(`${r.statusColor}${r.status}${RESET}`, W_STATUS)}${r.pid.padStart(W_PID)} ${GRAY}${r.url}${RESET}`);
899
+ }
900
+ console.log("");
901
+ }
902
+ };
903
+ function registerListCommand(program) {
904
+ program.command("list").alias("ls").description("Interactive agent management dashboard").action(async () => {
905
+ const tui = new ListTUI();
906
+ await tui.run();
907
+ });
908
+ }
909
+
910
+ export {
911
+ loadConfig,
912
+ updateConfig,
913
+ getConfigPath,
914
+ getAgent,
915
+ addAgent,
916
+ removeAgent,
917
+ listAgents,
918
+ findAgentByAgentId,
919
+ getAgentWorkspaceDir,
920
+ uniqueSlug,
921
+ writePid,
922
+ readPid,
923
+ removePid,
924
+ isProcessAlive,
925
+ getLogPath,
926
+ spawnBackground,
927
+ stopProcess,
928
+ RESET,
929
+ GREEN,
930
+ YELLOW,
931
+ GRAY,
932
+ BOLD,
933
+ renderTable,
934
+ ListTUI,
935
+ registerListCommand
936
+ };