diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 026f58fc62..646700463b 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -10,7 +10,9 @@ import sys from pathlib import Path from typing import Dict, List, Set -from hermes_cli.config import load_config, save_config, get_env_value +import os + +from hermes_cli.config import load_config, save_config, get_env_value, save_env_value from hermes_cli.colors import Colors, color # Toolsets shown in the configurator, grouped for display. @@ -202,6 +204,67 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str return {CONFIGURABLE_TOOLSETS[i][0] for i in selected} +# Map toolset keys to the env vars they require and where to get them +TOOLSET_ENV_REQUIREMENTS = { + "web": [("FIRECRAWL_API_KEY", "https://firecrawl.dev/")], + "browser": [("BROWSERBASE_API_KEY", "https://browserbase.com/"), + ("BROWSERBASE_PROJECT_ID", None)], + "vision": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")], + "image_gen": [("FAL_KEY", "https://fal.ai/")], + "moa": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")], + "tts": [], # Edge TTS is free, no key needed + "rl": [("TINKER_API_KEY", "https://tinker-console.thinkingmachines.ai/keys"), + ("WANDB_API_KEY", "https://wandb.ai/authorize")], +} + + +def _check_and_prompt_requirements(newly_enabled: Set[str]): + """Check if newly enabled toolsets have missing API keys and offer to set them up.""" + for ts_key in sorted(newly_enabled): + requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, []) + if not requirements: + continue + + missing = [(var, url) for var, url in requirements if not get_env_value(var)] + if not missing: + continue + + ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key) + print() + print(color(f" ⚠ {ts_label} requires configuration:", Colors.YELLOW)) + + for var, url in missing: + if url: + print(color(f" {var}", Colors.CYAN) + color(f" ({url})", Colors.DIM)) + else: + print(color(f" {var}", Colors.CYAN)) + + print() + try: + response = input(color(" Set up now? [Y/n] ", Colors.YELLOW)).strip().lower() + except (KeyboardInterrupt, EOFError): + print() + continue + + if response in ("", "y", "yes"): + for var, url in missing: + if url: + print(color(f" Get key at: {url}", Colors.DIM)) + try: + import getpass + value = getpass.getpass(color(f" {var}: ", Colors.YELLOW)) + except (KeyboardInterrupt, EOFError): + print() + break + if value.strip(): + save_env_value(var, value.strip()) + print(color(f" ✓ Saved", Colors.GREEN)) + else: + print(color(f" Skipped", Colors.DIM)) + else: + print(color(" Skipped — configure later with 'hermes setup'", Colors.DIM)) + + def tools_command(args): """Entry point for `hermes tools`.""" config = load_config() @@ -243,8 +306,6 @@ def tools_command(args): new_enabled = _prompt_toolset_checklist(pinfo["label"], current_enabled) if new_enabled != current_enabled: - _save_platform_tools(config, pkey, new_enabled) - added = new_enabled - current_enabled removed = current_enabled - new_enabled @@ -257,6 +318,11 @@ def tools_command(args): label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts) print(color(f" - {label}", Colors.RED)) + # Prompt for missing API keys on newly enabled toolsets + if added: + _check_and_prompt_requirements(added) + + _save_platform_tools(config, pkey, new_enabled) print(color(f" ✓ Saved {pinfo['label']} configuration", Colors.GREEN)) else: print(color(f" No changes to {pinfo['label']}", Colors.DIM)) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 128997ba51..6a72f7c17e 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -29,6 +29,7 @@ $ErrorActionPreference = "Stop" $RepoUrlSsh = "git@github.com:NousResearch/hermes-agent.git" $RepoUrlHttps = "https://github.com/NousResearch/hermes-agent.git" $PythonVersion = "3.11" +$NodeVersion = "22" # ============================================================================ # Helper functions @@ -174,111 +175,202 @@ function Test-Git { } function Test-Node { - Write-Info "Checking Node.js (optional, for browser tools)..." - + Write-Info "Checking Node.js (for browser tools)..." + if (Get-Command node -ErrorAction SilentlyContinue) { $version = node --version Write-Success "Node.js $version found" $script:HasNode = $true return $true } - - Write-Warn "Node.js not found (browser tools will be limited)" - Write-Info "To install Node.js (optional):" - Write-Info " https://nodejs.org/en/download/" + + # Check our own managed install from a previous run + $managedNode = "$HermesHome\node\node.exe" + if (Test-Path $managedNode) { + $version = & $managedNode --version + $env:Path = "$HermesHome\node;$env:Path" + Write-Success "Node.js $version found (Hermes-managed)" + $script:HasNode = $true + return $true + } + + Write-Info "Node.js not found — installing Node.js $NodeVersion LTS..." + + # Try winget first (cleanest on modern Windows) + if (Get-Command winget -ErrorAction SilentlyContinue) { + Write-Info "Installing via winget..." + try { + winget install OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + # Refresh PATH + $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") + if (Get-Command node -ErrorAction SilentlyContinue) { + $version = node --version + Write-Success "Node.js $version installed via winget" + $script:HasNode = $true + return $true + } + } catch { } + } + + # Fallback: download binary zip to ~/.hermes/node/ + Write-Info "Downloading Node.js $NodeVersion binary..." + try { + $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } + $indexUrl = "https://nodejs.org/dist/latest-v${NodeVersion}.x/" + $indexPage = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing + $zipName = ($indexPage.Content | Select-String -Pattern "node-v${NodeVersion}\.\d+\.\d+-win-${arch}\.zip" -AllMatches).Matches[0].Value + + if ($zipName) { + $downloadUrl = "${indexUrl}${zipName}" + $tmpZip = "$env:TEMP\$zipName" + $tmpDir = "$env:TEMP\hermes-node-extract" + + Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing + if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir } + Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force + + $extractedDir = Get-ChildItem $tmpDir -Directory | Select-Object -First 1 + if ($extractedDir) { + if (Test-Path "$HermesHome\node") { Remove-Item -Recurse -Force "$HermesHome\node" } + Move-Item $extractedDir.FullName "$HermesHome\node" + $env:Path = "$HermesHome\node;$env:Path" + + $version = & "$HermesHome\node\node.exe" --version + Write-Success "Node.js $version installed to ~/.hermes/node/" + $script:HasNode = $true + + Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue + return $true + } + } + } catch { + Write-Warn "Download failed: $_" + } + + Write-Warn "Could not auto-install Node.js" + Write-Info "Install manually: https://nodejs.org/en/download/" $script:HasNode = $false - return $true # Don't fail - Node is optional + return $true } -function Test-Ripgrep { - Write-Info "Checking ripgrep (optional, for faster file search)..." - +function Install-SystemPackages { + $script:HasRipgrep = $false + $script:HasFfmpeg = $false + $needRipgrep = $false + $needFfmpeg = $false + + Write-Info "Checking ripgrep (fast file search)..." if (Get-Command rg -ErrorAction SilentlyContinue) { $version = rg --version | Select-Object -First 1 Write-Success "$version found" $script:HasRipgrep = $true - return $true + } else { + $needRipgrep = $true } - - Write-Warn "ripgrep not found (file search will use findstr fallback)" - - # Check what package managers are available + + Write-Info "Checking ffmpeg (TTS voice messages)..." + if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { + Write-Success "ffmpeg found" + $script:HasFfmpeg = $true + } else { + $needFfmpeg = $true + } + + if (-not $needRipgrep -and -not $needFfmpeg) { return } + + # Build description and package lists for each package manager + $descParts = @() + $wingetPkgs = @() + $chocoPkgs = @() + $scoopPkgs = @() + + if ($needRipgrep) { + $descParts += "ripgrep for faster file search" + $wingetPkgs += "BurntSushi.ripgrep.MSVC" + $chocoPkgs += "ripgrep" + $scoopPkgs += "ripgrep" + } + if ($needFfmpeg) { + $descParts += "ffmpeg for TTS voice messages" + $wingetPkgs += "Gyan.FFmpeg" + $chocoPkgs += "ffmpeg" + $scoopPkgs += "ffmpeg" + } + + $description = $descParts -join " and " $hasWinget = Get-Command winget -ErrorAction SilentlyContinue $hasChoco = Get-Command choco -ErrorAction SilentlyContinue $hasScoop = Get-Command scoop -ErrorAction SilentlyContinue - - # Offer to install - Write-Host "" - $response = Read-Host "Would you like to install ripgrep? (faster search, recommended) [Y/n]" - - if ($response -eq "" -or $response -match "^[Yy]") { - Write-Info "Installing ripgrep..." - - if ($hasWinget) { - try { - winget install BurntSushi.ripgrep.MSVC --silent 2>&1 | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Success "ripgrep installed via winget" - $script:HasRipgrep = $true - return $true - } - } catch { } - } - - if ($hasChoco) { - try { - choco install ripgrep -y 2>&1 | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Success "ripgrep installed via chocolatey" - $script:HasRipgrep = $true - return $true - } - } catch { } - } - - if ($hasScoop) { - try { - scoop install ripgrep 2>&1 | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Success "ripgrep installed via scoop" - $script:HasRipgrep = $true - return $true - } - } catch { } - } - - Write-Warn "Auto-install failed. You can install manually:" - } else { - Write-Info "Skipping ripgrep installation. To install manually:" - } - - # Show manual install instructions - Write-Info " winget install BurntSushi.ripgrep.MSVC" - Write-Info " Or: choco install ripgrep" - Write-Info " Or: scoop install ripgrep" - Write-Info " Or download from: https://github.com/BurntSushi/ripgrep/releases" - - $script:HasRipgrep = $false - return $true # Don't fail - ripgrep is optional -} -function Test-Ffmpeg { - Write-Info "Checking ffmpeg (optional, for TTS voice messages)..." - - if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { - $version = ffmpeg -version 2>&1 | Select-Object -First 1 - Write-Success "ffmpeg found" - $script:HasFfmpeg = $true - return $true + # Try winget first (most common on modern Windows) + if ($hasWinget) { + Write-Info "Installing $description via winget..." + foreach ($pkg in $wingetPkgs) { + try { + winget install $pkg --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + } catch { } + } + # Refresh PATH and recheck + $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") + if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { + Write-Success "ripgrep installed" + $script:HasRipgrep = $true + $needRipgrep = $false + } + if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { + Write-Success "ffmpeg installed" + $script:HasFfmpeg = $true + $needFfmpeg = $false + } + if (-not $needRipgrep -and -not $needFfmpeg) { return } + } + + # Fallback: choco + if ($hasChoco -and ($needRipgrep -or $needFfmpeg)) { + Write-Info "Trying Chocolatey..." + foreach ($pkg in $chocoPkgs) { + try { choco install $pkg -y 2>&1 | Out-Null } catch { } + } + if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { + Write-Success "ripgrep installed via chocolatey" + $script:HasRipgrep = $true + $needRipgrep = $false + } + if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { + Write-Success "ffmpeg installed via chocolatey" + $script:HasFfmpeg = $true + $needFfmpeg = $false + } + } + + # Fallback: scoop + if ($hasScoop -and ($needRipgrep -or $needFfmpeg)) { + Write-Info "Trying Scoop..." + foreach ($pkg in $scoopPkgs) { + try { scoop install $pkg 2>&1 | Out-Null } catch { } + } + if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) { + Write-Success "ripgrep installed via scoop" + $script:HasRipgrep = $true + $needRipgrep = $false + } + if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { + Write-Success "ffmpeg installed via scoop" + $script:HasFfmpeg = $true + $needFfmpeg = $false + } + } + + # Show manual instructions for anything still missing + if ($needRipgrep) { + Write-Warn "ripgrep not installed (file search will use findstr fallback)" + Write-Info " winget install BurntSushi.ripgrep.MSVC" + } + if ($needFfmpeg) { + Write-Warn "ffmpeg not installed (TTS voice messages will be limited)" + Write-Info " winget install Gyan.FFmpeg" } - - Write-Warn "ffmpeg not found (TTS voice bubbles on Telegram will send as audio files instead)" - Write-Info " Install with: winget install ffmpeg" - Write-Info " Or: choco install ffmpeg" - Write-Info " Or download from: https://ffmpeg.org/download.html" - - $script:HasFfmpeg = $false - return $true # Don't fail - ffmpeg is optional } # ============================================================================ @@ -651,14 +743,14 @@ function Write-Completion { Write-Host "" if (-not $HasNode) { - Write-Host "Note: Node.js was not found. Browser automation tools" -ForegroundColor Yellow - Write-Host "will have limited functionality." -ForegroundColor Yellow + Write-Host "Note: Node.js could not be installed automatically." -ForegroundColor Yellow + Write-Host "Browser tools need Node.js. Install manually:" -ForegroundColor Yellow + Write-Host " https://nodejs.org/en/download/" -ForegroundColor Yellow Write-Host "" } if (-not $HasRipgrep) { - Write-Host "Note: ripgrep (rg) was not found. File search will use" -ForegroundColor Yellow - Write-Host "findstr as a fallback. For faster search:" -ForegroundColor Yellow + Write-Host "Note: ripgrep (rg) was not installed. For faster file search:" -ForegroundColor Yellow Write-Host " winget install BurntSushi.ripgrep.MSVC" -ForegroundColor Yellow Write-Host "" } @@ -674,9 +766,8 @@ function Main { if (-not (Install-Uv)) { exit 1 } if (-not (Test-Python)) { exit 1 } if (-not (Test-Git)) { exit 1 } - Test-Node # Optional, doesn't fail - Test-Ripgrep # Optional, doesn't fail - Test-Ffmpeg # Optional, doesn't fail + Test-Node # Auto-installs if missing + Install-SystemPackages # ripgrep + ffmpeg in one step Install-Repository Install-Venv