voice-vibecoder 2.8.0__tar.gz → 2.8.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.
Files changed (44) hide show
  1. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/CHANGELOG.md +8 -0
  2. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/__init__.py +1 -1
  5. voice_vibecoder-2.8.1/src/voice_vibecoder/code_providers/openclaw.py +183 -0
  6. voice_vibecoder-2.8.0/src/voice_vibecoder/code_providers/openclaw.py +0 -369
  7. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.github/workflows/ci.yml +0 -0
  8. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.github/workflows/release.yml +0 -0
  9. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.gitignore +0 -0
  10. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/LICENSE +0 -0
  11. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/README.md +0 -0
  12. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/app.py +0 -0
  13. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/index.html +0 -0
  14. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/app_config.py +0 -0
  15. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/audio.py +0 -0
  16. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/cli_serve.py +0 -0
  17. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/registry.py +0 -0
  21. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/config.py +0 -0
  22. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/instances.py +0 -0
  23. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/server.py +0 -0
  24. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/session.py +0 -0
  25. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/__init__.py +0 -0
  26. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/agent_task.py +0 -0
  27. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/definitions.py +0 -0
  28. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/dispatch.py +0 -0
  29. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/file_tree.py +0 -0
  30. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/handlers.py +0 -0
  31. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/__init__.py +0 -0
  32. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/callbacks.py +0 -0
  33. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/diff_view.py +0 -0
  34. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/help_modal.py +0 -0
  35. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/panels.py +0 -0
  36. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/screen.py +0 -0
  37. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/setup.py +0 -0
  38. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/state.py +0 -0
  39. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/styles.py +0 -0
  40. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/update_modal.py +0 -0
  41. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/wizard.py +0 -0
  42. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  43. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.8.1 (2026-02-21)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Simplify OpenClaw provider to use CLI only
10
+ ([`dfe92c8`](https://github.com/snokam/voice-vibecoder/commit/dfe92c888e390bb5b50a5594452d276f2ff2a8d8))
11
+
12
+
5
13
  ## v2.8.0 (2026-02-20)
6
14
 
7
15
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.8.0
3
+ Version: 2.8.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "voice-vibecoder"
7
- version = "2.8.0"
7
+ version = "2.8.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"
@@ -3,7 +3,7 @@
3
3
  A voice-controlled coding assistant using OpenAI Realtime API + Claude Code.
4
4
  """
5
5
 
6
- __version__ = "2.8.0"
6
+ __version__ = "2.8.1"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -0,0 +1,183 @@
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
@@ -1,369 +0,0 @@
1
- """OpenClaw Agent code provider.
2
-
3
- Implements the AgentRunner protocol using the OpenClaw Gateway WebSocket
4
- for streaming code agent interactions. Routes coding tasks to the
5
- OpenClaw gateway which can use any configured model and tools.
6
-
7
- This provider enables voice-vibecoder to use OpenClaw (Olaf) as the
8
- coding agent instead of Claude Code CLI. Benefits:
9
- - Same agent identity across all channels (Slack, voice, web)
10
- - Access to all MCP tools (Jira, GitHub, CVPartner, Snøkam APIs)
11
- - Persistent memory and context
12
- - Any model backend (Claude, GPT-4, etc.)
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import asyncio
18
- import json
19
- import logging
20
- import os
21
- import re
22
- import secrets
23
- import uuid
24
- from typing import Any, Awaitable, Callable
25
-
26
- from voice_vibecoder.code_providers import AgentOutput, AgentResult
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
- # Default OpenClaw Gateway URL (loopback)
31
- DEFAULT_GATEWAY_URL = "ws://localhost:18789"
32
-
33
-
34
- def _extract_tool_info(line: str) -> tuple[str, str] | None:
35
- """Extract tool name and description from agent output lines."""
36
- match = re.match(r"^\[(\w+)\]\s*(.*)$", line.strip())
37
- if match:
38
- return match.group(1), match.group(2)
39
- return None
40
-
41
-
42
- class OpenClawRunner:
43
- """AgentRunner implementation using OpenClaw Gateway WebSocket.
44
-
45
- This provider routes coding tasks to OpenClaw, which can:
46
- - Use any configured model (Claude, GPT-4, etc.)
47
- - Access all MCP tools (Jira, GitHub, CVPartner, etc.)
48
- - Maintain conversation context across sessions
49
- - Use the same identity/persona as the main agent
50
- """
51
-
52
- def __init__(
53
- self,
54
- session_id: str | None = None,
55
- agent_id: str | None = None,
56
- env: dict[str, str] | None = None,
57
- gateway_url: str | None = None,
58
- gateway_token: str | None = None,
59
- **_kwargs,
60
- ) -> None:
61
- self._session_id = session_id or f"voice-{uuid.uuid4().hex[:8]}"
62
- self._agent_id = agent_id
63
- self._env = env
64
- self._gateway_url = gateway_url or os.environ.get("OPENCLAW_GATEWAY_URL", DEFAULT_GATEWAY_URL)
65
- self._gateway_token = gateway_token or os.environ.get("OPENCLAW_GATEWAY_TOKEN")
66
- self._task: asyncio.Task | None = None
67
- self._ws = None
68
-
69
- async def run(
70
- self,
71
- message: str,
72
- cwd: str,
73
- session_id: str | None = None,
74
- on_output: Callable[[AgentOutput], None] | None = None,
75
- can_use_tool: Callable[[str, dict, Any], Awaitable[Any]] | None = None,
76
- ) -> AgentResult:
77
- """Run a coding task via OpenClaw Gateway WebSocket."""
78
- self._task = asyncio.current_task()
79
- effective_session = session_id or self._session_id
80
-
81
- # Add working directory context
82
- full_message = f"[Working directory: {cwd}]\n\n{message}"
83
-
84
- if on_output:
85
- on_output(AgentOutput(
86
- category="text",
87
- content=f"🦞 Connecting to OpenClaw...",
88
- ))
89
-
90
- try:
91
- # Try WebSocket first, fall back to CLI if it fails
92
- result = await self._run_via_websocket(
93
- full_message, effective_session, on_output, can_use_tool
94
- )
95
- except Exception as ws_error:
96
- logger.warning("WebSocket connection failed, falling back to CLI: %s", ws_error)
97
- if on_output:
98
- on_output(AgentOutput(
99
- category="text",
100
- content="WebSocket unavailable, using CLI...",
101
- ))
102
- result = await self._run_via_cli(
103
- full_message, cwd, effective_session, on_output
104
- )
105
-
106
- return result
107
-
108
- async def _run_via_websocket(
109
- self,
110
- message: str,
111
- session_id: str,
112
- on_output: Callable[[AgentOutput], None] | None,
113
- can_use_tool: Callable[[str, dict, Any], Awaitable[Any]] | None,
114
- ) -> AgentResult:
115
- """Connect to OpenClaw Gateway via WebSocket and stream the response."""
116
- import websockets
117
-
118
- result_text = ""
119
- result_session_id = session_id
120
-
121
- async with websockets.connect(self._gateway_url) as ws:
122
- self._ws = ws
123
-
124
- # Wait for challenge
125
- challenge_raw = await asyncio.wait_for(ws.recv(), timeout=5.0)
126
- challenge = json.loads(challenge_raw)
127
-
128
- if challenge.get("event") != "connect.challenge":
129
- raise ValueError(f"Expected connect.challenge, got: {challenge}")
130
-
131
- # Generate device identity
132
- device_id = f"voice-vibecoder-{uuid.uuid4().hex[:8]}"
133
-
134
- # Send connect request
135
- connect_req = {
136
- "type": "req",
137
- "id": secrets.token_hex(8),
138
- "method": "connect",
139
- "params": {
140
- "minProtocol": 3,
141
- "maxProtocol": 3,
142
- "client": {
143
- "id": "voice-vibecoder",
144
- "version": "1.0.0",
145
- "platform": "linux",
146
- "mode": "operator",
147
- },
148
- "role": "operator",
149
- "scopes": ["operator.read", "operator.write"],
150
- "caps": [],
151
- "commands": [],
152
- "permissions": {},
153
- "auth": {"token": self._gateway_token} if self._gateway_token else {},
154
- "locale": "en-US",
155
- "userAgent": "voice-vibecoder/1.0.0",
156
- "device": {
157
- "id": device_id,
158
- },
159
- },
160
- }
161
- await ws.send(json.dumps(connect_req))
162
-
163
- # Wait for hello-ok
164
- hello_raw = await asyncio.wait_for(ws.recv(), timeout=5.0)
165
- hello = json.loads(hello_raw)
166
-
167
- if not hello.get("ok"):
168
- error = hello.get("error", {}).get("message", "Unknown error")
169
- raise ValueError(f"Connect failed: {error}")
170
-
171
- if on_output:
172
- on_output(AgentOutput(category="text", content="Connected to OpenClaw"))
173
-
174
- # Send chat message
175
- chat_req = {
176
- "type": "req",
177
- "id": secrets.token_hex(8),
178
- "method": "chat.send",
179
- "params": {
180
- "message": message,
181
- "sessionKey": session_id,
182
- },
183
- }
184
-
185
- if self._agent_id:
186
- chat_req["params"]["agentId"] = self._agent_id
187
-
188
- await ws.send(json.dumps(chat_req))
189
-
190
- # Stream response
191
- while True:
192
- try:
193
- msg_raw = await asyncio.wait_for(ws.recv(), timeout=600.0)
194
- msg = json.loads(msg_raw)
195
- except asyncio.TimeoutError:
196
- break
197
-
198
- msg_type = msg.get("type")
199
-
200
- if msg_type == "event":
201
- event = msg.get("event", "")
202
- payload = msg.get("payload", {})
203
-
204
- if event == "chat.chunk":
205
- # Streaming text chunk
206
- text = payload.get("text", "")
207
- if text and on_output:
208
- for line in text.splitlines():
209
- if line.strip():
210
- tool_info = _extract_tool_info(line)
211
- if tool_info:
212
- tool_name, tool_desc = tool_info
213
- category = (
214
- "file_edit" if tool_name in ("Edit", "Write")
215
- else "bash" if tool_name == "Bash"
216
- else "tool_call"
217
- )
218
- on_output(AgentOutput(category=category, content=line))
219
- else:
220
- on_output(AgentOutput(category="text", content=line))
221
- result_text += text
222
-
223
- elif event == "chat.done":
224
- # Final response
225
- result_text = payload.get("text", result_text)
226
- result_session_id = payload.get("sessionKey", session_id)
227
- break
228
-
229
- elif event == "chat.error":
230
- error = payload.get("message", "Unknown error")
231
- raise RuntimeError(f"OpenClaw error: {error}")
232
-
233
- elif msg_type == "res":
234
- # Response to our request
235
- if not msg.get("ok"):
236
- error = msg.get("error", {}).get("message", "Unknown error")
237
- raise RuntimeError(f"Request failed: {error}")
238
-
239
- # chat.send returns immediately, response comes via events
240
- payload = msg.get("payload", {})
241
- if "text" in payload:
242
- result_text = payload["text"]
243
- break
244
-
245
- self._ws = None
246
-
247
- return AgentResult(
248
- text=result_text,
249
- session_id=result_session_id,
250
- )
251
-
252
- async def _run_via_cli(
253
- self,
254
- message: str,
255
- cwd: str,
256
- session_id: str,
257
- on_output: Callable[[AgentOutput], None] | None,
258
- ) -> AgentResult:
259
- """Fallback: run via openclaw CLI."""
260
- cmd = [
261
- "openclaw", "agent",
262
- "--session-id", session_id,
263
- "--message", message,
264
- "--json",
265
- "--local",
266
- ]
267
-
268
- if self._agent_id:
269
- cmd.extend(["--agent", self._agent_id])
270
-
271
- env = os.environ.copy()
272
- if self._env:
273
- env.update(self._env)
274
- if self._gateway_token:
275
- env["OPENCLAW_GATEWAY_TOKEN"] = self._gateway_token
276
-
277
- process = await asyncio.create_subprocess_exec(
278
- *cmd,
279
- stdout=asyncio.subprocess.PIPE,
280
- stderr=asyncio.subprocess.PIPE,
281
- env=env,
282
- cwd=cwd,
283
- )
284
-
285
- result_text = ""
286
-
287
- async def read_stdout():
288
- nonlocal result_text
289
- assert process.stdout
290
-
291
- buffer = ""
292
- while True:
293
- chunk = await process.stdout.read(1024)
294
- if not chunk:
295
- break
296
-
297
- text = chunk.decode("utf-8", errors="replace")
298
- buffer += text
299
-
300
- while "\n" in buffer:
301
- line, buffer = buffer.split("\n", 1)
302
- line = line.strip()
303
-
304
- if not line:
305
- continue
306
-
307
- try:
308
- data = json.loads(line)
309
- if isinstance(data, dict):
310
- if "result" in data:
311
- result_text = data["result"]
312
- elif "text" in data:
313
- result_text = data["text"]
314
- continue
315
- except json.JSONDecodeError:
316
- pass
317
-
318
- if on_output:
319
- tool_info = _extract_tool_info(line)
320
- if tool_info:
321
- tool_name, tool_desc = tool_info
322
- category = (
323
- "file_edit" if tool_name in ("Edit", "Write")
324
- else "bash" if tool_name == "Bash"
325
- else "tool_call"
326
- )
327
- on_output(AgentOutput(category=category, content=f"[{tool_name}] {tool_desc}"))
328
- else:
329
- on_output(AgentOutput(category="text", content=line))
330
-
331
- if buffer.strip():
332
- try:
333
- data = json.loads(buffer)
334
- if isinstance(data, dict) and "result" in data:
335
- result_text = data["result"]
336
- except json.JSONDecodeError:
337
- if on_output:
338
- on_output(AgentOutput(category="text", content=buffer.strip()))
339
-
340
- async def read_stderr():
341
- assert process.stderr
342
- async for line in process.stderr:
343
- text = line.decode("utf-8", errors="replace").strip()
344
- if text and on_output:
345
- on_output(AgentOutput(category="text", content=f"[stderr] {text}"))
346
-
347
- await asyncio.gather(read_stdout(), read_stderr())
348
- await process.wait()
349
-
350
- if process.returncode != 0 and not result_text:
351
- raise RuntimeError(f"OpenClaw CLI failed with exit code {process.returncode}")
352
-
353
- return AgentResult(
354
- text=result_text,
355
- session_id=session_id,
356
- )
357
-
358
- def cancel(self) -> bool:
359
- """Cancel the running task."""
360
- if self._ws:
361
- asyncio.create_task(self._ws.close())
362
- return True
363
-
364
- task = self._task
365
- if task and not task.done():
366
- task.cancel()
367
- return True
368
-
369
- return False
File without changes
File without changes