@appkit/llamacpp-cli 1.9.0 → 1.10.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 (93) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +171 -42
  3. package/dist/cli.js +75 -10
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/completion.d.ts +9 -0
  6. package/dist/commands/completion.d.ts.map +1 -0
  7. package/dist/commands/completion.js +83 -0
  8. package/dist/commands/completion.js.map +1 -0
  9. package/dist/commands/monitor.js +1 -1
  10. package/dist/commands/monitor.js.map +1 -1
  11. package/dist/commands/ps.d.ts +1 -3
  12. package/dist/commands/ps.d.ts.map +1 -1
  13. package/dist/commands/ps.js +36 -115
  14. package/dist/commands/ps.js.map +1 -1
  15. package/dist/commands/router/config.d.ts +1 -0
  16. package/dist/commands/router/config.d.ts.map +1 -1
  17. package/dist/commands/router/config.js +7 -2
  18. package/dist/commands/router/config.js.map +1 -1
  19. package/dist/commands/router/logs.d.ts +12 -0
  20. package/dist/commands/router/logs.d.ts.map +1 -0
  21. package/dist/commands/router/logs.js +238 -0
  22. package/dist/commands/router/logs.js.map +1 -0
  23. package/dist/commands/tui.d.ts +2 -0
  24. package/dist/commands/tui.d.ts.map +1 -0
  25. package/dist/commands/tui.js +27 -0
  26. package/dist/commands/tui.js.map +1 -0
  27. package/dist/lib/completion.d.ts +5 -0
  28. package/dist/lib/completion.d.ts.map +1 -0
  29. package/dist/lib/completion.js +195 -0
  30. package/dist/lib/completion.js.map +1 -0
  31. package/dist/lib/model-downloader.d.ts +5 -1
  32. package/dist/lib/model-downloader.d.ts.map +1 -1
  33. package/dist/lib/model-downloader.js +53 -20
  34. package/dist/lib/model-downloader.js.map +1 -1
  35. package/dist/lib/router-logger.d.ts +61 -0
  36. package/dist/lib/router-logger.d.ts.map +1 -0
  37. package/dist/lib/router-logger.js +200 -0
  38. package/dist/lib/router-logger.js.map +1 -0
  39. package/dist/lib/router-manager.d.ts.map +1 -1
  40. package/dist/lib/router-manager.js +1 -0
  41. package/dist/lib/router-manager.js.map +1 -1
  42. package/dist/lib/router-server.d.ts +9 -0
  43. package/dist/lib/router-server.d.ts.map +1 -1
  44. package/dist/lib/router-server.js +169 -57
  45. package/dist/lib/router-server.js.map +1 -1
  46. package/dist/tui/ConfigApp.d.ts +7 -0
  47. package/dist/tui/ConfigApp.d.ts.map +1 -0
  48. package/dist/tui/ConfigApp.js +1002 -0
  49. package/dist/tui/ConfigApp.js.map +1 -0
  50. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  51. package/dist/tui/HistoricalMonitorApp.js +85 -49
  52. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  53. package/dist/tui/ModelsApp.d.ts +7 -0
  54. package/dist/tui/ModelsApp.d.ts.map +1 -0
  55. package/dist/tui/ModelsApp.js +362 -0
  56. package/dist/tui/ModelsApp.js.map +1 -0
  57. package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
  58. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  59. package/dist/tui/MultiServerMonitorApp.js +1038 -122
  60. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  61. package/dist/tui/RootNavigator.d.ts +7 -0
  62. package/dist/tui/RootNavigator.d.ts.map +1 -0
  63. package/dist/tui/RootNavigator.js +55 -0
  64. package/dist/tui/RootNavigator.js.map +1 -0
  65. package/dist/tui/SearchApp.d.ts +6 -0
  66. package/dist/tui/SearchApp.d.ts.map +1 -0
  67. package/dist/tui/SearchApp.js +451 -0
  68. package/dist/tui/SearchApp.js.map +1 -0
  69. package/dist/tui/SplashScreen.d.ts +16 -0
  70. package/dist/tui/SplashScreen.d.ts.map +1 -0
  71. package/dist/tui/SplashScreen.js +129 -0
  72. package/dist/tui/SplashScreen.js.map +1 -0
  73. package/dist/types/router-config.d.ts +1 -0
  74. package/dist/types/router-config.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/cli.ts +41 -10
  77. package/src/commands/monitor.ts +1 -1
  78. package/src/commands/ps.ts +44 -133
  79. package/src/commands/router/config.ts +9 -2
  80. package/src/commands/router/logs.ts +256 -0
  81. package/src/commands/tui.ts +25 -0
  82. package/src/lib/model-downloader.ts +57 -20
  83. package/src/lib/router-logger.ts +201 -0
  84. package/src/lib/router-manager.ts +1 -0
  85. package/src/lib/router-server.ts +193 -62
  86. package/src/tui/ConfigApp.ts +1085 -0
  87. package/src/tui/HistoricalMonitorApp.ts +88 -49
  88. package/src/tui/ModelsApp.ts +368 -0
  89. package/src/tui/MultiServerMonitorApp.ts +1163 -122
  90. package/src/tui/RootNavigator.ts +74 -0
  91. package/src/tui/SearchApp.ts +511 -0
  92. package/src/tui/SplashScreen.ts +149 -0
  93. package/src/types/router-config.ts +1 -0
package/src/cli.ts CHANGED
@@ -24,6 +24,7 @@ import { routerStopCommand } from './commands/router/stop';
24
24
  import { routerStatusCommand } from './commands/router/status';
25
25
  import { routerRestartCommand } from './commands/router/restart';
26
26
  import { routerConfigCommand } from './commands/router/config';
27
+ import { routerLogsCommand } from './commands/router/logs';
27
28
  import packageJson from '../package.json';
28
29
 
29
30
  const program = new Command();
@@ -31,7 +32,17 @@ const program = new Command();
31
32
  program
32
33
  .name('llamacpp')
33
34
  .description('CLI tool to manage local llama.cpp servers on macOS')
34
- .version(packageJson.version, '-v, --version', 'Output the version number');
35
+ .version(packageJson.version, '-v, --version', 'Output the version number')
36
+ .action(async () => {
37
+ // Default action: launch TUI when no command provided
38
+ try {
39
+ const { tuiCommand } = await import('./commands/tui');
40
+ await tuiCommand();
41
+ } catch (error) {
42
+ console.error(chalk.red('❌ Error:'), (error as Error).message);
43
+ process.exit(1);
44
+ }
45
+ });
35
46
 
36
47
  // List models
37
48
  program
@@ -46,14 +57,13 @@ program
46
57
  }
47
58
  });
48
59
 
49
- // List running servers
60
+ // List servers (static table)
50
61
  program
51
- .command('ps [identifier]')
52
- .description('Interactive server monitoring dashboard')
53
- .option('--table', 'Show static table instead of TUI (for scripting)')
54
- .action(async (identifier?: string, options?: { table?: boolean }) => {
62
+ .command('ps')
63
+ .description('List all servers with status (static table)')
64
+ .action(async () => {
55
65
  try {
56
- await psCommand(identifier, options);
66
+ await psCommand();
57
67
  } catch (error) {
58
68
  console.error(chalk.red('❌ Error:'), (error as Error).message);
59
69
  process.exit(1);
@@ -298,14 +308,14 @@ server
298
308
  }
299
309
  });
300
310
 
301
- // Monitor server (deprecated - redirects to ps)
311
+ // Monitor server (deprecated - redirects to TUI)
302
312
  server
303
313
  .command('monitor [identifier]')
304
- .description('Monitor server with real-time metrics TUI (deprecated: use "llamacpp ps" instead)')
314
+ .description('Monitor server with real-time metrics TUI (deprecated: use "llamacpp" instead)')
305
315
  .action(async (identifier?: string) => {
306
316
  try {
307
317
  console.log(chalk.yellow('⚠️ The "monitor" command is deprecated and will be removed in a future version.'));
308
- console.log(chalk.dim(' Please use "llamacpp ps" instead for the same functionality.\n'));
318
+ console.log(chalk.dim(' Please use "llamacpp" instead for the same functionality.\n'));
309
319
  await monitorCommand(identifier);
310
320
  } catch (error) {
311
321
  console.error(chalk.red('❌ Error:'), (error as Error).message);
@@ -378,6 +388,7 @@ router
378
388
  .option('-h, --host <address>', 'Update bind address')
379
389
  .option('--timeout <ms>', 'Update request timeout (milliseconds)', parseInt)
380
390
  .option('--health-interval <ms>', 'Update health check interval (milliseconds)', parseInt)
391
+ .option('-v, --verbose [boolean]', 'Enable/disable verbose logging to file (true/false)', (val) => val === 'true' || val === '1')
381
392
  .option('-r, --restart', 'Automatically restart router if running')
382
393
  .action(async (options) => {
383
394
  try {
@@ -388,5 +399,25 @@ router
388
399
  }
389
400
  });
390
401
 
402
+ // Router logs
403
+ router
404
+ .command('logs')
405
+ .description('View router logs')
406
+ .option('-f, --follow', 'Follow logs in real-time (like tail -f)')
407
+ .option('-n, --lines <number>', 'Number of lines to show (default: 50)', parseInt)
408
+ .option('--stderr', 'Show system logs (stderr) instead of activity logs (stdout)')
409
+ .option('-v, --verbose', 'Show verbose JSON log file (if enabled)')
410
+ .option('--clear', 'Clear the log file')
411
+ .option('--rotate', 'Rotate the log file with timestamp')
412
+ .option('--clear-all', 'Clear all router logs (activity, system, verbose)')
413
+ .action(async (options) => {
414
+ try {
415
+ await routerLogsCommand(options);
416
+ } catch (error) {
417
+ console.error(chalk.red('❌ Error:'), (error as Error).message);
418
+ process.exit(1);
419
+ }
420
+ });
421
+
391
422
  // Parse arguments
392
423
  program.parse();
@@ -31,7 +31,7 @@ export async function monitorCommand(identifier?: string): Promise<void> {
31
31
  screen.destroy();
32
32
  throw new Error(
33
33
  `Server not found: ${identifier}\n\n` +
34
- `Use: llamacpp ps\n` +
34
+ `Use: llamacpp ps (to list servers)\n` +
35
35
  `Or create a new server: llamacpp server create <model>`
36
36
  );
37
37
  }
@@ -1,14 +1,32 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
- import blessed from 'blessed';
4
- import { stateManager } from '../lib/state-manager';
5
- import { statusChecker } from '../lib/status-checker';
6
- import { formatUptime, formatBytes } from '../utils/format-utils';
7
- import { getProcessMemory } from '../utils/process-utils';
8
- import { createMultiServerMonitorUI } from '../tui/MultiServerMonitorApp.js';
3
+ import { stateManager } from '../lib/state-manager.js';
4
+ import { statusChecker } from '../lib/status-checker.js';
5
+ import { formatUptime, formatBytes } from '../utils/format-utils.js';
6
+ import { getProcessMemory } from '../utils/process-utils.js';
9
7
  import { ServerConfig } from '../types/server-config.js';
10
8
 
11
- async function showStaticTable(): Promise<void> {
9
+ const STATUS_CONFIG = {
10
+ running: { text: '✅ RUNNING', color: chalk.green },
11
+ crashed: { text: '❌ CRASHED', color: chalk.red },
12
+ stopped: { text: '⚠️ STOPPED', color: chalk.yellow },
13
+ } as const;
14
+
15
+ async function getServerMemory(server: ServerConfig): Promise<string> {
16
+ if (server.status !== 'running' || !server.pid) {
17
+ return '-';
18
+ }
19
+
20
+ const cpuMemoryBytes = await getProcessMemory(server.pid);
21
+ if (cpuMemoryBytes === null) {
22
+ return '-';
23
+ }
24
+
25
+ const metalMemoryBytes = server.metalMemoryMB ? server.metalMemoryMB * 1024 * 1024 : 0;
26
+ return formatBytes(cpuMemoryBytes + metalMemoryBytes);
27
+ }
28
+
29
+ export async function psCommand(): Promise<void> {
12
30
  const servers = await stateManager.getAllServers();
13
31
 
14
32
  if (servers.length === 0) {
@@ -17,60 +35,31 @@ async function showStaticTable(): Promise<void> {
17
35
  return;
18
36
  }
19
37
 
20
- // Update all server statuses
21
38
  console.log(chalk.dim('Checking server statuses...\n'));
22
- const updated = await statusChecker.updateAllServerStatuses();
39
+ const serversWithStatus = await statusChecker.updateAllServerStatuses();
23
40
 
24
41
  const table = new Table({
25
42
  head: ['SERVER ID', 'MODEL', 'PORT', 'STATUS', 'PID', 'MEMORY', 'UPTIME'],
26
43
  });
27
44
 
28
- let runningCount = 0;
29
- let stoppedCount = 0;
30
- let crashedCount = 0;
31
-
32
- for (const server of updated) {
33
- let statusText: string;
34
- let statusColor: (text: string) => string;
35
-
36
- switch (server.status) {
37
- case 'running':
38
- statusText = '✅ RUNNING';
39
- statusColor = chalk.green;
40
- runningCount++;
41
- break;
42
- case 'crashed':
43
- statusText = '❌ CRASHED';
44
- statusColor = chalk.red;
45
- crashedCount++;
46
- break;
47
- default:
48
- statusText = '⚠️ STOPPED';
49
- statusColor = chalk.yellow;
50
- stoppedCount++;
51
- }
52
-
53
- const uptime =
54
- server.status === 'running' && server.lastStarted
55
- ? formatUptime(server.lastStarted)
56
- : '-';
57
-
58
- // Get memory usage for running servers (CPU + Metal GPU memory)
59
- let memoryText = '-';
60
- if (server.status === 'running' && server.pid) {
61
- const cpuMemoryBytes = await getProcessMemory(server.pid);
62
- if (cpuMemoryBytes !== null) {
63
- const metalMemoryBytes = server.metalMemoryMB ? server.metalMemoryMB * 1024 * 1024 : 0;
64
- const totalMemoryBytes = cpuMemoryBytes + metalMemoryBytes;
65
- memoryText = formatBytes(totalMemoryBytes);
66
- }
67
- }
45
+ const counts = { running: 0, stopped: 0, crashed: 0 };
46
+
47
+ for (const server of serversWithStatus) {
48
+ const status = server.status || 'stopped';
49
+ const config = STATUS_CONFIG[status] || STATUS_CONFIG.stopped;
50
+ counts[status]++;
51
+
52
+ const uptime = server.status === 'running' && server.lastStarted
53
+ ? formatUptime(server.lastStarted)
54
+ : '-';
55
+
56
+ const memoryText = await getServerMemory(server);
68
57
 
69
58
  table.push([
70
59
  server.id,
71
60
  server.modelName,
72
61
  server.port.toString(),
73
- statusColor(statusText),
62
+ config.color(config.text),
74
63
  server.pid?.toString() || '-',
75
64
  memoryText,
76
65
  uptime,
@@ -80,94 +69,16 @@ async function showStaticTable(): Promise<void> {
80
69
  console.log(table.toString());
81
70
 
82
71
  const summary = [
83
- chalk.green(`${runningCount} running`),
84
- chalk.yellow(`${stoppedCount} stopped`),
72
+ chalk.green(`${counts.running} running`),
73
+ chalk.yellow(`${counts.stopped} stopped`),
85
74
  ];
86
- if (crashedCount > 0) {
87
- summary.push(chalk.red(`${crashedCount} crashed`));
75
+ if (counts.crashed > 0) {
76
+ summary.push(chalk.red(`${counts.crashed} crashed`));
88
77
  }
89
78
 
90
79
  console.log(chalk.dim(`\nTotal: ${servers.length} servers (${summary.join(', ')})`));
91
80
 
92
- if (crashedCount > 0) {
81
+ if (counts.crashed > 0) {
93
82
  console.log(chalk.red('\n⚠️ Some servers have crashed. Check logs with: llamacpp server logs <id> --errors'));
94
83
  }
95
84
  }
96
-
97
- export async function psCommand(identifier?: string, options?: { table?: boolean }): Promise<void> {
98
- // If --table flag is set, show static table (backward compatibility)
99
- if (options?.table) {
100
- await showStaticTable();
101
- return;
102
- }
103
-
104
- // Get all servers and update their statuses
105
- const servers = await stateManager.getAllServers();
106
-
107
- if (servers.length === 0) {
108
- console.log(chalk.yellow('No servers configured.'));
109
- console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
110
- return;
111
- }
112
-
113
- // Update all server statuses
114
- const updated = await statusChecker.updateAllServerStatuses();
115
-
116
- // If identifier is provided, find the server and jump to detail view
117
- if (identifier) {
118
- const server = await findServer(identifier, updated);
119
- if (!server) {
120
- console.log(chalk.red(`❌ Server not found: ${identifier}`));
121
- console.log(chalk.dim('\nAvailable servers:'));
122
- updated.forEach((s: ServerConfig) => {
123
- console.log(chalk.dim(` - ${s.id} (port ${s.port})`));
124
- });
125
- process.exit(1);
126
- }
127
-
128
- // Find the server index for direct jump
129
- const serverIndex = updated.findIndex(s => s.id === server.id);
130
-
131
- // Launch multi-server TUI with direct jump to detail view
132
- const screen = blessed.screen({
133
- smartCSR: true,
134
- title: 'llama.cpp Multi-Server Monitor',
135
- fullUnicode: true,
136
- });
137
-
138
- await createMultiServerMonitorUI(screen, updated, true, serverIndex); // fromPs = true, directJumpIndex
139
- return;
140
- }
141
-
142
- // No identifier - launch multi-server TUI
143
- const runningServers = updated.filter((s: ServerConfig) => s.status === 'running');
144
-
145
- // Launch multi-server TUI (shows all servers, not just running ones)
146
- const screen = blessed.screen({
147
- smartCSR: true,
148
- title: 'llama.cpp Multi-Server Monitor',
149
- fullUnicode: true,
150
- });
151
-
152
- await createMultiServerMonitorUI(screen, updated, true); // fromPs = true
153
- }
154
-
155
- // Helper function to find server by identifier
156
- async function findServer(identifier: string, servers: ServerConfig[]): Promise<ServerConfig | null> {
157
- // Try by port
158
- const port = parseInt(identifier);
159
- if (!isNaN(port)) {
160
- const server = servers.find(s => s.port === port);
161
- if (server) return server;
162
- }
163
-
164
- // Try by exact ID
165
- const byId = servers.find(s => s.id === identifier);
166
- if (byId) return byId;
167
-
168
- // Try by partial model name
169
- const byModel = servers.find(s => s.modelName.toLowerCase().includes(identifier.toLowerCase()));
170
- if (byModel) return byModel;
171
-
172
- return null;
173
- }
@@ -6,6 +6,7 @@ interface ConfigOptions {
6
6
  host?: string;
7
7
  timeout?: number;
8
8
  healthInterval?: number;
9
+ verbose?: boolean;
9
10
  restart?: boolean;
10
11
  }
11
12
 
@@ -18,9 +19,9 @@ export async function routerConfigCommand(options: ConfigOptions): Promise<void>
18
19
  }
19
20
 
20
21
  // Check if any options were provided
21
- const hasOptions = options.port || options.host || options.timeout || options.healthInterval;
22
+ const hasOptions = options.port || options.host || options.timeout || options.healthInterval || options.verbose !== undefined;
22
23
  if (!hasOptions) {
23
- throw new Error('No configuration options provided. Use --port, --host, --timeout, or --health-interval');
24
+ throw new Error('No configuration options provided. Use --port, --host, --timeout, --health-interval, or --verbose');
24
25
  }
25
26
 
26
27
  const isRunning = config.status === 'running';
@@ -55,6 +56,12 @@ export async function routerConfigCommand(options: ConfigOptions): Promise<void>
55
56
  updates.healthCheckInterval = options.healthInterval;
56
57
  }
57
58
 
59
+ if (options.verbose !== undefined) {
60
+ const verboseStr = (val: boolean) => val ? 'enabled' : 'disabled';
61
+ changes.push(`Verbose Logging: ${verboseStr(config.verbose)} → ${verboseStr(options.verbose)}`);
62
+ updates.verbose = options.verbose;
63
+ }
64
+
58
65
  // Display changes
59
66
  console.log(chalk.blue('📝 Configuration changes:'));
60
67
  console.log();
@@ -0,0 +1,256 @@
1
+ import chalk from 'chalk';
2
+ import { spawn } from 'child_process';
3
+ import * as readline from 'readline';
4
+ import * as fs from 'fs';
5
+ import { routerManager } from '../../lib/router-manager';
6
+ import { fileExists } from '../../utils/file-utils';
7
+ import {
8
+ getFileSize,
9
+ formatFileSize,
10
+ rotateLogFile,
11
+ clearLogFile,
12
+ } from '../../utils/log-utils';
13
+
14
+ interface RouterLogsOptions {
15
+ follow?: boolean;
16
+ lines?: number;
17
+ stderr?: boolean; // View system logs (stderr) instead of activity logs (stdout)
18
+ verbose?: boolean;
19
+ clear?: boolean;
20
+ rotate?: boolean;
21
+ clearAll?: boolean;
22
+ }
23
+
24
+ export async function routerLogsCommand(options: RouterLogsOptions): Promise<void> {
25
+ // Load router config
26
+ const config = await routerManager.loadConfig();
27
+ if (!config) {
28
+ throw new Error('Router configuration not found. Use "llamacpp router start" to create it.');
29
+ }
30
+
31
+ // Determine log file (default to stdout for activity logs, stderr for system logs)
32
+ const logPath = options.stderr ? config.stderrPath : config.stdoutPath;
33
+ const logType = options.stderr ? 'system' : 'activity';
34
+
35
+ // Also check for verbose JSON log file if --verbose flag is used
36
+ const verboseLogPath = '/Users/dweaver/.llamacpp/logs/router.log';
37
+ const useVerboseLog = options.verbose && (await fileExists(verboseLogPath));
38
+
39
+ // Handle --clear-all option (clears both stderr and stdout)
40
+ if (options.clearAll) {
41
+ let totalFreed = 0;
42
+
43
+ // Clear stderr
44
+ if (await fileExists(config.stderrPath)) {
45
+ totalFreed += await getFileSize(config.stderrPath);
46
+ await clearLogFile(config.stderrPath);
47
+ }
48
+
49
+ // Clear stdout
50
+ if (await fileExists(config.stdoutPath)) {
51
+ totalFreed += await getFileSize(config.stdoutPath);
52
+ await clearLogFile(config.stdoutPath);
53
+ }
54
+
55
+ // Clear verbose log file
56
+ if (await fileExists(verboseLogPath)) {
57
+ totalFreed += await getFileSize(verboseLogPath);
58
+ await clearLogFile(verboseLogPath);
59
+ }
60
+
61
+ console.log(chalk.green('✅ Cleared all router logs'));
62
+ console.log(chalk.dim(` Total freed: ${formatFileSize(totalFreed)}`));
63
+ return;
64
+ }
65
+
66
+ // Handle --clear option
67
+ if (options.clear) {
68
+ const targetPath = useVerboseLog ? verboseLogPath : logPath;
69
+
70
+ if (!(await fileExists(targetPath))) {
71
+ console.log(chalk.yellow(`⚠️ No ${useVerboseLog ? 'verbose log' : logType} found for router`));
72
+ console.log(chalk.dim(` Log file does not exist: ${targetPath}`));
73
+ return;
74
+ }
75
+
76
+ const sizeBefore = await getFileSize(targetPath);
77
+ await clearLogFile(targetPath);
78
+
79
+ console.log(chalk.green(`✅ Cleared router ${useVerboseLog ? 'verbose log' : logType}`));
80
+ console.log(chalk.dim(` Freed: ${formatFileSize(sizeBefore)}`));
81
+ console.log(chalk.dim(` ${targetPath}`));
82
+ return;
83
+ }
84
+
85
+ // Handle --rotate option
86
+ if (options.rotate) {
87
+ const targetPath = useVerboseLog ? verboseLogPath : logPath;
88
+
89
+ if (!(await fileExists(targetPath))) {
90
+ console.log(chalk.yellow(`⚠️ No ${useVerboseLog ? 'verbose log' : logType} found for router`));
91
+ console.log(chalk.dim(` Log file does not exist: ${targetPath}`));
92
+ return;
93
+ }
94
+
95
+ try {
96
+ const archivedPath = await rotateLogFile(targetPath);
97
+ const size = await getFileSize(archivedPath);
98
+
99
+ console.log(chalk.green(`✅ Rotated router ${useVerboseLog ? 'verbose log' : logType}`));
100
+ console.log(chalk.dim(` Archived: ${formatFileSize(size)}`));
101
+ console.log(chalk.dim(` → ${archivedPath}`));
102
+ } catch (error) {
103
+ throw new Error(`Failed to rotate log: ${(error as Error).message}`);
104
+ }
105
+ return;
106
+ }
107
+
108
+ // Determine which log to display
109
+ const displayPath = useVerboseLog ? verboseLogPath : logPath;
110
+ const displayType = useVerboseLog ? 'verbose JSON log' : logType;
111
+
112
+ // Check if log file exists
113
+ if (!(await fileExists(displayPath))) {
114
+ console.log(chalk.yellow(`⚠️ No ${displayType} found for router`));
115
+ console.log(chalk.dim(` Log file does not exist: ${displayPath}`));
116
+
117
+ if (useVerboseLog) {
118
+ console.log();
119
+ console.log(chalk.dim(' Verbose logging is disabled. Enable with:'));
120
+ console.log(chalk.dim(' llamacpp router config --verbose true --restart'));
121
+ }
122
+ return;
123
+ }
124
+
125
+ console.log(chalk.blue(`📋 Router logs (${displayType})`));
126
+ console.log(chalk.dim(` ${displayPath}`));
127
+
128
+ // Show log size information
129
+ const currentSize = await getFileSize(displayPath);
130
+ console.log(chalk.dim(` Size: ${formatFileSize(currentSize)}`));
131
+
132
+ if (!useVerboseLog && config.verbose) {
133
+ console.log(chalk.dim(` Verbose logging is enabled (use --verbose to view JSON log)`));
134
+ } else if (!useVerboseLog && !config.verbose) {
135
+ console.log(chalk.dim(` Verbose logging is disabled`));
136
+ }
137
+
138
+ console.log();
139
+
140
+ if (options.follow) {
141
+ // Follow logs in real-time
142
+ if (useVerboseLog) {
143
+ // Pretty-print JSON logs
144
+ const tailProcess = spawn('tail', ['-f', displayPath]);
145
+ const rl = readline.createInterface({
146
+ input: tailProcess.stdout,
147
+ crlfDelay: Infinity,
148
+ });
149
+
150
+ rl.on('line', (line) => {
151
+ try {
152
+ const entry = JSON.parse(line);
153
+ // Format timestamp
154
+ const timestamp = new Date(entry.timestamp).toLocaleTimeString();
155
+ // Color code status
156
+ const statusColor = entry.status === 'success' ? chalk.green : chalk.red;
157
+
158
+ console.log(
159
+ chalk.dim(`[${timestamp}]`),
160
+ statusColor(entry.statusCode),
161
+ entry.method,
162
+ entry.endpoint,
163
+ '→',
164
+ chalk.cyan(entry.model),
165
+ chalk.dim(`(${entry.backend || 'N/A'})`),
166
+ chalk.yellow(`${entry.durationMs}ms`)
167
+ );
168
+ if (entry.prompt) {
169
+ console.log(chalk.dim(` Prompt: "${entry.prompt}"`));
170
+ }
171
+ if (entry.error) {
172
+ console.log(chalk.red(` Error: ${entry.error}`));
173
+ }
174
+ } catch {
175
+ // Not JSON, just print raw line
176
+ console.log(line);
177
+ }
178
+ });
179
+
180
+ tailProcess.on('close', () => {
181
+ process.exit(0);
182
+ });
183
+
184
+ // Handle Ctrl+C gracefully
185
+ process.on('SIGINT', () => {
186
+ tailProcess.kill();
187
+ process.exit(0);
188
+ });
189
+ } else {
190
+ // Standard tail for stderr/stdout
191
+ const tailProcess = spawn('tail', ['-f', displayPath]);
192
+ tailProcess.stdout.pipe(process.stdout);
193
+ tailProcess.stderr.pipe(process.stderr);
194
+
195
+ tailProcess.on('close', () => {
196
+ process.exit(0);
197
+ });
198
+
199
+ // Handle Ctrl+C gracefully
200
+ process.on('SIGINT', () => {
201
+ tailProcess.kill();
202
+ process.exit(0);
203
+ });
204
+ }
205
+ } else {
206
+ // Show last N lines (default 50)
207
+ const linesToShow = options.lines || 50;
208
+
209
+ if (useVerboseLog) {
210
+ // Pretty-print JSON logs
211
+ const lines = fs.readFileSync(displayPath, 'utf-8')
212
+ .split('\n')
213
+ .filter(line => line.trim())
214
+ .slice(-linesToShow);
215
+
216
+ for (const line of lines) {
217
+ try {
218
+ const entry = JSON.parse(line);
219
+ // Format timestamp
220
+ const timestamp = new Date(entry.timestamp).toLocaleTimeString();
221
+ // Color code status
222
+ const statusColor = entry.status === 'success' ? chalk.green : chalk.red;
223
+
224
+ console.log(
225
+ chalk.dim(`[${timestamp}]`),
226
+ statusColor(entry.statusCode),
227
+ entry.method,
228
+ entry.endpoint,
229
+ '→',
230
+ chalk.cyan(entry.model),
231
+ chalk.dim(`(${entry.backend || 'N/A'})`),
232
+ chalk.yellow(`${entry.durationMs}ms`)
233
+ );
234
+ if (entry.prompt) {
235
+ console.log(chalk.dim(` Prompt: "${entry.prompt}"`));
236
+ }
237
+ if (entry.error) {
238
+ console.log(chalk.red(` Error: ${entry.error}`));
239
+ }
240
+ } catch {
241
+ // Not JSON, just print raw line
242
+ console.log(line);
243
+ }
244
+ }
245
+ } else {
246
+ // Standard tail for stderr/stdout
247
+ const { execSync } = require('child_process');
248
+ try {
249
+ const output = execSync(`tail -n ${linesToShow} "${displayPath}"`, { encoding: 'utf-8' });
250
+ process.stdout.write(output);
251
+ } catch (error) {
252
+ throw new Error(`Failed to read log file: ${(error as Error).message}`);
253
+ }
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,25 @@
1
+ import chalk from 'chalk';
2
+ import blessed from 'blessed';
3
+ import { stateManager } from '../lib/state-manager.js';
4
+ import { statusChecker } from '../lib/status-checker.js';
5
+ import { createRootNavigator } from '../tui/RootNavigator.js';
6
+
7
+ export async function tuiCommand(): Promise<void> {
8
+ const servers = await stateManager.getAllServers();
9
+
10
+ if (servers.length === 0) {
11
+ console.log(chalk.yellow('No servers configured.'));
12
+ console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
13
+ return;
14
+ }
15
+
16
+ const serversWithStatus = await statusChecker.updateAllServerStatuses();
17
+
18
+ const screen = blessed.screen({
19
+ smartCSR: true,
20
+ title: 'llama.cpp Server Monitor',
21
+ fullUnicode: true,
22
+ });
23
+
24
+ await createRootNavigator(screen, serversWithStatus);
25
+ }