opencode-py 0.1.0__py3-none-any.whl
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/__init__.py +45 -0
- opencode/__main__.py +20 -0
- opencode/_async_client.py +538 -0
- opencode/_async_opencode.py +255 -0
- opencode/_async_session.py +155 -0
- opencode/_binary.py +135 -0
- opencode/_client.py +532 -0
- opencode/_errors.py +20 -0
- opencode/_models.py +110 -0
- opencode/_opencode.py +290 -0
- opencode/_process.py +24 -0
- opencode/_server.py +97 -0
- opencode/_session.py +160 -0
- opencode/_tools.py +156 -0
- opencode_py-0.1.0.dist-info/METADATA +201 -0
- opencode_py-0.1.0.dist-info/RECORD +17 -0
- opencode_py-0.1.0.dist-info/WHEEL +4 -0
opencode/_tools.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import glob as glob_mod
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
Permission = str
|
|
10
|
+
PERMIT_ALLOW: Permission = "allow"
|
|
11
|
+
PERMIT_ASK: Permission = "ask"
|
|
12
|
+
PERMIT_DENY: Permission = "deny"
|
|
13
|
+
|
|
14
|
+
DEFAULT_PERMISSIONS: Dict[str, Permission] = {
|
|
15
|
+
"bash": PERMIT_ASK,
|
|
16
|
+
"write": PERMIT_ALLOW,
|
|
17
|
+
"edit": PERMIT_ALLOW,
|
|
18
|
+
"read": PERMIT_ALLOW,
|
|
19
|
+
"glob": PERMIT_ALLOW,
|
|
20
|
+
"grep": PERMIT_ALLOW,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_confirm(tool_name: str, tool_input: Dict[str, Any]) -> bool:
|
|
25
|
+
print(f"\n\033[33m[Tool] {tool_name}({tool_input!r})\033[0m")
|
|
26
|
+
answer = input(" Allow? [Y/n] ").strip().lower()
|
|
27
|
+
return answer not in ("n", "no")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ToolExecutor:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
permissions: Optional[Dict[str, Permission]] = None,
|
|
34
|
+
confirm: Optional[Callable[[str, Dict[str, Any]], bool]] = None,
|
|
35
|
+
workdir: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
self._permissions = {**DEFAULT_PERMISSIONS, **(permissions or {})}
|
|
38
|
+
self._confirm = confirm or _default_confirm
|
|
39
|
+
self._workdir = workdir
|
|
40
|
+
|
|
41
|
+
def execute(self, tool_name: str, tool_input: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
permit = self._permissions.get(tool_name, PERMIT_ALLOW)
|
|
43
|
+
if permit == PERMIT_DENY:
|
|
44
|
+
return {"error": f"Tool '{tool_name}' is denied by configuration"}
|
|
45
|
+
if permit == PERMIT_ASK:
|
|
46
|
+
allowed = self._confirm(tool_name, tool_input)
|
|
47
|
+
if not allowed:
|
|
48
|
+
return {"error": f"Tool '{tool_name}' was rejected by user"}
|
|
49
|
+
|
|
50
|
+
handler = self._handlers().get(tool_name)
|
|
51
|
+
if not handler:
|
|
52
|
+
return {"error": f"Unknown tool '{tool_name}'"}
|
|
53
|
+
return handler(tool_input)
|
|
54
|
+
|
|
55
|
+
def _handlers(self) -> Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]]:
|
|
56
|
+
return {
|
|
57
|
+
"bash": self._bash,
|
|
58
|
+
"write": self._write,
|
|
59
|
+
"edit": self._edit,
|
|
60
|
+
"read": self._read,
|
|
61
|
+
"glob": self._glob,
|
|
62
|
+
"grep": self._grep,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Tool implementations
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def _bash(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
command = inp.get("command", "")
|
|
71
|
+
timeout = inp.get("timeout", 30)
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
command,
|
|
75
|
+
shell=True,
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
timeout=timeout,
|
|
79
|
+
cwd=self._workdir,
|
|
80
|
+
)
|
|
81
|
+
return {
|
|
82
|
+
"exitStatus": result.returncode,
|
|
83
|
+
"stdout": result.stdout,
|
|
84
|
+
"stderr": result.stderr,
|
|
85
|
+
}
|
|
86
|
+
except subprocess.TimeoutExpired:
|
|
87
|
+
return {"exitStatus": -1, "stdout": "", "stderr": "Command timed out"}
|
|
88
|
+
|
|
89
|
+
def _write(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
90
|
+
path = inp.get("path", "")
|
|
91
|
+
content = inp.get("content", "")
|
|
92
|
+
full_path = os.path.join(self._workdir, path) if self._workdir else path
|
|
93
|
+
os.makedirs(os.path.dirname(os.path.abspath(full_path)), exist_ok=True)
|
|
94
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
95
|
+
f.write(content)
|
|
96
|
+
return {"success": True, "path": path}
|
|
97
|
+
|
|
98
|
+
def _edit(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
99
|
+
path = inp.get("path", "")
|
|
100
|
+
old = inp.get("old", "")
|
|
101
|
+
new = inp.get("new", "")
|
|
102
|
+
full_path = os.path.join(self._workdir, path) if self._workdir else path
|
|
103
|
+
try:
|
|
104
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
105
|
+
text = f.read()
|
|
106
|
+
if old not in text:
|
|
107
|
+
return {"error": "old string not found", "success": False}
|
|
108
|
+
text = text.replace(old, new, 1)
|
|
109
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
110
|
+
f.write(text)
|
|
111
|
+
return {"success": True}
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
return {"error": f"File not found: {path}", "success": False}
|
|
114
|
+
|
|
115
|
+
def _read(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
116
|
+
path = inp.get("path", "")
|
|
117
|
+
full_path = os.path.join(self._workdir, path) if self._workdir else path
|
|
118
|
+
try:
|
|
119
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
120
|
+
content = f.read()
|
|
121
|
+
return {"content": content}
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
return {"error": f"File not found: {path}"}
|
|
124
|
+
|
|
125
|
+
def _glob(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
126
|
+
pattern = inp.get("pattern", "")
|
|
127
|
+
full_pattern = os.path.join(self._workdir, pattern) if self._workdir else pattern
|
|
128
|
+
files = glob_mod.glob(full_pattern, recursive=True)
|
|
129
|
+
return {"files": files}
|
|
130
|
+
|
|
131
|
+
def _grep(self, inp: Dict[str, Any]) -> Dict[str, Any]:
|
|
132
|
+
pattern = inp.get("pattern", "")
|
|
133
|
+
search_path = inp.get("path", "")
|
|
134
|
+
if search_path:
|
|
135
|
+
full_path = os.path.join(self._workdir, search_path) if self._workdir else search_path
|
|
136
|
+
else:
|
|
137
|
+
full_path = self._workdir or "."
|
|
138
|
+
import re
|
|
139
|
+
|
|
140
|
+
results: List[Dict[str, Any]] = []
|
|
141
|
+
for root, dirs, files in os.walk(full_path):
|
|
142
|
+
dirs[:] = [d for d in dirs if not d.startswith(".") and d != "node_modules" and d != ".git"]
|
|
143
|
+
for fname in files:
|
|
144
|
+
fpath = os.path.join(root, fname)
|
|
145
|
+
try:
|
|
146
|
+
with open(fpath, "r", encoding="utf-8", errors="ignore") as f:
|
|
147
|
+
for lineno, line in enumerate(f, 1):
|
|
148
|
+
if re.search(pattern, line):
|
|
149
|
+
results.append({
|
|
150
|
+
"path": os.path.relpath(fpath, self._workdir) if self._workdir else fpath,
|
|
151
|
+
"line": lineno,
|
|
152
|
+
"content": line.rstrip(),
|
|
153
|
+
})
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return {"results": results}
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
opencode/__init__.py,sha256=q3odOwNB6lfK5PVg1jcGxx-pspYr5KVeL6XkNbhhrIw,1230
|
|
2
|
+
opencode/__main__.py,sha256=RgEMXv19aTIrktpTlon0JnoiUcFD5Cyz5d7n1NREZLA,390
|
|
3
|
+
opencode/_async_client.py,sha256=_AsfFNckUbs0QS49dWJAy7fZA8h-KEToLQa045GMCo8,22923
|
|
4
|
+
opencode/_async_opencode.py,sha256=UJsevh4d9ijDdEyvJ1shc31XjSDQ7LqakLaK3fv42Es,8428
|
|
5
|
+
opencode/_async_session.py,sha256=wAMOI2c8-qODi_CqDpkEkodl5M1vq3jz3QXbB7oHtd4,5708
|
|
6
|
+
opencode/_binary.py,sha256=btfsHOPUc2iYQxYWl8BCgjBRK5_jQ2QVFrBQl-mp_so,3851
|
|
7
|
+
opencode/_client.py,sha256=y-BA-W_7_dyy9fRPBa7sTp6Ut9IFRzZGV-VXF6RzTv8,21390
|
|
8
|
+
opencode/_errors.py,sha256=G65WyH15Nbtmauzfi_8KpvsSwT5WsdyxwCeJYBBKpQQ,387
|
|
9
|
+
opencode/_models.py,sha256=BGBF2jfkVPfVJqcUfcKw24JQUHmYv8QvYqxs-dyq82w,2543
|
|
10
|
+
opencode/_opencode.py,sha256=gSzGoODCwlI9mO3Hcq6i09whTxW2YxM5yide1JjV8Cc,9364
|
|
11
|
+
opencode/_process.py,sha256=gwF1tgWUENOMUniY2Out6RqKvcFfENru_ds9pVtZKGs,546
|
|
12
|
+
opencode/_server.py,sha256=cd7P0OnUA6YbqlDmDjj2MDRvrsKOamdvPlWcvmlmFjw,2630
|
|
13
|
+
opencode/_session.py,sha256=U7dhYkXrBtnkMmPuPSK1b2KYuyPQPzpie5kblIuoGKs,5840
|
|
14
|
+
opencode/_tools.py,sha256=Tov-dr4x-03QAzwf1QHQzdxLEv10zv4LZkLWP79dXic,6020
|
|
15
|
+
opencode_py-0.1.0.dist-info/METADATA,sha256=OMLzjfxNzMgKwlFhGsZhDVrEm3YceCK_Bj1WgLmrgtc,4570
|
|
16
|
+
opencode_py-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
+
opencode_py-0.1.0.dist-info/RECORD,,
|