Skip to content
Open
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
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# CLAUDE.md

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

## Project Overview

OpenCode is an open-source AI-powered coding agent, similar to Claude Code but provider-agnostic. It supports multiple LLM providers (Anthropic, OpenAI, Google, Azure, local models) and features a TUI built with SolidJS, LSP support, and client/server architecture.

## Development Commands

```bash
# Install and run development server
bun install
bun dev # Run in packages/opencode directory
bun dev <directory> # Run against a specific directory
bun dev . # Run against repo root

# Type checking
bun run typecheck # Single package
bun turbo typecheck # All packages

# Testing (per-package, not from root)
cd packages/opencode && bun test

# Build standalone executable
./packages/opencode/script/build.ts --single
# Output: ./packages/opencode/dist/opencode-<platform>/bin/opencode

# Regenerate SDK after API changes
./script/generate.ts
# Or for JS SDK specifically:
./packages/sdk/js/script/build.ts

# Web app development
bun run --cwd packages/app dev # http://localhost:5173

# Desktop app (requires Tauri/Rust)
bun run --cwd packages/desktop tauri dev # Native + web server
bun run --cwd packages/desktop dev # Web only (port 1420)
bun run --cwd packages/desktop tauri build # Production build
```

## Architecture

**Monorepo Structure** (Bun workspaces + Turbo):

| Package | Purpose |
|---------|---------|
| `packages/opencode` | Core CLI, server, business logic |
| `packages/app` | Shared web UI components (SolidJS + Vite) |
| `packages/desktop` | Native desktop app (Tauri wrapper) |
| `packages/ui` | Shared component library (Kobalte + Tailwind) |
| `packages/console/app` | Console dashboard (Solid Start) |
| `packages/console/core` | Backend services (Hono + DrizzleORM) |
| `packages/sdk/js` | JavaScript SDK |
| `packages/plugin` | Plugin system API |

**Key Directories in `packages/opencode/src`**:
- `cli/cmd/tui/` - Terminal UI (SolidJS + opentui)
- `agent/` - Agent logic and state
- `provider/` - AI provider implementations
- `server/` - Server mode
- `mcp/` - Model Context Protocol integration
- `lsp/` - Language Server Protocol support

**Default branch**: `dev`

## Code Style

- Keep logic in single functions unless reusable
- Avoid destructuring: use `obj.a` instead of `const { a } = obj`
- Avoid `try/catch` - prefer `.catch()`
- Avoid `else` statements
- Avoid `any` type
- Avoid `let` - use immutable patterns
- Prefer single-word variable names when descriptive
- Use Bun APIs (e.g., `Bun.file()`) when applicable

## Built-in Agents

- **build** - Default agent with full access for development
- **plan** - Read-only agent for analysis (denies edits, asks before bash)
- **general** - Subagent for complex tasks, invoked with `@general`

Switch agents with `Tab` key in TUI.

## Debugging

```bash
# Debug with inspector
bun run --inspect=ws://localhost:6499/ dev

# Debug server separately
bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096
opencode attach http://localhost:4096

# Debug TUI
bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts

# Use spawn for breakpoints in server code
bun dev spawn
```

Use `--inspect-wait` or `--inspect-brk` for different breakpoint behaviors.

## PR Guidelines

- All PRs must reference an existing issue (`Fixes #123`)
- UI/core feature changes require design review with core team
- PR titles follow conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`
- Optional scope: `feat(app):`, `fix(desktop):`
- Include screenshots/videos for UI changes
- Explain verification steps for logic changes
72 changes: 13 additions & 59 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, streamObject, type ModelMessage } from "ai"
import { generateObject, type ModelMessage } from "ai"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { Truncate } from "../tool/truncation"
import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform"

import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
Expand All @@ -15,8 +13,6 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { PermissionNext } from "@/permission/next"
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"

export namespace Agent {
export const Info = z
Expand All @@ -39,6 +35,7 @@ export namespace Agent {
prompt: z.string().optional(),
options: z.record(z.string(), z.any()),
steps: z.number().int().positive().optional(),
task_budget: z.number().int().nonnegative().optional(),
})
.meta({
ref: "Agent",
Expand All @@ -54,16 +51,13 @@ export namespace Agent {
external_directory: {
"*": "ask",
[Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
},
question: "deny",
plan_enter: "deny",
plan_exit: "deny",
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
"*": "allow",
"*.env": "ask",
"*.env.*": "ask",
"*.env": "deny",
"*.env.*": "deny",
"*.env.example": "allow",
},
})
Expand All @@ -77,7 +71,6 @@ export namespace Agent {
defaults,
PermissionNext.fromConfig({
question: "allow",
plan_enter: "allow",
}),
user,
),
Expand All @@ -91,14 +84,9 @@ export namespace Agent {
defaults,
PermissionNext.fromConfig({
question: "allow",
plan_exit: "allow",
external_directory: {
[path.join(Global.Path.data, "plans", "*")]: "allow",
},
edit: {
"*": "deny",
[path.join(".opencode", "plans", "*.md")]: "allow",
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
".opencode/plan/*.md": "allow",
},
}),
user,
Expand Down Expand Up @@ -137,7 +125,6 @@ export namespace Agent {
read: "allow",
external_directory: {
[Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
},
}),
user,
Expand Down Expand Up @@ -220,23 +207,22 @@ export namespace Agent {
item.hidden = value.hidden ?? item.hidden
item.name = value.name ?? item.name
item.steps = value.steps ?? item.steps
item.task_budget = value.task_budget ?? item.task_budget
item.options = mergeDeep(item.options, value.options ?? {})
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
}

// Ensure Truncate.DIR is allowed unless explicitly configured
for (const name in result) {
const agent = result[name]
const explicit = agent.permission.some((r) => {
if (r.permission !== "external_directory") return false
if (r.action !== "deny") return false
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
})
const explicit = agent.permission.some(
(r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny",
)
if (explicit) continue

result[name].permission = PermissionNext.merge(
result[name].permission,
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }),
)
}

Expand All @@ -257,33 +243,18 @@ export namespace Agent {
}

export async function defaultAgent() {
const cfg = await Config.get()
const agents = await state()

if (cfg.default_agent) {
const agent = agents[cfg.default_agent]
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${cfg.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${cfg.default_agent}" is hidden`)
return agent.name
}

const primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
if (!primaryVisible) throw new Error("no primary visible agent found")
return primaryVisible.name
return state().then((x) => Object.keys(x)[0])
}

export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
const cfg = await Config.get()
const defaultModel = input.model ?? (await Provider.defaultModel())
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const language = await Provider.getLanguage(model)

const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE)
const existing = await list()

const params = {
const result = await generateObject({
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
Expand All @@ -309,24 +280,7 @@ export namespace Agent {
whenToUse: z.string(),
systemPrompt: z.string(),
}),
} satisfies Parameters<typeof generateObject>[0]

if (defaultModel.providerID === "openai" && (await Auth.get(defaultModel.providerID))?.type === "oauth") {
const result = streamObject({
...params,
providerOptions: ProviderTransform.providerOptions(model, {
instructions: SystemPrompt.instructions(),
store: false,
}),
onError: () => {},
})
for await (const part of result.fullStream) {
if (part.type === "error") throw part.error
}
return result.object
}

const result = await generateObject(params)
})
return result.object
}
}
Loading