@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,114 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { modelScanner } from '../lib/model-scanner';
|
|
5
|
+
import { stateManager } from '../lib/state-manager';
|
|
6
|
+
import { launchctlManager } from '../lib/launchctl-manager';
|
|
7
|
+
|
|
8
|
+
export async function rmCommand(modelIdentifier: string): Promise<void> {
|
|
9
|
+
await stateManager.initialize();
|
|
10
|
+
|
|
11
|
+
// 1. Resolve model path
|
|
12
|
+
const modelPath = await modelScanner.resolveModelPath(modelIdentifier);
|
|
13
|
+
if (!modelPath) {
|
|
14
|
+
throw new Error(`Model not found: ${modelIdentifier}\n\nRun: llamacpp ls`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Check if any servers are using this model
|
|
18
|
+
const allServers = await stateManager.getAllServers();
|
|
19
|
+
const serversUsingModel = allServers.filter((s) => s.modelPath === modelPath);
|
|
20
|
+
|
|
21
|
+
// 3. Confirm deletion
|
|
22
|
+
console.log(chalk.yellow(`⚠️ Delete model file: ${modelPath}`));
|
|
23
|
+
|
|
24
|
+
if (serversUsingModel.length > 0) {
|
|
25
|
+
console.log(chalk.yellow(`\n This model has ${serversUsingModel.length} server(s) configured:`));
|
|
26
|
+
for (const server of serversUsingModel) {
|
|
27
|
+
const statusColor = server.status === 'running' ? chalk.green : chalk.dim;
|
|
28
|
+
console.log(chalk.yellow(` - ${server.id} (${statusColor(server.status)})`));
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.yellow(`\n These servers will be removed before deleting the model.`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
const confirmed = await confirmDeletion();
|
|
36
|
+
if (!confirmed) {
|
|
37
|
+
console.log(chalk.dim('Cancelled'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
// 4. Delete all servers using this model
|
|
44
|
+
if (serversUsingModel.length > 0) {
|
|
45
|
+
console.log(chalk.blue(`🗑️ Removing ${serversUsingModel.length} server(s)...\n`));
|
|
46
|
+
|
|
47
|
+
for (const server of serversUsingModel) {
|
|
48
|
+
console.log(chalk.dim(` Removing server: ${server.id}`));
|
|
49
|
+
|
|
50
|
+
// Stop server if running
|
|
51
|
+
if (server.status === 'running') {
|
|
52
|
+
try {
|
|
53
|
+
await launchctlManager.stopService(server.label);
|
|
54
|
+
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(chalk.yellow(` ⚠️ Failed to stop server gracefully`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Unload service
|
|
61
|
+
try {
|
|
62
|
+
await launchctlManager.unloadService(server.plistPath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Ignore errors if service is already unloaded
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Delete plist
|
|
68
|
+
await launchctlManager.deletePlist(server.plistPath);
|
|
69
|
+
|
|
70
|
+
// Delete server config
|
|
71
|
+
await stateManager.deleteServerConfig(server.id);
|
|
72
|
+
|
|
73
|
+
console.log(chalk.dim(` ✓ Server removed`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5. Delete model file
|
|
80
|
+
console.log(chalk.blue(`🗑️ Deleting model file...`));
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await fs.unlink(modelPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Failed to delete model file: ${(error as Error).message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Success
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(chalk.green('✅ Model deleted successfully'));
|
|
91
|
+
|
|
92
|
+
if (serversUsingModel.length > 0) {
|
|
93
|
+
console.log(chalk.dim(` Removed ${serversUsingModel.length} server(s)`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.dim(` Deleted: ${modelPath}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Prompt user for confirmation
|
|
101
|
+
*/
|
|
102
|
+
function confirmDeletion(): Promise<boolean> {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const rl = readline.createInterface({
|
|
105
|
+
input: process.stdin,
|
|
106
|
+
output: process.stdout,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
rl.question(chalk.yellow(" Type 'yes' to confirm: "), (answer) => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolve(answer.toLowerCase() === 'yes');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import { stateManager } from '../lib/state-manager';
|
|
4
|
+
import { startCommand } from './start';
|
|
5
|
+
import { statusChecker } from '../lib/status-checker';
|
|
6
|
+
import { ServerConfig } from '../types/server-config';
|
|
7
|
+
|
|
8
|
+
interface ChatMessage {
|
|
9
|
+
role: 'system' | 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ChatCompletionChunk {
|
|
14
|
+
id: string;
|
|
15
|
+
object: string;
|
|
16
|
+
created: number;
|
|
17
|
+
model: string;
|
|
18
|
+
choices: Array<{
|
|
19
|
+
index: number;
|
|
20
|
+
delta: {
|
|
21
|
+
role?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
};
|
|
24
|
+
finish_reason: string | null;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runCommand(modelIdentifier: string): Promise<void> {
|
|
29
|
+
await stateManager.initialize();
|
|
30
|
+
|
|
31
|
+
// 1. Find or start server
|
|
32
|
+
let server = await stateManager.findServer(modelIdentifier);
|
|
33
|
+
|
|
34
|
+
if (!server) {
|
|
35
|
+
// Try to resolve as a model name and start it
|
|
36
|
+
console.log(chalk.blue(`🚀 No running server found. Starting ${modelIdentifier}...\n`));
|
|
37
|
+
try {
|
|
38
|
+
await startCommand(modelIdentifier, {});
|
|
39
|
+
server = await stateManager.findServer(modelIdentifier);
|
|
40
|
+
if (!server) {
|
|
41
|
+
throw new Error('Failed to start server');
|
|
42
|
+
}
|
|
43
|
+
console.log(); // Add blank line after start output
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`Failed to start server: ${(error as Error).message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Verify server is running
|
|
50
|
+
const status = await statusChecker.checkServer(server);
|
|
51
|
+
if (!status.isRunning) {
|
|
52
|
+
throw new Error(`Server exists but is not running. Start it with: llamacpp start ${server.id}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Start REPL
|
|
56
|
+
console.log(chalk.green(`💬 Connected to ${server.modelName} (port ${server.port})`));
|
|
57
|
+
console.log(chalk.dim(`Type your message and press Enter. Use /exit to quit, /clear to reset history, /help for commands.\n`));
|
|
58
|
+
|
|
59
|
+
const conversationHistory: ChatMessage[] = [];
|
|
60
|
+
const rl = readline.createInterface({
|
|
61
|
+
input: process.stdin,
|
|
62
|
+
output: process.stdout,
|
|
63
|
+
prompt: chalk.cyan('You: '),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Handle graceful shutdown
|
|
67
|
+
const cleanup = () => {
|
|
68
|
+
rl.close();
|
|
69
|
+
console.log(chalk.dim('\n\nGoodbye!'));
|
|
70
|
+
process.exit(0);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
process.on('SIGINT', cleanup);
|
|
74
|
+
process.on('SIGTERM', cleanup);
|
|
75
|
+
|
|
76
|
+
rl.prompt();
|
|
77
|
+
|
|
78
|
+
rl.on('line', async (input: string) => {
|
|
79
|
+
const line = input.trim();
|
|
80
|
+
|
|
81
|
+
// Handle special commands
|
|
82
|
+
if (line === '/exit' || line === '/quit') {
|
|
83
|
+
cleanup();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (line === '/clear') {
|
|
88
|
+
conversationHistory.length = 0;
|
|
89
|
+
console.log(chalk.dim('✓ Conversation history cleared\n'));
|
|
90
|
+
rl.prompt();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (line === '/help') {
|
|
95
|
+
console.log(chalk.bold('\nAvailable commands:'));
|
|
96
|
+
console.log(chalk.dim(' /exit, /quit - Exit the chat'));
|
|
97
|
+
console.log(chalk.dim(' /clear - Clear conversation history'));
|
|
98
|
+
console.log(chalk.dim(' /help - Show this help message\n'));
|
|
99
|
+
rl.prompt();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!line) {
|
|
104
|
+
rl.prompt();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add user message to history
|
|
109
|
+
conversationHistory.push({
|
|
110
|
+
role: 'user',
|
|
111
|
+
content: line,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Send to API and stream response
|
|
115
|
+
try {
|
|
116
|
+
await streamChatCompletion(server, conversationHistory);
|
|
117
|
+
console.log(); // Blank line after response
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(chalk.red(`\n❌ Error: ${(error as Error).message}\n`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
rl.prompt();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
rl.on('close', () => {
|
|
126
|
+
cleanup();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function streamChatCompletion(
|
|
131
|
+
server: ServerConfig,
|
|
132
|
+
messages: ChatMessage[]
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const url = `http://localhost:${server.port}/v1/chat/completions`;
|
|
135
|
+
|
|
136
|
+
const response = await fetch(url, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
model: server.modelName,
|
|
143
|
+
messages: messages,
|
|
144
|
+
stream: true,
|
|
145
|
+
temperature: 0.7,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const errorText = await response.text();
|
|
151
|
+
throw new Error(`API request failed (${response.status}): ${errorText}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!response.body) {
|
|
155
|
+
throw new Error('Response body is null');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Display assistant prefix
|
|
159
|
+
process.stdout.write(chalk.magenta('Assistant: '));
|
|
160
|
+
|
|
161
|
+
let fullResponse = '';
|
|
162
|
+
const reader = response.body.getReader();
|
|
163
|
+
const decoder = new TextDecoder();
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const { done, value } = await reader.read();
|
|
168
|
+
if (done) break;
|
|
169
|
+
|
|
170
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
171
|
+
const lines = chunk.split('\n').filter((line) => line.trim().startsWith('data:'));
|
|
172
|
+
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
const data = line.replace(/^data:\s*/, '').trim();
|
|
175
|
+
|
|
176
|
+
if (data === '[DONE]') {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!data) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const parsed: ChatCompletionChunk = JSON.parse(data);
|
|
186
|
+
const content = parsed.choices[0]?.delta?.content;
|
|
187
|
+
|
|
188
|
+
if (content) {
|
|
189
|
+
process.stdout.write(content);
|
|
190
|
+
fullResponse += content;
|
|
191
|
+
}
|
|
192
|
+
} catch (parseError) {
|
|
193
|
+
// Skip malformed JSON chunks
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
reader.releaseLock();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add assistant response to history
|
|
203
|
+
if (fullResponse) {
|
|
204
|
+
messages.push({
|
|
205
|
+
role: 'assistant',
|
|
206
|
+
content: fullResponse,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { modelSearch } from '../lib/model-search';
|
|
4
|
+
import { formatBytes } from '../utils/format-utils';
|
|
5
|
+
|
|
6
|
+
interface SearchOptions {
|
|
7
|
+
limit?: number;
|
|
8
|
+
files?: number | boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function searchCommand(query: string, options: SearchOptions): Promise<void> {
|
|
12
|
+
const limit = options.limit || 20;
|
|
13
|
+
|
|
14
|
+
console.log(chalk.blue(`🔍 Searching Hugging Face for: "${query}"\n`));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const results = await modelSearch.searchModels(query, limit);
|
|
18
|
+
|
|
19
|
+
if (results.length === 0) {
|
|
20
|
+
console.log(chalk.yellow('No models found.'));
|
|
21
|
+
console.log(chalk.dim('Try a different search query or browse: https://huggingface.co/models'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const table = new Table({
|
|
26
|
+
head: ['#', 'MODEL ID', 'DOWNLOADS', 'LIKES'],
|
|
27
|
+
colWidths: [4, 55, 12, 8],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < results.length; i++) {
|
|
31
|
+
const model = results[i];
|
|
32
|
+
table.push([
|
|
33
|
+
chalk.dim((i + 1).toString()),
|
|
34
|
+
model.modelId,
|
|
35
|
+
model.downloads.toLocaleString(),
|
|
36
|
+
model.likes.toString(),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(table.toString());
|
|
41
|
+
|
|
42
|
+
console.log(chalk.dim(`\nShowing ${results.length} results`));
|
|
43
|
+
console.log(chalk.dim('\nTo see files in a model:'));
|
|
44
|
+
console.log(chalk.dim(' llamacpp search "<query>" --files <number>'));
|
|
45
|
+
console.log(chalk.dim(' Example: llamacpp search "llama 3b" --files 1'));
|
|
46
|
+
console.log(chalk.dim('\nTo download:'));
|
|
47
|
+
console.log(chalk.dim(' llamacpp pull <model-id>/<file.gguf>'));
|
|
48
|
+
|
|
49
|
+
// Handle --files flag
|
|
50
|
+
if (options.files !== undefined && options.files !== false) {
|
|
51
|
+
let selectedIndex: number;
|
|
52
|
+
|
|
53
|
+
if (typeof options.files === 'number') {
|
|
54
|
+
// User specified a number: --files 1
|
|
55
|
+
selectedIndex = options.files - 1; // Convert to 0-based index
|
|
56
|
+
} else if (results.length === 1) {
|
|
57
|
+
// No number specified but only one result
|
|
58
|
+
selectedIndex = 0;
|
|
59
|
+
} else {
|
|
60
|
+
// Multiple results but no number specified
|
|
61
|
+
console.log(chalk.yellow('\n⚠️ Multiple results found. Specify which one:'));
|
|
62
|
+
console.log(chalk.dim(' llamacpp search "<query>" --files 1'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate index
|
|
67
|
+
if (selectedIndex < 0 || selectedIndex >= results.length) {
|
|
68
|
+
console.log(chalk.red(`\n❌ Invalid index. Please specify a number between 1 and ${results.length}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await showModelFiles(results[selectedIndex].modelId, selectedIndex + 1);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Search failed: ${(error as Error).message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function showModelFiles(modelId: string, index?: number): Promise<void> {
|
|
80
|
+
const indexPrefix = index ? chalk.dim(`[${index}] `) : '';
|
|
81
|
+
console.log(chalk.blue(`\n📦 GGUF files in ${indexPrefix}${modelId}:\n`));
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const files = await modelSearch.getModelFiles(modelId);
|
|
85
|
+
|
|
86
|
+
if (files.length === 0) {
|
|
87
|
+
console.log(chalk.yellow('No GGUF files found in this model.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const table = new Table({
|
|
92
|
+
head: ['FILENAME'],
|
|
93
|
+
colWidths: [70],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
table.push([file]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(table.toString());
|
|
101
|
+
|
|
102
|
+
console.log(chalk.dim(`\nTo download:`));
|
|
103
|
+
console.log(chalk.dim(` llamacpp pull ${modelId}/${files[0]}`));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.log(chalk.yellow(`\n⚠️ Could not fetch file list: ${(error as Error).message}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { modelSearch } from '../lib/model-search';
|
|
4
|
+
import { modelDownloader } from '../lib/model-downloader';
|
|
5
|
+
import { formatBytes } from '../utils/format-utils';
|
|
6
|
+
import * as https from 'https';
|
|
7
|
+
|
|
8
|
+
interface ShowOptions {
|
|
9
|
+
file?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ModelDetails {
|
|
13
|
+
modelId: string;
|
|
14
|
+
author: string;
|
|
15
|
+
modelName: string;
|
|
16
|
+
downloads: number;
|
|
17
|
+
likes: number;
|
|
18
|
+
lastModified: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
library?: string;
|
|
21
|
+
license?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FileDetails {
|
|
25
|
+
filename: string;
|
|
26
|
+
size: number;
|
|
27
|
+
lfs?: {
|
|
28
|
+
oid: string;
|
|
29
|
+
size: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function showCommand(identifier: string, options: ShowOptions): Promise<void> {
|
|
34
|
+
// Parse identifier
|
|
35
|
+
const parsed = modelDownloader.parseHFIdentifier(identifier);
|
|
36
|
+
const filename = options.file || parsed.file;
|
|
37
|
+
|
|
38
|
+
console.log(chalk.blue('📋 Fetching model information...\n'));
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Get model details
|
|
42
|
+
const modelDetails = await getModelDetails(parsed.repo);
|
|
43
|
+
|
|
44
|
+
// Display model information
|
|
45
|
+
displayModelInfo(modelDetails);
|
|
46
|
+
|
|
47
|
+
// If specific file requested, show file details
|
|
48
|
+
if (filename) {
|
|
49
|
+
console.log(chalk.blue('\n📄 File Details:\n'));
|
|
50
|
+
await displayFileInfo(parsed.repo, filename);
|
|
51
|
+
} else {
|
|
52
|
+
// Show all GGUF files
|
|
53
|
+
console.log(chalk.blue('\n📦 Available GGUF Files:\n'));
|
|
54
|
+
await displayAllFiles(parsed.repo);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to fetch model details: ${(error as Error).message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getModelDetails(modelId: string): Promise<ModelDetails> {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const url = `https://huggingface.co/api/models/${modelId}`;
|
|
64
|
+
|
|
65
|
+
https.get(url, (response) => {
|
|
66
|
+
let data = '';
|
|
67
|
+
|
|
68
|
+
response.on('data', (chunk) => {
|
|
69
|
+
data += chunk;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
response.on('end', () => {
|
|
73
|
+
try {
|
|
74
|
+
const json = JSON.parse(data);
|
|
75
|
+
const parts = modelId.split('/');
|
|
76
|
+
|
|
77
|
+
resolve({
|
|
78
|
+
modelId,
|
|
79
|
+
author: parts[0] || '',
|
|
80
|
+
modelName: parts.slice(1).join('/') || '',
|
|
81
|
+
downloads: json.downloads || 0,
|
|
82
|
+
likes: json.likes || 0,
|
|
83
|
+
lastModified: json.lastModified || '',
|
|
84
|
+
tags: json.tags || [],
|
|
85
|
+
library: json.library_name,
|
|
86
|
+
license: json.cardData?.license,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
reject(new Error(`Failed to parse model data: ${(error as Error).message}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}).on('error', (error) => {
|
|
93
|
+
reject(error);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function displayModelInfo(details: ModelDetails): void {
|
|
99
|
+
console.log(chalk.bold('Model Information:'));
|
|
100
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
101
|
+
console.log(`${chalk.bold('ID:')} ${details.modelId}`);
|
|
102
|
+
console.log(`${chalk.bold('Author:')} ${details.author}`);
|
|
103
|
+
console.log(`${chalk.bold('Downloads:')} ${details.downloads.toLocaleString()}`);
|
|
104
|
+
console.log(`${chalk.bold('Likes:')} ${details.likes.toLocaleString()}`);
|
|
105
|
+
console.log(`${chalk.bold('Last Updated:')} ${new Date(details.lastModified).toLocaleDateString()}`);
|
|
106
|
+
|
|
107
|
+
if (details.license) {
|
|
108
|
+
console.log(`${chalk.bold('License:')} ${details.license}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (details.tags.length > 0) {
|
|
112
|
+
const relevantTags = details.tags
|
|
113
|
+
.filter(tag => !tag.startsWith('arxiv:') && !tag.startsWith('dataset:'))
|
|
114
|
+
.slice(0, 5);
|
|
115
|
+
if (relevantTags.length > 0) {
|
|
116
|
+
console.log(`${chalk.bold('Tags:')} ${relevantTags.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function displayFileInfo(modelId: string, filename: string): Promise<void> {
|
|
122
|
+
const files = await getModelFiles(modelId);
|
|
123
|
+
const file = files.find(f => f.filename === filename);
|
|
124
|
+
|
|
125
|
+
if (!file) {
|
|
126
|
+
console.log(chalk.yellow(`⚠️ File not found: ${filename}`));
|
|
127
|
+
console.log(chalk.dim('\nAvailable files:'));
|
|
128
|
+
files.forEach(f => console.log(chalk.dim(` - ${f.filename}`)));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const size = file.lfs?.size || file.size || 0;
|
|
133
|
+
|
|
134
|
+
console.log(`${chalk.bold('Filename:')} ${file.filename}`);
|
|
135
|
+
console.log(`${chalk.bold('Size:')} ${formatBytes(size)}`);
|
|
136
|
+
|
|
137
|
+
if (file.lfs) {
|
|
138
|
+
console.log(`${chalk.bold('SHA256:')} ${file.lfs.oid.substring(0, 16)}...`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(chalk.dim('\nTo download:'));
|
|
142
|
+
console.log(chalk.dim(` llamacpp pull ${modelId}/${filename}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function displayAllFiles(modelId: string): Promise<void> {
|
|
146
|
+
const files = await getModelFiles(modelId);
|
|
147
|
+
const ggufFiles = files.filter(f => f.filename.toLowerCase().endsWith('.gguf'));
|
|
148
|
+
|
|
149
|
+
if (ggufFiles.length === 0) {
|
|
150
|
+
console.log(chalk.yellow('No GGUF files found in this model.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const table = new Table({
|
|
155
|
+
head: ['FILENAME', 'SIZE'],
|
|
156
|
+
colWidths: [55, 12],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
for (const file of ggufFiles) {
|
|
160
|
+
// LFS files store size in the lfs object, regular files in size field
|
|
161
|
+
const size = (file.lfs && typeof file.lfs.size === 'number') ? file.lfs.size : file.size;
|
|
162
|
+
table.push([
|
|
163
|
+
file.filename,
|
|
164
|
+
size > 0 ? formatBytes(size) : chalk.dim('Unknown'),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(table.toString());
|
|
169
|
+
|
|
170
|
+
const totalSize = ggufFiles.reduce((sum, f) => {
|
|
171
|
+
const size = (f.lfs && typeof f.lfs.size === 'number') ? f.lfs.size : f.size;
|
|
172
|
+
return sum + size;
|
|
173
|
+
}, 0);
|
|
174
|
+
console.log(chalk.dim(`\nTotal: ${ggufFiles.length} files (${formatBytes(totalSize)})`));
|
|
175
|
+
console.log(chalk.dim('\nTo download a specific file:'));
|
|
176
|
+
console.log(chalk.dim(` llamacpp pull ${modelId}/<filename>`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function getModelFiles(modelId: string): Promise<FileDetails[]> {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const url = `https://huggingface.co/api/models/${modelId}`;
|
|
182
|
+
|
|
183
|
+
https.get(url, (response) => {
|
|
184
|
+
let data = '';
|
|
185
|
+
|
|
186
|
+
response.on('data', (chunk) => {
|
|
187
|
+
data += chunk;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
response.on('end', () => {
|
|
191
|
+
try {
|
|
192
|
+
const json = JSON.parse(data);
|
|
193
|
+
const files: FileDetails[] = (json.siblings || []).map((file: any) => ({
|
|
194
|
+
filename: file.rfilename,
|
|
195
|
+
size: file.size || 0,
|
|
196
|
+
lfs: file.lfs,
|
|
197
|
+
}));
|
|
198
|
+
resolve(files);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
reject(new Error(`Failed to parse file data: ${(error as Error).message}`));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}).on('error', (error) => {
|
|
204
|
+
reject(error);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|