#!/bin/sh # Regent's installer and fixer for macOS. https://regent.moneymax.app # # curl -fsSL https://regent.moneymax.app/install.sh | sh set up Regent, or fix and update it # curl -fsSL https://regent.moneymax.app/install.sh | sh -s -- --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. set -u VERSION="1.8.0" SITE="https://regent.moneymax.app" LIBRARY="https://claude.ai/customize/plugins/plugin_01G6waSEaGNWCsrkvLLRKou9" RHOME="${REGENT_HOME:-$HOME/.regent}" CLAUDE="$HOME/.local/bin/claude" # where Regent looks for Claude Code LABEL="sg.com.moneymax.regent" # the 5-minute schedule; the host is $LABEL.host check=0 yes=0 for a in "$@"; do case "$a" in --check) check=1 ;; --yes | -y) yes=1 ;; *) echo "Unknown option: $a (use --check or --yes)"; exit 2 ;; esac done if [ -t 1 ]; then B=$(printf '\033[1m'); G=$(printf '\033[32m'); Y=$(printf '\033[33m'); D=$(printf '\033[2m'); N=$(printf '\033[0m'); else B= G= Y= D= N=; fi left=0 step() { printf '\n%s%s%s\n' "$B" "$*" "$N"; } ok() { printf ' %s✓%s %s\n' "$G" "$N" "$*"; } bad() { printf ' %s✗%s %s\n' "$Y" "$N" "$*"; } note() { printf ' %s%s%s\n' "$D" "$*" "$N"; } todo() { bad "$*"; left=$((left + 1)); } # Ask before a change. The script itself arrives on stdin, so answers come from the terminal. ask() { [ "$check" = 1 ] && { note "(check only: not changed)"; return 1; } [ "$yes" = 1 ] && return 0 if ! (: /dev/null; then note "(no terminal to ask in: run it again with --yes to make changes)"; return 1; fi printf ' %s [Y/n] ' "$1" read -r answer /dev/null || { printf ' %s' "$1"; read -r _ /dev/null | sed -n 's/^v\([0-9]*\)\..*/\1/p'; } good_node() { m=$(major "$1"); [ -n "$m" ] && [ "$m" -ge 24 ]; } NODE="" for n in "$(command -v node 2>/dev/null)" /opt/homebrew/bin/node /usr/local/bin/node; do if [ -n "$n" ] && [ -x "$n" ] && good_node "$n"; then NODE="$n"; break; fi done FNM="" for f in "$(command -v fnm 2>/dev/null)" "$HOME/.local/share/fnm/fnm" /opt/homebrew/bin/fnm /usr/local/bin/fnm "$HOME/Library/Application Support/fnm/fnm"; do if [ -n "$f" ] && [ -x "$f" ]; then FNM="$f"; break; fi done fnm_node() { "$FNM" exec --using=24 node -p 'require("fs").realpathSync(process.execPath)' 2>/dev/null; } [ -z "$NODE" ] && [ -n "$FNM" ] && NODE=$(fnm_node) if [ -n "$NODE" ]; then ok "Node $("$NODE" -v) ($NODE)" else if [ -z "$FNM" ]; then bad "Node 24 isn't installed." note "It can be installed for you only, with fnm (no admin rights; other Node versions are left alone)." if ask "Install fnm and Node 24?"; then curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell >/dev/null && FNM="$HOME/.local/share/fnm/fnm" fi else bad "fnm is installed, but not Node 24." fi if [ -n "$FNM" ] && [ -x "$FNM" ] && ask "Install Node 24 with fnm and make it the default?"; then "$FNM" install 24 >/dev/null 2>&1 && "$FNM" default 24 >/dev/null 2>&1 NODE=$(fnm_node) fi if [ -n "$NODE" ]; then ok "Node $("$NODE" -v) ($NODE)"; else todo "Regent needs Node 24. If this didn't work, ask MIS to install Node 24."; fi fi # Let new terminal windows find fnm's Node. if [ -n "$FNM" ] && [ "${SHELL##*/}" = zsh ] && ! grep -qsE 'fnm"? env' "$HOME/.zshrc" "$HOME/.zprofile"; then bad "New terminal windows don't load fnm's Node." if ask "Add fnm's line to ~/.zshrc?"; then printf '\n# fnm (Node versions)\neval "$(%s env --use-on-cd --shell zsh)"\n' "\"$FNM\"" >>"$HOME/.zshrc" && ok "Added to ~/.zshrc" fi fi # ---- 2. Claude Code ---- step "2. Claude Code" if [ ! -x "$CLAUDE" ]; then other=$(command -v claude 2>/dev/null || true) if [ -n "$other" ]; then bad "Claude Code is at $other, but Regent needs Anthropic's own install in ~/.local/bin." else bad "Claude Code isn't installed." fi if ask "Install Claude Code with Anthropic's installer (curl -fsSL https://claude.ai/install.sh | bash)?"; then curl -fsSL https://claude.ai/install.sh | bash fi fi signed_in=0 if [ -x "$CLAUDE" ]; then ok "Claude Code $("$CLAUDE" --version 2>/dev/null | sed 's/ (Claude Code)//')" status=$("$CLAUDE" auth status --json 2>/dev/null || true) field() { printf '%s' "$status" | sed -n "s/.*\"$1\": *\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}\$/\1/p" | head -1; } if [ "$(field loggedIn)" != true ] || [ "$(field authMethod)" != claude.ai ]; then bad "Claude Code isn't signed in to claude.ai." if ask "Sign in now? A browser opens; use your MoneyMax account."; then "$CLAUDE" auth login /dev/null || true) fi fi if [ "$(field loggedIn)" = true ] && [ "$(field authMethod)" = claude.ai ]; then org=$(field orgName) if [ "$org" = MoneyMax ]; then ok "Signed in to MoneyMax's Claude"; signed_in=1 else todo "Signed in to ${org:-another account}, not MoneyMax. Run 'claude', type /login, and choose your MoneyMax account."; fi else todo "Not signed in. Run 'claude' and sign in with your MoneyMax account, then run this again." fi else todo "Claude Code is needed. See https://claude.ai/install.sh, or ask MIS." fi # ---- 3. Regent from the library ---- step "3. Regent from the MoneyMax library" # Library copies sit in ~/.claude/plugins/synced//; an update lands beside a copy in use as ~g. # The newest is the highest N (as scripts/regent.mjs picks it). Prints "|". newest() { [ -n "$NODE" ] || return 0 "$NODE" -e ' const fs = require("fs"), path = require("path"); const root = path.join(process.argv[1], ".claude/plugins/synced"); let best; for (const org of fs.existsSync(root) ? fs.readdirSync(root) : []) { if (org.startsWith(".")) continue; let dirs = []; try { dirs = fs.readdirSync(path.join(root, org)); } catch {} for (const d of dirs) { const dir = path.join(root, org, d); let p; try { p = JSON.parse(fs.readFileSync(path.join(dir, ".claude-plugin/plugin.json"), "utf8").replace(/^/, "")); } catch { continue; } const gen = Number(/~g(\d+)$/.exec(d)?.[1] ?? 1); if (p.name === "regent" && fs.existsSync(path.join(dir, "engine/cli.ts")) && (!best || gen > best.gen)) best = { gen, dir, v: p.version }; } } if (best) console.log(best.dir + "|" + best.v); ' "$HOME" } # One tiny request (haiku, no tools, not saved): Claude Code fetches library updates when it talks to Claude. fetch() { [ "$signed_in" = 1 ] && (cd /tmp && "$CLAUDE" -p --model haiku --strict-mcp-config --tools "" --no-session-persistence "Reply with the word ok." /dev/null 2>&1) return 0 } COPY="" found=$(newest) if [ "$check" = 0 ] && [ "$signed_in" = 1 ]; then note "Asking Claude Code to fetch the latest version…" fetch found=$(newest) fi if [ -z "$found" ] && [ "$signed_in" = 1 ]; then 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."; then open "$LIBRARY" pause "Press Enter once you've chosen Add… " fetch found=$(newest) fi fi if [ -n "$found" ]; then COPY=${found%|*} ok "Regent ${found#*|} from the library" else todo "Regent isn't on this computer yet. Add it from $LIBRARY, open Claude Code once, then run this again." fi # ---- 4. Your setup ---- step "4. The regent command and the background schedule" ENGINE="$COPY" dev="$RHOME/dev" if [ -f "$dev" ]; then d=$(cat "$dev") if [ -d "$d/.git" ] && [ -f "$d/engine/cli.ts" ]; then 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?"; then rm -f "$dev" && ok "Removed"; else ENGINE="$d"; left=$((left + 1)); fi fi fi plist="$HOME/Library/LaunchAgents/$LABEL.plist" hplist="$HOME/Library/LaunchAgents/$LABEL.host.plist" reasons="" because() { reasons="${reasons}${reasons:+; }$1"; } [ -f "$RHOME/bin/regent" ] && grep -q "regent.mjs" "$RHOME/bin/regent" || because "the regent command is missing or old" [ -n "$ENGINE" ] && ! cmp -s "$RHOME/bin/regent.mjs" "$ENGINE/scripts/regent.mjs" && because "the regent command needs a refresh" launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || because "the 5-minute schedule isn't running" launchctl print "gui/$(id -u)/$LABEL.host" >/dev/null 2>&1 || because "the host for live agents isn't running" # The schedule runs with the Node it was installed with; a removed Node version stops it. if [ -f "$plist" ]; then p=$(plutil -extract EnvironmentVariables.PATH raw "$plist" 2>/dev/null || true) has=0 IFS=: for d in $p; do [ -x "$d/node" ] && good_node "$d/node" && has=1; done unset IFS [ "$has" = 1 ] || because "the schedule's Node is gone" fi if [ -z "$reasons" ]; then ok "The regent command, the 5-minute schedule and the host are in place" elif [ -z "$ENGINE" ] || [ -z "$NODE" ]; then todo "Needs setting up ($reasons), after the steps above." else bad "Needs setting up: $reasons." if ask "Set up the regent command, the 5-minute schedule and the host now?"; then out=$(REGENT_ENGINE="$ENGINE" "$NODE" --disable-warning=ExperimentalWarning "$ENGINE/scripts/regent.mjs" install 2>&1) rc=$? printf '%s\n' "$out" | sed 's/^/ /' if [ "$rc" = 0 ]; then ok "Done"; else todo "regent install failed (see above)."; fi else left=$((left + 1)); fi fi # ---- 5. PATH ---- step "5. Typing 'regent' and 'claude' in a terminal" new_window=0 case "${SHELL##*/}" in zsh) rc="$HOME/.zshrc" others="$HOME/.zprofile $HOME/.zshenv" ;; bash) rc="$HOME/.bash_profile" others="$HOME/.bashrc $HOME/.profile" ;; *) rc="$HOME/.profile" others="" ;; esac for dir in "$RHOME/bin" "$HOME/.local/bin"; do case "$dir" in "$RHOME/bin") what=regent ;; *) what=claude ;; esac case ":$PATH:" in *":$dir:"*) ok "'$what' works: $dir is on your PATH"; continue ;; esac case "$dir" in "$HOME"/*) line="\$HOME${dir#"$HOME"}" ;; *) line="$dir" ;; esac # shellcheck disable=SC2086 if grep -qsF "${dir#"$HOME"}" "$rc" $others; then ok "'$what' works in a new terminal window ($dir is in your shell's settings)" new_window=1 else bad "$dir isn't on your PATH, so typing '$what' won't work." if ask "Add it to $rc?"; then printf '\n# %s\nexport PATH="%s:$PATH"\n' "$what" "$line" >>"$rc" && ok "Added to $rc" && new_window=1 else left=$((left + 1)); fi fi done # ---- Check ---- if [ -x "$RHOME/bin/regent" ] && [ -n "$NODE" ]; then step "Regent's own check" PATH="$(dirname "$NODE"):$PATH" "$RHOME/bin/regent" doctor 2>&1 | grep -v "on your PATH" | sed 's/^/ /' fi echo if [ "$check" = 1 ]; then echo "${B}Check done.${N} To fix anything marked ✗, run it without --check: curl -fsSL $SITE/install.sh | sh" elif [ "$left" -gt 0 ]; then echo "${B}Almost there:${N} $left thing(s) above still need doing. Then run this again. Stuck? Ask MIS." else echo "${B}All set.${N}" [ "$new_window" = 1 ] && echo "Open a new terminal window, then type: regent" || echo "Type: regent" echo "If Regent's dashboard is already open, quit it (q) and open it again to use the new version." fi