# Sovereign LLM Workbench — install extension in Cursor/VS Code + deploy API key (do NOT double-click .vsix). param( [Parameter(Mandatory = $true)][string]$BaseUrl, [Parameter(Mandatory = $true)][string]$ApiKey, [string]$Model = "", [string]$ProjectDir = "", [switch]$SkipExtension ) $ErrorActionPreference = "Stop" $BaseUrl = $BaseUrl.TrimEnd("/") function Resolve-ProjectDir { param([string]$Explicit) if ($Explicit) { return (Resolve-Path $Explicit).Path } $fromScript = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path foreach ($candidate in @($fromScript, (Get-Location).Path)) { if (Test-Path (Join-Path $candidate "start.bat")) { return $candidate } if (Test-Path (Join-Path $candidate "venv\Scripts\python.exe")) { return $candidate } } return $fromScript } function Install-EditorExtension { param( [Parameter(Mandatory = $true)][string]$EditorCli, [Parameter(Mandatory = $true)][string]$VsixPath, [Parameter(Mandatory = $true)][string]$Label ) Write-Host "Installing extension in $Label (not Visual Studio VSIX Installer)..." -ForegroundColor Cyan $prevPref = $ErrorActionPreference $ErrorActionPreference = "Continue" $output = & $EditorCli --install-extension $VsixPath --force 2>&1 $exitCode = $LASTEXITCODE $ErrorActionPreference = $prevPref foreach ($line in @($output)) { $text = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.ToString() } else { [string]$line } if ($text -match 'DeprecationWarning|DEP0040|punycode') { continue } if ($text.Trim()) { Write-Host $text } } if ($null -ne $exitCode -and $exitCode -ne 0) { Write-Host "[WARN] $Label install exit code $exitCode (check Extensions view; may still be OK)." -ForegroundColor Yellow } } $ProjectDir = Resolve-ProjectDir -Explicit $ProjectDir function Find-EditorCli { param([string[]]$Names) foreach ($name in $Names) { $cmd = Get-Command $name -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } } $candidates = @( (Join-Path $env:LOCALAPPDATA "Programs\cursor\resources\app\bin\cursor.cmd"), (Join-Path $env:LOCALAPPDATA "Programs\Cursor\resources\app\bin\cursor.cmd"), "E:\Programs\cursor\resources\app\bin\cursor.cmd" ) foreach ($p in $candidates) { if (Test-Path $p) { return $p } } return $null } $vsixUrl = "$BaseUrl/static/extensions/sovereign-llm-workbench.vsix" $vsixLocal = Join-Path $env:TEMP "sovereign-llm-workbench.vsix" Write-Host "=== Sovereign LLM Cursor setup ===" -ForegroundColor Cyan Write-Host "Project: $ProjectDir" Write-Host "Workbench: $BaseUrl" if (-not $SkipExtension) { Write-Host "Downloading VSIX..." -ForegroundColor Cyan Invoke-WebRequest -Uri $vsixUrl -OutFile $vsixLocal -UseBasicParsing $cursor = Find-EditorCli @("cursor") if ($cursor) { Install-EditorExtension -EditorCli $cursor -VsixPath $vsixLocal -Label "Cursor" } else { Write-Host "[WARN] Cursor CLI not found. Install manually:" -ForegroundColor Yellow Write-Host " cursor --install-extension `"$vsixLocal`" --force" Write-Host "Do NOT double-click the .vsix file - Windows opens Visual Studio VSIX Installer." } $code = Find-EditorCli @("code") if ($code) { Install-EditorExtension -EditorCli $code -VsixPath $vsixLocal -Label "VS Code" } } # Workspace credentials (extension reads these automatically) $cursorDir = Join-Path $ProjectDir ".cursor" $vscodeDir = Join-Path $ProjectDir ".vscode" New-Item -ItemType Directory -Force -Path $cursorDir, $vscodeDir | Out-Null $cred = @{ api_key = $ApiKey base_url = $BaseUrl model = $Model updated_at = (Get-Date).ToUniversalTime().ToString("o") } | ConvertTo-Json -Depth 4 $cred | Set-Content (Join-Path $cursorDir "sovereign-credentials.json") -Encoding UTF8 $cred | Set-Content (Join-Path $vscodeDir "sovereign-credentials.json") -Encoding UTF8 $settingsPath = Join-Path $vscodeDir "settings.json" $settings = @{} if (Test-Path $settingsPath) { $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json if ($settings -is [pscustomobject]) { $hash = @{} $settings.PSObject.Properties | ForEach-Object { $hash[$_.Name] = $_.Value } $settings = $hash } } $settings["sovereignWorkbench.baseUrl"] = $BaseUrl if ($Model) { $settings["sovereignWorkbench.model"] = $Model } ($settings | ConvertTo-Json -Depth 6) | Set-Content $settingsPath -Encoding UTF8 Write-Host "Credentials: $cursorDir\sovereign-credentials.json" -ForegroundColor DarkGray # MCP for Cursor Agent prompts $repoRoot = Resolve-ProjectDir -Explicit "" $py = Join-Path $repoRoot "venv\Scripts\python.exe" if (-not (Test-Path $py)) { $pyCmd = Get-Command python -ErrorAction SilentlyContinue if ($pyCmd) { $py = $pyCmd.Source } } $mcpScript = Join-Path $repoRoot "scripts\sovereign_ide_mcp.py" if (-not (Test-Path $mcpScript)) { $mcpScript = Join-Path $ProjectDir "scripts\sovereign_ide_mcp.py" } $mcpScript = ($mcpScript -replace "\\", "/") $py = ($py -replace "\\", "/") $projectRootNorm = ($ProjectDir -replace "\\", "/") $mcpEntry = @{ command = $py args = @($mcpScript) cwd = $projectRootNorm env = @{ SOVEREIGN_BASE_URL = $BaseUrl SOVEREIGN_API_KEY = $ApiKey SOVEREIGN_MODEL = $Model SOVEREIGN_PROJECT_ROOT = $projectRootNorm } } $mcpProject = Join-Path $cursorDir "mcp.json" @{ mcpServers = @{ "sovereign-llm-workbench" = $mcpEntry } } | ConvertTo-Json -Depth 8 | Set-Content $mcpProject -Encoding UTF8 $userCursor = Join-Path $env:USERPROFILE ".cursor\mcp.json" New-Item -ItemType Directory -Force -Path (Split-Path $userCursor) | Out-Null if (Test-Path $userCursor) { $existing = Get-Content $userCursor -Raw | ConvertFrom-Json if (-not $existing.mcpServers) { $existing | Add-Member -NotePropertyName mcpServers -NotePropertyValue ([pscustomobject]@{}) } $existing.mcpServers | Add-Member -NotePropertyName "sovereign-llm-workbench" -NotePropertyValue $mcpEntry -Force $existing | ConvertTo-Json -Depth 10 | Set-Content $userCursor -Encoding UTF8 } else { @{ mcpServers = @{ "sovereign-llm-workbench" = $mcpEntry } } | ConvertTo-Json -Depth 8 | Set-Content $userCursor -Encoding UTF8 } # Deep-link to push key into running extension $qs = "apiKey=$([uri]::EscapeDataString($ApiKey))&baseUrl=$([uri]::EscapeDataString($BaseUrl))" if ($Model) { $qs += "&model=$([uri]::EscapeDataString($Model))" } Write-Host "Opening Cursor with API key..." -ForegroundColor Cyan Start-Process "cursor://sovereign-llm-workbench/did-authenticate?$qs" -ErrorAction SilentlyContinue Write-Host "" Write-Host "Done. Reload Cursor (Developer: Reload Window)." -ForegroundColor Green Write-Host "Chat: Activity bar -> Sovereign LLM. Agent: MCP sovereign_chat tool."