voice-vibecoder 2.8.1__tar.gz → 2.9.1__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.
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/CHANGELOG.md +16 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/PKG-INFO +2 -1
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/pyproject.toml +2 -1
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/__init__.py +1 -1
- voice_vibecoder-2.9.1/src/voice_vibecoder/code_providers/openclaw.py +315 -0
- voice_vibecoder-2.8.1/src/voice_vibecoder/code_providers/openclaw.py +0 -183
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/.gitignore +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/LICENSE +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/README.md +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/app.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/index.html +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/session.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.9.1 (2026-02-21)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Correct connect params for gateway protocol
|
|
10
|
+
([`89589e2`](https://github.com/snokam/voice-vibecoder/commit/89589e2d732d21f2f3225b005eff7c33b8f18d77))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v2.9.0 (2026-02-21)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- Add WebSocket streaming support to OpenClaw provider
|
|
18
|
+
([`97e107a`](https://github.com/snokam/voice-vibecoder/commit/97e107a8371a02783b51258bc75c0d64807b3964))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v2.8.1 (2026-02-21)
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.1
|
|
4
4
|
Summary: Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code
|
|
5
5
|
Project-URL: Homepage, https://github.com/snokam/voice-vibecoder
|
|
6
6
|
Project-URL: Repository, https://github.com/snokam/voice-vibecoder
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Software Development
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Requires-Dist: claude-agent-sdk>=0.1.30
|
|
24
|
+
Requires-Dist: cryptography>=42.0
|
|
24
25
|
Requires-Dist: numpy>=1.24
|
|
25
26
|
Requires-Dist: platformdirs>=4.0
|
|
26
27
|
Requires-Dist: rich>=13.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voice-vibecoder"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.1"
|
|
8
8
|
description = "Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -24,6 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
+
"cryptography>=42.0",
|
|
27
28
|
"textual>=0.50",
|
|
28
29
|
"rich>=13.0",
|
|
29
30
|
"websockets>=12",
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""OpenClaw Agent code provider.
|
|
2
|
+
|
|
3
|
+
Routes coding tasks to OpenClaw Gateway via WebSocket for streaming output.
|
|
4
|
+
Falls back to CLI if WebSocket connection fails.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Awaitable, Callable
|
|
16
|
+
|
|
17
|
+
from voice_vibecoder.code_providers import AgentOutput, AgentResult
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from cryptography.hazmat.primitives import serialization
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
24
|
+
HAS_CRYPTO = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAS_CRYPTO = False
|
|
27
|
+
logger.warning("cryptography not installed, WebSocket auth will not work")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_device_keys(state_dir: str) -> tuple[str, Ed25519PrivateKey | None, str | None]:
|
|
31
|
+
"""Load device identity from OpenClaw state directory."""
|
|
32
|
+
device_path = os.path.join(state_dir, "identity", "device.json")
|
|
33
|
+
auth_path = os.path.join(state_dir, "identity", "device-auth.json")
|
|
34
|
+
|
|
35
|
+
device_id = None
|
|
36
|
+
private_key = None
|
|
37
|
+
token = None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with open(device_path) as f:
|
|
41
|
+
device = json.load(f)
|
|
42
|
+
device_id = device["deviceId"]
|
|
43
|
+
if HAS_CRYPTO:
|
|
44
|
+
pem = device["privateKeyPem"].encode()
|
|
45
|
+
private_key = serialization.load_pem_private_key(pem, password=None)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug("Could not load device.json: %s", e)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(auth_path) as f:
|
|
51
|
+
auth = json.load(f)
|
|
52
|
+
if "tokens" in auth and "operator" in auth["tokens"]:
|
|
53
|
+
token = auth["tokens"]["operator"]["token"]
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.debug("Could not load device-auth.json: %s", e)
|
|
56
|
+
|
|
57
|
+
return device_id, private_key, token
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sign_challenge(private_key: Ed25519PrivateKey, nonce: str, ts: int) -> tuple[str, int]:
|
|
61
|
+
"""Sign the gateway challenge."""
|
|
62
|
+
message = f"{nonce}:{ts}".encode()
|
|
63
|
+
signature = private_key.sign(message)
|
|
64
|
+
return base64.urlsafe_b64encode(signature).decode().rstrip("="), ts
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OpenClawRunner:
|
|
68
|
+
"""AgentRunner implementation using OpenClaw Gateway WebSocket."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
session_id: str | None = None,
|
|
73
|
+
agent_id: str | None = None,
|
|
74
|
+
env: dict[str, str] | None = None,
|
|
75
|
+
gateway_url: str | None = None,
|
|
76
|
+
state_dir: str | None = None,
|
|
77
|
+
**_kwargs,
|
|
78
|
+
) -> None:
|
|
79
|
+
self._session_id = session_id or f"voice-{uuid.uuid4().hex[:8]}"
|
|
80
|
+
self._agent_id = agent_id
|
|
81
|
+
self._env = env or {}
|
|
82
|
+
self._gateway_url = gateway_url or os.environ.get("OPENCLAW_GATEWAY_URL", "ws://127.0.0.1:18789")
|
|
83
|
+
self._state_dir = state_dir or os.environ.get("OPENCLAW_STATE_DIR", "/data/olaf")
|
|
84
|
+
self._task: asyncio.Task | None = None
|
|
85
|
+
self._ws = None
|
|
86
|
+
|
|
87
|
+
self._device_id, self._private_key, self._device_token = _load_device_keys(self._state_dir)
|
|
88
|
+
|
|
89
|
+
async def _connect_websocket(self) -> Any:
|
|
90
|
+
"""Connect to OpenClaw Gateway via WebSocket."""
|
|
91
|
+
try:
|
|
92
|
+
import websockets
|
|
93
|
+
except ImportError:
|
|
94
|
+
logger.warning("websockets not installed")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if not self._device_id or not self._private_key:
|
|
98
|
+
logger.warning("No device identity found, cannot connect via WebSocket")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
ws = await asyncio.wait_for(
|
|
103
|
+
websockets.connect(self._gateway_url),
|
|
104
|
+
timeout=5.0
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Wait for challenge
|
|
108
|
+
challenge_msg = await asyncio.wait_for(ws.recv(), timeout=5.0)
|
|
109
|
+
challenge = json.loads(challenge_msg)
|
|
110
|
+
|
|
111
|
+
if challenge.get("event") != "connect.challenge":
|
|
112
|
+
logger.warning("Unexpected challenge: %s", challenge)
|
|
113
|
+
await ws.close()
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
nonce = challenge["payload"]["nonce"]
|
|
117
|
+
ts = challenge["payload"]["ts"]
|
|
118
|
+
|
|
119
|
+
# Sign challenge
|
|
120
|
+
signature, signed_at = _sign_challenge(self._private_key, nonce, ts)
|
|
121
|
+
|
|
122
|
+
# Get public key in base64
|
|
123
|
+
public_key_bytes = self._private_key.public_key().public_bytes(
|
|
124
|
+
encoding=serialization.Encoding.Raw,
|
|
125
|
+
format=serialization.PublicFormat.Raw
|
|
126
|
+
)
|
|
127
|
+
public_key_b64 = base64.urlsafe_b64encode(public_key_bytes).decode().rstrip("=")
|
|
128
|
+
|
|
129
|
+
# Send connect request - match CLI format exactly
|
|
130
|
+
connect_req = {
|
|
131
|
+
"type": "req",
|
|
132
|
+
"id": str(uuid.uuid4()),
|
|
133
|
+
"method": "connect",
|
|
134
|
+
"params": {
|
|
135
|
+
"minProtocol": 3,
|
|
136
|
+
"maxProtocol": 3,
|
|
137
|
+
"client": {
|
|
138
|
+
"id": "cli", # Must be "cli" for operator role
|
|
139
|
+
"version": "1.0.0",
|
|
140
|
+
"platform": "linux",
|
|
141
|
+
"mode": "operator" # Must be "operator"
|
|
142
|
+
},
|
|
143
|
+
"role": "operator",
|
|
144
|
+
"scopes": ["operator.read", "operator.write"],
|
|
145
|
+
"caps": [],
|
|
146
|
+
"commands": [],
|
|
147
|
+
"permissions": {},
|
|
148
|
+
"auth": {"token": self._device_token} if self._device_token else {},
|
|
149
|
+
"locale": "en-US",
|
|
150
|
+
"userAgent": "voice-server/1.0.0",
|
|
151
|
+
"device": {
|
|
152
|
+
"id": self._device_id,
|
|
153
|
+
"publicKey": public_key_b64,
|
|
154
|
+
"signature": signature,
|
|
155
|
+
"signedAt": signed_at,
|
|
156
|
+
"nonce": nonce
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await ws.send(json.dumps(connect_req))
|
|
162
|
+
|
|
163
|
+
# Wait for response
|
|
164
|
+
response_msg = await asyncio.wait_for(ws.recv(), timeout=5.0)
|
|
165
|
+
response = json.loads(response_msg)
|
|
166
|
+
|
|
167
|
+
if not response.get("ok"):
|
|
168
|
+
error = response.get("error", {}).get("message", "Unknown error")
|
|
169
|
+
logger.warning("Gateway connect failed: %s", error)
|
|
170
|
+
await ws.close()
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
logger.info("Connected to OpenClaw Gateway via WebSocket")
|
|
174
|
+
return ws
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning("WebSocket connection failed: %s", e)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
async def _run_via_websocket(
|
|
181
|
+
self,
|
|
182
|
+
ws,
|
|
183
|
+
message: str,
|
|
184
|
+
cwd: str,
|
|
185
|
+
session_id: str,
|
|
186
|
+
on_output: Callable[[AgentOutput], None] | None = None,
|
|
187
|
+
) -> AgentResult | None:
|
|
188
|
+
"""Run agent via WebSocket for streaming output."""
|
|
189
|
+
try:
|
|
190
|
+
req_id = str(uuid.uuid4())
|
|
191
|
+
agent_req = {
|
|
192
|
+
"type": "req",
|
|
193
|
+
"id": req_id,
|
|
194
|
+
"method": "agent.run",
|
|
195
|
+
"params": {
|
|
196
|
+
"sessionId": session_id,
|
|
197
|
+
"message": message,
|
|
198
|
+
"cwd": cwd,
|
|
199
|
+
"local": True
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await ws.send(json.dumps(agent_req))
|
|
204
|
+
|
|
205
|
+
result_text = ""
|
|
206
|
+
|
|
207
|
+
while True:
|
|
208
|
+
msg = await asyncio.wait_for(ws.recv(), timeout=300.0)
|
|
209
|
+
data = json.loads(msg)
|
|
210
|
+
|
|
211
|
+
if data.get("type") == "event":
|
|
212
|
+
event = data.get("event", "")
|
|
213
|
+
payload = data.get("payload", {})
|
|
214
|
+
|
|
215
|
+
if event == "agent.output":
|
|
216
|
+
text = payload.get("text", "")
|
|
217
|
+
if text and on_output:
|
|
218
|
+
on_output(AgentOutput(category="text", content=text))
|
|
219
|
+
elif event == "agent.tool":
|
|
220
|
+
tool = payload.get("tool", "")
|
|
221
|
+
if on_output:
|
|
222
|
+
on_output(AgentOutput(category="tool_call", content=f"[{tool}]"))
|
|
223
|
+
|
|
224
|
+
elif data.get("type") == "res" and data.get("id") == req_id:
|
|
225
|
+
if data.get("ok"):
|
|
226
|
+
result_text = data.get("payload", {}).get("result", "")
|
|
227
|
+
else:
|
|
228
|
+
error = data.get("error", {}).get("message", "Unknown error")
|
|
229
|
+
raise RuntimeError(f"Agent error: {error}")
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
return AgentResult(text=result_text.strip(), session_id=session_id)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning("WebSocket agent run failed: %s", e)
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
async def _run_via_cli(
|
|
239
|
+
self,
|
|
240
|
+
message: str,
|
|
241
|
+
cwd: str,
|
|
242
|
+
session_id: str,
|
|
243
|
+
on_output: Callable[[AgentOutput], None] | None = None,
|
|
244
|
+
) -> AgentResult:
|
|
245
|
+
"""Fallback: run agent via CLI."""
|
|
246
|
+
if on_output:
|
|
247
|
+
on_output(AgentOutput(category="text", content="🦞 Running via CLI..."))
|
|
248
|
+
|
|
249
|
+
full_message = f"[Working directory: {cwd}]\n\n{message}"
|
|
250
|
+
|
|
251
|
+
cmd = [
|
|
252
|
+
"openclaw", "agent",
|
|
253
|
+
"--session-id", session_id,
|
|
254
|
+
"--message", full_message,
|
|
255
|
+
"--local",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
env = os.environ.copy()
|
|
259
|
+
env["PATH"] = f"/home/olaf/.npm-global/bin:/home/olaf/.local/bin:{env.get('PATH', '')}"
|
|
260
|
+
env["OPENCLAW_STATE_DIR"] = self._state_dir
|
|
261
|
+
if self._env:
|
|
262
|
+
env.update(self._env)
|
|
263
|
+
|
|
264
|
+
process = await asyncio.create_subprocess_exec(
|
|
265
|
+
*cmd,
|
|
266
|
+
stdout=asyncio.subprocess.PIPE,
|
|
267
|
+
stderr=asyncio.subprocess.PIPE,
|
|
268
|
+
env=env,
|
|
269
|
+
cwd=cwd,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
stdout, stderr = await process.communicate()
|
|
273
|
+
result_text = stdout.decode("utf-8", errors="replace").strip()
|
|
274
|
+
|
|
275
|
+
if process.returncode != 0 and not result_text:
|
|
276
|
+
raise RuntimeError(f"CLI failed: {stderr.decode()}")
|
|
277
|
+
|
|
278
|
+
return AgentResult(text=result_text, session_id=session_id)
|
|
279
|
+
|
|
280
|
+
async def run(
|
|
281
|
+
self,
|
|
282
|
+
message: str,
|
|
283
|
+
cwd: str,
|
|
284
|
+
session_id: str | None = None,
|
|
285
|
+
on_output: Callable[[AgentOutput], None] | None = None,
|
|
286
|
+
can_use_tool: Callable[[str, dict, Any], Awaitable[Any]] | None = None,
|
|
287
|
+
) -> AgentResult:
|
|
288
|
+
"""Run a coding task via OpenClaw."""
|
|
289
|
+
self._task = asyncio.current_task()
|
|
290
|
+
effective_session = session_id or self._session_id
|
|
291
|
+
|
|
292
|
+
if on_output:
|
|
293
|
+
on_output(AgentOutput(category="text", content="🦞 Connecting to OpenClaw..."))
|
|
294
|
+
|
|
295
|
+
ws = await self._connect_websocket()
|
|
296
|
+
if ws:
|
|
297
|
+
try:
|
|
298
|
+
result = await self._run_via_websocket(ws, message, cwd, effective_session, on_output)
|
|
299
|
+
if result:
|
|
300
|
+
return result
|
|
301
|
+
finally:
|
|
302
|
+
await ws.close()
|
|
303
|
+
|
|
304
|
+
if on_output:
|
|
305
|
+
on_output(AgentOutput(category="text", content="WebSocket unavailable, using CLI..."))
|
|
306
|
+
|
|
307
|
+
return await self._run_via_cli(message, cwd, effective_session, on_output)
|
|
308
|
+
|
|
309
|
+
def cancel(self) -> bool:
|
|
310
|
+
"""Cancel the running task."""
|
|
311
|
+
task = self._task
|
|
312
|
+
if task and not task.done():
|
|
313
|
+
task.cancel()
|
|
314
|
+
return True
|
|
315
|
+
return False
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
"""OpenClaw Agent code provider.
|
|
2
|
-
|
|
3
|
-
Routes coding tasks to OpenClaw via CLI. Uses `openclaw agent --local`
|
|
4
|
-
for embedded agent execution with full tool access.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import json
|
|
11
|
-
import logging
|
|
12
|
-
import os
|
|
13
|
-
import re
|
|
14
|
-
import uuid
|
|
15
|
-
from typing import Any, Awaitable, Callable
|
|
16
|
-
|
|
17
|
-
from voice_vibecoder.code_providers import AgentOutput, AgentResult
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _extract_tool_info(line: str) -> tuple[str, str] | None:
|
|
23
|
-
"""Extract tool name and description from agent output lines."""
|
|
24
|
-
match = re.match(r"^\[(\w+)\]\s*(.*)$", line.strip())
|
|
25
|
-
if match:
|
|
26
|
-
return match.group(1), match.group(2)
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class OpenClawRunner:
|
|
31
|
-
"""AgentRunner implementation using OpenClaw CLI."""
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
session_id: str | None = None,
|
|
36
|
-
agent_id: str | None = None,
|
|
37
|
-
env: dict[str, str] | None = None,
|
|
38
|
-
**_kwargs,
|
|
39
|
-
) -> None:
|
|
40
|
-
self._session_id = session_id or f"voice-{uuid.uuid4().hex[:8]}"
|
|
41
|
-
self._agent_id = agent_id
|
|
42
|
-
self._env = env
|
|
43
|
-
self._task: asyncio.Task | None = None
|
|
44
|
-
self._process: asyncio.subprocess.Process | None = None
|
|
45
|
-
|
|
46
|
-
async def run(
|
|
47
|
-
self,
|
|
48
|
-
message: str,
|
|
49
|
-
cwd: str,
|
|
50
|
-
session_id: str | None = None,
|
|
51
|
-
on_output: Callable[[AgentOutput], None] | None = None,
|
|
52
|
-
can_use_tool: Callable[[str, dict, Any], Awaitable[Any]] | None = None,
|
|
53
|
-
) -> AgentResult:
|
|
54
|
-
"""Run a coding task via OpenClaw CLI."""
|
|
55
|
-
self._task = asyncio.current_task()
|
|
56
|
-
effective_session = session_id or self._session_id
|
|
57
|
-
|
|
58
|
-
full_message = f"[Working directory: {cwd}]\n\n{message}"
|
|
59
|
-
|
|
60
|
-
if on_output:
|
|
61
|
-
on_output(AgentOutput(category="text", content="🦞 Running via OpenClaw..."))
|
|
62
|
-
|
|
63
|
-
cmd = [
|
|
64
|
-
"openclaw", "agent",
|
|
65
|
-
"--session-id", effective_session,
|
|
66
|
-
"--message", full_message,
|
|
67
|
-
"--local",
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
if self._agent_id:
|
|
71
|
-
cmd.extend(["--agent", self._agent_id])
|
|
72
|
-
|
|
73
|
-
env = os.environ.copy()
|
|
74
|
-
env["PATH"] = f"/home/olaf/.npm-global/bin:/home/olaf/.local/bin:{env.get('PATH', '')}"
|
|
75
|
-
if self._env:
|
|
76
|
-
env.update(self._env)
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
self._process = await asyncio.create_subprocess_exec(
|
|
80
|
-
*cmd,
|
|
81
|
-
stdout=asyncio.subprocess.PIPE,
|
|
82
|
-
stderr=asyncio.subprocess.PIPE,
|
|
83
|
-
env=env,
|
|
84
|
-
cwd=cwd,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
result_text = ""
|
|
88
|
-
|
|
89
|
-
async def read_stdout():
|
|
90
|
-
nonlocal result_text
|
|
91
|
-
assert self._process and self._process.stdout
|
|
92
|
-
|
|
93
|
-
buffer = ""
|
|
94
|
-
while True:
|
|
95
|
-
chunk = await self._process.stdout.read(1024)
|
|
96
|
-
if not chunk:
|
|
97
|
-
break
|
|
98
|
-
|
|
99
|
-
text = chunk.decode("utf-8", errors="replace")
|
|
100
|
-
buffer += text
|
|
101
|
-
|
|
102
|
-
while "\n" in buffer:
|
|
103
|
-
line, buffer = buffer.split("\n", 1)
|
|
104
|
-
line = line.strip()
|
|
105
|
-
|
|
106
|
-
if not line:
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
data = json.loads(line)
|
|
111
|
-
if isinstance(data, dict):
|
|
112
|
-
if "result" in data:
|
|
113
|
-
result_text = data["result"]
|
|
114
|
-
elif "text" in data:
|
|
115
|
-
result_text = data["text"]
|
|
116
|
-
continue
|
|
117
|
-
except json.JSONDecodeError:
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
|
-
result_text += line + "\n"
|
|
121
|
-
|
|
122
|
-
if on_output:
|
|
123
|
-
tool_info = _extract_tool_info(line)
|
|
124
|
-
if tool_info:
|
|
125
|
-
tool_name, tool_desc = tool_info
|
|
126
|
-
category = (
|
|
127
|
-
"file_edit" if tool_name in ("Edit", "Write")
|
|
128
|
-
else "bash" if tool_name == "Bash"
|
|
129
|
-
else "tool_call"
|
|
130
|
-
)
|
|
131
|
-
on_output(AgentOutput(category=category, content=f"[{tool_name}] {tool_desc}"))
|
|
132
|
-
else:
|
|
133
|
-
on_output(AgentOutput(category="text", content=line))
|
|
134
|
-
|
|
135
|
-
if buffer.strip():
|
|
136
|
-
try:
|
|
137
|
-
data = json.loads(buffer)
|
|
138
|
-
if isinstance(data, dict) and "result" in data:
|
|
139
|
-
result_text = data["result"]
|
|
140
|
-
except json.JSONDecodeError:
|
|
141
|
-
result_text += buffer
|
|
142
|
-
|
|
143
|
-
async def read_stderr():
|
|
144
|
-
assert self._process and self._process.stderr
|
|
145
|
-
async for line in self._process.stderr:
|
|
146
|
-
text = line.decode("utf-8", errors="replace").strip()
|
|
147
|
-
if text:
|
|
148
|
-
logger.debug("stderr: %s", text)
|
|
149
|
-
|
|
150
|
-
await asyncio.gather(read_stdout(), read_stderr())
|
|
151
|
-
await self._process.wait()
|
|
152
|
-
|
|
153
|
-
if self._process.returncode != 0 and not result_text:
|
|
154
|
-
raise RuntimeError(f"OpenClaw CLI failed with exit code {self._process.returncode}")
|
|
155
|
-
|
|
156
|
-
return AgentResult(
|
|
157
|
-
text=result_text.strip(),
|
|
158
|
-
session_id=effective_session,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
except asyncio.CancelledError:
|
|
162
|
-
if self._process:
|
|
163
|
-
self._process.terminate()
|
|
164
|
-
try:
|
|
165
|
-
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
166
|
-
except asyncio.TimeoutError:
|
|
167
|
-
self._process.kill()
|
|
168
|
-
raise
|
|
169
|
-
finally:
|
|
170
|
-
self._process = None
|
|
171
|
-
|
|
172
|
-
def cancel(self) -> bool:
|
|
173
|
-
"""Cancel the running task."""
|
|
174
|
-
if self._process:
|
|
175
|
-
self._process.terminate()
|
|
176
|
-
return True
|
|
177
|
-
|
|
178
|
-
task = self._task
|
|
179
|
-
if task and not task.done():
|
|
180
|
-
task.cancel()
|
|
181
|
-
return True
|
|
182
|
-
|
|
183
|
-
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/code_providers/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.1 → voice_vibecoder-2.9.1}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|