# Nodule: one-command setup for Claude Code and Codex (Windows PowerShell 5.1+ and PowerShell 7). # Usage: & ([scriptblock]::Create((irm https://nodule-provider.store/i.ps1))) NOD-SETUP-XXXX-XXXX # The code is one-time and lasts 15 minutes; your key is not in the command. # You can read this script first: https://nodule-provider.store/i.ps1 param([string]$Code = '') $ErrorActionPreference = 'Stop' # Windows PowerShell 5.1 on older .NET may not offer TLS 1.2 by default. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } $Base = 'https://nodule-provider.store' try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch { } function Write-Utf8([string]$Path, [string]$Text) { [System.IO.File]::WriteAllText($Path, $Text, (New-Object System.Text.UTF8Encoding $false)) } function Invoke-NoduleSetup([string]$Code) { $Code = ([string]$Code).Trim().ToUpperInvariant() if ($Code -notmatch '^NOD-SETUP-[0-9A-Z]{4}-[0-9A-Z]{4}$') { throw "нужен одноразовый код: & ([scriptblock]::Create((irm $Base/i.ps1))) NOD-SETUP-XXXX-XXXX (получите его на $Base/setup)" } $body = '{"code":"' + $Code + '"}' try { $response = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$Base/setup/api/install" -ContentType 'application/json' -Body $body } catch { $status = 0 if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode } if ($status -eq 410) { throw "код уже использован или истёк. Получите новый на $Base/setup" } if ($status -eq 429) { throw 'слишком много попыток. Подождите несколько минут' } if ($status -gt 0) { throw "сервер ответил $status. Попробуйте получить новый код на $Base/setup" } throw "не удалось связаться с $Base" } $content = $response.Content if ($content -is [byte[]]) { $content = [System.Text.Encoding]::UTF8.GetString($content) } $values = @{} foreach ($line in ([string]$content -split "`n")) { if ($line -match '^(NODULE_[A-Z_]+)=(.*)$') { $values[$Matches[1]] = $Matches[2].Trim() } } $url = [string]$values['NODULE_BASE_URL']; $key = [string]$values['NODULE_KEY'] $claudeModel = [string]$values['NODULE_CLAUDE_MODEL']; $codexModel = [string]$values['NODULE_CODEX_MODEL'] if ($url -notmatch '^https?://[A-Za-z0-9.-]+(:\d{1,5})?$') { throw 'сервер вернул неверный адрес' } if ($key -notmatch '^[A-Za-z0-9_-]{16,200}$') { throw 'сервер не вернул ключ' } if ($claudeModel -notmatch '^([a-z0-9][a-z0-9._-]{0,80}(\[1m\])?)?$' -or $codexModel -notmatch '^([a-z0-9][a-z0-9._-]{0,80})?$') { throw 'сервер вернул неожиданные символы' } $homeDir = [Environment]::GetFolderPath('UserProfile') if (-not $homeDir) { $homeDir = $HOME } $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') $backups = New-Object System.Collections.Generic.List[string] function Save-Backup([string]$Path) { if (Test-Path -LiteralPath $Path) { Copy-Item -LiteralPath $Path -Destination "$Path.bak-$stamp" -Force $backups.Add("$Path.bak-$stamp") } } $done = @() $note = '' if ($claudeModel) { $dir = Join-Path $homeDir '.claude' New-Item -ItemType Directory -Force -Path $dir | Out-Null $file = Join-Path $dir 'settings.json' Save-Backup $file $settings = $null if (Test-Path -LiteralPath $file) { try { $settings = Get-Content -LiteralPath $file -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $settings = $null } if ($null -eq $settings -and (Get-Item -LiteralPath $file).Length -gt 0) { $note = ' Прежний ~\.claude\settings.json прочитать не удалось, записан новый; прежний лежит в копии.' } } if ($null -eq $settings -or $settings -isnot [System.Management.Automation.PSCustomObject]) { $settings = New-Object PSObject } if ($null -eq $settings.env -or $settings.env -isnot [System.Management.Automation.PSCustomObject]) { $settings | Add-Member -NotePropertyName env -NotePropertyValue (New-Object PSObject) -Force } $settings.env.PSObject.Properties.Remove('ANTHROPIC_API_KEY') $settings | Add-Member -NotePropertyName model -NotePropertyValue $claudeModel -Force $envValues = [ordered]@{ ANTHROPIC_BASE_URL = $url; ANTHROPIC_AUTH_TOKEN = $key CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'; CLAUDE_CODE_ATTRIBUTION_HEADER = '0' } foreach ($name in $envValues.Keys) { $settings.env | Add-Member -NotePropertyName $name -NotePropertyValue $envValues[$name] -Force } Write-Utf8 $file (($settings | ConvertTo-Json -Depth 32) + "`n") $done += 'claude' } if ($codexModel) { $dir = Join-Path $homeDir '.codex' New-Item -ItemType Directory -Force -Path $dir | Out-Null $config = Join-Path $dir 'config.toml' $auth = Join-Path $dir 'auth.json' Save-Backup $config Save-Backup $auth $lines = New-Object System.Collections.Generic.List[string] $lines.Add('model_provider = "nodule"'); $lines.Add("model = `"$codexModel`"") $lines.Add('model_reasoning_effort = "high"'); $lines.Add('disable_response_storage = true') if (Test-Path -LiteralPath $config) { $inTable = $false; $skip = $false foreach ($line in (Get-Content -LiteralPath $config -Encoding UTF8)) { if ($line -match '^\s*\[') { $inTable = $true; $skip = $line -match '^\s*\[model_providers\.nodule\]\s*(#.*)?$' } if ($skip) { continue } if (-not $inTable -and $line -match '^\s*(model_provider|model|model_reasoning_effort|disable_response_storage)\s*=') { continue } $lines.Add($line) } } $lines.Add(''); $lines.Add('[model_providers.nodule]'); $lines.Add('name = "Nodule"'); $lines.Add("base_url = `"$url`"") $lines.Add('wire_api = "responses"'); $lines.Add('requires_openai_auth = true'); $lines.Add('supports_websockets = false') Write-Utf8 $config (($lines -join "`n") + "`n") Write-Utf8 $auth ("{`n `"OPENAI_API_KEY`": `"$key`"`n}`n") $done += 'codex' } if ($done.Count -eq 0) { throw 'по этому ключу нет моделей Claude или GPT для Claude Code и Codex' } $check = 0 try { $check = [int](Invoke-WebRequest -UseBasicParsing -Uri "$url/v1/models" -Headers @{ Authorization = "Bearer $key" }).StatusCode } catch { $check = 0 } Write-Host '' Write-Host 'Готово, Nodule подключён.' -ForegroundColor Green if ($done -contains 'claude') { Write-Host " Claude Code: ~\.claude\settings.json, модель $claudeModel" if (Get-Command claude -ErrorAction SilentlyContinue) { Write-Host ' запустите в папке проекта: claude' } else { Write-Host ' Claude Code не найден, установите: npm install -g @anthropic-ai/claude-code' } } if ($done -contains 'codex') { Write-Host " Codex: ~\.codex\config.toml и auth.json, модель $codexModel" if (Get-Command codex -ErrorAction SilentlyContinue) { Write-Host ' запустите в папке проекта: codex' } else { Write-Host ' Codex не найден, установите: npm install -g @openai/codex' } } if ($check -eq 200) { Write-Host ' Проверка ключа: работает.' } else { Write-Host " Проверка ключа не прошла. Откройте $Base/setup, там подскажем." } if ($backups.Count -gt 0) { Write-Host (' Старые настройки сохранены: ' + (($backups | ForEach-Object { $_.Replace($homeDir, '~') }) -join ' ')) } if ($note) { Write-Host $note } Write-Host " Баланс и расход: $Base/usage" } # Report errors without closing the user's PowerShell window. try { Invoke-NoduleSetup $Code } catch { Write-Host ('Nodule: ' + $_.Exception.Message) -ForegroundColor Red }