@appkit/llamacpp-cli 1.11.0 → 1.12.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 (126) hide show
  1. package/README.md +572 -170
  2. package/dist/cli.js +99 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/admin/config.d.ts +10 -0
  5. package/dist/commands/admin/config.d.ts.map +1 -0
  6. package/dist/commands/admin/config.js +100 -0
  7. package/dist/commands/admin/config.js.map +1 -0
  8. package/dist/commands/admin/logs.d.ts +10 -0
  9. package/dist/commands/admin/logs.d.ts.map +1 -0
  10. package/dist/commands/admin/logs.js +114 -0
  11. package/dist/commands/admin/logs.js.map +1 -0
  12. package/dist/commands/admin/restart.d.ts +2 -0
  13. package/dist/commands/admin/restart.d.ts.map +1 -0
  14. package/dist/commands/admin/restart.js +29 -0
  15. package/dist/commands/admin/restart.js.map +1 -0
  16. package/dist/commands/admin/start.d.ts +2 -0
  17. package/dist/commands/admin/start.d.ts.map +1 -0
  18. package/dist/commands/admin/start.js +30 -0
  19. package/dist/commands/admin/start.js.map +1 -0
  20. package/dist/commands/admin/status.d.ts +2 -0
  21. package/dist/commands/admin/status.d.ts.map +1 -0
  22. package/dist/commands/admin/status.js +82 -0
  23. package/dist/commands/admin/status.js.map +1 -0
  24. package/dist/commands/admin/stop.d.ts +2 -0
  25. package/dist/commands/admin/stop.d.ts.map +1 -0
  26. package/dist/commands/admin/stop.js +21 -0
  27. package/dist/commands/admin/stop.js.map +1 -0
  28. package/dist/commands/logs.d.ts +1 -0
  29. package/dist/commands/logs.d.ts.map +1 -1
  30. package/dist/commands/logs.js +22 -0
  31. package/dist/commands/logs.js.map +1 -1
  32. package/dist/lib/admin-manager.d.ts +111 -0
  33. package/dist/lib/admin-manager.d.ts.map +1 -0
  34. package/dist/lib/admin-manager.js +413 -0
  35. package/dist/lib/admin-manager.js.map +1 -0
  36. package/dist/lib/admin-server.d.ts +148 -0
  37. package/dist/lib/admin-server.d.ts.map +1 -0
  38. package/dist/lib/admin-server.js +1161 -0
  39. package/dist/lib/admin-server.js.map +1 -0
  40. package/dist/lib/download-job-manager.d.ts +64 -0
  41. package/dist/lib/download-job-manager.d.ts.map +1 -0
  42. package/dist/lib/download-job-manager.js +164 -0
  43. package/dist/lib/download-job-manager.js.map +1 -0
  44. package/dist/tui/MultiServerMonitorApp.js +1 -1
  45. package/dist/types/admin-config.d.ts +19 -0
  46. package/dist/types/admin-config.d.ts.map +1 -0
  47. package/dist/types/admin-config.js +3 -0
  48. package/dist/types/admin-config.js.map +1 -0
  49. package/dist/utils/log-parser.d.ts +9 -0
  50. package/dist/utils/log-parser.d.ts.map +1 -1
  51. package/dist/utils/log-parser.js +11 -0
  52. package/dist/utils/log-parser.js.map +1 -1
  53. package/package.json +10 -2
  54. package/web/README.md +429 -0
  55. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  56. package/web/dist/assets/index-CVmonw3T.js +17 -0
  57. package/web/dist/index.html +14 -0
  58. package/web/dist/vite.svg +1 -0
  59. package/.versionrc.json +0 -16
  60. package/CHANGELOG.md +0 -203
  61. package/MONITORING-ACCURACY-FIX.md +0 -199
  62. package/PER-PROCESS-METRICS.md +0 -190
  63. package/docs/images/.gitkeep +0 -1
  64. package/src/cli.ts +0 -423
  65. package/src/commands/config-global.ts +0 -38
  66. package/src/commands/config.ts +0 -323
  67. package/src/commands/create.ts +0 -183
  68. package/src/commands/delete.ts +0 -74
  69. package/src/commands/list.ts +0 -37
  70. package/src/commands/logs-all.ts +0 -251
  71. package/src/commands/logs.ts +0 -321
  72. package/src/commands/monitor.ts +0 -110
  73. package/src/commands/ps.ts +0 -84
  74. package/src/commands/pull.ts +0 -44
  75. package/src/commands/rm.ts +0 -107
  76. package/src/commands/router/config.ts +0 -116
  77. package/src/commands/router/logs.ts +0 -256
  78. package/src/commands/router/restart.ts +0 -36
  79. package/src/commands/router/start.ts +0 -60
  80. package/src/commands/router/status.ts +0 -119
  81. package/src/commands/router/stop.ts +0 -33
  82. package/src/commands/run.ts +0 -233
  83. package/src/commands/search.ts +0 -107
  84. package/src/commands/server-show.ts +0 -161
  85. package/src/commands/show.ts +0 -207
  86. package/src/commands/start.ts +0 -101
  87. package/src/commands/stop.ts +0 -39
  88. package/src/commands/tui.ts +0 -25
  89. package/src/lib/config-generator.ts +0 -130
  90. package/src/lib/history-manager.ts +0 -172
  91. package/src/lib/launchctl-manager.ts +0 -225
  92. package/src/lib/metrics-aggregator.ts +0 -257
  93. package/src/lib/model-downloader.ts +0 -328
  94. package/src/lib/model-scanner.ts +0 -157
  95. package/src/lib/model-search.ts +0 -114
  96. package/src/lib/models-dir-setup.ts +0 -46
  97. package/src/lib/port-manager.ts +0 -80
  98. package/src/lib/router-logger.ts +0 -201
  99. package/src/lib/router-manager.ts +0 -414
  100. package/src/lib/router-server.ts +0 -538
  101. package/src/lib/state-manager.ts +0 -206
  102. package/src/lib/status-checker.ts +0 -113
  103. package/src/lib/system-collector.ts +0 -315
  104. package/src/tui/ConfigApp.ts +0 -1085
  105. package/src/tui/HistoricalMonitorApp.ts +0 -587
  106. package/src/tui/ModelsApp.ts +0 -368
  107. package/src/tui/MonitorApp.ts +0 -386
  108. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  109. package/src/tui/RootNavigator.ts +0 -74
  110. package/src/tui/SearchApp.ts +0 -511
  111. package/src/tui/SplashScreen.ts +0 -149
  112. package/src/types/global-config.ts +0 -26
  113. package/src/types/history-types.ts +0 -39
  114. package/src/types/model-info.ts +0 -8
  115. package/src/types/monitor-types.ts +0 -162
  116. package/src/types/router-config.ts +0 -25
  117. package/src/types/server-config.ts +0 -46
  118. package/src/utils/downsample-utils.ts +0 -128
  119. package/src/utils/file-utils.ts +0 -146
  120. package/src/utils/format-utils.ts +0 -98
  121. package/src/utils/log-parser.ts +0 -271
  122. package/src/utils/log-utils.ts +0 -178
  123. package/src/utils/process-utils.ts +0 -316
  124. package/src/utils/prompt-utils.ts +0 -47
  125. package/test-load.sh +0 -100
  126. package/tsconfig.json +0 -20
@@ -1,146 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
-
5
- /**
6
- * Ensure a directory exists, creating it if necessary
7
- */
8
- export async function ensureDir(dirPath: string): Promise<void> {
9
- try {
10
- await fs.mkdir(dirPath, { recursive: true, mode: 0o755 });
11
- } catch (error) {
12
- // Ignore error if directory already exists
13
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
14
- throw error;
15
- }
16
- }
17
- }
18
-
19
- /**
20
- * Write a file atomically (write to temp, then rename)
21
- */
22
- export async function writeFileAtomic(filePath: string, content: string): Promise<void> {
23
- const tempPath = `${filePath}.tmp`;
24
- await fs.writeFile(tempPath, content, 'utf-8');
25
- await fs.rename(tempPath, filePath);
26
- }
27
-
28
- /**
29
- * Write JSON to a file atomically
30
- */
31
- export async function writeJsonAtomic(filePath: string, data: any): Promise<void> {
32
- const content = JSON.stringify(data, null, 2);
33
- await writeFileAtomic(filePath, content);
34
- }
35
-
36
- /**
37
- * Read and parse JSON file
38
- */
39
- export async function readJson<T>(filePath: string): Promise<T> {
40
- const content = await fs.readFile(filePath, 'utf-8');
41
- return JSON.parse(content);
42
- }
43
-
44
- /**
45
- * Check if a file exists
46
- */
47
- export async function fileExists(filePath: string): Promise<boolean> {
48
- try {
49
- await fs.access(filePath);
50
- return true;
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- /**
57
- * Get the llamacpp config directory (~/.llamacpp)
58
- */
59
- export function getConfigDir(): string {
60
- return path.join(os.homedir(), '.llamacpp');
61
- }
62
-
63
- /**
64
- * Get the servers directory (~/.llamacpp/servers)
65
- */
66
- export function getServersDir(): string {
67
- return path.join(getConfigDir(), 'servers');
68
- }
69
-
70
- /**
71
- * Get the logs directory (~/.llamacpp/logs)
72
- */
73
- export function getLogsDir(): string {
74
- return path.join(getConfigDir(), 'logs');
75
- }
76
-
77
- /**
78
- * Get the global config file path
79
- */
80
- export function getGlobalConfigPath(): string {
81
- return path.join(getConfigDir(), 'config.json');
82
- }
83
-
84
- /**
85
- * Get the default models directory (~/.llamacpp/models)
86
- */
87
- export function getModelsDir(): string {
88
- return path.join(getConfigDir(), 'models');
89
- }
90
-
91
- /**
92
- * Get the LaunchAgents directory
93
- */
94
- export function getLaunchAgentsDir(): string {
95
- return path.join(os.homedir(), 'Library', 'LaunchAgents');
96
- }
97
-
98
- /**
99
- * Expand tilde (~) in path to home directory
100
- */
101
- export function expandHome(filePath: string): string {
102
- if (filePath.startsWith('~/')) {
103
- return path.join(os.homedir(), filePath.slice(2));
104
- }
105
- return filePath;
106
- }
107
-
108
- /**
109
- * Parse Metal (GPU) memory allocation from llama-server stderr logs
110
- * Looks for line: "load_tensors: Metal_Mapped model buffer size = 11120.23 MiB"
111
- * Returns memory in MB, or null if not found
112
- */
113
- export async function parseMetalMemoryFromLog(stderrPath: string): Promise<number | null> {
114
- try {
115
- // Check if log file exists
116
- if (!(await fileExists(stderrPath))) {
117
- return null;
118
- }
119
-
120
- // Open file for reading
121
- const fileHandle = await fs.open(stderrPath, 'r');
122
-
123
- try {
124
- // Read first 256KB (Metal allocation happens early during model loading)
125
- const buffer = Buffer.alloc(256 * 1024);
126
- const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
127
- const content = buffer.toString('utf-8', 0, bytesRead);
128
- const lines = content.split('\n');
129
-
130
- // Look for Metal_Mapped buffer size
131
- for (const line of lines) {
132
- const match = line.match(/Metal_Mapped model buffer size\s*=\s*([\d.]+)\s*MiB/);
133
- if (match) {
134
- const sizeInMB = parseFloat(match[1]);
135
- return isNaN(sizeInMB) ? null : sizeInMB;
136
- }
137
- }
138
-
139
- return null;
140
- } finally {
141
- await fileHandle.close();
142
- }
143
- } catch {
144
- return null;
145
- }
146
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * Format bytes to human-readable size
3
- * Example: 1900000000 → "1.9 GB"
4
- */
5
- export function formatBytes(bytes: number): string {
6
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
7
- let size = bytes;
8
- let unitIndex = 0;
9
-
10
- while (size >= 1024 && unitIndex < units.length - 1) {
11
- size /= 1024;
12
- unitIndex++;
13
- }
14
-
15
- return `${size.toFixed(1)} ${units[unitIndex]}`;
16
- }
17
-
18
- /**
19
- * Format a date to a human-readable string
20
- * Example: "Nov 20, 2025 10:30 AM"
21
- */
22
- export function formatDate(date: Date | string): string {
23
- const d = typeof date === 'string' ? new Date(date) : date;
24
- return d.toLocaleString('en-US', {
25
- month: 'short',
26
- day: 'numeric',
27
- year: 'numeric',
28
- hour: 'numeric',
29
- minute: '2-digit',
30
- });
31
- }
32
-
33
- /**
34
- * Format a date to a short string (no time)
35
- * Example: "Nov 20"
36
- */
37
- export function formatDateShort(date: Date | string): string {
38
- const d = typeof date === 'string' ? new Date(date) : date;
39
- return d.toLocaleString('en-US', {
40
- month: 'short',
41
- day: 'numeric',
42
- });
43
- }
44
-
45
- /**
46
- * Calculate uptime from start time
47
- * Example: "2d 4h" or "45m" or "30s"
48
- */
49
- export function formatUptime(startTime: string): string {
50
- const start = new Date(startTime).getTime();
51
- const now = Date.now();
52
- const seconds = Math.floor((now - start) / 1000);
53
-
54
- if (seconds < 60) return `${seconds}s`;
55
-
56
- const minutes = Math.floor(seconds / 60);
57
- if (minutes < 60) return `${minutes}m`;
58
-
59
- const hours = Math.floor(minutes / 60);
60
- if (hours < 24) return `${hours}h`;
61
-
62
- const days = Math.floor(hours / 24);
63
- const remainingHours = hours % 24;
64
- return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
65
- }
66
-
67
- /**
68
- * Truncate a string to a maximum length
69
- */
70
- export function truncate(str: string, maxLength: number): string {
71
- if (str.length <= maxLength) return str;
72
- return str.slice(0, maxLength - 3) + '...';
73
- }
74
-
75
- /**
76
- * Pad a string to a specific length
77
- */
78
- export function pad(str: string, length: number, char = ' '): string {
79
- return str.padEnd(length, char);
80
- }
81
-
82
- /**
83
- * Format context size to human-readable format
84
- * Uses "k" suffix for clean multiples of 1024 (e.g., 32768 → "32k")
85
- * Falls back to full number with "tokens" for non-standard sizes
86
- */
87
- export function formatContextSize(tokens: number): string {
88
- // Check if it's a clean multiple of 1024
89
- if (tokens % 1024 === 0) {
90
- const k = tokens / 1024;
91
- // Only use "k" format for reasonable sizes (1k to 1024k i.e., up to 1M)
92
- if (k >= 1 && k <= 1024) {
93
- return `${k}k`;
94
- }
95
- }
96
- // For non-standard sizes or very large values, show full number
97
- return `${tokens.toLocaleString()} tokens`;
98
- }
@@ -1,271 +0,0 @@
1
- /**
2
- * Parse and consolidate verbose llama-server logs into compact single-line format
3
- */
4
-
5
- interface CompactLogEntry {
6
- timestamp: string;
7
- method: string;
8
- endpoint: string;
9
- ip: string;
10
- status: number;
11
- userMessage: string;
12
- tokensIn: number;
13
- tokensOut: number;
14
- responseTimeMs: number;
15
- }
16
-
17
- export class LogParser {
18
- private buffer: string[] = [];
19
- private isBuffering = false;
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
-
35
- /**
36
- * Process log lines and output compact format
37
- */
38
- processLine(line: string, callback: (compactLine: string) => void): void {
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)) {
42
- // Check if this is the start of verbose format (status line before JSON)
43
- // or a simple single-line log
44
- if (this.isBuffering) {
45
- // We're already buffering, so this is a new request - process previous buffer
46
- const compactLine = this.consolidateRequest(this.buffer);
47
- if (compactLine) {
48
- callback(compactLine);
49
- }
50
- this.buffer = [];
51
- this.isBuffering = false;
52
- }
53
-
54
- // Start buffering (might be verbose or simple)
55
- this.isBuffering = true;
56
- this.buffer = [line];
57
- return;
58
- }
59
-
60
- // If we're buffering, collect lines
61
- if (this.isBuffering) {
62
- this.buffer.push(line);
63
-
64
- // Check if we have a complete request (found response line in verbose mode)
65
- if (line.includes('log_server_r: response:')) {
66
- const compactLine = this.consolidateRequest(this.buffer);
67
- if (compactLine) {
68
- callback(compactLine);
69
- }
70
- this.buffer = [];
71
- this.isBuffering = false;
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Flush any buffered simple format logs
78
- * Call this at the end of processing to handle simple logs that don't have response lines
79
- */
80
- flush(callback: (compactLine: string) => void): void {
81
- if (this.isBuffering && this.buffer.length > 0) {
82
- // If we only have one line, it's a simple format log
83
- if (this.buffer.length === 1) {
84
- const simpleLine = this.parseSimpleFormat(this.buffer[0]);
85
- if (simpleLine) {
86
- callback(simpleLine);
87
- }
88
- }
89
- this.buffer = [];
90
- this.isBuffering = false;
91
- }
92
- }
93
-
94
- /**
95
- * Parse simple single-line format (non-verbose mode)
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
99
- */
100
- private parseSimpleFormat(line: string): string | null {
101
- try {
102
- const timestamp = this.extractTimestamp(line);
103
- // Match both "request:" and "done request:" formats
104
- const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
105
- if (!requestMatch) return null;
106
-
107
- const [, method, endpoint, ip, status] = requestMatch;
108
-
109
- // Simple format doesn't include message/token details
110
- return `${timestamp} ${method} ${endpoint} ${ip} ${status}`;
111
- } catch (error) {
112
- return null;
113
- }
114
- }
115
-
116
- /**
117
- * Consolidate buffered request/response lines into single line
118
- * Handles both old and new llama.cpp log formats
119
- */
120
- private consolidateRequest(lines: string[]): string | null {
121
- try {
122
- // Parse first line: timestamp and request info
123
- // Match both "request:" and "done request:" formats
124
- const firstLine = lines[0];
125
- const timestamp = this.extractTimestamp(firstLine);
126
- const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
127
- if (!requestMatch) return null;
128
-
129
- const [, method, endpoint, ip, status] = requestMatch;
130
-
131
- // Parse request JSON (line with JSON body)
132
- const requestLine = lines.find((l) => l.includes('log_server_r: request:') && l.includes('{'));
133
-
134
- let userMessage = '';
135
- if (requestLine) {
136
- const requestJson = this.extractJson(requestLine);
137
- if (requestJson) {
138
- userMessage = this.extractUserMessage(requestJson);
139
- }
140
- }
141
-
142
- // Parse response JSON (may be empty in new format)
143
- const responseLine = lines.find((l) => l.includes('log_server_r: response:'));
144
- let tokensIn = 0;
145
- let tokensOut = 0;
146
- let responseTimeMs = 0;
147
-
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
- }
156
-
157
- // Format compact line (works even without response data)
158
- return this.formatCompactLine({
159
- timestamp,
160
- method,
161
- endpoint,
162
- ip,
163
- status: parseInt(status, 10),
164
- userMessage,
165
- tokensIn,
166
- tokensOut,
167
- responseTimeMs,
168
- });
169
- } catch (error) {
170
- return null;
171
- }
172
- }
173
-
174
- /**
175
- * Extract timestamp from log line
176
- */
177
- private extractTimestamp(line: string): string {
178
- // Look for timestamp format like [2025-12-09 10:13:45]
179
- const match = line.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
180
- if (match) {
181
- return match[1]; // Return as-is: 2025-12-09 10:13:45
182
- }
183
- // If no timestamp in logs, use current time in same format
184
- const now = new Date();
185
- return now.toISOString().substring(0, 19).replace('T', ' '); // 2025-12-09 10:13:45
186
- }
187
-
188
- /**
189
- * Extract JSON from log line
190
- */
191
- private extractJson(line: string): any {
192
- const jsonStart = line.indexOf('{');
193
- if (jsonStart === -1) return null;
194
-
195
- try {
196
- const jsonStr = line.substring(jsonStart);
197
- return JSON.parse(jsonStr);
198
- } catch {
199
- return null;
200
- }
201
- }
202
-
203
- /**
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"}]}
208
- */
209
- private extractUserMessage(requestJson: any): string {
210
- const messages = requestJson.messages || [];
211
- const userMsg = messages.find((m: any) => m.role === 'user');
212
- if (!userMsg || !userMsg.content) return '';
213
-
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();
226
- return content.length > 50 ? content.substring(0, 47) + '...' : content;
227
- }
228
-
229
- /**
230
- * Extract response time from response JSON
231
- */
232
- private extractResponseTime(responseJson: any): number {
233
- // Check __verbose.timings first (has total time)
234
- const verboseTimings = responseJson.__verbose?.timings;
235
- if (verboseTimings) {
236
- const promptMs = verboseTimings.prompt_ms || 0;
237
- const predictedMs = verboseTimings.predicted_ms || 0;
238
- return Math.round(promptMs + predictedMs);
239
- }
240
-
241
- // Fallback to top-level timings
242
- const timings = responseJson.timings;
243
- if (timings) {
244
- const promptMs = timings.prompt_ms || 0;
245
- const predictedMs = timings.predicted_ms || 0;
246
- return Math.round(promptMs + predictedMs);
247
- }
248
-
249
- return 0;
250
- }
251
-
252
- /**
253
- * Format compact log line
254
- */
255
- private formatCompactLine(entry: CompactLogEntry): string {
256
- return [
257
- entry.timestamp,
258
- entry.method,
259
- entry.endpoint,
260
- entry.ip,
261
- entry.status,
262
- `"${entry.userMessage}"`,
263
- entry.tokensIn,
264
- entry.tokensOut,
265
- entry.responseTimeMs,
266
- ].join(' ');
267
- }
268
- }
269
-
270
- // Export singleton instance
271
- export const logParser = new LogParser();
@@ -1,178 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import { fileExists, getLogsDir } from './file-utils';
4
-
5
- /**
6
- * Get the size of a file in bytes
7
- */
8
- export async function getFileSize(filePath: string): Promise<number> {
9
- try {
10
- const stats = await fs.stat(filePath);
11
- return stats.size;
12
- } catch {
13
- return 0;
14
- }
15
- }
16
-
17
- /**
18
- * Format bytes to human-readable size
19
- */
20
- export function formatFileSize(bytes: number): string {
21
- if (bytes === 0) return '0 B';
22
- const k = 1024;
23
- const sizes = ['B', 'KB', 'MB', 'GB'];
24
- const i = Math.floor(Math.log(bytes) / Math.log(k));
25
- return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
26
- }
27
-
28
- /**
29
- * Rotate a log file with timestamp
30
- * Renames current log to <name>.YYYY-MM-DD-HH-MM-SS.<ext>
31
- * Returns the new archived filename
32
- */
33
- export async function rotateLogFile(logPath: string): Promise<string> {
34
- if (!(await fileExists(logPath))) {
35
- throw new Error(`Log file does not exist: ${logPath}`);
36
- }
37
-
38
- // Get file size before rotation
39
- const size = await getFileSize(logPath);
40
- if (size === 0) {
41
- throw new Error('Log file is empty, nothing to rotate');
42
- }
43
-
44
- // Generate timestamp
45
- const timestamp = new Date()
46
- .toISOString()
47
- .replace(/T/, '-')
48
- .replace(/:/g, '-')
49
- .replace(/\..+/, '');
50
-
51
- // Parse path components
52
- const dir = path.dirname(logPath);
53
- const ext = path.extname(logPath);
54
- const basename = path.basename(logPath, ext);
55
-
56
- // New archived filename
57
- const archivedPath = path.join(dir, `${basename}.${timestamp}${ext}`);
58
-
59
- // Rename current log to archived version
60
- await fs.rename(logPath, archivedPath);
61
-
62
- return archivedPath;
63
- }
64
-
65
- /**
66
- * Clear (truncate) a log file to zero bytes
67
- */
68
- export async function clearLogFile(logPath: string): Promise<void> {
69
- if (!(await fileExists(logPath))) {
70
- throw new Error(`Log file does not exist: ${logPath}`);
71
- }
72
-
73
- // Truncate file to 0 bytes
74
- await fs.truncate(logPath, 0);
75
- }
76
-
77
- /**
78
- * Auto-rotate log files if they exceed threshold
79
- * Returns true if rotation occurred, false otherwise
80
- */
81
- export async function autoRotateIfNeeded(
82
- stdoutPath: string,
83
- stderrPath: string,
84
- thresholdMB: number = 100
85
- ): Promise<{ rotated: boolean; files: string[] }> {
86
- const thresholdBytes = thresholdMB * 1024 * 1024;
87
- const rotatedFiles: string[] = [];
88
-
89
- // Check stdout
90
- if (await fileExists(stdoutPath)) {
91
- const stdoutSize = await getFileSize(stdoutPath);
92
- if (stdoutSize > thresholdBytes) {
93
- const archived = await rotateLogFile(stdoutPath);
94
- rotatedFiles.push(archived);
95
- }
96
- }
97
-
98
- // Check stderr
99
- if (await fileExists(stderrPath)) {
100
- const stderrSize = await getFileSize(stderrPath);
101
- if (stderrSize > thresholdBytes) {
102
- const archived = await rotateLogFile(stderrPath);
103
- rotatedFiles.push(archived);
104
- }
105
- }
106
-
107
- return {
108
- rotated: rotatedFiles.length > 0,
109
- files: rotatedFiles,
110
- };
111
- }
112
-
113
- /**
114
- * Get information about archived log files for a server
115
- * Returns count and total size of timestamped archived logs
116
- */
117
- export async function getArchivedLogInfo(serverId: string): Promise<{
118
- count: number;
119
- totalSize: number;
120
- }> {
121
- const logsDir = getLogsDir();
122
- let count = 0;
123
- let totalSize = 0;
124
-
125
- try {
126
- const files = await fs.readdir(logsDir);
127
-
128
- // Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
129
- const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
130
-
131
- for (const file of files) {
132
- if (pattern.test(file)) {
133
- count++;
134
- const filePath = path.join(logsDir, file);
135
- totalSize += await getFileSize(filePath);
136
- }
137
- }
138
- } catch {
139
- // Directory doesn't exist or can't be read
140
- return { count: 0, totalSize: 0 };
141
- }
142
-
143
- return { count, totalSize };
144
- }
145
-
146
- /**
147
- * Delete all archived log files for a server
148
- * Returns count and total size of deleted files
149
- */
150
- export async function deleteArchivedLogs(serverId: string): Promise<{
151
- count: number;
152
- totalSize: number;
153
- }> {
154
- const logsDir = getLogsDir();
155
- let count = 0;
156
- let totalSize = 0;
157
-
158
- try {
159
- const files = await fs.readdir(logsDir);
160
-
161
- // Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
162
- const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
163
-
164
- for (const file of files) {
165
- if (pattern.test(file)) {
166
- const filePath = path.join(logsDir, file);
167
- const size = await getFileSize(filePath);
168
- await fs.unlink(filePath);
169
- count++;
170
- totalSize += size;
171
- }
172
- }
173
- } catch (error) {
174
- throw new Error(`Failed to delete archived logs: ${(error as Error).message}`);
175
- }
176
-
177
- return { count, totalSize };
178
- }