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.
- opencode_py-0.1.0/.github/workflows/publish.yml +35 -0
- opencode_py-0.1.0/.github/workflows/test.yml +30 -0
- opencode_py-0.1.0/.gitignore +10 -0
- opencode_py-0.1.0/AGENTS.md +335 -0
- opencode_py-0.1.0/PKG-INFO +201 -0
- opencode_py-0.1.0/README.md +174 -0
- opencode_py-0.1.0/README.ru.md +174 -0
- opencode_py-0.1.0/demo.py +94 -0
- opencode_py-0.1.0/docs/opencode-docs-ru.md +355 -0
- opencode_py-0.1.0/live.py +29 -0
- opencode_py-0.1.0/live_async.py +51 -0
- opencode_py-0.1.0/live_streaming.py +43 -0
- opencode_py-0.1.0/pyproject.toml +47 -0
- opencode_py-0.1.0/scripts/check-upstream.py +93 -0
- opencode_py-0.1.0/src/opencode/__init__.py +45 -0
- opencode_py-0.1.0/src/opencode/__main__.py +20 -0
- opencode_py-0.1.0/src/opencode/_async_client.py +538 -0
- opencode_py-0.1.0/src/opencode/_async_opencode.py +255 -0
- opencode_py-0.1.0/src/opencode/_async_session.py +155 -0
- opencode_py-0.1.0/src/opencode/_binary.py +135 -0
- opencode_py-0.1.0/src/opencode/_client.py +532 -0
- opencode_py-0.1.0/src/opencode/_errors.py +20 -0
- opencode_py-0.1.0/src/opencode/_models.py +110 -0
- opencode_py-0.1.0/src/opencode/_opencode.py +290 -0
- opencode_py-0.1.0/src/opencode/_process.py +24 -0
- opencode_py-0.1.0/src/opencode/_server.py +97 -0
- opencode_py-0.1.0/src/opencode/_session.py +160 -0
- opencode_py-0.1.0/src/opencode/_tools.py +156 -0
- opencode_py-0.1.0/test_all.py +56 -0
- opencode_py-0.1.0/test_live.py +108 -0
- opencode_py-0.1.0/tests/test_async_client.py +133 -0
- opencode_py-0.1.0/tests/test_client.py +122 -0
- opencode_py-0.1.0/tests/test_opencode.py +136 -0
- opencode_py-0.1.0/web/index.html +199 -0
- 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,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
|
+
```
|