codetether-agent 1.1.6-alpha-8.4

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
# CodeTether Agent Installer for Windows
# Usage: irm https://raw.githubusercontent.com/rileyseaburg/codetether-agent/main/install.ps1 | iex
#
# Installs the latest release of codetether to a directory on PATH.
# No Rust toolchain required.
#
# Tries multiple artifact formats to support both GitHub Actions (msvc+zip) and Jenkins (gnu+tar.gz) releases.
#
# Options:
#   -FunctionGemma      Download the FunctionGemma model for local tool-call routing (optional)
#   -FunctionGemmaOnly  Only download the FunctionGemma model (skip binary install)

param(
    [switch]$FunctionGemma,
    [switch]$FunctionGemmaOnly,
    [switch]$Help
)

$ErrorActionPreference = 'Stop'

$Repo = 'rileyseaburg/codetether-agent'
$BinaryName = 'codetether'
$InstallDir = Join-Path (Join-Path $env:LOCALAPPDATA 'codetether') 'bin'

# FunctionGemma model configuration
$DataDir = if ($env:XDG_DATA_HOME) { $env:XDG_DATA_HOME } else { Join-Path $env:LOCALAPPDATA 'codetether' }
$FunctionGemmaModelDir = Join-Path (Join-Path $DataDir 'models') 'functiongemma'
$FunctionGemmaModelUrl = 'https://huggingface.co/unsloth/functiongemma-270m-it-GGUF/resolve/main/functiongemma-270m-it-Q8_0.gguf'
$FunctionGemmaModelFile = 'functiongemma-270m-it-Q8_0.gguf'
$FunctionGemmaTokenizerUrl = 'https://huggingface.co/google/functiongemma-270m-it/resolve/main/tokenizer.json'
$FunctionGemmaTokenizerFile = 'tokenizer.json'

function Write-Info  { param([string]$Msg) Write-Host "info: " -ForegroundColor Cyan -NoNewline; Write-Host $Msg }
function Write-Warn  { param([string]$Msg) Write-Host "warn: " -ForegroundColor Yellow -NoNewline; Write-Host $Msg }
function Write-Err   { param([string]$Msg) Write-Host "error: " -ForegroundColor Red -NoNewline; Write-Host $Msg }
function Write-Ok    { param([string]$Msg) Write-Host "  ok: " -ForegroundColor Green -NoNewline; Write-Host $Msg }

function Get-Platform {
    $arch = $env:PROCESSOR_ARCHITECTURE
    switch ($arch) {
        'AMD64' { $archStr = 'x86_64' }
        'ARM64' { $archStr = 'aarch64' }
        default { Write-Err "Unsupported architecture: $arch"; exit 1 }
    }
    # Return array of targets to try (native first, then cross-compiled)
    return @(
        "$archStr-pc-windows-msvc",  # GitHub Actions native build
        "$archStr-pc-windows-gnu"     # Jenkins MinGW cross-compile
    )
}

function Get-LatestVersion {
    $apiUrl = "https://api.github.com/repos/$Repo/releases/latest"
    try {
        $response = Invoke-RestMethod -Uri $apiUrl -Headers @{ 'User-Agent' = 'codetether-installer' }
        return $response.tag_name
    }
    catch {
        Write-Err "Failed to fetch latest release from GitHub"
        exit 1
    }
}

function Install-FunctionGemma {
    Write-Host "`nFunctionGemma Model Setup`n" -NoNewline
    Write-Info "model directory: $FunctionGemmaModelDir"

    if (-not (Test-Path $FunctionGemmaModelDir)) {
        New-Item -ItemType Directory -Path $FunctionGemmaModelDir -Force | Out-Null
    }

    # Download GGUF model
    $modelPath = Join-Path $FunctionGemmaModelDir $FunctionGemmaModelFile
    if (Test-Path $modelPath) {
        Write-Ok "model already exists: $modelPath"
    }
    else {
        Write-Info "downloading FunctionGemma GGUF model (~292 MB)..."
        try {
            Invoke-WebRequest -Uri $FunctionGemmaModelUrl -OutFile $modelPath -UseBasicParsing
            Write-Ok "model downloaded: $modelPath"
        }
        catch {
            Write-Err "failed to download FunctionGemma model"
            Write-Warn "you can retry later: install.ps1 -FunctionGemmaOnly"
            return
        }
    }

    # Download tokenizer (gated model - requires HuggingFace authentication)
    $tokenizerPath = Join-Path $FunctionGemmaModelDir $FunctionGemmaTokenizerFile
    if (Test-Path $tokenizerPath) {
        Write-Ok "tokenizer already exists: $tokenizerPath"
    }
    else {
        $hfToken = $env:HF_TOKEN
        if (-not $hfToken) { $hfToken = $env:HUGGING_FACE_HUB_TOKEN }

        # Check huggingface-cli cached token
        if (-not $hfToken) {
            $hfCacheDir = if ($env:HF_HOME) { $env:HF_HOME } else { Join-Path (Join-Path $env:USERPROFILE '.cache') 'huggingface' }
            $hfCacheToken = Join-Path $hfCacheDir 'token'
            if (Test-Path $hfCacheToken) {
                $hfToken = (Get-Content $hfCacheToken -Raw).Trim()
                if ($hfToken) {
                    Write-Ok "found cached HuggingFace token (from huggingface-cli login)"
                }
            }
        }

        # Interactive prompt
        if (-not $hfToken) {
            Write-Host "`nHuggingFace Authentication Required" -ForegroundColor White
            Write-Host "  The FunctionGemma tokenizer is a gated model that requires"
            Write-Host "  a HuggingFace account with model access granted.`n"
            Write-Host "  Before continuing:"
            Write-Host "  Accept the model license at:"
            Write-Host "  https://huggingface.co/google/functiongemma-270m-it" -ForegroundColor Cyan
            Write-Host ""
            Write-Host "  Choose authentication method:"
            Write-Host "  [1] Open browser to create a token (recommended)" -ForegroundColor White
            Write-Host "  [2] Paste an existing token" -ForegroundColor White
            Write-Host "  [3] Skip tokenizer download" -ForegroundColor White
            Write-Host ""
            $choice = Read-Host "  Choice [1/2/3]"

            switch ($choice) {
                { $_ -eq '1' -or $_ -eq '' } {
                    $tokenUrl = 'https://huggingface.co/settings/tokens/new?tokenType=read&description=codetether-install'
                    Write-Info "opening browser..."
                    Start-Process $tokenUrl
                    Write-Host "  Create a read token, then paste it here."
                    $hfToken = Read-Host "  HuggingFace token"
                }
                '2' {
                    $hfToken = Read-Host "  HuggingFace token"
                }
                '3' {
                    Write-Warn "skipping tokenizer download"
                    Write-Warn "re-run later: `$env:HF_TOKEN='hf_...'; .\install.ps1 -FunctionGemmaOnly"
                    return
                }
            }
        }

        if (-not $hfToken) { $hfToken = '' }
        $hfToken = $hfToken.Trim()

        if (-not $hfToken) {
            Write-Warn "no token provided - skipping tokenizer download"
            Write-Warn "re-run later: `$env:HF_TOKEN='hf_...'; .\install.ps1 -FunctionGemmaOnly"
            return
        }

        Write-Info "downloading tokenizer (authenticated)..."
        try {
            $headers = @{
                'Authorization' = "Bearer $hfToken"
                'User-Agent'    = 'codetether-installer'
            }
            Invoke-WebRequest -Uri $FunctionGemmaTokenizerUrl -OutFile $tokenizerPath -Headers $headers -UseBasicParsing
            if ((Get-Item $tokenizerPath).Length -eq 0) {
                Remove-Item $tokenizerPath -Force
                throw "empty file"
            }
            Write-Ok "tokenizer downloaded: $tokenizerPath"
        }
        catch {
            if (Test-Path $tokenizerPath) { Remove-Item $tokenizerPath -Force }
            Write-Err "failed to download tokenizer (check token and model license access)"
            Write-Warn "1. Accept license: https://huggingface.co/google/functiongemma-270m-it"
            Write-Warn "2. Re-run: `$env:HF_TOKEN='hf_...'; .\install.ps1 -FunctionGemmaOnly"
            return
        }
    }

    Write-Ok "FunctionGemma installed to $FunctionGemmaModelDir"

    # Set persistent user environment variables
    $envVars = @{
        'CODETETHER_TOOL_ROUTER_ENABLED'        = 'true'
        'CODETETHER_TOOL_ROUTER_MODEL_PATH'     = $modelPath
        'CODETETHER_TOOL_ROUTER_TOKENIZER_PATH' = $tokenizerPath
    }

    foreach ($kv in $envVars.GetEnumerator()) {
        [System.Environment]::SetEnvironmentVariable($kv.Key, $kv.Value, 'User')
        Set-Item -Path "Env:$($kv.Key)" -Value $kv.Value
    }

    Write-Ok "FunctionGemma tool-call router is enabled"
    Write-Info "environment variables set for current and future sessions"
}

function Add-ToUserPath {
    param([string]$Dir)

    $currentPath = [System.Environment]::GetEnvironmentVariable('PATH', 'User')
    if ($currentPath -and ($currentPath -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $Dir.TrimEnd('\')) {
        return $false
    }

    $newPath = if ($currentPath) { "$Dir;$currentPath" } else { $Dir }
    [System.Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')

    # Also update current session
    if ($env:PATH -notlike "*$Dir*") {
        $env:PATH = "$Dir;$env:PATH"
    }
    return $true
}

# --- Main ---

if ($Help) {
    Write-Host @"
CodeTether Agent Installer for Windows

Usage: .\install.ps1 [OPTIONS]

Options:
  -FunctionGemma      Download the FunctionGemma model for local tool-call routing
  -FunctionGemmaOnly  Only download the FunctionGemma model
  -Help               Show this help message
"@
    exit 0
}

if ($FunctionGemmaOnly) {
    Install-FunctionGemma
    exit 0
}

Write-Host "`nCodeTether Agent Installer`n" -ForegroundColor White

# Detect platform targets to try
$platforms = Get-Platform
Write-Info "detected architecture: $($platforms[0].Split('-')[0])"

# Get latest version
Write-Info "fetching latest release..."
$version = Get-LatestVersion
if (-not $version) {
    Write-Err "could not determine latest version"
    exit 1
}
Write-Info "latest version: $version"

# Create temp directory
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "codetether-install-$([guid]::NewGuid().ToString('N').Substring(0,8))"
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null

$downloadSuccess = $false
$exePath = $null

try {
    # Try each platform + archive combination
    foreach ($platform in $platforms) {
        foreach ($archiveExt in @('zip', 'tar.gz')) {
            $artifactName = "codetether-$version-$platform"
            $archiveName = "$artifactName.$archiveExt"
            $url = "https://github.com/$Repo/releases/download/$version/$archiveName"
            
            Write-Info "trying $archiveName..."
            $archivePath = Join-Path $tmpDir $archiveName
            
            try {
                Invoke-WebRequest -Uri $url -OutFile $archivePath -UseBasicParsing -ErrorAction Stop
                Write-Ok "downloaded $archiveName"
                $downloadSuccess = $true
            }
            catch {
                continue
            }

            # Extract based on archive type
            Write-Info "extracting..."
            try {
                if ($archiveExt -eq 'zip') {
                    Expand-Archive -Path $archivePath -DestinationPath $tmpDir -Force
                }
                else {
                    # .tar.gz - use Windows built-in tar
                    $tarCmd = Get-Command tar -ErrorAction SilentlyContinue
                    if (-not $tarCmd) {
                        Write-Warn "tar command not found - skipping $archiveName"
                        continue
                    }
                    & tar -xzf $archivePath -C $tmpDir
                    if ($LASTEXITCODE -ne 0) {
                        Write-Warn "tar extraction failed - skipping $archiveName"
                        continue
                    }
                }
            }
            catch {
                Write-Warn "extraction failed for $archiveName - skipping"
                continue
            }

            # Find the binary
            $expectedExe = "$artifactName.exe"
            $exePath = Get-ChildItem -Path $tmpDir -Filter $expectedExe -Recurse | Select-Object -First 1
            if (-not $exePath) {
                # Fallback: try finding any .exe
                $exePath = Get-ChildItem -Path $tmpDir -Filter "codetether*.exe" -Recurse | Select-Object -First 1
            }
            
            if ($exePath) {
                Write-Ok "found binary: $($exePath.Name)"
                break
            }
        }
        
        if ($exePath) {
            break
        }
    }

    if (-not $downloadSuccess) {
        Write-Err "download failed - no pre-built binary available"
        Write-Err "tried platforms: $($platforms -join ', ')"
        Write-Err "you can build from source: cargo install codetether-agent"
        exit 1
    }

    if (-not $exePath) {
        Write-Err "expected binary not found in archive"
        Write-Info "archive contents:"
        Get-ChildItem -Path $tmpDir -Recurse | ForEach-Object { Write-Info "  $($_.Name)" }
        exit 1
    }

    # Ensure install directory exists
    if (-not (Test-Path $InstallDir)) {
        New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
    }

    # Copy binary
    $destPath = Join-Path $InstallDir "$BinaryName.exe"
    Copy-Item -Path $exePath.FullName -Destination $destPath -Force
    Write-Ok "installed $BinaryName $version to $destPath"

    # Add to PATH if not already there
    $pathAdded = Add-ToUserPath -Dir $InstallDir
    if ($pathAdded) {
        Write-Ok "added $InstallDir to user PATH"
    }

    # Verify installation
    $installed = Get-Command $BinaryName -ErrorAction SilentlyContinue
    if ($installed) {
        $installedVersion = & $BinaryName --version 2>$null
        Write-Ok $installedVersion
    }
    else {
        Write-Warn "$BinaryName may not be available until you restart your terminal"
    }

    Write-Host "`nGet started:" -ForegroundColor White
    Write-Host "  codetether tui       " -ForegroundColor Cyan -NoNewline; Write-Host "- interactive TUI"
    Write-Host "  codetether run `"...`" " -ForegroundColor Cyan -NoNewline; Write-Host "- single prompt"
    Write-Host "  codetether --help    " -ForegroundColor Cyan -NoNewline; Write-Host "- all commands"
    Write-Host ""

    # Install FunctionGemma model (optional, opt-in)
    if ($FunctionGemma) {
        Install-FunctionGemma
    }
    else {
        Write-Info "skipping FunctionGemma model (use -FunctionGemma to install)"
    }
}
finally {
    # Cleanup
    if (Test-Path $tmpDir) {
        Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
    }
}