llm-fit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # llm-fit
2
+
3
+ `llm-fit` is a production-ready, open-source CLI that scans your hardware and recommends local LLMs and coding-focused models that fit your machine.
4
+
5
+ It supports Windows first, and also works on macOS and Linux.
6
+
7
+ ## Features
8
+
9
+ - Hardware scan (RAM, CPU, GPU, VRAM, OS, WSL detection)
10
+ - Tiered recommendations: Recommended, Might Work, Not Recommended
11
+ - Curated model catalog with category filters
12
+ - Optional model installs through Ollama
13
+ - Sync of trending GGUF models from Hugging Face
14
+ - ESM-only Node.js codebase for modern ecosystem compatibility
15
+
16
+ ## Requirements
17
+
18
+ - Node.js 18+
19
+ - npm
20
+ - Ollama (optional, only needed for `install` command)
21
+
22
+ ## Installation
23
+
24
+ ### From source
25
+
26
+ ```bash
27
+ git clone https://github.com/your-username/llm-fit
28
+ cd llm-fit
29
+ npm install
30
+ ```
31
+
32
+ ### Link globally for development
33
+
34
+ ```bash
35
+ npm link
36
+ ```
37
+
38
+ ### Use it
39
+
40
+ ```bash
41
+ llm-fit scan
42
+ llm-fit recommend
43
+ llm-fit recommend --category coding
44
+ llm-fit models
45
+ llm-fit models --category general
46
+ llm-fit models --json
47
+ llm-fit install llama3.2:3b
48
+ llm-fit update-models
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ ### `llm-fit scan`
54
+
55
+ Scans your local machine and prints a hardware summary.
56
+
57
+ ### `llm-fit recommend [--category coding|general|all]`
58
+
59
+ Scans your machine and returns three recommendation tiers.
60
+
61
+ - `--category coding` shows coding models only
62
+ - `--category general` shows general-purpose models only
63
+ - `--category all` (default) shows all curated models
64
+
65
+ ### `llm-fit models [--category coding|general|all] [--json]`
66
+
67
+ Lists curated models in a table format or raw JSON.
68
+
69
+ ### `llm-fit install <modelName>`
70
+
71
+ Installs a model via Ollama.
72
+
73
+ - Accepts either local `name` or `ollama_tag`
74
+ - If a model is unknown to local DB, install is still attempted
75
+
76
+ ### `llm-fit update-models`
77
+
78
+ Fetches trending GGUF models from Hugging Face and appends only new entries without overwriting curated records.
79
+
80
+ ## Output Design
81
+
82
+ - Cyan section headers for scan/results sections
83
+ - Green/Yellow/Red tiers for recommendation status
84
+ - 80-column-friendly table and boxed output
85
+
86
+ ## Project Structure
87
+
88
+ ```text
89
+ llm-fit/
90
+ ├── index.js
91
+ ├── commands/
92
+ │ ├── scan.js
93
+ │ ├── recommend.js
94
+ │ ├── models.js
95
+ │ ├── install.js
96
+ │ └── update-models.js
97
+ ├── services/
98
+ │ ├── systemScanner.js
99
+ │ ├── recommender.js
100
+ │ └── huggingface.js
101
+ ├── data/
102
+ │ └── models.json
103
+ ├── utils/
104
+ │ └── formatter.js
105
+ ├── package.json
106
+ └── README.md
107
+ ```
108
+
109
+ ## Extensibility Notes
110
+
111
+ - `services/recommender.js` is intentionally stateless so the scoring function can be replaced by an AI-driven ranking service later.
112
+ - `data/models.json` is schema-version-ready by design; a top-level migration strategy can be introduced in a future release.
113
+ - `services/huggingface.js` uses only Node built-ins (`https`) so retry, caching, and offline fallback strategies can be added without external HTTP libraries.
114
+
115
+ ## Development
116
+
117
+ Run directly without global linking:
118
+
119
+ ```bash
120
+ node index.js --help
121
+ node index.js scan
122
+ node index.js recommend --category coding
123
+ ```
124
+
125
+ ## Contributing
126
+
127
+ 1. Fork the repository
128
+ 2. Create a feature branch
129
+ 3. Make changes with clear commit messages
130
+ 4. Open a pull request with before/after command output samples
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,116 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { execSync, spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const MODELS_PATH = path.join(__dirname, '..', 'data', 'models.json');
11
+
12
+ async function loadModels() {
13
+ const raw = await fs.readFile(MODELS_PATH, 'utf8');
14
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
15
+
16
+ if (!Array.isArray(parsed)) {
17
+ throw new Error('Model database is invalid: expected an array');
18
+ }
19
+
20
+ return parsed;
21
+ }
22
+
23
+ function resolveInstallTarget(models, modelNameInput) {
24
+ const normalized = modelNameInput.toLowerCase();
25
+ const found = models.find((model) => {
26
+ const byName = String(model.name || '').toLowerCase() === normalized;
27
+ const byTag = String(model.ollama_tag || '').toLowerCase() === normalized;
28
+ return byName || byTag;
29
+ });
30
+
31
+ if (!found) {
32
+ return {
33
+ model: null,
34
+ tag: modelNameInput
35
+ };
36
+ }
37
+
38
+ return {
39
+ model: found,
40
+ tag: found.ollama_tag || found.name
41
+ };
42
+ }
43
+
44
+ function ensureOllamaInstalled() {
45
+ const checkCmd = process.platform === 'win32' ? 'where ollama' : 'which ollama';
46
+
47
+ try {
48
+ execSync(checkCmd, { stdio: 'ignore' });
49
+ } catch {
50
+ throw new Error(
51
+ 'Ollama is not installed. Install it from https://ollama.com/download'
52
+ );
53
+ }
54
+ }
55
+
56
+ async function pullModelViaOllama(tag) {
57
+ const spinner = ora(`Preparing Ollama install for ${tag}...`).start();
58
+
59
+ return new Promise((resolve, reject) => {
60
+ try {
61
+ ensureOllamaInstalled();
62
+ spinner.text = `Installing ${tag} via Ollama...`;
63
+ } catch (error) {
64
+ spinner.fail('Ollama not found');
65
+ reject(error);
66
+ return;
67
+ }
68
+
69
+ const child = spawn('ollama', ['pull', tag], {
70
+ stdio: 'inherit'
71
+ });
72
+
73
+ child.on('close', (code) => {
74
+ if (code === 0) {
75
+ spinner.succeed(`Installed ${tag}`);
76
+ resolve();
77
+ return;
78
+ }
79
+
80
+ spinner.fail(`Install failed for ${tag}`);
81
+ reject(new Error(`Ollama exited with code ${code}`));
82
+ });
83
+
84
+ child.on('error', (error) => {
85
+ spinner.fail(`Install failed for ${tag}`);
86
+ reject(new Error(`Failed to launch Ollama: ${error.message}`));
87
+ });
88
+ });
89
+ }
90
+
91
+ export function registerInstallCommand(program) {
92
+ program
93
+ .command('install <modelName>')
94
+ .description('Install a model via Ollama')
95
+ .action(async (modelName) => {
96
+ try {
97
+ const models = await loadModels();
98
+ const resolved = resolveInstallTarget(models, modelName);
99
+
100
+ if (!resolved.model) {
101
+ console.log(
102
+ chalk.yellow(
103
+ `⚠️ Model '${modelName}' not found in local database. Attempting install anyway.`
104
+ )
105
+ );
106
+ }
107
+
108
+ await pullModelViaOllama(resolved.tag);
109
+ process.exit(0);
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : 'Unknown install error';
112
+ console.error(chalk.red(`❌ ${message}`));
113
+ process.exit(1);
114
+ }
115
+ });
116
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { formatModelsTable, formatSectionHeader } from '../utils/formatter.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const MODELS_PATH = path.join(__dirname, '..', 'data', 'models.json');
9
+
10
+ async function loadModels() {
11
+ const raw = await fs.readFile(MODELS_PATH, 'utf8');
12
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
13
+
14
+ if (!Array.isArray(parsed)) {
15
+ throw new Error('Model database is invalid: expected an array');
16
+ }
17
+
18
+ return parsed;
19
+ }
20
+
21
+ function filterByCategory(models, category) {
22
+ if (category === 'all') {
23
+ return models;
24
+ }
25
+
26
+ return models.filter((model) => model.category === category);
27
+ }
28
+
29
+ export function registerModelsCommand(program) {
30
+ program
31
+ .command('models')
32
+ .description('List all curated models')
33
+ .option('--category <category>', 'coding|general|all', 'all')
34
+ .option('--json', 'Output as raw JSON', false)
35
+ .action(async (options) => {
36
+ const category = String(options.category || 'all').toLowerCase();
37
+ if (!['coding', 'general', 'all'].includes(category)) {
38
+ console.error("error: invalid --category. Use 'coding', 'general', or 'all'.");
39
+ process.exit(1);
40
+ }
41
+
42
+ try {
43
+ const models = await loadModels();
44
+ const filtered = filterByCategory(models, category);
45
+
46
+ if (options.json) {
47
+ console.log(JSON.stringify(filtered, null, 2));
48
+ process.exit(0);
49
+ }
50
+
51
+ console.log(formatSectionHeader('Model Catalog'));
52
+ console.log(formatModelsTable(filtered));
53
+ process.exit(0);
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : 'Unknown models error';
56
+ console.error(`error: ${message}`);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ }
@@ -0,0 +1,62 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import ora from 'ora';
5
+ import { scanSystemWithScore } from './scan.js';
6
+ import { rankModelsForSystem } from '../services/recommender.js';
7
+ import {
8
+ formatRecommendationBuckets,
9
+ formatSectionHeader
10
+ } from '../utils/formatter.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const MODELS_PATH = path.join(__dirname, '..', 'data', 'models.json');
15
+
16
+ async function loadModels() {
17
+ const raw = await fs.readFile(MODELS_PATH, 'utf8');
18
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
19
+
20
+ if (!Array.isArray(parsed)) {
21
+ throw new Error('Model database is invalid: expected an array');
22
+ }
23
+
24
+ return parsed;
25
+ }
26
+
27
+ export function registerRecommendCommand(program) {
28
+ program
29
+ .command('recommend')
30
+ .description('Recommend models based on your hardware')
31
+ .option('--category <category>', 'coding|general|all', 'all')
32
+ .action(async (options) => {
33
+ const category = String(options.category || 'all').toLowerCase();
34
+ if (!['coding', 'general', 'all'].includes(category)) {
35
+ console.error("error: invalid --category. Use 'coding', 'general', or 'all'.");
36
+ process.exit(1);
37
+ }
38
+
39
+ const spinner = ora('Scanning your system hardware...').start();
40
+
41
+ try {
42
+ const [{ system, score }, models] = await Promise.all([
43
+ scanSystemWithScore(),
44
+ loadModels()
45
+ ]);
46
+
47
+ spinner.succeed('Hardware scan complete');
48
+
49
+ const buckets = rankModelsForSystem(system, models, category);
50
+ console.log(formatSectionHeader('Model Recommendations'));
51
+ console.log(`Readiness score: ${score.score}/100 (${score.grade} - ${score.label})`);
52
+ console.log(formatRecommendationBuckets(buckets));
53
+ process.exit(0);
54
+ } catch (error) {
55
+ spinner.fail('Recommendation failed');
56
+ const message =
57
+ error instanceof Error ? error.message : 'Unknown recommendation error';
58
+ console.error(`error: ${message}`);
59
+ process.exit(1);
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,97 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { scanSystemHardware } from '../services/systemScanner.js';
4
+ import { formatSectionHeader, formatSystemBox } from '../utils/formatter.js';
5
+ import { computeScore } from '../utils/scoreEngine.js';
6
+
7
+ const BAR_WIDTH = 30;
8
+
9
+ function getScoreColor(score) {
10
+ if (score >= 70) {
11
+ return chalk.green;
12
+ }
13
+ if (score >= 40) {
14
+ return chalk.yellow;
15
+ }
16
+ return chalk.red;
17
+ }
18
+
19
+ function toBar(value, max) {
20
+ const ratio = max <= 0 ? 0 : value / max;
21
+ const filledCount = Math.max(0, Math.min(BAR_WIDTH, Math.round(ratio * BAR_WIDTH)));
22
+ return `${'█'.repeat(filledCount)}${'░'.repeat(BAR_WIDTH - filledCount)}`;
23
+ }
24
+
25
+ function formatVerdictLines(verdict) {
26
+ const [firstSentence, ...rest] = String(verdict).split('. ').filter(Boolean);
27
+ if (rest.length === 0) {
28
+ return [firstSentence.endsWith('.') ? firstSentence : `${firstSentence}.`];
29
+ }
30
+
31
+ const first = firstSentence.endsWith('.') ? firstSentence : `${firstSentence}.`;
32
+ const second = rest.join('. ');
33
+ return [first, second.endsWith('.') ? second : `${second}.`];
34
+ }
35
+
36
+ function formatReadinessSection(scoreResult) {
37
+ const colorize = getScoreColor(scoreResult.score);
38
+ const headlineBar = toBar(scoreResult.score, 100);
39
+ const verdictLines = formatVerdictLines(scoreResult.verdict);
40
+ const lines = [
41
+ formatSectionHeader('LLM Readiness Score'),
42
+ '',
43
+ ` ${colorize(headlineBar)} ${scoreResult.score} / 100`,
44
+ '',
45
+ ` Grade : ${scoreResult.grade} - ${scoreResult.label}`,
46
+ ` Verdict : ${verdictLines[0]}`
47
+ ];
48
+
49
+ if (verdictLines[1]) {
50
+ lines.push(` ${verdictLines[1]}`);
51
+ }
52
+
53
+ lines.push('', ' Breakdown:');
54
+
55
+ const breakdownRows = [
56
+ ['RAM', scoreResult.breakdown.ram, 40],
57
+ ['CPU Cores', scoreResult.breakdown.cpu, 25],
58
+ ['GPU', scoreResult.breakdown.gpu, 25],
59
+ ['VRAM', scoreResult.breakdown.vram, 10]
60
+ ];
61
+
62
+ for (const [label, value, max] of breakdownRows) {
63
+ const paddedLabel = String(label).padEnd(11, ' ');
64
+ lines.push(` ${paddedLabel} ${toBar(value, max)} ${value} / ${max}`);
65
+ }
66
+
67
+ return lines.join('\n');
68
+ }
69
+
70
+ export async function scanSystemWithScore() {
71
+ const system = await scanSystemHardware();
72
+ const score = computeScore(system);
73
+ return { system, score };
74
+ }
75
+
76
+ export function registerScanCommand(program) {
77
+ program
78
+ .command('scan')
79
+ .description('Scan and print system hardware information')
80
+ .action(async () => {
81
+ const spinner = ora('Scanning your system hardware...').start();
82
+
83
+ try {
84
+ const { system, score } = await scanSystemWithScore();
85
+ spinner.succeed('Hardware scan complete');
86
+ console.log(formatSectionHeader('System Info'));
87
+ console.log(formatSystemBox(system));
88
+ console.log(formatReadinessSection(score));
89
+ process.exit(0);
90
+ } catch (error) {
91
+ spinner.fail('Hardware scan failed');
92
+ const message = error instanceof Error ? error.message : 'Unknown scan error';
93
+ console.error(`error: ${message}`);
94
+ process.exit(1);
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,52 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import ora from 'ora';
5
+ import {
6
+ fetchTrendingGgufModels,
7
+ mergeModelsWithLocalDb,
8
+ writeJsonAtomic
9
+ } from '../services/huggingface.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const MODELS_PATH = path.join(__dirname, '..', 'data', 'models.json');
14
+
15
+ async function readLocalModels() {
16
+ const raw = await fs.readFile(MODELS_PATH, 'utf8');
17
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
18
+
19
+ if (!Array.isArray(parsed)) {
20
+ throw new Error('Model database is invalid: expected an array');
21
+ }
22
+
23
+ return parsed;
24
+ }
25
+
26
+ export function registerUpdateModelsCommand(program) {
27
+ program
28
+ .command('update-models')
29
+ .description('Sync trending GGUF models from Hugging Face')
30
+ .action(async () => {
31
+ const spinner = ora('Syncing models from Hugging Face...').start();
32
+
33
+ try {
34
+ const localModels = await readLocalModels();
35
+ const fetched = await fetchTrendingGgufModels();
36
+ const { merged, addedCount } = mergeModelsWithLocalDb(localModels, fetched);
37
+
38
+ await writeJsonAtomic(MODELS_PATH, merged);
39
+
40
+ spinner.succeed('Model database updated');
41
+ console.log(
42
+ `Added ${addedCount} new models. Database now has ${merged.length} models.`
43
+ );
44
+ process.exit(0);
45
+ } catch (error) {
46
+ spinner.fail('Model sync failed');
47
+ const message = error instanceof Error ? error.message : 'Unknown sync error';
48
+ console.error(`error: ${message}`);
49
+ process.exit(1);
50
+ }
51
+ });
52
+ }