install.ps1
1 # ============================================================================ 2 # Hermes Agent Installer for Windows 3 # ============================================================================ 4 # Installation script for Windows (PowerShell). 5 # Uses uv for fast Python provisioning and package management. 6 # 7 # Usage: 8 # irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex 9 # 10 # Or download and run with options: 11 # .\install.ps1 -NoVenv -SkipSetup 12 # 13 # ============================================================================ 14 15 param( 16 [switch]$NoVenv, 17 [switch]$SkipSetup, 18 [string]$Branch = "main", 19 [string]$HermesHome = "$env:LOCALAPPDATA\hermes", 20 [string]$InstallDir = "$env:LOCALAPPDATA\hermes\hermes-agent" 21 ) 22 23 $ErrorActionPreference = "Stop" 24 25 # ============================================================================ 26 # Configuration 27 # ============================================================================ 28 29 $RepoUrlSsh = "git@github.com:NousResearch/hermes-agent.git" 30 $RepoUrlHttps = "https://github.com/NousResearch/hermes-agent.git" 31 $PythonVersion = "3.11" 32 $NodeVersion = "22" 33 34 # ============================================================================ 35 # Helper functions 36 # ============================================================================ 37 38 function Write-Banner { 39 Write-Host "" 40 Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Magenta 41 Write-Host "│ ⚕ Hermes Agent Installer │" -ForegroundColor Magenta 42 Write-Host "├─────────────────────────────────────────────────────────┤" -ForegroundColor Magenta 43 Write-Host "│ An open source AI agent by Nous Research. │" -ForegroundColor Magenta 44 Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Magenta 45 Write-Host "" 46 } 47 48 function Write-Info { 49 param([string]$Message) 50 Write-Host "→ $Message" -ForegroundColor Cyan 51 } 52 53 function Write-Success { 54 param([string]$Message) 55 Write-Host "✓ $Message" -ForegroundColor Green 56 } 57 58 function Write-Warn { 59 param([string]$Message) 60 Write-Host "⚠ $Message" -ForegroundColor Yellow 61 } 62 63 function Write-Err { 64 param([string]$Message) 65 Write-Host "✗ $Message" -ForegroundColor Red 66 } 67 68 # ============================================================================ 69 # Dependency checks 70 # ============================================================================ 71 72 function Install-Uv { 73 Write-Info "Checking for uv package manager..." 74 75 # Check if uv is already available 76 if (Get-Command uv -ErrorAction SilentlyContinue) { 77 $version = uv --version 78 $script:UvCmd = "uv" 79 Write-Success "uv found ($version)" 80 return $true 81 } 82 83 # Check common install locations 84 $uvPaths = @( 85 "$env:USERPROFILE\.local\bin\uv.exe", 86 "$env:USERPROFILE\.cargo\bin\uv.exe" 87 ) 88 foreach ($uvPath in $uvPaths) { 89 if (Test-Path $uvPath) { 90 $script:UvCmd = $uvPath 91 $version = & $uvPath --version 92 Write-Success "uv found at $uvPath ($version)" 93 return $true 94 } 95 } 96 97 # Install uv 98 Write-Info "Installing uv (fast Python package manager)..." 99 try { 100 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 2>&1 | Out-Null 101 102 # Find the installed binary 103 $uvExe = "$env:USERPROFILE\.local\bin\uv.exe" 104 if (-not (Test-Path $uvExe)) { 105 $uvExe = "$env:USERPROFILE\.cargo\bin\uv.exe" 106 } 107 if (-not (Test-Path $uvExe)) { 108 # Refresh PATH and try again 109 $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") 110 if (Get-Command uv -ErrorAction SilentlyContinue) { 111 $uvExe = (Get-Command uv).Source 112 } 113 } 114 115 if (Test-Path $uvExe) { 116 $script:UvCmd = $uvExe 117 $version = & $uvExe --version 118 Write-Success "uv installed ($version)" 119 return $true 120 } 121 122 Write-Err "uv installed but not found on PATH" 123 Write-Info "Try restarting your terminal and re-running" 124 return $false 125 } catch { 126 Write-Err "Failed to install uv" 127 Write-Info "Install manually: https://docs.astral.sh/uv/getting-started/installation/" 128 return $false 129 } 130 } 131 132 function Test-Python { 133 Write-Info "Checking Python $PythonVersion..." 134 135 # Let uv find or install Python 136 try { 137 $pythonPath = & $UvCmd python find $PythonVersion 2>$null 138 if ($pythonPath) { 139 $ver = & $pythonPath --version 2>$null 140 Write-Success "Python found: $ver" 141 return $true 142 } 143 } catch { } 144 145 # Python not found — use uv to install it (no admin needed!) 146 Write-Info "Python $PythonVersion not found, installing via uv..." 147 try { 148 $uvOutput = & $UvCmd python install $PythonVersion 2>&1 149 if ($LASTEXITCODE -eq 0) { 150 $pythonPath = & $UvCmd python find $PythonVersion 2>$null 151 if ($pythonPath) { 152 $ver = & $pythonPath --version 2>$null 153 Write-Success "Python installed: $ver" 154 return $true 155 } 156 } else { 157 Write-Warn "uv python install output:" 158 Write-Host $uvOutput -ForegroundColor DarkGray 159 } 160 } catch { 161 Write-Warn "uv python install error: $_" 162 } 163 164 # Fallback: check if ANY Python 3.10+ is already available on the system 165 Write-Info "Trying to find any existing Python 3.10+..." 166 foreach ($fallbackVer in @("3.12", "3.13", "3.10")) { 167 try { 168 $pythonPath = & $UvCmd python find $fallbackVer 2>$null 169 if ($pythonPath) { 170 $ver = & $pythonPath --version 2>$null 171 Write-Success "Found fallback: $ver" 172 $script:PythonVersion = $fallbackVer 173 return $true 174 } 175 } catch { } 176 } 177 178 # Fallback: try system python 179 if (Get-Command python -ErrorAction SilentlyContinue) { 180 $sysVer = python --version 2>$null 181 if ($sysVer -match "3\.(1[0-9]|[1-9][0-9])") { 182 Write-Success "Using system Python: $sysVer" 183 return $true 184 } 185 } 186 187 Write-Err "Failed to install Python $PythonVersion" 188 Write-Info "Install Python 3.11 manually, then re-run this script:" 189 Write-Info " https://www.python.org/downloads/" 190 Write-Info " Or: winget install Python.Python.3.11" 191 return $false 192 } 193 194 function Test-Git { 195 Write-Info "Checking Git..." 196 197 if (Get-Command git -ErrorAction SilentlyContinue) { 198 $version = git --version 199 Write-Success "Git found ($version)" 200 return $true 201 } 202 203 Write-Err "Git not found" 204 Write-Info "Please install Git from:" 205 Write-Info " https://git-scm.com/download/win" 206 return $false 207 } 208 209 function Test-Node { 210 Write-Info "Checking Node.js (for browser tools)..." 211 212 if (Get-Command node -ErrorAction SilentlyContinue) { 213 $version = node --version 214 Write-Success "Node.js $version found" 215 $script:HasNode = $true 216 return $true 217 } 218 219 # Check our own managed install from a previous run 220 $managedNode = "$HermesHome\node\node.exe" 221 if (Test-Path $managedNode) { 222 $version = & $managedNode --version 223 $env:Path = "$HermesHome\node;$env:Path" 224 Write-Success "Node.js $version found (Hermes-managed)" 225 $script:HasNode = $true 226 return $true 227 } 228 229 Write-Info "Node.js not found — installing Node.js $NodeVersion LTS..." 230 231 # Try winget first (cleanest on modern Windows) 232 if (Get-Command winget -ErrorAction SilentlyContinue) { 233 Write-Info "Installing via winget..." 234 try { 235 winget install OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null 236 # Refresh PATH 237 $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") 238 if (Get-Command node -ErrorAction SilentlyContinue) { 239 $version = node --version 240 Write-Success "Node.js $version installed via winget" 241 $script:HasNode = $true 242 return $true 243 } 244 } catch { } 245 } 246 247 # Fallback: download binary zip to ~/.hermes/node/ 248 Write-Info "Downloading Node.js $NodeVersion binary..." 249 try { 250 $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } 251 $indexUrl = "https://nodejs.org/dist/latest-v${NodeVersion}.x/" 252 $indexPage = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing 253 $zipName = ($indexPage.Content | Select-String -Pattern "node-v${NodeVersion}\.\d+\.\d+-win-${arch}\.zip" -AllMatches).Matches[0].Value 254 255 if ($zipName) { 256 $downloadUrl = "${indexUrl}${zipName}" 257 $tmpZip = "$env:TEMP\$zipName" 258 $tmpDir = "$env:TEMP\hermes-node-extract" 259 260 Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing 261 if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir } 262 Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force 263 264 $extractedDir = Get-ChildItem $tmpDir -Directory | Select-Object -First 1 265 if ($extractedDir) { 266 if (Test-Path "$HermesHome\node") { Remove-Item -Recurse -Force "$HermesHome\node" } 267 Move-Item $extractedDir.FullName "$HermesHome\node" 268 $env:Path = "$HermesHome\node;$env:Path" 269 270 $version = & "$HermesHome\node\node.exe" --version 271 Write-Success "Node.js $version installed to ~/.hermes/node/" 272 $script:HasNode = $true 273 274 Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue 275 Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue 276 return $true 277 } 278 } 279 } catch { 280 Write-Warn "Download failed: $_" 281 } 282 283 Write-Warn "Could not auto-install Node.js" 284 Write-Info "Install manually: https://nodejs.org/en/download/" 285 $script:HasNode = $false 286 return $true 287 } 288 289 function Install-SystemPackages { 290 $script:HasRipgrep = $false 291 $script:HasFfmpeg = $false 292 $needRipgrep = $false 293 $needFfmpeg = $false 294 295 Write-Info "Checking ripgrep (fast file search)..." 296 if (Get-Command rg -ErrorAction SilentlyContinue) { 297 $version = rg --version | Select-Object -First 1 298 Write-Success "$version found" 299 $script:HasRipgrep = $true 300 } else { 301 $needRipgrep = $true 302 } 303 304 Write-Info "Checking ffmpeg (TTS voice messages)..." 305 if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { 306 Write-Success "ffmpeg found" 307 $script:HasFfmpeg = $true 308 } else { 309 $needFfmpeg = $true 310 } 311 312 if (-not $needRipgrep -and -not $needFfmpeg) { return } 313 314 # Build description and package lists for each package manager 315 $descParts = @() 316 $wingetPkgs = @() 317 $chocoPkgs = @() 318 $scoopPkgs = @() 319 320 if ($needRipgrep) { 321 $descParts += "ripgrep for faster file search" 322 $wingetPkgs += "BurntSushi.ripgrep.MSVC" 323 $chocoPkgs += "ripgrep" 324 $scoopPkgs += "ripgrep" 325 } 326 if ($needFfmpeg) { 327 $descParts += "ffmpeg for TTS voice messages" 328 $wingetPkgs += "Gyan.FFmpeg" 329 $chocoPkgs += "ffmpeg" 330 $scoopPkgs += "ffmpeg" 331 } 332 333 $description = $descParts -join " and " 334 $hasWinget = Get-Command winget -ErrorAction SilentlyContinue 335 $hasChoco = Get-Command choco -ErrorAction SilentlyContinue 336 $hasScoop = Get-Command scoop -ErrorAction SilentlyContinue 337 338 # Try winget first (most common on modern Windows) 339 if ($hasWinget) { 340 Write-Info "Installing $description via winget..." 341 foreach ($pkg in $wingetPkgs) { 342 try { 343 winget install $pkg --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null 344 } catch { } 345 } 346 # Refresh PATH and recheck 347 $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") 348 if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { 349 Write-Success "ripgrep installed" 350 $script:HasRipgrep = $true 351 $needRipgrep = $false 352 } 353 if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { 354 Write-Success "ffmpeg installed" 355 $script:HasFfmpeg = $true 356 $needFfmpeg = $false 357 } 358 if (-not $needRipgrep -and -not $needFfmpeg) { return } 359 } 360 361 # Fallback: choco 362 if ($hasChoco -and ($needRipgrep -or $needFfmpeg)) { 363 Write-Info "Trying Chocolatey..." 364 foreach ($pkg in $chocoPkgs) { 365 try { choco install $pkg -y 2>&1 | Out-Null } catch { } 366 } 367 if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { 368 Write-Success "ripgrep installed via chocolatey" 369 $script:HasRipgrep = $true 370 $needRipgrep = $false 371 } 372 if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { 373 Write-Success "ffmpeg installed via chocolatey" 374 $script:HasFfmpeg = $true 375 $needFfmpeg = $false 376 } 377 } 378 379 # Fallback: scoop 380 if ($hasScoop -and ($needRipgrep -or $needFfmpeg)) { 381 Write-Info "Trying Scoop..." 382 foreach ($pkg in $scoopPkgs) { 383 try { scoop install $pkg 2>&1 | Out-Null } catch { } 384 } 385 if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { 386 Write-Success "ripgrep installed via scoop" 387 $script:HasRipgrep = $true 388 $needRipgrep = $false 389 } 390 if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { 391 Write-Success "ffmpeg installed via scoop" 392 $script:HasFfmpeg = $true 393 $needFfmpeg = $false 394 } 395 } 396 397 # Show manual instructions for anything still missing 398 if ($needRipgrep) { 399 Write-Warn "ripgrep not installed (file search will use findstr fallback)" 400 Write-Info " winget install BurntSushi.ripgrep.MSVC" 401 } 402 if ($needFfmpeg) { 403 Write-Warn "ffmpeg not installed (TTS voice messages will be limited)" 404 Write-Info " winget install Gyan.FFmpeg" 405 } 406 } 407 408 # ============================================================================ 409 # Installation 410 # ============================================================================ 411 412 function Install-Repository { 413 Write-Info "Installing to $InstallDir..." 414 415 if (Test-Path $InstallDir) { 416 if (Test-Path "$InstallDir\.git") { 417 Write-Info "Existing installation found, updating..." 418 Push-Location $InstallDir 419 git -c windows.appendAtomically=false fetch origin 420 git -c windows.appendAtomically=false checkout $Branch 421 git -c windows.appendAtomically=false pull origin $Branch 422 Pop-Location 423 } else { 424 Write-Err "Directory exists but is not a git repository: $InstallDir" 425 Write-Info "Remove it or choose a different directory with -InstallDir" 426 throw "Directory exists but is not a git repository: $InstallDir" 427 } 428 } else { 429 $cloneSuccess = $false 430 431 # Fix Windows git "copy-fd: write returned: Invalid argument" error. 432 # Git for Windows can fail on atomic file operations (hook templates, 433 # config lock files) due to antivirus, OneDrive, or NTFS filter drivers. 434 # The -c flag injects config before any file I/O occurs. 435 Write-Info "Configuring git for Windows compatibility..." 436 $env:GIT_CONFIG_COUNT = "1" 437 $env:GIT_CONFIG_KEY_0 = "windows.appendAtomically" 438 $env:GIT_CONFIG_VALUE_0 = "false" 439 git config --global windows.appendAtomically false 2>$null 440 441 # Try SSH first, then HTTPS, with -c flag for atomic write fix 442 Write-Info "Trying SSH clone..." 443 $env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5" 444 try { 445 git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlSsh $InstallDir 446 if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true } 447 } catch { } 448 $env:GIT_SSH_COMMAND = $null 449 450 if (-not $cloneSuccess) { 451 if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } 452 Write-Info "SSH failed, trying HTTPS..." 453 try { 454 git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlHttps $InstallDir 455 if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true } 456 } catch { } 457 } 458 459 # Fallback: download ZIP archive (bypasses git file I/O issues entirely) 460 if (-not $cloneSuccess) { 461 if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } 462 Write-Warn "Git clone failed — downloading ZIP archive instead..." 463 try { 464 $zipUrl = "https://github.com/NousResearch/hermes-agent/archive/refs/heads/$Branch.zip" 465 $zipPath = "$env:TEMP\hermes-agent-$Branch.zip" 466 $extractPath = "$env:TEMP\hermes-agent-extract" 467 468 Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing 469 if (Test-Path $extractPath) { Remove-Item -Recurse -Force $extractPath } 470 Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force 471 472 # GitHub ZIPs extract to repo-branch/ subdirectory 473 $extractedDir = Get-ChildItem $extractPath -Directory | Select-Object -First 1 474 if ($extractedDir) { 475 New-Item -ItemType Directory -Force -Path (Split-Path $InstallDir) -ErrorAction SilentlyContinue | Out-Null 476 Move-Item $extractedDir.FullName $InstallDir -Force 477 Write-Success "Downloaded and extracted" 478 479 # Initialize git repo so updates work later 480 Push-Location $InstallDir 481 git -c windows.appendAtomically=false init 2>$null 482 git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null 483 git remote add origin $RepoUrlHttps 2>$null 484 Pop-Location 485 Write-Success "Git repo initialized for future updates" 486 487 $cloneSuccess = $true 488 } 489 490 # Cleanup temp files 491 Remove-Item -Force $zipPath -ErrorAction SilentlyContinue 492 Remove-Item -Recurse -Force $extractPath -ErrorAction SilentlyContinue 493 } catch { 494 Write-Err "ZIP download also failed: $_" 495 } 496 } 497 498 if (-not $cloneSuccess) { 499 throw "Failed to download repository (tried git clone SSH, HTTPS, and ZIP)" 500 } 501 } 502 503 # Set per-repo config (harmless if it fails) 504 Push-Location $InstallDir 505 git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null 506 507 # Ensure submodules are initialized and updated 508 Write-Info "Initializing submodules..." 509 git -c windows.appendAtomically=false submodule update --init --recursive 2>$null 510 if ($LASTEXITCODE -ne 0) { 511 Write-Warn "Submodule init failed (terminal/RL tools may need manual setup)" 512 } else { 513 Write-Success "Submodules ready" 514 } 515 Pop-Location 516 517 Write-Success "Repository ready" 518 } 519 520 function Install-Venv { 521 if ($NoVenv) { 522 Write-Info "Skipping virtual environment (-NoVenv)" 523 return 524 } 525 526 Write-Info "Creating virtual environment with Python $PythonVersion..." 527 528 Push-Location $InstallDir 529 530 if (Test-Path "venv") { 531 Write-Info "Virtual environment already exists, recreating..." 532 Remove-Item -Recurse -Force "venv" 533 } 534 535 # uv creates the venv and pins the Python version in one step 536 & $UvCmd venv venv --python $PythonVersion 537 538 Pop-Location 539 540 Write-Success "Virtual environment ready (Python $PythonVersion)" 541 } 542 543 function Install-Dependencies { 544 Write-Info "Installing dependencies..." 545 546 Push-Location $InstallDir 547 548 if (-not $NoVenv) { 549 # Tell uv to install into our venv (no activation needed) 550 $env:VIRTUAL_ENV = "$InstallDir\venv" 551 } 552 553 # Install main package with all extras 554 try { 555 & $UvCmd pip install -e ".[all]" 2>&1 | Out-Null 556 } catch { 557 & $UvCmd pip install -e "." | Out-Null 558 } 559 560 Write-Success "Main package installed" 561 562 # Install optional submodules 563 Write-Info "Installing tinker-atropos (RL training backend)..." 564 if (Test-Path "tinker-atropos\pyproject.toml") { 565 try { 566 & $UvCmd pip install -e ".\tinker-atropos" 2>&1 | Out-Null 567 Write-Success "tinker-atropos installed" 568 } catch { 569 Write-Warn "tinker-atropos install failed (RL tools may not work)" 570 } 571 } else { 572 Write-Warn "tinker-atropos not found (run: git submodule update --init)" 573 } 574 575 Pop-Location 576 577 Write-Success "All dependencies installed" 578 } 579 580 function Set-PathVariable { 581 Write-Info "Setting up hermes command..." 582 583 if ($NoVenv) { 584 $hermesBin = "$InstallDir" 585 } else { 586 $hermesBin = "$InstallDir\venv\Scripts" 587 } 588 589 # Add the venv Scripts dir to user PATH so hermes is globally available 590 # On Windows, the hermes.exe in venv\Scripts\ has the venv Python baked in 591 $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") 592 593 if ($currentPath -notlike "*$hermesBin*") { 594 [Environment]::SetEnvironmentVariable( 595 "Path", 596 "$hermesBin;$currentPath", 597 "User" 598 ) 599 Write-Success "Added to user PATH: $hermesBin" 600 } else { 601 Write-Info "PATH already configured" 602 } 603 604 # Set HERMES_HOME so the Python code finds config/data in the right place. 605 # Only needed on Windows where we install to %LOCALAPPDATA%\hermes instead 606 # of the Unix default ~/.hermes 607 $currentHermesHome = [Environment]::GetEnvironmentVariable("HERMES_HOME", "User") 608 if (-not $currentHermesHome -or $currentHermesHome -ne $HermesHome) { 609 [Environment]::SetEnvironmentVariable("HERMES_HOME", $HermesHome, "User") 610 Write-Success "Set HERMES_HOME=$HermesHome" 611 } 612 $env:HERMES_HOME = $HermesHome 613 614 # Update current session 615 $env:Path = "$hermesBin;$env:Path" 616 617 Write-Success "hermes command ready" 618 } 619 620 function Copy-ConfigTemplates { 621 Write-Info "Setting up configuration files..." 622 623 # Create ~/.hermes directory structure 624 New-Item -ItemType Directory -Force -Path "$HermesHome\cron" | Out-Null 625 New-Item -ItemType Directory -Force -Path "$HermesHome\sessions" | Out-Null 626 New-Item -ItemType Directory -Force -Path "$HermesHome\logs" | Out-Null 627 New-Item -ItemType Directory -Force -Path "$HermesHome\pairing" | Out-Null 628 New-Item -ItemType Directory -Force -Path "$HermesHome\hooks" | Out-Null 629 New-Item -ItemType Directory -Force -Path "$HermesHome\image_cache" | Out-Null 630 New-Item -ItemType Directory -Force -Path "$HermesHome\audio_cache" | Out-Null 631 New-Item -ItemType Directory -Force -Path "$HermesHome\memories" | Out-Null 632 New-Item -ItemType Directory -Force -Path "$HermesHome\skills" | Out-Null 633 634 635 # Create .env 636 $envPath = "$HermesHome\.env" 637 if (-not (Test-Path $envPath)) { 638 $examplePath = "$InstallDir\.env.example" 639 if (Test-Path $examplePath) { 640 Copy-Item $examplePath $envPath 641 Write-Success "Created ~/.hermes/.env from template" 642 } else { 643 New-Item -ItemType File -Force -Path $envPath | Out-Null 644 Write-Success "Created ~/.hermes/.env" 645 } 646 } else { 647 Write-Info "~/.hermes/.env already exists, keeping it" 648 } 649 650 # Create config.yaml 651 $configPath = "$HermesHome\config.yaml" 652 if (-not (Test-Path $configPath)) { 653 $examplePath = "$InstallDir\cli-config.yaml.example" 654 if (Test-Path $examplePath) { 655 Copy-Item $examplePath $configPath 656 Write-Success "Created ~/.hermes/config.yaml from template" 657 } 658 } else { 659 Write-Info "~/.hermes/config.yaml already exists, keeping it" 660 } 661 662 # Create SOUL.md if it doesn't exist (global persona file) 663 $soulPath = "$HermesHome\SOUL.md" 664 if (-not (Test-Path $soulPath)) { 665 @" 666 # Hermes Agent Persona 667 668 <!-- 669 This file defines the agent's personality and tone. 670 The agent will embody whatever you write here. 671 Edit this to customize how Hermes communicates with you. 672 673 Examples: 674 - "You are a warm, playful assistant who uses kaomoji occasionally." 675 - "You are a concise technical expert. No fluff, just facts." 676 - "You speak like a friendly coworker who happens to know everything." 677 678 This file is loaded fresh each message -- no restart needed. 679 Delete the contents (or this file) to use the default personality. 680 --> 681 "@ | Set-Content -Path $soulPath -Encoding UTF8 682 Write-Success "Created ~/.hermes/SOUL.md (edit to customize personality)" 683 } 684 685 Write-Success "Configuration directory ready: ~/.hermes/" 686 687 # Seed bundled skills into ~/.hermes/skills/ (manifest-based, one-time per skill) 688 Write-Info "Syncing bundled skills to ~/.hermes/skills/ ..." 689 $pythonExe = "$InstallDir\venv\Scripts\python.exe" 690 if (Test-Path $pythonExe) { 691 try { 692 & $pythonExe "$InstallDir\tools\skills_sync.py" 2>$null 693 Write-Success "Skills synced to ~/.hermes/skills/" 694 } catch { 695 # Fallback: simple directory copy 696 $bundledSkills = "$InstallDir\skills" 697 $userSkills = "$HermesHome\skills" 698 if ((Test-Path $bundledSkills) -and -not (Get-ChildItem $userSkills -Exclude '.bundled_manifest' -ErrorAction SilentlyContinue)) { 699 Copy-Item -Path "$bundledSkills\*" -Destination $userSkills -Recurse -Force -ErrorAction SilentlyContinue 700 Write-Success "Skills copied to ~/.hermes/skills/" 701 } 702 } 703 } 704 } 705 706 function Install-NodeDeps { 707 if (-not $HasNode) { 708 Write-Info "Skipping Node.js dependencies (Node not installed)" 709 return 710 } 711 712 Push-Location $InstallDir 713 714 if (Test-Path "package.json") { 715 Write-Info "Installing Node.js dependencies (browser tools)..." 716 try { 717 npm install --silent 2>&1 | Out-Null 718 Write-Success "Node.js dependencies installed" 719 } catch { 720 Write-Warn "npm install failed (browser tools may not work)" 721 } 722 } 723 724 # Install TUI dependencies 725 $tuiDir = "$InstallDir\ui-tui" 726 if (Test-Path "$tuiDir\package.json") { 727 Write-Info "Installing TUI dependencies..." 728 Push-Location $tuiDir 729 try { 730 npm install --silent 2>&1 | Out-Null 731 Write-Success "TUI dependencies installed" 732 } catch { 733 Write-Warn "TUI npm install failed (hermes --tui may not work)" 734 } 735 Pop-Location 736 } 737 738 739 740 Pop-Location 741 } 742 743 function Invoke-SetupWizard { 744 if ($SkipSetup) { 745 Write-Info "Skipping setup wizard (-SkipSetup)" 746 return 747 } 748 749 Write-Host "" 750 Write-Info "Starting setup wizard..." 751 Write-Host "" 752 753 Push-Location $InstallDir 754 755 # Run hermes setup using the venv Python directly (no activation needed) 756 if (-not $NoVenv) { 757 & ".\venv\Scripts\python.exe" -m hermes_cli.main setup 758 } else { 759 python -m hermes_cli.main setup 760 } 761 762 Pop-Location 763 } 764 765 function Start-GatewayIfConfigured { 766 $envPath = "$HermesHome\.env" 767 if (-not (Test-Path $envPath)) { return } 768 769 $hasMessaging = $false 770 $content = Get-Content $envPath -ErrorAction SilentlyContinue 771 foreach ($var in @("TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "WHATSAPP_ENABLED")) { 772 $match = $content | Where-Object { $_ -match "^${var}=.+" -and $_ -notmatch "your-token-here" } 773 if ($match) { $hasMessaging = $true; break } 774 } 775 776 if (-not $hasMessaging) { return } 777 778 $hermesCmd = "$InstallDir\venv\Scripts\hermes.exe" 779 if (-not (Test-Path $hermesCmd)) { 780 $hermesCmd = "hermes" 781 } 782 783 # If WhatsApp is enabled but not yet paired, run foreground for QR scan 784 $whatsappEnabled = $content | Where-Object { $_ -match "^WHATSAPP_ENABLED=true" } 785 $whatsappSession = "$HermesHome\whatsapp\session\creds.json" 786 if ($whatsappEnabled -and -not (Test-Path $whatsappSession)) { 787 Write-Host "" 788 Write-Info "WhatsApp is enabled but not yet paired." 789 Write-Info "Running 'hermes whatsapp' to pair via QR code..." 790 Write-Host "" 791 $response = Read-Host "Pair WhatsApp now? [Y/n]" 792 if ($response -eq "" -or $response -match "^[Yy]") { 793 try { 794 & $hermesCmd whatsapp 795 } catch { 796 # Expected after pairing completes 797 } 798 } 799 } 800 801 Write-Host "" 802 Write-Info "Messaging platform token detected!" 803 Write-Info "The gateway handles messaging platforms and cron job execution." 804 Write-Host "" 805 $response = Read-Host "Would you like to start the gateway now? [Y/n]" 806 807 if ($response -eq "" -or $response -match "^[Yy]") { 808 Write-Info "Starting gateway in background..." 809 try { 810 $logFile = "$HermesHome\logs\gateway.log" 811 Start-Process -FilePath $hermesCmd -ArgumentList "gateway" ` 812 -RedirectStandardOutput $logFile ` 813 -RedirectStandardError "$HermesHome\logs\gateway-error.log" ` 814 -WindowStyle Hidden 815 Write-Success "Gateway started! Your bot is now online." 816 Write-Info "Logs: $logFile" 817 Write-Info "To stop: close the gateway process from Task Manager" 818 } catch { 819 Write-Warn "Failed to start gateway. Run manually: hermes gateway" 820 } 821 } else { 822 Write-Info "Skipped. Start the gateway later with: hermes gateway" 823 } 824 } 825 826 function Write-Completion { 827 Write-Host "" 828 Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Green 829 Write-Host "│ ✓ Installation Complete! │" -ForegroundColor Green 830 Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Green 831 Write-Host "" 832 833 # Show file locations 834 Write-Host "📁 Your files:" -ForegroundColor Cyan 835 Write-Host "" 836 Write-Host " Config: " -NoNewline -ForegroundColor Yellow 837 Write-Host "$HermesHome\config.yaml" 838 Write-Host " API Keys: " -NoNewline -ForegroundColor Yellow 839 Write-Host "$HermesHome\.env" 840 Write-Host " Data: " -NoNewline -ForegroundColor Yellow 841 Write-Host "$HermesHome\cron\, sessions\, logs\" 842 Write-Host " Code: " -NoNewline -ForegroundColor Yellow 843 Write-Host "$HermesHome\hermes-agent\" 844 Write-Host "" 845 846 Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan 847 Write-Host "" 848 Write-Host "🚀 Commands:" -ForegroundColor Cyan 849 Write-Host "" 850 Write-Host " hermes " -NoNewline -ForegroundColor Green 851 Write-Host "Start chatting" 852 Write-Host " hermes setup " -NoNewline -ForegroundColor Green 853 Write-Host "Configure API keys & settings" 854 Write-Host " hermes config " -NoNewline -ForegroundColor Green 855 Write-Host "View/edit configuration" 856 Write-Host " hermes config edit " -NoNewline -ForegroundColor Green 857 Write-Host "Open config in editor" 858 Write-Host " hermes gateway " -NoNewline -ForegroundColor Green 859 Write-Host "Start messaging gateway (Telegram, Discord, etc.)" 860 Write-Host " hermes update " -NoNewline -ForegroundColor Green 861 Write-Host "Update to latest version" 862 Write-Host "" 863 864 Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan 865 Write-Host "" 866 Write-Host "⚡ Restart your terminal for PATH changes to take effect" -ForegroundColor Yellow 867 Write-Host "" 868 869 if (-not $HasNode) { 870 Write-Host "Note: Node.js could not be installed automatically." -ForegroundColor Yellow 871 Write-Host "Browser tools need Node.js. Install manually:" -ForegroundColor Yellow 872 Write-Host " https://nodejs.org/en/download/" -ForegroundColor Yellow 873 Write-Host "" 874 } 875 876 if (-not $HasRipgrep) { 877 Write-Host "Note: ripgrep (rg) was not installed. For faster file search:" -ForegroundColor Yellow 878 Write-Host " winget install BurntSushi.ripgrep.MSVC" -ForegroundColor Yellow 879 Write-Host "" 880 } 881 } 882 883 # ============================================================================ 884 # Main 885 # ============================================================================ 886 887 function Main { 888 Write-Banner 889 890 if (-not (Install-Uv)) { throw "uv installation failed — cannot continue" } 891 if (-not (Test-Python)) { throw "Python $PythonVersion not available — cannot continue" } 892 if (-not (Test-Git)) { throw "Git not found — install from https://git-scm.com/download/win" } 893 Test-Node # Auto-installs if missing 894 Install-SystemPackages # ripgrep + ffmpeg in one step 895 896 Install-Repository 897 Install-Venv 898 Install-Dependencies 899 Install-NodeDeps 900 Set-PathVariable 901 Copy-ConfigTemplates 902 Invoke-SetupWizard 903 Start-GatewayIfConfigured 904 905 Write-Completion 906 } 907 908 # Wrap in try/catch so errors don't kill the terminal when run via: 909 # irm https://...install.ps1 | iex 910 # (exit/throw inside iex kills the entire PowerShell session) 911 try { 912 Main 913 } catch { 914 Write-Host "" 915 Write-Err "Installation failed: $_" 916 Write-Host "" 917 Write-Info "If the error is unclear, try downloading and running the script directly:" 918 Write-Host " Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1' -OutFile install.ps1" -ForegroundColor Yellow 919 Write-Host " .\install.ps1" -ForegroundColor Yellow 920 Write-Host "" 921 }