Skip to content
Closed
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
191 changes: 116 additions & 75 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,103 +1,144 @@
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
# OpenCode AGENTS.md

## Style Guide
## Ground Rules

- Default branch is `dev`, not `main`.
- Package manager: Bun 1.3.13 (exact). Pre-push hook enforces version match.
- PRs require conventional commit titles (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`) + linked issue (`Fixes #N`). Docs/refactor/feat PRs skip the issue requirement.
- Use `--no-verify` only when CI codegen (`chore: generate`) or user explicitly requests it.
- Run `./script/generate.ts` after changing API routes or SDK surfaces; CI auto-generates on push to `dev`.

### General Principles
## Monorepo Map

- Keep things in one function unless composable or reusable
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Use Bun APIs when possible, like `Bun.file()`
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
```
packages/opencode Core server, CLI, TUI (SolidJS + opentui)
packages/app Web UI (SolidJS, Vite)
packages/desktop Tauri desktop app (wraps packages/app)
packages/desktop-electron Electron desktop app
packages/ui Shared UI components
packages/plugin @opencode-ai/plugin
packages/sdk SDK (JS)
packages/console Console app
packages/function Backend functions
packages/identity Auth/identity
packages/enterprise Enterprise features
```

Reduce total variable count by inlining when a value is only used once.
Dependencies: `@opencode-ai/sdk`, `@opencode-ai/ui`, `@opencode-ai/shared`, `@opencode-ai/plugin` are `workspace:*`.

```ts
// Good
const journal = await Bun.file(path.join(dir, "journal.json")).json()
## Dev Commands

// Bad
const journalPath = path.join(dir, "journal.json")
const journal = await Bun.file(journalPath).json()
```bash
bun install # Install deps (exact versions via bunfig.toml)
bun dev # Run TUI against packages/opencode dir
bun dev <directory> # Run TUI against different dir
bun dev serve # Headless API server (port 4096)
bun dev web # Server + proxy web UI (hits production app.opencode.ai)
bun lint # oxlint (type-aware)
bun typecheck # turbo typecheck across all packages
```

### Destructuring
For local UI dev, run server + app separately:
```bash
# Server (from root or packages/opencode)
bun dev serve
# App (from packages/app)
bun dev -- --port 4444
# Open http://localhost:4444
```

Avoid unnecessary destructuring. Use dot notation to preserve context.
## Testing

```ts
// Good
obj.a
obj.b
**NEVER run tests from repo root.** Guard exists in `bunfig.toml` and `package.json`.

// Bad
const { a, b } = obj
```
```bash
# Unit tests
bun test # packages/opencode: bun test --timeout 30000
bun test:unit # packages/app: bun test --preload ./happydom.ts ./src
bun test:unit:watch # packages/app: watch mode

### Variables
# E2E (packages/app, uses Playwright chromium)
bun test:e2e:local # local e2e run

Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
# CI
bun turbo test:ci # runs all test:ci tasks
```

```ts
// Good
const foo = condition ? 1 : 2
Test fixtures (`packages/opencode/test/fixture/fixture.ts`):
- `tmpdir({ git, config, init, dispose })` – creates temp dir, auto-cleans via `await using`
- `testEffect(layers)` – for Effect-based tests; use `it.live()` for real I/O, `it.effect()` for simulated clock
- `provideTmpdirInstance(cb)` – creates temp dir, binds as active Instance, runs Effect, cleans up

// Bad
let foo
if (condition) foo = 1
else foo = 2
## Type Checking

```bash
bun typecheck # from root: turbo typecheck (all packages)
bun typecheck # from packages/opencode: tsgo --noEmit
bun typecheck # from packages/app: tsgo -b
```

### Control Flow
Never use `tsc` directly. Each package uses `tsgo` (TypeScript native preview) or `--noEmit`.

## Style Guide

Avoid `else` statements. Prefer early returns.
### General
- Keep logic in one function unless composable/reusable.
- Avoid `try`/`catch`; prefer `.catch(...)`.
- Avoid `any`; prefer precise types.
- Use Bun APIs when available (`Bun.file()`, `Bun.write()`, etc.).
- Prefer `const` over `let`; ternaries over reassignment; early returns over `else`.
- Avoid unnecessary destructuring — use dot notation to preserve context.
- Inline variables used only once.

```ts
// Good
function foo() {
if (condition) return 1
return 2
}
### Effect Framework (packages/opencode)
- Use `Effect.gen(function* () { ... })` for composition.
- Use `Effect.fn("Domain.method")` for named/traced effects, `Effect.fnUntraced` for internal.
- No `export namespace Foo {}` — use flat top-level exports + `export * as Foo from "."` at bottom.
- Multi-sibling directories (e.g., `src/session/`, `src/config/`): no barrel `index.ts` — import specific files.
- Use `makeRuntime` for services, `InstanceState` (via `ScopedCache`) for per-directory state.
- `Effect.forkIn(scope)` not `Effect.fork`/`Effect.forkDaemon` (Effect v4 beta).
- Prefer Effect services (`FileSystem`, `HttpClient`, `ChildProcessSpawner`, `Path`, `Clock`) over raw platform APIs.

// Bad
function foo() {
if (condition) return 1
else return 2
}
```
### Database
- Drizzle schema in `src/**/*.sql.ts`. Snake_case tables/columns.
- Migrations: `bun run db generate --name <slug>` (from `packages/opencode`).
- Output: `migration/<timestamp>_<slug>/migration.sql`.

### Schema Definitions (Drizzle)
### SolidJS (packages/app, packages/opencode TUI)
- Prefer `createStore` over multiple `createSignal` calls.

Use snake_case for field names so column names don't need to be redefined as strings.
### Desktop Packages
- **Tauri** (`packages/desktop`): Never call `invoke` manually; use generated bindings from `src/bindings.ts`.
- **Electron** (`packages/desktop-electron`): Renderer calls `window.api` only; main process registers IPC in `src/main/ipc.ts`.

```ts
// Good
const table = sqliteTable("session", {
id: text().primaryKey(),
project_id: text().notNull(),
created_at: integer().notNull(),
})
## Pre-push Hook

// Bad
const table = sqliteTable("session", {
id: text("id").primaryKey(),
projectID: text("project_id").notNull(),
createdAt: integer("created_at").notNull(),
})
```
Runs `bun typecheck`. If it fails, push is rejected. Fix type errors locally before pushing.

## Testing
Lint is NOT in the hook — run `bun lint` manually or let CI catch it.

- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
## CI (GitHub Actions)

## Type Checking
- **test**: Unit (linux + windows, `bun turbo test:ci`) + E2E (Playwright chromium in packages/app)
- **typecheck**: `bun typecheck`
- **generate**: Runs `./script/generate.ts` on push to dev, commits result
- **pr-standards**: Enforces conventional commit titles and linked issues for PRs
- **publish**: Full build pipeline (CLI + Tauri + Electron + npm + Docker)

## Code Generation

After changing API routes, server endpoints, or SDK types:
```bash
./script/generate.ts
```
This regenerates the JS SDK and related files. CI auto-runs this on push to dev.

## Debugging

```bash
bun dev spawn # Debug-friendly TUI (server in separate process)
bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096
opencode attach http://localhost:4096 # Attach TUI to debugged server
```

- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
For app debugging: NEVER restart the app or server process from within the debugging session.
2 changes: 1 addition & 1 deletion infra/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const api = new sst.cloudflare.Worker("Api", {
args.migrations = {
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
newTag: "v2",
//newSqliteClasses: ["SyncServer"],
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/script/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bun

import { z } from "zod"
import { Config } from "../src/config"
import { Config } from "../src/config/config"
import { TuiConfig } from "../src/cli/cmd/tui/config/tui"

function generate(schema: z.ZodType) {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { Agent as AgentModule } from "../agent/agent"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config"
import { Config } from "@/config/config"
import { ConfigMCP } from "@/config/mcp"
import { Todo } from "@/session/todo"
import { Result, Schema } from "effect"
Expand Down
3 changes: 0 additions & 3 deletions packages/opencode/src/acp/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export class ACPSessionManager {
setModel(sessionId: string, model: ACPSessionState["model"]) {
const session = this.get(sessionId)
session.model = model
this.sessions.set(sessionId, session)
return session
}

Expand All @@ -103,14 +102,12 @@ export class ACPSessionManager {
setVariant(sessionId: string, variant?: string) {
const session = this.get(sessionId)
session.variant = variant
this.sessions.set(sessionId, session)
return session
}

setMode(sessionId: string, modeId: string) {
const session = this.get(sessionId)
session.modeId = modeId
this.sessions.set(sessionId, session)
return session
}
}
2 changes: 1 addition & 1 deletion packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Config } from "../config"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider"
import { ModelID, ProviderID } from "../provider/schema"
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { zod } from "@/util/effect-zod"
import { Global } from "../global"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Log } from "@/util"

export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"

Expand Down Expand Up @@ -47,6 +48,8 @@ export interface Interface {
readonly remove: (key: string) => Effect.Effect<void, AuthError>
}

const log = Log.create({ service: "auth" })

export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}

export const layer = Layer.effect(
Expand All @@ -59,7 +62,9 @@ export const layer = Layer.effect(
if (process.env.OPENCODE_AUTH_CONTENT) {
try {
return JSON.parse(process.env.OPENCODE_AUTH_CONTENT)
} catch (err) {}
} catch (err) {
log.warn("failed to parse OPENCODE_AUTH_CONTENT", { error: err })
}
}

const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record<string, unknown>
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/debug/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider"
import { Session } from "../../../session"
import { Session } from "../../../session/session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
import { ToolRegistry } from "../../../tool"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/debug/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Config } from "../../../config"
import { Config } from "../../../config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/export.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Argv } from "yargs"
import { Session } from "../../session"
import { Session } from "../../session/session"
import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema"
import { cmd } from "./cmd"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ModelsDev } from "../../provider"
import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share"
import { Session } from "../../session"
import { Session } from "../../session/session"
import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema"
import { Provider } from "../../provider"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/import.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { Session } from "../../session"
import { Session } from "../../session/session"
import { MessageV2 } from "../../session/message-v2"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UI } from "../ui"
import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "../../config"
import { Config } from "../../config/config"
import { ConfigMCP } from "../../config/mcp"
import { Instance } from "../../project/instance"
import { Installation } from "../../installation"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/plug.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { intro, log, outro, spinner } from "@clack/prompts"
import type { Argv } from "yargs"

import { ConfigPaths } from "../../config"
import { ConfigPaths } from "../../config/paths"
import { Global } from "../../global"
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
import { resolvePluginTarget } from "../../plugin/shared"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ModelsDev } from "../../provider"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Config } from "../../config"
import { Config } from "../../config/config"
import { Global } from "../../global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
import { Session } from "../../session/session"
import { SessionID } from "../../session/schema"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/stats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
import { Session } from "../../session/session"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage"
import { SessionTable } from "../../session/session.sql"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { DialogAlert } from "./ui/dialog-alert"
import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session"
import { Session as SessionApi } from "@/session/session"
import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv"
import { Provider } from "@/provider"
Expand Down
Loading
Loading