# Budget Pilot for Windows - one line in PowerShell (the HQ fills in its address and your key): # irm "https:///install.ps1?key=" | iex # Remove it again: # irm "https:///install.ps1?remove=1" | iex # No admin rights: the program lives in %LOCALAPPDATA%\budget-pilot, Claude Code's hooks go into your own # ~\.claude\settings.json, the meter starts with your Windows login (HKCU Run key, no window). $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $Server = "https://budgetpilot.net" $Key = "" $Remove = "0" -eq "1" if ($env:BP_KEY) { $Key = $env:BP_KEY } $P = Join-Path $env:LOCALAPPDATA "budget-pilot" $Bin = Join-Path $P "bin" $B = Join-Path $Bin "budget-pilot.exe" $Run = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" $Stamp = Get-Date -Format "yyyyMMdd-HHmmss" function Stop-Meter { Get-Process -Name "budget-pilot" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 } function Drop-Link { # bin is a directory junction: rmdir takes the link away and never the version's files if (Test-Path $Bin) { cmd /c rmdir "$Bin" | Out-Null } } function Set-UserPath([bool]$add) { $cur = [Environment]::GetEnvironmentVariable("Path", "User") $parts = @($cur -split ";" | Where-Object { $_ -and ($_.TrimEnd("\") -ne $Bin) }) if ($add) { $parts = @($Bin) + $parts } [Environment]::SetEnvironmentVariable("Path", ($parts -join ";"), "User") if ($add -and (($env:Path -split ";") -notcontains $Bin)) { $env:Path = "$Bin;$env:Path" } } function Start-Meter { $conhost = Join-Path $env:SystemRoot "System32\conhost.exe" if (Test-Path $conhost) { Set-ItemProperty -Path $Run -Name "Budget Pilot" -Value "`"$conhost`" --headless `"$B`" daemon" Start-Process -FilePath $conhost -ArgumentList "--headless", "`"$B`"", "daemon" -WindowStyle Hidden } else { Set-ItemProperty -Path $Run -Name "Budget Pilot" -Value "`"$B`" daemon" Start-Process -FilePath $B -ArgumentList "daemon" -WindowStyle Hidden } } if ($Remove) { Stop-Meter if (Test-Path $B) { & $B off | Out-Null } Remove-ItemProperty -Path $Run -Name "Budget Pilot" -ErrorAction SilentlyContinue Set-UserPath $false Drop-Link if (Test-Path (Join-Path $P "versions")) { Remove-Item -Recurse -Force (Join-Path $P "versions") } Write-Host "Budget Pilot removed (its hooks and settings are out of your AI tools' settings; the meter is stopped)." Write-Host "Your settings, log and memory (saved chat pieces) stay in $P (delete the folder if you like)." return } if (-not $Key -or $Key.StartsWith("__")) { # no key in the link (the HQ fills in the one it was given) $kf = Join-Path $P "licence.key" if (Test-Path $kf) { $Key = (Get-Content $kf -Raw).Trim() } else { $Key = "" } } if (-not $Key) { Write-Host "Add your key: irm `"$Server/install.ps1?key=YOUR-KEY`" | iex (get one at $Server)" return } $Tar = Join-Path $env:SystemRoot "System32\tar.exe" # Windows' own tar (another tar on PATH may not take C:\) if (-not (Test-Path $Tar)) { Write-Host "Budget Pilot needs Windows 10 (version 1803) or newer." return } $Tmp = Join-Path ([IO.Path]::GetTempPath()) ("budget-pilot-" + [Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $Tmp | Out-Null try { Write-Host "Downloading Budget Pilot (Windows)..." try { Invoke-WebRequest -UseBasicParsing -Uri ("$Server/download/windows-x86_64?key=" + [Uri]::EscapeDataString($Key)) ` -OutFile (Join-Path $Tmp "bp.tgz") } catch { $msg = $_.ErrorDetails.Message if (-not $msg) { $msg = $_.Exception.Message } Write-Host "Download refused: $msg" return } & $Tar -xzf (Join-Path $Tmp "bp.tgz") -C $Tmp $New = Join-Path $Tmp "budget-pilot" if (-not (Test-Path (Join-Path $New "budget-pilot.exe"))) { Write-Host "The download looks broken; please try again."; return } $V = (Get-Content (Join-Path $New "VERSION") -Raw).Trim() if ($V -notmatch '^\d+\.\d+\.\d+$') { Write-Host "The download looks broken; please try again."; return } $Vers = Join-Path $P "versions" New-Item -ItemType Directory -Force -Path $Vers | Out-Null Stop-Meter $Dest = Join-Path $Vers $V if (Test-Path $Dest) { Remove-Item -Recurse -Force $Dest } Move-Item $New $Dest Drop-Link New-Item -ItemType Junction -Path $Bin -Target $Dest | Out-Null } finally { Remove-Item -Recurse -Force $Tmp -ErrorAction SilentlyContinue } if (-not (Test-Path $B)) { return } if (Test-Path (Join-Path $Bin "LICENSE.txt")) { Copy-Item (Join-Path $Bin "LICENSE.txt") (Join-Path $P "LICENSE.txt") -Force } & $B init-config $Server | Out-Null & $B licence $Key if ($LASTEXITCODE -ne 0) { return } $cs = Join-Path $HOME ".claude\settings.json" if (Test-Path $cs) { Copy-Item $cs (Join-Path $P "settings.backup.$Stamp.json") } & $B config claude_settings user | Out-Null & $B on Set-UserPath $true & $B link | Out-Null Start-Meter Start-Sleep -Seconds 3 if ([version]$V -ge [version]"4.3.0") { & $B screen } else { & $B status; Write-Host "All commands: budget-pilot help" }