From 9eded80a0e1b298c79b3b973fc5afdb6e57d8ce2 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 11 Apr 2026 23:38:46 -0700 Subject: [PATCH] Replace Claude Code with Pi in VM tooling layer The inner coding agent has been rebranded. Update binary paths, config directories, install scripts, exported functions, and environment variables accordingly. --- src/vm.ts | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/vm.ts b/src/vm.ts index 8f18160..c831ed4 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -9,7 +9,7 @@ import { get as getConfig, DEFAULTS, validateMemory } from "./config.ts" const DEBUG = !!process.env.DEBUG const CONTAINER_NAME = "sandlot" const USER = "ubuntu" -const CLAUDE_BIN = `/home/${USER}/.local/bin/claude` +const PI_BIN = `/home/${USER}/.local/bin/pi` const CONTAINER_PATH = `/sandlot/bin:/sandlot/.cargo/bin:/sandlot/.go/bin:/sandlot/.gopath/bin:/home/${USER}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` const CONTAINER_ENV = { RUSTUP_HOME: "/sandlot/.rustup", @@ -107,12 +107,12 @@ const CACHE_DIR = join(homedir(), '.sandlot', '.cache') /** Check whether the package cache is populated. */ async function hasCachedTooling(): Promise { - const files = ['bun', 'claude', 'neofetch', 'nvim.tar.gz'] + const files = ['bun', 'pi', 'neofetch', 'nvim.tar.gz'] const checks = await Promise.all(files.map(f => Bun.file(join(CACHE_DIR, f)).exists())) return checks.every(Boolean) } -/** Install Bun, Claude Code, neofetch, and Neovim using cached binaries when available. */ +/** Install Bun, Pi, neofetch, and Neovim using cached binaries when available. */ async function installTooling(cached: boolean, log?: (msg: string) => void): Promise { // Ensure cache directory exists on the host (mounted at /sandlot/.cache in the container) await $`mkdir -p ${CACHE_DIR}`.quiet() @@ -123,7 +123,7 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p ~/.local/bin"}`, "Create bin directory") await run( - $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp /sandlot/.cache/bun /sandlot/.cache/claude /sandlot/.cache/neofetch ~/.local/bin/ && chmod +x ~/.local/bin/bun ~/.local/bin/claude ~/.local/bin/neofetch && ln -sf bun ~/.local/bin/bunx"}`, + $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp /sandlot/.cache/bun /sandlot/.cache/pi /sandlot/.cache/neofetch ~/.local/bin/ && chmod +x ~/.local/bin/bun ~/.local/bin/pi ~/.local/bin/neofetch && ln -sf bun ~/.local/bin/bunx"}`, "Install cached binaries") await run( $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"tar xzf /sandlot/.cache/nvim.tar.gz -C ~/.local --strip-components=1"}`, @@ -136,10 +136,10 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro $`container exec --user ${USER} ${CONTAINER_NAME} env BUN_INSTALL=/home/${USER}/.local bash -c ${"curl -fsSL https://bun.sh/install | bash"}`, "Bun installation") - log?.("Installing Claude Code") + log?.("Installing Pi") await run( - $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://claude.ai/install.sh | bash"}`, - "Claude Code installation") + $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://pi.ai/install.sh | bash"}`, + "Pi installation") log?.("Installing neofetch") await run( @@ -152,7 +152,7 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro "Neovim installation") // Cache binaries for next time - await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp ~/.local/bin/bun ~/.local/bin/claude ~/.local/bin/neofetch /sandlot/.cache/ && cp /tmp/nvim.tar.gz /sandlot/.cache/nvim.tar.gz"}`.nothrow().quiet() + await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp ~/.local/bin/bun ~/.local/bin/pi ~/.local/bin/neofetch /sandlot/.cache/ && cp /tmp/nvim.tar.gz /sandlot/.cache/nvim.tar.gz"}`.nothrow().quiet() await installPersistentTooling(log) } @@ -212,7 +212,7 @@ async function installScript(home: string, name: string, content: string): Promi await Bun.file(tmp).unlink() } -/** Configure git identity, API key helper, activity hook, and Claude settings. */ +/** Configure git identity, API key helper, activity hook, and Pi settings. */ async function configureEnvironment(home: string, apiKey: string): Promise { const gitName = (await $`git config user.name`.quiet().text()).trim() const gitEmail = (await $`git config user.email`.quiet().text()).trim() @@ -225,24 +225,24 @@ async function configureEnvironment(home: string, apiKey: string): Promise PreToolUse: [{ hooks: [{ type: "command", command: `${activityBin} active` }] }], } const statusLine = { type: "command", command: `/home/${USER}/.local/bin/sandlot-statusline` } - const settingsJson = JSON.stringify({ apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks, statusLine }) - const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true, projects: { "/": { hasTrustDialogAccepted: true } } }) + const settingsJson = JSON.stringify({ apiKeyHelper: "~/.pi/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks, statusLine }) + const piJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true, projects: { "/": { hasTrustDialogAccepted: true } } }) // Write the helper script to a temp file and copy it in so the key // never appears in a process argument visible in `ps`. const tmp = `${home}/.sandlot/.api-key-helper.tmp` await Bun.write(tmp, `#!/bin/sh\necho '${apiKey.replace(/'/g, "'\\''")}'\n`) await $`chmod +x ${tmp}`.quiet() - await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p ~/.claude && cp /sandlot/.api-key-helper.tmp ~/.claude/api-key-helper.sh"}`.quiet() + await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p ~/.pi && cp /sandlot/.api-key-helper.tmp ~/.pi/api-key-helper.sh"}`.quiet() await Bun.file(tmp).unlink() - await installScript(home, "sandlot-activity", `#!/bin/bash\nP="\${CLAUDE_PROJECT_DIR%/}"\necho "$1" > "$(dirname "$P")/.activity-$(basename "$P")"\n`) + await installScript(home, "sandlot-activity", `#!/bin/bash\nP="\${PI_PROJECT_DIR%/}"\necho "$1" > "$(dirname "$P")/.activity-$(basename "$P")"\n`) await installScript(home, "sandlot-statusline", `#!/bin/bash\ninput=$(cat)\ncwd=$(echo "$input" | grep -oP '"cwd"\\s*:\\s*"\\K[^"]+' | head -1)\n[ -n "$cwd" ] && printf '\\033[36m\u2387 %s\\033[0m\\n' "$(basename "$cwd")"\n`) await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${` -mkdir -p ~/.claude -echo '${settingsJson}' > ~/.claude/settings.json -echo '${claudeJson}' > ~/.claude.json +mkdir -p ~/.pi +echo '${settingsJson}' > ~/.pi/settings.json +echo '${piJson}' > ~/.pi.json `}`.quiet() } @@ -339,8 +339,8 @@ export async function status(): Promise<"running" | "stopped" | "missing"> { } } -/** Launch claude in the container at the given workdir. */ -export async function claude(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<{ exitCode: number; output?: string }> { +/** Launch pi in the container at the given workdir. */ +export async function pi(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<{ exitCode: number; output?: string }> { const cwd = containerPath(workdir) const mounts = hostMounts(homedir()) const systemPromptLines = [ @@ -361,7 +361,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?: const term = process.env.TERM || "xterm-256color" const envArgs = [`TERM=${term}`, `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)] - const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", ...envArgs, CLAUDE_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--effort", "max", "--append-system-prompt", systemPrompt] + const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", ...envArgs, PI_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--effort", "max", "--append-system-prompt", systemPrompt] if (opts?.continue) args.push("--continue") if (opts?.print) args.push("-p", opts.print) else if (opts?.prompt) args.push(opts.prompt) @@ -372,7 +372,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?: const exitCode = await proc.exited if (exitCode !== 0 && opts?.continue) { info("Retrying without --continue") - return claude(workdir, { ...opts, continue: false }) + return pi(workdir, { ...opts, continue: false }) } return { exitCode, output } } @@ -381,7 +381,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?: const exitCode = await proc.exited if (exitCode !== 0 && opts?.continue) { info("Retrying without --continue") - return claude(workdir, { ...opts, continue: false }) + return pi(workdir, { ...opts, continue: false }) } return { exitCode } } @@ -416,23 +416,23 @@ export async function exec(workdir: string, command: string): Promise<{ exitCode } } -/** Pipe input text to Claude in the container with a prompt, returning the output. */ -export async function claudePipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { - const tmpName = `.claude-pipe-${crypto.randomUUID()}` +/** Pipe input text to Pi in the container with a prompt, returning the output. */ +export async function piPipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const tmpName = `.pi-pipe-${crypto.randomUUID()}` const tmpPath = join(homedir(), '.sandlot', tmpName) try { await Bun.write(tmpPath, input) return await exec( join(homedir(), '.sandlot'), - `cat /sandlot/${tmpName} | claude --model claude-opus-4-6 --effort max -p "${prompt.replace(/"/g, '\\"')}"`, + `cat /sandlot/${tmpName} | pi --model claude-opus-4-6 --effort max -p "${prompt.replace(/"/g, '\\"')}"`, ) } finally { await Bun.file(tmpPath).unlink().catch(() => {}) } } -/** Check if Claude is actively working in the given worktree (based on activity hook). */ -export async function isClaudeActive(worktree: string, branch: string): Promise { +/** Check if Pi is actively working in the given worktree (based on activity hook). */ +export async function isPiActive(worktree: string, branch: string): Promise { const file = `${dirname(worktree)}/.activity-${branch}` try { const content = await Bun.file(file).text()