codemaxxing 0.3.1 → 0.4.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.
@@ -0,0 +1,137 @@
1
+ import type { HardwareInfo } from "./hardware.js";
2
+
3
+ export interface RecommendedModel {
4
+ name: string; // Display name
5
+ ollamaId: string; // Ollama model ID
6
+ size: number; // Download size in GB
7
+ ramRequired: number; // Minimum RAM in GB
8
+ vramOptimal: number; // Optimal VRAM in GB (0 = CPU fine)
9
+ description: string; // One-liner
10
+ speed: string; // e.g., "~45 tok/s on M1"
11
+ quality: "good" | "great" | "best";
12
+ }
13
+
14
+ export type ModelFit = "perfect" | "good" | "tight" | "skip";
15
+
16
+ export interface ScoredModel extends RecommendedModel {
17
+ fit: ModelFit;
18
+ }
19
+
20
+ const MODELS: RecommendedModel[] = [
21
+ {
22
+ name: "Qwen 2.5 Coder 3B",
23
+ ollamaId: "qwen2.5-coder:3b",
24
+ size: 2,
25
+ ramRequired: 8,
26
+ vramOptimal: 4,
27
+ description: "Lightweight, fast coding model",
28
+ speed: "~60 tok/s on M1",
29
+ quality: "good",
30
+ },
31
+ {
32
+ name: "Qwen 2.5 Coder 7B",
33
+ ollamaId: "qwen2.5-coder:7b",
34
+ size: 5,
35
+ ramRequired: 16,
36
+ vramOptimal: 8,
37
+ description: "Sweet spot for most machines",
38
+ speed: "~45 tok/s on M1",
39
+ quality: "great",
40
+ },
41
+ {
42
+ name: "Qwen 2.5 Coder 14B",
43
+ ollamaId: "qwen2.5-coder:14b",
44
+ size: 9,
45
+ ramRequired: 32,
46
+ vramOptimal: 16,
47
+ description: "High quality coding",
48
+ speed: "~25 tok/s on M1 Pro",
49
+ quality: "best",
50
+ },
51
+ {
52
+ name: "Qwen 2.5 Coder 32B",
53
+ ollamaId: "qwen2.5-coder:32b",
54
+ size: 20,
55
+ ramRequired: 48,
56
+ vramOptimal: 32,
57
+ description: "Premium quality, needs lots of RAM",
58
+ speed: "~12 tok/s on M1 Max",
59
+ quality: "best",
60
+ },
61
+ {
62
+ name: "DeepSeek Coder V2 16B",
63
+ ollamaId: "deepseek-coder-v2:16b",
64
+ size: 9,
65
+ ramRequired: 32,
66
+ vramOptimal: 16,
67
+ description: "Strong alternative for coding",
68
+ speed: "~30 tok/s on M1 Pro",
69
+ quality: "great",
70
+ },
71
+ {
72
+ name: "CodeLlama 7B",
73
+ ollamaId: "codellama:7b",
74
+ size: 4,
75
+ ramRequired: 16,
76
+ vramOptimal: 8,
77
+ description: "Meta's coding model",
78
+ speed: "~40 tok/s on M1",
79
+ quality: "good",
80
+ },
81
+ {
82
+ name: "StarCoder2 7B",
83
+ ollamaId: "starcoder2:7b",
84
+ size: 4,
85
+ ramRequired: 16,
86
+ vramOptimal: 8,
87
+ description: "Good for code completion",
88
+ speed: "~40 tok/s on M1",
89
+ quality: "good",
90
+ },
91
+ ];
92
+
93
+ function scoreModel(model: RecommendedModel, ramGB: number, vramGB: number): ModelFit {
94
+ if (ramGB < model.ramRequired) return "skip";
95
+
96
+ const ramHeadroom = ramGB - model.ramRequired;
97
+ const hasGoodVRAM = vramGB >= model.vramOptimal;
98
+
99
+ if (hasGoodVRAM && ramHeadroom >= 4) return "perfect";
100
+ if (hasGoodVRAM || ramHeadroom >= 8) return "good";
101
+ if (ramHeadroom >= 0) return "tight";
102
+ return "skip";
103
+ }
104
+
105
+ const qualityOrder: Record<string, number> = { best: 3, great: 2, good: 1 };
106
+ const fitOrder: Record<string, number> = { perfect: 4, good: 3, tight: 2, skip: 1 };
107
+
108
+ export function getRecommendations(hardware: HardwareInfo): ScoredModel[] {
109
+ const ramGB = hardware.ram / (1024 * 1024 * 1024);
110
+ const vramGB = hardware.gpu?.vram ? hardware.gpu.vram / (1024 * 1024 * 1024) : 0;
111
+
112
+ // Apple Silicon uses unified memory — VRAM = RAM
113
+ const effectiveVRAM = hardware.appleSilicon ? ramGB : vramGB;
114
+
115
+ const scored: ScoredModel[] = MODELS.map((m) => ({
116
+ ...m,
117
+ fit: scoreModel(m, ramGB, effectiveVRAM),
118
+ }));
119
+
120
+ // Sort: perfect first, then by quality descending
121
+ scored.sort((a, b) => {
122
+ const fitDiff = (fitOrder[b.fit] ?? 0) - (fitOrder[a.fit] ?? 0);
123
+ if (fitDiff !== 0) return fitDiff;
124
+ return (qualityOrder[b.quality] ?? 0) - (qualityOrder[a.quality] ?? 0);
125
+ });
126
+
127
+ return scored;
128
+ }
129
+
130
+ export function getFitIcon(fit: ModelFit): string {
131
+ switch (fit) {
132
+ case "perfect": return "\u2B50"; // ⭐
133
+ case "good": return "\u2705"; // ✅
134
+ case "tight": return "\u26A0\uFE0F"; // ⚠️
135
+ case "skip": return "\u274C"; // ❌
136
+ }
137
+ }
@@ -0,0 +1,137 @@
1
+ import { execSync, spawn } from "child_process";
2
+
3
+ /** Check if ollama binary exists on PATH */
4
+ export function isOllamaInstalled(): boolean {
5
+ try {
6
+ const cmd = process.platform === "win32" ? "where ollama" : "which ollama";
7
+ execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ /** Check if ollama server is responding */
15
+ export async function isOllamaRunning(): Promise<boolean> {
16
+ try {
17
+ const controller = new AbortController();
18
+ const timeout = setTimeout(() => controller.abort(), 2000);
19
+ const res = await fetch("http://localhost:11434/api/tags", { signal: controller.signal });
20
+ clearTimeout(timeout);
21
+ return res.ok;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /** Get the install command for the user's OS */
28
+ export function getOllamaInstallCommand(os: "macos" | "linux" | "windows"): string {
29
+ switch (os) {
30
+ case "macos": return "brew install ollama";
31
+ case "linux": return "curl -fsSL https://ollama.com/install.sh | sh";
32
+ case "windows": return "winget install Ollama.Ollama";
33
+ }
34
+ }
35
+
36
+ /** Start ollama serve in background */
37
+ export function startOllama(): void {
38
+ const child = spawn("ollama", ["serve"], {
39
+ detached: true,
40
+ stdio: "ignore",
41
+ });
42
+ child.unref();
43
+ }
44
+
45
+ export interface PullProgress {
46
+ status: string;
47
+ total?: number;
48
+ completed?: number;
49
+ percent: number;
50
+ }
51
+
52
+ /**
53
+ * Pull a model from Ollama registry.
54
+ * Calls onProgress with download updates.
55
+ * Returns a promise that resolves when complete.
56
+ */
57
+ export function pullModel(
58
+ modelId: string,
59
+ onProgress?: (progress: PullProgress) => void
60
+ ): Promise<void> {
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn("ollama", ["pull", modelId], {
63
+ stdio: ["pipe", "pipe", "pipe"],
64
+ });
65
+
66
+ let lastOutput = "";
67
+
68
+ const parseLine = (data: string) => {
69
+ lastOutput = data;
70
+ // Ollama pull output looks like:
71
+ // pulling manifest
72
+ // pulling abc123... 58% ▕██████████░░░░░░░░░░▏ 2.9 GB/5.0 GB
73
+ // verifying sha256 digest
74
+ // writing manifest
75
+ // success
76
+
77
+ // Try to parse percentage
78
+ const pctMatch = data.match(/(\d+)%/);
79
+ const sizeMatch = data.match(/([\d.]+)\s*GB\s*\/\s*([\d.]+)\s*GB/);
80
+
81
+ if (pctMatch) {
82
+ const percent = parseInt(pctMatch[1]);
83
+ let completed: number | undefined;
84
+ let total: number | undefined;
85
+ if (sizeMatch) {
86
+ completed = parseFloat(sizeMatch[1]) * 1024 * 1024 * 1024;
87
+ total = parseFloat(sizeMatch[2]) * 1024 * 1024 * 1024;
88
+ }
89
+ onProgress?.({ status: "downloading", total, completed, percent });
90
+ } else if (data.includes("pulling manifest")) {
91
+ onProgress?.({ status: "pulling manifest", percent: 0 });
92
+ } else if (data.includes("verifying")) {
93
+ onProgress?.({ status: "verifying", percent: 100 });
94
+ } else if (data.includes("writing manifest")) {
95
+ onProgress?.({ status: "writing manifest", percent: 100 });
96
+ } else if (data.includes("success")) {
97
+ onProgress?.({ status: "success", percent: 100 });
98
+ }
99
+ };
100
+
101
+ child.stdout?.on("data", (data: Buffer) => {
102
+ parseLine(data.toString().trim());
103
+ });
104
+
105
+ child.stderr?.on("data", (data: Buffer) => {
106
+ // Ollama writes progress to stderr
107
+ parseLine(data.toString().trim());
108
+ });
109
+
110
+ child.on("close", (code) => {
111
+ if (code === 0) {
112
+ resolve();
113
+ } else {
114
+ reject(new Error(`ollama pull failed (exit ${code}): ${lastOutput}`));
115
+ }
116
+ });
117
+
118
+ child.on("error", (err) => {
119
+ reject(new Error(`Failed to run ollama pull: ${err.message}`));
120
+ });
121
+ });
122
+ }
123
+
124
+ /** List models installed in Ollama */
125
+ export async function listInstalledModels(): Promise<string[]> {
126
+ try {
127
+ const controller = new AbortController();
128
+ const timeout = setTimeout(() => controller.abort(), 3000);
129
+ const res = await fetch("http://localhost:11434/api/tags", { signal: controller.signal });
130
+ clearTimeout(timeout);
131
+ if (res.ok) {
132
+ const data = (await res.json()) as { models?: Array<{ name: string }> };
133
+ return (data.models ?? []).map((m) => m.name);
134
+ }
135
+ } catch { /* not running */ }
136
+ return [];
137
+ }