$ErrorActionPreference = 'Stop' $ImageDefault = 'ghcr.io/chopratejas/headroom:latest' $InstallImage = if ($env:HEADROOM_DOCKER_IMAGE) { $env:HEADROOM_DOCKER_IMAGE } else { $ImageDefault } $InstallDir = Join-Path $HOME '.local\bin' if (-not (Test-Path (Join-Path $HOME '.local'))) { $InstallDir = Join-Path $HOME 'bin' } function Write-Info { param([string]$Message) Write-Host "==> $Message" } function Require-Command { param([string]$Name) if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "Missing required command: $Name" } } function Ensure-PathEntry { param([string]$PathEntry) $currentPath = [Environment]::GetEnvironmentVariable('Path', 'User') $parts = @() if ($currentPath) { $parts = $currentPath -split ';' | Where-Object { $_ } } if ($parts -notcontains $PathEntry) { $newPath = @($PathEntry) + $parts [Environment]::SetEnvironmentVariable('Path', ($newPath -join ';'), 'User') } } function Ensure-ProfileBlock { param([string]$PathEntry) $markerStart = '# >>> headroom docker-native >>>' $markerEnd = '# <<< headroom docker-native <<<' $escapedPathEntry = $PathEntry.Replace("'", "''") $block = @" $markerStart if (-not ((`$env:Path -split ';') -contains '$escapedPathEntry')) { `$env:Path = '$escapedPathEntry;' + `$env:Path } $markerEnd "@ $profileDir = Split-Path -Parent $PROFILE if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Force -Path $profileDir | Out-Null } if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Force -Path $PROFILE | Out-Null } $existing = Get-Content -Raw -Path $PROFILE if ($existing -notmatch [regex]::Escape($markerStart)) { Add-Content -Path $PROFILE -Value "`n$block" } } function Write-Wrapper { param([string]$TargetDir) $wrapperPath = Join-Path $TargetDir 'headroom.ps1' $cmdPath = Join-Path $TargetDir 'headroom.cmd' $resolvedInstallImage = $InstallImage.Replace("'", "''") $wrapper = @' $ErrorActionPreference = 'Stop' $HeadroomImage = if ($env:HEADROOM_DOCKER_IMAGE) { $env:HEADROOM_DOCKER_IMAGE } else { '__HEADROOM_INSTALL_IMAGE__' } $ContainerHome = if ($env:HEADROOM_CONTAINER_HOME) { $env:HEADROOM_CONTAINER_HOME } else { '/tmp/headroom-home' } $HostHome = $HOME function Fail { param([string]$Message) throw $Message } function Require-Command { param([string]$Name) if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { Fail "Missing required command: $Name" } } function Get-RtkTarget { $arch = if ($env:PROCESSOR_ARCHITECTURE -match 'ARM64') { 'aarch64' } else { 'x86_64' } return "${arch}-pc-windows-msvc" } function Ensure-HostDirs { foreach ($dir in @( (Join-Path $HostHome '.headroom'), (Join-Path $HostHome '.claude'), (Join-Path $HostHome '.codex'), (Join-Path $HostHome '.gemini') )) { if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } } } function Get-PassthroughEnvArgs { $args = New-Object System.Collections.Generic.List[string] $prefixes = @( 'HEADROOM_','ANTHROPIC_','OPENAI_','GEMINI_','AWS_','AZURE_','VERTEX_', 'GOOGLE_','GOOGLE_CLOUD_','MISTRAL_','GROQ_','OPENROUTER_','XAI_', 'TOGETHER_','COHERE_','OLLAMA_','LITELLM_','OTEL_','SUPABASE_', 'QDRANT_','NEO4J_','LANGSMITH_' ) foreach ($item in Get-ChildItem Env:) { foreach ($prefix in $prefixes) { if ($item.Name.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $args.Add('--env') $args.Add($item.Name) break } } } return ,$args.ToArray() } function Get-SharedDockerArgs { Ensure-HostDirs $args = New-Object System.Collections.Generic.List[string] $args.Add('--workdir') $args.Add('/workspace') $args.Add('--env') $args.Add("HOME=$ContainerHome") $args.Add('--env') $args.Add('PYTHONUNBUFFERED=1') # Canonical Headroom filesystem contract (issue #175). $args.Add('--env') $args.Add("HEADROOM_WORKSPACE_DIR=$ContainerHome/.headroom") $args.Add('--env') $args.Add("HEADROOM_CONFIG_DIR=$ContainerHome/.headroom/config") $args.Add('--volume') $args.Add("${PWD}:/workspace") $args.Add('--volume') $args.Add((Join-Path $HostHome '.headroom') + ":$ContainerHome/.headroom") $args.Add('--volume') $args.Add((Join-Path $HostHome '.claude') + ":$ContainerHome/.claude") $args.Add('--volume') $args.Add((Join-Path $HostHome '.codex') + ":$ContainerHome/.codex") $args.Add('--volume') $args.Add((Join-Path $HostHome '.gemini') + ":$ContainerHome/.gemini") foreach ($entry in (Get-PassthroughEnvArgs)) { $args.Add($entry) } return ,$args.ToArray() } function Add-TtyArgs { param($ArgsList) if (-not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected) { $ArgsList.Add('-it') return } if (-not [Console]::IsInputRedirected) { $ArgsList.Add('-i') } if (-not [Console]::IsOutputRedirected) { $ArgsList.Add('-t') } } function Invoke-HeadroomDocker { param([string[]]$Arguments) $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','--rm')) Add-TtyArgs -ArgsList $dockerArgs $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add('--entrypoint') $dockerArgs.Add('headroom') $dockerArgs.Add($HeadroomImage) foreach ($arg in $Arguments) { $dockerArgs.Add($arg) } & docker @dockerArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } function Wait-Proxy { param( [string]$ContainerName, [int]$Port ) for ($attempt = 0; $attempt -lt 45; $attempt++) { try { Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$Port/readyz" | Out-Null return } catch { $running = docker ps --format '{{.Names}}' if ($running -notcontains $ContainerName) { break } Start-Sleep -Seconds 1 } } docker logs $ContainerName | Write-Error throw "Headroom proxy failed to start on port $Port" } function Start-ProxyContainer { param( [int]$Port, [string[]]$ProxyArgs ) $containerName = "headroom-proxy-$Port-$PID" $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','-d','--rm','--name',$containerName,'-p',"$Port`:$Port")) $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add($HeadroomImage) $dockerArgs.Add('--host') $dockerArgs.Add('0.0.0.0') $dockerArgs.Add('--port') $dockerArgs.Add("$Port") foreach ($arg in $ProxyArgs) { $dockerArgs.Add($arg) } & docker @dockerArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "Failed to start Headroom proxy container" } Wait-Proxy -ContainerName $containerName -Port $Port return $containerName } function Stop-ProxyContainer { param([string]$ContainerName) if ($ContainerName) { docker stop $ContainerName | Out-Null } } function Get-PersistentProfileRoot { param([string]$Profile) Assert-ValidProfileName -Profile $Profile return Join-Path (Join-Path $HostHome '.headroom\deploy') $Profile } function Get-PersistentStatePath { param([string]$Profile) return Join-Path (Get-PersistentProfileRoot -Profile $Profile) 'docker-native.json' } function Get-PersistentManifestPath { param([string]$Profile) return Join-Path (Get-PersistentProfileRoot -Profile $Profile) 'manifest.json' } function Get-PersistentContainerName { param([string]$Profile) return "headroom-$Profile" } function Assert-ValidProfileName { param([string]$Profile) if ($Profile -notmatch '^[A-Za-z0-9._-]+$' -or $Profile -in @('.', '..')) { Fail "Invalid profile name '$Profile'" } } function Parse-PortValue { param([string]$Value) $parsed = 0 if (-not [int]::TryParse($Value, [ref]$parsed) -or $parsed -lt 1 -or $parsed -gt 65535) { Fail "Invalid port '$Value'" } return $parsed } function Parse-PositiveIntegerValue { param([string]$Value) $parsed = 0 if (-not [int]::TryParse($Value, [ref]$parsed) -or $parsed -lt 1) { Fail "Invalid value '$Value'" } return $parsed } function Require-OptionValue { param( [string[]]$Arguments, [int]$Index, [string]$Option ) if ($Index + 1 -ge $Arguments.Count) { Fail "Option $Option requires a value" } } function Write-Utf8NoBomFile { param( [string]$Path, [string]$Content ) [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) } function Get-PersistentDockerArgs { Ensure-HostDirs $args = New-Object System.Collections.Generic.List[string] $args.Add('--workdir') $args.Add($ContainerHome) $args.Add('--env') $args.Add("HOME=$ContainerHome") $args.Add('--env') $args.Add('PYTHONUNBUFFERED=1') # Canonical Headroom filesystem contract (issue #175). $args.Add('--env') $args.Add("HEADROOM_WORKSPACE_DIR=$ContainerHome/.headroom") $args.Add('--env') $args.Add("HEADROOM_CONFIG_DIR=$ContainerHome/.headroom/config") $args.Add('--volume') $args.Add((Join-Path $HostHome '.headroom') + ":$ContainerHome/.headroom") $args.Add('--volume') $args.Add((Join-Path $HostHome '.claude') + ":$ContainerHome/.claude") $args.Add('--volume') $args.Add((Join-Path $HostHome '.codex') + ":$ContainerHome/.codex") $args.Add('--volume') $args.Add((Join-Path $HostHome '.gemini') + ":$ContainerHome/.gemini") foreach ($entry in (Get-PassthroughEnvArgs)) { $args.Add($entry) } return ,$args.ToArray() } function Get-ManifestProxyArgs { param( [int]$Port, [string]$Backend, [string]$AnyllmProvider, [string]$Region, [string]$Mode, [bool]$Memory, [bool]$TelemetryEnabled ) $args = New-Object System.Collections.Generic.List[string] $args.AddRange([string[]]@('--host','127.0.0.1','--port',"$Port",'--mode',$Mode,'--backend',$Backend)) if (-not $TelemetryEnabled) { $args.Add('--no-telemetry') } if ($Memory) { $args.AddRange([string[]]@('--memory','--memory-db-path',"$ContainerHome/.headroom/memory.db")) } if ($AnyllmProvider) { $args.AddRange([string[]]@('--anyllm-provider', $AnyllmProvider)) } if ($Region) { $args.AddRange([string[]]@('--region', $Region)) } return ,$args.ToArray() } function Write-PersistentState { param( [string]$Profile, [string]$Image, [int]$Port, [string]$Backend, [string]$AnyllmProvider, [string]$Region, [string]$Mode, [bool]$Memory, [bool]$TelemetryEnabled ) $root = Get-PersistentProfileRoot -Profile $Profile New-Item -ItemType Directory -Force -Path $root | Out-Null $state = [ordered]@{ profile = $Profile image = $Image port = $Port backend = $Backend anyllm_provider = $AnyllmProvider region = $Region proxy_mode = $Mode memory_enabled = $Memory telemetry_enabled = $TelemetryEnabled container_name = Get-PersistentContainerName -Profile $Profile health_url = "http://127.0.0.1:$Port/readyz" } Write-Utf8NoBomFile -Path (Get-PersistentStatePath -Profile $Profile) -Content ($state | ConvertTo-Json -Depth 4) } function Write-PersistentManifest { param( [string]$Profile, [string]$Image, [int]$Port, [string]$Backend, [string]$AnyllmProvider, [string]$Region, [string]$Mode, [bool]$Memory, [bool]$TelemetryEnabled, [string[]]$ProxyArgs ) $root = Get-PersistentProfileRoot -Profile $Profile New-Item -ItemType Directory -Force -Path $root | Out-Null $baseEnv = [ordered]@{ HEADROOM_PORT = "$Port" HEADROOM_HOST = '127.0.0.1' HEADROOM_MODE = $Mode HEADROOM_BACKEND = $Backend } $manifest = [ordered]@{ profile = $Profile preset = 'persistent-docker' runtime_kind = 'docker' supervisor_kind = 'none' scope = 'user' provider_mode = 'manual' targets = @() port = $Port host = '127.0.0.1' backend = $Backend anyllm_provider = if ($AnyllmProvider) { $AnyllmProvider } else { $null } region = if ($Region) { $Region } else { $null } proxy_mode = $Mode memory_enabled = $Memory memory_db_path = "$ContainerHome/.headroom/memory.db" telemetry_enabled = $TelemetryEnabled image = $Image service_name = "headroom-$Profile" container_name = Get-PersistentContainerName -Profile $Profile health_url = "http://127.0.0.1:$Port/readyz" base_env = $baseEnv tool_envs = @{} proxy_args = $ProxyArgs mutations = @() artifacts = @() } Write-Utf8NoBomFile -Path (Get-PersistentManifestPath -Profile $Profile) -Content ($manifest | ConvertTo-Json -Depth 8) } function Read-PersistentState { param([string]$Profile) Assert-ValidProfileName -Profile $Profile $statePath = Get-PersistentStatePath -Profile $Profile if (-not (Test-Path $statePath)) { Fail "No docker-native persistent deployment profile named '$Profile'" } return Get-Content -Raw -Path $statePath | ConvertFrom-Json } function Start-PersistentDockerInstall { param( [string]$Profile, [string]$Image, [int]$Port, [string]$Backend, [string]$AnyllmProvider, [string]$Region, [string]$Mode, [bool]$Memory, [bool]$TelemetryEnabled ) Assert-ValidProfileName -Profile $Profile $containerName = Get-PersistentContainerName -Profile $Profile $proxyArgs = Get-ManifestProxyArgs -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled docker rm -f $containerName | Out-Null 2>$null $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','-d','--restart','unless-stopped','--name',$containerName,'-p',"$Port`:$Port")) $dockerArgs.AddRange((Get-PersistentDockerArgs)) $dockerArgs.AddRange([string[]]@( '--env',"HEADROOM_DEPLOYMENT_PROFILE=$Profile", '--env','HEADROOM_DEPLOYMENT_PRESET=persistent-docker', '--env','HEADROOM_DEPLOYMENT_RUNTIME=docker', '--env','HEADROOM_DEPLOYMENT_SUPERVISOR=none', '--env','HEADROOM_DEPLOYMENT_SCOPE=user' )) $dockerArgs.Add($Image) $dockerArgs.Add('--host') $dockerArgs.Add('0.0.0.0') for ($i = 2; $i -lt $proxyArgs.Count; $i++) { $dockerArgs.Add($proxyArgs[$i]) } & docker @dockerArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "Failed to start docker-native persistent deployment" } try { Wait-Proxy -ContainerName $containerName -Port $Port } catch { docker rm -f $containerName | Out-Null 2>$null throw } Write-PersistentState -Profile $Profile -Image $Image -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled Write-PersistentManifest -Profile $Profile -Image $Image -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled -ProxyArgs $proxyArgs } function Stop-PersistentDockerInstall { param([string]$Profile) $state = Read-PersistentState -Profile $Profile docker stop $state.container_name | Out-Null 2>$null docker rm -f $state.container_name | Out-Null 2>$null } function Remove-PersistentDockerInstall { param([string]$Profile) $state = Read-PersistentState -Profile $Profile docker stop $state.container_name | Out-Null 2>$null docker rm -f $state.container_name | Out-Null 2>$null $root = Get-PersistentProfileRoot -Profile $Profile if (Test-Path $root) { Remove-Item -Recurse -Force -Path $root } } function Show-PersistentDockerInstallStatus { param([string]$Profile) $state = Read-PersistentState -Profile $Profile $status = 'stopped' $ready = 'no' $running = docker ps --format '{{.Names}}' if ($running -contains $state.container_name) { $status = 'running' try { Invoke-WebRequest -UseBasicParsing -Uri $state.health_url | Out-Null $ready = 'yes' } catch { $ready = 'no' } } Write-Host "Profile: $($state.profile)" Write-Host 'Preset: persistent-docker' Write-Host 'Runtime: docker' Write-Host 'Supervisor: none' Write-Host "Port: $($state.port)" Write-Host "Status: $status" Write-Host "Ready: $ready" Write-Host "Health URL: $($state.health_url)" } function Show-InstallHelp { $lines = @( 'Usage: headroom install [OPTIONS] COMMAND [ARGS]...', '', ' Manage persistent Docker-native Headroom deployments.', '', ' The Docker-native wrapper currently supports the persistent-docker preset only.', ' Use the Python-native `headroom install` command for persistent-service and', ' persistent-task installs, or when you need provider/user/system config mutation.', '', 'Options:', ' -?, --help Show this message and exit.', '', 'Commands:', ' apply Install a persistent Docker deployment.', ' remove Remove a persistent Docker deployment.', ' restart Restart a persistent Docker deployment.', ' start Start a persistent Docker deployment.', ' status Show persistent Docker deployment status.', ' stop Stop a persistent Docker deployment.' ) Write-Host ($lines -join [Environment]::NewLine) } function Show-InstallApplyHelp { $lines = @( 'Usage: headroom install apply [OPTIONS]', '', ' Install a persistent Docker deployment.', '', 'Options:', ' --preset [persistent-docker] Docker-native wrapper supports persistent-docker only.', ' --runtime [docker] Docker-native wrapper supports runtime=docker only.', ' --profile TEXT Deployment profile name. [default: default]', ' -p, --port INTEGER Persistent proxy port. [default: 8787]', ' --backend TEXT Proxy backend. [default: anthropic]', ' --anyllm-provider TEXT Provider for any-llm backends.', ' --region TEXT Cloud region for Bedrock / Vertex style backends.', ' --mode TEXT Proxy optimization mode. [default: token]', ' --memory Enable persistent memory in the runtime.', ' --no-telemetry Disable anonymous telemetry in the runtime.', ' --image TEXT Docker image to use. [default: HEADROOM_DOCKER_IMAGE or ghcr.io/chopratejas/headroom:latest]', ' -?, --help Show this message and exit.' ) Write-Host ($lines -join [Environment]::NewLine) } function Show-WrapHelp { $lines = @( 'Usage: headroom wrap [OPTIONS] [-- ARGS...]', '', ' Launch supported host tools through a Docker-native Headroom proxy.', '', 'Supported commands:', ' claude', ' codex', ' aider', ' cursor', ' openclaw', '', 'Notes:', ' - GitHub Copilot CLI wrapping is not supported by the Docker-native wrapper.', ' - Use the Python-native CLI for unsupported wrap targets.' ) Write-Host ($lines -join [Environment]::NewLine) } function Parse-InstallApplyArgs { param([string[]]$Arguments) $profile = 'default' $port = 8787 $backend = 'anthropic' $anyllmProvider = $null $region = $null $mode = 'token' $memory = $false $telemetryEnabled = $true $image = $HeadroomImage $i = 0 while ($i -lt $Arguments.Count) { $arg = $Arguments[$i] switch -Regex ($arg) { '^--preset$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--preset' if ($Arguments[$i + 1] -ne 'persistent-docker') { Fail 'Docker-native wrapper supports only --preset persistent-docker' } $i += 2 continue } '^--preset=' { if (($arg -replace '^--preset=', '') -ne 'persistent-docker') { Fail 'Docker-native wrapper supports only --preset persistent-docker' } $i += 1 continue } '^--runtime$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--runtime' if ($Arguments[$i + 1] -ne 'docker') { Fail 'Docker-native wrapper supports only --runtime docker' } $i += 2 continue } '^--runtime=' { if (($arg -replace '^--runtime=', '') -ne 'docker') { Fail 'Docker-native wrapper supports only --runtime docker' } $i += 1 continue } '^(--scope|--providers|--target)$' { Fail 'Docker-native wrapper install does not support provider/user/system mutation flags; use the Python-native CLI for those flows' } '^(--scope=|--providers=|--target=)' { Fail 'Docker-native wrapper install does not support provider/user/system mutation flags; use the Python-native CLI for those flows' } '^--profile$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--profile' $profile = $Arguments[$i + 1] $i += 2 continue } '^--profile=' { $profile = $arg -replace '^--profile=', '' $i += 1 continue } '^(--port|-p)$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $port = Parse-PortValue -Value $Arguments[$i + 1] $i += 2 continue } '^(--port=|-p=)' { $port = Parse-PortValue -Value ($arg -replace '^(--port=|-p=)', '') $i += 1 continue } '^--backend$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--backend' $backend = $Arguments[$i + 1] $i += 2 continue } '^--backend=' { $backend = $arg -replace '^--backend=', '' $i += 1 continue } '^--anyllm-provider$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--anyllm-provider' $anyllmProvider = $Arguments[$i + 1] $i += 2 continue } '^--anyllm-provider=' { $anyllmProvider = $arg -replace '^--anyllm-provider=', '' $i += 1 continue } '^--region$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--region' $region = $Arguments[$i + 1] $i += 2 continue } '^--region=' { $region = $arg -replace '^--region=', '' $i += 1 continue } '^--mode$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--mode' $mode = $Arguments[$i + 1] $i += 2 continue } '^--mode=' { $mode = $arg -replace '^--mode=', '' $i += 1 continue } '^--memory$' { $memory = $true $i += 1 continue } '^--no-telemetry$' { $telemetryEnabled = $false $i += 1 continue } '^--image$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--image' $image = $Arguments[$i + 1] $i += 2 continue } '^--image=' { $image = $arg -replace '^--image=', '' $i += 1 continue } '^(--help|-\?)$' { Show-InstallApplyHelp exit 0 } default { Fail "Unsupported option for 'headroom install apply': $arg" } } } return [pscustomobject]@{ Profile = $profile Port = $port Backend = $backend AnyllmProvider = $anyllmProvider Region = $region Mode = $mode Memory = $memory TelemetryEnabled = $telemetryEnabled Image = $image } } function Parse-InstallProfileArgs { param([string[]]$Arguments) $profile = 'default' $i = 0 while ($i -lt $Arguments.Count) { $arg = $Arguments[$i] switch -Regex ($arg) { '^--profile$' { Require-OptionValue -Arguments $Arguments -Index $i -Option '--profile' $profile = $Arguments[$i + 1] $i += 2 continue } '^--profile=' { $profile = $arg -replace '^--profile=', '' $i += 1 continue } '^(--help|-\?)$' { Show-InstallHelp exit 0 } default { Fail "Unsupported option for 'headroom install': $arg" } } } return $profile } function Invoke-ClaudeRtkInit { $rtkPath = Join-Path $HostHome '.headroom\bin\rtk.exe' if (-not (Test-Path $rtkPath)) { Write-Warning "rtk was not installed at $rtkPath; Claude hooks were not registered" return } try { & $rtkPath init --global --auto-patch | Out-Null } catch { Write-Warning "Failed to register Claude hooks with rtk; continuing without hook registration" } } function Invoke-WithTemporaryEnv { param( [hashtable]$Environment, [string]$Command, [string[]]$Arguments ) $previous = @{} foreach ($pair in $Environment.GetEnumerator()) { $previous[$pair.Key] = [Environment]::GetEnvironmentVariable($pair.Key, 'Process') [Environment]::SetEnvironmentVariable($pair.Key, $pair.Value, 'Process') } try { & $Command @Arguments return $LASTEXITCODE } finally { foreach ($pair in $Environment.GetEnumerator()) { [Environment]::SetEnvironmentVariable($pair.Key, $previous[$pair.Key], 'Process') } } } function Test-HelpFlag { param([string[]]$Arguments) foreach ($arg in $Arguments) { if ($arg -eq '--') { break } if ($arg -eq '--help' -or $arg -eq '-?') { return $true } } return $false } function Parse-OpenClawWrapArgs { param([string[]]$Arguments) $gatewayProviderIds = New-Object System.Collections.Generic.List[string] $pluginPath = $null $pluginSpec = 'headroom-ai/openclaw' $skipBuild = $false $copy = $false $proxyPort = 8787 $startupTimeoutMs = 20000 $pythonPath = $null $noAutoStart = $false $noRestart = $false $verbose = $false $i = 0 while ($i -lt $Arguments.Count) { $arg = $Arguments[$i] switch -Regex ($arg) { '^--plugin-path$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $pluginPath = $Arguments[$i + 1] $i += 2 continue } '^--plugin-path=' { $pluginPath = $arg -replace '^--plugin-path=', '' $i += 1 continue } '^--plugin-spec$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $pluginSpec = $Arguments[$i + 1] $i += 2 continue } '^--plugin-spec=' { $pluginSpec = $arg -replace '^--plugin-spec=', '' $i += 1 continue } '^--skip-build$' { $skipBuild = $true $i += 1 continue } '^--copy$' { $copy = $true $i += 1 continue } '^--proxy-port$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $proxyPort = Parse-PortValue -Value $Arguments[$i + 1] $i += 2 continue } '^--proxy-port=' { $proxyPort = Parse-PortValue -Value ($arg -replace '^--proxy-port=', '') $i += 1 continue } '^--startup-timeout-ms$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $startupTimeoutMs = Parse-PositiveIntegerValue -Value $Arguments[$i + 1] $i += 2 continue } '^--startup-timeout-ms=' { $startupTimeoutMs = Parse-PositiveIntegerValue -Value ($arg -replace '^--startup-timeout-ms=', '') $i += 1 continue } '^--gateway-provider-id$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $gatewayProviderIds.Add($Arguments[$i + 1]) $i += 2 continue } '^--gateway-provider-id=' { $gatewayProviderIds.Add($arg -replace '^--gateway-provider-id=', '') $i += 1 continue } '^--python-path$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $pythonPath = $Arguments[$i + 1] $i += 2 continue } '^--python-path=' { $pythonPath = $arg -replace '^--python-path=', '' $i += 1 continue } '^--no-auto-start$' { $noAutoStart = $true $i += 1 continue } '^--no-restart$' { $noRestart = $true $i += 1 continue } '^--verbose$|^-v$' { $verbose = $true $i += 1 continue } default { Fail "Unsupported option for 'headroom wrap openclaw': $arg" } } } [pscustomobject]@{ PluginPath = $pluginPath PluginSpec = $pluginSpec SkipBuild = $skipBuild Copy = $copy ProxyPort = $proxyPort StartupTimeoutMs = $startupTimeoutMs GatewayProviderIds = $gatewayProviderIds.ToArray() PythonPath = $pythonPath NoAutoStart = $noAutoStart NoRestart = $noRestart Verbose = $verbose } } function Parse-OpenClawUnwrapArgs { param([string[]]$Arguments) $noRestart = $false $verbose = $false $i = 0 while ($i -lt $Arguments.Count) { $arg = $Arguments[$i] switch -Regex ($arg) { '^--no-restart$' { $noRestart = $true $i += 1 continue } '^--verbose$|^-v$' { $verbose = $true $i += 1 continue } default { Fail "Unsupported option for 'headroom unwrap openclaw': $arg" } } } [pscustomobject]@{ NoRestart = $noRestart Verbose = $verbose } } function Invoke-CapturedCommand { param( [string]$Action, [string]$Command, [string[]]$Arguments, [string]$WorkingDirectory ) $previous = $null try { if ($WorkingDirectory) { $previous = Get-Location Set-Location $WorkingDirectory } $output = (& $Command @Arguments 2>&1 | Out-String).Trim() $exitCode = $LASTEXITCODE } finally { if ($previous) { Set-Location $previous } } if ($exitCode -ne 0) { if (-not $output) { $output = "exit code $exitCode" } Fail "$Action failed: $output" } return $output } function Get-OpenClawExistingEntryJson { $output = (& openclaw config get plugins.entries.headroom 2>$null | Out-String).Trim() if ($LASTEXITCODE -ne 0) { return $null } return $output } function Invoke-OpenClawPrepareEntryJson { param( [string]$ExistingEntryJson, [pscustomobject]$Parsed ) $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','--rm')) $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add('--entrypoint') $dockerArgs.Add('headroom') $dockerArgs.Add($HeadroomImage) $dockerArgs.AddRange([string[]]@('wrap','openclaw','--prepare-only','--proxy-port',"$($Parsed.ProxyPort)",'--startup-timeout-ms',"$($Parsed.StartupTimeoutMs)")) if ($ExistingEntryJson) { $dockerArgs.Add('--existing-entry-json') $dockerArgs.Add($ExistingEntryJson) } if ($Parsed.PythonPath) { $dockerArgs.Add('--python-path') $dockerArgs.Add($Parsed.PythonPath) } if ($Parsed.NoAutoStart) { $dockerArgs.Add('--no-auto-start') } foreach ($providerId in $Parsed.GatewayProviderIds) { $dockerArgs.Add('--gateway-provider-id') $dockerArgs.Add($providerId) } $output = (& docker @dockerArgs 2>&1 | Out-String).Trim() if ($LASTEXITCODE -ne 0) { Fail "Failed to prepare docker-native OpenClaw config: $output" } return $output } function Invoke-OpenClawPrepareUnwrapEntryJson { param([string]$ExistingEntryJson) $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','--rm')) $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add('--entrypoint') $dockerArgs.Add('headroom') $dockerArgs.Add($HeadroomImage) $dockerArgs.AddRange([string[]]@('unwrap','openclaw','--prepare-only')) if ($ExistingEntryJson) { $dockerArgs.Add('--existing-entry-json') $dockerArgs.Add($ExistingEntryJson) } $output = (& docker @dockerArgs 2>&1 | Out-String).Trim() if ($LASTEXITCODE -ne 0) { Fail "Failed to prepare docker-native OpenClaw unwrap config: $output" } return $output } function Resolve-OpenClawExtensionsDir { $configOutput = Invoke-CapturedCommand -Action 'openclaw config file' -Command 'openclaw' -Arguments @('config','file') $configPath = ($configOutput -split "`r?`n")[-1].Trim() if (-not $configPath) { Fail 'Unable to resolve OpenClaw config path.' } return (Join-Path (Split-Path -Parent $configPath) 'extensions') } function Copy-OpenClawPluginIntoExtensions { param([string]$PluginPath) $distDir = Join-Path $PluginPath 'dist' $hookShimDir = Join-Path $PluginPath 'hook-shim' if (-not (Test-Path $distDir)) { Fail "Plugin dist folder missing at $distDir. Build the plugin first." } if (-not (Test-Path $hookShimDir)) { Fail "Plugin hook-shim folder missing at $hookShimDir. Build the plugin first." } $extensionsDir = Resolve-OpenClawExtensionsDir $targetDir = Join-Path $extensionsDir 'headroom' $targetDist = Join-Path $targetDir 'dist' $targetHookShim = Join-Path $targetDir 'hook-shim' New-Item -ItemType Directory -Force -Path $targetDir | Out-Null if (Test-Path $targetDist) { Remove-Item -Recurse -Force $targetDist } if (Test-Path $targetHookShim) { Remove-Item -Recurse -Force $targetHookShim } Copy-Item -Recurse -Force $distDir $targetDist Copy-Item -Recurse -Force $hookShimDir $targetHookShim foreach ($fileName in @('openclaw.plugin.json','package.json','README.md')) { $source = Join-Path $PluginPath $fileName if (Test-Path $source) { Copy-Item -Force $source (Join-Path $targetDir $fileName) } } return $targetDir } function Install-OpenClawPlugin { param([pscustomobject]$Parsed) if ($Parsed.PluginPath) { if (-not (Test-Path $Parsed.PluginPath)) { Fail "Plugin path not found: $($Parsed.PluginPath)." } if (-not (Test-Path (Join-Path $Parsed.PluginPath 'package.json'))) { Fail "Invalid plugin path (missing package.json): $($Parsed.PluginPath)" } if (-not (Test-Path (Join-Path $Parsed.PluginPath 'openclaw.plugin.json'))) { Fail "Invalid plugin path (missing openclaw.plugin.json): $($Parsed.PluginPath)" } } if ($Parsed.PluginPath -and -not $Parsed.SkipBuild) { Require-Command npm Write-Host ' Building OpenClaw plugin (npm install + npm run build)...' [void](Invoke-CapturedCommand -Action 'npm install' -Command 'npm' -Arguments @('install') -WorkingDirectory $Parsed.PluginPath) [void](Invoke-CapturedCommand -Action 'npm run build' -Command 'npm' -Arguments @('run','build') -WorkingDirectory $Parsed.PluginPath) } if ($Parsed.PluginPath) { if ($Parsed.Copy) { $arguments = @('plugins','install','--dangerously-force-unsafe-install',$Parsed.PluginPath) $workingDirectory = $null } else { $arguments = @('plugins','install','--dangerously-force-unsafe-install','--link','.') $workingDirectory = $Parsed.PluginPath } } else { $arguments = @('plugins','install','--dangerously-force-unsafe-install',$Parsed.PluginSpec) $workingDirectory = $null } $previous = $null try { if ($workingDirectory) { $previous = Get-Location Set-Location $workingDirectory } $installOutput = (& openclaw @arguments 2>&1 | Out-String).Trim() $installExitCode = $LASTEXITCODE } finally { if ($previous) { Set-Location $previous } } if ($installExitCode -eq 0) { if ($Parsed.Verbose -and $installOutput) { Write-Host $installOutput } return } $lowerOutput = $installOutput.ToLowerInvariant() if ($lowerOutput.Contains('plugin already exists')) { Write-Host ' Plugin already installed; continuing with configuration/update steps.' return } if ($Parsed.PluginPath -and -not $Parsed.Copy -and $lowerOutput.Contains('also not a valid hook pack')) { Write-Host ' OpenClaw linked-path install bug detected; applying extension-path fallback...' $targetDir = Copy-OpenClawPluginIntoExtensions -PluginPath $Parsed.PluginPath Write-Host " Fallback plugin copy completed: $targetDir" return } if (-not $installOutput) { $installOutput = "exit code $installExitCode" } Fail "openclaw plugins install failed: $installOutput" } function Restart-OrStartOpenClawGateway { $restartOutput = (& openclaw gateway restart 2>&1 | Out-String).Trim() if ($LASTEXITCODE -eq 0) { return [pscustomobject]@{ Action = 'restarted'; Output = $restartOutput } } $startOutput = Invoke-CapturedCommand -Action 'openclaw gateway start' -Command 'openclaw' -Arguments @('gateway','start') return [pscustomobject]@{ Action = 'started'; Output = $startOutput } } function Invoke-OpenClawWrap { param([string[]]$Arguments) Require-Command openclaw $parsed = Parse-OpenClawWrapArgs -Arguments $Arguments $existingEntryJson = Get-OpenClawExistingEntryJson $entryJson = Invoke-OpenClawPrepareEntryJson -ExistingEntryJson $existingEntryJson -Parsed $parsed Write-Host "" Write-Host " ╔═══════════════════════════════════════════════╗" Write-Host " ║ HEADROOM WRAP: OPENCLAW ║" Write-Host " ╚═══════════════════════════════════════════════╝" Write-Host "" if ($parsed.PluginPath) { Write-Host " Plugin source: local ($($parsed.PluginPath))" } else { Write-Host " Plugin source: npm ($($parsed.PluginSpec))" } Write-Host ' Writing plugin configuration...' [void](Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @('config','set','plugins.entries.headroom',$entryJson,'--strict-json')) Write-Host ' Installing OpenClaw plugin with required unsafe-install flag...' Install-OpenClawPlugin -Parsed $parsed [void](Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @('config','set','plugins.slots.contextEngine','"headroom"','--strict-json')) [void](Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @('config','validate')) if ($parsed.NoRestart) { Write-Host ' Skipping gateway restart (--no-restart).' Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply plugin changes.' } else { Write-Host ' Applying plugin changes to OpenClaw gateway...' $gatewayResult = Restart-OrStartOpenClawGateway Write-Host " Gateway $($gatewayResult.Action)." if ($parsed.Verbose -and $gatewayResult.Output) { Write-Host $gatewayResult.Output } } $inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @('plugins','inspect','headroom') if ($parsed.Verbose -and $inspectOutput) { Write-Host $inspectOutput } Write-Host "" Write-Host "✓ OpenClaw is configured to use Headroom context compression." Write-Host " Plugin: headroom" Write-Host " Slot: plugins.slots.contextEngine = headroom" Write-Host "" } function Invoke-OpenClawUnwrap { param([string[]]$Arguments) Require-Command openclaw $parsed = Parse-OpenClawUnwrapArgs -Arguments $Arguments $existingEntryJson = Get-OpenClawExistingEntryJson $entryJson = Invoke-OpenClawPrepareUnwrapEntryJson -ExistingEntryJson $existingEntryJson Write-Host "" Write-Host " ╔═══════════════════════════════════════════════╗" Write-Host " ║ HEADROOM UNWRAP: OPENCLAW ║" Write-Host " ╚═══════════════════════════════════════════════╝" Write-Host "" Write-Host ' Disabling Headroom plugin and removing engine mapping...' [void](Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @('config','set','plugins.entries.headroom',$entryJson,'--strict-json')) [void](Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @('config','set','plugins.slots.contextEngine','"legacy"','--strict-json')) [void](Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @('config','validate')) if ($parsed.NoRestart) { Write-Host ' Skipping gateway restart (--no-restart).' Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply unwrap changes.' } else { Write-Host ' Applying unwrap changes to OpenClaw gateway...' $gatewayResult = Restart-OrStartOpenClawGateway Write-Host " Gateway $($gatewayResult.Action)." if ($parsed.Verbose -and $gatewayResult.Output) { Write-Host $gatewayResult.Output } } if ($parsed.Verbose) { $inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @('plugins','inspect','headroom') if ($inspectOutput) { Write-Host $inspectOutput } } Write-Host "" Write-Host "✓ OpenClaw Headroom wrap removed." Write-Host " Plugin: headroom (installed, disabled)" Write-Host " Slot: plugins.slots.contextEngine = legacy" Write-Host "" } function Parse-WrapArgs { param([string[]]$Arguments) $known = New-Object System.Collections.Generic.List[string] $hostArgs = New-Object System.Collections.Generic.List[string] $port = 8787 $noRtk = $false $noProxy = $false $learn = $false $backend = $null $anyllm = $null $region = $null $i = 0 while ($i -lt $Arguments.Count) { $arg = $Arguments[$i] switch -Regex ($arg) { '^--$' { for ($j = $i + 1; $j -lt $Arguments.Count; $j++) { $hostArgs.Add($Arguments[$j]) } $i = $Arguments.Count continue } '^--port$|^-p$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $port = Parse-PortValue -Value $Arguments[$i + 1] $known.Add($arg) $known.Add($Arguments[$i + 1]) $i += 2 continue } '^--port=' { $port = Parse-PortValue -Value ($arg -replace '^--port=', '') $known.Add($arg) $i += 1 continue } '^--no-rtk$' { $noRtk = $true $known.Add($arg) $i += 1 continue } '^--no-proxy$' { $noProxy = $true $known.Add($arg) $i += 1 continue } '^--learn$' { $learn = $true $known.Add($arg) $i += 1 continue } '^--verbose$|^-v$' { $known.Add($arg) $i += 1 continue } '^--backend$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $backend = $Arguments[$i + 1] $known.Add($arg) $known.Add($Arguments[$i + 1]) $i += 2 continue } '^--backend=' { $backend = $arg -replace '^--backend=', '' $known.Add($arg) $i += 1 continue } '^--anyllm-provider$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $anyllm = $Arguments[$i + 1] $known.Add($arg) $known.Add($Arguments[$i + 1]) $i += 2 continue } '^--anyllm-provider=' { $anyllm = $arg -replace '^--anyllm-provider=', '' $known.Add($arg) $i += 1 continue } '^--region$' { Require-OptionValue -Arguments $Arguments -Index $i -Option $arg $region = $Arguments[$i + 1] $known.Add($arg) $known.Add($Arguments[$i + 1]) $i += 2 continue } '^--region=' { $region = $arg -replace '^--region=', '' $known.Add($arg) $i += 1 continue } default { for ($j = $i; $j -lt $Arguments.Count; $j++) { $hostArgs.Add($Arguments[$j]) } $i = $Arguments.Count } } } [pscustomobject]@{ KnownArgs = $known.ToArray() HostArgs = $hostArgs.ToArray() Port = $port NoRtk = $noRtk NoProxy = $noProxy Learn = $learn Backend = $backend Anyllm = $anyllm Region = $region } } function Invoke-PrepareOnly { param( [string]$Tool, [string[]]$KnownArgs ) $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','--rm')) Add-TtyArgs -ArgsList $dockerArgs $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add('--env') $dockerArgs.Add("HEADROOM_RTK_TARGET=$(Get-RtkTarget)") $dockerArgs.Add('--entrypoint') $dockerArgs.Add('headroom') $dockerArgs.Add($HeadroomImage) $dockerArgs.AddRange([string[]]@('wrap',$Tool,'--prepare-only')) foreach ($arg in $KnownArgs) { $dockerArgs.Add($arg) } & docker @dockerArgs if ($LASTEXITCODE -ne 0) { throw "Failed to prepare docker-native wrap for $Tool" } } Require-Command docker if ($args.Count -eq 0) { Invoke-HeadroomDocker -Arguments @('--help') exit 0 } switch ($args[0]) { 'install' { if ($args.Count -eq 1 -or $args[1] -eq '--help' -or $args[1] -eq '-?') { Show-InstallHelp exit 0 } $installCommand = $args[1] $installArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() } switch ($installCommand) { 'apply' { $parsed = Parse-InstallApplyArgs -Arguments $installArgs Start-PersistentDockerInstall -Profile $parsed.Profile -Image $parsed.Image -Port $parsed.Port -Backend $parsed.Backend -AnyllmProvider $parsed.AnyllmProvider -Region $parsed.Region -Mode $parsed.Mode -Memory $parsed.Memory -TelemetryEnabled $parsed.TelemetryEnabled Write-Host "Installed docker-native persistent deployment '$($parsed.Profile)' on port $($parsed.Port)." exit 0 } 'status' { $profile = Parse-InstallProfileArgs -Arguments $installArgs Show-PersistentDockerInstallStatus -Profile $profile exit 0 } 'start' { $profile = Parse-InstallProfileArgs -Arguments $installArgs $state = Read-PersistentState -Profile $profile Start-PersistentDockerInstall -Profile $state.profile -Image $state.image -Port $state.port -Backend $state.backend -AnyllmProvider $state.anyllm_provider -Region $state.region -Mode $state.proxy_mode -Memory ([bool]$state.memory_enabled) -TelemetryEnabled ([bool]$state.telemetry_enabled) Write-Host "Started docker-native persistent deployment '$profile'." exit 0 } 'stop' { $profile = Parse-InstallProfileArgs -Arguments $installArgs Stop-PersistentDockerInstall -Profile $profile Write-Host "Stopped docker-native persistent deployment '$profile'." exit 0 } 'restart' { $profile = Parse-InstallProfileArgs -Arguments $installArgs $state = Read-PersistentState -Profile $profile Start-PersistentDockerInstall -Profile $state.profile -Image $state.image -Port $state.port -Backend $state.backend -AnyllmProvider $state.anyllm_provider -Region $state.region -Mode $state.proxy_mode -Memory ([bool]$state.memory_enabled) -TelemetryEnabled ([bool]$state.telemetry_enabled) Write-Host "Restarted docker-native persistent deployment '$profile'." exit 0 } 'remove' { $profile = Parse-InstallProfileArgs -Arguments $installArgs Remove-PersistentDockerInstall -Profile $profile Write-Host "Removed docker-native persistent deployment '$profile'." exit 0 } default { Fail "Unsupported install target: $installCommand" } } } 'wrap' { if ($args.Count -eq 1 -or $args[1] -eq '--help' -or $args[1] -eq '-?') { Show-WrapHelp exit 0 } if ($args.Count -lt 2) { Fail 'Usage: headroom wrap [...]' } $tool = $args[1] $wrapArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() } switch ($tool) { 'claude' { } 'codex' { } 'aider' { } 'cursor' { } 'openclaw' { } default { Fail "Docker-native wrapper does not support 'wrap $tool'. Supported targets: claude, codex, aider, cursor, openclaw" } } if ($tool -eq 'openclaw') { if (Test-HelpFlag -Arguments $wrapArgs) { $helpArgs = @('wrap','openclaw') + $wrapArgs Invoke-HeadroomDocker -Arguments $helpArgs exit 0 } Invoke-OpenClawWrap -Arguments $wrapArgs exit 0 } if (Test-HelpFlag -Arguments $wrapArgs) { $helpArgs = @('wrap', $tool) + $wrapArgs Invoke-HeadroomDocker -Arguments $helpArgs exit 0 } $parsed = Parse-WrapArgs -Arguments $wrapArgs $proxyArgs = New-Object System.Collections.Generic.List[string] if ($parsed.Learn) { $proxyArgs.Add('--learn') } if ($parsed.Backend) { $proxyArgs.AddRange([string[]]@('--backend', $parsed.Backend)) } if ($parsed.Anyllm) { $proxyArgs.AddRange([string[]]@('--anyllm-provider', $parsed.Anyllm)) } if ($parsed.Region) { $proxyArgs.AddRange([string[]]@('--region', $parsed.Region)) } $containerName = $null try { if (-not $parsed.NoProxy) { $containerName = Start-ProxyContainer -Port $parsed.Port -ProxyArgs $proxyArgs.ToArray() } $prepareArgs = New-Object System.Collections.Generic.List[string] foreach ($arg in $parsed.KnownArgs) { $prepareArgs.Add($arg) } if (-not $parsed.NoProxy) { $prepareArgs.Add('--no-proxy') } Invoke-PrepareOnly -Tool $tool -KnownArgs $prepareArgs.ToArray() switch ($tool) { 'claude' { if (-not $parsed.NoRtk) { Invoke-ClaudeRtkInit } $exitCode = Invoke-WithTemporaryEnv -Environment @{ ANTHROPIC_BASE_URL = "http://127.0.0.1:$($parsed.Port)" } -Command 'claude' -Arguments $parsed.HostArgs exit $exitCode } 'codex' { $exitCode = Invoke-WithTemporaryEnv -Environment @{ OPENAI_BASE_URL = "http://127.0.0.1:$($parsed.Port)/v1" } -Command 'codex' -Arguments $parsed.HostArgs exit $exitCode } 'aider' { $exitCode = Invoke-WithTemporaryEnv -Environment @{ OPENAI_API_BASE = "http://127.0.0.1:$($parsed.Port)/v1" ANTHROPIC_BASE_URL = "http://127.0.0.1:$($parsed.Port)" } -Command 'aider' -Arguments $parsed.HostArgs exit $exitCode } 'cursor' { Write-Host "Headroom proxy is running for Cursor." Write-Host "" Write-Host "OpenAI base URL: http://127.0.0.1:$($parsed.Port)/v1" Write-Host "Anthropic base URL: http://127.0.0.1:$($parsed.Port)" Write-Host "" Write-Host "Press Ctrl+C to stop the proxy." while ($true) { Start-Sleep -Seconds 1 } } } } finally { Stop-ProxyContainer -ContainerName $containerName } } 'unwrap' { if ($args.Count -eq 1 -or $args[1] -eq '--help' -or $args[1] -eq '-?') { Invoke-HeadroomDocker -Arguments @('unwrap','--help') exit 0 } if ($args.Count -ge 2 -and $args[1] -eq 'openclaw') { $unwrapArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() } if (Test-HelpFlag -Arguments $unwrapArgs) { $helpArgs = @('unwrap','openclaw') + $unwrapArgs Invoke-HeadroomDocker -Arguments $helpArgs exit 0 } Invoke-OpenClawUnwrap -Arguments $unwrapArgs exit 0 } Invoke-HeadroomDocker -Arguments $args } 'proxy' { $port = 8787 $forwardArgs = New-Object System.Collections.Generic.List[string] foreach ($arg in $args) { $forwardArgs.Add($arg) } for ($i = 1; $i -lt $args.Count; $i++) { if ($args[$i] -eq '--port' -or $args[$i] -eq '-p') { Require-OptionValue -Arguments $args -Index $i -Option $args[$i] $port = Parse-PortValue -Value $args[$i + 1] break } if ($args[$i] -match '^--port=') { $port = Parse-PortValue -Value ($args[$i] -replace '^--port=', '') break } } $dockerArgs = New-Object System.Collections.Generic.List[string] $dockerArgs.AddRange([string[]]@('run','--rm')) Add-TtyArgs -ArgsList $dockerArgs $dockerArgs.AddRange([string[]]@('-p',"$port`:$port")) $dockerArgs.AddRange((Get-SharedDockerArgs)) $dockerArgs.Add('--entrypoint') $dockerArgs.Add('headroom') $dockerArgs.Add($HeadroomImage) foreach ($arg in $forwardArgs) { $dockerArgs.Add($arg) } & docker @dockerArgs exit $LASTEXITCODE } default { Invoke-HeadroomDocker -Arguments $args } } '@ $wrapper = $wrapper.Replace('__HEADROOM_INSTALL_IMAGE__', $resolvedInstallImage) $cmdWrapper = ([string][char]64) + "echo off`r`npowershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File ""%~dp0headroom.ps1"" %*`r`n" Set-Content -Path $wrapperPath -Value $wrapper -Encoding utf8 Set-Content -Path $cmdPath -Value $cmdWrapper -Encoding ascii } Require-Command docker docker version | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Docker is installed but not available to the current user' } New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Write-Wrapper -TargetDir $InstallDir Ensure-PathEntry -PathEntry $InstallDir Ensure-ProfileBlock -PathEntry $InstallDir if ($env:HEADROOM_DOCKER_IMAGE) { $null = docker image inspect $InstallImage 2>$null if ($LASTEXITCODE -eq 0) { Write-Info "Using existing HEADROOM_DOCKER_IMAGE=$InstallImage" } else { Write-Info "Pulling $InstallImage" docker pull $InstallImage | Out-Null } } else { Write-Info "Pulling $ImageDefault" docker pull $ImageDefault | Out-Null } Write-Host "" Write-Host "Headroom Docker-native install complete." Write-Host "" Write-Host "Installed wrappers:" Write-Host " $InstallDir\headroom.ps1" Write-Host " $InstallDir\headroom.cmd" Write-Host "" Write-Host "Next steps:" Write-Host " 1. Restart PowerShell" Write-Host " 2. Try: headroom proxy" Write-Host " 3. Docs: https://github.com/chopratejas/headroom/blob/main/docs/docker-install.md"