opencode-py 0.2.2__tar.gz → 0.3.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.3.0/CHANGELOG.md +62 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/PKG-INFO +12 -1
- {opencode_py-0.2.2 → opencode_py-0.3.0}/README.md +11 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/README.ru.md +11 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/RELEASE.md +6 -4
- opencode_py-0.3.0/VERIFY.md +84 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/demo.py +12 -31
- {opencode_py-0.2.2 → opencode_py-0.3.0}/pyproject.toml +4 -1
- {opencode_py-0.2.2 → opencode_py-0.3.0}/scripts/check-release.py +95 -95
- {opencode_py-0.2.2 → opencode_py-0.3.0}/scripts/check-upstream.py +17 -6
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_client.py +159 -194
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_client.py +145 -162
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_errors.py +94 -94
- opencode_py-0.3.0/src/opencode/_response_models.py +494 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/docker/test_smoke.py +3 -1
- {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_async_client.py +4 -1
- {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_client.py +9 -2
- {opencode_py-0.2.2 → opencode_py-0.3.0}/web/index.html +199 -199
- opencode_py-0.2.2/CHANGELOG.md +0 -22
- opencode_py-0.2.2/src/opencode/_response_models.py +0 -242
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.editorconfig +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.gitattributes +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/workflows/publish.yml +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/workflows/test.yml +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.gitignore +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/.pre-commit-config.yaml +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/AGENTS.md +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/CONTRIBUTING.md +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/LICENSE +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/SECURITY.md +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/docs/opencode-docs-ru.md +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/live.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/live_async.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/live_streaming.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/__init__.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/__main__.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_opencode.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_session.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_binary.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_logs.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_models.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_opencode.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_process.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_server.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_session.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_tools.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_types.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/py.typed +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/test_all.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/test_live.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/docker/Dockerfile +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_opencode.py +0 -0
- {opencode_py-0.2.2 → opencode_py-0.3.0}/web/server.py +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.3.0 (2026-07-05)
|
|
4
|
+
|
|
5
|
+
- feat(client): add typed Pydantic response models (`cast_to`) to all ~75 client methods
|
|
6
|
+
- feat(client): add 30+ response model classes (AgentResponse, CommandResponse, ConfigResponse, FileNode, FindMatch, VcsInfo, PtyResponse, WorktreeResponse, WorkspaceResponse, etc.)
|
|
7
|
+
- feat(client): `_construct_type` handles generic `list[X]` via `get_origin/get_args` and `{"data": [...]}` response format
|
|
8
|
+
- fix(client): `AgentResponse.permission` changed from `dict` to `Any` (server returns list)
|
|
9
|
+
- fix(scripts): resolve mypy `no-any-return` in check-upstream.py
|
|
10
|
+
- fix(docs): correct web UI port in VERIFY.md (3000, not 8000)
|
|
11
|
+
- fix(tests): import SessionMessage from `_models`, not `_session`
|
|
12
|
+
- chore(lint): suppress N815 camelCase warnings for `_response_models.py`
|
|
13
|
+
- chore(docs): add VERIFY.md release checklist
|
|
14
|
+
|
|
15
|
+
## v0.2.2 (2026-07-04)
|
|
16
|
+
|
|
17
|
+
- docs: comprehensive README documentation in EN and RU (CLI flags, structured output, session methods, error hierarchy, ToolExecutor, binary management, OpencodeServer, config reference, async API, response models)
|
|
18
|
+
- fix(demo): Pydantic model compatibility
|
|
19
|
+
- fix(scripts): mypy errors in check-upstream.py
|
|
20
|
+
- docs(readme): clarify binary auto-download behavior (NOT system-wide, NOT in PATH)
|
|
21
|
+
|
|
22
|
+
## v0.2.1 (2026-07-04)
|
|
23
|
+
|
|
24
|
+
- fix: rename entry point to `opencode-py` to avoid conflict with the real `opencode` binary
|
|
25
|
+
|
|
26
|
+
## v0.2.0 (2026-07-03)
|
|
27
|
+
|
|
28
|
+
- feat: Pydantic response models (HealthResponse, SessionResponse, FileContentResponse, V1SessionResponse)
|
|
29
|
+
- feat: retry logic with exponential backoff and jitter
|
|
30
|
+
- feat: typed error hierarchy (15+ classes)
|
|
31
|
+
- feat: logging via OPENCODE_LOG env var
|
|
32
|
+
- feat: async full support (AsyncOpendcodeClient, AsyncSession, AsyncOpendcode)
|
|
33
|
+
- feat: streaming (ask_stream sync + async)
|
|
34
|
+
- feat: auto_tools mode with ToolExecutor and permissions
|
|
35
|
+
- feat: web UI with proxy server (zero dependencies)
|
|
36
|
+
- feat: structured output (format parameter)
|
|
37
|
+
- feat: OpencodeServer lifecycle management
|
|
38
|
+
- feat: binary auto-download (PATH → ~/.opencode/bin/ → GitHub)
|
|
39
|
+
- feat: check-upstream.py script for monitoring openapi.json changes
|
|
40
|
+
- feat: `.copy()` / `.with_options()` for immutable client cloning
|
|
41
|
+
- feat: py.typed marker for PEP 561 compliance
|
|
42
|
+
|
|
43
|
+
## v0.1.1 (2026-07-03)
|
|
44
|
+
|
|
45
|
+
- chore: add keywords to pyproject.toml and .gitattributes
|
|
46
|
+
- docs: add badges to README and MIT license file
|
|
47
|
+
- feat(tests): add Docker smoke test for clean-machine scenario
|
|
48
|
+
- chore: use importlib.metadata for `__version__`
|
|
49
|
+
- chore: bump version to 0.1.1 for author/URLs fix
|
|
50
|
+
- fix(publish): set author to Sergey Kislyakov, fix URLs
|
|
51
|
+
|
|
52
|
+
## v0.1.0 (2026-06-30)
|
|
53
|
+
|
|
54
|
+
- feat: initial Python SDK for Opencode
|
|
55
|
+
- fix: resolve npm .cmd wrappers to real .exe binary on Windows
|
|
56
|
+
- fix(session): use V1 sync prompt with model support instead of V2
|
|
57
|
+
- feat(sdk): add keep parameter for multi-turn conversations
|
|
58
|
+
- feat(sdk): add auto_tools mode with ToolExecutor and permissions
|
|
59
|
+
- feat(web): add zero-dependency web UI with proxy server
|
|
60
|
+
- feat(async): add AsyncOpendcodeClient, AsyncSession, AsyncOpendcode
|
|
61
|
+
- feat(stream): add live_streaming.py, SSE streaming via ask_stream
|
|
62
|
+
- feat(sdk): add async_opencode, structured output, check-upstream
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python SDK for Opencode — the open source AI coding agent
|
|
5
5
|
Project-URL: Homepage, https://github.com/skislyakow/opencode-py
|
|
6
6
|
Project-URL: Repository, https://github.com/skislyakow/opencode-py
|
|
@@ -52,6 +52,17 @@ Python SDK for [Opencode](https://opencode.ai) — the open source AI coding age
|
|
|
52
52
|
pip install opencode-py
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
**Do I need Opencode pre-installed?** No. The SDK automatically downloads the
|
|
56
|
+
`opencode` binary for your OS (Windows/macOS/Linux, x64/arm64) on first use to
|
|
57
|
+
`~/.opencode/bin/`. The binary is only used internally by the SDK — it is NOT
|
|
58
|
+
added to PATH, NOT registered system-wide, and NOT shown in the Start Menu.
|
|
59
|
+
|
|
60
|
+
**What if I install the official Opencode later?** If you install Opencode
|
|
61
|
+
via `npm install -g opencode-ai` or another method, the SDK will use the
|
|
62
|
+
PATH version instead — no conflict.
|
|
63
|
+
|
|
64
|
+
*See [Binary management](#binary-management) for details.*
|
|
65
|
+
|
|
55
66
|
## CLI
|
|
56
67
|
|
|
57
68
|
After installation, the `opencode-py` command is available **system-wide** from any directory:
|
|
@@ -17,6 +17,17 @@ Python SDK for [Opencode](https://opencode.ai) — the open source AI coding age
|
|
|
17
17
|
pip install opencode-py
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
**Do I need Opencode pre-installed?** No. The SDK automatically downloads the
|
|
21
|
+
`opencode` binary for your OS (Windows/macOS/Linux, x64/arm64) on first use to
|
|
22
|
+
`~/.opencode/bin/`. The binary is only used internally by the SDK — it is NOT
|
|
23
|
+
added to PATH, NOT registered system-wide, and NOT shown in the Start Menu.
|
|
24
|
+
|
|
25
|
+
**What if I install the official Opencode later?** If you install Opencode
|
|
26
|
+
via `npm install -g opencode-ai` or another method, the SDK will use the
|
|
27
|
+
PATH version instead — no conflict.
|
|
28
|
+
|
|
29
|
+
*See [Binary management](#binary-management) for details.*
|
|
30
|
+
|
|
20
31
|
## CLI
|
|
21
32
|
|
|
22
33
|
After installation, the `opencode-py` command is available **system-wide** from any directory:
|
|
@@ -16,6 +16,17 @@ Python SDK для [Opencode](https://opencode.ai) — open source AI coding agen
|
|
|
16
16
|
pip install opencode-py
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
**Нужен ли предустановленный Opencode?** Нет. SDK автоматически скачивает
|
|
20
|
+
бинарник `opencode` под вашу ОС (Windows/macOS/Linux, x64/arm64) при первом
|
|
21
|
+
запуске в `~/.opencode/bin/`. Бинарник используется только внутри SDK — он
|
|
22
|
+
НЕ добавляется в PATH, НЕ устанавливается системно и НЕ появляется в меню Пуск.
|
|
23
|
+
|
|
24
|
+
**Что если позже установить официальный Opencode?** Если вы установите opencode
|
|
25
|
+
через `npm install -g opencode-ai` или другим способом, SDK будет использовать
|
|
26
|
+
версию из PATH — конфликтов нет.
|
|
27
|
+
|
|
28
|
+
*Подробнее в разделе [Управление бинарником](#управление-бинарником).*
|
|
29
|
+
|
|
19
30
|
## CLI
|
|
20
31
|
|
|
21
32
|
После установки команда `opencode-py` доступна **общесистемно** из любой директории:
|
|
@@ -48,7 +48,9 @@ git push
|
|
|
48
48
|
|
|
49
49
|
| Version | Date | Highlights |
|
|
50
50
|
|---------|------|------------|
|
|
51
|
-
| 0.
|
|
52
|
-
| 0.2.
|
|
53
|
-
| 0.2.
|
|
54
|
-
| 0.
|
|
51
|
+
| 0.3.0 | 2026-07-05 | Typed Pydantic models for all client methods (cast_to), 30+ new model classes, _construct_type handles list[X] generics and {"data": [...]} format |
|
|
52
|
+
| 0.2.2 | 2026-07-04 | Comprehensive README documentation (EN/RU), Pydantic fixes in demo.py, mypy fixes |
|
|
53
|
+
| 0.2.1 | 2026-07-04 | Fix: rename entry point to `opencode-py` to avoid conflict with the real `opencode` binary |
|
|
54
|
+
| 0.2.0 | 2026-07-03 | Pydantic models, retry, typed errors, logging, async, streaming, auto-tools, web UI |
|
|
55
|
+
| 0.1.1 | 2026-07-03 | Fix author/URLs, add keywords, Docker smoke test, badges |
|
|
56
|
+
| 0.1.0 | 2026-06-30 | Initial PyPI release |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Verification Checklist — v0.3.0-dev
|
|
2
|
+
|
|
3
|
+
Перед повышением версии и публикацией нужно проверить:
|
|
4
|
+
|
|
5
|
+
## 1. Unit-тесты (без сервера)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pytest tests/ -v
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Ожидается: **31 passed**.
|
|
12
|
+
|
|
13
|
+
## 2. Линтер и типы
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ruff check src/ tests/
|
|
17
|
+
mypy src/
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Ожидается: `ruff` — All checks passed, `mypy` — Success.
|
|
21
|
+
|
|
22
|
+
## 3. Демо
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python demo.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Должен пройти все 12 шагов без ошибок, завершиться "SDK is working correctly!".
|
|
29
|
+
|
|
30
|
+
## 4. Живой тест
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python test_live.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Запускает `opencode serve`, проходит 38 endpoint-чеков.
|
|
37
|
+
|
|
38
|
+
## 5. Интерактивный режим
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python live.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Проверить multi-turn (Ctrl+C для выхода).
|
|
45
|
+
|
|
46
|
+
## 6. Streaming
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python live_streaming.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 7. Web UI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python web/server.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Открыть браузер на `http://localhost:3000`.
|
|
59
|
+
|
|
60
|
+
## 8. Async
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python live_async.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 9. Проверка upstream
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python scripts/check-upstream.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Ожидается: "No SDK changes needed" (если upstream не менялся).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
После успешной проверки скажи — я подниму версию в `pyproject.toml`, сделаю тег `v0.3.0` и опубликую:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git tag v0.3.0
|
|
80
|
+
git push origin master --tags
|
|
81
|
+
python -m build
|
|
82
|
+
twine check dist/*
|
|
83
|
+
twine upload dist/*
|
|
84
|
+
```
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import json
|
|
7
|
-
|
|
8
6
|
from opencode import OpencodeClient, create_opencode_server
|
|
9
7
|
|
|
10
8
|
print("=" * 60)
|
|
@@ -13,7 +11,7 @@ print("=" * 60)
|
|
|
13
11
|
|
|
14
12
|
# 1. Start the server
|
|
15
13
|
print("\n[1] Starting opencode serve...")
|
|
16
|
-
server = create_opencode_server(port=
|
|
14
|
+
server = create_opencode_server(port=4097)
|
|
17
15
|
print(f" Server at {server.url} (pid={server.pid})")
|
|
18
16
|
|
|
19
17
|
client = OpencodeClient(base_url=server.url)
|
|
@@ -24,7 +22,7 @@ print(f"\n[2] Server: version={h.version}, healthy={h.healthy}")
|
|
|
24
22
|
|
|
25
23
|
# 3. Current project
|
|
26
24
|
pj = client.project_current()
|
|
27
|
-
print(f"\n[3] Project: id={pj
|
|
25
|
+
print(f"\n[3] Project: id={pj.id[:12]}... vcs={pj.vcs}")
|
|
28
26
|
|
|
29
27
|
# 4. Session management
|
|
30
28
|
print("\n[4] Creating session...")
|
|
@@ -40,25 +38,20 @@ print(f" File: {len(f.content)} chars")
|
|
|
40
38
|
# 6. VCS info
|
|
41
39
|
print("\n[6] VCS status...")
|
|
42
40
|
vcs = client.vcs_status()
|
|
43
|
-
|
|
41
|
+
n = len(vcs) if vcs else 0
|
|
42
|
+
print(f" {n} changed files")
|
|
44
43
|
|
|
45
44
|
# 7. Available models
|
|
46
45
|
print("\n[7] Available models...")
|
|
47
46
|
models = client.v2_model_list()
|
|
47
|
+
providers = sorted(set(m.providerID for m in models if m.providerID))
|
|
48
48
|
print(f" Format: {type(models).__name__}")
|
|
49
|
-
|
|
50
|
-
data = data if isinstance(data, list) else []
|
|
51
|
-
providers = [m.get("id", str(m)[:20]) for m in data[:5]]
|
|
52
|
-
print(f" Providers/models: {providers}")
|
|
49
|
+
print(f" Providers/models: {providers[:5]}")
|
|
53
50
|
|
|
54
51
|
# 8. Code search
|
|
55
52
|
print("\n[8] Searching text...")
|
|
56
53
|
found = client.find_text("create_opencode")
|
|
57
|
-
n = (
|
|
58
|
-
len(found)
|
|
59
|
-
if isinstance(found, list)
|
|
60
|
-
else (len(found.get("data", [])) if isinstance(found, dict) else "?")
|
|
61
|
-
)
|
|
54
|
+
n = len(found) if isinstance(found, list) else "?"
|
|
62
55
|
print(f" Found: {n}")
|
|
63
56
|
|
|
64
57
|
# 9. Send prompt (no LLM — queue only)
|
|
@@ -67,11 +60,7 @@ try:
|
|
|
67
60
|
msg = client.v2_session_prompt(
|
|
68
61
|
sid, {"text": "Hello! Answer in one word."}, delivery="queue"
|
|
69
62
|
)
|
|
70
|
-
|
|
71
|
-
if isinstance(data, list):
|
|
72
|
-
resp_type = data[0].get("type", "?")
|
|
73
|
-
else:
|
|
74
|
-
resp_type = type(msg).__name__
|
|
63
|
+
resp_type = getattr(msg, "type", type(msg).__name__)
|
|
75
64
|
print(f" Response: type={resp_type}")
|
|
76
65
|
print(" Prompt queued for processing")
|
|
77
66
|
except Exception as e:
|
|
@@ -80,23 +69,15 @@ except Exception as e:
|
|
|
80
69
|
# 10. Session context messages
|
|
81
70
|
print("\n[10] Session messages...")
|
|
82
71
|
ctx = client.v2_session_context(sid)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if isinstance(ctx, dict)
|
|
86
|
-
else len(ctx)
|
|
87
|
-
if isinstance(ctx, list)
|
|
88
|
-
else "?"
|
|
89
|
-
)
|
|
90
|
-
print(f" Messages: {count}")
|
|
72
|
+
items = ctx.get("data", []) if isinstance(ctx, dict) else ctx if isinstance(ctx, list) else []
|
|
73
|
+
print(f" Messages: {len(items)}")
|
|
91
74
|
|
|
92
75
|
# 11. Extra capabilities
|
|
93
76
|
print("\n[11] Additional:")
|
|
94
77
|
path = client.path_get()
|
|
95
|
-
print(f" Working directory: {path.
|
|
78
|
+
print(f" Working directory: {path.worktree}")
|
|
96
79
|
cmds = client.command_list()
|
|
97
|
-
print(
|
|
98
|
-
f" Opencode commands: {len(cmds) if isinstance(cmds, list) else '?'}"
|
|
99
|
-
)
|
|
80
|
+
print(f" Opencode commands: {len(cmds) if isinstance(cmds, list) else '?'}")
|
|
100
81
|
agents = client.app_agents()
|
|
101
82
|
print(f" Agents: {len(agents) if isinstance(agents, list) else '?'}")
|
|
102
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opencode-py"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Python SDK for Opencode — the open source AI coding agent"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
long_description_content_type = "text/markdown"
|
|
@@ -62,6 +62,9 @@ src = ["src"]
|
|
|
62
62
|
[tool.ruff.lint]
|
|
63
63
|
select = ["E", "F", "I", "N", "W", "UP"]
|
|
64
64
|
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"src/opencode/_response_models.py" = ["N815"]
|
|
67
|
+
|
|
65
68
|
[tool.mypy]
|
|
66
69
|
python_version = "3.10"
|
|
67
70
|
strict = true
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
"""Check if a new PyPI release is needed.
|
|
2
|
-
|
|
3
|
-
Compares the version in pyproject.toml with the latest git tag.
|
|
4
|
-
Exits with code 0 if no release needed, 1 if a release is recommended.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
python scripts/check-release.py
|
|
8
|
-
python scripts/check-release.py --verbose
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import re
|
|
14
|
-
import subprocess
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def get_current_version() -> str:
|
|
20
|
-
pyproject = Path(__file__).parent.parent / "pyproject.toml"
|
|
21
|
-
text = pyproject.read_text()
|
|
22
|
-
m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
23
|
-
if m:
|
|
24
|
-
return m.group(1)
|
|
25
|
-
raise RuntimeError("Could not find version in pyproject.toml")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_latest_tag() -> str | None:
|
|
29
|
-
result = subprocess.run(
|
|
30
|
-
["git", "tag", "--list", "v*", "--sort=-version:refname"],
|
|
31
|
-
capture_output=True, text=True, cwd=Path(__file__).parent.parent,
|
|
32
|
-
)
|
|
33
|
-
tags = [t.strip() for t in result.stdout.splitlines() if t.strip()]
|
|
34
|
-
return tags[0] if tags else None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_commit_count_since_tag(tag: str) -> int:
|
|
38
|
-
result = subprocess.run(
|
|
39
|
-
["git", "rev-list", f"{tag}..HEAD", "--count"],
|
|
40
|
-
capture_output=True, text=True, cwd=Path(__file__).parent.parent,
|
|
41
|
-
)
|
|
42
|
-
return int(result.stdout.strip() or "0")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def main() -> int:
|
|
46
|
-
verbose = "--verbose" in sys.argv or "-v" in sys.argv
|
|
47
|
-
|
|
48
|
-
current = get_current_version()
|
|
49
|
-
latest_tag = get_latest_tag()
|
|
50
|
-
|
|
51
|
-
print(f"Current version (pyproject.toml): {current}")
|
|
52
|
-
|
|
53
|
-
if latest_tag is None:
|
|
54
|
-
print("No git tags found — this would be the first release.")
|
|
55
|
-
return 0
|
|
56
|
-
|
|
57
|
-
tag_version = latest_tag.lstrip("v")
|
|
58
|
-
print(f"Latest git tag: {latest_tag}")
|
|
59
|
-
|
|
60
|
-
commits_since = get_commit_count_since_tag(latest_tag)
|
|
61
|
-
|
|
62
|
-
if current == tag_version:
|
|
63
|
-
if commits_since > 0:
|
|
64
|
-
print(f"\n==> {commits_since} commit(s) since {latest_tag}, "
|
|
65
|
-
f"but version hasn't been bumped ({current}).")
|
|
66
|
-
if verbose:
|
|
67
|
-
result = subprocess.run(
|
|
68
|
-
["git", "log", f"{latest_tag}..HEAD", "--oneline"],
|
|
69
|
-
capture_output=True, text=True,
|
|
70
|
-
cwd=Path(__file__).parent.parent,
|
|
71
|
-
)
|
|
72
|
-
print("\nUnreleased commits:")
|
|
73
|
-
for line in result.stdout.splitlines():
|
|
74
|
-
print(f" {line}")
|
|
75
|
-
print("\n=> Run `python -m build` and publish when ready:")
|
|
76
|
-
print(" python -m build")
|
|
77
|
-
print(" twine upload dist/*")
|
|
78
|
-
return 1
|
|
79
|
-
else:
|
|
80
|
-
print("\n[ok] Everything is up to date. No release needed.")
|
|
81
|
-
return 0
|
|
82
|
-
else:
|
|
83
|
-
print(f"\n=> Version in pyproject.toml ({current}) differs from "
|
|
84
|
-
f"latest tag ({tag_version}).")
|
|
85
|
-
print(" A new release may be ready to publish.")
|
|
86
|
-
if verbose:
|
|
87
|
-
print("\n To publish:")
|
|
88
|
-
print(" git tag v" + current)
|
|
89
|
-
print(" git push origin v" + current)
|
|
90
|
-
print(" python -m build && twine upload dist/*")
|
|
91
|
-
return 0
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if __name__ == "__main__":
|
|
95
|
-
sys.exit(main())
|
|
1
|
+
"""Check if a new PyPI release is needed.
|
|
2
|
+
|
|
3
|
+
Compares the version in pyproject.toml with the latest git tag.
|
|
4
|
+
Exits with code 0 if no release needed, 1 if a release is recommended.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python scripts/check-release.py
|
|
8
|
+
python scripts/check-release.py --verbose
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_current_version() -> str:
|
|
20
|
+
pyproject = Path(__file__).parent.parent / "pyproject.toml"
|
|
21
|
+
text = pyproject.read_text()
|
|
22
|
+
m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
23
|
+
if m:
|
|
24
|
+
return m.group(1)
|
|
25
|
+
raise RuntimeError("Could not find version in pyproject.toml")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_latest_tag() -> str | None:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git", "tag", "--list", "v*", "--sort=-version:refname"],
|
|
31
|
+
capture_output=True, text=True, cwd=Path(__file__).parent.parent,
|
|
32
|
+
)
|
|
33
|
+
tags = [t.strip() for t in result.stdout.splitlines() if t.strip()]
|
|
34
|
+
return tags[0] if tags else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_commit_count_since_tag(tag: str) -> int:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["git", "rev-list", f"{tag}..HEAD", "--count"],
|
|
40
|
+
capture_output=True, text=True, cwd=Path(__file__).parent.parent,
|
|
41
|
+
)
|
|
42
|
+
return int(result.stdout.strip() or "0")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> int:
|
|
46
|
+
verbose = "--verbose" in sys.argv or "-v" in sys.argv
|
|
47
|
+
|
|
48
|
+
current = get_current_version()
|
|
49
|
+
latest_tag = get_latest_tag()
|
|
50
|
+
|
|
51
|
+
print(f"Current version (pyproject.toml): {current}")
|
|
52
|
+
|
|
53
|
+
if latest_tag is None:
|
|
54
|
+
print("No git tags found — this would be the first release.")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
tag_version = latest_tag.lstrip("v")
|
|
58
|
+
print(f"Latest git tag: {latest_tag}")
|
|
59
|
+
|
|
60
|
+
commits_since = get_commit_count_since_tag(latest_tag)
|
|
61
|
+
|
|
62
|
+
if current == tag_version:
|
|
63
|
+
if commits_since > 0:
|
|
64
|
+
print(f"\n==> {commits_since} commit(s) since {latest_tag}, "
|
|
65
|
+
f"but version hasn't been bumped ({current}).")
|
|
66
|
+
if verbose:
|
|
67
|
+
result = subprocess.run(
|
|
68
|
+
["git", "log", f"{latest_tag}..HEAD", "--oneline"],
|
|
69
|
+
capture_output=True, text=True,
|
|
70
|
+
cwd=Path(__file__).parent.parent,
|
|
71
|
+
)
|
|
72
|
+
print("\nUnreleased commits:")
|
|
73
|
+
for line in result.stdout.splitlines():
|
|
74
|
+
print(f" {line}")
|
|
75
|
+
print("\n=> Run `python -m build` and publish when ready:")
|
|
76
|
+
print(" python -m build")
|
|
77
|
+
print(" twine upload dist/*")
|
|
78
|
+
return 1
|
|
79
|
+
else:
|
|
80
|
+
print("\n[ok] Everything is up to date. No release needed.")
|
|
81
|
+
return 0
|
|
82
|
+
else:
|
|
83
|
+
print(f"\n=> Version in pyproject.toml ({current}) differs from "
|
|
84
|
+
f"latest tag ({tag_version}).")
|
|
85
|
+
print(" A new release may be ready to publish.")
|
|
86
|
+
if verbose:
|
|
87
|
+
print("\n To publish:")
|
|
88
|
+
print(" git tag v" + current)
|
|
89
|
+
print(" git push origin v" + current)
|
|
90
|
+
print(" python -m build && twine upload dist/*")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
sys.exit(main())
|
|
@@ -17,14 +17,17 @@ UPSTREAM_URL = "https://raw.githubusercontent.com/anomalyco/opencode/dev/package
|
|
|
17
17
|
|
|
18
18
|
def fetch_json(url: str) -> dict[str, Any]:
|
|
19
19
|
with urllib.request.urlopen(url, timeout=15) as resp:
|
|
20
|
-
|
|
20
|
+
data: dict[str, Any] = json.loads(resp.read().decode())
|
|
21
|
+
return data
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def get_inline_delivery_enum(spec: dict[str, Any]) -> list[str] | None:
|
|
24
25
|
"""Extract the delivery enum from the v2 prompt endpoint."""
|
|
25
26
|
try:
|
|
26
27
|
prompt = spec["paths"]["/api/session/{sessionID}/prompt"]["post"]
|
|
27
|
-
props = prompt["requestBody"]["content"]["application/json"]["schema"][
|
|
28
|
+
props = prompt["requestBody"]["content"]["application/json"]["schema"][
|
|
29
|
+
"properties"
|
|
30
|
+
]
|
|
28
31
|
delivery = props["delivery"]
|
|
29
32
|
return cast(list[str] | None, delivery.get("enum"))
|
|
30
33
|
except Exception:
|
|
@@ -40,7 +43,9 @@ def main() -> None:
|
|
|
40
43
|
return
|
|
41
44
|
|
|
42
45
|
info = upstream.get("info", {})
|
|
43
|
-
print(
|
|
46
|
+
print(
|
|
47
|
+
f"Upstream info: title={info.get('title')}, version={info.get('version')}"
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
# Check 1: delivery enum
|
|
46
51
|
delivery = get_inline_delivery_enum(upstream)
|
|
@@ -62,7 +67,9 @@ def main() -> None:
|
|
|
62
67
|
elif "queue" in delivery:
|
|
63
68
|
print(" => No change needed (still 'queue' compatible).")
|
|
64
69
|
else:
|
|
65
|
-
changes_needed.append(
|
|
70
|
+
changes_needed.append(
|
|
71
|
+
"Delivery enum not found in spec — endpoint may have changed."
|
|
72
|
+
)
|
|
66
73
|
|
|
67
74
|
# Check 2: structured output format
|
|
68
75
|
schemas = upstream.get("components", {}).get("schemas", {})
|
|
@@ -71,11 +78,15 @@ def main() -> None:
|
|
|
71
78
|
# Check inline in V1 session/message endpoint
|
|
72
79
|
try:
|
|
73
80
|
v1 = upstream["paths"]["/session/{sessionID}/message"]["post"]
|
|
74
|
-
props = v1["requestBody"]["content"]["application/json"]["schema"][
|
|
81
|
+
props = v1["requestBody"]["content"]["application/json"]["schema"][
|
|
82
|
+
"properties"
|
|
83
|
+
]
|
|
75
84
|
has_structured = "format" in props
|
|
76
85
|
except Exception:
|
|
77
86
|
pass
|
|
78
|
-
print(
|
|
87
|
+
print(
|
|
88
|
+
f"\nStructured output (format field): {'PRESENT' if has_structured else 'MISSING'}"
|
|
89
|
+
)
|
|
79
90
|
if not has_structured:
|
|
80
91
|
changes_needed.append(
|
|
81
92
|
"'format' field in V1 prompt endpoint missing — structured output may have changed."
|