voice-vibecoder 2.14.0__tar.gz → 2.16.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.
Files changed (44) hide show
  1. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/CHANGELOG.md +6 -0
  2. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/cli_serve.py +13 -5
  6. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/config.py +20 -4
  7. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/server.py +16 -1
  8. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/.github/workflows/ci.yml +0 -0
  9. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/.github/workflows/release.yml +0 -0
  10. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/.gitignore +0 -0
  11. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/LICENSE +0 -0
  12. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/README.md +0 -0
  13. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/app.py +0 -0
  14. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/index.html +0 -0
  15. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/app_config.py +0 -0
  16. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/audio.py +0 -0
  17. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  21. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  22. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/instances.py +0 -0
  23. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/session.py +0 -0
  24. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  25. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  26. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/definitions.py +0 -0
  27. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
  28. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  29. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/tools/handlers.py +0 -0
  30. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  31. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  32. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  33. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  34. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/panels.py +0 -0
  35. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/screen.py +0 -0
  36. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/setup.py +0 -0
  37. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/state.py +0 -0
  38. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/styles.py +0 -0
  39. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  40. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  41. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  42. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  43. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.14.0 → voice_vibecoder-2.16.0}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.16.0 (2026-02-22)
6
+
7
+
8
+ ## v2.15.0 (2026-02-22)
9
+
10
+
5
11
  ## v2.14.0 (2026-02-21)
6
12
 
7
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.14.0
3
+ Version: 2.16.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.14.0"
7
+ version = "2.16.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"
@@ -3,7 +3,7 @@
3
3
  A voice-controlled coding assistant using OpenAI Realtime API + Claude Code.
4
4
  """
5
5
 
6
- __version__ = "2.14.0"
6
+ __version__ = "2.16.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -40,7 +40,6 @@ def _apply_env_overrides(settings: RealtimeSettings) -> RealtimeSettings:
40
40
  # Top-level overrides
41
41
  simple_map = {
42
42
  "VOICE_PROVIDER": "provider",
43
- "VOICE_API_KEY": "api_key",
44
43
  "VOICE_LANGUAGE": "language",
45
44
  "VOICE_PERMISSION_MODE": "permission_mode",
46
45
  }
@@ -48,20 +47,29 @@ def _apply_env_overrides(settings: RealtimeSettings) -> RealtimeSettings:
48
47
  value = os.environ.get(env_var)
49
48
  if value:
50
49
  setattr(settings, attr, value)
51
- safe = env_var if "KEY" not in env_var else f"{env_var}=***"
52
- applied.append(safe)
50
+ applied.append(env_var)
51
+
52
+ # Legacy fallback: VOICE_API_KEY sets the top-level key
53
+ legacy_key = os.environ.get("VOICE_API_KEY")
54
+ if legacy_key:
55
+ settings.api_key = legacy_key
56
+ applied.append("VOICE_API_KEY=***")
53
57
 
54
- # Nested provider overrides
58
+ # Nested provider overrides (including per-provider API keys)
55
59
  nested_map = {
60
+ "VOICE_OPENAI_API_KEY": (settings.openai, "api_key"),
61
+ "VOICE_AZURE_API_KEY": (settings.azure, "api_key"),
56
62
  "VOICE_AZURE_ENDPOINT": (settings.azure, "endpoint"),
57
63
  "VOICE_AZURE_DEPLOYMENT": (settings.azure, "deployment"),
64
+ "VOICE_GEMINI_API_KEY": (settings.gemini, "api_key"),
58
65
  "VOICE_GEMINI_MODEL": (settings.gemini, "model"),
59
66
  }
60
67
  for env_var, (obj, attr) in nested_map.items():
61
68
  value = os.environ.get(env_var)
62
69
  if value:
63
70
  setattr(obj, attr, value)
64
- applied.append(env_var)
71
+ safe = env_var if "KEY" not in env_var else f"{env_var}=***"
72
+ applied.append(safe)
65
73
 
66
74
  if applied:
67
75
  logger.info("Env overrides applied: %s", ", ".join(applied))
@@ -267,6 +267,7 @@ def configure_paths(config_dir: Path) -> None:
267
267
  @dataclass
268
268
  class OpenAIConfig:
269
269
  """Per-provider settings for OpenAI."""
270
+ api_key: str = ""
270
271
  model: str = OPENAI_DEFAULT_MODEL
271
272
  voice: str = "ash"
272
273
  vad_threshold: float = 0.8
@@ -277,6 +278,7 @@ class OpenAIConfig:
277
278
  @dataclass
278
279
  class AzureConfig:
279
280
  """Per-provider settings for Azure OpenAI."""
281
+ api_key: str = ""
280
282
  endpoint: str = AZURE_DEFAULT_ENDPOINT
281
283
  deployment: str = AZURE_DEFAULT_DEPLOYMENT
282
284
  voice: str = "ash"
@@ -288,6 +290,7 @@ class AzureConfig:
288
290
  @dataclass
289
291
  class GeminiConfig:
290
292
  """Per-provider settings for Gemini."""
293
+ api_key: str = ""
291
294
  model: str = GEMINI_DEFAULT_MODEL
292
295
  voice: str = "Puck"
293
296
  start_sensitivity: str = "HIGH" # "HIGH" or "LOW"
@@ -329,6 +332,16 @@ class RealtimeSettings:
329
332
  self.input_mode = input_mode
330
333
  self.enabled_agents: list[str] = enabled_agents or ["claude"]
331
334
 
335
+ @property
336
+ def active_api_key(self) -> str:
337
+ """Return the API key for the active provider.
338
+
339
+ Checks the per-provider key first, falls back to the legacy
340
+ top-level ``api_key`` for backward compatibility.
341
+ """
342
+ provider_key = self.active_provider_config.api_key
343
+ return provider_key or self.api_key
344
+
332
345
  @property
333
346
  def active_voice(self) -> str:
334
347
  """Return the voice setting for the active provider."""
@@ -356,23 +369,23 @@ class RealtimeSettings:
356
369
  f"&deployment={self.azure.deployment}"
357
370
  )
358
371
  if self.provider == "gemini":
359
- return f"{GEMINI_WS_URL}?key={self.api_key}"
372
+ return f"{GEMINI_WS_URL}?key={self.active_api_key}"
360
373
  return f"{OPENAI_WS_URL}?model={self.openai.model}"
361
374
 
362
375
  @property
363
376
  def ws_headers(self) -> dict[str, str]:
364
377
  if self.provider == "azure":
365
- return {"api-key": self.api_key}
378
+ return {"api-key": self.active_api_key}
366
379
  if self.provider == "gemini":
367
380
  return {}
368
381
  return {
369
- "Authorization": f"Bearer {self.api_key}",
382
+ "Authorization": f"Bearer {self.active_api_key}",
370
383
  "OpenAI-Beta": "realtime=v1",
371
384
  }
372
385
 
373
386
  @property
374
387
  def is_configured(self) -> bool:
375
- return bool(self.api_key)
388
+ return bool(self.active_api_key)
376
389
 
377
390
  @property
378
391
  def provider_label(self) -> str:
@@ -427,6 +440,7 @@ class RealtimeSettings:
427
440
  @staticmethod
428
441
  def _load_openai(d: dict) -> OpenAIConfig:
429
442
  return OpenAIConfig(
443
+ api_key=d.get("api_key", ""),
430
444
  model=d.get("model", OPENAI_DEFAULT_MODEL),
431
445
  voice=d.get("voice", "ash"),
432
446
  vad_threshold=d.get("vad_threshold", 0.8),
@@ -437,6 +451,7 @@ class RealtimeSettings:
437
451
  @staticmethod
438
452
  def _load_azure(d: dict) -> AzureConfig:
439
453
  return AzureConfig(
454
+ api_key=d.get("api_key", ""),
440
455
  endpoint=d.get("endpoint", AZURE_DEFAULT_ENDPOINT),
441
456
  deployment=d.get("deployment", AZURE_DEFAULT_DEPLOYMENT),
442
457
  voice=d.get("voice", "ash"),
@@ -448,6 +463,7 @@ class RealtimeSettings:
448
463
  @staticmethod
449
464
  def _load_gemini(d: dict) -> GeminiConfig:
450
465
  return GeminiConfig(
466
+ api_key=d.get("api_key", ""),
451
467
  model=d.get("model", GEMINI_DEFAULT_MODEL),
452
468
  voice=d.get("voice", "Puck"),
453
469
  start_sensitivity=d.get("start_sensitivity", "HIGH"),
@@ -52,6 +52,15 @@ logger = logging.getLogger(__name__)
52
52
  AUTH_TIMEOUT_SECONDS = 10
53
53
 
54
54
 
55
+ def _sanitize_settings(d: dict[str, Any]) -> dict[str, Any]:
56
+ """Strip API keys from a settings dict before sending to the frontend."""
57
+ d = {k: v for k, v in d.items() if k != "api_key"}
58
+ for provider in ("openai", "azure", "gemini"):
59
+ if provider in d and isinstance(d[provider], dict):
60
+ d[provider] = {k: v for k, v in d[provider].items() if k != "api_key"}
61
+ return d
62
+
63
+
55
64
  def _merge_settings(settings: RealtimeSettings, incoming: dict[str, Any]) -> None:
56
65
  """Merge a partial settings dict into an existing RealtimeSettings.
57
66
 
@@ -210,6 +219,12 @@ class WebSocketBridge:
210
219
  "line": f"text:{inst.output_buffer}",
211
220
  })
212
221
 
222
+ # Send current settings so the frontend can render controls
223
+ self._send({
224
+ "type": "settings_updated",
225
+ "settings": _sanitize_settings(self._settings._to_dict()),
226
+ })
227
+
213
228
  async def _handle_settings_update(self, incoming: dict[str, Any]) -> None:
214
229
  """Apply a partial settings update, save to disk, and reconnect the provider."""
215
230
  if not incoming:
@@ -240,7 +255,7 @@ class WebSocketBridge:
240
255
  self._send({"type": "status", "status": "ready"})
241
256
  self._send({
242
257
  "type": "settings_updated",
243
- "settings": self._settings._to_dict(),
258
+ "settings": _sanitize_settings(self._settings._to_dict()),
244
259
  })
245
260
  except Exception as e:
246
261
  logger.error("Failed to reconnect after settings update: %s", e, exc_info=True)