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.
This commit is contained in:
Chris Wanstrath 2026-04-11 23:38:46 -07:00
parent 846f2cd021
commit 9eded80a0e

View File

@ -9,7 +9,7 @@ import { get as getConfig, DEFAULTS, validateMemory } from "./config.ts"
const DEBUG = !!process.env.DEBUG const DEBUG = !!process.env.DEBUG
const CONTAINER_NAME = "sandlot" const CONTAINER_NAME = "sandlot"
const USER = "ubuntu" 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_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 = { const CONTAINER_ENV = {
RUSTUP_HOME: "/sandlot/.rustup", RUSTUP_HOME: "/sandlot/.rustup",
@ -107,12 +107,12 @@ const CACHE_DIR = join(homedir(), '.sandlot', '.cache')
/** Check whether the package cache is populated. */ /** Check whether the package cache is populated. */
async function hasCachedTooling(): Promise<boolean> { async function hasCachedTooling(): Promise<boolean> {
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())) const checks = await Promise.all(files.map(f => Bun.file(join(CACHE_DIR, f)).exists()))
return checks.every(Boolean) 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<void> { async function installTooling(cached: boolean, log?: (msg: string) => void): Promise<void> {
// Ensure cache directory exists on the host (mounted at /sandlot/.cache in the container) // Ensure cache directory exists on the host (mounted at /sandlot/.cache in the container)
await $`mkdir -p ${CACHE_DIR}`.quiet() 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"}`, $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p ~/.local/bin"}`,
"Create bin directory") "Create bin directory")
await run( 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") "Install cached binaries")
await run( await run(
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"tar xzf /sandlot/.cache/nvim.tar.gz -C ~/.local --strip-components=1"}`, $`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"}`, $`container exec --user ${USER} ${CONTAINER_NAME} env BUN_INSTALL=/home/${USER}/.local bash -c ${"curl -fsSL https://bun.sh/install | bash"}`,
"Bun installation") "Bun installation")
log?.("Installing Claude Code") log?.("Installing Pi")
await run( await run(
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://claude.ai/install.sh | bash"}`, $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://pi.ai/install.sh | bash"}`,
"Claude Code installation") "Pi installation")
log?.("Installing neofetch") log?.("Installing neofetch")
await run( await run(
@ -152,7 +152,7 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro
"Neovim installation") "Neovim installation")
// Cache binaries for next time // 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) await installPersistentTooling(log)
} }
@ -212,7 +212,7 @@ async function installScript(home: string, name: string, content: string): Promi
await Bun.file(tmp).unlink() 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<void> { async function configureEnvironment(home: string, apiKey: string): Promise<void> {
const gitName = (await $`git config user.name`.quiet().text()).trim() const gitName = (await $`git config user.name`.quiet().text()).trim()
const gitEmail = (await $`git config user.email`.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<void>
PreToolUse: [{ hooks: [{ type: "command", command: `${activityBin} active` }] }], PreToolUse: [{ hooks: [{ type: "command", command: `${activityBin} active` }] }],
} }
const statusLine = { type: "command", command: `/home/${USER}/.local/bin/sandlot-statusline` } 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 settingsJson = JSON.stringify({ apiKeyHelper: "~/.pi/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks, statusLine })
const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true, projects: { "/": { hasTrustDialogAccepted: true } } }) 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 // Write the helper script to a temp file and copy it in so the key
// never appears in a process argument visible in `ps`. // never appears in a process argument visible in `ps`.
const tmp = `${home}/.sandlot/.api-key-helper.tmp` const tmp = `${home}/.sandlot/.api-key-helper.tmp`
await Bun.write(tmp, `#!/bin/sh\necho '${apiKey.replace(/'/g, "'\\''")}'\n`) await Bun.write(tmp, `#!/bin/sh\necho '${apiKey.replace(/'/g, "'\\''")}'\n`)
await $`chmod +x ${tmp}`.quiet() 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 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 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 ${` await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${`
mkdir -p ~/.claude mkdir -p ~/.pi
echo '${settingsJson}' > ~/.claude/settings.json echo '${settingsJson}' > ~/.pi/settings.json
echo '${claudeJson}' > ~/.claude.json echo '${piJson}' > ~/.pi.json
`}`.quiet() `}`.quiet()
} }
@ -339,8 +339,8 @@ export async function status(): Promise<"running" | "stopped" | "missing"> {
} }
} }
/** Launch claude in the container at the given workdir. */ /** Launch pi 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 }> { export async function pi(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<{ exitCode: number; output?: string }> {
const cwd = containerPath(workdir) const cwd = containerPath(workdir)
const mounts = hostMounts(homedir()) const mounts = hostMounts(homedir())
const systemPromptLines = [ const systemPromptLines = [
@ -361,7 +361,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
const term = process.env.TERM || "xterm-256color" const term = process.env.TERM || "xterm-256color"
const envArgs = [`TERM=${term}`, `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)] 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?.continue) args.push("--continue")
if (opts?.print) args.push("-p", opts.print) if (opts?.print) args.push("-p", opts.print)
else if (opts?.prompt) args.push(opts.prompt) 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 const exitCode = await proc.exited
if (exitCode !== 0 && opts?.continue) { if (exitCode !== 0 && opts?.continue) {
info("Retrying without --continue") info("Retrying without --continue")
return claude(workdir, { ...opts, continue: false }) return pi(workdir, { ...opts, continue: false })
} }
return { exitCode, output } return { exitCode, output }
} }
@ -381,7 +381,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
const exitCode = await proc.exited const exitCode = await proc.exited
if (exitCode !== 0 && opts?.continue) { if (exitCode !== 0 && opts?.continue) {
info("Retrying without --continue") info("Retrying without --continue")
return claude(workdir, { ...opts, continue: false }) return pi(workdir, { ...opts, continue: false })
} }
return { exitCode } 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. */ /** Pipe input text to Pi in the container with a prompt, returning the output. */
export async function claudePipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { export async function piPipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const tmpName = `.claude-pipe-${crypto.randomUUID()}` const tmpName = `.pi-pipe-${crypto.randomUUID()}`
const tmpPath = join(homedir(), '.sandlot', tmpName) const tmpPath = join(homedir(), '.sandlot', tmpName)
try { try {
await Bun.write(tmpPath, input) await Bun.write(tmpPath, input)
return await exec( return await exec(
join(homedir(), '.sandlot'), 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 { } finally {
await Bun.file(tmpPath).unlink().catch(() => {}) await Bun.file(tmpPath).unlink().catch(() => {})
} }
} }
/** Check if Claude is actively working in the given worktree (based on activity hook). */ /** Check if Pi is actively working in the given worktree (based on activity hook). */
export async function isClaudeActive(worktree: string, branch: string): Promise<boolean> { export async function isPiActive(worktree: string, branch: string): Promise<boolean> {
const file = `${dirname(worktree)}/.activity-${branch}` const file = `${dirname(worktree)}/.activity-${branch}`
try { try {
const content = await Bun.file(file).text() const content = await Bun.file(file).text()