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.
Files changed (56) hide show
  1. opencode_py-0.3.0/CHANGELOG.md +62 -0
  2. {opencode_py-0.2.2 → opencode_py-0.3.0}/PKG-INFO +12 -1
  3. {opencode_py-0.2.2 → opencode_py-0.3.0}/README.md +11 -0
  4. {opencode_py-0.2.2 → opencode_py-0.3.0}/README.ru.md +11 -0
  5. {opencode_py-0.2.2 → opencode_py-0.3.0}/RELEASE.md +6 -4
  6. opencode_py-0.3.0/VERIFY.md +84 -0
  7. {opencode_py-0.2.2 → opencode_py-0.3.0}/demo.py +12 -31
  8. {opencode_py-0.2.2 → opencode_py-0.3.0}/pyproject.toml +4 -1
  9. {opencode_py-0.2.2 → opencode_py-0.3.0}/scripts/check-release.py +95 -95
  10. {opencode_py-0.2.2 → opencode_py-0.3.0}/scripts/check-upstream.py +17 -6
  11. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_client.py +159 -194
  12. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_client.py +145 -162
  13. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_errors.py +94 -94
  14. opencode_py-0.3.0/src/opencode/_response_models.py +494 -0
  15. {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/docker/test_smoke.py +3 -1
  16. {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_async_client.py +4 -1
  17. {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_client.py +9 -2
  18. {opencode_py-0.2.2 → opencode_py-0.3.0}/web/index.html +199 -199
  19. opencode_py-0.2.2/CHANGELOG.md +0 -22
  20. opencode_py-0.2.2/src/opencode/_response_models.py +0 -242
  21. {opencode_py-0.2.2 → opencode_py-0.3.0}/.editorconfig +0 -0
  22. {opencode_py-0.2.2 → opencode_py-0.3.0}/.gitattributes +0 -0
  23. {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  24. {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  25. {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/workflows/publish.yml +0 -0
  26. {opencode_py-0.2.2 → opencode_py-0.3.0}/.github/workflows/test.yml +0 -0
  27. {opencode_py-0.2.2 → opencode_py-0.3.0}/.gitignore +0 -0
  28. {opencode_py-0.2.2 → opencode_py-0.3.0}/.pre-commit-config.yaml +0 -0
  29. {opencode_py-0.2.2 → opencode_py-0.3.0}/AGENTS.md +0 -0
  30. {opencode_py-0.2.2 → opencode_py-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  31. {opencode_py-0.2.2 → opencode_py-0.3.0}/CONTRIBUTING.md +0 -0
  32. {opencode_py-0.2.2 → opencode_py-0.3.0}/LICENSE +0 -0
  33. {opencode_py-0.2.2 → opencode_py-0.3.0}/SECURITY.md +0 -0
  34. {opencode_py-0.2.2 → opencode_py-0.3.0}/docs/opencode-docs-ru.md +0 -0
  35. {opencode_py-0.2.2 → opencode_py-0.3.0}/live.py +0 -0
  36. {opencode_py-0.2.2 → opencode_py-0.3.0}/live_async.py +0 -0
  37. {opencode_py-0.2.2 → opencode_py-0.3.0}/live_streaming.py +0 -0
  38. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/__init__.py +0 -0
  39. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/__main__.py +0 -0
  40. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_opencode.py +0 -0
  41. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_async_session.py +0 -0
  42. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_binary.py +0 -0
  43. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_logs.py +0 -0
  44. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_models.py +0 -0
  45. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_opencode.py +0 -0
  46. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_process.py +0 -0
  47. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_server.py +0 -0
  48. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_session.py +0 -0
  49. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_tools.py +0 -0
  50. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/_types.py +0 -0
  51. {opencode_py-0.2.2 → opencode_py-0.3.0}/src/opencode/py.typed +0 -0
  52. {opencode_py-0.2.2 → opencode_py-0.3.0}/test_all.py +0 -0
  53. {opencode_py-0.2.2 → opencode_py-0.3.0}/test_live.py +0 -0
  54. {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/docker/Dockerfile +0 -0
  55. {opencode_py-0.2.2 → opencode_py-0.3.0}/tests/test_opencode.py +0 -0
  56. {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.2.2
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.2.2 | 2025-07-04 | Comprehensive README documentation, Pydantic fixes in demo.py, mypy fixes in check-upstream.py |
52
- | 0.2.1 | 2025-07-04 | Fix: rename entry point to `opencode-py` to avoid conflict with the real `opencode` binary |
53
- | 0.2.0 | 2025-07-03 | Pydantic models, retry, typed errors, logging, async, streaming, auto-tools, web UI |
54
- | 0.1.1 | | Initial PyPI release |
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=4098)
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['id'][:12]}... vcs={pj['vcs']}")
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
- print(f" {json.dumps(vcs, indent=2, ensure_ascii=False)[:200]}")
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
- data = models.get("data", models) if isinstance(models, dict) else models
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
- data = msg.get("data", [{}])
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
- count = (
84
- len(ctx.get("data", []))
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.get('worktree', '?')}")
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.2.2"
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
- return cast(dict[str, Any], json.loads(resp.read().decode()))
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"]["properties"]
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(f"Upstream info: title={info.get('title')}, version={info.get('version')}")
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("Delivery enum not found in spec — endpoint may have changed.")
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"]["properties"]
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(f"\nStructured output (format field): {'PRESENT' if has_structured else 'MISSING'}")
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."