llm-checker/bin/enhanced_cli.js
signerless 2bfd8fdc4c feat(quality): ship benchmark scores in the snapshot and add model-card results
- `sync:seed` runs scripts/update-quality-seed.js, which refreshes every
  quality source inside the snapshot (a failed source keeps its previous
  scores). A fresh install had no benchmark data and ranked every model by
  parameter count until the user ran `quality-sync`.
- Existing databases import a snapshot's quality source when it is newer
  than their own copy, and rebuild the catalog cohort.
- New `hf_eval_results` source: GPQA, MMLU-Pro, HLE, SWE-bench,
  Terminal-Bench, GSM8K, AIME/HMMT, IFEval and MMMU results attached to Hub
  repos. Self-reported, marked not independent, last in each category's
  metric preference.
- LiveBench discovers releases from the site bundle and adds those newer
  than the curated list; older question sets stay out.
2026-10-04 15:38:10 +00:00

6679 lines
293 KiB
JavaScript
Executable file

#!/usr/bin/env node
const { Command } = require('commander');
const chalk = require('chalk');
const ora = require('ora');
const { table } = require('table');
const os = require('os');
const readline = require('readline');
const { spawn } = require('child_process');
// LLMChecker is loaded lazily to avoid slow systeminformation init
let _LLMChecker = null;
function getLLMChecker() {
if (!_LLMChecker) {
_LLMChecker = require('../src/index');
}
return _LLMChecker;
}
const { getLogger } = require('../src/utils/logger');
const fs = require('fs');
const path = require('path');
const { normalizePlatform, isTermuxEnvironment } = require('../src/utils/platform');
const { resolveCpuOnlyMode } = require('../src/hardware/cpu-only');
const {
SUPPORTED_RUNTIMES,
normalizeRuntime,
runtimeSupportedOnHardware,
getRuntimeDisplayName,
getRuntimeCommandSet
} = require('../src/runtime/runtime-support');
const { evaluateFineTuningSupport } = require('../src/models/fine-tuning-support');
const { filterModelsBySafety } = require('../src/models/model-safety');
const { CalibrationManager } = require('../src/calibration/calibration-manager');
const { SUPPORTED_CALIBRATION_OBJECTIVES } = require('../src/calibration/schemas');
const {
resolveRoutingPolicyPreference,
normalizeTaskName,
inferTaskFromPrompt,
resolveCalibrationRoute,
getRouteModelCandidates,
selectModelFromRoute
} = require('../src/calibration/policy-routing');
const SpeculativeDecodingEstimator = require('../src/models/speculative-decoding-estimator');
const PolicyManager = require('../src/policy/policy-manager');
const PolicyEngine = require('../src/policy/policy-engine');
const {
collectCandidatesFromAnalysis,
collectCandidatesFromRecommendationData,
buildPolicyRuntimeContext,
evaluatePolicyCandidates,
evaluatePolicyCandidatesAsync,
resolvePolicyEnforcement
} = require('../src/policy/cli-policy');
const {
buildComplianceReport,
serializeComplianceReport
} = require('../src/policy/audit-reporter');
const { estimateTokenSpeedFromHardware } = require('../src/utils/token-speed-estimator');
const { renderCommandHeader, renderPersistentBanner } = require('../src/ui/cli-theme');
const { launchInteractivePanel } = require('../src/ui/interactive-panel');
const policyManager = new PolicyManager();
const calibrationManager = new CalibrationManager();
const COMMAND_HEADER_LABELS = {
'hw-detect': 'Hardware Detection',
'smart-recommend': 'Smart Recommend (Experimental)',
search: 'Model Search',
sync: 'Database Sync',
'registry-sync': 'Model Registry Sync',
'registry-search': 'Model Registry Search',
'registry-recommend': 'Registry Recommendations',
'mcp-setup': 'MCP Setup',
check: 'Compatibility Check',
installed: 'Installed Models',
'ai-check': 'AI Check',
'ai-run': 'AI Run',
demo: 'Demo',
ollama: 'Ollama Integration',
recommend: 'Recommendations',
simulate: 'Hardware Simulation',
'list-models': 'Model Catalog'
};
// Kept as function name for backwards compatibility in command handlers.
function showAsciiArt(command) {
const label = COMMAND_HEADER_LABELS[command] || command;
renderCommandHeader(label);
}
const RECOMMENDATION_COMMAND_NOTES = {
check: 'Compatibility report: shows hardware fit first. Use `llm-checker recommend` for canonical ranked model picks.',
recommend: 'Canonical recommendations: deterministic hardware-aware selector by category.',
'smart-recommend': 'Experimental scoring engine: results can differ from `recommend` while this path is being unified.'
};
function displayRecommendationCommandNote(command) {
const note = RECOMMENDATION_COMMAND_NOTES[command];
if (!note) return;
console.log(chalk.gray(`Recommendation mode: ${note}`));
console.log('');
}
function getCpuOnlyCommandMode(options = {}) {
return resolveCpuOnlyMode(options.cpuOnly ? true : undefined);
}
function displayCpuOnlyModeNotice(cpuOnly, simulated = false) {
if (!cpuOnly) return;
const source = simulated ? 'Simulated CPU and RAM are retained; simulated GPU is diagnostic only.' : 'Detected GPU inventory is diagnostic only.';
console.log(chalk.yellow.bold('\n CPU-ONLY MODE: GPU acceleration and VRAM are disabled.'));
console.log(chalk.gray(` ${source}\n`));
}
// Function to search Ollama models by use case
function getOllamaCacheFile(filename) {
try {
const homePath = path.join(os.homedir(), '.llm-checker', 'cache', 'ollama', filename);
const legacyPath = path.join(__dirname, '../src/ollama/.cache', filename);
if (fs.existsSync(homePath)) return homePath;
if (fs.existsSync(legacyPath)) return legacyPath;
return homePath; // default preferred path
} catch {
return path.join(__dirname, '../src/ollama/.cache', filename);
}
}
function searchOllamaModelsForUseCase(useCase, hardware) {
try {
const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
if (!fs.existsSync(cacheFile)) return [];
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const models = cacheData.models || [];
// Filter models by use case with typo tolerance
const useCaseModels = models.filter(model => {
const lowerUseCase = useCase.toLowerCase();
switch (lowerUseCase) {
case 'creative':
case 'writing':
return model.primary_category === 'creative';
case 'coding':
case 'code':
return model.primary_category === 'coding';
case 'chat':
case 'conversation':
case 'talking':
return model.primary_category === 'chat';
case 'multimodal':
case 'vision':
return model.primary_category === 'multimodal';
case 'embeddings':
case 'embedings': // typo tolerance
case 'embedding':
case 'embeding': // typo tolerance
return model.primary_category === 'embeddings';
case 'reasoning':
case 'reason':
return model.primary_category === 'reasoning';
default:
// Check for partial matches
if (lowerUseCase.includes('embed')) return model.primary_category === 'embeddings';
if (lowerUseCase.includes('code')) return model.primary_category === 'coding';
if (lowerUseCase.includes('creat')) return model.primary_category === 'creative';
if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) return model.primary_category === 'chat';
if (lowerUseCase.includes('vision') || lowerUseCase.includes('multimodal')) return model.primary_category === 'multimodal';
if (lowerUseCase.includes('reason')) return model.primary_category === 'reasoning';
return false;
}
});
// Convert Ollama models to compatible format and add basic compatibility scoring
return useCaseModels.map(model => {
// Find a suitable variant (prefer 7b-13b for high-end hardware)
let bestVariant = null;
if (model.variants && model.variants.length > 0) {
// For high-tier hardware, prefer 7B-13B models
bestVariant = model.variants.find(v =>
v.real_size_gb >= 3 && v.real_size_gb <= 15 &&
!v.tag.includes('-instruct') && !v.tag.includes('-code')
) || model.variants[0];
}
const size = bestVariant ? bestVariant.real_size_gb : 7;
const ollamaTag = bestVariant ? bestVariant.tag : model.model_identifier + ':latest';
return {
name: model.model_name || model.model_identifier,
size: size + 'GB',
type: 'ollama',
category: model.primary_category,
specialization: model.primary_category,
primary_category: model.primary_category,
categories: model.categories,
requirements: {
ram: Math.max(4, Math.ceil(size * 1.2)),
vram: 0,
cpu_cores: 2,
storage: size,
recommended_ram: Math.max(8, Math.ceil(size * 1.5))
},
frameworks: ['ollama'],
performance: {
speed: size <= 7 ? 'fast' : size <= 13 ? 'medium' : 'slow',
quality: model.primary_category === 'coding' ? 'excellent_for_code' :
model.primary_category === 'creative' ? 'excellent_for_creative' : 'good',
context_length: 4096,
tokens_per_second_estimate: size <= 7 ? '30-50' : '15-30'
},
installation: {
ollama: `ollama pull ${ollamaTag}`,
description: model.detailed_description || model.description || `${model.primary_category} model`
},
ollamaId: model.model_identifier,
ollamaTag: ollamaTag,
source: 'ollama_database',
// Basic compatibility score (can be improved)
score: calculateBasicCompatibilityScore(size, hardware),
isOllamaInstalled: false,
ollamaAvailable: true
};
}).slice(0, 10); // Limit to top 10 models
} catch (error) {
console.warn('Error searching Ollama models:', error.message);
return [];
}
}
// Basic compatibility scoring for Ollama models
function calculateBasicCompatibilityScore(modelSizeGB, hardware) {
const totalRAM = hardware.memory?.total || 8;
const availableRAM = totalRAM * 0.8; // Assume 80% available
// RAM compatibility
let ramScore = 0;
if (modelSizeGB * 1.5 <= availableRAM) {
ramScore = 100;
} else if (modelSizeGB <= availableRAM) {
ramScore = 80;
} else {
ramScore = Math.max(0, 50 - (modelSizeGB - availableRAM) * 10);
}
// Size efficiency (prefer 7B-13B for high-end hardware)
let sizeScore = 100;
if (totalRAM >= 16) { // High-end hardware
if (modelSizeGB >= 7 && modelSizeGB <= 13) {
sizeScore = 100;
} else if (modelSizeGB < 7) {
sizeScore = 85; // Small models are okay but not optimal
} else {
sizeScore = Math.max(60, 100 - (modelSizeGB - 13) * 5);
}
}
return Math.round((ramScore * 0.7 + sizeScore * 0.3));
}
// Function to get real size directly from Ollama cache
function estimateModelSize(model) {
// Extract parameter count from model name (e.g., "3B", "7B", "13B")
const nameMatch = model.name.match(/(\d+\.?\d*)[bB]\b/i);
if (nameMatch) {
const paramCount = parseFloat(nameMatch[1]);
// Estimate size using Q4_K_M quantization (~0.5 bytes per parameter + overhead)
const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
return `~${estimatedGB}GB (Q4_K_M)`;
}
// Try to extract from model identifier or fallback patterns
if (model.model_identifier) {
const idMatch = model.model_identifier.match(/(\d+\.?\d*)b/i);
if (idMatch) {
const paramCount = parseFloat(idMatch[1]);
const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
return `~${estimatedGB}GB (Q4_K_M)`;
}
}
// Known model size patterns
const sizeMappings = {
'tinyllama': '~1.1GB (Q4_K_M)',
'mobilellama': '~1.4GB (Q4_K_M)',
'phi': '~2.7GB (Q4_K_M)',
'gemma': '~5.3GB (Q4_K_M)',
'llama.*3b': '~2.0GB (Q4_K_M)',
'llama.*7b': '~4.4GB (Q4_K_M)',
'llama.*13b': '~7.8GB (Q4_K_M)',
'dolphincoder': '~4.2GB (Q4_K_M)',
'deepseek-coder': '~4.0GB (Q4_K_M)',
'starcoder': '~8.4GB (Q4_K_M)'
};
const modelNameLower = (model.name || '').toLowerCase();
for (const [pattern, size] of Object.entries(sizeMappings)) {
if (new RegExp(pattern, 'i').test(modelNameLower)) {
return size;
}
}
// If we have size field but it's not formatted well
if (model.size && typeof model.size === 'string') {
const sizeMatch = model.size.match(/(\d+\.?\d*)\s*(GB|MB|B)/i);
if (sizeMatch) {
const num = parseFloat(sizeMatch[1]);
const unit = sizeMatch[2].toUpperCase();
if (unit === 'GB') return `${num}GB`;
if (unit === 'MB') return `${Math.round(num / 1024 * 10) / 10}GB`;
}
}
// Final fallback
return '~4.5GB (estimated)';
}
function getRealSizeFromOllamaCache(model) {
try {
const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
if (!fs.existsSync(cacheFile)) return null;
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const models = cacheData.models || [];
// Try to find the model by different strategies
let targetModel = null;
// Strategy 1: Match by ollamaId directly (e.g., "codellama")
if (model.ollamaId) {
// Special case: if looking for phind-codellama but model is actually CodeLlama, use codellama instead
if (model.ollamaId === 'phind-codellama' &&
(model.name.toLowerCase().includes('codellama') || model.name.toLowerCase().includes('code llama'))) {
targetModel = models.find(m => m.model_identifier === 'codellama');
}
// Special case: DeepSeek Coder has wrong ollamaId
else if (model.ollamaId === 'deepseek-v2.5' &&
model.name.toLowerCase().includes('deepseek') &&
model.name.toLowerCase().includes('coder')) {
targetModel = models.find(m => m.model_identifier === 'deepseek-coder');
}
// Special case: TinyLlama incorrectly mapped to llama-pro
else if (model.ollamaId === 'llama-pro' &&
model.name && model.name.toLowerCase().includes('tinyllama')) {
targetModel = models.find(m => m.model_identifier === 'tinyllama');
} else {
targetModel = models.find(m => m.model_identifier === model.ollamaId);
}
}
// Strategy 2: Match by name similarity
if (!targetModel && model.name) {
const modelNameLower = model.name.toLowerCase();
// Special handling for specific models - be very specific
if (modelNameLower.includes('deepseek') && modelNameLower.includes('coder')) {
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'deepseek-coder');
} else if (modelNameLower.includes('llama3.3')) {
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.3');
} else if (modelNameLower.includes('llama3.2')) {
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.2');
} else if (modelNameLower.includes('llama3.1') || modelNameLower.includes('llama 3.1')) {
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.1');
} else {
targetModel = models.find(m => {
const identifier = m.model_identifier.toLowerCase();
return identifier.includes('codellama') && modelNameLower.includes('codellama') ||
identifier.includes('qwen') && modelNameLower.includes('qwen') ||
identifier.includes('mistral') && modelNameLower.includes('mistral');
});
}
}
if (!targetModel || !targetModel.variants) return null;
// Extract size from model name (e.g., "CodeLlama 7B" -> "7b")
let targetSize = null;
if (model.size) {
targetSize = model.size.toLowerCase().replace('b', '') + 'b';
} else if (model.name) {
const sizeMatch = model.name.match(/(\d+\.?\d*)[bB]/);
if (sizeMatch) {
targetSize = sizeMatch[1] + 'b';
}
}
// Find the right variant
let variant = null;
if (targetSize) {
// Look for exact size match (e.g., "codellama:7b")
variant = targetModel.variants.find(v =>
v.tag.includes(':' + targetSize) &&
!v.tag.includes('-instruct') &&
!v.tag.includes(':code-') // Exclude variants like ":code-" but allow "coder"
);
}
// Fallback to latest or first variant
if (!variant) {
variant = targetModel.variants.find(v => v.tag.includes(':latest')) ||
targetModel.variants[0];
}
if (variant && variant.real_size_gb) {
return variant.real_size_gb + 'GB';
}
return null;
} catch (error) {
console.warn('Error reading Ollama cache:', error.message);
return null;
}
}
function parsePositiveNumberOption(value, fallback = null) {
if (value === undefined || value === null || value === '') return fallback;
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
// Allowed enum values for the registry commands. Invalid values must be rejected
// with a clear error instead of silently returning "no results" or falling back
// to the built-in catalog.
const REGISTRY_SOURCES = ['ollama', 'huggingface', 'gpt4all'];
const REGISTRY_FORMATS = ['gguf', 'safetensors', 'mlx', 'ollama', 'pytorch', 'pytorch_bin', 'ggml'];
const REGISTRY_RUNTIMES = ['auto', 'all', '*', 'ollama', 'llama.cpp', 'transformers', 'vllm', 'mlx'];
const REGISTRY_OPTIMIZE = ['balanced', 'speed', 'quality', 'context', 'coding'];
function assertRegistryEnum(label, value, allowed) {
if (value === undefined || value === null || value === '') return;
if (!allowed.includes(String(value).toLowerCase())) {
const shown = allowed.filter((v) => !['all', '*'].includes(v)).join(', ');
throw new Error(`Invalid --${label} "${value}". Allowed: ${shown}`);
}
}
// Throws on the first invalid registry enum option. Returns nothing on success.
function validateRegistryFilters(options = {}) {
assertRegistryEnum('source', options.source, REGISTRY_SOURCES);
assertRegistryEnum('format', options.format, REGISTRY_FORMATS);
assertRegistryEnum('runtime', options.runtime, REGISTRY_RUNTIMES);
assertRegistryEnum('optimize', options.optimize, REGISTRY_OPTIMIZE);
}
function truncateMiddle(value, maxLength = 48) {
const text = String(value || '');
if (text.length <= maxLength) return text;
if (maxLength <= 4) return text.slice(0, maxLength);
const head = Math.ceil((maxLength - 3) / 2);
const tail = Math.floor((maxLength - 3) / 2);
return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
}
function formatRegistryNumber(value, suffix = '') {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return '?';
const rounded = parsed >= 100 ? Math.round(parsed) : Math.round(parsed * 10) / 10;
return `${rounded}${suffix}`;
}
function formatRegistrySize(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return '?';
return `${Math.round(parsed * 100) / 100}GB`;
}
function formatRegistryList(value, maxItems = 3) {
const items = Array.isArray(value) ? value : [];
if (items.length === 0) return '-';
const shown = items.slice(0, maxItems).join(', ');
return items.length > maxItems ? `${shown}, +${items.length - maxItems}` : shown;
}
// Shared multi-source registry search used by `registry-search` and by
// `search`/`list-models` when run with `--registry`/`--source`. Searches the
// packaged registry of HF + Ollama + GPT4All artifacts (not just the Ollama
// catalog). Renders a table or JSON. Sets process.exitCode on validation error.
async function executeRegistrySearch(query = '', options = {}) {
try {
validateRegistryFilters(options);
} catch (validationError) {
if (options.json) {
console.log(JSON.stringify({ error: validationError.message }, null, 2));
} else {
console.error(chalk.red(`✗ ${validationError.message}`));
}
process.exitCode = 1;
return;
}
if (!options.json) showAsciiArt('registry-search');
const ModelDatabase = require('../src/data/model-database');
const database = new ModelDatabase();
try {
await database.initialize();
const filters = {
source: options.source,
format: options.format ? String(options.format).toLowerCase() : undefined,
runtime: options.runtime,
quantization: options.quant,
maxSizeGB: parsePositiveNumberOption(options.maxSize),
minParamsB: parsePositiveNumberOption(options.minParams),
maxParamsB: parsePositiveNumberOption(options.maxParams),
localOnly: Boolean(options.localOnly),
limit: parsePositiveNumberOption(options.limit, 20)
};
const databaseFilters = options.enforceDefaultSafety === true
? { ...filters, limit: undefined }
: filters;
const rawResults = database.searchModelArtifacts(query, databaseFilters);
const results = options.enforceDefaultSafety === true
? filterModelsBySafety(rawResults, {
includeUncensored: options.includeUncensored === true
}).slice(0, filters.limit)
: rawResults;
const stats = database.getRegistryStats();
if (options.json) {
console.log(JSON.stringify({ query, filters, count: results.length, stats, results }, null, 2));
return;
}
if (results.length === 0) {
console.log(chalk.yellow('No registry artifacts found.'));
if (stats.artifacts === 0) {
console.log(chalk.gray('Populate the registry first with: llm-checker registry-sync'));
}
return;
}
console.log(chalk.blue.bold('\nRegistry Results'));
console.log(chalk.gray(`Stored registry: ${stats.artifacts} artifacts across ${stats.sources} sources`));
console.log('');
const rows = [['Source', 'Model', 'Artifact', 'Params', 'Size', 'Format', 'Runtime', 'Install']];
for (const item of results) {
rows.push([
item.source_id,
truncateMiddle(item.canonical_model_id, 34),
truncateMiddle(item.artifact_name || item.filename, 34),
formatRegistryNumber(item.parameter_count_b, 'B'),
formatRegistrySize(item.size_gb),
item.quantization ? `${item.format}/${item.quantization}` : item.format,
formatRegistryList(item.runtime_support, 2),
truncateMiddle(item.install_command || item.download_url, 46)
]);
}
console.log(table(rows));
const links = results.filter((item) => item.download_url).slice(0, 5);
if (links.length > 0) {
console.log(chalk.blue.bold('Exact download links:'));
links.forEach((item, index) => {
console.log(chalk.gray(` ${index + 1}. ${item.canonical_model_id} -> ${item.download_url}`));
});
}
} catch (error) {
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exitCode = 1;
} finally {
database.close();
}
}
const program = new Command();
program
.name('llm-checker')
.description('Check which LLM models your computer can run')
.version(require('../package.json').version);
const logger = getLogger({ console: false });
function canRenderColoredHelp() {
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
if (process.env.NO_COLOR) return false;
if (process.env.FORCE_COLOR === '0') return false;
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
}
function colorizeHelpInformation(helpText) {
const raw = String(helpText || '');
if (!raw || !canRenderColoredHelp()) {
return raw;
}
const sectionColor = chalk.hex('#22D3EE').bold;
const usageLabelColor = chalk.hex('#60A5FA').bold;
const usageValueColor = chalk.hex('#F8FAFC').bold;
const optionColor = chalk.hex('#A7F3D0');
const commandColor = chalk.hex('#93C5FD').bold;
const placeholderColor = chalk.hex('#F59E0B');
const descriptionColor = chalk.hex('#D1D5DB');
const defaultColor = chalk.hex('#C7D2FE');
const colorizePlaceholders = (value) =>
String(value || '').replace(/(\[[^\]]+\]|<[^>]+>)/g, (token) => placeholderColor(token));
return raw
.split('\n')
.map((line) => {
if (!line.trim()) return line;
const usageMatch = line.match(/^(\s*)(Usage:)(\s*)(.+)$/);
if (usageMatch) {
return `${usageMatch[1]}${usageLabelColor(usageMatch[2])}${usageMatch[3]}${usageValueColor(usageMatch[4])}`;
}
const sectionMatch = line.match(/^(\s*)(Options:|Commands:|Enterprise policy examples:|Calibrated routing examples:)\s*$/);
if (sectionMatch) {
return `${sectionMatch[1]}${sectionColor(sectionMatch[2])}`;
}
if (/^\s*\$ /.test(line.trimStart())) {
return line.replace(/\$ .+$/, (commandText) => chalk.hex('#60A5FA')(commandText));
}
if (/^\s*-/.test(line)) {
const optionLine = line.match(/^(\s+)(.+?)(\s{2,})(.+)$/);
if (optionLine) {
return (
optionLine[1] +
optionColor(colorizePlaceholders(optionLine[2])) +
optionLine[3] +
descriptionColor(optionLine[4])
);
}
}
if (/^\s+[a-z0-9]/i.test(line)) {
const commandLine = line.match(/^(\s+)(.+?)(\s{2,})(.+)$/);
if (commandLine) {
return (
commandLine[1] +
commandColor(colorizePlaceholders(commandLine[2])) +
commandLine[3] +
descriptionColor(commandLine[4])
);
}
}
return defaultColor(line);
})
.join('\n');
}
function findCommandByName(commandName) {
const requested = String(commandName || '').trim();
if (!requested) return null;
return program.commands.find((cmd) => {
if (cmd.name() === requested) return true;
try {
return cmd.aliases().includes(requested);
} catch {
return false;
}
}) || null;
}
if (!program.commands.some((cmd) => cmd.name() === 'help')) {
program
.command('help [command]')
.description('Show all commands and how to use them')
.action((commandName) => {
renderPersistentBanner();
console.log('');
if (!commandName) {
console.log(colorizeHelpInformation(program.helpInformation()));
return;
}
const target = findCommandByName(commandName);
if (!target) {
const available = program.commands
.map((cmd) => cmd.name())
.filter((name) => name !== 'help')
.sort((a, b) => a.localeCompare(b))
.join(', ');
console.error(chalk.red(`Unknown command: ${commandName}`));
console.log(chalk.gray(`Available commands: ${available}`));
process.exitCode = 1;
return;
}
console.log(colorizeHelpInformation(target.helpInformation()));
});
}
// Ollama installation helper
function getOllamaInstallInstructions() {
const rawPlatform = os.platform();
const platform = normalizePlatform(rawPlatform);
const arch = os.arch();
if (isTermuxEnvironment(rawPlatform, process.env)) {
return {
name: `Termux (Android${arch ? ` ${arch}` : ''})`,
downloadUrl: 'https://github.com/termux/termux-packages',
instructions: [
'1. Update Termux packages: pkg update',
'2. Install Ollama from the Termux repository: pkg install ollama',
'3. Start Ollama in the current shell: ollama serve',
'4. In a new Termux session, test with: ollama run llama3.2:1b'
],
alternativeInstall: 'pkg install ollama'
};
}
const instructions = {
'darwin': {
name: 'macOS',
downloadUrl: 'https://ollama.com/download/mac',
instructions: [
'1. Download Ollama for macOS from the link above',
'2. Open the downloaded .pkg file and follow the installer',
'3. Once installed, open Terminal and run: ollama serve',
'4. In a new terminal window, test with: ollama run llama2:7b'
],
alternativeInstall: 'brew install ollama'
},
'win32': {
name: 'Windows',
downloadUrl: 'https://ollama.com/download/windows',
instructions: [
'1. Download Ollama for Windows from the link above',
'2. Run the downloaded installer (.exe file)',
'3. Open Command Prompt or PowerShell',
'4. Test with: ollama run llama2:7b'
],
alternativeInstall: 'winget install Ollama.Ollama'
},
'linux': {
name: 'Linux',
downloadUrl: 'https://ollama.com/download/linux',
instructions: [
'1. Review official installation options:',
' https://github.com/ollama/ollama/blob/main/docs/linux.md',
'2. Prefer a package manager (apt/dnf/pacman) when available',
'3. Start Ollama after install:',
' ollama serve',
'4. Test with: ollama run llama2:7b'
],
alternativeInstall: 'Manual install: https://github.com/ollama/ollama/blob/main/docs/linux.md'
}
};
return instructions[platform] || instructions['linux'];
}
function displayOllamaInstallHelp() {
const installInfo = getOllamaInstallInstructions();
console.log(chalk.red.bold('\nOllama is not installed or not running!'));
console.log(chalk.yellow('\nLLM Checker requires Ollama to function properly.'));
console.log(chalk.cyan.bold(`\nInstall Ollama for ${installInfo.name}:`));
console.log(chalk.blue(`\nDownload: ${installInfo.downloadUrl}`));
console.log(chalk.green.bold('\nInstallation Steps:'));
installInfo.instructions.forEach(step => {
console.log(chalk.gray(` ${step}`));
});
if (installInfo.alternativeInstall) {
console.log(chalk.magenta.bold('\nQuick Install (if available):'));
console.log(chalk.white(` ${installInfo.alternativeInstall}`));
}
console.log(chalk.yellow.bold('\nAfter installation:'));
console.log(chalk.gray(' 1. Restart your terminal'));
console.log(chalk.gray(' 2. Run: llm-checker check'));
console.log(chalk.gray(' 3. Start using the AI model selector!'));
console.log(chalk.cyan('\nNeed help? Visit: https://github.com/ollama/ollama'));
}
async function checkOllamaAndExit() {
const spinner = ora('Checking Ollama availability...').start();
try {
// Quick check if ollama command exists
const checkCommand = os.platform() === 'win32' ? 'where' : 'which';
return new Promise((resolve) => {
const proc = spawn(checkCommand, ['ollama'], { stdio: 'pipe' });
proc.on('close', (code) => {
spinner.stop();
if (code !== 0) {
displayOllamaInstallHelp();
process.exit(1);
}
resolve(true);
});
proc.on('error', () => {
spinner.stop();
displayOllamaInstallHelp();
process.exit(1);
});
});
} catch (error) {
spinner.stop();
displayOllamaInstallHelp();
process.exit(1);
}
}
function quoteCliArg(value) {
const stringValue = String(value ?? '');
if (!stringValue) return '""';
if (/^[A-Za-z0-9._:/=-]+$/.test(stringValue)) return stringValue;
return `"${stringValue.replace(/(["\\$`])/g, '\\$1')}"`;
}
function getClaudeDesktopConfigPath() {
const homeDir = os.homedir();
if (process.platform === 'darwin') {
return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
}
if (process.platform === 'win32') {
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
return path.join(appData, 'Claude', 'claude_desktop_config.json');
}
return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json');
}
function buildClaudeMcpSetup(useNpx = false, serverName = 'llm-checker') {
const normalizedServerName = String(serverName || 'llm-checker').trim() || 'llm-checker';
const runner = useNpx
? ['npx', '--yes', '--package', 'llm-checker', 'llm-checker-mcp']
: ['llm-checker-mcp'];
const claudeArgs = ['mcp', 'add', normalizedServerName, '--', ...runner];
const commandLine = ['claude', ...claudeArgs].map(quoteCliArg).join(' ');
const desktopServerConfig = useNpx
? { command: 'npx', args: ['--yes', '--package', 'llm-checker', 'llm-checker-mcp'] }
: { command: 'llm-checker-mcp', args: [] };
return {
serverName: normalizedServerName,
useNpx: Boolean(useNpx),
claudeArgs,
commandLine,
desktopConfigPath: getClaudeDesktopConfigPath(),
desktopConfig: {
mcpServers: {
[normalizedServerName]: desktopServerConfig
}
}
};
}
async function runExternalCommand(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'inherit',
env: process.env
});
child.on('error', reject);
child.on('close', (code) => resolve(code));
});
}
// ---------------------------------------------------------------------------
// Multi-client MCP setup helpers (mcp-setup --client <name>)
// ---------------------------------------------------------------------------
const MCP_SETUP_CLIENTS = ['claude', 'codex', 'cursor', 'windsurf', 'gemini', 'kimi', 'grok', 'generic'];
// The stdio server entry every client launches: the globally installed
// `llm-checker-mcp` binary, or an explicit `npx --package llm-checker`
// invocation with --npx. `llm-checker-mcp` is a bin exposed by the
// `llm-checker` package, not a standalone npm package name.
function getMcpServerEntry(useNpx = false) {
return useNpx
? { command: 'npx', args: ['--yes', '--package', 'llm-checker', 'llm-checker-mcp'] }
: { command: 'llm-checker-mcp', args: [] };
}
function getCodexConfigPath() {
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
return path.join(codexHome, 'config.toml');
}
function getCursorMcpConfigPath() {
return path.join(os.homedir(), '.cursor', 'mcp.json');
}
function getWindsurfMcpConfigPath() {
return path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
}
function getGeminiConfigPath() {
return path.join(os.homedir(), '.gemini', 'settings.json');
}
function getKimiMcpConfigPath() {
const homeDir = os.homedir();
// Official Kimi Code CLI docs: ~/.kimi/mcp.json. The kimi-code fork keeps
// its config under ~/.kimi-code/ and reads mcp.json there instead. Prefer
// whichever layout is actually present; default to the documented one.
const upstream = path.join(homeDir, '.kimi', 'mcp.json');
const fork = path.join(homeDir, '.kimi-code', 'mcp.json');
if (fs.existsSync(upstream)) return upstream;
if (fs.existsSync(fork) || fs.existsSync(path.dirname(fork))) return fork;
return upstream;
}
function getGrokConfigPath() {
const grokHome = process.env.GROK_HOME || path.join(os.homedir(), '.grok');
return path.join(grokHome, 'config.toml');
}
function tomlBasicString(value) {
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
function tomlMcpServerKey(serverName) {
if (/^[A-Za-z0-9_-]+$/.test(serverName)) return serverName;
return tomlBasicString(serverName);
}
// Render one [mcp_servers.<name>] TOML table (Codex / Grok config format).
function buildTomlMcpServerSection(serverName, entry) {
const lines = [
`[mcp_servers.${tomlMcpServerKey(serverName)}]`,
`command = ${tomlBasicString(entry.command)}`
];
if (Array.isArray(entry.args) && entry.args.length > 0) {
lines.push(`args = [${entry.args.map(tomlBasicString).join(', ')}]`);
}
return lines.join('\n');
}
// Merge a [mcp_servers.<name>] table into existing TOML text. Any previous
// table for the same server (including dotted subtables like
// [mcp_servers.<name>.env]) is replaced; every other line is preserved.
function mergeTomlMcpServer(existingText, serverName, entry) {
const section = buildTomlMcpServerSection(serverName, entry);
const text = String(existingText || '');
if (!text.trim()) return section + '\n';
const escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const keyPattern = `(?:${escapeRe(serverName)}|${escapeRe(tomlBasicString(serverName))})`;
const sectionStart = new RegExp(`^\\s*\\[mcp_servers\\.${keyPattern}(?:\\..*)?\\]\\s*$`);
const anyHeader = /^\s*\[/;
const kept = [];
let skipping = false;
for (const line of text.split('\n')) {
if (sectionStart.test(line)) {
skipping = true;
continue;
}
if (skipping && anyHeader.test(line)) skipping = false;
if (!skipping) kept.push(line);
}
const body = kept.join('\n').replace(/\s*$/, '');
return `${body}\n\n${section}\n`;
}
// Merge one mcpServers entry into a JSON config file. Refuses to touch a file
// that exists but is not a valid JSON object — never clobbers user content.
function mergeJsonMcpConfig(filePath, serverName, entry) {
let config = {};
if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, 'utf8');
if (raw.trim()) {
try {
config = JSON.parse(raw);
} catch (error) {
throw new Error(
`Refusing to modify ${filePath}: existing file is not valid JSON (${error.message}). ` +
'Fix or back it up manually, then re-run.'
);
}
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error(`Refusing to modify ${filePath}: existing config is not a JSON object.`);
}
}
}
if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
config.mcpServers = {};
}
config.mcpServers[serverName] = entry;
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
return filePath;
}
function mergeTomlMcpConfig(filePath, serverName, entry) {
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
const merged = mergeTomlMcpServer(existing, serverName, entry);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, merged);
return filePath;
}
// Build the per-client setup description for mcp-setup. Returns
// { client, serverName, useNpx, format, configPath, snippet, commandLine,
// apply, notes } where snippet is a JSON object (format 'json') or a TOML
// string (format 'toml'), and apply is 'json-merge' | 'toml-merge' | null.
function buildMcpClientSetup(client, useNpx = false, serverName = 'llm-checker') {
const normalizedServerName = String(serverName || 'llm-checker').trim() || 'llm-checker';
const entry = getMcpServerEntry(useNpx);
const jsonSnippet = { mcpServers: { [normalizedServerName]: entry } };
const runnerWords = [entry.command, ...(entry.args || [])].map(quoteCliArg).join(' ');
const base = {
client,
serverName: normalizedServerName,
useNpx: Boolean(useNpx),
format: 'json',
configPath: null,
snippet: jsonSnippet,
commandLine: null,
apply: null,
notes: []
};
switch (client) {
case 'codex':
return {
...base,
format: 'toml',
configPath: getCodexConfigPath(),
snippet: buildTomlMcpServerSection(normalizedServerName, entry),
commandLine: `codex mcp add ${quoteCliArg(normalizedServerName)} -- ${runnerWords}`,
apply: 'toml-merge',
notes: ['Codex CLI reads [mcp_servers.<name>] tables from ~/.codex/config.toml (CODEX_HOME overrides the directory).']
};
case 'cursor':
return {
...base,
configPath: getCursorMcpConfigPath(),
apply: 'json-merge',
notes: ['Cursor reads global MCP servers from ~/.cursor/mcp.json (mcpServers key).']
};
case 'windsurf':
return {
...base,
configPath: getWindsurfMcpConfigPath(),
apply: 'json-merge',
notes: ['Windsurf reads MCP servers from ~/.codeium/windsurf/mcp_config.json (mcpServers key).']
};
case 'gemini':
return {
...base,
configPath: getGeminiConfigPath(),
apply: 'json-merge',
notes: ['Gemini CLI reads MCP servers from the mcpServers key in ~/.gemini/settings.json.']
};
case 'kimi':
return {
...base,
configPath: getKimiMcpConfigPath(),
commandLine: `kimi mcp add --transport stdio ${quoteCliArg(normalizedServerName)} -- ${runnerWords}`,
apply: 'json-merge',
notes: [
'Kimi Code CLI reads MCP servers from ~/.kimi/mcp.json (official docs); the kimi-code fork uses ~/.kimi-code/mcp.json.',
'You can also run `kimi mcp add` (shown above) or `/mcp-config` inside the CLI.'
]
};
case 'grok':
return {
...base,
format: 'toml',
configPath: getGrokConfigPath(),
snippet: buildTomlMcpServerSection(normalizedServerName, entry),
commandLine: `grok mcp add ${quoteCliArg(normalizedServerName)} -- ${runnerWords}`,
apply: 'toml-merge',
notes: ['Grok CLI reads [mcp_servers.<name>] tables from ~/.grok/config.toml (GROK_HOME overrides the directory).']
};
case 'generic':
return {
...base,
format: 'json',
notes: [
'Standard mcpServers JSON snippet — merge it into the MCP configuration of any MCP-compatible client.',
'There is no well-known config path for a generic client, so --apply is a no-op here.'
]
};
default:
return base;
}
}
function parsePositiveIntegerOption(rawValue, optionName) {
const parsed = Number(rawValue);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`Invalid ${optionName}: ${rawValue}`);
}
return Math.round(parsed);
}
function parseNonNegativeNumberOption(rawValue, optionName) {
const parsed = Number(rawValue);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`Invalid ${optionName}: ${rawValue}`);
}
return parsed;
}
function selectModelsForPlan(installedModels, requestedModels = []) {
const runnableModels = (installedModels || []).filter((model) => {
const name = String(model?.name || '').toLowerCase();
const source = String(model?.source || '').toLowerCase();
const type = String(model?.type || model?.model_type || '').toLowerCase();
const fileSizeGB = Number(model?.fileSizeGB) || 0;
const cloudTagged = (
name.includes('-cloud') ||
name.endsWith(':cloud') ||
source.includes('cloud') ||
type === 'cloud' ||
type === 'remote' ||
type === 'hosted'
);
// Keep only models that are actually present locally for memory planning.
return !(cloudTagged && fileSizeGB <= 0);
});
const requested = Array.isArray(requestedModels)
? requestedModels.map((model) => String(model || '').trim()).filter(Boolean)
: [];
if (!requested.length) {
return {
selected: runnableModels.slice(),
missing: []
};
}
const selected = [];
const missing = [];
const seen = new Set();
for (const request of requested) {
const normalized = request.toLowerCase();
let match = runnableModels.find(
(model) => String(model.name || '').toLowerCase() === normalized
);
if (!match) {
match = runnableModels.find((model) =>
String(model.name || '').toLowerCase().startsWith(`${normalized}:`)
);
}
if (!match) {
match = runnableModels.find(
(model) => String(model.family || '').toLowerCase() === normalized
);
}
if (!match) {
match = runnableModels.find((model) =>
String(model.name || '').toLowerCase().includes(normalized)
);
}
if (!match) {
missing.push(request);
continue;
}
if (!seen.has(match.name)) {
selected.push(match);
seen.add(match.name);
}
}
return {
selected,
missing
};
}
function getStatusIcon(model, ollamaModels) {
const ollamaModel = ollamaModels?.find(om => om.matchedModel?.name === model.name);
if (ollamaModel?.isRunning) return 'R';
if (ollamaModel?.isInstalled) return 'I';
if (model.specialization === 'code') return 'C';
if (model.specialization === 'multimodal' || model.multimodal) return 'M';
if (model.specialization === 'embeddings') return 'E';
if (model.category === 'ultra_small') return 'XS';
if (model.category === 'small') return 'S';
if (model.category === 'medium') return 'M';
if (model.category === 'large') return 'L';
return '-';
}
function formatSize(size) {
if (!size) return 'Unknown';
const cleanSize = size.replace(/[^\d.BMK]/gi, '');
const numMatch = cleanSize.match(/(\d+\.?\d*)/);
const unitMatch = cleanSize.match(/[BMK]/i);
if (numMatch && unitMatch) {
const num = parseFloat(numMatch[1]);
const unit = unitMatch[0].toUpperCase();
return `${num}${unit}`;
}
return size;
}
// Helper function to calculate model compatibility score
function calculateModelCompatibilityScore(model, hardware) {
let score = 50; // Base score
// Estimar tamaño del modelo
const sizeMatch = model.model_identifier.match(/(\d+\.?\d*)[bm]/i);
let modelSizeB = 1; // Default 1B
if (sizeMatch) {
const num = parseFloat(sizeMatch[1]);
const unit = sizeMatch[0].slice(-1).toLowerCase();
modelSizeB = unit === 'm' ? num / 1000 : num;
}
// Calcular requerimientos estimados
const estimatedRAM = modelSizeB * 1.2; // 1.2x el tamaño del modelo
const ramRatio = hardware.memory.total / estimatedRAM;
// Puntuación por compatibilidad de RAM (40% del score)
if (ramRatio >= 3) score += 40;
else if (ramRatio >= 2) score += 30;
else if (ramRatio >= 1.5) score += 20;
else if (ramRatio >= 1.2) score += 10;
else if (ramRatio >= 1) score += 5;
else score -= 20; // Penalización por RAM insuficiente
// Puntuación por tamaño del modelo (30% del score)
if (modelSizeB <= 1) score += 30; // Modelos pequeños funcionan en cualquier lado
else if (modelSizeB <= 3) score += 25;
else if (modelSizeB <= 7) score += 20;
else if (modelSizeB <= 13) score += 15;
else if (modelSizeB <= 30) score += 10;
else score -= 10; // Modelos muy grandes
// Puntuación por CPU cores (20% del score)
if (hardware.cpu.cores >= 12) score += 20;
else if (hardware.cpu.cores >= 8) score += 15;
else if (hardware.cpu.cores >= 6) score += 10;
else if (hardware.cpu.cores >= 4) score += 5;
// Bonus por popularidad (10% del score)
const pulls = model.pulls || 0;
if (pulls > 1000000) score += 10;
else if (pulls > 100000) score += 7;
else if (pulls > 10000) score += 5;
else if (pulls > 1000) score += 3;
// Bonus especial para Apple Silicon
if (hardware.cpu.architecture === 'Apple Silicon') {
score += 5;
// Bonus extra para modelos optimizados
const modelName = model.model_identifier.toLowerCase();
if (modelName.includes('llama') || modelName.includes('mistral') ||
modelName.includes('phi') || modelName.includes('gemma')) {
score += 3;
}
}
return Math.max(0, Math.min(100, Math.round(score)));
}
function formatGpuInventoryList(models = []) {
if (!Array.isArray(models) || models.length === 0) return 'None';
return models
.map(({ name, count }) => (count > 1 ? `${count}x ${name}` : name))
.join(' + ');
}
// Helper function to get hardware tier for display
function getHardwareTierForDisplay(hardware) {
const canonicalTier = hardware?.summary?.hardwareTier;
if (typeof canonicalTier === 'string' && canonicalTier.trim()) {
return canonicalTier.replace(/_/g, ' ').toUpperCase();
}
const ram = hardware.memory.total;
const cores = hardware.cpu.cores;
const gpuModel = hardware.gpu?.model || '';
const vramGB = hardware.gpu?.vram || 0;
const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
: '';
const isIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
? hardware.summary.hasIntegratedGPU
: /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(`${gpuModel} ${integratedGpuInventory}`);
const hasDedicatedGPU = typeof hardware.summary?.hasDedicatedGPU === 'boolean'
? hardware.summary.hasDedicatedGPU
: (vramGB > 0 && !isIntegratedGPU);
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'));
// Base tier calculation
let tier;
if (ram >= 64 && cores >= 16) tier = 'EXTREME';
else if (ram >= 32 && cores >= 12) tier = 'VERY HIGH';
else if (ram >= 16 && cores >= 8) tier = 'HIGH';
else if (ram >= 8 && cores >= 4) tier = 'MEDIUM';
else if (ram >= 4 && cores >= 2) tier = 'LOW';
else tier = 'ULTRA LOW';
// Special cases for edge configurations
if (ram >= 16 && ram < 32 && cores >= 12) tier = 'HIGH';
if (ram >= 32 && ram < 64 && cores >= 8 && tier === 'ULTRA LOW') tier = 'VERY HIGH';
// Cap tier for integrated GPU systems (most important fix)
if (isIntegratedGPU && !isAppleSilicon) {
// Cap iGPU systems at HIGH maximum (Iris Xe, Intel UHD, AMD integrated, etc.)
const tierPriority = { 'ULTRA LOW': 0, 'LOW': 1, 'MEDIUM': 2, 'HIGH': 3, 'VERY HIGH': 4, 'EXTREME': 5 };
const currentPriority = tierPriority[tier] || 0;
if (currentPriority > 3) { // HIGH = 3
tier = 'HIGH';
}
}
return tier;
}
function getBackendLabelForDisplay(hardware) {
const summary = hardware?.summary || {};
if (typeof summary.bestBackendLabel === 'string' && summary.bestBackendLabel.trim()) {
return summary.bestBackendLabel;
}
const backendName = summary.backendName || String(summary.bestBackend || 'cpu').toUpperCase();
if (summary.runtimeBackend && summary.runtimeBackend !== summary.bestBackend) {
return `${backendName} + ${summary.runtimeBackendName || summary.runtimeBackend} assist`;
}
return backendName;
}
function formatSpeed(speed) {
const speedMap = {
'very_fast': 'very_fast',
'fast': 'fast',
'medium': 'medium',
'slow': 'slow',
'very_slow': 'very_slow'
};
return speedMap[speed] || (speed || 'unknown');
}
function getScoreColor(score) {
if (score >= 90) return chalk.green;
if (score >= 75) return chalk.yellow;
if (score >= 60) return chalk.hex('#FFA500');
return chalk.red;
}
function getOllamaCommand(modelName) {
const mapping = {
'TinyLlama 1.1B': 'tinyllama:1.1b',
'Qwen 0.5B': 'qwen:0.5b',
'Gemma 2B': 'gemma2:2b',
'Phi-3 Mini 3.8B': 'phi3:mini',
'Llama 3.2 3B': 'llama3.2:3b',
'Llama 3.1 8B': 'llama3.1:8b',
'Mistral 7B v0.3': 'mistral:7b',
'CodeLlama 7B': 'codellama:7b',
'Qwen 2.5 7B': 'qwen2.5:7b'
};
return mapping[modelName] || '-';
}
function displaySystemInfo(hardware, analysis) {
const cpuOnly = Boolean(hardware.cpuOnly);
const cpuColor = hardware.cpu.cores >= 8 ? chalk.green : hardware.cpu.cores >= 4 ? chalk.yellow : chalk.red;
const ramColor = hardware.memory.total >= 32 ? chalk.green : hardware.memory.total >= 16 ? chalk.yellow : chalk.red;
const gpuColor = cpuOnly ? chalk.gray : (hardware.gpu.dedicated ? chalk.green : chalk.hex('#FFA500'));
const integratedInventory = cpuOnly
? hardware.detectedGpu?.integratedGpuModels
: (hardware.gpu.integratedGpuModels || hardware.summary?.integratedGpuModels);
const dedicatedInventory = cpuOnly
? hardware.detectedGpu?.dedicatedGpuModels
: (hardware.gpu.dedicatedGpuModels || hardware.summary?.dedicatedGpuModels);
const integratedList = formatGpuInventoryList(integratedInventory);
const dedicatedList = formatGpuInventoryList(dedicatedInventory);
const integratedSharedMemory = hardware.gpu.sharedMemory || hardware.summary?.integratedSharedMemory || 0;
const vramDisplay = cpuOnly
? 'Disabled (CPU-only)'
: (!hardware.gpu.dedicated && integratedSharedMemory > 0
? `${integratedSharedMemory}GB shared`
: (hardware.gpu.vram === 0 && hardware.gpu.model && hardware.gpu.model.toLowerCase().includes('apple')
? 'Unified Memory'
: `${hardware.gpu.vram || 'N/A'}GB`));
const gpuDisplay = cpuOnly ? 'Disabled by CPU-only mode' : (hardware.gpu.model || 'Not detected');
const gpuTypeSuffix = cpuOnly
? ''
: (hardware.gpu.dedicated ? chalk.green(' (Dedicated)') : chalk.hex('#FFA500')(' (Integrated)'));
const inventorySuffix = cpuOnly ? ' (diagnostic)' : '';
const lines = [
`${chalk.cyan('CPU:')} ${cpuColor(hardware.cpu.brand)} ${chalk.gray(`(${hardware.cpu.cores} cores, ${hardware.cpu.speed}GHz)`)}`,
`${chalk.cyan('Architecture:')} ${hardware.cpu.architecture}`,
`${chalk.cyan('RAM:')} ${ramColor(hardware.memory.total + 'GB')}`,
`${chalk.cyan('GPU:')} ${gpuColor(gpuDisplay)}`,
`${chalk.cyan('Backend:')} ${chalk.white(getBackendLabelForDisplay(hardware))}`,
`${chalk.cyan('VRAM:')} ${vramDisplay}${gpuTypeSuffix}`,
`${chalk.cyan(`Dedicated GPUs${inventorySuffix}:`)} ${chalk.green(dedicatedList)}`,
`${chalk.cyan(`Integrated GPUs${inventorySuffix}:`)} ${chalk.hex('#FFA500')(integratedList)}`,
];
const tier = analysis.summary.hardwareTier?.replace(/_/g, ' ').toUpperCase() || getHardwareTierForDisplay(hardware);
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
lines.push(`${chalk.bold('Hardware Tier:')} ${tierColor.bold(tier)}`);
console.log('\n' + chalk.bgBlue.white.bold(' SYSTEM INFORMATION '));
console.log(chalk.blue('╭' + '─'.repeat(50)));
lines.forEach(line => {
console.log(chalk.blue('│') + ' ' + line);
});
console.log(chalk.blue('╰'));
}
function displayOllamaIntegration(ollamaInfo, ollamaModels) {
const lines = [];
if (ollamaInfo.available) {
lines.push(`${chalk.green('✅ Status:')} Running ${chalk.gray(`(v${ollamaInfo.version || 'unknown'})`)}`);
if (ollamaModels && ollamaModels.length > 0) {
const compatibleCount = ollamaModels.filter(m => {
return m.canRun === true ||
m.compatibilityScore >= 60 ||
(m.matchedModel && true);
}).length;
const runningCount = ollamaModels.filter(m => m.isRunning).length;
lines.push(`${chalk.cyan('Installed:')} ${ollamaModels.length} total, ${chalk.green(compatibleCount)} compatible`);
if (runningCount > 0) {
lines.push(`${chalk.cyan('Running:')} ${chalk.green(runningCount)} models`);
}
} else {
lines.push(`${chalk.gray('No models installed yet')}`);
}
} else {
lines.push(`${chalk.red('Status:')} Not available`);
}
console.log('\n' + chalk.bgMagenta.white.bold(' OLLAMA INTEGRATION '));
console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(50)));
lines.forEach(line => {
console.log(chalk.hex('#a259ff')('│') + ' ' + line);
});
console.log(chalk.hex('#a259ff')('╰'));
}
function displayEnhancedCompatibleModels(compatible, ollamaModels) {
if (compatible.length === 0) {
console.log('\n' + chalk.yellow('No compatible models found.'));
return;
}
console.log('\n' + chalk.green.bold(' ✅ Compatible Models (Score ≥ 75)'));
const data = [
[
chalk.bgGreen.white.bold(' Model '),
chalk.bgGreen.white.bold(' Size '),
chalk.bgGreen.white.bold(' Score '),
chalk.bgGreen.white.bold(' RAM '),
chalk.bgGreen.white.bold(' VRAM '),
chalk.bgGreen.white.bold(' Speed '),
chalk.bgGreen.white.bold(' Status ')
]
];
compatible.slice(0, 15).forEach(model => {
const tokensPerSec = model.performanceEstimate?.estimatedTokensPerSecond || 'N/A';
const ramReq = model.requirements?.ram || 1;
const vramReq = model.requirements?.vram || 0;
const speedFormatted = formatSpeed(model.performance?.speed || 'medium');
const scoreColor = getScoreColor(model.score || 0);
const scoreDisplay = scoreColor(`${model.score || 0}/100`);
let statusDisplay = `${tokensPerSec}t/s`;
if (model.isOllamaInstalled) {
const ollamaInfo = model.ollamaInfo || {};
if (ollamaInfo.isRunning) {
statusDisplay = 'Running';
} else {
statusDisplay = 'Installed';
}
}
let modelName = model.name;
if (model.isOllamaInstalled) {
modelName = `${model.name}`;
}
const row = [
modelName,
formatSize(model.size || 'Unknown'),
scoreDisplay,
`${ramReq}GB`,
`${vramReq}GB`,
speedFormatted,
statusDisplay
];
data.push(row);
});
console.log(table(data));
if (compatible.length > 15) {
console.log(chalk.gray(`\n... and ${compatible.length - 15} more compatible models`));
}
displayCompatibleModelsSummary(compatible.length);
}
function displayCompatibleModelsSummary(count) {
console.log('\n' + chalk.bgMagenta.white.bold(' COMPATIBLE MODELS '));
console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(40)));
console.log(chalk.hex('#a259ff')('│') + ` Total compatible models: ${chalk.green.bold(count)}`);
console.log(chalk.hex('#a259ff')('╰'));
}
function displayMarginalModels(marginal) {
if (marginal.length === 0) return;
console.log('\n' + chalk.yellow.bold('Marginal Performance (Score 60-74)'));
const data = [
[
chalk.bgYellow.white.bold(' Model '),
chalk.bgYellow.white.bold(' Size '),
chalk.bgYellow.white.bold(' Score '),
chalk.bgYellow.white.bold(' RAM '),
chalk.bgYellow.white.bold(' VRAM '),
chalk.bgYellow.white.bold(' Issue ')
]
];
marginal.slice(0, 6).forEach(model => {
const mainIssue = model.issues?.[0] || 'Performance limitations';
const scoreColor = getScoreColor(model.score || 0);
const scoreDisplay = scoreColor(`${model.score || 0}/100`);
const ramReq = model.requirements?.ram || 1;
const vramReq = model.requirements?.vram || 0;
const truncatedIssue = mainIssue.length > 30 ? mainIssue.substring(0, 27) + '...' : mainIssue;
const row = [
model.name,
formatSize(model.size || 'Unknown'),
scoreDisplay,
`${ramReq}GB`,
`${vramReq}GB`,
truncatedIssue
];
data.push(row);
});
console.log(table(data));
if (marginal.length > 6) {
console.log(chalk.gray(`\n... and ${marginal.length - 6} more marginal models`));
}
}
function displayStructuredRecommendations(recommendations) {
if (!recommendations) return;
if (Array.isArray(recommendations)) {
displayLegacyRecommendations(recommendations);
return;
}
console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
console.log(chalk.cyan('╭' + '─'.repeat(50)));
if (recommendations.general && recommendations.general.length > 0) {
console.log(chalk.cyan('│') + ` ${chalk.bold.white('General Recommendations:')}`);
recommendations.general.slice(0, 4).forEach((rec, index) => {
console.log(chalk.cyan('│') + ` ${index + 1}. ${chalk.white(rec)}`);
});
console.log(chalk.cyan('│'));
}
if (recommendations.installedModels && recommendations.installedModels.length > 0) {
console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
recommendations.installedModels.forEach(rec => {
console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
});
console.log(chalk.cyan('│'));
}
if (recommendations.cloudSuggestions && recommendations.cloudSuggestions.length > 0) {
console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Recommended from Ollama Cloud:')}`);
recommendations.cloudSuggestions.forEach(rec => {
if (rec.includes('ollama pull')) {
console.log(chalk.cyan('│') + ` ${chalk.cyan.bold(rec)}`);
} else {
console.log(chalk.cyan('│') + ` ${chalk.blue(rec)}`);
}
});
console.log(chalk.cyan('│'));
}
if (recommendations.quickCommands && recommendations.quickCommands.length > 0) {
console.log(chalk.cyan('│') + ` ${chalk.bold.yellow('⚡ Quick Commands:')}`);
const uniqueCommands = [...new Set(recommendations.quickCommands)];
uniqueCommands.slice(0, 3).forEach(cmd => {
console.log(chalk.cyan('│') + ` > ${chalk.yellow.bold(cmd)}`);
});
}
console.log(chalk.cyan('╰'));
}
function displayLegacyRecommendations(recommendations) {
if (!recommendations || recommendations.length === 0) return;
const generalRecs = [];
const ollamaFoundRecs = [];
const quickInstallRecs = [];
recommendations.forEach(rec => {
if (rec.includes('Score:')) {
ollamaFoundRecs.push(rec);
} else if (rec.includes('ollama pull')) {
quickInstallRecs.push(rec);
} else if (rec.includes('ollama run')) {
quickInstallRecs.push(rec);
} else {
generalRecs.push(rec);
}
});
console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
console.log(chalk.cyan('╭' + '─'.repeat(40)));
generalRecs.slice(0, 8).forEach((rec, index) => {
const number = chalk.green.bold(`${index + 1}.`);
console.log(chalk.cyan('│') + ` ${number} ${chalk.white(rec)}`);
});
if (ollamaFoundRecs.length > 0) {
console.log(chalk.cyan('│'));
console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
ollamaFoundRecs.forEach(rec => {
console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
});
}
if (quickInstallRecs.length > 0) {
console.log(chalk.cyan('│'));
console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Quick Commands:')}`);
quickInstallRecs.slice(0, 3).forEach(cmd => {
console.log(chalk.cyan('│') + ` > ${chalk.cyan.bold(cmd)}`);
});
}
console.log(chalk.cyan('╰'));
}
function displayIntelligentRecommendations(intelligentData, hardware = null) {
if (!intelligentData || !intelligentData.summary) return;
const { summary, recommendations } = intelligentData;
const tier = summary.hardware_tier.replace('_', ' ').toUpperCase();
const optimizeProfile = (summary.optimize_for || intelligentData.optimizeFor || 'balanced').toUpperCase();
const runtimeLabel = (intelligentData.runtime || summary.best_overall?.runtime || 'auto').toUpperCase();
const sourceLabel = intelligentData.recommendationSource === 'registry'
? 'Multi-source registry'
: 'Ollama catalog';
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
console.log('\n' + chalk.bgRed.white.bold(' INTELLIGENT RECOMMENDATIONS BY CATEGORY '));
console.log(chalk.red('╭' + '─'.repeat(65)));
console.log(chalk.red('│') + ` Hardware Tier: ${tierColor.bold(tier)} | Models Analyzed: ${chalk.cyan.bold(intelligentData.totalModelsAnalyzed)}`);
console.log(chalk.red('│') + ` Optimization: ${chalk.magenta.bold(optimizeProfile)} | Runtime: ${chalk.cyan.bold(runtimeLabel)}`);
console.log(chalk.red('│') + ` Source: ${chalk.white.bold(sourceLabel)}`);
console.log(chalk.red('│'));
// Mostrar mejor modelo general
if (summary.best_overall) {
const best = summary.best_overall;
const bestFineTuning = evaluateFineTuningSupport(best, hardware || {});
console.log(chalk.red('│') + ` ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(best.command)}`);
console.log(chalk.red('│') + ` Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(best.quantization || 'Q4_K_M')}`);
console.log(chalk.red('│') + ` Runtime: ${chalk.cyan.bold(best.runtime || intelligentData.runtime || 'ollama')} | Source: ${chalk.gray(best.source || 'unknown')}`);
console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(bestFineTuning.shortLabel)}`);
console.log(chalk.red('│'));
}
// Mostrar por categorías
const categories = {
coding: 'Coding',
talking: 'Chat',
reading: 'Reading',
reasoning: 'Reasoning',
multimodal: 'Multimodal',
creative: 'Creative',
general: 'General'
};
Object.entries(summary.by_category).forEach(([category, model]) => {
const icon = categories[category] || 'Other';
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
const scoreColor = getScoreColor(model.score);
const fineTuningSupport = evaluateFineTuningSupport(model, hardware || {});
console.log(chalk.red('│') + ` ${chalk.bold.white(categoryName)} (${icon}):`);
console.log(chalk.red('│') + ` ${chalk.green(model.name)} (${model.size})`);
console.log(chalk.red('│') + ` Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(model.quantization || 'Q4_K_M')}`);
console.log(chalk.red('│') + ` Runtime: ${chalk.cyan(model.runtime || intelligentData.runtime || 'ollama')} | Source: ${chalk.gray(model.source || 'unknown')}`);
console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(model.command)}`);
console.log(chalk.red('│'));
});
console.log(chalk.red('╰'));
}
function toCalibrationSourceLabel(source) {
if (source === 'default-discovery') {
return '~/.llm-checker/calibration-policy.{yaml,yml,json}';
}
return source || 'unknown';
}
function collectRecommendationModelIdentifiers(intelligentData) {
const identifiers = new Set();
const summary = intelligentData?.summary || {};
if (summary.best_overall?.identifier) {
identifiers.add(summary.best_overall.identifier);
}
if (summary.by_category && typeof summary.by_category === 'object') {
Object.values(summary.by_category).forEach((entry) => {
if (entry?.identifier) {
identifiers.add(entry.identifier);
}
});
}
const recommendationGroups = intelligentData?.recommendations || {};
Object.values(recommendationGroups).forEach((group) => {
const models = Array.isArray(group?.bestModels) ? group.bestModels : [];
models.forEach((model) => {
if (model?.model_identifier) {
identifiers.add(model.model_identifier);
}
});
});
return Array.from(identifiers);
}
function resolveCalibratedRouteDecision(calibratedPolicy, requestedTask, availableModels = []) {
if (!calibratedPolicy?.policy) return null;
const resolvedRoute = resolveCalibrationRoute(calibratedPolicy.policy, requestedTask);
if (!resolvedRoute?.route) return null;
const routeCandidates = getRouteModelCandidates(resolvedRoute.route);
const routeSelection = selectModelFromRoute(resolvedRoute.route, availableModels);
const selectedModel = routeSelection?.selectedModel || routeCandidates[0] || null;
return {
requestedTask: resolvedRoute.requestedTask,
resolvedTask: resolvedRoute.resolvedTask,
usedTaskFallback: Boolean(resolvedRoute.usedTaskFallback),
primary: resolvedRoute.route.primary,
fallbacks: Array.isArray(resolvedRoute.route.fallbacks) ? resolvedRoute.route.fallbacks : [],
routeCandidates,
selectedModel,
matchedRouteModel: routeSelection?.matchedRouteModel || (routeCandidates[0] || null),
matchedAvailableModel: Boolean(routeSelection),
usedRouteFallbackModel: Boolean(routeSelection?.usedFallback)
};
}
function displayCalibratedRoutingDecision(commandName, calibratedPolicy, routeDecision, warnings = []) {
if (!calibratedPolicy && (!warnings || warnings.length === 0)) {
return;
}
console.log('\n' + chalk.bgBlue.white.bold(' CALIBRATED ROUTING '));
console.log(chalk.blue('╭' + '─'.repeat(78)));
console.log(chalk.blue('│') + ` Command: ${chalk.cyan(commandName)}`);
if (calibratedPolicy) {
console.log(chalk.blue('│') + ` Policy: ${chalk.green(calibratedPolicy.policyPath)}`);
console.log(chalk.blue('│') + ` Source: ${chalk.magenta(toCalibrationSourceLabel(calibratedPolicy.source))}`);
} else {
console.log(chalk.blue('│') + chalk.yellow(' Policy: not active (deterministic fallback)'));
}
if (routeDecision) {
const requestedTask = routeDecision.requestedTask || 'general';
const resolvedTask = routeDecision.resolvedTask || requestedTask;
const taskDisplay = routeDecision.usedTaskFallback
? `${requestedTask} → ${resolvedTask}`
: requestedTask;
const selectedModel = routeDecision.selectedModel || routeDecision.primary || 'N/A';
const selectedLabel = routeDecision.usedRouteFallbackModel
? `${selectedModel} (fallback)`
: selectedModel;
console.log(chalk.blue('│') + ` Task: ${chalk.white(taskDisplay)}`);
console.log(chalk.blue('│') + ` Route primary: ${chalk.green(routeDecision.primary || 'N/A')}`);
if (routeDecision.fallbacks && routeDecision.fallbacks.length > 0) {
console.log(chalk.blue('│') + ` Route fallbacks: ${chalk.gray(routeDecision.fallbacks.join(', '))}`);
}
console.log(chalk.blue('│') + ` Selected model: ${chalk.green.bold(selectedLabel)}`);
if (!routeDecision.matchedAvailableModel) {
console.log(
chalk.blue('│') +
chalk.yellow(' Route did not match local/recommended models; using route primary for visibility.')
);
}
}
if (warnings && warnings.length > 0) {
warnings.forEach((warning) => {
console.log(chalk.blue('│') + chalk.yellow(` Warning: ${warning}`));
});
}
console.log(chalk.blue('╰'));
}
function parseAiRunModelSizeB(value) {
const match = String(value || '').match(/(\d+(?:\.\d+)?)\s*([kmb])\+?/i);
if (!match) return null;
const amount = Number(match[1]);
if (!Number.isFinite(amount) || amount <= 0) return null;
const unit = match[2].toLowerCase();
if (unit === 'b') return amount;
if (unit === 'm') return amount / 1000;
if (unit === 'k') return amount / 1_000_000;
return null;
}
function normalizeAiRunModelName(value) {
return String(value || '')
.trim()
.toLowerCase()
.replace(/:latest$/, '');
}
function findAiRunLocalModel(localModels = [], modelName = '') {
const target = normalizeAiRunModelName(modelName);
if (!target) return null;
return localModels.find((model) => {
const name = normalizeAiRunModelName(model.name || model.model);
if (!name) return false;
return name === target || name.includes(target) || target.includes(name);
}) || null;
}
function resolveAiRunModelSizeB(modelName, aiSelector, localModel = null) {
const localParameterSize = localModel?.details?.parameter_size || localModel?.size;
const parsedLocalSize = parseAiRunModelSizeB(localParameterSize);
if (parsedLocalSize) return parsedLocalSize;
const parsedNameSize = parseAiRunModelSizeB(modelName);
if (parsedNameSize) return parsedNameSize;
if (aiSelector && typeof aiSelector.estimateModelSize === 'function') {
const selectorSize = Number(aiSelector.estimateModelSize(modelName));
if (Number.isFinite(selectorSize) && selectorSize > 0) return selectorSize;
}
return 7;
}
function formatAiRunNumber(value, decimals = 1) {
const number = Number(value);
if (!Number.isFinite(number)) return 'N/A';
return number.toFixed(decimals).replace(/\.0$/, '');
}
function estimateAiRunWorkingSetGB(modelSizeB, localModel = null) {
const fileSizeGB = Number(localModel?.fileSizeGB) || 0;
const parameterEstimateGB = (Number(modelSizeB) * 0.75) + 2;
if (fileSizeGB > 0) {
return Math.max(fileSizeGB * 1.15, parameterEstimateGB * 0.85);
}
return parameterEstimateGB;
}
function formatAiRunHardwareSummary(systemInfo = {}) {
const cpuBrand = systemInfo.cpu?.brand || systemInfo.cpu?.model || 'CPU';
const cores = systemInfo.cpu?.cores ? ` (${systemInfo.cpu.cores} cores)` : '';
const memory = systemInfo.memory?.total ? `${systemInfo.memory.total}GB RAM` : 'RAM unknown';
const gpu = systemInfo.gpu?.model || 'GPU not detected';
return `${cpuBrand}${cores}, ${memory}, ${gpu}`;
}
function formatAiRunMethod(method = '') {
return String(method || 'selector')
.replace(/[_-]+/g, ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function formatAiRunReason(reason = '') {
const text = String(reason || '').replace(/\s+/g, ' ').trim();
if (!text) return 'Selected from local model compatibility scoring.';
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
}
function formatAiRunMeasuredSpeed(benchmark = null) {
if (!benchmark) return null;
if (!benchmark.success) {
return `not available (${benchmark.error || 'benchmark failed'})`;
}
const parts = [];
if (Number(benchmark.evalTokensPerSecond) > 0) {
parts.push(`${formatAiRunNumber(benchmark.evalTokensPerSecond)} eval t/s`);
}
if (Number(benchmark.endToEndTokensPerSecond) > 0) {
parts.push(`${formatAiRunNumber(benchmark.endToEndTokensPerSecond)} end-to-end t/s`);
}
if (parts.length === 0 && Number(benchmark.tokensPerSecond) > 0) {
parts.push(`${formatAiRunNumber(benchmark.tokensPerSecond)} t/s`);
}
const generated = Number(benchmark.tokensGenerated) > 0
? `, ${benchmark.tokensGenerated} tokens`
: '';
return `${parts.join(', ')}${generated}`;
}
function displayAiRunReference({ result, systemInfo, taskHint, candidateModels, localModels, aiSelector, benchmark }) {
const localModel = findAiRunLocalModel(localModels, result.bestModel);
const modelSizeB = resolveAiRunModelSizeB(result.bestModel, aiSelector, localModel);
const speedEstimate = estimateTokenSpeedFromHardware(systemInfo, {
modelSizeB,
modelName: result.bestModel
});
const workingSetGB = estimateAiRunWorkingSetGB(modelSizeB, localModel);
const localCount = result.localModelsCount || candidateModels.length;
const dbCount = result.totalModelsEvaluated;
const confidence = Number(result.confidence);
const confidenceText = Number.isFinite(confidence)
? `${Math.round(confidence * 100)}%`
: 'N/A';
const idealModel = result.recommendedFromDatabase;
const usesFallback = idealModel && idealModel !== result.bestModel && result.isRecommendedInstalled === false;
const measuredSpeed = formatAiRunMeasuredSpeed(benchmark);
console.log('\n' + chalk.bold('AI Run reference'));
console.log(chalk.gray('----------------'));
console.log(`${chalk.gray('Task:')} ${chalk.white(taskHint || 'general')}`);
console.log(`${chalk.gray('Selected local model:')} ${chalk.green.bold(result.bestModel)}`);
if (idealModel) {
const idealStatus = usesFallback ? chalk.yellow('not installed') : chalk.green('available');
console.log(`${chalk.gray('Best database match:')} ${chalk.cyan(idealModel)} ${chalk.gray('(')}${idealStatus}${chalk.gray(')')}`);
}
console.log(`${chalk.gray('Why this model:')} ${formatAiRunReason(result.reasoning || result.reason)}`);
console.log(`${chalk.gray('Confidence:')} ${chalk.white(confidenceText)} ${chalk.gray(`via ${formatAiRunMethod(result.method)}`)}`);
console.log(`${chalk.gray('Models evaluated:')} ${chalk.white(`${localCount} local`)}${dbCount ? chalk.gray(`, ${dbCount} database`) : ''}`);
console.log(`${chalk.gray('Hardware:')} ${formatAiRunHardwareSummary(systemInfo)}`);
console.log(`${chalk.gray('Estimated speed:')} ${chalk.yellow(`~${speedEstimate.tokensPerSecond} tokens/sec`)} ${chalk.gray(`${speedEstimate.backend}, generation only`)}`);
if (measuredSpeed) {
const speedColor = benchmark?.success ? chalk.green : chalk.yellow;
console.log(`${chalk.gray('Measured speed:')} ${speedColor(measuredSpeed)}`);
}
console.log(`${chalk.gray('Memory reference:')} ${chalk.white(`~${formatAiRunNumber(modelSizeB)}B params, ~${formatAiRunNumber(workingSetGB)}GB working set`)}`);
if (usesFallback) {
console.log(`${chalk.gray('Install ideal model:')} ${chalk.cyan(`ollama pull ${idealModel}`)}`);
}
}
function formatAiRunTurnSpeed(result = {}) {
const evalSpeed = Number(result.evalTokensPerSecond);
const preferredSpeed = evalSpeed > 0 ? evalSpeed : Number(result.tokensPerSecond);
if (!Number.isFinite(preferredSpeed) || preferredSpeed <= 0) {
return '[speed unavailable]';
}
return `[${formatAiRunNumber(preferredSpeed)} tokens/sec]`;
}
async function runAiRunChatTurn(client, modelName, messages) {
let printed = false;
const result = await client.streamChat(
modelName,
messages,
{
keepAlive: '5m',
timeoutMs: 180000
},
(chunk) => {
printed = true;
process.stdout.write(chunk);
}
);
if (!printed && result.response) {
process.stdout.write(result.response);
}
const responseText = result.response || result.message?.content || '';
const needsSpace = responseText.length > 0 && !/\s$/.test(responseText);
process.stdout.write(`${needsSpace ? ' ' : ''}${chalk.gray(formatAiRunTurnSpeed(result))}\n\n`);
return result;
}
function askAiRunQuestion(rl, promptText) {
return new Promise((resolve) => {
let settled = false;
const handleClose = () => {
if (!settled) {
settled = true;
resolve(null);
}
};
rl.once('close', handleClose);
rl.question(promptText, (answer) => {
if (settled) return;
settled = true;
rl.off('close', handleClose);
resolve(answer);
});
});
}
async function runAiRunInteractiveChat(client, modelName) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const messages = [];
let closed = false;
rl.on('SIGINT', () => {
process.stdout.write('\n');
rl.close();
});
rl.on('close', () => {
closed = true;
});
try {
while (!closed) {
const input = await askAiRunQuestion(rl, chalk.cyan('>>> '));
if (input === null) break;
const trimmed = String(input || '').trim();
if (!trimmed) {
continue;
}
if (['/bye', '/exit', '/quit', 'q'].includes(trimmed.toLowerCase())) {
break;
}
if (['/?', '/help'].includes(trimmed.toLowerCase())) {
console.log('Commands: /bye, /exit, /quit');
continue;
}
messages.push({ role: 'user', content: input });
try {
const response = await runAiRunChatTurn(client, modelName, messages);
const assistantContent = response.response || response.message?.content || '';
messages.push({ role: 'assistant', content: assistantContent });
} catch (error) {
console.error(chalk.red(`Chat request failed: ${error.message}`));
}
}
} finally {
rl.close();
}
}
function displayModelsStats(originalCount, filteredCount, options) {
console.log('\n' + chalk.bgGreen.white.bold(' DATABASE STATS '));
console.log(chalk.green('╭' + '─'.repeat(60)));
console.log(chalk.green('│') + ` Total models in database: ${chalk.cyan.bold(originalCount)}`);
console.log(chalk.green('│') + ` After filters: ${chalk.yellow.bold(filteredCount)}`);
if (options.category) {
console.log(chalk.green('│') + ` Category filter: ${chalk.magenta.bold(options.category)}`);
}
if (options.size) {
console.log(chalk.green('│') + ` Size filter: ${chalk.magenta.bold(options.size)}`);
}
if (options.popular) {
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Popular models only (>100k pulls)')}`);
}
if (options.recent) {
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Recent models only')}`);
}
console.log(chalk.green('╰'));
}
async function displayTopRecommended(models, categoryFilter) {
console.log('\n' + chalk.bgGreen.white.bold(' TOP 3 RECOMMENDED FOR YOUR HARDWARE '));
try {
const DeterministicModelSelector = require('../src/models/deterministic-selector.js');
const selector = new DeterministicModelSelector();
// Use deterministic selector to get top 3 for this category
const result = await selector.selectModels(categoryFilter || 'general', {
topN: 3,
enableProbe: false,
silent: true
});
const top3 = result.candidates.map(candidate => selector.mapCandidateToLegacyFormat(candidate));
if (top3.length === 0) {
console.log(chalk.green('│') + chalk.yellow(' No models found for this category with current hardware'));
console.log(chalk.green('╰' + '─'.repeat(65)));
return;
}
top3.forEach((model, index) => {
const rankEmoji = ['🥇', '🥈', '🥉'][index];
const categoryColor = getCategoryColor(model.category || categoryFilter || 'general');
const scoreColor = model.categoryScore >= 80 ? chalk.green.bold :
model.categoryScore >= 60 ? chalk.yellow : chalk.red;
const size = model.size ? `${model.size}B` : 'Unknown';
console.log(chalk.green('│'));
console.log(chalk.green('│') + ` ${rankEmoji} ${chalk.cyan.bold(model.model_identifier)}`);
console.log(chalk.green('│') + ` Size: ${chalk.green(size)} | Score: ${scoreColor(Math.round(model.categoryScore) + '%')} | Category: ${categoryColor(model.category || 'general')}`);
console.log(chalk.green('│') + ` Command: ${chalk.yellow.bold('ollama pull ' + model.model_identifier)}`);
console.log(chalk.green('│') + ` ${chalk.gray(`Hardware: ${Math.round(model.hardwareScore)}/100, Quality: ${Math.round(model.specializationScore)}/100, Speed: ${Math.round(model.efficiencyScore)}/100`)}`);
});
console.log(chalk.green('╰' + '─'.repeat(65)));
} catch (error) {
console.log(chalk.green('│') + chalk.red(' Error calculating intelligent recommendations: ' + error.message));
console.log(chalk.green('╰' + '─'.repeat(65)));
}
}
async function displayCompactModelsList(models, categoryFilter = null) {
// Si hay modelos con compatibilityScore, mostrar top 3 recomendados primero
const showCompatibility = models.length > 0 && models[0].compatibilityScore !== undefined;
if (showCompatibility && categoryFilter) {
await displayTopRecommended(models, categoryFilter);
}
console.log('\n' + chalk.bgBlue.white.bold(' 📋 MODELS LIST '));
const headers = [
chalk.bgBlue.white.bold(' # '),
chalk.bgBlue.white.bold(' Model '),
chalk.bgBlue.white.bold(' Size ')
];
if (showCompatibility) {
headers.push(chalk.bgBlue.white.bold(' Score '));
}
headers.push(
chalk.bgBlue.white.bold(' Context '),
chalk.bgBlue.white.bold(' Input '),
chalk.bgBlue.white.bold(' Category ')
);
const data = [headers];
let rowIndex = 0;
models.forEach((model) => {
const category = model.category || 'general';
const categoryColor = getCategoryColor(category);
// Context length
const contextLength = model.context_length || 'Unknown';
// Input types
const inputTypes = (model.input_types && model.input_types.length > 0) ?
model.input_types.slice(0, 2).join(',') : 'text';
// Si el modelo tiene tags/variantes, crear una fila por cada tag
if (model.tags && model.tags.length > 0) {
model.tags.forEach((tag) => {
rowIndex++;
// Extraer el tamaño del tag si está presente
const tagSize = extractSizeFromIdentifier(tag) ||
model.main_size ||
(model.model_sizes && model.model_sizes[0]) ||
'Unknown';
const row = [
chalk.gray(`${rowIndex}`),
tag, // Mostrar el tag completo como nombre del modelo
chalk.green(tagSize)
];
// Agregar score si está disponible
if (showCompatibility) {
const score = model.compatibilityScore || 0;
const scoreColor = score >= 80 ? chalk.green.bold :
score >= 60 ? chalk.yellow : chalk.red;
row.push(scoreColor(`${score}%`));
}
row.push(
chalk.blue(contextLength),
chalk.magenta(inputTypes),
categoryColor(category)
);
data.push(row);
});
} else {
// Si no tiene tags, mostrar el modelo base
rowIndex++;
const mainSize = model.main_size ||
(model.model_sizes && model.model_sizes[0]) ||
extractSizeFromIdentifier(model.model_identifier) ||
'Unknown';
const row = [
chalk.gray(`${rowIndex}`),
model.model_name || model.model_identifier || 'Unknown',
chalk.green(mainSize)
];
// Agregar score si está disponible
if (showCompatibility) {
const score = model.compatibilityScore || 0;
const scoreColor = score >= 80 ? chalk.green.bold :
score >= 60 ? chalk.yellow : chalk.red;
row.push(scoreColor(`${score}%`));
}
row.push(
chalk.blue(contextLength),
chalk.magenta(inputTypes),
categoryColor(category)
);
data.push(row);
}
});
console.log(table(data));
}
function extractSizeFromIdentifier(identifier) {
const sizeMatch = identifier.match(/(\d+\.?\d*[bg])/i);
return sizeMatch ? sizeMatch[1].toLowerCase() : null;
}
function displayFullModelsList(models) {
console.log('\n' + chalk.bgBlue.white.bold(' 📋 DETAILED MODELS LIST '));
models.forEach((model, index) => {
console.log(`\n${chalk.cyan.bold(`${index + 1}. ${model.model_name}`)}`);
console.log(` ${chalk.gray('Identifier:')} ${chalk.yellow(model.model_identifier)}`);
console.log(` ${chalk.gray('Size:')} ${chalk.green(model.main_size || 'Unknown')}`);
console.log(` ${chalk.gray('Context:')} ${chalk.blue(model.context_length || 'Unknown')}`);
console.log(` ${chalk.gray('Input types:')} ${chalk.magenta((model.input_types || ['text']).join(', '))}`);
console.log(` ${chalk.gray('Category:')} ${getCategoryColor(model.category || 'general')(model.category || 'general')}`);
console.log(` ${chalk.gray('Pulls:')} ${chalk.green((model.pulls || 0).toLocaleString())}`);
console.log(` ${chalk.gray('Description:')} ${model.description || model.detailed_description || 'No description'}`);
if (model.use_cases && model.use_cases.length > 0) {
console.log(` ${chalk.gray('Use cases:')} ${model.use_cases.map(uc => chalk.magenta(uc)).join(', ')}`);
}
if (model.tags && model.tags.length > 0) {
console.log(` ${chalk.gray(`Available variants (${model.tags.length}):`)} `);
// Mostrar las primeras 10 variantes, agrupadas de 5 por línea
const tagsToShow = model.tags.slice(0, 15);
for (let i = 0; i < tagsToShow.length; i += 5) {
const batch = tagsToShow.slice(i, i + 5);
console.log(` ${batch.map(tag => chalk.blue(tag)).join(', ')}`);
}
if (model.tags.length > 15) {
console.log(` ${chalk.gray(`... and ${model.tags.length - 15} more variants`)}`);
}
}
if (model.quantizations && model.quantizations.length > 0) {
console.log(` ${chalk.gray('Quantizations found:')} ${model.quantizations.map(q => chalk.green(q)).join(', ')}`);
}
console.log(` ${chalk.gray('Base command:')} ${chalk.cyan.bold(`ollama pull ${model.model_identifier}`)}`);
console.log(` ${chalk.gray('Example variant:')} ${chalk.cyan.bold(`ollama pull ${model.tags && model.tags.length > 0 ? model.tags[0] : model.model_identifier}`)}`);
console.log(` ${chalk.gray('Updated:')} ${model.last_updated || 'Unknown'}`);
});
}
function getCategoryColor(category) {
const colors = {
coding: chalk.blue,
talking: chalk.green,
reading: chalk.yellow,
reasoning: chalk.red,
multimodal: chalk.magenta,
creative: chalk.cyan,
general: chalk.gray,
chat: chalk.green,
embeddings: chalk.blue
};
return colors[category] || chalk.gray;
}
function displaySampleCommands(topModels) {
console.log('\n' + chalk.bgYellow.black.bold(' ⚡ SAMPLE COMMANDS '));
console.log(chalk.yellow('╭' + '─'.repeat(60)));
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Try these popular models:')}`);
topModels.forEach((model, index) => {
const command = `ollama pull ${model.model_identifier}`;
console.log(chalk.yellow('│') + ` ${index + 1}. ${chalk.cyan.bold(command)}`);
});
console.log(chalk.yellow('│'));
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Browse models by category:')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category coding')} ${chalk.gray('(Programming & development)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reasoning')} ${chalk.gray('(Logic & math problems)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category talking')} ${chalk.gray('(Chat & conversations)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reading')} ${chalk.gray('(Text analysis & comprehension)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category multimodal')} ${chalk.gray('(Image & vision tasks)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category creative')} ${chalk.gray('(Creative writing & stories)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category general')} ${chalk.gray('(General purpose tasks)')}`);
console.log(chalk.yellow('│'));
console.log(chalk.yellow('│') + ` ${chalk.bold.white('AI-powered selection:')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-check --category coding --top 12')} ${chalk.gray('(AI meta-evaluation)')}`);
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-run')} ${chalk.gray('(Smart model selection & launch)')}`);
console.log(chalk.yellow('│'));
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Additional options:')}`);
console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --popular --limit 10')}`);
console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --json > models.json')}`);
console.log(chalk.yellow('╰'));
}
async function checkIfModelInstalled(model, ollamaInfo) {
try {
// Si Ollama no está disponible, no hay modelos instalados
if (!ollamaInfo || !ollamaInfo.available) {
return false;
}
// Ejecutar 'ollama list' para obtener modelos instalados
const installedModels = await new Promise((resolve, reject) => {
try {
const ollama = spawn('ollama', ['list'], { stdio: 'pipe' });
let output = '';
ollama.stdout.on('data', (data) => {
output += data.toString();
});
ollama.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
resolve(''); // Si falla, asumimos que no hay modelos
}
});
ollama.on('error', (err) => {
// Handle ENOENT and other spawn errors gracefully
if (err.code === 'ENOENT') {
resolve(''); // Ollama not found, no models installed
} else {
resolve(''); // Any other error, assume no models
}
});
} catch (spawnError) {
// Handle synchronous spawn errors
resolve(''); // If spawn itself fails, no models available
}
});
// Parsear la salida de 'ollama list'
const lines = installedModels.split('\n');
const modelNames = [];
for (let i = 1; i < lines.length; i++) { // Skip header
const line = lines[i].trim();
if (line) {
const parts = line.split(/\s+/);
if (parts.length > 0) {
modelNames.push(parts[0].toLowerCase());
}
}
}
// Generar el comando de instalación esperado para el modelo
const expectedCommand = getOllamaInstallCommand(model);
if (!expectedCommand) return false;
// Extraer el nombre del modelo del comando (ej: "ollama pull mistral:7b" -> "mistral:7b")
const modelNameMatch = expectedCommand.match(/ollama pull (.+)/);
if (!modelNameMatch) return false;
const expectedModelName = modelNameMatch[1].toLowerCase();
// Verificar si el modelo está en la lista de instalados
return modelNames.some(installedName =>
installedName === expectedModelName ||
installedName.startsWith(expectedModelName.split(':')[0])
);
} catch (error) {
// Si hay algún error, asumimos que no está instalado
return false;
}
}
function displaySimplifiedSystemInfo(hardware) {
console.log(chalk.cyan.bold('\nSYSTEM SUMMARY'));
console.log(chalk.gray('─'.repeat(50)));
const cpuInfo = `${hardware.cpu.brand} (${hardware.cpu.cores} cores)`;
const memInfo = `${hardware.memory.total}GB RAM`;
const gpuInfo = hardware.gpu.model || 'Integrated GPU';
console.log(`CPU: ${chalk.white(cpuInfo)}`);
console.log(`Memory: ${chalk.white(memInfo)}`);
console.log(`GPU: ${chalk.white(gpuInfo)}`);
console.log(`Architecture: ${chalk.white(hardware.cpu.architecture)}`);
console.log(`Backend: ${chalk.white(getBackendLabelForDisplay(hardware))}`);
const tier = getHardwareTierForDisplay(hardware);
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
console.log(`Hardware Tier: ${tierColor.bold(tier)}`);
}
async function displayModelRecommendations(analysis, hardware, useCase = 'general', limit = 1, runtime = 'ollama') {
const title = limit === 1 ? 'RECOMMENDED MODEL' : `TOP ${limit} COMPATIBLE MODELS`;
console.log(chalk.green.bold(`\n${title}`));
console.log(chalk.gray('─'.repeat(50)));
const selectedRuntime = normalizeRuntime(runtime);
const runtimeLabel = getRuntimeDisplayName(selectedRuntime);
const speculativeEstimator = new SpeculativeDecodingEstimator();
const speculativeCandidatePool = [
...(analysis?.compatible || []),
...(analysis?.marginal || [])
];
// Find the best models from compatible models considering use case
let selectedModels = [];
let reason = '';
if (analysis.compatible && analysis.compatible.length > 0) {
// First, try to find models that match the use case
let candidateModels = analysis.compatible;
// Apply intelligent filtering based on use case
if (useCase && useCase !== 'general') {
// Specific use case filtering
const useCaseModels = candidateModels.filter(model => {
const specialization = model.specialization?.toLowerCase();
const category = model.category?.toLowerCase();
const lowerUseCase = useCase.toLowerCase();
switch (lowerUseCase) {
case 'coding':
case 'code':
return model.primary_category === 'coding' ||
model.categories?.includes('coding') ||
specialization === 'code' || category === 'coding' ||
model.name.toLowerCase().includes('code') ||
model.name.toLowerCase().includes('coder');
case 'creative':
case 'writing':
return model.primary_category === 'creative' ||
model.categories?.includes('creative') ||
category === 'creative' || specialization === 'creative' ||
model.name.toLowerCase().includes('dolphin') ||
model.name.toLowerCase().includes('wizard') ||
model.name.toLowerCase().includes('uncensored');
case 'chat':
case 'conversation':
case 'talking':
// Prefer chat models, exclude coding models
// First, hard exclude coding models
if (model.primary_category === 'coding' ||
specialization === 'code' ||
model.name.toLowerCase().includes('code') ||
model.name.toLowerCase().includes('coder')) {
return false;
}
// Then include chat models (coding exclusion above takes precedence)
return model.primary_category === 'chat' ||
model.categories?.includes('chat') ||
category === 'talking' || specialization === 'chat' ||
(model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
(model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
(model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
(!model.name.toLowerCase().includes('llava') &&
(specialization === 'general' || category === 'medium'));
case 'multimodal':
case 'vision':
return model.primary_category === 'multimodal' ||
model.categories?.includes('multimodal') ||
category === 'multimodal' ||
model.name.toLowerCase().includes('llava') ||
model.name.toLowerCase().includes('vision');
case 'embeddings':
case 'embedings': // typo tolerance
case 'embedding':
case 'embeding': // typo tolerance
return model.primary_category === 'embeddings' ||
model.categories?.includes('embeddings') ||
category === 'embeddings' ||
model.name.toLowerCase().includes('embed') ||
model.name.toLowerCase().includes('bge');
case 'reasoning':
case 'reason':
return model.primary_category === 'reasoning' ||
model.categories?.includes('reasoning') ||
category === 'reasoning' ||
model.name.toLowerCase().includes('deepseek-r1') ||
model.name.toLowerCase().includes('reasoning');
default:
// Check for partial matches with typo tolerance
if (lowerUseCase.includes('embed')) {
return model.primary_category === 'embeddings' ||
model.categories?.includes('embeddings') ||
category === 'embeddings' ||
model.name.toLowerCase().includes('embed');
}
if (lowerUseCase.includes('code')) {
return model.primary_category === 'coding' ||
model.categories?.includes('coding');
}
if (lowerUseCase.includes('creat')) {
return model.primary_category === 'creative' ||
model.categories?.includes('creative');
}
return true;
}
});
// If we found use case specific models, use those, otherwise search Ollama database
if (useCaseModels.length > 0) {
candidateModels = useCaseModels;
reason = `Best ${useCase} model for your hardware`;
} else {
// Search directly in Ollama database for use case specific models
const ollamaModels = searchOllamaModelsForUseCase(useCase, hardware);
if (ollamaModels.length > 0) {
candidateModels = ollamaModels;
reason = `Best ${useCase} model from Ollama database`;
}
}
} else {
// No specific use case - apply intelligent general filtering
// First, infer categories for static models that don't have them
const modelsWithCategories = candidateModels.map(model => {
if (!model.primary_category) {
const modelName = model.name.toLowerCase();
let inferredCategory = 'general';
if (modelName.includes('code') || modelName.includes('coder')) {
inferredCategory = 'coding';
} else if (modelName.includes('llava') || modelName.includes('vision')) {
inferredCategory = 'multimodal';
} else if (modelName.includes('embed')) {
inferredCategory = 'embeddings';
} else if (modelName.includes('llama') || modelName.includes('mistral') ||
modelName.includes('qwen') || modelName.includes('gemma')) {
inferredCategory = 'chat';
} else if (modelName.includes('phi') && modelName.includes('mini')) {
inferredCategory = 'reasoning';
}
return { ...model, primary_category: inferredCategory };
}
return model;
});
// Prefer versatile models, exclude highly specialized ones
const generalModels = modelsWithCategories.filter(model => {
// Exclude very specialized models
if (model.primary_category === 'embeddings' ||
model.primary_category === 'safety' ||
model.primary_category === 'multimodal') {
return false;
}
// Include chat, coding, reasoning, creative, and general models
return model.primary_category === 'chat' ||
model.primary_category === 'coding' ||
model.primary_category === 'reasoning' ||
model.primary_category === 'creative' ||
model.primary_category === 'general' ||
model.specialization === 'general' ||
model.category === 'medium' ||
model.category === 'small';
});
if (generalModels.length > 0) {
// Re-score general models with category bonus
const scoredModels = generalModels.map(model => {
let adjustedScore = model.score || 0;
// Apply category bonuses for general use
if (model.primary_category === 'chat') {
adjustedScore += 5; // Chat models are great for general use
} else if (model.primary_category === 'coding') {
adjustedScore += 3; // Coding models are versatile
} else if (model.primary_category === 'reasoning') {
adjustedScore += 4; // Reasoning models are smart
} else if (model.primary_category === 'creative') {
adjustedScore += 2; // Creative models are fun
}
return { ...model, adjustedScore };
});
candidateModels = scoredModels.sort((a, b) => b.adjustedScore - a.adjustedScore);
reason = 'Best general-purpose model for your hardware';
}
}
// Filter out unreasonably large models before final selection
const reasonableSizedModels = candidateModels.filter(model => {
const realSize = getRealSizeFromOllamaCache(model);
const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
// For hardware with 24GB RAM, models >25GB are not practical
const maxReasonableSize = hardware.memory.total > 32 ? 50 : 25;
return sizeGB === 0 || sizeGB <= maxReasonableSize; // 0 means unknown/fallback
});
// Sort by score and get the top models (use adjustedScore if available)
const sortedModels = reasonableSizedModels.sort((a, b) =>
(b.adjustedScore || b.score || 0) - (a.adjustedScore || a.score || 0)
);
selectedModels = sortedModels.slice(0, limit);
if (!reason) {
reason = 'Highest compatibility score for your hardware';
}
} else if (analysis.marginal && analysis.marginal.length > 0) {
let marginalCandidates = analysis.marginal;
// Apply same use case filtering to marginal models
if (useCase && useCase !== 'general') {
const useCaseMarginal = marginalCandidates.filter(model => {
const specialization = model.specialization?.toLowerCase();
const category = model.category?.toLowerCase();
const lowerUseCase = useCase.toLowerCase();
switch (lowerUseCase) {
case 'coding':
case 'code':
return model.primary_category === 'coding' ||
model.categories?.includes('coding') ||
specialization === 'code' || category === 'coding' ||
model.name.toLowerCase().includes('code') ||
model.name.toLowerCase().includes('coder');
case 'creative':
case 'writing':
return model.primary_category === 'creative' ||
model.categories?.includes('creative') ||
category === 'creative' || specialization === 'creative' ||
model.name.toLowerCase().includes('dolphin') ||
model.name.toLowerCase().includes('wizard') ||
model.name.toLowerCase().includes('uncensored');
case 'chat':
case 'conversation':
case 'talking':
// First, hard exclude coding models
if (model.primary_category === 'coding' ||
specialization === 'code' ||
model.name.toLowerCase().includes('code') ||
model.name.toLowerCase().includes('coder')) {
return false;
}
// Then include chat models
return model.primary_category === 'chat' ||
model.categories?.includes('chat') ||
category === 'talking' || specialization === 'chat' ||
(model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
(model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
(model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
(!model.name.toLowerCase().includes('llava') &&
(specialization === 'general' || category === 'medium'));
case 'multimodal':
case 'vision':
return model.primary_category === 'multimodal' ||
model.categories?.includes('multimodal') ||
category === 'multimodal' ||
model.name.toLowerCase().includes('llava') ||
model.name.toLowerCase().includes('vision');
case 'embeddings':
case 'embedings': // typo tolerance
case 'embedding':
case 'embeding': // typo tolerance
return model.primary_category === 'embeddings' ||
model.categories?.includes('embeddings') ||
category === 'embeddings' ||
model.name.toLowerCase().includes('embed') ||
model.name.toLowerCase().includes('bge');
case 'reasoning':
case 'reason':
return model.primary_category === 'reasoning' ||
model.categories?.includes('reasoning') ||
category === 'reasoning' ||
model.name.toLowerCase().includes('deepseek-r1') ||
model.name.toLowerCase().includes('reasoning');
default:
// Check for partial matches with typo tolerance
if (lowerUseCase.includes('embed')) {
return model.primary_category === 'embeddings' ||
model.categories?.includes('embeddings') ||
category === 'embeddings' ||
model.name.toLowerCase().includes('embed');
}
if (lowerUseCase.includes('code')) {
return model.primary_category === 'coding' ||
model.categories?.includes('coding') ||
category === 'coding' ||
model.name.toLowerCase().includes('code');
}
if (lowerUseCase.includes('creat')) {
return model.primary_category === 'creative' ||
model.categories?.includes('creative') ||
category === 'creative';
}
if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) {
return model.primary_category === 'chat' ||
model.categories?.includes('chat') ||
category === 'chat';
}
if (lowerUseCase.includes('vision') || lowerUseCase.includes('image')) {
return model.primary_category === 'multimodal' ||
model.categories?.includes('multimodal') ||
model.name.toLowerCase().includes('llava');
}
return true; // Include if no specific pattern matches
}
});
if (useCaseMarginal.length > 0) {
marginalCandidates = useCaseMarginal;
reason = `Best ${useCase} model for your hardware`;
} else {
reason = 'Best available option (marginal performance)';
}
} else {
reason = 'Best available option (marginal performance)';
}
const sortedMarginal = marginalCandidates.sort((a, b) => (b.score || 0) - (a.score || 0));
selectedModels = sortedMarginal.slice(0, limit);
}
if (selectedModels && selectedModels.length > 0) {
for (let index = 0; index < selectedModels.length; index++) {
const model = selectedModels[index];
if (limit > 1) {
const rank = index + 1;
const rankColor = rank === 1 ? chalk.yellow : chalk.gray;
console.log(`\n${rankColor.bold(`#${rank} - ${model.name}`)}`);
} else {
console.log(`Model: ${chalk.cyan.bold(model.name)}`);
}
// Get real size from Ollama cache or estimate
const realSize = getRealSizeFromOllamaCache(model) || estimateModelSize(model);
console.log(`Size: ${chalk.white(realSize)}`);
console.log(`Compatibility Score: ${chalk.green.bold(model.adjustedScore || model.score || 'N/A')}/100`);
const fineTuningSupport = evaluateFineTuningSupport(model, hardware);
console.log(`Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
if (index === 0) {
console.log(`Reason: ${chalk.gray(reason)}`);
}
// Show performance if available
if (model.performanceEstimate) {
console.log(`Estimated Speed: ${chalk.yellow(model.performanceEstimate.estimatedTokensPerSecond || 'N/A')} tokens/sec`);
}
console.log(`Runtime: ${chalk.white(runtimeLabel)}`);
const runtimeCommands = getRuntimeCommandSet(model, selectedRuntime);
// Check installation only when using Ollama runtime.
let isInstalled = false;
if (selectedRuntime === 'ollama') {
try {
isInstalled = await checkIfModelInstalled(model, analysis.ollamaInfo);
if (isInstalled) {
console.log(`Status: ${chalk.green('Already installed in Ollama')}`);
} else if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
console.log(`Status: ${chalk.gray('Available for installation')}`);
} else {
console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
}
} catch (installCheckError) {
if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
console.log(`Status: ${chalk.gray('Available for installation')}`);
} else {
console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
}
}
const ollamaCommand = getOllamaInstallCommand(model);
if (ollamaCommand) {
const modelName = extractModelName(ollamaCommand);
if (isInstalled) {
console.log(`\nRun: ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
} else {
console.log(`\nPull: ${chalk.cyan.bold(ollamaCommand)}`);
}
} else if (model.ollamaTag || model.ollamaId) {
const tag = model.ollamaTag || model.ollamaId;
if (isInstalled) {
console.log(`\nRun: ${chalk.cyan.bold(`ollama run ${tag}`)}`);
} else {
console.log(`\nPull: ${chalk.cyan.bold(`ollama pull ${tag}`)}`);
}
}
} else {
console.log(`Status: ${chalk.gray(`${runtimeLabel} runtime selected`)}`);
console.log(`\nRun: ${runtimeCommands.run ? chalk.cyan.bold(runtimeCommands.run) : chalk.yellow('Select a matching repository or local model file for this runtime.')}`);
if (index === 0) {
if (runtimeCommands.install) console.log(`Install runtime: ${chalk.cyan.bold(runtimeCommands.install)}`);
if (runtimeCommands.pull) console.log(`Fetch model: ${chalk.cyan.bold(runtimeCommands.pull)}`);
}
}
const speculativeInfo =
model.speculativeDecoding ||
speculativeEstimator.estimate({
model,
candidates: speculativeCandidatePool,
hardware,
runtime: selectedRuntime
});
if (speculativeInfo && speculativeInfo.runtime === selectedRuntime) {
if (speculativeInfo.enabled) {
console.log(
`SpecDec: ${chalk.green(`+${speculativeInfo.estimatedThroughputGainPct}%`)} ` +
`(${chalk.gray(`draft: ${speculativeInfo.draftModel}`)})`
);
} else if (speculativeInfo.estimatedSpeedup) {
const suggested = speculativeInfo.suggestedDraftModel ? ` with ${speculativeInfo.suggestedDraftModel}` : '';
console.log(
`SpecDec estimate: ${chalk.yellow(`+${speculativeInfo.estimatedThroughputGainPct}%`)}${chalk.gray(suggested)}`
);
}
}
}
} else {
console.log(chalk.yellow('No compatible models found for your hardware'));
console.log(chalk.gray('Try running with --include-cloud to see more options'));
}
return selectedModels;
}
async function displayQuickStartCommands(analysis, recommendedModel = null, allRecommended = null, runtime = 'ollama') {
console.log(chalk.yellow.bold('\nQUICK START'));
console.log(chalk.gray('─'.repeat(50)));
const selectedRuntime = normalizeRuntime(runtime);
const runtimeLabel = getRuntimeDisplayName(selectedRuntime);
// Use the first model from allRecommended if available, otherwise fallback to recommendedModel
let bestModel = (allRecommended && allRecommended.length > 0) ? allRecommended[0] : recommendedModel;
if (!bestModel) {
if (analysis.compatible && analysis.compatible.length > 0) {
const sortedModels = analysis.compatible.sort((a, b) => (b.score || 0) - (a.score || 0));
bestModel = sortedModels[0];
} else if (analysis.marginal && analysis.marginal.length > 0) {
const sortedMarginal = analysis.marginal.sort((a, b) => (b.score || 0) - (a.score || 0));
bestModel = sortedMarginal[0];
}
}
if (selectedRuntime !== 'ollama') {
if (!bestModel) {
console.log(`1. Try expanding search: ${chalk.cyan('llm-checker check --include-cloud')}`);
return;
}
const runtimeCommands = getRuntimeCommandSet(bestModel, selectedRuntime);
if (!runtimeCommands.run || !runtimeCommands.pull) {
console.log(chalk.yellow(`Select a matching repository or local model file for ${runtimeLabel}.`));
return;
}
console.log(`1. Install ${runtimeLabel}:`);
console.log(` ${chalk.cyan.bold(runtimeCommands.install)}`);
console.log(`2. Fetch model weights:`);
console.log(` ${chalk.cyan.bold(runtimeCommands.pull)}`);
console.log(`3. Run model:`);
console.log(` ${chalk.cyan.bold(runtimeCommands.run)}`);
const speculative = bestModel.speculativeDecoding;
if (speculative && speculative.enabled) {
console.log(`4. SpecDec suggestion (${chalk.green(`+${speculative.estimatedThroughputGainPct}%`)}):`);
if (selectedRuntime === 'vllm') {
console.log(` ${chalk.cyan.bold(`${runtimeCommands.run} --speculative-model '${speculative.draftModelRef || speculative.draftModel}'`)}`);
} else if (selectedRuntime === 'mlx') {
console.log(` ${chalk.gray(`Use draft model ${speculative.draftModelRef || speculative.draftModel} when enabling speculative decoding in MLX-LM`)}`);
}
}
return;
}
if (analysis.ollamaInfo && !analysis.ollamaInfo.available) {
console.log(`1. Install Ollama: ${chalk.underline('https://ollama.ai')}`);
console.log(`2. Come back and run this command again`);
} else if (bestModel) {
let isInstalled = false;
try {
isInstalled = await checkIfModelInstalled(bestModel, analysis.ollamaInfo);
} catch (installCheckError) {
// If checking installation status fails, assume not installed
isInstalled = false;
}
if (isInstalled) {
const ollamaCommand = getOllamaInstallCommand(bestModel);
const modelName = ollamaCommand ? extractModelName(ollamaCommand) : bestModel.name.toLowerCase();
console.log(`1. Start using your installed model:`);
console.log(` ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
} else {
// Try to find Ollama command
const ollamaCommand = getOllamaInstallCommand(bestModel);
if (ollamaCommand) {
console.log(`1. Install the recommended model:`);
console.log(` ${chalk.cyan.bold(ollamaCommand)}`);
console.log(`2. Start using it:`);
console.log(` ${chalk.cyan.bold(`ollama run ${extractModelName(ollamaCommand)}`)}`);
} else {
console.log(`1. Search for ${bestModel.name} on Ollama Hub`);
console.log(`2. Install and run the model`);
}
}
// If multiple models were shown, suggest trying alternatives (only reasonable ones)
if (allRecommended && allRecommended.length > 1) {
console.log(`\n${chalk.gray('Alternative options:')}`);
// Filter out unreasonable alternatives (>50GB, no ollama command)
const reasonableAlternatives = allRecommended.slice(1).filter(model => {
const realSize = getRealSizeFromOllamaCache(model);
const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
const ollamaCommand = getOllamaInstallCommand(model);
// Only show if size is reasonable (<50GB) and has ollama command
return sizeGB < 50 && ollamaCommand;
});
// Show max 2 alternatives, avoid duplicating commands
const seenCommands = new Set();
const bestModelCommand = getOllamaInstallCommand(bestModel);
if (bestModelCommand) seenCommands.add(bestModelCommand);
let alternativeCount = 0;
reasonableAlternatives.forEach((model) => {
if (alternativeCount >= 2) return; // Max 2 alternatives
const ollamaCommand = getOllamaInstallCommand(model);
if (ollamaCommand && !seenCommands.has(ollamaCommand)) {
console.log(` ${chalk.gray(`${alternativeCount + 2}. ${ollamaCommand}`)}`);
seenCommands.add(ollamaCommand);
alternativeCount++;
}
});
// If no reasonable alternatives, don't show the section
if (reasonableAlternatives.length === 0) {
console.log(` ${chalk.gray('No other reasonable alternatives found for your hardware')}`);
}
}
} else {
console.log(`1. Try expanding search: ${chalk.cyan('llm-checker check --include-cloud')}`);
console.log(`2. Or see all available models: ${chalk.cyan('llm-checker list-models')}`);
}
}
function getOllamaInstallCommand(model) {
// Special handling for specific models that need corrected commands
const modelName = model.name.toLowerCase();
if (modelName.includes('codellama') && modelName.includes('7b')) {
return 'ollama pull codellama:7b';
}
if (modelName.includes('mistral') && modelName.includes('7b')) {
return 'ollama pull mistral:7b';
}
if (modelName.includes('llama 3.1') && modelName.includes('8b')) {
return 'ollama pull llama3.1:8b';
}
if (modelName.includes('llama3.1') && !modelName.includes('8b')) {
return 'ollama pull llama3.1:8b'; // Default to 8b variant
}
if (modelName.includes('llama3.2-vision')) {
return 'ollama pull llama3.2-vision:latest';
}
if (modelName.includes('llama3.2')) {
return 'ollama pull llama3.2:3b'; // Most common variant
}
if (modelName.includes('llama3.3')) {
return 'ollama pull llama3.3:70b'; // This is the actual size
}
if (modelName.includes('qwen') && modelName.includes('7b')) {
return 'ollama pull qwen2.5:7b';
}
if (modelName.includes('phi4-reasoning')) {
return 'ollama pull phi4-reasoning:latest';
}
if (modelName.includes('deepseek-r1')) {
return 'ollama pull deepseek-r1:8b';
}
if (modelName.includes('dolphin3')) {
return 'ollama pull dolphin3:latest';
}
if (modelName === 'phi' || modelName.includes('phi ')) {
return 'ollama pull phi:latest';
}
// First priority: use ollamaTag if available (from Ollama database)
if (model.ollamaTag) {
return `ollama pull ${model.ollamaTag}`;
}
// Second priority: use installation.ollama if available
if (model.installation && model.installation.ollama) {
return model.installation.ollama;
}
// Third priority: try to generate from model name
const mapping = {
'tinyllama 1.1b': 'ollama pull tinyllama:1.1b',
'phi-3 mini 3.8b': 'ollama pull phi3:mini',
'llama 3.2 3b': 'ollama pull llama3.2:3b',
'llama 3.1 8b': 'ollama pull llama3.1:8b',
'mistral 7b': 'ollama pull mistral:7b',
'mistral 7b v0.3': 'ollama pull mistral:7b',
'qwen 2.5 7b': 'ollama pull qwen2.5:7b',
'codellama 7b': 'ollama pull codellama:7b',
'codellama': 'ollama pull codellama:7b'
};
for (const [key, command] of Object.entries(mapping)) {
if (modelName.includes(key) || key.includes(modelName)) {
return command;
}
}
// Last resort: use ollamaId if available
if (model.ollamaId) {
return `ollama pull ${model.ollamaId}`;
}
return null;
}
function extractModelName(command) {
const match = command.match(/ollama pull (.+)/);
return match ? match[1] : 'model';
}
function loadPolicyConfiguration(policyFile) {
const validation = policyManager.validatePolicyFile(policyFile);
if (!validation.valid) {
const details = validation.errors
.map((entry) => `${entry.path}: ${entry.message}`)
.join('; ');
throw new Error(`Invalid policy file: ${details}`);
}
return {
policyPath: validation.path,
policy: validation.policy,
policyEngine: new PolicyEngine(validation.policy)
};
}
function parseSizeFilterInput(sizeStr) {
if (!sizeStr) return null;
const match = String(sizeStr)
.toUpperCase()
.trim()
.match(/^([0-9]+(?:\.[0-9]+)?)\s*(B|GB)?$/);
if (!match) return null;
const value = Number.parseFloat(match[1]);
const unit = match[2] || 'B';
// Convert to "B params" approximation used by existing check flow
return unit === 'GB' ? value / 0.5 : value;
}
function normalizeUseCaseInput(useCase = '') {
const alias = String(useCase || '')
.toLowerCase()
.trim();
const useCaseMap = {
embed: 'embeddings',
embedding: 'embeddings',
embeddings: 'embeddings',
embedings: 'embeddings',
talk: 'chat',
talking: 'chat',
conversation: 'chat',
chat: 'chat'
};
return useCaseMap[alias] || alias || 'general';
}
function resolveAuditFormats(formatOption, policy) {
const requested = String(formatOption || 'json').trim().toLowerCase();
const allowed = new Set(['json', 'csv', 'sarif']);
if (requested === 'all') {
const configured = Array.isArray(policy?.reporting?.formats)
? policy.reporting.formats
.map((entry) => String(entry || '').trim().toLowerCase())
.filter((entry) => allowed.has(entry))
: [];
return configured.length > 0 ? configured : ['json', 'csv', 'sarif'];
}
if (!allowed.has(requested)) {
throw new Error('Invalid format. Use one of: json, csv, sarif, all');
}
return [requested];
}
function toAuditOutputPath({ outputPath, outputDir, commandName, format, timestamp }) {
if (outputPath) {
return path.resolve(outputPath);
}
const safeTimestamp = timestamp.replace(/[:.]/g, '-');
const extension = format === 'sarif' ? 'sarif.json' : format;
const fileName = `${commandName}-policy-audit-${safeTimestamp}.${extension}`;
return path.resolve(outputDir || 'audit-reports', fileName);
}
function writeReportFile(filePath, content) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
}
function displayPolicySummary(commandName, policyConfig, evaluation, enforcement) {
if (!policyConfig || !evaluation || !enforcement) return;
console.log('\n' + chalk.bgMagenta.white.bold(` POLICY SUMMARY (${commandName.toUpperCase()}) `));
console.log(chalk.magenta('╭' + '─'.repeat(65)));
console.log(chalk.magenta('│') + ` File: ${chalk.white(policyConfig.policyPath)}`);
console.log(
chalk.magenta('│') +
` Mode: ${chalk.cyan(enforcement.mode)} | Action: ${chalk.cyan(enforcement.onViolation)}`
);
console.log(chalk.magenta('│') + ` Total checked: ${chalk.white.bold(evaluation.totalChecked)}`);
console.log(chalk.magenta('│') + ` Pass: ${chalk.green.bold(evaluation.passCount)} | Fail: ${chalk.red.bold(evaluation.failCount)}`);
console.log(
chalk.magenta('│') +
` Suppressed: ${chalk.yellow.bold(evaluation.suppressedViolationCount || 0)} | Exceptions: ${chalk.cyan.bold(
evaluation.exceptionsAppliedCount || 0
)}`
);
if (evaluation.topViolations.length === 0) {
console.log(chalk.magenta('│') + ` Top violations: ${chalk.green('none')}`);
} else {
console.log(chalk.magenta('│') + ` Top violations:`);
evaluation.topViolations.slice(0, 3).forEach((violation) => {
console.log(
chalk.magenta('│') +
` - ${chalk.yellow(violation.code)}: ${chalk.white(violation.count)}`
);
});
}
if (enforcement.shouldBlock) {
console.log(
chalk.magenta('│') +
chalk.red.bold(
` Enforcement result: blocking violations detected (exit ${enforcement.exitCode})`
)
);
} else if (enforcement.mode === 'audit' && enforcement.hasFailures) {
console.log(
chalk.magenta('│') +
chalk.yellow(' Audit mode: violations reported, command exits with code 0')
);
} else if (enforcement.onViolation === 'warn' && enforcement.hasFailures) {
console.log(
chalk.magenta('│') +
chalk.yellow(' Enforce+warn: violations reported, command exits with code 0')
);
} else {
console.log(chalk.magenta('│') + chalk.green(' Policy check passed'));
}
console.log(chalk.magenta('╰' + '─'.repeat(65)));
}
program
.command('mcp-setup')
.description('Show or apply MCP setup for llm-checker (Claude Code, Codex, Cursor, Windsurf, Gemini, Kimi, Grok, or generic)')
.option('--client <name>', `Target MCP client (${MCP_SETUP_CLIENTS.join(', ')})`, 'claude')
.option('--name <server-name>', 'MCP server name in the client config', 'llm-checker')
.option('--npx', 'Use npx --yes --package llm-checker llm-checker-mcp instead of a global install')
.option('--apply', 'Apply the setup (run the client CLI or merge its config file)')
.option('-j, --json', 'Output setup details as JSON')
.action(async (options) => {
const client = String(options.client || 'claude').trim().toLowerCase();
if (!MCP_SETUP_CLIENTS.includes(client)) {
console.error(chalk.red(`Unknown MCP client "${options.client}".`));
console.error(chalk.yellow(`Supported clients: ${MCP_SETUP_CLIENTS.join(', ')}`));
process.exit(1);
}
// --- Claude Code (default): original behavior, unchanged ------------
if (client === 'claude') {
const primarySetup = buildClaudeMcpSetup(Boolean(options.npx), options.name);
const alternateSetup = buildClaudeMcpSetup(!Boolean(options.npx), options.name);
if (options.json) {
console.log(JSON.stringify({
client: 'claude',
recommended: {
command: 'claude',
args: primarySetup.claudeArgs,
commandLine: primarySetup.commandLine
},
alternatives: [
{
command: 'claude',
args: alternateSetup.claudeArgs,
commandLine: alternateSetup.commandLine
}
],
claudeDesktop: {
configPath: primarySetup.desktopConfigPath,
snippet: primarySetup.desktopConfig
}
}, null, 2));
return;
}
showAsciiArt('mcp-setup');
console.log(chalk.blue.bold('\nClaude Code MCP Setup'));
console.log(chalk.white('\nRecommended command:'));
console.log(chalk.cyan(` ${primarySetup.commandLine}`));
console.log(chalk.white('\nAlternative command:'));
console.log(chalk.gray(` ${alternateSetup.commandLine}`));
console.log(chalk.white('\nClaude Desktop config path (manual):'));
console.log(chalk.gray(` ${primarySetup.desktopConfigPath}`));
console.log(chalk.white('\nConfig snippet:'));
console.log(chalk.gray(JSON.stringify(primarySetup.desktopConfig, null, 2)));
if (!options.apply) {
console.log(chalk.green('\nTip: run with --apply to execute the command automatically.'));
return;
}
console.log(chalk.blue('\nApplying MCP setup via Claude CLI...\n'));
try {
const exitCode = await runExternalCommand('claude', primarySetup.claudeArgs);
if (exitCode === 0) {
console.log(chalk.green('\nClaude MCP setup applied successfully.'));
} else {
console.error(chalk.red(`\nClaude command exited with code ${exitCode}.`));
process.exit(exitCode || 1);
}
} catch (error) {
if (error && error.code === 'ENOENT') {
console.error(chalk.red('Could not find `claude` in PATH.'));
console.log(chalk.yellow('Run the printed command manually once Claude CLI is installed.'));
} else {
console.error(chalk.red(`Failed to apply MCP setup: ${error.message}`));
}
process.exit(1);
}
return;
}
// --- Other MCP clients ----------------------------------------------
const setup = buildMcpClientSetup(client, Boolean(options.npx), options.name);
if (options.json) {
console.log(JSON.stringify(setup, null, 2));
return;
}
showAsciiArt('mcp-setup');
console.log(chalk.blue.bold(`\nMCP Setup for ${client}`));
if (setup.configPath) {
console.log(chalk.white('\nConfig path:'));
console.log(chalk.gray(` ${setup.configPath}`));
}
console.log(chalk.white(`\nConfig snippet (${setup.format}):`));
const renderedSnippet = setup.format === 'toml'
? setup.snippet
: JSON.stringify(setup.snippet, null, 2);
console.log(chalk.gray(renderedSnippet));
if (setup.commandLine) {
console.log(chalk.white('\nEquivalent CLI command:'));
console.log(chalk.cyan(` ${setup.commandLine}`));
}
for (const note of setup.notes) {
console.log(chalk.gray(`\nNote: ${note}`));
}
if (!options.apply) {
if (setup.apply) {
console.log(chalk.green('\nTip: run with --apply to merge this into the config file automatically.'));
}
return;
}
if (!setup.apply) {
console.log(chalk.yellow('\nNothing to apply: merge the snippet above into your client config manually.'));
return;
}
console.log(chalk.blue(`\nApplying MCP setup to ${setup.configPath} ...\n`));
try {
const entry = getMcpServerEntry(setup.useNpx);
if (setup.apply === 'json-merge') {
mergeJsonMcpConfig(setup.configPath, setup.serverName, entry);
} else {
mergeTomlMcpConfig(setup.configPath, setup.serverName, entry);
}
console.log(chalk.green(`MCP setup applied: merged "${setup.serverName}" into ${setup.configPath}`));
} catch (error) {
console.error(chalk.red(`Failed to apply MCP setup: ${error.message}`));
process.exit(1);
}
});
const policyCommand = program
.command('policy')
.description('Manage enterprise policy files (policy.yaml)')
.showHelpAfterError();
policyCommand
.command('init')
.description('Create a policy.yaml template')
.option('-f, --file <path>', 'Policy file path', 'policy.yaml')
.option('--force', 'Overwrite existing file if it already exists')
.action((options) => {
try {
const result = policyManager.initPolicy(options.file, {
force: Boolean(options.force)
});
const status = result.overwritten ? 'overwritten' : 'created';
console.log(chalk.green(`Policy file ${status}: ${result.path}`));
} catch (error) {
console.error(chalk.red(`Failed to initialize policy: ${error.message}`));
process.exit(1);
}
});
policyCommand
.command('validate')
.description('Validate policy.yaml against the v1 schema')
.option('-f, --file <path>', 'Policy file path', 'policy.yaml')
.option('-j, --json', 'Output validation result as JSON')
.action((options) => {
try {
const result = policyManager.validatePolicyFile(options.file);
if (options.json) {
console.log(JSON.stringify({
valid: result.valid,
file: result.path,
errorCount: result.errors.length,
errors: result.errors
}, null, 2));
if (!result.valid) {
process.exit(1);
}
} else if (result.valid) {
const mode = result.policy?.mode || 'unknown';
console.log(chalk.green(`Policy is valid (${mode} mode): ${result.path}`));
} else {
console.error(chalk.red(`Policy validation failed: ${result.path}`));
result.errors.forEach((entry) => {
console.error(chalk.red(` - ${entry.path}: ${entry.message}`));
});
process.exit(1);
}
} catch (error) {
if (options.json) {
console.log(JSON.stringify({
valid: false,
file: policyManager.resolvePolicyPath(options.file),
errorCount: 1,
errors: [{ path: 'file', message: error.message }]
}, null, 2));
} else {
console.error(chalk.red(`Policy validation failed: ${error.message}`));
}
process.exit(1);
}
});
policyCommand.action(() => {
policyCommand.outputHelp();
});
const auditCommand = program
.command('audit')
.description('Run policy audits and export compliance reports')
.showHelpAfterError();
auditCommand
.command('export')
.description('Evaluate policy compliance and export JSON/CSV/SARIF reports')
.requiredOption('--policy <file>', 'Policy file path')
.option('--command <name>', 'Evaluation source: check | recommend', 'check')
.option('--format <format>', 'Report format: json | csv | sarif | all', 'json')
.option('--out <path>', 'Output file path (single-format export only)')
.option('--out-dir <path>', 'Output directory when --out is omitted', 'audit-reports')
.option('-u, --use-case <case>', 'Use case when --command check is selected', 'general')
.option('-c, --category <category>', 'Category hint when --command recommend is selected')
.option('--optimize <profile>', 'Optimization profile for recommend mode (balanced|speed|quality|context|coding)', 'balanced')
.option('--runtime <runtime>', 'Runtime for check/recommend mode (auto|ollama|vllm|mlx|llama.cpp|transformers)', 'auto')
.option('--include-cloud', 'Include cloud models in check-mode analysis')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--max-size <size>', 'Maximum model size for check mode (e.g., "24B" or "12GB")')
.option('--min-size <size>', 'Minimum model size for check mode (e.g., "3B" or "2GB")')
.option('-l, --limit <number>', 'Model analysis limit for check mode', '25')
.option('--no-verbose', 'Disable verbose progress while collecting audit inputs')
.action(async (options) => {
try {
const policyConfig = loadPolicyConfiguration(options.policy);
const selectedCommand = String(options.command || 'check')
.toLowerCase()
.trim();
if (!['check', 'recommend'].includes(selectedCommand)) {
throw new Error('Invalid --command value. Use "check" or "recommend".');
}
const exportFormats = resolveAuditFormats(options.format, policyConfig.policy);
if (options.out && exportFormats.length > 1) {
throw new Error('--out can only be used with a single export format.');
}
const verboseEnabled = options.verbose !== false;
const checker = new (getLLMChecker())({ verbose: verboseEnabled });
const hardware = await checker.getSystemInfo();
let runtimeBackend = 'ollama';
let policyCandidates = [];
let analysisResult = null;
let recommendationResult = null;
if (selectedCommand === 'check') {
let selectedRuntime = normalizeRuntime(options.runtime);
if (!selectedRuntime) throw new Error(`Invalid --runtime: ${options.runtime}`);
if (!runtimeSupportedOnHardware(selectedRuntime, hardware)) {
selectedRuntime = 'ollama';
}
const maxSize = parseSizeFilterInput(options.maxSize);
const minSize = parseSizeFilterInput(options.minSize);
const normalizedUseCase = normalizeUseCaseInput(options.useCase);
analysisResult = await checker.analyze({
useCase: normalizedUseCase,
includeCloud: Boolean(options.includeCloud),
limit: Number.parseInt(options.limit, 10) || 25,
maxSize,
minSize,
runtime: selectedRuntime,
includeUncensored: options.includeUncensored === true
});
runtimeBackend = selectedRuntime;
policyCandidates = collectCandidatesFromAnalysis(analysisResult);
} else {
recommendationResult = await checker.generateIntelligentRecommendations(hardware, {
optimizeFor: options.optimize,
runtime: options.runtime,
includeUncensored: options.includeUncensored === true
});
if (!recommendationResult) {
throw new Error('Unable to generate recommendation data for policy audit export.');
}
runtimeBackend = recommendationResult.runtime || options.runtime || 'auto';
policyCandidates = collectCandidatesFromRecommendationData(recommendationResult);
}
const policyContext = buildPolicyRuntimeContext({
hardware,
runtimeBackend
});
const policyEvaluation = await evaluatePolicyCandidatesAsync(
policyConfig.policyEngine,
policyCandidates,
policyContext,
policyConfig.policy
);
const policyEnforcement = resolvePolicyEnforcement(policyConfig.policy, policyEvaluation);
const report = buildComplianceReport({
commandName: selectedCommand,
policyPath: policyConfig.policyPath,
policy: policyConfig.policy,
evaluation: policyEvaluation,
enforcement: policyEnforcement,
runtimeContext: policyContext,
options: {
format: exportFormats,
runtime: runtimeBackend,
use_case: selectedCommand === 'check' ? normalizeUseCaseInput(options.useCase) : null,
category: selectedCommand === 'recommend' ? options.category || null : null,
optimize: selectedCommand === 'recommend' ? options.optimize || 'balanced' : null,
include_cloud: Boolean(options.includeCloud),
include_uncensored: options.includeUncensored === true
},
hardware
});
const generatedAt = report.generated_at || new Date().toISOString();
const writtenFiles = [];
exportFormats.forEach((format) => {
const filePath = toAuditOutputPath({
outputPath: options.out,
outputDir: options.outDir,
commandName: selectedCommand,
format,
timestamp: generatedAt
});
const content = serializeComplianceReport(report, format);
writeReportFile(filePath, content);
writtenFiles.push({ format, filePath });
});
displayPolicySummary(`audit ${selectedCommand}`, policyConfig, policyEvaluation, policyEnforcement);
console.log('\n' + chalk.bgBlue.white.bold(' AUDIT EXPORT '));
writtenFiles.forEach((entry) => {
console.log(`${chalk.cyan(entry.format.toUpperCase())}: ${chalk.white(entry.filePath)}`);
});
if (policyEnforcement.shouldBlock) {
process.exit(policyEnforcement.exitCode);
}
} catch (error) {
console.error(chalk.red(`Audit export failed: ${error.message}`));
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
auditCommand.action(() => {
auditCommand.outputHelp();
});
program
.command('calibrate')
.description('Generate calibration contract artifacts from a JSONL prompt suite')
.requiredOption('--suite <file>', 'Prompt suite path in JSONL format')
.requiredOption(
'--models <identifiers...>',
'Model identifiers to include (repeat flag and/or comma-separate values)'
)
.requiredOption(
'--output <file>',
'Calibration result output path (.json, .yaml, or .yml)'
)
.option(
'--runtime <runtime>',
`Inference runtime (${SUPPORTED_RUNTIMES.join('|')})`,
'ollama'
)
.option(
'--mode <mode>',
'Execution mode (dry-run|contract-only|full). Default: contract-only'
)
.option(
'--objective <objective>',
`Calibration objective (${SUPPORTED_CALIBRATION_OBJECTIVES.join('|')})`,
'balanced'
)
.option(
'--policy-out <file>',
'Optional calibration policy output path (.json, .yaml, or .yml)'
)
.option('--warmup <count>', 'Warmup runs per prompt in full mode', '1')
.option('--iterations <count>', 'Measured iterations per prompt in full mode', '2')
.option('--timeout-ms <ms>', 'Per-prompt timeout in full mode', '120000')
.option('--dry-run', 'Produce draft artifacts without benchmark execution')
.addHelpText(
'after',
`
Examples:
$ llm-checker calibrate --suite ./prompts.jsonl --models qwen2.5-coder:7b llama3.2:3b --output ./calibration.json
$ llm-checker calibrate --suite ./prompts.jsonl --models qwen2.5-coder:7b --mode full --iterations 3 --output ./calibration.json --policy-out ./routing.yaml
$ llm-checker calibrate --suite ./prompts.jsonl --models qwen2.5-coder:7b,llama3.2:3b --output ./calibration.yaml --policy-out ./routing.yaml --dry-run
`
)
.action((options) => {
try {
const runtime = calibrationManager.validateRuntime(options.runtime);
const objective = calibrationManager.validateObjective(options.objective);
const executionMode = calibrationManager.resolveExecutionMode({
mode: options.mode,
dryRun: Boolean(options.dryRun)
});
const models = calibrationManager.parseModelIdentifiers(options.models);
const suite = calibrationManager.parsePromptSuite(options.suite);
let calibrationResult = null;
if (executionMode === 'full') {
calibrationResult = calibrationManager.runFullCalibration({
models,
suite,
runtime,
objective,
benchmarkConfig: {
warmupRuns: Number.parseInt(options.warmup, 10),
measuredIterations: Number.parseInt(options.iterations, 10),
timeoutMs: Number.parseInt(options.timeoutMs, 10)
}
});
} else {
calibrationResult = calibrationManager.buildDraftCalibrationResult({
models,
suiteMetadata: suite.metadata,
runtime,
objective,
executionMode
});
}
const resultPath = calibrationManager.writeArtifact(options.output, calibrationResult);
let policyPath = null;
if (options.policyOut) {
const calibrationPolicy = calibrationManager.buildDraftCalibrationPolicy({
calibrationResult,
calibrationResultPath: resultPath
});
policyPath = calibrationManager.writeArtifact(options.policyOut, calibrationPolicy);
}
console.log('\n' + chalk.bgBlue.white.bold(' CALIBRATION ARTIFACTS GENERATED '));
console.log(chalk.blue('╭' + '─'.repeat(72)));
console.log(chalk.blue('│') + ` Suite: ${chalk.white(suite.path)}`);
console.log(chalk.blue('│') + ` Runtime: ${chalk.cyan(runtime)} | Objective: ${chalk.cyan(objective)}`);
console.log(chalk.blue('│') + ` Models: ${chalk.white(String(models.length))}`);
console.log(chalk.blue('│') + ` Execution mode: ${chalk.yellow(executionMode)}`);
if (executionMode === 'full') {
console.log(
chalk.blue('│') +
` Successful: ${chalk.green(
String(calibrationResult.summary.successful_models)
)} | Failed: ${chalk.red(String(calibrationResult.summary.failed_models))}`
);
}
console.log(chalk.blue('│') + ` Result: ${chalk.green(resultPath)}`);
if (policyPath) {
console.log(chalk.blue('│') + ` Policy: ${chalk.green(policyPath)}`);
}
console.log(chalk.blue('╰' + '─'.repeat(72)));
} catch (error) {
console.error(chalk.red(`Calibration failed: ${error.message}`));
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('check')
.description('Analyze your system and show compatible LLM models')
.option('-d, --detailed', 'Show detailed hardware information')
.option('-f, --filter <type>', 'Filter by model type')
.option('-u, --use-case <case>', 'Specify use case', 'general')
.option('-l, --limit <number>', 'Number of compatible models to show (default: 1)', '1')
.option('--max-size <size>', 'Maximum model size to consider (e.g., "30B" or "30GB")')
.option('--min-size <size>', 'Minimum model size to consider (e.g., "7B" or "7GB")')
.option('--include-cloud', 'Include cloud models in analysis')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--ollama-only', 'Only show models available in Ollama')
.option('--runtime <runtime>', `Inference runtime (${SUPPORTED_RUNTIMES.join('|')})`, 'ollama')
.option('--policy <file>', 'Evaluate candidate models against a policy file')
.option('--performance-test', 'Run performance benchmarks')
.option('--show-ollama-analysis', 'Show detailed Ollama model analysis')
.option('--no-verbose', 'Disable step-by-step progress display')
.option('--simulate <profile>', 'Simulate a hardware profile instead of detecting real hardware (use "list" to see profiles)')
.option('--cpu-only', 'Force CPU/RAM execution even when a GPU is detected')
.option('--gpu <model>', 'Custom GPU model for simulation (e.g., "RTX 5060", "RX 7800 XT")')
.option('--ram <gb>', 'Custom RAM in GB for simulation (e.g., 32)')
.option('--cpu <model>', 'Custom CPU model for simulation (e.g., "AMD Ryzen 7 5700X")')
.option('--vram <gb>', 'Override GPU VRAM in GB for simulation (auto-detected if omitted)')
.addHelpText(
'after',
`
Enterprise policy examples:
$ llm-checker check --policy ./policy.yaml
$ llm-checker check --policy ./policy.yaml --use-case coding --runtime vllm
$ llm-checker check --policy ./policy.yaml --include-cloud --max-size 24B
Hardware simulation:
$ llm-checker check --simulate list
$ llm-checker check --simulate rtx4090
$ llm-checker check --simulate rtx4090 --cpu-only
$ llm-checker check --simulate m4pro24 --use-case coding
$ llm-checker check --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
CPU-only precedence:
- --cpu-only keeps the selected real/simulated CPU and RAM, but disables its GPU for fit and scoring
Policy scope:
- Evaluates all compatible and marginal candidates discovered during analysis
- Not limited to the top --limit results shown in output
`
)
.action(async (options) => {
showAsciiArt('check');
displayRecommendationCommandNote('check');
try {
// Use verbose progress unless explicitly disabled
const verboseEnabled = options.verbose !== false;
const cpuOnly = getCpuOnlyCommandMode(options);
const checker = new (getLLMChecker())({ verbose: verboseEnabled, cpuOnly });
const policyConfig = options.policy ? loadPolicyConfiguration(options.policy) : null;
// Handle hardware simulation (preset profile or custom flags)
const hasCustomHwFlags = options.gpu || options.ram || options.cpu || options.vram;
if (options.simulate || hasCustomHwFlags) {
const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, listProfiles } = require('../src/hardware/profiles');
if (options.simulate === 'list') {
console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
listProfiles().forEach(line => console.log(line));
console.log('');
return;
}
let simulatedHardware;
let displayLabel;
if (hasCustomHwFlags) {
const ramValue = options.ram ? parseInt(options.ram) : undefined;
const vramValue = options.vram ? parseInt(options.vram) : undefined;
if (options.vram && !options.gpu) {
console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
process.exit(1);
}
if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
process.exit(1);
}
if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
process.exit(1);
}
simulatedHardware = buildCustomHardwareObject({
gpu: options.gpu || null,
ram: ramValue,
cpu: options.cpu || null,
vram: vramValue
});
displayLabel = simulatedHardware._displayName;
} else {
const profile = getProfile(options.simulate);
if (!profile) {
console.error(chalk.red(`\n Unknown profile: ${options.simulate}`));
console.log(chalk.gray('\n Available profiles:'));
listProfiles().forEach(line => console.log(line));
console.log('');
process.exit(1);
}
simulatedHardware = buildFullHardwareObject(options.simulate);
displayLabel = profile.displayName;
}
checker.setSimulatedHardware(simulatedHardware);
console.log(chalk.magenta.bold(`\n SIMULATION MODE: ${displayLabel}\n`));
}
displayCpuOnlyModeNotice(cpuOnly, checker.isSimulated);
// If verbose is disabled, show simple loading message
if (!verboseEnabled) {
process.stdout.write(chalk.gray('Analyzing your system...'));
}
const hardware = await checker.getSystemInfo();
let selectedRuntime = normalizeRuntime(options.runtime);
if (!selectedRuntime) throw new Error(`Invalid --runtime: ${options.runtime}`);
if (!runtimeSupportedOnHardware(selectedRuntime, hardware)) {
const runtimeLabel = getRuntimeDisplayName(selectedRuntime);
console.log(
chalk.yellow(
`\nWarning: ${runtimeLabel} is not supported on this hardware. Falling back to Ollama.`
)
);
selectedRuntime = 'ollama';
}
// Normalize and fix use-case typos
const normalizeUseCase = (useCase = '') => {
const alias = useCase.toLowerCase().trim();
const useCaseMap = {
'embed': 'embeddings',
'embedding': 'embeddings',
'embeddings': 'embeddings',
'embedings': 'embeddings', // common typo
'talk': 'chat',
'chat': 'chat',
'talking': 'chat'
};
return useCaseMap[alias] || alias || 'general';
};
const normalizedUseCase = normalizeUseCase(options.useCase);
// Parse size filters
const parseSizeFilter = (sizeStr) => {
if (!sizeStr) return null;
const match = sizeStr.toUpperCase().match(/^(\d+\.?\d*)\s*(B|GB)?$/);
if (match) {
const num = parseFloat(match[1]);
const unit = match[2] || 'B';
// Return size in billions of parameters (B)
return unit === 'GB' ? num / 0.5 : num; // Approximate: 0.5GB per 1B params (Q4)
}
return null;
};
const maxSize = parseSizeFilter(options.maxSize);
const minSize = parseSizeFilter(options.minSize);
const analysis = await checker.analyze({
filter: options.filter,
useCase: normalizedUseCase,
includeCloud: options.includeCloud,
performanceTest: options.performanceTest,
limit: parseInt(options.limit) || 10,
maxSize: maxSize,
minSize: minSize,
runtime: selectedRuntime,
includeUncensored: options.includeUncensored === true,
cpuOnly
});
if (!verboseEnabled) {
console.log(chalk.green(' done'));
}
let policyEvaluation = null;
let policyEnforcement = null;
if (policyConfig) {
const policyCandidates = collectCandidatesFromAnalysis(analysis);
const policyContext = buildPolicyRuntimeContext({
hardware,
runtimeBackend: selectedRuntime
});
policyEvaluation = await evaluatePolicyCandidatesAsync(
policyConfig.policyEngine,
policyCandidates,
policyContext,
policyConfig.policy
);
policyEnforcement = resolvePolicyEnforcement(policyConfig.policy, policyEvaluation);
}
// Simplified output - show only essential information
displaySimplifiedSystemInfo(hardware);
const recommendedModels = await displayModelRecommendations(
analysis,
hardware,
normalizedUseCase,
parseInt(options.limit) || 1,
selectedRuntime
);
await displayQuickStartCommands(analysis, recommendedModels[0], recommendedModels, selectedRuntime);
if (policyConfig && policyEvaluation && policyEnforcement) {
displayPolicySummary('check', policyConfig, policyEvaluation, policyEnforcement);
if (policyEnforcement.shouldBlock) {
process.exit(policyEnforcement.exitCode);
}
}
} catch (error) {
console.error(chalk.red('\nError:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('ollama')
.description('Check Ollama integration status (use `installed` to rank installed models)')
.option('-l, --list', 'List installed models ranked by compatibility (runs `installed`)')
.action(async (options) => {
showAsciiArt('ollama');
const spinner = ora('Checking Ollama integration...').start();
try {
const checker = new (getLLMChecker())({ verbose: false });
const analysis = await checker.analyze();
if (!analysis.ollamaInfo.available) {
spinner.fail(`Ollama not available`);
console.log('\nTo install Ollama:');
console.log('Visit: https://ollama.ai');
if (analysis.ollamaInfo.hint) {
console.log(chalk.yellow('Hint: ' + analysis.ollamaInfo.hint));
}
if (analysis.ollamaInfo.attemptedURL) {
console.log(chalk.gray('Attempted URL: ' + analysis.ollamaInfo.attemptedURL));
console.log(chalk.gray('Set OLLAMA_BASE_URL environment variable to use a different client URL'));
}
return;
}
spinner.succeed(`Ollama integration active`);
if (options.list) {
console.log(chalk.cyan('\nRun `llm-checker installed` to rank your installed models by compatibility and use-case.'));
} else {
console.log(chalk.gray('\nTip: `llm-checker installed` ranks installed models; `llm-checker recommend` suggests models for your hardware.'));
}
} catch (error) {
spinner.fail('Error with Ollama integration');
console.error(chalk.red('Error:'), error.message);
}
});
// New command: installed - Show ranking of installed Ollama models
function formatVerificationCell(verification) {
if (!verification) return chalk.gray('-');
if (verification.status === 'verified') return chalk.green('✓');
if (verification.status === 'rejected') {
const name = verification.report ? verification.report.violationName : 'REJECTED';
return chalk.red(`✗ ${name}`);
}
const reason = String(verification.reason || 'unknown');
return chalk.gray(`skipped: ${reason.length > 30 ? reason.substring(0, 27) + '...' : reason}`);
}
program
.command('installed')
.description('Show ranking of installed Ollama models by compatibility and use-case')
.option('--sort <by>', 'Sort by: score, size, name (default: score)', 'score')
.option('--json', 'Output in JSON format')
.option('--verify', 'Structurally verify each installed model blob with modelvet (GGUF/safetensors)')
.action(async (options) => {
if (!options.json) showAsciiArt('installed');
const spinner = ora('Analyzing installed models...').start();
try {
const checker = new (getLLMChecker())({ verbose: false });
const OllamaClient = require('../src/ollama/client');
const ollamaClient = new OllamaClient();
// Check Ollama availability
const availability = await ollamaClient.checkOllamaAvailability();
if (!availability.available) {
spinner.fail('Ollama not available');
if (options.json) {
// --json must always emit parseable JSON on stdout (the success
// path prints an array); previously these branches printed
// ANSI-colored prose and broke `installed --json | jq`.
console.log(JSON.stringify([], null, 2));
} else {
console.log(chalk.red('\n' + availability.error));
if (availability.hint) {
console.log(chalk.yellow('Hint: ' + availability.hint));
}
}
process.exitCode = 1;
return;
}
// Get installed models
const installedModels = await ollamaClient.getLocalModels();
if (!installedModels || installedModels.length === 0) {
spinner.fail('No models installed');
if (options.json) {
console.log(JSON.stringify([], null, 2));
} else {
console.log(chalk.yellow('\nNo Ollama models found. Install one with:'));
console.log(chalk.cyan(' ollama pull llama3.2:3b'));
}
return;
}
// Get hardware info for scoring
const hardware = await checker.getSystemInfo();
const analysis = await checker.analyze({ limit: 100 });
spinner.succeed(`Found ${installedModels.length} installed models`);
// Score and categorize each installed model
const scoredModels = installedModels.map(model => {
// Find matching model in analysis
const matchingModel = [...(analysis.compatible || []), ...(analysis.marginal || [])].find(m =>
m.name && model.name && (
m.name.toLowerCase().includes(model.family) ||
model.name.toLowerCase().includes(m.name.toLowerCase().split(' ')[0])
)
);
// Determine use-case from model name
const nameLower = model.name.toLowerCase();
let useCase = 'general';
if (nameLower.includes('code') || nameLower.includes('coder') || nameLower.includes('deepseek-coder')) {
useCase = 'coding';
} else if (nameLower.includes('embed') || nameLower.includes('nomic') || nameLower.includes('bge')) {
useCase = 'embeddings';
} else if (nameLower.includes('llava') || nameLower.includes('vision') || nameLower.includes('bakllava')) {
useCase = 'multimodal';
} else if (nameLower.includes('r1') || nameLower.includes('qwq') || nameLower.includes('reasoning')) {
useCase = 'reasoning';
} else if (nameLower.includes('wizard') || nameLower.includes('creative')) {
useCase = 'creative';
} else if (nameLower.includes('chat') || nameLower.includes('instruct')) {
useCase = 'chat';
}
// Calculate compatibility score
const fileSizeGB = model.fileSizeGB || 0;
const availableRAM = hardware.memory.total * 0.8;
let score = 50;
// RAM fit score
if (fileSizeGB <= availableRAM * 0.3) score += 30;
else if (fileSizeGB <= availableRAM * 0.5) score += 20;
else if (fileSizeGB <= availableRAM * 0.7) score += 10;
else score -= 10;
// Size efficiency for hardware tier
const sizeMatch = (model.size || '').match(/(\d+)/);
const paramSize = sizeMatch ? parseInt(sizeMatch[1]) : 7;
if (hardware.memory.total >= 32 && paramSize >= 13) score += 10;
else if (hardware.memory.total >= 16 && paramSize >= 7) score += 10;
else if (hardware.memory.total >= 8 && paramSize <= 7) score += 10;
// Use matched model score if available
if (matchingModel && matchingModel.score) {
score = Math.round((score + matchingModel.score) / 2);
}
return {
name: model.name,
displayName: model.displayName,
size: model.size,
fileSizeGB: model.fileSizeGB,
quantization: model.quantization,
useCase: useCase,
score: Math.min(100, Math.max(0, score)),
command: `ollama run ${model.name}`
};
});
// Sort models
scoredModels.sort((a, b) => {
switch (options.sort) {
case 'size':
return b.fileSizeGB - a.fileSizeGB;
case 'name':
return a.name.localeCompare(b.name);
case 'score':
default:
return b.score - a.score;
}
});
// Optional modelvet structural verification of each model's local
// blob(s). Fail-soft: unresolvable blobs or verifier errors (e.g.
// >3 GiB wasm32 ceiling, missing artifact) become
// { status: 'skipped', reason }; only an affirmative REJECT turns
// the run red (exit code 1, also in --json mode).
let rejectedCount = 0;
if (options.verify) {
const { verifyOllamaModel } = require('../src/security/ollama-blobs');
const verifySpinner = options.json ? null : ora('Verifying model blobs with modelvet...').start();
for (const model of scoredModels) {
if (verifySpinner) verifySpinner.text = `Verifying ${model.name}...`;
const verification = await verifyOllamaModel(model.name);
model.verification = verification;
if (verification.status === 'rejected') rejectedCount += 1;
}
if (verifySpinner) {
if (rejectedCount > 0) {
verifySpinner.fail(`${rejectedCount} model(s) REJECTED by modelvet`);
} else {
verifySpinner.succeed('modelvet verification complete');
}
}
if (rejectedCount > 0) {
process.exitCode = 1;
}
}
// Output
if (options.json) {
console.log(JSON.stringify(scoredModels, null, 2));
return;
}
console.log('\n' + chalk.bgGreen.white.bold(' INSTALLED MODELS RANKING '));
console.log(chalk.green('╭' + '─'.repeat(75)));
console.log(chalk.green('│') + ` Sorted by: ${chalk.cyan(options.sort)} | Hardware: ${chalk.yellow(hardware.memory.total + 'GB RAM')}`);
console.log(chalk.green('├' + '─'.repeat(75)));
const headers = [
chalk.bold(' # '),
chalk.bold(' Model '),
chalk.bold(' Size '),
chalk.bold(' Score '),
chalk.bold(' Use Case '),
...(options.verify ? [chalk.bold(' Verified ')] : []),
chalk.bold(' Command ')
];
const data = [headers];
scoredModels.forEach((model, index) => {
const rank = index + 1;
const rankIcon = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : `${rank}.`;
const scoreColor = model.score >= 75 ? chalk.green : model.score >= 50 ? chalk.yellow : chalk.red;
const row = [
rankIcon,
model.name.length > 25 ? model.name.substring(0, 22) + '...' : model.name,
`${model.fileSizeGB}GB`,
scoreColor(`${model.score}/100`),
model.useCase
];
if (options.verify) {
row.push(formatVerificationCell(model.verification));
}
row.push(chalk.cyan(`ollama run ${model.name.split(':')[0]}`));
data.push(row);
});
console.log(table(data));
if (options.verify) {
const rejectedModels = scoredModels.filter(m => m.verification && m.verification.status === 'rejected');
if (rejectedModels.length > 0) {
console.log(chalk.red.bold('\nmodelvet REJECTED these models — do NOT load them in any runtime:'));
rejectedModels.forEach(m => {
const report = m.verification.report;
console.log(chalk.red(
` ✗ ${m.name}: ${report.violationName} (code ${report.violation}) at 0x${report.offset.toString(16)}`
));
});
}
console.log(chalk.gray('\nNote: a ✓ verdict is structural only. It says nothing about model'));
console.log(chalk.gray('behavior, provenance, or poisoned weights.'));
}
// Show suggestions for low-ranking models
const lowRankingModels = scoredModels.filter(m => m.score < 50);
if (lowRankingModels.length > 0) {
console.log(chalk.yellow('\nConsider removing these low-ranking models to free up space:'));
lowRankingModels.forEach(m => {
console.log(chalk.gray(` ollama rm ${m.name} # Score: ${m.score}/100, Size: ${m.fileSizeGB}GB`));
});
}
console.log(chalk.green('╰' + '─'.repeat(75)));
} catch (error) {
spinner.fail('Error analyzing installed models');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
}
});
program
.command('ollama-plan')
.description('Plan safe Ollama runtime settings for selected local models')
.option('--models <models...>', 'Model tags/families to include (default: all local models)')
.option('--ctx <tokens>', 'Target context window in tokens', '8192')
.option('--concurrency <n>', 'Target parallel request count', '2')
.option('--objective <mode>', 'Optimization objective (latency|balanced|throughput)', 'balanced')
.option('--reserve-gb <gb>', 'Memory reserve for OS and background workloads', '2')
.option('--json', 'Output plan as JSON')
.action(async (options) => {
const spinner = options.json ? null : ora('Building Ollama capacity plan...').start();
try {
const requestedObjective = String(options.objective || 'balanced').toLowerCase();
const supportedObjectives = new Set(['latency', 'balanced', 'throughput']);
if (!supportedObjectives.has(requestedObjective)) {
throw new Error(`Invalid objective "${options.objective}". Use latency, balanced, or throughput.`);
}
const targetContext = parsePositiveIntegerOption(options.ctx, '--ctx');
const targetConcurrency = parsePositiveIntegerOption(options.concurrency, '--concurrency');
const reserveGB = parseNonNegativeNumberOption(options.reserveGb, '--reserve-gb');
const OllamaClient = require('../src/ollama/client');
const UnifiedDetector = require('../src/hardware/unified-detector');
const OllamaCapacityPlanner = require('../src/ollama/capacity-planner');
const ollamaClient = new OllamaClient();
const availability = await ollamaClient.checkOllamaAvailability();
if (!availability.available) {
throw new Error(availability.error || 'Ollama is not available');
}
const localModels = await ollamaClient.getLocalModels();
if (!localModels || localModels.length === 0) {
throw new Error('No local Ollama models found. Install one with: ollama pull llama3.2:3b');
}
const { selected, missing } = selectModelsForPlan(localModels, options.models || []);
if (selected.length === 0) {
throw new Error(
`No matching local models found for: ${(options.models || []).join(', ')}`
);
}
const detector = new UnifiedDetector();
const hardware = await detector.detect();
const planner = new OllamaCapacityPlanner();
const plan = planner.plan({
hardware,
models: selected,
targetContext,
targetConcurrency,
objective: requestedObjective,
reserveGB
});
if (options.json) {
console.log(JSON.stringify({
generated_at: new Date().toISOString(),
selection: {
requested: options.models || [],
selected: selected.map((model) => model.name),
missing
},
plan
}, null, 2));
return;
}
if (spinner) spinner.succeed('Capacity plan generated');
console.log('\n' + chalk.bgBlue.white.bold(' OLLAMA CAPACITY PLAN '));
console.log(
chalk.blue('Hardware:'),
`${plan.hardware.backendName} (${plan.hardware.backend})`
);
console.log(
chalk.blue('Memory budget:'),
`${plan.memory.budgetGB}GB usable (reserve ${plan.hardware.reserveGB}GB)`
);
if (missing.length > 0) {
console.log(
chalk.yellow('Missing model filters:'),
missing.join(', ')
);
}
console.log(chalk.blue.bold('\nSelected models:'));
for (const model of plan.models) {
console.log(
` - ${model.name} (${model.size}, ~${model.estimatedBaseMemoryGB}GB base)`
);
}
console.log(chalk.blue.bold('\nRecommended envelope:'));
console.log(
` Context: ${plan.envelope.context.recommended} (requested ${plan.envelope.context.requested})`
);
console.log(
` Parallel: ${plan.envelope.parallel.recommended} (requested ${plan.envelope.parallel.requested})`
);
console.log(
` Loaded models: ${plan.envelope.loaded_models.recommended} (requested ${plan.envelope.loaded_models.requested})`
);
console.log(
` Estimated memory: ${plan.memory.recommendedEstimatedGB}GB / ${plan.memory.budgetGB}GB (${plan.memory.utilizationPercent}%)`
);
console.log(` Risk: ${plan.risk.level.toUpperCase()} (${plan.risk.score}/100)`);
if (plan.notes.length > 0) {
console.log(chalk.blue.bold('\nNotes:'));
for (const note of plan.notes) {
console.log(` - ${note}`);
}
}
console.log(chalk.blue.bold('\nRecommended env vars:'));
for (const [key, value] of Object.entries(plan.shell.env)) {
console.log(` export ${key}=${value}`);
}
console.log(chalk.blue.bold('\nFallback profile:'));
console.log(
` OLLAMA_NUM_CTX=${plan.fallback.num_ctx} OLLAMA_NUM_PARALLEL=${plan.fallback.num_parallel} OLLAMA_MAX_LOADED_MODELS=${plan.fallback.max_loaded_models}`
);
console.log('');
} catch (error) {
if (spinner) spinner.fail('Failed to build capacity plan');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('recommend')
.description('Get intelligent model recommendations for your hardware')
.option('-c, --category <category>', 'Get recommendations for specific category (coding, talking, reading, etc.)')
.option('--optimize <profile>', 'Optimization profile (balanced|speed|quality|context|coding)', 'balanced')
.option('--runtime <runtime>', 'Runtime target for registry recommendations (auto|ollama|vllm|mlx|llama.cpp|transformers)', 'auto')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--no-registry', 'Use the legacy Ollama catalog recommendation path')
.option('--no-verbose', 'Disable step-by-step progress display')
.option('--policy <file>', 'Evaluate recommendations against a policy file')
.option('--simulate <profile>', 'Simulate a hardware profile instead of detecting real hardware (use "list" to see profiles)')
.option('--cpu-only', 'Force CPU/RAM execution even when a GPU is detected')
.option('--gpu <model>', 'Custom GPU model for simulation (e.g., "RTX 5060", "RX 7800 XT")')
.option('--ram <gb>', 'Custom RAM in GB for simulation (e.g., 32)')
.option('--cpu <model>', 'Custom CPU model for simulation (e.g., "AMD Ryzen 7 5700X")')
.option('--vram <gb>', 'Override GPU VRAM in GB for simulation (auto-detected if omitted)')
.option(
'--calibrated [file]',
'Use calibrated routing policy (optional file path; defaults to ~/.llm-checker/calibration-policy.{yaml,yml,json})'
)
.addHelpText(
'after',
`
Enterprise policy examples:
$ llm-checker recommend --policy ./policy.yaml
$ llm-checker recommend --policy ./policy.yaml --category coding
$ llm-checker recommend --policy ./policy.yaml --no-verbose
Hardware simulation:
$ llm-checker recommend --simulate rtx4090
$ llm-checker recommend --simulate rtx4090 --cpu-only
$ llm-checker recommend --simulate m4pro24 --category coding
$ llm-checker recommend --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
CPU-only precedence:
- --cpu-only keeps the selected real/simulated CPU and RAM, but disables its GPU for fit and scoring
Registry/runtime examples:
$ llm-checker recommend --runtime auto --category coding
$ llm-checker recommend --runtime vllm --category coding
$ llm-checker recommend --runtime mlx --category general
Calibrated routing examples:
$ llm-checker recommend --calibrated --category coding
$ llm-checker recommend --calibrated ./calibration-policy.yaml --category reasoning
$ llm-checker recommend --policy ./calibration-policy.yaml --category coding
`
)
.action(async (options) => {
showAsciiArt('recommend');
displayRecommendationCommandNote('recommend');
try {
const verboseEnabled = options.verbose !== false;
const cpuOnly = getCpuOnlyCommandMode(options);
const checker = new (getLLMChecker())({ verbose: verboseEnabled, cpuOnly });
// Handle hardware simulation (preset profile or custom flags)
const hasCustomHwFlags = options.gpu || options.ram || options.cpu || options.vram;
if (options.simulate || hasCustomHwFlags) {
const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, listProfiles } = require('../src/hardware/profiles');
if (options.simulate === 'list') {
console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
listProfiles().forEach(line => console.log(line));
console.log('');
return;
}
let simulatedHardware;
let displayLabel;
if (hasCustomHwFlags) {
const ramValue = options.ram ? parseInt(options.ram) : undefined;
const vramValue = options.vram ? parseInt(options.vram) : undefined;
if (options.vram && !options.gpu) {
console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
process.exit(1);
}
if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
process.exit(1);
}
if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
process.exit(1);
}
simulatedHardware = buildCustomHardwareObject({
gpu: options.gpu || null,
ram: ramValue,
cpu: options.cpu || null,
vram: vramValue
});
displayLabel = simulatedHardware._displayName;
} else {
const profile = getProfile(options.simulate);
if (!profile) {
console.error(chalk.red(`\n Unknown profile: ${options.simulate}`));
console.log(chalk.gray('\n Available profiles:'));
listProfiles().forEach(line => console.log(line));
console.log('');
process.exit(1);
}
simulatedHardware = buildFullHardwareObject(options.simulate);
displayLabel = profile.displayName;
}
checker.setSimulatedHardware(simulatedHardware);
console.log(chalk.magenta.bold(`\n SIMULATION MODE: ${displayLabel}\n`));
}
displayCpuOnlyModeNotice(cpuOnly, checker.isSimulated);
const routingPreference = resolveRoutingPolicyPreference({
policyOption: options.policy,
calibratedOption: options.calibrated,
loadEnterprisePolicy: loadPolicyConfiguration
});
const policyConfig = routingPreference.enterprisePolicy;
const calibratedPolicy = routingPreference.calibratedPolicy;
if (!verboseEnabled) {
process.stdout.write(chalk.gray('Generating recommendations...'));
}
const hardware = await checker.getSystemInfo();
let recommendationRuntime = normalizeRuntime(options.runtime);
if (!recommendationRuntime) throw new Error(`Invalid --runtime: ${options.runtime}`);
const requestedRuntimeName = String(options.runtime || 'auto').toLowerCase();
if (
!['auto', 'all', '*'].includes(requestedRuntimeName) &&
!runtimeSupportedOnHardware(options.runtime, hardware)
) {
const runtimeLabel = getRuntimeDisplayName(options.runtime);
console.log(chalk.yellow(
`${runtimeLabel} is not compatible with CPU-only/current hardware; using Ollama instead.`
));
recommendationRuntime = 'ollama';
}
const intelligentRecommendations = await checker.generateIntelligentRecommendations(hardware, {
optimizeFor: options.optimize,
runtime: recommendationRuntime,
registry: options.registry,
includeUncensored: options.includeUncensored === true,
cpuOnly
});
if (!intelligentRecommendations) {
console.error(chalk.red('\nFailed to generate recommendations'));
return;
}
if (!verboseEnabled) {
console.log(chalk.green(' done'));
}
let policyEvaluation = null;
let policyEnforcement = null;
if (policyConfig) {
const policyCandidates = collectCandidatesFromRecommendationData(intelligentRecommendations);
const policyContext = buildPolicyRuntimeContext({
hardware,
runtimeBackend: 'ollama'
});
policyEvaluation = await evaluatePolicyCandidatesAsync(
policyConfig.policyEngine,
policyCandidates,
policyContext,
policyConfig.policy
);
policyEnforcement = resolvePolicyEnforcement(policyConfig.policy, policyEvaluation);
}
const routingTask = normalizeTaskName(options.category || 'general');
const recommendationIdentifiers = collectRecommendationModelIdentifiers(intelligentRecommendations);
const routeDecision = calibratedPolicy
? resolveCalibratedRouteDecision(calibratedPolicy, routingTask, recommendationIdentifiers)
: null;
// Mostrar información del sistema
displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
// Mostrar recomendaciones
displayIntelligentRecommendations(intelligentRecommendations, hardware);
displayCalibratedRoutingDecision('recommend', calibratedPolicy, routeDecision, routingPreference.warnings);
if (policyConfig && policyEvaluation && policyEnforcement) {
displayPolicySummary('recommend', policyConfig, policyEvaluation, policyEnforcement);
if (policyEnforcement.shouldBlock) {
process.exit(policyEnforcement.exitCode);
}
}
} catch (error) {
console.error(chalk.red('\nError:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('simulate')
.description('Simulate hardware profiles to see compatible LLM models for different systems')
.option('-p, --profile <name>', 'Hardware profile to simulate (e.g., rtx4090, m4pro24, h100)')
.option('-l, --list', 'List all available hardware profiles')
.option('--gpu <model>', 'Custom GPU model (e.g., "RTX 5060", "RX 7800 XT", "Apple M4 Pro")')
.option('--ram <gb>', 'Custom RAM in GB (e.g., 32)')
.option('--cpu <model>', 'Custom CPU model (e.g., "AMD Ryzen 7 5700X")')
.option('--vram <gb>', 'Override GPU VRAM in GB (auto-detected from GPU model if omitted)')
.option('-u, --use-case <case>', 'Specify use case', 'general')
.option('--optimize <profile>', 'Optimization profile (balanced|speed|quality|context|coding)', 'balanced')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--limit <number>', 'Number of compatible models to show (default: 1)', '1')
.option('--no-verbose', 'Disable step-by-step progress display')
.addHelpText(
'after',
`
Preset profiles:
$ llm-checker simulate --list
$ llm-checker simulate
$ llm-checker simulate -p rtx4090
$ llm-checker simulate -p m4pro24 --use-case coding
Custom hardware:
$ llm-checker simulate --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
$ llm-checker simulate --gpu "RTX 4090" --ram 64
$ llm-checker simulate --gpu "RX 7800 XT" --ram 32 --vram 16
$ llm-checker simulate --ram 16
`
)
.action(async (options) => {
const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, getProfilesByCategory, listProfiles, CATEGORY_LABELS } = require('../src/hardware/profiles');
// List mode
if (options.list) {
console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
listProfiles().forEach(line => console.log(line));
console.log('');
return;
}
let simulatedHardware;
let displayLabel;
// Custom hardware mode: --gpu, --ram, --cpu, --vram
const hasCustomFlags = options.gpu || options.ram || options.cpu || options.vram;
if (hasCustomFlags) {
const ramValue = options.ram ? parseInt(options.ram) : undefined;
const vramValue = options.vram ? parseInt(options.vram) : undefined;
if (options.vram && !options.gpu) {
console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
process.exit(1);
}
if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
process.exit(1);
}
if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
process.exit(1);
}
simulatedHardware = buildCustomHardwareObject({
gpu: options.gpu || null,
ram: ramValue,
cpu: options.cpu || null,
vram: vramValue
});
displayLabel = simulatedHardware._displayName;
} else {
// Preset profile mode
if (!options.profile) {
// Guard against non-interactive environments
if (!process.stdin.isTTY || !process.stdout.isTTY) {
console.error(chalk.red('\n No hardware profile specified.'));
console.log(chalk.gray(' Use --profile <name>, --gpu/--ram/--cpu flags, or --list to see profiles.\n'));
process.exit(1);
}
// Interactive selection
try {
const inquirer = require('inquirer');
const categories = getProfilesByCategory();
const choices = [];
for (const [category, profiles] of Object.entries(categories)) {
const label = CATEGORY_LABELS[category] || category;
choices.push(new inquirer.Separator(chalk.gray(`── ${label} ──`)));
for (const [key, profile] of Object.entries(profiles)) {
const vramLabel = profile.gpu.unified
? `${profile.memory.total}GB unified`
: (profile.gpu.vram > 0 ? `${profile.gpu.vram}GB VRAM` : 'No GPU');
const ramLabel = profile.gpu.unified ? '' : ` / ${profile.memory.total}GB RAM`;
choices.push({
name: `${profile.displayName} ${chalk.gray(`(${vramLabel}${ramLabel})`)}`,
value: key
});
}
}
const { selectedProfile } = await inquirer.prompt([{
type: 'list',
name: 'selectedProfile',
message: 'Select a hardware profile to simulate:',
choices,
pageSize: 20
}]);
options.profile = selectedProfile;
} catch (error) {
if (error.isTtyError) {
console.error(chalk.red('Interactive mode requires a TTY terminal.'));
console.log(chalk.gray('Use --profile <name>, --gpu/--ram flags, or --list to see available profiles.'));
process.exit(1);
}
throw error;
}
}
// Validate profile
const profile = getProfile(options.profile);
if (!profile) {
console.error(chalk.red(`\n Unknown profile: ${options.profile}`));
console.log(chalk.gray('\n Available profiles:'));
listProfiles().forEach(line => console.log(line));
console.log('');
process.exit(1);
}
simulatedHardware = buildFullHardwareObject(options.profile);
displayLabel = profile.displayName;
}
showAsciiArt('simulate');
try {
const verboseEnabled = options.verbose !== false;
const checker = new (getLLMChecker())({ verbose: verboseEnabled });
checker.setSimulatedHardware(simulatedHardware);
console.log(chalk.magenta.bold(` SIMULATION MODE: ${displayLabel}\n`));
if (!verboseEnabled) {
process.stdout.write(chalk.gray('Analyzing simulated hardware...'));
}
const hardware = await checker.getSystemInfo();
const normalizeUseCase = (useCase = '') => {
const alias = useCase.toLowerCase().trim();
const useCaseMap = {
'embed': 'embeddings', 'embedding': 'embeddings', 'embeddings': 'embeddings',
'embedings': 'embeddings', 'talk': 'chat', 'chat': 'chat', 'talking': 'chat'
};
return useCaseMap[alias] || alias || 'general';
};
const analysis = await checker.analyze({
useCase: normalizeUseCase(options.useCase),
limit: parseInt(options.limit) || 10,
runtime: 'ollama',
includeUncensored: options.includeUncensored === true
});
if (!verboseEnabled) {
console.log(chalk.green(' done'));
}
displaySimplifiedSystemInfo(hardware);
const normalizedUseCase = normalizeUseCase(options.useCase);
const limit = parseInt(options.limit) || 1;
const recommendedModels = await displayModelRecommendations(
analysis,
hardware,
normalizedUseCase,
limit,
'ollama'
);
await displayQuickStartCommands(analysis, recommendedModels[0], recommendedModels, 'ollama');
} catch (error) {
console.error(chalk.red('\nError:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('list-models')
.description('List models (Ollama catalog by default; --registry/--source lists HF + Ollama + GPT4All)')
.option('-c, --category <category>', 'Filter by category (coding, talking, reading, reasoning, multimodal, creative, general)')
.option('-s, --size <size>', 'Filter by size (small, medium, large, e.g., "7b", "13b")')
.option('-p, --popular', 'Show only popular models (>100k pulls)')
.option('-r, --recent', 'Show only recent models (updated in last 30 days)')
.option('--registry', 'List from the multi-source registry (Hugging Face + Ollama + GPT4All)')
.option('--source <source>', 'Registry source filter (implies --registry): ollama, huggingface, gpt4all')
.option('--format <format>', 'Registry artifact format: gguf, safetensors, mlx, ollama')
.option('--runtime <runtime>', 'Registry runtime: auto, ollama, llama.cpp, transformers, vllm, mlx')
.option('--limit <number>', 'Limit number of results (default: 50)', '50')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--full', 'Show full details including variants and tags')
.option('--json', 'Output in JSON format')
.action(async (options) => {
// Registry mode: list the packaged multi-source registry instead of the
// Ollama-only catalog.
if (options.registry || options.source) {
await executeRegistrySearch('', {
...options,
enforceDefaultSafety: true
});
return;
}
if (!options.json) showAsciiArt('list-models');
const spinner = options.json ? null : ora('📋 Loading models database...').start();
try {
const checker = new (getLLMChecker())();
const data = await checker.loadOllamaModelData();
if (!data || !data.models) {
if (spinner) spinner.fail('No models found in database');
else console.error('No models found in database');
return;
}
const originalCount = data.models.length;
let models = filterModelsBySafety(data.models, {
includeUncensored: options.includeUncensored === true
});
// Aplicar filtros
if (options.category) {
const categoryFilter = options.category.toLowerCase();
models = models.filter(model => {
// Buscar en categoría principal
if (model.category === categoryFilter) return true;
// Buscar en use_cases
if (model.use_cases && model.use_cases.includes(categoryFilter)) return true;
// Buscar por palabras clave en el nombre/identificador
const modelText = `${model.model_name} ${model.model_identifier}`.toLowerCase();
switch(categoryFilter) {
case 'coding':
case 'code':
return modelText.includes('code') || modelText.includes('coder') ||
modelText.includes('programming') || modelText.includes('deepseek') ||
modelText.includes('starcoder');
case 'talking':
case 'chat':
return modelText.includes('chat') || modelText.includes('llama') ||
modelText.includes('mistral') || modelText.includes('gemma') ||
modelText.includes('phi');
case 'reasoning':
return modelText.includes('reasoning') || modelText.includes('deepseek-r1') ||
modelText.includes('qwq') || modelText.includes('r1');
case 'multimodal':
case 'vision':
return modelText.includes('vision') || modelText.includes('llava') ||
modelText.includes('minicpm-v');
case 'creative':
case 'writing':
return modelText.includes('wizard') || modelText.includes('creative') ||
modelText.includes('uncensored');
case 'embeddings':
case 'embed':
return modelText.includes('embed') || modelText.includes('bge') ||
modelText.includes('nomic');
default:
return false;
}
});
}
if (options.size) {
const sizeFilter = options.size.toLowerCase();
models = models.filter(model =>
model.model_identifier.toLowerCase().includes(sizeFilter) ||
(model.model_sizes && model.model_sizes.some(size => size.includes(sizeFilter)))
);
}
if (options.popular) {
models = models.filter(model => (model.pulls || 0) > 100000);
}
if (options.recent) {
models = models.filter(model =>
model.last_updated && model.last_updated.includes('day')
);
}
// Si hay filtro de categoría, ordenar por compatibilidad con hardware
if (options.category) {
try {
const LLMChecker = require('../src/index.js');
const hardwareDetector = new (require('../src/hardware/detector.js'))();
const hardware = await hardwareDetector.getSystemInfo();
// Calcular puntuación de compatibilidad para cada modelo
models = models.map(model => {
const compatibilityScore = calculateModelCompatibilityScore(model, hardware);
return { ...model, compatibilityScore };
});
// Ordenar por compatibilidad primero, luego por popularidad
models.sort((a, b) => {
if (b.compatibilityScore !== a.compatibilityScore) {
return b.compatibilityScore - a.compatibilityScore;
}
return (b.pulls || 0) - (a.pulls || 0);
});
if (spinner) spinner.text = `Sorted by hardware compatibility (${getHardwareTierForDisplay(hardware)})`;
} catch (error) {
if (!options.json) console.warn('Could not sort by hardware compatibility:', error.message);
// Fallback a ordenar por popularidad
models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
}
} else {
// Sin filtro de categoría, ordenar solo por popularidad
models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
}
// Limitar resultados
const limit = parseInt(options.limit) || 50;
const displayModels = models.slice(0, limit);
if (spinner) spinner.succeed(`✅ Found ${models.length} models (showing ${displayModels.length})`);
if (options.json) {
console.log(JSON.stringify(displayModels, null, 2));
return;
}
// Mostrar estadísticas
displayModelsStats(originalCount, models.length, options);
// Mostrar modelos
if (options.full) {
displayFullModelsList(displayModels);
} else {
await displayCompactModelsList(displayModels, options.category);
}
// Mostrar comandos de ejemplo
if (displayModels.length > 0) {
displaySampleCommands(displayModels.slice(0, 3));
}
} catch (error) {
if (spinner) spinner.fail('Failed to load models');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
});
function getStatusColor(status) {
const colors = {
'TRAINED': chalk.green,
'NOT TRAINED': chalk.yellow,
'CORRUPTED': chalk.red
};
return colors[status] || chalk.gray;
}
function getConfidenceColor(confidence) {
if (confidence >= 0.8) return chalk.green.bold;
if (confidence >= 0.6) return chalk.yellow.bold;
if (confidence >= 0.4) return chalk.red.bold; // orange doesn't exist, use red
return chalk.red.bold;
}
function getScoreColor(score) {
if (score >= 85) return chalk.green.bold;
if (score >= 70) return chalk.cyan.bold;
if (score >= 55) return chalk.yellow.bold;
if (score >= 40) return chalk.red.bold;
return chalk.gray;
}
function getTierColor(tier) {
const colors = {
'extreme': chalk.magenta.bold,
'very_high': chalk.green.bold,
'high': chalk.cyan.bold,
'medium': chalk.yellow,
'low': chalk.red,
'ultra_low': chalk.gray
};
return colors[tier] || chalk.white;
}
program
.command('ai-check')
.description('AI-powered model evaluation with meta-analysis')
.option('-c, --category <category>', 'Category: coding, reasoning, multimodal, general', 'general')
.option('-t, --top <number>', 'Number of top models to show', '12')
.option('--ctx <number>', 'Target context length', '8192')
.option('-e, --evaluator <model>', 'Evaluator model (auto for best available)', 'auto')
.option('-w, --weight <number>', 'AI weight (0.0-1.0, default 0.3)', '0.3')
.option('-m, --models <list>', 'Restrict evaluation to these models (comma-separated)')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--cpu-only', 'Force CPU/RAM execution even when a GPU is detected')
.action(async (options) => {
showAsciiArt('ai-check');
// Check if Ollama is installed first
await checkOllamaAndExit();
const AICheckSelector = require('../src/models/ai-check-selector');
try {
const cpuOnly = getCpuOnlyCommandMode(options);
const spinner = ora('AI-Check Mode: Meta-evaluation in progress...').start();
const aiCheckSelector = new AICheckSelector({ cpuOnly });
// Validate numeric options up front: bad input (e.g. --weight abc, --top
// foo) used to flow through as NaN, which survived the downstream clamp
// and made the displayed AI weight and every weighted score NaN.
const weight = Number.parseFloat(options.weight);
const top = Number.parseInt(options.top, 10);
const ctx = options.ctx ? Number.parseInt(options.ctx, 10) : undefined;
const invalidNumeric =
(!Number.isFinite(weight) || weight < 0 || weight > 1) ? `--weight ${options.weight} (use a number from 0.0 to 1.0)`
: (!Number.isInteger(top) || top <= 0) ? `--top ${options.top} (use a positive integer)`
: (ctx !== undefined && (!Number.isInteger(ctx) || ctx <= 0)) ? `--ctx ${options.ctx} (use a positive integer)`
: null;
if (invalidNumeric) {
spinner.stop();
console.error(chalk.red(`Invalid ${invalidNumeric}`));
process.exitCode = 1;
return;
}
const checkOptions = {
category: options.category,
top,
ctx,
evaluator: options.evaluator,
weight,
models: options.models || process.env.LLM_CHECKER_AI_CHECK_MODELS || undefined,
cpuOnly,
includeUncensored: options.includeUncensored === true
};
spinner.stop();
displayCpuOnlyModeNotice(cpuOnly);
const result = await aiCheckSelector.aiCheck(checkOptions);
// Format and display results
aiCheckSelector.formatResults(result);
} catch (error) {
console.error(chalk.red('❌ AI-Check failed:'), error.message);
if (process.argv.includes('--verbose')) {
console.error(error.stack);
}
process.exit(1);
}
});
program
.command('ai-run')
.description('AI-powered model selection and execution')
.option('-m, --models <models...>', 'Specific models to choose from')
.option('-c, --category <category>', 'Task category hint (coding, reasoning, multimodal, general, etc.)')
.option('--prompt <prompt>', 'Prompt to run with selected model')
.option('--policy <file>', 'Explicit calibrated routing policy file (takes precedence over --calibrated)')
.option(
'--calibrated [file]',
'Enable calibrated routing policy (optional file path; defaults to ~/.llm-checker/calibration-policy.{yaml,yml,json})'
)
.option('--benchmark', 'Run a short local speed test before launching')
.option('--reference-only', 'Show model choice and speed reference without launching Ollama')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--cpu-only', 'Force CPU/RAM model selection even when a GPU is detected')
.option('--verify', 'Verify the selected model blob with modelvet before running (fails closed unless ACCEPT)')
.option('--allow-unverified', 'Continue only when --verify cannot produce a verdict; never bypasses REJECT')
.action(async (options) => {
showAsciiArt('ai-run');
if (options.allowUnverified && !options.verify) {
console.error(chalk.red('Error: --allow-unverified requires --verify.'));
process.exit(1);
}
// Check if Ollama is installed first
await checkOllamaAndExit();
const AIModelSelector = require('../src/ai/model-selector');
try {
const cpuOnly = getCpuOnlyCommandMode(options);
displayCpuOnlyModeNotice(cpuOnly);
const spinner = ora('Selecting best model and launching...').start();
const aiSelector = new AIModelSelector({ cpuOnly });
const checker = new (getLLMChecker())({ cpuOnly });
const systemInfo = await checker.getSystemInfo({ cpuOnly });
let ollamaClient = null;
const getOllamaClient = () => {
if (!ollamaClient) {
const OllamaClient = require('../src/ollama/client');
ollamaClient = new OllamaClient({ cpuOnly });
}
return ollamaClient;
};
const routingPreference = resolveRoutingPolicyPreference({
policyOption: options.policy,
calibratedOption: options.calibrated
});
const calibratedPolicy = routingPreference.calibratedPolicy;
// Get available models or use provided ones
let candidateModels = options.models;
let localModels = [];
if (!candidateModels) {
spinner.text = 'Getting available Ollama models...';
const client = getOllamaClient();
try {
localModels = await client.getLocalModels();
candidateModels = localModels.map(m => m.name || m.model);
if (candidateModels.length === 0) {
spinner.fail('No Ollama models found');
console.log('\nInstall some models first:');
console.log(' ollama pull llama2:7b');
console.log(' ollama pull mistral:7b');
console.log(' ollama pull phi3:mini');
return;
}
} catch (error) {
spinner.fail('Failed to get Ollama models');
console.error(chalk.red('Error:'), error.message);
return;
}
}
candidateModels = Array.isArray(candidateModels)
? candidateModels.filter((model) => typeof model === 'string' && model.trim().length > 0)
: [];
candidateModels = filterModelsBySafety(candidateModels, {
includeUncensored: options.includeUncensored === true
});
if (candidateModels.length === 0) {
throw new Error(
'No eligible local models remain after the default safety filter. ' +
'Re-run with --include-uncensored to opt in.'
);
}
// AI selection
const systemSpecs = {
cpu_cores: systemInfo.cpu?.cores || 4,
cpu_freq_max: systemInfo.cpu?.speed || 3.0,
total_ram_gb: systemInfo.memory?.total || 8,
gpu_vram_gb: cpuOnly ? 0 : (systemInfo.gpu?.vram || 0),
gpu_model_normalized: cpuOnly ? 'cpu_only' : (systemInfo.gpu?.model ||
(systemInfo.cpu?.manufacturer === 'Apple' ? 'apple_silicon' : 'cpu_only')
)
};
const taskHint = normalizeTaskName(options.category || inferTaskFromPrompt(options.prompt));
const routeDecision = calibratedPolicy
? resolveCalibratedRouteDecision(calibratedPolicy, taskHint, candidateModels)
: null;
let result;
if (routeDecision && routeDecision.matchedAvailableModel && routeDecision.selectedModel) {
result = {
bestModel: routeDecision.selectedModel,
confidence: routeDecision.usedRouteFallbackModel ? 0.82 : 0.94,
method: 'calibrated-policy-route',
reasoning: `Selected from calibrated policy route for ${routeDecision.resolvedTask}`
};
} else {
if (routeDecision && routeDecision.routeCandidates.length > 0) {
routingPreference.warnings.push(
`Calibrated route candidates (${routeDecision.routeCandidates.join(
', '
)}) are not installed locally. Falling back to AI selector.`
);
}
result = await aiSelector.selectBestModel(candidateModels, systemSpecs, taskHint, {
silent: true,
includeUncensored: options.includeUncensored === true,
cpuOnly
});
}
spinner.succeed(`Selected ${chalk.green.bold(result.bestModel)} (${result.method}, ${Math.round(result.confidence * 100)}% confidence)`);
// Opt-in modelvet gate: structurally verify the selected model's
// local blob BEFORE the model is ever loaded/run (also covers the
// post-`ollama pull` case, since pull only writes these same
// manifests/blobs). Semantics:
// REJECT -> hard stop, refuse to run, exit 1. No override.
// skipped -> manifest/blob missing or verifier error (e.g.
// >3 GiB wasm32 ceiling, wasm artifact missing):
// fail closed by default. --allow-unverified is the
// explicit opt-in escape hatch for this state only.
if (options.verify) {
const { verifyOllamaModel } = require('../src/security/ollama-blobs');
const verifySpinner = ora(`Verifying ${result.bestModel} blob structure with modelvet...`).start();
const verification = await verifyOllamaModel(result.bestModel);
if (verification.status === 'rejected') {
const report = verification.report;
verifySpinner.fail(`modelvet REJECTED ${result.bestModel}`);
console.log(chalk.red.bold('\nThis model blob failed structural verification:'));
console.log(` Violation: ${chalk.red(report.violationName)} (code ${report.violation})`);
console.log(` Offset: ${chalk.yellow(`0x${report.offset.toString(16)}`)}`);
console.log(` Blob: ${chalk.gray(verification.blob)}`);
console.log(chalk.red('\nRefusing to run this model. Remove it with:'));
console.log(chalk.cyan(` ollama rm ${result.bestModel}`));
process.exit(1);
} else if (verification.status === 'verified') {
verifySpinner.succeed(`modelvet ACCEPT: ${result.bestModel} is structurally well-formed`);
console.log(chalk.gray(' (ACCEPT is structural only — no provenance or poisoned-weights guarantee.)'));
} else {
verifySpinner.stop();
const reason = verification.reason || 'modelvet did not produce a verification verdict';
console.log(chalk.yellow(`Verification unavailable: ${reason}`));
if (options.allowUnverified) {
console.log(chalk.yellow.bold(
'Continuing without verification because --allow-unverified was explicitly set.'
));
} else {
console.log(chalk.red.bold('Refusing to run without an ACCEPT verdict (fail-closed, exit 2).'));
console.log(chalk.gray(
'If you accept this risk, re-run with --verify --allow-unverified. ' +
'This override never bypasses a REJECT verdict.'
));
process.exit(2);
}
}
}
let benchmark = null;
if (options.benchmark) {
const benchmarkSpinner = ora(`Measuring local throughput for ${result.bestModel}...`).start();
try {
benchmark = await getOllamaClient().testModelPerformance(
result.bestModel,
'Write one concise sentence about local LLM performance.'
);
if (benchmark.success) {
benchmarkSpinner.succeed(`Measured ${formatAiRunNumber(benchmark.tokensPerSecond)} tokens/sec`);
} else {
benchmarkSpinner.stop();
console.log(chalk.yellow(`Benchmark unavailable: ${benchmark.error || 'unknown error'}`));
}
} catch (error) {
benchmark = { success: false, error: error.message };
benchmarkSpinner.stop();
console.log(chalk.yellow(`Benchmark unavailable: ${error.message}`));
}
}
displayCalibratedRoutingDecision('ai-run', calibratedPolicy, routeDecision, routingPreference.warnings);
displayAiRunReference({
result,
systemInfo,
taskHint,
candidateModels,
localModels,
aiSelector,
benchmark
});
if (options.referenceOnly) {
console.log(chalk.gray('\nReference-only mode: not launching Ollama.'));
return;
}
if (options.prompt) {
console.log(chalk.cyan(`\n>>> ${options.prompt}`));
await runAiRunChatTurn(
getOllamaClient(),
result.bestModel,
[{ role: 'user', content: options.prompt }]
);
return;
}
console.log(chalk.magenta.bold(`\nStarting chat with ${result.bestModel}...`));
console.log(chalk.gray(`Tip: Type ${chalk.cyan('/bye')} to exit the chat when finished\n`));
await runAiRunInteractiveChat(getOllamaClient(), result.bestModel);
} catch (error) {
console.error(chalk.red('❌ AI-powered execution failed:'), error.message);
process.exit(1);
}
});
// Comando especial para demostrar el nuevo estilo de verbosity
program
.command('demo')
.description('Demo of the enhanced verbose progress with progress bars')
.action(async () => {
showAsciiArt('demo');
console.log(chalk.cyan.bold('\nLLM Checker - Enhanced Progress Demo'));
console.log(chalk.gray('This demonstrates the new step-by-step progress display with visual indicators'));
console.log(chalk.gray('─'.repeat(60)));
// Simular el proceso de análisis con verbosity
const VerboseProgress = require('../src/utils/verbose-progress');
const progress = VerboseProgress.create(true);
progress.startOperation('LLM Model Analysis & Compatibility Demo', 5);
// Simular paso 1
progress.step('System Detection', 'Scanning hardware specifications...');
await new Promise(resolve => setTimeout(resolve, 1000));
progress.substep('CPU detected: Apple M4 Pro (12 cores)');
await new Promise(resolve => setTimeout(resolve, 500));
progress.substep('Memory detected: 24GB unified memory', true);
progress.stepComplete('Apple M4 Pro, 24GB RAM, Apple Silicon GPU');
// Simular paso 2
progress.step('Database Sync', 'Updating model database...');
await new Promise(resolve => setTimeout(resolve, 800));
progress.found('3,247 models in database');
progress.stepComplete('Database synchronized');
// Simular paso 3
progress.step('Compatibility Analysis', 'Running mathematical heuristics...');
await new Promise(resolve => setTimeout(resolve, 1200));
progress.substep('Analyzing hardware requirements...');
await new Promise(resolve => setTimeout(resolve, 600));
progress.substep('Calculating performance scores...', true);
progress.found('127 compatible models found');
progress.stepComplete('Compatibility analysis complete');
// Simular paso 4
progress.step('AI Evaluation', 'Running intelligent model selection...');
await new Promise(resolve => setTimeout(resolve, 900));
progress.substep('Mathematical heuristics applied');
progress.found('Top 15 models selected by AI');
progress.stepComplete('AI evaluation complete');
// Simular paso 5
progress.step('Smart Recommendations', 'Generating personalized suggestions...');
await new Promise(resolve => setTimeout(resolve, 600));
progress.substep('Analyzing use case: general');
await new Promise(resolve => setTimeout(resolve, 400));
progress.substep('Generating Ollama commands...', true);
progress.stepComplete('23 recommendations generated');
// Completar
progress.complete('Analysis complete! Found optimal models for your hardware');
console.log(chalk.green.bold('Demo completed successfully!'));
console.log(chalk.gray('\\nNow try running: ') + chalk.cyan.bold('llm-checker check'));
console.log(chalk.gray('For silent mode: ') + chalk.cyan.bold('llm-checker check --no-verbose'));
});
// ============================================================
// NEW ENHANCED COMMANDS (v3.0 - Intelligent Model Selection)
// ============================================================
program
.command('sync')
.description('Sync the model database from Ollama registry (scrapes all models)')
.option('-f, --force', 'Force full sync even if recent data exists')
.option('--incremental', 'Only sync new and updated models')
.option('--exact', 'Read exact sizes and quantizations from registry.ollama.ai (two requests per distinct tag)')
.option('-q, --quiet', 'Suppress progress output')
.action(async (options) => {
if (!options.quiet) showAsciiArt('sync');
const SyncManager = require('../src/data/sync-manager');
const spinner = options.quiet ? null : ora('Initializing sync...').start();
try {
const syncManager = new SyncManager({
exact: Boolean(options.exact),
onProgress: (info) => {
if (!options.quiet && spinner) {
if (info.phase === 'complete') {
spinner.succeed(info.message);
} else {
spinner.text = info.message;
}
}
},
onError: (err) => {
if (!options.quiet) console.error(chalk.yellow('Warning:'), err);
}
});
let result;
if (options.incremental) {
result = await syncManager.incrementalSync();
} else {
result = await syncManager.sync({ force: options.force });
}
if (!options.quiet) {
console.log(chalk.green('\n[OK] Sync complete!'));
console.log(chalk.gray(` Models: ${result.stats?.models || result.models || 0}`));
console.log(chalk.gray(` Variants: ${result.stats?.variants || result.variants || 0}`));
}
syncManager.close();
} catch (error) {
if (spinner) spinner.fail('Sync failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('quality-sync')
.description('Refresh public benchmark scores used for model quality recommendations')
.option('-s, --sources <list>', 'Comma-separated sources: hf_open_llm,lmarena,bigcodebench,evalplus,livebench,mmmu,hf_eval_results')
.option('--db <path>', 'Model database path (defaults to the local catalog)')
.option('-j, --json', 'Output the sync and coverage report as JSON')
.action(async (options) => {
const ModelDatabase = require('../src/data/model-database');
const { QualityEvals, SOURCES } = require('../src/data/quality-evals');
const database = new ModelDatabase({ dbPath: options.db });
try {
const sources = options.sources === undefined ? Object.keys(SOURCES)
: [...new Set(options.sources.split(',').map((source) => source.trim()).filter(Boolean))];
if (!sources.length || sources.some((source) => !Object.hasOwn(SOURCES, source))) {
throw new Error(`Invalid quality sources. Choose from: ${Object.keys(SOURCES).join(', ')}`);
}
await database.initialize();
const quality = new QualityEvals(database);
const catalog = database.all('SELECT name FROM models');
quality.refreshCatalogCohort(catalog);
const before = quality.coverage(catalog);
const reports = [];
for (const source of sources) {
try {
reports.push(await quality.ingest(source));
} catch (error) {
reports.push({ source, error: error.message });
process.exitCode = 1;
}
}
const coverage = quality.coverage(catalog);
const report = { reports, coverage: { ...coverage, unit: 'catalog families', before: before.measured }, stats: quality.stats() };
if (options.json) console.log(JSON.stringify(report, null, 2));
else {
for (const result of reports) console.log(result.error
? `${result.source}: ${result.error}` : `${result.source}: ${result.rows} scores refreshed`);
console.log(`Benchmark coverage: ${coverage.measured}/${coverage.total} catalog families (previously ${before.measured}).`);
console.log('Recommendations use measurements only for matching sizes and tasks; remaining scores are labeled estimated.');
}
} catch (error) {
if (options.json) console.log(JSON.stringify({ error: error.message }));
else console.error(chalk.red('Error:'), error.message);
process.exitCode = 1;
} finally {
database.close();
}
});
program
.command('registry-sync')
.description('Sync the multi-source model registry (Ollama, Hugging Face, GPT4All)')
.option('-s, --sources <list>', 'Comma-separated sources: ollama,huggingface,gpt4all', 'ollama,huggingface,gpt4all')
.option('-l, --limit <n>', 'Fallback maximum records per source')
.option('--hf-limit <n>', 'Maximum Hugging Face repos to ingest', '3000')
.option('--ollama-limit <n>', 'Maximum Ollama artifacts to ingest', '10000')
.option('--gpt4all-limit <n>', 'Maximum GPT4All entries to ingest', '1000')
.option('--query <text>', 'Hugging Face search query')
.option('--task <task>', 'Hugging Face task/filter, for example text-generation or text-embeddings-inference')
.option('--official', 'Also sweep the catalogs of official Hugging Face model publishers')
.option('--publisher-limit <n>', 'Maximum repos per official publisher with --official', '500')
.option('--deep', 'Fetch exact Hugging Face file sizes, hashes and config.json context windows (one or two requests per repo)')
.option('--dry-run', 'Fetch and normalize without writing to the database')
.option('-q, --quiet', 'Suppress progress output')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
const quiet = Boolean(options.quiet || options.json);
if (!quiet) showAsciiArt('registry-sync');
const ModelDatabase = require('../src/data/model-database');
const { RegistryIngestor } = require('../src/data/registry-ingestors');
const database = new ModelDatabase();
const spinner = quiet ? null : ora('Preparing model registry sync...').start();
try {
await database.initialize();
const ingestor = new RegistryIngestor({
database,
onProgress: (info) => {
if (spinner && info.message) {
spinner.text = info.message;
}
}
});
const summary = await ingestor.ingest({
sources: options.sources,
limit: parsePositiveNumberOption(options.limit),
hfLimit: parsePositiveNumberOption(options.hfLimit, 3000),
ollamaLimit: parsePositiveNumberOption(options.ollamaLimit, 10000),
gpt4allLimit: parsePositiveNumberOption(options.gpt4allLimit, 1000),
query: options.query,
task: options.task,
publishers: Boolean(options.official),
publisherLimit: parsePositiveNumberOption(options.publisherLimit, 500),
fileSizes: Boolean(options.deep),
configs: Boolean(options.deep),
dryRun: Boolean(options.dryRun)
});
const stats = options.dryRun ? null : database.getRegistryStats();
if (options.json) {
console.log(JSON.stringify({ summary, stats }, null, 2));
return;
}
if (spinner) {
const action = options.dryRun ? 'normalized' : 'synced';
spinner.succeed(`Registry ${action}: ${summary.repos} repos, ${summary.artifacts} artifacts`);
}
console.log(chalk.green('\n[OK] Registry sync complete'));
console.log(chalk.gray(` Sources touched: ${summary.sources}`));
console.log(chalk.gray(` Collections: ${summary.collections}`));
console.log(chalk.gray(` Repositories: ${summary.repos}`));
console.log(chalk.gray(` Artifacts: ${summary.artifacts}`));
if (stats) {
console.log(chalk.blue.bold('\nRegistry totals:'));
console.log(chalk.gray(` Sources: ${stats.sources}`));
console.log(chalk.gray(` Repositories: ${stats.repos}`));
console.log(chalk.gray(` Artifacts: ${stats.artifacts}`));
if (stats.bySource.length > 0) {
const rows = [['Source', 'Artifacts']];
for (const item of stats.bySource) {
rows.push([item.source_id, String(item.artifact_count)]);
}
console.log('\n' + table(rows));
}
}
console.log(chalk.cyan('Try: llm-checker registry-search llama --runtime auto --limit 10'));
} catch (error) {
if (spinner) spinner.fail('Registry sync failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exitCode = 1;
} finally {
database.close();
}
});
program
.command('registry-search [query]')
.description('Search exact downloadable/installable artifacts in the multi-source model registry')
.option('-s, --source <source>', 'Filter by source: ollama, huggingface, gpt4all')
.option('--format <format>', 'Filter by artifact format: gguf, safetensors, mlx, ollama')
.option('--runtime <runtime>', 'Filter by runtime support: auto, ollama, llama.cpp, transformers, vllm, mlx')
.option('--quant <type>', 'Filter by quantization, for example Q4_K_M or Q8_0')
.option('--max-size <gb>', 'Maximum artifact size in GB')
.option('--min-params <billion>', 'Minimum parameter count in billions')
.option('--max-params <billion>', 'Maximum parameter count in billions')
.option('--local-only', 'Exclude gated/auth-required artifacts')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('-l, --limit <n>', 'Maximum number of results', '20')
.option('-j, --json', 'Output as JSON')
.action(async (query = '', options) => {
await executeRegistrySearch(query, {
...options,
enforceDefaultSafety: true
});
});
program
.command('registry-recommend [query]')
.description('Recommend the best exact model artifacts from the multi-source registry for this hardware')
.option('-c, --category <category>', 'Task category (general, coding, reasoning, embeddings, multimodal)', 'general')
.option('--optimize <profile>', 'Optimization profile (balanced|speed|quality|context|coding)', 'balanced')
.option('--runtime <runtime>', 'Runtime target: auto, ollama, llama.cpp, vllm, mlx, transformers', 'auto')
.option('-s, --source <source>', 'Filter by source: ollama, huggingface, gpt4all')
.option('--format <format>', 'Filter by artifact format: gguf, safetensors, mlx, ollama')
.option('--quant <type>', 'Filter by quantization, for example Q4_K_M or Q8_0')
.option('--max-size <gb>', 'Maximum artifact size in GB')
.option('--min-params <billion>', 'Minimum parameter count in billions')
.option('--max-params <billion>', 'Maximum parameter count in billions')
.option('--target-context <tokens>', 'Target context window for scoring')
.option('--include-gated', 'Include gated/auth-required artifacts')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--pool-limit <n>', 'Maximum registry artifacts to score before ranking', '20000')
.option('-l, --limit <n>', 'Maximum number of recommendations', '10')
.option('--cpu-only', 'Force CPU/RAM execution even when a GPU is detected')
.option('-j, --json', 'Output as JSON')
.action(async (query = '', options) => {
try {
validateRegistryFilters(options);
} catch (validationError) {
if (options.json) {
console.log(JSON.stringify({ error: validationError.message }, null, 2));
} else {
console.error(chalk.red(`✗ ${validationError.message}`));
}
process.exitCode = 1;
return;
}
if (!options.json) showAsciiArt('registry-recommend');
const UnifiedDetector = require('../src/hardware/unified-detector');
const { RegistryRecommender } = require('../src/data/registry-recommender');
const recommender = new RegistryRecommender();
const spinner = options.json ? null : ora('Scoring registry artifacts...').start();
try {
const cpuOnly = getCpuOnlyCommandMode(options);
await recommender.initialize();
const detector = new UnifiedDetector({ cpuOnly });
const hardware = await detector.detect();
const category = normalizeTaskName(options.category || 'general');
const result = await recommender.recommend({
query,
category,
optimizeFor: options.optimize,
runtime: options.runtime,
source: options.source,
format: options.format ? String(options.format).toLowerCase() : undefined,
quantization: options.quant,
maxSizeGB: parsePositiveNumberOption(options.maxSize),
minParamsB: parsePositiveNumberOption(options.minParams),
maxParamsB: parsePositiveNumberOption(options.maxParams),
targetContext: parsePositiveNumberOption(options.targetContext),
localOnly: !options.includeGated,
includeUncensored: options.includeUncensored === true,
poolLimit: parsePositiveNumberOption(options.poolLimit, 20000),
limit: parsePositiveNumberOption(options.limit, 10),
cpuOnly,
hardware
});
if (options.json) {
console.log(JSON.stringify({
query,
hardware: hardware.summary || hardware,
...result
}, null, 2));
return;
}
if (spinner) {
spinner.succeed(
`Scored ${result.total_evaluated} candidates from ${result.total_artifacts} registry artifacts`
);
}
const requestedRuntime = String(options.runtime || 'auto').toLowerCase();
if (
!['auto', 'all', '*'].includes(requestedRuntime) &&
String(result.runtime || '').toLowerCase() !== requestedRuntime
) {
console.log(chalk.yellow(
`${getRuntimeDisplayName(options.runtime)} is not compatible with CPU-only/current hardware; using ${getRuntimeDisplayName(result.runtime)} instead.`
));
}
displayCpuOnlyModeNotice(cpuOnly);
if (result.recommendations.length === 0) {
console.log(chalk.yellow('No registry recommendations found for those filters.'));
if (result.registry.artifacts === 0) {
console.log(chalk.gray('Populate the registry first with: llm-checker registry-sync'));
}
return;
}
console.log(chalk.blue.bold('\nRegistry Recommendations'));
console.log(chalk.gray(`Registry: ${result.registry.repos} repos, ${result.registry.artifacts} artifacts`));
console.log(chalk.gray(`Runtime: ${result.runtime} | Category: ${result.category} | Optimize: ${result.optimizeFor}`));
console.log('');
const rows = [['#', 'Score', 'Source', 'Model', 'Artifact', 'Params', 'Size', 'Install']];
result.recommendations.forEach((item, index) => {
rows.push([
String(index + 1),
String(item.score),
item.source,
truncateMiddle(item.model, 30),
truncateMiddle(item.artifact, 32),
formatRegistryNumber(item.params_b, 'B'),
formatRegistrySize(item.size_gb),
truncateMiddle(item.install_command || item.download_url, 44)
]);
});
console.log(table(rows));
console.log(chalk.blue.bold('Top pick:'));
const best = result.recommendations[0];
console.log(chalk.white.bold(` ${best.model}`));
console.log(chalk.gray(` Artifact: ${best.artifact}`));
console.log(chalk.gray(` Why: ${best.rationale}`));
if (best.install_command) console.log(chalk.cyan(` ${best.install_command}`));
if (best.download_url) console.log(chalk.gray(` ${best.download_url}`));
} catch (error) {
if (spinner) spinner.fail('Registry recommendation failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exitCode = 1;
} finally {
recommender.close();
}
});
program
.command('search <query>')
.description('Search models (Ollama catalog by default; --registry/--source searches HF + Ollama + GPT4All)')
.option('-u, --use-case <case>', 'Optimize for use case (general, coding, chat, reasoning, creative)', 'general')
.option('-l, --limit <n>', 'Maximum number of results', '10')
.option('--max-size <gb>', 'Maximum model size in GB')
.option('--min-size <gb>', 'Minimum model size in GB')
.option('--quant <type>', 'Filter by quantization (Q4_K_M, Q5_K_M, Q8_0, etc.)')
.option('--family <name>', 'Filter by model family (llama, qwen, mistral, etc.)')
.option('--registry', 'Search the multi-source registry (Hugging Face + Ollama + GPT4All)')
.option('-s, --source <source>', 'Registry source filter (implies --registry): ollama, huggingface, gpt4all')
.option('--format <format>', 'Registry artifact format: gguf, safetensors, mlx, ollama')
.option('--runtime <runtime>', 'Registry runtime: auto, ollama, llama.cpp, transformers, vllm, mlx')
.option('--min-params <billion>', 'Minimum parameter count in billions (registry mode)')
.option('--max-params <billion>', 'Maximum parameter count in billions (registry mode)')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('-j, --json', 'Output as JSON')
.action(async (query, options) => {
// Registry mode: search the packaged multi-source registry (HF + Ollama +
// GPT4All) instead of the Ollama-only catalog.
if (options.registry || options.source) {
await executeRegistrySearch(query, {
...options,
enforceDefaultSafety: true
});
return;
}
if (!options.json) showAsciiArt('search');
const SyncManager = require('../src/data/sync-manager');
const IntelligentSelector = require('../src/models/intelligent-selector');
const UnifiedDetector = require('../src/hardware/unified-detector');
const spinner = options.json ? null : ora('Searching models...').start();
try {
// Detect hardware first to determine max size
const detector = new UnifiedDetector();
const hardware = await detector.detect();
const hardwareMaxSize = detector.getMaxModelSize();
const syncManager = new SyncManager({ onProgress: () => {} });
await syncManager.init();
// Check if we need to sync first
const syncStatus = await syncManager.needsSync();
if (syncStatus.needed && !options.json) {
spinner.text = 'Database needs sync, running quick check...';
}
// Use user-provided maxSize or hardware-detected max
const effectiveMaxSize = options.maxSize
? parseFloat(options.maxSize)
: hardwareMaxSize + 2; // Add some headroom
// Search for variants in database
const searchResults = await syncManager.searchVariants(query, {
maxSize: effectiveMaxSize,
minSize: options.minSize ? parseFloat(options.minSize) : null,
quant: options.quant,
family: options.family,
limit: parseInt(options.limit) * 5 // Get more for scoring
});
if (searchResults.length === 0) {
syncManager.close();
if (options.json) {
// --json must always emit parseable JSON, even on zero matches
// (previously it printed nothing and broke `search ... --json | jq`).
console.log(JSON.stringify({
query,
all: [],
recommendations: [],
insights: [],
meta: { afterFiltering: 0, totalMatches: 0 }
}, null, 2));
} else if (spinner) {
spinner.info('No models found matching your query');
}
return;
}
// Score with intelligent selector (reuse detector from above)
const selector = new IntelligentSelector({ detector });
const recommendations = await selector.recommend(searchResults, {
useCase: options.useCase,
limit: parseInt(options.limit),
includeUncensored: options.includeUncensored === true
});
syncManager.close();
if (options.json) {
console.log(JSON.stringify(recommendations, null, 2));
return;
}
if (spinner) spinner.succeed(`Found ${recommendations.meta.afterFiltering} matching models`);
// Display results
console.log(chalk.blue.bold('\nSearch Results for: ') + chalk.white(query));
console.log(chalk.gray(`Hardware: ${recommendations.hardware.description}`));
console.log(chalk.gray(`Max model size: ${recommendations.hardware.maxSize}GB`));
console.log('');
for (const item of recommendations.all) {
const v = item.variant;
const s = item.score;
// Format model name (tag already contains model:variant format)
const fullTag = v.tag || 'latest';
const displayName = fullTag.includes(':') ? fullTag : `${v.model_id || v.modelId}:${fullTag}`;
const scoreColor = s.final >= 80 ? chalk.green : s.final >= 60 ? chalk.yellow : chalk.red;
console.log(
scoreColor(`[${s.final}]`) + ' ' +
chalk.white.bold(displayName)
);
console.log(
chalk.gray(` ${v.params_b || v.paramsB || '?'}B params, `) +
chalk.gray(`${v.size_gb || v.sizeGB || '?'}GB, `) +
chalk.gray(`${v.quant || 'Q4_K_M'}, `) +
chalk.cyan(`~${s.meta.estimatedTPS} tok/s`)
);
console.log(
chalk.gray(` Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit} C:${s.components.context}`)
);
console.log(chalk.cyan(` ollama pull ${displayName}`));
console.log('');
}
// Show insights
if (recommendations.insights.length > 0) {
console.log(chalk.blue.bold('Insights:'));
for (const insight of recommendations.insights) {
const icon = insight.type === 'success' ? '[OK]' : insight.type === 'warning' ? '[!]' : '[i]';
const color = insight.type === 'success' ? chalk.green : insight.type === 'warning' ? chalk.yellow : chalk.cyan;
console.log(color(` ${icon} ${insight.message}`));
}
}
} catch (error) {
if (spinner) spinner.fail('Search failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('smart-recommend')
.description('Experimental recommendations using the alternate scoring engine')
.option('-u, --use-case <case>', 'Optimize for use case', 'general')
.option('-l, --limit <n>', 'Maximum number of recommendations', '5')
.option('--target-tps <n>', 'Target tokens per second', '20')
.option('--target-context <n>', 'Target context length', '8192')
.option('--include-vision', 'Include vision/multimodal models')
.option('--include-embeddings', 'Include embedding models')
.option('--include-uncensored', 'Explicitly include uncensored, abliterated, or heretic models')
.option('--cpu-only', 'Force CPU/RAM execution even when a GPU is detected')
.option('-j, --json', 'Output as JSON')
.addHelpText(
'after',
`
Recommendation engine note:
smart-recommend is experimental and may intentionally differ from recommend.
Use "llm-checker recommend" for canonical package recommendations.
`
)
.action(async (options) => {
if (!options.json) {
showAsciiArt('smart-recommend');
displayRecommendationCommandNote('smart-recommend');
}
const SyncManager = require('../src/data/sync-manager');
const IntelligentSelector = require('../src/models/intelligent-selector');
const UnifiedDetector = require('../src/hardware/unified-detector');
const spinner = options.json ? null : ora('Analyzing hardware and models...').start();
try {
const cpuOnly = getCpuOnlyCommandMode(options);
// Detect hardware
const detector = new UnifiedDetector({ cpuOnly });
const hardware = await detector.detect();
if (spinner) spinner.text = 'Loading model database...';
// Load models from database
const syncManager = new SyncManager({ onProgress: () => {} });
await syncManager.init();
const syncStatus = await syncManager.needsSync();
if (syncStatus.needed) {
if (spinner) spinner.text = 'Syncing model database (first time takes a few minutes)...';
await syncManager.sync();
}
// Get all variants that might fit
const maxSize = detector.getMaxModelSize() + 2;
const variants = await syncManager.getCompatibleVariants(maxSize, {});
if (spinner) spinner.text = `Scoring ${variants.length} model variants...`;
// Get intelligent recommendations
const selector = new IntelligentSelector({ detector, cpuOnly });
const recommendations = await selector.recommend(variants, {
useCase: options.useCase,
targetTPS: parseInt(options.targetTps) || 20,
targetContext: parseInt(options.targetContext) || 8192,
includeVision: options.includeVision,
includeEmbeddings: options.includeEmbeddings,
includeUncensored: options.includeUncensored === true,
cpuOnly,
limit: parseInt(options.limit)
});
syncManager.close();
if (options.json) {
console.log(JSON.stringify(recommendations, null, 2));
return;
}
if (spinner) spinner.succeed('Analysis complete!');
displayCpuOnlyModeNotice(cpuOnly);
// Display hardware info
console.log(chalk.blue.bold('\n=== Hardware Analysis ==='));
console.log(chalk.white(` ${recommendations.hardware.description}`));
console.log(chalk.gray(` Tier: ${recommendations.hardware.tier.replace('_', ' ').toUpperCase()}`));
console.log(chalk.gray(` Backend: ${recommendations.hardware.backend}`));
console.log(chalk.gray(` Max model size: ${recommendations.hardware.maxSize}GB`));
// Display top picks
console.log(chalk.blue.bold('\n=== Top Recommendations ==='));
// Helper to format model name (tag already contains model:variant)
const formatModelName = (v) => {
const fullTag = v.tag || 'latest';
return fullTag.includes(':') ? fullTag : `${v.model_id}:${fullTag}`;
};
const picks = recommendations.topPicks;
if (picks.best) {
const v = picks.best.variant;
const s = picks.best.score;
const name = formatModelName(v);
console.log(chalk.green.bold('\n[BEST] Best Overall:'));
console.log(chalk.white.bold(` ${name}`));
console.log(chalk.gray(` ${v.params_b || '?'}B params | ${v.size_gb || '?'}GB | ${v.quant || 'Q4_K_M'}`));
console.log(chalk.cyan(` Score: ${s.final}/100 (Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit})`));
console.log(chalk.yellow(` ~${s.meta.estimatedTPS} tokens/sec`));
console.log(chalk.cyan(` ollama pull ${name}`));
}
if (picks.fast && picks.fast !== picks.best) {
const v = picks.fast.variant;
const s = picks.fast.score;
const name = formatModelName(v);
console.log(chalk.blue.bold('\n⚡ Fastest:'));
console.log(chalk.white(` ${name}`));
console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | ~${s.meta.estimatedTPS} tok/s`));
console.log(chalk.cyan(` ollama pull ${name}`));
}
if (picks.quality && picks.quality !== picks.best) {
const v = picks.quality.variant;
const s = picks.quality.score;
const name = formatModelName(v);
console.log(chalk.magenta.bold('\nHighest Quality:'));
console.log(chalk.white(` ${name}`));
console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | Quality: ${s.components.quality}/100`));
console.log(chalk.cyan(` ollama pull ${name}`));
}
// Show other recommendations
if (recommendations.all.length > 1) {
console.log(chalk.blue.bold('\n=== Other Good Options ==='));
for (const item of recommendations.all.slice(1, parseInt(options.limit))) {
const v = item.variant;
const s = item.score;
const name = formatModelName(v);
console.log(
chalk.gray(`[${s.final}] `) +
chalk.white(name) +
chalk.gray(` - ${v.params_b || '?'}B, ${v.size_gb || '?'}GB`)
);
}
}
// Show insights
if (recommendations.insights.length > 0) {
console.log(chalk.blue.bold('\n=== Insights ==='));
for (const insight of recommendations.insights) {
const icon = insight.type === 'success' ? chalk.green('[OK]') :
insight.type === 'warning' ? chalk.yellow('[!]') :
insight.type === 'tip' ? chalk.cyan('[TIP]') : chalk.blue('[i]');
console.log(` ${icon} ${insight.message}`);
}
}
console.log('');
} catch (error) {
if (spinner) spinner.fail('Recommendation failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('gpu-plan')
.description('Multi-GPU placement advisor with safe model-size envelopes')
.option('--model-size <gb>', 'Validate a target model size (e.g. 14 or 14GB)')
.option('--cpu-only', 'Force a CPU/RAM plan and ignore detected GPU capacity')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
if (!options.json) showAsciiArt('hw-detect');
const spinner = options.json ? null : ora('Building GPU placement plan...').start();
try {
const UnifiedDetector = require('../src/hardware/unified-detector');
const { buildGpuPlan } = require('../src/commands/roadmap-tools');
const cpuOnly = getCpuOnlyCommandMode(options);
const detector = new UnifiedDetector({ cpuOnly });
const hardware = await detector.detect();
const modelSizeGB = options.modelSize !== undefined ? parseFloat(options.modelSize) : null;
if (options.modelSize !== undefined && (!Number.isFinite(modelSizeGB) || modelSizeGB <= 0)) {
throw new Error('Invalid --model-size value. Use a positive number (GB).');
}
const plan = buildGpuPlan(hardware, { modelSizeGB, cpuOnly });
if (options.json) {
console.log(JSON.stringify(plan, null, 2));
return;
}
if (spinner) spinner.succeed('GPU placement plan ready');
displayCpuOnlyModeNotice(cpuOnly);
console.log(chalk.blue.bold('\n=== Multi-GPU Placement Plan ==='));
console.log(`Backend: ${chalk.cyan((plan.backend || 'cpu').toUpperCase())}`);
console.log(`Detected GPUs: ${chalk.white(plan.gpuCount)}`);
console.log(`Total VRAM/Unified: ${chalk.green(`${plan.totalVRAM}GB`)}`);
if (plan.cpuOnly) {
console.log(`CPU/RAM safe envelope: ${chalk.yellow(`${plan.cpuMaxModelGB}GB`)}`);
} else {
console.log(`Single-GPU safe envelope: ${chalk.yellow(`${plan.singleMaxModelGB}GB`)}`);
console.log(`Pooled safe envelope: ${chalk.yellow(`${plan.pooledMaxModelGB}GB`)}`);
}
console.log(`Strategy: ${chalk.cyan(plan.strategy)} (${plan.strategyReason})`);
if (plan.gpus.length > 0) {
const rows = [
['#', 'Backend', 'GPU', 'VRAM/Unified', 'Speed Coef']
];
plan.gpus.forEach((gpu, index) => {
rows.push([
String(index + 1),
gpu.backend.toUpperCase(),
gpu.name,
`${gpu.vramGB}GB`,
String(gpu.speedCoefficient || 0)
]);
});
console.log('\n' + table(rows));
}
if (plan.fit) {
const fit = plan.fit;
const fitsActiveMode = plan.cpuOnly ? fit.fitsCPU : (fit.fitsSingleGPU || fit.fitsPooled);
const status = fitsActiveMode ? chalk.green('[OK]') : chalk.red('[FAIL]');
console.log(`${status} Target model ${fit.modelSizeGB}GB`);
if (plan.cpuOnly) {
console.log(` Fits CPU/RAM budget: ${fit.fitsCPU ? 'yes' : 'no'}`);
} else {
console.log(` Fits single GPU: ${fit.fitsSingleGPU ? 'yes' : 'no'}`);
console.log(` Fits pooled setup: ${fit.fitsPooled ? 'yes' : 'no'}`);
}
}
console.log(chalk.blue.bold('\nRecommended env:'));
for (const [key, value] of Object.entries(plan.env || {})) {
console.log(chalk.cyan(` export ${key}="${value}"`));
}
if (Array.isArray(plan.recommendations) && plan.recommendations.length > 0) {
console.log(chalk.blue.bold('\nRecommendations:'));
for (const item of plan.recommendations) {
console.log(chalk.gray(` - ${item}`));
}
}
console.log('');
} catch (error) {
if (spinner) spinner.fail('GPU plan failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('verify-context')
.description('Verify practical context window limits for a local Ollama model')
.option('-m, --model <name>', 'Model to verify (default: first installed model)')
.option('-t, --target <tokens>', 'Target context window tokens to validate', '8192')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
if (!options.json) showAsciiArt('ollama');
const spinner = options.json ? null : ora('Verifying context window...').start();
try {
const OllamaClient = require('../src/ollama/client');
const UnifiedDetector = require('../src/hardware/unified-detector');
const {
buildContextVerification,
extractContextWindow
} = require('../src/commands/roadmap-tools');
const targetTokens = parseInt(options.target, 10);
if (!Number.isFinite(targetTokens) || targetTokens <= 0) {
throw new Error('Invalid --target value. Use a positive integer.');
}
const ollama = new OllamaClient();
const availability = await ollama.checkOllamaAvailability();
if (!availability.available) {
throw new Error(availability.error || 'Ollama is not available');
}
const installed = await ollama.getLocalModels();
if (!installed.length) {
throw new Error('No local Ollama models installed. Pull one model first.');
}
let selected = installed[0];
if (options.model) {
const needle = options.model.toLowerCase();
selected = installed.find((m) =>
m.name.toLowerCase() === needle ||
m.name.toLowerCase().startsWith(`${needle}:`) ||
m.name.toLowerCase().includes(needle)
);
if (!selected) {
throw new Error(`Model "${options.model}" not found in local Ollama models.`);
}
}
let showPayload = null;
try {
showPayload = await ollama.showModel(selected.name);
} catch (err) {
// Continue even if show metadata fails; memory verification still works.
if (!options.json && spinner) {
spinner.info(`Metadata probe warning: ${err.message}`);
spinner.start('Continuing with hardware-based estimate...');
}
}
const detector = new UnifiedDetector();
const hardware = await detector.detect();
const declaredContext = extractContextWindow(showPayload);
const verification = buildContextVerification({
modelName: selected.name,
targetTokens,
declaredContext,
modelSizeGB: selected.fileSizeGB || 7,
hardware
});
const output = {
model: selected.name,
targetTokens,
declaredContext,
modelSizeGB: selected.fileSizeGB || null,
verification
};
if (options.json) {
console.log(JSON.stringify(output, null, 2));
return;
}
if (spinner) spinner.succeed('Context verification complete');
const statusColor = verification.status === 'pass'
? chalk.green
: verification.status === 'warn'
? chalk.yellow
: chalk.red;
console.log(chalk.blue.bold('\n=== Context Verification ==='));
console.log(`Model: ${chalk.white.bold(selected.name)}`);
console.log(`Target: ${chalk.cyan(`${targetTokens} tokens`)}`);
console.log(`Declared context: ${chalk.cyan(declaredContext ? `${declaredContext} tokens` : 'not exposed')}`);
console.log(`Estimated memory-safe context: ${chalk.cyan(`${verification.memoryLimitedContext} tokens`)}`);
console.log(`Recommended runtime context: ${chalk.cyan(`${verification.recommendedContext} tokens`)}`);
console.log(statusColor(`Status: ${verification.status.toUpperCase()}`));
console.log(chalk.blue.bold('\nChecks:'));
for (const check of verification.checks) {
const icon = check.status === 'pass' ? chalk.green('[OK]') :
check.status === 'warn' ? chalk.yellow('[!]') : chalk.red('[FAIL]');
console.log(` ${icon} ${check.message}`);
}
if (verification.suggestions.length > 0) {
console.log(chalk.blue.bold('\nSuggestions:'));
for (const suggestion of verification.suggestions) {
console.log(chalk.gray(` - ${suggestion}`));
}
}
console.log(chalk.cyan(`\nSuggested run: ollama run ${selected.name}`));
console.log(chalk.cyan(`# with context budget: --ctx-size ${verification.recommendedContext}`));
console.log('');
} catch (error) {
if (spinner) spinner.fail('Context verification failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('verify <file>')
.description('ModelVet: verify GGUF or safetensors structural safety before loading')
.option('-j, --json', 'Output as JSON')
.action(async (file, options) => {
const spinner = options.json ? null : ora('Verifying model file structure...').start();
try {
const modelvet = require('../src/security/modelvet-verifier');
const report = await modelvet.verifyFile(file);
if (options.json) {
console.log(JSON.stringify(report, null, 2));
} else {
const sizeGB = (report.sizeBytes / 1024 / 1024 / 1024).toFixed(2);
if (report.accepted) {
if (spinner) spinner.succeed('Model verification complete');
console.log(chalk.blue.bold('\n=== Model Verification (modelvet) ==='));
console.log(`File: ${chalk.white.bold(report.file)}`);
console.log(`Format: ${chalk.cyan(report.format)} (${chalk.cyan(`${sizeGB} GiB`)})`);
console.log(`Verdict: ${chalk.green.bold('ACCEPT')} — structurally safe to load`);
console.log(chalk.gray('\nNote: ACCEPT is structural only. It says nothing about model'));
console.log(chalk.gray('behavior, provenance, or poisoned weights.'));
console.log('');
} else {
if (spinner) spinner.fail('Model verification failed');
console.log(chalk.blue.bold('\n=== Model Verification (modelvet) ==='));
console.log(`File: ${chalk.white.bold(report.file)}`);
console.log(`Format: ${chalk.cyan(report.format)} (${chalk.cyan(`${sizeGB} GiB`)})`);
console.log(`Verdict: ${chalk.red.bold('REJECT')}`);
console.log(`Violation: ${chalk.red(report.violationName)} (code ${report.violation})`);
console.log(`Offset: ${chalk.yellow(`0x${report.offset.toString(16)}`)}`);
console.log(`Detail: ${chalk.gray(`[${report.detail.join(', ')}]`)}`);
console.log(chalk.red('\nThis file must NOT be loaded by any model runtime.'));
console.log('');
}
}
// Mirror the modelvet CLI contract: 0 = ACCEPT, 1 = REJECT, 2 = no verdict.
process.exit(report.accepted ? 0 : 1);
} catch (error) {
if (spinner) spinner.fail('Model verification error');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(2);
}
});
program
.command('amd-guard')
.description('AMD/Windows reliability guard with actionable mitigation hints')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
if (!options.json) showAsciiArt('hw-detect');
const spinner = options.json ? null : ora('Running AMD reliability guard...').start();
try {
const UnifiedDetector = require('../src/hardware/unified-detector');
const ROCmDetector = require('../src/hardware/backends/rocm-detector');
const { buildAmdGuard } = require('../src/commands/roadmap-tools');
const detector = new UnifiedDetector();
const hardware = await detector.detect();
const rocmDetector = new ROCmDetector();
const rocmAvailable = rocmDetector.checkAvailability();
const report = buildAmdGuard({
platform: process.platform,
hardware,
rocmAvailable,
rocmDetectionMethod: rocmDetector.detectionMethod
});
if (options.json) {
console.log(JSON.stringify(report, null, 2));
return;
}
if (spinner) spinner.succeed('AMD guard report ready');
const statusColor = report.status === 'pass'
? chalk.green
: report.status === 'warn'
? chalk.yellow
: chalk.red;
console.log(chalk.blue.bold('\n=== AMD Reliability Guard ==='));
console.log(`Platform: ${chalk.white(process.platform)}`);
console.log(`Primary backend: ${chalk.cyan((report.primaryBackend || 'cpu').toUpperCase())}`);
console.log(`ROCm available: ${chalk.cyan(report.rocmAvailable ? 'yes' : 'no')}`);
console.log(`Detection method: ${chalk.cyan(report.rocmDetectionMethod || 'none')}`);
console.log(statusColor(`Status: ${report.status.toUpperCase()}`));
console.log(chalk.blue.bold('\nChecks:'));
for (const check of report.checks) {
const icon = check.status === 'pass' ? chalk.green('[OK]') :
check.status === 'warn' ? chalk.yellow('[!]') : chalk.red('[FAIL]');
console.log(` ${icon} ${check.message}`);
}
if (report.recommendations.length > 0) {
console.log(chalk.blue.bold('\nRecommendations:'));
for (const recommendation of report.recommendations) {
console.log(chalk.gray(` - ${recommendation}`));
}
}
console.log('');
} catch (error) {
if (spinner) spinner.fail('AMD guard failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('toolcheck')
.description('Tool-calling compatibility tester for local Ollama models')
.option('-m, --model <name>', 'Test a specific model')
.option('--all', 'Test all installed models')
.option('--timeout <ms>', 'Per-model timeout in milliseconds', '45000')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
if (!options.json) showAsciiArt('ollama');
const spinner = options.json ? null : ora('Running tool-calling compatibility checks...').start();
try {
const OllamaClient = require('../src/ollama/client');
const { evaluateToolCallingResult } = require('../src/commands/roadmap-tools');
const timeoutMs = parseInt(options.timeout, 10);
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new Error('Invalid --timeout value. Use a positive integer in ms.');
}
const ollama = new OllamaClient();
const availability = await ollama.checkOllamaAvailability();
if (!availability.available) {
throw new Error(availability.error || 'Ollama is not available');
}
const installed = await ollama.getLocalModels();
if (!installed.length) {
throw new Error('No local Ollama models installed.');
}
let targets = installed;
if (options.model) {
const needle = options.model.toLowerCase();
targets = installed.filter((m) =>
m.name.toLowerCase() === needle ||
m.name.toLowerCase().startsWith(`${needle}:`) ||
m.name.toLowerCase().includes(needle)
);
if (!targets.length) {
throw new Error(`Model "${options.model}" not found in local Ollama models.`);
}
} else if (!options.all) {
targets = installed.slice(0, 1);
}
const toolSpec = [
{
type: 'function',
function: {
name: 'add_numbers',
description: 'Add two integers and return the sum',
parameters: {
type: 'object',
properties: {
a: { type: 'integer' },
b: { type: 'integer' }
},
required: ['a', 'b']
}
}
}
];
const results = [];
for (const model of targets) {
if (spinner) spinner.text = `Testing ${model.name}...`;
let payload = null;
let err = null;
try {
payload = await ollama.chat(
model.name,
[{ role: 'user', content: 'Use the add_numbers tool with a=2 and b=3. Call the tool directly.' }],
{
tools: toolSpec,
timeoutMs,
generationOptions: {
temperature: 0,
num_predict: 64
}
}
);
} catch (error) {
err = error;
}
const evaluation = evaluateToolCallingResult(payload, err);
results.push({
model: model.name,
status: evaluation.status,
score: evaluation.score,
reason: evaluation.reason,
toolCalls: evaluation.toolCalls
});
}
if (options.json) {
console.log(JSON.stringify({
testedModels: results.length,
results
}, null, 2));
return;
}
if (spinner) spinner.succeed(`Toolcheck completed (${results.length} model${results.length > 1 ? 's' : ''})`);
const rows = [['Model', 'Status', 'Score', 'Reason']];
for (const result of results) {
const statusLabel = result.status === 'supported'
? chalk.green('SUPPORTED')
: result.status === 'partial'
? chalk.yellow('PARTIAL')
: chalk.red('UNSUPPORTED');
rows.push([
result.model,
statusLabel,
String(result.score),
result.reason
]);
}
console.log('\n' + table(rows));
const supported = results.filter((r) => r.status === 'supported').length;
const partial = results.filter((r) => r.status === 'partial').length;
const unsupported = results.filter((r) => r.status === 'unsupported').length;
console.log(chalk.blue.bold('Summary:'));
console.log(chalk.green(` Supported: ${supported}`));
console.log(chalk.yellow(` Partial: ${partial}`));
console.log(chalk.red(` Unsupported: ${unsupported}`));
console.log('');
} catch (error) {
if (spinner) spinner.fail('Toolcheck failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
program
.command('hw-detect')
.description('Detect and display detailed hardware capabilities')
.option('--cpu-only', 'Force CPU/RAM execution while retaining GPU inventory as diagnostics')
.option('-j, --json', 'Output as JSON')
.action(async (options) => {
if (!options.json) showAsciiArt('hw-detect');
const UnifiedDetector = require('../src/hardware/unified-detector');
const spinner = options.json ? null : ora('Detecting hardware...').start();
try {
const cpuOnly = getCpuOnlyCommandMode(options);
const detector = new UnifiedDetector({ cpuOnly });
const hardware = await detector.detect();
if (options.json) {
console.log(JSON.stringify(hardware, null, 2));
return;
}
if (spinner) spinner.succeed('Hardware detected!');
displayCpuOnlyModeNotice(cpuOnly);
console.log(chalk.blue.bold('\n=== Hardware Detection ===\n'));
// Summary
console.log(chalk.white.bold('Summary:'));
console.log(` ${detector.getHardwareDescription()}`);
console.log(` Tier: ${chalk.cyan(detector.getHardwareTier().replace('_', ' ').toUpperCase())}`);
console.log(` Max model size: ${chalk.green(detector.getMaxModelSize() + 'GB')}`);
console.log(` Best backend: ${chalk.cyan(getBackendLabelForDisplay(hardware))}`);
if (hardware.summary.runtimeBackend && hardware.summary.runtimeBackend !== hardware.summary.bestBackend) {
console.log(` Runtime assist: ${chalk.green(hardware.summary.runtimeBackendName || hardware.summary.runtimeBackend)}`);
}
const inventorySummary = hardware.cpuOnly && hardware.detectedGpu
? {
dedicatedGpuModels: hardware.detectedGpu.dedicatedGpuModels || [],
integratedGpuModels: hardware.detectedGpu.integratedGpuModels || []
}
: hardware.summary;
const inventoryLabel = hardware.cpuOnly ? ' (diagnostic)' : '';
console.log(` Dedicated GPUs${inventoryLabel}: ${chalk.green(formatGpuInventoryList(inventorySummary.dedicatedGpuModels))}`);
console.log(` Integrated GPUs${inventoryLabel}: ${chalk.hex('#FFA500')(formatGpuInventoryList(inventorySummary.integratedGpuModels))}`);
if (hardware.summary.hasIntegratedGPU && hardware.summary.bestBackend === 'cpu') {
const assistMessage = hardware.summary.runtimeBackend && hardware.summary.runtimeBackend !== hardware.summary.bestBackend
? `Integrated/shared-memory GPU detected, runtime may use ${hardware.summary.runtimeBackendName || hardware.summary.runtimeBackend} acceleration`
: 'Integrated/shared-memory GPU detected, runtime remains CPU';
console.log(` Assist path: ${chalk.yellow(assistMessage)}`);
}
// CPU
if (hardware.cpu) {
console.log(chalk.blue.bold('\nCPU:'));
console.log(` ${hardware.cpu.brand}`);
console.log(` Cores: ${hardware.cpu.cores.logical} (${hardware.cpu.cores.physical} physical)`);
console.log(` SIMD: ${hardware.cpu.capabilities.bestSimd}`);
if (hardware.cpu.capabilities.avx512) console.log(chalk.green(' [OK] AVX-512'));
if (hardware.cpu.capabilities.avx2) console.log(chalk.green(' [OK] AVX2'));
if (hardware.cpu.capabilities.neon) console.log(chalk.green(' [OK] ARM NEON'));
}
// GPU backends
for (const [backend, info] of Object.entries(hardware.backends)) {
if (!info.available || backend === 'cpu') continue;
console.log(chalk.blue.bold(`\n${backend.toUpperCase()}:`));
if (backend === 'metal' && info.info) {
console.log(` ${info.info.chip}`);
console.log(` GPU Cores: ${info.info.gpu.cores}`);
console.log(` Unified Memory: ${info.info.memory.unified}GB`);
console.log(` Memory Bandwidth: ${info.info.memory.bandwidth}GB/s`);
}
if (backend === 'cuda' && info.info) {
console.log(` Driver: ${info.info.driver || 'unknown'}`);
console.log(` CUDA: ${info.info.cuda || 'unknown'}`);
console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
for (const gpu of info.info.gpus) {
console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
}
}
if (backend === 'rocm' && info.info) {
console.log(` ROCm: ${info.info.rocmVersion}`);
const integratedOnly = (info.info.gpus || []).length > 0 &&
(info.info.gpus || []).every((gpu) => gpu.type === 'integrated');
if (integratedOnly) {
console.log(` Total dedicated aperture: ${info.info.totalVRAM || 0}GB`);
console.log(` Total shared memory: ${info.info.totalSharedMemory || 0}GB`);
} else {
console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
}
for (const gpu of info.info.gpus) {
if (gpu.type === 'integrated') {
const dedicated = gpu.memory?.dedicated || 0;
const shared = gpu.memory?.shared || gpu.memory?.total || 0;
const dedicatedLabel = dedicated > 0 ? `, ${dedicated}GB aperture` : '';
console.log(` ${gpu.name}: ${shared}GB shared${dedicatedLabel} (Integrated)`);
} else {
console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
}
}
}
if (backend === 'generic' && info.info) {
console.log(` Source: ${info.info.source || 'systeminformation'}`);
console.log(` Total dedicated VRAM: ${info.info.totalVRAM || 0}GB`);
for (const gpu of info.info.gpus || []) {
const memory = gpu.memory?.total || 0;
const typeLabel = gpu.type === 'integrated' ? 'Integrated' : 'Discrete';
const memoryLabel = memory > 0 ? `${memory}GB` : 'shared/unknown';
console.log(` ${gpu.name}: ${memoryLabel} (${typeLabel})`);
}
}
}
console.log(chalk.gray(`\nFingerprint: ${hardware.fingerprint}`));
console.log('');
} catch (error) {
if (spinner) spinner.fail('Detection failed');
console.error(chalk.red('Error:'), error.message);
if (process.env.DEBUG) console.error(error.stack);
process.exit(1);
}
});
async function bootstrapCli() {
const userArgs = process.argv.slice(2);
const shouldLaunchPanel =
userArgs.length === 0 &&
process.stdin.isTTY &&
process.stdout.isTTY &&
process.env.LLM_CHECKER_DISABLE_PANEL !== '1';
if (shouldLaunchPanel) {
await launchInteractivePanel({
program,
binaryPath: __filename,
appName: 'llm-checker'
});
return;
}
if (userArgs.length === 0) {
renderPersistentBanner();
console.log('');
program.outputHelp();
return;
}
await program.parseAsync(process.argv);
}
bootstrapCli().catch((error) => {
console.error(chalk.red('CLI bootstrap failed:'), error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
});