opencode-py 0.2.0__tar.gz → 0.2.2__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 (56) hide show
  1. opencode_py-0.2.2/PKG-INFO +491 -0
  2. opencode_py-0.2.2/README.md +456 -0
  3. opencode_py-0.2.2/README.ru.md +455 -0
  4. {opencode_py-0.2.0 → opencode_py-0.2.2}/RELEASE.md +3 -1
  5. {opencode_py-0.2.0 → opencode_py-0.2.2}/demo.py +5 -8
  6. {opencode_py-0.2.0 → opencode_py-0.2.2}/pyproject.toml +2 -2
  7. {opencode_py-0.2.0 → opencode_py-0.2.2}/scripts/check-release.py +95 -95
  8. {opencode_py-0.2.0 → opencode_py-0.2.2}/scripts/check-upstream.py +3 -3
  9. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/__main__.py +3 -2
  10. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_errors.py +94 -94
  11. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_response_models.py +242 -242
  12. {opencode_py-0.2.0 → opencode_py-0.2.2}/web/index.html +199 -199
  13. opencode_py-0.2.0/PKG-INFO +0 -246
  14. opencode_py-0.2.0/README.md +0 -211
  15. opencode_py-0.2.0/README.ru.md +0 -184
  16. {opencode_py-0.2.0 → opencode_py-0.2.2}/.editorconfig +0 -0
  17. {opencode_py-0.2.0 → opencode_py-0.2.2}/.gitattributes +0 -0
  18. {opencode_py-0.2.0 → opencode_py-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {opencode_py-0.2.0 → opencode_py-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  20. {opencode_py-0.2.0 → opencode_py-0.2.2}/.github/workflows/publish.yml +0 -0
  21. {opencode_py-0.2.0 → opencode_py-0.2.2}/.github/workflows/test.yml +0 -0
  22. {opencode_py-0.2.0 → opencode_py-0.2.2}/.gitignore +0 -0
  23. {opencode_py-0.2.0 → opencode_py-0.2.2}/.pre-commit-config.yaml +0 -0
  24. {opencode_py-0.2.0 → opencode_py-0.2.2}/AGENTS.md +0 -0
  25. {opencode_py-0.2.0 → opencode_py-0.2.2}/CHANGELOG.md +0 -0
  26. {opencode_py-0.2.0 → opencode_py-0.2.2}/CODE_OF_CONDUCT.md +0 -0
  27. {opencode_py-0.2.0 → opencode_py-0.2.2}/CONTRIBUTING.md +0 -0
  28. {opencode_py-0.2.0 → opencode_py-0.2.2}/LICENSE +0 -0
  29. {opencode_py-0.2.0 → opencode_py-0.2.2}/SECURITY.md +0 -0
  30. {opencode_py-0.2.0 → opencode_py-0.2.2}/docs/opencode-docs-ru.md +0 -0
  31. {opencode_py-0.2.0 → opencode_py-0.2.2}/live.py +0 -0
  32. {opencode_py-0.2.0 → opencode_py-0.2.2}/live_async.py +0 -0
  33. {opencode_py-0.2.0 → opencode_py-0.2.2}/live_streaming.py +0 -0
  34. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/__init__.py +0 -0
  35. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_async_client.py +0 -0
  36. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_async_opencode.py +0 -0
  37. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_async_session.py +0 -0
  38. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_binary.py +0 -0
  39. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_client.py +0 -0
  40. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_logs.py +0 -0
  41. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_models.py +0 -0
  42. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_opencode.py +0 -0
  43. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_process.py +0 -0
  44. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_server.py +0 -0
  45. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_session.py +0 -0
  46. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_tools.py +0 -0
  47. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/_types.py +0 -0
  48. {opencode_py-0.2.0 → opencode_py-0.2.2}/src/opencode/py.typed +0 -0
  49. {opencode_py-0.2.0 → opencode_py-0.2.2}/test_all.py +0 -0
  50. {opencode_py-0.2.0 → opencode_py-0.2.2}/test_live.py +0 -0
  51. {opencode_py-0.2.0 → opencode_py-0.2.2}/tests/docker/Dockerfile +0 -0
  52. {opencode_py-0.2.0 → opencode_py-0.2.2}/tests/docker/test_smoke.py +0 -0
  53. {opencode_py-0.2.0 → opencode_py-0.2.2}/tests/test_async_client.py +0 -0
  54. {opencode_py-0.2.0 → opencode_py-0.2.2}/tests/test_client.py +0 -0
  55. {opencode_py-0.2.0 → opencode_py-0.2.2}/tests/test_opencode.py +0 -0
  56. {opencode_py-0.2.0 → opencode_py-0.2.2}/web/server.py +0 -0
@@ -0,0 +1,491 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-py
3
+ Version: 0.2.2
4
+ Summary: Python SDK for Opencode — the open source AI coding agent
5
+ Project-URL: Homepage, https://github.com/skislyakow/opencode-py
6
+ Project-URL: Repository, https://github.com/skislyakow/opencode-py
7
+ Project-URL: Documentation, https://github.com/skislyakow/opencode-py
8
+ Project-URL: Changelog, https://github.com/skislyakow/opencode-py/blob/main/CHANGELOG.md
9
+ Author-email: Sergey Kislyakov <s.kislyakov84@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,coding-agent,llm,opencode,python-sdk,sdk
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: typing-extensions>=4.6.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.0; extra == 'dev'
28
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
29
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.5.0; extra == 'dev'
33
+ Requires-Dist: twine>=4.0; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Opencode Python SDK
37
+
38
+ <p align="center">
39
+ <a href="https://pypi.org/project/opencode-py/"><img src="https://img.shields.io/pypi/v/opencode-py" alt="PyPI version"></a>
40
+ <a href="https://pypi.org/project/opencode-py/"><img src="https://img.shields.io/pypi/pyversions/opencode-py" alt="Python versions"></a>
41
+ <a href="https://github.com/skislyakow/opencode-py/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/opencode-py" alt="License"></a>
42
+ <a href="https://pypi.org/project/opencode-py/"><img src="https://img.shields.io/pypi/dm/opencode-py" alt="Downloads"></a>
43
+ <a href="https://github.com/skislyakow/opencode-py/actions/workflows/test.yml"><img src="https://github.com/skislyakow/opencode-py/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
44
+ <img src="https://img.shields.io/badge/build-hatchling-4051b5" alt="Hatchling">
45
+ <img src="https://img.shields.io/badge/http-httpx-blue" alt="httpx">
46
+ <img src="https://img.shields.io/badge/models-pydantic-E92063" alt="pydantic">
47
+ </p>
48
+
49
+ Python SDK for [Opencode](https://opencode.ai) — the open source AI coding agent.
50
+
51
+ ```bash
52
+ pip install opencode-py
53
+ ```
54
+
55
+ ## CLI
56
+
57
+ After installation, the `opencode-py` command is available **system-wide** from any directory:
58
+
59
+ ```bash
60
+ opencode-py "What is the capital of France?" # one-shot prompt
61
+ echo "What is the capital of France?" | opencode-py # via pipe
62
+ opencode-py --help # show all options
63
+ ```
64
+
65
+ All CLI flags:
66
+
67
+ | Flag | Description |
68
+ |------|-------------|
69
+ | `prompt` (positional) | Prompt text or read from stdin |
70
+ | `--model` / `-m` | Model name (e.g. `opencode/big-pickle`) |
71
+ | `--keep` / `-k` | Keep session alive between calls |
72
+ | `--auto-tools` | Enable agentic tool execution |
73
+ | `--directory` / `-d` | Working directory |
74
+ | `--port` / `-p` | Server port (default: 4096) |
75
+
76
+ You can also use `python -m opencode`:
77
+
78
+ ```bash
79
+ python -m opencode "Explain dependency injection"
80
+ python -m opencode --model "opencode/big-pickle" "Hello"
81
+ ```
82
+
83
+ ## Client library reference
84
+
85
+ ### One-shot (spawns server, asks, cleans up)
86
+
87
+ ```python
88
+ from opencode import opencode
89
+
90
+ answer = opencode("What is the capital of France?")
91
+ print(answer)
92
+ ```
93
+
94
+ ### Context manager (recommended)
95
+
96
+ ```python
97
+ from opencode import Opencode
98
+
99
+ with Opencode() as ai:
100
+ answer = ai.ask("Explain dependency injection")
101
+ print(answer)
102
+ ```
103
+
104
+ ### Streaming
105
+
106
+ ```python
107
+ with Opencode() as ai:
108
+ for chunk in ai.ask_stream("Write a Python function"):
109
+ print(chunk, end="")
110
+ ```
111
+
112
+ ### Conversations
113
+
114
+ ```python
115
+ with Opencode() as ai:
116
+ session = ai.create_session()
117
+ msg1 = session.prompt("Suggest a project name")
118
+ print(f"AI: {msg1}")
119
+ msg2 = session.prompt("Now write a tagline for it")
120
+ print(f"AI: {msg2}")
121
+ ```
122
+
123
+ ### Session methods
124
+
125
+ Every `Session` object provides additional methods:
126
+
127
+ ```python
128
+ with Opencode() as ai:
129
+ session = ai.create_session()
130
+ session.prompt("Hello")
131
+
132
+ # Get conversation history
133
+ ctx = session.context() # list of all messages
134
+ msgs = session.messages() # paginated message list
135
+
136
+ # Control
137
+ session.abort() # abort current generation
138
+ session.compact() # compact conversation
139
+ session.fork() # fork into new session
140
+
141
+ # Inspect
142
+ session.diff() # file changes made by AI
143
+ session.todo() # remaining TODOs
144
+ ```
145
+
146
+ ### Multi-turn (keep mode)
147
+
148
+ Reuses server and session across calls:
149
+
150
+ ```python
151
+ from opencode import opencode
152
+
153
+ r1 = opencode("My name is Alice", keep=True)
154
+ r2 = opencode("What's my name?", keep=True) # remembers conversation
155
+ r3 = opencode("That's all", keep=False) # closes server
156
+
157
+ # Also accepts: model, format, port, directory, config, agent
158
+ ```
159
+
160
+ ### Auto-tools (agentic tool execution)
161
+
162
+ ```python
163
+ r = opencode("Create a file called hello.txt", auto_tools=True)
164
+ ```
165
+
166
+ Available tools: `bash`, `write`, `edit`, `read`, `glob`, `grep`.
167
+
168
+ By default `bash` asks for permission in the console, all others run without prompting.
169
+
170
+ Custom permissions via `Session.ask()`:
171
+
172
+ ```python
173
+ from opencode import Opencode, ToolExecutor
174
+
175
+ with Opencode() as ai:
176
+ session = ai.create_session()
177
+ msg = session.ask(
178
+ "Write test.py with print('hello')",
179
+ tool_executor=ToolExecutor(
180
+ permissions={"write": "allow"},
181
+ workdir="/path/to/sandbox", # restrict file operations
182
+ ),
183
+ max_tool_rounds=25, # safety limit
184
+ quiet=True, # suppress tool logs
185
+ )
186
+ ```
187
+
188
+ The first AI response in `ask()` enters plan mode — the SDK auto-confirms with
189
+ `"Exit plan mode and proceed"` to make the model execute tools immediately.
190
+
191
+ ### Low-level client (any endpoint)
192
+
193
+ ```python
194
+ with Opencode() as ai:
195
+ content = ai.client.file_read("src/main.py")
196
+ diff = ai.client.vcs_diff("HEAD~3")
197
+ config = ai.client.config_get()
198
+ session = ai.client.session_create()
199
+ ai.client.v2_session_prompt(session.id, {"text": "Hello"})
200
+ ```
201
+
202
+ All client methods return typed Pydantic models — IDE autocomplete,
203
+ `.model_dump()`, `.model_dump_json()`.
204
+
205
+ #### Connecting to an existing server
206
+
207
+ Skip subprocess management by pointing at a running `opencode serve`:
208
+
209
+ ```python
210
+ from opencode import OpencodeClient
211
+
212
+ client = OpencodeClient(base_url="http://127.0.0.1:4096", directory=".")
213
+ health = client.health()
214
+ ```
215
+
216
+ ```python
217
+ from opencode import AsyncOpendcodeClient
218
+
219
+ async with AsyncOpendcodeClient(base_url="http://127.0.0.1:4096") as client:
220
+ health = await client.health()
221
+ ```
222
+
223
+ #### Cloning a client
224
+
225
+ ```python
226
+ client2 = client.copy(base_url="http://other:4096", timeout=60.0)
227
+
228
+ # Or via with_options:
229
+ faster = client.with_options(timeout=10.0, max_retries=0)
230
+ ```
231
+
232
+ ### Retry & error handling
233
+
234
+ Typed exception hierarchy. All errors are importable from `opencode`:
235
+
236
+ ```python
237
+ from opencode import OpencodeClient, RateLimitError, InternalServerError
238
+
239
+ client = OpencodeClient(max_retries=3) # exponential backoff with jitter
240
+
241
+ try:
242
+ health = client.health()
243
+ print(health.version)
244
+ except RateLimitError:
245
+ print("too many requests — retried but failed")
246
+ except InternalServerError:
247
+ print("server error")
248
+ ```
249
+
250
+ Full error class hierarchy:
251
+
252
+ | Class | HTTP status | When raised |
253
+ |-------|-------------|-------------|
254
+ | `OpencodeError` | — | Base for all SDK errors |
255
+ | `APIConnectionError` | — | Network / connection failure |
256
+ | `APITimeoutError` | — | Request timed out |
257
+ | `APIResponseValidationError` | — | Response doesn't match schema |
258
+ | `APIStatusError` | 4xx/5xx | Base for HTTP error responses |
259
+ | `BadRequestError` | 400 | Malformed request |
260
+ | `AuthenticationError` | 401 | Invalid or missing API key |
261
+ | `PermissionDeniedError` | 403 | Access denied |
262
+ | `NotFoundError` | 404 | Resource not found |
263
+ | `ConflictError` | 409 | Resource conflict |
264
+ | `UnprocessableEntityError` | 422 | Validation error in request body |
265
+ | `RateLimitError` | 429 | Rate limit exceeded |
266
+ | `InternalServerError` | 500+ | Server-side error |
267
+ | `BinaryNotFoundError` | — | `opencode` binary not on PATH |
268
+ | `ServerStartupTimeoutError` | — | Server didn't start in time |
269
+
270
+ Retry policy: 408, 409, 429, 5xx and timeouts are retried with exponential
271
+ backoff + jitter. `Retry-After` and `retry-after-ms` headers are respected.
272
+
273
+ ### Structured output
274
+
275
+ ```python
276
+ with Opencode(model="anthropic/claude-sonnet-4") as ai:
277
+ result = ai.ask(
278
+ "Generate a user profile",
279
+ format={
280
+ "type": "json_schema",
281
+ "schema": {
282
+ "type": "object",
283
+ "properties": {
284
+ "name": {"type": "string"},
285
+ "age": {"type": "integer"},
286
+ },
287
+ "required": ["name", "age"],
288
+ },
289
+ },
290
+ )
291
+ # result is a JSON string matching the schema
292
+ ```
293
+
294
+ Works with `opencode()`, `async_opencode()`, `Session.prompt()`, and `Session.ask()`.
295
+
296
+ Requires a model that supports `tool_choice="required"` (Claude, GPT-4).
297
+ The free `opencode/big-pickle` (DeepSeek) does NOT support this.
298
+
299
+ ### Debug logging
300
+
301
+ ```bash
302
+ # Linux / macOS (bash/zsh)
303
+ OPENCODE_LOG=debug python my_script.py
304
+
305
+ # Windows (PowerShell)
306
+ $env:OPENCODE_LOG="debug"; python my_script.py
307
+
308
+ # Windows (cmd)
309
+ set OPENCODE_LOG=debug && python my_script.py
310
+ ```
311
+
312
+ Shows all HTTP requests/responses with timing.
313
+
314
+ ### Web UI (zero dependencies)
315
+
316
+ ```bash
317
+ python web/server.py
318
+ # → open http://127.0.0.1:3000
319
+ ```
320
+
321
+ Built-in HTTP server + proxy to `opencode serve` — no extra dependencies.
322
+
323
+ ### Interactive dialog
324
+
325
+ ```bash
326
+ python live.py
327
+ ```
328
+
329
+ Multi-turn dialog with `keep=True`, server cleaned up on exit via `atexit`.
330
+
331
+ ### ToolExecutor reference
332
+
333
+ ```python
334
+ from opencode import ToolExecutor
335
+
336
+ # Default permissions:
337
+ # bash → "ask" (prompts in console)
338
+ # write → "allow"
339
+ # edit → "allow"
340
+ # read → "allow"
341
+ # glob → "allow"
342
+ # grep → "allow"
343
+
344
+ executor = ToolExecutor(
345
+ permissions={
346
+ "bash": "allow", # always allow
347
+ "write": "deny", # always deny
348
+ "grep": "ask", # ask each time
349
+ },
350
+ workdir="/path/to/sandbox", # restrict file operations here
351
+ confirm=lambda name, inp: name != "bash", # custom confirm function
352
+ )
353
+
354
+ # Use with Session.ask():
355
+ session.ask("Create a project", tool_executor=executor)
356
+ ```
357
+
358
+ ### Binary management
359
+
360
+ When `opencode` is not on PATH, the SDK auto-downloads it to
361
+ `~/.opencode/bin/opencode`.
362
+
363
+ Resolution order:
364
+ 1. `PATH` — `shutil.which("opencode")`
365
+ 2. `~/.opencode/bin/opencode` — previously downloaded copy
366
+ 3. GitHub releases — download for current platform
367
+
368
+ Supported platforms: `win32-x64`, `win32-arm64`, `darwin-x64`, `darwin-arm64`,
369
+ `linux-x64`, `linux-arm64`.
370
+
371
+ Override the binary path directly:
372
+
373
+ ```python
374
+ with Opencode(opencode_binary="/custom/path/opencode") as ai:
375
+ ...
376
+ ```
377
+
378
+ ### OpencodeServer (low-level server control)
379
+
380
+ ```python
381
+ from opencode import OpencodeServer, create_opencode_server
382
+
383
+ server = create_opencode_server(
384
+ port=4096,
385
+ hostname="127.0.0.1",
386
+ timeout=30.0,
387
+ config={"model": "opencode/big-pickle"},
388
+ opencode_binary="/path/to/opencode",
389
+ )
390
+ print(server.url) # "http://127.0.0.1:4096"
391
+
392
+ # Later:
393
+ server.close() # kills the subprocess
394
+ ```
395
+
396
+ ## Configuration reference
397
+
398
+ All parameters for `Opendcode()` / `AsyncOpendcode()`:
399
+
400
+ | Parameter | Default | Description |
401
+ |-----------|---------|-------------|
402
+ | `model` | `None` | Model name, e.g. `"opencode/big-pickle"` or `"provider/model"` |
403
+ | `hostname` | `"127.0.0.1"` | Bind address for the server |
404
+ | `port` | `4096` | Port for the server |
405
+ | `directory` | `None` | Working directory passed to all API calls |
406
+ | `workspace` | `None` | Workspace directory for the session |
407
+ | `server_timeout` | `30.0` | Seconds to wait for server startup |
408
+ | `client_timeout` | `300.0` | Seconds before HTTP request timeout |
409
+ | `config` | `None` | Server config dict (see opencode docs) |
410
+ | `opencode_binary` | `None` | Path to opencode binary (auto-downloaded if not set) |
411
+
412
+ All parameters are keyword-only.
413
+
414
+ ## Async API
415
+
416
+ ### Basic
417
+
418
+ ```python
419
+ import asyncio
420
+ from opencode import AsyncOpendcode
421
+
422
+ async def main():
423
+ async with AsyncOpendcode() as ai:
424
+ answer = await ai.ask("Explain async/await in Python")
425
+ print(answer)
426
+
427
+ asyncio.run(main())
428
+ ```
429
+
430
+ ### Async streaming
431
+
432
+ ```python
433
+ async with AsyncOpendcode() as ai:
434
+ async for chunk in ai.ask_stream("Write a poem"):
435
+ print(chunk, end="")
436
+ ```
437
+
438
+ ### Async conversations
439
+
440
+ ```python
441
+ async with AsyncOpendcode() as ai:
442
+ session = await ai.create_session()
443
+ msg1 = await session.prompt("Suggest a project name")
444
+ msg2 = await session.prompt("Now write a tagline for it")
445
+ ```
446
+
447
+ ### Async low-level client
448
+
449
+ ```python
450
+ from opencode import AsyncOpendcodeClient
451
+
452
+ async with AsyncOpendcodeClient() as client:
453
+ health = await client.health()
454
+ print(health.version) # typed Pydantic model
455
+ ```
456
+
457
+ ### Async convenience function
458
+
459
+ ```python
460
+ from opencode import async_opencode
461
+
462
+ result = await async_opencode("Hello", keep=True)
463
+ result2 = await async_opencode("Still there?", keep=True)
464
+ result3 = await async_opencode("Bye")
465
+
466
+ # Also accepts: model, format, port, directory, config, agent, auto_tools
467
+ ```
468
+
469
+ ## OpenAPI response models
470
+
471
+ ```python
472
+ from opencode._response_models import HealthResponse, SessionResponse, FileContentResponse
473
+
474
+ # These are Pydantic BaseModel classes with:
475
+ # .model_dump() -> dict
476
+ # .model_dump_json() -> str
477
+ # .model_validate(dict) -> classmethod
478
+ ```
479
+
480
+ ## Development
481
+
482
+ ```bash
483
+ # Install in editable mode
484
+ pip install -e ".[dev]"
485
+
486
+ # Run tests
487
+ pytest
488
+
489
+ # Build
490
+ python -m build --wheel
491
+ ```