# RouteAPI: переезд с Cursor за 5 минут (Windows, PowerShell 5.1+). # # & ([scriptblock]::Create((irm https://app.routeapi.ru/install.ps1))) -Key sk-gw-... # # Что делает (и ничего больше): # 1. Направляет Claude Code на RouteAPI: %USERPROFILE%\.claude\settings.json, блок env (ANTHROPIC_BASE_URL, ключ, # модель). Эти настройки общие для Claude Code в терминале и расширения Claude Code для VS Code. # Спрашивает разрешения; без интерактивной консоли меняет их только с -Yes. # 2. Ставит официальное расширение Claude Code (anthropic.claude-code) в Cursor и/или VS Code — во все найденные — # и выключает в нём окно входа в аккаунт Anthropic (claudeCode.disableLoginPrompt). # 3. Переносит правила проекта: .cursorrules, .cursor\rules\*, .roorules, .roo\rules\*, .clinerules -> .claude\rules\ # (globs -> paths). Существующие # файлы не трогает. # Перед изменением settings.json делается копия *.bak-routeapi. Ключ никогда не выводится на экран. # # Параметры: -Key (или переменная ROUTEAPI_KEY; без них ключ спросят скрыто), -Model, -Project, -Yes, -NoClaude, -NoEditors param( [string]$Key = $env:ROUTEAPI_KEY, [string]$Model = "claude-sonnet-5-5", [string]$Project = ".", [switch]$Yes, [switch]$NoClaude, [Alias("NoRoo", "NoVSCode")][switch]$NoEditors ) $ErrorActionPreference = "Stop" $Api = "https://api.routeapi.ru" $HaikuModel = "claude-haiku-5-5" function Fail($msg) { Write-Host "Ошибка: $msg" -ForegroundColor Red; exit 1 } function Warn($msg) { Write-Host "! $msg" -ForegroundColor Yellow } $Interactive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected if (-not $Key) { if (-not $Interactive) { Fail "передайте ключ: -Key sk-gw-… (ключ — в кабинете https://app.routeapi.ru/dashboard/keys)" } $secure = Read-Host "Ключ RouteAPI (sk-gw-…, ввод не отображается)" -AsSecureString $Key = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)) } if ($Key -notmatch '^sk-gw-[A-Za-z0-9_-]{8,200}$') { Fail "ключ должен начинаться с sk-gw- и содержать только буквы, цифры, - и _ (создайте его в https://app.routeapi.ru/dashboard/keys)" } if ($Model -notmatch '^[a-z0-9][a-z0-9.-]{1,99}$') { Fail "некорректный id модели: $Model" } $Masked = $Key.Substring(0, 10) + "…" # JSON with comments -> object. Strips // and /* */ outside strings and trailing commas. function Read-Jsonc([string]$path) { if (-not (Test-Path -LiteralPath $path)) { return [pscustomobject]@{} } $s = [IO.File]::ReadAllText($path) if (-not $s.Trim()) { return [pscustomobject]@{} } $sb = New-Object System.Text.StringBuilder $i = 0; $inStr = $false while ($i -lt $s.Length) { $c = $s[$i]; $n = if ($i + 1 -lt $s.Length) { $s[$i + 1] } else { [char]0 } if ($inStr) { [void]$sb.Append($c) if ($c -eq '\') { if ($n -ne [char]0) { [void]$sb.Append($n) }; $i += 2; continue } if ($c -eq '"') { $inStr = $false } $i++; continue } if ($c -eq '"') { $inStr = $true; [void]$sb.Append($c); $i++; continue } if ($c -eq '/' -and $n -eq '/') { while ($i -lt $s.Length -and $s[$i] -ne "`n") { $i++ }; continue } if ($c -eq '/' -and $n -eq '*') { $i += 2; while ($i + 1 -lt $s.Length -and -not ($s[$i] -eq '*' -and $s[$i + 1] -eq '/')) { $i++ }; $i += 2; continue } [void]$sb.Append($c); $i++ } $clean = [regex]::Replace($sb.ToString(), ',(\s*[}\]])', '$1') return $clean | ConvertFrom-Json } function Set-Prop($obj, [string]$name, $value) { if ($obj.PSObject.Properties[$name]) { $obj.$name = $value } else { $obj | Add-Member -NotePropertyName $name -NotePropertyValue $value } } # Writes UTF-8 without BOM (VS Code and Claude Code read it either way; no BOM is the safe choice). function Save-Json($obj, [string]$path) { $dir = Split-Path -Parent $path if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } if (Test-Path -LiteralPath $path) { Copy-Item -LiteralPath $path -Destination "$path.bak-routeapi" -Force } [IO.File]::WriteAllText($path, ($obj | ConvertTo-Json -Depth 32) + "`n", (New-Object System.Text.UTF8Encoding($false))) } function Restrict-ToMe([string]$path) { try { $acl = Get-Acl -LiteralPath $path $acl.SetAccessRuleProtection($true, $false) $me = [Security.Principal.WindowsIdentity]::GetCurrent().Name $acl.SetAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($me, "FullControl", "Allow"))) Set-Acl -LiteralPath $path -AclObject $acl } catch { } } Write-Host "RouteAPI: настройка с ключом $Masked, модель $Model" $VsUser = if ($env:ROUTEAPI_VSCODE_USER_DIR) { $env:ROUTEAPI_VSCODE_USER_DIR } else { Join-Path $env:APPDATA "Code\User" } $CursorUser = if ($env:ROUTEAPI_CURSOR_USER_DIR) { $env:ROUTEAPI_CURSOR_USER_DIR } else { Join-Path $env:APPDATA "Cursor\User" } $HomeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME } # ---------------------------------------------------------------- 1. Claude Code settings $claudeOk = $false if (-not $NoClaude) { $go = $Yes if (-not $go -and $Interactive) { $a = Read-Host "Направить Claude Code (терминал и VS Code) на RouteAPI? Изменится ~/.claude/settings.json, копия сохранится. [Y/n]" $go = -not ($a -match '^(n|no|н|нет)$') } if ($go) { try { $ccPath = Join-Path $HomeDir ".claude\settings.json" $cc = Read-Jsonc $ccPath if (-not $cc.PSObject.Properties["env"]) { Set-Prop $cc "env" ([pscustomobject]@{}) } Set-Prop $cc.env "ANTHROPIC_BASE_URL" $Api Set-Prop $cc.env "ANTHROPIC_AUTH_TOKEN" $Key Set-Prop $cc.env "ANTHROPIC_MODEL" $Model if ($HaikuModel) { Set-Prop $cc.env "ANTHROPIC_DEFAULT_HAIKU_MODEL" $HaikuModel } Save-Json $cc $ccPath Restrict-ToMe $ccPath $claudeOk = $true Write-Host "→ Claude Code направлен на RouteAPI (~/.claude/settings.json)." } catch { Warn "не удалось записать ~/.claude/settings.json. Инструкция: https://app.routeapi.ru/connect/claude-code" } } else { Write-Host "→ Настройки Claude Code не тронуты (без интерактивной консоли — только с -Yes)." } } # ---------------------------------------------------------------- 2. Editors: Cursor and VS Code function Setup-Editor([string]$name, [string]$cli, [string]$userDir) { Write-Host "→ Ставлю расширение Claude Code в $name…" & $cli --install-extension anthropic.claude-code *> $null if ($LASTEXITCODE -eq 0) { Write-Host " ${name}: расширение Claude Code установлено." } else { Warn "${name}: не удалось поставить расширение. Поставьте вручную: Extensions → «Claude Code» (Anthropic)." } if ($claudeOk) { try { $s = Read-Jsonc (Join-Path $userDir "settings.json") Set-Prop $s "claudeCode.disableLoginPrompt" $true Save-Json $s (Join-Path $userDir "settings.json") } catch { Warn "${name}: не удалось выключить окно входа — включите Claude Code → Disable Login Prompt в настройках." } } } $editors = @() if (-not $NoEditors) { $cursorCli = $null; $codeCli = $null $c = (Get-Command cursor -ErrorAction SilentlyContinue | Select-Object -First 1).Source if (-not $c -and $env:LOCALAPPDATA) { $g = Join-Path $env:LOCALAPPDATA "Programs\cursor\resources\app\bin\cursor.cmd" if (Test-Path -LiteralPath $g) { $c = $g } } if ($c) { $cursorCli = $c } $v = (Get-Command code -ErrorAction SilentlyContinue | Select-Object -First 1).Source # Cursor ships its own `code`: when that one is first in PATH it is Cursor, not VS Code. if ($v -and $v -match '[\\/]cursor[\\/]') { if (-not $cursorCli) { $cursorCli = $v }; $v = $null } if (-not $v -and $env:LOCALAPPDATA) { $g = Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code\bin\code.cmd" if (Test-Path -LiteralPath $g) { $v = $g } } if ($v) { $codeCli = $v } if ($cursorCli) { Setup-Editor "Cursor" $cursorCli $CursorUser; $editors += "Cursor" } if ($codeCli) { Setup-Editor "VS Code" $codeCli $VsUser; $editors += "VS Code" } if (-not $editors) { Warn "Не нашёл ни Cursor, ни VS Code. Claude Code работает и в терминале: npm install -g @anthropic-ai/claude-code" } } # ---------------------------------------------------------------- 3. Project rules $rules = New-Object System.Collections.Generic.List[string] if (Test-Path -LiteralPath $Project -PathType Container) { Set-Location -LiteralPath $Project # Cursor front matter -> Claude Code rule: globs become paths, alwaysApply rules load always. Never overwrites. function Copy-Rule([string]$src, [string]$name) { $dest = Join-Path ".claude\rules" $name if (Test-Path -LiteralPath $dest) { return } New-Item -ItemType Directory -Force -Path ".claude\rules" | Out-Null $lines = [IO.File]::ReadAllLines((Resolve-Path -LiteralPath $src)) $out = New-Object System.Collections.Generic.List[string] $fm = $false; $globs = ""; $always = $false; $j = 0 if ($lines.Length -gt 0 -and $lines[0] -eq "---") { $fm = $true for ($j = 1; $j -lt $lines.Length; $j++) { if ($lines[$j] -eq "---") { $j++; break } if ($lines[$j] -match '^globs:\s*(.*)$') { $globs = $Matches[1] } if ($lines[$j] -match '^alwaysApply:\s*true') { $always = $true } } } $paths = @($globs.Split(",") | ForEach-Object { $_.Trim().Trim('[', ']', '"', "'", ' ') } | Where-Object { $_ }) if ($fm -and $paths.Count -gt 0 -and -not $always) { $out.Add("---"); $out.Add("paths:") foreach ($p in $paths) { $out.Add(" - `"$p`"") } $out.Add("---") } for ($k = $j; $k -lt $lines.Length; $k++) { $out.Add($lines[$k]) } [IO.File]::WriteAllLines((Join-Path (Get-Location) $dest), $out, (New-Object System.Text.UTF8Encoding($false))) $rules.Add($dest) } if (Test-Path -LiteralPath ".cursorrules" -PathType Leaf) { Copy-Rule ".cursorrules" "cursorrules.md" } # Roo Code and Cline rules too (Roo Code is closed; its users move the same way). if (Test-Path -LiteralPath ".roorules" -PathType Leaf) { Copy-Rule ".roorules" "roorules.md" } if (Test-Path -LiteralPath ".clinerules" -PathType Leaf) { Copy-Rule ".clinerules" "clinerules.md" } if (Test-Path -LiteralPath ".roo\rules" -PathType Container) { Get-ChildItem -LiteralPath ".roo\rules" -File -Filter *.md | ForEach-Object { Copy-Rule $_.FullName ("roo-" + $_.BaseName + ".md") } } if (Test-Path -LiteralPath ".cursor\rules" -PathType Container) { Get-ChildItem -LiteralPath ".cursor\rules" -File | Where-Object { $_.Extension -in ".mdc", ".md" } | ForEach-Object { Copy-Rule $_.FullName ("cursor-" + $_.BaseName + ".md") } } if ($rules.Count -gt 0) { Write-Host "→ Перенесено правил проекта: $($rules.Count) (в .claude\rules — Claude Code читает их сам)" } } Write-Host "" if ($claudeOk) { $where = if ($editors) { $editors -join " или " } else { "редакторе" } Write-Host "Готово. Откройте проект в $where и нажмите значок Claude Code, или запустите claude в терминале." } else { Write-Host "Готово. Чтобы Claude Code работал через RouteAPI, запустите скрипт ещё раз в консоли или с -Yes." } Write-Host "Расход и ключи: https://app.routeapi.ru/dashboard"