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
49 changes: 49 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Docs

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Install social card dependencies
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev
- run: uv sync --locked --group docs
- run: uv run mkdocs build --strict
env:
SOCIAL_CARDS: "true"
- if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v4
with:
path: site

deploy:
name: Deploy to GitHub Pages
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ htmlcov/
codegen/extract/node_modules/
codegen/extract/.cache/
.DS_Store

# MkDocs output
site/
.cache/
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ uv run python -m codegen --check
uv run pytest
```

## Documentation

The site in `docs/` is built with MkDocs Material and mkdocstrings. The API reference is generated from docstrings
at build time by `docs/hooks/types_reference.py`.

```bash
uv sync --group docs
uv run mkdocs serve
uv run mkdocs build --strict
```

### Docs must keep up with the code

A change to the public API is not done until the docs cover it. CI enforces this:

| Check | Where | Fails when |
| --- | --- | --- |
| `tests/test_docs_coverage.py` | `pytest` job | A resource, method, `opencode.__all__` name or `opencode.types` model is not in the reference; an exported class or one of its public methods has no docstring; a resource group is missing from `docs/guides/index.md`; a guide mentions a method or type that does not exist |
| ruff `D1` rules | Lint job | A public function in a public module has no docstring |
| `mkdocs build --strict` | Docs workflow | A link or cross-reference is broken |

Each failure names what is missing and the file to change. In practice:

- **New endpoint or resource group** (from a spec sync): resource and type pages are generated from the code, so
they appear on their own. Add a row for a new group to `docs/guides/index.md`.
- **New hand-written class or function exported from `opencode`**: give it a docstring and add a `::: opencode.Name`
directive to the right page in `docs/reference/`.
- **Renamed or removed API**: update every guide that mentions it.

## Layout

| Path | What it is | Edit by hand? |
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

[![CI](https://github.com/shrijayan/opencode-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/shrijayan/opencode-python-sdk/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/opencode-python-sdk.svg)](https://pypi.org/project/opencode-python-sdk/)
[![Docs](https://img.shields.io/badge/docs-shrijayan.github.io-d9730d)](https://shrijayan.github.io/opencode-python-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/opencode-python-sdk.svg)](https://pypi.org/project/opencode-python-sdk/)

Typed Python client for the [OpenCode](https://github.com/anomalyco/opencode) 2.x server API.

**Documentation: <https://shrijayan.github.io/opencode-python-sdk/>**

> **Unofficial, community-maintained.** This package is not affiliated with or endorsed by the OpenCode team.
> If the OpenCode maintainers want to take it over, we are happy to transfer this repository and the `opencode-python-sdk`
> name on PyPI. Open an issue and we will hand it over.
Expand Down
11 changes: 6 additions & 5 deletions codegen/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,14 @@ def _method_doc(operation: Operation, errors: dict[str, dict[str, Any]], indent:
documented = [param for param in operation.params if param.doc]
if documented:
parts.append("Args:\n" + "\n".join(f" {param.name}: {_one_line(param.doc or '')}" for param in documented))
raised = []
raised: dict[str, int] = {}
for error in operation.errors:
if error.tag and error.tag in errors:
name = errors[error.tag]["class"]
if name not in raised:
raised.append(name)
raised.setdefault(errors[error.tag]["class"], errors[error.tag]["status"])
if raised:
parts.append("Raises:\n" + "\n".join(f" {name}" for name in raised))
parts.append(
"Raises:\n" + "\n".join(f" {name}: The server answered {status}." for name, status in raised.items())
)
parts.append(f"``{operation.http_method} {operation.path}``")
return docstring("\n\n".join(parts), indent) or ""

Expand Down Expand Up @@ -391,6 +391,7 @@ def _resource_classes(resource: Resource, errors: dict[str, dict[str, Any]], *,
lines += [
" @cached_property",
f" def {name}(self) -> {prefix}{child.class_name}Resource:",
f' """Operations under ``client.{_attr_path(child)}``."""',
f" return {prefix}{child.class_name}Resource(self._client)\n",
]
for operation in resource.operations:
Expand Down
1 change: 1 addition & 0 deletions docs/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions docs/getting-started/connect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Connect to a server

Start a server with the CLI, then point a client at it:

```bash
OPENCODE_PASSWORD=secret opencode serve --port 4096
```

```python
from opencode import Opencode

client = Opencode("http://127.0.0.1:4096", password="secret")
print(client.server.info().version)
```

## Environment variables

`base_url` and `password` fall back to environment variables, so the shortest form is `Opencode()`.

| Argument | Variable |
| --- | --- |
| `base_url` | `OPENCODE_BASE_URL` |
| `password` | `OPENCODE_PASSWORD`, then `OPENCODE_SERVER_PASSWORD` |

`OPENCODE_PASSWORD` is the same variable the OpenCode CLI reads. Pass `password=""` to send no credentials.

## Closing the client

The client owns an `httpx.Client` connection pool. Use it as a context manager, or call `close()`:

```python
with Opencode() as client:
client.agent.list()
```

## Calling the API

Methods mirror the official TypeScript client: `client.<group>.<operation>()`, in `snake_case`. Path parameters are
positional; everything else is keyword-only.

```python
client.session.get("ses_...")
client.session.prompt("ses_...", text="Fix the failing test", resume=True)
client.session.revert.stage("ses_...", message_id="msg_...")
client.mcp.list()
```

Every method is listed under [Resources](../reference/resources.md).

### Optional and null arguments

The server distinguishes a field that is absent from a field set to `null`. Optional arguments default to
`NOT_GIVEN` and are left out of the request; passing `None` sends `null`.

```python
client.session.update("ses_...", title="New title") # (1)!
client.session.list(parent_id=None) # (2)!
```

1. Sends `{"title": "New title"}`.
2. Sends `parentID=null`, which returns only root sessions.
35 changes: 35 additions & 0 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Install

```bash
pip install opencode-python-sdk
```

=== "uv"

```bash
uv add opencode-python-sdk
```

=== "poetry"

```bash
poetry add opencode-python-sdk
```

The distribution is called `opencode-python-sdk`; the package imports as `opencode`. It needs Python 3.10 or newer
and depends only on `httpx`, `pydantic`, `anyio` and `typing-extensions`.

```python
import opencode

print(opencode.__version__) # this package
print(opencode.API_VERSION) # the OpenCode server version it was generated from
```

To start a server from Python with [`OpencodeServer`](server.md), install the [OpenCode CLI](https://opencode.ai)
as well, so that `opencode` is on `PATH`.

## Next

- [Connect to a server](connect.md) that is already running.
- [Run a server from Python](server.md) for scripts, tests and embedded use.
54 changes: 54 additions & 0 deletions docs/getting-started/server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Run a server from Python

`OpencodeServer` runs `opencode serve` as a child process, on a free local port with a random password, for the
lifetime of a `with` block.

```python
from opencode import OpencodeServer

server = OpencodeServer(
cwd="/path/to/repo", # (1)!
isolated=True, # (2)!
config={"model": "anthropic/claude-sonnet-5-5"}, # (3)!
)
with server:
client = server.client()
print(client.server.info().version)
```

1. The server's working directory, which becomes its default project.
2. Ignore project `opencode.json`, `AGENTS.md` and Claude Code files on disk, for reproducible runs.
3. Passed to the server as `OPENCODE_CONFIG_CONTENT`.

The server runs in `--stdio` mode, so it exits when the Python process dies, even if the `with` block never
finishes. Its password reaches the child through `OPENCODE_PASSWORD`, which the server strips from the environment
its tools inherit.

## Async

`AsyncOpencodeServer` takes the same arguments:

```python
from opencode import AsyncOpencodeServer

async with AsyncOpencodeServer(cwd="/path/to/repo") as server:
async with server.client() as client:
await client.session.create(title="Async")
```

## When it fails to start

If `opencode` is missing or the server exits early, `ServerStartError` is raised. Its `output` attribute holds the
last lines the server printed.

```python
from opencode import OpencodeServer, ServerStartError

try:
with OpencodeServer() as server:
...
except ServerStartError as error:
print(error.output)
```

See the [`OpencodeServer` reference](../reference/server.md) for every argument.
68 changes: 68 additions & 0 deletions docs/guides/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Async

`AsyncOpencode` has the same resources and methods as `Opencode`. Methods return awaitables, list methods return an
`AsyncPaginator`, and streams return an `AsyncStream`. It runs on [AnyIO](https://anyio.readthedocs.io), so it works
under both asyncio and trio.

=== "asyncio"

```python
import asyncio
from opencode import AsyncOpencode


async def main() -> None:
async with AsyncOpencode("http://127.0.0.1:4096") as client:
session = await client.session.create(title="Async")
await client.session.prompt(session.id, text="Hello")
async for message in client.message.list(session.id):
print(message.type)


asyncio.run(main())
```

=== "trio"

```python
import trio
from opencode import AsyncOpencode


async def main() -> None:
async with AsyncOpencode("http://127.0.0.1:4096") as client:
info = await client.server.info()
print(info.version)


trio.run(main)
```

## Concurrency

One client can serve many concurrent tasks; it shares one connection pool.

```python
import asyncio


async def titles(client: AsyncOpencode, ids: list[str]) -> list[str]:
sessions = await asyncio.gather(*(client.session.get(i) for i in ids))
return [s.title for s in sessions]
```

## Pages and streams

```python
page = await client.session.list(limit=20) # (1)!
async for session in client.session.list(): # (2)!
...

async with await client.event.subscribe() as events: # (3)!
async for event in events:
print(event.type)
```

1. `await` the paginator for the first page.
2. `async for` over it for every item across pages.
3. `subscribe()` returns an `AsyncStream` once awaited.
22 changes: 22 additions & 0 deletions docs/guides/directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# The project directory

An OpenCode server can serve several projects. Most endpoints take a `location` that says which one a request is
about. Set a default once on the client:

```python
client = Opencode("http://127.0.0.1:4096", directory="/path/to/repo")
client.agent.list() # (1)!
client.agent.list(location={"directory": "/other"}) # (2)!
```

1. Sent with `location` = `/path/to/repo`.
2. A per-call `location` overrides the default.

Without a default, the server uses its own working directory. With `OpencodeServer`, that is `cwd`; you can also pass
`directory=` to `server.client()`.

```python
with OpencodeServer(cwd="/repos/api") as server:
api = server.client()
web = server.client(directory="/repos/web")
```
Loading
Loading