@appkit/llamacpp-cli 1.12.0 → 1.13.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.
- package/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import * as https from 'https';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { getModelsDir } from '../utils/file-utils';
|
|
6
|
-
import { formatBytes } from '../utils/format-utils';
|
|
7
|
-
|
|
8
|
-
export interface DownloadProgress {
|
|
9
|
-
filename: string;
|
|
10
|
-
downloaded: number;
|
|
11
|
-
total: number;
|
|
12
|
-
percentage: number;
|
|
13
|
-
speed: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface DownloadOptions {
|
|
17
|
-
silent?: boolean; // Suppress console output (for TUI)
|
|
18
|
-
signal?: AbortSignal; // Abort signal for cancellation
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class ModelDownloader {
|
|
22
|
-
private modelsDir?: string;
|
|
23
|
-
private getModelsDirFn?: () => Promise<string>;
|
|
24
|
-
|
|
25
|
-
constructor(modelsDir?: string, getModelsDirFn?: () => Promise<string>) {
|
|
26
|
-
this.modelsDir = modelsDir;
|
|
27
|
-
this.getModelsDirFn = getModelsDirFn;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get the models directory (either configured or default)
|
|
32
|
-
*/
|
|
33
|
-
private async getModelsDirectory(): Promise<string> {
|
|
34
|
-
if (this.modelsDir) {
|
|
35
|
-
return this.modelsDir;
|
|
36
|
-
}
|
|
37
|
-
if (this.getModelsDirFn) {
|
|
38
|
-
return await this.getModelsDirFn();
|
|
39
|
-
}
|
|
40
|
-
return getModelsDir();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse Hugging Face identifier
|
|
45
|
-
* Examples:
|
|
46
|
-
* "bartowski/Llama-3.2-3B-Instruct-GGUF" → { repo: "...", file: undefined }
|
|
47
|
-
* "bartowski/Llama-3.2-3B-Instruct-GGUF/file.gguf" → { repo: "...", file: "file.gguf" }
|
|
48
|
-
*/
|
|
49
|
-
parseHFIdentifier(identifier: string): { repo: string; file?: string } {
|
|
50
|
-
const parts = identifier.split('/');
|
|
51
|
-
if (parts.length === 2) {
|
|
52
|
-
return { repo: identifier };
|
|
53
|
-
} else if (parts.length === 3) {
|
|
54
|
-
return {
|
|
55
|
-
repo: `${parts[0]}/${parts[1]}`,
|
|
56
|
-
file: parts[2],
|
|
57
|
-
};
|
|
58
|
-
} else {
|
|
59
|
-
throw new Error(`Invalid Hugging Face identifier: ${identifier}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Build Hugging Face download URL
|
|
65
|
-
*/
|
|
66
|
-
buildDownloadUrl(repoId: string, filename: string, branch = 'main'): string {
|
|
67
|
-
return `https://huggingface.co/${repoId}/resolve/${branch}/${filename}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Download a file via HTTPS with progress tracking
|
|
72
|
-
*/
|
|
73
|
-
private downloadFile(
|
|
74
|
-
url: string,
|
|
75
|
-
destPath: string,
|
|
76
|
-
onProgress?: (downloaded: number, total: number) => void,
|
|
77
|
-
signal?: AbortSignal
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
const file = fs.createWriteStream(destPath);
|
|
81
|
-
let downloadedBytes = 0;
|
|
82
|
-
let totalBytes = 0;
|
|
83
|
-
let lastUpdateTime = Date.now();
|
|
84
|
-
let lastDownloadedBytes = 0;
|
|
85
|
-
let completed = false;
|
|
86
|
-
let request: ReturnType<typeof https.get> | null = null;
|
|
87
|
-
|
|
88
|
-
const cleanup = (sigintHandler?: () => void) => {
|
|
89
|
-
if (sigintHandler) {
|
|
90
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const handleError = (err: Error, sigintHandler?: () => void) => {
|
|
95
|
-
if (completed) return;
|
|
96
|
-
completed = true;
|
|
97
|
-
cleanup(sigintHandler);
|
|
98
|
-
file.close(() => {
|
|
99
|
-
fs.unlink(destPath, () => {});
|
|
100
|
-
});
|
|
101
|
-
reject(err);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const sigintHandler = () => {
|
|
105
|
-
if (request) request.destroy();
|
|
106
|
-
handleError(new Error('Download interrupted by user'), sigintHandler);
|
|
107
|
-
};
|
|
108
|
-
|
|
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) => {
|
|
124
|
-
// Handle redirects (301, 302, 307, 308)
|
|
125
|
-
if (response.statusCode === 301 || response.statusCode === 302 ||
|
|
126
|
-
response.statusCode === 307 || response.statusCode === 308) {
|
|
127
|
-
const redirectUrl = response.headers.location;
|
|
128
|
-
if (redirectUrl) {
|
|
129
|
-
cleanup(sigintHandler);
|
|
130
|
-
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
131
|
-
// Wait for file to close before starting new download
|
|
132
|
-
file.close(() => {
|
|
133
|
-
fs.unlink(destPath, () => {
|
|
134
|
-
// Start recursive download only after cleanup is complete
|
|
135
|
-
this.downloadFile(redirectUrl, destPath, onProgress, signal)
|
|
136
|
-
.then(resolve)
|
|
137
|
-
.catch(reject);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (response.statusCode !== 200) {
|
|
145
|
-
return handleError(
|
|
146
|
-
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`),
|
|
147
|
-
sigintHandler
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
|
152
|
-
|
|
153
|
-
response.on('data', (chunk: Buffer) => {
|
|
154
|
-
downloadedBytes += chunk.length;
|
|
155
|
-
|
|
156
|
-
// Update progress every 500ms
|
|
157
|
-
const now = Date.now();
|
|
158
|
-
if (onProgress && now - lastUpdateTime >= 500) {
|
|
159
|
-
onProgress(downloadedBytes, totalBytes);
|
|
160
|
-
lastUpdateTime = now;
|
|
161
|
-
lastDownloadedBytes = downloadedBytes;
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
response.pipe(file);
|
|
166
|
-
|
|
167
|
-
file.on('finish', () => {
|
|
168
|
-
if (completed) return;
|
|
169
|
-
completed = true;
|
|
170
|
-
|
|
171
|
-
// Final progress update
|
|
172
|
-
if (onProgress) {
|
|
173
|
-
onProgress(downloadedBytes, totalBytes);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Use callback to ensure close completes before resolving
|
|
177
|
-
file.close((err) => {
|
|
178
|
-
cleanup(sigintHandler);
|
|
179
|
-
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
180
|
-
if (err) reject(err);
|
|
181
|
-
else resolve();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
request.on('error', (err) => {
|
|
187
|
-
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
188
|
-
handleError(err, sigintHandler);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
file.on('error', (err) => {
|
|
192
|
-
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
193
|
-
handleError(err, sigintHandler);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Handle Ctrl+C gracefully
|
|
197
|
-
process.on('SIGINT', sigintHandler);
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Display progress bar
|
|
203
|
-
*/
|
|
204
|
-
private displayProgress(downloaded: number, total: number, filename: string): void {
|
|
205
|
-
const percentage = total > 0 ? (downloaded / total) * 100 : 0;
|
|
206
|
-
const barLength = 40;
|
|
207
|
-
const filledLength = Math.round((barLength * downloaded) / total);
|
|
208
|
-
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
|
209
|
-
|
|
210
|
-
const downloadedFormatted = formatBytes(downloaded);
|
|
211
|
-
const totalFormatted = formatBytes(total);
|
|
212
|
-
const percentFormatted = percentage.toFixed(1);
|
|
213
|
-
|
|
214
|
-
// Clear line and print progress
|
|
215
|
-
process.stdout.write('\r\x1b[K');
|
|
216
|
-
process.stdout.write(
|
|
217
|
-
chalk.blue(`[${bar}] ${percentFormatted}% | ${downloadedFormatted} / ${totalFormatted}`)
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Download a model from Hugging Face
|
|
223
|
-
*/
|
|
224
|
-
async downloadModel(
|
|
225
|
-
repoId: string,
|
|
226
|
-
filename: string,
|
|
227
|
-
onProgress?: (progress: DownloadProgress) => void,
|
|
228
|
-
modelsDir?: string,
|
|
229
|
-
options?: DownloadOptions
|
|
230
|
-
): Promise<string> {
|
|
231
|
-
const silent = options?.silent ?? false;
|
|
232
|
-
const signal = options?.signal;
|
|
233
|
-
|
|
234
|
-
// Use provided models directory or get from config
|
|
235
|
-
const targetDir = modelsDir || await this.getModelsDirectory();
|
|
236
|
-
|
|
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
|
-
}
|
|
243
|
-
|
|
244
|
-
// Build download URL
|
|
245
|
-
const url = this.buildDownloadUrl(repoId, filename);
|
|
246
|
-
const destPath = path.join(targetDir, filename);
|
|
247
|
-
|
|
248
|
-
// Check if file already exists
|
|
249
|
-
if (fs.existsSync(destPath)) {
|
|
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
|
-
}
|
|
254
|
-
throw new Error('File already exists');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Download with progress
|
|
258
|
-
const startTime = Date.now();
|
|
259
|
-
let lastDownloaded = 0;
|
|
260
|
-
let lastTime = startTime;
|
|
261
|
-
|
|
262
|
-
await this.downloadFile(url, destPath, (downloaded, total) => {
|
|
263
|
-
// Calculate speed
|
|
264
|
-
const now = Date.now();
|
|
265
|
-
const timeDiff = (now - lastTime) / 1000; // seconds
|
|
266
|
-
const bytesDiff = downloaded - lastDownloaded;
|
|
267
|
-
const speed = timeDiff > 0 ? bytesDiff / timeDiff : 0;
|
|
268
|
-
|
|
269
|
-
// Update for next calculation
|
|
270
|
-
lastTime = now;
|
|
271
|
-
lastDownloaded = downloaded;
|
|
272
|
-
|
|
273
|
-
// Display progress bar (only if not silent)
|
|
274
|
-
if (!silent) {
|
|
275
|
-
this.displayProgress(downloaded, total, filename);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Call user progress callback if provided
|
|
279
|
-
if (onProgress) {
|
|
280
|
-
onProgress({
|
|
281
|
-
filename,
|
|
282
|
-
downloaded,
|
|
283
|
-
total,
|
|
284
|
-
percentage: total > 0 ? (downloaded / total) * 100 : 0,
|
|
285
|
-
speed: `${formatBytes(speed)}/s`,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}, signal);
|
|
289
|
-
|
|
290
|
-
if (!silent) {
|
|
291
|
-
// Clear progress line and show completion
|
|
292
|
-
process.stdout.write('\r\x1b[K');
|
|
293
|
-
console.log(chalk.green('✅ Download complete!'));
|
|
294
|
-
|
|
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
|
-
}
|
|
299
|
-
|
|
300
|
-
return destPath;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* List GGUF files in a Hugging Face repository
|
|
305
|
-
* (This would require calling the HF API - simplified for now)
|
|
306
|
-
*/
|
|
307
|
-
async listGGUFFiles(repoId: string): Promise<string[]> {
|
|
308
|
-
console.log(chalk.yellow('Listing files is not yet implemented.'));
|
|
309
|
-
console.log(chalk.dim('Please specify the file with --file <filename>'));
|
|
310
|
-
return [];
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Create singleton that uses configured models directory
|
|
315
|
-
// Use lazy import to avoid circular dependency
|
|
316
|
-
let _modelDownloader: ModelDownloader | null = null;
|
|
317
|
-
|
|
318
|
-
export function getModelDownloader(): ModelDownloader {
|
|
319
|
-
if (!_modelDownloader) {
|
|
320
|
-
// Import stateManager dynamically to avoid circular dependency
|
|
321
|
-
const { stateManager } = require('./state-manager');
|
|
322
|
-
_modelDownloader = new ModelDownloader(undefined, () => stateManager.getModelsDirectory());
|
|
323
|
-
}
|
|
324
|
-
return _modelDownloader;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Export singleton instance for backward compatibility
|
|
328
|
-
export const modelDownloader = getModelDownloader();
|
package/src/lib/model-scanner.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { ModelInfo } from '../types/model-info';
|
|
4
|
-
import { getModelsDir } from '../utils/file-utils';
|
|
5
|
-
import { formatBytes } from '../utils/format-utils';
|
|
6
|
-
|
|
7
|
-
export class ModelScanner {
|
|
8
|
-
private modelsDir?: string;
|
|
9
|
-
private getModelsDirFn?: () => Promise<string>;
|
|
10
|
-
|
|
11
|
-
constructor(modelsDir?: string, getModelsDirFn?: () => Promise<string>) {
|
|
12
|
-
this.modelsDir = modelsDir;
|
|
13
|
-
this.getModelsDirFn = getModelsDirFn;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Get the models directory (either configured or default)
|
|
18
|
-
*/
|
|
19
|
-
private async getModelsDirectory(): Promise<string> {
|
|
20
|
-
if (this.modelsDir) {
|
|
21
|
-
return this.modelsDir;
|
|
22
|
-
}
|
|
23
|
-
if (this.getModelsDirFn) {
|
|
24
|
-
return await this.getModelsDirFn();
|
|
25
|
-
}
|
|
26
|
-
return getModelsDir();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Scan models directory for GGUF files
|
|
31
|
-
*/
|
|
32
|
-
async scanModels(): Promise<ModelInfo[]> {
|
|
33
|
-
const modelsDir = await this.getModelsDirectory();
|
|
34
|
-
try {
|
|
35
|
-
const files = await fs.readdir(modelsDir);
|
|
36
|
-
const ggufFiles = files.filter((f) => f.toLowerCase().endsWith('.gguf'));
|
|
37
|
-
|
|
38
|
-
const models: ModelInfo[] = [];
|
|
39
|
-
for (const file of ggufFiles) {
|
|
40
|
-
const modelInfo = await this.getModelInfo(file);
|
|
41
|
-
if (modelInfo) {
|
|
42
|
-
models.push(modelInfo);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Sort by modified date (newest first)
|
|
47
|
-
models.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
48
|
-
|
|
49
|
-
return models;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
// Models directory doesn't exist or is not accessible
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get information about a specific model file
|
|
58
|
-
*/
|
|
59
|
-
async getModelInfo(filename: string): Promise<ModelInfo | null> {
|
|
60
|
-
const modelsDir = await this.getModelsDirectory();
|
|
61
|
-
const modelPath = path.join(modelsDir, filename);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const stats = await fs.stat(modelPath);
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
filename,
|
|
68
|
-
path: modelPath,
|
|
69
|
-
size: stats.size,
|
|
70
|
-
sizeFormatted: formatBytes(stats.size),
|
|
71
|
-
modified: stats.mtime,
|
|
72
|
-
exists: true,
|
|
73
|
-
};
|
|
74
|
-
} catch (error) {
|
|
75
|
-
// File doesn't exist or is not accessible
|
|
76
|
-
return {
|
|
77
|
-
filename,
|
|
78
|
-
path: modelPath,
|
|
79
|
-
size: 0,
|
|
80
|
-
sizeFormatted: '0 B',
|
|
81
|
-
modified: new Date(),
|
|
82
|
-
exists: false,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate that a model file exists and is readable
|
|
89
|
-
*/
|
|
90
|
-
async validateModel(filename: string): Promise<boolean> {
|
|
91
|
-
const modelInfo = await this.getModelInfo(filename);
|
|
92
|
-
return modelInfo !== null && modelInfo.exists && modelInfo.size > 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Resolve a model filename to full path
|
|
97
|
-
*/
|
|
98
|
-
async resolveModelPath(filename: string): Promise<string | null> {
|
|
99
|
-
// If already absolute path, return it
|
|
100
|
-
if (path.isAbsolute(filename)) {
|
|
101
|
-
return filename;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const modelsDir = await this.getModelsDirectory();
|
|
105
|
-
|
|
106
|
-
// Try in models directory
|
|
107
|
-
const modelPath = path.join(modelsDir, filename);
|
|
108
|
-
const modelInfo = await this.getModelInfo(filename);
|
|
109
|
-
|
|
110
|
-
if (modelInfo && modelInfo.exists) {
|
|
111
|
-
return modelPath;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Try adding .gguf extension
|
|
115
|
-
if (!filename.toLowerCase().endsWith('.gguf')) {
|
|
116
|
-
const withExtension = `${filename}.gguf`;
|
|
117
|
-
const modelInfoWithExt = await this.getModelInfo(withExtension);
|
|
118
|
-
if (modelInfoWithExt && modelInfoWithExt.exists) {
|
|
119
|
-
return path.join(modelsDir, withExtension);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get the size of a model file
|
|
128
|
-
*/
|
|
129
|
-
async getModelSize(filename: string): Promise<number | null> {
|
|
130
|
-
const modelInfo = await this.getModelInfo(filename);
|
|
131
|
-
return modelInfo?.size || null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get total size of all models
|
|
136
|
-
*/
|
|
137
|
-
async getTotalSize(): Promise<number> {
|
|
138
|
-
const models = await this.scanModels();
|
|
139
|
-
return models.reduce((total, model) => total + model.size, 0);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Create singleton that uses configured models directory
|
|
144
|
-
// Use lazy import to avoid circular dependency
|
|
145
|
-
let _modelScanner: ModelScanner | null = null;
|
|
146
|
-
|
|
147
|
-
export function getModelScanner(): ModelScanner {
|
|
148
|
-
if (!_modelScanner) {
|
|
149
|
-
// Import stateManager dynamically to avoid circular dependency
|
|
150
|
-
const { stateManager } = require('./state-manager');
|
|
151
|
-
_modelScanner = new ModelScanner(undefined, () => stateManager.getModelsDirectory());
|
|
152
|
-
}
|
|
153
|
-
return _modelScanner;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Export singleton instance for backward compatibility
|
|
157
|
-
export const modelScanner = getModelScanner();
|
package/src/lib/model-search.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import * as https from 'https';
|
|
2
|
-
|
|
3
|
-
export interface HFModelResult {
|
|
4
|
-
modelId: string;
|
|
5
|
-
author: string;
|
|
6
|
-
modelName: string;
|
|
7
|
-
downloads: number;
|
|
8
|
-
likes: number;
|
|
9
|
-
tags: string[];
|
|
10
|
-
lastModified: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ModelSearch {
|
|
14
|
-
/**
|
|
15
|
-
* Search Hugging Face for GGUF models
|
|
16
|
-
*/
|
|
17
|
-
async searchModels(query: string, limit = 20): Promise<HFModelResult[]> {
|
|
18
|
-
const searchUrl = this.buildSearchUrl(query, limit);
|
|
19
|
-
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
https.get(searchUrl, (response) => {
|
|
22
|
-
let data = '';
|
|
23
|
-
|
|
24
|
-
response.on('data', (chunk) => {
|
|
25
|
-
data += chunk;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
response.on('end', () => {
|
|
29
|
-
try {
|
|
30
|
-
const results = JSON.parse(data);
|
|
31
|
-
const models = this.parseResults(results);
|
|
32
|
-
resolve(models);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
reject(new Error(`Failed to parse search results: ${(error as Error).message}`));
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}).on('error', (error) => {
|
|
38
|
-
reject(new Error(`Search request failed: ${error.message}`));
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build Hugging Face search URL
|
|
45
|
-
*/
|
|
46
|
-
private buildSearchUrl(query: string, limit: number): string {
|
|
47
|
-
const params = new URLSearchParams({
|
|
48
|
-
search: query,
|
|
49
|
-
filter: 'gguf',
|
|
50
|
-
sort: 'downloads',
|
|
51
|
-
direction: '-1',
|
|
52
|
-
limit: limit.toString(),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return `https://huggingface.co/api/models?${params.toString()}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parse API results into our model format
|
|
60
|
-
*/
|
|
61
|
-
private parseResults(results: any[]): HFModelResult[] {
|
|
62
|
-
return results.map((result) => {
|
|
63
|
-
const modelId = result.id || result.modelId || '';
|
|
64
|
-
const parts = modelId.split('/');
|
|
65
|
-
const author = parts[0] || '';
|
|
66
|
-
const modelName = parts.slice(1).join('/') || '';
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
modelId,
|
|
70
|
-
author,
|
|
71
|
-
modelName,
|
|
72
|
-
downloads: result.downloads || 0,
|
|
73
|
-
likes: result.likes || 0,
|
|
74
|
-
tags: result.tags || [],
|
|
75
|
-
lastModified: result.lastModified || '',
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get GGUF files for a specific model
|
|
82
|
-
*/
|
|
83
|
-
async getModelFiles(modelId: string): Promise<string[]> {
|
|
84
|
-
const apiUrl = `https://huggingface.co/api/models/${modelId}`;
|
|
85
|
-
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
https.get(apiUrl, (response) => {
|
|
88
|
-
let data = '';
|
|
89
|
-
|
|
90
|
-
response.on('data', (chunk) => {
|
|
91
|
-
data += chunk;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
response.on('end', () => {
|
|
95
|
-
try {
|
|
96
|
-
const modelInfo = JSON.parse(data);
|
|
97
|
-
const files = modelInfo.siblings || [];
|
|
98
|
-
const ggufFiles = files
|
|
99
|
-
.filter((file: any) => file.rfilename?.toLowerCase().endsWith('.gguf'))
|
|
100
|
-
.map((file: any) => file.rfilename);
|
|
101
|
-
resolve(ggufFiles);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
reject(new Error(`Failed to fetch model files: ${(error as Error).message}`));
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}).on('error', (error) => {
|
|
107
|
-
reject(new Error(`API request failed: ${error.message}`));
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Export singleton instance
|
|
114
|
-
export const modelSearch = new ModelSearch();
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { stateManager } from './state-manager';
|
|
4
|
-
import { expandHome } from '../utils/file-utils';
|
|
5
|
-
import { prompt } from '../utils/prompt-utils';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Ensure models directory exists, prompting user if needed
|
|
9
|
-
* Returns the final models directory path
|
|
10
|
-
*/
|
|
11
|
-
export async function ensureModelsDirectory(): Promise<string> {
|
|
12
|
-
const configuredPath = await stateManager.getModelsDirectory();
|
|
13
|
-
|
|
14
|
-
// If directory exists, we're good
|
|
15
|
-
if (fs.existsSync(configuredPath)) {
|
|
16
|
-
return configuredPath;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Directory doesn't exist - prompt user
|
|
20
|
-
console.log(chalk.yellow('⚠️ Models directory not found'));
|
|
21
|
-
console.log();
|
|
22
|
-
console.log(chalk.dim('The models directory is where GGUF model files are stored.'));
|
|
23
|
-
console.log(chalk.dim(`Configured path: ${configuredPath}`));
|
|
24
|
-
console.log();
|
|
25
|
-
|
|
26
|
-
const answer = await prompt(
|
|
27
|
-
'Enter models directory path (press Enter to use default)',
|
|
28
|
-
configuredPath
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const finalPath = expandHome(answer);
|
|
32
|
-
|
|
33
|
-
// If user changed the path, update config
|
|
34
|
-
if (finalPath !== configuredPath) {
|
|
35
|
-
console.log(chalk.dim(`Updating configuration to: ${finalPath}`));
|
|
36
|
-
await stateManager.setModelsDirectory(finalPath);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Create the directory
|
|
40
|
-
console.log(chalk.dim(`Creating directory: ${finalPath}`));
|
|
41
|
-
fs.mkdirSync(finalPath, { recursive: true, mode: 0o755 });
|
|
42
|
-
console.log(chalk.green('✅ Models directory created'));
|
|
43
|
-
console.log();
|
|
44
|
-
|
|
45
|
-
return finalPath;
|
|
46
|
-
}
|