llm-checker 3.4.2 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/analyzer/performance.js +40 -94
- package/bin/enhanced_cli.js +320 -254
- package/bin/mcp-server.mjs +0 -0
- package/package.json +1 -1
- package/src/models/ai-check-selector.js +2 -2
- package/src/models/deterministic-selector.js +1 -0
- package/src/models/expanded_database.js +10 -83
- package/src/ollama/client.js +29 -4
- package/src/ui/cli-theme.js +733 -0
- package/src/ui/interactive-panel.js +599 -0
- package/src/utils/fetch.js +17 -0
- package/src/utils/token-speed-estimator.js +207 -0
- package/src/ollama/gpu-placement-planner.js +0 -496
package/bin/enhanced_cli.js
CHANGED
|
@@ -47,126 +47,31 @@ const {
|
|
|
47
47
|
buildComplianceReport,
|
|
48
48
|
serializeComplianceReport
|
|
49
49
|
} = require('../src/policy/audit-reporter');
|
|
50
|
+
const { renderCommandHeader, renderPersistentBanner } = require('../src/ui/cli-theme');
|
|
51
|
+
const { launchInteractivePanel } = require('../src/ui/interactive-panel');
|
|
50
52
|
const policyManager = new PolicyManager();
|
|
51
53
|
const calibrationManager = new CalibrationManager();
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
███████╗██╔████╔██║███████║██████╔╝ ██║
|
|
68
|
-
╚════██║██║╚██╔╝██║██╔══██║██╔══██╗ ██║
|
|
69
|
-
███████║██║ ╚═╝ ██║██║ ██║██║ ██║ ██║
|
|
70
|
-
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
|
|
71
|
-
RECOMMEND - AI Powered`,
|
|
72
|
-
|
|
73
|
-
'search': `
|
|
74
|
-
███████╗███████╗ █████╗ ██████╗ ██████╗██╗ ██╗
|
|
75
|
-
██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██║ ██║
|
|
76
|
-
███████╗█████╗ ███████║██████╔╝██║ ███████║
|
|
77
|
-
╚════██║██╔══╝ ██╔══██║██╔══██╗██║ ██╔══██║
|
|
78
|
-
███████║███████╗██║ ██║██║ ██║╚██████╗██║ ██║
|
|
79
|
-
╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
80
|
-
6900+ Models Available`,
|
|
81
|
-
|
|
82
|
-
'sync': `
|
|
83
|
-
███████╗██╗ ██╗███╗ ██╗ ██████╗
|
|
84
|
-
██╔════╝╚██╗ ██╔╝████╗ ██║██╔════╝
|
|
85
|
-
███████╗ ╚████╔╝ ██╔██╗ ██║██║
|
|
86
|
-
╚════██║ ╚██╔╝ ██║╚██╗██║██║
|
|
87
|
-
███████║ ██║ ██║ ╚████║╚██████╗
|
|
88
|
-
╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝
|
|
89
|
-
Database Synchronization`,
|
|
90
|
-
|
|
91
|
-
'check': `
|
|
92
|
-
██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
|
|
93
|
-
██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
|
|
94
|
-
██║ ███████║█████╗ ██║ █████╔╝
|
|
95
|
-
██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
|
|
96
|
-
╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
|
|
97
|
-
╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
|
|
98
|
-
Compatibility Analysis`,
|
|
99
|
-
|
|
100
|
-
'installed': `
|
|
101
|
-
██╗███╗ ██╗███████╗████████╗ █████╗ ██╗ ██╗ ███████╗██████╗
|
|
102
|
-
██║████╗ ██║██╔════╝╚══██╔══╝██╔══██╗██║ ██║ ██╔════╝██╔══██╗
|
|
103
|
-
██║██╔██╗ ██║███████╗ ██║ ███████║██║ ██║ █████╗ ██║ ██║
|
|
104
|
-
██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║ ██║ ██╔══╝ ██║ ██║
|
|
105
|
-
██║██║ ╚████║███████║ ██║ ██║ ██║███████╗███████╗███████╗██████╔╝
|
|
106
|
-
╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═════╝
|
|
107
|
-
Local Models`,
|
|
108
|
-
|
|
109
|
-
'ai-check': `
|
|
110
|
-
█████╗ ██╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
|
|
111
|
-
██╔══██╗██║ ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
|
|
112
|
-
███████║██║ ██║ ███████║█████╗ ██║ █████╔╝
|
|
113
|
-
██╔══██║██║ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
|
|
114
|
-
██║ ██║██║ ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
|
|
115
|
-
╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
|
|
116
|
-
AI-Powered Evaluation`,
|
|
117
|
-
|
|
118
|
-
'ai-run': `
|
|
119
|
-
█████╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
120
|
-
██╔══██╗██║ ██╔══██╗██║ ██║████╗ ██║
|
|
121
|
-
███████║██║ ██████╔╝██║ ██║██╔██╗ ██║
|
|
122
|
-
██╔══██║██║ ██╔══██╗██║ ██║██║╚██╗██║
|
|
123
|
-
██║ ██║██║ ██║ ██║╚██████╔╝██║ ╚████║
|
|
124
|
-
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
125
|
-
Launch & Execute Model`,
|
|
126
|
-
|
|
127
|
-
'demo': `
|
|
128
|
-
██████╗ ███████╗███╗ ███╗ ██████╗
|
|
129
|
-
██╔══██╗██╔════╝████╗ ████║██╔═══██╗
|
|
130
|
-
██║ ██║█████╗ ██╔████╔██║██║ ██║
|
|
131
|
-
██║ ██║██╔══╝ ██║╚██╔╝██║██║ ██║
|
|
132
|
-
██████╔╝███████╗██║ ╚═╝ ██║╚██████╔╝
|
|
133
|
-
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝
|
|
134
|
-
Interactive Preview`,
|
|
135
|
-
|
|
136
|
-
'ollama': `
|
|
137
|
-
██████╗ ██╗ ██╗ █████╗ ███╗ ███╗ █████╗
|
|
138
|
-
██╔═══██╗██║ ██║ ██╔══██╗████╗ ████║██╔══██╗
|
|
139
|
-
██║ ██║██║ ██║ ███████║██╔████╔██║███████║
|
|
140
|
-
██║ ██║██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║
|
|
141
|
-
╚██████╔╝███████╗███████╗██║ ██║██║ ╚═╝ ██║██║ ██║
|
|
142
|
-
╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
143
|
-
Status & Integration`,
|
|
144
|
-
|
|
145
|
-
'recommend': `
|
|
146
|
-
██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗███████╗███╗ ██╗██████╗
|
|
147
|
-
██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔════╝████╗ ██║██╔══██╗
|
|
148
|
-
██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔████╔██║█████╗ ██╔██╗ ██║██║ ██║
|
|
149
|
-
██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║██║ ██║
|
|
150
|
-
██║ ██║███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║███████╗██║ ╚████║██████╔╝
|
|
151
|
-
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═════╝
|
|
152
|
-
Top Picks For You`,
|
|
153
|
-
|
|
154
|
-
'list-models': `
|
|
155
|
-
███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ ███████╗
|
|
156
|
-
████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║ ██╔════╝
|
|
157
|
-
██╔████╔██║██║ ██║██║ ██║█████╗ ██║ ███████╗
|
|
158
|
-
██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██║ ╚════██║
|
|
159
|
-
██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗███████║
|
|
160
|
-
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
|
|
161
|
-
Browse All Available`
|
|
55
|
+
const COMMAND_HEADER_LABELS = {
|
|
56
|
+
'hw-detect': 'Hardware Detection',
|
|
57
|
+
'smart-recommend': 'Smart Recommend',
|
|
58
|
+
search: 'Model Search',
|
|
59
|
+
sync: 'Database Sync',
|
|
60
|
+
'mcp-setup': 'Claude MCP Setup',
|
|
61
|
+
check: 'Compatibility Check',
|
|
62
|
+
installed: 'Installed Models',
|
|
63
|
+
'ai-check': 'AI Check',
|
|
64
|
+
'ai-run': 'AI Run',
|
|
65
|
+
demo: 'Demo',
|
|
66
|
+
ollama: 'Ollama Integration',
|
|
67
|
+
recommend: 'Recommendations',
|
|
68
|
+
'list-models': 'Model Catalog'
|
|
162
69
|
};
|
|
163
70
|
|
|
164
|
-
//
|
|
71
|
+
// Kept as function name for backwards compatibility in command handlers.
|
|
165
72
|
function showAsciiArt(command) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
console.log('');
|
|
169
|
-
}
|
|
73
|
+
const label = COMMAND_HEADER_LABELS[command] || command;
|
|
74
|
+
renderCommandHeader(label);
|
|
170
75
|
}
|
|
171
76
|
|
|
172
77
|
// Function to search Ollama models by use case
|
|
@@ -487,6 +392,123 @@ program
|
|
|
487
392
|
|
|
488
393
|
const logger = getLogger({ console: false });
|
|
489
394
|
|
|
395
|
+
function canRenderColoredHelp() {
|
|
396
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
|
|
397
|
+
if (process.env.NO_COLOR) return false;
|
|
398
|
+
if (process.env.FORCE_COLOR === '0') return false;
|
|
399
|
+
return Boolean(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function colorizeHelpInformation(helpText) {
|
|
403
|
+
const raw = String(helpText || '');
|
|
404
|
+
if (!raw || !canRenderColoredHelp()) {
|
|
405
|
+
return raw;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const sectionColor = chalk.hex('#22D3EE').bold;
|
|
409
|
+
const usageLabelColor = chalk.hex('#60A5FA').bold;
|
|
410
|
+
const usageValueColor = chalk.hex('#F8FAFC').bold;
|
|
411
|
+
const optionColor = chalk.hex('#A7F3D0');
|
|
412
|
+
const commandColor = chalk.hex('#93C5FD').bold;
|
|
413
|
+
const placeholderColor = chalk.hex('#F59E0B');
|
|
414
|
+
const descriptionColor = chalk.hex('#D1D5DB');
|
|
415
|
+
const defaultColor = chalk.hex('#C7D2FE');
|
|
416
|
+
|
|
417
|
+
const colorizePlaceholders = (value) =>
|
|
418
|
+
String(value || '').replace(/(\[[^\]]+\]|<[^>]+>)/g, (token) => placeholderColor(token));
|
|
419
|
+
|
|
420
|
+
return raw
|
|
421
|
+
.split('\n')
|
|
422
|
+
.map((line) => {
|
|
423
|
+
if (!line.trim()) return line;
|
|
424
|
+
|
|
425
|
+
const usageMatch = line.match(/^(\s*)(Usage:)(\s*)(.+)$/);
|
|
426
|
+
if (usageMatch) {
|
|
427
|
+
return `${usageMatch[1]}${usageLabelColor(usageMatch[2])}${usageMatch[3]}${usageValueColor(usageMatch[4])}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const sectionMatch = line.match(/^(\s*)(Options:|Commands:|Enterprise policy examples:|Calibrated routing examples:)\s*$/);
|
|
431
|
+
if (sectionMatch) {
|
|
432
|
+
return `${sectionMatch[1]}${sectionColor(sectionMatch[2])}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (/^\s*\$ /.test(line.trimStart())) {
|
|
436
|
+
return line.replace(/\$ .+$/, (commandText) => chalk.hex('#60A5FA')(commandText));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (/^\s*-/.test(line)) {
|
|
440
|
+
const optionLine = line.match(/^(\s+)(.+?)(\s{2,})(.+)$/);
|
|
441
|
+
if (optionLine) {
|
|
442
|
+
return (
|
|
443
|
+
optionLine[1] +
|
|
444
|
+
optionColor(colorizePlaceholders(optionLine[2])) +
|
|
445
|
+
optionLine[3] +
|
|
446
|
+
descriptionColor(optionLine[4])
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (/^\s+[a-z0-9]/i.test(line)) {
|
|
452
|
+
const commandLine = line.match(/^(\s+)(.+?)(\s{2,})(.+)$/);
|
|
453
|
+
if (commandLine) {
|
|
454
|
+
return (
|
|
455
|
+
commandLine[1] +
|
|
456
|
+
commandColor(colorizePlaceholders(commandLine[2])) +
|
|
457
|
+
commandLine[3] +
|
|
458
|
+
descriptionColor(commandLine[4])
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return defaultColor(line);
|
|
464
|
+
})
|
|
465
|
+
.join('\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function findCommandByName(commandName) {
|
|
469
|
+
const requested = String(commandName || '').trim();
|
|
470
|
+
if (!requested) return null;
|
|
471
|
+
|
|
472
|
+
return program.commands.find((cmd) => {
|
|
473
|
+
if (cmd.name() === requested) return true;
|
|
474
|
+
try {
|
|
475
|
+
return cmd.aliases().includes(requested);
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}) || null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!program.commands.some((cmd) => cmd.name() === 'help')) {
|
|
483
|
+
program
|
|
484
|
+
.command('help [command]')
|
|
485
|
+
.description('Show all commands and how to use them')
|
|
486
|
+
.action((commandName) => {
|
|
487
|
+
renderPersistentBanner();
|
|
488
|
+
console.log('');
|
|
489
|
+
|
|
490
|
+
if (!commandName) {
|
|
491
|
+
console.log(colorizeHelpInformation(program.helpInformation()));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const target = findCommandByName(commandName);
|
|
496
|
+
if (!target) {
|
|
497
|
+
const available = program.commands
|
|
498
|
+
.map((cmd) => cmd.name())
|
|
499
|
+
.filter((name) => name !== 'help')
|
|
500
|
+
.sort((a, b) => a.localeCompare(b))
|
|
501
|
+
.join(', ');
|
|
502
|
+
console.error(chalk.red(`Unknown command: ${commandName}`));
|
|
503
|
+
console.log(chalk.gray(`Available commands: ${available}`));
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
console.log(colorizeHelpInformation(target.helpInformation()));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
490
512
|
// Ollama installation helper
|
|
491
513
|
function getOllamaInstallInstructions() {
|
|
492
514
|
const platform = os.platform();
|
|
@@ -591,6 +613,60 @@ async function checkOllamaAndExit() {
|
|
|
591
613
|
}
|
|
592
614
|
}
|
|
593
615
|
|
|
616
|
+
function quoteCliArg(value) {
|
|
617
|
+
const stringValue = String(value ?? '');
|
|
618
|
+
if (!stringValue) return '""';
|
|
619
|
+
if (/^[A-Za-z0-9._:/=-]+$/.test(stringValue)) return stringValue;
|
|
620
|
+
return `"${stringValue.replace(/(["\\$`])/g, '\\$1')}"`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function getClaudeDesktopConfigPath() {
|
|
624
|
+
const homeDir = os.homedir();
|
|
625
|
+
if (process.platform === 'darwin') {
|
|
626
|
+
return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
627
|
+
}
|
|
628
|
+
if (process.platform === 'win32') {
|
|
629
|
+
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
630
|
+
return path.join(appData, 'Claude', 'claude_desktop_config.json');
|
|
631
|
+
}
|
|
632
|
+
return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function buildClaudeMcpSetup(useNpx = false, serverName = 'llm-checker') {
|
|
636
|
+
const normalizedServerName = String(serverName || 'llm-checker').trim() || 'llm-checker';
|
|
637
|
+
const runner = useNpx ? ['npx', 'llm-checker-mcp'] : ['llm-checker-mcp'];
|
|
638
|
+
const claudeArgs = ['mcp', 'add', normalizedServerName, '--', ...runner];
|
|
639
|
+
const commandLine = ['claude', ...claudeArgs].map(quoteCliArg).join(' ');
|
|
640
|
+
const desktopServerConfig = useNpx
|
|
641
|
+
? { command: 'npx', args: ['llm-checker-mcp'] }
|
|
642
|
+
: { command: 'llm-checker-mcp', args: [] };
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
serverName: normalizedServerName,
|
|
646
|
+
useNpx: Boolean(useNpx),
|
|
647
|
+
claudeArgs,
|
|
648
|
+
commandLine,
|
|
649
|
+
desktopConfigPath: getClaudeDesktopConfigPath(),
|
|
650
|
+
desktopConfig: {
|
|
651
|
+
mcpServers: {
|
|
652
|
+
[normalizedServerName]: desktopServerConfig
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function runExternalCommand(command, args) {
|
|
659
|
+
return new Promise((resolve, reject) => {
|
|
660
|
+
const child = spawn(command, args, {
|
|
661
|
+
stdio: 'inherit',
|
|
662
|
+
env: process.env
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
child.on('error', reject);
|
|
666
|
+
child.on('close', (code) => resolve(code));
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
594
670
|
function parsePositiveIntegerOption(rawValue, optionName) {
|
|
595
671
|
const parsed = Number(rawValue);
|
|
596
672
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -608,13 +684,32 @@ function parseNonNegativeNumberOption(rawValue, optionName) {
|
|
|
608
684
|
}
|
|
609
685
|
|
|
610
686
|
function selectModelsForPlan(installedModels, requestedModels = []) {
|
|
687
|
+
const runnableModels = (installedModels || []).filter((model) => {
|
|
688
|
+
const name = String(model?.name || '').toLowerCase();
|
|
689
|
+
const source = String(model?.source || '').toLowerCase();
|
|
690
|
+
const type = String(model?.type || model?.model_type || '').toLowerCase();
|
|
691
|
+
const fileSizeGB = Number(model?.fileSizeGB) || 0;
|
|
692
|
+
|
|
693
|
+
const cloudTagged = (
|
|
694
|
+
name.includes('-cloud') ||
|
|
695
|
+
name.endsWith(':cloud') ||
|
|
696
|
+
source.includes('cloud') ||
|
|
697
|
+
type === 'cloud' ||
|
|
698
|
+
type === 'remote' ||
|
|
699
|
+
type === 'hosted'
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// Keep only models that are actually present locally for memory planning.
|
|
703
|
+
return !(cloudTagged && fileSizeGB <= 0);
|
|
704
|
+
});
|
|
705
|
+
|
|
611
706
|
const requested = Array.isArray(requestedModels)
|
|
612
707
|
? requestedModels.map((model) => String(model || '').trim()).filter(Boolean)
|
|
613
708
|
: [];
|
|
614
709
|
|
|
615
710
|
if (!requested.length) {
|
|
616
711
|
return {
|
|
617
|
-
selected:
|
|
712
|
+
selected: runnableModels.slice(),
|
|
618
713
|
missing: []
|
|
619
714
|
};
|
|
620
715
|
}
|
|
@@ -626,24 +721,24 @@ function selectModelsForPlan(installedModels, requestedModels = []) {
|
|
|
626
721
|
for (const request of requested) {
|
|
627
722
|
const normalized = request.toLowerCase();
|
|
628
723
|
|
|
629
|
-
let match =
|
|
724
|
+
let match = runnableModels.find(
|
|
630
725
|
(model) => String(model.name || '').toLowerCase() === normalized
|
|
631
726
|
);
|
|
632
727
|
|
|
633
728
|
if (!match) {
|
|
634
|
-
match =
|
|
729
|
+
match = runnableModels.find((model) =>
|
|
635
730
|
String(model.name || '').toLowerCase().startsWith(`${normalized}:`)
|
|
636
731
|
);
|
|
637
732
|
}
|
|
638
733
|
|
|
639
734
|
if (!match) {
|
|
640
|
-
match =
|
|
735
|
+
match = runnableModels.find(
|
|
641
736
|
(model) => String(model.family || '').toLowerCase() === normalized
|
|
642
737
|
);
|
|
643
738
|
}
|
|
644
739
|
|
|
645
740
|
if (!match) {
|
|
646
|
-
match =
|
|
741
|
+
match = runnableModels.find((model) =>
|
|
647
742
|
String(model.name || '').toLowerCase().includes(normalized)
|
|
648
743
|
);
|
|
649
744
|
}
|
|
@@ -2418,6 +2513,78 @@ function displayPolicySummary(commandName, policyConfig, evaluation, enforcement
|
|
|
2418
2513
|
console.log(chalk.magenta('╰' + '─'.repeat(65)));
|
|
2419
2514
|
}
|
|
2420
2515
|
|
|
2516
|
+
program
|
|
2517
|
+
.command('mcp-setup')
|
|
2518
|
+
.description('Show or apply Claude MCP setup for llm-checker')
|
|
2519
|
+
.option('--name <server-name>', 'MCP server name in Claude', 'llm-checker')
|
|
2520
|
+
.option('--npx', 'Use npx llm-checker-mcp instead of global llm-checker-mcp')
|
|
2521
|
+
.option('--apply', 'Run `claude mcp add ...` automatically')
|
|
2522
|
+
.option('-j, --json', 'Output setup details as JSON')
|
|
2523
|
+
.action(async (options) => {
|
|
2524
|
+
const primarySetup = buildClaudeMcpSetup(Boolean(options.npx), options.name);
|
|
2525
|
+
const alternateSetup = buildClaudeMcpSetup(!Boolean(options.npx), options.name);
|
|
2526
|
+
|
|
2527
|
+
if (options.json) {
|
|
2528
|
+
console.log(JSON.stringify({
|
|
2529
|
+
recommended: {
|
|
2530
|
+
command: 'claude',
|
|
2531
|
+
args: primarySetup.claudeArgs,
|
|
2532
|
+
commandLine: primarySetup.commandLine
|
|
2533
|
+
},
|
|
2534
|
+
alternatives: [
|
|
2535
|
+
{
|
|
2536
|
+
command: 'claude',
|
|
2537
|
+
args: alternateSetup.claudeArgs,
|
|
2538
|
+
commandLine: alternateSetup.commandLine
|
|
2539
|
+
}
|
|
2540
|
+
],
|
|
2541
|
+
claudeDesktop: {
|
|
2542
|
+
configPath: primarySetup.desktopConfigPath,
|
|
2543
|
+
snippet: primarySetup.desktopConfig
|
|
2544
|
+
}
|
|
2545
|
+
}, null, 2));
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
showAsciiArt('mcp-setup');
|
|
2550
|
+
|
|
2551
|
+
console.log(chalk.blue.bold('\nClaude Code MCP Setup'));
|
|
2552
|
+
console.log(chalk.white('\nRecommended command:'));
|
|
2553
|
+
console.log(chalk.cyan(` ${primarySetup.commandLine}`));
|
|
2554
|
+
|
|
2555
|
+
console.log(chalk.white('\nAlternative command:'));
|
|
2556
|
+
console.log(chalk.gray(` ${alternateSetup.commandLine}`));
|
|
2557
|
+
|
|
2558
|
+
console.log(chalk.white('\nClaude Desktop config path (manual):'));
|
|
2559
|
+
console.log(chalk.gray(` ${primarySetup.desktopConfigPath}`));
|
|
2560
|
+
console.log(chalk.white('\nConfig snippet:'));
|
|
2561
|
+
console.log(chalk.gray(JSON.stringify(primarySetup.desktopConfig, null, 2)));
|
|
2562
|
+
|
|
2563
|
+
if (!options.apply) {
|
|
2564
|
+
console.log(chalk.green('\nTip: run with --apply to execute the command automatically.'));
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
console.log(chalk.blue('\nApplying MCP setup via Claude CLI...\n'));
|
|
2569
|
+
try {
|
|
2570
|
+
const exitCode = await runExternalCommand('claude', primarySetup.claudeArgs);
|
|
2571
|
+
if (exitCode === 0) {
|
|
2572
|
+
console.log(chalk.green('\nClaude MCP setup applied successfully.'));
|
|
2573
|
+
} else {
|
|
2574
|
+
console.error(chalk.red(`\nClaude command exited with code ${exitCode}.`));
|
|
2575
|
+
process.exit(exitCode || 1);
|
|
2576
|
+
}
|
|
2577
|
+
} catch (error) {
|
|
2578
|
+
if (error && error.code === 'ENOENT') {
|
|
2579
|
+
console.error(chalk.red('Could not find `claude` in PATH.'));
|
|
2580
|
+
console.log(chalk.yellow('Run the printed command manually once Claude CLI is installed.'));
|
|
2581
|
+
} else {
|
|
2582
|
+
console.error(chalk.red(`Failed to apply MCP setup: ${error.message}`));
|
|
2583
|
+
}
|
|
2584
|
+
process.exit(1);
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2421
2588
|
const policyCommand = program
|
|
2422
2589
|
.command('policy')
|
|
2423
2590
|
.description('Manage enterprise policy files (policy.yaml)')
|
|
@@ -3255,140 +3422,6 @@ program
|
|
|
3255
3422
|
}
|
|
3256
3423
|
});
|
|
3257
3424
|
|
|
3258
|
-
program
|
|
3259
|
-
.command('gpu-plan')
|
|
3260
|
-
.description('Recommend multi-GPU placement strategies for selected local models')
|
|
3261
|
-
.option('--models <models...>', 'Model tags/families to include (default: all local models)')
|
|
3262
|
-
.option('--ctx <tokens>', 'Target context window in tokens', '8192')
|
|
3263
|
-
.option('--concurrency <n>', 'Target parallel request count', '2')
|
|
3264
|
-
.option('--objective <mode>', 'Optimization objective (latency|balanced|throughput)', 'balanced')
|
|
3265
|
-
.option('--reserve-gb <gb>', 'Memory reserve to subtract from available GPU memory', '1')
|
|
3266
|
-
.option('--json', 'Output plan as JSON')
|
|
3267
|
-
.action(async (options) => {
|
|
3268
|
-
const spinner = options.json ? null : ora('Building GPU placement plan...').start();
|
|
3269
|
-
|
|
3270
|
-
try {
|
|
3271
|
-
const requestedObjective = String(options.objective || 'balanced').toLowerCase();
|
|
3272
|
-
const supportedObjectives = new Set(['latency', 'balanced', 'throughput']);
|
|
3273
|
-
if (!supportedObjectives.has(requestedObjective)) {
|
|
3274
|
-
throw new Error(`Invalid objective "${options.objective}". Use latency, balanced, or throughput.`);
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
const targetContext = parsePositiveIntegerOption(options.ctx, '--ctx');
|
|
3278
|
-
const targetConcurrency = parsePositiveIntegerOption(options.concurrency, '--concurrency');
|
|
3279
|
-
const reserveGB = parseNonNegativeNumberOption(options.reserveGb, '--reserve-gb');
|
|
3280
|
-
|
|
3281
|
-
const OllamaClient = require('../src/ollama/client');
|
|
3282
|
-
const UnifiedDetector = require('../src/hardware/unified-detector');
|
|
3283
|
-
const OllamaGPUPlacementPlanner = require('../src/ollama/gpu-placement-planner');
|
|
3284
|
-
|
|
3285
|
-
const ollamaClient = new OllamaClient();
|
|
3286
|
-
const availability = await ollamaClient.checkOllamaAvailability();
|
|
3287
|
-
if (!availability.available) {
|
|
3288
|
-
throw new Error(availability.error || 'Ollama is not available');
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
const localModels = await ollamaClient.getLocalModels();
|
|
3292
|
-
if (!localModels || localModels.length === 0) {
|
|
3293
|
-
throw new Error('No local Ollama models found. Install one with: ollama pull llama3.2:3b');
|
|
3294
|
-
}
|
|
3295
|
-
|
|
3296
|
-
const { selected, missing } = selectModelsForPlan(localModels, options.models || []);
|
|
3297
|
-
if (selected.length === 0) {
|
|
3298
|
-
throw new Error(
|
|
3299
|
-
`No matching local models found for: ${(options.models || []).join(', ')}`
|
|
3300
|
-
);
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
|
-
const detector = new UnifiedDetector();
|
|
3304
|
-
const hardware = await detector.detect();
|
|
3305
|
-
const planner = new OllamaGPUPlacementPlanner();
|
|
3306
|
-
|
|
3307
|
-
const plan = planner.plan({
|
|
3308
|
-
hardware,
|
|
3309
|
-
models: selected,
|
|
3310
|
-
targetContext,
|
|
3311
|
-
targetConcurrency,
|
|
3312
|
-
objective: requestedObjective,
|
|
3313
|
-
reserveGB
|
|
3314
|
-
});
|
|
3315
|
-
|
|
3316
|
-
if (options.json) {
|
|
3317
|
-
console.log(JSON.stringify({
|
|
3318
|
-
generated_at: new Date().toISOString(),
|
|
3319
|
-
selection: {
|
|
3320
|
-
requested: options.models || [],
|
|
3321
|
-
selected: selected.map((model) => model.name),
|
|
3322
|
-
missing
|
|
3323
|
-
},
|
|
3324
|
-
plan
|
|
3325
|
-
}, null, 2));
|
|
3326
|
-
return;
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
if (spinner) spinner.succeed('GPU placement plan generated');
|
|
3330
|
-
|
|
3331
|
-
console.log('\n' + chalk.bgMagenta.white.bold(' GPU PLACEMENT PLAN '));
|
|
3332
|
-
console.log(chalk.magenta('Backend:'), `${plan.hardware.backend_name} (${plan.hardware.backend})`);
|
|
3333
|
-
console.log(
|
|
3334
|
-
chalk.magenta('GPU inventory:'),
|
|
3335
|
-
`${plan.hardware.gpu_count} device(s), ${plan.hardware.total_usable_memory_gb}GB usable (reserve ${plan.hardware.reserve_gb}GB)`
|
|
3336
|
-
);
|
|
3337
|
-
console.log(
|
|
3338
|
-
chalk.magenta('Target envelope:'),
|
|
3339
|
-
`ctx=${plan.inputs.target_context}, concurrency=${plan.inputs.target_concurrency}, objective=${plan.objective}`
|
|
3340
|
-
);
|
|
3341
|
-
|
|
3342
|
-
if (missing.length > 0) {
|
|
3343
|
-
console.log(chalk.yellow('Missing model filters:'), missing.join(', '));
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
if (!plan.hardware.is_multi_gpu) {
|
|
3347
|
-
console.log(chalk.yellow('Only one GPU detected: replica/spread are included for simulation but may be infeasible.'));
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
for (const modelPlan of plan.models) {
|
|
3351
|
-
const recommended = modelPlan.recommended || {};
|
|
3352
|
-
const recFit = recommended.feasible ? chalk.green('fit') : chalk.red('no-fit');
|
|
3353
|
-
const recRisk = recommended.risk ? `${recommended.risk.level.toUpperCase()} (${recommended.risk.score})` : 'N/A';
|
|
3354
|
-
|
|
3355
|
-
console.log(chalk.magenta.bold(`\nModel: ${modelPlan.name} (${modelPlan.size})`));
|
|
3356
|
-
console.log(
|
|
3357
|
-
` Recommended: ${chalk.bold((recommended.strategy || 'unknown').toUpperCase())} | ${recFit} | ~${recommended.estimated_tps || 0} tok/s | risk ${recRisk}`
|
|
3358
|
-
);
|
|
3359
|
-
|
|
3360
|
-
if (recommended.device_env_var && recommended.visible_devices) {
|
|
3361
|
-
console.log(` Device pinning hint: export ${recommended.device_env_var}=${recommended.visible_devices}`);
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
console.log(chalk.magenta(' Strategies:'));
|
|
3365
|
-
for (const strategy of modelPlan.strategies) {
|
|
3366
|
-
const fit = strategy.feasible ? chalk.green('fit') : chalk.red('no-fit');
|
|
3367
|
-
const risk = strategy.risk ? `${strategy.risk.level} (${strategy.risk.score})` : 'n/a';
|
|
3368
|
-
console.log(
|
|
3369
|
-
` - ${strategy.strategy.padEnd(7)} ${fit} | ~${strategy.estimated_tps} tok/s | ${strategy.memory_per_gpu_gb}GB/GPU | risk ${risk}`
|
|
3370
|
-
);
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
if (plan.notes && plan.notes.length > 0) {
|
|
3375
|
-
console.log(chalk.magenta.bold('\nNotes:'));
|
|
3376
|
-
for (const note of plan.notes) {
|
|
3377
|
-
console.log(` - ${note}`);
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
console.log('');
|
|
3382
|
-
} catch (error) {
|
|
3383
|
-
if (spinner) spinner.fail('Failed to build GPU placement plan');
|
|
3384
|
-
console.error(chalk.red('Error:'), error.message);
|
|
3385
|
-
if (process.env.DEBUG) {
|
|
3386
|
-
console.error(error.stack);
|
|
3387
|
-
}
|
|
3388
|
-
process.exit(1);
|
|
3389
|
-
}
|
|
3390
|
-
});
|
|
3391
|
-
|
|
3392
3425
|
program
|
|
3393
3426
|
.command('recommend')
|
|
3394
3427
|
.description('Get intelligent model recommendations for your hardware')
|
|
@@ -4317,4 +4350,37 @@ program
|
|
|
4317
4350
|
}
|
|
4318
4351
|
});
|
|
4319
4352
|
|
|
4320
|
-
|
|
4353
|
+
async function bootstrapCli() {
|
|
4354
|
+
const userArgs = process.argv.slice(2);
|
|
4355
|
+
const shouldLaunchPanel =
|
|
4356
|
+
userArgs.length === 0 &&
|
|
4357
|
+
process.stdin.isTTY &&
|
|
4358
|
+
process.stdout.isTTY &&
|
|
4359
|
+
process.env.LLM_CHECKER_DISABLE_PANEL !== '1';
|
|
4360
|
+
|
|
4361
|
+
if (shouldLaunchPanel) {
|
|
4362
|
+
await launchInteractivePanel({
|
|
4363
|
+
program,
|
|
4364
|
+
binaryPath: __filename,
|
|
4365
|
+
appName: 'llm-checker'
|
|
4366
|
+
});
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
if (userArgs.length === 0) {
|
|
4371
|
+
renderPersistentBanner();
|
|
4372
|
+
console.log('');
|
|
4373
|
+
program.outputHelp();
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
await program.parseAsync(process.argv);
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
bootstrapCli().catch((error) => {
|
|
4381
|
+
console.error(chalk.red('CLI bootstrap failed:'), error.message);
|
|
4382
|
+
if (process.env.DEBUG) {
|
|
4383
|
+
console.error(error.stack);
|
|
4384
|
+
}
|
|
4385
|
+
process.exit(1);
|
|
4386
|
+
});
|
package/bin/mcp-server.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ const { OllamaNativeScraper } = require('../ollama/native-scraper');
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const fetch = require('
|
|
13
|
+
const fetch = require('../utils/fetch');
|
|
14
14
|
|
|
15
15
|
class AICheckSelector {
|
|
16
16
|
constructor() {
|
|
@@ -803,4 +803,4 @@ Return JSON with this structure:
|
|
|
803
803
|
}
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
module.exports = AICheckSelector;
|
|
806
|
+
module.exports = AICheckSelector;
|