llm-checker 3.5.2 → 3.5.3

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.
@@ -65,6 +65,7 @@ const COMMAND_HEADER_LABELS = {
65
65
  demo: 'Demo',
66
66
  ollama: 'Ollama Integration',
67
67
  recommend: 'Recommendations',
68
+ simulate: 'Hardware Simulation',
68
69
  'list-models': 'Model Catalog'
69
70
  };
70
71
 
@@ -2938,6 +2939,11 @@ program
2938
2939
  .option('--performance-test', 'Run performance benchmarks')
2939
2940
  .option('--show-ollama-analysis', 'Show detailed Ollama model analysis')
2940
2941
  .option('--no-verbose', 'Disable step-by-step progress display')
2942
+ .option('--simulate <profile>', 'Simulate a hardware profile instead of detecting real hardware (use "list" to see profiles)')
2943
+ .option('--gpu <model>', 'Custom GPU model for simulation (e.g., "RTX 5060", "RX 7800 XT")')
2944
+ .option('--ram <gb>', 'Custom RAM in GB for simulation (e.g., 32)')
2945
+ .option('--cpu <model>', 'Custom CPU model for simulation (e.g., "AMD Ryzen 7 5700X")')
2946
+ .option('--vram <gb>', 'Override GPU VRAM in GB for simulation (auto-detected if omitted)')
2941
2947
  .addHelpText(
2942
2948
  'after',
2943
2949
  `
@@ -2946,6 +2952,12 @@ Enterprise policy examples:
2946
2952
  $ llm-checker check --policy ./policy.yaml --use-case coding --runtime vllm
2947
2953
  $ llm-checker check --policy ./policy.yaml --include-cloud --max-size 24B
2948
2954
 
2955
+ Hardware simulation:
2956
+ $ llm-checker check --simulate list
2957
+ $ llm-checker check --simulate rtx4090
2958
+ $ llm-checker check --simulate m4pro24 --use-case coding
2959
+ $ llm-checker check --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
2960
+
2949
2961
  Policy scope:
2950
2962
  - Evaluates all compatible and marginal candidates discovered during analysis
2951
2963
  - Not limited to the top --limit results shown in output
@@ -2958,7 +2970,57 @@ Policy scope:
2958
2970
  const verboseEnabled = options.verbose !== false;
2959
2971
  const checker = new (getLLMChecker())({ verbose: verboseEnabled });
2960
2972
  const policyConfig = options.policy ? loadPolicyConfiguration(options.policy) : null;
2961
-
2973
+
2974
+ // Handle hardware simulation (preset profile or custom flags)
2975
+ const hasCustomHwFlags = options.gpu || options.ram || options.cpu || options.vram;
2976
+ if (options.simulate || hasCustomHwFlags) {
2977
+ const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, listProfiles } = require('../src/hardware/profiles');
2978
+ if (options.simulate === 'list') {
2979
+ console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
2980
+ listProfiles().forEach(line => console.log(line));
2981
+ console.log('');
2982
+ return;
2983
+ }
2984
+ let simulatedHardware;
2985
+ let displayLabel;
2986
+ if (hasCustomHwFlags) {
2987
+ const ramValue = options.ram ? parseInt(options.ram) : undefined;
2988
+ const vramValue = options.vram ? parseInt(options.vram) : undefined;
2989
+ if (options.vram && !options.gpu) {
2990
+ console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
2991
+ process.exit(1);
2992
+ }
2993
+ if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
2994
+ console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
2995
+ process.exit(1);
2996
+ }
2997
+ if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
2998
+ console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
2999
+ process.exit(1);
3000
+ }
3001
+ simulatedHardware = buildCustomHardwareObject({
3002
+ gpu: options.gpu || null,
3003
+ ram: ramValue,
3004
+ cpu: options.cpu || null,
3005
+ vram: vramValue
3006
+ });
3007
+ displayLabel = simulatedHardware._displayName;
3008
+ } else {
3009
+ const profile = getProfile(options.simulate);
3010
+ if (!profile) {
3011
+ console.error(chalk.red(`\n Unknown profile: ${options.simulate}`));
3012
+ console.log(chalk.gray('\n Available profiles:'));
3013
+ listProfiles().forEach(line => console.log(line));
3014
+ console.log('');
3015
+ process.exit(1);
3016
+ }
3017
+ simulatedHardware = buildFullHardwareObject(options.simulate);
3018
+ displayLabel = profile.displayName;
3019
+ }
3020
+ checker.setSimulatedHardware(simulatedHardware);
3021
+ console.log(chalk.magenta.bold(`\n SIMULATION MODE: ${displayLabel}\n`));
3022
+ }
3023
+
2962
3024
  // If verbose is disabled, show simple loading message
2963
3025
  if (!verboseEnabled) {
2964
3026
  process.stdout.write(chalk.gray('Analyzing your system...'));
@@ -3429,6 +3491,11 @@ program
3429
3491
  .option('--optimize <profile>', 'Optimization profile (balanced|speed|quality|context|coding)', 'balanced')
3430
3492
  .option('--no-verbose', 'Disable step-by-step progress display')
3431
3493
  .option('--policy <file>', 'Evaluate recommendations against a policy file')
3494
+ .option('--simulate <profile>', 'Simulate a hardware profile instead of detecting real hardware (use "list" to see profiles)')
3495
+ .option('--gpu <model>', 'Custom GPU model for simulation (e.g., "RTX 5060", "RX 7800 XT")')
3496
+ .option('--ram <gb>', 'Custom RAM in GB for simulation (e.g., 32)')
3497
+ .option('--cpu <model>', 'Custom CPU model for simulation (e.g., "AMD Ryzen 7 5700X")')
3498
+ .option('--vram <gb>', 'Override GPU VRAM in GB for simulation (auto-detected if omitted)')
3432
3499
  .option(
3433
3500
  '--calibrated [file]',
3434
3501
  'Use calibrated routing policy (optional file path; defaults to ~/.llm-checker/calibration-policy.{yaml,yml,json})'
@@ -3441,6 +3508,11 @@ Enterprise policy examples:
3441
3508
  $ llm-checker recommend --policy ./policy.yaml --category coding
3442
3509
  $ llm-checker recommend --policy ./policy.yaml --no-verbose
3443
3510
 
3511
+ Hardware simulation:
3512
+ $ llm-checker recommend --simulate rtx4090
3513
+ $ llm-checker recommend --simulate m4pro24 --category coding
3514
+ $ llm-checker recommend --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
3515
+
3444
3516
  Calibrated routing examples:
3445
3517
  $ llm-checker recommend --calibrated --category coding
3446
3518
  $ llm-checker recommend --calibrated ./calibration-policy.yaml --category reasoning
@@ -3452,6 +3524,57 @@ Calibrated routing examples:
3452
3524
  try {
3453
3525
  const verboseEnabled = options.verbose !== false;
3454
3526
  const checker = new (getLLMChecker())({ verbose: verboseEnabled });
3527
+
3528
+ // Handle hardware simulation (preset profile or custom flags)
3529
+ const hasCustomHwFlags = options.gpu || options.ram || options.cpu || options.vram;
3530
+ if (options.simulate || hasCustomHwFlags) {
3531
+ const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, listProfiles } = require('../src/hardware/profiles');
3532
+ if (options.simulate === 'list') {
3533
+ console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
3534
+ listProfiles().forEach(line => console.log(line));
3535
+ console.log('');
3536
+ return;
3537
+ }
3538
+ let simulatedHardware;
3539
+ let displayLabel;
3540
+ if (hasCustomHwFlags) {
3541
+ const ramValue = options.ram ? parseInt(options.ram) : undefined;
3542
+ const vramValue = options.vram ? parseInt(options.vram) : undefined;
3543
+ if (options.vram && !options.gpu) {
3544
+ console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
3545
+ process.exit(1);
3546
+ }
3547
+ if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
3548
+ console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
3549
+ process.exit(1);
3550
+ }
3551
+ if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
3552
+ console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
3553
+ process.exit(1);
3554
+ }
3555
+ simulatedHardware = buildCustomHardwareObject({
3556
+ gpu: options.gpu || null,
3557
+ ram: ramValue,
3558
+ cpu: options.cpu || null,
3559
+ vram: vramValue
3560
+ });
3561
+ displayLabel = simulatedHardware._displayName;
3562
+ } else {
3563
+ const profile = getProfile(options.simulate);
3564
+ if (!profile) {
3565
+ console.error(chalk.red(`\n Unknown profile: ${options.simulate}`));
3566
+ console.log(chalk.gray('\n Available profiles:'));
3567
+ listProfiles().forEach(line => console.log(line));
3568
+ console.log('');
3569
+ process.exit(1);
3570
+ }
3571
+ simulatedHardware = buildFullHardwareObject(options.simulate);
3572
+ displayLabel = profile.displayName;
3573
+ }
3574
+ checker.setSimulatedHardware(simulatedHardware);
3575
+ console.log(chalk.magenta.bold(`\n SIMULATION MODE: ${displayLabel}\n`));
3576
+ }
3577
+
3455
3578
  const routingPreference = resolveRoutingPolicyPreference({
3456
3579
  policyOption: options.policy,
3457
3580
  calibratedOption: options.calibrated,
@@ -3524,6 +3647,191 @@ Calibrated routing examples:
3524
3647
  }
3525
3648
  });
3526
3649
 
3650
+ program
3651
+ .command('simulate')
3652
+ .description('Simulate hardware profiles to see compatible LLM models for different systems')
3653
+ .option('-p, --profile <name>', 'Hardware profile to simulate (e.g., rtx4090, m4pro24, h100)')
3654
+ .option('-l, --list', 'List all available hardware profiles')
3655
+ .option('--gpu <model>', 'Custom GPU model (e.g., "RTX 5060", "RX 7800 XT", "Apple M4 Pro")')
3656
+ .option('--ram <gb>', 'Custom RAM in GB (e.g., 32)')
3657
+ .option('--cpu <model>', 'Custom CPU model (e.g., "AMD Ryzen 7 5700X")')
3658
+ .option('--vram <gb>', 'Override GPU VRAM in GB (auto-detected from GPU model if omitted)')
3659
+ .option('-u, --use-case <case>', 'Specify use case', 'general')
3660
+ .option('--optimize <profile>', 'Optimization profile (balanced|speed|quality|context|coding)', 'balanced')
3661
+ .option('--limit <number>', 'Number of compatible models to show (default: 1)', '1')
3662
+ .option('--no-verbose', 'Disable step-by-step progress display')
3663
+ .addHelpText(
3664
+ 'after',
3665
+ `
3666
+ Preset profiles:
3667
+ $ llm-checker simulate --list
3668
+ $ llm-checker simulate
3669
+ $ llm-checker simulate -p rtx4090
3670
+ $ llm-checker simulate -p m4pro24 --use-case coding
3671
+
3672
+ Custom hardware:
3673
+ $ llm-checker simulate --gpu "RTX 5060" --ram 32 --cpu "AMD Ryzen 7 5700X"
3674
+ $ llm-checker simulate --gpu "RTX 4090" --ram 64
3675
+ $ llm-checker simulate --gpu "RX 7800 XT" --ram 32 --vram 16
3676
+ $ llm-checker simulate --ram 16
3677
+ `
3678
+ )
3679
+ .action(async (options) => {
3680
+ const { buildFullHardwareObject, buildCustomHardwareObject, getProfile, getProfilesByCategory, listProfiles, CATEGORY_LABELS } = require('../src/hardware/profiles');
3681
+
3682
+ // List mode
3683
+ if (options.list) {
3684
+ console.log(chalk.cyan.bold('\n Available Hardware Profiles:\n'));
3685
+ listProfiles().forEach(line => console.log(line));
3686
+ console.log('');
3687
+ return;
3688
+ }
3689
+
3690
+ let simulatedHardware;
3691
+ let displayLabel;
3692
+
3693
+ // Custom hardware mode: --gpu, --ram, --cpu, --vram
3694
+ const hasCustomFlags = options.gpu || options.ram || options.cpu || options.vram;
3695
+ if (hasCustomFlags) {
3696
+ const ramValue = options.ram ? parseInt(options.ram) : undefined;
3697
+ const vramValue = options.vram ? parseInt(options.vram) : undefined;
3698
+ if (options.vram && !options.gpu) {
3699
+ console.error(chalk.red('\n --vram requires --gpu in custom hardware mode (e.g., --gpu "RTX 4090" --vram 24).'));
3700
+ process.exit(1);
3701
+ }
3702
+ if (options.ram && (!Number.isFinite(ramValue) || ramValue <= 0)) {
3703
+ console.error(chalk.red(`\n Invalid --ram value: "${options.ram}". Must be a positive number (e.g., 32).`));
3704
+ process.exit(1);
3705
+ }
3706
+ if (options.vram && (!Number.isFinite(vramValue) || vramValue <= 0)) {
3707
+ console.error(chalk.red(`\n Invalid --vram value: "${options.vram}". Must be a positive number (e.g., 8).`));
3708
+ process.exit(1);
3709
+ }
3710
+ simulatedHardware = buildCustomHardwareObject({
3711
+ gpu: options.gpu || null,
3712
+ ram: ramValue,
3713
+ cpu: options.cpu || null,
3714
+ vram: vramValue
3715
+ });
3716
+ displayLabel = simulatedHardware._displayName;
3717
+ } else {
3718
+ // Preset profile mode
3719
+ if (!options.profile) {
3720
+ // Guard against non-interactive environments
3721
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3722
+ console.error(chalk.red('\n No hardware profile specified.'));
3723
+ console.log(chalk.gray(' Use --profile <name>, --gpu/--ram/--cpu flags, or --list to see profiles.\n'));
3724
+ process.exit(1);
3725
+ }
3726
+ // Interactive selection
3727
+ try {
3728
+ const inquirer = require('inquirer');
3729
+ const categories = getProfilesByCategory();
3730
+ const choices = [];
3731
+
3732
+ for (const [category, profiles] of Object.entries(categories)) {
3733
+ const label = CATEGORY_LABELS[category] || category;
3734
+ choices.push(new inquirer.Separator(chalk.gray(`── ${label} ──`)));
3735
+ for (const [key, profile] of Object.entries(profiles)) {
3736
+ const vramLabel = profile.gpu.unified
3737
+ ? `${profile.memory.total}GB unified`
3738
+ : (profile.gpu.vram > 0 ? `${profile.gpu.vram}GB VRAM` : 'No GPU');
3739
+ const ramLabel = profile.gpu.unified ? '' : ` / ${profile.memory.total}GB RAM`;
3740
+ choices.push({
3741
+ name: `${profile.displayName} ${chalk.gray(`(${vramLabel}${ramLabel})`)}`,
3742
+ value: key
3743
+ });
3744
+ }
3745
+ }
3746
+
3747
+ const { selectedProfile } = await inquirer.prompt([{
3748
+ type: 'list',
3749
+ name: 'selectedProfile',
3750
+ message: 'Select a hardware profile to simulate:',
3751
+ choices,
3752
+ pageSize: 20
3753
+ }]);
3754
+ options.profile = selectedProfile;
3755
+ } catch (error) {
3756
+ if (error.isTtyError) {
3757
+ console.error(chalk.red('Interactive mode requires a TTY terminal.'));
3758
+ console.log(chalk.gray('Use --profile <name>, --gpu/--ram flags, or --list to see available profiles.'));
3759
+ process.exit(1);
3760
+ }
3761
+ throw error;
3762
+ }
3763
+ }
3764
+
3765
+ // Validate profile
3766
+ const profile = getProfile(options.profile);
3767
+ if (!profile) {
3768
+ console.error(chalk.red(`\n Unknown profile: ${options.profile}`));
3769
+ console.log(chalk.gray('\n Available profiles:'));
3770
+ listProfiles().forEach(line => console.log(line));
3771
+ console.log('');
3772
+ process.exit(1);
3773
+ }
3774
+
3775
+ simulatedHardware = buildFullHardwareObject(options.profile);
3776
+ displayLabel = profile.displayName;
3777
+ }
3778
+
3779
+ showAsciiArt('simulate');
3780
+
3781
+ try {
3782
+ const verboseEnabled = options.verbose !== false;
3783
+ const checker = new (getLLMChecker())({ verbose: verboseEnabled });
3784
+ checker.setSimulatedHardware(simulatedHardware);
3785
+
3786
+ console.log(chalk.magenta.bold(` SIMULATION MODE: ${displayLabel}\n`));
3787
+
3788
+ if (!verboseEnabled) {
3789
+ process.stdout.write(chalk.gray('Analyzing simulated hardware...'));
3790
+ }
3791
+
3792
+ const hardware = await checker.getSystemInfo();
3793
+
3794
+ const normalizeUseCase = (useCase = '') => {
3795
+ const alias = useCase.toLowerCase().trim();
3796
+ const useCaseMap = {
3797
+ 'embed': 'embeddings', 'embedding': 'embeddings', 'embeddings': 'embeddings',
3798
+ 'embedings': 'embeddings', 'talk': 'chat', 'chat': 'chat', 'talking': 'chat'
3799
+ };
3800
+ return useCaseMap[alias] || alias || 'general';
3801
+ };
3802
+
3803
+ const analysis = await checker.analyze({
3804
+ useCase: normalizeUseCase(options.useCase),
3805
+ limit: parseInt(options.limit) || 10,
3806
+ runtime: 'ollama'
3807
+ });
3808
+
3809
+ if (!verboseEnabled) {
3810
+ console.log(chalk.green(' done'));
3811
+ }
3812
+
3813
+ displaySimplifiedSystemInfo(hardware);
3814
+
3815
+ const normalizedUseCase = normalizeUseCase(options.useCase);
3816
+ const limit = parseInt(options.limit) || 1;
3817
+ const recommendedModels = await displayModelRecommendations(
3818
+ analysis,
3819
+ hardware,
3820
+ normalizedUseCase,
3821
+ limit,
3822
+ 'ollama'
3823
+ );
3824
+ await displayQuickStartCommands(analysis, recommendedModels[0], recommendedModels, 'ollama');
3825
+
3826
+ } catch (error) {
3827
+ console.error(chalk.red('\nError:'), error.message);
3828
+ if (process.env.DEBUG) {
3829
+ console.error(error.stack);
3830
+ }
3831
+ process.exit(1);
3832
+ }
3833
+ });
3834
+
3527
3835
  program
3528
3836
  .command('list-models')
3529
3837
  .description('List all models from Ollama database')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-checker",
3
- "version": "3.5.2",
3
+ "version": "3.5.3",
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",
@@ -47,7 +47,7 @@
47
47
  "inquirer": "^8.2.6",
48
48
  "node-fetch": "^2.7.0",
49
49
  "ora": "^5.4.1",
50
- "systeminformation": "^5.21.0",
50
+ "systeminformation": "^5.31.1",
51
51
  "table": "^6.8.1",
52
52
  "yaml": "^2.8.1",
53
53
  "zod": "^3.23.0"
@@ -55,9 +55,16 @@
55
55
  "optionalDependencies": {
56
56
  "sql.js": "^1.14.0"
57
57
  },
58
+ "overrides": {
59
+ "ajv": "^8.18.0",
60
+ "hono": "^4.11.10",
61
+ "glob": "^13.0.0",
62
+ "minimatch": "^10.2.2",
63
+ "test-exclude": "^7.0.1"
64
+ },
58
65
  "devDependencies": {
59
66
  "@types/node": "^20.0.0",
60
- "jest": "^29.7.0"
67
+ "jest": "^30.2.0"
61
68
  },
62
69
  "keywords": [
63
70
  "llm",
@@ -7,9 +7,22 @@ class HardwareDetector {
7
7
  this.cacheExpiry = 5 * 60 * 1000;
8
8
  this.cacheTime = 0;
9
9
  this.unifiedDetector = new UnifiedDetector();
10
+ this._simulatedHardware = null;
11
+ }
12
+
13
+ setSimulatedHardware(hardwareObject) {
14
+ this._simulatedHardware = hardwareObject;
15
+ }
16
+
17
+ clearSimulatedHardware() {
18
+ this._simulatedHardware = null;
10
19
  }
11
20
 
12
21
  async getSystemInfo(forceFresh = false) {
22
+ // Return simulated hardware if set (bypasses real detection)
23
+ if (this._simulatedHardware) {
24
+ return this._simulatedHardware;
25
+ }
13
26
 
14
27
  if (!forceFresh && this.cache && (Date.now() - this.cacheTime < this.cacheExpiry)) {
15
28
  return this.cache;
@@ -0,0 +1,484 @@
1
+ const HardwareSpecs = require('./specs');
2
+
3
+ const specs = new HardwareSpecs();
4
+
5
+ // Curated hardware profiles for simulation
6
+ const HARDWARE_PROFILES = {
7
+ // NVIDIA Data Center
8
+ h100: {
9
+ displayName: 'NVIDIA H100 80GB (Data Center)',
10
+ category: 'data_center',
11
+ gpu: { model: 'NVIDIA H100', vendor: 'NVIDIA', vram: 80, dedicated: true },
12
+ cpu: { brand: 'AMD EPYC 9654', cores: 96, physicalCores: 96, speed: 2.4, architecture: 'x86_64' },
13
+ memory: { total: 256 },
14
+ backend: 'cuda',
15
+ os: { platform: 'linux' }
16
+ },
17
+ a100: {
18
+ displayName: 'NVIDIA A100 80GB (Data Center)',
19
+ category: 'data_center',
20
+ gpu: { model: 'NVIDIA A100', vendor: 'NVIDIA', vram: 80, dedicated: true },
21
+ cpu: { brand: 'AMD EPYC 7763', cores: 64, physicalCores: 64, speed: 2.45, architecture: 'x86_64' },
22
+ memory: { total: 128 },
23
+ backend: 'cuda',
24
+ os: { platform: 'linux' }
25
+ },
26
+
27
+ // NVIDIA Desktop
28
+ rtx4090: {
29
+ displayName: 'NVIDIA RTX 4090 (Desktop)',
30
+ category: 'nvidia_desktop',
31
+ gpu: { model: 'NVIDIA GeForce RTX 4090', vendor: 'NVIDIA', vram: 24, dedicated: true },
32
+ cpu: { brand: 'AMD Ryzen 9 7950X', cores: 16, physicalCores: 16, speed: 4.5, architecture: 'x86_64' },
33
+ memory: { total: 64 },
34
+ backend: 'cuda',
35
+ os: { platform: 'linux' }
36
+ },
37
+ rtx3090: {
38
+ displayName: 'NVIDIA RTX 3090 (Desktop)',
39
+ category: 'nvidia_desktop',
40
+ gpu: { model: 'NVIDIA GeForce RTX 3090', vendor: 'NVIDIA', vram: 24, dedicated: true },
41
+ cpu: { brand: 'AMD Ryzen 9 5950X', cores: 16, physicalCores: 16, speed: 3.4, architecture: 'x86_64' },
42
+ memory: { total: 64 },
43
+ backend: 'cuda',
44
+ os: { platform: 'linux' }
45
+ },
46
+ rtx4070ti: {
47
+ displayName: 'NVIDIA RTX 4070 Ti (Desktop)',
48
+ category: 'nvidia_desktop',
49
+ gpu: { model: 'NVIDIA GeForce RTX 4070 Ti', vendor: 'NVIDIA', vram: 12, dedicated: true },
50
+ cpu: { brand: 'Intel Core i7-13700K', cores: 16, physicalCores: 16, speed: 3.4, architecture: 'x86_64' },
51
+ memory: { total: 32 },
52
+ backend: 'cuda',
53
+ os: { platform: 'linux' }
54
+ },
55
+ rtx3060: {
56
+ displayName: 'NVIDIA RTX 3060 (Desktop)',
57
+ category: 'nvidia_desktop',
58
+ gpu: { model: 'NVIDIA GeForce RTX 3060', vendor: 'NVIDIA', vram: 12, dedicated: true },
59
+ cpu: { brand: 'Intel Core i5-12600K', cores: 10, physicalCores: 10, speed: 3.7, architecture: 'x86_64' },
60
+ memory: { total: 32 },
61
+ backend: 'cuda',
62
+ os: { platform: 'linux' }
63
+ },
64
+
65
+ // Apple Silicon
66
+ m4max48: {
67
+ displayName: 'Apple M4 Max 48GB',
68
+ category: 'apple_silicon',
69
+ gpu: { model: 'Apple M4 Max', vendor: 'Apple', vram: 0, dedicated: false, unified: true },
70
+ cpu: { brand: 'Apple M4 Max', cores: 16, physicalCores: 16, speed: 4.5, architecture: 'Apple Silicon' },
71
+ memory: { total: 48 },
72
+ backend: 'metal',
73
+ os: { platform: 'darwin' }
74
+ },
75
+ m4pro24: {
76
+ displayName: 'Apple M4 Pro 24GB',
77
+ category: 'apple_silicon',
78
+ gpu: { model: 'Apple M4 Pro', vendor: 'Apple', vram: 0, dedicated: false, unified: true },
79
+ cpu: { brand: 'Apple M4 Pro', cores: 14, physicalCores: 14, speed: 4.5, architecture: 'Apple Silicon' },
80
+ memory: { total: 24 },
81
+ backend: 'metal',
82
+ os: { platform: 'darwin' }
83
+ },
84
+ m3_16: {
85
+ displayName: 'Apple M3 16GB',
86
+ category: 'apple_silicon',
87
+ gpu: { model: 'Apple M3', vendor: 'Apple', vram: 0, dedicated: false, unified: true },
88
+ cpu: { brand: 'Apple M3', cores: 8, physicalCores: 8, speed: 4.0, architecture: 'Apple Silicon' },
89
+ memory: { total: 16 },
90
+ backend: 'metal',
91
+ os: { platform: 'darwin' }
92
+ },
93
+ m1_16: {
94
+ displayName: 'Apple M1 16GB',
95
+ category: 'apple_silicon',
96
+ gpu: { model: 'Apple M1', vendor: 'Apple', vram: 0, dedicated: false, unified: true },
97
+ cpu: { brand: 'Apple M1', cores: 8, physicalCores: 8, speed: 3.2, architecture: 'Apple Silicon' },
98
+ memory: { total: 16 },
99
+ backend: 'metal',
100
+ os: { platform: 'darwin' }
101
+ },
102
+
103
+ // AMD Desktop
104
+ rx7900xtx: {
105
+ displayName: 'AMD RX 7900 XTX (Desktop)',
106
+ category: 'amd_desktop',
107
+ gpu: { model: 'AMD Radeon RX 7900 XTX', vendor: 'AMD', vram: 24, dedicated: true },
108
+ cpu: { brand: 'AMD Ryzen 9 7900X', cores: 12, physicalCores: 12, speed: 4.7, architecture: 'x86_64' },
109
+ memory: { total: 64 },
110
+ backend: 'rocm',
111
+ os: { platform: 'linux' }
112
+ },
113
+
114
+ // CPU Only
115
+ cpu_high: {
116
+ displayName: 'CPU Only (32GB RAM)',
117
+ category: 'cpu_only',
118
+ gpu: { model: '', vendor: '', vram: 0, dedicated: false },
119
+ cpu: { brand: 'Intel Core i9-13900K', cores: 24, physicalCores: 24, speed: 3.0, architecture: 'x86_64' },
120
+ memory: { total: 32 },
121
+ backend: 'cpu',
122
+ os: { platform: 'linux' }
123
+ },
124
+ cpu_mid: {
125
+ displayName: 'CPU Only (16GB RAM)',
126
+ category: 'cpu_only',
127
+ gpu: { model: '', vendor: '', vram: 0, dedicated: false },
128
+ cpu: { brand: 'AMD Ryzen 7 5800X', cores: 8, physicalCores: 8, speed: 3.8, architecture: 'x86_64' },
129
+ memory: { total: 16 },
130
+ backend: 'cpu',
131
+ os: { platform: 'linux' }
132
+ },
133
+ cpu_low: {
134
+ displayName: 'CPU Only (8GB RAM)',
135
+ category: 'cpu_only',
136
+ gpu: { model: '', vendor: '', vram: 0, dedicated: false },
137
+ cpu: { brand: 'Intel Core i5-12600K', cores: 10, physicalCores: 10, speed: 3.7, architecture: 'x86_64' },
138
+ memory: { total: 8 },
139
+ backend: 'cpu',
140
+ os: { platform: 'linux' }
141
+ }
142
+ };
143
+
144
+ const CATEGORY_LABELS = {
145
+ data_center: 'Data Center',
146
+ nvidia_desktop: 'NVIDIA Desktop',
147
+ apple_silicon: 'Apple Silicon',
148
+ amd_desktop: 'AMD Desktop',
149
+ cpu_only: 'CPU Only'
150
+ };
151
+
152
+ function buildFullHardwareObject(profileKey) {
153
+ const profile = HARDWARE_PROFILES[profileKey];
154
+ if (!profile) return null;
155
+
156
+ const isApple = profile.os.platform === 'darwin';
157
+ const isUnified = Boolean(profile.gpu.unified);
158
+ const totalRAM = profile.memory.total;
159
+ const vram = profile.gpu.vram;
160
+
161
+ // Compute effective memory (matches unified-detector logic)
162
+ const effectiveMemory = isUnified
163
+ ? totalRAM
164
+ : (vram > 0 ? vram : Math.round(totalRAM * 0.7));
165
+
166
+ // Get scores from HardwareSpecs where available
167
+ const cpuSpecs = specs.getCPUScore(profile.cpu.brand);
168
+ const gpuSpecs = profile.gpu.model ? specs.getGPUScore(profile.gpu.model) : { score: 0 };
169
+
170
+ // Simulate ~60% free memory
171
+ const freeRAM = Math.round(totalRAM * 0.6);
172
+ const usedRAM = totalRAM - freeRAM;
173
+
174
+ // Build the full hardware object (Shape A - matches HardwareDetector.getSystemInfo() output)
175
+ const hardware = {
176
+ cpu: {
177
+ brand: profile.cpu.brand,
178
+ manufacturer: isApple ? 'Apple' : (profile.cpu.brand.includes('Intel') ? 'Intel' : 'AMD'),
179
+ family: 'Unknown',
180
+ model: 'Unknown',
181
+ speed: profile.cpu.speed,
182
+ speedMax: profile.cpu.speed,
183
+ cores: profile.cpu.cores,
184
+ physicalCores: profile.cpu.physicalCores,
185
+ processors: 1,
186
+ cache: { l1d: 0, l1i: 0, l2: 0, l3: 0 },
187
+ architecture: profile.cpu.architecture,
188
+ score: cpuSpecs.score || 70
189
+ },
190
+ memory: {
191
+ total: totalRAM,
192
+ free: freeRAM,
193
+ used: usedRAM,
194
+ available: freeRAM,
195
+ usagePercent: Math.round((usedRAM / totalRAM) * 100),
196
+ swapTotal: 0,
197
+ swapUsed: 0,
198
+ score: totalRAM >= 64 ? 55 : (totalRAM >= 32 ? 50 : (totalRAM >= 16 ? 40 : 25))
199
+ },
200
+ gpu: {
201
+ model: profile.gpu.model || 'No GPU detected',
202
+ vendor: profile.gpu.vendor || 'Unknown',
203
+ vram: vram,
204
+ vramPerGPU: vram,
205
+ vramDynamic: false,
206
+ dedicated: profile.gpu.dedicated,
207
+ driverVersion: 'Simulated',
208
+ gpuCount: 1,
209
+ isMultiGPU: false,
210
+ all: profile.gpu.model ? [{
211
+ model: profile.gpu.model,
212
+ vram: vram,
213
+ vendor: profile.gpu.vendor
214
+ }] : [],
215
+ displays: 1,
216
+ score: gpuSpecs.score || 0,
217
+ unified: isUnified,
218
+ backend: profile.backend
219
+ },
220
+ system: {
221
+ manufacturer: isApple ? 'Apple' : 'Simulated System',
222
+ model: profile.displayName,
223
+ version: 'Simulated'
224
+ },
225
+ os: {
226
+ platform: profile.os.platform,
227
+ distro: isApple ? 'macOS' : 'Linux',
228
+ release: 'Simulated',
229
+ codename: 'Simulated',
230
+ kernel: 'Simulated',
231
+ arch: profile.cpu.architecture === 'Apple Silicon' ? 'arm64' : 'x64',
232
+ hostname: 'simulated-host',
233
+ logofile: ''
234
+ },
235
+ timestamp: Date.now(),
236
+
237
+ // Shape B - for ScoringEngine / test compatibility
238
+ summary: {
239
+ bestBackend: profile.backend,
240
+ gpuModel: profile.gpu.model,
241
+ effectiveMemory: effectiveMemory,
242
+ systemRAM: totalRAM,
243
+ totalVRAM: vram
244
+ }
245
+ };
246
+
247
+ // Add CPU capabilities for scoring engine
248
+ hardware.cpu.capabilities = {};
249
+ if (profile.cpu.architecture === 'Apple Silicon') {
250
+ hardware.cpu.capabilities.neon = true;
251
+ } else {
252
+ hardware.cpu.capabilities.avx2 = true;
253
+ if (profile.cpu.cores >= 64) {
254
+ hardware.cpu.capabilities.avx512 = true;
255
+ }
256
+ }
257
+
258
+ return hardware;
259
+ }
260
+
261
+ function inferGpuDetails(gpuName) {
262
+ if (!gpuName) return { model: '', vendor: '', vram: 0, dedicated: false, unified: false, backend: 'cpu', platform: 'linux' };
263
+
264
+ const lower = gpuName.toLowerCase();
265
+ let vendor = 'Unknown';
266
+ let dedicated = true;
267
+ let unified = false;
268
+ let backend = 'cuda';
269
+ let platform = 'linux';
270
+
271
+ if (lower.includes('nvidia') || lower.includes('rtx') || lower.includes('gtx') || lower.includes('geforce')) {
272
+ vendor = 'NVIDIA';
273
+ backend = 'cuda';
274
+ } else if (lower.includes('amd') || lower.includes('radeon') || lower.includes('rx ')) {
275
+ vendor = 'AMD';
276
+ backend = 'rocm';
277
+ } else if (lower.includes('apple') || /\bm[1-9]\b/.test(lower)) {
278
+ vendor = 'Apple';
279
+ backend = 'metal';
280
+ platform = 'darwin';
281
+ dedicated = false;
282
+ unified = true;
283
+ } else if (lower.includes('intel') && (lower.includes('arc') || lower.includes('iris') || lower.includes('uhd'))) {
284
+ vendor = 'Intel';
285
+ backend = 'cpu';
286
+ dedicated = lower.includes('arc');
287
+ }
288
+
289
+ // Normalize model name to match what estimateVRAMFromModel expects
290
+ let model = gpuName;
291
+ if (vendor === 'NVIDIA' && !lower.includes('nvidia')) {
292
+ model = `NVIDIA GeForce ${gpuName}`;
293
+ }
294
+
295
+ // Use HardwareDetector's VRAM estimation logic
296
+ const HardwareDetector = require('./detector');
297
+ const detector = new HardwareDetector();
298
+ const estimatedVram = detector.estimateVRAMFromModel(model);
299
+
300
+ return { model, vendor, vram: estimatedVram, dedicated, unified, backend, platform };
301
+ }
302
+
303
+ function inferCpuDetails(cpuName) {
304
+ if (!cpuName) return { brand: 'Unknown CPU', cores: 8, physicalCores: 8, speed: 3.5, architecture: 'x86_64', manufacturer: 'Unknown' };
305
+
306
+ const lower = cpuName.toLowerCase();
307
+ const cpuSpecs = specs.getCPUScore(cpuName);
308
+ let manufacturer = 'Unknown';
309
+ let architecture = 'x86_64';
310
+
311
+ if (lower.includes('apple') || /\bm[1-9]\b/.test(lower)) {
312
+ manufacturer = 'Apple';
313
+ architecture = 'Apple Silicon';
314
+ } else if (lower.includes('intel')) {
315
+ manufacturer = 'Intel';
316
+ } else if (lower.includes('amd') || lower.includes('ryzen') || lower.includes('epyc')) {
317
+ manufacturer = 'AMD';
318
+ }
319
+
320
+ return {
321
+ brand: cpuName,
322
+ cores: cpuSpecs.cores || 8,
323
+ physicalCores: cpuSpecs.cores || 8,
324
+ speed: 3.5,
325
+ architecture,
326
+ manufacturer
327
+ };
328
+ }
329
+
330
+ function buildCustomHardwareObject({ gpu, ram, cpu, vram: overrideVram }) {
331
+ const gpuDetails = inferGpuDetails(gpu);
332
+ const cpuDetails = inferCpuDetails(cpu);
333
+
334
+ const totalRAM = ram || 16;
335
+ const vram = (overrideVram != null && Number.isFinite(overrideVram) && overrideVram > 0)
336
+ ? overrideVram : gpuDetails.vram;
337
+ const isApple = gpuDetails.platform === 'darwin' || cpuDetails.architecture === 'Apple Silicon';
338
+ const isUnified = gpuDetails.unified || isApple;
339
+ const platform = isApple ? 'darwin' : 'linux';
340
+ const backend = gpuDetails.backend;
341
+
342
+ const effectiveMemory = isUnified
343
+ ? totalRAM
344
+ : (vram > 0 ? vram : Math.round(totalRAM * 0.7));
345
+
346
+ const gpuSpecs = gpuDetails.model ? specs.getGPUScore(gpuDetails.model) : { score: 0 };
347
+ const cpuScore = specs.getCPUScore(cpuDetails.brand);
348
+ const freeRAM = Math.round(totalRAM * 0.6);
349
+ const usedRAM = totalRAM - freeRAM;
350
+
351
+ const displayParts = [];
352
+ if (cpuDetails.brand && cpuDetails.brand !== 'Unknown CPU') displayParts.push(cpuDetails.brand);
353
+ displayParts.push(`${totalRAM}GB RAM`);
354
+ if (gpuDetails.model) displayParts.push(gpuDetails.model);
355
+ const displayName = `Custom: ${displayParts.join(' + ')}`;
356
+
357
+ const hardware = {
358
+ cpu: {
359
+ brand: cpuDetails.brand,
360
+ manufacturer: cpuDetails.manufacturer,
361
+ family: 'Unknown',
362
+ model: 'Unknown',
363
+ speed: cpuDetails.speed,
364
+ speedMax: cpuDetails.speed,
365
+ cores: cpuDetails.cores,
366
+ physicalCores: cpuDetails.physicalCores,
367
+ processors: 1,
368
+ cache: { l1d: 0, l1i: 0, l2: 0, l3: 0 },
369
+ architecture: cpuDetails.architecture,
370
+ score: cpuScore.score || 70
371
+ },
372
+ memory: {
373
+ total: totalRAM,
374
+ free: freeRAM,
375
+ used: usedRAM,
376
+ available: freeRAM,
377
+ usagePercent: Math.round((usedRAM / totalRAM) * 100),
378
+ swapTotal: 0,
379
+ swapUsed: 0,
380
+ score: totalRAM >= 64 ? 55 : (totalRAM >= 32 ? 50 : (totalRAM >= 16 ? 40 : 25))
381
+ },
382
+ gpu: {
383
+ model: gpuDetails.model || 'No GPU detected',
384
+ vendor: gpuDetails.vendor || 'Unknown',
385
+ vram: vram,
386
+ vramPerGPU: vram,
387
+ vramDynamic: false,
388
+ dedicated: gpuDetails.dedicated,
389
+ driverVersion: 'Simulated',
390
+ gpuCount: 1,
391
+ isMultiGPU: false,
392
+ all: gpuDetails.model ? [{
393
+ model: gpuDetails.model,
394
+ vram: vram,
395
+ vendor: gpuDetails.vendor
396
+ }] : [],
397
+ displays: 1,
398
+ score: gpuSpecs.score || 0,
399
+ unified: isUnified,
400
+ backend: backend
401
+ },
402
+ system: {
403
+ manufacturer: isApple ? 'Apple' : 'Simulated System',
404
+ model: displayName,
405
+ version: 'Simulated'
406
+ },
407
+ os: {
408
+ platform: platform,
409
+ distro: isApple ? 'macOS' : 'Linux',
410
+ release: 'Simulated',
411
+ codename: 'Simulated',
412
+ kernel: 'Simulated',
413
+ arch: cpuDetails.architecture === 'Apple Silicon' ? 'arm64' : 'x64',
414
+ hostname: 'simulated-host',
415
+ logofile: ''
416
+ },
417
+ timestamp: Date.now(),
418
+ summary: {
419
+ bestBackend: backend,
420
+ gpuModel: gpuDetails.model,
421
+ effectiveMemory: effectiveMemory,
422
+ systemRAM: totalRAM,
423
+ totalVRAM: vram
424
+ },
425
+ _displayName: displayName
426
+ };
427
+
428
+ hardware.cpu.capabilities = {};
429
+ if (cpuDetails.architecture === 'Apple Silicon') {
430
+ hardware.cpu.capabilities.neon = true;
431
+ } else {
432
+ hardware.cpu.capabilities.avx2 = true;
433
+ }
434
+
435
+ return hardware;
436
+ }
437
+
438
+ function getProfile(key) {
439
+ return HARDWARE_PROFILES[key] || null;
440
+ }
441
+
442
+ function getProfileKeys() {
443
+ return Object.keys(HARDWARE_PROFILES);
444
+ }
445
+
446
+ function getProfilesByCategory() {
447
+ const grouped = {};
448
+ for (const [key, profile] of Object.entries(HARDWARE_PROFILES)) {
449
+ const cat = profile.category;
450
+ if (!grouped[cat]) grouped[cat] = {};
451
+ grouped[cat][key] = profile;
452
+ }
453
+ return grouped;
454
+ }
455
+
456
+ function listProfiles() {
457
+ const lines = [];
458
+ const grouped = getProfilesByCategory();
459
+
460
+ for (const [category, profiles] of Object.entries(grouped)) {
461
+ const label = CATEGORY_LABELS[category] || category;
462
+ lines.push(`\n ${label}:`);
463
+ for (const [key, profile] of Object.entries(profiles)) {
464
+ const vramLabel = profile.gpu.unified
465
+ ? `${profile.memory.total}GB unified`
466
+ : (profile.gpu.vram > 0 ? `${profile.gpu.vram}GB VRAM` : 'No GPU');
467
+ const ramLabel = profile.gpu.unified ? '' : `, ${profile.memory.total}GB RAM`;
468
+ lines.push(` ${key.padEnd(14)} ${profile.displayName.padEnd(38)} ${vramLabel}${ramLabel}`);
469
+ }
470
+ }
471
+
472
+ return lines;
473
+ }
474
+
475
+ module.exports = {
476
+ HARDWARE_PROFILES,
477
+ buildFullHardwareObject,
478
+ buildCustomHardwareObject,
479
+ getProfile,
480
+ getProfileKeys,
481
+ getProfilesByCategory,
482
+ listProfiles,
483
+ CATEGORY_LABELS
484
+ };
package/src/index.js CHANGED
@@ -32,6 +32,21 @@ class LLMChecker {
32
32
  this.logger = getLogger().createChild('LLMChecker');
33
33
  this.verbose = options.verbose !== false; // Default to verbose unless explicitly disabled
34
34
  this.progress = null; // Will be initialized when needed
35
+ this._isSimulated = false;
36
+ }
37
+
38
+ setSimulatedHardware(hardwareObject) {
39
+ this.hardwareDetector.setSimulatedHardware(hardwareObject);
40
+ this._isSimulated = true;
41
+ }
42
+
43
+ clearSimulatedHardware() {
44
+ this.hardwareDetector.clearSimulatedHardware();
45
+ this._isSimulated = false;
46
+ }
47
+
48
+ get isSimulated() {
49
+ return this._isSimulated;
35
50
  }
36
51
 
37
52
  async analyze(options = {}) {
@@ -47,7 +62,10 @@ class LLMChecker {
47
62
 
48
63
  // Step 1: Hardware Detection
49
64
  if (this.progress) {
50
- this.progress.step('System Detection', 'Scanning hardware specifications...');
65
+ const detectionLabel = this._isSimulated
66
+ ? 'Using simulated hardware profile...'
67
+ : 'Scanning hardware specifications...';
68
+ this.progress.step('System Detection', detectionLabel);
51
69
  }
52
70
 
53
71
  const hardware = await this.hardwareDetector.getSystemInfo();
@@ -48,7 +48,9 @@ const MASCOT_MASK = [
48
48
 
49
49
  const DEFAULT_LOOP = true;
50
50
  const FRAMES_PER_SECOND = 14;
51
- const DEFAULT_BANNER_SOURCE = path.join(os.homedir(), 'Downloads', 'ascii-motion-cli.tsx');
51
+ // Security: do not auto-load executable-style banner sources from user-writable folders.
52
+ // External banner loading is opt-in via LLM_CHECKER_BANNER_SOURCE and supports JSON only.
53
+ const DEFAULT_BANNER_SOURCE = null;
52
54
  const DEFAULT_TEXT_BANNER_SOURCE = path.join(
53
55
  os.homedir(),
54
56
  'Desktop',
@@ -74,80 +76,28 @@ function fitLine(line, width) {
74
76
  return `${value.slice(0, width - 3)}...`;
75
77
  }
76
78
 
77
- function extractBalanced(source, startIndex, openChar, closeChar) {
78
- if (startIndex < 0 || source[startIndex] !== openChar) return null;
79
-
80
- let depth = 0;
81
- let inString = null;
82
- let escape = false;
83
-
84
- for (let index = startIndex; index < source.length; index += 1) {
85
- const char = source[index];
86
-
87
- if (inString) {
88
- if (escape) {
89
- escape = false;
90
- continue;
91
- }
92
-
93
- if (char === '\\') {
94
- escape = true;
95
- continue;
96
- }
97
-
98
- if (char === inString) {
99
- inString = null;
100
- }
101
-
102
- continue;
103
- }
104
-
105
- if (char === '"' || char === '\'' || char === '`') {
106
- inString = char;
107
- continue;
108
- }
109
-
110
- if (char === openChar) {
111
- depth += 1;
112
- } else if (char === closeChar) {
113
- depth -= 1;
114
- if (depth === 0) {
115
- return source.slice(startIndex, index + 1);
116
- }
117
- }
118
- }
119
-
120
- return null;
79
+ function isPlainObject(value) {
80
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
121
81
  }
122
82
 
123
- function extractAssignedLiteral(source, constName, openChar, closeChar) {
124
- const marker = `const ${constName}`;
125
- const markerIndex = source.indexOf(marker);
126
- if (markerIndex < 0) return null;
127
-
128
- const equalsIndex = source.indexOf('=', markerIndex);
129
- if (equalsIndex < 0) return null;
130
-
131
- const startIndex = source.indexOf(openChar, equalsIndex);
132
- if (startIndex < 0) return null;
133
-
134
- return extractBalanced(source, startIndex, openChar, closeChar);
135
- }
136
-
137
- function evaluateLiteral(literal) {
138
- if (!literal) return null;
83
+ function parseExternalBannerPayload(source) {
84
+ let parsed = null;
139
85
  try {
140
- return Function(`"use strict"; return (${literal});`)();
86
+ parsed = JSON.parse(source);
141
87
  } catch {
142
88
  return null;
143
89
  }
144
- }
145
90
 
146
- function parseNumericConstant(source, constName) {
147
- const match = source.match(new RegExp(`const\\s+${constName}\\s*=\\s*(\\d+(?:\\.\\d+)?)`));
148
- if (!match) return null;
149
- const parsed = Number.parseFloat(match[1]);
150
- return Number.isFinite(parsed) ? parsed : null;
91
+ if (!isPlainObject(parsed)) return null;
92
+ const frames = Array.isArray(parsed.frames) ? parsed.frames : null;
93
+ if (!frames || frames.length === 0) return null;
94
+
95
+ return {
96
+ frames,
97
+ themeDark: isPlainObject(parsed.themeDark) ? parsed.themeDark : {},
98
+ themeLight: isPlainObject(parsed.themeLight) ? parsed.themeLight : {},
99
+ canvasWidth: Number.isFinite(parsed.canvasWidth) ? parsed.canvasWidth : null
100
+ };
151
101
  }
152
102
 
153
103
  function getLongestFrameLine(frames) {
@@ -176,6 +126,18 @@ function normalizeExternalFrame(frame, contentWidth, defaultDuration) {
176
126
 
177
127
  function loadExternalBanner(sourceFile) {
178
128
  const filePath = sourceFile || process.env.LLM_CHECKER_BANNER_SOURCE || DEFAULT_BANNER_SOURCE;
129
+ if (!filePath) return null;
130
+
131
+ const extension = path.extname(filePath).toLowerCase();
132
+ if (extension !== '.json') {
133
+ cachedExternalBanner = {
134
+ filePath,
135
+ mtimeMs: -1,
136
+ payload: null
137
+ };
138
+ return null;
139
+ }
140
+
179
141
  let mtimeMs = -1;
180
142
 
181
143
  try {
@@ -200,16 +162,8 @@ function loadExternalBanner(sourceFile) {
200
162
 
201
163
  try {
202
164
  const source = fs.readFileSync(filePath, 'utf8');
203
- const framesLiteral = extractAssignedLiteral(source, 'FRAMES', '[', ']');
204
- const darkThemeLiteral = extractAssignedLiteral(source, 'THEME_DARK', '{', '}');
205
- const lightThemeLiteral = extractAssignedLiteral(source, 'THEME_LIGHT', '{', '}');
206
-
207
- const frames = evaluateLiteral(framesLiteral);
208
- const themeDark = evaluateLiteral(darkThemeLiteral);
209
- const themeLight = evaluateLiteral(lightThemeLiteral);
210
- const canvasWidth = parseNumericConstant(source, 'CANVAS_WIDTH');
211
-
212
- if (!Array.isArray(frames) || frames.length === 0) {
165
+ const payload = parseExternalBannerPayload(source);
166
+ if (!payload) {
213
167
  cachedExternalBanner = {
214
168
  filePath,
215
169
  mtimeMs,
@@ -218,13 +172,6 @@ function loadExternalBanner(sourceFile) {
218
172
  return null;
219
173
  }
220
174
 
221
- const payload = {
222
- frames,
223
- themeDark: themeDark && typeof themeDark === 'object' ? themeDark : {},
224
- themeLight: themeLight && typeof themeLight === 'object' ? themeLight : {},
225
- canvasWidth: Number.isFinite(canvasWidth) ? canvasWidth : null
226
- };
227
-
228
175
  cachedExternalBanner = {
229
176
  filePath,
230
177
  mtimeMs,
@@ -11,6 +11,7 @@ const PRIMARY_COMMAND_PRIORITY = [
11
11
  'help',
12
12
  'mcp-setup',
13
13
  'recommend',
14
+ 'simulate',
14
15
  'ai-run',
15
16
  'ollama-plan',
16
17
  'list-models',
@@ -1,17 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3464 | 10:03 PM | 🔵 | SQL Database Schema - Indexed Model Repository with Benchmarks | ~555 |
11
-
12
- ### Feb 14, 2026
13
-
14
- | ID | Time | T | Title | Read |
15
- |----|------|---|-------|------|
16
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
17
- </claude-mem-context>
@@ -1,18 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3490 | 10:24 PM | 🔵 | Hardware Detector Cache Implementation - 5-Minute TTL Without Force Refresh Option | ~536 |
11
- | #3440 | 9:58 PM | 🔵 | Hardware Detection System - Multi-GPU Support with Intelligent Selection | ~611 |
12
-
13
- ### Feb 14, 2026
14
-
15
- | ID | Time | T | Title | Read |
16
- |----|------|---|-------|------|
17
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
18
- </claude-mem-context>
@@ -1,17 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3453 | 10:01 PM | 🔵 | CUDA Detector Implementation - NVIDIA GPU Detection via nvidia-smi | ~497 |
11
-
12
- ### Feb 14, 2026
13
-
14
- | ID | Time | T | Title | Read |
15
- |----|------|---|-------|------|
16
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
17
- </claude-mem-context>
@@ -1,23 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3442 | 9:59 PM | 🔵 | Static Model Database Structure - Hardcoded LLM Specifications | ~572 |
11
-
12
- ### Feb 13, 2026
13
-
14
- | ID | Time | T | Title | Read |
15
- |----|------|---|-------|------|
16
- | #3699 | 12:05 AM | ✅ | Git Push Consolidated Architecture Changes to GitHub | ~367 |
17
-
18
- ### Feb 14, 2026
19
-
20
- | ID | Time | T | Title | Read |
21
- |----|------|---|-------|------|
22
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
23
- </claude-mem-context>
@@ -1,30 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3500 | 10:26 PM | 🔴 | pullModel() Stream Handling Improved - Success Validation Added | ~458 |
11
- | #3499 | " | 🔴 | Race Condition Fixed in Ollama Availability Cache | ~440 |
12
- | #3498 | 10:25 PM | 🔵 | testModelPerformance() Timeout Already Fixed | ~418 |
13
- | #3497 | " | 🔴 | Timeout Fixed in deleteModel() Using AbortController | ~391 |
14
- | #3496 | " | 🔴 | Timeout Fixed in testConnection() Using AbortController | ~395 |
15
- | #3495 | " | 🔴 | Fixed unbounded memory growth in native scraper HTTP request handler | ~361 |
16
- | #3493 | " | 🔴 | Fixed race condition in checkOllamaAvailability() with promise deduplication | ~398 |
17
- | #3491 | 10:24 PM | 🔴 | Added missing clearTimeout() in testModelPerformance() | ~319 |
18
- | #3489 | " | 🔴 | Fixed node-fetch timeout handling in testModelPerformance() | ~332 |
19
- | #3488 | " | 🔴 | Fixed node-fetch timeout handling in testConnection() tags check | ~303 |
20
- | #3486 | " | 🔴 | Fixed node-fetch timeout handling in getRunningModels() | ~308 |
21
- | #3484 | 10:23 PM | 🔵 | Ollama Client Timeout Implementation - Mixed Patterns with AbortController | ~554 |
22
- | #3443 | 9:59 PM | 🔵 | Ollama Native Scraper - Web Scraping with Dual Cache Strategy | ~594 |
23
- | #3437 | 9:58 PM | 🔵 | Ollama Client Implementation - HTTP API Wrapper with Connection Management | ~605 |
24
-
25
- ### Feb 14, 2026
26
-
27
- | ID | Time | T | Title | Read |
28
- |----|------|---|-------|------|
29
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
30
- </claude-mem-context>
@@ -1,17 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3462 | 10:02 PM | 🔵 | Plugin System Architecture - Hook-Based Extensibility Framework | ~648 |
11
-
12
- ### Feb 14, 2026
13
-
14
- | ID | Time | T | Title | Read |
15
- |----|------|---|-------|------|
16
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
17
- </claude-mem-context>
@@ -1,17 +0,0 @@
1
- <claude-mem-context>
2
- # Recent Activity
3
-
4
- <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
-
6
- ### Feb 12, 2026
7
-
8
- | ID | Time | T | Title | Read |
9
- |----|------|---|-------|------|
10
- | #3438 | 9:58 PM | 🔵 | Configuration Management System - Comprehensive Settings with Environment Overrides | ~580 |
11
-
12
- ### Feb 14, 2026
13
-
14
- | ID | Time | T | Title | Read |
15
- |----|------|---|-------|------|
16
- | #4339 | 6:49 PM | 🟣 | MCP server implementation and documentation added to llm-checker repository | ~457 |
17
- </claude-mem-context>