@echo off
REM ===========================================================================
REM  AirLLM - One-File Setup  (Windows / double-click)
REM  https://api.airllm.ai
REM
REM  Same setup as airllm-setup-windows.ps1, packaged as a .bat so it runs on
REM  a double-click - no PowerShell execution-policy issues, no right-click
REM  menus. (The .bat runs the PowerShell logic embedded below with
REM  -ExecutionPolicy Bypass, which works on every Windows 10/11 machine.)
REM
REM  WHAT IT DOES
REM    Points the coding tool you choose (or ALL supported tools) at AirLLM:
REM    1. Writes each tool's own CONFIG FILE (the reliable part):
REM         Claude Code -> %%USERPROFILE%%\.claude\settings.json
REM         Codex       -> %%USERPROFILE%%\.codex\config.toml
REM         OpenCode    -> %%USERPROFILE%%\.config\opencode\opencode.json
REM         Aider       -> %%USERPROFILE%%\.aider.conf.yml
REM    2. Sets User-scope environment variables for the rest (Codex's key,
REM         GitHub Copilot CLI BYOK, agent frameworks) - only what the chosen
REM         tool needs. Only agent frameworks get the shared OPENAI_* variables,
REM         and always as base URL + key together.
REM    3. A saved Claude/Anthropic or ChatGPT login does NOT need removing:
REM         gateway credentials take precedence (Claude Code) and Codex
REM         ignores its ChatGPT login for custom providers.
REM
REM  HOW TO USE
REM    1. REQUIRED: replace your_airllm_api_key below with your AirLLM API key.
REM       (Skipped if you downloaded this file with the key pre-filled.)
REM    2. Double-click this file.
REM    3. Open a new terminal afterwards.
REM
REM  Safe to re-run: it merges into existing config, never duplicates.
REM ===========================================================================

REM ---------------------------------------------------------------------------
REM  STEP 1 - PASTE YOUR AIRLLM API KEY BETWEEN THE QUOTES vvv
REM ---------------------------------------------------------------------------
set "AIRLLM_API_KEY=your_airllm_api_key"

REM Default model. airllm-auto picks a model for each request. For coding tools
REM you can use instead: airllm-openai, airllm-anthropic or airllm-agent.
REM airllm-coding and airllm-thinking are chat aliases (for chat apps, not
REM coding agents). With a Token-plan key, a catalog model id listed at
REM https://airllm.ai/models works too (for example z-ai/glm-5).
set "AIRLLM_MODEL=airllm-auto"

REM Which tool to configure. Set automatically when you pick a tool on the
REM download page. Valid values:
REM   all / claude-code / codex / opencode / aider / copilot-cli /
REM   agent-frameworks / cursor / continue / cline / roo-code / openhands
set "AIRLLM_TOOL=all"

REM Gateway endpoints (do not change). Claude Code and other Anthropic-style
REM clients append /v1/... to AIRLLM_BASE_URL themselves; OpenAI-style tools
REM (Codex, Aider, Cursor, agent frameworks...) append /chat/completions or
REM /responses to AIRLLM_OPENAI_URL. Mixing them up doubles or drops the /v1.
set "AIRLLM_BASE_URL=https://api.airllm.ai/"
set "AIRLLM_OPENAI_URL=https://api.airllm.ai/v1"

REM ===========================================================================
REM  No edits needed below this line. Runs the PowerShell payload at the end
REM  of this file (marker is split so this line doesn't match it).
REM ===========================================================================
set "AIRLLM_SELF=%~f0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$s=[IO.File]::ReadAllText($env:AIRLLM_SELF); $m=':PS'+'PAYLOAD'; $p=$s.Substring($s.LastIndexOf($m)+$m.Length); Invoke-Expression $p"
echo.
pause
exit /b 0

:PSPAYLOAD
# ============================================================================
#  PowerShell payload (executed by the batch line above; cmd never reads this)
#  Identical behavior to airllm-setup-windows.ps1 - config comes from the
#  batch `set` lines via environment variables.
# ============================================================================
$ErrorActionPreference = "Stop"
$AirLLMApiKey    = $env:AIRLLM_API_KEY
$AirLLMModel     = if ($env:AIRLLM_MODEL) { $env:AIRLLM_MODEL } else { "airllm-auto" }
$AirLLMTool      = if ($env:AIRLLM_TOOL) { $env:AIRLLM_TOOL } else { "all" }
$AirLLMBaseUrl   = if ($env:AIRLLM_BASE_URL) { $env:AIRLLM_BASE_URL } else { "https://api.airllm.ai/" }
$AirLLMOpenAIUrl = if ($env:AIRLLM_OPENAI_URL) { $env:AIRLLM_OPENAI_URL } else { "https://api.airllm.ai/v1" }
function Ok($m)   { Write-Host "  [ok] $m" -ForegroundColor Green }
function Warn($m) { Write-Host "  [!] $m"  -ForegroundColor Yellow }
function Note($m) { Write-Host "  [i] $m"  -ForegroundColor Cyan }
# Configure this tool this run? (true for the tool itself, or when tool = all)
function Want-Tool($t) { return ($AirLLMTool -eq "all" -or $AirLLMTool -eq $t) }

$toolLabels = @{
  "all" = "all supported tools"; "claude-code" = "Claude Code"; "codex" = "Codex";
  "opencode" = "OpenCode"; "aider" = "Aider"; "copilot-cli" = "GitHub Copilot CLI";
  "agent-frameworks" = "agent frameworks"; "cursor" = "Cursor"; "continue" = "Continue";
  "cline" = "Cline"; "roo-code" = "Roo Code"; "openhands" = "OpenHands"
}
if (-not $toolLabels.ContainsKey($AirLLMTool)) {
  Write-Host ""
  Write-Host "  [x] Unknown tool value: `"$AirLLMTool`". Nothing was changed." -ForegroundColor Red
  Write-Host "  Valid values: all, claude-code, codex, opencode, aider, copilot-cli,"
  Write-Host "  agent-frameworks, cursor, continue, cline, roo-code, openhands"
  exit 1
}
$ToolLabel = $toolLabels[$AirLLMTool]

Write-Host ""
Write-Host "  AirLLM one-file setup - Windows" -ForegroundColor White
Write-Host "  Target: $ToolLabel" -ForegroundColor Cyan
Write-Host "  ==============================="
Write-Host ""

# --- API key -----------------------------------------------------------------
if (-not $AirLLMApiKey -or $AirLLMApiKey -eq "your_airllm_api_key") {
  Write-Host "  No API key found in this file." -ForegroundColor Yellow
  Write-Host "  Open airllm-setup-windows.bat in Notepad and replace"
  Write-Host "  your_airllm_api_key with your real key - or paste it now."
  Write-Host ""
  $AirLLMApiKey = Read-Host "  Paste your AirLLM API key (or Ctrl+C to quit)"
  Write-Host ""
}
if (-not $AirLLMApiKey -or $AirLLMApiKey -eq "your_airllm_api_key") {
  Write-Host "  [x] No API key provided. Nothing was changed." -ForegroundColor Red
  exit 1
}

# Print the settings for a UI-configured tool (Cursor, Cline, ...). These tools
# store provider settings inside their own UI, so a script can't write them.
function Print-UiTool($name) {
  Write-Host ""
  Write-Host "  $name - enter these in the tool's settings UI:" -ForegroundColor White
  Write-Host "  ---------------------------------------------------------------"
  Write-Host "    Base URL : $AirLLMOpenAIUrl" -ForegroundColor Cyan
  Write-Host "    API key  : $AirLLMApiKey" -ForegroundColor Cyan
  if ($name -eq "OpenHands") {
    Write-Host "    Model    : openai/$AirLLMModel   (Custom Model - LiteLLM needs the openai/ prefix)" -ForegroundColor Cyan
  } else {
    Write-Host "    Model    : $AirLLMModel" -ForegroundColor Cyan
  }
}

# --- 1. Claude Code -> settings.json (merge; model values cover background calls;
#        ANTHROPIC_AUTH_TOKEN needs no 'trust this key?' approval and takes
#        precedence over any saved claude.ai login - no sign-out needed) --------
if (Want-Tool "claude-code") {
  $claudeDir  = Join-Path $env:USERPROFILE ".claude"
  New-Item -ItemType Directory -Force -Path $claudeDir | Out-Null
  $claudeFile = Join-Path $claudeDir "settings.json"
  $cfg = $null
  if (Test-Path $claudeFile) {
    try { $cfg = Get-Content -Raw $claudeFile | ConvertFrom-Json } catch { $cfg = $null }
    if ($null -eq $cfg -or $cfg -is [System.Array] -or $cfg -is [string] -or $cfg -is [System.ValueType]) {
      Copy-Item -Force $claudeFile ($claudeFile + ".airllm-bak")   # unparseable content is preserved, never lost
      $cfg = $null
    }
  }
  if ($null -eq $cfg) { $cfg = [pscustomobject]@{} }
  # "env" may exist as null/array/string in a hand-mangled file - Add-Member on
  # those silently no-ops or corrupts, so replace anything that isn't an object.
  $envBad = -not ($cfg.PSObject.Properties.Name -contains "env")
  if (-not $envBad) { $v = $cfg.env; $envBad = ($null -eq $v -or $v -is [System.Array] -or $v -is [string] -or $v -is [System.ValueType]) }
  if ($envBad) {
    $cfg | Add-Member -NotePropertyName env -NotePropertyValue ([pscustomobject]@{}) -Force
  }
  $vals = [ordered]@{
    ANTHROPIC_BASE_URL             = $AirLLMBaseUrl
    ANTHROPIC_AUTH_TOKEN           = $AirLLMApiKey
    API_TIMEOUT_MS                 = "3000000"
    CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"
    ANTHROPIC_MODEL                = $AirLLMModel
    ANTHROPIC_DEFAULT_HAIKU_MODEL  = $AirLLMModel
    ANTHROPIC_DEFAULT_SONNET_MODEL = $AirLLMModel
    ANTHROPIC_DEFAULT_OPUS_MODEL   = $AirLLMModel
    ANTHROPIC_DEFAULT_FABLE_MODEL  = $AirLLMModel
    ANTHROPIC_SMALL_FAST_MODEL     = $AirLLMModel
    CLAUDE_CODE_SUBAGENT_MODEL     = $AirLLMModel   # subagents -> the gateway model too
    CLAUDE_CODE_SUBAGENT_MODEL_FORCE = "1"          # ...even ones whose definition names a model (Claude Code 2.1.257+)
  }
  foreach ($k in $vals.Keys) { $cfg.env | Add-Member -NotePropertyName $k -NotePropertyValue $vals[$k] -Force }
  [System.IO.File]::WriteAllText($claudeFile, ($cfg | ConvertTo-Json -Depth 100))
  Ok "Claude Code configured -> $claudeFile"

  # Mark first-run onboarding done in .claude.json so a FRESH install goes
  # straight to the gateway instead of the setup wizard (merged; other fields
  # preserved; skipped when the file can't be parsed).
  $claudeState = Join-Path $env:USERPROFILE ".claude.json"
  try {
    $st = $null
    if (Test-Path $claudeState) { $st = Get-Content -Raw $claudeState | ConvertFrom-Json }
    if ($st -is [System.Array] -or $st -is [string] -or $st -is [System.ValueType]) { throw "claude.json is not a JSON object" }
    if ($null -eq $st) { $st = [pscustomobject]@{} }
    $st | Add-Member -NotePropertyName hasCompletedOnboarding -NotePropertyValue $true -Force
    [System.IO.File]::WriteAllText($claudeState, ($st | ConvertTo-Json -Depth 100))
  } catch { }

  # A saved login does NOT block the gateway: ANTHROPIC_AUTH_TOKEN takes
  # precedence (code.claude.com/docs/en/authentication). Just mention it.
  $claudeCreds = Join-Path $claudeDir ".credentials.json"
  $loggedIn = (Test-Path $claudeCreds)
  if (-not $loggedIn -and (Test-Path $claudeState)) {
    try { $loggedIn = ((Get-Content -Raw $claudeState) -match '"oauthAccount"') } catch {}
  }
  if ($loggedIn) {
    Note "Claude Code has a saved Anthropic login. That's fine - the AirLLM token takes precedence and the login stays saved but unused. If Claude Code ever warns about auth, run /logout inside claude once."
  }
}

# --- 2. Codex -> config.toml (custom provider; Responses API is the only wire
#        API Codex supports for custom providers since Feb 2026; the key is read
#        from the AIRLLM_API_KEY env var set below - Codex's own name, so other
#        OpenAI tools never pick up the AirLLM key). Existing files ARE merged
#        when safe: top-level keys prepended + provider table appended, with a
#        backup kept. Files that pick their own model/provider are left alone. ---
if (Want-Tool "codex") {
  $codexDir  = Join-Path $env:USERPROFILE ".codex"
  New-Item -ItemType Directory -Force -Path $codexDir | Out-Null
  $codexFile = Join-Path $codexDir "config.toml"
  $airllmProviderToml = @"
[model_providers.airllm]
name = "AirLLM"
base_url = "$AirLLMOpenAIUrl"
env_key = "AIRLLM_API_KEY"
wire_api = "responses"
"@
  if (Test-Path $codexFile) {
    $raw = Get-Content -Raw $codexFile
    if ($null -eq $raw) { $raw = "" }   # 0-byte file: -Raw returns $null; .TrimEnd() below must not NPE
    $urlEsc = [regex]::Escape($AirLLMOpenAIUrl)
    $mdlEsc = [regex]::Escape($AirLLMModel)
    # Anchored, non-comment matches only: a commented-out block (# [model_pro...)
    # must NOT count as "already configured".
    if ($raw -match '(?m)^[ \t]*\[model_providers\.airllm\]') {
      # A block this script wrote before read the key from OPENAI_API_KEY: move it to
      # AIRLLM_API_KEY (inside the airllm table only).
      if ($raw.Contains("by airllm-setup") -and -not ($raw -match '(?m)^[ \t]*env_key[ \t]*=[ \t]*"AIRLLM_API_KEY"')) {
        $raw = ([regex]'(?m)(^[ \t]*\[model_providers\.airllm\][^\n]*\n(?:(?![ \t]*\[)[^\n]*\n)*?)[ \t]*env_key[ \t]*=[^\r\n]*').Replace($raw, '${1}env_key = "AIRLLM_API_KEY"', 1)
        [System.IO.File]::WriteAllText($codexFile, $raw)
      }
      # Don't just claim success - make sure the existing block still has the
      # right endpoint, key variable and wire API (an old or hand-written block may not).
      if (($raw -match "(?m)^[ \t]*base_url[ \t]*=[ \t]*`"$urlEsc`"") -and ($raw -match '(?m)^[ \t]*env_key[ \t]*=[ \t]*"AIRLLM_API_KEY"') -and ($raw -match '(?m)^[ \t]*wire_api[ \t]*=[ \t]*"responses"')) {
        if ($raw -match "(?m)^model[ \t]*=[ \t]*`"$mdlEsc`"") {
          Ok "Codex already points at AirLLM in $codexFile"
        } elseif ($raw.Contains("by airllm-setup")) {
          # A file this script wrote: refresh the default-model line so a newly
          # chosen model actually lands on re-runs.
          $updated = ([regex]'(?m)^model[ \t]*=[^\r\n]*').Replace($raw, "model = `"$AirLLMModel`"", 1)
          if ($updated -match "(?m)^model[ \t]*=[ \t]*`"$mdlEsc`"") {
            [System.IO.File]::WriteAllText($codexFile, $updated)
            Ok "Codex already pointed at AirLLM - default model updated to $AirLLMModel"
          } else {
            Warn "Codex: config.toml points at AirLLM but its default model is not $AirLLMModel - left as-is. Edit its  model =  line to switch."
          }
        } else {
          Warn "Codex: config.toml points at AirLLM but its default model is not $AirLLMModel - left as-is. Edit its  model =  line to switch."
        }
      } else {
        Warn "Codex: config.toml has an [model_providers.airllm] block with UNEXPECTED settings - left as-is. Make sure it has  base_url = `"$AirLLMOpenAIUrl`",  env_key = `"AIRLLM_API_KEY`"  and  wire_api = `"responses`"  (Codex removed every other wire API in Feb 2026)."
      }
    } else {
      # No [model_providers.airllm] block yet. Point Codex at AirLLM UNLESS the
      # file makes a deliberate provider choice we must not hijack: its own
      # model_provider = "<non-airllm>", or a bare [model_providers] table we
      # can't extend blindly. A lone  model = "gpt-x"  (just a saved model pick,
      # no provider) is NOT that - we rewrite it below so `codex` stops falling
      # back to its default ChatGPT-login provider. (The old rule skipped on any
      # top-level  model =  line, which left ChatGPT users unconfigured.)
      # TOML root keys must precede the first [table]: split the file there so we
      # only inspect/scrub the head and never touch keys inside tables.
      $firstTable = [regex]::Match($raw, '(?m)^[ \t]*\[')
      if ($firstTable.Success) { $chead = $raw.Substring(0, $firstTable.Index); $cbody = $raw.Substring($firstTable.Index) }
      else { $chead = $raw; $cbody = "" }
      $otherProvider = $false
      if ($chead -match '(?m)^[ \t]*model_provider[ \t]*=[ \t]*"([^"]*)"') { $otherProvider = ($Matches[1] -ne "airllm") }
      $bareTable = $raw -match '(?m)^\s*\[model_providers\]\s*$'
      if ($otherProvider -or $bareTable) {
        Warn "Codex: config.toml already sets its own model_provider - left as-is. To use AirLLM set  model_provider = `"airllm`"  and  model = `"$AirLLMModel`"  at the top, plus a [model_providers.airllm] block (base_url = `"$AirLLMOpenAIUrl`", env_key = `"AIRLLM_API_KEY`", wire_api = `"responses`")."
      } else {
        Copy-Item -Force $codexFile ($codexFile + ".airllm-bak")
        # Drop any existing top-level model / model_provider line (it would
        # duplicate our keys and make the TOML invalid); keys inside tables live
        # in $cbody and are left untouched.
        $chead = $chead -replace '(?m)^[ \t]*model[ \t]*=.*(\r?\n)?', ''
        $chead = $chead -replace '(?m)^[ \t]*model_provider[ \t]*=.*(\r?\n)?', ''
        $chead = $chead.Trim()
        $merged = "# AirLLM defaults - added by airllm-setup-windows.bat`nmodel = `"$AirLLMModel`"`nmodel_provider = `"airllm`"`n"
        if ($chead.Length -gt 0) { $merged += "`n" + $chead + "`n" }
        $merged += "`n" + $cbody.TrimEnd() + "`n`n# AirLLM provider for Codex - added by airllm-setup-windows.bat`n" + $airllmProviderToml
        [System.IO.File]::WriteAllText($codexFile, $merged)
        Ok "Codex configured -> $codexFile (merged; backup at config.toml.airllm-bak)"
      }
    }
  } else {
    $toml = "# AirLLM provider for Codex - written by airllm-setup-windows.bat`nmodel = `"$AirLLMModel`"`nmodel_provider = `"airllm`"`n`n" + $airllmProviderToml
    [System.IO.File]::WriteAllText($codexFile, $toml)
    Ok "Codex configured -> $codexFile"
  }
  # Ensure the model-metadata keys exist (all paths, incl. older installs).
  # Codex has no built-in entry for the AirLLM alias - without these it warns
  # "Model metadata not found" and falls back to guessed context/output limits.
  # Inserted top-level right after model_provider - never appended after a [table].
  if (Test-Path $codexFile) {
    $craw = [System.IO.File]::ReadAllText($codexFile)
    if ($craw -match '(?m)^model_provider[ \t]*=[ \t]*"airllm"' -and $craw -notmatch '(?m)^model_context_window') {
      $metaBlock = "model_provider = `"airllm`"`n# Model metadata for the AirLLM alias - added by airllm-setup-windows.bat`nmodel_context_window = 200000`nmodel_max_output_tokens = 32000"
      $craw = ([regex]'(?m)^model_provider[ \t]*=[ \t]*"airllm"').Replace($craw, $metaBlock, 1)
      [System.IO.File]::WriteAllText($codexFile, $craw)
      Ok "Codex model metadata set (context 200k / output 32k)"
    }
  }
  # A ChatGPT login in .codex\auth.json is IGNORED for custom providers -
  # Codex reads the env_key instead. No sign-out needed; just mention it.
  $codexAuth = Join-Path $codexDir "auth.json"
  if (Test-Path $codexAuth) {
    Note "Codex has a saved ChatGPT login (.codex\auth.json). That's fine - custom providers use their env_key (AIRLLM_API_KEY) and ignore the login. Run 'codex logout' only if you want it gone."
  }
}

# --- 3. OpenCode -> opencode.json (merge; default model so plain `opencode`
#        works; the key lives in the config file - no env var needed) ------------
if (Want-Tool "opencode") {
  $ocDir  = Join-Path $env:USERPROFILE ".config\opencode"
  New-Item -ItemType Directory -Force -Path $ocDir | Out-Null
  $ocFile = Join-Path $ocDir "opencode.json"
  $oc = $null
  if (Test-Path $ocFile) {
    try { $oc = Get-Content -Raw $ocFile | ConvertFrom-Json } catch { $oc = $null }
    if ($null -eq $oc -or $oc -is [System.Array] -or $oc -is [string] -or $oc -is [System.ValueType]) {
      Copy-Item -Force $ocFile ($ocFile + ".airllm-bak")   # unparseable content is preserved, never lost
      $oc = $null
    }
  }
  if ($null -eq $oc) { $oc = [pscustomobject]@{} }
  if (-not ($oc.PSObject.Properties.Name -contains '$schema')) {
    $oc | Add-Member -NotePropertyName '$schema' -NotePropertyValue "https://opencode.ai/config.json" -Force
  }
  $provBad = -not ($oc.PSObject.Properties.Name -contains "provider")
  if (-not $provBad) { $v = $oc.provider; $provBad = ($null -eq $v -or $v -is [System.Array] -or $v -is [string] -or $v -is [System.ValueType]) }
  if ($provBad) {
    $oc | Add-Member -NotePropertyName provider -NotePropertyValue ([pscustomobject]@{}) -Force
  }
  $airllmProvider = [pscustomobject]@{
    npm     = "@ai-sdk/openai-compatible"
    name    = "AirLLM"
    options = [pscustomobject]@{ baseURL = $AirLLMOpenAIUrl; apiKey = $AirLLMApiKey }
    models  = [pscustomobject]@{ $AirLLMModel = [pscustomobject]@{ name = $AirLLMModel } }
  }
  $oc.provider | Add-Member -NotePropertyName airllm -NotePropertyValue $airllmProvider -Force
  $oc | Add-Member -NotePropertyName model -NotePropertyValue ("airllm/" + $AirLLMModel) -Force
  [System.IO.File]::WriteAllText($ocFile, ($oc | ConvertTo-Json -Depth 100))
  Ok "OpenCode configured -> $ocFile"
}

# --- 4. Aider -> .aider.conf.yml (model + base URL + key in the file, so plain
#        `aider` works from any launcher - no env var required) ------------------
if (Want-Tool "aider") {
  $aiderFile = Join-Path $env:USERPROFILE ".aider.conf.yml"
  $aiderYml = @"
# AirLLM defaults - written by airllm-setup (safe to delete; re-created on each run)
model: openai/$AirLLMModel
openai-api-base: $AirLLMOpenAIUrl
openai-api-key: $AirLLMApiKey
"@
  if (-not (Test-Path $aiderFile)) {
    [System.IO.File]::WriteAllText($aiderFile, $aiderYml)
    Ok "Aider configured -> .aider.conf.yml"
  } elseif (Select-String -Path $aiderFile -Pattern "written by airllm-setup" -Quiet) {
    # Our own file: rewrite on every run so a regenerated key or changed model always lands.
    [System.IO.File]::WriteAllText($aiderFile, $aiderYml)
    Ok "Aider configured -> .aider.conf.yml (refreshed - key and model are current)"
  } elseif (Select-String -Path $aiderFile -Pattern "airllm" -Quiet) {
    Warn "Aider: .aider.conf.yml mentions AirLLM but wasn't written by this script - left as-is. VERIFY it has:  model: openai/$AirLLMModel  plus  openai-api-base: $AirLLMOpenAIUrl  and  openai-api-key: (your CURRENT AirLLM key - old keys stop working after a regenerate)"
  } else {
    Warn "Aider: .aider.conf.yml already exists - left as-is. To default to AirLLM set:  model: openai/$AirLLMModel  plus  openai-api-base: $AirLLMOpenAIUrl  and  openai-api-key: (your AirLLM key)"
  }
}

# --- 5. Environment variables (User scope, persistent) - only the variables the
#        chosen tool needs, so configuring one tool never disturbs another ------
# Helper: mirror env vars into the user's PowerShell profiles (5.1 + 7) as a
# marker-fenced, per-tool block, replacing any earlier block with the same id.
# This is what makes a NEW PowerShell tab in an already-open Windows Terminal /
# VS Code pick the keys up - those apps cache their environment, so a User-scope
# registry var alone stays invisible until the terminal app is FULLY restarted
# (exactly why Codex used to report "Missing environment variable"). Each line
# READS the User-scope variable, so no key is ever written into Documents (often
# synced to OneDrive). It mirrors what airllm-setup-mac.sh does with ~/.zshrc.
# Best-effort: any error is swallowed so the registry vars below remain the
# source of truth. Documents is resolved via the API so OneDrive-redirected
# folders are handled correctly. With $pairs = $null it only removes the block.
function Write-ProfileBlock($id, $pairs) {
  try {
    $docs = [Environment]::GetFolderPath('MyDocuments')
    if (-not $docs) { return }
    $targets = @((Join-Path $docs 'WindowsPowerShell\profile.ps1'), (Join-Path $docs 'PowerShell\profile.ps1'))
    $start = "# >>> AirLLM setup: $id >>> (managed by airllm-setup - do not edit inside)"
    $end   = "# <<< AirLLM setup: $id <<<"
    $block = $null
    if ($pairs) {
      $lines = @()
      foreach ($k in $pairs.Keys) { $lines += ('$env:' + $k + " = [Environment]::GetEnvironmentVariable('" + $k + "', 'User')") }
      $block = $start + "`r`n" + ($lines -join "`r`n") + "`r`n" + $end
    }
    foreach ($p in $targets) {
      try {
        if (-not $block -and -not (Test-Path $p)) { continue }   # removing: never create a profile
        New-Item -ItemType Directory -Force -Path (Split-Path $p -Parent) | Out-Null
        $content = ""
        if (Test-Path $p) { $content = [System.IO.File]::ReadAllText($p) }
        $pat = [regex]::Escape($start) + '.*?' + [regex]::Escape($end) + '(\r?\n)?'
        $content = [regex]::Replace($content, $pat, '', [System.Text.RegularExpressions.RegexOptions]::Singleline)
        # Older versions wrote the values themselves ($env:X = 'value', keys included): make any
        # left in other AirLLM blocks read the User-scope variable too.
        $out = @(); $inBlock = $false
        foreach ($line in ($content -split "\r?\n")) {
          if ($line -like '# >>> AirLLM setup:*') { $inBlock = $true }
          elseif ($line -like '# <<< AirLLM setup:*') { $inBlock = $false }
          elseif ($inBlock -and $line -match "^\`$env:([A-Za-z_][A-Za-z0-9_]*) = '") { $line = '$env:' + $Matches[1] + " = [Environment]::GetEnvironmentVariable('" + $Matches[1] + "', 'User')" }
          $out += $line
        }
        $content = [regex]::Replace(($out -join "`r`n").TrimEnd().TrimStart([char[]]"`r`n"), '(\r\n){3,}', "`r`n`r`n")   # no blank lines piling up where blocks were
        if ($block) {
          if ($content.Length -gt 0) { $content += "`r`n`r`n" }
          $content += $block
        }
        [System.IO.File]::WriteAllText($p, $content + "`r`n")
      } catch { }
    }
  } catch { }
}

# Older versions put the AirLLM key in OPENAI_API_KEY (for Codex; for Aider with OPENAI_API_BASE),
# where every other OpenAI tool on the machine picks it up and sends it to api.openai.com. Remove
# them when this script set them - unless agent frameworks use them (OPENAI_BASE_URL is AirLLM's).
function Remove-LegacyOpenAIVars {
  if ((Want-Tool "agent-frameworks") -or ([Environment]::GetEnvironmentVariable("OPENAI_BASE_URL", "User") -eq $AirLLMOpenAIUrl)) { return }
  $ours = [Environment]::GetEnvironmentVariable("OPENAI_API_BASE", "User") -eq $AirLLMOpenAIUrl
  try {
    $docs = [Environment]::GetFolderPath('MyDocuments')
    foreach ($p in @((Join-Path $docs 'WindowsPowerShell\profile.ps1'), (Join-Path $docs 'PowerShell\profile.ps1'))) {
      if ((Test-Path $p) -and ([System.IO.File]::ReadAllText($p) -match '# >>> AirLLM setup: (codex|aider) >>>[^<]*\$env:OPENAI_API_KEY')) { $ours = $true }
    }
  } catch { }
  if (-not $ours) { return }
  foreach ($k in @("OPENAI_API_KEY", "OPENAI_API_BASE")) {
    [Environment]::SetEnvironmentVariable($k, $null, "User")
    Remove-Item -Path ("Env:" + $k) -ErrorAction SilentlyContinue
  }
  Note "Removed the OPENAI_API_KEY / OPENAI_API_BASE an older version of this script set for Codex/Aider - other OpenAI tools would have sent the AirLLM key to OpenAI."
}

# Per-tool env var groups: only the vars the chosen tool needs, so configuring
# one tool never disturbs another's block.
$envGroups = @()
if (Want-Tool "claude-code") {
  $envGroups += ,@("claude-code", [ordered]@{
    ANTHROPIC_BASE_URL            = $AirLLMBaseUrl
    ANTHROPIC_AUTH_TOKEN          = $AirLLMApiKey
    API_TIMEOUT_MS                = "3000000"        # gateway routing can exceed default timeouts
    CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"   # keep non-model calls off the gateway
    ANTHROPIC_MODEL               = $AirLLMModel
    ANTHROPIC_DEFAULT_HAIKU_MODEL = $AirLLMModel
    ANTHROPIC_SMALL_FAST_MODEL    = $AirLLMModel
  })
}
if (Want-Tool "codex") {
  $envGroups += ,@("codex", [ordered]@{ AIRLLM_API_KEY = $AirLLMApiKey })   # Codex env_key (its own name: other OpenAI tools are unaffected)
}
# Aider reads .aider.conf.yml from any launcher, so it needs no variables (AIDER_* ones would
# override a hand-written config). Older versions set OPENAI_API_BASE + OPENAI_API_KEY for it.
$prevOpenAIKey = $env:OPENAI_API_KEY
if ((Want-Tool "codex") -or (Want-Tool "aider")) { Remove-LegacyOpenAIVars }
if (Want-Tool "aider") { Write-ProfileBlock "aider" $null }
if (Want-Tool "copilot-cli") {
  $envGroups += ,@("copilot-cli", [ordered]@{
    COPILOT_PROVIDER_TYPE     = "openai"
    COPILOT_PROVIDER_BASE_URL = $AirLLMOpenAIUrl
    COPILOT_PROVIDER_API_KEY  = $AirLLMApiKey
    COPILOT_MODEL             = $AirLLMModel
  })
}
if (Want-Tool "agent-frameworks") {
  $envGroups += ,@("agent-frameworks", [ordered]@{
    OPENAI_BASE_URL      = $AirLLMOpenAIUrl
    OPENAI_API_KEY       = $AirLLMApiKey
    OPENAI_MODEL         = $AirLLMModel
    OPENAI_API_BASE      = $AirLLMOpenAIUrl
    ANTHROPIC_BASE_URL   = $AirLLMBaseUrl
    ANTHROPIC_AUTH_TOKEN = $AirLLMApiKey
    ANTHROPIC_MODEL      = $AirLLMModel
  })
}
$envCount = 0
foreach ($g in $envGroups) {
  $pairs = $g[1]
  foreach ($k in $pairs.Keys) {
    [Environment]::SetEnvironmentVariable($k, $pairs[$k], "User")
    Set-Item -Path ("Env:" + $k) -Value $pairs[$k]
    $envCount++
  }
  Write-ProfileBlock $g[0] $pairs
}
if ($envCount -gt 0) {
  Ok "$envCount environment variables set (User scope + PowerShell profile)"
  $ep = $null
  try { $ep = (Get-ExecutionPolicy -Scope CurrentUser).ToString() } catch {}
  if ($ep -eq "Restricted" -or $ep -eq "AllSigned") {
    Note "PowerShell execution policy is '$ep', so the profile lines may not auto-run. Fix once with:  Set-ExecutionPolicy -Scope CurrentUser RemoteSigned  - or just FULLY restart your terminal (the User-scope variables always apply to a brand-new terminal process)."
  }
}
if (Want-Tool "copilot-cli") {
  Ok "GitHub Copilot CLI configured (BYOK env vars). GitHub sign-in is NOT required with a custom provider."
}

if (Want-Tool "agent-frameworks") {
  # These are the shared SDK variables, so say what they now mean for keys set elsewhere.
  if ($prevOpenAIKey -and $prevOpenAIKey -ne $AirLLMApiKey) {
    Warn "OPENAI_API_KEY was set to another key. New terminals send OpenAI-SDK calls to AirLLM (OPENAI_BASE_URL) with your AirLLM key - if you also call OpenAI directly, pass that key per project together with its own base URL, or it goes to AirLLM."
  }
  if ($env:ANTHROPIC_API_KEY) {
    Warn "ANTHROPIC_API_KEY is set. Anthropic-SDK frameworks would send it to AirLLM alongside the AirLLM token - clear it."
  }
}
if ((Want-Tool "claude-code") -and $env:ANTHROPIC_API_KEY) {
  Warn "ANTHROPIC_API_KEY is set in your environment. For a gateway, clear it (this setup uses ANTHROPIC_AUTH_TOKEN) to avoid a 'trust this key?' prompt."
}

# --- UI-configured tools --------------------------------------------------------
switch ($AirLLMTool) {
  "cursor"    { Print-UiTool "Cursor" }
  "continue"  { Print-UiTool "Continue" }
  "cline"     { Print-UiTool "Cline" }
  "roo-code"  { Print-UiTool "Roo Code" }
  "openhands" { Print-UiTool "OpenHands" }
}

# --- connectivity test (GET /v1/models: lightweight, non-billable) ------------
Write-Host ""
Write-Host "  Testing gateway connection ... " -NoNewline
try {
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
  $resp = Invoke-WebRequest -UseBasicParsing -TimeoutSec 20 -Uri "$AirLLMOpenAIUrl/models" `
            -Headers @{ Authorization = "Bearer $AirLLMApiKey" }
  if ($resp.StatusCode -eq 200) { Write-Host "ok - gateway reachable, key accepted." -ForegroundColor Green }
  else { Write-Host "reachable (HTTP $($resp.StatusCode))." -ForegroundColor Yellow }
} catch {
  $code = $null
  try { $code = [int]$_.Exception.Response.StatusCode } catch {}
  if ($code -eq 401 -or $code -eq 403) { Write-Host "reachable, but the key was rejected (HTTP $code). Check your key." -ForegroundColor Yellow }
  else { Write-Host "could not verify. Config was still written." -ForegroundColor Yellow }
}

# --- summary --------------------------------------------------------------------
Write-Host ""
if ($AirLLMTool -eq "all") {
  Write-Host "  UI-configured tools - enter these in each tool's settings:" -ForegroundColor White
  Write-Host "  ---------------------------------------------------------------"
  Write-Host "    Cursor / Continue / Cline / Roo Code / OpenHands"
  Write-Host "      Base URL : $AirLLMOpenAIUrl" -ForegroundColor Cyan
  Write-Host "      API key  : (your AirLLM API key)" -ForegroundColor Cyan
  Write-Host "      Model    : $AirLLMModel   (OpenHands: openai/$AirLLMModel)" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "    Any other tool"
  Write-Host "      Asks for an OpenAI-compatible base URL  -> $AirLLMOpenAIUrl" -ForegroundColor Cyan
  Write-Host "      Adds /v1/... itself (Anthropic-style)   -> $AirLLMBaseUrl" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "  NOTE  If Claude Code or Codex run inside WSL, run the Mac/Linux script" -ForegroundColor Yellow
  Write-Host "        (airllm-setup-mac.sh) inside WSL - it does not read Windows settings."
  Write-Host ""
  Write-Host "  Done. Claude Code, OpenCode and Aider read the key from their own config" -ForegroundColor Green
  Write-Host "  file - they work from any launcher right away. Codex, Copilot CLI and agent" -ForegroundColor Green
  Write-Host "  frameworks read the key from an ENVIRONMENT VARIABLE, so open a BRAND-NEW" -ForegroundColor Green
  Write-Host "  terminal - a new TAB in an already-open Windows Terminal / VS Code is NOT" -ForegroundColor Green
  Write-Host "  enough (they cache the old environment); fully close and reopen it." -ForegroundColor Green
} elseif (@("cursor","continue","cline","roo-code","openhands") -contains $AirLLMTool) {
  Write-Host "  Done. $ToolLabel stores its settings in its own UI - paste the values" -ForegroundColor Green
  Write-Host "  printed above. Nothing on this machine was changed." -ForegroundColor Green
} else {
  Write-Host "  Done. $ToolLabel is configured. If it reads its key from an environment" -ForegroundColor Green
  Write-Host "  variable (Codex, Copilot CLI, agent frameworks), open a BRAND-NEW terminal" -ForegroundColor Green
  Write-Host "  - a new TAB in an already-open Windows Terminal / VS Code is NOT enough" -ForegroundColor Green
  Write-Host "  (they cache the old environment); fully close and reopen it." -ForegroundColor Green
  if ($AirLLMTool -eq "codex") {
    Write-Host ""
    Write-Host "  To use Codex in THIS window right now (no restart), run:" -ForegroundColor Green
    Write-Host "      `$env:AIRLLM_API_KEY = '$AirLLMApiKey'" -ForegroundColor Cyan
  }
  if (@("claude-code","codex") -contains $AirLLMTool) {
    Write-Host ""
    Write-Host "  NOTE  If $ToolLabel runs inside WSL, run the Mac/Linux script" -ForegroundColor Yellow
    Write-Host "        (airllm-setup-mac.sh) inside WSL - it does not read Windows settings."
  }
}
