Fixed the current issues with links to github not updating and added code to auto update in setup-windows.ps1
Couldn't attach files so here is the code.
============================================================================
Hermes Portable - Windows Runtime Setup
============================================================================
Downloads and installs portable Python, Node.js, uv, ripgrep, Git,
clones Hermes source, creates venv, and installs dependencies.
============================================================================
param(
[Parameter(Mandatory = $true)]
[string]$Root
)
$ErrorActionPreference = "Stop"
---------------------------------------------------------------------------
Paths
---------------------------------------------------------------------------
$CacheDir = Join-Path $Root ".cache"
$RuntimeDir = Join-Path $CacheDir "runtimes\windows-x64"
$SrcDir = Join-Path $Root "src"
$BinDir = Join-Path $RuntimeDir "bin"
$TempDir = Join-Path $RuntimeDir "_temp"
New-Item -ItemType Directory -Force -Path $RuntimeDir, $SrcDir, $BinDir, $TempDir | Out-Null
---------------------------------------------------------------------------
Download URLs — auto-updated at runtime via GitHub API
Fallback pinned versions used if API is unreachable
---------------------------------------------------------------------------
function Get-GitHubLatestRelease($repo) {
try {
$headers = @{ "User-Agent" = "hermes-usb-setup"; "Accept" = "application/vnd.github+json" }
$url = "https://api.github.com/repos/$repo/releases/latest"
$response = Invoke-RestMethod -Uri $url -Headers $headers -TimeoutSec 10 -ErrorAction Stop
return $response.tag_name
} catch {
return $null
}
}
function Get-GitHubLatestTag($repo) {
try {
$headers = @{ "User-Agent" = "hermes-usb-setup"; "Accept" = "application/vnd.github+json" }
$url = "https://api.github.com/repos/$repo/tags?per_page=1"
$response = Invoke-RestMethod -Uri $url -Headers $headers -TimeoutSec 10 -ErrorAction Stop
if ($response.Count -gt 0) { return $response[0].name }
return $null
} catch {
return $null
}
}
function Test-UrlExists($url) {
try {
$req = [System.Net.WebRequest]::Create($url)
$req.Method = "HEAD"
$req.Timeout = 8000
$resp = $req.GetResponse()
$resp.Close()
return $true
} catch {
return $false
}
}
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Checking for latest component versions..." -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
--- Python (astral-sh/python-build-standalone) ---
$PythonFallbackTag = "20250409"
$PythonFallbackVer = "3.11.12"
$PythonTag = $PythonFallbackTag
$PythonVer = $PythonFallbackVer
$latestPythonTag = Get-GitHubLatestTag "astral-sh/python-build-standalone"
if ($latestPythonTag) {
# Find the latest 3.11.x asset that exists for windows msvc
$candidateUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/$latestPythonTag/cpython-3.11.12+$latestPythonTag-x86_64-pc-windows-msvc-install_only.tar.gz"
if (Test-UrlExists $candidateUrl) {
$PythonTag = $latestPythonTag
Write-Host " [OK] Python build: $PythonTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Python build: keeping $PythonFallbackTag (asset not found for $latestPythonTag)" -ForegroundColor Yellow
}
} else {
Write-Host " [--] Python build: keeping $PythonFallbackTag (API unreachable)" -ForegroundColor Yellow
}
$PythonUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/$PythonTag/cpython-$PythonVer+$PythonTag-x86_64-pc-windows-msvc-install_only.tar.gz"
--- Node.js 22 LTS ---
$NodeFallback = "22.22.3"
$NodeVer = $NodeFallback
try {
$nodeHeaders = @{ "User-Agent" = "hermes-usb-setup" }
$nodeIndex = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" -Headers $nodeHeaders -TimeoutSec 10 -ErrorAction Stop
$latest22 = $nodeIndex | Where-Object { $.version -match "^v22." -and $.lts } | Select-Object -First 1
if ($latest22) {
$NodeVer = $latest22.version.TrimStart("v")
Write-Host " [OK] Node.js: v$NodeVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Node.js: keeping v$NodeFallback (no LTS found)" -ForegroundColor Yellow
}
} catch {
Write-Host " [--] Node.js: keeping v$NodeFallback (API unreachable)" -ForegroundColor Yellow
}
$NodeUrl = "https://nodejs.org/dist/v$NodeVer/node-v$NodeVer-win-x64.zip"
--- uv ---
$UvFallback = "0.11.16"
$UvVer = $UvFallback
$latestUv = Get-GitHubLatestRelease "astral-sh/uv"
if ($latestUv) {
$UvVer = $latestUv.TrimStart("v")
Write-Host " [OK] uv: v$UvVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] uv: keeping v$UvFallback (API unreachable)" -ForegroundColor Yellow
}
$UvUrl = "https://github.com/astral-sh/uv/releases/download/$UvVer/uv-x86_64-pc-windows-msvc.zip"
--- ripgrep ---
$RgFallback = "15.1.0"
$RgVer = $RgFallback
$latestRg = Get-GitHubLatestRelease "BurntSushi/ripgrep"
if ($latestRg) {
$RgVer = $latestRg.TrimStart("v")
Write-Host " [OK] ripgrep: v$RgVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] ripgrep: keeping v$RgFallback (API unreachable)" -ForegroundColor Yellow
}
$RgUrl = "https://github.com/BurntSushi/ripgrep/releases/download/$RgVer/ripgrep-$RgVer-x86_64-pc-windows-msvc.zip"
--- MinGit ---
$GitFallbackTag = "v2.53.0.windows.2"
$GitFallbackVer = "2.53.0"
$GitTag = $GitFallbackTag
$GitVer = $GitFallbackVer
$latestGit = Get-GitHubLatestRelease "git-for-windows/git"
if ($latestGit) {
# Extract version number from tag like "v2.53.0.windows.2"
if ($latestGit -match "v(\d+.\d+.\d+).windows") {
$GitTag = $latestGit
$GitVer = $Matches[1]
Write-Host " [OK] MinGit: $GitTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] MinGit: keeping $GitFallbackTag (unexpected tag format: $latestGit)" -ForegroundColor Yellow
}
} else {
Write-Host " [--] MinGit: keeping $GitFallbackTag (API unreachable)" -ForegroundColor Yellow
}
$GitUrl = "https://github.com/git-for-windows/git/releases/download/$GitTag/MinGit-$GitVer-64-bit.zip"
--- Hermes Agent source ---
$HermesFallback = "v2026.5.28"
$HermesTag = $HermesFallback
$latestHermes = Get-GitHubLatestRelease "NousResearch/hermes-agent"
if ($latestHermes) {
$HermesTag = $latestHermes
Write-Host " [OK] Hermes Agent: $HermesTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Hermes Agent: keeping $HermesFallback (API unreachable)" -ForegroundColor Yellow
}
$SourceUrl = "https://github.com/NousResearch/hermes-agent/archive/refs/tags/$HermesTag.zip"
---------------------------------------------------------------------------
Confirm before proceeding
---------------------------------------------------------------------------
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Components to be installed:" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Python: $PythonVer (build $PythonTag)"
Write-Host " Node.js: v$NodeVer"
Write-Host " uv: v$UvVer"
Write-Host " ripgrep: v$RgVer"
Write-Host " MinGit: $GitTag"
Write-Host " Hermes Agent: $HermesTag"
Write-Host ""
Write-Host " Install root: $Root"
Write-Host ""
$confirm = Read-Host "Proceed with installation? [Y/n]"
if ($confirm -match "^[Nn]") {
Write-Host "Setup cancelled." -ForegroundColor Yellow
exit 0
}
---------------------------------------------------------------------------
Helpers
---------------------------------------------------------------------------
function Write-Step($msg) {
Write-Host ""
Write-Host "[SETUP] $msg" -ForegroundColor Cyan
}
function Write-Done($msg) {
Write-Host "[OK] $msg" -ForegroundColor Green
}
function Write-Warn($msg) {
Write-Host "[WARN] $msg" -ForegroundColor Yellow
}
function Download-File($Url, $OutFile) {
$name = Split-Path $Url -Leaf
if (Test-Path $OutFile) {
$size = (Get-Item $OutFile).Length
if ($size -gt 0) {
$sizeMB = [math]::Round($size / 1048576, 2)
$msg = " " + $name + " already cached (" + $sizeMB + " MB)."
Write-Host $msg
return
} else {
Write-Warn ($name + " exists but is 0 bytes - re-downloading ...")
Remove-Item $OutFile -Force
}
}
$msg1 = " Downloading " + $name + " ..."
$msg2 = " URL: " + $Url
Write-Host $msg1 -ForegroundColor Cyan
Write-Host $msg2 -ForegroundColor DarkGray
# Force HTTP/1.1 — Windows curl uses Schannel which has a known issue dropping
# HTTP/2 streams mid-transfer, causing "Recv failure: Connection was reset" at
# consistent file sizes. --http1.1 bypasses this entirely.
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
$maxAttempts = 5
$success = $false
for ($attempt = 1; $attempt -le $maxAttempts -and -not $success; $attempt++) {
if ($attempt -gt 1) {
Write-Host (" Retry $attempt/$maxAttempts...") -ForegroundColor Yellow
Start-Sleep -Seconds (4 * $attempt)
}
& curl.exe -L -f --http1.1 --ssl-no-revoke --connect-timeout 30 --max-time 900 -o $OutFile $Url
if ($LASTEXITCODE -eq 0) {
$success = $true
} else {
if (Test-Path $OutFile) { Remove-Item $OutFile -Force }
}
}
if (-not $success) {
throw "curl.exe failed after $maxAttempts attempts for $name"
}
} else {
$ProgressPreference = 'Continue'
try {
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -TimeoutSec 900
}
catch {
if (Test-Path $OutFile) { Remove-Item $OutFile -Force }
throw "Failed to download " + $name + ": " + $_
}
finally {
$ProgressPreference = 'Continue'
}
}
# Validate downloaded file
if (-not (Test-Path $OutFile)) {
throw "Download succeeded but file not found: " + $OutFile
}
$downloadedSize = (Get-Item $OutFile).Length
if ($downloadedSize -eq 0) {
Remove-Item $OutFile -Force
throw "Downloaded file is 0 bytes: " + $name
}
$sizeMB = [math]::Round($downloadedSize / 1048576, 2)
$msgDone = " Download complete: " + $sizeMB + " MB."
Write-Host $msgDone -ForegroundColor Green
}
function Extract-TarGz($Archive, $Destination) {
$label = Split-Path $Archive -Leaf
Write-Host " Extracting $label ..." -NoNewline
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
# Use Windows built-in tar to avoid Git Bash tar path issues
$winTar = "C:\Windows\System32\tar.exe"
if (Test-Path $winTar) {
& $winTar -xzf "$Archive" -C "$Destination" --strip-components=1
} else {
& tar.exe -xzf "$Archive" -C "$Destination" --strip-components=1
}
if ($LASTEXITCODE -ne 0) {
Remove-Item $Destination -Recurse -Force -ErrorAction SilentlyContinue
throw "tar extraction failed for " + $label
}
Write-Host " done" -ForegroundColor Green
}
function Extract-Zip($Archive, $Destination) {
$label = Split-Path $Archive -Leaf
Write-Host " Extracting $label ..." -NoNewline
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
try {
$extracted = $false
$winTar = "C:\Windows\System32\tar.exe"
if (Test-Path $winTar) {
& $winTar -xf "$Archive" -C "$Destination"
if ($LASTEXITCODE -eq 0) {
$extracted = $true
}
} elseif (Get-Command tar.exe -ErrorAction SilentlyContinue) {
& tar.exe -xf "$Archive" -C "$Destination"
if ($LASTEXITCODE -eq 0) {
$extracted = $true
}
}
if (-not $extracted) {
Expand-Archive -Path $Archive -DestinationPath $Destination -Force
}
if (-not (Get-ChildItem $Destination -Force | Select-Object -First 1)) {
throw "archive extracted with no files"
}
} catch {
Remove-Item $Destination -Recurse -Force -ErrorAction SilentlyContinue
throw "zip extraction failed for " + $label + ": " + $_
}
Write-Host " done" -ForegroundColor Green
}
function Move-SubfolderContents($Source, $Dest) {
$sub = Get-ChildItem $Source -Directory | Select-Object -First 1
if ($sub) {
if (Test-Path $Dest) {
Remove-Item $Dest -Recurse -Force
}
Move-Item $sub.FullName $Dest -Force
Remove-Item $Source -Recurse -Force -ErrorAction SilentlyContinue
}
}
---------------------------------------------------------------------------
Health check: if ready.flag exists but core files are missing, start fresh
---------------------------------------------------------------------------
$readyFlag = Join-Path $RuntimeDir "ready.flag"
if (Test-Path $readyFlag) {
$coreFiles = @("python\python.exe", "uv\uv.exe", "venv\Scripts\hermes.exe")
$missing = $coreFiles | Where-Object { -not (Test-Path (Join-Path $RuntimeDir $_)) }
if ($missing) {
Write-Warn "ready.flag exists but core files are missing - restarting setup ..."
Remove-Item $readyFlag -Force
}
}
---------------------------------------------------------------------------
1. Portable Python
---------------------------------------------------------------------------
Write-Step "Installing portable Python 3.11 ..."
$pyArchive = Join-Path $RuntimeDir "python.tar.gz"
Download-File $PythonUrl $pyArchive
Extract-TarGz $pyArchive (Join-Path $RuntimeDir "python")
Write-Done "Python ready"
---------------------------------------------------------------------------
2. Node.js
---------------------------------------------------------------------------
Write-Step "Installing Node.js 22 LTS ..."
$nodeArchive = Join-Path $RuntimeDir "node.zip"
Download-File $NodeUrl $nodeArchive
$nodeTemp = Join-Path $TempDir "node"
Extract-Zip $nodeArchive $nodeTemp
Move-SubfolderContents $nodeTemp (Join-Path $RuntimeDir "node")
Write-Done "Node.js ready"
---------------------------------------------------------------------------
3. uv (Python package manager)
---------------------------------------------------------------------------
Write-Step "Installing uv ..."
$uvArchive = Join-Path $RuntimeDir "uv.zip"
Download-File $UvUrl $uvArchive
Extract-Zip $uvArchive (Join-Path $RuntimeDir "uv")
Write-Done "uv ready"
---------------------------------------------------------------------------
4. ripgrep
---------------------------------------------------------------------------
Write-Step "Installing ripgrep ..."
$rgArchive = Join-Path $RuntimeDir "rg.zip"
Download-File $RgUrl $rgArchive
$rgTemp = Join-Path $TempDir "rg"
Extract-Zip $rgArchive $rgTemp
$rgExe = Get-ChildItem $rgTemp -Recurse -Filter "rg.exe" | Select-Object -First 1
if ($rgExe) {
Copy-Item $rgExe.FullName (Join-Path $BinDir "rg.exe") -Force
Write-Done "ripgrep ready"
} else {
Write-Warn "ripgrep exe not found in archive"
}
---------------------------------------------------------------------------
5. Git (MinGit) - optional
---------------------------------------------------------------------------
Write-Step "Installing portable Git (optional) ..."
$gitArchive = Join-Path $RuntimeDir "git.zip"
try {
Download-File $GitUrl $gitArchive
Extract-Zip $gitArchive (Join-Path $RuntimeDir "git")
Write-Done "Git ready"
} catch {
Write-Warn "Git download failed - continuing without it (not required for core functionality)"
}
---------------------------------------------------------------------------
6. Hermes source code
---------------------------------------------------------------------------
Write-Step "Downloading Hermes Agent source code ..."
$srcArchive = Join-Path $RuntimeDir "source.zip"
Download-File $SourceUrl $srcArchive
$srcTemp = Join-Path $TempDir "source"
Extract-Zip $srcArchive $srcTemp
$srcSub = Get-ChildItem $srcTemp -Directory | Select-Object -First 1
if (-not $srcSub) {
throw "Hermes source archive did not contain a source folder"
}
$destSrc = Join-Path $SrcDir "hermes-agent"
if (Test-Path $destSrc) { Remove-Item $destSrc -Recurse -Force }
Move-Item $srcSub.FullName $destSrc -Force
Write-Done "Source code ready"
---------------------------------------------------------------------------
7. Create virtual environment
---------------------------------------------------------------------------
Write-Step "Creating Python virtual environment ..."
$pythonExe = Join-Path $RuntimeDir "python\python.exe"
$venvDir = Join-Path $RuntimeDir "venv"
$uvExe = Join-Path $RuntimeDir "uv\uv.exe"
& $uvExe venv $venvDir --python $pythonExe
if ($LASTEXITCODE -ne 0) { throw "Failed to create venv" }
Write-Done "Virtual environment ready"
---------------------------------------------------------------------------
8. Install Hermes dependencies
---------------------------------------------------------------------------
$ErrorActionPreference = "Continue"
Write-Step "Installing Hermes Python dependencies ..."
Write-Host " This may take 3-10 minutes depending on your connection."
$venvPython = Join-Path $venvDir "Scripts\python.exe"
Try uv first (faster), fall back to pip on unsupported filesystem (e.g. ExFAT)
$uvResult = & $uvExe pip install --python $venvPython --link-mode=copy -e "$destSrc[all]" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host " uv install failed - falling back to pip ..."
& $venvPython -m ensurepip --upgrade | Out-Null
& $venvPython -m pip install -e "$destSrc[all]"
if ($LASTEXITCODE -ne 0) { throw "Failed to install Hermes dependencies" }
}
Write-Done "Dependencies installed"
---------------------------------------------------------------------------
9. Install messaging dependencies (Telegram, etc.)
---------------------------------------------------------------------------
Hermes [all] intentionally excludes messaging deps for size.
The lazy-install system is supposed to auto-install on first use,
but it can fail silently in some environments. Pre-install here
so Telegram works out of the box.
---------------------------------------------------------------------------
Write-Step "Installing messaging dependencies (Telegram) ..."
$tgResult = & $uvExe pip install --python $venvPython --link-mode=copy "python-telegram-bot[webhooks]==22.6" 2>&1
if ($LASTEXITCODE -ne 0) {
& $venvPython -m pip install "python-telegram-bot[webhooks]==22.6" 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Done "python-telegram-bot ready"
} else {
Write-Warn "python-telegram-bot install failed - will retry on first use"
}
} else {
Write-Done "python-telegram-bot ready"
}
---------------------------------------------------------------------------
10. Install Playwright browsers (optional, for web tools)
---------------------------------------------------------------------------
Write-Step "Installing Playwright browsers (optional) ..."
$env:PLAYWRIGHT_BROWSERS_PATH = Join-Path $RuntimeDir "playwright"
try {
& $venvPython -m playwright install chromium 2>$null
Write-Done "Playwright browsers ready"
} catch {
Write-Warn "Playwright browser install failed (web tools may be limited)"
}
---------------------------------------------------------------------------
11. Mark ready
---------------------------------------------------------------------------
"" | Out-File (Join-Path $RuntimeDir "ready.flag") -Encoding utf8
Cleanup temp
Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host " Setup Complete! Launching Hermes..." -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Start-Sleep -Seconds 1
Fixed the current issues with links to github not updating and added code to auto update in setup-windows.ps1
Couldn't attach files so here is the code.
============================================================================
Hermes Portable - Windows Runtime Setup
============================================================================
Downloads and installs portable Python, Node.js, uv, ripgrep, Git,
clones Hermes source, creates venv, and installs dependencies.
============================================================================
param(
[Parameter(Mandatory = $true)]
[string]$Root
)
$ErrorActionPreference = "Stop"
---------------------------------------------------------------------------
Paths
---------------------------------------------------------------------------
$CacheDir = Join-Path $Root ".cache"
$RuntimeDir = Join-Path $CacheDir "runtimes\windows-x64"
$SrcDir = Join-Path $Root "src"
$BinDir = Join-Path $RuntimeDir "bin"
$TempDir = Join-Path $RuntimeDir "_temp"
New-Item -ItemType Directory -Force -Path $RuntimeDir, $SrcDir, $BinDir, $TempDir | Out-Null
---------------------------------------------------------------------------
Download URLs — auto-updated at runtime via GitHub API
Fallback pinned versions used if API is unreachable
---------------------------------------------------------------------------
function Get-GitHubLatestRelease($repo) {
try {
$headers = @{ "User-Agent" = "hermes-usb-setup"; "Accept" = "application/vnd.github+json" }
$url = "https://api.github.com/repos/$repo/releases/latest"
$response = Invoke-RestMethod -Uri $url -Headers $headers -TimeoutSec 10 -ErrorAction Stop
return $response.tag_name
} catch {
return $null
}
}
function Get-GitHubLatestTag($repo) {
try {
$headers = @{ "User-Agent" = "hermes-usb-setup"; "Accept" = "application/vnd.github+json" }
$url = "https://api.github.com/repos/$repo/tags?per_page=1"
$response = Invoke-RestMethod -Uri $url -Headers $headers -TimeoutSec 10 -ErrorAction Stop
if ($response.Count -gt 0) { return $response[0].name }
return $null
} catch {
return $null
}
}
function Test-UrlExists($url) {
try {
$req = [System.Net.WebRequest]::Create($url)
$req.Method = "HEAD"
$req.Timeout = 8000
$resp = $req.GetResponse()
$resp.Close()
return $true
} catch {
return $false
}
}
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Checking for latest component versions..." -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
--- Python (astral-sh/python-build-standalone) ---
$PythonFallbackTag = "20250409"
$PythonFallbackVer = "3.11.12"
$PythonTag = $PythonFallbackTag
$PythonVer = $PythonFallbackVer
$latestPythonTag = Get-GitHubLatestTag "astral-sh/python-build-standalone"
if ($latestPythonTag) {
# Find the latest 3.11.x asset that exists for windows msvc
$candidateUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/$latestPythonTag/cpython-3.11.12+$latestPythonTag-x86_64-pc-windows-msvc-install_only.tar.gz"
if (Test-UrlExists $candidateUrl) {
$PythonTag = $latestPythonTag
Write-Host " [OK] Python build: $PythonTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Python build: keeping $PythonFallbackTag (asset not found for $latestPythonTag)" -ForegroundColor Yellow
}
} else {
Write-Host " [--] Python build: keeping $PythonFallbackTag (API unreachable)" -ForegroundColor Yellow
}
$PythonUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/$PythonTag/cpython-$PythonVer+$PythonTag-x86_64-pc-windows-msvc-install_only.tar.gz"
--- Node.js 22 LTS ---
$NodeFallback = "22.22.3"$nodeIndex | Where-Object { $ .version -match "^v22." -and $.lts } | Select-Object -First 1
$NodeVer = $NodeFallback
try {
$nodeHeaders = @{ "User-Agent" = "hermes-usb-setup" }
$nodeIndex = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" -Headers $nodeHeaders -TimeoutSec 10 -ErrorAction Stop
$latest22 =
if ($latest22) {
$NodeVer = $latest22.version.TrimStart("v")
Write-Host " [OK] Node.js: v$NodeVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Node.js: keeping v$NodeFallback (no LTS found)" -ForegroundColor Yellow
}
} catch {
Write-Host " [--] Node.js: keeping v$NodeFallback (API unreachable)" -ForegroundColor Yellow
}
$NodeUrl = "https://nodejs.org/dist/v$NodeVer/node-v$NodeVer-win-x64.zip"
--- uv ---
$UvFallback = "0.11.16"
$UvVer = $UvFallback
$latestUv = Get-GitHubLatestRelease "astral-sh/uv"
if ($latestUv) {
$UvVer = $latestUv.TrimStart("v")
Write-Host " [OK] uv: v$UvVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] uv: keeping v$UvFallback (API unreachable)" -ForegroundColor Yellow
}
$UvUrl = "https://github.com/astral-sh/uv/releases/download/$UvVer/uv-x86_64-pc-windows-msvc.zip"
--- ripgrep ---
$RgFallback = "15.1.0"
$RgVer = $RgFallback
$latestRg = Get-GitHubLatestRelease "BurntSushi/ripgrep"
if ($latestRg) {
$RgVer = $latestRg.TrimStart("v")
Write-Host " [OK] ripgrep: v$RgVer (updated)" -ForegroundColor Green
} else {
Write-Host " [--] ripgrep: keeping v$RgFallback (API unreachable)" -ForegroundColor Yellow
}
$RgUrl = "https://github.com/BurntSushi/ripgrep/releases/download/$RgVer/ripgrep-$RgVer-x86_64-pc-windows-msvc.zip"
--- MinGit ---
$GitFallbackTag = "v2.53.0.windows.2"
$GitFallbackVer = "2.53.0"
$GitTag = $GitFallbackTag
$GitVer = $GitFallbackVer
$latestGit = Get-GitHubLatestRelease "git-for-windows/git"
if ($latestGit) {
# Extract version number from tag like "v2.53.0.windows.2"
if ($latestGit -match "v(\d+.\d+.\d+).windows") {
$GitTag = $latestGit
$GitVer = $Matches[1]
Write-Host " [OK] MinGit: $GitTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] MinGit: keeping $GitFallbackTag (unexpected tag format: $latestGit)" -ForegroundColor Yellow
}
} else {
Write-Host " [--] MinGit: keeping $GitFallbackTag (API unreachable)" -ForegroundColor Yellow
}
$GitUrl = "https://github.com/git-for-windows/git/releases/download/$GitTag/MinGit-$GitVer-64-bit.zip"
--- Hermes Agent source ---
$HermesFallback = "v2026.5.28"
$HermesTag = $HermesFallback
$latestHermes = Get-GitHubLatestRelease "NousResearch/hermes-agent"
if ($latestHermes) {
$HermesTag = $latestHermes
Write-Host " [OK] Hermes Agent: $HermesTag (updated)" -ForegroundColor Green
} else {
Write-Host " [--] Hermes Agent: keeping $HermesFallback (API unreachable)" -ForegroundColor Yellow
}
$SourceUrl = "https://github.com/NousResearch/hermes-agent/archive/refs/tags/$HermesTag.zip"
---------------------------------------------------------------------------
Confirm before proceeding
---------------------------------------------------------------------------
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Components to be installed:" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Python: $PythonVer (build $PythonTag)"
Write-Host " Node.js: v$NodeVer"
Write-Host " uv: v$UvVer"
Write-Host " ripgrep: v$RgVer"
Write-Host " MinGit: $GitTag"
Write-Host " Hermes Agent: $HermesTag"
Write-Host ""
Write-Host " Install root: $Root"
Write-Host ""
$confirm = Read-Host "Proceed with installation? [Y/n]"
if ($confirm -match "^[Nn]") {
Write-Host "Setup cancelled." -ForegroundColor Yellow
exit 0
}
---------------------------------------------------------------------------
Helpers
---------------------------------------------------------------------------
function Write-Step($msg) {
Write-Host ""
Write-Host "[SETUP] $msg" -ForegroundColor Cyan
}
function Write-Done($msg) {
Write-Host "[OK] $msg" -ForegroundColor Green
}
function Write-Warn($msg) {
Write-Host "[WARN] $msg" -ForegroundColor Yellow
}
function Download-File($Url, $OutFile) {
$name = Split-Path $Url -Leaf
if (Test-Path $OutFile) {
$size = (Get-Item $OutFile).Length
if ($size -gt 0) {
$sizeMB = [math]::Round($size / 1048576, 2)
$msg = " " + $name + " already cached (" + $sizeMB + " MB)."
Write-Host $msg
return
} else {
Write-Warn ($name + " exists but is 0 bytes - re-downloading ...")
Remove-Item $OutFile -Force
}
}
$msg1 = " Downloading " + $name + " ..."
$msg2 = " URL: " + $Url
Write-Host $msg1 -ForegroundColor Cyan
Write-Host $msg2 -ForegroundColor DarkGray
}
function Extract-TarGz($Archive, $Destination) {
$label = Split-Path $Archive -Leaf
Write-Host " Extracting $label ..." -NoNewline
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
# Use Windows built-in tar to avoid Git Bash tar path issues
$winTar = "C:\Windows\System32\tar.exe"
if (Test-Path $winTar) {
& $winTar -xzf "$Archive" -C "$Destination" --strip-components=1
} else {
& tar.exe -xzf "$Archive" -C "$Destination" --strip-components=1
}
if ($LASTEXITCODE -ne 0) {
Remove-Item $Destination -Recurse -Force -ErrorAction SilentlyContinue
throw "tar extraction failed for " + $label
}
Write-Host " done" -ForegroundColor Green
}
function Extract-Zip($Archive, $Destination) {
$label = Split-Path $Archive -Leaf
Write-Host " Extracting $label ..." -NoNewline
if (Test-Path $Destination) {
Remove-Item $Destination -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
try {
$extracted = $false
$winTar = "C:\Windows\System32\tar.exe"
if (Test-Path $winTar) {
& $winTar -xf "$Archive" -C "$Destination"
if ($LASTEXITCODE -eq 0) {
$extracted = $true
}
} elseif (Get-Command tar.exe -ErrorAction SilentlyContinue) {
& tar.exe -xf "$Archive" -C "$Destination"
if ($LASTEXITCODE -eq 0) {
$extracted = $true
}
}
}
function Move-SubfolderContents($Source, $Dest) {
$sub = Get-ChildItem $Source -Directory | Select-Object -First 1
if ($sub) {
if (Test-Path $Dest) {
Remove-Item $Dest -Recurse -Force
}
Move-Item $sub.FullName $Dest -Force
Remove-Item $Source -Recurse -Force -ErrorAction SilentlyContinue
}
}
---------------------------------------------------------------------------
Health check: if ready.flag exists but core files are missing, start fresh
---------------------------------------------------------------------------
$readyFlag = Join-Path $RuntimeDir "ready.flag"
if (Test-Path $readyFlag) {
$coreFiles = @("python\python.exe", "uv\uv.exe", "venv\Scripts\hermes.exe")
$missing = $coreFiles | Where-Object { -not (Test-Path (Join-Path $RuntimeDir $_)) }
if ($missing) {
Write-Warn "ready.flag exists but core files are missing - restarting setup ..."
Remove-Item $readyFlag -Force
}
}
---------------------------------------------------------------------------
1. Portable Python
---------------------------------------------------------------------------
Write-Step "Installing portable Python 3.11 ..."
$pyArchive = Join-Path $RuntimeDir "python.tar.gz"
Download-File $PythonUrl $pyArchive
Extract-TarGz $pyArchive (Join-Path $RuntimeDir "python")
Write-Done "Python ready"
---------------------------------------------------------------------------
2. Node.js
---------------------------------------------------------------------------
Write-Step "Installing Node.js 22 LTS ..."
$nodeArchive = Join-Path $RuntimeDir "node.zip"
Download-File $NodeUrl $nodeArchive
$nodeTemp = Join-Path $TempDir "node"
Extract-Zip $nodeArchive $nodeTemp
Move-SubfolderContents $nodeTemp (Join-Path $RuntimeDir "node")
Write-Done "Node.js ready"
---------------------------------------------------------------------------
3. uv (Python package manager)
---------------------------------------------------------------------------
Write-Step "Installing uv ..."
$uvArchive = Join-Path $RuntimeDir "uv.zip"
Download-File $UvUrl $uvArchive
Extract-Zip $uvArchive (Join-Path $RuntimeDir "uv")
Write-Done "uv ready"
---------------------------------------------------------------------------
4. ripgrep
---------------------------------------------------------------------------
Write-Step "Installing ripgrep ..."
$rgArchive = Join-Path $RuntimeDir "rg.zip"
Download-File $RgUrl $rgArchive
$rgTemp = Join-Path $TempDir "rg"
Extract-Zip $rgArchive $rgTemp
$rgExe = Get-ChildItem $rgTemp -Recurse -Filter "rg.exe" | Select-Object -First 1
if ($rgExe) {
Copy-Item $rgExe.FullName (Join-Path $BinDir "rg.exe") -Force
Write-Done "ripgrep ready"
} else {
Write-Warn "ripgrep exe not found in archive"
}
---------------------------------------------------------------------------
5. Git (MinGit) - optional
---------------------------------------------------------------------------
Write-Step "Installing portable Git (optional) ..."
$gitArchive = Join-Path $RuntimeDir "git.zip"
try {
Download-File $GitUrl $gitArchive
Extract-Zip $gitArchive (Join-Path $RuntimeDir "git")
Write-Done "Git ready"
} catch {
Write-Warn "Git download failed - continuing without it (not required for core functionality)"
}
---------------------------------------------------------------------------
6. Hermes source code
---------------------------------------------------------------------------
Write-Step "Downloading Hermes Agent source code ..."
$srcArchive = Join-Path $RuntimeDir "source.zip"
Download-File $SourceUrl $srcArchive
$srcTemp = Join-Path $TempDir "source"
Extract-Zip $srcArchive $srcTemp
$srcSub = Get-ChildItem $srcTemp -Directory | Select-Object -First 1
if (-not $srcSub) {
throw "Hermes source archive did not contain a source folder"
}
$destSrc = Join-Path $SrcDir "hermes-agent"
if (Test-Path $destSrc) { Remove-Item $destSrc -Recurse -Force }
Move-Item $srcSub.FullName $destSrc -Force
Write-Done "Source code ready"
---------------------------------------------------------------------------
7. Create virtual environment
---------------------------------------------------------------------------
Write-Step "Creating Python virtual environment ..."
$pythonExe = Join-Path $RuntimeDir "python\python.exe"
$venvDir = Join-Path $RuntimeDir "venv"
$uvExe = Join-Path $RuntimeDir "uv\uv.exe"
& $uvExe venv $venvDir --python $pythonExe
if ($LASTEXITCODE -ne 0) { throw "Failed to create venv" }
Write-Done "Virtual environment ready"
---------------------------------------------------------------------------
8. Install Hermes dependencies
---------------------------------------------------------------------------
$ErrorActionPreference = "Continue"
Write-Step "Installing Hermes Python dependencies ..."
Write-Host " This may take 3-10 minutes depending on your connection."
$venvPython = Join-Path $venvDir "Scripts\python.exe"
Try uv first (faster), fall back to pip on unsupported filesystem (e.g. ExFAT)
$uvResult = & $uvExe pip install --python $venvPython --link-mode=copy -e "$destSrc[all]" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host " uv install failed - falling back to pip ..."
& $venvPython -m ensurepip --upgrade | Out-Null
& $venvPython -m pip install -e "$destSrc[all]"
if ($LASTEXITCODE -ne 0) { throw "Failed to install Hermes dependencies" }
}
Write-Done "Dependencies installed"
---------------------------------------------------------------------------
9. Install messaging dependencies (Telegram, etc.)
---------------------------------------------------------------------------
Hermes [all] intentionally excludes messaging deps for size.
The lazy-install system is supposed to auto-install on first use,
but it can fail silently in some environments. Pre-install here
so Telegram works out of the box.
---------------------------------------------------------------------------
Write-Step "Installing messaging dependencies (Telegram) ..."
$tgResult = & $uvExe pip install --python $venvPython --link-mode=copy "python-telegram-bot[webhooks]==22.6" 2>&1
if ($LASTEXITCODE -ne 0) {
& $venvPython -m pip install "python-telegram-bot[webhooks]==22.6" 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Done "python-telegram-bot ready"
} else {
Write-Warn "python-telegram-bot install failed - will retry on first use"
}
} else {
Write-Done "python-telegram-bot ready"
}
---------------------------------------------------------------------------
10. Install Playwright browsers (optional, for web tools)
---------------------------------------------------------------------------
Write-Step "Installing Playwright browsers (optional) ..."
$env:PLAYWRIGHT_BROWSERS_PATH = Join-Path $RuntimeDir "playwright"
try {
& $venvPython -m playwright install chromium 2>$null
Write-Done "Playwright browsers ready"
} catch {
Write-Warn "Playwright browser install failed (web tools may be limited)"
}
---------------------------------------------------------------------------
11. Mark ready
---------------------------------------------------------------------------
"" | Out-File (Join-Path $RuntimeDir "ready.flag") -Encoding utf8
Cleanup temp
Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host " Setup Complete! Launching Hermes..." -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Start-Sleep -Seconds 1