@appkit/llamacpp-cli 1.8.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 (116) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +249 -40
  3. package/dist/cli.js +154 -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 +11 -0
  16. package/dist/commands/router/config.d.ts.map +1 -0
  17. package/dist/commands/router/config.js +100 -0
  18. package/dist/commands/router/config.js.map +1 -0
  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/router/restart.d.ts +2 -0
  24. package/dist/commands/router/restart.d.ts.map +1 -0
  25. package/dist/commands/router/restart.js +39 -0
  26. package/dist/commands/router/restart.js.map +1 -0
  27. package/dist/commands/router/start.d.ts +2 -0
  28. package/dist/commands/router/start.d.ts.map +1 -0
  29. package/dist/commands/router/start.js +60 -0
  30. package/dist/commands/router/start.js.map +1 -0
  31. package/dist/commands/router/status.d.ts +2 -0
  32. package/dist/commands/router/status.d.ts.map +1 -0
  33. package/dist/commands/router/status.js +116 -0
  34. package/dist/commands/router/status.js.map +1 -0
  35. package/dist/commands/router/stop.d.ts +2 -0
  36. package/dist/commands/router/stop.d.ts.map +1 -0
  37. package/dist/commands/router/stop.js +36 -0
  38. package/dist/commands/router/stop.js.map +1 -0
  39. package/dist/commands/tui.d.ts +2 -0
  40. package/dist/commands/tui.d.ts.map +1 -0
  41. package/dist/commands/tui.js +27 -0
  42. package/dist/commands/tui.js.map +1 -0
  43. package/dist/lib/completion.d.ts +5 -0
  44. package/dist/lib/completion.d.ts.map +1 -0
  45. package/dist/lib/completion.js +195 -0
  46. package/dist/lib/completion.js.map +1 -0
  47. package/dist/lib/model-downloader.d.ts +5 -1
  48. package/dist/lib/model-downloader.d.ts.map +1 -1
  49. package/dist/lib/model-downloader.js +53 -20
  50. package/dist/lib/model-downloader.js.map +1 -1
  51. package/dist/lib/router-logger.d.ts +61 -0
  52. package/dist/lib/router-logger.d.ts.map +1 -0
  53. package/dist/lib/router-logger.js +200 -0
  54. package/dist/lib/router-logger.js.map +1 -0
  55. package/dist/lib/router-manager.d.ts +103 -0
  56. package/dist/lib/router-manager.d.ts.map +1 -0
  57. package/dist/lib/router-manager.js +394 -0
  58. package/dist/lib/router-manager.js.map +1 -0
  59. package/dist/lib/router-server.d.ts +61 -0
  60. package/dist/lib/router-server.d.ts.map +1 -0
  61. package/dist/lib/router-server.js +485 -0
  62. package/dist/lib/router-server.js.map +1 -0
  63. package/dist/tui/ConfigApp.d.ts +7 -0
  64. package/dist/tui/ConfigApp.d.ts.map +1 -0
  65. package/dist/tui/ConfigApp.js +1002 -0
  66. package/dist/tui/ConfigApp.js.map +1 -0
  67. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  68. package/dist/tui/HistoricalMonitorApp.js +85 -49
  69. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  70. package/dist/tui/ModelsApp.d.ts +7 -0
  71. package/dist/tui/ModelsApp.d.ts.map +1 -0
  72. package/dist/tui/ModelsApp.js +362 -0
  73. package/dist/tui/ModelsApp.js.map +1 -0
  74. package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
  75. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  76. package/dist/tui/MultiServerMonitorApp.js +1038 -122
  77. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  78. package/dist/tui/RootNavigator.d.ts +7 -0
  79. package/dist/tui/RootNavigator.d.ts.map +1 -0
  80. package/dist/tui/RootNavigator.js +55 -0
  81. package/dist/tui/RootNavigator.js.map +1 -0
  82. package/dist/tui/SearchApp.d.ts +6 -0
  83. package/dist/tui/SearchApp.d.ts.map +1 -0
  84. package/dist/tui/SearchApp.js +451 -0
  85. package/dist/tui/SearchApp.js.map +1 -0
  86. package/dist/tui/SplashScreen.d.ts +16 -0
  87. package/dist/tui/SplashScreen.d.ts.map +1 -0
  88. package/dist/tui/SplashScreen.js +129 -0
  89. package/dist/tui/SplashScreen.js.map +1 -0
  90. package/dist/types/router-config.d.ts +19 -0
  91. package/dist/types/router-config.d.ts.map +1 -0
  92. package/dist/types/router-config.js +3 -0
  93. package/dist/types/router-config.js.map +1 -0
  94. package/package.json +1 -1
  95. package/src/cli.ts +121 -10
  96. package/src/commands/monitor.ts +1 -1
  97. package/src/commands/ps.ts +44 -133
  98. package/src/commands/router/config.ts +116 -0
  99. package/src/commands/router/logs.ts +256 -0
  100. package/src/commands/router/restart.ts +36 -0
  101. package/src/commands/router/start.ts +60 -0
  102. package/src/commands/router/status.ts +119 -0
  103. package/src/commands/router/stop.ts +33 -0
  104. package/src/commands/tui.ts +25 -0
  105. package/src/lib/model-downloader.ts +57 -20
  106. package/src/lib/router-logger.ts +201 -0
  107. package/src/lib/router-manager.ts +414 -0
  108. package/src/lib/router-server.ts +538 -0
  109. package/src/tui/ConfigApp.ts +1085 -0
  110. package/src/tui/HistoricalMonitorApp.ts +88 -49
  111. package/src/tui/ModelsApp.ts +368 -0
  112. package/src/tui/MultiServerMonitorApp.ts +1163 -122
  113. package/src/tui/RootNavigator.ts +74 -0
  114. package/src/tui/SearchApp.ts +511 -0
  115. package/src/tui/SplashScreen.ts +149 -0
  116. package/src/types/router-config.ts +25 -0
@@ -13,6 +13,11 @@ export interface DownloadProgress {
13
13
  speed: string;
14
14
  }
15
15
 
16
+ export interface DownloadOptions {
17
+ silent?: boolean; // Suppress console output (for TUI)
18
+ signal?: AbortSignal; // Abort signal for cancellation
19
+ }
20
+
16
21
  export class ModelDownloader {
17
22
  private modelsDir?: string;
18
23
  private getModelsDirFn?: () => Promise<string>;
@@ -68,7 +73,8 @@ export class ModelDownloader {
68
73
  private downloadFile(
69
74
  url: string,
70
75
  destPath: string,
71
- onProgress?: (downloaded: number, total: number) => void
76
+ onProgress?: (downloaded: number, total: number) => void,
77
+ signal?: AbortSignal
72
78
  ): Promise<void> {
73
79
  return new Promise((resolve, reject) => {
74
80
  const file = fs.createWriteStream(destPath);
@@ -77,6 +83,7 @@ export class ModelDownloader {
77
83
  let lastUpdateTime = Date.now();
78
84
  let lastDownloadedBytes = 0;
79
85
  let completed = false;
86
+ let request: ReturnType<typeof https.get> | null = null;
80
87
 
81
88
  const cleanup = (sigintHandler?: () => void) => {
82
89
  if (sigintHandler) {
@@ -95,22 +102,37 @@ export class ModelDownloader {
95
102
  };
96
103
 
97
104
  const sigintHandler = () => {
98
- request.destroy();
105
+ if (request) request.destroy();
99
106
  handleError(new Error('Download interrupted by user'), sigintHandler);
100
107
  };
101
108
 
102
- const request = https.get(url, { agent: new https.Agent({ keepAlive: false }) }, (response) => {
109
+ // Handle abort signal
110
+ const abortHandler = () => {
111
+ if (request) request.destroy();
112
+ handleError(new Error('Download cancelled'), sigintHandler);
113
+ };
114
+
115
+ if (signal) {
116
+ if (signal.aborted) {
117
+ handleError(new Error('Download cancelled'), sigintHandler);
118
+ return;
119
+ }
120
+ signal.addEventListener('abort', abortHandler, { once: true });
121
+ }
122
+
123
+ request = https.get(url, { agent: new https.Agent({ keepAlive: false }) }, (response) => {
103
124
  // Handle redirects (301, 302, 307, 308)
104
125
  if (response.statusCode === 301 || response.statusCode === 302 ||
105
126
  response.statusCode === 307 || response.statusCode === 308) {
106
127
  const redirectUrl = response.headers.location;
107
128
  if (redirectUrl) {
108
129
  cleanup(sigintHandler);
130
+ if (signal) signal.removeEventListener('abort', abortHandler);
109
131
  // Wait for file to close before starting new download
110
132
  file.close(() => {
111
133
  fs.unlink(destPath, () => {
112
134
  // Start recursive download only after cleanup is complete
113
- this.downloadFile(redirectUrl, destPath, onProgress)
135
+ this.downloadFile(redirectUrl, destPath, onProgress, signal)
114
136
  .then(resolve)
115
137
  .catch(reject);
116
138
  });
@@ -154,6 +176,7 @@ export class ModelDownloader {
154
176
  // Use callback to ensure close completes before resolving
155
177
  file.close((err) => {
156
178
  cleanup(sigintHandler);
179
+ if (signal) signal.removeEventListener('abort', abortHandler);
157
180
  if (err) reject(err);
158
181
  else resolve();
159
182
  });
@@ -161,10 +184,12 @@ export class ModelDownloader {
161
184
  });
162
185
 
163
186
  request.on('error', (err) => {
187
+ if (signal) signal.removeEventListener('abort', abortHandler);
164
188
  handleError(err, sigintHandler);
165
189
  });
166
190
 
167
191
  file.on('error', (err) => {
192
+ if (signal) signal.removeEventListener('abort', abortHandler);
168
193
  handleError(err, sigintHandler);
169
194
  });
170
195
 
@@ -200,15 +225,21 @@ export class ModelDownloader {
200
225
  repoId: string,
201
226
  filename: string,
202
227
  onProgress?: (progress: DownloadProgress) => void,
203
- modelsDir?: string
228
+ modelsDir?: string,
229
+ options?: DownloadOptions
204
230
  ): Promise<string> {
231
+ const silent = options?.silent ?? false;
232
+ const signal = options?.signal;
233
+
205
234
  // Use provided models directory or get from config
206
235
  const targetDir = modelsDir || await this.getModelsDirectory();
207
236
 
208
- console.log(chalk.blue(`📥 Downloading ${filename} from Hugging Face...`));
209
- console.log(chalk.dim(`Repository: ${repoId}`));
210
- console.log(chalk.dim(`Destination: ${targetDir}`));
211
- console.log();
237
+ if (!silent) {
238
+ console.log(chalk.blue(`📥 Downloading ${filename} from Hugging Face...`));
239
+ console.log(chalk.dim(`Repository: ${repoId}`));
240
+ console.log(chalk.dim(`Destination: ${targetDir}`));
241
+ console.log();
242
+ }
212
243
 
213
244
  // Build download URL
214
245
  const url = this.buildDownloadUrl(repoId, filename);
@@ -216,8 +247,10 @@ export class ModelDownloader {
216
247
 
217
248
  // Check if file already exists
218
249
  if (fs.existsSync(destPath)) {
219
- console.log(chalk.yellow(`⚠️ File already exists: ${filename}`));
220
- console.log(chalk.dim(' Remove it first or choose a different filename'));
250
+ if (!silent) {
251
+ console.log(chalk.yellow(`⚠️ File already exists: ${filename}`));
252
+ console.log(chalk.dim(' Remove it first or choose a different filename'));
253
+ }
221
254
  throw new Error('File already exists');
222
255
  }
223
256
 
@@ -237,8 +270,10 @@ export class ModelDownloader {
237
270
  lastTime = now;
238
271
  lastDownloaded = downloaded;
239
272
 
240
- // Display progress bar
241
- this.displayProgress(downloaded, total, filename);
273
+ // Display progress bar (only if not silent)
274
+ if (!silent) {
275
+ this.displayProgress(downloaded, total, filename);
276
+ }
242
277
 
243
278
  // Call user progress callback if provided
244
279
  if (onProgress) {
@@ -250,15 +285,17 @@ export class ModelDownloader {
250
285
  speed: `${formatBytes(speed)}/s`,
251
286
  });
252
287
  }
253
- });
288
+ }, signal);
254
289
 
255
- // Clear progress line and show completion
256
- process.stdout.write('\r\x1b[K');
257
- console.log(chalk.green('✅ Download complete!'));
290
+ if (!silent) {
291
+ // Clear progress line and show completion
292
+ process.stdout.write('\r\x1b[K');
293
+ console.log(chalk.green('✅ Download complete!'));
258
294
 
259
- const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
260
- console.log(chalk.dim(` Time: ${totalTime}s`));
261
- console.log(chalk.dim(` Location: ${destPath}`));
295
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
296
+ console.log(chalk.dim(` Time: ${totalTime}s`));
297
+ console.log(chalk.dim(` Location: ${destPath}`));
298
+ }
262
299
 
263
300
  return destPath;
264
301
  }
@@ -0,0 +1,201 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { getLogsDir } from '../utils/file-utils';
4
+
5
+ export interface RouterLogEntry {
6
+ timestamp: string;
7
+ model: string;
8
+ endpoint: string;
9
+ method: string;
10
+ status: 'success' | 'error';
11
+ statusCode: number;
12
+ durationMs: number;
13
+ error?: string;
14
+ backend?: string; // e.g., "localhost:9001"
15
+ prompt?: string; // First part of the prompt/message
16
+ }
17
+
18
+ export class RouterLogger {
19
+ private logFilePath: string;
20
+ private verbose: boolean;
21
+
22
+ constructor(verbose: boolean = false) {
23
+ this.verbose = verbose;
24
+ this.logFilePath = path.join(getLogsDir(), 'router.log');
25
+ }
26
+
27
+ /**
28
+ * Log a request with timing and outcome
29
+ */
30
+ async logRequest(entry: RouterLogEntry): Promise<void> {
31
+ // Human-readable format for console
32
+ const humanLog = this.formatHumanReadable(entry);
33
+
34
+ // Output request activity to stdout (separate from system messages on stderr)
35
+ console.log(humanLog);
36
+
37
+ // Verbose mode: append detailed JSON to log file
38
+ if (this.verbose) {
39
+ const jsonLog = JSON.stringify(entry) + '\n';
40
+ try {
41
+ await fs.appendFile(this.logFilePath, jsonLog, 'utf-8');
42
+ } catch (error) {
43
+ console.error('[Router Logger] Failed to write to log file:', error);
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Format log entry for human reading (console output)
50
+ */
51
+ private formatHumanReadable(entry: RouterLogEntry): string {
52
+ const { timestamp, model, endpoint, method, status, statusCode, durationMs, error, backend, prompt } = entry;
53
+
54
+ // Color coding based on status (using ANSI codes)
55
+ const statusColor = status === 'success' ? '\x1b[32m' : '\x1b[31m'; // Green or Red
56
+ const resetColor = '\x1b[0m';
57
+
58
+ // Base log format (no [Router] prefix, no icons)
59
+ let log = `${statusColor}${statusCode}${resetColor} ${method} ${endpoint} → ${model}`;
60
+
61
+ // Add backend if available
62
+ if (backend) {
63
+ log += ` (${backend})`;
64
+ }
65
+
66
+ // Add duration
67
+ log += ` ${durationMs}ms`;
68
+
69
+ // Add prompt preview if available
70
+ if (prompt) {
71
+ log += ` | "${prompt}"`;
72
+ }
73
+
74
+ // Add error if present
75
+ if (error) {
76
+ log += ` | Error: ${error}`;
77
+ }
78
+
79
+ return log;
80
+ }
81
+
82
+ /**
83
+ * Format log entry for LLM parsing (verbose JSON format)
84
+ */
85
+ static formatForLLM(entry: RouterLogEntry): string {
86
+ return JSON.stringify(entry, null, 2);
87
+ }
88
+
89
+ /**
90
+ * Read log file and return all entries (for verbose mode)
91
+ */
92
+ async readLogs(limit?: number): Promise<RouterLogEntry[]> {
93
+ try {
94
+ const content = await fs.readFile(this.logFilePath, 'utf-8');
95
+ const lines = content.trim().split('\n').filter(line => line);
96
+
97
+ // Parse JSON entries
98
+ const entries = lines
99
+ .map(line => {
100
+ try {
101
+ return JSON.parse(line) as RouterLogEntry;
102
+ } catch {
103
+ return null;
104
+ }
105
+ })
106
+ .filter((entry): entry is RouterLogEntry => entry !== null);
107
+
108
+ // Apply limit if specified
109
+ if (limit && limit > 0) {
110
+ return entries.slice(-limit);
111
+ }
112
+
113
+ return entries;
114
+ } catch (error) {
115
+ // Log file doesn't exist or can't be read
116
+ return [];
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Clear the log file
122
+ */
123
+ async clearLogs(): Promise<void> {
124
+ try {
125
+ await fs.writeFile(this.logFilePath, '', 'utf-8');
126
+ console.error('[Router Logger] Log file cleared');
127
+ } catch (error) {
128
+ console.error('[Router Logger] Failed to clear log file:', error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get log file size
134
+ */
135
+ async getLogFileSize(): Promise<number> {
136
+ try {
137
+ const stats = await fs.stat(this.logFilePath);
138
+ return stats.size;
139
+ } catch {
140
+ return 0;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Rotate log file if it exceeds threshold
146
+ */
147
+ async rotateIfNeeded(thresholdMB: number = 100): Promise<boolean> {
148
+ const size = await this.getLogFileSize();
149
+ const thresholdBytes = thresholdMB * 1024 * 1024;
150
+
151
+ if (size > thresholdBytes) {
152
+ try {
153
+ // Generate timestamp
154
+ const timestamp = new Date()
155
+ .toISOString()
156
+ .replace(/T/, '-')
157
+ .replace(/:/g, '-')
158
+ .replace(/\..+/, '');
159
+
160
+ const logsDir = getLogsDir();
161
+ const archivedPath = path.join(logsDir, `router.${timestamp}.log`);
162
+
163
+ // Rename current log to archived version
164
+ await fs.rename(this.logFilePath, archivedPath);
165
+
166
+ console.error(`[Router Logger] Rotated log file to ${archivedPath}`);
167
+ return true;
168
+ } catch (error) {
169
+ console.error('[Router Logger] Failed to rotate log file:', error);
170
+ return false;
171
+ }
172
+ }
173
+
174
+ return false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Utility class for tracking request timing
180
+ */
181
+ export class RequestTimer {
182
+ private startTime: number;
183
+
184
+ constructor() {
185
+ this.startTime = Date.now();
186
+ }
187
+
188
+ /**
189
+ * Get elapsed time in milliseconds
190
+ */
191
+ elapsed(): number {
192
+ return Date.now() - this.startTime;
193
+ }
194
+
195
+ /**
196
+ * Get current ISO timestamp
197
+ */
198
+ static now(): string {
199
+ return new Date().toISOString();
200
+ }
201
+ }