@appkit/llamacpp-cli 1.9.0 → 1.10.1

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 (98) hide show
  1. package/CHANGELOG.md +28 -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/dist/utils/log-parser.d.ts +14 -1
  76. package/dist/utils/log-parser.d.ts.map +1 -1
  77. package/dist/utils/log-parser.js +57 -26
  78. package/dist/utils/log-parser.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/cli.ts +41 -10
  81. package/src/commands/monitor.ts +1 -1
  82. package/src/commands/ps.ts +44 -133
  83. package/src/commands/router/config.ts +9 -2
  84. package/src/commands/router/logs.ts +256 -0
  85. package/src/commands/tui.ts +25 -0
  86. package/src/lib/model-downloader.ts +57 -20
  87. package/src/lib/router-logger.ts +201 -0
  88. package/src/lib/router-manager.ts +1 -0
  89. package/src/lib/router-server.ts +193 -62
  90. package/src/tui/ConfigApp.ts +1085 -0
  91. package/src/tui/HistoricalMonitorApp.ts +88 -49
  92. package/src/tui/ModelsApp.ts +368 -0
  93. package/src/tui/MultiServerMonitorApp.ts +1163 -122
  94. package/src/tui/RootNavigator.ts +74 -0
  95. package/src/tui/SearchApp.ts +511 -0
  96. package/src/tui/SplashScreen.ts +149 -0
  97. package/src/types/router-config.ts +1 -0
  98. package/src/utils/log-parser.ts +61 -25
@@ -0,0 +1,149 @@
1
+ import blessed from "blessed";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ // Get version from package.json at runtime
6
+ function getVersion(): string {
7
+ try {
8
+ // Try to find package.json relative to this file's location
9
+ // Works both in src/ (dev) and dist/ (production)
10
+ const possiblePaths = [
11
+ join(__dirname, "../../package.json"), // From dist/tui/
12
+ join(__dirname, "../../../package.json"), // Fallback
13
+ ];
14
+
15
+ for (const pkgPath of possiblePaths) {
16
+ try {
17
+ const content = readFileSync(pkgPath, "utf-8");
18
+ const pkg = JSON.parse(content);
19
+ if (pkg.version) return pkg.version;
20
+ } catch {
21
+ continue;
22
+ }
23
+ }
24
+ return "1.0.0";
25
+ } catch {
26
+ return "1.0.0";
27
+ }
28
+ }
29
+
30
+ // ASCII art logo for llama.cpp
31
+ const LOGO = `
32
+ {cyan-fg} ██╗ ██╗ █████╗ ███╗ ███╗ █████╗ ██████╗██████╗ ██████╗{/cyan-fg}
33
+ {cyan-fg}██║ ██║ ██╔══██╗████╗ ████║██╔══██╗ ██╔════╝██╔══██╗██╔══██╗{/cyan-fg}
34
+ {cyan-fg}██║ ██║ ███████║██╔████╔██║███████║ ██║ ██████╔╝██████╔╝{/cyan-fg}
35
+ {cyan-fg}██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║ ██║ ██╔═══╝ ██╔═══╝{/cyan-fg}
36
+ {cyan-fg}███████╗███████╗██║ ██║██║ ╚═╝ ██║██║ ██║ ╚██████╗██║ ██║{/cyan-fg}
37
+ {cyan-fg}╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝{/cyan-fg}
38
+ `.trim();
39
+
40
+ export interface SplashCallbacks {
41
+ onLoadConfigs: () => Promise<void>;
42
+ onCheckServices: () => Promise<void>;
43
+ onInitMetrics: () => Promise<void>;
44
+ }
45
+
46
+ export interface SplashScreenControls {
47
+ updateStatus: (line: number, text: string, done?: boolean) => void;
48
+ complete: () => void;
49
+ }
50
+
51
+ /**
52
+ * Creates and displays the splash screen with loading status
53
+ * Returns a cleanup function to remove the splash when the main UI is ready
54
+ */
55
+ export async function createSplashScreen(
56
+ screen: blessed.Widgets.Screen,
57
+ callbacks: SplashCallbacks,
58
+ ): Promise<() => void> {
59
+ const version = getVersion();
60
+
61
+ // Create top-left aligned container
62
+ const container = blessed.box({
63
+ top: 0,
64
+ left: 0,
65
+ width: 75,
66
+ height: 19,
67
+ tags: true,
68
+ });
69
+ screen.append(container);
70
+
71
+ // Logo box
72
+ const logoBox = blessed.box({
73
+ parent: container,
74
+ top: 0,
75
+ left: 0,
76
+ width: 73,
77
+ height: 7,
78
+ tags: true,
79
+ content: LOGO,
80
+ });
81
+
82
+ // Info box with border
83
+ const infoBox = blessed.box({
84
+ parent: container,
85
+ top: 7,
86
+ left: 0,
87
+ width: 73,
88
+ height: 10,
89
+ tags: true,
90
+ border: {
91
+ type: "line",
92
+ },
93
+ style: {
94
+ border: {
95
+ fg: "blue",
96
+ },
97
+ },
98
+ });
99
+
100
+ // Status lines (inside the info box)
101
+ const statusLines = [
102
+ "{bold}Local LLM Server Manager{/bold} v" +
103
+ version,
104
+ "",
105
+ "{gray-fg}>{/gray-fg} Loading server configurations...",
106
+ "{gray-fg}>{/gray-fg} Checking launchctl services...",
107
+ "{gray-fg}>{/gray-fg} Initializing metrics collectors...",
108
+ "{gray-fg}>{/gray-fg} Loading UI...",
109
+ ];
110
+
111
+ function updateDisplay() {
112
+ infoBox.setContent(statusLines.join("\n"));
113
+ screen.render();
114
+ }
115
+
116
+ function updateStatus(line: number, text: string, done: boolean = false) {
117
+ const prefix = done ? "{green-fg}✓{/green-fg}" : "{yellow-fg}>{/yellow-fg}";
118
+ statusLines[line + 2] = `${prefix} ${text}`;
119
+ updateDisplay();
120
+ }
121
+
122
+ // Initial render
123
+ updateDisplay();
124
+
125
+ // Run loading sequence
126
+ // Step 1: Load configs
127
+ updateStatus(0, "Loading server configurations...", false);
128
+ await callbacks.onLoadConfigs();
129
+ updateStatus(0, "Server configurations loaded", true);
130
+
131
+ // Step 2: Check services
132
+ updateStatus(1, "Checking launchctl services...", false);
133
+ await callbacks.onCheckServices();
134
+ updateStatus(1, "Launchctl services checked", true);
135
+
136
+ // Step 3: Init metrics
137
+ updateStatus(2, "Initializing metrics collectors...", false);
138
+ await callbacks.onInitMetrics();
139
+ updateStatus(2, "Metrics collectors ready", true);
140
+
141
+ // Step 4: Loading UI (stays active until cleanup is called)
142
+ updateStatus(3, "Loading UI...", false);
143
+
144
+ // Return cleanup function - caller decides when to remove splash
145
+ return () => {
146
+ screen.remove(container);
147
+ screen.render();
148
+ };
149
+ }
@@ -21,4 +21,5 @@ export interface RouterConfig {
21
21
  // Router settings
22
22
  healthCheckInterval: number; // ms between health checks (default: 5000)
23
23
  requestTimeout: number; // ms for backend requests (default: 120000)
24
+ verbose: boolean; // Enable verbose logging to file (default: false)
24
25
  }
@@ -18,13 +18,27 @@ export class LogParser {
18
18
  private buffer: string[] = [];
19
19
  private isBuffering = false;
20
20
 
21
+ /**
22
+ * Check if line is a request status line (contains method/endpoint/status, no JSON)
23
+ * Handles both old and new formats:
24
+ * - Old: log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
25
+ * - New: log_server_r: done request: POST /v1/messages 172.16.0.114 200
26
+ */
27
+ private isRequestStatusLine(line: string): boolean {
28
+ return (
29
+ (line.includes('log_server_r: request:') || line.includes('log_server_r: done request:')) &&
30
+ !line.includes('{') &&
31
+ /(?:done )?request: (POST|GET|PUT|DELETE)/.test(line)
32
+ );
33
+ }
34
+
21
35
  /**
22
36
  * Process log lines and output compact format
23
37
  */
24
38
  processLine(line: string, callback: (compactLine: string) => void): void {
25
- // Check if this is a simple single-line format (no JSON, non-verbose mode)
26
- // Format: srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
27
- if (line.includes('log_server_r: request:') && !line.includes('{')) {
39
+ // Check if this is a request status line (no JSON, has method/endpoint/status)
40
+ // Handles both old format (request:) and new format (done request:)
41
+ if (this.isRequestStatusLine(line)) {
28
42
  // Check if this is the start of verbose format (status line before JSON)
29
43
  // or a simple single-line log
30
44
  if (this.isBuffering) {
@@ -79,12 +93,15 @@ export class LogParser {
79
93
 
80
94
  /**
81
95
  * Parse simple single-line format (non-verbose mode)
82
- * Format: srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
96
+ * Handles both old and new formats:
97
+ * - Old: srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
98
+ * - New: srv log_server_r: done request: POST /v1/messages 172.16.0.114 200
83
99
  */
84
100
  private parseSimpleFormat(line: string): string | null {
85
101
  try {
86
102
  const timestamp = this.extractTimestamp(line);
87
- const requestMatch = line.match(/request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
103
+ // Match both "request:" and "done request:" formats
104
+ const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
88
105
  if (!requestMatch) return null;
89
106
 
90
107
  const [, method, endpoint, ip, status] = requestMatch;
@@ -98,40 +115,46 @@ export class LogParser {
98
115
 
99
116
  /**
100
117
  * Consolidate buffered request/response lines into single line
118
+ * Handles both old and new llama.cpp log formats
101
119
  */
102
120
  private consolidateRequest(lines: string[]): string | null {
103
121
  try {
104
122
  // Parse first line: timestamp and request info
123
+ // Match both "request:" and "done request:" formats
105
124
  const firstLine = lines[0];
106
125
  const timestamp = this.extractTimestamp(firstLine);
107
- const requestMatch = firstLine.match(/request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
126
+ const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
108
127
  if (!requestMatch) return null;
109
128
 
110
129
  const [, method, endpoint, ip, status] = requestMatch;
111
130
 
112
- // Parse request JSON (second line)
131
+ // Parse request JSON (line with JSON body)
113
132
  const requestLine = lines.find((l) => l.includes('log_server_r: request:') && l.includes('{'));
114
- if (!requestLine) return null;
115
133
 
116
- const requestJson = this.extractJson(requestLine);
117
- if (!requestJson) return null;
118
-
119
- const userMessage = this.extractUserMessage(requestJson);
134
+ let userMessage = '';
135
+ if (requestLine) {
136
+ const requestJson = this.extractJson(requestLine);
137
+ if (requestJson) {
138
+ userMessage = this.extractUserMessage(requestJson);
139
+ }
140
+ }
120
141
 
121
- // Parse response JSON (last line)
142
+ // Parse response JSON (may be empty in new format)
122
143
  const responseLine = lines.find((l) => l.includes('log_server_r: response:'));
123
- if (!responseLine) return null;
144
+ let tokensIn = 0;
145
+ let tokensOut = 0;
146
+ let responseTimeMs = 0;
124
147
 
125
- const responseJson = this.extractJson(responseLine);
126
- if (!responseJson) return null;
127
-
128
- const tokensIn = responseJson.usage?.prompt_tokens || 0;
129
- const tokensOut = responseJson.usage?.completion_tokens || 0;
130
-
131
- // Extract response time from verbose timings
132
- const responseTimeMs = this.extractResponseTime(responseJson);
148
+ if (responseLine) {
149
+ const responseJson = this.extractJson(responseLine);
150
+ if (responseJson) {
151
+ tokensIn = responseJson.usage?.prompt_tokens || 0;
152
+ tokensOut = responseJson.usage?.completion_tokens || 0;
153
+ responseTimeMs = this.extractResponseTime(responseJson);
154
+ }
155
+ }
133
156
 
134
- // Format compact line
157
+ // Format compact line (works even without response data)
135
158
  return this.formatCompactLine({
136
159
  timestamp,
137
160
  method,
@@ -179,14 +202,27 @@ export class LogParser {
179
202
 
180
203
  /**
181
204
  * Extract first user message from request JSON
205
+ * Handles both string content and array content formats:
206
+ * - String: {"role":"user","content":"Hello"}
207
+ * - Array: {"role":"user","content":[{"type":"text","text":"Hello"}]}
182
208
  */
183
209
  private extractUserMessage(requestJson: any): string {
184
210
  const messages = requestJson.messages || [];
185
211
  const userMsg = messages.find((m: any) => m.role === 'user');
186
212
  if (!userMsg || !userMsg.content) return '';
187
213
 
188
- // Truncate to first 50 characters
189
- const content = userMsg.content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
214
+ let content: string;
215
+
216
+ // Handle array format (e.g., Claude/Anthropic API style)
217
+ if (Array.isArray(userMsg.content)) {
218
+ const textPart = userMsg.content.find((p: any) => p.type === 'text');
219
+ content = textPart?.text || '';
220
+ } else {
221
+ content = userMsg.content;
222
+ }
223
+
224
+ // Clean and truncate to first 50 characters
225
+ content = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
190
226
  return content.length > 50 ? content.substring(0, 47) + '...' : content;
191
227
  }
192
228