# ============================================================================= # Clodex - Codex installer for Windows (PowerShell) # ============================================================================= # Connects Codex App/CLI to Clodex (https://clodex.xyz/v1). # # Installs the Clodex Codex model catalog so Sol, Terra, Luna, and Spark have stable # names and capabilities in the Codex model picker. Existing auth.json and # unrelated config.toml settings are preserved. # # Recommended one-liner: # $env:CLODEX_API_KEY='clodex_YOUR_KEY'; iex(irm 'https://clodex.xyz/api/iw.ps1') # ============================================================================= [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [string] $ApiKey = $env:CLODEX_API_KEY, [string] $BaseUrl = "https://clodex.xyz/v1", [string] $ProviderId = "clodex", [string] $ProviderName = "Clodex", [string] $EnvVarName = "CLODEX_API_KEY", # Backward-compatible model parameters used by older documentation. [string] $Model = "", [string] $ReasoningEffort = "", [string] $ModelCatalogUrl = "https://clodex.xyz/api/downloads/codex/clodex-models.json", # Used by automated validation; normal users should leave these unset. [switch] $SkipProcessManagement, [switch] $ProcessScopeOnly ) $ErrorActionPreference = "Stop" $IsWindowsPlatform = ( $env:OS -eq "Windows_NT" -or $PSVersionTable.PSEdition -eq "Desktop" ) $EnvironmentScopes = if ($IsWindowsPlatform -and -not $ProcessScopeOnly) { @("User", "Process") } else { @("Process") } function Write-Step { param([string] $Message) Write-Host "[Clodex] $Message" -ForegroundColor Cyan } function Write-Done { param([string] $Message) Write-Host "[OK] $Message" -ForegroundColor Green } function Backup-FileIfExists { param( [string] $SourcePath, [string] $BackupDirectory ) if (Test-Path -LiteralPath $SourcePath) { $backupPath = Join-Path $BackupDirectory (Split-Path -Leaf $SourcePath) Copy-Item -LiteralPath $SourcePath -Destination $backupPath -Force Write-Done "Backup saved: $backupPath" } } function Write-Utf8NoBomFile { param( [string] $Path, [string] $Content ) $encoding = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($Path, $Content, $encoding) } function Test-ClodexModelCatalog { param([string] $Path) if (-not (Test-Path -LiteralPath $Path)) { return $false } try { $catalog = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json } catch { return $false } $models = @($catalog.models) $expectedModels = @{ "gpt-5.6-sol" = "GPT-5.6 Sol" "gpt-5.6-terra" = "GPT-5.6 Terra" "gpt-5.6-luna" = "GPT-5.6 Luna" "gpt-5.3-codex-spark" = "GPT-5.3-Codex-Spark" } foreach ($slug in $expectedModels.Keys) { $model = @($models | Where-Object { $_.slug -eq $slug }) | Select-Object -First 1 if ($null -eq $model) { return $false } if ( $model.use_responses_lite -ne $false -or $model.display_name -ne $expectedModels[$slug] -or $model.shell_type -ne "shell_command" -or $model.apply_patch_tool_type -ne "freeform" -or $model.supports_parallel_tool_calls -ne $true -or $model.visibility -ne "list" ) { return $false } } return $true } function Install-ClodexModelCatalog { param( [string] $Source, [string] $Destination ) $temporaryPath = "$Destination.download-$PID-$([guid]::NewGuid().ToString('N'))" try { if ($Source -match '^https?://') { $separator = if ($Source.Contains('?')) { '&' } else { '?' } Invoke-WebRequest ` -UseBasicParsing ` -Uri ("{0}{1}installer={2}" -f $Source, $separator, [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) ` -Headers @{ "Cache-Control" = "no-cache" } ` -OutFile $temporaryPath } elseif (Test-Path -LiteralPath $Source) { Copy-Item -LiteralPath $Source -Destination $temporaryPath -Force } else { throw "Model catalog source is unavailable: $Source" } if (-not (Test-ClodexModelCatalog -Path $temporaryPath)) { throw "Downloaded Clodex model catalog is invalid." } Move-Item -LiteralPath $temporaryPath -Destination $Destination -Force Write-Done "Installed Clodex Codex model catalog: $Destination" } finally { Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue } } function ConvertTo-TomlBasicString { param([string] $Value) return $Value.Replace('\', '\\').Replace('"', '\"') } function Update-ClodexDotEnv { param( [string] $Path, [string] $TargetEnvVarName, [string] $TargetValue ) $existing = "" if (Test-Path -LiteralPath $Path) { $existing = Get-Content -LiteralPath $Path -Raw } $escapedName = [regex]::Escape($TargetEnvVarName) $lines = if ([string]::IsNullOrEmpty($existing)) { @() } else { @($existing -split '\r?\n') } $kept = [System.Collections.Generic.List[string]]::new() foreach ($line in $lines) { if ( $line -match ( '^[ \t]*#[ \t]*Managed by Clodex installer:[ \t]*' + $escapedName + '[ \t]*$' ) ) { continue } if ($line -match ('^[ \t]*(?:export[ \t]+)?' + $escapedName + '[ \t]*=')) { continue } $kept.Add($line) | Out-Null } while ($kept.Count -gt 0 -and [string]::IsNullOrWhiteSpace($kept[$kept.Count - 1])) { $kept.RemoveAt($kept.Count - 1) } if ($kept.Count -gt 0) { $kept.Add("") | Out-Null } $kept.Add("# Managed by Clodex installer: $TargetEnvVarName") | Out-Null $kept.Add("$TargetEnvVarName=$TargetValue") | Out-Null Write-Utf8NoBomFile -Path $Path -Content ($kept -join [Environment]::NewLine) } function Test-LegacyClodexAuthFile { param([string] $Path) if (-not (Test-Path -LiteralPath $Path)) { return $false } try { $auth = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json } catch { return $false } $propertyNames = @($auth.PSObject.Properties.Name) if ( $propertyNames.Count -ne 2 -or $propertyNames -notcontains "auth_mode" -or $propertyNames -notcontains "OPENAI_API_KEY" ) { return $false } $storedKey = [string] $auth.OPENAI_API_KEY return ( $auth.auth_mode -eq "apikey" -and -not [string]::IsNullOrWhiteSpace($storedKey) -and $storedKey.StartsWith("clodex_", [StringComparison]::OrdinalIgnoreCase) ) } function Remove-LegacyClodexAuthFile { param( [string] $Path, [string] $BackupDirectory ) if (Test-LegacyClodexAuthFile -Path $Path) { Backup-FileIfExists -SourcePath $Path -BackupDirectory $BackupDirectory Remove-Item -LiteralPath $Path -Force Write-Done "Removed legacy Clodex-generated auth.json; other auth files are preserved." } } function Clear-LegacyClodexOpenAiEnvironmentKey { foreach ($scope in $EnvironmentScopes) { $currentValue = [Environment]::GetEnvironmentVariable("OPENAI_API_KEY", $scope) if ( -not [string]::IsNullOrWhiteSpace($currentValue) -and $currentValue.StartsWith("clodex_", [StringComparison]::OrdinalIgnoreCase) ) { [Environment]::SetEnvironmentVariable("OPENAI_API_KEY", $null, $scope) Write-Done "Removed legacy Clodex value from OPENAI_API_KEY ($scope scope)." } } } function Update-ClodexConfig { param( [string] $Path, [string] $TargetProviderId, [string] $TargetProviderName, [string] $TargetBaseUrl, [string] $TargetEnvVarName, [string] $TargetModelCatalogPath ) $existing = "" if (Test-Path -LiteralPath $Path) { $existing = Get-Content -LiteralPath $Path -Raw } $providerSectionName = "model_providers.$TargetProviderId" $lines = if ([string]::IsNullOrEmpty($existing)) { @() } else { @($existing -split '\r?\n') } $kept = [System.Collections.Generic.List[string]]::new() $inTopLevel = $true $skipProviderSection = $false foreach ($line in $lines) { if ($line -match '^[ \t]*\[\[?([^\]]+)\]\]?[ \t]*(?:#.*)?$') { $sectionName = $Matches[1].Trim() $isTargetProviderSection = ( $sectionName -ieq $providerSectionName -or $sectionName.StartsWith( "$providerSectionName.", [StringComparison]::OrdinalIgnoreCase ) ) $skipProviderSection = $isTargetProviderSection $inTopLevel = $false if (-not $skipProviderSection) { $kept.Add($line) | Out-Null } continue } if ($skipProviderSection) { continue } if ($inTopLevel) { if ($line -match '^[ \t]*#[ \t]*Generated by Clodex installer.*$') { continue } if ($line -match '^[ \t]*model_provider[ \t]*=') { continue } if ($line -match '^[ \t]*model_catalog_json[ \t]*=') { continue } } $kept.Add($line) | Out-Null } while ($kept.Count -gt 0 -and [string]::IsNullOrWhiteSpace($kept[$kept.Count - 1])) { $kept.RemoveAt($kept.Count - 1) } $firstSectionIndex = $kept.Count for ($index = 0; $index -lt $kept.Count; $index++) { if ($kept[$index] -match '^[ \t]*\[') { $firstSectionIndex = $index break } } $updated = [System.Collections.Generic.List[string]]::new() for ($index = 0; $index -lt $firstSectionIndex; $index++) { $updated.Add($kept[$index]) | Out-Null } while ( $updated.Count -gt 0 -and [string]::IsNullOrWhiteSpace($updated[$updated.Count - 1]) ) { $updated.RemoveAt($updated.Count - 1) } if ($updated.Count -gt 0) { $updated.Add("") | Out-Null } $updated.Add("model_provider = `"$TargetProviderId`"") | Out-Null $catalogPathToml = ConvertTo-TomlBasicString -Value $TargetModelCatalogPath $updated.Add("model_catalog_json = `"$catalogPathToml`"") | Out-Null if ($firstSectionIndex -lt $kept.Count) { $updated.Add("") | Out-Null for ($index = $firstSectionIndex; $index -lt $kept.Count; $index++) { $updated.Add($kept[$index]) | Out-Null } while ( $updated.Count -gt 0 -and [string]::IsNullOrWhiteSpace($updated[$updated.Count - 1]) ) { $updated.RemoveAt($updated.Count - 1) } } $updated.Add("") | Out-Null $updated.Add("[model_providers.$TargetProviderId]") | Out-Null $updated.Add("name = `"$TargetProviderName`"") | Out-Null $updated.Add("base_url = `"$TargetBaseUrl`"") | Out-Null $updated.Add('wire_api = "responses"') | Out-Null $updated.Add("env_key = `"$TargetEnvVarName`"") | Out-Null $updated.Add("supports_websockets = false") | Out-Null $updated.Add("requires_openai_auth = false") | Out-Null Write-Utf8NoBomFile -Path $Path -Content ($updated -join [Environment]::NewLine) } if ([string]::IsNullOrWhiteSpace($ApiKey)) { Write-Host "Error: API key is missing." -ForegroundColor Red Write-Host "Run: `$env:CLODEX_API_KEY=''; iex(irm 'https://clodex.xyz/api/iw.ps1')" -ForegroundColor Yellow exit 1 } $ApiKey = $ApiKey.Trim() if ( $ApiKey.Length -lt 16 -or $ApiKey -match "[`r`n]" -or $ApiKey -notmatch '^[A-Za-z0-9._~+/=-]+$' ) { Write-Host "Error: the Clodex API key is invalid or incomplete." -ForegroundColor Red exit 1 } if ( -not [string]::IsNullOrWhiteSpace($Model) -or -not [string]::IsNullOrWhiteSpace($ReasoningEffort) ) { Write-Host "Note: legacy model/reasoning arguments are ignored." -ForegroundColor Yellow Write-Host "Choose the model and reasoning effort in Codex after installation." } $CodexDir = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) { Join-Path $env:USERPROFILE ".codex" } else { $env:CODEX_HOME } $AuthPath = Join-Path $CodexDir "auth.json" $ConfigPath = Join-Path $CodexDir "config.toml" $DotEnvPath = Join-Path $CodexDir ".env" $LegacyEnvPath = Join-Path $CodexDir "clodex.env" $ModelCatalogPath = Join-Path $CodexDir "clodex-models.json" $BackupDir = Join-Path $CodexDir ("backups\clodex-install-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss")) New-Item -ItemType Directory -Force -Path $CodexDir, $BackupDir | Out-Null $chatGptWasRunning = $false if (-not $SkipProcessManagement) { $chatGptWasRunning = @( Get-Process -Name "ChatGPT" -ErrorAction SilentlyContinue ).Count -gt 0 if ($chatGptWasRunning) { Write-Step "ChatGPT is running. It will not be force-closed; fully quit and reopen it after installation." } } Backup-FileIfExists -SourcePath $ConfigPath -BackupDirectory $BackupDir Backup-FileIfExists -SourcePath $DotEnvPath -BackupDirectory $BackupDir if (Test-Path -LiteralPath $LegacyEnvPath) { Backup-FileIfExists -SourcePath $LegacyEnvPath -BackupDirectory $BackupDir Remove-Item -LiteralPath $LegacyEnvPath -Force Write-Done "Removed obsolete clodex.env; the desktop app uses $DotEnvPath." } if (Test-Path -LiteralPath $ModelCatalogPath) { Backup-FileIfExists -SourcePath $ModelCatalogPath -BackupDirectory $BackupDir } Remove-LegacyClodexAuthFile -Path $AuthPath -BackupDirectory $BackupDir Install-ClodexModelCatalog ` -Source $ModelCatalogUrl ` -Destination $ModelCatalogPath Update-ClodexDotEnv ` -Path $DotEnvPath ` -TargetEnvVarName $EnvVarName ` -TargetValue $ApiKey Update-ClodexConfig ` -Path $ConfigPath ` -TargetProviderId $ProviderId ` -TargetProviderName $ProviderName ` -TargetBaseUrl $BaseUrl ` -TargetEnvVarName $EnvVarName ` -TargetModelCatalogPath $ModelCatalogPath foreach ($scope in $EnvironmentScopes) { [Environment]::SetEnvironmentVariable($EnvVarName, $ApiKey, $scope) } Clear-LegacyClodexOpenAiEnvironmentKey $writtenConfig = Get-Content -LiteralPath $ConfigPath -Raw $requiredConfigLines = @( "model_provider = `"$ProviderId`"", "model_catalog_json = `"$(ConvertTo-TomlBasicString -Value $ModelCatalogPath)`"", "[model_providers.$ProviderId]", "base_url = `"$BaseUrl`"", 'wire_api = "responses"', "env_key = `"$EnvVarName`"", "requires_openai_auth = false" ) $missingConfigLines = @( $requiredConfigLines | Where-Object { $writtenConfig -notmatch [regex]::Escape($_) } ) if ($missingConfigLines.Count -gt 0) { throw "Config validation failed. Missing: $($missingConfigLines -join ', ')" } if (-not (Test-ClodexModelCatalog -Path $ModelCatalogPath)) { throw "Config validation failed: Clodex model catalog is missing or invalid." } if ([Environment]::GetEnvironmentVariable($EnvVarName, "Process") -ne $ApiKey) { throw "Environment validation failed: $EnvVarName is not available to this process." } $writtenDotEnv = Get-Content -LiteralPath $DotEnvPath -Raw $expectedDotEnvLine = '(?m)^[ \t]*' + [regex]::Escape($EnvVarName) + '[ \t]*=[ \t]*' + [regex]::Escape($ApiKey) + '[ \t]*$' if ($writtenDotEnv -notmatch $expectedDotEnvLine) { throw "Desktop app environment validation failed: $EnvVarName is missing from $DotEnvPath." } Write-Done "Validated provider config and desktop app API-key environment." if (-not $SkipProcessManagement -and -not $chatGptWasRunning) { try { Start-Process -FilePath "codex://threads/new" -ErrorAction Stop Write-Done "Opened the ChatGPT desktop app." } catch { Write-Host "Open the ChatGPT desktop app from the Start menu." -ForegroundColor Yellow } } Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host " Done. Codex is connected to Clodex." -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green Write-Host " Endpoint : $BaseUrl" Write-Host " Provider : $ProviderId" Write-Host " Catalog : $ModelCatalogPath" Write-Host " Env var : $EnvVarName" Write-Host " Models : use the model picker in Codex" Write-Host " Config : $ConfigPath" Write-Host " App env : $DotEnvPath" Write-Host " Backup : $BackupDir" Write-Host "" Write-Host " auth.json and unrelated config.toml settings were preserved." Write-Host " Do not launch Codex.exe directly; that executable is the CLI/agent sidecar." Write-Host " Fully quit ChatGPT (including the tray icon), reopen it, and create a NEW TASK." Write-Host " Then choose Sol, Terra, or Luna in the model picker." Write-Host "============================================================" -ForegroundColor Green