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.
@@ -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
- // ASCII Art for each command - Large text banners
54
- const ASCII_ART = {
55
- 'hw-detect': `
56
- ██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ███████╗
57
- ██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔════╝
58
- ███████║███████║██████╔╝██║ ██║██║ █╗ ██║███████║██████╔╝█████╗
59
- ██╔══██║██╔══██║██╔══██╗██║ ██║██║███╗██║██╔══██║██╔══██╗██╔══╝
60
- ██║ ██║██║ ██║██║ ██║██████╔╝╚███╔███╔╝██║ ██║██║ ██║███████╗
61
- ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
62
- DETECTION`,
63
-
64
- 'smart-recommend': `
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
- // Function to display ASCII art for a command
71
+ // Kept as function name for backwards compatibility in command handlers.
165
72
  function showAsciiArt(command) {
166
- if (ASCII_ART[command]) {
167
- console.log(chalk.cyan(ASCII_ART[command]));
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: installedModels.slice(),
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 = installedModels.find(
724
+ let match = runnableModels.find(
630
725
  (model) => String(model.name || '').toLowerCase() === normalized
631
726
  );
632
727
 
633
728
  if (!match) {
634
- match = installedModels.find((model) =>
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 = installedModels.find(
735
+ match = runnableModels.find(
641
736
  (model) => String(model.family || '').toLowerCase() === normalized
642
737
  );
643
738
  }
644
739
 
645
740
  if (!match) {
646
- match = installedModels.find((model) =>
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
- program.parse();
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
+ });
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-checker",
3
- "version": "3.4.2",
3
+ "version": "3.5.1",
4
4
  "description": "Intelligent CLI tool with AI-powered model selection that analyzes your hardware and recommends optimal LLM models for your system",
5
5
  "bin": {
6
6
  "llm-checker": "bin/cli.js",
@@ -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('node-fetch');
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;