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/_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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any