Upload files to "/"

This commit is contained in:
2026-07-12 19:09:52 +02:00
commit a04de18db4
+581
View File
@@ -0,0 +1,581 @@
#requires -Version 5.1
<#
.SYNOPSIS
Idempotent Windows setup for Claude Code + CLIProxyAPI + claudex aliases.
.DESCRIPTION
- Installs Claude Code only when the `claude` command is missing.
- Downloads CLIProxyAPI v7.2.71 only when the expected executable is missing
(or when -ForceReinstall is supplied).
- Creates/updates config.yaml, preserving a timestamped backup when replacing it.
- Adds a managed `claudex` function to Windows PowerShell and PowerShell 7 profiles.
- Runs Codex OAuth login only when no Codex auth JSON is detected.
- Creates/updates a Task Scheduler task that starts at boot and restarts the proxy.
- Starts the task and verifies that TCP port 8317 is listening.
Run from PowerShell:
powershell -ExecutionPolicy Bypass -File .\setup-claudex-proxy.ps1
Optional:
.\setup-claudex-proxy.ps1 -SkipCodexLogin
.\setup-claudex-proxy.ps1 -ForceReinstall
.\setup-claudex-proxy.ps1 -NoStart
#>
[CmdletBinding()]
param(
[switch]$ForceReinstall,
[switch]$SkipCodexLogin,
[switch]$NoStart
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProxyVersion = '7.2.71'
$ProxyArchiveName = "CLIProxyAPI_${ProxyVersion}_windows_amd64.zip"
$ProxyDownloadUrl = "https://github.com/router-for-me/CLIProxyAPI/releases/download/v$ProxyVersion/$ProxyArchiveName"
$InstallDir = Join-Path $env:ProgramFiles "CLIProxyAPI_${ProxyVersion}_windows_amd64"
$ProxyExe = Join-Path $InstallDir 'cli-proxy-api.exe'
$ConfigPath = Join-Path $InstallDir 'config.yaml'
$RunnerPath = Join-Path $InstallDir 'run-proxy.ps1'
$TaskName = 'CLIProxyAPI'
$ProxyPort = 8317
$ProxyApiKey = 'sk-dummy'
$AuthDir = Join-Path $env:USERPROFILE '.cli-proxy-api'
function Write-Step {
param([Parameter(Mandatory)][string]$Message)
Write-Host "`n==> $Message" -ForegroundColor Cyan
}
function Write-Skip {
param([Parameter(Mandatory)][string]$Message)
Write-Host " SKIP: $Message" -ForegroundColor DarkGray
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Restart-Elevated {
Write-Step 'Administrator rights are required; reopening this script with UAC'
$forward = @(
'-NoLogo',
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', ('"{0}"' -f $PSCommandPath)
)
if ($ForceReinstall) { $forward += '-ForceReinstall' }
if ($SkipCodexLogin) { $forward += '-SkipCodexLogin' }
if ($NoStart) { $forward += '-NoStart' }
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList ($forward -join ' ')
exit
}
function Refresh-ProcessPath {
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = @($machinePath, $userPath) -join ';'
}
function Write-Utf8NoBom {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Content
)
$parent = Split-Path -Parent $Path
if ($parent) {
[IO.Directory]::CreateDirectory($parent) | Out-Null
}
$encoding = New-Object Text.UTF8Encoding($false)
[IO.File]::WriteAllText($Path, $Content, $encoding)
}
function Set-ManagedFile {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Content,
[switch]$BackupExisting
)
$normalized = $Content.TrimEnd() + [Environment]::NewLine
if (Test-Path -LiteralPath $Path) {
$existing = [IO.File]::ReadAllText($Path)
if ($existing -eq $normalized) {
Write-Skip "$Path is already correct"
return $false
}
if ($BackupExisting) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backup = "$Path.bak-$stamp"
Copy-Item -LiteralPath $Path -Destination $backup -Force
Write-Host " Backed up existing file to: $backup"
}
}
Write-Utf8NoBom -Path $Path -Content $normalized
Write-Host " Wrote: $Path"
return $true
}
function Install-ClaudeCodeIfMissing {
Write-Step 'Checking Claude Code'
if (Get-Command 'claude' -ErrorAction SilentlyContinue) {
$version = try { (& claude --version 2>$null | Select-Object -First 1) } catch { $null }
Write-Skip ("Claude Code is already installed" + $(if ($version) { ": $version" } else { '' }))
return
}
Write-Host ' Claude Code was not found. Running the official Windows installer...'
$installer = Invoke-RestMethod -Uri 'https://claude.ai/install.ps1'
Invoke-Expression $installer
Refresh-ProcessPath
if (-not (Get-Command 'claude' -ErrorAction SilentlyContinue)) {
throw 'Claude Code installation finished, but `claude` is still unavailable in PATH. Open a new PowerShell window and rerun this script.'
}
Write-Host ' Claude Code installed.'
}
function Install-ProxyIfMissing {
Write-Step "Checking CLIProxyAPI v$ProxyVersion"
if ((Test-Path -LiteralPath $ProxyExe) -and -not $ForceReinstall) {
Write-Skip "$ProxyExe already exists"
return
}
$tempRoot = Join-Path ([IO.Path]::GetTempPath()) ("cliproxy-setup-" + [guid]::NewGuid().ToString('N'))
$zipPath = Join-Path $tempRoot $ProxyArchiveName
$extractPath = Join-Path $tempRoot 'extract'
try {
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
New-Item -ItemType Directory -Path $extractPath -Force | Out-Null
Write-Host " Downloading: $ProxyDownloadUrl"
Invoke-WebRequest -Uri $ProxyDownloadUrl -OutFile $zipPath -UseBasicParsing
Write-Host ' Extracting archive...'
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractPath -Force
$foundExe = Get-ChildItem -LiteralPath $extractPath -Filter 'cli-proxy-api.exe' -File -Recurse |
Select-Object -First 1
if (-not $foundExe) {
throw 'The downloaded archive did not contain cli-proxy-api.exe.'
}
$sourceRoot = $foundExe.Directory.FullName
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Copy-Item -Path (Join-Path $sourceRoot '*') -Destination $InstallDir -Recurse -Force
if (-not (Test-Path -LiteralPath $ProxyExe)) {
throw "Extraction completed, but $ProxyExe was not created."
}
Write-Host " Installed to: $InstallDir"
}
finally {
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
function Configure-Proxy {
Write-Step 'Configuring CLIProxyAPI'
New-Item -ItemType Directory -Path $AuthDir -Force | Out-Null
$authDirYaml = $AuthDir.Replace('\', '/')
$config = @"
host: "127.0.0.1"
port: $ProxyPort
auth-dir: "$authDirYaml"
api-keys:
- "$ProxyApiKey"
debug: false
logging-to-file: true
logs-max-total-size-mb: 256
usage-statistics-enabled: false
"@
Set-ManagedFile -Path $ConfigPath -Content $config -BackupExisting | Out-Null
}
function Configure-Runner {
Write-Step 'Configuring the proxy watchdog script'
$escapedInstallDir = $InstallDir.Replace("'", "''")
$escapedProxyExe = $ProxyExe.Replace("'", "''")
$escapedConfigPath = $ConfigPath.Replace("'", "''")
$wrapperLog = (Join-Path $InstallDir 'proxy-watchdog.log').Replace("'", "''")
$runner = @"
`$ErrorActionPreference = 'Continue'
`$installDir = '$escapedInstallDir'
`$proxyExe = '$escapedProxyExe'
`$configPath = '$escapedConfigPath'
`$wrapperLog = '$wrapperLog'
Set-Location -LiteralPath `$installDir
while (`$true) {
`$started = Get-Date
try {
& `$proxyExe -config `$configPath
`$exitCode = `$LASTEXITCODE
Add-Content -LiteralPath `$wrapperLog -Value ("{0:u} Proxy exited with code {1}; restarting in 5 seconds." -f (Get-Date), `$exitCode)
}
catch {
Add-Content -LiteralPath `$wrapperLog -Value ("{0:u} Proxy crashed: {1}; restarting in 5 seconds." -f (Get-Date), `$_.Exception.Message)
}
# Avoid a hot restart loop if startup fails instantly.
`$runtime = (Get-Date) - `$started
if (`$runtime.TotalSeconds -lt 5) {
Start-Sleep -Seconds 5
}
}
"@
Set-ManagedFile -Path $RunnerPath -Content $runner | Out-Null
}
function Upsert-ManagedProfileBlock {
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Block
)
$begin = '# >>> CLIProxyAPI claudex BEGIN >>>'
$end = '# <<< CLIProxyAPI claudex END <<<'
$pattern = '(?ms)^[ \t]*' + [regex]::Escape($begin) + '.*?^[ \t]*' + [regex]::Escape($end) + '[ \t]*(?:\r?\n)?'
$existing = if (Test-Path -LiteralPath $Path) {
[IO.File]::ReadAllText($Path)
}
else {
''
}
$withoutManagedBlock = [regex]::Replace($existing, $pattern, '').TrimEnd()
$newContent = if ([string]::IsNullOrWhiteSpace($withoutManagedBlock)) {
$Block.TrimEnd() + [Environment]::NewLine
}
else {
$withoutManagedBlock + [Environment]::NewLine + [Environment]::NewLine + $Block.TrimEnd() + [Environment]::NewLine
}
if ($existing -eq $newContent) {
Write-Skip "$Path already contains the current claudex block"
return
}
Write-Utf8NoBom -Path $Path -Content $newContent
Write-Host " Updated: $Path"
}
function Configure-PowerShellProfiles {
Write-Step 'Configuring the claudex model aliases'
$profileBlock = @'
# >>> CLIProxyAPI claudex BEGIN >>>
$global:ClaudeModelAliases = @{
sol = 'gpt-5.6-sol'
terra = 'gpt-5.6-terra'
luna = 'gpt-5.6-luna'
}
function claudex {
param(
[Parameter(Position = 0)]
[string]$Model = 'sol',
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ClaudeArgs
)
$resolvedModel = if ($global:ClaudeModelAliases.ContainsKey($Model)) {
$global:ClaudeModelAliases[$Model]
}
else {
$Model
}
$names = @(
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN',
'CLAUDE_CODE_SUBAGENT_MODEL'
)
$previous = @{}
foreach ($name in $names) {
$previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
}
[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', 'http://127.0.0.1:8317', 'Process')
[Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', 'sk-dummy', 'Process')
[Environment]::SetEnvironmentVariable('CLAUDE_CODE_SUBAGENT_MODEL', $resolvedModel, 'Process')
try {
& claude --model $resolvedModel @ClaudeArgs
}
finally {
foreach ($name in $names) {
[Environment]::SetEnvironmentVariable($name, $previous[$name], 'Process')
}
}
}
# <<< CLIProxyAPI claudex END <<<
'@
$documents = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)
# All-host and ConsoleHost profiles for Windows PowerShell 5.1 and PowerShell 7.
# Writing the managed block last also overrides an older unmarked `claudex`
# function that may already exist earlier in a profile.
$profilePaths = @(
(Join-Path $documents 'WindowsPowerShell\profile.ps1'),
(Join-Path $documents 'WindowsPowerShell\Microsoft.PowerShell_profile.ps1'),
(Join-Path $documents 'PowerShell\profile.ps1'),
(Join-Path $documents 'PowerShell\Microsoft.PowerShell_profile.ps1')
) | Select-Object -Unique
foreach ($profilePath in $profilePaths) {
Upsert-ManagedProfileBlock -Path $profilePath -Block $profileBlock
}
}
function Test-CodexAuthentication {
if (-not (Test-Path -LiteralPath $AuthDir)) {
return $false
}
foreach ($file in Get-ChildItem -LiteralPath $AuthDir -Filter '*.json' -File -ErrorAction SilentlyContinue) {
try {
$data = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json
if (($data.type -eq 'codex') -and ($data.refresh_token -or $data.access_token)) {
return $true
}
}
catch {
# Ignore unrelated or malformed JSON files.
}
}
return $false
}
function Ensure-CodexAuthentication {
Write-Step 'Checking Codex OAuth authentication'
if (Test-CodexAuthentication) {
Write-Skip 'A Codex OAuth credential already exists'
return
}
if ($SkipCodexLogin) {
Write-Warning 'No Codex credential was found, and -SkipCodexLogin was supplied.'
return
}
Write-Host ' No Codex credential found. A browser login will open now.'
& $ProxyExe -config $ConfigPath -codex-login
if ($LASTEXITCODE -ne 0) {
throw "Codex login exited with code $LASTEXITCODE."
}
if (-not (Test-CodexAuthentication)) {
throw "Codex login completed, but no Codex credential was detected in $AuthDir."
}
Write-Host ' Codex OAuth credential saved.'
}
function Get-ExpectedTaskActionArguments {
$escapedRunner = $RunnerPath.Replace('"', '\"')
return "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$escapedRunner`""
}
function Test-ScheduledTaskMatches {
param([Parameter(Mandatory)]$Task)
$expectedExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$expectedArgs = Get-ExpectedTaskActionArguments
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = @($Task.Actions) | Select-Object -First 1
$trigger = @($Task.Triggers) | Select-Object -First 1
if (-not $action -or -not $trigger) {
return $false
}
$executeMatches = ([IO.Path]::GetFullPath($action.Execute) -ieq [IO.Path]::GetFullPath($expectedExe))
$argumentsMatch = ($action.Arguments -eq $expectedArgs)
$userMatches = ($Task.Principal.UserId -ieq $currentUser)
$logonMatches = ($Task.Principal.LogonType -eq 'S4U')
$startupTrigger = ($trigger.CimClass.CimClassName -eq 'MSFT_TaskBootTrigger')
return ($executeMatches -and $argumentsMatch -and $userMatches -and $logonMatches -and $startupTrigger)
}
function Configure-ScheduledTask {
Write-Step 'Configuring Task Scheduler'
Import-Module ScheduledTasks
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask -and (Test-ScheduledTaskMatches -Task $existingTask)) {
Write-Skip "Scheduled task '$TaskName' is already correct"
return
}
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$action = New-ScheduledTaskAction `
-Execute $powershellExe `
-Argument (Get-ExpectedTaskActionArguments) `
-WorkingDirectory $InstallDir
$trigger = New-ScheduledTaskTrigger -AtStartup
# S4U runs under the current Windows user without storing a password.
# The proxy config uses an explicit auth directory, so it does not depend on
# the task receiving a normal interactive user profile.
$principal = New-ScheduledTaskPrincipal `
-UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) `
-LogonType S4U `
-RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RestartCount 999 `
-RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) `
-MultipleInstances IgnoreNew
$task = New-ScheduledTask `
-Action $action `
-Trigger $trigger `
-Principal $principal `
-Settings $settings `
-Description 'Runs CLIProxyAPI for Claude Code through the local Codex-compatible proxy.'
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
Write-Host " Registered scheduled task: $TaskName"
}
function Test-TcpPort {
param(
[Parameter(Mandatory)][string]$HostName,
[Parameter(Mandatory)][int]$Port,
[int]$TimeoutMs = 1500
)
$client = New-Object Net.Sockets.TcpClient
try {
$result = $client.BeginConnect($HostName, $Port, $null, $null)
if (-not $result.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
return $false
}
$client.EndConnect($result)
return $true
}
catch {
return $false
}
finally {
$client.Close()
}
}
function Start-ProxyTask {
if ($NoStart) {
Write-Step 'Skipping task startup because -NoStart was supplied'
return
}
Write-Step 'Starting CLIProxyAPI'
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
if ($task.State -eq 'Running') {
Write-Skip "Scheduled task '$TaskName' is already running"
}
else {
# Stop a manually launched copy from this exact installation before
# starting the managed task, preventing a port-conflict restart loop.
Get-CimInstance Win32_Process -Filter "Name = 'cli-proxy-api.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and ($_.ExecutablePath -ieq $ProxyExe) } |
ForEach-Object {
Write-Host " Stopping unmanaged process PID $($_.ProcessId)..."
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
}
Start-ScheduledTask -TaskName $TaskName
}
$listening = $false
foreach ($attempt in 1..12) {
Start-Sleep -Seconds 1
if (Test-TcpPort -HostName '127.0.0.1' -Port $ProxyPort) {
$listening = $true
break
}
}
if ($listening) {
Write-Host " Proxy is listening on http://127.0.0.1:$ProxyPort" -ForegroundColor Green
}
else {
$info = Get-ScheduledTaskInfo -TaskName $TaskName
Write-Warning "Task started, but port $ProxyPort is not listening yet. LastTaskResult: $($info.LastTaskResult)"
Write-Warning "Check: $InstallDir\proxy-watchdog.log and $InstallDir\logs"
}
}
if (-not (Test-Administrator)) {
Restart-Elevated
}
try {
Install-ClaudeCodeIfMissing
Install-ProxyIfMissing
Configure-Proxy
Configure-Runner
Configure-PowerShellProfiles
Ensure-CodexAuthentication
Configure-ScheduledTask
Start-ProxyTask
Write-Host "`nSetup complete." -ForegroundColor Green
Write-Host 'Open a new PowerShell window, then use:'
Write-Host ' claudex sol'
Write-Host ' claudex terra'
Write-Host ' claudex luna'
Write-Host ' claudex gpt-5.6-sol --dangerously-skip-permissions'
}
catch {
Write-Host "`nSetup failed: $($_.Exception.Message)" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
exit 1
}