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.
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/CHANGELOG.md +8 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/PKG-INFO +1 -1
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/pyproject.toml +1 -1
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/__init__.py +1 -1
- voice_vibecoder-2.8.1/src/voice_vibecoder/code_providers/openclaw.py +183 -0
- voice_vibecoder-2.8.0/src/voice_vibecoder/code_providers/openclaw.py +0 -369
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/.gitignore +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/LICENSE +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/README.md +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/app.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/index.html +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/session.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
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.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.0 → voice_vibecoder-2.8.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.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.8.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|