1+ import { execSync , spawn } from "node:child_process" ;
2+ import readline from "node:readline" ;
3+ import process from "node:process" ;
4+ import { loadConfig , saveConfig } from "./config.js" ;
5+
6+ const MODELS = [
7+ { id : "gemma" , name : "Gemma (Standard, Great balance of speed and intelligence)" } ,
8+ { id : "gemma:2b" , name : "Gemma 2B (Fastest, Good for Mobile/Laptops)" } ,
9+ { id : "llama3:8b" , name : "Llama 3 8B (Smarter, Needs 8GB+ RAM)" } ,
10+ { id : "phi3:mini" , name : "Phi 3 Mini (Great reasoning, Lightweight)" }
11+ ] ;
12+
13+ export async function setupOllama ( ) {
14+ console . log ( "Checking AI engine prerequisites..." ) ;
15+
16+ if ( ! isOllamaInstalled ( ) ) {
17+ console . log ( "Ollama is not installed. Installing Ollama..." ) ;
18+ installOllama ( ) ;
19+ }
20+
21+ await ensureOllamaRunning ( ) ;
22+
23+ const installedModels = await getInstalledModels ( ) ;
24+ const { config, configPath } = await loadConfig ( ) ;
25+
26+ let selectedModel = config . ai ?. model ;
27+
28+ if ( ! selectedModel ) {
29+ selectedModel = await promptModelSelection ( installedModels ) ;
30+ config . ai = { ...config . ai , model : selectedModel } ;
31+ await saveConfig ( config , configPath ) ;
32+ }
33+
34+ // Resolve to actual installed model if it's a close match to avoid re-downloading
35+ const exactMatch = installedModels . find ( m => m === selectedModel || m === `${ selectedModel } :latest` ) ;
36+ const partialMatch = installedModels . find ( m => m . includes ( selectedModel ) ) ;
37+
38+ if ( exactMatch || partialMatch ) {
39+ const finalModel = exactMatch || partialMatch ;
40+ if ( finalModel !== selectedModel ) {
41+ console . log ( `Auto-resolved '${ selectedModel } ' to installed model '${ finalModel } '.` ) ;
42+ selectedModel = finalModel ;
43+ config . ai . model = finalModel ;
44+ await saveConfig ( config , configPath ) ;
45+ }
46+ } else {
47+ console . log ( `Model '${ selectedModel } ' not found locally. Downloading...` ) ;
48+ pullModel ( selectedModel ) ;
49+ }
50+
51+ console . log ( `AI engine ready using model: ${ selectedModel } ` ) ;
52+ return selectedModel ;
53+ }
54+
55+ async function getInstalledModels ( ) {
56+ try {
57+ const response = await fetch ( "http://localhost:11434/api/tags" ) ;
58+ if ( response . ok ) {
59+ const data = await response . json ( ) ;
60+ return data . models . map ( m => m . name ) ;
61+ }
62+ } catch { }
63+ return [ ] ;
64+ }
65+
66+ function isOllamaInstalled ( ) {
67+ try {
68+ execSync ( "ollama --version" , { stdio : "ignore" } ) ;
69+ return true ;
70+ } catch {
71+ return false ;
72+ }
73+ }
74+
75+ function installOllama ( ) {
76+ try {
77+ if ( process . platform === "win32" ) {
78+ execSync ( "winget install Ollama.Ollama -e --silent" , { stdio : "inherit" } ) ;
79+ } else if ( process . platform === "darwin" ) {
80+ execSync ( "brew install ollama" , { stdio : "inherit" } ) ;
81+ } else {
82+ execSync ( "curl -fsSL https://ollama.com/install.sh | sh" , { stdio : "inherit" } ) ;
83+ }
84+ } catch ( error ) {
85+ console . error ( "Failed to automatically install Ollama. Please install it manually from https://ollama.com" ) ;
86+ process . exit ( 1 ) ;
87+ }
88+ }
89+
90+ async function ensureOllamaRunning ( ) {
91+ try {
92+ await fetch ( "http://localhost:11434/api/tags" ) ;
93+ } catch {
94+ console . log ( "Starting Ollama background service..." ) ;
95+ const subprocess = spawn ( "ollama" , [ "serve" ] , {
96+ detached : true ,
97+ stdio : "ignore" ,
98+ windowsHide : true
99+ } ) ;
100+ subprocess . unref ( ) ;
101+
102+ // Wait for it to start
103+ for ( let i = 0 ; i < 10 ; i ++ ) {
104+ await new Promise ( r => setTimeout ( r , 1000 ) ) ;
105+ try {
106+ await fetch ( "http://localhost:11434/api/tags" ) ;
107+ return ;
108+ } catch { }
109+ }
110+ console . warn ( "Ollama service might not have started correctly, but we will continue." ) ;
111+ }
112+ }
113+
114+ async function promptModelSelection ( ) {
115+ const rl = readline . createInterface ( {
116+ input : process . stdin ,
117+ output : process . stdout
118+ } ) ;
119+
120+ console . log ( "\n=========================================" ) ;
121+ console . log ( " Select an AI Model for Nyx " ) ;
122+ console . log ( "=========================================" ) ;
123+ MODELS . forEach ( ( m , i ) => {
124+ console . log ( ` ${ i + 1 } ) ${ m . name } (${ m . id } )` ) ;
125+ } ) ;
126+ console . log ( "=========================================\n" ) ;
127+
128+ return new Promise ( ( resolve ) => {
129+ const ask = ( ) => {
130+ rl . question ( "Enter the number of your choice (default 1): " , ( answer ) => {
131+ const choice = parseInt ( answer . trim ( ) , 10 ) ;
132+ if ( ! answer . trim ( ) || choice === 1 ) {
133+ rl . close ( ) ;
134+ resolve ( MODELS [ 0 ] . id ) ;
135+ } else if ( choice > 1 && choice <= MODELS . length ) {
136+ rl . close ( ) ;
137+ resolve ( MODELS [ choice - 1 ] . id ) ;
138+ } else {
139+ console . log ( "Invalid choice. Please try again." ) ;
140+ ask ( ) ;
141+ }
142+ } ) ;
143+ } ;
144+ ask ( ) ;
145+ } ) ;
146+ }
147+
148+ function pullModel ( model ) {
149+ try {
150+ execSync ( `ollama pull ${ model } ` , { stdio : "inherit" } ) ;
151+ } catch ( error ) {
152+ console . error ( `Failed to pull model ${ model } .` , error . message ) ;
153+ }
154+ }
0 commit comments