Skip to content

Fixed the current issues with links to github in setup-windows.ps1 #15

Description

@Timthebrewer1

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions