# Regent's installer and fixer for Windows. https://regent.moneymax.app # # irm https://regent.moneymax.app/install.ps1 | iex set up, fix or update # & ([scriptblock]::Create((irm https://regent.moneymax.app/install.ps1))) -Check only look: change nothing # # It asks before each change. It never touches your agents, notes or reports, never asks for a password, and sends # nothing anywhere except one tiny request to Claude, so Claude Code fetches the newest Regent. # Regent itself comes from the MoneyMax plugin library on claude.ai, not from this website. param([switch]$Check, [switch]$Yes) & { $ErrorActionPreference = 'Continue' $Version = '1.8.0' $Site = 'https://regent.moneymax.app' $Library = 'https://claude.ai/customize/plugins/plugin_01G6waSEaGNWCsrkvLLRKou9' $RHome = if ($env:REGENT_HOME) { $env:REGENT_HOME } else { Join-Path $env:USERPROFILE '.regent' } $Claude = Join-Path $env:USERPROFILE '.local\bin\claude.exe' # where Regent looks for Claude Code $Bin = Join-Path $RHome 'bin' $script:Left = 0 function Step($t) { Write-Host ''; Write-Host $t -ForegroundColor White } function Ok($t) { Write-Host ' ' -NoNewline; Write-Host ([char]0x2713) -ForegroundColor Green -NoNewline; Write-Host " $t" } function Bad($t) { Write-Host ' ' -NoNewline; Write-Host ([char]0x2717) -ForegroundColor Yellow -NoNewline; Write-Host " $t" } function Note($t) { Write-Host " $t" -ForegroundColor DarkGray } function Todo($t) { Bad $t; $script:Left++ } function Ask($q) { if ($Check) { Note '(check only: not changed)'; return $false } if ($Yes) { return $true } $a = Read-Host " $q [Y/n]" if ($a -eq '' -or $a -match '^(y|yes)$') { return $true } Note 'Skipped.'; return $false } function Major($exe) { try { $v = & $exe -v 2>$null; if ($v -match '^v(\d+)\.') { [int]$Matches[1] } else { 0 } } catch { 0 } } # Windows spells it Path; this process's copy, from the user and machine settings, so new installs are seen. function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path', 'User') + ';' + [Environment]::GetEnvironmentVariable('Path', 'Machine') } $mode = if ($Check) { ' - check only, nothing will change' } else { '' } Write-Host "Regent installer $Version$mode" -ForegroundColor White # ---- 1. Node 24 ---- Step '1. Node 24' $Node = $null $cands = @((Get-Command node -ErrorAction SilentlyContinue).Source, "$env:ProgramFiles\nodejs\node.exe") | Where-Object { $_ } foreach ($n in $cands) { if ((Test-Path $n) -and (Major $n) -ge 24) { $Node = $n; break } } $Fnm = @((Get-Command fnm -ErrorAction SilentlyContinue).Source, "$env:LOCALAPPDATA\Microsoft\WinGet\Links\fnm.exe") | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 function FnmNode { if ($Fnm) { try { $p = & $Fnm exec --using=24 node -p "require('fs').realpathSync(process.execPath)" 2>$null; if ($p -and (Test-Path $p)) { $p } } catch {} } } if (-not $Node) { $Node = FnmNode } if ($Node) { Ok "Node $(& $Node -v) ($Node)" } else { if (-not $Fnm) { Bad "Node 24 isn't installed." Note 'It can be installed for you only, with fnm from winget (no admin rights; other Node versions are left alone).' if (Ask 'Install fnm and Node 24?') { winget install --id Schniz.fnm -e --accept-source-agreements --accept-package-agreements | Out-Null Refresh-Path $Fnm = @((Get-Command fnm -ErrorAction SilentlyContinue).Source, "$env:LOCALAPPDATA\Microsoft\WinGet\Links\fnm.exe") | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 } } else { Bad 'fnm is installed, but not Node 24.' } if ($Fnm -and (Ask 'Install Node 24 with fnm and make it the default?')) { & $Fnm install 24 *> $null; & $Fnm default 24 *> $null $Node = FnmNode } if ($Node) { Ok "Node $(& $Node -v) ($Node)" } else { Todo 'Regent needs Node 24. If this did not work, ask MIS to install Node 24.' } } # A OneDrive file that isn't downloaded can hang when read (over SSH, for one), so it gets 15 seconds. $null: unreadable. function ReadText($path) { if (-not (Test-Path $path)) { return '' } if (-not ((Get-Item $path -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { return [string](Get-Content $path -Raw) } $j = Start-Job { param($p) [string](Get-Content $p -Raw) } -ArgumentList $path $t = if (Wait-Job $j -Timeout 15) { Receive-Job $j } else { $null } Remove-Job $j -Force $t } $prof = if ($Fnm) { ReadText $PROFILE } else { '' } if ($Fnm -and $null -eq $prof) { Note "Couldn't read your PowerShell profile (a OneDrive file not on this PC), so fnm's line wasn't checked." } elseif ($Fnm -and $prof -notmatch 'fnm env') { Bad "New PowerShell windows don't load fnm's Node." if (Ask 'Add the fnm line to your PowerShell profile?') { New-Item -ItemType File -Force -Path $PROFILE -ErrorAction SilentlyContinue | Out-Null Add-Content -Path $PROFILE -Value "`r`n# fnm (Node versions)`r`nfnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression" Ok 'Added to your PowerShell profile' } } # ---- 2. Claude Code ---- Step '2. Claude Code' if (-not (Test-Path $Claude)) { $other = (Get-Command claude -ErrorAction SilentlyContinue).Source if ($other) { Bad "Claude Code is at $other, but Regent needs Anthropic's own install in .local\bin." } else { Bad "Claude Code isn't installed." } if (Ask "Install Claude Code with Anthropic's installer (irm https://claude.ai/install.ps1 | iex)?") { Invoke-RestMethod https://claude.ai/install.ps1 | Invoke-Expression } } $SignedIn = $false if (Test-Path $Claude) { Ok "Claude Code $((& $Claude --version 2>$null) -replace ' \(Claude Code\)', '')" function AuthStatus { try { (& $Claude auth status --json 2>$null | Out-String) | ConvertFrom-Json } catch { $null } } $st = AuthStatus if (-not ($st -and $st.loggedIn -and $st.authMethod -eq 'claude.ai')) { Bad "Claude Code isn't signed in to claude.ai." if (Ask 'Sign in now? A browser opens; use your MoneyMax account.') { & $Claude auth login; $st = AuthStatus } } if ($st -and $st.loggedIn -and $st.authMethod -eq 'claude.ai') { if ($st.orgName -eq 'MoneyMax') { Ok "Signed in to MoneyMax's Claude"; $SignedIn = $true } else { Todo "Signed in to $(if ($st.orgName) { $st.orgName } else { 'another account' }), not MoneyMax. Run 'claude', type /login, and choose your MoneyMax account." } } else { Todo "Not signed in. Run 'claude' and sign in with your MoneyMax account, then run this again." } if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Todo "Git for Windows isn't installed; Claude Code needs it. Ask MIS to install it (from git-scm.com, with 'Add to PATH')." } } else { Todo 'Claude Code is needed. See https://claude.ai/install.ps1, or ask MIS.' } # ---- 3. Regent from the library ---- Step '3. Regent from the MoneyMax library' # Library copies sit in .claude\plugins\synced\\ (a name or an id); an update lands beside a copy in use # as ~g. The newest is the highest N, as scripts\regent.mjs picks it. function Newest { $best = $null $root = Join-Path $env:USERPROFILE '.claude\plugins\synced' if (-not (Test-Path $root)) { return $null } foreach ($org in Get-ChildItem $root -Directory -ErrorAction SilentlyContinue | Where-Object { -not $_.Name.StartsWith('.') }) { foreach ($d in Get-ChildItem $org.FullName -Directory -ErrorAction SilentlyContinue) { $pj = Join-Path $d.FullName '.claude-plugin\plugin.json' if (-not (Test-Path $pj)) { continue } try { $p = (Get-Content $pj -Raw -Encoding UTF8 -ErrorAction Stop) | ConvertFrom-Json } catch { continue } if ($p.name -ne 'regent' -or -not (Test-Path (Join-Path $d.FullName 'engine\cli.ts'))) { continue } $gen = if ($d.Name -match '~g(\d+)$') { [int]$Matches[1] } else { 1 } if (-not $best -or $gen -gt $best.Gen) { $best = [pscustomobject]@{ Gen = $gen; Dir = $d.FullName; Version = $p.version } } } } $best } # One tiny request (haiku, no tools, not saved): Claude Code fetches library updates when it talks to Claude. Started # through .NET, since Windows PowerShell 5 drops the empty "" that --tools needs. function Fetch { if (-not $SignedIn) { return } $psi = New-Object Diagnostics.ProcessStartInfo $Claude, '-p --model haiku --strict-mcp-config --tools "" --no-session-persistence "Reply with the word ok."' $psi.UseShellExecute = $false; $psi.RedirectStandardInput = $true; $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true $psi.WorkingDirectory = $env:TEMP; $psi.CreateNoWindow = $true try { $p = [Diagnostics.Process]::Start($psi) $p.StandardInput.Close() $null = $p.StandardOutput.ReadToEndAsync(); $null = $p.StandardError.ReadToEndAsync() if (-not $p.WaitForExit(120000)) { $p.Kill() } Start-Sleep -Seconds 2 # let the download finish landing } catch {} } $found = Newest if (-not $Check -and $SignedIn) { Note 'Asking Claude Code to fetch the latest version...'; Fetch; $found = Newest } if (-not $found -and $SignedIn) { Bad "Regent isn't added to your Claude yet." Note "Its page in the MoneyMax library: $Library" if (Ask 'Open that page? Choose Add there, then come back here.') { Start-Process $Library if (-not $Yes) { Read-Host " Press Enter once you've chosen Add" | Out-Null } Fetch; $found = Newest } } $Copy = $null if ($found) { $Copy = $found.Dir; Ok "Regent $($found.Version) from the library" } else { Todo "Regent isn't on this computer yet. Add it from $Library, open Claude Code once, then run this again." } # ---- 4. Your setup ---- Step '4. The regent command and the background schedule' $Engine = $Copy $dev = Join-Path $RHome 'dev' if (Test-Path $dev) { $d = (Get-Content $dev -Raw).Trim() if ((Test-Path (Join-Path $d '.git')) -and (Test-Path (Join-Path $d 'engine\cli.ts'))) { Ok "Runs your own Regent checkout ($d). Delete $dev to use the library copy." $Engine = $d } else { Bad "An old setting ($dev) makes regent run $d instead of the library copy." if (Ask 'Remove that setting?') { Remove-Item $dev -Force; Ok 'Removed' } else { $Engine = $d; $script:Left++ } } } $reasons = @() $cmd = Join-Path $Bin 'regent.cmd' if (-not ((Test-Path $cmd) -and (Select-String -Path $cmd -Pattern 'regent.mjs' -Quiet))) { $reasons += 'the regent command is missing or old' } elseif ((Get-Content $cmd -Raw) -match '"([^"]+node\.exe)"' -and -not (Test-Path $Matches[1])) { $reasons += "the regent command's Node is gone" } if ($Engine -and (Test-Path (Join-Path $Engine 'scripts\regent.mjs'))) { $mine = Join-Path $Bin 'regent.mjs' if (-not (Test-Path $mine) -or (Get-FileHash $mine).Hash -ne (Get-FileHash (Join-Path $Engine 'scripts\regent.mjs')).Hash) { $reasons += 'the regent command needs a refresh' } } foreach ($t in @(@('tick', 'the 5-minute schedule'), @('host', 'the host for live agents'))) { if (-not (Get-ScheduledTask -TaskPath '\Regent\' -TaskName $t[0] -ErrorAction SilentlyContinue)) { $reasons += "$($t[1]) isn't set up" } } if (-not $reasons) { Ok 'The regent command, the 5-minute schedule and the host are in place' } elseif (-not $Engine -or -not $Node) { Todo "Needs setting up ($($reasons -join '; ')), after the steps above." } else { Bad "Needs setting up: $($reasons -join '; ')." if (Ask 'Set up the regent command, the 5-minute schedule and the host now?') { $env:REGENT_ENGINE = $Engine & $Node --disable-warning=ExperimentalWarning (Join-Path $Engine 'scripts\regent.mjs') install 2>&1 | ForEach-Object { " $_" } $ok = $LASTEXITCODE -eq 0 Remove-Item Env:REGENT_ENGINE if ($ok) { Ok 'Done' } else { Todo 'regent install failed (see above).' } } else { $script:Left++ } } # ---- 5. PATH ---- Step "5. Typing 'regent' and 'claude' in a terminal" $NewWindow = $false $has = { param($list, $dir) ($list -split ';') | Where-Object { $_ -and ($_.TrimEnd('\') -ieq $dir.TrimEnd('\')) } } foreach ($pair in @(@($Bin, 'regent'), @((Join-Path $env:USERPROFILE '.local\bin'), 'claude'))) { $dir = $pair[0]; $what = $pair[1] $user = [Environment]::GetEnvironmentVariable('Path', 'User') if (& $has $env:Path $dir) { Ok "'$what' works: $dir is on your PATH" } elseif (& $has $user $dir) { Ok "'$what' works in a new terminal window"; $NewWindow = $true } else { Bad "$dir isn't on your PATH, so typing '$what' won't work." if (Ask 'Add it to your PATH (for you only)?') { [Environment]::SetEnvironmentVariable('Path', ("$user".TrimEnd(';') + ";$dir").TrimStart(';'), 'User') Ok 'Added'; $NewWindow = $true } else { $script:Left++ } } } # ---- Check ---- if ((Test-Path $cmd) -and $Node) { Step "Regent's own check" & $cmd doctor 2>&1 | Where-Object { $_ -notmatch 'on your PATH' } | ForEach-Object { " $_" } } Write-Host '' if ($Check) { Write-Host "Check done. To fix anything marked with a cross, run it without -Check: irm $Site/install.ps1 | iex" } elseif ($script:Left -gt 0) { Write-Host "Almost there: $($script:Left) thing(s) above still need doing. Then run this again. Stuck? Ask MIS." } else { Write-Host 'All set.' if ($NewWindow) { Write-Host 'Open a new terminal window, then type: regent' } else { Write-Host 'Type: regent' } Write-Host "If Regent's dashboard is already open, quit it (q) and open it again to use the new version." } }