@appkit/llamacpp-cli 1.0.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/.versionrc.json +16 -0
- package/CHANGELOG.md +10 -0
- package/README.md +474 -0
- package/bin/llamacpp +26 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/delete.d.ts +2 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +104 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +37 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +8 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +57 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/ps.d.ts +2 -0
- package/dist/commands/ps.d.ts.map +1 -0
- package/dist/commands/ps.js +72 -0
- package/dist/commands/ps.js.map +1 -0
- package/dist/commands/pull.d.ts +6 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +36 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/rm.d.ts +2 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +134 -0
- package/dist/commands/rm.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +198 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +93 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/show.d.ts +6 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +196 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/start.d.ts +9 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +150 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +39 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/lib/config-generator.d.ts +30 -0
- package/dist/lib/config-generator.d.ts.map +1 -0
- package/dist/lib/config-generator.js +125 -0
- package/dist/lib/config-generator.js.map +1 -0
- package/dist/lib/launchctl-manager.d.ts +55 -0
- package/dist/lib/launchctl-manager.d.ts.map +1 -0
- package/dist/lib/launchctl-manager.js +227 -0
- package/dist/lib/launchctl-manager.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +44 -0
- package/dist/lib/model-downloader.d.ts.map +1 -0
- package/dist/lib/model-downloader.js +248 -0
- package/dist/lib/model-downloader.js.map +1 -0
- package/dist/lib/model-scanner.d.ts +31 -0
- package/dist/lib/model-scanner.d.ts.map +1 -0
- package/dist/lib/model-scanner.js +145 -0
- package/dist/lib/model-scanner.js.map +1 -0
- package/dist/lib/model-search.d.ts +29 -0
- package/dist/lib/model-search.d.ts.map +1 -0
- package/dist/lib/model-search.js +131 -0
- package/dist/lib/model-search.js.map +1 -0
- package/dist/lib/port-manager.d.ts +26 -0
- package/dist/lib/port-manager.d.ts.map +1 -0
- package/dist/lib/port-manager.js +75 -0
- package/dist/lib/port-manager.js.map +1 -0
- package/dist/lib/state-manager.d.ts +59 -0
- package/dist/lib/state-manager.d.ts.map +1 -0
- package/dist/lib/state-manager.js +178 -0
- package/dist/lib/state-manager.js.map +1 -0
- package/dist/lib/status-checker.d.ts +28 -0
- package/dist/lib/status-checker.d.ts.map +1 -0
- package/dist/lib/status-checker.js +99 -0
- package/dist/lib/status-checker.js.map +1 -0
- package/dist/types/global-config.d.ts +16 -0
- package/dist/types/global-config.d.ts.map +1 -0
- package/dist/types/global-config.js +18 -0
- package/dist/types/global-config.js.map +1 -0
- package/dist/types/model-info.d.ts +9 -0
- package/dist/types/model-info.d.ts.map +1 -0
- package/dist/types/model-info.js +3 -0
- package/dist/types/model-info.js.map +1 -0
- package/dist/types/server-config.d.ts +27 -0
- package/dist/types/server-config.d.ts.map +1 -0
- package/dist/types/server-config.js +15 -0
- package/dist/types/server-config.js.map +1 -0
- package/dist/utils/file-utils.d.ts +49 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +144 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/format-utils.d.ts +29 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +82 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/process-utils.d.ts +27 -0
- package/dist/utils/process-utils.d.ts.map +1 -0
- package/dist/utils/process-utils.js +66 -0
- package/dist/utils/process-utils.js.map +1 -0
- package/package.json +56 -0
- package/src/cli.ts +195 -0
- package/src/commands/delete.ts +74 -0
- package/src/commands/list.ts +37 -0
- package/src/commands/logs.ts +61 -0
- package/src/commands/ps.ts +79 -0
- package/src/commands/pull.ts +40 -0
- package/src/commands/rm.ts +114 -0
- package/src/commands/run.ts +209 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/show.ts +207 -0
- package/src/commands/start.ts +140 -0
- package/src/commands/stop.ts +39 -0
- package/src/lib/config-generator.ts +119 -0
- package/src/lib/launchctl-manager.ts +209 -0
- package/src/lib/model-downloader.ts +259 -0
- package/src/lib/model-scanner.ts +125 -0
- package/src/lib/model-search.ts +114 -0
- package/src/lib/port-manager.ts +80 -0
- package/src/lib/state-manager.ts +177 -0
- package/src/lib/status-checker.ts +113 -0
- package/src/types/global-config.ts +26 -0
- package/src/types/model-info.ts +8 -0
- package/src/types/server-config.ts +42 -0
- package/src/utils/file-utils.ts +106 -0
- package/src/utils/format-utils.ts +80 -0
- package/src/utils/process-utils.ts +60 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,259 @@
|
|
|
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 class ModelDownloader {
|
|
17
|
+
private modelsDir: string;
|
|
18
|
+
|
|
19
|
+
constructor(modelsDir?: string) {
|
|
20
|
+
this.modelsDir = modelsDir || getModelsDir();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse Hugging Face identifier
|
|
25
|
+
* Examples:
|
|
26
|
+
* "bartowski/Llama-3.2-3B-Instruct-GGUF" → { repo: "...", file: undefined }
|
|
27
|
+
* "bartowski/Llama-3.2-3B-Instruct-GGUF/file.gguf" → { repo: "...", file: "file.gguf" }
|
|
28
|
+
*/
|
|
29
|
+
parseHFIdentifier(identifier: string): { repo: string; file?: string } {
|
|
30
|
+
const parts = identifier.split('/');
|
|
31
|
+
if (parts.length === 2) {
|
|
32
|
+
return { repo: identifier };
|
|
33
|
+
} else if (parts.length === 3) {
|
|
34
|
+
return {
|
|
35
|
+
repo: `${parts[0]}/${parts[1]}`,
|
|
36
|
+
file: parts[2],
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Invalid Hugging Face identifier: ${identifier}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build Hugging Face download URL
|
|
45
|
+
*/
|
|
46
|
+
buildDownloadUrl(repoId: string, filename: string, branch = 'main'): string {
|
|
47
|
+
return `https://huggingface.co/${repoId}/resolve/${branch}/${filename}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Download a file via HTTPS with progress tracking
|
|
52
|
+
*/
|
|
53
|
+
private downloadFile(
|
|
54
|
+
url: string,
|
|
55
|
+
destPath: string,
|
|
56
|
+
onProgress?: (downloaded: number, total: number) => void
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const file = fs.createWriteStream(destPath);
|
|
60
|
+
let downloadedBytes = 0;
|
|
61
|
+
let totalBytes = 0;
|
|
62
|
+
let lastUpdateTime = Date.now();
|
|
63
|
+
let lastDownloadedBytes = 0;
|
|
64
|
+
let completed = false;
|
|
65
|
+
|
|
66
|
+
const cleanup = (sigintHandler?: () => void) => {
|
|
67
|
+
if (sigintHandler) {
|
|
68
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleError = (err: Error, sigintHandler?: () => void) => {
|
|
73
|
+
if (completed) return;
|
|
74
|
+
completed = true;
|
|
75
|
+
cleanup(sigintHandler);
|
|
76
|
+
file.close(() => {
|
|
77
|
+
fs.unlink(destPath, () => {});
|
|
78
|
+
});
|
|
79
|
+
reject(err);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const sigintHandler = () => {
|
|
83
|
+
request.destroy();
|
|
84
|
+
handleError(new Error('Download interrupted by user'), sigintHandler);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const request = https.get(url, { agent: new https.Agent({ keepAlive: false }) }, (response) => {
|
|
88
|
+
// Handle redirects (301, 302, 307, 308)
|
|
89
|
+
if (response.statusCode === 301 || response.statusCode === 302 ||
|
|
90
|
+
response.statusCode === 307 || response.statusCode === 308) {
|
|
91
|
+
const redirectUrl = response.headers.location;
|
|
92
|
+
if (redirectUrl) {
|
|
93
|
+
cleanup(sigintHandler);
|
|
94
|
+
// Wait for file to close before starting new download
|
|
95
|
+
file.close(() => {
|
|
96
|
+
fs.unlink(destPath, () => {
|
|
97
|
+
// Start recursive download only after cleanup is complete
|
|
98
|
+
this.downloadFile(redirectUrl, destPath, onProgress)
|
|
99
|
+
.then(resolve)
|
|
100
|
+
.catch(reject);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (response.statusCode !== 200) {
|
|
108
|
+
return handleError(
|
|
109
|
+
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`),
|
|
110
|
+
sigintHandler
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
|
115
|
+
|
|
116
|
+
response.on('data', (chunk: Buffer) => {
|
|
117
|
+
downloadedBytes += chunk.length;
|
|
118
|
+
|
|
119
|
+
// Update progress every 500ms
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
if (onProgress && now - lastUpdateTime >= 500) {
|
|
122
|
+
onProgress(downloadedBytes, totalBytes);
|
|
123
|
+
lastUpdateTime = now;
|
|
124
|
+
lastDownloadedBytes = downloadedBytes;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
response.pipe(file);
|
|
129
|
+
|
|
130
|
+
file.on('finish', () => {
|
|
131
|
+
if (completed) return;
|
|
132
|
+
completed = true;
|
|
133
|
+
|
|
134
|
+
// Final progress update
|
|
135
|
+
if (onProgress) {
|
|
136
|
+
onProgress(downloadedBytes, totalBytes);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Use callback to ensure close completes before resolving
|
|
140
|
+
file.close((err) => {
|
|
141
|
+
cleanup(sigintHandler);
|
|
142
|
+
if (err) reject(err);
|
|
143
|
+
else resolve();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
request.on('error', (err) => {
|
|
149
|
+
handleError(err, sigintHandler);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
file.on('error', (err) => {
|
|
153
|
+
handleError(err, sigintHandler);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Handle Ctrl+C gracefully
|
|
157
|
+
process.on('SIGINT', sigintHandler);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Display progress bar
|
|
163
|
+
*/
|
|
164
|
+
private displayProgress(downloaded: number, total: number, filename: string): void {
|
|
165
|
+
const percentage = total > 0 ? (downloaded / total) * 100 : 0;
|
|
166
|
+
const barLength = 40;
|
|
167
|
+
const filledLength = Math.round((barLength * downloaded) / total);
|
|
168
|
+
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
|
169
|
+
|
|
170
|
+
const downloadedFormatted = formatBytes(downloaded);
|
|
171
|
+
const totalFormatted = formatBytes(total);
|
|
172
|
+
const percentFormatted = percentage.toFixed(1);
|
|
173
|
+
|
|
174
|
+
// Clear line and print progress
|
|
175
|
+
process.stdout.write('\r\x1b[K');
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
chalk.blue(`[${bar}] ${percentFormatted}% | ${downloadedFormatted} / ${totalFormatted}`)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Download a model from Hugging Face
|
|
183
|
+
*/
|
|
184
|
+
async downloadModel(
|
|
185
|
+
repoId: string,
|
|
186
|
+
filename: string,
|
|
187
|
+
onProgress?: (progress: DownloadProgress) => void
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
console.log(chalk.blue(`📥 Downloading ${filename} from Hugging Face...`));
|
|
190
|
+
console.log(chalk.dim(`Repository: ${repoId}`));
|
|
191
|
+
console.log(chalk.dim(`Destination: ${this.modelsDir}`));
|
|
192
|
+
console.log();
|
|
193
|
+
|
|
194
|
+
// Build download URL
|
|
195
|
+
const url = this.buildDownloadUrl(repoId, filename);
|
|
196
|
+
const destPath = path.join(this.modelsDir, filename);
|
|
197
|
+
|
|
198
|
+
// Check if file already exists
|
|
199
|
+
if (fs.existsSync(destPath)) {
|
|
200
|
+
console.log(chalk.yellow(`⚠️ File already exists: ${filename}`));
|
|
201
|
+
console.log(chalk.dim(' Remove it first or choose a different filename'));
|
|
202
|
+
throw new Error('File already exists');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Download with progress
|
|
206
|
+
const startTime = Date.now();
|
|
207
|
+
let lastDownloaded = 0;
|
|
208
|
+
let lastTime = startTime;
|
|
209
|
+
|
|
210
|
+
await this.downloadFile(url, destPath, (downloaded, total) => {
|
|
211
|
+
// Calculate speed
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
const timeDiff = (now - lastTime) / 1000; // seconds
|
|
214
|
+
const bytesDiff = downloaded - lastDownloaded;
|
|
215
|
+
const speed = timeDiff > 0 ? bytesDiff / timeDiff : 0;
|
|
216
|
+
|
|
217
|
+
// Update for next calculation
|
|
218
|
+
lastTime = now;
|
|
219
|
+
lastDownloaded = downloaded;
|
|
220
|
+
|
|
221
|
+
// Display progress bar
|
|
222
|
+
this.displayProgress(downloaded, total, filename);
|
|
223
|
+
|
|
224
|
+
// Call user progress callback if provided
|
|
225
|
+
if (onProgress) {
|
|
226
|
+
onProgress({
|
|
227
|
+
filename,
|
|
228
|
+
downloaded,
|
|
229
|
+
total,
|
|
230
|
+
percentage: total > 0 ? (downloaded / total) * 100 : 0,
|
|
231
|
+
speed: `${formatBytes(speed)}/s`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Clear progress line and show completion
|
|
237
|
+
process.stdout.write('\r\x1b[K');
|
|
238
|
+
console.log(chalk.green('✅ Download complete!'));
|
|
239
|
+
|
|
240
|
+
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
241
|
+
console.log(chalk.dim(` Time: ${totalTime}s`));
|
|
242
|
+
console.log(chalk.dim(` Location: ${destPath}`));
|
|
243
|
+
|
|
244
|
+
return destPath;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* List GGUF files in a Hugging Face repository
|
|
249
|
+
* (This would require calling the HF API - simplified for now)
|
|
250
|
+
*/
|
|
251
|
+
async listGGUFFiles(repoId: string): Promise<string[]> {
|
|
252
|
+
console.log(chalk.yellow('Listing files is not yet implemented.'));
|
|
253
|
+
console.log(chalk.dim('Please specify the file with --file <filename>'));
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Export singleton instance
|
|
259
|
+
export const modelDownloader = new ModelDownloader();
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
|
|
10
|
+
constructor(modelsDir?: string) {
|
|
11
|
+
this.modelsDir = modelsDir || getModelsDir();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scan models directory for GGUF files
|
|
16
|
+
*/
|
|
17
|
+
async scanModels(): Promise<ModelInfo[]> {
|
|
18
|
+
try {
|
|
19
|
+
const files = await fs.readdir(this.modelsDir);
|
|
20
|
+
const ggufFiles = files.filter((f) => f.toLowerCase().endsWith('.gguf'));
|
|
21
|
+
|
|
22
|
+
const models: ModelInfo[] = [];
|
|
23
|
+
for (const file of ggufFiles) {
|
|
24
|
+
const modelInfo = await this.getModelInfo(file);
|
|
25
|
+
if (modelInfo) {
|
|
26
|
+
models.push(modelInfo);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Sort by modified date (newest first)
|
|
31
|
+
models.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
32
|
+
|
|
33
|
+
return models;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Models directory doesn't exist or is not accessible
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get information about a specific model file
|
|
42
|
+
*/
|
|
43
|
+
async getModelInfo(filename: string): Promise<ModelInfo | null> {
|
|
44
|
+
const modelPath = path.join(this.modelsDir, filename);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const stats = await fs.stat(modelPath);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
filename,
|
|
51
|
+
path: modelPath,
|
|
52
|
+
size: stats.size,
|
|
53
|
+
sizeFormatted: formatBytes(stats.size),
|
|
54
|
+
modified: stats.mtime,
|
|
55
|
+
exists: true,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// File doesn't exist or is not accessible
|
|
59
|
+
return {
|
|
60
|
+
filename,
|
|
61
|
+
path: modelPath,
|
|
62
|
+
size: 0,
|
|
63
|
+
sizeFormatted: '0 B',
|
|
64
|
+
modified: new Date(),
|
|
65
|
+
exists: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate that a model file exists and is readable
|
|
72
|
+
*/
|
|
73
|
+
async validateModel(filename: string): Promise<boolean> {
|
|
74
|
+
const modelInfo = await this.getModelInfo(filename);
|
|
75
|
+
return modelInfo !== null && modelInfo.exists && modelInfo.size > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a model filename to full path
|
|
80
|
+
*/
|
|
81
|
+
async resolveModelPath(filename: string): Promise<string | null> {
|
|
82
|
+
// If already absolute path, return it
|
|
83
|
+
if (path.isAbsolute(filename)) {
|
|
84
|
+
return filename;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Try in models directory
|
|
88
|
+
const modelPath = path.join(this.modelsDir, filename);
|
|
89
|
+
const modelInfo = await this.getModelInfo(filename);
|
|
90
|
+
|
|
91
|
+
if (modelInfo && modelInfo.exists) {
|
|
92
|
+
return modelPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Try adding .gguf extension
|
|
96
|
+
if (!filename.toLowerCase().endsWith('.gguf')) {
|
|
97
|
+
const withExtension = `${filename}.gguf`;
|
|
98
|
+
const modelInfoWithExt = await this.getModelInfo(withExtension);
|
|
99
|
+
if (modelInfoWithExt && modelInfoWithExt.exists) {
|
|
100
|
+
return path.join(this.modelsDir, withExtension);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the size of a model file
|
|
109
|
+
*/
|
|
110
|
+
async getModelSize(filename: string): Promise<number | null> {
|
|
111
|
+
const modelInfo = await this.getModelInfo(filename);
|
|
112
|
+
return modelInfo?.size || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get total size of all models
|
|
117
|
+
*/
|
|
118
|
+
async getTotalSize(): Promise<number> {
|
|
119
|
+
const models = await this.scanModels();
|
|
120
|
+
return models.reduce((total, model) => total + model.size, 0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Export singleton instance
|
|
125
|
+
export const modelScanner = new ModelScanner();
|
|
@@ -0,0 +1,114 @@
|
|
|
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();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isPortInUse } from '../utils/process-utils';
|
|
2
|
+
import { stateManager } from './state-manager';
|
|
3
|
+
|
|
4
|
+
export class PortManager {
|
|
5
|
+
private readonly portRangeStart = 9000;
|
|
6
|
+
private readonly portRangeEnd = 9999;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find an available port in the range
|
|
10
|
+
*/
|
|
11
|
+
async findAvailablePort(startPort?: number): Promise<number> {
|
|
12
|
+
const start = startPort || this.portRangeStart;
|
|
13
|
+
|
|
14
|
+
// Get ports used by existing servers
|
|
15
|
+
const usedPorts = await stateManager.getUsedPorts();
|
|
16
|
+
|
|
17
|
+
// Find first available port
|
|
18
|
+
for (let port = start; port <= this.portRangeEnd; port++) {
|
|
19
|
+
if (!usedPorts.has(port)) {
|
|
20
|
+
// Check if port is actually available (not used by other processes)
|
|
21
|
+
const inUse = await isPortInUse(port);
|
|
22
|
+
if (!inUse) {
|
|
23
|
+
return port;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`No available ports in range ${start}-${this.portRangeEnd}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a port is available
|
|
33
|
+
*/
|
|
34
|
+
async isPortAvailable(port: number): Promise<boolean> {
|
|
35
|
+
// Check if port is in valid range
|
|
36
|
+
if (port < 1024 || port > 65535) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if port is used by any server
|
|
41
|
+
const usedPorts = await stateManager.getUsedPorts();
|
|
42
|
+
if (usedPorts.has(port)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if port is actually in use
|
|
47
|
+
return !(await isPortInUse(port));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate a port number
|
|
52
|
+
*/
|
|
53
|
+
validatePort(port: number): void {
|
|
54
|
+
if (port < 1024) {
|
|
55
|
+
throw new Error('Port must be >= 1024 (ports below 1024 require root)');
|
|
56
|
+
}
|
|
57
|
+
if (port > 65535) {
|
|
58
|
+
throw new Error('Port must be <= 65535');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find a server using a given port
|
|
64
|
+
*/
|
|
65
|
+
async findServerByPort(port: number) {
|
|
66
|
+
return await stateManager.findServerByPort(port);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check for port conflicts
|
|
71
|
+
*/
|
|
72
|
+
async checkPortConflict(port: number, exceptId?: string): Promise<boolean> {
|
|
73
|
+
const servers = await stateManager.getAllServers();
|
|
74
|
+
const conflict = servers.find((s) => s.port === port && s.id !== exceptId);
|
|
75
|
+
return conflict !== undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Export singleton instance
|
|
80
|
+
export const portManager = new PortManager();
|