voice-vibecoder 2.5.3__tar.gz → 2.6.0__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.5.3 → voice_vibecoder-2.6.0}/CHANGELOG.md +13 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/PKG-INFO +1 -1
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/pyproject.toml +1 -1
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/instances.py +6 -12
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/session.py +4 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/agent_task.py +53 -38
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/file_tree.py +24 -42
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/.gitignore +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/LICENSE +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/README.md +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/app.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/index.html +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.6.0 (2026-02-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **agent_task**: Support matching by display name in session lookup
|
|
10
|
+
([`a97e0e6`](https://github.com/snokam/voice-vibecoder/commit/a97e0e67fdf7178ddee182acc275957a471e2691))
|
|
11
|
+
|
|
12
|
+
### Refactoring
|
|
13
|
+
|
|
14
|
+
- **instances**: Simplify branch key assignment logic
|
|
15
|
+
([`58f5d34`](https://github.com/snokam/voice-vibecoder/commit/58f5d3475d1696dd3b92252e3843ee2e4a04826b))
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## v2.5.3 (2026-02-20)
|
|
6
19
|
|
|
7
20
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
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.
|
|
7
|
+
version = "2.6.0"
|
|
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"
|
|
@@ -125,9 +125,6 @@ class InstanceRegistry:
|
|
|
125
125
|
self.on_status_change(instance.instance_id, instance.status)
|
|
126
126
|
if self.on_instance_change:
|
|
127
127
|
self.on_instance_change()
|
|
128
|
-
# Start file tree updates for code instances (skip for sessions)
|
|
129
|
-
from voice_vibecoder.tools.file_tree import start_file_tree_updates
|
|
130
|
-
start_file_tree_updates(instance.instance_id, worktree_path, self)
|
|
131
128
|
return instance
|
|
132
129
|
|
|
133
130
|
def create_session(self, name: str, agent_type: str = "claude") -> AgentInstance:
|
|
@@ -136,16 +133,20 @@ class InstanceRegistry:
|
|
|
136
133
|
if existing:
|
|
137
134
|
return existing
|
|
138
135
|
with self._lock:
|
|
136
|
+
# Re-check under lock to prevent race between concurrent creates
|
|
137
|
+
branch_key = f"_session_{name}"
|
|
138
|
+
if branch_key in self._branch_index:
|
|
139
|
+
return self._instances[self._branch_index[branch_key]]
|
|
139
140
|
instance = AgentInstance(
|
|
140
141
|
instance_id=uuid.uuid4().hex[:8],
|
|
141
|
-
branch=
|
|
142
|
+
branch=branch_key,
|
|
142
143
|
worktree_path="",
|
|
143
144
|
agent_type=agent_type,
|
|
144
145
|
instance_type=InstanceType.SESSION,
|
|
145
146
|
display_name=name,
|
|
146
147
|
)
|
|
147
148
|
self._instances[instance.instance_id] = instance
|
|
148
|
-
self._branch_index[
|
|
149
|
+
self._branch_index[branch_key] = instance.instance_id
|
|
149
150
|
self._save_state()
|
|
150
151
|
if self.on_status_change:
|
|
151
152
|
self.on_status_change(instance.instance_id, instance.status)
|
|
@@ -415,11 +416,4 @@ class InstanceRegistry:
|
|
|
415
416
|
self._branch_index[branch] = instance.instance_id
|
|
416
417
|
loaded.append(instance)
|
|
417
418
|
|
|
418
|
-
# Start file tree updates for code instances only
|
|
419
|
-
from voice_vibecoder.tools.file_tree import start_file_tree_updates
|
|
420
|
-
for inst in loaded:
|
|
421
|
-
if inst.instance_type == InstanceType.SESSION:
|
|
422
|
-
continue
|
|
423
|
-
start_file_tree_updates(inst.instance_id, inst.worktree_path, self)
|
|
424
|
-
|
|
425
419
|
return loaded
|
|
@@ -128,8 +128,12 @@ class RealtimeSession:
|
|
|
128
128
|
self._on_status("connected")
|
|
129
129
|
|
|
130
130
|
async def configure_session(self) -> None:
|
|
131
|
+
from voice_vibecoder.tools.agent_task import warm_mcp_cache
|
|
131
132
|
from voice_vibecoder.tools.handlers import get_active_instances_text, get_startup_context
|
|
132
133
|
|
|
134
|
+
# Pre-warm MCP server cache so first agent spawn doesn't pay the cost
|
|
135
|
+
warm_mcp_cache()
|
|
136
|
+
|
|
133
137
|
s = self._settings
|
|
134
138
|
overrides = self._voice_config.system_instructions if self._voice_config else None
|
|
135
139
|
instructions = get_system_instructions(
|
|
@@ -23,10 +23,11 @@ _CLAUDE_JSON = Path.home() / ".claude.json"
|
|
|
23
23
|
|
|
24
24
|
# Cached MCP servers — loaded once per session, files rarely change
|
|
25
25
|
_mcp_cache: dict[str, dict] | None = None
|
|
26
|
+
_mcp_lock = threading.Lock()
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def _load_mcp_servers() -> dict[str, dict]:
|
|
29
|
-
"""Load MCP servers (cached after first call).
|
|
30
|
+
"""Load MCP servers (cached after first call, thread-safe).
|
|
30
31
|
|
|
31
32
|
Priority order:
|
|
32
33
|
1. MCP_SERVERS_JSON environment variable (for Azure/production)
|
|
@@ -37,50 +38,60 @@ def _load_mcp_servers() -> dict[str, dict]:
|
|
|
37
38
|
if _mcp_cache is not None:
|
|
38
39
|
return _mcp_cache
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
with _mcp_lock:
|
|
42
|
+
# Double-check after acquiring the lock
|
|
43
|
+
if _mcp_cache is not None:
|
|
44
|
+
return _mcp_cache
|
|
45
|
+
|
|
46
|
+
import os
|
|
47
|
+
from voice_vibecoder.tools.dispatch import _repo_root
|
|
48
|
+
|
|
49
|
+
servers: dict[str, dict] = {}
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
# Load from .mcp.json in repo root
|
|
52
|
+
if _repo_root:
|
|
53
|
+
mcp_json = Path(_repo_root) / ".mcp.json"
|
|
54
|
+
if mcp_json.exists():
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(mcp_json.read_text())
|
|
57
|
+
servers.update(data.get("mcpServers", {}))
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.warning("Failed to read .mcp.json: %s", e)
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
mcp_json = Path(_repo_root) / ".mcp.json"
|
|
48
|
-
if mcp_json.exists():
|
|
61
|
+
# Load from ~/.claude.json
|
|
62
|
+
if _repo_root and _CLAUDE_JSON.exists():
|
|
49
63
|
try:
|
|
50
|
-
data = json.loads(
|
|
51
|
-
|
|
64
|
+
data = json.loads(_CLAUDE_JSON.read_text())
|
|
65
|
+
project_key = str(Path(_repo_root))
|
|
66
|
+
project_data = data.get("projects", {}).get(project_key, {})
|
|
67
|
+
local_servers = project_data.get("mcpServers", {})
|
|
68
|
+
servers.update(local_servers)
|
|
52
69
|
except Exception as e:
|
|
53
|
-
logger.warning("Failed to read .
|
|
70
|
+
logger.warning("Failed to read ~/.claude.json: %s", e)
|
|
54
71
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.warning("Failed to parse MCP_SERVERS_JSON: %s", e)
|
|
72
|
+
# MCP_SERVERS_JSON takes precedence (for Azure Container Apps)
|
|
73
|
+
mcp_env = os.environ.get("MCP_SERVERS_JSON")
|
|
74
|
+
if mcp_env:
|
|
75
|
+
try:
|
|
76
|
+
env_servers = json.loads(mcp_env)
|
|
77
|
+
servers.update(env_servers.get("mcpServers", env_servers))
|
|
78
|
+
logger.info("Loaded MCP servers from MCP_SERVERS_JSON environment variable")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.warning("Failed to parse MCP_SERVERS_JSON: %s", e)
|
|
81
|
+
|
|
82
|
+
# Ensure each MCP server inherits process env vars (the Claude CLI may
|
|
83
|
+
# not forward its own env to MCP server subprocesses).
|
|
84
|
+
for _name, cfg in servers.items():
|
|
85
|
+
if "env" not in cfg:
|
|
86
|
+
cfg["env"] = dict(os.environ)
|
|
87
|
+
|
|
88
|
+
_mcp_cache = servers
|
|
89
|
+
return servers
|
|
75
90
|
|
|
76
|
-
# Ensure each MCP server inherits process env vars (the Claude CLI may
|
|
77
|
-
# not forward its own env to MCP server subprocesses).
|
|
78
|
-
for _name, cfg in servers.items():
|
|
79
|
-
if "env" not in cfg:
|
|
80
|
-
cfg["env"] = dict(os.environ)
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
def warm_mcp_cache() -> None:
|
|
93
|
+
"""Pre-load MCP server config so it's ready when agents spawn."""
|
|
94
|
+
_load_mcp_servers()
|
|
84
95
|
|
|
85
96
|
|
|
86
97
|
def _create_runner(agent_type: str, permission_mode: str, mcp_servers: dict | None, env: dict | None = None) -> AgentRunner:
|
|
@@ -198,6 +209,10 @@ def _handle_send_to_session(message: str, session_name: str | None = None, agent
|
|
|
198
209
|
session_name = "session"
|
|
199
210
|
|
|
200
211
|
instance = _registry.get_session_by_name(session_name)
|
|
212
|
+
# Also try matching by display_name (LLM may use different session keys
|
|
213
|
+
# for the same logical session, e.g. "calendar-today" vs "today-s-calendar")
|
|
214
|
+
if not instance and display_name:
|
|
215
|
+
instance = _registry.get_session_by_name(display_name)
|
|
201
216
|
is_new_session = instance is None
|
|
202
217
|
if not instance:
|
|
203
218
|
instance = _registry.create_session(session_name, agent_type=target_agent)
|
|
@@ -9,6 +9,7 @@ timer while an agent is running.
|
|
|
9
9
|
import logging
|
|
10
10
|
import subprocess
|
|
11
11
|
import threading
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
13
|
from pathlib import PurePosixPath
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -16,16 +17,15 @@ logger = logging.getLogger(__name__)
|
|
|
16
17
|
MAX_DEPTH = 10 # Maximum tree depth to prevent excessive nesting
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
["git",
|
|
23
|
-
cwd=
|
|
20
|
+
def _git(args: list[str], cwd: str) -> subprocess.CompletedProcess:
|
|
21
|
+
"""Run a git command and return the CompletedProcess."""
|
|
22
|
+
return subprocess.run(
|
|
23
|
+
["git", *args],
|
|
24
|
+
cwd=cwd,
|
|
24
25
|
capture_output=True,
|
|
25
26
|
text=True,
|
|
26
27
|
timeout=10,
|
|
27
28
|
)
|
|
28
|
-
return result.stdout.strip() if result.returncode == 0 else "main"
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _build_tree_node(path: str, status: str | None, is_folder: bool) -> dict:
|
|
@@ -112,26 +112,25 @@ def get_file_tree(worktree_path: str) -> dict:
|
|
|
112
112
|
"summary": {"modified": N, "new": N, "deleted": N, "unchanged": N}}
|
|
113
113
|
"""
|
|
114
114
|
try:
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
# Run independent git commands in parallel, then diff after merge-base
|
|
116
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
117
|
+
branch_fut = pool.submit(_git, ["rev-parse", "--abbrev-ref", "HEAD"], worktree_path)
|
|
118
|
+
base_fut = pool.submit(_git, ["merge-base", "main", "HEAD"], worktree_path)
|
|
119
|
+
untracked_fut = pool.submit(_git, ["ls-files", "--others", "--exclude-standard"], worktree_path)
|
|
120
|
+
all_files_fut = pool.submit(_git, ["ls-files"], worktree_path)
|
|
121
|
+
|
|
122
|
+
# Wait for merge-base, then submit diff (overlaps with ls-files)
|
|
123
|
+
base_result = base_fut.result()
|
|
124
|
+
base = base_result.stdout.strip() if base_result.returncode == 0 else "main"
|
|
125
|
+
diff_fut = pool.submit(_git, ["diff", "--name-status", base], worktree_path)
|
|
126
|
+
|
|
127
|
+
# Collect remaining results
|
|
128
|
+
branch_result = branch_fut.result()
|
|
129
|
+
diff_result = diff_fut.result()
|
|
130
|
+
untracked_result = untracked_fut.result()
|
|
131
|
+
all_files_result = all_files_fut.result()
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Get modified/deleted files vs merge-base
|
|
128
|
-
diff_result = subprocess.run(
|
|
129
|
-
["git", "diff", "--name-status", base],
|
|
130
|
-
cwd=worktree_path,
|
|
131
|
-
capture_output=True,
|
|
132
|
-
text=True,
|
|
133
|
-
timeout=10,
|
|
134
|
-
)
|
|
133
|
+
root = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
|
|
135
134
|
|
|
136
135
|
changed: dict[str, str] = {} # path -> status
|
|
137
136
|
if diff_result.returncode == 0:
|
|
@@ -151,29 +150,12 @@ def get_file_tree(worktree_path: str) -> dict:
|
|
|
151
150
|
elif status_code in ("R", "C"):
|
|
152
151
|
changed[path] = "modified"
|
|
153
152
|
|
|
154
|
-
# Get untracked files
|
|
155
|
-
untracked_result = subprocess.run(
|
|
156
|
-
["git", "ls-files", "--others", "--exclude-standard"],
|
|
157
|
-
cwd=worktree_path,
|
|
158
|
-
capture_output=True,
|
|
159
|
-
text=True,
|
|
160
|
-
timeout=10,
|
|
161
|
-
)
|
|
162
153
|
if untracked_result.returncode == 0:
|
|
163
154
|
for path in untracked_result.stdout.strip().splitlines():
|
|
164
155
|
path = path.strip()
|
|
165
156
|
if path and path not in changed:
|
|
166
157
|
changed[path] = "new"
|
|
167
158
|
|
|
168
|
-
# Get all tracked files to show complete project structure
|
|
169
|
-
all_files_result = subprocess.run(
|
|
170
|
-
["git", "ls-files"],
|
|
171
|
-
cwd=worktree_path,
|
|
172
|
-
capture_output=True,
|
|
173
|
-
text=True,
|
|
174
|
-
timeout=10,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
159
|
all_paths: dict[str, str] = {} # path -> status
|
|
178
160
|
if all_files_result.returncode == 0:
|
|
179
161
|
for f in all_files_result.stdout.strip().splitlines():
|
|
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.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/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
|
{voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.5.3 → voice_vibecoder-2.6.0}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|