opencode-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. opencode_py-0.1.0/.github/workflows/publish.yml +35 -0
  2. opencode_py-0.1.0/.github/workflows/test.yml +30 -0
  3. opencode_py-0.1.0/.gitignore +10 -0
  4. opencode_py-0.1.0/AGENTS.md +335 -0
  5. opencode_py-0.1.0/PKG-INFO +201 -0
  6. opencode_py-0.1.0/README.md +174 -0
  7. opencode_py-0.1.0/README.ru.md +174 -0
  8. opencode_py-0.1.0/demo.py +94 -0
  9. opencode_py-0.1.0/docs/opencode-docs-ru.md +355 -0
  10. opencode_py-0.1.0/live.py +29 -0
  11. opencode_py-0.1.0/live_async.py +51 -0
  12. opencode_py-0.1.0/live_streaming.py +43 -0
  13. opencode_py-0.1.0/pyproject.toml +47 -0
  14. opencode_py-0.1.0/scripts/check-upstream.py +93 -0
  15. opencode_py-0.1.0/src/opencode/__init__.py +45 -0
  16. opencode_py-0.1.0/src/opencode/__main__.py +20 -0
  17. opencode_py-0.1.0/src/opencode/_async_client.py +538 -0
  18. opencode_py-0.1.0/src/opencode/_async_opencode.py +255 -0
  19. opencode_py-0.1.0/src/opencode/_async_session.py +155 -0
  20. opencode_py-0.1.0/src/opencode/_binary.py +135 -0
  21. opencode_py-0.1.0/src/opencode/_client.py +532 -0
  22. opencode_py-0.1.0/src/opencode/_errors.py +20 -0
  23. opencode_py-0.1.0/src/opencode/_models.py +110 -0
  24. opencode_py-0.1.0/src/opencode/_opencode.py +290 -0
  25. opencode_py-0.1.0/src/opencode/_process.py +24 -0
  26. opencode_py-0.1.0/src/opencode/_server.py +97 -0
  27. opencode_py-0.1.0/src/opencode/_session.py +160 -0
  28. opencode_py-0.1.0/src/opencode/_tools.py +156 -0
  29. opencode_py-0.1.0/test_all.py +56 -0
  30. opencode_py-0.1.0/test_live.py +108 -0
  31. opencode_py-0.1.0/tests/test_async_client.py +133 -0
  32. opencode_py-0.1.0/tests/test_client.py +122 -0
  33. opencode_py-0.1.0/tests/test_opencode.py +136 -0
  34. opencode_py-0.1.0/web/index.html +199 -0
  35. opencode_py-0.1.0/web/server.py +145 -0
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/project/opencode-py/
15
+
16
+ permissions:
17
+ contents: read
18
+ id-token: write # for Trusted Publishing
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Set up Python 3.12
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Install build tools
29
+ run: pip install build
30
+
31
+ - name: Build package
32
+ run: python -m build
33
+
34
+ - name: Publish to PyPI
35
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,30 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest]
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Install dependencies
26
+ run: |
27
+ pip install -e ".[dev]"
28
+
29
+ - name: Run unit tests
30
+ run: pytest tests/ -v
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ *.egg
10
+ .pytest_cache/
@@ -0,0 +1,335 @@
1
+ # Opencode Python SDK — AGENTS.md
2
+
3
+ ## Project Overview
4
+
5
+ Python SDK for [Opencode](https://opencode.ai) — a PyPI package (`opencode-py`) that launches an `opencode serve` subprocess and provides both high-level and low-level APIs.
6
+
7
+ **Current version**: 0.1.0 (unreleased)
8
+ **Python**: >=3.10
9
+ **Dependencies**: only `httpx>=0.27.0`
10
+ **Build**: hatchling
11
+
12
+ ## Repository
13
+
14
+ - `C:\Code\opencode-py\` (local)
15
+ - GitHub: https://github.com/[user]/opencode-py
16
+ - Upstream opencode: https://github.com/anomalyco/opencode
17
+ - OpenAPI spec: `packages/sdk/openapi.json` (committed)
18
+ - Raw URL: `https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/sdk/openapi.json`
19
+ - Local copy of this repo: `C:\Code\opencode\`
20
+
21
+ ## File Structure
22
+
23
+ ```
24
+ opencode-py/
25
+ ├── src/opencode/
26
+ │ ├── __init__.py # Public API exports
27
+ │ ├── __main__.py # python -m opencode "question"
28
+ │ ├── _opencode.py # Opencode class + opencode() convenience fn
29
+ │ ├── _async_opencode.py # AsyncOpendcode class + async_opencode()
30
+ │ ├── _client.py # OpencodeClient — sync REST (528 lines)
31
+ │ ├── _async_client.py # AsyncOpendcodeClient — async REST
32
+ │ ├── _server.py # OpencodeServer — subprocess lifecycle
33
+ │ ├── _session.py # Session — sync conversation management
34
+ │ ├── _async_session.py # AsyncSession — async conversation management
35
+ │ ├── _binary.py # Binary find in PATH + GitHub download
36
+ │ ├── _process.py # Cross-platform process termination
37
+ │ ├── _models.py # TypedDict types (OutputFormatJsonSchema, etc.)
38
+ │ ├── _tools.py # ToolExecutor — run tools locally with permissions
39
+ │ └── _errors.py # OpencodeError, ApiError, BinaryNotFound
40
+ ├── tests/
41
+ │ ├── test_client.py # 11 unit tests (sync, httpx MockTransport)
42
+ │ ├── test_async_client.py # 11 unit tests (async, httpx MockTransport)
43
+ │ └── test_opencode.py # 9 tests (resolve_model, keep, structured, format)
44
+ ├── scripts/
45
+ │ └── check-upstream.py # Compare openapi.json with upstream GitHub
46
+ ├── demo.py # Live demo (38 endpoint checks)
47
+ ├── live.py # Interactive multi-turn dialog (sync)
48
+ ├── live_async.py # Interactive multi-turn dialog (async)
49
+ ├── live_streaming.py # Streaming interactive dialog
50
+ ├── test_live.py # Live integration test
51
+ ├── web/
52
+ │ ├── server.py # Start opencode + proxy for web UI
53
+ │ └── index.html # Chat interface (vanilla JS)
54
+ ├── AGENTS.md # This file
55
+ ├── README.md
56
+ ├── README.ru.md
57
+ ├── pyproject.toml
58
+ ├── .gitignore
59
+ └── docs/
60
+ └── opencode-docs-ru.md # Russian docs from opencode.ai
61
+ ```
62
+
63
+ ## Current State (commit history)
64
+
65
+ ```
66
+ ad54a39 feat(web): add zero-dependency web UI with proxy server
67
+ b8d205f feat(sdk): add auto_tools mode with ToolExecutor and permissions
68
+ 2fd170c docs: update AGENTS.md with keep mode and live.py
69
+ 67ae119 feat(sdk): add keep parameter for multi-turn conversations
70
+ 514b0a2 fix(session): use V1 sync prompt with model support instead of V2
71
+ 4323247 fix: resolve npm .cmd wrappers to real .exe binary on Windows
72
+ fb4b884 feat: initial Python SDK for Opencode
73
+ ```
74
+
75
+ ### What works
76
+ - Server starts/stops via subprocess (`opencode serve`)
77
+ - Binary auto-detection: PATH -> `~/.opencode/bin/` -> GitHub download
78
+ - Full REST API coverage (V1 + V2):
79
+ - Global, config, sessions, files, VCS, find, MCP, auth, providers, models, LSP, formatter, tools, permissions, questions, PTY, worktree, workspace, sync, TUI
80
+ - Session prompt via V1 sync API (V2 `v2_session_wait` is broken, replaced with V1 `POST /session/:id/message`)
81
+ - `Opencode(config={"model": "opencode/big-pickle"}).ask("...")` — works end-to-end with free model
82
+ - `opencode(prompt, keep=True)` — multi-turn conversation in same session
83
+ - `opencode(prompt, auto_tools=True)` — agentic tool execution (bash, write, edit, read, glob, grep) with permission system
84
+ - `async_opencode(prompt, keep=True)` — async version of `opencode()`
85
+ - Structured output — `format={"type": "json_schema", "schema": {...}}` on `ask()` / `prompt()` / `opencode()`
86
+ - Async full support — `AsyncOpendcode`, `AsyncOpendcodeClient`, `AsyncSession`
87
+ - Streaming — `ask_stream()` (sync + async) via `/event` SSE, works with `big-pickle` (non-streaming model) and streaming models
88
+ - `scripts/check-upstream.py` — fetches upstream openapi.json, flags needed changes
89
+ - 38/38 live endpoints tested against opencode v1.17.13
90
+ - 31/31 unit tests passing (sync + async)
91
+ - Python 3.10 compatibility (`NotRequired` via `typing_extensions`)
92
+ - `live.py` (sync), `live_async.py`, `live_streaming.py` — interactive dialog scripts with `atexit` cleanup
93
+
94
+ ### Known issues to fix
95
+
96
+ 1. **Delivery enum mismatch** — npm v1.17.13 uses `"steer"/"queue"`, but upstream `dev` branch source also uses `"steer"/"queue"`. The local clone (`C:\Code\opencode`) has been modified to use `"immediate"/"deferred"` but this is NOT yet upstream. When upstream switches, update `_client.py:delivery="queue"` and `_async_client.py` to `"deferred"`. Run `scripts/check-upstream.py` to monitor.
97
+
98
+ 2. ~~**Config format** — `Opencode(config={"model": "anthropic/..."})` fails because config expects `provider.{id}.options.apiKey` format.~~ **PARTIALLY RESOLVED**: The free `opencode` provider models work without API keys, but `OPENCODE_CONFIG_CONTENT={"model": "opencode/big-pickle"}` crashes the server with "ServeError" in v1.17.13. The model should be specified in the V1 prompt request body instead of server config. `Opencode` class works by passing `model` per-request, not via server config. **FIXED**: `opencode(model=...)` no longer puts model in server config — passes it per-request.
99
+
100
+ 3. **`v2_session_wait` broken** — `POST /api/session/{sessionID}/wait` returns "Session wait is not available yet" in v1.17.13. **FIXED**: `Session.prompt()` now polls `v2_session_context()` until an assistant message appears (see `_session.py:_poll_response`).
101
+
102
+ 4. ~~**No async support** — `OpencodeClient` is sync-only. `httpx.AsyncClient` not wired up.~~ **DONE**: `AsyncOpendcodeClient`, `AsyncSession`, `AsyncOpendcode`, `async_opencode()` all implemented.
103
+
104
+ 5. **Streaming** — `ask_stream()` reads SSE events via `/event` but delta format may differ between server versions.
105
+
106
+ 6. ~~**No upstream monitoring** — No script to compare local `openapi.json` with upstream.~~ **DONE**: `scripts/check-upstream.py` fetches upstream, checks delivery enum + structured output.
107
+
108
+ ## Architecture
109
+
110
+ ### Opencode (high-level, context manager)
111
+ ```
112
+ Opencode.__enter__()
113
+ → OpencodeServer.start() # subprocess: opencode serve --port=N
114
+ → OpencodeClient(base_url, ...) # httpx client
115
+ → return self
116
+ Opencode.ask(prompt)
117
+ → create_session()
118
+ → Session.prompt(text)
119
+ → v2_session_prompt(delivery="queue") # send prompt
120
+ → v2_session_context() # poll until assistant message
121
+ → _extract_text(message) # extract text content
122
+ Opencode.__exit__()
123
+ → OpencodeClient.close()
124
+ → OpencodeServer.close() # taskkill /T /F (win32) or SIGTERM (unix)
125
+ ```
126
+
127
+ ### opencode() — convenience function with keep
128
+ ```
129
+ _opencode_state = {"ai": ..., "session": ..., "config": ...}
130
+ opencode(prompt, keep=True)
131
+ → reuse existing session.prompt(prompt)
132
+ → return _extract_text(msg) # server stays alive
133
+ opencode(prompt) # keep=False by default
134
+ → Opencode.__enter__()
135
+ → create_session().prompt(prompt)
136
+ → _extract_text(msg)
137
+ → Opencode.__exit__() # server closed
138
+ ```
139
+ When `keep=True`, state is reused across calls (same session, same server).
140
+ Warns if different config/model passed. Clean up with `atexit` in scripts.
141
+
142
+ ### async_opencode() — async convenience function
143
+ Identical to `opencode()` but uses `AsyncOpendcode` and `await`.
144
+ Module-level `_async_opencode_state` for server/session reuse.
145
+ ```python
146
+ r1 = await async_opencode("hello", keep=True)
147
+ r2 = await async_opencode("what's my name?", keep=True)
148
+ r3 = await async_opencode("bye")
149
+ ```
150
+
151
+ ### OpencodeClient (low-level)
152
+ All HTTP methods follow the pattern:
153
+ ```
154
+ _request("GET"|"POST"|"DELETE"|"PATCH", path, params, json_body)
155
+ ```
156
+ with automatic `directory`/`workspace` query param injection via `_merge_params()`.
157
+
158
+ ### Correct API paths (OpenAPI spec)
159
+ V2 session operations:
160
+ ```
161
+ POST /api/session/{sessionID}/prompt — send prompt
162
+ POST /api/session/{sessionID}/wait — wait for idle (204, BROKEN in v1.17.13)
163
+ GET /api/session/{sessionID}/context — get context messages (used for polling)
164
+ GET /api/session/{sessionID}/message — list messages (paginated)
165
+ POST /api/session/{sessionID}/compact — compact
166
+ GET /api/session — list sessions
167
+ ```
168
+
169
+ All other endpoints use V1 paths (see AGENTS.md of the upstream repo or inline comments in `_client.py`).
170
+
171
+ ### Tool Execution
172
+ ```
173
+ Session.ask(text, tool_executor=ToolExecutor())
174
+ → send user message (POST /session/:id/message)
175
+ → if tool-use parts: execute via ToolExecutor, send results, loop
176
+ → if no tool-use and not confirmed: auto-confirm "Exit plan mode"
177
+ → if no tool-use and confirmed: return text
178
+
179
+ ToolExecutor:
180
+ - permissions: allow/ask/deny per tool
181
+ - default: bash=ask, others=allow
182
+ - tools: bash, write, edit, read, glob, grep
183
+ ```
184
+
185
+ ### Structured Output
186
+ ```
187
+ prompt() / ask() / opencode() / async_opencode()
188
+ accept: format={"type": "json_schema", "schema": {...}, "retryCount": 2}
189
+ → body["format"] = format (passed to V1 POST /session/:id/message)
190
+ → server injects StructuredOutput tool with toolChoice: "required"
191
+ → response may include "structured" field with parsed JSON
192
+ → _extract_text() returns JSON string when structured present
193
+
194
+ Schema: opencode/big-pickle (DeepSeek) does NOT support tool_choice="required"
195
+ Need Claude/GPT-4 with API key.
196
+ ```
197
+
198
+ ## Binary Management
199
+
200
+ ```python
201
+ # Resolution order:
202
+ 1. find_in_path() — shutil.which("opencode")
203
+ - On Windows: resolves .cmd wrappers to actual .exe via _resolve_wrapper()
204
+ 2. find_local() — ~/.opencode/bin/opencode
205
+ 3. download_opencode() — GitHub releases
206
+ ```
207
+
208
+ **Key Windows fix**: `shutil.which("opencode")` returns `opencode.cmd` (npm wrapper). `_resolve_wrapper()` reads the .cmd file and extracts the `.exe` path from the line `"%dp0%\node_modules\opencode-ai\bin\opencode.exe"`.
209
+
210
+ Platform detection for download:
211
+ - `win32-x64`, `win32-arm64`, `darwin-x64`, `darwin-arm64`, `linux-x64`, `linux-arm64`
212
+
213
+ ## Testing
214
+
215
+ ```bash
216
+ # Install
217
+ pip install -e ".[dev]"
218
+
219
+ # Unit tests (no server needed)
220
+ pytest tests/ -v
221
+
222
+ # Check upstream openapi for changes
223
+ python scripts/check-upstream.py
224
+
225
+ # Live test (requires opencode in PATH or npm global install)
226
+ python test_live.py
227
+
228
+ # Demo
229
+ python demo.py
230
+
231
+ # Interactive dialog
232
+ python live.py # sync
233
+ python live_async.py # async
234
+ python live_streaming.py
235
+
236
+ # Web UI (zero deps, opens browser)
237
+ python web/server.py
238
+ ```
239
+
240
+ ## Next Steps (priority order)
241
+
242
+ ### Step B: Test `ask()` with free model (Big Pickle) ✅ DONE
243
+ 1. Figure out correct config for free model usage
244
+ - Check `/api/provider` on running server for "big-pickle" or free providers
245
+ - Check if Big Pickle works without any API key/config
246
+ 2. Test `Session.prompt()` with delivery="queue" + `v2_session_wait()`
247
+ 3. Test `Opencode().ask()` end-to-end
248
+ 4. Fix any response parsing issues
249
+ 5. Add `keep=True` for multi-turn conversations in `opencode()` convenience function
250
+ - Module-level `_opencode_state` for server/session reuse
251
+ - `atexit` cleanup in `live.py`
252
+ - 6 unit tests for `keep` / `_resolve_model`
253
+
254
+ ### Step C: Publish v0.1.0 to PyPI
255
+ 1. ~~Create `scripts/check-upstream.py` — fetches openapi.json, diffs with local~~ ✅ DONE
256
+ 2. ~~Create GitHub Actions CI (tests on push)~~ ✅ DONE
257
+ 3. Publish to TestPyPI first, then PyPI
258
+ - `pip install build twine`
259
+ - `python -m build`
260
+ - `twine upload --repository testpypi dist/*` (check first)
261
+ - `twine upload dist/*`
262
+ - Or: push tag `v0.1.0` → GitHub Actions publishes via Trusted Publishing
263
+
264
+ ### Step D: Async support ✅ DONE
265
+ 1. `AsyncOpendcodeClient` using `httpx.AsyncClient` ✅
266
+ 2. `AsyncSession` with `async prompt()` ✅
267
+ 3. `AsyncOpendcode` with `async def ask()` ✅
268
+ 4. `async_opencode()` convenience function ✅
269
+ 5. 11 unit tests for async client ✅
270
+
271
+ ### Step E: Streaming improvements
272
+ 1. Better SSE parsing in `ask_stream()`
273
+ 2. Handle `message.part.delta` and `message.updated` events
274
+
275
+ ### Step F: Structured output ✅ DONE
276
+ 1. `format` parameter on `prompt()` / `ask()` / `opencode()` / `async_opencode()` ✅
277
+ 2. `_extract_text()` handles `structured` field ✅
278
+ 3. 3 unit tests ✅
279
+
280
+ ### Step G: Upstream monitoring ✅ DONE
281
+ 1. `scripts/check-upstream.py` checks delivery enum + structured output ✅
282
+
283
+ ## Style Guide
284
+
285
+ - Keep in one function unless composable/reusable
286
+ - No single-use helpers preemptively
287
+ - Avoid try/except where possible
288
+ - Prefer httpx over urllib/requests (HTTP client), except for `_binary.py` which uses `urllib.request` for download (stdlib, no extra deps)
289
+ - Method naming: snake_case, category prefix (e.g., `v2_session_*`, `file_*`, `config_*`)
290
+ - Type hints everywhere
291
+ - Avoid `Any` where possible — use `TypedDict` from `_models.py`
292
+
293
+ ## Commit Convention
294
+
295
+ ```
296
+ type(scope): summary
297
+ ```
298
+
299
+ Types: feat, fix, docs, chore, refactor, test
300
+ Always include package scope.
301
+
302
+ Examples:
303
+ ```
304
+ feat(server): add async context manager support
305
+ fix(binary): resolve npm .cmd wrappers to real .exe on Windows
306
+ refactor(client): extract error handling to _handle()
307
+ ```
308
+
309
+ ## Quick Reference for New Agent
310
+
311
+ ```
312
+ # Setup
313
+ git clone <repo> # or cd C:\Code\opencode-py
314
+ pip install -e ".[dev]"
315
+
316
+ # Check opencode availability
317
+ opencode serve --help # should work
318
+ python -c "from opencode._binary import ensure_opencode; print(ensure_opencode())"
319
+
320
+ # Run live test
321
+ python test_live.py
322
+
323
+ # Run demo
324
+ python demo.py
325
+
326
+ # Try a simple interactive test
327
+ python -c "
328
+ from opencode import OpencodeClient, create_opencode_server
329
+ s = create_opencode_server(port=4097)
330
+ c = OpencodeClient(base_url=s.url)
331
+ print('Health:', c.health())
332
+ c.close()
333
+ s.close()
334
+ "
335
+ ```
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-py
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Opencode — the open source AI coding agent
5
+ Project-URL: Homepage, https://opencode.ai
6
+ Project-URL: Repository, https://github.com/anomalyco/opencode
7
+ Project-URL: Documentation, https://opencode.ai/docs
8
+ Author-email: Anomaly <hello@opencode.ai>
9
+ License: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: build>=1.0; extra == 'dev'
22
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: twine>=4.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Opencode Python SDK
29
+
30
+ Python SDK for [Opencode](https://opencode.ai) — the open source AI coding agent.
31
+
32
+ ```bash
33
+ pip install opencode-py
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ### One-shot (spawns server, asks, cleans up)
39
+
40
+ ```python
41
+ from opencode import opencode
42
+
43
+ answer = opencode("What is the capital of France?")
44
+ print(answer)
45
+ ```
46
+
47
+ ### Context manager (recommended)
48
+
49
+ ```python
50
+ from opencode import Opencode
51
+
52
+ with Opencode() as ai:
53
+ answer = ai.ask("Explain dependency injection")
54
+ print(answer)
55
+ ```
56
+
57
+ ### Streaming
58
+
59
+ ```python
60
+ with Opencode() as ai:
61
+ for chunk in ai.ask_stream("Write a Python function"):
62
+ print(chunk, end="")
63
+ ```
64
+
65
+ ### Conversations
66
+
67
+ ```python
68
+ with Opencode() as ai:
69
+ session = ai.create_session()
70
+ msg1 = session.prompt("Suggest a project name")
71
+ print(f"AI: {msg1}")
72
+ msg2 = session.prompt("Now write a tagline for it")
73
+ print(f"AI: {msg2}")
74
+ ```
75
+
76
+ ### Multi-turn (keep mode)
77
+
78
+ ```python
79
+ from opencode import opencode
80
+
81
+ # keep=True — server and session stay alive between calls
82
+ r1 = opencode("My name is Alice", keep=True)
83
+ r2 = opencode("What's my name?", keep=True) # remembers the conversation
84
+ r3 = opencode("That's all", keep=False) # keep=False closes the server
85
+ ```
86
+
87
+ ### Auto-tools (agentic tool execution)
88
+
89
+ ```python
90
+ r = opencode("Create a file called hello.txt", auto_tools=True)
91
+ ```
92
+
93
+ Available tools: `bash`, `write`, `edit`, `read`, `glob`, `grep`.
94
+
95
+ By default, `bash` asks for permission in the console, all others run without prompting.
96
+
97
+ Custom permissions via `Session.ask()`:
98
+
99
+ ```python
100
+ from opencode import Opencode, ToolExecutor
101
+
102
+ with Opencode() as ai:
103
+ session = ai.create_session()
104
+ msg = session.ask(
105
+ "Write test.py with print('hello')",
106
+ tool_executor=ToolExecutor(permissions={"write": "allow"}),
107
+ )
108
+ ```
109
+
110
+ ### Low-level API (any endpoint)
111
+
112
+ ```python
113
+ with Opencode() as ai:
114
+ content = ai.client.file_read("src/main.py")
115
+ diff = ai.client.vcs_diff("HEAD~3")
116
+ config = ai.client.config_get()
117
+ session = ai.client.session_create()
118
+ ai.client.v2_session_prompt(session["id"], {"text": "Hello"})
119
+ ```
120
+
121
+ ### Web UI (zero dependencies)
122
+
123
+ ```bash
124
+ python web/server.py
125
+ # → open http://127.0.0.1:3000
126
+ ```
127
+
128
+ Built-in HTTP server + proxy to `opencode serve` — no extra dependencies.
129
+
130
+ ### Interactive dialog
131
+
132
+ ```bash
133
+ python live.py
134
+ ```
135
+
136
+ Multi-turn dialog with `keep=True`, server cleaned up on exit via `atexit`.
137
+
138
+ ### Configuration
139
+
140
+ ```python
141
+ with Opencode(
142
+ model="claude-sonnet-4-20250514",
143
+ directory="/path/to/project",
144
+ port=4096,
145
+ ) as ai:
146
+ ...
147
+ ```
148
+
149
+ ## Async API
150
+
151
+ ```python
152
+ import asyncio
153
+ from opencode import AsyncOpendcode
154
+
155
+ async def main():
156
+ async with AsyncOpendcode() as ai:
157
+ answer = await ai.ask("Explain async/await in Python")
158
+ print(answer)
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ ### Async streaming
164
+
165
+ ```python
166
+ async with AsyncOpendcode() as ai:
167
+ async for chunk in ai.ask_stream("Write a poem"):
168
+ print(chunk, end="")
169
+ ```
170
+
171
+ ### Async conversations
172
+
173
+ ```python
174
+ async with AsyncOpendcode() as ai:
175
+ session = await ai.create_session()
176
+ msg1 = await session.prompt("Suggest a project name")
177
+ msg2 = await session.prompt("Now write a tagline for it")
178
+ ```
179
+
180
+ ### Async low-level client
181
+
182
+ ```python
183
+ from opencode import AsyncOpendcodeClient
184
+
185
+ async with AsyncOpendcodeClient() as client:
186
+ health = await client.health()
187
+ print(health)
188
+ ```
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ # Install in editable mode
194
+ pip install -e ".[dev]"
195
+
196
+ # Run tests
197
+ pytest
198
+
199
+ # Build
200
+ python -m build --wheel
201
+ ```