voice-vibecoder 2.3.0__tar.gz → 2.5.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.3.0 → voice_vibecoder-2.5.0}/CHANGELOG.md +16 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/PKG-INFO +1 -2
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/pyproject.toml +1 -2
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/app_config.py +7 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/cli_serve.py +0 -2
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/server.py +16 -94
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/definitions.py +1 -1
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/.gitignore +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/LICENSE +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/README.md +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/app.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/index.html +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/session.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.5.0 (2026-02-19)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **tools**: Require display_name for send_to_session
|
|
10
|
+
([`d50f71d`](https://github.com/snokam/voice-vibecoder/commit/d50f71dafc624ebe17f00bcf9880ec546786cbe9))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v2.4.0 (2026-02-19)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **server**: Handle unexpected errors during authentication
|
|
18
|
+
([`f0ac102`](https://github.com/snokam/voice-vibecoder/commit/f0ac1025cac939f8fc048c796d63668c1d687f9e))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v2.3.0 (2026-02-19)
|
|
6
22
|
|
|
7
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -23,7 +23,6 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Requires-Dist: claude-agent-sdk>=0.1.30
|
|
24
24
|
Requires-Dist: numpy>=1.24
|
|
25
25
|
Requires-Dist: platformdirs>=4.0
|
|
26
|
-
Requires-Dist: pyjwt[crypto]>=2.8
|
|
27
26
|
Requires-Dist: rich>=13.0
|
|
28
27
|
Requires-Dist: sounddevice>=0.4
|
|
29
28
|
Requires-Dist: textual>=0.50
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voice-vibecoder"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.5.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"
|
|
@@ -31,7 +31,6 @@ dependencies = [
|
|
|
31
31
|
"numpy>=1.24",
|
|
32
32
|
"claude-agent-sdk>=0.1.30",
|
|
33
33
|
"platformdirs>=4.0",
|
|
34
|
-
"PyJWT[crypto]>=2.8",
|
|
35
34
|
]
|
|
36
35
|
|
|
37
36
|
[project.urls]
|
|
@@ -9,6 +9,7 @@ Embedders can override any directory to colocate everything (e.g. ~/.snokam/).
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from collections.abc import Awaitable
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Callable
|
|
@@ -65,3 +66,9 @@ class VoiceConfig:
|
|
|
65
66
|
# Called before an agent task runs. Receives (mcp_servers: dict, user_jwt: str | None)
|
|
66
67
|
# and can mutate mcp_servers in place (e.g. inject OBO tokens).
|
|
67
68
|
pre_agent_hook: Callable[[dict, str | None], None] | None = None
|
|
69
|
+
|
|
70
|
+
# Async callback for WebSocket authentication.
|
|
71
|
+
# Receives the raw token string, returns (user_email, raw_token).
|
|
72
|
+
# Raises ValueError on validation failure.
|
|
73
|
+
# When None, auth is disabled (no auth frame expected from clients).
|
|
74
|
+
authenticate: Callable[[str], Awaitable[tuple[str, str]]] | None = None
|
|
@@ -61,7 +61,6 @@ def main(voice_config: VoiceConfig | None = None) -> None:
|
|
|
61
61
|
parser.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
|
|
62
62
|
parser.add_argument("--port", type=int, default=8765, help="Port (default: 8765)")
|
|
63
63
|
parser.add_argument("--repo", type=str, default=None, help="Repo root (auto-detects git root)")
|
|
64
|
-
parser.add_argument("--no-auth", action="store_true", help="Disable JWT authentication (for local dev)")
|
|
65
64
|
parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging")
|
|
66
65
|
args = parser.parse_args()
|
|
67
66
|
|
|
@@ -94,7 +93,6 @@ def main(voice_config: VoiceConfig | None = None) -> None:
|
|
|
94
93
|
repo_root=repo_root,
|
|
95
94
|
voice_config=config,
|
|
96
95
|
settings=settings,
|
|
97
|
-
no_auth=args.no_auth,
|
|
98
96
|
))
|
|
99
97
|
|
|
100
98
|
|
|
@@ -29,12 +29,9 @@ import asyncio
|
|
|
29
29
|
import json
|
|
30
30
|
import logging
|
|
31
31
|
import os
|
|
32
|
-
import time
|
|
33
32
|
from pathlib import Path
|
|
34
33
|
from typing import Any
|
|
35
|
-
from urllib.parse import urlparse
|
|
36
34
|
|
|
37
|
-
import jwt
|
|
38
35
|
import websockets
|
|
39
36
|
from websockets.asyncio.server import ServerConnection
|
|
40
37
|
from websockets.http11 import Request, Response
|
|
@@ -47,70 +44,6 @@ from voice_vibecoder.voice_providers.openai import OpenAIVoiceProvider
|
|
|
47
44
|
|
|
48
45
|
logger = logging.getLogger(__name__)
|
|
49
46
|
|
|
50
|
-
# ── JWT validation for Azure AD ──────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
_JWKS_CACHE_TTL = 3600 # 1 hour
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class _JWKSEntry:
|
|
56
|
-
__slots__ = ("client", "fetched_at")
|
|
57
|
-
|
|
58
|
-
def __init__(self, client: jwt.PyJWKClient, fetched_at: float) -> None:
|
|
59
|
-
self.client = client
|
|
60
|
-
self.fetched_at = fetched_at
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
_jwks_cache: dict[str, _JWKSEntry] = {}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _get_jwks_client(tenant_id: str) -> jwt.PyJWKClient:
|
|
67
|
-
"""Return a cached JWKS client for the given Azure AD tenant."""
|
|
68
|
-
now = time.monotonic()
|
|
69
|
-
entry = _jwks_cache.get(tenant_id)
|
|
70
|
-
if entry and (now - entry.fetched_at) < _JWKS_CACHE_TTL:
|
|
71
|
-
return entry.client
|
|
72
|
-
|
|
73
|
-
jwks_url = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
|
74
|
-
client = jwt.PyJWKClient(jwks_url)
|
|
75
|
-
_jwks_cache[tenant_id] = _JWKSEntry(client, now)
|
|
76
|
-
return client
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def validate_token(token: str, tenant_id: str, audience: str) -> dict:
|
|
80
|
-
"""Validate an Azure AD JWT and return decoded claims.
|
|
81
|
-
|
|
82
|
-
Raises ValueError if the token is invalid.
|
|
83
|
-
"""
|
|
84
|
-
try:
|
|
85
|
-
jwks_client = _get_jwks_client(tenant_id)
|
|
86
|
-
signing_key = jwks_client.get_signing_key_from_jwt(token)
|
|
87
|
-
# Accept both v1 (aud=client_id) and v2 (aud=api://client_id) tokens,
|
|
88
|
-
# and both v1/v2 issuer formats.
|
|
89
|
-
audiences = [audience]
|
|
90
|
-
if audience.startswith("api://"):
|
|
91
|
-
audiences.append(audience[len("api://"):])
|
|
92
|
-
issuers = [
|
|
93
|
-
f"https://login.microsoftonline.com/{tenant_id}/v2.0",
|
|
94
|
-
f"https://sts.windows.net/{tenant_id}/",
|
|
95
|
-
]
|
|
96
|
-
claims = jwt.decode(
|
|
97
|
-
token,
|
|
98
|
-
signing_key.key,
|
|
99
|
-
algorithms=["RS256"],
|
|
100
|
-
audience=audiences,
|
|
101
|
-
issuer=issuers,
|
|
102
|
-
)
|
|
103
|
-
return claims
|
|
104
|
-
except jwt.ExpiredSignatureError:
|
|
105
|
-
raise ValueError("Token has expired")
|
|
106
|
-
except jwt.InvalidAudienceError:
|
|
107
|
-
raise ValueError("Invalid audience")
|
|
108
|
-
except jwt.InvalidIssuerError:
|
|
109
|
-
raise ValueError("Invalid issuer")
|
|
110
|
-
except Exception as exc:
|
|
111
|
-
raise ValueError(f"Token validation failed: {exc}")
|
|
112
|
-
|
|
113
|
-
|
|
114
47
|
# ── Auth timeout ─────────────────────────────────────────────────────────────
|
|
115
48
|
|
|
116
49
|
AUTH_TIMEOUT_SECONDS = 10
|
|
@@ -313,11 +246,11 @@ def _log_send_error(future: asyncio.Future) -> None:
|
|
|
313
246
|
|
|
314
247
|
async def _authenticate_ws(
|
|
315
248
|
ws: ServerConnection,
|
|
316
|
-
|
|
317
|
-
audience: str,
|
|
249
|
+
authenticate: Any,
|
|
318
250
|
) -> tuple[str, str] | None:
|
|
319
251
|
"""Wait for an auth message as the first frame.
|
|
320
252
|
|
|
253
|
+
*authenticate* is the ``VoiceConfig.authenticate`` async callback.
|
|
321
254
|
Returns ``(user_email, raw_token)`` on success, ``None`` on failure.
|
|
322
255
|
"""
|
|
323
256
|
try:
|
|
@@ -338,15 +271,18 @@ async def _authenticate_ws(
|
|
|
338
271
|
token = token[7:]
|
|
339
272
|
|
|
340
273
|
try:
|
|
341
|
-
|
|
274
|
+
result = await authenticate(token)
|
|
342
275
|
except ValueError as exc:
|
|
343
|
-
logger.warning("
|
|
276
|
+
logger.warning("Authentication failed: %s", exc)
|
|
277
|
+
await ws.send(json.dumps({"type": "error", "message": "Authentication failed"}))
|
|
278
|
+
return None
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
logger.error("Unexpected error during authentication: %s", exc, exc_info=True)
|
|
344
281
|
await ws.send(json.dumps({"type": "error", "message": "Authentication failed"}))
|
|
345
282
|
return None
|
|
346
283
|
|
|
347
284
|
await ws.send(json.dumps({"type": "auth_ok"}))
|
|
348
|
-
|
|
349
|
-
return (user_email, token)
|
|
285
|
+
return result
|
|
350
286
|
|
|
351
287
|
|
|
352
288
|
async def _handle_connection(
|
|
@@ -354,17 +290,15 @@ async def _handle_connection(
|
|
|
354
290
|
settings: RealtimeSettings,
|
|
355
291
|
repo_root: Path,
|
|
356
292
|
voice_config: VoiceConfig | None,
|
|
357
|
-
auth_enabled: bool,
|
|
358
|
-
tenant_id: str,
|
|
359
|
-
audience: str,
|
|
360
293
|
) -> None:
|
|
361
294
|
remote = ws.remote_address
|
|
362
295
|
logger.info("Client connected from %s", remote)
|
|
363
296
|
|
|
364
297
|
user_email = None
|
|
365
298
|
user_jwt = None
|
|
366
|
-
if
|
|
367
|
-
|
|
299
|
+
authenticate = voice_config.authenticate if voice_config else None
|
|
300
|
+
if authenticate:
|
|
301
|
+
auth_result = await _authenticate_ws(ws, authenticate)
|
|
368
302
|
if not auth_result:
|
|
369
303
|
logger.warning("Auth failed for %s — closing", remote)
|
|
370
304
|
await ws.close(4001, "Unauthorized")
|
|
@@ -387,7 +321,6 @@ async def serve(
|
|
|
387
321
|
repo_root: Path | None = None,
|
|
388
322
|
voice_config: VoiceConfig | None = None,
|
|
389
323
|
settings: RealtimeSettings | None = None,
|
|
390
|
-
no_auth: bool = False,
|
|
391
324
|
allowed_origins: list[str] | None = None,
|
|
392
325
|
) -> None:
|
|
393
326
|
"""Start the WebSocket server. Blocks until stopped."""
|
|
@@ -404,22 +337,11 @@ async def serve(
|
|
|
404
337
|
logger.error("Not configured. Run 'vibecoder' TUI first to set API keys.")
|
|
405
338
|
return
|
|
406
339
|
|
|
407
|
-
|
|
408
|
-
tenant_id = os.environ.get("AZURE_AD_TENANT_ID", "")
|
|
409
|
-
client_id = os.environ.get("AZURE_AD_CLIENT_ID", "")
|
|
410
|
-
auth_enabled = bool(tenant_id and client_id) and not no_auth
|
|
340
|
+
auth_enabled = voice_config is not None and voice_config.authenticate is not None
|
|
411
341
|
if auth_enabled:
|
|
412
|
-
|
|
413
|
-
logger.info("JWT auth enabled tenant=%s audience=%s", tenant_id, audience)
|
|
414
|
-
elif no_auth:
|
|
415
|
-
audience = ""
|
|
416
|
-
logger.warning("JWT auth explicitly disabled via --no-auth")
|
|
342
|
+
logger.info("WebSocket auth enabled (authenticate callback provided)")
|
|
417
343
|
else:
|
|
418
|
-
|
|
419
|
-
logger.warning(
|
|
420
|
-
"JWT auth disabled (AZURE_AD_TENANT_ID / AZURE_AD_CLIENT_ID not set). "
|
|
421
|
-
"Set these env vars or pass --no-auth to suppress this warning."
|
|
422
|
-
)
|
|
344
|
+
logger.info("WebSocket auth disabled (no authenticate callback)")
|
|
423
345
|
|
|
424
346
|
# Origin checking — build allowed set from env var + explicit param
|
|
425
347
|
origin_env = os.environ.get("VOICE_ALLOWED_ORIGINS", "")
|
|
@@ -444,7 +366,7 @@ async def serve(
|
|
|
444
366
|
)
|
|
445
367
|
|
|
446
368
|
async def handler(ws: ServerConnection) -> None:
|
|
447
|
-
await _handle_connection(ws, settings, repo_root, voice_config
|
|
369
|
+
await _handle_connection(ws, settings, repo_root, voice_config)
|
|
448
370
|
|
|
449
371
|
async with websockets.serve(handler, host, port, process_request=process_request):
|
|
450
372
|
await asyncio.Future() # Block forever
|
|
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.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.3.0 → voice_vibecoder-2.5.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
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.3.0 → voice_vibecoder-2.5.0}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|