diff --git a/RESUME.bat b/RESUME.bat new file mode 100644 index 0000000..7ffdb4c --- /dev/null +++ b/RESUME.bat @@ -0,0 +1,244 @@ +@echo off & setlocal EnableDelayedExpansion +chcp 65001 >nul +title OpenClaude - Resume Last Session + +set "ENGINE_DIR=%~dp0engine\" +set "USB_ROOT=%ENGINE_DIR%..\" +set "DATA_DIR=%USB_ROOT%data" +set "ENV_FILE=%DATA_DIR%\ai_settings.env" +set "NODE_DIR=%ENGINE_DIR%\node-win-x64" +set "GIT_DIR=%ENGINE_DIR%\git-win-x64" +set "GIT_BASH=%GIT_DIR%\bin\bash.exe" +set "GIT_EXE=%GIT_DIR%\bin\git.exe" +set "OC_BIN=%ENGINE_DIR%\node_modules\@gitlawb\openclaude\bin\openclaude" +set "CLAUDE_CONFIG_DIR=%DATA_DIR%\openclaude" +set "PORTABLE_HOME=%DATA_DIR%\home" +set "XDG_CONFIG_HOME=%DATA_DIR%\config" +set "XDG_DATA_HOME=%DATA_DIR%\app_data" +set "XDG_CACHE_HOME=%DATA_DIR%\cache" +set "APPDATA=%DATA_DIR%\app_data" +set "LOCALAPPDATA=%DATA_DIR%\local_app_data" +set "HOME=%PORTABLE_HOME%" +set "USERPROFILE=%PORTABLE_HOME%" +set "PATH=%NODE_DIR%;%GIT_DIR%\cmd;%GIT_DIR%\bin;%GIT_DIR%\usr\bin;%PATH%" +set "CLAUDE_CODE_GIT_BASH_PATH=%GIT_BASH%" + +if not exist "%NODE_DIR%\node.exe" ( + echo [ERROR] Node.js was not found: %NODE_DIR%\node.exe + pause + exit /b 1 +) + +if not exist "%GIT_BASH%" ( + echo [ERROR] Git Bash was not found: %GIT_BASH% + pause + exit /b 1 +) + +if not exist "%GIT_EXE%" ( + echo [ERROR] Git executable was not found: %GIT_EXE% + pause + exit /b 1 +) + +if not exist "%OC_BIN%" ( + echo [ERROR] OpenClaude was not found: %OC_BIN% + pause + exit /b 1 +) + +if exist "%ENV_FILE%" ( + for /f "usebackq tokens=1,* delims==" %%A in ("%ENV_FILE%") do ( + set "%%A=%%~B" + ) +) + +if /i not "!AI_PROVIDER!"=="anthropic" ( + set "ANTHROPIC_API_KEY=" +) + +set "SESSION_ID=" +set "WORK_DIR=" +set "RESUME_DEFINED=" +set "CWD_DEFINED=" + +:PARSE_ARGS +if "%~1"=="" goto ARGS_DONE + +if /i "%~1"=="--resume" ( + if defined RESUME_DEFINED ( + echo [ERROR] Duplicate --resume parameter. + pause + exit /b 1 + ) + if "%~2"=="" ( + echo [ERROR] Missing session ID after --resume. + pause + exit /b 1 + ) + if /i "%~2:~0,2%"=="--" ( + echo [ERROR] Invalid session ID: %~2 + pause + exit /b 1 + ) + echo(%~2| findstr /r /c:"^[A-Za-z0-9._-][A-Za-z0-9._-]*$" >nul + if errorlevel 1 ( + echo [ERROR] Invalid session ID format: %~2 + pause + exit /b 1 + ) + set "SESSION_ID=%~2" + set "RESUME_DEFINED=1" + shift + shift + goto PARSE_ARGS +) + +if /i "%~1"=="--cwd" ( + if defined CWD_DEFINED ( + echo [ERROR] Duplicate --cwd parameter. + pause + exit /b 1 + ) + if "%~2"=="" ( + echo [ERROR] Missing working directory after --cwd. + pause + exit /b 1 + ) + if /i "%~2:~0,2%"=="--" ( + echo [ERROR] Invalid working directory: %~2 + pause + exit /b 1 + ) + set "WORK_DIR=%~2" + set "CWD_DEFINED=1" + shift + shift + goto PARSE_ARGS +) + +if not defined RESUME_DEFINED ( + if exist "%~1\" ( + if defined CWD_DEFINED ( + echo [ERROR] Duplicate working directory argument. + pause + exit /b 1 + ) + set "WORK_DIR=%~1" + set "CWD_DEFINED=1" + shift + goto PARSE_ARGS + ) + echo(%~1| findstr /r /c:"^[A-Za-z0-9._-][A-Za-z0-9._-]*$" >nul + if errorlevel 1 ( + echo [ERROR] Invalid positional session ID: %~1 + pause + exit /b 1 + ) + set "SESSION_ID=%~1" + set "RESUME_DEFINED=1" + shift + goto PARSE_ARGS +) + +if not defined CWD_DEFINED ( + if /i "%~1:~0,2%"=="--" ( + echo [ERROR] Unknown argument: %~1 + pause + exit /b 1 + ) + set "WORK_DIR=%~1" + set "CWD_DEFINED=1" + shift + goto PARSE_ARGS +) + +echo [ERROR] Unknown argument: %~1 +pause +exit /b 1 + +:ARGS_DONE +if not defined WORK_DIR ( + set "WORK_DIR=%ENGINE_DIR%" +) + +if "%WORK_DIR%"=="" ( + echo [ERROR] Working directory is empty. + pause + exit /b 1 +) + +if not exist "%WORK_DIR%\" ( + echo [ERROR] Working directory not found: %WORK_DIR% + pause + exit /b 1 +) + +set "PROVIDER_ARGS=" +if /i "!AI_PROVIDER!"=="anthropic" set "PROVIDER_ARGS=--provider anthropic" +if /i "!AI_PROVIDER!"=="gemini" set "PROVIDER_ARGS=--provider gemini" +if /i "!AI_PROVIDER!"=="ollama" set "PROVIDER_ARGS=--provider ollama" +if /i "!AI_PROVIDER!"=="nvidia" set "PROVIDER_ARGS=--provider nvidia-nim" +if /i "!AI_PROVIDER!"=="openai" ( + echo !OPENAI_BASE_URL! | findstr /c:"integrate.api.nvidia.com" >nul && set "PROVIDER_ARGS=--provider nvidia-nim" +) + +set "MODEL_ARGS=" +if defined OPENAI_MODEL set "MODEL_ARGS=--model !OPENAI_MODEL!" +if defined GEMINI_MODEL set "MODEL_ARGS=--model !GEMINI_MODEL!" +if defined ANTHROPIC_MODEL set "MODEL_ARGS=--model !ANTHROPIC_MODEL!" + +set "SETTINGS_ARGS=--setting-sources local" +set "CMD_ARGS=--dangerously-skip-permissions" + +if /i "!AI_PROVIDER!"=="ollama" ( + if exist "%DATA_DIR%\ollama\ollama.exe" ( + echo [~] Starting Local Ollama Server... + set "OLLAMA_MODELS=%DATA_DIR%\ollama\data" + start "Ollama Portable" /b /min "%DATA_DIR%\ollama\ollama.exe" serve >nul 2>&1 + timeout /t 3 /nobreak >nul + echo [OK] Ollama running! + if exist "%USB_ROOT%tools\local-proxy.js" ( + echo [~] Starting local speed proxy... + powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name = 'node.exe'\" | Where-Object { $_.CommandLine -like '*local-proxy.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" >nul 2>&1 + start "LocalProxy" /b /min "%NODE_DIR%\node.exe" "%USB_ROOT%tools\local-proxy.js" + timeout /t 2 /nobreak >nul + set "OPENAI_BASE_URL=http://localhost:11435/v1" + echo [OK] Speed proxy active. + ) + ) +) + +pushd "%WORK_DIR%" >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Failed to enter working directory: %WORK_DIR% + pause + exit /b 1 +) + +if defined SESSION_ID ( + call "%NODE_DIR%\node.exe" "%OC_BIN%" !SETTINGS_ARGS! !PROVIDER_ARGS! !MODEL_ARGS! !CMD_ARGS! --resume "%SESSION_ID%" + set "OC_STATUS=!ERRORLEVEL!" +) else ( + call "%NODE_DIR%\node.exe" "%OC_BIN%" !SETTINGS_ARGS! !PROVIDER_ARGS! !MODEL_ARGS! !CMD_ARGS! --continue + set "OC_STATUS=!ERRORLEVEL!" + if not "!OC_STATUS!"=="0" ( + echo [WARN] No conversation found to continue. Starting a new session... + call "%NODE_DIR%\node.exe" "%OC_BIN%" !SETTINGS_ARGS! !PROVIDER_ARGS! !MODEL_ARGS! !CMD_ARGS! + set "OC_STATUS=!ERRORLEVEL!" + ) +) + +popd + +if /i "!AI_PROVIDER!"=="ollama" ( + if exist "%DATA_DIR%\ollama\ollama.exe" ( + echo. + echo [~] Stopping Local Ollama Server... + taskkill /f /im ollama.exe >nul 2>&1 + powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name = 'node.exe'\" | Where-Object { $_.CommandLine -like '*local-proxy.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" >nul 2>&1 + ) +) + +pause +exit /b %OC_STATUS% diff --git a/tools/Open_Dashboard.bat b/tools/Open_Dashboard.bat index b593a69..325f021 100644 --- a/tools/Open_Dashboard.bat +++ b/tools/Open_Dashboard.bat @@ -1,103 +1,103 @@ -@echo off -setlocal enabledelayedexpansion -title Portable AI USB - Dashboard - -:: Define ANSI Colors using PowerShell -for /F %%a in ('powershell -NoProfile -Command "[char]27"') do set "ESC=%%a" -set "CYAN=%ESC%[36m" -set "GREEN=%ESC%[32m" -set "YELLOW=%ESC%[33m" -set "RED=%ESC%[31m" -set "DIM=%ESC%[90m" -set "R=%ESC%[0m" -set "BOLD=%ESC%[1m" - -set "USB_ROOT=%~dp0..\" -set "ENGINE_DIR=%USB_ROOT%engine" -set "NODE=%ENGINE_DIR%\node-win-x64\node.exe" -set "DASHBOARD=%USB_ROOT%dashboard\server.mjs" -set "DATA_DIR=%USB_ROOT%data" - -:: Portable data - keep everything on USB -set "CLAUDE_CONFIG_DIR=%DATA_DIR%\openclaude" -set "XDG_CONFIG_HOME=%DATA_DIR%\config" -set "XDG_DATA_HOME=%DATA_DIR%\app_data" -if not exist "%CLAUDE_CONFIG_DIR%" mkdir "%CLAUDE_CONFIG_DIR%" -if not exist "%XDG_CONFIG_HOME%" mkdir "%XDG_CONFIG_HOME%" -if not exist "%XDG_DATA_HOME%" mkdir "%XDG_DATA_HOME%" - -echo. -echo %CYAN%=========================================================%R% -echo %BOLD%Portable AI USB - Configuration Dashboard%R% -echo %CYAN%=========================================================%R% -echo. - -:: Check Node.js -if not exist "%NODE%" goto err_nonode - -:: Check dashboard file -if not exist "%DASHBOARD%" goto err_nodash - -:: Check if port 3000 is already in use -set "PORT_BUSY=0" -netstat -ano 2>nul | findstr ":3000 " | findstr "LISTENING" >nul 2>&1 -if not errorlevel 1 set "PORT_BUSY=1" - -if "%PORT_BUSY%"=="1" goto port_conflict - -echo %CYAN%[~] Starting dashboard server...%R% -echo %DIM%Dashboard will be available at %BOLD%http://localhost:3000%R% -echo. - -:: Open browser -start "" "http://localhost:3000" - -echo %GREEN%[OK] Browser opened!%R% -echo %DIM%Press Ctrl+C to stop the dashboard.%R% -echo. - -"%NODE%" "%DASHBOARD%" -pause -goto :eof - -:: --------------------------------------------------------- -:: ERROR HANDLERS -:: --------------------------------------------------------- -:err_nonode -echo %RED%[ERROR] Node.js not found.%R% -echo %YELLOW%Please run START.bat first.%R% -echo. -pause -goto :eof - -:err_nodash -echo %RED%[ERROR] Dashboard files not found!%R% -echo %YELLOW%Expected: %DASHBOARD%%R% -echo. -pause -goto :eof - -:port_conflict -echo %YELLOW%[WARNING] Port 3000 is already in use!%R% -echo. -echo %DIM%Another application is using port 3000.%R% -echo %DIM%The dashboard may already be running.%R% -echo. -echo %CYAN%1)%R% Open browser anyway (dashboard may already be running) -echo %CYAN%2)%R% Cancel -echo. -set "PORT_CHOICE=" -set /p "PORT_CHOICE= Select (1 or 2): " -if "%PORT_CHOICE%"=="1" ( - echo. - echo %CYAN%[~] Opening browser...%R% - start "" "http://localhost:3000" - echo %GREEN%[OK] Browser opened!%R% - echo. - pause - goto :eof -) -echo. -echo %DIM%Cancelled.%R% -pause -goto :eof +@echo off +setlocal enabledelayedexpansion +title Portable AI USB - Dashboard + +:: Define ANSI Colors using PowerShell +for /F %%a in ('powershell -NoProfile -Command "[char]27"') do set "ESC=%%a" +set "CYAN=%ESC%[36m" +set "GREEN=%ESC%[32m" +set "YELLOW=%ESC%[33m" +set "RED=%ESC%[31m" +set "DIM=%ESC%[90m" +set "R=%ESC%[0m" +set "BOLD=%ESC%[1m" + +set "USB_ROOT=%~dp0..\" +set "ENGINE_DIR=%USB_ROOT%engine" +set "NODE=%ENGINE_DIR%\node-win-x64\node.exe" +set "DASHBOARD=%USB_ROOT%dashboard\server.mjs" +set "DATA_DIR=%USB_ROOT%data" + +:: Portable data - keep everything on USB +set "CLAUDE_CONFIG_DIR=%DATA_DIR%\openclaude" +set "XDG_CONFIG_HOME=%DATA_DIR%\config" +set "XDG_DATA_HOME=%DATA_DIR%\app_data" +if not exist "%CLAUDE_CONFIG_DIR%" mkdir "%CLAUDE_CONFIG_DIR%" +if not exist "%XDG_CONFIG_HOME%" mkdir "%XDG_CONFIG_HOME%" +if not exist "%XDG_DATA_HOME%" mkdir "%XDG_DATA_HOME%" + +echo. +echo %CYAN%=========================================================%R% +echo %BOLD%Portable AI USB - Configuration Dashboard%R% +echo %CYAN%=========================================================%R% +echo. + +:: Check Node.js +if not exist "%NODE%" goto err_nonode + +:: Check dashboard file +if not exist "%DASHBOARD%" goto err_nodash + +:: Check if port 3000 is already in use +set "PORT_BUSY=0" +netstat -ano 2>nul | findstr ":3000 " | findstr "LISTENING" >nul 2>&1 +if not errorlevel 1 set "PORT_BUSY=1" + +if "%PORT_BUSY%"=="1" goto port_conflict + +echo %CYAN%[~] Starting dashboard server...%R% +echo %DIM%Dashboard will be available at %BOLD%http://localhost:3000%R% +echo. + +:: Open browser +start "" "http://localhost:3000" + +echo %GREEN%[OK] Browser opened!%R% +echo %DIM%Press Ctrl+C to stop the dashboard.%R% +echo. + +"%NODE%" "%DASHBOARD%" +pause +goto :eof + +:: --------------------------------------------------------- +:: ERROR HANDLERS +:: --------------------------------------------------------- +:err_nonode +echo %RED%[ERROR] Node.js not found.%R% +echo %YELLOW%Please run START.bat first.%R% +echo. +pause +goto :eof + +:err_nodash +echo %RED%[ERROR] Dashboard files not found!%R% +echo %YELLOW%Expected: %DASHBOARD%%R% +echo. +pause +goto :eof + +:port_conflict +echo %YELLOW%[WARNING] Port 3000 is already in use!%R% +echo. +echo %DIM%Another application is using port 3000.%R% +echo %DIM%The dashboard may already be running.%R% +echo. +echo %CYAN%1)%R% Open browser anyway (dashboard may already be running) +echo %CYAN%2)%R% Cancel +echo. +set "PORT_CHOICE=" +set /p "PORT_CHOICE= Select (1 or 2): " +if "%PORT_CHOICE%"=="1" ( + echo. + echo %CYAN%[~] Opening browser...%R% + start "" "http://localhost:3000" + echo %GREEN%[OK] Browser opened!%R% + echo. + pause + goto :eof +) +echo. +echo %DIM%Cancelled.%R% +pause +goto :eof diff --git a/tools/Setup_Local_Models.bat b/tools/Setup_Local_Models.bat index bcd9c60..86a23bf 100644 --- a/tools/Setup_Local_Models.bat +++ b/tools/Setup_Local_Models.bat @@ -1,12 +1,12 @@ -@echo off -title Portable AI USB - Local Model Setup -cls - -echo. -echo Running Local Model Setup... -echo. - -powershell -ExecutionPolicy Bypass -NoProfile -File "%~dp0setup_local_models.ps1" - -echo. -pause +@echo off +title Portable AI USB - Local Model Setup +cls + +echo. +echo Running Local Model Setup... +echo. + +powershell -ExecutionPolicy Bypass -NoProfile -File "%~dp0setup_local_models.ps1" + +echo. +pause diff --git a/tools/setup_local_models.ps1 b/tools/setup_local_models.ps1 index ce985fd..d501133 100644 --- a/tools/setup_local_models.ps1 +++ b/tools/setup_local_models.ps1 @@ -1,321 +1,321 @@ -$ErrorActionPreference = "Continue" -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$RootDir = (Resolve-Path "$ScriptDir\..").Path -$DataDir = "$RootDir\data" -$EnvFile = "$DataDir\ai_settings.env" -$ModelsDir = "$DataDir\models" -$OllamaDir = "$DataDir\ollama" - -$ModelCatalog = @( - # Category 1: Gemma 4 Family (Optimized GGUFs) - @{ Num=1; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E2B (Q4_K_M)"; Tag="https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf"; Size="3.1"; Input="Text"; Label="STANDARD"; Badge="BEST BALANCE" }, - @{ Num=2; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E2B (Q6_K)"; Tag="https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q6_K.gguf"; Size="4.5"; Input="Text"; Label="STANDARD"; Badge="STRONG CPU/GPU" }, - @{ Num=3; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E4B (Q4_K_M)"; Tag="https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf"; Size="5.0"; Input="Text"; Label="STANDARD"; Badge="MOST USERS" }, - - # Category 2: Qwen 3.5 & Ministral 3 - @{ Num=4; Category="Qwen 3.5 & Ministral 3 (Daily Drivers)"; Name="Qwen 3.5 (9B)"; Tag="qwen3.5:9b"; Size="6.6"; Input="Text, Image"; Label="STANDARD"; Badge="RECOMMENDED" }, - @{ Num=5; Category="Qwen 3.5 & Ministral 3 (Daily Drivers)"; Name="Ministral 3 (8B)"; Tag="ministral-3:8b"; Size="6.0"; Input="Text, Image"; Label="STANDARD"; Badge="DAILY" } -) - -function Get-USBFreeSpaceGB { - try { - $driveLetter = (Get-Item $ScriptDir).PSDrive.Name - $drive = Get-PSDrive $driveLetter -ErrorAction SilentlyContinue - if ($drive) { return [math]::Round($drive.Free / 1GB, 1) } - } catch {} - return -1 -} - -Write-Host "" -Write-Host "==========================================================" -ForegroundColor Cyan -Write-Host " PORTABLE AI USB - Local Model Setup (Official)" -ForegroundColor Cyan -Write-Host "==========================================================" -ForegroundColor Cyan -Write-Host "" - -$freeGB = Get-USBFreeSpaceGB -if ($freeGB -gt 0) { Write-Host " USB Free Space: $freeGB GB" -ForegroundColor DarkGray; Write-Host "" } - -Write-Host "[1/4] Choose your AI model(s):" -ForegroundColor Yellow - -$currentCategory = "" -foreach ($m in $ModelCatalog) { - if ($m.Category -ne $currentCategory) { - $currentCategory = $m.Category - Write-Host "`n --- $currentCategory ---" -ForegroundColor Cyan - } - - if ($m.Label -eq "UNCENSORED") { $labelColor = "Red"; $labelStr = " [UNCENSORED]" } - elseif ($m.Label -in @("UTILITY", "VISION", "POWERFUL")) { $labelColor = "DarkYellow"; $labelStr = " [$($m.Label)]" } - elseif ($m.Label -eq "CLOUD") { $labelColor = "Magenta"; $labelStr = " [CLOUD-API]" } - else { $labelColor = "DarkCyan"; $labelStr = " [STANDARD]" } - - $badgeStr = if ($m.Badge) { " - $($m.Badge)" } else { "" } - - $padNum = $m.Num.ToString().PadLeft(2) - Write-Host " [$padNum]" -ForegroundColor Yellow -NoNewline - Write-Host " $($m.Name.PadRight(24))" -ForegroundColor White -NoNewline - Write-Host ("[" + $m.Input + "]").PadRight(16) -ForegroundColor DarkCyan -NoNewline - - $sizeStr = if ($m.Size -eq "-") { " (-)".PadRight(12) } else { " (~$($m.Size) GB)".PadRight(12) } - Write-Host $sizeStr -ForegroundColor DarkGray -NoNewline - - Write-Host $labelStr -ForegroundColor $labelColor -NoNewline - Write-Host $badgeStr -ForegroundColor Magenta -} - -# --- Detect Already Downloaded Models (not in preset list) --- -$ManifestDir = "$OllamaDir\data\manifests\registry.ollama.ai\library" -$DlStartNum = 6 -$DlCount = 0 - -if (Test-Path $ManifestDir) { - $PresetSkipRegex = 'gemma-4-e2b-it-q4_k_m-local|gemma-4-e2b-it-q6_k-local|gemma-4-e4b-it-q4_k_m-local|qwen3.5|ministral-3' - $ModelDirs = Get-ChildItem -Path $ManifestDir -Directory -ErrorAction SilentlyContinue - - foreach ($dir in $ModelDirs) { - $modelBase = $dir.Name - $TagFiles = Get-ChildItem -Path $dir.FullName -File -ErrorAction SilentlyContinue - foreach ($file in $TagFiles) { - $tagName = $file.Name - $fullTag = if ($tagName -eq "latest") { $modelBase } else { "${modelBase}:${tagName}" } - - if ($fullTag -match $PresetSkipRegex) { continue } - - # Read JSON size simply (using regex to avoid full parsing overhead if invalid JSON) - $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue - if ($content -match '"size"\s*:\s*(\d+)') { - $sizeBytes = [long]$matches[1] - if ($sizeBytes -lt 100000000) { continue } # Skip < 100MB - - $sizeGB = [math]::Round($sizeBytes / 1GB, 1) - $Num = $DlStartNum + $DlCount - - # Append to ModelCatalog so existing selection logic works automatically - $ModelCatalog += @{ Num=$Num; Category="Already Downloaded"; Name=$fullTag; Tag=$fullTag; Size=$sizeGB.ToString(); Input="Text"; Label="DOWNLOADED"; Badge="" } - $DlCount++ - } - } - } -} - -if ($DlCount -gt 0) { - Write-Host "`n --- " -ForegroundColor Cyan -NoNewline - Write-Host "Already Downloaded" -ForegroundColor Green -NoNewline - Write-Host " ---" -ForegroundColor Cyan - - $dlModels = $ModelCatalog | Where-Object { $_.Category -eq "Already Downloaded" } - foreach ($m in $dlModels) { - $padNum = $m.Num.ToString().PadLeft(2) - Write-Host " [$padNum]" -ForegroundColor Yellow -NoNewline - Write-Host " $($m.Name.PadRight(24))" -ForegroundColor White -NoNewline - Write-Host ("[" + $m.Input + "]").PadRight(16) -ForegroundColor DarkCyan -NoNewline - Write-Host " (~$($m.Size) GB)".PadRight(12) -ForegroundColor DarkGray -NoNewline - Write-Host " [DOWNLOADED]" -ForegroundColor Green - } -} - -Write-Host "`n [C] CUSTOM - Enter an Official Ollama Tag" -ForegroundColor Green -Write-Host " Browse ALL models here: " -ForegroundColor Gray -NoNewline -Write-Host "https://ollama.com/library" -ForegroundColor Blue -Write-Host "`n ------------------------------------------------" -ForegroundColor DarkGray -Write-Host " Enter number(s) separated by commas (e.g. 1,4)" -ForegroundColor Gray -Write-Host " Type 'all' for every preset model, 'c' for custom`n" -ForegroundColor Gray - -$UserChoice = Read-Host " Your choice" -if ([string]::IsNullOrWhiteSpace($UserChoice)) { - Write-Host "`n No input! Defaulting to [3] Gemma 4 E4B..." -ForegroundColor Yellow - $UserChoice = "3" -} - -$SelectedModels = @() -$HasCustom = $false - -if ($UserChoice.Trim().ToLower() -eq "all") { $SelectedModels = @($ModelCatalog) } -else { - foreach ($t in ($UserChoice -split "," | ForEach-Object { $_.Trim().ToLower() })) { - if ($t -eq "c" -or $t -eq "custom") { $HasCustom = $true } - elseif ($t -match '^\d+$') { - $num = [int]$t - $found = $ModelCatalog | Where-Object { $_.Num -eq $num } - if ($found -and -Not ($SelectedModels | Where-Object { $_.Num -eq $num })) { $SelectedModels += $found } - } - } -} - -if ($HasCustom) { - Write-Host "`n ---- Custom Model Setup ----" -ForegroundColor Green - $customTag = Read-Host " Ollama Tag (e.g. mistral-nemo, phi3)" - if ($customTag) { - $customName = (CultureInfo.CurrentCulture.TextInfo.ToTitleCase($customTag.ToLower())) - $SelectedModels += @{ Num=99; Name="Custom: $customName"; Tag=$customTag.Trim(); Size="?"; Label="CUSTOM" } - Write-Host " Custom model added!" -ForegroundColor Green - } -} - -if ($SelectedModels.Count -eq 0) { Write-Host "`n ERROR: No models selected!" -ForegroundColor Red; exit 1 } - -$totalSizeGB = 0 -foreach ($m in $SelectedModels) { - if ($m.Size -match '\d') { $totalSizeGB += [double]$m.Size } -} - -if ($totalSizeGB -ge ($freeGB - 1) -and $freeGB -gt 0 -or $UserChoice.Trim().ToLower() -eq "all") { - Write-Host "`n WARNING: These models total ~$([math]::Ceiling($totalSizeGB)) GB. USB drive has $freeGB GB free!" -ForegroundColor Red - $confirm = Read-Host " Continue? (yes/no)" - if ($confirm.Trim().ToLower() -ne "yes" -and $confirm.Trim().ToLower() -ne "y") { exit } -} - -# Directories -New-Item -ItemType Directory -Force -Path $ModelsDir | Out-Null -New-Item -ItemType Directory -Force -Path "$OllamaDir\data" | Out-Null -Write-Host "`n[2/4] Created storage folders." -ForegroundColor Green - -# Ollama Engine Setup -Write-Host "`n[3/4] Setting up Portable Ollama Engine..." -ForegroundColor Yellow -$OllamaURL = "https://github.com/ollama/ollama/releases/latest/download/ollama-windows-amd64.zip" -$OllamaDest = "$OllamaDir\ollama.zip" -$OllamaExe = "$OllamaDir\ollama.exe" - -if (Test-Path $OllamaExe) { - Write-Host " Engine already installed!" -ForegroundColor Green -} else { - Write-Host " Downloading Ollama Engine (~100MB)..." -ForegroundColor Yellow - curl.exe -L --ssl-no-revoke $OllamaURL -o $OllamaDest - if (Test-Path $OllamaDest) { - Write-Host " Extracting to USB..." -ForegroundColor Yellow - Expand-Archive -Path $OllamaDest -DestinationPath $OllamaDir -Force - Remove-Item $OllamaDest -Force -ErrorAction SilentlyContinue - Write-Host " Engine Installed successfully!" -ForegroundColor Green - } else { - Write-Host " ERROR: Failed to download engine!" -ForegroundColor Red - exit 1 - } -} - -# Downloading Models via Ollama -Write-Host "`n[4/4] Pulling Models (This guarantees perfectly configured Tool Support)..." -ForegroundColor Yellow - -$downloadErrors = @() - -$env:OLLAMA_MODELS = "$OllamaDir\data" -Write-Host "`n Starting background Ollama server on USB..." -ForegroundColor DarkGray -$ServerProcess = Start-Process -FilePath $OllamaExe -ArgumentList "serve" -WindowStyle Hidden -PassThru -Start-Sleep -Seconds 5 - -$idx = 1 -foreach ($m in $SelectedModels) { - if ($m.Tag -match "^http.*" -and $m.Tag -match "\.gguf") { - $tagUrlNoQuery = $m.Tag.Split('?')[0] - $fileName = $tagUrlNoQuery.Split('/')[-1] - if (-not $fileName.EndsWith(".gguf")) { $fileName += ".gguf" } - $dest = "$ModelsDir\$fileName" - - $baseName = $fileName -ireplace '\.gguf$', '' - $modelNameLocal = "$baseName-local".ToLower() -replace '[^a-z0-9_-]', '-' - - # --- Always verify real file size against server --- - Write-Host "`n ($idx/$($SelectedModels.Count)) Checking $($m.Name)..." -ForegroundColor Yellow - $idx++ - - $expectedSize = 0 - $existingSize = 0 - try { - $headResponse = Invoke-WebRequest -Uri $m.Tag -Method Head -UseBasicParsing -ErrorAction Stop - $expectedSize = [long]$headResponse.Headers['Content-Length'] - } catch {} - if (Test-Path $dest) { $existingSize = (Get-Item $dest).Length } - - $fileComplete = ($expectedSize -gt 0 -and $existingSize -ge $expectedSize) - - # Only skip if file is FULLY downloaded AND ollama has it imported - $showResult = & $OllamaExe show $modelNameLocal 2>&1 - if ($fileComplete -and $LASTEXITCODE -eq 0) { - $existMegabytes = [math]::Round($existingSize / 1MB) - Write-Host (" $([char]0x2705) {0} fully downloaded ({1} MB) & imported - skipping!" -f $m.Name, $existMegabytes) -ForegroundColor Green - $m.Tag = $modelNameLocal - continue - } - - Write-Host " Do not close this window! Download may take a while." -ForegroundColor Magenta - - try { - # --- Download or resume --- - if ((Test-Path $dest) -and -not $fileComplete) { - $existMegabytes = [math]::Round($existingSize / 1MB) - $expectMegabytes = [math]::Round($expectedSize / 1MB) - Write-Host (" {0} is incomplete ({1} MB / {2} MB). Resuming..." -f $fileName, $existMegabytes, $expectMegabytes) -ForegroundColor Yellow - curl.exe -L -C - $($m.Tag) -o $dest - } elseif (-not $fileComplete) { - Write-Host " Downloading $fileName (speed + ETA shown below)..." -ForegroundColor Cyan - curl.exe -L -C - $($m.Tag) -o $dest - } - - $modelFileContent = "FROM ./$fileName`nPARAMETER temperature 0.7`nPARAMETER top_p 0.9" - $modelFilePath = "$ModelsDir\Modelfile-$modelNameLocal" - Set-Content -Path $modelFilePath -Value $modelFileContent -Encoding Ascii - - Write-Host " Importing into Ollama as '$modelNameLocal'..." -ForegroundColor Cyan - Push-Location $ModelsDir - $createArgs = "create $modelNameLocal -f Modelfile-$modelNameLocal" - $createProcess = Start-Process -FilePath $OllamaExe -ArgumentList $createArgs -Wait -NoNewWindow -PassThru - Pop-Location - - if ($createProcess.ExitCode -eq 0) { - Write-Host " Import complete!" -ForegroundColor Green - $m.Tag = $modelNameLocal - } else { - throw "Exit code $($createProcess.ExitCode)" - } - } catch { - Write-Host " FAILED to import custom model: $fileName" -ForegroundColor Red - $downloadErrors += $m.Name - } - continue - } - - # --- Check if standard Ollama model already exists --- - $showResult = & $OllamaExe show $($m.Tag) 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "`n ($idx/$($SelectedModels.Count)) " -ForegroundColor Green -NoNewline - Write-Host ("$([char]0x2705) {0} [{1}] already pulled - skipping!" -f $m.Name, $m.Tag) -ForegroundColor Green - $idx++ - continue - } - - Write-Host "`n ($idx/$($SelectedModels.Count)) Pulling $($m.Name) [$($m.Tag)]..." -ForegroundColor Yellow - Write-Host " Do not close this window! Download may take a while depending on bandwidth." -ForegroundColor Magenta - $idx++ - - try { - $pullArgs = "pull $($m.Tag)" - $pullProcess = Start-Process -FilePath $OllamaExe -ArgumentList $pullArgs -Wait -NoNewWindow -PassThru - if ($pullProcess.ExitCode -ne 0) { throw "Exit code $($pullProcess.ExitCode)" } - Write-Host " Pull complete!" -ForegroundColor Green - } catch { - Write-Host " FAILED to pull model: $($m.Tag)" -ForegroundColor Red - $downloadErrors += $m.Name - } -} - -Write-Host "`n Stopping background Ollama server..." -ForegroundColor DarkGray -Stop-Process -Id $ServerProcess.Id -Force -ErrorAction SilentlyContinue - -# Record Models for the Dashboard -$installedList = $SelectedModels | ForEach-Object { "$($_.Tag)|$($_.Name)|$($_.Label)" } -if (Test-Path "$ModelsDir\installed-models.txt") { - $existing = Get-Content "$ModelsDir\installed-models.txt" - $installedList = ($existing + $installedList) | Select-Object -Unique -} -Set-Content -Path "$ModelsDir\installed-models.txt" -Value ($installedList -join "`n") -Force -Encoding UTF8 - -Write-Host "`n[5/5] Finalizing Configurations..." -ForegroundColor Yellow - -$firstModelTag = $SelectedModels[0].Tag -$configContent = "AI_PROVIDER=ollama`nCLAUDE_CODE_USE_OPENAI=1`nOPENAI_API_KEY=ollama`nOPENAI_BASE_URL=http://localhost:11434/v1`nOPENAI_MODEL=$firstModelTag`nAI_DISPLAY_MODEL=$firstModelTag" -Set-Content -Path $EnvFile -Value $configContent -Force -Encoding Ascii -Write-Host " Default Model set to: $firstModelTag" -ForegroundColor Green - -Write-Host "`n==========================================================" -ForegroundColor Cyan -if ($downloadErrors.Count -gt 0) { Write-Host " SETUP COMPLETE (with some download errors)" -ForegroundColor Yellow } -else { Write-Host " SETUP COMPLETE! LOCAL AI AGENTS ARE READY!" -ForegroundColor Green } -Write-Host "==========================================================" -ForegroundColor Cyan +$ErrorActionPreference = "Continue" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RootDir = (Resolve-Path "$ScriptDir\..").Path +$DataDir = "$RootDir\data" +$EnvFile = "$DataDir\ai_settings.env" +$ModelsDir = "$DataDir\models" +$OllamaDir = "$DataDir\ollama" + +$ModelCatalog = @( + # Category 1: Gemma 4 Family (Optimized GGUFs) + @{ Num=1; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E2B (Q4_K_M)"; Tag="https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf"; Size="3.1"; Input="Text"; Label="STANDARD"; Badge="BEST BALANCE" }, + @{ Num=2; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E2B (Q6_K)"; Tag="https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q6_K.gguf"; Size="4.5"; Input="Text"; Label="STANDARD"; Badge="STRONG CPU/GPU" }, + @{ Num=3; Category="Gemma 4 Family (Optimized GGUFs)"; Name="Gemma 4 E4B (Q4_K_M)"; Tag="https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf"; Size="5.0"; Input="Text"; Label="STANDARD"; Badge="MOST USERS" }, + + # Category 2: Qwen 3.5 & Ministral 3 + @{ Num=4; Category="Qwen 3.5 & Ministral 3 (Daily Drivers)"; Name="Qwen 3.5 (9B)"; Tag="qwen3.5:9b"; Size="6.6"; Input="Text, Image"; Label="STANDARD"; Badge="RECOMMENDED" }, + @{ Num=5; Category="Qwen 3.5 & Ministral 3 (Daily Drivers)"; Name="Ministral 3 (8B)"; Tag="ministral-3:8b"; Size="6.0"; Input="Text, Image"; Label="STANDARD"; Badge="DAILY" } +) + +function Get-USBFreeSpaceGB { + try { + $driveLetter = (Get-Item $ScriptDir).PSDrive.Name + $drive = Get-PSDrive $driveLetter -ErrorAction SilentlyContinue + if ($drive) { return [math]::Round($drive.Free / 1GB, 1) } + } catch {} + return -1 +} + +Write-Host "" +Write-Host "==========================================================" -ForegroundColor Cyan +Write-Host " PORTABLE AI USB - Local Model Setup (Official)" -ForegroundColor Cyan +Write-Host "==========================================================" -ForegroundColor Cyan +Write-Host "" + +$freeGB = Get-USBFreeSpaceGB +if ($freeGB -gt 0) { Write-Host " USB Free Space: $freeGB GB" -ForegroundColor DarkGray; Write-Host "" } + +Write-Host "[1/4] Choose your AI model(s):" -ForegroundColor Yellow + +$currentCategory = "" +foreach ($m in $ModelCatalog) { + if ($m.Category -ne $currentCategory) { + $currentCategory = $m.Category + Write-Host "`n --- $currentCategory ---" -ForegroundColor Cyan + } + + if ($m.Label -eq "UNCENSORED") { $labelColor = "Red"; $labelStr = " [UNCENSORED]" } + elseif ($m.Label -in @("UTILITY", "VISION", "POWERFUL")) { $labelColor = "DarkYellow"; $labelStr = " [$($m.Label)]" } + elseif ($m.Label -eq "CLOUD") { $labelColor = "Magenta"; $labelStr = " [CLOUD-API]" } + else { $labelColor = "DarkCyan"; $labelStr = " [STANDARD]" } + + $badgeStr = if ($m.Badge) { " - $($m.Badge)" } else { "" } + + $padNum = $m.Num.ToString().PadLeft(2) + Write-Host " [$padNum]" -ForegroundColor Yellow -NoNewline + Write-Host " $($m.Name.PadRight(24))" -ForegroundColor White -NoNewline + Write-Host ("[" + $m.Input + "]").PadRight(16) -ForegroundColor DarkCyan -NoNewline + + $sizeStr = if ($m.Size -eq "-") { " (-)".PadRight(12) } else { " (~$($m.Size) GB)".PadRight(12) } + Write-Host $sizeStr -ForegroundColor DarkGray -NoNewline + + Write-Host $labelStr -ForegroundColor $labelColor -NoNewline + Write-Host $badgeStr -ForegroundColor Magenta +} + +# --- Detect Already Downloaded Models (not in preset list) --- +$ManifestDir = "$OllamaDir\data\manifests\registry.ollama.ai\library" +$DlStartNum = 6 +$DlCount = 0 + +if (Test-Path $ManifestDir) { + $PresetSkipRegex = 'gemma-4-e2b-it-q4_k_m-local|gemma-4-e2b-it-q6_k-local|gemma-4-e4b-it-q4_k_m-local|qwen3.5|ministral-3' + $ModelDirs = Get-ChildItem -Path $ManifestDir -Directory -ErrorAction SilentlyContinue + + foreach ($dir in $ModelDirs) { + $modelBase = $dir.Name + $TagFiles = Get-ChildItem -Path $dir.FullName -File -ErrorAction SilentlyContinue + foreach ($file in $TagFiles) { + $tagName = $file.Name + $fullTag = if ($tagName -eq "latest") { $modelBase } else { "${modelBase}:${tagName}" } + + if ($fullTag -match $PresetSkipRegex) { continue } + + # Read JSON size simply (using regex to avoid full parsing overhead if invalid JSON) + $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue + if ($content -match '"size"\s*:\s*(\d+)') { + $sizeBytes = [long]$matches[1] + if ($sizeBytes -lt 100000000) { continue } # Skip < 100MB + + $sizeGB = [math]::Round($sizeBytes / 1GB, 1) + $Num = $DlStartNum + $DlCount + + # Append to ModelCatalog so existing selection logic works automatically + $ModelCatalog += @{ Num=$Num; Category="Already Downloaded"; Name=$fullTag; Tag=$fullTag; Size=$sizeGB.ToString(); Input="Text"; Label="DOWNLOADED"; Badge="" } + $DlCount++ + } + } + } +} + +if ($DlCount -gt 0) { + Write-Host "`n --- " -ForegroundColor Cyan -NoNewline + Write-Host "Already Downloaded" -ForegroundColor Green -NoNewline + Write-Host " ---" -ForegroundColor Cyan + + $dlModels = $ModelCatalog | Where-Object { $_.Category -eq "Already Downloaded" } + foreach ($m in $dlModels) { + $padNum = $m.Num.ToString().PadLeft(2) + Write-Host " [$padNum]" -ForegroundColor Yellow -NoNewline + Write-Host " $($m.Name.PadRight(24))" -ForegroundColor White -NoNewline + Write-Host ("[" + $m.Input + "]").PadRight(16) -ForegroundColor DarkCyan -NoNewline + Write-Host " (~$($m.Size) GB)".PadRight(12) -ForegroundColor DarkGray -NoNewline + Write-Host " [DOWNLOADED]" -ForegroundColor Green + } +} + +Write-Host "`n [C] CUSTOM - Enter an Official Ollama Tag" -ForegroundColor Green +Write-Host " Browse ALL models here: " -ForegroundColor Gray -NoNewline +Write-Host "https://ollama.com/library" -ForegroundColor Blue +Write-Host "`n ------------------------------------------------" -ForegroundColor DarkGray +Write-Host " Enter number(s) separated by commas (e.g. 1,4)" -ForegroundColor Gray +Write-Host " Type 'all' for every preset model, 'c' for custom`n" -ForegroundColor Gray + +$UserChoice = Read-Host " Your choice" +if ([string]::IsNullOrWhiteSpace($UserChoice)) { + Write-Host "`n No input! Defaulting to [3] Gemma 4 E4B..." -ForegroundColor Yellow + $UserChoice = "3" +} + +$SelectedModels = @() +$HasCustom = $false + +if ($UserChoice.Trim().ToLower() -eq "all") { $SelectedModels = @($ModelCatalog) } +else { + foreach ($t in ($UserChoice -split "," | ForEach-Object { $_.Trim().ToLower() })) { + if ($t -eq "c" -or $t -eq "custom") { $HasCustom = $true } + elseif ($t -match '^\d+$') { + $num = [int]$t + $found = $ModelCatalog | Where-Object { $_.Num -eq $num } + if ($found -and -Not ($SelectedModels | Where-Object { $_.Num -eq $num })) { $SelectedModels += $found } + } + } +} + +if ($HasCustom) { + Write-Host "`n ---- Custom Model Setup ----" -ForegroundColor Green + $customTag = Read-Host " Ollama Tag (e.g. mistral-nemo, phi3)" + if ($customTag) { + $customName = (CultureInfo.CurrentCulture.TextInfo.ToTitleCase($customTag.ToLower())) + $SelectedModels += @{ Num=99; Name="Custom: $customName"; Tag=$customTag.Trim(); Size="?"; Label="CUSTOM" } + Write-Host " Custom model added!" -ForegroundColor Green + } +} + +if ($SelectedModels.Count -eq 0) { Write-Host "`n ERROR: No models selected!" -ForegroundColor Red; exit 1 } + +$totalSizeGB = 0 +foreach ($m in $SelectedModels) { + if ($m.Size -match '\d') { $totalSizeGB += [double]$m.Size } +} + +if ($totalSizeGB -ge ($freeGB - 1) -and $freeGB -gt 0 -or $UserChoice.Trim().ToLower() -eq "all") { + Write-Host "`n WARNING: These models total ~$([math]::Ceiling($totalSizeGB)) GB. USB drive has $freeGB GB free!" -ForegroundColor Red + $confirm = Read-Host " Continue? (yes/no)" + if ($confirm.Trim().ToLower() -ne "yes" -and $confirm.Trim().ToLower() -ne "y") { exit } +} + +# Directories +New-Item -ItemType Directory -Force -Path $ModelsDir | Out-Null +New-Item -ItemType Directory -Force -Path "$OllamaDir\data" | Out-Null +Write-Host "`n[2/4] Created storage folders." -ForegroundColor Green + +# Ollama Engine Setup +Write-Host "`n[3/4] Setting up Portable Ollama Engine..." -ForegroundColor Yellow +$OllamaURL = "https://github.com/ollama/ollama/releases/latest/download/ollama-windows-amd64.zip" +$OllamaDest = "$OllamaDir\ollama.zip" +$OllamaExe = "$OllamaDir\ollama.exe" + +if (Test-Path $OllamaExe) { + Write-Host " Engine already installed!" -ForegroundColor Green +} else { + Write-Host " Downloading Ollama Engine (~100MB)..." -ForegroundColor Yellow + curl.exe -L --ssl-no-revoke $OllamaURL -o $OllamaDest + if (Test-Path $OllamaDest) { + Write-Host " Extracting to USB..." -ForegroundColor Yellow + Expand-Archive -Path $OllamaDest -DestinationPath $OllamaDir -Force + Remove-Item $OllamaDest -Force -ErrorAction SilentlyContinue + Write-Host " Engine Installed successfully!" -ForegroundColor Green + } else { + Write-Host " ERROR: Failed to download engine!" -ForegroundColor Red + exit 1 + } +} + +# Downloading Models via Ollama +Write-Host "`n[4/4] Pulling Models (This guarantees perfectly configured Tool Support)..." -ForegroundColor Yellow + +$downloadErrors = @() + +$env:OLLAMA_MODELS = "$OllamaDir\data" +Write-Host "`n Starting background Ollama server on USB..." -ForegroundColor DarkGray +$ServerProcess = Start-Process -FilePath $OllamaExe -ArgumentList "serve" -WindowStyle Hidden -PassThru +Start-Sleep -Seconds 5 + +$idx = 1 +foreach ($m in $SelectedModels) { + if ($m.Tag -match "^http.*" -and $m.Tag -match "\.gguf") { + $tagUrlNoQuery = $m.Tag.Split('?')[0] + $fileName = $tagUrlNoQuery.Split('/')[-1] + if (-not $fileName.EndsWith(".gguf")) { $fileName += ".gguf" } + $dest = "$ModelsDir\$fileName" + + $baseName = $fileName -ireplace '\.gguf$', '' + $modelNameLocal = "$baseName-local".ToLower() -replace '[^a-z0-9_-]', '-' + + # --- Always verify real file size against server --- + Write-Host "`n ($idx/$($SelectedModels.Count)) Checking $($m.Name)..." -ForegroundColor Yellow + $idx++ + + $expectedSize = 0 + $existingSize = 0 + try { + $headResponse = Invoke-WebRequest -Uri $m.Tag -Method Head -UseBasicParsing -ErrorAction Stop + $expectedSize = [long]$headResponse.Headers['Content-Length'] + } catch {} + if (Test-Path $dest) { $existingSize = (Get-Item $dest).Length } + + $fileComplete = ($expectedSize -gt 0 -and $existingSize -ge $expectedSize) + + # Only skip if file is FULLY downloaded AND ollama has it imported + $showResult = & $OllamaExe show $modelNameLocal 2>&1 + if ($fileComplete -and $LASTEXITCODE -eq 0) { + $existMegabytes = [math]::Round($existingSize / 1MB) + Write-Host (" $([char]0x2705) {0} fully downloaded ({1} MB) & imported - skipping!" -f $m.Name, $existMegabytes) -ForegroundColor Green + $m.Tag = $modelNameLocal + continue + } + + Write-Host " Do not close this window! Download may take a while." -ForegroundColor Magenta + + try { + # --- Download or resume --- + if ((Test-Path $dest) -and -not $fileComplete) { + $existMegabytes = [math]::Round($existingSize / 1MB) + $expectMegabytes = [math]::Round($expectedSize / 1MB) + Write-Host (" {0} is incomplete ({1} MB / {2} MB). Resuming..." -f $fileName, $existMegabytes, $expectMegabytes) -ForegroundColor Yellow + curl.exe -L -C - $($m.Tag) -o $dest + } elseif (-not $fileComplete) { + Write-Host " Downloading $fileName (speed + ETA shown below)..." -ForegroundColor Cyan + curl.exe -L -C - $($m.Tag) -o $dest + } + + $modelFileContent = "FROM ./$fileName`nPARAMETER temperature 0.7`nPARAMETER top_p 0.9" + $modelFilePath = "$ModelsDir\Modelfile-$modelNameLocal" + Set-Content -Path $modelFilePath -Value $modelFileContent -Encoding Ascii + + Write-Host " Importing into Ollama as '$modelNameLocal'..." -ForegroundColor Cyan + Push-Location $ModelsDir + $createArgs = "create $modelNameLocal -f Modelfile-$modelNameLocal" + $createProcess = Start-Process -FilePath $OllamaExe -ArgumentList $createArgs -Wait -NoNewWindow -PassThru + Pop-Location + + if ($createProcess.ExitCode -eq 0) { + Write-Host " Import complete!" -ForegroundColor Green + $m.Tag = $modelNameLocal + } else { + throw "Exit code $($createProcess.ExitCode)" + } + } catch { + Write-Host " FAILED to import custom model: $fileName" -ForegroundColor Red + $downloadErrors += $m.Name + } + continue + } + + # --- Check if standard Ollama model already exists --- + $showResult = & $OllamaExe show $($m.Tag) 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "`n ($idx/$($SelectedModels.Count)) " -ForegroundColor Green -NoNewline + Write-Host ("$([char]0x2705) {0} [{1}] already pulled - skipping!" -f $m.Name, $m.Tag) -ForegroundColor Green + $idx++ + continue + } + + Write-Host "`n ($idx/$($SelectedModels.Count)) Pulling $($m.Name) [$($m.Tag)]..." -ForegroundColor Yellow + Write-Host " Do not close this window! Download may take a while depending on bandwidth." -ForegroundColor Magenta + $idx++ + + try { + $pullArgs = "pull $($m.Tag)" + $pullProcess = Start-Process -FilePath $OllamaExe -ArgumentList $pullArgs -Wait -NoNewWindow -PassThru + if ($pullProcess.ExitCode -ne 0) { throw "Exit code $($pullProcess.ExitCode)" } + Write-Host " Pull complete!" -ForegroundColor Green + } catch { + Write-Host " FAILED to pull model: $($m.Tag)" -ForegroundColor Red + $downloadErrors += $m.Name + } +} + +Write-Host "`n Stopping background Ollama server..." -ForegroundColor DarkGray +Stop-Process -Id $ServerProcess.Id -Force -ErrorAction SilentlyContinue + +# Record Models for the Dashboard +$installedList = $SelectedModels | ForEach-Object { "$($_.Tag)|$($_.Name)|$($_.Label)" } +if (Test-Path "$ModelsDir\installed-models.txt") { + $existing = Get-Content "$ModelsDir\installed-models.txt" + $installedList = ($existing + $installedList) | Select-Object -Unique +} +Set-Content -Path "$ModelsDir\installed-models.txt" -Value ($installedList -join "`n") -Force -Encoding UTF8 + +Write-Host "`n[5/5] Finalizing Configurations..." -ForegroundColor Yellow + +$firstModelTag = $SelectedModels[0].Tag +$configContent = "AI_PROVIDER=ollama`nCLAUDE_CODE_USE_OPENAI=1`nOPENAI_API_KEY=ollama`nOPENAI_BASE_URL=http://localhost:11434/v1`nOPENAI_MODEL=$firstModelTag`nAI_DISPLAY_MODEL=$firstModelTag" +Set-Content -Path $EnvFile -Value $configContent -Force -Encoding Ascii +Write-Host " Default Model set to: $firstModelTag" -ForegroundColor Green + +Write-Host "`n==========================================================" -ForegroundColor Cyan +if ($downloadErrors.Count -gt 0) { Write-Host " SETUP COMPLETE (with some download errors)" -ForegroundColor Yellow } +else { Write-Host " SETUP COMPLETE! LOCAL AI AGENTS ARE READY!" -ForegroundColor Green } +Write-Host "==========================================================" -ForegroundColor Cyan