gephyr 1.16.18

Gephyr is a headless local AI relay/proxy API handling OpenAI, Claude, and Gemini-compatible APIs
Documentation
[CmdletBinding(PositionalBinding = $false)]
param(
    [switch]$SkipBuild,
    [switch]$UseBuildCache,
    [switch]$SkipLogin,
    [switch]$NoBrowser,
    [switch]$RunApiTest,
    [switch]$DisableAdminAfter,
    [int]$Port = 8045,
    [string]$Image = "gephyr:latest",
    [string]$Model = "gpt-5.3-codex",
    [string]$Prompt = "hello from gephyr",
    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]]$CommandArgs
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

# Check Docker availability early to fail fast with a single message
function Test-DockerAvailable {
    try {
        $null = docker info 2>&1
        return $LASTEXITCODE -eq 0
    } catch {
        return $false
    }
}

$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$consoleScript = Join-Path $scriptDir "console.ps1"
$allowGuardScript = Join-Path $scriptDir "scripts/check-allow-attributes.ps1"

if (-not (Test-Path $consoleScript)) {
    throw "Missing script: $consoleScript"
}
if (-not (Test-Path $allowGuardScript)) {
    throw "Missing script: $allowGuardScript"
}

if ($CommandArgs -and $CommandArgs.Count -gt 0) {
    $firstArg = $CommandArgs[0].ToLowerInvariant()
    $passthroughCommands = @("status", "health", "accounts", "login", "restart", "api-test")

    if ($passthroughCommands -contains $firstArg) {
        Write-Host "Forwarding to console.ps1: $($CommandArgs -join ' ')"
        & $consoleScript @CommandArgs
        exit $LASTEXITCODE
    }

    throw "Unknown positional argument(s): $($CommandArgs -join ' '). Use named flags for test-clean.ps1 or call .\console.ps1 <command>."
}

if (-not (Test-DockerAvailable)) {
    Write-Host ""
    Write-Host "Docker is not running." -ForegroundColor Red
    Write-Host "The Docker daemon is not accessible." -ForegroundColor Yellow
    Write-Host "Please ensure:" -ForegroundColor Yellow
    Write-Host "  1. Docker Desktop is installed" -ForegroundColor Yellow
    Write-Host "  2. Docker Desktop is running (check system tray)" -ForegroundColor Yellow
    Write-Host "  3. Docker engine has finished starting up" -ForegroundColor Yellow
    Write-Host "On Windows, check the Docker whale icon in the system tray." -ForegroundColor Yellow
    Write-Host "If it is animating, Docker is still starting up." -ForegroundColor Yellow
    Write-Host ""
    exit 1
}

function Invoke-Step {
    param(
        [Parameter(Mandatory = $true)][string]$Name,
        [Parameter(Mandatory = $true)][scriptblock]$Action
    )

    Write-Host "==> $Name"
    & $Action
    Write-Host ""
}

function Invoke-DockerBuildWithGuidance {
    param(
        [Parameter(Mandatory = $true)][string[]]$Args
    )

    & docker @Args
    if ($LASTEXITCODE -ne 0) {
        Write-Host ""
        Write-Host "Docker build failed." -ForegroundColor Red
        Write-Host "Try repairing builder cache, then retry:" -ForegroundColor Yellow
        Write-Host "  .\console.ps1 docker-repair" -ForegroundColor Yellow
        Write-Host "If still failing, use aggressive mode:" -ForegroundColor Yellow
        Write-Host "  .\console.ps1 docker-repair -Aggressive" -ForegroundColor Yellow
        throw "Docker build failed with exit code $LASTEXITCODE"
    }
}

function Wait-OAuthAccountLink {
    param(
        [int]$TimeoutSec = 180,
        [int]$PollSec = 2
    )

    $apiKey = $env:API_KEY
    if (-not $apiKey) {
        $envPath = Join-Path $scriptDir ".env.local"
        if (Test-Path $envPath) {
            foreach ($raw in Get-Content $envPath) {
                $line = $raw.Trim()
                if ($line -and -not $line.StartsWith("#") -and $line.StartsWith("API_KEY=")) {
                    $apiKey = $line.Split("=", 2)[1].Trim().Trim([char]34).Trim([char]39)
                    break
                }
            }
        }
    }

    if (-not $apiKey) {
        Write-Warning "[W-OAUTH-MISSING-API-KEY] Skipping OAuth wait: API_KEY is missing (env and .env.local)."
        return $false
    }

    $headers = @{ Authorization = "Bearer $apiKey" }
    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    $startAt = Get-Date
    $nextProgressAt = $startAt.AddSeconds(10)
    $statusEndpointSupported = $true
    $lastKnownPhase = $null

    Write-Host "Waiting for OAuth callback/account link (timeout: ${TimeoutSec}s)..."
    Write-Host "Complete login in your browser, then this script will continue automatically."

    while ((Get-Date) -lt $deadline) {
        if ($statusEndpointSupported) {
            try {
                $statusResp = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/auth/status" -Headers $headers -Method Get -TimeoutSec 8
                $phase = ""
                if ($statusResp -and $statusResp.phase) {
                    $phase = "$($statusResp.phase)".ToLowerInvariant()
                }
                if ($phase) {
                    $lastKnownPhase = $phase
                }

                if ($phase -eq "linked") {
                    if ($statusResp.account_email) {
                        Write-Host "OAuth account linked ($($statusResp.account_email))."
                    } else {
                        Write-Host "OAuth account linked."
                    }
                    return $true
                }
                if ($phase -eq "failed") {
                    $detail = if ($statusResp -and $statusResp.detail) { "$($statusResp.detail)" } else { "unknown_error" }
                    Write-Warning "OAuth wait aborted [E-OAUTH-FLOW-FAILED]: $detail"
                    return $false
                }
                if ($phase -eq "cancelled") {
                    $detail = if ($statusResp -and $statusResp.detail) { "$($statusResp.detail)" } else { "oauth_flow_cancelled" }
                    Write-Warning "OAuth wait aborted [E-OAUTH-FLOW-CANCELLED]: $detail"
                    return $false
                }
            } catch {
                $statusCode = $null
                if ($_.Exception -and $_.Exception.Response -and $_.Exception.Response.StatusCode) {
                    try {
                        $statusCode = [int]$_.Exception.Response.StatusCode
                    } catch {
                        $statusCode = $null
                    }
                }

                if ($statusCode -eq 401) {
                    Write-Warning "OAuth wait aborted [E-OAUTH-STATUS-401]: /api/auth/status returned 401 Unauthorized. Verify API_KEY in shell/.env.local and restart container."
                    return $false
                }
                if ($statusCode -eq 404) {
                    Write-Warning "[W-OAUTH-STATUS-UNSUPPORTED] OAuth status endpoint not available on this runtime; falling back to legacy /api/accounts polling."
                    $statusEndpointSupported = $false
                }
            }
        }

        if (-not $statusEndpointSupported) {
            try {
                $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/accounts" -Headers $headers -Method Get -TimeoutSec 8
                $count = 0
                if ($resp -and $resp.accounts) {
                    $count = @($resp.accounts).Count
                }
                if ($count -gt 0) {
                    Write-Host "OAuth account linked ($count account(s) found)."
                    return $true
                }
            } catch {
                $statusCode = $null
                if ($_.Exception -and $_.Exception.Response -and $_.Exception.Response.StatusCode) {
                    try {
                        $statusCode = [int]$_.Exception.Response.StatusCode
                    } catch {
                        $statusCode = $null
                    }
                }

                if ($statusCode -eq 401) {
                    Write-Warning "OAuth wait aborted [E-OAUTH-ACCOUNTS-401]: /api/accounts returned 401 Unauthorized. Verify API_KEY in shell/.env.local and restart container."
                    return $false
                }
                if ($statusCode -eq 404) {
                    Write-Warning "OAuth wait aborted [E-OAUTH-ACCOUNTS-404]: /api/accounts returned 404. Ensure admin API is enabled (ENABLE_ADMIN_API=true)."
                    return $false
                }
            }
        }

        $now = Get-Date
        if ($now -ge $nextProgressAt) {
            $elapsed = [int]($now - $startAt).TotalSeconds
            if ($lastKnownPhase) {
                Write-Host "Still waiting for OAuth linkage... ${elapsed}s elapsed (phase: $lastKnownPhase)."
            } else {
                Write-Host "Still waiting for OAuth linkage... ${elapsed}s elapsed."
            }
            try {
                $recentLogs = docker logs --tail 160 gephyr 2>&1
                if ($recentLogs -match "encryption_key_unavailable|Failed to save account in background OAuth") {
                    Write-Warning "OAuth callback succeeded but account persistence failed [E-CRYPTO-KEY-UNAVAILABLE] (missing/invalid ENCRYPTION_KEY; in Docker/container environments machine UID may be unavailable). Remediation: set ENCRYPTION_KEY in .env.local, restart container, then rerun login."
                    return $false
                }
                if ($recentLogs -match "OAuth callback state mismatch") {
                    Write-Warning "[E-OAUTH-STATE-MISMATCH] OAuth callback state mismatch detected. Restart login flow and complete only the latest opened OAuth URL."
                    return $false
                }
                if ($recentLogs -match "Background OAuth exchange failed:") {
                    Write-Warning "[E-OAUTH-TOKEN-EXCHANGE] OAuth wait aborted: token exchange failed. Check network/proxy settings and Google OAuth client credentials."
                    return $false
                }
                if ($recentLogs -match "Background OAuth error: Google did not return a refresh_token") {
                    Write-Warning "[E-OAUTH-REFRESH-MISSING] OAuth wait aborted: Google returned no refresh_token. Revoke prior app consent and retry."
                    return $false
                }
                if ($recentLogs -match "Failed to fetch user info in background OAuth:") {
                    Write-Warning "[E-OAUTH-USER-INFO] OAuth wait aborted: token accepted but user-info lookup failed."
                    return $false
                }
            } catch {
                # If docker logs is unavailable temporarily, continue polling.
            }
            $nextProgressAt = $nextProgressAt.AddSeconds(10)
        }

        Start-Sleep -Seconds $PollSec
    }

    Write-Warning "[W-OAUTH-TIMEOUT] Timed out waiting for OAuth account linkage. You can still finish OAuth and rerun .\console.ps1 accounts."
    return $false
}

Invoke-Step -Name "Running allow-attribute guard" -Action {
    & $allowGuardScript
}

if (-not $SkipBuild) {
    Invoke-Step -Name "Building image $Image" -Action {
        $buildArgs = @("build", "-t", $Image, "-f", "docker/Dockerfile", ".")
        if (-not $UseBuildCache) {
            $buildArgs = @("build", "--no-cache", "-t", $Image, "-f", "docker/Dockerfile", ".")
        }
        Invoke-DockerBuildWithGuidance -Args $buildArgs
    }
}

Invoke-Step -Name "Restarting container with admin API enabled" -Action {
    & $consoleScript restart -EnableAdminApi -Image $Image -Port $Port
}

Invoke-Step -Name "Health check" -Action {
    & $consoleScript health -Port $Port
}

if (-not $SkipLogin) {
    if (-not $env:ENCRYPTION_KEY) {
        $envPath = Join-Path $scriptDir ".env.local"
        $hasEncryptionKey = $false
        if (Test-Path $envPath) {
            foreach ($raw in Get-Content $envPath) {
                $line = $raw.Trim()
                if ($line -and -not $line.StartsWith("#") -and $line.StartsWith("ENCRYPTION_KEY=")) {
                    $value = $line.Split("=", 2)[1].Trim().Trim([char]34).Trim([char]39)
                    if ($value) {
                        $hasEncryptionKey = $true
                    }
                    break
                }
            }
        }
        if (-not $hasEncryptionKey) {
            Write-Warning "[W-CRYPTO-KEY-MISSING] ENCRYPTION_KEY is not set. In Docker/container environments machine UID may be unavailable, so OAuth callback may succeed in browser while account save fails. Remediation: set ENCRYPTION_KEY, restart container, then rerun login."
        }
    }

    Invoke-Step -Name "Starting OAuth login flow" -Action {
        & $consoleScript login -NoBrowser:$NoBrowser -Image $Image -Port $Port
    }

    Invoke-Step -Name "Waiting for OAuth account linkage" -Action {
        Wait-OAuthAccountLink | Out-Null
    }
}

Invoke-Step -Name "List accounts" -Action {
    & $consoleScript accounts -Port $Port
}

if ($RunApiTest) {
    Invoke-Step -Name "Run API test" -Action {
        & $consoleScript "api-test" -Model $Model -Prompt $Prompt -Port $Port
    }
}

if ($DisableAdminAfter) {
    Invoke-Step -Name "Restarting with admin API disabled" -Action {
        & $consoleScript restart -Image $Image -Port $Port
    }
}

Write-Host "test-clean completed."