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

View File

@ -104,7 +104,7 @@ const SKIP_RESOLVE = new Set([
"yarn.lock", "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( export async function resolveConflicts(
files: string[], files: string[],
cwd: string, cwd: string,
@ -124,13 +124,13 @@ export async function resolveConflicts(
throw new Error(`Failed to read conflicted file: ${file}`) throw new Error(`Failed to read conflicted file: ${file}`)
}) })
const resolved = await vm.piPipe( const resolved = await vm.claudePipe(
content, content,
"resolve this merge conflict. output ONLY the resolved file content with no markdown fences, no explanation, no surrounding text.", "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()) { 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") await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n")
@ -162,7 +162,7 @@ export async function mergeAndClose(branch: string, opts?: { force?: boolean }):
return return
} }
// Resolve conflicts with Pi // Resolve conflicts with Claude
spin.text = `Resolving ${conflicts.length} conflict(s)` spin.text = `Resolving ${conflicts.length} conflict(s)`
try { try {
@ -210,7 +210,7 @@ export async function saveChanges(worktree: string, branch: string, message?: st
spin.text = "Generating commit message" spin.text = "Generating commit message"
const diff = await $`git -C ${worktree} diff --staged`.nothrow().quiet().text() const diff = await $`git -C ${worktree} diff --staged`.nothrow().quiet().text()
const gen = await vm.piPipe( const gen = await vm.claudePipe(
diff, 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.", "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> { ): Promise<string> {
try { await stat(s.worktree) } catch { return "idle" } try { await stat(s.worktree) } catch { return "idle" }
if (vmRunning) { 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 && s.in_review) return "review"
if (active) return "active" 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( async function clearStaleReviews(
sessions: state.GlobalSession[], sessions: state.GlobalSession[],
statusMap: Map<state.GlobalSession, string>, statusMap: Map<state.GlobalSession, string>,
@ -79,7 +79,7 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[]
if (!vmRunning) return if (!vmRunning) return
const needsPrompt = sessions.filter(s => !s.prompt) const needsPrompt = sessions.filter(s => !s.prompt)
if (needsPrompt.length === 0) return 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 if (!result || result.exitCode !== 0 || !result.stdout) return
const byProject = new Map<string, string>() const byProject = new Map<string, string>()

View File

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

View File

@ -21,7 +21,7 @@ export async function action(
if (opts.print) { if (opts.print) {
spin.text = "Running prompt…" 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) { if (result.output) {
spin.stop() spin.stop()
process.stdout.write(renderMarkdown(result.output) + "\n") process.stdout.write(renderMarkdown(result.output) + "\n")
@ -30,7 +30,7 @@ export async function action(
} }
} else { } else {
spin.succeed("Session ready") 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) await vm.clearActivity(session.worktree, branch)

View File

@ -34,7 +34,7 @@ export async function action(branch: string) {
} }
fetchSpin.stop() 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) const resolveSpin = spinner("Starting container", branch)
try { try {

View File

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

View File

@ -40,7 +40,7 @@ export async function action(branch: string) {
process.exit(1) process.exit(1)
} }
const gen = await vm.piPipe( const gen = await vm.claudePipe(
diff, 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.", "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]> => { sessions.map(async (sess): Promise<[string, string]> => {
const key = `${basename(sess.repoRoot)}/${sess.branch}` const key = `${basename(sess.repoRoot)}/${sess.branch}`
try { 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.isDirty(sess.worktree)) return [key, "dirty"]
if (await git.hasNewCommits(sess.worktree)) return [key, "saved"] if (await git.hasNewCommits(sess.worktree)) return [key, "saved"]
} catch {} } catch {}

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 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_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', '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())) 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, 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> { 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/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") "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 Pi") log?.("Installing Claude Code")
await run( await run(
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://pi.ai/install.sh | bash"}`, $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://claude.ai/install.sh | bash"}`,
"Pi installation") "Claude Code 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/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) 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 Pi settings. */ /** Configure git identity, API key helper, activity hook, and Claude 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: "~/.pi/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks, statusLine }) const settingsJson = JSON.stringify({ apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks, statusLine })
const piJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true, projects: { "/": { hasTrustDialogAccepted: true } } }) 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 // 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 ~/.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 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 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 ~/.pi mkdir -p ~/.claude
echo '${settingsJson}' > ~/.pi/settings.json echo '${settingsJson}' > ~/.claude/settings.json
echo '${piJson}' > ~/.pi.json echo '${claudeJson}' > ~/.claude.json
`}`.quiet() `}`.quiet()
} }
@ -339,8 +339,8 @@ export async function status(): Promise<"running" | "stopped" | "missing"> {
} }
} }
/** Launch pi in the container at the given workdir. */ /** Launch claude 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 }> { export async function claude(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 pi(workdir: string, opts?: { prompt?: string; print?: stri
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, 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?.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 pi(workdir: string, opts?: { prompt?: string; print?: stri
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 pi(workdir, { ...opts, continue: false }) return claude(workdir, { ...opts, continue: false })
} }
return { exitCode, output } return { exitCode, output }
} }
@ -381,7 +381,7 @@ export async function pi(workdir: string, opts?: { prompt?: string; print?: stri
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 pi(workdir, { ...opts, continue: false }) return claude(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 Pi in the container with a prompt, returning the output. */ /** Pipe input text to Claude in the container with a prompt, returning the output. */
export async function piPipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { export async function claudePipe(input: string, prompt: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const tmpName = `.pi-pipe-${crypto.randomUUID()}` const tmpName = `.claude-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} | 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 { } finally {
await Bun.file(tmpPath).unlink().catch(() => {}) await Bun.file(tmpPath).unlink().catch(() => {})
} }
} }
/** Check if Pi is actively working in the given worktree (based on activity hook). */ /** Check if Claude is actively working in the given worktree (based on activity hook). */
export async function isPiActive(worktree: string, branch: string): Promise<boolean> { export async function isClaudeActive(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()