feat: add API key requirement checks for toolsets
- Introduced a new mapping for toolset environment variable requirements, enhancing the configuration process by prompting users for missing API keys. - Implemented a function to check and prompt users for necessary API keys when enabling toolsets, improving user experience and ensuring proper setup. - Updated the tools command to integrate the new API key checks, streamlining the configuration workflow for users.
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user