@appkit/llamacpp-cli 1.4.1 → 1.6.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +136 -1
  5. package/dist/cli.js +21 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/create.d.ts.map +1 -1
  8. package/dist/commands/create.js +12 -3
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/monitor.d.ts +2 -0
  11. package/dist/commands/monitor.d.ts.map +1 -0
  12. package/dist/commands/monitor.js +126 -0
  13. package/dist/commands/monitor.js.map +1 -0
  14. package/dist/commands/ps.d.ts +3 -1
  15. package/dist/commands/ps.d.ts.map +1 -1
  16. package/dist/commands/ps.js +75 -5
  17. package/dist/commands/ps.js.map +1 -1
  18. package/dist/commands/server-show.d.ts.map +1 -1
  19. package/dist/commands/server-show.js +10 -3
  20. package/dist/commands/server-show.js.map +1 -1
  21. package/dist/commands/start.d.ts.map +1 -1
  22. package/dist/commands/start.js +14 -2
  23. package/dist/commands/start.js.map +1 -1
  24. package/dist/lib/history-manager.d.ts +46 -0
  25. package/dist/lib/history-manager.d.ts.map +1 -0
  26. package/dist/lib/history-manager.js +157 -0
  27. package/dist/lib/history-manager.js.map +1 -0
  28. package/dist/lib/metrics-aggregator.d.ts +40 -0
  29. package/dist/lib/metrics-aggregator.d.ts.map +1 -0
  30. package/dist/lib/metrics-aggregator.js +211 -0
  31. package/dist/lib/metrics-aggregator.js.map +1 -0
  32. package/dist/lib/system-collector.d.ts +80 -0
  33. package/dist/lib/system-collector.d.ts.map +1 -0
  34. package/dist/lib/system-collector.js +311 -0
  35. package/dist/lib/system-collector.js.map +1 -0
  36. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  37. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  38. package/dist/tui/HistoricalMonitorApp.js +490 -0
  39. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  40. package/dist/tui/MonitorApp.d.ts +4 -0
  41. package/dist/tui/MonitorApp.d.ts.map +1 -0
  42. package/dist/tui/MonitorApp.js +315 -0
  43. package/dist/tui/MonitorApp.js.map +1 -0
  44. package/dist/tui/MultiServerMonitorApp.d.ts +4 -0
  45. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -0
  46. package/dist/tui/MultiServerMonitorApp.js +712 -0
  47. package/dist/tui/MultiServerMonitorApp.js.map +1 -0
  48. package/dist/types/history-types.d.ts +30 -0
  49. package/dist/types/history-types.d.ts.map +1 -0
  50. package/dist/types/history-types.js +11 -0
  51. package/dist/types/history-types.js.map +1 -0
  52. package/dist/types/monitor-types.d.ts +123 -0
  53. package/dist/types/monitor-types.d.ts.map +1 -0
  54. package/dist/types/monitor-types.js +3 -0
  55. package/dist/types/monitor-types.js.map +1 -0
  56. package/dist/types/server-config.d.ts +1 -0
  57. package/dist/types/server-config.d.ts.map +1 -1
  58. package/dist/types/server-config.js.map +1 -1
  59. package/dist/utils/downsample-utils.d.ts +35 -0
  60. package/dist/utils/downsample-utils.d.ts.map +1 -0
  61. package/dist/utils/downsample-utils.js +107 -0
  62. package/dist/utils/downsample-utils.js.map +1 -0
  63. package/dist/utils/file-utils.d.ts +6 -0
  64. package/dist/utils/file-utils.d.ts.map +1 -1
  65. package/dist/utils/file-utils.js +38 -0
  66. package/dist/utils/file-utils.js.map +1 -1
  67. package/dist/utils/process-utils.d.ts +35 -2
  68. package/dist/utils/process-utils.d.ts.map +1 -1
  69. package/dist/utils/process-utils.js +220 -25
  70. package/dist/utils/process-utils.js.map +1 -1
  71. package/docs/images/.gitkeep +1 -0
  72. package/package.json +5 -1
  73. package/src/cli.ts +21 -4
  74. package/src/commands/create.ts +14 -4
  75. package/src/commands/monitor.ts +110 -0
  76. package/src/commands/ps.ts +88 -5
  77. package/src/commands/server-show.ts +10 -3
  78. package/src/commands/start.ts +15 -2
  79. package/src/lib/history-manager.ts +172 -0
  80. package/src/lib/metrics-aggregator.ts +257 -0
  81. package/src/lib/system-collector.ts +315 -0
  82. package/src/tui/HistoricalMonitorApp.ts +548 -0
  83. package/src/tui/MonitorApp.ts +386 -0
  84. package/src/tui/MultiServerMonitorApp.ts +792 -0
  85. package/src/types/history-types.ts +39 -0
  86. package/src/types/monitor-types.ts +162 -0
  87. package/src/types/server-config.ts +1 -0
  88. package/src/utils/downsample-utils.ts +128 -0
  89. package/src/utils/file-utils.ts +40 -0
  90. package/src/utils/process-utils.ts +243 -25
  91. package/test-load.sh +100 -0
@@ -0,0 +1,712 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createMultiServerMonitorUI = createMultiServerMonitorUI;
40
+ const blessed_1 = __importDefault(require("blessed"));
41
+ const metrics_aggregator_js_1 = require("../lib/metrics-aggregator.js");
42
+ const system_collector_js_1 = require("../lib/system-collector.js");
43
+ const history_manager_js_1 = require("../lib/history-manager.js");
44
+ const HistoricalMonitorApp_js_1 = require("./HistoricalMonitorApp.js");
45
+ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, directJumpIndex) {
46
+ let updateInterval = 2000;
47
+ let intervalId = null;
48
+ let viewMode = directJumpIndex !== undefined ? 'detail' : 'list';
49
+ let selectedServerIndex = directJumpIndex ?? 0;
50
+ let selectedRowIndex = directJumpIndex ?? 0; // Track which row is highlighted in list view
51
+ let isLoading = false;
52
+ let lastSystemMetrics = null;
53
+ let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
54
+ let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
55
+ // Spinner animation
56
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
57
+ let spinnerFrameIndex = 0;
58
+ let spinnerIntervalId = null;
59
+ const systemCollector = new system_collector_js_1.SystemCollector();
60
+ const aggregators = new Map();
61
+ const historyManagers = new Map();
62
+ const serverDataMap = new Map();
63
+ // Initialize aggregators and history managers for each server
64
+ for (const server of servers) {
65
+ aggregators.set(server.id, new metrics_aggregator_js_1.MetricsAggregator(server));
66
+ historyManagers.set(server.id, new history_manager_js_1.HistoryManager(server.id));
67
+ serverDataMap.set(server.id, {
68
+ server,
69
+ data: null,
70
+ error: null,
71
+ });
72
+ }
73
+ // Single scrollable content box
74
+ const contentBox = blessed_1.default.box({
75
+ top: 0,
76
+ left: 0,
77
+ width: '100%',
78
+ height: '100%',
79
+ tags: true,
80
+ scrollable: true,
81
+ alwaysScroll: true,
82
+ keys: true,
83
+ vi: true,
84
+ mouse: true,
85
+ scrollbar: {
86
+ ch: '█',
87
+ style: {
88
+ fg: 'blue',
89
+ },
90
+ },
91
+ });
92
+ screen.append(contentBox);
93
+ // Helper to create progress bar
94
+ function createProgressBar(percentage, width = 30) {
95
+ const filled = Math.round((percentage / 100) * width);
96
+ const empty = width - filled;
97
+ return '[' + '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty)) + ']';
98
+ }
99
+ // Render system resources section (system-wide for list view)
100
+ function renderSystemResources(systemMetrics) {
101
+ let content = '';
102
+ content += '{bold}System Resources{/bold}\n';
103
+ const termWidth = screen.width || 80;
104
+ const divider = '─'.repeat(termWidth - 2);
105
+ content += divider + '\n';
106
+ if (systemMetrics) {
107
+ if (systemMetrics.gpuUsage !== undefined) {
108
+ const bar = createProgressBar(systemMetrics.gpuUsage);
109
+ content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.gpuUsage)}%`;
110
+ if (systemMetrics.temperature !== undefined) {
111
+ content += ` - ${Math.round(systemMetrics.temperature)}°C`;
112
+ }
113
+ content += '\n';
114
+ }
115
+ if (systemMetrics.cpuUsage !== undefined) {
116
+ const bar = createProgressBar(systemMetrics.cpuUsage);
117
+ content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.cpuUsage)}%\n`;
118
+ }
119
+ if (systemMetrics.aneUsage !== undefined && systemMetrics.aneUsage > 1) {
120
+ const bar = createProgressBar(systemMetrics.aneUsage);
121
+ content += `ANE: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.aneUsage)}%\n`;
122
+ }
123
+ if (systemMetrics.memoryTotal > 0) {
124
+ const memoryUsedGB = systemMetrics.memoryUsed / (1024 ** 3);
125
+ const memoryTotalGB = systemMetrics.memoryTotal / (1024 ** 3);
126
+ const memoryPercentage = (systemMetrics.memoryUsed / systemMetrics.memoryTotal) * 100;
127
+ const bar = createProgressBar(memoryPercentage);
128
+ content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${Math.round(memoryPercentage)}% `;
129
+ content += `(${memoryUsedGB.toFixed(1)} / ${memoryTotalGB.toFixed(1)} GB)\n`;
130
+ }
131
+ if (systemMetrics.warnings && systemMetrics.warnings.length > 0) {
132
+ content += `\n{yellow-fg}⚠ ${systemMetrics.warnings.join(', ')}{/yellow-fg}\n`;
133
+ }
134
+ }
135
+ else {
136
+ content += '{gray-fg}Collecting system metrics...{/gray-fg}\n';
137
+ }
138
+ return content;
139
+ }
140
+ // Render aggregate model resources (all running servers in list view)
141
+ function renderAggregateModelResources() {
142
+ let content = '';
143
+ content += '{bold}Model Resources{/bold}\n';
144
+ const termWidth = screen.width || 80;
145
+ const divider = '─'.repeat(termWidth - 2);
146
+ content += divider + '\n';
147
+ // Aggregate CPU and memory across all running servers (skip stopped servers)
148
+ let totalCpu = 0;
149
+ let totalMemoryBytes = 0;
150
+ let serverCount = 0;
151
+ for (const serverData of serverDataMap.values()) {
152
+ // Only count running servers with valid data
153
+ if (serverData.server.status === 'running' && serverData.data?.server && !serverData.data.server.stale) {
154
+ if (serverData.data.server.processCpuUsage !== undefined) {
155
+ totalCpu += serverData.data.server.processCpuUsage;
156
+ serverCount++;
157
+ }
158
+ if (serverData.data.server.processMemory !== undefined) {
159
+ totalMemoryBytes += serverData.data.server.processMemory;
160
+ }
161
+ }
162
+ }
163
+ if (serverCount === 0) {
164
+ content += '{gray-fg}No running servers{/gray-fg}\n';
165
+ return content;
166
+ }
167
+ // CPU: Sum of all process CPU percentages
168
+ const cpuBar = createProgressBar(Math.min(totalCpu, 100));
169
+ content += `CPU: {cyan-fg}${cpuBar}{/cyan-fg} ${Math.round(totalCpu)}%`;
170
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
171
+ // Memory: Sum of all process memory
172
+ const totalMemoryGB = totalMemoryBytes / (1024 ** 3);
173
+ const estimatedMaxGB = serverCount * 8; // Assume ~8GB per server max
174
+ const memoryPercentage = Math.min((totalMemoryGB / estimatedMaxGB) * 100, 100);
175
+ const memoryBar = createProgressBar(memoryPercentage);
176
+ content += `Memory: {cyan-fg}${memoryBar}{/cyan-fg} ${totalMemoryGB.toFixed(2)} GB`;
177
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
178
+ return content;
179
+ }
180
+ // Render model resources section (per-process for detail view)
181
+ function renderModelResources(data) {
182
+ let content = '';
183
+ content += '{bold}Model Resources{/bold}\n';
184
+ const termWidth = screen.width || 80;
185
+ const divider = '─'.repeat(termWidth - 2);
186
+ content += divider + '\n';
187
+ // GPU: System-wide (can't get per-process on macOS)
188
+ if (data.system && data.system.gpuUsage !== undefined) {
189
+ const bar = createProgressBar(data.system.gpuUsage);
190
+ content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.system.gpuUsage)}% {gray-fg}(system){/gray-fg}`;
191
+ if (data.system.temperature !== undefined) {
192
+ content += ` - ${Math.round(data.system.temperature)}°C`;
193
+ }
194
+ content += '\n';
195
+ }
196
+ // CPU: Per-process
197
+ if (data.server.processCpuUsage !== undefined) {
198
+ const bar = createProgressBar(data.server.processCpuUsage);
199
+ content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.server.processCpuUsage)}%\n`;
200
+ }
201
+ // Memory: Per-process
202
+ if (data.server.processMemory !== undefined) {
203
+ const memoryGB = data.server.processMemory / (1024 ** 3);
204
+ const estimatedMax = 8;
205
+ const memoryPercentage = Math.min((memoryGB / estimatedMax) * 100, 100);
206
+ const bar = createProgressBar(memoryPercentage);
207
+ content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${memoryGB.toFixed(2)} GB\n`;
208
+ }
209
+ if (data.system && data.system.warnings && data.system.warnings.length > 0) {
210
+ content += `\n{yellow-fg}⚠ ${data.system.warnings.join(', ')}{/yellow-fg}\n`;
211
+ }
212
+ return content;
213
+ }
214
+ // Show loading spinner
215
+ function showLoading() {
216
+ if (isLoading)
217
+ return; // Already loading
218
+ isLoading = true;
219
+ spinnerFrameIndex = 0;
220
+ // Start spinner animation (80ms per frame = smooth rotation)
221
+ spinnerIntervalId = setInterval(() => {
222
+ spinnerFrameIndex = (spinnerFrameIndex + 1) % spinnerFrames.length;
223
+ // Re-render current view with updated spinner frame
224
+ let content = '';
225
+ if (viewMode === 'list') {
226
+ content = renderListView(lastSystemMetrics);
227
+ }
228
+ else {
229
+ content = renderDetailView(lastSystemMetrics);
230
+ }
231
+ contentBox.setContent(content);
232
+ screen.render();
233
+ }, 80);
234
+ // Immediate first render
235
+ let content = '';
236
+ if (viewMode === 'list') {
237
+ content = renderListView(lastSystemMetrics);
238
+ }
239
+ else {
240
+ content = renderDetailView(lastSystemMetrics);
241
+ }
242
+ contentBox.setContent(content);
243
+ screen.render();
244
+ }
245
+ // Hide loading spinner
246
+ function hideLoading() {
247
+ isLoading = false;
248
+ if (spinnerIntervalId) {
249
+ clearInterval(spinnerIntervalId);
250
+ spinnerIntervalId = null;
251
+ }
252
+ }
253
+ // Render list view
254
+ function renderListView(systemMetrics) {
255
+ const termWidth = screen.width || 80;
256
+ const divider = '─'.repeat(termWidth - 2);
257
+ let content = '';
258
+ // Header
259
+ content += '{bold}{blue-fg}═══ llama.cpp{/blue-fg}{/bold}\n\n';
260
+ // System resources
261
+ content += renderSystemResources(systemMetrics);
262
+ content += '\n';
263
+ // Aggregate model resources (CPU + memory for all running servers)
264
+ content += renderAggregateModelResources();
265
+ content += '\n';
266
+ // Server list header
267
+ const runningCount = servers.filter(s => s.status === 'running').length;
268
+ const stoppedCount = servers.filter(s => s.status !== 'running').length;
269
+ content += `{bold}Servers (${runningCount} running, ${stoppedCount} stopped){/bold}\n`;
270
+ content += '{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n';
271
+ content += divider + '\n';
272
+ // Calculate Server ID column width (variable based on screen width)
273
+ // Fixed columns breakdown:
274
+ // indicator(1) + " │ "(3) + " │ "(3) + port(4) + " │ "(3) + status(6) + "│ "(2) +
275
+ // slots(5) + " │ "(3) + tok/s(6) + " │ "(3) + memory(7) = 46
276
+ const fixedColumnsWidth = 48; // Add 2 extra for safety margin
277
+ const minServerIdWidth = 20;
278
+ const maxServerIdWidth = 60;
279
+ const serverIdWidth = Math.max(minServerIdWidth, Math.min(maxServerIdWidth, termWidth - fixedColumnsWidth));
280
+ // Table header with variable Server ID width
281
+ const serverIdHeader = 'Server ID'.padEnd(serverIdWidth);
282
+ content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
283
+ content += divider + '\n';
284
+ // Server rows
285
+ servers.forEach((server, index) => {
286
+ const serverData = serverDataMap.get(server.id);
287
+ const isSelected = index === selectedRowIndex;
288
+ // Selection indicator (arrow for selected row)
289
+ // Use plain arrow for selected (will be white), colored for unselected indicator
290
+ const indicator = isSelected ? '►' : ' ';
291
+ // Server ID (variable width, truncate if longer than available space)
292
+ const serverId = server.id.padEnd(serverIdWidth).substring(0, serverIdWidth);
293
+ // Port
294
+ const port = server.port.toString().padStart(4);
295
+ // Status - Check actual server status first, then health
296
+ // Build two versions: colored for normal, plain for selected
297
+ let status = '';
298
+ let statusPlain = '';
299
+ if (server.status !== 'running') {
300
+ // Server is stopped according to config
301
+ status = '{gray-fg}○ OFF{/gray-fg} ';
302
+ statusPlain = '○ OFF ';
303
+ }
304
+ else if (serverData?.data) {
305
+ // Server is running and we have data
306
+ if (serverData.data.server.healthy) {
307
+ status = '{green-fg}● RUN{/green-fg} ';
308
+ statusPlain = '● RUN ';
309
+ }
310
+ else {
311
+ status = '{red-fg}● ERR{/red-fg} ';
312
+ statusPlain = '● ERR ';
313
+ }
314
+ }
315
+ else {
316
+ // Server is running but no data yet (still loading)
317
+ status = '{yellow-fg}● ...{/yellow-fg} ';
318
+ statusPlain = '● ... ';
319
+ }
320
+ // Slots
321
+ let slots = '- ';
322
+ if (serverData?.data?.server) {
323
+ const active = serverData.data.server.activeSlots;
324
+ const total = serverData.data.server.totalSlots;
325
+ slots = `${active}/${total}`.padStart(5);
326
+ }
327
+ // tok/s
328
+ let tokensPerSec = '- ';
329
+ if (serverData?.data?.server.avgGenerateSpeed !== undefined &&
330
+ serverData.data.server.avgGenerateSpeed > 0) {
331
+ tokensPerSec = Math.round(serverData.data.server.avgGenerateSpeed).toString().padStart(6);
332
+ }
333
+ // Memory (actual process memory from top command)
334
+ let memory = '- ';
335
+ if (serverData?.data?.server.processMemory) {
336
+ const bytes = serverData.data.server.processMemory;
337
+ // Format as GB/MB depending on size
338
+ if (bytes >= 1024 * 1024 * 1024) {
339
+ const gb = (bytes / (1024 * 1024 * 1024)).toFixed(1);
340
+ memory = `${gb} GB`.padStart(7);
341
+ }
342
+ else {
343
+ const mb = Math.round(bytes / (1024 * 1024));
344
+ memory = `${mb} MB`.padStart(7);
345
+ }
346
+ }
347
+ // Build row content - use plain status for selected rows
348
+ let rowContent = '';
349
+ if (isSelected) {
350
+ // Use color code 15 (bright white) with cyan background
351
+ // When white-bg worked, it was probably auto-selecting bright white fg
352
+ rowContent = `{cyan-bg}{15-fg}${indicator} │ ${serverId} │ ${port} │ ${statusPlain}│ ${slots} │ ${tokensPerSec} │ ${memory}{/15-fg}{/cyan-bg}`;
353
+ }
354
+ else {
355
+ // Use colored status for normal rows
356
+ rowContent = `${indicator} │ ${serverId} │ ${port} │ ${status}│ ${slots} │ ${tokensPerSec} │ ${memory}`;
357
+ }
358
+ content += rowContent + '\n';
359
+ });
360
+ // Footer
361
+ content += '\n' + divider + '\n';
362
+ content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [H]istory [Q]uit{/gray-fg}`;
363
+ return content;
364
+ }
365
+ // Render detail view for selected server
366
+ function renderDetailView(systemMetrics) {
367
+ const server = servers[selectedServerIndex];
368
+ const serverData = serverDataMap.get(server.id);
369
+ const termWidth = screen.width || 80;
370
+ const divider = '─'.repeat(termWidth - 2);
371
+ let content = '';
372
+ // Header
373
+ content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
374
+ // Check if server is stopped
375
+ if (server.status !== 'running') {
376
+ // Show stopped server configuration (no metrics)
377
+ content += '{bold}Server Information{/bold}\n';
378
+ content += divider + '\n';
379
+ content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
380
+ content += `Model: ${server.modelName}\n`;
381
+ const displayHost = server.host || '127.0.0.1';
382
+ content += `Endpoint: http://${displayHost}:${server.port}\n`;
383
+ content += '\n';
384
+ content += '{bold}Configuration{/bold}\n';
385
+ content += divider + '\n';
386
+ content += `Threads: ${server.threads}\n`;
387
+ content += `Context: ${server.ctxSize} tokens\n`;
388
+ content += `GPU Layers: ${server.gpuLayers}\n`;
389
+ if (server.verbose) {
390
+ content += `Verbose: Enabled\n`;
391
+ }
392
+ if (server.customFlags && server.customFlags.length > 0) {
393
+ content += `Flags: ${server.customFlags.join(', ')}\n`;
394
+ }
395
+ content += '\n';
396
+ if (server.lastStarted) {
397
+ content += '{bold}Last Activity{/bold}\n';
398
+ content += divider + '\n';
399
+ content += `Started: ${new Date(server.lastStarted).toLocaleString()}\n`;
400
+ if (server.lastStopped) {
401
+ content += `Stopped: ${new Date(server.lastStopped).toLocaleString()}\n`;
402
+ }
403
+ content += '\n';
404
+ }
405
+ content += '{bold}Quick Actions{/bold}\n';
406
+ content += divider + '\n';
407
+ content += `{dim}Start server: llamacpp server start ${server.port}{/dim}\n`;
408
+ content += `{dim}Update config: llamacpp server config ${server.port} [options]{/dim}\n`;
409
+ content += `{dim}View logs: llamacpp server logs ${server.port}{/dim}\n`;
410
+ return content;
411
+ }
412
+ if (!serverData?.data) {
413
+ content += '{yellow-fg}Loading server data...{/yellow-fg}\n';
414
+ return content;
415
+ }
416
+ const data = serverData.data;
417
+ // Model resources (per-process)
418
+ content += renderModelResources(data);
419
+ content += '\n';
420
+ // Server Information
421
+ content += '{bold}Server Information{/bold}\n';
422
+ content += divider + '\n';
423
+ const statusIcon = data.server.healthy ? '{green-fg}●{/green-fg}' : '{red-fg}●{/red-fg}';
424
+ const statusText = data.server.healthy ? 'RUNNING' : 'UNHEALTHY';
425
+ content += `Status: ${statusIcon} ${statusText}`;
426
+ if (data.server.uptime) {
427
+ content += ` Uptime: ${data.server.uptime}`;
428
+ }
429
+ content += '\n';
430
+ content += `Model: ${server.modelName}`;
431
+ if (data.server.contextSize) {
432
+ content += ` Context: ${data.server.contextSize} tokens`;
433
+ }
434
+ content += '\n';
435
+ // Handle null host (legacy configs) by defaulting to 127.0.0.1
436
+ const displayHost = server.host || '127.0.0.1';
437
+ content += `Endpoint: http://${displayHost}:${server.port}\n`;
438
+ content += `Slots: ${data.server.activeSlots} active / ${data.server.totalSlots} total\n`;
439
+ content += '\n';
440
+ // Request Metrics
441
+ if (data.server.totalSlots > 0) {
442
+ content += '{bold}Request Metrics{/bold}\n';
443
+ content += divider + '\n';
444
+ content += `Active: ${data.server.activeSlots} / ${data.server.totalSlots}\n`;
445
+ content += `Idle: ${data.server.idleSlots} / ${data.server.totalSlots}\n`;
446
+ if (data.server.avgPromptSpeed !== undefined && data.server.avgPromptSpeed > 0) {
447
+ content += `Prompt: ${Math.round(data.server.avgPromptSpeed)} tokens/sec\n`;
448
+ }
449
+ if (data.server.avgGenerateSpeed !== undefined && data.server.avgGenerateSpeed > 0) {
450
+ content += `Generate: ${Math.round(data.server.avgGenerateSpeed)} tokens/sec\n`;
451
+ }
452
+ content += '\n';
453
+ }
454
+ // Active Slots Detail
455
+ if (data.server.slots.length > 0) {
456
+ const activeSlots = data.server.slots.filter(s => s.state === 'processing');
457
+ if (activeSlots.length > 0) {
458
+ content += '{bold}Active Slots{/bold}\n';
459
+ content += divider + '\n';
460
+ activeSlots.forEach((slot) => {
461
+ content += `Slot #${slot.id}: {yellow-fg}PROCESSING{/yellow-fg}`;
462
+ if (slot.timings?.predicted_per_second) {
463
+ content += ` - ${Math.round(slot.timings.predicted_per_second)} tok/s`;
464
+ }
465
+ if (slot.n_decoded !== undefined) {
466
+ content += ` - ${slot.n_decoded}`;
467
+ if (slot.n_ctx) {
468
+ content += ` / ${slot.n_ctx}`;
469
+ }
470
+ content += ' tokens';
471
+ }
472
+ content += '\n';
473
+ });
474
+ content += '\n';
475
+ }
476
+ }
477
+ // Footer
478
+ content += divider + '\n';
479
+ content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | [H]istory [ESC] Back [Q]uit{/gray-fg}`;
480
+ return content;
481
+ }
482
+ // Fetch and update display
483
+ async function fetchData() {
484
+ try {
485
+ // Collect system metrics ONCE for all servers (not per-server)
486
+ // This prevents spawning multiple macmon processes
487
+ const systemMetricsPromise = systemCollector.collectSystemMetrics();
488
+ // Batch collect process memory and CPU for ALL servers in parallel
489
+ // This prevents spawning multiple top processes (5x speedup)
490
+ const { getBatchProcessMemory, getBatchProcessCpu } = await Promise.resolve().then(() => __importStar(require('../utils/process-utils.js')));
491
+ const pids = servers.filter(s => s.pid).map(s => s.pid);
492
+ const memoryMapPromise = pids.length > 0
493
+ ? getBatchProcessMemory(pids)
494
+ : Promise.resolve(new Map());
495
+ const cpuMapPromise = pids.length > 0
496
+ ? getBatchProcessCpu(pids)
497
+ : Promise.resolve(new Map());
498
+ // Wait for both batches to complete
499
+ const [memoryMap, cpuMap] = await Promise.all([memoryMapPromise, cpuMapPromise]);
500
+ // Collect server metrics only for RUNNING servers (skip stopped servers)
501
+ const promises = servers
502
+ .filter(server => server.status === 'running')
503
+ .map(async (server) => {
504
+ const aggregator = aggregators.get(server.id);
505
+ try {
506
+ // Use collectServerMetrics instead of collectMonitorData
507
+ // to avoid spawning macmon per server
508
+ // Pass pre-fetched memory and CPU to avoid spawning top per server
509
+ const serverMetrics = await aggregator.collectServerMetrics(server, server.pid ? memoryMap.get(server.pid) ?? null : null, server.pid ? cpuMap.get(server.pid) ?? null : null);
510
+ // Build MonitorData manually with shared system metrics
511
+ const data = {
512
+ server: serverMetrics,
513
+ system: undefined, // Will be set after system metrics resolve
514
+ lastUpdated: new Date(),
515
+ updateInterval,
516
+ consecutiveFailures: 0,
517
+ };
518
+ serverDataMap.set(server.id, {
519
+ server,
520
+ data,
521
+ error: null,
522
+ });
523
+ }
524
+ catch (err) {
525
+ serverDataMap.set(server.id, {
526
+ server,
527
+ data: null,
528
+ error: err instanceof Error ? err.message : 'Unknown error',
529
+ });
530
+ }
531
+ });
532
+ // Set null data for stopped servers (no metrics collection)
533
+ servers
534
+ .filter(server => server.status !== 'running')
535
+ .forEach(server => {
536
+ serverDataMap.set(server.id, {
537
+ server,
538
+ data: null,
539
+ error: null,
540
+ });
541
+ });
542
+ // Wait for both system metrics and server metrics to complete
543
+ const systemMetrics = await systemMetricsPromise;
544
+ await Promise.all(promises);
545
+ // Store system metrics for loading state
546
+ lastSystemMetrics = systemMetrics;
547
+ // Update all server data with shared system metrics
548
+ for (const serverData of serverDataMap.values()) {
549
+ if (serverData.data) {
550
+ serverData.data.system = systemMetrics;
551
+ }
552
+ }
553
+ // Append to history for each server (silent failure)
554
+ // Only save history for servers that are healthy and not stale
555
+ for (const [serverId, serverData] of serverDataMap) {
556
+ if (serverData.data && !serverData.data.server.stale && serverData.data.server.healthy) {
557
+ const manager = historyManagers.get(serverId);
558
+ manager?.appendSnapshot(serverData.data.server, serverData.data.system)
559
+ .catch(err => {
560
+ // Don't interrupt monitoring on history write failure
561
+ console.error(`Failed to save history for ${serverId}:`, err);
562
+ });
563
+ }
564
+ }
565
+ // Render once with complete data
566
+ let content = '';
567
+ if (viewMode === 'list') {
568
+ content = renderListView(systemMetrics);
569
+ }
570
+ else {
571
+ content = renderDetailView(systemMetrics);
572
+ }
573
+ contentBox.setContent(content);
574
+ screen.render();
575
+ // Clear loading state
576
+ hideLoading();
577
+ }
578
+ catch (err) {
579
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
580
+ contentBox.setContent('{bold}{red-fg}Error{/red-fg}{/bold}\n\n' +
581
+ `{red-fg}${errorMsg}{/red-fg}\n\n` +
582
+ '{gray-fg}Press [R] to retry or [Q] to quit{/gray-fg}');
583
+ screen.render();
584
+ // Clear loading state on error too
585
+ isLoading = false;
586
+ }
587
+ }
588
+ // Polling
589
+ function startPolling() {
590
+ if (intervalId)
591
+ clearInterval(intervalId);
592
+ fetchData();
593
+ intervalId = setInterval(fetchData, updateInterval);
594
+ }
595
+ // Keyboard shortcuts - List view navigation with arrow keys
596
+ screen.key(['up', 'k'], () => {
597
+ if (viewMode === 'list') {
598
+ selectedRowIndex = Math.max(0, selectedRowIndex - 1);
599
+ // Re-render immediately for responsive feel
600
+ const content = renderListView(lastSystemMetrics);
601
+ contentBox.setContent(content);
602
+ screen.render();
603
+ }
604
+ });
605
+ screen.key(['down', 'j'], () => {
606
+ if (viewMode === 'list') {
607
+ selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
608
+ // Re-render immediately for responsive feel
609
+ const content = renderListView(lastSystemMetrics);
610
+ contentBox.setContent(content);
611
+ screen.render();
612
+ }
613
+ });
614
+ // Enter key to view details for selected server
615
+ screen.key(['enter'], () => {
616
+ if (viewMode === 'list') {
617
+ showLoading();
618
+ selectedServerIndex = selectedRowIndex;
619
+ viewMode = 'detail';
620
+ fetchData();
621
+ }
622
+ });
623
+ // Keyboard shortcuts - Detail view
624
+ screen.key(['escape'], () => {
625
+ // Don't handle ESC if we're in historical view - let historical view handle it
626
+ if (inHistoricalView)
627
+ return;
628
+ if (viewMode === 'detail') {
629
+ showLoading();
630
+ viewMode = 'list';
631
+ cameFromDirectJump = false; // Clear direct jump flag when returning to list
632
+ fetchData();
633
+ }
634
+ else if (viewMode === 'list') {
635
+ // ESC in list view - exit
636
+ showLoading();
637
+ if (intervalId)
638
+ clearInterval(intervalId);
639
+ if (spinnerIntervalId)
640
+ clearInterval(spinnerIntervalId);
641
+ setTimeout(() => {
642
+ screen.destroy();
643
+ process.exit(0);
644
+ }, 100);
645
+ }
646
+ });
647
+ // Keyboard shortcuts - Common
648
+ screen.key(['h', 'H'], async () => {
649
+ // Prevent entering historical view if already there
650
+ if (inHistoricalView)
651
+ return;
652
+ // Keep polling in background for live historical updates
653
+ // Stop spinner if running
654
+ if (spinnerIntervalId)
655
+ clearInterval(spinnerIntervalId);
656
+ // Remove current content box
657
+ screen.remove(contentBox);
658
+ // Mark that we're in historical view
659
+ inHistoricalView = true;
660
+ if (viewMode === 'list') {
661
+ // Show multi-server historical view
662
+ await (0, HistoricalMonitorApp_js_1.createMultiServerHistoricalUI)(screen, servers, selectedServerIndex, () => {
663
+ // Mark that we've left historical view
664
+ inHistoricalView = false;
665
+ // Re-attach content box when returning from history
666
+ screen.append(contentBox);
667
+ // Re-render the list view
668
+ const content = renderListView(lastSystemMetrics);
669
+ contentBox.setContent(content);
670
+ screen.render();
671
+ });
672
+ }
673
+ else {
674
+ // Show single-server historical view for selected server
675
+ const selectedServer = servers[selectedServerIndex];
676
+ await (0, HistoricalMonitorApp_js_1.createHistoricalUI)(screen, selectedServer, () => {
677
+ // Mark that we've left historical view
678
+ inHistoricalView = false;
679
+ // Re-attach content box when returning from history
680
+ screen.append(contentBox);
681
+ // Re-render the detail view
682
+ const content = renderDetailView(lastSystemMetrics);
683
+ contentBox.setContent(content);
684
+ screen.render();
685
+ });
686
+ }
687
+ });
688
+ screen.key(['q', 'Q', 'C-c'], () => {
689
+ showLoading();
690
+ if (intervalId)
691
+ clearInterval(intervalId);
692
+ if (spinnerIntervalId)
693
+ clearInterval(spinnerIntervalId);
694
+ // Small delay to show the loading state before exit
695
+ setTimeout(() => {
696
+ screen.destroy();
697
+ process.exit(0);
698
+ }, 100);
699
+ });
700
+ // Initial display
701
+ contentBox.setContent('{cyan-fg}⏳ Connecting to servers...{/cyan-fg}');
702
+ screen.render();
703
+ startPolling();
704
+ // Cleanup
705
+ screen.on('destroy', () => {
706
+ if (intervalId)
707
+ clearInterval(intervalId);
708
+ // Note: macmon child processes will automatically die when parent exits
709
+ // since they're spawned with detached: false
710
+ });
711
+ }
712
+ //# sourceMappingURL=MultiServerMonitorApp.js.map