opencode v2

# API map

A single `/api` route surface for simple clients and multi-directory frontends. The important design question is not route nesting; it is where runtime context comes from.

Server scoped Request context Session pinned
Everything has one canonical route. Some routes are server-scoped; runtime routes use context; session item routes use the session.

Server-scoped routes manage the whole server: projects, workspace lifecycle, and auth accounts. Runtime context is for anything resolved from an active directory, including config, provider capabilities, tools, files, and VCS.

## Context Model

### Request-context calls

These calls operate against a directory, optionally through a workspace. Simple clients omit context and use the default runtime.

    GET /api/fs/tree?path=.&directory=/repo/app&workspace=ws_123

### Session-pinned calls

These calls never take request context. The session is already pinned to the directory and workspace it was created in.

    POST /api/session/ses_123/prompt

    // server resolves
    sessionID -> { directory, workspaceID? }

## Operation Inventory

The SDK is the source of truth. HTTP routes are mounts for RPC-style operations. server operations do not use runtime context. request operations use request/default runtime context from `directory` and `workspace` query parameters. session operations use pinned session context and should not accept context input.

Operation | Input | Context | HTTP mount | Purpose
--- | --- | --- | --- | ---
`agent.list` | `{}` | request | `GET /api/agent` | Available agents.
`auth.activate` | `{ accountID: AccountID }` | server | `POST /api/auth/:accountID/activate` | Set the account as active for its service.
`auth.create` | `{ serviceID: ServiceID credential: | { type: "oauth", refresh: string, access: string, expires: number } | { type: "api", key: string, metadata?: Record<string, string> } description?: string active?: boolean }` | server | `POST /api/auth` | Create an auth account.
`auth.delete` | `{ accountID: AccountID }` | server | `DELETE /api/auth/:accountID` | Remove an auth account.
`auth.get` | `{ accountID: AccountID }` | server | `GET /api/auth/:accountID` | Get one auth account.
`auth.list` | `{ serviceID?: ServiceID }` | server | `GET /api/auth` | List saved auth accounts. Response includes active account mapping.
`auth.update` | `{ accountID: AccountID description?: string credential?: | { type: "oauth", refresh: string, access: string, expires: number } | { type: "api", key: string, metadata?: Record<string, string> } }` | server | `PATCH /api/auth/:accountID` | Update account description or credential.
`catalog.model.get` | `{ providerID: ProviderID modelID: ModelID }` | server | `GET /api/catalog/model/:providerID/:modelID` | Get one catalog model.
`catalog.model.list` | `{}` | server | `GET /api/catalog/model` | List flattened catalog models.
`command.list` | `{}` | request | `GET /api/command` | Available commands.
`config.get` | `{}` | request | `GET /api/config` | Resolved config.
`config.update` | `{ config: Config }` | request | `PATCH /api/config` | Update config.
`event.subscribe` | `{}` | request | `GET /api/event` | Server-sent events for the resolved runtime context.
`formatter.status` | `{}` | request | `GET /api/formatter` | Formatter status.
`fs.file` | `{ path: string }` | request | `GET /api/fs/file` | Read one file.
`fs.grep` | `{ pattern: string include?: string limit?: number }` | request | `POST /api/fs/grep` | Search file contents.
`fs.search` | `{ query: string type?: "file" | "directory" limit?: number }` | request | `POST /api/fs/search` | Search paths by name.
`fs.tree` | `{ path: string }` | request | `GET /api/fs/tree` | Browse a directory.
`lsp.status` | `{}` | request | `GET /api/lsp` | LSP status.
`mcp.prompt.list` | `{}` | request | `GET /api/mcp/prompt` | List MCP prompts.
`mcp.prompt.render` | `{ server: string name: string arguments?: Record<string, string> }` | request | `POST /api/mcp/prompt/render` | Render one MCP prompt.
`mcp.resource.list` | `{}` | request | `GET /api/mcp/resource` | List MCP resources.
`mcp.resource.read` | `{ server: string uri: string }` | request | `GET /api/mcp/resource/read` | Read one MCP resource.
`mcp.server.create` | `{ name: string config: | { type: "local", command: string, arguments?: string[], environment?: Record<string, string> } | { type: "remote", url: string, headers?: Record<string, string>, oauth?: boolean | object } }` | request | `POST /api/mcp/server` | Add an MCP server to runtime config.
`mcp.server.list` | `{}` | request | `GET /api/mcp/server` | List MCP servers with status and auth state.
`mcp.server.oauth.callback` | `{ name: string code: string }` | request | `POST /api/mcp/server/:name/oauth/callback` | Complete MCP OAuth.
`mcp.server.oauth.delete` | `{ name: string }` | request | `DELETE /api/mcp/server/:name/oauth` | Remove MCP OAuth credentials.
`mcp.server.oauth.start` | `{ name: string }` | request | `POST /api/mcp/server/:name/oauth` | Start MCP OAuth.
`permission.list` | `{}` | request | `GET /api/permission` | Pending permission requests.
`permission.reply` | `{ permissionID: PermissionID response: PermissionReply }` | request | `POST /api/permission/:permissionID/reply` | Reply to a permission request.
`project.get` | `{ projectID: ProjectID }` | server | `GET /api/project/:projectID` | Get project metadata.
`project.list` | `{}` | server | `GET /api/project` | List projects known to this server.
`project.update` | `{ projectID: ProjectID name?: string icon?: string commands?: Array<{ name: string command: string }> }` | server | `PATCH /api/project/:projectID` | Update project metadata.
`provider.list` | `{}` | request | `GET /api/provider` | Provider inventory for the runtime context.
`pty.create` | `{ command?: string cwd?: string shell?: string }` | request | `POST /api/pty` | Create PTY in the runtime context.
`pty.delete` | `{ ptyID: PtyID }` | request | `DELETE /api/pty/:ptyID` | Delete PTY.
`pty.get` | `{ ptyID: PtyID }` | request | `GET /api/pty/:ptyID` | Get PTY info.
`pty.list` | `{}` | request | `GET /api/pty` | List PTYs for the runtime.
`pty.update` | `{ ptyID: PtyID title?: string size?: { columns: number, rows: number } }` | request | `PATCH /api/pty/:ptyID` | Update PTY.
`question.list` | `{}` | request | `GET /api/question` | Pending user questions.
`question.reject` | `{ questionID: QuestionID }` | request | `POST /api/question/:questionID/reject` | Reject a question.
`question.reply` | `{ questionID: QuestionID response: QuestionResponse }` | request | `POST /api/question/:questionID/reply` | Reply to a question.
`session.compact` | `{ sessionID: SessionID }` | session | `POST /api/session/:sessionID/compact` | Compact the session conversation.
`session.context` | `{ sessionID: SessionID }` | session | `GET /api/session/:sessionID/context` | Return active context messages after the last compaction.
`session.create` | `{ title?: string agent?: string model?: { providerID: ProviderID, modelID: ModelID } permission?: PermissionRule[] }` | request | `POST /api/session` | Create a session pinned to resolved runtime context.
`session.delete` | `{ sessionID: SessionID }` | session | `DELETE /api/session/:sessionID` | Delete a session.
`session.diff` | `{ sessionID: SessionID }` | session | `GET /api/session/:sessionID/diff` | Return session diff summary.
`session.get` | `{ sessionID: SessionID }` | session | `GET /api/session/:sessionID` | Get one session.
`session.list` | `{ limit?: number order?: "asc" | "desc" path?: string roots?: boolean start?: number search?: string cursor?: string }` | request | `GET /api/session` | List sessions for the current runtime context by default.
`session.message.list` | `{ sessionID: SessionID limit?: number order?: "asc" | "desc" cursor?: string }` | session | `GET /api/session/:sessionID/message` | Page through session messages.
`session.prompt` | `{ sessionID: SessionID prompt: Prompt delivery?: "immediate" | "deferred" }` | session | `POST /api/session/:sessionID/prompt` | Create a user message and queue the agent loop.
`session.todo` | `{ sessionID: SessionID }` | session | `GET /api/session/:sessionID/todo` | Return todos associated with the session.
`session.update` | `{ sessionID: SessionID title?: string archived?: number permission?: PermissionRule[] }` | session | `PATCH /api/session/:sessionID` | Update title, archival state, or session metadata.
`session.wait` | `{ sessionID: SessionID }` | session | `POST /api/session/:sessionID/wait` | Wait until the session is idle.
`skill.list` | `{}` | request | `GET /api/skill` | Available skills.
`vcs.diff` | `{ format?: "json" | "patch" mode?: "worktree" | "default" }` | request | `GET /api/vcs/diff` | Diff for the runtime directory.
`vcs.get` | `{}` | request | `GET /api/vcs` | VCS metadata.
`vcs.patch` | `{ patch: string }` | request | `POST /api/vcs/patch` | Apply a patch to the runtime directory.
`vcs.status` | `{}` | request | `GET /api/vcs/status` | Changed files.
`workspace.create` | `{ projectID?: ProjectID name?: string directory?: string type: string metadata?: Record<string, unknown> }` | server | `POST /api/workspace` | Create or register a workspace.
`workspace.delete` | `{ workspaceID: WorkspaceID }` | server | `DELETE /api/workspace/:workspaceID` | Remove a workspace registration.
`workspace.get` | `{ workspaceID: WorkspaceID }` | server | `GET /api/workspace/:workspaceID` | Get workspace metadata.
`workspace.list` | `{ projectID?: ProjectID }` | server | `GET /api/workspace` | List workspaces, optionally filtered by project.
`workspace.status` | `{}` | server | `GET /api/workspace/status` | Connection/lifecycle status for all workspaces. Needs team discussion.
`workspace.sync` | `{}` | server | `POST /api/workspace/sync` | Sync workspace metadata from adapters. Needs team discussion.
`workspace.update` | `{ workspaceID: WorkspaceID name?: string metadata?: Record<string, unknown> archived?: boolean }` | server | `PATCH /api/workspace/:workspaceID` | Update workspace metadata or lifecycle state.
`workspace.warp` | `{ workspaceID?: WorkspaceID sessionID: SessionID copyChanges: boolean }` | server | `POST /api/workspace/warp` | Move a session into or out of a workspace. Needs team discussion.

## Event Envelope

Every event uses the same envelope. Resource identity belongs in `payload`. Runtime identity belongs in `context`.

    type ApiEvent<Payload> = {
      id: string
      type: string
      time: number
      context: {
        directory: string
        workspaceID?: string
      }
      payload: Payload
    }

    {
      "id": "evt_01",
      "type": "message.part.delta",
      "time": 1760000000000,
      "context": {
        "directory": "/repo/app",
        "workspaceID": "ws_123"
      },
      "payload": {
        "sessionID": "ses_123",
        "messageID": "msg_456",
        "partID": "part_789",
        "field": "text",
        "delta": "hello"
      }
    }

## Frontend Sync Store

A frontend can keep one giant store like the current TUI. Runtime data is partitioned by `contextKey`. Durable entities such as sessions and messages are keyed by their own IDs.

    type RuntimeContext = {
      directory: string
      workspaceID?: string
    }

    type ContextKey = string
    type SessionID = string
    type MessageID = string

    type SyncStore = {
      status: "loading" | "partial" | "complete"

      shared: {
        provider: Provider[]
        provider_default: Record<string, string>
        provider_next: ProviderListResponse
        provider_auth: Record<string, ProviderAuthMethod[]>
        console_state: ConsoleState
      }

      contexts: Record<
        ContextKey,
        {
          context: RuntimeContext

          config: Config
          agent: Agent[]
          command: Command[]
          lsp: LspStatus[]
          formatter: FormatterStatus[]
          vcs: VcsInfo | undefined
          mcp: Record<string, McpStatus>
          mcp_resource: Record<string, McpResource>

          session: SessionID[]
          session_status: Record<SessionID, SessionStatus>
        }
      >

      session: Record<SessionID, Session & { context: RuntimeContext }>
      session_diff: Record<SessionID, Snapshot.FileDiff[]>
      todo: Record<SessionID, Todo[]>
      permission: Record<SessionID, PermissionRequest[]>
      question: Record<SessionID, QuestionRequest[]>

      message: Record<SessionID, Message[]>
      part: Record<MessageID, Part[]>
    }

    function contextKey(context: RuntimeContext) {
      return `${context.workspaceID ?? "local"}:${context.directory}`
    }