<# .SYNOPSIS claude-mode - switch Claude Code system-wide between native Anthropic auth and the gateway providers in providers.json (OpenRouter, Z.AI, LM Studio, Ollama, custom endpoints), with named model presets. .DESCRIPTION State lives in %USERPROFILE%\.claude-mode. Switching rewrites the managed keys inside %USERPROFILE%\.claude\settings.json, which Claude Code re-reads at every startup - so a switch applies to every new `claude` invocation from any shell, the VS Code extension, and the desktop app, with nothing to re-source. Secrets are never written to settings.json. A remote provider's API key is stored DPAPI-encrypted (bound to this Windows user + machine) and handed to Claude Code at runtime via the `apiKeyHelper` hook. LM Studio's placeholder token is not a secret and is written inline. Run with no arguments for an interactive menu. .NOTES Windows PowerShell 5.1 compatible. No external dependencies. #> [CmdletBinding()] param( [Parameter(Position = 0)] [string] $Command = '', [Parameter(Position = 1, ValueFromRemainingArguments = $true)] [string[]] $Rest ) # v1 only (uninitialised variables). v2 would throw on absent JSON properties, # which is normal here - presets and settings.json are both partially shaped. Set-StrictMode -Version 1.0 $ErrorActionPreference = 'Stop' try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- $script:Root = Join-Path $env:USERPROFILE '.claude-mode' $script:PresetDir = Join-Path $script:Root 'presets' $script:VaultDir = Join-Path $script:Root 'vault' $script:BackupDir = Join-Path $script:Root 'backups' $script:BinDir = Join-Path $script:Root 'bin' $script:StatePath = Join-Path $script:Root 'state.json' $script:HelperCmd = Join-Path $script:BinDir 'claude-key-helper.cmd' $script:SettingsDir = Join-Path $env:USERPROFILE '.claude' $script:Settings = Join-Path $script:SettingsDir 'settings.json' $script:LmStudioDir = Join-Path $env:USERPROFILE '.lmstudio' # Baseline set of settings.json env keys this tool owns. On every switch these # are deleted first, together with whatever a previous switch actually wrote # (tracked in state.json), so no value can survive a mode change. $script:BaseManagedEnvKeys = @( 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_FABLE_MODEL', # Both are read by the CLI (21 and 37 references in 2.1.221) and both pin a # concrete model outside the tier mapping. A stale value in either survives a # switch and is a known cause of "works normally, dies on compaction", # because background summarisation uses the small/fast slot. 'ANTHROPIC_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL', 'CLAUDE_CODE_DISABLE_1M_CONTEXT', 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY', 'CLAUDE_CODE_ATTRIBUTION_HEADER', 'CLAUDE_CODE_AUTO_COMPACT_WINDOW', 'CLAUDE_CODE_MAX_CONTEXT_TOKENS', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'API_TIMEOUT_MS' ) $script:Tiers = @('opus', 'sonnet', 'haiku', 'fable') $script:Version = '0.0.0' try { $__v = Join-Path $PSScriptRoot 'VERSION'; if (Test-Path $__v) { $script:Version = (Get-Content $__v -Raw).Trim() } } catch { } $script:NodeExe = $null # resolved lazily by Format-JsonPretty # Where this script lives. The modules below are dot-sourced, and inside them # $PSScriptRoot is lib\ - so anything beside this script (providers.json) # is reached through $script:Here instead. $script:Here = $PSScriptRoot # The rest of the script, one file per concern, loaded in the order it was # written: a few modules set script-scope tables as they load (the providers, # mode colours, UI state), and every function exists before the dispatch below. foreach ($__m in @('providers', 'output', 'files', 'core', 'vault', 'switch', 'guards', 'health', 'catalogue', 'commands', 'menu')) { $__f = Join-Path $PSScriptRoot "lib\$__m.ps1" if (-not (Test-Path -LiteralPath $__f)) { Write-Host " FAIL $__f is missing - re-run install.ps1" -ForegroundColor Red exit 1 } . $__f } # --------------------------------------------------------------------------- # Dispatch # --------------------------------------------------------------------------- Initialize-Root try { $cmd = $Command.ToLower() switch ($cmd) { '' { Invoke-Menu } 'menu' { Invoke-Menu } 'status' { Invoke-Status } 'anthropic' { Set-ClaudeMode -Mode 'anthropic' -PresetName '' } 'presets' { Invoke-Presets } 'preset' { Invoke-PresetCmd -Argv $Rest } 'set-key' { # `set-key [ref] [key]`. The key is normally typed at the hidden # prompt; anything key-shaped in the ref position is taken as the # key for the default ref, which is the easy mistake to make. $kRef = ''; $kKey = '' foreach ($a in $Rest) { if (-not $kRef -and -not ($a -like 'sk-*' -or $a.Length -gt 24)) { $kRef = $a; continue } if (-not $kKey) { $kKey = $a; continue } throw "unexpected extra argument '$a'" } if (-not $kRef) { $kRef = 'openrouter' } Set-VaultKey -Ref $kRef -Key $kKey } 'models' { Invoke-Models -Filter $(if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }) } 'doctor' { Invoke-Doctor } 'repair' { [void](Invoke-Repair -All:([bool]($Rest -and ($Rest -contains '--all')))) } 'help' { Show-Usage } '--help' { Show-Usage } '-h' { Show-Usage } default { # Any provider in providers.json, by id or alias, is a mode. Last, # so a provider can never shadow a real command. $prov = Resolve-ProviderId $cmd if ($prov) { $req = if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null } Set-ClaudeMode -Mode $prov -PresetName (Resolve-PresetForProvider $prov $req) } else { Write-Err2 "unknown command '$Command'"; Show-Usage; exit 1 } } } } catch { Write-Err2 $_.Exception.Message exit 1 }