Compare commits

..

No commits in common. "e64564a045f26755f8f173711b7a56ee772dd956" and "846f2cd021ea03623d057e9d0a6faccf497cb933" have entirely different histories.

10 changed files with 53 additions and 53 deletions

View File

@ -35,7 +35,7 @@ const program = new Command()
program
.name("sandlot")
.description("Sandboxed development with Pi.")
.description("Sandboxed development with Claude.")
.configureHelp({ styleTitle: (str) => `${yellow}${str}${reset}` })
.helpOption(false)
.addOption(new Option("-h, --help").hideHelp())
@ -59,19 +59,19 @@ program
program
.command("new")
.argument("[branch]", "branch name or prompt (if it contains spaces)")
.argument("[prompt]", "initial prompt for Pi")
.option("-p, --print <prompt>", "run Pi in non-interactive mode with -p")
.option("-n, --no-save", "skip auto-save after Pi exits")
.description("Create a new session and launch Pi")
.argument("[prompt]", "initial prompt for Claude")
.option("-p, --print <prompt>", "run Claude in non-interactive mode with -p")
.option("-n, --no-save", "skip auto-save after Claude exits")
.description("Create a new session and launch Claude")
.action(newAction)
program
.command("open")
.argument("<branch>", "branch name")
.argument("[prompt]", "initial prompt for Pi")
.option("-p, --print <prompt>", "run Pi in non-interactive mode with -p")
.option("-n, --no-save", "skip auto-save after Pi exits")
.description("Open an existing Pi session")
.argument("[prompt]", "initial prompt for Claude")
.option("-p, --print <prompt>", "run Claude in non-interactive mode with -p")
.option("-n, --no-save", "skip auto-save after Claude exits")
.description("Open an existing Claude session")
.action(openAction)
program

View File

@ -104,7 +104,7 @@ const SKIP_RESOLVE = new Set([
"yarn.lock",
])
/** Resolve conflict markers in files using Pi, then stage them. */
/** Resolve conflict markers in files using Claude, then stage them. */
export async function resolveConflicts(
files: string[],
cwd: string,
@ -124,13 +124,13 @@ export async function resolveConflicts(
throw new Error(`Failed to read conflicted file: ${file}`)
})
const resolved = await vm.piPipe(
const resolved = await vm.claudePipe(
content,
"resolve this merge conflict. output ONLY the resolved file content with no markdown fences, no explanation, no surrounding text.",
)
if (resolved.exitCode !== 0 || !resolved.stdout.trim()) {
throw new Error(`Pi failed to resolve ${file}: ${resolved.stderr.trim() || "(no output)"}`)
throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.trim() || "(no output)"}`)
}
await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n")
@ -162,7 +162,7 @@ export async function mergeAndClose(branch: string, opts?: { force?: boolean }):
return
}
// Resolve conflicts with Pi
// Resolve conflicts with Claude
spin.text = `Resolving ${conflicts.length} conflict(s)`
try {
@ -210,7 +210,7 @@ export async function saveChanges(worktree: string, branch: string, message?: st
spin.text = "Generating commit message"
const diff = await $`git -C ${worktree} diff --staged`.nothrow().quiet().text()
const gen = await vm.piPipe(
const gen = await vm.claudePipe(
diff,
"Write a commit message for this diff. Subject line: imperative mood, max 72 characters, no period. If the changes warrant it, add a blank line then a brief body explaining why, not what. Output only the raw commit message, no quotes or markdown.",
)

View File

@ -44,7 +44,7 @@ async function resolveStatus(
): Promise<string> {
try { await stat(s.worktree) } catch { return "idle" }
if (vmRunning) {
const active = await vm.isPiActive(s.worktree, s.branch).catch(() => false)
const active = await vm.isClaudeActive(s.worktree, s.branch).catch(() => false)
if (active && s.in_review) return "review"
if (active) return "active"
}
@ -58,7 +58,7 @@ async function resolveStatus(
}
}
/** Clear in_review flags for sessions where Pi is no longer active. */
/** Clear in_review flags for sessions where Claude is no longer active. */
async function clearStaleReviews(
sessions: state.GlobalSession[],
statusMap: Map<state.GlobalSession, string>,
@ -79,7 +79,7 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[]
if (!vmRunning) return
const needsPrompt = sessions.filter(s => !s.prompt)
if (needsPrompt.length === 0) return
const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.pi/history.jsonl 2>/dev/null").catch(() => null)
const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null").catch(() => null)
if (!result || result.exitCode !== 0 || !result.stdout) return
const byProject = new Map<string, string>()

View File

@ -126,7 +126,7 @@ export async function action(
if (opts.print) {
spin.text = "Running prompt…"
const result = await vm.pi(worktreeAbs, { prompt, print: opts.print })
const result = await vm.claude(worktreeAbs, { prompt, print: opts.print })
if (result.output) {
spin.stop()
process.stdout.write(renderMarkdown(result.output) + "\n")
@ -134,7 +134,7 @@ export async function action(
spin.succeed("Done")
}
} else {
await vm.pi(worktreeAbs, { prompt, print: opts.print })
await vm.claude(worktreeAbs, { prompt, print: opts.print })
}
await vm.clearActivity(worktreeAbs, branch)

View File

@ -21,7 +21,7 @@ export async function action(
if (opts.print) {
spin.text = "Running prompt…"
const result = await vm.pi(session.worktree, { prompt, print: opts.print, continue: true })
const result = await vm.claude(session.worktree, { prompt, print: opts.print, continue: true })
if (result.output) {
spin.stop()
process.stdout.write(renderMarkdown(result.output) + "\n")
@ -30,7 +30,7 @@ export async function action(
}
} else {
spin.succeed("Session ready")
await vm.pi(session.worktree, { prompt, print: opts.print, continue: true })
await vm.claude(session.worktree, { prompt, print: opts.print, continue: true })
}
await vm.clearActivity(session.worktree, branch)

View File

@ -34,7 +34,7 @@ export async function action(branch: string) {
}
fetchSpin.stop()
console.log(`◆ Rebase conflicts in ${conflicts.length} file(s). Resolving with Pi...`)
console.log(`◆ Rebase conflicts in ${conflicts.length} file(s). Resolving with Claude...`)
const resolveSpin = spinner("Starting container", branch)
try {

View File

@ -74,11 +74,11 @@ Your thoughts, in brief.
try {
if (opts.print) {
spin.text = "Running review…"
const result = await vm.pi(session.worktree, { print: prompt })
const result = await vm.claude(session.worktree, { print: prompt })
if (result.output) process.stdout.write(result.output + "\n")
} else {
spin.succeed("Session ready")
await vm.pi(session.worktree, { prompt })
await vm.claude(session.worktree, { prompt })
}
} finally {
spin.stop()

View File

@ -40,7 +40,7 @@ export async function action(branch: string) {
process.exit(1)
}
const gen = await vm.piPipe(
const gen = await vm.claudePipe(
diff,
"Write a commit message summarizing all changes. Subject line: imperative mood, max 72 characters, no period. Add a blank line then a concise body with the key changes as bullet points. Output only the raw commit message, no quotes or markdown.",
)

View File

@ -70,7 +70,7 @@ export function register(program: Command) {
sessions.map(async (sess): Promise<[string, string]> => {
const key = `${basename(sess.repoRoot)}/${sess.branch}`
try {
if (await vm.isPiActive(sess.worktree, sess.branch)) return [key, "active"]
if (await vm.isClaudeActive(sess.worktree, sess.branch)) return [key, "active"]
if (await git.isDirty(sess.worktree)) return [key, "dirty"]
if (await git.hasNewCommits(sess.worktree)) return [key, "saved"]
} catch {}

View File

@ -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 PI_BIN = `/home/${USER}/.local/bin/pi`
const CLAUDE_BIN = `/home/${USER}/.local/bin/claude`
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<boolean> {
const files = ['bun', 'pi', 'neofetch', 'nvim.tar.gz']
const files = ['bun', 'claude', '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, Pi, neofetch, and Neovim using cached binaries when available. */
/** Install Bun, Claude Code, neofetch, and Neovim using cached binaries when available. */
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)
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/pi /sandlot/.cache/neofetch ~/.local/bin/ && chmod +x ~/.local/bin/bun ~/.local/bin/pi ~/.local/bin/neofetch && ln -sf bun ~/.local/bin/bunx"}`,
$`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"}`,
"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 Pi")
log?.("Installing Claude Code")
await run(
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://pi.ai/install.sh | bash"}`,
"Pi installation")
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://claude.ai/install.sh | bash"}`,
"Claude Code 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/pi ~/.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/claude ~/.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 Pi settings. */
/** Configure git identity, API key helper, activity hook, and Claude settings. */
async function configureEnvironment(home: string, apiKey: string): Promise<void> {
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<void>
PreToolUse: [{ hooks: [{ type: "command", command: `${activityBin} active` }] }],
}
const statusLine = { type: "command", command: `/home/${USER}/.local/bin/sandlot-statusline` }
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 } } })
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 } } })
// 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 ~/.pi && cp /sandlot/.api-key-helper.tmp ~/.pi/api-key-helper.sh"}`.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 Bun.file(tmp).unlink()
await installScript(home, "sandlot-activity", `#!/bin/bash\nP="\${PI_PROJECT_DIR%/}"\necho "$1" > "$(dirname "$P")/.activity-$(basename "$P")"\n`)
await installScript(home, "sandlot-activity", `#!/bin/bash\nP="\${CLAUDE_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 ~/.pi
echo '${settingsJson}' > ~/.pi/settings.json
echo '${piJson}' > ~/.pi.json
mkdir -p ~/.claude
echo '${settingsJson}' > ~/.claude/settings.json
echo '${claudeJson}' > ~/.claude.json
`}`.quiet()
}
@ -339,8 +339,8 @@ export async function status(): Promise<"running" | "stopped" | "missing"> {
}
}
/** 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 }> {
/** 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 }> {
const cwd = containerPath(workdir)
const mounts = hostMounts(homedir())
const systemPromptLines = [
@ -361,7 +361,7 @@ export async function pi(workdir: string, opts?: { prompt?: string; print?: stri
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, PI_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, CLAUDE_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 pi(workdir: string, opts?: { prompt?: string; print?: stri
const exitCode = await proc.exited
if (exitCode !== 0 && opts?.continue) {
info("Retrying without --continue")
return pi(workdir, { ...opts, continue: false })
return claude(workdir, { ...opts, continue: false })
}
return { exitCode, output }
}
@ -381,7 +381,7 @@ export async function pi(workdir: string, opts?: { prompt?: string; print?: stri
const exitCode = await proc.exited
if (exitCode !== 0 && opts?.continue) {
info("Retrying without --continue")
return pi(workdir, { ...opts, continue: false })
return claude(workdir, { ...opts, continue: false })
}
return { exitCode }
}
@ -416,23 +416,23 @@ export async function exec(workdir: string, command: string): Promise<{ exitCode
}
}
/** 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()}`
/** 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()}`
const tmpPath = join(homedir(), '.sandlot', tmpName)
try {
await Bun.write(tmpPath, input)
return await exec(
join(homedir(), '.sandlot'),
`cat /sandlot/${tmpName} | pi --model claude-opus-4-6 --effort max -p "${prompt.replace(/"/g, '\\"')}"`,
`cat /sandlot/${tmpName} | claude --model claude-opus-4-6 --effort max -p "${prompt.replace(/"/g, '\\"')}"`,
)
} finally {
await Bun.file(tmpPath).unlink().catch(() => {})
}
}
/** Check if Pi is actively working in the given worktree (based on activity hook). */
export async function isPiActive(worktree: string, branch: string): Promise<boolean> {
/** Check if Claude is actively working in the given worktree (based on activity hook). */
export async function isClaudeActive(worktree: string, branch: string): Promise<boolean> {
const file = `${dirname(worktree)}/.activity-${branch}`
try {
const content = await Bun.file(file).text()