llm-checker 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/analyzer/compatibility.js +584 -0
  4. package/analyzer/performance.js +505 -0
  5. package/bin/CLAUDE.md +12 -0
  6. package/bin/enhanced_cli.js +3118 -0
  7. package/bin/test-deterministic.js +41 -0
  8. package/package.json +96 -0
  9. package/src/CLAUDE.md +12 -0
  10. package/src/ai/intelligent-selector.js +615 -0
  11. package/src/ai/model-selector.js +312 -0
  12. package/src/ai/multi-objective-selector.js +820 -0
  13. package/src/commands/check.js +58 -0
  14. package/src/data/CLAUDE.md +11 -0
  15. package/src/data/model-database.js +637 -0
  16. package/src/data/sync-manager.js +279 -0
  17. package/src/hardware/CLAUDE.md +12 -0
  18. package/src/hardware/backends/CLAUDE.md +11 -0
  19. package/src/hardware/backends/apple-silicon.js +318 -0
  20. package/src/hardware/backends/cpu-detector.js +490 -0
  21. package/src/hardware/backends/cuda-detector.js +417 -0
  22. package/src/hardware/backends/intel-detector.js +436 -0
  23. package/src/hardware/backends/rocm-detector.js +440 -0
  24. package/src/hardware/detector.js +573 -0
  25. package/src/hardware/pc-optimizer.js +635 -0
  26. package/src/hardware/specs.js +286 -0
  27. package/src/hardware/unified-detector.js +442 -0
  28. package/src/index.js +2289 -0
  29. package/src/models/CLAUDE.md +17 -0
  30. package/src/models/ai-check-selector.js +806 -0
  31. package/src/models/catalog.json +426 -0
  32. package/src/models/deterministic-selector.js +1145 -0
  33. package/src/models/expanded_database.js +1142 -0
  34. package/src/models/intelligent-selector.js +532 -0
  35. package/src/models/requirements.js +310 -0
  36. package/src/models/scoring-config.js +57 -0
  37. package/src/models/scoring-engine.js +715 -0
  38. package/src/ollama/.cache/README.md +33 -0
  39. package/src/ollama/CLAUDE.md +24 -0
  40. package/src/ollama/client.js +438 -0
  41. package/src/ollama/enhanced-client.js +113 -0
  42. package/src/ollama/enhanced-scraper.js +634 -0
  43. package/src/ollama/manager.js +357 -0
  44. package/src/ollama/native-scraper.js +776 -0
  45. package/src/plugins/CLAUDE.md +11 -0
  46. package/src/plugins/examples/custom_model_plugin.js +87 -0
  47. package/src/plugins/index.js +295 -0
  48. package/src/utils/CLAUDE.md +11 -0
  49. package/src/utils/config.js +359 -0
  50. package/src/utils/formatter.js +315 -0
  51. package/src/utils/logger.js +272 -0
  52. package/src/utils/model-classifier.js +167 -0
  53. package/src/utils/verbose-progress.js +266 -0
@@ -0,0 +1,3118 @@
1
+ #!/usr/bin/env node
2
+ const { Command } = require('commander');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const { table } = require('table');
6
+ const os = require('os');
7
+ const { spawn } = require('child_process');
8
+ // LLMChecker is loaded lazily to avoid slow systeminformation init
9
+ let _LLMChecker = null;
10
+ function getLLMChecker() {
11
+ if (!_LLMChecker) {
12
+ _LLMChecker = require('../src/index');
13
+ }
14
+ return _LLMChecker;
15
+ }
16
+ const { getLogger } = require('../src/utils/logger');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ // ASCII Art for each command - Large text banners
21
+ const ASCII_ART = {
22
+ 'hw-detect': `
23
+ ██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ███████╗
24
+ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔════╝
25
+ ███████║███████║██████╔╝██║ ██║██║ █╗ ██║███████║██████╔╝█████╗
26
+ ██╔══██║██╔══██║██╔══██╗██║ ██║██║███╗██║██╔══██║██╔══██╗██╔══╝
27
+ ██║ ██║██║ ██║██║ ██║██████╔╝╚███╔███╔╝██║ ██║██║ ██║███████╗
28
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
29
+ DETECTION`,
30
+
31
+ 'smart-recommend': `
32
+ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗
33
+ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝
34
+ ███████╗██╔████╔██║███████║██████╔╝ ██║
35
+ ╚════██║██║╚██╔╝██║██╔══██║██╔══██╗ ██║
36
+ ███████║██║ ╚═╝ ██║██║ ██║██║ ██║ ██║
37
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
38
+ RECOMMEND - AI Powered`,
39
+
40
+ 'search': `
41
+ ███████╗███████╗ █████╗ ██████╗ ██████╗██╗ ██╗
42
+ ██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██║ ██║
43
+ ███████╗█████╗ ███████║██████╔╝██║ ███████║
44
+ ╚════██║██╔══╝ ██╔══██║██╔══██╗██║ ██╔══██║
45
+ ███████║███████╗██║ ██║██║ ██║╚██████╗██║ ██║
46
+ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
47
+ 6900+ Models Available`,
48
+
49
+ 'sync': `
50
+ ███████╗██╗ ██╗███╗ ██╗ ██████╗
51
+ ██╔════╝╚██╗ ██╔╝████╗ ██║██╔════╝
52
+ ███████╗ ╚████╔╝ ██╔██╗ ██║██║
53
+ ╚════██║ ╚██╔╝ ██║╚██╗██║██║
54
+ ███████║ ██║ ██║ ╚████║╚██████╗
55
+ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝
56
+ Database Synchronization`,
57
+
58
+ 'check': `
59
+ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
60
+ ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
61
+ ██║ ███████║█████╗ ██║ █████╔╝
62
+ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
63
+ ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
64
+ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
65
+ Compatibility Analysis`,
66
+
67
+ 'installed': `
68
+ ██╗███╗ ██╗███████╗████████╗ █████╗ ██╗ ██╗ ███████╗██████╗
69
+ ██║████╗ ██║██╔════╝╚══██╔══╝██╔══██╗██║ ██║ ██╔════╝██╔══██╗
70
+ ██║██╔██╗ ██║███████╗ ██║ ███████║██║ ██║ █████╗ ██║ ██║
71
+ ██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║ ██║ ██╔══╝ ██║ ██║
72
+ ██║██║ ╚████║███████║ ██║ ██║ ██║███████╗███████╗███████╗██████╔╝
73
+ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═════╝
74
+ Local Models`,
75
+
76
+ 'ai-check': `
77
+ █████╗ ██╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
78
+ ██╔══██╗██║ ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
79
+ ███████║██║ ██║ ███████║█████╗ ██║ █████╔╝
80
+ ██╔══██║██║ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
81
+ ██║ ██║██║ ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
82
+ ╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
83
+ AI-Powered Evaluation`,
84
+
85
+ 'ai-run': `
86
+ █████╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
87
+ ██╔══██╗██║ ██╔══██╗██║ ██║████╗ ██║
88
+ ███████║██║ ██████╔╝██║ ██║██╔██╗ ██║
89
+ ██╔══██║██║ ██╔══██╗██║ ██║██║╚██╗██║
90
+ ██║ ██║██║ ██║ ██║╚██████╔╝██║ ╚████║
91
+ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
92
+ Launch & Execute Model`,
93
+
94
+ 'demo': `
95
+ ██████╗ ███████╗███╗ ███╗ ██████╗
96
+ ██╔══██╗██╔════╝████╗ ████║██╔═══██╗
97
+ ██║ ██║█████╗ ██╔████╔██║██║ ██║
98
+ ██║ ██║██╔══╝ ██║╚██╔╝██║██║ ██║
99
+ ██████╔╝███████╗██║ ╚═╝ ██║╚██████╔╝
100
+ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝
101
+ Interactive Preview`,
102
+
103
+ 'ollama': `
104
+ ██████╗ ██╗ ██╗ █████╗ ███╗ ███╗ █████╗
105
+ ██╔═══██╗██║ ██║ ██╔══██╗████╗ ████║██╔══██╗
106
+ ██║ ██║██║ ██║ ███████║██╔████╔██║███████║
107
+ ██║ ██║██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║
108
+ ╚██████╔╝███████╗███████╗██║ ██║██║ ╚═╝ ██║██║ ██║
109
+ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
110
+ Status & Integration`,
111
+
112
+ 'recommend': `
113
+ ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗███████╗███╗ ██╗██████╗
114
+ ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔════╝████╗ ██║██╔══██╗
115
+ ██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔████╔██║█████╗ ██╔██╗ ██║██║ ██║
116
+ ██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║██║ ██║
117
+ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║███████╗██║ ╚████║██████╔╝
118
+ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═════╝
119
+ Top Picks For You`,
120
+
121
+ 'list-models': `
122
+ ███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ ███████╗
123
+ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║ ██╔════╝
124
+ ██╔████╔██║██║ ██║██║ ██║█████╗ ██║ ███████╗
125
+ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██║ ╚════██║
126
+ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗███████║
127
+ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
128
+ Browse All Available`
129
+ };
130
+
131
+ // Function to display ASCII art for a command
132
+ function showAsciiArt(command) {
133
+ if (ASCII_ART[command]) {
134
+ console.log(chalk.cyan(ASCII_ART[command]));
135
+ console.log('');
136
+ }
137
+ }
138
+
139
+ // Function to search Ollama models by use case
140
+ function getOllamaCacheFile(filename) {
141
+ try {
142
+ const homePath = path.join(os.homedir(), '.llm-checker', 'cache', 'ollama', filename);
143
+ const legacyPath = path.join(__dirname, '../src/ollama/.cache', filename);
144
+ if (fs.existsSync(homePath)) return homePath;
145
+ if (fs.existsSync(legacyPath)) return legacyPath;
146
+ return homePath; // default preferred path
147
+ } catch {
148
+ return path.join(__dirname, '../src/ollama/.cache', filename);
149
+ }
150
+ }
151
+
152
+ function searchOllamaModelsForUseCase(useCase, hardware) {
153
+ try {
154
+ const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
155
+ if (!fs.existsSync(cacheFile)) return [];
156
+
157
+ const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
158
+ const models = cacheData.models || [];
159
+
160
+ // Filter models by use case with typo tolerance
161
+ const useCaseModels = models.filter(model => {
162
+ const lowerUseCase = useCase.toLowerCase();
163
+ switch (lowerUseCase) {
164
+ case 'creative':
165
+ case 'writing':
166
+ return model.primary_category === 'creative';
167
+
168
+ case 'coding':
169
+ case 'code':
170
+ return model.primary_category === 'coding';
171
+
172
+ case 'chat':
173
+ case 'conversation':
174
+ case 'talking':
175
+ return model.primary_category === 'chat';
176
+
177
+ case 'multimodal':
178
+ case 'vision':
179
+ return model.primary_category === 'multimodal';
180
+
181
+ case 'embeddings':
182
+ case 'embedings': // typo tolerance
183
+ case 'embedding':
184
+ case 'embeding': // typo tolerance
185
+ return model.primary_category === 'embeddings';
186
+
187
+ case 'reasoning':
188
+ case 'reason':
189
+ return model.primary_category === 'reasoning';
190
+
191
+ default:
192
+ // Check for partial matches
193
+ if (lowerUseCase.includes('embed')) return model.primary_category === 'embeddings';
194
+ if (lowerUseCase.includes('code')) return model.primary_category === 'coding';
195
+ if (lowerUseCase.includes('creat')) return model.primary_category === 'creative';
196
+ if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) return model.primary_category === 'chat';
197
+ if (lowerUseCase.includes('vision') || lowerUseCase.includes('multimodal')) return model.primary_category === 'multimodal';
198
+ if (lowerUseCase.includes('reason')) return model.primary_category === 'reasoning';
199
+ return false;
200
+ }
201
+ });
202
+
203
+ // Convert Ollama models to compatible format and add basic compatibility scoring
204
+ return useCaseModels.map(model => {
205
+ // Find a suitable variant (prefer 7b-13b for high-end hardware)
206
+ let bestVariant = null;
207
+ if (model.variants && model.variants.length > 0) {
208
+ // For high-tier hardware, prefer 7B-13B models
209
+ bestVariant = model.variants.find(v =>
210
+ v.real_size_gb >= 3 && v.real_size_gb <= 15 &&
211
+ !v.tag.includes('-instruct') && !v.tag.includes('-code')
212
+ ) || model.variants[0];
213
+ }
214
+
215
+ const size = bestVariant ? bestVariant.real_size_gb : 7;
216
+ const ollamaTag = bestVariant ? bestVariant.tag : model.model_identifier + ':latest';
217
+
218
+ return {
219
+ name: model.model_name || model.model_identifier,
220
+ size: size + 'GB',
221
+ type: 'ollama',
222
+ category: model.primary_category,
223
+ specialization: model.primary_category,
224
+ primary_category: model.primary_category,
225
+ categories: model.categories,
226
+ requirements: {
227
+ ram: Math.max(4, Math.ceil(size * 1.2)),
228
+ vram: 0,
229
+ cpu_cores: 2,
230
+ storage: size,
231
+ recommended_ram: Math.max(8, Math.ceil(size * 1.5))
232
+ },
233
+ frameworks: ['ollama'],
234
+ performance: {
235
+ speed: size <= 7 ? 'fast' : size <= 13 ? 'medium' : 'slow',
236
+ quality: model.primary_category === 'coding' ? 'excellent_for_code' :
237
+ model.primary_category === 'creative' ? 'excellent_for_creative' : 'good',
238
+ context_length: 4096,
239
+ tokens_per_second_estimate: size <= 7 ? '30-50' : '15-30'
240
+ },
241
+ installation: {
242
+ ollama: `ollama pull ${ollamaTag}`,
243
+ description: model.detailed_description || model.description || `${model.primary_category} model`
244
+ },
245
+ ollamaId: model.model_identifier,
246
+ ollamaTag: ollamaTag,
247
+ source: 'ollama_database',
248
+ // Basic compatibility score (can be improved)
249
+ score: calculateBasicCompatibilityScore(size, hardware),
250
+ isOllamaInstalled: false,
251
+ ollamaAvailable: true
252
+ };
253
+ }).slice(0, 10); // Limit to top 10 models
254
+
255
+ } catch (error) {
256
+ console.warn('Error searching Ollama models:', error.message);
257
+ return [];
258
+ }
259
+ }
260
+
261
+ // Basic compatibility scoring for Ollama models
262
+ function calculateBasicCompatibilityScore(modelSizeGB, hardware) {
263
+ const totalRAM = hardware.memory?.total || 8;
264
+ const availableRAM = totalRAM * 0.8; // Assume 80% available
265
+
266
+ // RAM compatibility
267
+ let ramScore = 0;
268
+ if (modelSizeGB * 1.5 <= availableRAM) {
269
+ ramScore = 100;
270
+ } else if (modelSizeGB <= availableRAM) {
271
+ ramScore = 80;
272
+ } else {
273
+ ramScore = Math.max(0, 50 - (modelSizeGB - availableRAM) * 10);
274
+ }
275
+
276
+ // Size efficiency (prefer 7B-13B for high-end hardware)
277
+ let sizeScore = 100;
278
+ if (totalRAM >= 16) { // High-end hardware
279
+ if (modelSizeGB >= 7 && modelSizeGB <= 13) {
280
+ sizeScore = 100;
281
+ } else if (modelSizeGB < 7) {
282
+ sizeScore = 85; // Small models are okay but not optimal
283
+ } else {
284
+ sizeScore = Math.max(60, 100 - (modelSizeGB - 13) * 5);
285
+ }
286
+ }
287
+
288
+ return Math.round((ramScore * 0.7 + sizeScore * 0.3));
289
+ }
290
+
291
+ // Function to get real size directly from Ollama cache
292
+ function estimateModelSize(model) {
293
+ // Extract parameter count from model name (e.g., "3B", "7B", "13B")
294
+ const nameMatch = model.name.match(/(\d+\.?\d*)[bB]\b/i);
295
+ if (nameMatch) {
296
+ const paramCount = parseFloat(nameMatch[1]);
297
+ // Estimate size using Q4_K_M quantization (~0.5 bytes per parameter + overhead)
298
+ const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
299
+ return `~${estimatedGB}GB (Q4_K_M)`;
300
+ }
301
+
302
+ // Try to extract from model identifier or fallback patterns
303
+ if (model.model_identifier) {
304
+ const idMatch = model.model_identifier.match(/(\d+\.?\d*)b/i);
305
+ if (idMatch) {
306
+ const paramCount = parseFloat(idMatch[1]);
307
+ const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
308
+ return `~${estimatedGB}GB (Q4_K_M)`;
309
+ }
310
+ }
311
+
312
+ // Known model size patterns
313
+ const sizeMappings = {
314
+ 'tinyllama': '~1.1GB (Q4_K_M)',
315
+ 'mobilellama': '~1.4GB (Q4_K_M)',
316
+ 'phi': '~2.7GB (Q4_K_M)',
317
+ 'gemma': '~5.3GB (Q4_K_M)',
318
+ 'llama.*3b': '~2.0GB (Q4_K_M)',
319
+ 'llama.*7b': '~4.4GB (Q4_K_M)',
320
+ 'llama.*13b': '~7.8GB (Q4_K_M)',
321
+ 'dolphincoder': '~4.2GB (Q4_K_M)',
322
+ 'deepseek-coder': '~4.0GB (Q4_K_M)',
323
+ 'starcoder': '~8.4GB (Q4_K_M)'
324
+ };
325
+
326
+ const modelNameLower = (model.name || '').toLowerCase();
327
+ for (const [pattern, size] of Object.entries(sizeMappings)) {
328
+ if (new RegExp(pattern, 'i').test(modelNameLower)) {
329
+ return size;
330
+ }
331
+ }
332
+
333
+ // If we have size field but it's not formatted well
334
+ if (model.size && typeof model.size === 'string') {
335
+ const sizeMatch = model.size.match(/(\d+\.?\d*)\s*(GB|MB|B)/i);
336
+ if (sizeMatch) {
337
+ const num = parseFloat(sizeMatch[1]);
338
+ const unit = sizeMatch[2].toUpperCase();
339
+ if (unit === 'GB') return `${num}GB`;
340
+ if (unit === 'MB') return `${Math.round(num / 1024 * 10) / 10}GB`;
341
+ }
342
+ }
343
+
344
+ // Final fallback
345
+ return '~4.5GB (estimated)';
346
+ }
347
+
348
+ function getRealSizeFromOllamaCache(model) {
349
+ try {
350
+ const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
351
+ if (!fs.existsSync(cacheFile)) return null;
352
+
353
+ const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
354
+ const models = cacheData.models || [];
355
+
356
+ // Try to find the model by different strategies
357
+ let targetModel = null;
358
+
359
+ // Strategy 1: Match by ollamaId directly (e.g., "codellama")
360
+ if (model.ollamaId) {
361
+ // Special case: if looking for phind-codellama but model is actually CodeLlama, use codellama instead
362
+ if (model.ollamaId === 'phind-codellama' &&
363
+ (model.name.toLowerCase().includes('codellama') || model.name.toLowerCase().includes('code llama'))) {
364
+ targetModel = models.find(m => m.model_identifier === 'codellama');
365
+ }
366
+ // Special case: DeepSeek Coder has wrong ollamaId
367
+ else if (model.ollamaId === 'deepseek-v2.5' &&
368
+ model.name.toLowerCase().includes('deepseek') &&
369
+ model.name.toLowerCase().includes('coder')) {
370
+ targetModel = models.find(m => m.model_identifier === 'deepseek-coder');
371
+ }
372
+ // Special case: TinyLlama incorrectly mapped to llama-pro
373
+ else if (model.ollamaId === 'llama-pro' &&
374
+ model.name && model.name.toLowerCase().includes('tinyllama')) {
375
+ targetModel = models.find(m => m.model_identifier === 'tinyllama');
376
+ } else {
377
+ targetModel = models.find(m => m.model_identifier === model.ollamaId);
378
+ }
379
+ }
380
+
381
+ // Strategy 2: Match by name similarity
382
+ if (!targetModel && model.name) {
383
+ const modelNameLower = model.name.toLowerCase();
384
+
385
+ // Special handling for specific models - be very specific
386
+ if (modelNameLower.includes('deepseek') && modelNameLower.includes('coder')) {
387
+ targetModel = models.find(m => m.model_identifier.toLowerCase() === 'deepseek-coder');
388
+ } else if (modelNameLower.includes('llama3.3')) {
389
+ targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.3');
390
+ } else if (modelNameLower.includes('llama3.2')) {
391
+ targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.2');
392
+ } else if (modelNameLower.includes('llama3.1') || modelNameLower.includes('llama 3.1')) {
393
+ targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.1');
394
+ } else {
395
+ targetModel = models.find(m => {
396
+ const identifier = m.model_identifier.toLowerCase();
397
+ return identifier.includes('codellama') && modelNameLower.includes('codellama') ||
398
+ identifier.includes('qwen') && modelNameLower.includes('qwen') ||
399
+ identifier.includes('mistral') && modelNameLower.includes('mistral');
400
+ });
401
+ }
402
+ }
403
+
404
+ if (!targetModel || !targetModel.variants) return null;
405
+
406
+ // Extract size from model name (e.g., "CodeLlama 7B" -> "7b")
407
+ let targetSize = null;
408
+ if (model.size) {
409
+ targetSize = model.size.toLowerCase().replace('b', '') + 'b';
410
+ } else if (model.name) {
411
+ const sizeMatch = model.name.match(/(\d+\.?\d*)[bB]/);
412
+ if (sizeMatch) {
413
+ targetSize = sizeMatch[1] + 'b';
414
+ }
415
+ }
416
+
417
+
418
+ // Find the right variant
419
+ let variant = null;
420
+ if (targetSize) {
421
+ // Look for exact size match (e.g., "codellama:7b")
422
+ variant = targetModel.variants.find(v =>
423
+ v.tag.includes(':' + targetSize) &&
424
+ !v.tag.includes('-instruct') &&
425
+ !v.tag.includes(':code-') // Exclude variants like ":code-" but allow "coder"
426
+ );
427
+
428
+ }
429
+
430
+ // Fallback to latest or first variant
431
+ if (!variant) {
432
+ variant = targetModel.variants.find(v => v.tag.includes(':latest')) ||
433
+ targetModel.variants[0];
434
+
435
+ }
436
+
437
+ if (variant && variant.real_size_gb) {
438
+ return variant.real_size_gb + 'GB';
439
+ }
440
+
441
+ return null;
442
+ } catch (error) {
443
+ console.warn('Error reading Ollama cache:', error.message);
444
+ return null;
445
+ }
446
+ }
447
+
448
+ const program = new Command();
449
+
450
+ program
451
+ .name('llm-checker')
452
+ .description('Check which LLM models your computer can run')
453
+ .version(require('../package.json').version);
454
+
455
+ const logger = getLogger({ console: false });
456
+
457
+ // Ollama installation helper
458
+ function getOllamaInstallInstructions() {
459
+ const platform = os.platform();
460
+ const arch = os.arch();
461
+
462
+ const instructions = {
463
+ 'darwin': {
464
+ name: 'macOS',
465
+ downloadUrl: 'https://ollama.com/download/mac',
466
+ instructions: [
467
+ '1. Download Ollama for macOS from the link above',
468
+ '2. Open the downloaded .pkg file and follow the installer',
469
+ '3. Once installed, open Terminal and run: ollama serve',
470
+ '4. In a new terminal window, test with: ollama run llama2:7b'
471
+ ],
472
+ alternativeInstall: 'brew install ollama'
473
+ },
474
+ 'win32': {
475
+ name: 'Windows',
476
+ downloadUrl: 'https://ollama.com/download/windows',
477
+ instructions: [
478
+ '1. Download Ollama for Windows from the link above',
479
+ '2. Run the downloaded installer (.exe file)',
480
+ '3. Open Command Prompt or PowerShell',
481
+ '4. Test with: ollama run llama2:7b'
482
+ ],
483
+ alternativeInstall: 'winget install Ollama.Ollama'
484
+ },
485
+ 'linux': {
486
+ name: 'Linux',
487
+ downloadUrl: 'https://ollama.com/download/linux',
488
+ instructions: [
489
+ '1. Review official installation options:',
490
+ ' https://github.com/ollama/ollama/blob/main/docs/linux.md',
491
+ '2. Prefer a package manager (apt/dnf/pacman) when available',
492
+ '3. Start service after install:',
493
+ ' sudo systemctl start ollama',
494
+ '4. Test with: ollama run llama2:7b'
495
+ ],
496
+ alternativeInstall: 'Manual install: https://github.com/ollama/ollama/blob/main/docs/linux.md'
497
+ }
498
+ };
499
+
500
+ return instructions[platform] || instructions['linux'];
501
+ }
502
+
503
+ function displayOllamaInstallHelp() {
504
+ const installInfo = getOllamaInstallInstructions();
505
+
506
+ console.log(chalk.red.bold('\nOllama is not installed or not running!'));
507
+ console.log(chalk.yellow('\nLLM Checker requires Ollama to function properly.'));
508
+ console.log(chalk.cyan.bold(`\nInstall Ollama for ${installInfo.name}:`));
509
+ console.log(chalk.blue(`\nDownload: ${installInfo.downloadUrl}`));
510
+
511
+ console.log(chalk.green.bold('\nInstallation Steps:'));
512
+ installInfo.instructions.forEach(step => {
513
+ console.log(chalk.gray(` ${step}`));
514
+ });
515
+
516
+ if (installInfo.alternativeInstall) {
517
+ console.log(chalk.magenta.bold('\nQuick Install (if available):'));
518
+ console.log(chalk.white(` ${installInfo.alternativeInstall}`));
519
+ }
520
+
521
+ console.log(chalk.yellow.bold('\nAfter installation:'));
522
+ console.log(chalk.gray(' 1. Restart your terminal'));
523
+ console.log(chalk.gray(' 2. Run: llm-checker check'));
524
+ console.log(chalk.gray(' 3. Start using the AI model selector!'));
525
+
526
+ console.log(chalk.cyan('\nNeed help? Visit: https://github.com/ollama/ollama'));
527
+ }
528
+
529
+ async function checkOllamaAndExit() {
530
+ const spinner = ora('Checking Ollama availability...').start();
531
+
532
+ try {
533
+ // Quick check if ollama command exists
534
+ const checkCommand = os.platform() === 'win32' ? 'where' : 'which';
535
+
536
+ return new Promise((resolve) => {
537
+ const proc = spawn(checkCommand, ['ollama'], { stdio: 'pipe' });
538
+
539
+ proc.on('close', (code) => {
540
+ spinner.stop();
541
+ if (code !== 0) {
542
+ displayOllamaInstallHelp();
543
+ process.exit(1);
544
+ }
545
+ resolve(true);
546
+ });
547
+
548
+ proc.on('error', () => {
549
+ spinner.stop();
550
+ displayOllamaInstallHelp();
551
+ process.exit(1);
552
+ });
553
+ });
554
+ } catch (error) {
555
+ spinner.stop();
556
+ displayOllamaInstallHelp();
557
+ process.exit(1);
558
+ }
559
+ }
560
+
561
+ function getStatusIcon(model, ollamaModels) {
562
+ const ollamaModel = ollamaModels?.find(om => om.matchedModel?.name === model.name);
563
+
564
+ if (ollamaModel?.isRunning) return 'R';
565
+ if (ollamaModel?.isInstalled) return 'I';
566
+
567
+ if (model.specialization === 'code') return 'C';
568
+ if (model.specialization === 'multimodal' || model.multimodal) return 'M';
569
+ if (model.specialization === 'embeddings') return 'E';
570
+ if (model.category === 'ultra_small') return 'XS';
571
+ if (model.category === 'small') return 'S';
572
+ if (model.category === 'medium') return 'M';
573
+ if (model.category === 'large') return 'L';
574
+
575
+ return '-';
576
+ }
577
+
578
+ function formatSize(size) {
579
+ if (!size) return 'Unknown';
580
+
581
+ const cleanSize = size.replace(/[^\d.BMK]/gi, '');
582
+ const numMatch = cleanSize.match(/(\d+\.?\d*)/);
583
+ const unitMatch = cleanSize.match(/[BMK]/i);
584
+
585
+ if (numMatch && unitMatch) {
586
+ const num = parseFloat(numMatch[1]);
587
+ const unit = unitMatch[0].toUpperCase();
588
+ return `${num}${unit}`;
589
+ }
590
+
591
+ return size;
592
+ }
593
+
594
+ // Helper function to calculate model compatibility score
595
+ function calculateModelCompatibilityScore(model, hardware) {
596
+ let score = 50; // Base score
597
+
598
+ // Estimar tamaño del modelo
599
+ const sizeMatch = model.model_identifier.match(/(\d+\.?\d*)[bm]/i);
600
+ let modelSizeB = 1; // Default 1B
601
+
602
+ if (sizeMatch) {
603
+ const num = parseFloat(sizeMatch[1]);
604
+ const unit = sizeMatch[0].slice(-1).toLowerCase();
605
+ modelSizeB = unit === 'm' ? num / 1000 : num;
606
+ }
607
+
608
+ // Calcular requerimientos estimados
609
+ const estimatedRAM = modelSizeB * 1.2; // 1.2x el tamaño del modelo
610
+ const ramRatio = hardware.memory.total / estimatedRAM;
611
+
612
+ // Puntuación por compatibilidad de RAM (40% del score)
613
+ if (ramRatio >= 3) score += 40;
614
+ else if (ramRatio >= 2) score += 30;
615
+ else if (ramRatio >= 1.5) score += 20;
616
+ else if (ramRatio >= 1.2) score += 10;
617
+ else if (ramRatio >= 1) score += 5;
618
+ else score -= 20; // Penalización por RAM insuficiente
619
+
620
+ // Puntuación por tamaño del modelo (30% del score)
621
+ if (modelSizeB <= 1) score += 30; // Modelos pequeños funcionan en cualquier lado
622
+ else if (modelSizeB <= 3) score += 25;
623
+ else if (modelSizeB <= 7) score += 20;
624
+ else if (modelSizeB <= 13) score += 15;
625
+ else if (modelSizeB <= 30) score += 10;
626
+ else score -= 10; // Modelos muy grandes
627
+
628
+ // Puntuación por CPU cores (20% del score)
629
+ if (hardware.cpu.cores >= 12) score += 20;
630
+ else if (hardware.cpu.cores >= 8) score += 15;
631
+ else if (hardware.cpu.cores >= 6) score += 10;
632
+ else if (hardware.cpu.cores >= 4) score += 5;
633
+
634
+ // Bonus por popularidad (10% del score)
635
+ const pulls = model.pulls || 0;
636
+ if (pulls > 1000000) score += 10;
637
+ else if (pulls > 100000) score += 7;
638
+ else if (pulls > 10000) score += 5;
639
+ else if (pulls > 1000) score += 3;
640
+
641
+ // Bonus especial para Apple Silicon
642
+ if (hardware.cpu.architecture === 'Apple Silicon') {
643
+ score += 5;
644
+ // Bonus extra para modelos optimizados
645
+ const modelName = model.model_identifier.toLowerCase();
646
+ if (modelName.includes('llama') || modelName.includes('mistral') ||
647
+ modelName.includes('phi') || modelName.includes('gemma')) {
648
+ score += 3;
649
+ }
650
+ }
651
+
652
+ return Math.max(0, Math.min(100, Math.round(score)));
653
+ }
654
+
655
+ // Helper function to get hardware tier for display
656
+ function getHardwareTierForDisplay(hardware) {
657
+ const ram = hardware.memory.total;
658
+ const cores = hardware.cpu.cores;
659
+ const gpuModel = hardware.gpu?.model || '';
660
+ const vramGB = hardware.gpu?.vram || 0;
661
+
662
+ // Check if it's integrated GPU (should cap tier)
663
+ const isIntegratedGPU = /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(gpuModel);
664
+ const hasDedicatedGPU = vramGB > 0 && !isIntegratedGPU;
665
+ const isAppleSilicon = process.platform === 'darwin' && (gpuModel.toLowerCase().includes('apple') || gpuModel.toLowerCase().includes('m1') || gpuModel.toLowerCase().includes('m2') || gpuModel.toLowerCase().includes('m3') || gpuModel.toLowerCase().includes('m4'));
666
+
667
+ // Base tier calculation
668
+ let tier;
669
+ if (ram >= 64 && cores >= 16) tier = 'EXTREME';
670
+ else if (ram >= 32 && cores >= 12) tier = 'VERY HIGH';
671
+ else if (ram >= 16 && cores >= 8) tier = 'HIGH';
672
+ else if (ram >= 8 && cores >= 4) tier = 'MEDIUM';
673
+ else if (ram >= 4 && cores >= 2) tier = 'LOW';
674
+ else tier = 'ULTRA LOW';
675
+
676
+ // Special cases for edge configurations
677
+ if (ram >= 16 && ram < 32 && cores >= 12) tier = 'HIGH';
678
+ if (ram >= 32 && ram < 64 && cores >= 8 && tier === 'ULTRA LOW') tier = 'VERY HIGH';
679
+
680
+ // Cap tier for integrated GPU systems (most important fix)
681
+ if (isIntegratedGPU && !isAppleSilicon) {
682
+ // Cap iGPU systems at HIGH maximum (Iris Xe, Intel UHD, AMD integrated, etc.)
683
+ const tierPriority = { 'ULTRA LOW': 0, 'LOW': 1, 'MEDIUM': 2, 'HIGH': 3, 'VERY HIGH': 4, 'EXTREME': 5 };
684
+ const currentPriority = tierPriority[tier] || 0;
685
+ if (currentPriority > 3) { // HIGH = 3
686
+ tier = 'HIGH';
687
+ }
688
+ }
689
+
690
+ return tier;
691
+ }
692
+
693
+ function formatSpeed(speed) {
694
+ const speedMap = {
695
+ 'very_fast': 'very_fast',
696
+ 'fast': 'fast',
697
+ 'medium': 'medium',
698
+ 'slow': 'slow',
699
+ 'very_slow': 'very_slow'
700
+ };
701
+ return speedMap[speed] || (speed || 'unknown');
702
+ }
703
+
704
+ function getScoreColor(score) {
705
+ if (score >= 90) return chalk.green;
706
+ if (score >= 75) return chalk.yellow;
707
+ if (score >= 60) return chalk.hex('#FFA500');
708
+ return chalk.red;
709
+ }
710
+
711
+ function getOllamaCommand(modelName) {
712
+ const mapping = {
713
+ 'TinyLlama 1.1B': 'tinyllama:1.1b',
714
+ 'Qwen 0.5B': 'qwen:0.5b',
715
+ 'Gemma 2B': 'gemma2:2b',
716
+ 'Phi-3 Mini 3.8B': 'phi3:mini',
717
+ 'Llama 3.2 3B': 'llama3.2:3b',
718
+ 'Llama 3.1 8B': 'llama3.1:8b',
719
+ 'Mistral 7B v0.3': 'mistral:7b',
720
+ 'CodeLlama 7B': 'codellama:7b',
721
+ 'Qwen 2.5 7B': 'qwen2.5:7b'
722
+ };
723
+
724
+ return mapping[modelName] || '-';
725
+ }
726
+
727
+ function displaySystemInfo(hardware, analysis) {
728
+ const cpuColor = hardware.cpu.cores >= 8 ? chalk.green : hardware.cpu.cores >= 4 ? chalk.yellow : chalk.red;
729
+ const ramColor = hardware.memory.total >= 32 ? chalk.green : hardware.memory.total >= 16 ? chalk.yellow : chalk.red;
730
+ const gpuColor = hardware.gpu.dedicated ? chalk.green : chalk.hex('#FFA500');
731
+
732
+ const lines = [
733
+ `${chalk.cyan('CPU:')} ${cpuColor(hardware.cpu.brand)} ${chalk.gray(`(${hardware.cpu.cores} cores, ${hardware.cpu.speed}GHz)`)}`,
734
+ `${chalk.cyan('Architecture:')} ${hardware.cpu.architecture}`,
735
+ `${chalk.cyan('RAM:')} ${ramColor(hardware.memory.total + 'GB')}`,
736
+ `${chalk.cyan('GPU:')} ${gpuColor(hardware.gpu.model || 'Not detected')}`,
737
+ `${chalk.cyan('VRAM:')} ${hardware.gpu.vram === 0 && hardware.gpu.model && hardware.gpu.model.toLowerCase().includes('apple') ? 'Unified Memory' : `${hardware.gpu.vram || 'N/A'}GB`}${hardware.gpu.dedicated ? chalk.green(' (Dedicated)') : chalk.hex('#FFA500')(' (Integrated)')}`,
738
+ ];
739
+
740
+ const tier = analysis.summary.hardwareTier?.replace('_', ' ').toUpperCase() || 'UNKNOWN';
741
+ const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
742
+
743
+ lines.push(`${chalk.bold('Hardware Tier:')} ${tierColor.bold(tier)}`);
744
+
745
+ console.log('\n' + chalk.bgBlue.white.bold(' SYSTEM INFORMATION '));
746
+ console.log(chalk.blue('╭' + '─'.repeat(50)));
747
+
748
+ lines.forEach(line => {
749
+ console.log(chalk.blue('│') + ' ' + line);
750
+ });
751
+
752
+ console.log(chalk.blue('╰'));
753
+ }
754
+
755
+ function displayOllamaIntegration(ollamaInfo, ollamaModels) {
756
+ const lines = [];
757
+
758
+ if (ollamaInfo.available) {
759
+ lines.push(`${chalk.green('✅ Status:')} Running ${chalk.gray(`(v${ollamaInfo.version || 'unknown'})`)}`);
760
+
761
+ if (ollamaModels && ollamaModels.length > 0) {
762
+ const compatibleCount = ollamaModels.filter(m => {
763
+ return m.canRun === true ||
764
+ m.compatibilityScore >= 60 ||
765
+ (m.matchedModel && true);
766
+ }).length;
767
+
768
+ const runningCount = ollamaModels.filter(m => m.isRunning).length;
769
+
770
+ lines.push(`${chalk.cyan('Installed:')} ${ollamaModels.length} total, ${chalk.green(compatibleCount)} compatible`);
771
+ if (runningCount > 0) {
772
+ lines.push(`${chalk.cyan('Running:')} ${chalk.green(runningCount)} models`);
773
+ }
774
+ } else {
775
+ lines.push(`${chalk.gray('No models installed yet')}`);
776
+ }
777
+ } else {
778
+ lines.push(`${chalk.red('Status:')} Not available`);
779
+ }
780
+
781
+ console.log('\n' + chalk.bgMagenta.white.bold(' OLLAMA INTEGRATION '));
782
+ console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(50)));
783
+
784
+ lines.forEach(line => {
785
+ console.log(chalk.hex('#a259ff')('│') + ' ' + line);
786
+ });
787
+
788
+ console.log(chalk.hex('#a259ff')('╰'));
789
+ }
790
+
791
+ function displayEnhancedCompatibleModels(compatible, ollamaModels) {
792
+ if (compatible.length === 0) {
793
+ console.log('\n' + chalk.yellow('No compatible models found.'));
794
+ return;
795
+ }
796
+
797
+ console.log('\n' + chalk.green.bold(' ✅ Compatible Models (Score ≥ 75)'));
798
+
799
+ const data = [
800
+ [
801
+ chalk.bgGreen.white.bold(' Model '),
802
+ chalk.bgGreen.white.bold(' Size '),
803
+ chalk.bgGreen.white.bold(' Score '),
804
+ chalk.bgGreen.white.bold(' RAM '),
805
+ chalk.bgGreen.white.bold(' VRAM '),
806
+ chalk.bgGreen.white.bold(' Speed '),
807
+ chalk.bgGreen.white.bold(' Status ')
808
+ ]
809
+ ];
810
+
811
+ compatible.slice(0, 15).forEach(model => {
812
+ const tokensPerSec = model.performanceEstimate?.estimatedTokensPerSecond || 'N/A';
813
+ const ramReq = model.requirements?.ram || 1;
814
+ const vramReq = model.requirements?.vram || 0;
815
+ const speedFormatted = formatSpeed(model.performance?.speed || 'medium');
816
+ const scoreColor = getScoreColor(model.score || 0);
817
+ const scoreDisplay = scoreColor(`${model.score || 0}/100`);
818
+
819
+ let statusDisplay = `${tokensPerSec}t/s`;
820
+ if (model.isOllamaInstalled) {
821
+ const ollamaInfo = model.ollamaInfo || {};
822
+ if (ollamaInfo.isRunning) {
823
+ statusDisplay = 'Running';
824
+ } else {
825
+ statusDisplay = 'Installed';
826
+ }
827
+ }
828
+
829
+ let modelName = model.name;
830
+ if (model.isOllamaInstalled) {
831
+ modelName = `${model.name}`;
832
+ }
833
+
834
+ const row = [
835
+ modelName,
836
+ formatSize(model.size || 'Unknown'),
837
+ scoreDisplay,
838
+ `${ramReq}GB`,
839
+ `${vramReq}GB`,
840
+ speedFormatted,
841
+ statusDisplay
842
+ ];
843
+ data.push(row);
844
+ });
845
+
846
+ console.log(table(data));
847
+
848
+ if (compatible.length > 15) {
849
+ console.log(chalk.gray(`\n... and ${compatible.length - 15} more compatible models`));
850
+ }
851
+
852
+ displayCompatibleModelsSummary(compatible.length);
853
+ }
854
+
855
+ function displayCompatibleModelsSummary(count) {
856
+ console.log('\n' + chalk.bgMagenta.white.bold(' COMPATIBLE MODELS '));
857
+ console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(40)));
858
+ console.log(chalk.hex('#a259ff')('│') + ` Total compatible models: ${chalk.green.bold(count)}`);
859
+ console.log(chalk.hex('#a259ff')('╰'));
860
+ }
861
+
862
+ function displayMarginalModels(marginal) {
863
+ if (marginal.length === 0) return;
864
+
865
+ console.log('\n' + chalk.yellow.bold('Marginal Performance (Score 60-74)'));
866
+
867
+ const data = [
868
+ [
869
+ chalk.bgYellow.white.bold(' Model '),
870
+ chalk.bgYellow.white.bold(' Size '),
871
+ chalk.bgYellow.white.bold(' Score '),
872
+ chalk.bgYellow.white.bold(' RAM '),
873
+ chalk.bgYellow.white.bold(' VRAM '),
874
+ chalk.bgYellow.white.bold(' Issue ')
875
+ ]
876
+ ];
877
+
878
+ marginal.slice(0, 6).forEach(model => {
879
+ const mainIssue = model.issues?.[0] || 'Performance limitations';
880
+ const scoreColor = getScoreColor(model.score || 0);
881
+ const scoreDisplay = scoreColor(`${model.score || 0}/100`);
882
+
883
+ const ramReq = model.requirements?.ram || 1;
884
+ const vramReq = model.requirements?.vram || 0;
885
+
886
+ const truncatedIssue = mainIssue.length > 30 ? mainIssue.substring(0, 27) + '...' : mainIssue;
887
+
888
+ const row = [
889
+ model.name,
890
+ formatSize(model.size || 'Unknown'),
891
+ scoreDisplay,
892
+ `${ramReq}GB`,
893
+ `${vramReq}GB`,
894
+ truncatedIssue
895
+ ];
896
+ data.push(row);
897
+ });
898
+
899
+ console.log(table(data));
900
+
901
+ if (marginal.length > 6) {
902
+ console.log(chalk.gray(`\n... and ${marginal.length - 6} more marginal models`));
903
+ }
904
+ }
905
+
906
+
907
+ function displayStructuredRecommendations(recommendations) {
908
+ if (!recommendations) return;
909
+
910
+ if (Array.isArray(recommendations)) {
911
+ displayLegacyRecommendations(recommendations);
912
+ return;
913
+ }
914
+
915
+ console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
916
+ console.log(chalk.cyan('╭' + '─'.repeat(50)));
917
+
918
+ if (recommendations.general && recommendations.general.length > 0) {
919
+ console.log(chalk.cyan('│') + ` ${chalk.bold.white('General Recommendations:')}`);
920
+ recommendations.general.slice(0, 4).forEach((rec, index) => {
921
+ console.log(chalk.cyan('│') + ` ${index + 1}. ${chalk.white(rec)}`);
922
+ });
923
+ console.log(chalk.cyan('│'));
924
+ }
925
+
926
+ if (recommendations.installedModels && recommendations.installedModels.length > 0) {
927
+ console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
928
+ recommendations.installedModels.forEach(rec => {
929
+ console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
930
+ });
931
+ console.log(chalk.cyan('│'));
932
+ }
933
+
934
+ if (recommendations.cloudSuggestions && recommendations.cloudSuggestions.length > 0) {
935
+ console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Recommended from Ollama Cloud:')}`);
936
+ recommendations.cloudSuggestions.forEach(rec => {
937
+ if (rec.includes('ollama pull')) {
938
+ console.log(chalk.cyan('│') + ` ${chalk.cyan.bold(rec)}`);
939
+ } else {
940
+ console.log(chalk.cyan('│') + ` ${chalk.blue(rec)}`);
941
+ }
942
+ });
943
+ console.log(chalk.cyan('│'));
944
+ }
945
+
946
+ if (recommendations.quickCommands && recommendations.quickCommands.length > 0) {
947
+ console.log(chalk.cyan('│') + ` ${chalk.bold.yellow('⚡ Quick Commands:')}`);
948
+ const uniqueCommands = [...new Set(recommendations.quickCommands)];
949
+ uniqueCommands.slice(0, 3).forEach(cmd => {
950
+ console.log(chalk.cyan('│') + ` > ${chalk.yellow.bold(cmd)}`);
951
+ });
952
+ }
953
+
954
+ console.log(chalk.cyan('╰'));
955
+ }
956
+
957
+ function displayLegacyRecommendations(recommendations) {
958
+ if (!recommendations || recommendations.length === 0) return;
959
+
960
+ const generalRecs = [];
961
+ const ollamaFoundRecs = [];
962
+ const quickInstallRecs = [];
963
+
964
+ recommendations.forEach(rec => {
965
+ if (rec.includes('Score:')) {
966
+ ollamaFoundRecs.push(rec);
967
+ } else if (rec.includes('ollama pull')) {
968
+ quickInstallRecs.push(rec);
969
+ } else if (rec.includes('ollama run')) {
970
+ quickInstallRecs.push(rec);
971
+ } else {
972
+ generalRecs.push(rec);
973
+ }
974
+ });
975
+
976
+ console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
977
+ console.log(chalk.cyan('╭' + '─'.repeat(40)));
978
+
979
+ generalRecs.slice(0, 8).forEach((rec, index) => {
980
+ const number = chalk.green.bold(`${index + 1}.`);
981
+ console.log(chalk.cyan('│') + ` ${number} ${chalk.white(rec)}`);
982
+ });
983
+
984
+ if (ollamaFoundRecs.length > 0) {
985
+ console.log(chalk.cyan('│'));
986
+ console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
987
+ ollamaFoundRecs.forEach(rec => {
988
+ console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
989
+ });
990
+ }
991
+
992
+ if (quickInstallRecs.length > 0) {
993
+ console.log(chalk.cyan('│'));
994
+ console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Quick Commands:')}`);
995
+ quickInstallRecs.slice(0, 3).forEach(cmd => {
996
+ console.log(chalk.cyan('│') + ` > ${chalk.cyan.bold(cmd)}`);
997
+ });
998
+ }
999
+
1000
+ console.log(chalk.cyan('╰'));
1001
+ }
1002
+
1003
+ function displayIntelligentRecommendations(intelligentData) {
1004
+ if (!intelligentData || !intelligentData.summary) return;
1005
+
1006
+ const { summary, recommendations } = intelligentData;
1007
+ const tier = summary.hardware_tier.replace('_', ' ').toUpperCase();
1008
+ const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
1009
+
1010
+ console.log('\n' + chalk.bgRed.white.bold(' INTELLIGENT RECOMMENDATIONS BY CATEGORY '));
1011
+ console.log(chalk.red('╭' + '─'.repeat(65)));
1012
+ console.log(chalk.red('│') + ` Hardware Tier: ${tierColor.bold(tier)} | Models Analyzed: ${chalk.cyan.bold(intelligentData.totalModelsAnalyzed)}`);
1013
+ console.log(chalk.red('│'));
1014
+
1015
+ // Mostrar mejor modelo general
1016
+ if (summary.best_overall) {
1017
+ const best = summary.best_overall;
1018
+ console.log(chalk.red('│') + ` ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
1019
+ console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(best.command)}`);
1020
+ console.log(chalk.red('│') + ` Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
1021
+ console.log(chalk.red('│'));
1022
+ }
1023
+
1024
+ // Mostrar por categorías
1025
+ const categories = {
1026
+ coding: 'Coding',
1027
+ talking: 'Chat',
1028
+ reading: 'Reading',
1029
+ reasoning: 'Reasoning',
1030
+ multimodal: 'Multimodal',
1031
+ creative: 'Creative',
1032
+ general: 'General'
1033
+ };
1034
+
1035
+ Object.entries(summary.by_category).forEach(([category, model]) => {
1036
+ const icon = categories[category] || 'Other';
1037
+ const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
1038
+ const scoreColor = getScoreColor(model.score);
1039
+
1040
+ console.log(chalk.red('│') + ` ${chalk.bold.white(categoryName)} (${icon}):`);
1041
+ console.log(chalk.red('│') + ` ${chalk.green(model.name)} (${model.size})`);
1042
+ console.log(chalk.red('│') + ` Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
1043
+ console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(model.command)}`);
1044
+ console.log(chalk.red('│'));
1045
+ });
1046
+
1047
+ console.log(chalk.red('╰'));
1048
+ }
1049
+
1050
+ function displayModelsStats(originalCount, filteredCount, options) {
1051
+ console.log('\n' + chalk.bgGreen.white.bold(' DATABASE STATS '));
1052
+ console.log(chalk.green('╭' + '─'.repeat(60)));
1053
+ console.log(chalk.green('│') + ` Total models in database: ${chalk.cyan.bold(originalCount)}`);
1054
+ console.log(chalk.green('│') + ` After filters: ${chalk.yellow.bold(filteredCount)}`);
1055
+
1056
+ if (options.category) {
1057
+ console.log(chalk.green('│') + ` Category filter: ${chalk.magenta.bold(options.category)}`);
1058
+ }
1059
+ if (options.size) {
1060
+ console.log(chalk.green('│') + ` Size filter: ${chalk.magenta.bold(options.size)}`);
1061
+ }
1062
+ if (options.popular) {
1063
+ console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Popular models only (>100k pulls)')}`);
1064
+ }
1065
+ if (options.recent) {
1066
+ console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Recent models only')}`);
1067
+ }
1068
+
1069
+ console.log(chalk.green('╰'));
1070
+ }
1071
+
1072
+ async function displayTopRecommended(models, categoryFilter) {
1073
+ console.log('\n' + chalk.bgGreen.white.bold(' TOP 3 RECOMMENDED FOR YOUR HARDWARE '));
1074
+
1075
+ try {
1076
+ const DeterministicModelSelector = require('../src/models/deterministic-selector.js');
1077
+ const selector = new DeterministicModelSelector();
1078
+
1079
+ // Use deterministic selector to get top 3 for this category
1080
+ const result = await selector.selectModels(categoryFilter || 'general', {
1081
+ topN: 3,
1082
+ enableProbe: false,
1083
+ silent: true
1084
+ });
1085
+
1086
+ const top3 = result.candidates.map(candidate => selector.mapCandidateToLegacyFormat(candidate));
1087
+
1088
+ if (top3.length === 0) {
1089
+ console.log(chalk.green('│') + chalk.yellow(' No models found for this category with current hardware'));
1090
+ console.log(chalk.green('╰' + '─'.repeat(65)));
1091
+ return;
1092
+ }
1093
+
1094
+ top3.forEach((model, index) => {
1095
+ const rankEmoji = ['🥇', '🥈', '🥉'][index];
1096
+ const categoryColor = getCategoryColor(model.category || categoryFilter || 'general');
1097
+ const scoreColor = model.categoryScore >= 80 ? chalk.green.bold :
1098
+ model.categoryScore >= 60 ? chalk.yellow : chalk.red;
1099
+ const size = model.size ? `${model.size}B` : 'Unknown';
1100
+
1101
+ console.log(chalk.green('│'));
1102
+ console.log(chalk.green('│') + ` ${rankEmoji} ${chalk.cyan.bold(model.model_identifier)}`);
1103
+ console.log(chalk.green('│') + ` Size: ${chalk.green(size)} | Score: ${scoreColor(Math.round(model.categoryScore) + '%')} | Category: ${categoryColor(model.category || 'general')}`);
1104
+ console.log(chalk.green('│') + ` Command: ${chalk.yellow.bold('ollama pull ' + model.model_identifier)}`);
1105
+ console.log(chalk.green('│') + ` ${chalk.gray(`Hardware: ${Math.round(model.hardwareScore)}/100, Quality: ${Math.round(model.specializationScore)}/100, Speed: ${Math.round(model.efficiencyScore)}/100`)}`);
1106
+ });
1107
+
1108
+ console.log(chalk.green('╰' + '─'.repeat(65)));
1109
+
1110
+ } catch (error) {
1111
+ console.log(chalk.green('│') + chalk.red(' Error calculating intelligent recommendations: ' + error.message));
1112
+ console.log(chalk.green('╰' + '─'.repeat(65)));
1113
+ }
1114
+ }
1115
+
1116
+ async function displayCompactModelsList(models, categoryFilter = null) {
1117
+ // Si hay modelos con compatibilityScore, mostrar top 3 recomendados primero
1118
+ const showCompatibility = models.length > 0 && models[0].compatibilityScore !== undefined;
1119
+
1120
+ if (showCompatibility && categoryFilter) {
1121
+ await displayTopRecommended(models, categoryFilter);
1122
+ }
1123
+
1124
+ console.log('\n' + chalk.bgBlue.white.bold(' 📋 MODELS LIST '));
1125
+
1126
+ const headers = [
1127
+ chalk.bgBlue.white.bold(' # '),
1128
+ chalk.bgBlue.white.bold(' Model '),
1129
+ chalk.bgBlue.white.bold(' Size ')
1130
+ ];
1131
+
1132
+ if (showCompatibility) {
1133
+ headers.push(chalk.bgBlue.white.bold(' Score '));
1134
+ }
1135
+
1136
+ headers.push(
1137
+ chalk.bgBlue.white.bold(' Context '),
1138
+ chalk.bgBlue.white.bold(' Input '),
1139
+ chalk.bgBlue.white.bold(' Category ')
1140
+ );
1141
+
1142
+ const data = [headers];
1143
+
1144
+ let rowIndex = 0;
1145
+ models.forEach((model) => {
1146
+ const category = model.category || 'general';
1147
+ const categoryColor = getCategoryColor(category);
1148
+
1149
+ // Context length
1150
+ const contextLength = model.context_length || 'Unknown';
1151
+
1152
+ // Input types
1153
+ const inputTypes = (model.input_types && model.input_types.length > 0) ?
1154
+ model.input_types.slice(0, 2).join(',') : 'text';
1155
+
1156
+ // Si el modelo tiene tags/variantes, crear una fila por cada tag
1157
+ if (model.tags && model.tags.length > 0) {
1158
+ model.tags.forEach((tag) => {
1159
+ rowIndex++;
1160
+
1161
+ // Extraer el tamaño del tag si está presente
1162
+ const tagSize = extractSizeFromIdentifier(tag) ||
1163
+ model.main_size ||
1164
+ (model.model_sizes && model.model_sizes[0]) ||
1165
+ 'Unknown';
1166
+
1167
+ const row = [
1168
+ chalk.gray(`${rowIndex}`),
1169
+ tag, // Mostrar el tag completo como nombre del modelo
1170
+ chalk.green(tagSize)
1171
+ ];
1172
+
1173
+ // Agregar score si está disponible
1174
+ if (showCompatibility) {
1175
+ const score = model.compatibilityScore || 0;
1176
+ const scoreColor = score >= 80 ? chalk.green.bold :
1177
+ score >= 60 ? chalk.yellow : chalk.red;
1178
+ row.push(scoreColor(`${score}%`));
1179
+ }
1180
+
1181
+ row.push(
1182
+ chalk.blue(contextLength),
1183
+ chalk.magenta(inputTypes),
1184
+ categoryColor(category)
1185
+ );
1186
+
1187
+ data.push(row);
1188
+ });
1189
+ } else {
1190
+ // Si no tiene tags, mostrar el modelo base
1191
+ rowIndex++;
1192
+
1193
+ const mainSize = model.main_size ||
1194
+ (model.model_sizes && model.model_sizes[0]) ||
1195
+ extractSizeFromIdentifier(model.model_identifier) ||
1196
+ 'Unknown';
1197
+
1198
+ const row = [
1199
+ chalk.gray(`${rowIndex}`),
1200
+ model.model_name || model.model_identifier || 'Unknown',
1201
+ chalk.green(mainSize)
1202
+ ];
1203
+
1204
+ // Agregar score si está disponible
1205
+ if (showCompatibility) {
1206
+ const score = model.compatibilityScore || 0;
1207
+ const scoreColor = score >= 80 ? chalk.green.bold :
1208
+ score >= 60 ? chalk.yellow : chalk.red;
1209
+ row.push(scoreColor(`${score}%`));
1210
+ }
1211
+
1212
+ row.push(
1213
+ chalk.blue(contextLength),
1214
+ chalk.magenta(inputTypes),
1215
+ categoryColor(category)
1216
+ );
1217
+
1218
+ data.push(row);
1219
+ }
1220
+ });
1221
+
1222
+ console.log(table(data));
1223
+ }
1224
+
1225
+ function extractSizeFromIdentifier(identifier) {
1226
+ const sizeMatch = identifier.match(/(\d+\.?\d*[bg])/i);
1227
+ return sizeMatch ? sizeMatch[1].toLowerCase() : null;
1228
+ }
1229
+
1230
+ function displayFullModelsList(models) {
1231
+ console.log('\n' + chalk.bgBlue.white.bold(' 📋 DETAILED MODELS LIST '));
1232
+
1233
+ models.forEach((model, index) => {
1234
+ console.log(`\n${chalk.cyan.bold(`${index + 1}. ${model.model_name}`)}`);
1235
+ console.log(` ${chalk.gray('Identifier:')} ${chalk.yellow(model.model_identifier)}`);
1236
+ console.log(` ${chalk.gray('Size:')} ${chalk.green(model.main_size || 'Unknown')}`);
1237
+ console.log(` ${chalk.gray('Context:')} ${chalk.blue(model.context_length || 'Unknown')}`);
1238
+ console.log(` ${chalk.gray('Input types:')} ${chalk.magenta((model.input_types || ['text']).join(', '))}`);
1239
+ console.log(` ${chalk.gray('Category:')} ${getCategoryColor(model.category || 'general')(model.category || 'general')}`);
1240
+ console.log(` ${chalk.gray('Pulls:')} ${chalk.green((model.pulls || 0).toLocaleString())}`);
1241
+ console.log(` ${chalk.gray('Description:')} ${model.description || model.detailed_description || 'No description'}`);
1242
+
1243
+ if (model.use_cases && model.use_cases.length > 0) {
1244
+ console.log(` ${chalk.gray('Use cases:')} ${model.use_cases.map(uc => chalk.magenta(uc)).join(', ')}`);
1245
+ }
1246
+
1247
+ if (model.tags && model.tags.length > 0) {
1248
+ console.log(` ${chalk.gray(`Available variants (${model.tags.length}):`)} `);
1249
+ // Mostrar las primeras 10 variantes, agrupadas de 5 por línea
1250
+ const tagsToShow = model.tags.slice(0, 15);
1251
+ for (let i = 0; i < tagsToShow.length; i += 5) {
1252
+ const batch = tagsToShow.slice(i, i + 5);
1253
+ console.log(` ${batch.map(tag => chalk.blue(tag)).join(', ')}`);
1254
+ }
1255
+ if (model.tags.length > 15) {
1256
+ console.log(` ${chalk.gray(`... and ${model.tags.length - 15} more variants`)}`);
1257
+ }
1258
+ }
1259
+
1260
+ if (model.quantizations && model.quantizations.length > 0) {
1261
+ console.log(` ${chalk.gray('Quantizations found:')} ${model.quantizations.map(q => chalk.green(q)).join(', ')}`);
1262
+ }
1263
+
1264
+ console.log(` ${chalk.gray('Base command:')} ${chalk.cyan.bold(`ollama pull ${model.model_identifier}`)}`);
1265
+ console.log(` ${chalk.gray('Example variant:')} ${chalk.cyan.bold(`ollama pull ${model.tags && model.tags.length > 0 ? model.tags[0] : model.model_identifier}`)}`);
1266
+ console.log(` ${chalk.gray('Updated:')} ${model.last_updated || 'Unknown'}`);
1267
+ });
1268
+ }
1269
+
1270
+ function getCategoryColor(category) {
1271
+ const colors = {
1272
+ coding: chalk.blue,
1273
+ talking: chalk.green,
1274
+ reading: chalk.yellow,
1275
+ reasoning: chalk.red,
1276
+ multimodal: chalk.magenta,
1277
+ creative: chalk.cyan,
1278
+ general: chalk.gray,
1279
+ chat: chalk.green,
1280
+ embeddings: chalk.blue
1281
+ };
1282
+
1283
+ return colors[category] || chalk.gray;
1284
+ }
1285
+
1286
+ function displaySampleCommands(topModels) {
1287
+ console.log('\n' + chalk.bgYellow.black.bold(' ⚡ SAMPLE COMMANDS '));
1288
+ console.log(chalk.yellow('╭' + '─'.repeat(60)));
1289
+ console.log(chalk.yellow('│') + ` ${chalk.bold.white('Try these popular models:')}`);
1290
+
1291
+ topModels.forEach((model, index) => {
1292
+ const command = `ollama pull ${model.model_identifier}`;
1293
+ console.log(chalk.yellow('│') + ` ${index + 1}. ${chalk.cyan.bold(command)}`);
1294
+ });
1295
+
1296
+ console.log(chalk.yellow('│'));
1297
+ console.log(chalk.yellow('│') + ` ${chalk.bold.white('Browse models by category:')}`);
1298
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category coding')} ${chalk.gray('(Programming & development)')}`);
1299
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reasoning')} ${chalk.gray('(Logic & math problems)')}`);
1300
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category talking')} ${chalk.gray('(Chat & conversations)')}`);
1301
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reading')} ${chalk.gray('(Text analysis & comprehension)')}`);
1302
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category multimodal')} ${chalk.gray('(Image & vision tasks)')}`);
1303
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category creative')} ${chalk.gray('(Creative writing & stories)')}`);
1304
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category general')} ${chalk.gray('(General purpose tasks)')}`);
1305
+ console.log(chalk.yellow('│'));
1306
+ console.log(chalk.yellow('│') + ` ${chalk.bold.white('AI-powered selection:')}`);
1307
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-check --category coding --top 12')} ${chalk.gray('(AI meta-evaluation)')}`);
1308
+ console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-run')} ${chalk.gray('(Smart model selection & launch)')}`);
1309
+ console.log(chalk.yellow('│'));
1310
+ console.log(chalk.yellow('│') + ` ${chalk.bold.white('Additional options:')}`);
1311
+ console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --popular --limit 10')}`);
1312
+ console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --json > models.json')}`);
1313
+ console.log(chalk.yellow('╰'));
1314
+ }
1315
+
1316
+ async function checkIfModelInstalled(model, ollamaInfo) {
1317
+ try {
1318
+ // Si Ollama no está disponible, no hay modelos instalados
1319
+ if (!ollamaInfo || !ollamaInfo.available) {
1320
+ return false;
1321
+ }
1322
+
1323
+ // Ejecutar 'ollama list' para obtener modelos instalados
1324
+ const installedModels = await new Promise((resolve, reject) => {
1325
+ try {
1326
+ const ollama = spawn('ollama', ['list'], { stdio: 'pipe' });
1327
+ let output = '';
1328
+
1329
+ ollama.stdout.on('data', (data) => {
1330
+ output += data.toString();
1331
+ });
1332
+
1333
+ ollama.on('close', (code) => {
1334
+ if (code === 0) {
1335
+ resolve(output);
1336
+ } else {
1337
+ resolve(''); // Si falla, asumimos que no hay modelos
1338
+ }
1339
+ });
1340
+
1341
+ ollama.on('error', (err) => {
1342
+ // Handle ENOENT and other spawn errors gracefully
1343
+ if (err.code === 'ENOENT') {
1344
+ resolve(''); // Ollama not found, no models installed
1345
+ } else {
1346
+ resolve(''); // Any other error, assume no models
1347
+ }
1348
+ });
1349
+ } catch (spawnError) {
1350
+ // Handle synchronous spawn errors
1351
+ resolve(''); // If spawn itself fails, no models available
1352
+ }
1353
+ });
1354
+
1355
+ // Parsear la salida de 'ollama list'
1356
+ const lines = installedModels.split('\n');
1357
+ const modelNames = [];
1358
+
1359
+ for (let i = 1; i < lines.length; i++) { // Skip header
1360
+ const line = lines[i].trim();
1361
+ if (line) {
1362
+ const parts = line.split(/\s+/);
1363
+ if (parts.length > 0) {
1364
+ modelNames.push(parts[0].toLowerCase());
1365
+ }
1366
+ }
1367
+ }
1368
+
1369
+ // Generar el comando de instalación esperado para el modelo
1370
+ const expectedCommand = getOllamaInstallCommand(model);
1371
+ if (!expectedCommand) return false;
1372
+
1373
+ // Extraer el nombre del modelo del comando (ej: "ollama pull mistral:7b" -> "mistral:7b")
1374
+ const modelNameMatch = expectedCommand.match(/ollama pull (.+)/);
1375
+ if (!modelNameMatch) return false;
1376
+
1377
+ const expectedModelName = modelNameMatch[1].toLowerCase();
1378
+
1379
+ // Verificar si el modelo está en la lista de instalados
1380
+ return modelNames.some(installedName =>
1381
+ installedName === expectedModelName ||
1382
+ installedName.startsWith(expectedModelName.split(':')[0])
1383
+ );
1384
+
1385
+ } catch (error) {
1386
+ // Si hay algún error, asumimos que no está instalado
1387
+ return false;
1388
+ }
1389
+ }
1390
+
1391
+ function displaySimplifiedSystemInfo(hardware) {
1392
+ console.log(chalk.cyan.bold('\nSYSTEM SUMMARY'));
1393
+ console.log(chalk.gray('─'.repeat(50)));
1394
+
1395
+ const cpuInfo = `${hardware.cpu.brand} (${hardware.cpu.cores} cores)`;
1396
+ const memInfo = `${hardware.memory.total}GB RAM`;
1397
+ const gpuInfo = hardware.gpu.model || 'Integrated GPU';
1398
+
1399
+ console.log(`CPU: ${chalk.white(cpuInfo)}`);
1400
+ console.log(`Memory: ${chalk.white(memInfo)}`);
1401
+ console.log(`GPU: ${chalk.white(gpuInfo)}`);
1402
+ console.log(`Architecture: ${chalk.white(hardware.cpu.architecture)}`);
1403
+
1404
+ const tier = getHardwareTierForDisplay(hardware);
1405
+ const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
1406
+ console.log(`Hardware Tier: ${tierColor.bold(tier)}`);
1407
+ }
1408
+
1409
+ async function displayModelRecommendations(analysis, hardware, useCase = 'general', limit = 1) {
1410
+ const title = limit === 1 ? 'RECOMMENDED MODEL' : `TOP ${limit} COMPATIBLE MODELS`;
1411
+ console.log(chalk.green.bold(`\n${title}`));
1412
+ console.log(chalk.gray('─'.repeat(50)));
1413
+
1414
+ // Find the best models from compatible models considering use case
1415
+ let selectedModels = [];
1416
+ let reason = '';
1417
+
1418
+ if (analysis.compatible && analysis.compatible.length > 0) {
1419
+ // First, try to find models that match the use case
1420
+ let candidateModels = analysis.compatible;
1421
+
1422
+
1423
+ // Apply intelligent filtering based on use case
1424
+ if (useCase && useCase !== 'general') {
1425
+ // Specific use case filtering
1426
+ const useCaseModels = candidateModels.filter(model => {
1427
+ const specialization = model.specialization?.toLowerCase();
1428
+ const category = model.category?.toLowerCase();
1429
+
1430
+ const lowerUseCase = useCase.toLowerCase();
1431
+ switch (lowerUseCase) {
1432
+ case 'coding':
1433
+ case 'code':
1434
+ return model.primary_category === 'coding' ||
1435
+ model.categories?.includes('coding') ||
1436
+ specialization === 'code' || category === 'coding' ||
1437
+ model.name.toLowerCase().includes('code') ||
1438
+ model.name.toLowerCase().includes('coder');
1439
+
1440
+ case 'creative':
1441
+ case 'writing':
1442
+ return model.primary_category === 'creative' ||
1443
+ model.categories?.includes('creative') ||
1444
+ category === 'creative' || specialization === 'creative' ||
1445
+ model.name.toLowerCase().includes('dolphin') ||
1446
+ model.name.toLowerCase().includes('wizard') ||
1447
+ model.name.toLowerCase().includes('uncensored');
1448
+
1449
+ case 'chat':
1450
+ case 'conversation':
1451
+ case 'talking':
1452
+ // Prefer chat models, exclude coding models
1453
+ // First, hard exclude coding models
1454
+ if (model.primary_category === 'coding' ||
1455
+ specialization === 'code' ||
1456
+ model.name.toLowerCase().includes('code') ||
1457
+ model.name.toLowerCase().includes('coder')) {
1458
+ return false;
1459
+ }
1460
+ // Then include chat models (coding exclusion above takes precedence)
1461
+ return model.primary_category === 'chat' ||
1462
+ model.categories?.includes('chat') ||
1463
+ category === 'talking' || specialization === 'chat' ||
1464
+ (model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
1465
+ (model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
1466
+ (model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
1467
+ (!model.name.toLowerCase().includes('llava') &&
1468
+ (specialization === 'general' || category === 'medium'));
1469
+
1470
+ case 'multimodal':
1471
+ case 'vision':
1472
+ return model.primary_category === 'multimodal' ||
1473
+ model.categories?.includes('multimodal') ||
1474
+ category === 'multimodal' ||
1475
+ model.name.toLowerCase().includes('llava') ||
1476
+ model.name.toLowerCase().includes('vision');
1477
+
1478
+ case 'embeddings':
1479
+ case 'embedings': // typo tolerance
1480
+ case 'embedding':
1481
+ case 'embeding': // typo tolerance
1482
+ return model.primary_category === 'embeddings' ||
1483
+ model.categories?.includes('embeddings') ||
1484
+ category === 'embeddings' ||
1485
+ model.name.toLowerCase().includes('embed') ||
1486
+ model.name.toLowerCase().includes('bge');
1487
+
1488
+ case 'reasoning':
1489
+ case 'reason':
1490
+ return model.primary_category === 'reasoning' ||
1491
+ model.categories?.includes('reasoning') ||
1492
+ category === 'reasoning' ||
1493
+ model.name.toLowerCase().includes('deepseek-r1') ||
1494
+ model.name.toLowerCase().includes('reasoning');
1495
+
1496
+ default:
1497
+ // Check for partial matches with typo tolerance
1498
+ if (lowerUseCase.includes('embed')) {
1499
+ return model.primary_category === 'embeddings' ||
1500
+ model.categories?.includes('embeddings') ||
1501
+ category === 'embeddings' ||
1502
+ model.name.toLowerCase().includes('embed');
1503
+ }
1504
+ if (lowerUseCase.includes('code')) {
1505
+ return model.primary_category === 'coding' ||
1506
+ model.categories?.includes('coding');
1507
+ }
1508
+ if (lowerUseCase.includes('creat')) {
1509
+ return model.primary_category === 'creative' ||
1510
+ model.categories?.includes('creative');
1511
+ }
1512
+ return true;
1513
+ }
1514
+ });
1515
+
1516
+ // If we found use case specific models, use those, otherwise search Ollama database
1517
+ if (useCaseModels.length > 0) {
1518
+ candidateModels = useCaseModels;
1519
+ reason = `Best ${useCase} model for your hardware`;
1520
+ } else {
1521
+ // Search directly in Ollama database for use case specific models
1522
+ const ollamaModels = searchOllamaModelsForUseCase(useCase, hardware);
1523
+ if (ollamaModels.length > 0) {
1524
+ candidateModels = ollamaModels;
1525
+ reason = `Best ${useCase} model from Ollama database`;
1526
+ }
1527
+ }
1528
+ } else {
1529
+ // No specific use case - apply intelligent general filtering
1530
+ // First, infer categories for static models that don't have them
1531
+ const modelsWithCategories = candidateModels.map(model => {
1532
+ if (!model.primary_category) {
1533
+ const modelName = model.name.toLowerCase();
1534
+ let inferredCategory = 'general';
1535
+
1536
+ if (modelName.includes('code') || modelName.includes('coder')) {
1537
+ inferredCategory = 'coding';
1538
+ } else if (modelName.includes('llava') || modelName.includes('vision')) {
1539
+ inferredCategory = 'multimodal';
1540
+ } else if (modelName.includes('embed')) {
1541
+ inferredCategory = 'embeddings';
1542
+ } else if (modelName.includes('llama') || modelName.includes('mistral') ||
1543
+ modelName.includes('qwen') || modelName.includes('gemma')) {
1544
+ inferredCategory = 'chat';
1545
+ } else if (modelName.includes('phi') && modelName.includes('mini')) {
1546
+ inferredCategory = 'reasoning';
1547
+ }
1548
+
1549
+ return { ...model, primary_category: inferredCategory };
1550
+ }
1551
+ return model;
1552
+ });
1553
+
1554
+ // Prefer versatile models, exclude highly specialized ones
1555
+ const generalModels = modelsWithCategories.filter(model => {
1556
+ // Exclude very specialized models
1557
+ if (model.primary_category === 'embeddings' ||
1558
+ model.primary_category === 'safety' ||
1559
+ model.primary_category === 'multimodal') {
1560
+ return false;
1561
+ }
1562
+
1563
+ // Include chat, coding, reasoning, creative, and general models
1564
+ return model.primary_category === 'chat' ||
1565
+ model.primary_category === 'coding' ||
1566
+ model.primary_category === 'reasoning' ||
1567
+ model.primary_category === 'creative' ||
1568
+ model.primary_category === 'general' ||
1569
+ model.specialization === 'general' ||
1570
+ model.category === 'medium' ||
1571
+ model.category === 'small';
1572
+ });
1573
+
1574
+ if (generalModels.length > 0) {
1575
+ // Re-score general models with category bonus
1576
+ const scoredModels = generalModels.map(model => {
1577
+ let adjustedScore = model.score || 0;
1578
+
1579
+ // Apply category bonuses for general use
1580
+ if (model.primary_category === 'chat') {
1581
+ adjustedScore += 5; // Chat models are great for general use
1582
+ } else if (model.primary_category === 'coding') {
1583
+ adjustedScore += 3; // Coding models are versatile
1584
+ } else if (model.primary_category === 'reasoning') {
1585
+ adjustedScore += 4; // Reasoning models are smart
1586
+ } else if (model.primary_category === 'creative') {
1587
+ adjustedScore += 2; // Creative models are fun
1588
+ }
1589
+
1590
+ return { ...model, adjustedScore };
1591
+ });
1592
+
1593
+ candidateModels = scoredModels.sort((a, b) => b.adjustedScore - a.adjustedScore);
1594
+ reason = 'Best general-purpose model for your hardware';
1595
+ }
1596
+ }
1597
+
1598
+ // Filter out unreasonably large models before final selection
1599
+ const reasonableSizedModels = candidateModels.filter(model => {
1600
+ const realSize = getRealSizeFromOllamaCache(model);
1601
+ const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
1602
+
1603
+ // For hardware with 24GB RAM, models >25GB are not practical
1604
+ const maxReasonableSize = hardware.memory.total > 32 ? 50 : 25;
1605
+ return sizeGB === 0 || sizeGB <= maxReasonableSize; // 0 means unknown/fallback
1606
+ });
1607
+
1608
+ // Sort by score and get the top models (use adjustedScore if available)
1609
+ const sortedModels = reasonableSizedModels.sort((a, b) =>
1610
+ (b.adjustedScore || b.score || 0) - (a.adjustedScore || a.score || 0)
1611
+ );
1612
+ selectedModels = sortedModels.slice(0, limit);
1613
+
1614
+ if (!reason) {
1615
+ reason = 'Highest compatibility score for your hardware';
1616
+ }
1617
+ } else if (analysis.marginal && analysis.marginal.length > 0) {
1618
+ let marginalCandidates = analysis.marginal;
1619
+
1620
+ // Apply same use case filtering to marginal models
1621
+ if (useCase && useCase !== 'general') {
1622
+ const useCaseMarginal = marginalCandidates.filter(model => {
1623
+ const specialization = model.specialization?.toLowerCase();
1624
+ const category = model.category?.toLowerCase();
1625
+
1626
+ const lowerUseCase = useCase.toLowerCase();
1627
+ switch (lowerUseCase) {
1628
+ case 'coding':
1629
+ case 'code':
1630
+ return model.primary_category === 'coding' ||
1631
+ model.categories?.includes('coding') ||
1632
+ specialization === 'code' || category === 'coding' ||
1633
+ model.name.toLowerCase().includes('code') ||
1634
+ model.name.toLowerCase().includes('coder');
1635
+
1636
+ case 'creative':
1637
+ case 'writing':
1638
+ return model.primary_category === 'creative' ||
1639
+ model.categories?.includes('creative') ||
1640
+ category === 'creative' || specialization === 'creative' ||
1641
+ model.name.toLowerCase().includes('dolphin') ||
1642
+ model.name.toLowerCase().includes('wizard') ||
1643
+ model.name.toLowerCase().includes('uncensored');
1644
+
1645
+ case 'chat':
1646
+ case 'conversation':
1647
+ case 'talking':
1648
+ // First, hard exclude coding models
1649
+ if (model.primary_category === 'coding' ||
1650
+ specialization === 'code' ||
1651
+ model.name.toLowerCase().includes('code') ||
1652
+ model.name.toLowerCase().includes('coder')) {
1653
+ return false;
1654
+ }
1655
+ // Then include chat models
1656
+ return model.primary_category === 'chat' ||
1657
+ model.categories?.includes('chat') ||
1658
+ category === 'talking' || specialization === 'chat' ||
1659
+ (model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
1660
+ (model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
1661
+ (model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
1662
+ (!model.name.toLowerCase().includes('llava') &&
1663
+ (specialization === 'general' || category === 'medium'));
1664
+
1665
+ case 'multimodal':
1666
+ case 'vision':
1667
+ return model.primary_category === 'multimodal' ||
1668
+ model.categories?.includes('multimodal') ||
1669
+ category === 'multimodal' ||
1670
+ model.name.toLowerCase().includes('llava') ||
1671
+ model.name.toLowerCase().includes('vision');
1672
+
1673
+ case 'embeddings':
1674
+ case 'embedings': // typo tolerance
1675
+ case 'embedding':
1676
+ case 'embeding': // typo tolerance
1677
+ return model.primary_category === 'embeddings' ||
1678
+ model.categories?.includes('embeddings') ||
1679
+ category === 'embeddings' ||
1680
+ model.name.toLowerCase().includes('embed') ||
1681
+ model.name.toLowerCase().includes('bge');
1682
+
1683
+ case 'reasoning':
1684
+ case 'reason':
1685
+ return model.primary_category === 'reasoning' ||
1686
+ model.categories?.includes('reasoning') ||
1687
+ category === 'reasoning' ||
1688
+ model.name.toLowerCase().includes('deepseek-r1') ||
1689
+ model.name.toLowerCase().includes('reasoning');
1690
+
1691
+ default:
1692
+ // Check for partial matches with typo tolerance
1693
+ if (lowerUseCase.includes('embed')) {
1694
+ return model.primary_category === 'embeddings' ||
1695
+ model.categories?.includes('embeddings') ||
1696
+ category === 'embeddings' ||
1697
+ model.name.toLowerCase().includes('embed');
1698
+ }
1699
+ if (lowerUseCase.includes('code')) {
1700
+ return model.primary_category === 'coding' ||
1701
+ model.categories?.includes('coding') ||
1702
+ category === 'coding' ||
1703
+ model.name.toLowerCase().includes('code');
1704
+ }
1705
+ if (lowerUseCase.includes('creat')) {
1706
+ return model.primary_category === 'creative' ||
1707
+ model.categories?.includes('creative') ||
1708
+ category === 'creative';
1709
+ }
1710
+ if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) {
1711
+ return model.primary_category === 'chat' ||
1712
+ model.categories?.includes('chat') ||
1713
+ category === 'chat';
1714
+ }
1715
+ if (lowerUseCase.includes('vision') || lowerUseCase.includes('image')) {
1716
+ return model.primary_category === 'multimodal' ||
1717
+ model.categories?.includes('multimodal') ||
1718
+ model.name.toLowerCase().includes('llava');
1719
+ }
1720
+ return true; // Include if no specific pattern matches
1721
+ }
1722
+ });
1723
+
1724
+ if (useCaseMarginal.length > 0) {
1725
+ marginalCandidates = useCaseMarginal;
1726
+ reason = `Best ${useCase} model for your hardware`;
1727
+ } else {
1728
+ reason = 'Best available option (marginal performance)';
1729
+ }
1730
+ } else {
1731
+ reason = 'Best available option (marginal performance)';
1732
+ }
1733
+
1734
+ const sortedMarginal = marginalCandidates.sort((a, b) => (b.score || 0) - (a.score || 0));
1735
+ selectedModels = sortedMarginal.slice(0, limit);
1736
+ }
1737
+
1738
+ if (selectedModels && selectedModels.length > 0) {
1739
+ for (let index = 0; index < selectedModels.length; index++) {
1740
+ const model = selectedModels[index];
1741
+
1742
+ if (limit > 1) {
1743
+ const rank = index + 1;
1744
+ const rankColor = rank === 1 ? chalk.yellow : chalk.gray;
1745
+ console.log(`\n${rankColor.bold(`#${rank} - ${model.name}`)}`);
1746
+ } else {
1747
+ console.log(`Model: ${chalk.cyan.bold(model.name)}`);
1748
+ }
1749
+
1750
+ // Get real size from Ollama cache or estimate
1751
+ const realSize = getRealSizeFromOllamaCache(model) || estimateModelSize(model);
1752
+ console.log(`Size: ${chalk.white(realSize)}`);
1753
+ console.log(`Compatibility Score: ${chalk.green.bold(model.adjustedScore || model.score || 'N/A')}/100`);
1754
+
1755
+ if (index === 0) {
1756
+ console.log(`Reason: ${chalk.gray(reason)}`);
1757
+ }
1758
+
1759
+ // Show performance if available
1760
+ if (model.performanceEstimate) {
1761
+ console.log(`Estimated Speed: ${chalk.yellow(model.performanceEstimate.estimatedTokensPerSecond || 'N/A')} tokens/sec`);
1762
+ }
1763
+
1764
+ // Check if it's already installed by comparing with Ollama integration
1765
+ let isInstalled = false;
1766
+ try {
1767
+ isInstalled = await checkIfModelInstalled(model, analysis.ollamaInfo);
1768
+ if (isInstalled) {
1769
+ console.log(`Status: ${chalk.green('Already installed in Ollama')}`);
1770
+ } else if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
1771
+ console.log(`Status: ${chalk.gray('Available for installation')}`);
1772
+ } else {
1773
+ console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
1774
+ }
1775
+ } catch (installCheckError) {
1776
+ // If checking installation status fails, show based on Ollama availability
1777
+ if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
1778
+ console.log(`Status: ${chalk.gray('Available for installation')}`);
1779
+ } else {
1780
+ console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
1781
+ }
1782
+ }
1783
+
1784
+ // Show pull/run command directly in each model block (Issue #3)
1785
+ const ollamaCommand = getOllamaInstallCommand(model);
1786
+ if (ollamaCommand) {
1787
+ const modelName = extractModelName(ollamaCommand);
1788
+ if (isInstalled) {
1789
+ console.log(`\nCommand: ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
1790
+ } else {
1791
+ console.log(`\nCommand: ${chalk.cyan.bold(ollamaCommand)}`);
1792
+ }
1793
+ } else if (model.ollamaTag || model.ollamaId) {
1794
+ const tag = model.ollamaTag || model.ollamaId;
1795
+ if (isInstalled) {
1796
+ console.log(`\nCommand: ${chalk.cyan.bold(`ollama run ${tag}`)}`);
1797
+ } else {
1798
+ console.log(`\nCommand: ${chalk.cyan.bold(`ollama pull ${tag}`)}`);
1799
+ }
1800
+ }
1801
+ }
1802
+ } else {
1803
+ console.log(chalk.yellow('No compatible models found for your hardware'));
1804
+ console.log(chalk.gray('Try running with --include-cloud to see more options'));
1805
+ }
1806
+
1807
+ return selectedModels;
1808
+ }
1809
+
1810
+ async function displayQuickStartCommands(analysis, recommendedModel = null, allRecommended = null) {
1811
+ console.log(chalk.yellow.bold('\nQUICK START'));
1812
+ console.log(chalk.gray('─'.repeat(50)));
1813
+
1814
+ // Use the first model from allRecommended if available, otherwise fallback to recommendedModel
1815
+ let bestModel = (allRecommended && allRecommended.length > 0) ? allRecommended[0] : recommendedModel;
1816
+
1817
+ if (!bestModel) {
1818
+ if (analysis.compatible && analysis.compatible.length > 0) {
1819
+ const sortedModels = analysis.compatible.sort((a, b) => (b.score || 0) - (a.score || 0));
1820
+ bestModel = sortedModels[0];
1821
+ } else if (analysis.marginal && analysis.marginal.length > 0) {
1822
+ const sortedMarginal = analysis.marginal.sort((a, b) => (b.score || 0) - (a.score || 0));
1823
+ bestModel = sortedMarginal[0];
1824
+ }
1825
+ }
1826
+
1827
+ if (analysis.ollamaInfo && !analysis.ollamaInfo.available) {
1828
+ console.log(`1. Install Ollama: ${chalk.underline('https://ollama.ai')}`);
1829
+ console.log(`2. Come back and run this command again`);
1830
+ } else if (bestModel) {
1831
+ let isInstalled = false;
1832
+ try {
1833
+ isInstalled = await checkIfModelInstalled(bestModel, analysis.ollamaInfo);
1834
+ } catch (installCheckError) {
1835
+ // If checking installation status fails, assume not installed
1836
+ isInstalled = false;
1837
+ }
1838
+
1839
+ if (isInstalled) {
1840
+ const ollamaCommand = getOllamaInstallCommand(bestModel);
1841
+ const modelName = ollamaCommand ? extractModelName(ollamaCommand) : bestModel.name.toLowerCase();
1842
+ console.log(`1. Start using your installed model:`);
1843
+ console.log(` ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
1844
+ } else {
1845
+ // Try to find Ollama command
1846
+ const ollamaCommand = getOllamaInstallCommand(bestModel);
1847
+ if (ollamaCommand) {
1848
+ console.log(`1. Install the recommended model:`);
1849
+ console.log(` ${chalk.cyan.bold(ollamaCommand)}`);
1850
+ console.log(`2. Start using it:`);
1851
+ console.log(` ${chalk.cyan.bold(`ollama run ${extractModelName(ollamaCommand)}`)}`);
1852
+ } else {
1853
+ console.log(`1. Search for ${bestModel.name} on Ollama Hub`);
1854
+ console.log(`2. Install and run the model`);
1855
+ }
1856
+ }
1857
+
1858
+ // If multiple models were shown, suggest trying alternatives (only reasonable ones)
1859
+ if (allRecommended && allRecommended.length > 1) {
1860
+ console.log(`\n${chalk.gray('Alternative options:')}`);
1861
+
1862
+ // Filter out unreasonable alternatives (>50GB, no ollama command)
1863
+ const reasonableAlternatives = allRecommended.slice(1).filter(model => {
1864
+ const realSize = getRealSizeFromOllamaCache(model);
1865
+ const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
1866
+ const ollamaCommand = getOllamaInstallCommand(model);
1867
+
1868
+ // Only show if size is reasonable (<50GB) and has ollama command
1869
+ return sizeGB < 50 && ollamaCommand;
1870
+ });
1871
+
1872
+ // Show max 2 alternatives, avoid duplicating commands
1873
+ const seenCommands = new Set();
1874
+ const bestModelCommand = getOllamaInstallCommand(bestModel);
1875
+ if (bestModelCommand) seenCommands.add(bestModelCommand);
1876
+
1877
+ let alternativeCount = 0;
1878
+ reasonableAlternatives.forEach((model) => {
1879
+ if (alternativeCount >= 2) return; // Max 2 alternatives
1880
+
1881
+ const ollamaCommand = getOllamaInstallCommand(model);
1882
+ if (ollamaCommand && !seenCommands.has(ollamaCommand)) {
1883
+ console.log(` ${chalk.gray(`${alternativeCount + 2}. ${ollamaCommand}`)}`);
1884
+ seenCommands.add(ollamaCommand);
1885
+ alternativeCount++;
1886
+ }
1887
+ });
1888
+
1889
+ // If no reasonable alternatives, don't show the section
1890
+ if (reasonableAlternatives.length === 0) {
1891
+ console.log(` ${chalk.gray('No other reasonable alternatives found for your hardware')}`);
1892
+ }
1893
+ }
1894
+ } else {
1895
+ console.log(`1. Try expanding search: ${chalk.cyan('llm-checker check --include-cloud')}`);
1896
+ console.log(`2. Or see all available models: ${chalk.cyan('llm-checker list-models')}`);
1897
+ }
1898
+ }
1899
+
1900
+ function getOllamaInstallCommand(model) {
1901
+ // Special handling for specific models that need corrected commands
1902
+ const modelName = model.name.toLowerCase();
1903
+
1904
+ if (modelName.includes('codellama') && modelName.includes('7b')) {
1905
+ return 'ollama pull codellama:7b';
1906
+ }
1907
+ if (modelName.includes('mistral') && modelName.includes('7b')) {
1908
+ return 'ollama pull mistral:7b';
1909
+ }
1910
+ if (modelName.includes('llama 3.1') && modelName.includes('8b')) {
1911
+ return 'ollama pull llama3.1:8b';
1912
+ }
1913
+ if (modelName.includes('llama3.1') && !modelName.includes('8b')) {
1914
+ return 'ollama pull llama3.1:8b'; // Default to 8b variant
1915
+ }
1916
+ if (modelName.includes('llama3.2-vision')) {
1917
+ return 'ollama pull llama3.2-vision:latest';
1918
+ }
1919
+ if (modelName.includes('llama3.2')) {
1920
+ return 'ollama pull llama3.2:3b'; // Most common variant
1921
+ }
1922
+ if (modelName.includes('llama3.3')) {
1923
+ return 'ollama pull llama3.3:70b'; // This is the actual size
1924
+ }
1925
+ if (modelName.includes('qwen') && modelName.includes('7b')) {
1926
+ return 'ollama pull qwen2.5:7b';
1927
+ }
1928
+ if (modelName.includes('phi4-reasoning')) {
1929
+ return 'ollama pull phi4-reasoning:latest';
1930
+ }
1931
+ if (modelName.includes('deepseek-r1')) {
1932
+ return 'ollama pull deepseek-r1:8b';
1933
+ }
1934
+ if (modelName.includes('dolphin3')) {
1935
+ return 'ollama pull dolphin3:latest';
1936
+ }
1937
+ if (modelName === 'phi' || modelName.includes('phi ')) {
1938
+ return 'ollama pull phi:latest';
1939
+ }
1940
+
1941
+ // First priority: use ollamaTag if available (from Ollama database)
1942
+ if (model.ollamaTag) {
1943
+ return `ollama pull ${model.ollamaTag}`;
1944
+ }
1945
+
1946
+ // Second priority: use installation.ollama if available
1947
+ if (model.installation && model.installation.ollama) {
1948
+ return model.installation.ollama;
1949
+ }
1950
+
1951
+ // Third priority: try to generate from model name
1952
+
1953
+ const mapping = {
1954
+ 'tinyllama 1.1b': 'ollama pull tinyllama:1.1b',
1955
+ 'phi-3 mini 3.8b': 'ollama pull phi3:mini',
1956
+ 'llama 3.2 3b': 'ollama pull llama3.2:3b',
1957
+ 'llama 3.1 8b': 'ollama pull llama3.1:8b',
1958
+ 'mistral 7b': 'ollama pull mistral:7b',
1959
+ 'mistral 7b v0.3': 'ollama pull mistral:7b',
1960
+ 'qwen 2.5 7b': 'ollama pull qwen2.5:7b',
1961
+ 'codellama 7b': 'ollama pull codellama:7b',
1962
+ 'codellama': 'ollama pull codellama:7b'
1963
+ };
1964
+
1965
+ for (const [key, command] of Object.entries(mapping)) {
1966
+ if (modelName.includes(key) || key.includes(modelName)) {
1967
+ return command;
1968
+ }
1969
+ }
1970
+
1971
+ // Last resort: use ollamaId if available
1972
+ if (model.ollamaId) {
1973
+ return `ollama pull ${model.ollamaId}`;
1974
+ }
1975
+
1976
+ return null;
1977
+ }
1978
+
1979
+ function extractModelName(command) {
1980
+ const match = command.match(/ollama pull (.+)/);
1981
+ return match ? match[1] : 'model';
1982
+ }
1983
+
1984
+ program
1985
+ .command('check')
1986
+ .description('Analyze your system and show compatible LLM models')
1987
+ .option('-d, --detailed', 'Show detailed hardware information')
1988
+ .option('-f, --filter <type>', 'Filter by model type')
1989
+ .option('-u, --use-case <case>', 'Specify use case', 'general')
1990
+ .option('-l, --limit <number>', 'Number of compatible models to show (default: 1)', '1')
1991
+ .option('--max-size <size>', 'Maximum model size to consider (e.g., "30B" or "30GB")')
1992
+ .option('--min-size <size>', 'Minimum model size to consider (e.g., "7B" or "7GB")')
1993
+ .option('--include-cloud', 'Include cloud models in analysis')
1994
+ .option('--ollama-only', 'Only show models available in Ollama')
1995
+ .option('--performance-test', 'Run performance benchmarks')
1996
+ .option('--show-ollama-analysis', 'Show detailed Ollama model analysis')
1997
+ .option('--no-verbose', 'Disable step-by-step progress display')
1998
+ .action(async (options) => {
1999
+ showAsciiArt('check');
2000
+ try {
2001
+ // Use verbose progress unless explicitly disabled
2002
+ const verboseEnabled = options.verbose !== false;
2003
+ const checker = new (getLLMChecker())({ verbose: verboseEnabled });
2004
+
2005
+ // If verbose is disabled, show simple loading message
2006
+ if (!verboseEnabled) {
2007
+ process.stdout.write(chalk.gray('Analyzing your system...'));
2008
+ }
2009
+
2010
+ const hardware = await checker.getSystemInfo();
2011
+
2012
+ // Normalize and fix use-case typos
2013
+ const normalizeUseCase = (useCase = '') => {
2014
+ const alias = useCase.toLowerCase().trim();
2015
+ const useCaseMap = {
2016
+ 'embed': 'embeddings',
2017
+ 'embedding': 'embeddings',
2018
+ 'embeddings': 'embeddings',
2019
+ 'embedings': 'embeddings', // common typo
2020
+ 'talk': 'chat',
2021
+ 'chat': 'chat',
2022
+ 'talking': 'chat'
2023
+ };
2024
+ return useCaseMap[alias] || alias || 'general';
2025
+ };
2026
+
2027
+ const normalizedUseCase = normalizeUseCase(options.useCase);
2028
+
2029
+ // Parse size filters
2030
+ const parseSizeFilter = (sizeStr) => {
2031
+ if (!sizeStr) return null;
2032
+ const match = sizeStr.toUpperCase().match(/^(\d+\.?\d*)\s*(B|GB)?$/);
2033
+ if (match) {
2034
+ const num = parseFloat(match[1]);
2035
+ const unit = match[2] || 'B';
2036
+ // Return size in billions of parameters (B)
2037
+ return unit === 'GB' ? num / 0.5 : num; // Approximate: 0.5GB per 1B params (Q4)
2038
+ }
2039
+ return null;
2040
+ };
2041
+
2042
+ const maxSize = parseSizeFilter(options.maxSize);
2043
+ const minSize = parseSizeFilter(options.minSize);
2044
+
2045
+ const analysis = await checker.analyze({
2046
+ filter: options.filter,
2047
+ useCase: normalizedUseCase,
2048
+ includeCloud: options.includeCloud,
2049
+ performanceTest: options.performanceTest,
2050
+ limit: parseInt(options.limit) || 10,
2051
+ maxSize: maxSize,
2052
+ minSize: minSize
2053
+ });
2054
+
2055
+ if (!verboseEnabled) {
2056
+ console.log(chalk.green(' done'));
2057
+ }
2058
+
2059
+ // Simplified output - show only essential information
2060
+ displaySimplifiedSystemInfo(hardware);
2061
+ const recommendedModels = await displayModelRecommendations(analysis, hardware, normalizedUseCase, parseInt(options.limit) || 1);
2062
+ await displayQuickStartCommands(analysis, recommendedModels[0], recommendedModels);
2063
+
2064
+ } catch (error) {
2065
+ console.error(chalk.red('\nError:'), error.message);
2066
+ if (process.env.DEBUG) {
2067
+ console.error(error.stack);
2068
+ }
2069
+ process.exit(1);
2070
+ }
2071
+ });
2072
+
2073
+ program
2074
+ .command('ollama')
2075
+ .description('Manage Ollama integration with hardware compatibility')
2076
+ .option('-l, --list', 'List installed models with compatibility scores')
2077
+ .option('-r, --running', 'Show running models with performance data')
2078
+ .option('-c, --compatible', 'Show only hardware-compatible installed models')
2079
+ .option('--recommendations', 'Show installation recommendations')
2080
+ .action(async (options) => {
2081
+ showAsciiArt('ollama');
2082
+ const spinner = ora('Checking Ollama integration...').start();
2083
+
2084
+ try {
2085
+ const checker = new (getLLMChecker())();
2086
+ const analysis = await checker.analyze();
2087
+
2088
+ if (!analysis.ollamaInfo.available) {
2089
+ spinner.fail(`Ollama not available`);
2090
+ console.log('\nTo install Ollama:');
2091
+ console.log('Visit: https://ollama.ai');
2092
+ if (analysis.ollamaInfo.hint) {
2093
+ console.log(chalk.yellow('Hint: ' + analysis.ollamaInfo.hint));
2094
+ }
2095
+ if (analysis.ollamaInfo.attemptedURL) {
2096
+ console.log(chalk.gray('Attempted URL: ' + analysis.ollamaInfo.attemptedURL));
2097
+ console.log(chalk.gray('Set OLLAMA_HOST environment variable to use a different URL'));
2098
+ }
2099
+ return;
2100
+ }
2101
+
2102
+ spinner.succeed(`Ollama integration active`);
2103
+
2104
+ if (options.list) {
2105
+ console.log('Ollama models list feature coming soon...');
2106
+ }
2107
+
2108
+ } catch (error) {
2109
+ spinner.fail('Error with Ollama integration');
2110
+ console.error(chalk.red('Error:'), error.message);
2111
+ }
2112
+ });
2113
+
2114
+ // New command: installed - Show ranking of installed Ollama models
2115
+ program
2116
+ .command('installed')
2117
+ .description('Show ranking of installed Ollama models by compatibility and use-case')
2118
+ .option('--sort <by>', 'Sort by: score, size, name (default: score)', 'score')
2119
+ .option('--json', 'Output in JSON format')
2120
+ .action(async (options) => {
2121
+ if (!options.json) showAsciiArt('installed');
2122
+ const spinner = ora('Analyzing installed models...').start();
2123
+
2124
+ try {
2125
+ const checker = new (getLLMChecker())({ verbose: false });
2126
+ const OllamaClient = require('../src/ollama/client');
2127
+ const ollamaClient = new OllamaClient();
2128
+
2129
+ // Check Ollama availability
2130
+ const availability = await ollamaClient.checkOllamaAvailability();
2131
+ if (!availability.available) {
2132
+ spinner.fail('Ollama not available');
2133
+ console.log(chalk.red('\n' + availability.error));
2134
+ if (availability.hint) {
2135
+ console.log(chalk.yellow('Hint: ' + availability.hint));
2136
+ }
2137
+ return;
2138
+ }
2139
+
2140
+ // Get installed models
2141
+ const installedModels = await ollamaClient.getLocalModels();
2142
+ if (!installedModels || installedModels.length === 0) {
2143
+ spinner.fail('No models installed');
2144
+ console.log(chalk.yellow('\nNo Ollama models found. Install one with:'));
2145
+ console.log(chalk.cyan(' ollama pull llama3.2:3b'));
2146
+ return;
2147
+ }
2148
+
2149
+ // Get hardware info for scoring
2150
+ const hardware = await checker.getSystemInfo();
2151
+ const analysis = await checker.analyze({ limit: 100 });
2152
+
2153
+ spinner.succeed(`Found ${installedModels.length} installed models`);
2154
+
2155
+ // Score and categorize each installed model
2156
+ const scoredModels = installedModels.map(model => {
2157
+ // Find matching model in analysis
2158
+ const matchingModel = [...(analysis.compatible || []), ...(analysis.marginal || [])].find(m =>
2159
+ m.name && model.name && (
2160
+ m.name.toLowerCase().includes(model.family) ||
2161
+ model.name.toLowerCase().includes(m.name.toLowerCase().split(' ')[0])
2162
+ )
2163
+ );
2164
+
2165
+ // Determine use-case from model name
2166
+ const nameLower = model.name.toLowerCase();
2167
+ let useCase = 'general';
2168
+ if (nameLower.includes('code') || nameLower.includes('coder') || nameLower.includes('deepseek-coder')) {
2169
+ useCase = 'coding';
2170
+ } else if (nameLower.includes('embed') || nameLower.includes('nomic') || nameLower.includes('bge')) {
2171
+ useCase = 'embeddings';
2172
+ } else if (nameLower.includes('llava') || nameLower.includes('vision') || nameLower.includes('bakllava')) {
2173
+ useCase = 'multimodal';
2174
+ } else if (nameLower.includes('r1') || nameLower.includes('qwq') || nameLower.includes('reasoning')) {
2175
+ useCase = 'reasoning';
2176
+ } else if (nameLower.includes('wizard') || nameLower.includes('creative')) {
2177
+ useCase = 'creative';
2178
+ } else if (nameLower.includes('chat') || nameLower.includes('instruct')) {
2179
+ useCase = 'chat';
2180
+ }
2181
+
2182
+ // Calculate compatibility score
2183
+ const fileSizeGB = model.fileSizeGB || 0;
2184
+ const availableRAM = hardware.memory.total * 0.8;
2185
+ let score = 50;
2186
+
2187
+ // RAM fit score
2188
+ if (fileSizeGB <= availableRAM * 0.3) score += 30;
2189
+ else if (fileSizeGB <= availableRAM * 0.5) score += 20;
2190
+ else if (fileSizeGB <= availableRAM * 0.7) score += 10;
2191
+ else score -= 10;
2192
+
2193
+ // Size efficiency for hardware tier
2194
+ const sizeMatch = (model.size || '').match(/(\d+)/);
2195
+ const paramSize = sizeMatch ? parseInt(sizeMatch[1]) : 7;
2196
+ if (hardware.memory.total >= 32 && paramSize >= 13) score += 10;
2197
+ else if (hardware.memory.total >= 16 && paramSize >= 7) score += 10;
2198
+ else if (hardware.memory.total >= 8 && paramSize <= 7) score += 10;
2199
+
2200
+ // Use matched model score if available
2201
+ if (matchingModel && matchingModel.score) {
2202
+ score = Math.round((score + matchingModel.score) / 2);
2203
+ }
2204
+
2205
+ return {
2206
+ name: model.name,
2207
+ displayName: model.displayName,
2208
+ size: model.size,
2209
+ fileSizeGB: model.fileSizeGB,
2210
+ quantization: model.quantization,
2211
+ useCase: useCase,
2212
+ score: Math.min(100, Math.max(0, score)),
2213
+ command: `ollama run ${model.name}`
2214
+ };
2215
+ });
2216
+
2217
+ // Sort models
2218
+ scoredModels.sort((a, b) => {
2219
+ switch (options.sort) {
2220
+ case 'size':
2221
+ return b.fileSizeGB - a.fileSizeGB;
2222
+ case 'name':
2223
+ return a.name.localeCompare(b.name);
2224
+ case 'score':
2225
+ default:
2226
+ return b.score - a.score;
2227
+ }
2228
+ });
2229
+
2230
+ // Output
2231
+ if (options.json) {
2232
+ console.log(JSON.stringify(scoredModels, null, 2));
2233
+ return;
2234
+ }
2235
+
2236
+ console.log('\n' + chalk.bgGreen.white.bold(' INSTALLED MODELS RANKING '));
2237
+ console.log(chalk.green('╭' + '─'.repeat(75)));
2238
+ console.log(chalk.green('│') + ` Sorted by: ${chalk.cyan(options.sort)} | Hardware: ${chalk.yellow(hardware.memory.total + 'GB RAM')}`);
2239
+ console.log(chalk.green('├' + '─'.repeat(75)));
2240
+
2241
+ const headers = [
2242
+ chalk.bold(' # '),
2243
+ chalk.bold(' Model '),
2244
+ chalk.bold(' Size '),
2245
+ chalk.bold(' Score '),
2246
+ chalk.bold(' Use Case '),
2247
+ chalk.bold(' Command ')
2248
+ ];
2249
+ const data = [headers];
2250
+
2251
+ scoredModels.forEach((model, index) => {
2252
+ const rank = index + 1;
2253
+ const rankIcon = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : `${rank}.`;
2254
+ const scoreColor = model.score >= 75 ? chalk.green : model.score >= 50 ? chalk.yellow : chalk.red;
2255
+
2256
+ data.push([
2257
+ rankIcon,
2258
+ model.name.length > 25 ? model.name.substring(0, 22) + '...' : model.name,
2259
+ `${model.fileSizeGB}GB`,
2260
+ scoreColor(`${model.score}/100`),
2261
+ model.useCase,
2262
+ chalk.cyan(`ollama run ${model.name.split(':')[0]}`)
2263
+ ]);
2264
+ });
2265
+
2266
+ console.log(table(data));
2267
+
2268
+ // Show suggestions for low-ranking models
2269
+ const lowRankingModels = scoredModels.filter(m => m.score < 50);
2270
+ if (lowRankingModels.length > 0) {
2271
+ console.log(chalk.yellow('\nConsider removing these low-ranking models to free up space:'));
2272
+ lowRankingModels.forEach(m => {
2273
+ console.log(chalk.gray(` ollama rm ${m.name} # Score: ${m.score}/100, Size: ${m.fileSizeGB}GB`));
2274
+ });
2275
+ }
2276
+
2277
+ console.log(chalk.green('╰' + '─'.repeat(75)));
2278
+
2279
+ } catch (error) {
2280
+ spinner.fail('Error analyzing installed models');
2281
+ console.error(chalk.red('Error:'), error.message);
2282
+ if (process.env.DEBUG) {
2283
+ console.error(error.stack);
2284
+ }
2285
+ }
2286
+ });
2287
+
2288
+ program
2289
+ .command('recommend')
2290
+ .description('Get intelligent model recommendations for your hardware')
2291
+ .option('-c, --category <category>', 'Get recommendations for specific category (coding, talking, reading, etc.)')
2292
+ .option('--no-verbose', 'Disable step-by-step progress display')
2293
+ .action(async (options) => {
2294
+ showAsciiArt('recommend');
2295
+ try {
2296
+ const verboseEnabled = options.verbose !== false;
2297
+ const checker = new (getLLMChecker())({ verbose: verboseEnabled });
2298
+
2299
+ if (!verboseEnabled) {
2300
+ process.stdout.write(chalk.gray('Generating recommendations...'));
2301
+ }
2302
+
2303
+ const hardware = await checker.getSystemInfo();
2304
+ const intelligentRecommendations = await checker.generateIntelligentRecommendations(hardware);
2305
+
2306
+ if (!intelligentRecommendations) {
2307
+ console.error(chalk.red('\nFailed to generate recommendations'));
2308
+ return;
2309
+ }
2310
+
2311
+ if (!verboseEnabled) {
2312
+ console.log(chalk.green(' done'));
2313
+ }
2314
+
2315
+ // Mostrar información del sistema
2316
+ displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
2317
+
2318
+ // Mostrar recomendaciones
2319
+ displayIntelligentRecommendations(intelligentRecommendations);
2320
+
2321
+ } catch (error) {
2322
+ console.error(chalk.red('\nError:'), error.message);
2323
+ if (process.env.DEBUG) {
2324
+ console.error(error.stack);
2325
+ }
2326
+ process.exit(1);
2327
+ }
2328
+ });
2329
+
2330
+ program
2331
+ .command('list-models')
2332
+ .description('List all models from Ollama database')
2333
+ .option('-c, --category <category>', 'Filter by category (coding, talking, reading, reasoning, multimodal, creative, general)')
2334
+ .option('-s, --size <size>', 'Filter by size (small, medium, large, e.g., "7b", "13b")')
2335
+ .option('-p, --popular', 'Show only popular models (>100k pulls)')
2336
+ .option('-r, --recent', 'Show only recent models (updated in last 30 days)')
2337
+ .option('--limit <number>', 'Limit number of results (default: 50)', '50')
2338
+ .option('--full', 'Show full details including variants and tags')
2339
+ .option('--json', 'Output in JSON format')
2340
+ .action(async (options) => {
2341
+ if (!options.json) showAsciiArt('list-models');
2342
+ const spinner = ora('📋 Loading models database...').start();
2343
+
2344
+ try {
2345
+ const checker = new (getLLMChecker())();
2346
+ const data = await checker.ollamaScraper.scrapeAllModels(false);
2347
+
2348
+ if (!data || !data.models) {
2349
+ spinner.fail('No models found in database');
2350
+ return;
2351
+ }
2352
+
2353
+ let models = data.models;
2354
+ let originalCount = models.length;
2355
+
2356
+ // Aplicar filtros
2357
+ if (options.category) {
2358
+ const categoryFilter = options.category.toLowerCase();
2359
+ models = models.filter(model => {
2360
+ // Buscar en categoría principal
2361
+ if (model.category === categoryFilter) return true;
2362
+
2363
+ // Buscar en use_cases
2364
+ if (model.use_cases && model.use_cases.includes(categoryFilter)) return true;
2365
+
2366
+ // Buscar por palabras clave en el nombre/identificador
2367
+ const modelText = `${model.model_name} ${model.model_identifier}`.toLowerCase();
2368
+
2369
+ switch(categoryFilter) {
2370
+ case 'coding':
2371
+ case 'code':
2372
+ return modelText.includes('code') || modelText.includes('coder') ||
2373
+ modelText.includes('programming') || modelText.includes('deepseek') ||
2374
+ modelText.includes('starcoder');
2375
+ case 'talking':
2376
+ case 'chat':
2377
+ return modelText.includes('chat') || modelText.includes('llama') ||
2378
+ modelText.includes('mistral') || modelText.includes('gemma') ||
2379
+ modelText.includes('phi');
2380
+ case 'reasoning':
2381
+ return modelText.includes('reasoning') || modelText.includes('deepseek-r1') ||
2382
+ modelText.includes('qwq') || modelText.includes('r1');
2383
+ case 'multimodal':
2384
+ case 'vision':
2385
+ return modelText.includes('vision') || modelText.includes('llava') ||
2386
+ modelText.includes('minicpm-v');
2387
+ case 'creative':
2388
+ case 'writing':
2389
+ return modelText.includes('wizard') || modelText.includes('creative') ||
2390
+ modelText.includes('uncensored');
2391
+ case 'embeddings':
2392
+ case 'embed':
2393
+ return modelText.includes('embed') || modelText.includes('bge') ||
2394
+ modelText.includes('nomic');
2395
+ default:
2396
+ return false;
2397
+ }
2398
+ });
2399
+ }
2400
+
2401
+ if (options.size) {
2402
+ const sizeFilter = options.size.toLowerCase();
2403
+ models = models.filter(model =>
2404
+ model.model_identifier.toLowerCase().includes(sizeFilter) ||
2405
+ (model.model_sizes && model.model_sizes.some(size => size.includes(sizeFilter)))
2406
+ );
2407
+ }
2408
+
2409
+ if (options.popular) {
2410
+ models = models.filter(model => (model.pulls || 0) > 100000);
2411
+ }
2412
+
2413
+ if (options.recent) {
2414
+ models = models.filter(model =>
2415
+ model.last_updated && model.last_updated.includes('day')
2416
+ );
2417
+ }
2418
+
2419
+ // Si hay filtro de categoría, ordenar por compatibilidad con hardware
2420
+ if (options.category) {
2421
+ try {
2422
+ const LLMChecker = require('../src/index.js');
2423
+ const hardwareDetector = new (require('../src/hardware/detector.js'))();
2424
+ const hardware = await hardwareDetector.getSystemInfo();
2425
+
2426
+ // Calcular puntuación de compatibilidad para cada modelo
2427
+ models = models.map(model => {
2428
+ const compatibilityScore = calculateModelCompatibilityScore(model, hardware);
2429
+ return { ...model, compatibilityScore };
2430
+ });
2431
+
2432
+ // Ordenar por compatibilidad primero, luego por popularidad
2433
+ models.sort((a, b) => {
2434
+ if (b.compatibilityScore !== a.compatibilityScore) {
2435
+ return b.compatibilityScore - a.compatibilityScore;
2436
+ }
2437
+ return (b.pulls || 0) - (a.pulls || 0);
2438
+ });
2439
+
2440
+ spinner.text = `Sorted by hardware compatibility (${getHardwareTierForDisplay(hardware)})`;
2441
+ } catch (error) {
2442
+ console.warn('Could not sort by hardware compatibility:', error.message);
2443
+ // Fallback a ordenar por popularidad
2444
+ models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
2445
+ }
2446
+ } else {
2447
+ // Sin filtro de categoría, ordenar solo por popularidad
2448
+ models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
2449
+ }
2450
+
2451
+ // Limitar resultados
2452
+ const limit = parseInt(options.limit) || 50;
2453
+ const displayModels = models.slice(0, limit);
2454
+
2455
+ spinner.succeed(`✅ Found ${models.length} models (showing ${displayModels.length})`);
2456
+
2457
+ if (options.json) {
2458
+ console.log(JSON.stringify(displayModels, null, 2));
2459
+ return;
2460
+ }
2461
+
2462
+ // Mostrar estadísticas
2463
+ displayModelsStats(originalCount, models.length, options);
2464
+
2465
+ // Mostrar modelos
2466
+ if (options.full) {
2467
+ displayFullModelsList(displayModels);
2468
+ } else {
2469
+ await displayCompactModelsList(displayModels, options.category);
2470
+ }
2471
+
2472
+ // Mostrar comandos de ejemplo
2473
+ if (displayModels.length > 0) {
2474
+ displaySampleCommands(displayModels.slice(0, 3));
2475
+ }
2476
+
2477
+ } catch (error) {
2478
+ spinner.fail('Failed to load models');
2479
+ console.error(chalk.red('Error:'), error.message);
2480
+ if (process.env.DEBUG) {
2481
+ console.error(error.stack);
2482
+ }
2483
+ process.exit(1);
2484
+ }
2485
+ });
2486
+
2487
+
2488
+ function getStatusColor(status) {
2489
+ const colors = {
2490
+ 'TRAINED': chalk.green,
2491
+ 'NOT TRAINED': chalk.yellow,
2492
+ 'CORRUPTED': chalk.red
2493
+ };
2494
+ return colors[status] || chalk.gray;
2495
+ }
2496
+
2497
+ function getConfidenceColor(confidence) {
2498
+ if (confidence >= 0.8) return chalk.green.bold;
2499
+ if (confidence >= 0.6) return chalk.yellow.bold;
2500
+ if (confidence >= 0.4) return chalk.red.bold; // orange doesn't exist, use red
2501
+ return chalk.red.bold;
2502
+ }
2503
+
2504
+ function getScoreColor(score) {
2505
+ if (score >= 85) return chalk.green.bold;
2506
+ if (score >= 70) return chalk.cyan.bold;
2507
+ if (score >= 55) return chalk.yellow.bold;
2508
+ if (score >= 40) return chalk.red.bold;
2509
+ return chalk.gray;
2510
+ }
2511
+
2512
+ function getTierColor(tier) {
2513
+ const colors = {
2514
+ 'extreme': chalk.magenta.bold,
2515
+ 'very_high': chalk.green.bold,
2516
+ 'high': chalk.cyan.bold,
2517
+ 'medium': chalk.yellow,
2518
+ 'low': chalk.red,
2519
+ 'ultra_low': chalk.gray
2520
+ };
2521
+ return colors[tier] || chalk.white;
2522
+ }
2523
+
2524
+ program
2525
+ .command('ai-check')
2526
+ .description('AI-powered model evaluation with meta-analysis')
2527
+ .option('-c, --category <category>', 'Category: coding, reasoning, multimodal, general', 'general')
2528
+ .option('-t, --top <number>', 'Number of top models to show', '12')
2529
+ .option('--ctx <number>', 'Target context length', '8192')
2530
+ .option('-e, --evaluator <model>', 'Evaluator model (auto for best available)', 'auto')
2531
+ .option('-w, --weight <number>', 'AI weight (0.0-1.0, default 0.3)', '0.3')
2532
+ .action(async (options) => {
2533
+ showAsciiArt('ai-check');
2534
+ // Check if Ollama is installed first
2535
+ await checkOllamaAndExit();
2536
+
2537
+ const AICheckSelector = require('../src/models/ai-check-selector');
2538
+
2539
+ try {
2540
+ const spinner = ora('AI-Check Mode: Meta-evaluation in progress...').start();
2541
+
2542
+ const aiCheckSelector = new AICheckSelector();
2543
+
2544
+ const checkOptions = {
2545
+ category: options.category,
2546
+ top: parseInt(options.top),
2547
+ ctx: options.ctx ? parseInt(options.ctx) : undefined,
2548
+ evaluator: options.evaluator,
2549
+ weight: parseFloat(options.weight)
2550
+ };
2551
+
2552
+ spinner.stop();
2553
+
2554
+ const result = await aiCheckSelector.aiCheck(checkOptions);
2555
+
2556
+ // Format and display results
2557
+ aiCheckSelector.formatResults(result);
2558
+
2559
+ } catch (error) {
2560
+ console.error(chalk.red('❌ AI-Check failed:'), error.message);
2561
+ if (process.argv.includes('--verbose')) {
2562
+ console.error(error.stack);
2563
+ }
2564
+ process.exit(1);
2565
+ }
2566
+ });
2567
+
2568
+ program
2569
+ .command('ai-run')
2570
+ .description('AI-powered model selection and execution')
2571
+ .option('-m, --models <models...>', 'Specific models to choose from')
2572
+ .option('--prompt <prompt>', 'Prompt to run with selected model')
2573
+ .action(async (options) => {
2574
+ showAsciiArt('ai-run');
2575
+ // Check if Ollama is installed first
2576
+ await checkOllamaAndExit();
2577
+
2578
+ const AIModelSelector = require('../src/ai/model-selector');
2579
+
2580
+ try {
2581
+ const spinner = ora('Selecting best model and launching...').start();
2582
+
2583
+ const aiSelector = new AIModelSelector();
2584
+ const checker = new (getLLMChecker())();
2585
+ const systemInfo = await checker.getSystemInfo();
2586
+
2587
+ // Get available models or use provided ones
2588
+ let candidateModels = options.models;
2589
+
2590
+ if (!candidateModels) {
2591
+ spinner.text = '📋 Getting available Ollama models...';
2592
+ const OllamaClient = require('../src/ollama/client');
2593
+ const client = new OllamaClient();
2594
+
2595
+ try {
2596
+ const models = await client.getLocalModels();
2597
+ candidateModels = models.map(m => m.name || m.model);
2598
+
2599
+ if (candidateModels.length === 0) {
2600
+ spinner.fail('❌ No Ollama models found');
2601
+ console.log('\nInstall some models first:');
2602
+ console.log(' ollama pull llama2:7b');
2603
+ console.log(' ollama pull mistral:7b');
2604
+ console.log(' ollama pull phi3:mini');
2605
+ return;
2606
+ }
2607
+ } catch (error) {
2608
+ spinner.fail('❌ Failed to get Ollama models');
2609
+ console.error(chalk.red('Error:'), error.message);
2610
+ return;
2611
+ }
2612
+ }
2613
+
2614
+ // AI selection
2615
+ const systemSpecs = {
2616
+ cpu_cores: systemInfo.cpu?.cores || 4,
2617
+ cpu_freq_max: systemInfo.cpu?.speed || 3.0,
2618
+ total_ram_gb: systemInfo.memory?.total || 8,
2619
+ gpu_vram_gb: systemInfo.gpu?.vram || 0,
2620
+ gpu_model_normalized: systemInfo.gpu?.model ||
2621
+ (systemInfo.cpu?.manufacturer === 'Apple' ? 'apple_silicon' : 'cpu_only')
2622
+ };
2623
+
2624
+ const result = await aiSelector.selectBestModel(candidateModels, systemSpecs);
2625
+
2626
+ spinner.succeed(`Selected ${chalk.green.bold(result.bestModel)} (${result.method}, ${Math.round(result.confidence * 100)}% confidence)`);
2627
+
2628
+ // Execute the selected model
2629
+ console.log(chalk.magenta.bold(`\nLaunching ${result.bestModel}...`));
2630
+ console.log(chalk.gray(`Tip: Type ${chalk.cyan('/bye')} to exit the chat when finished\n`));
2631
+
2632
+ const args = ['run', result.bestModel];
2633
+ if (options.prompt) {
2634
+ args.push(options.prompt);
2635
+ }
2636
+
2637
+ const ollamaProcess = spawn('ollama', args, {
2638
+ stdio: 'inherit'
2639
+ });
2640
+
2641
+ ollamaProcess.on('error', (error) => {
2642
+ console.error(chalk.red('Failed to launch Ollama:'), error.message);
2643
+ });
2644
+
2645
+ } catch (error) {
2646
+ console.error(chalk.red('❌ AI-powered execution failed:'), error.message);
2647
+ process.exit(1);
2648
+ }
2649
+ });
2650
+
2651
+ // Comando especial para demostrar el nuevo estilo de verbosity
2652
+ program
2653
+ .command('demo')
2654
+ .description('Demo of the enhanced verbose progress with progress bars')
2655
+ .action(async () => {
2656
+ showAsciiArt('demo');
2657
+ console.log(chalk.cyan.bold('\nLLM Checker - Enhanced Progress Demo'));
2658
+ console.log(chalk.gray('This demonstrates the new step-by-step progress display with visual indicators'));
2659
+ console.log(chalk.gray('─'.repeat(60)));
2660
+
2661
+ // Simular el proceso de análisis con verbosity
2662
+ const VerboseProgress = require('../src/utils/verbose-progress');
2663
+ const progress = VerboseProgress.create(true);
2664
+
2665
+ progress.startOperation('LLM Model Analysis & Compatibility Demo', 5);
2666
+
2667
+ // Simular paso 1
2668
+ progress.step('System Detection', 'Scanning hardware specifications...');
2669
+ await new Promise(resolve => setTimeout(resolve, 1000));
2670
+ progress.substep('CPU detected: Apple M4 Pro (12 cores)');
2671
+ await new Promise(resolve => setTimeout(resolve, 500));
2672
+ progress.substep('Memory detected: 24GB unified memory', true);
2673
+ progress.stepComplete('Apple M4 Pro, 24GB RAM, Apple Silicon GPU');
2674
+
2675
+ // Simular paso 2
2676
+ progress.step('Database Sync', 'Updating model database...');
2677
+ await new Promise(resolve => setTimeout(resolve, 800));
2678
+ progress.found('3,247 models in database');
2679
+ progress.stepComplete('Database synchronized');
2680
+
2681
+ // Simular paso 3
2682
+ progress.step('Compatibility Analysis', 'Running mathematical heuristics...');
2683
+ await new Promise(resolve => setTimeout(resolve, 1200));
2684
+ progress.substep('Analyzing hardware requirements...');
2685
+ await new Promise(resolve => setTimeout(resolve, 600));
2686
+ progress.substep('Calculating performance scores...', true);
2687
+ progress.found('127 compatible models found');
2688
+ progress.stepComplete('Compatibility analysis complete');
2689
+
2690
+ // Simular paso 4
2691
+ progress.step('AI Evaluation', 'Running intelligent model selection...');
2692
+ await new Promise(resolve => setTimeout(resolve, 900));
2693
+ progress.substep('Mathematical heuristics applied');
2694
+ progress.found('Top 15 models selected by AI');
2695
+ progress.stepComplete('AI evaluation complete');
2696
+
2697
+ // Simular paso 5
2698
+ progress.step('Smart Recommendations', 'Generating personalized suggestions...');
2699
+ await new Promise(resolve => setTimeout(resolve, 600));
2700
+ progress.substep('Analyzing use case: general');
2701
+ await new Promise(resolve => setTimeout(resolve, 400));
2702
+ progress.substep('Generating Ollama commands...', true);
2703
+ progress.stepComplete('23 recommendations generated');
2704
+
2705
+ // Completar
2706
+ progress.complete('Analysis complete! Found optimal models for your hardware');
2707
+
2708
+ console.log(chalk.green.bold('Demo completed successfully!'));
2709
+ console.log(chalk.gray('\\nNow try running: ') + chalk.cyan.bold('llm-checker check'));
2710
+ console.log(chalk.gray('For silent mode: ') + chalk.cyan.bold('llm-checker check --no-verbose'));
2711
+ });
2712
+
2713
+ // ============================================================
2714
+ // NEW ENHANCED COMMANDS (v3.0 - Intelligent Model Selection)
2715
+ // ============================================================
2716
+
2717
+ program
2718
+ .command('sync')
2719
+ .description('Sync the model database from Ollama registry (scrapes all models)')
2720
+ .option('-f, --force', 'Force full sync even if recent data exists')
2721
+ .option('--incremental', 'Only sync new and updated models')
2722
+ .option('-q, --quiet', 'Suppress progress output')
2723
+ .action(async (options) => {
2724
+ if (!options.quiet) showAsciiArt('sync');
2725
+ const SyncManager = require('../src/data/sync-manager');
2726
+
2727
+ const spinner = options.quiet ? null : ora('Initializing sync...').start();
2728
+
2729
+ try {
2730
+ const syncManager = new SyncManager({
2731
+ onProgress: (info) => {
2732
+ if (!options.quiet && spinner) {
2733
+ if (info.phase === 'complete') {
2734
+ spinner.succeed(info.message);
2735
+ } else {
2736
+ spinner.text = info.message;
2737
+ }
2738
+ }
2739
+ },
2740
+ onError: (err) => {
2741
+ if (!options.quiet) console.error(chalk.yellow('Warning:'), err);
2742
+ }
2743
+ });
2744
+
2745
+ let result;
2746
+ if (options.incremental) {
2747
+ result = await syncManager.incrementalSync();
2748
+ } else {
2749
+ result = await syncManager.sync({ force: options.force });
2750
+ }
2751
+
2752
+ if (!options.quiet) {
2753
+ console.log(chalk.green('\n[OK] Sync complete!'));
2754
+ console.log(chalk.gray(` Models: ${result.stats?.models || result.models || 0}`));
2755
+ console.log(chalk.gray(` Variants: ${result.stats?.variants || result.variants || 0}`));
2756
+ }
2757
+
2758
+ syncManager.close();
2759
+
2760
+ } catch (error) {
2761
+ if (spinner) spinner.fail('Sync failed');
2762
+ console.error(chalk.red('Error:'), error.message);
2763
+ if (process.env.DEBUG) console.error(error.stack);
2764
+ process.exit(1);
2765
+ }
2766
+ });
2767
+
2768
+ program
2769
+ .command('search <query>')
2770
+ .description('Search models in the database with intelligent scoring')
2771
+ .option('-u, --use-case <case>', 'Optimize for use case (general, coding, chat, reasoning, creative)', 'general')
2772
+ .option('-l, --limit <n>', 'Maximum number of results', '10')
2773
+ .option('--max-size <gb>', 'Maximum model size in GB')
2774
+ .option('--min-size <gb>', 'Minimum model size in GB')
2775
+ .option('--quant <type>', 'Filter by quantization (Q4_K_M, Q5_K_M, Q8_0, etc.)')
2776
+ .option('--family <name>', 'Filter by model family (llama, qwen, mistral, etc.)')
2777
+ .option('-j, --json', 'Output as JSON')
2778
+ .action(async (query, options) => {
2779
+ if (!options.json) showAsciiArt('search');
2780
+ const SyncManager = require('../src/data/sync-manager');
2781
+ const IntelligentSelector = require('../src/models/intelligent-selector');
2782
+ const UnifiedDetector = require('../src/hardware/unified-detector');
2783
+
2784
+ const spinner = options.json ? null : ora('Searching models...').start();
2785
+
2786
+ try {
2787
+ // Detect hardware first to determine max size
2788
+ const detector = new UnifiedDetector();
2789
+ const hardware = await detector.detect();
2790
+ const hardwareMaxSize = detector.getMaxModelSize();
2791
+
2792
+ const syncManager = new SyncManager({ onProgress: () => {} });
2793
+ await syncManager.init();
2794
+
2795
+ // Check if we need to sync first
2796
+ const syncStatus = await syncManager.needsSync();
2797
+ if (syncStatus.needed && !options.json) {
2798
+ spinner.text = 'Database needs sync, running quick check...';
2799
+ }
2800
+
2801
+ // Use user-provided maxSize or hardware-detected max
2802
+ const effectiveMaxSize = options.maxSize
2803
+ ? parseFloat(options.maxSize)
2804
+ : hardwareMaxSize + 2; // Add some headroom
2805
+
2806
+ // Search for variants in database
2807
+ const searchResults = await syncManager.searchVariants(query, {
2808
+ maxSize: effectiveMaxSize,
2809
+ minSize: options.minSize ? parseFloat(options.minSize) : null,
2810
+ quant: options.quant,
2811
+ family: options.family,
2812
+ limit: parseInt(options.limit) * 5 // Get more for scoring
2813
+ });
2814
+
2815
+ if (searchResults.length === 0) {
2816
+ if (spinner) spinner.info('No models found matching your query');
2817
+ syncManager.close();
2818
+ return;
2819
+ }
2820
+
2821
+ // Score with intelligent selector (reuse detector from above)
2822
+ const selector = new IntelligentSelector({ detector });
2823
+
2824
+ const recommendations = await selector.recommend(searchResults, {
2825
+ useCase: options.useCase,
2826
+ limit: parseInt(options.limit)
2827
+ });
2828
+
2829
+ syncManager.close();
2830
+
2831
+ if (options.json) {
2832
+ console.log(JSON.stringify(recommendations, null, 2));
2833
+ return;
2834
+ }
2835
+
2836
+ if (spinner) spinner.succeed(`Found ${recommendations.meta.afterFiltering} matching models`);
2837
+
2838
+ // Display results
2839
+ console.log(chalk.blue.bold('\nSearch Results for: ') + chalk.white(query));
2840
+ console.log(chalk.gray(`Hardware: ${recommendations.hardware.description}`));
2841
+ console.log(chalk.gray(`Max model size: ${recommendations.hardware.maxSize}GB`));
2842
+ console.log('');
2843
+
2844
+ for (const item of recommendations.all) {
2845
+ const v = item.variant;
2846
+ const s = item.score;
2847
+
2848
+ // Format model name (tag already contains model:variant format)
2849
+ const fullTag = v.tag || 'latest';
2850
+ const displayName = fullTag.includes(':') ? fullTag : `${v.model_id || v.modelId}:${fullTag}`;
2851
+
2852
+ const scoreColor = s.final >= 80 ? chalk.green : s.final >= 60 ? chalk.yellow : chalk.red;
2853
+
2854
+ console.log(
2855
+ scoreColor(`[${s.final}]`) + ' ' +
2856
+ chalk.white.bold(displayName)
2857
+ );
2858
+ console.log(
2859
+ chalk.gray(` ${v.params_b || v.paramsB || '?'}B params, `) +
2860
+ chalk.gray(`${v.size_gb || v.sizeGB || '?'}GB, `) +
2861
+ chalk.gray(`${v.quant || 'Q4_K_M'}, `) +
2862
+ chalk.cyan(`~${s.meta.estimatedTPS} tok/s`)
2863
+ );
2864
+ console.log(
2865
+ chalk.gray(` Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit} C:${s.components.context}`)
2866
+ );
2867
+ console.log(chalk.cyan(` ollama pull ${displayName}`));
2868
+ console.log('');
2869
+ }
2870
+
2871
+ // Show insights
2872
+ if (recommendations.insights.length > 0) {
2873
+ console.log(chalk.blue.bold('Insights:'));
2874
+ for (const insight of recommendations.insights) {
2875
+ const icon = insight.type === 'success' ? '[OK]' : insight.type === 'warning' ? '[!]' : '[i]';
2876
+ const color = insight.type === 'success' ? chalk.green : insight.type === 'warning' ? chalk.yellow : chalk.cyan;
2877
+ console.log(color(` ${icon} ${insight.message}`));
2878
+ }
2879
+ }
2880
+
2881
+ } catch (error) {
2882
+ if (spinner) spinner.fail('Search failed');
2883
+ console.error(chalk.red('Error:'), error.message);
2884
+ if (process.env.DEBUG) console.error(error.stack);
2885
+ process.exit(1);
2886
+ }
2887
+ });
2888
+
2889
+ program
2890
+ .command('smart-recommend')
2891
+ .description('Get intelligent model recommendations using the new scoring engine')
2892
+ .option('-u, --use-case <case>', 'Optimize for use case', 'general')
2893
+ .option('-l, --limit <n>', 'Maximum number of recommendations', '5')
2894
+ .option('--target-tps <n>', 'Target tokens per second', '20')
2895
+ .option('--target-context <n>', 'Target context length', '8192')
2896
+ .option('--include-vision', 'Include vision/multimodal models')
2897
+ .option('--include-embeddings', 'Include embedding models')
2898
+ .option('-j, --json', 'Output as JSON')
2899
+ .action(async (options) => {
2900
+ if (!options.json) showAsciiArt('smart-recommend');
2901
+ const SyncManager = require('../src/data/sync-manager');
2902
+ const IntelligentSelector = require('../src/models/intelligent-selector');
2903
+ const UnifiedDetector = require('../src/hardware/unified-detector');
2904
+
2905
+ const spinner = options.json ? null : ora('Analyzing hardware and models...').start();
2906
+
2907
+ try {
2908
+ // Detect hardware
2909
+ const detector = new UnifiedDetector();
2910
+ const hardware = await detector.detect();
2911
+
2912
+ if (spinner) spinner.text = 'Loading model database...';
2913
+
2914
+ // Load models from database
2915
+ const syncManager = new SyncManager({ onProgress: () => {} });
2916
+ await syncManager.init();
2917
+
2918
+ const syncStatus = await syncManager.needsSync();
2919
+ if (syncStatus.needed) {
2920
+ if (spinner) spinner.text = 'Syncing model database (first time takes a few minutes)...';
2921
+ await syncManager.sync();
2922
+ }
2923
+
2924
+ // Get all variants that might fit
2925
+ const maxSize = detector.getMaxModelSize() + 2;
2926
+ const variants = await syncManager.getCompatibleVariants(maxSize, {});
2927
+
2928
+ if (spinner) spinner.text = `Scoring ${variants.length} model variants...`;
2929
+
2930
+ // Get intelligent recommendations
2931
+ const selector = new IntelligentSelector({ detector });
2932
+ const recommendations = await selector.recommend(variants, {
2933
+ useCase: options.useCase,
2934
+ targetTPS: parseInt(options.targetTps) || 20,
2935
+ targetContext: parseInt(options.targetContext) || 8192,
2936
+ includeVision: options.includeVision,
2937
+ includeEmbeddings: options.includeEmbeddings,
2938
+ limit: parseInt(options.limit)
2939
+ });
2940
+
2941
+ syncManager.close();
2942
+
2943
+ if (options.json) {
2944
+ console.log(JSON.stringify(recommendations, null, 2));
2945
+ return;
2946
+ }
2947
+
2948
+ if (spinner) spinner.succeed('Analysis complete!');
2949
+
2950
+ // Display hardware info
2951
+ console.log(chalk.blue.bold('\n=== Hardware Analysis ==='));
2952
+ console.log(chalk.white(` ${recommendations.hardware.description}`));
2953
+ console.log(chalk.gray(` Tier: ${recommendations.hardware.tier.replace('_', ' ').toUpperCase()}`));
2954
+ console.log(chalk.gray(` Backend: ${recommendations.hardware.backend}`));
2955
+ console.log(chalk.gray(` Max model size: ${recommendations.hardware.maxSize}GB`));
2956
+
2957
+ // Display top picks
2958
+ console.log(chalk.blue.bold('\n=== Top Recommendations ==='));
2959
+
2960
+ // Helper to format model name (tag already contains model:variant)
2961
+ const formatModelName = (v) => {
2962
+ const fullTag = v.tag || 'latest';
2963
+ return fullTag.includes(':') ? fullTag : `${v.model_id}:${fullTag}`;
2964
+ };
2965
+
2966
+ const picks = recommendations.topPicks;
2967
+ if (picks.best) {
2968
+ const v = picks.best.variant;
2969
+ const s = picks.best.score;
2970
+ const name = formatModelName(v);
2971
+ console.log(chalk.green.bold('\n[BEST] Best Overall:'));
2972
+ console.log(chalk.white.bold(` ${name}`));
2973
+ console.log(chalk.gray(` ${v.params_b || '?'}B params | ${v.size_gb || '?'}GB | ${v.quant || 'Q4_K_M'}`));
2974
+ console.log(chalk.cyan(` Score: ${s.final}/100 (Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit})`));
2975
+ console.log(chalk.yellow(` ~${s.meta.estimatedTPS} tokens/sec`));
2976
+ console.log(chalk.cyan(` ollama pull ${name}`));
2977
+ }
2978
+
2979
+ if (picks.fast && picks.fast !== picks.best) {
2980
+ const v = picks.fast.variant;
2981
+ const s = picks.fast.score;
2982
+ const name = formatModelName(v);
2983
+ console.log(chalk.blue.bold('\n⚡ Fastest:'));
2984
+ console.log(chalk.white(` ${name}`));
2985
+ console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | ~${s.meta.estimatedTPS} tok/s`));
2986
+ console.log(chalk.cyan(` ollama pull ${name}`));
2987
+ }
2988
+
2989
+ if (picks.quality && picks.quality !== picks.best) {
2990
+ const v = picks.quality.variant;
2991
+ const s = picks.quality.score;
2992
+ const name = formatModelName(v);
2993
+ console.log(chalk.magenta.bold('\nHighest Quality:'));
2994
+ console.log(chalk.white(` ${name}`));
2995
+ console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | Quality: ${s.components.quality}/100`));
2996
+ console.log(chalk.cyan(` ollama pull ${name}`));
2997
+ }
2998
+
2999
+ // Show other recommendations
3000
+ if (recommendations.all.length > 1) {
3001
+ console.log(chalk.blue.bold('\n=== Other Good Options ==='));
3002
+ for (const item of recommendations.all.slice(1, parseInt(options.limit))) {
3003
+ const v = item.variant;
3004
+ const s = item.score;
3005
+ const name = formatModelName(v);
3006
+ console.log(
3007
+ chalk.gray(`[${s.final}] `) +
3008
+ chalk.white(name) +
3009
+ chalk.gray(` - ${v.params_b || '?'}B, ${v.size_gb || '?'}GB`)
3010
+ );
3011
+ }
3012
+ }
3013
+
3014
+ // Show insights
3015
+ if (recommendations.insights.length > 0) {
3016
+ console.log(chalk.blue.bold('\n=== Insights ==='));
3017
+ for (const insight of recommendations.insights) {
3018
+ const icon = insight.type === 'success' ? chalk.green('[OK]') :
3019
+ insight.type === 'warning' ? chalk.yellow('[!]') :
3020
+ insight.type === 'tip' ? chalk.cyan('[TIP]') : chalk.blue('[i]');
3021
+ console.log(` ${icon} ${insight.message}`);
3022
+ }
3023
+ }
3024
+
3025
+ console.log('');
3026
+
3027
+ } catch (error) {
3028
+ if (spinner) spinner.fail('Recommendation failed');
3029
+ console.error(chalk.red('Error:'), error.message);
3030
+ if (process.env.DEBUG) console.error(error.stack);
3031
+ process.exit(1);
3032
+ }
3033
+ });
3034
+
3035
+ program
3036
+ .command('hw-detect')
3037
+ .description('Detect and display detailed hardware capabilities')
3038
+ .option('-j, --json', 'Output as JSON')
3039
+ .action(async (options) => {
3040
+ if (!options.json) showAsciiArt('hw-detect');
3041
+ const UnifiedDetector = require('../src/hardware/unified-detector');
3042
+
3043
+ const spinner = options.json ? null : ora('Detecting hardware...').start();
3044
+
3045
+ try {
3046
+ const detector = new UnifiedDetector();
3047
+ const hardware = await detector.detect();
3048
+
3049
+ if (options.json) {
3050
+ console.log(JSON.stringify(hardware, null, 2));
3051
+ return;
3052
+ }
3053
+
3054
+ if (spinner) spinner.succeed('Hardware detected!');
3055
+
3056
+ console.log(chalk.blue.bold('\n=== Hardware Detection ===\n'));
3057
+
3058
+ // Summary
3059
+ console.log(chalk.white.bold('Summary:'));
3060
+ console.log(` ${detector.getHardwareDescription()}`);
3061
+ console.log(` Tier: ${chalk.cyan(detector.getHardwareTier().replace('_', ' ').toUpperCase())}`);
3062
+ console.log(` Max model size: ${chalk.green(detector.getMaxModelSize() + 'GB')}`);
3063
+ console.log(` Best backend: ${chalk.cyan(hardware.summary.bestBackend)}`);
3064
+
3065
+ // CPU
3066
+ if (hardware.cpu) {
3067
+ console.log(chalk.blue.bold('\nCPU:'));
3068
+ console.log(` ${hardware.cpu.brand}`);
3069
+ console.log(` Cores: ${hardware.cpu.cores.logical} (${hardware.cpu.cores.physical} physical)`);
3070
+ console.log(` SIMD: ${hardware.cpu.capabilities.bestSimd}`);
3071
+ if (hardware.cpu.capabilities.avx512) console.log(chalk.green(' [OK] AVX-512'));
3072
+ if (hardware.cpu.capabilities.avx2) console.log(chalk.green(' [OK] AVX2'));
3073
+ if (hardware.cpu.capabilities.neon) console.log(chalk.green(' [OK] ARM NEON'));
3074
+ }
3075
+
3076
+ // GPU backends
3077
+ for (const [backend, info] of Object.entries(hardware.backends)) {
3078
+ if (!info.available || backend === 'cpu') continue;
3079
+
3080
+ console.log(chalk.blue.bold(`\n${backend.toUpperCase()}:`));
3081
+
3082
+ if (backend === 'metal' && info.info) {
3083
+ console.log(` ${info.info.chip}`);
3084
+ console.log(` GPU Cores: ${info.info.gpu.cores}`);
3085
+ console.log(` Unified Memory: ${info.info.memory.unified}GB`);
3086
+ console.log(` Memory Bandwidth: ${info.info.memory.bandwidth}GB/s`);
3087
+ }
3088
+
3089
+ if (backend === 'cuda' && info.info) {
3090
+ console.log(` Driver: ${info.info.driver}`);
3091
+ console.log(` CUDA: ${info.info.cuda}`);
3092
+ console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
3093
+ for (const gpu of info.info.gpus) {
3094
+ console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
3095
+ }
3096
+ }
3097
+
3098
+ if (backend === 'rocm' && info.info) {
3099
+ console.log(` ROCm: ${info.info.rocmVersion}`);
3100
+ console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
3101
+ for (const gpu of info.info.gpus) {
3102
+ console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
3103
+ }
3104
+ }
3105
+ }
3106
+
3107
+ console.log(chalk.gray(`\nFingerprint: ${hardware.fingerprint}`));
3108
+ console.log('');
3109
+
3110
+ } catch (error) {
3111
+ if (spinner) spinner.fail('Detection failed');
3112
+ console.error(chalk.red('Error:'), error.message);
3113
+ if (process.env.DEBUG) console.error(error.stack);
3114
+ process.exit(1);
3115
+ }
3116
+ });
3117
+
3118
+ program.parse();