Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Git Workflow

**Always commit and push changes after completing a task.** Follow these rules:

1. After making code changes, always commit with a descriptive message
2. Push commits to the current feature branch
3. **NEVER push directly to `main`** - always use feature branches and PRs
4. Before pushing, verify the current branch is not `main`
5. **Open PRs against the `main` branch**
6. After pushing, check if a PR exists for the branch. If not, create one with `gh pr create --base main`
7. **After creating a PR, always wait for explicit user approval before merging.** Never merge PRs autonomously.

### Starting a New Task

Checkout main, pull latest, and create your feature branch from there:

```bash
git checkout main && git pull origin main && git checkout -b <branch-name>
```

## Build Commands

```bash
pnpm install # Install dependencies
pnpm dev # Start dev server
pnpm build # Fetch agent data + production build
pnpm lint # Run ESLint
```

## Architecture

- **Next.js 16** with App Router, React 19
- `app/` - Pages and API routes
- `app/api/agent/` - AI agent endpoint (Claude Haiku 4.5 via ToolLoopAgent)
- `app/api/fs/` - File serving endpoint
- `app/components/` - Terminal UI components
- `app/components/lite-terminal/` - Custom terminal emulator with ANSI support
- `app/components/terminal-parts/` - Terminal commands, input handling, markdown
- `lib/` - Core business logic:
- `lib/agent/` - AI agent configuration (system instructions, response handling)
- `lib/recoup-api/` - Recoup-API integration (sandbox creation, snapshot persistence)
- `lib/sandbox/` - Vercel Sandbox management (create, restore, snapshot)

## Key Technologies

- **AI**: Vercel AI SDK (`ai` package), ToolLoopAgent with Claude Haiku 4.5
- **Terminal**: `just-bash` (TypeScript bash interpreter), custom `LiteTerminal` emulator
- **Sandbox**: `@vercel/sandbox` for isolated execution environments
- **Auth**: Privy (`@privy-io/react-auth`)
- **Styling**: Tailwind CSS 4, Geist design system

## Code Principles

- **SRP (Single Responsibility Principle)**: One exported function per file
- **DRY (Don't Repeat Yourself)**: Extract shared logic into reusable utilities
- **KISS (Keep It Simple)**: Prefer simple solutions over clever ones
- **YAGNI**: Don't build for hypothetical future needs
- **File Organization**: Domain-specific directories (e.g., `lib/sandbox/`, `lib/recoup-api/`)

## Environment Variables

- `NEXT_PUBLIC_PRIVY_APP_ID` - Privy authentication
- `NEXT_PUBLIC_VERCEL_ENV` - Environment detection (`production` vs other) for API URL routing
2 changes: 1 addition & 1 deletion app/api/agent/new/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFreshSandbox } from "@/lib/sandbox/createFreshSandbox";
import { handleAgentRequest } from "@/lib/agent/createAgentResponse";
import { handleAgentRequest } from "@/lib/agent/handleAgentRequest";
import { AGENT_DATA_DIR } from "@/lib/agent/constants";

export async function POST(req: Request) {
Expand Down
2 changes: 1 addition & 1 deletion app/api/agent/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSnapshotSandbox } from "@/lib/sandbox/createSnapshotSandbox";
import { handleAgentRequest } from "@/lib/agent/createAgentResponse";
import { handleAgentRequest } from "@/lib/agent/handleAgentRequest";
import { AGENT_DATA_DIR } from "@/lib/agent/constants";

export async function POST(req: Request) {
Expand Down
48 changes: 20 additions & 28 deletions lib/agent/createAgentResponse.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai";
import { createBashTool } from "bash-tool";
import { Sandbox } from "@vercel/sandbox";
import { after } from "next/server";
import { SANDBOX_CWD, SYSTEM_INSTRUCTIONS, TOOL_PROMPT } from "./constants";
import { saveSnapshot } from "@/lib/sandbox/saveSnapshot";

type CreateSandbox = (bearerToken: string) => Promise<Sandbox>;

export async function handleAgentRequest(
req: Request,
createSandbox: CreateSandbox,
): Promise<Response> {
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const bearerToken = authHeader.slice("Bearer ".length);

const { messages } = await req.json();
const lastUserMessage = messages
.filter((m: { role: string }) => m.role === "user")
.pop();
console.log("Prompt:", lastUserMessage?.parts?.[0]?.text);

const sandbox = await createSandbox(bearerToken);

return createAgentResponse(sandbox, messages);
}

async function createAgentResponse(
export async function createAgentResponse(
sandbox: Sandbox,
messages: unknown[],
bearerToken: string,
): Promise<Response> {
try {
const bashToolkit = await createBashTool({
Expand Down Expand Up @@ -58,19 +37,32 @@ async function createAgentResponse(
const body = response.body;
if (body) {
const transform = new TransformStream();
body.pipeTo(transform.writable).finally(() => {
const pipePromise = body.pipeTo(transform.writable);

// Use after() so Vercel keeps the function alive until
// the snapshot save completes after streaming ends.
after(async () => {
await pipePromise.catch(() => {});
await saveSnapshot(sandbox, bearerToken);
sandbox.stop().catch(() => {});
});

return new Response(transform.readable, {
headers: response.headers,
status: response.status,
});
}

sandbox.stop().catch(() => {});
after(async () => {
await saveSnapshot(sandbox, bearerToken);
sandbox.stop().catch(() => {});
});
return response;
} catch (error) {
sandbox.stop().catch(() => {});
after(async () => {
await saveSnapshot(sandbox, bearerToken);
sandbox.stop().catch(() => {});
});
throw error;
}
}
26 changes: 26 additions & 0 deletions lib/agent/handleAgentRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Sandbox } from "@vercel/sandbox";
import { createAgentResponse } from "./createAgentResponse";

type CreateSandbox = (bearerToken: string) => Promise<Sandbox>;

export async function handleAgentRequest(
req: Request,
createSandbox: CreateSandbox,
): Promise<Response> {
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const bearerToken = authHeader.slice("Bearer ".length);

const { messages } = await req.json();
const lastUserMessage = messages
.filter((m: { role: string }) => m.role === "user")
.pop();
console.log("Prompt:", lastUserMessage?.parts?.[0]?.text);

const sandbox = await createSandbox(bearerToken);

return createAgentResponse(sandbox, messages, bearerToken);
}
28 changes: 28 additions & 0 deletions lib/recoup-api/updateAccountSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const RECOUP_API_URL = IS_PROD
? "https://recoup-api.vercel.app"
: "https://test-recoup-api.vercel.app";

export async function updateAccountSnapshot(
bearerToken: string,
snapshotId: string,
): Promise<void> {
try {
const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${bearerToken}`,
},
body: JSON.stringify({ snapshotId }),
signal: AbortSignal.timeout(10000),
});

if (!response.ok) {
const errorText = await response.text();
console.warn("Failed to update account snapshot:", response.status, errorText);
}
} catch (err) {
console.warn("Error updating account snapshot:", err);
}
}
14 changes: 14 additions & 0 deletions lib/sandbox/saveSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Sandbox } from "@vercel/sandbox";
import { updateAccountSnapshot } from "@/lib/recoup-api/updateAccountSnapshot";

export async function saveSnapshot(
sandbox: Sandbox,
bearerToken: string,
): Promise<void> {
try {
const result = await sandbox.snapshot();
await updateAccountSnapshot(bearerToken, result.snapshotId);
} catch (err) {
console.warn("Failed to save snapshot:", err);
}
}