gemcode 0.3.99__tar.gz → 0.3.101__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 (158) hide show
  1. {gemcode-0.3.99/src/gemcode.egg-info → gemcode-0.3.101}/PKG-INFO +4 -1
  2. {gemcode-0.3.99 → gemcode-0.3.101}/pyproject.toml +2 -1
  3. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/cli.py +106 -8
  4. gemcode-0.3.101/src/gemcode/live_audio_engine.py +252 -0
  5. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/repl_slash.py +0 -3
  6. {gemcode-0.3.99 → gemcode-0.3.101/src/gemcode.egg-info}/PKG-INFO +4 -1
  7. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/requires.txt +4 -0
  8. gemcode-0.3.99/src/gemcode/live_audio_engine.py +0 -124
  9. {gemcode-0.3.99 → gemcode-0.3.101}/LICENSE +0 -0
  10. {gemcode-0.3.99 → gemcode-0.3.101}/MANIFEST.in +0 -0
  11. {gemcode-0.3.99 → gemcode-0.3.101}/README.md +0 -0
  12. {gemcode-0.3.99 → gemcode-0.3.101}/setup.cfg +0 -0
  13. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/__init__.py +0 -0
  14. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/__main__.py +0 -0
  15. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/agent.py +0 -0
  16. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/autotune.py +0 -0
  19. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/callbacks.py +0 -0
  20. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/capability_routing.py +0 -0
  21. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/checkpoints.py +0 -0
  22. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/compaction.py +0 -0
  23. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/computer_use/__init__.py +0 -0
  24. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/computer_use/browser_computer.py +0 -0
  25. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/config.py +0 -0
  26. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/context_budget.py +0 -0
  27. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/context_warning.py +0 -0
  28. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/credentials.py +0 -0
  29. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/curated_memory.py +0 -0
  30. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/dynamic_policy.py +0 -0
  31. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/evals/harness.py +0 -0
  32. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/hitl_session.py +0 -0
  33. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/hooks.py +0 -0
  34. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/ide_protocol.py +0 -0
  35. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/ide_stdio.py +0 -0
  36. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/intent_classifier.py +0 -0
  37. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/interactions.py +0 -0
  38. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/invoke.py +0 -0
  39. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/kaira_daemon.py +0 -0
  40. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/learning.py +0 -0
  41. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/limits.py +0 -0
  42. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/logging_config.py +0 -0
  43. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/mcp_loader.py +0 -0
  44. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/__init__.py +0 -0
  45. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/embedding_memory_service.py +0 -0
  46. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/file_memory_service.py +0 -0
  47. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/modality_tools.py +0 -0
  48. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/model_errors.py +0 -0
  49. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/model_routing.py +0 -0
  50. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/multimodal_input.py +0 -0
  51. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/openapi_loader.py +0 -0
  52. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/output_styles.py +0 -0
  53. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/paths.py +0 -0
  54. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/permissions.py +0 -0
  55. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/__init__.py +0 -0
  56. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  57. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  58. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/policy_profile.py +0 -0
  59. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/pricing.py +0 -0
  60. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/prompt_suggestions.py +0 -0
  61. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/__init__.py +0 -0
  62. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/config.py +0 -0
  63. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/deps.py +0 -0
  64. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/engine.py +0 -0
  65. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/stop_hooks.py +0 -0
  66. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/token_budget.py +0 -0
  67. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/transitions.py +0 -0
  68. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query_sanitizer.py +0 -0
  69. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/refine.py +0 -0
  70. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/repl_commands.py +0 -0
  71. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/review_agent.py +0 -0
  72. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/rules.py +0 -0
  73. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_runtime.py +0 -0
  74. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_store.py +0 -0
  75. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_summariser.py +0 -0
  76. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/skills.py +0 -0
  77. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/slash_commands.py +0 -0
  78. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/thinking.py +0 -0
  79. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_prompt_manifest.py +0 -0
  80. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_registry.py +0 -0
  81. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_result_store.py +0 -0
  82. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/__init__.py +0 -0
  83. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/bash.py +0 -0
  84. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/browser.py +0 -0
  85. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/compress_memory.py +0 -0
  86. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/curated_memory.py +0 -0
  87. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/edit.py +0 -0
  88. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/filesystem.py +0 -0
  89. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/notebook.py +0 -0
  90. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/notes.py +0 -0
  91. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/repo_map.py +0 -0
  92. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/search.py +0 -0
  93. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/shell.py +0 -0
  94. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/shell_gate.py +0 -0
  95. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/skills.py +0 -0
  96. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/subtask.py +0 -0
  97. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/tasks.py +0 -0
  98. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/think.py +0 -0
  99. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/todo.py +0 -0
  100. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/veomem_tools.py +0 -0
  101. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/web.py +0 -0
  102. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/web_search.py +0 -0
  103. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools_inspector.py +0 -0
  104. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/trust.py +0 -0
  105. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/input_handler.py +0 -0
  106. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/scrollback.py +0 -0
  107. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/spinner.py +0 -0
  108. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/welcome_banner.py +0 -0
  109. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/welcome_rich.py +0 -0
  110. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/veomem_bridge.py +0 -0
  111. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/version.py +0 -0
  112. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/vertex.py +0 -0
  113. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/wal.py +0 -0
  114. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/__init__.py +0 -0
  115. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/sse_adapter.py +0 -0
  116. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/terminal_repl.py +0 -0
  117. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/web_sse_compat.py +0 -0
  118. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/workspace_hints.py +0 -0
  119. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/SOURCES.txt +0 -0
  120. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/dependency_links.txt +0 -0
  121. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/entry_points.txt +0 -0
  122. {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/top_level.txt +0 -0
  123. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_add_dir.py +0 -0
  124. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_agent_instruction.py +0 -0
  125. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_autocompact.py +0 -0
  126. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_capability_routing.py +0 -0
  127. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_checkpoint_diff_command.py +0 -0
  128. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_cli_init.py +0 -0
  129. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_compress_memory_tool.py +0 -0
  130. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_computer_use_permissions.py +0 -0
  131. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_context_budget.py +0 -0
  132. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_context_warning.py +0 -0
  133. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_credentials.py +0 -0
  134. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_eval_harness_layout.py +0 -0
  135. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_ide_stdio_attachments.py +0 -0
  136. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_interactive_permission_ask.py +0 -0
  137. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_kaira_scheduler.py +0 -0
  138. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_modality_tools.py +0 -0
  139. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_error_retry.py +0 -0
  140. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_errors.py +0 -0
  141. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_routing.py +0 -0
  142. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_multimodal_input.py +0 -0
  143. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_output_styles_and_rules.py +0 -0
  144. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_paths.py +0 -0
  145. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_permissions.py +0 -0
  146. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_prompt_suggestions.py +0 -0
  147. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_repl_commands.py +0 -0
  148. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_repl_slash.py +0 -0
  149. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_skills.py +0 -0
  150. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_slash_commands.py +0 -0
  151. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_slash_completion_registry.py +0 -0
  152. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_thinking_config.py +0 -0
  153. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_token_budget.py +0 -0
  154. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tool_context_circulation.py +0 -0
  155. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tools.py +0 -0
  156. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tools_inspector.py +0 -0
  157. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_web_sse_adapter.py +0 -0
  158. {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.99
3
+ Version: 0.3.101
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -185,6 +185,9 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
185
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
186
186
  Provides-Extra: mcp
187
187
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
188
+ Provides-Extra: live
189
+ Requires-Dist: numpy>=1.26.0; extra == "live"
190
+ Requires-Dist: sounddevice>=0.5.0; extra == "live"
188
191
  Dynamic: license-file
189
192
 
190
193
  # GemCode User Manual
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.99"
7
+ version = "0.3.101"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,6 +42,7 @@ Issues = "https://github.com/spiderdev27/GemCode/issues"
42
42
  [project.optional-dependencies]
43
43
  dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0"]
44
44
  mcp = ["mcp>=1.0.0"]
45
+ live = ["numpy>=1.26.0", "sounddevice>=0.5.0"]
45
46
 
46
47
  [project.scripts]
47
48
  gemcode = "gemcode.cli:main"
@@ -705,6 +705,11 @@ def main() -> None:
705
705
  action="store_true",
706
706
  help="Enable embeddings-based semantic retrieval",
707
707
  )
708
+ audio_parser.add_argument(
709
+ "--no-playback",
710
+ action="store_true",
711
+ help="Do not play model audio to speakers (still prints text if any)",
712
+ )
708
713
 
709
714
  args = audio_parser.parse_args(sys.argv[2:])
710
715
  load_cli_environment()
@@ -725,15 +730,108 @@ def main() -> None:
725
730
  session_id = args.session or str(uuid.uuid4())
726
731
  from gemcode.live_audio_engine import run_live_audio
727
732
 
728
- asyncio.run(
729
- run_live_audio(
730
- cfg,
731
- session_id=session_id,
732
- seconds=args.seconds,
733
- input_rate=args.rate,
734
- language_code=args.language,
733
+ # One-time explicit permission prompt (HITL) for mic/speaker use.
734
+ if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
735
+ try:
736
+ ask = os.environ.get("GEMCODE_LIVE_AUDIO_ASK", "1").lower() not in ("0", "false", "no", "off")
737
+ if ask and not args.yes:
738
+ print(
739
+ "\n[gemcode live-audio] Permissions\n"
740
+ "GemCode will access your microphone"
741
+ + (" and play audio to your speakers" if not args.no_playback else "")
742
+ + ".\n"
743
+ "Allow this now? [y/N] ",
744
+ file=sys.stderr,
745
+ end="",
746
+ )
747
+ ans = input().strip().lower()
748
+ if ans not in ("y", "yes"):
749
+ raise SystemExit("live-audio cancelled by user.")
750
+ except EOFError:
751
+ raise SystemExit("live-audio cancelled (no TTY input).")
752
+
753
+ try:
754
+ # Suppress non-actionable serialization warning seen in some SDK versions.
755
+ try:
756
+ import warnings as _warnings
757
+ _warnings.filterwarnings(
758
+ "ignore",
759
+ message=r".*Pydantic serializer warnings.*",
760
+ category=UserWarning,
761
+ )
762
+ except Exception:
763
+ pass
764
+
765
+ # Some SDK builds print a close-1000 traceback directly to stderr even when it's benign.
766
+ # Capture stderr during the run and suppress that specific known noise.
767
+ _hide = os.environ.get("GEMCODE_LIVE_AUDIO_HIDE_SDK_TRACE", "1").lower() not in (
768
+ "0",
769
+ "false",
770
+ "no",
771
+ "off",
735
772
  )
736
- )
773
+ if _hide:
774
+ import io
775
+ from contextlib import redirect_stderr
776
+
777
+ buf = io.StringIO()
778
+ with redirect_stderr(buf):
779
+ asyncio.run(
780
+ run_live_audio(
781
+ cfg,
782
+ session_id=session_id,
783
+ seconds=args.seconds,
784
+ input_rate=args.rate,
785
+ language_code=args.language,
786
+ playback=(not args.no_playback),
787
+ )
788
+ )
789
+ captured = buf.getvalue()
790
+ if captured and "An unexpected error occurred in live flow: 1000" not in captured:
791
+ # Re-emit unexpected stderr.
792
+ print(captured, file=sys.stderr, end="")
793
+ else:
794
+ asyncio.run(
795
+ run_live_audio(
796
+ cfg,
797
+ session_id=session_id,
798
+ seconds=args.seconds,
799
+ input_rate=args.rate,
800
+ language_code=args.language,
801
+ playback=(not args.no_playback),
802
+ )
803
+ )
804
+ except Exception as e:
805
+ # Some SDK/ADK versions surface a normal websocket close (1000 OK) as an exception.
806
+ try:
807
+ from google.genai.errors import APIError # type: ignore
808
+ if isinstance(e, APIError) and (getattr(e, "status_code", None) == 1000 or "1000" in str(e)):
809
+ print("\n[gemcode live-audio] Session ended.", file=sys.stderr)
810
+ raise SystemExit(0)
811
+ except Exception:
812
+ pass
813
+ # websockets can also surface a close directly.
814
+ if "ConnectionClosedOK" in repr(e) or "sent 1000 (OK)" in str(e):
815
+ print("\n[gemcode live-audio] Session ended.", file=sys.stderr)
816
+ raise SystemExit(0)
817
+ raise
818
+ except RuntimeError as e:
819
+ msg = str(e or "")
820
+ if "Mic capture requires `sounddevice` and `numpy`" in msg:
821
+ print(
822
+ "\n[gemcode live-audio] Microphone capture dependencies are missing.\n\n"
823
+ "Install:\n"
824
+ " python3 -m pip install -U \"gemcode[live]\"\n\n"
825
+ "If that fails on your system, try:\n"
826
+ " python3 -m pip install -U numpy sounddevice\n\n"
827
+ "Then re-run:\n"
828
+ f" gemcode live-audio -C {cfg.project_root}\n\n"
829
+ "If the mic is still blocked, enable Microphone access for your terminal app in:\n"
830
+ " System Settings → Privacy & Security → Microphone\n",
831
+ file=sys.stderr,
832
+ )
833
+ raise SystemExit(2)
834
+ raise
737
835
  print(f"\n[gemcode live-audio] session_id={session_id}", file=sys.stderr)
738
836
  return
739
837
 
@@ -0,0 +1,252 @@
1
+ """
2
+ Live audio engine (Gemini Live API via ADK).
3
+
4
+ This wires GemCode's existing outer session + callbacks into ADK's
5
+ `Runner.run_live()` path for real-time audio input/output.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import time
12
+ import sys
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+ from google.adk.agents.live_request_queue import LiveRequestQueue
17
+ from google.adk.agents.run_config import RunConfig
18
+ from google.genai import types
19
+
20
+ from gemcode.config import GemCodeConfig
21
+ from gemcode.session_runtime import create_runner
22
+
23
+
24
+ def _mime_type_for_rate(rate: int) -> str:
25
+ # ADK/examples commonly use this mime type.
26
+ return f"audio/pcm;rate={rate}"
27
+
28
+
29
+ def _require_audio_deps():
30
+ """
31
+ Import audio deps (sounddevice + numpy). Raised error is caught by CLI to show friendly instructions.
32
+ """
33
+ try:
34
+ import sounddevice as sd # type: ignore
35
+ import numpy as np # type: ignore
36
+ except ImportError as e:
37
+ raise RuntimeError(
38
+ "Mic capture requires `sounddevice` and `numpy`. Install them to use `gemcode live-audio`."
39
+ ) from e
40
+ return sd, np
41
+
42
+
43
+ def _parse_pcm_rate(mime_type: str | None) -> int | None:
44
+ mt = (mime_type or "").lower()
45
+ if "audio/pcm" not in mt:
46
+ return None
47
+ # e.g. audio/pcm;rate=24000
48
+ for part in mt.split(";"):
49
+ p = part.strip()
50
+ if p.startswith("rate="):
51
+ try:
52
+ return int(p.split("=", 1)[1])
53
+ except Exception:
54
+ return None
55
+ return None
56
+
57
+
58
+ @dataclass
59
+ class _AudioIO:
60
+ sd: object
61
+ np: object
62
+ input_rate: int
63
+ output_rate: int
64
+ playback: bool
65
+ _out_stream: object | None = None
66
+
67
+ def ensure_output(self) -> None:
68
+ if not self.playback:
69
+ return
70
+ if self._out_stream is not None:
71
+ return
72
+ # RawOutputStream writes bytes directly.
73
+ self._out_stream = self.sd.RawOutputStream( # type: ignore[attr-defined]
74
+ samplerate=int(self.output_rate),
75
+ channels=1,
76
+ dtype="int16",
77
+ )
78
+ self._out_stream.start()
79
+
80
+ def write_audio(self, pcm_bytes: bytes) -> None:
81
+ if not self.playback:
82
+ return
83
+ self.ensure_output()
84
+ try:
85
+ self._out_stream.write(pcm_bytes) # type: ignore[union-attr]
86
+ except Exception:
87
+ pass
88
+
89
+ def close(self) -> None:
90
+ try:
91
+ if self._out_stream is not None:
92
+ self._out_stream.stop()
93
+ self._out_stream.close()
94
+ except Exception:
95
+ pass
96
+ self._out_stream = None
97
+
98
+
99
+ async def run_live_audio(
100
+ cfg: GemCodeConfig,
101
+ *,
102
+ session_id: str,
103
+ user_id: str = "local",
104
+ seconds: int = 10,
105
+ input_rate: int = 24_000,
106
+ language_code: Optional[str] = None,
107
+ playback: bool = True,
108
+ ) -> None:
109
+ """
110
+ Realtime microphone streaming to Gemini Live + realtime model audio playback.
111
+
112
+ Behavior:
113
+ - streams mic audio in small PCM chunks for up to `seconds`
114
+ - prints model-authored text parts (if any)
115
+ - plays model audio parts live when `playback=True`
116
+ """
117
+
118
+ sd, np = _require_audio_deps()
119
+
120
+ runner = create_runner(cfg)
121
+ live_queue = LiveRequestQueue()
122
+
123
+ speech_config = None
124
+ if language_code:
125
+ speech_config = types.SpeechConfig(language_code=language_code)
126
+
127
+ run_config = RunConfig(
128
+ # Prefer the enum value (avoids pydantic serializer warnings in some SDK versions).
129
+ response_modalities=[types.Modality.AUDIO],
130
+ speech_config=speech_config,
131
+ # Keep SDK defaults for STT/TTS transcription configs.
132
+ )
133
+
134
+ agen = runner.run_live(
135
+ user_id=user_id,
136
+ session_id=session_id,
137
+ live_request_queue=live_queue,
138
+ run_config=run_config,
139
+ )
140
+
141
+ printed_any = False
142
+ audio_io = _AudioIO(sd=sd, np=np, input_rate=input_rate, output_rate=input_rate, playback=playback)
143
+
144
+ async def _consume_events() -> None:
145
+ nonlocal printed_any
146
+ try:
147
+ async for event in agen:
148
+ if not event.content or not event.content.parts:
149
+ continue
150
+ for part in event.content.parts:
151
+ part_text = getattr(part, "text", None)
152
+ # We only print model-authored text to avoid echoing user input.
153
+ if part_text and getattr(event, "author", None) != "user":
154
+ sys.stdout.write(part_text)
155
+ sys.stdout.flush()
156
+ printed_any = True
157
+ # Play audio responses when present.
158
+ inline = getattr(part, "inline_data", None)
159
+ if inline is not None and getattr(event, "author", None) != "user":
160
+ try:
161
+ mime = getattr(inline, "mime_type", None)
162
+ data = getattr(inline, "data", None)
163
+ if isinstance(data, (bytes, bytearray)) and _parse_pcm_rate(mime) is not None:
164
+ r = _parse_pcm_rate(mime) or input_rate
165
+ audio_io.output_rate = int(r)
166
+ audio_io.write_audio(bytes(data))
167
+ except Exception:
168
+ pass
169
+ except Exception as e:
170
+ # Some SDK/ADK versions surface a normal websocket close (1000 OK) as an exception.
171
+ # Treat it as a clean end-of-session (no error).
172
+ try:
173
+ from google.genai.errors import APIError # type: ignore
174
+ if isinstance(e, APIError) and (
175
+ getattr(e, "status_code", None) == 1000 or "1000" in str(e)
176
+ ):
177
+ return
178
+ except Exception:
179
+ pass
180
+ if "sent 1000 (OK)" in str(e) or "ConnectionClosedOK" in repr(e) or "1000 None" in str(e):
181
+ return
182
+ # Runner/live failures are expected to be surfaced as terminal errors
183
+ # in session state + audit logs; don't crash the CLI.
184
+ raise
185
+
186
+ consumer_task = asyncio.create_task(_consume_events())
187
+
188
+ # Mic capture → async queue (threaded producer).
189
+ pcm_q: asyncio.Queue[bytes] = asyncio.Queue(maxsize=50)
190
+ stop_at = time.time() + max(1, int(seconds))
191
+
192
+ def _mic_thread() -> None:
193
+ # ~20ms frames is a good latency/overhead balance.
194
+ blocksize = max(120, int(input_rate // 50))
195
+ try:
196
+ stream = sd.RawInputStream( # type: ignore[attr-defined]
197
+ samplerate=int(input_rate),
198
+ channels=1,
199
+ dtype="int16",
200
+ blocksize=int(blocksize),
201
+ )
202
+ except Exception:
203
+ # Let the consumer surface this as empty audio.
204
+ return
205
+ with stream:
206
+ while time.time() < stop_at:
207
+ try:
208
+ data, _overflow = stream.read(blocksize)
209
+ if not data:
210
+ continue
211
+ # Push into asyncio queue safely.
212
+ try:
213
+ asyncio.get_running_loop().call_soon_threadsafe(pcm_q.put_nowait, bytes(data))
214
+ except Exception:
215
+ # If the loop isn't available, just drop.
216
+ pass
217
+ except Exception:
218
+ break
219
+
220
+ # Send "user started speaking" signal and start streaming.
221
+ live_queue.send_activity_start()
222
+ mic_task = asyncio.create_task(asyncio.to_thread(_mic_thread))
223
+
224
+ try:
225
+ while time.time() < stop_at:
226
+ try:
227
+ chunk = await asyncio.wait_for(pcm_q.get(), timeout=0.25)
228
+ except asyncio.TimeoutError:
229
+ continue
230
+ live_queue.send_realtime(
231
+ types.Blob(data=chunk, mime_type=_mime_type_for_rate(input_rate))
232
+ )
233
+ finally:
234
+ # End speech activity and close the queue regardless of failures.
235
+ live_queue.send_activity_end()
236
+ live_queue.close()
237
+ try:
238
+ await mic_task
239
+ except Exception:
240
+ pass
241
+
242
+ # Wait for event stream to drain.
243
+ try:
244
+ await consumer_task
245
+ finally:
246
+ audio_io.close()
247
+
248
+ if not printed_any:
249
+ print("\n[gemcode live-audio] No model text received (audio may have been silent).")
250
+
251
+ await runner.close()
252
+
@@ -618,9 +618,6 @@ async def process_repl_slash(
618
618
 
619
619
  # ── /add-dir (safe multi-root access) ──────────────────────────────────────
620
620
  if name in ("add-dir", "add_dir", "adddir"):
621
- import os
622
- from pathlib import Path
623
-
624
621
  args = (sc.args or "").strip()
625
622
  added: dict[str, Path] = getattr(cfg, "_added_dirs", None) or {}
626
623
  setattr(cfg, "_added_dirs", added)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.99
3
+ Version: 0.3.101
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -185,6 +185,9 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
185
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
186
186
  Provides-Extra: mcp
187
187
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
188
+ Provides-Extra: live
189
+ Requires-Dist: numpy>=1.26.0; extra == "live"
190
+ Requires-Dist: sounddevice>=0.5.0; extra == "live"
188
191
  Dynamic: license-file
189
192
 
190
193
  # GemCode User Manual
@@ -8,5 +8,9 @@ prompt_toolkit>=3.0.0
8
8
  pytest>=8.0.0
9
9
  pytest-asyncio>=0.24.0
10
10
 
11
+ [live]
12
+ numpy>=1.26.0
13
+ sounddevice>=0.5.0
14
+
11
15
  [mcp]
12
16
  mcp>=1.0.0
@@ -1,124 +0,0 @@
1
- """
2
- Live audio engine (Gemini Live API via ADK).
3
-
4
- This wires GemCode's existing outer session + callbacks into ADK's
5
- `Runner.run_live()` path for real-time audio input/output.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import asyncio
11
- import sys
12
- from typing import Optional
13
-
14
- from google.adk.agents.live_request_queue import LiveRequestQueue
15
- from google.adk.agents.run_config import RunConfig
16
- from google.genai import types
17
-
18
- from gemcode.config import GemCodeConfig
19
- from gemcode.session_runtime import create_runner
20
-
21
-
22
- def _mime_type_for_rate(rate: int) -> str:
23
- # ADK/examples commonly use this mime type.
24
- return f"audio/pcm;rate={rate}"
25
-
26
-
27
- def _record_mic_pcm_blocking(*, rate: int, seconds: int) -> bytes:
28
- try:
29
- import sounddevice as sd
30
- import numpy as np
31
- except ImportError as e:
32
- raise RuntimeError(
33
- "Mic capture requires `sounddevice` and `numpy`. Install them to use `gemcode live-audio`."
34
- ) from e
35
-
36
- frames = int(rate * seconds)
37
- # mono int16
38
- audio = sd.rec(frames, samplerate=rate, channels=1, dtype="int16")
39
- sd.wait()
40
- pcm = np.asarray(audio).astype("int16", copy=False)
41
- return pcm.tobytes()
42
-
43
-
44
- async def run_live_audio(
45
- cfg: GemCodeConfig,
46
- *,
47
- session_id: str,
48
- user_id: str = "local",
49
- seconds: int = 10,
50
- input_rate: int = 24_000,
51
- language_code: Optional[str] = None,
52
- ) -> None:
53
- """
54
- Record microphone audio for `seconds` and send it to Gemini Live.
55
-
56
- MVP behavior:
57
- - sends the entire recorded buffer as a single audio blob
58
- - prints any model text parts it returns (typically transcriptions)
59
- """
60
-
61
- runner = create_runner(cfg)
62
- live_queue = LiveRequestQueue()
63
-
64
- speech_config = None
65
- if language_code:
66
- speech_config = types.SpeechConfig(language_code=language_code)
67
-
68
- run_config = RunConfig(
69
- response_modalities=["AUDIO"],
70
- speech_config=speech_config,
71
- # Keep SDK defaults for STT/TTS transcription configs.
72
- )
73
-
74
- agen = runner.run_live(
75
- user_id=user_id,
76
- session_id=session_id,
77
- live_request_queue=live_queue,
78
- run_config=run_config,
79
- )
80
-
81
- printed_any = False
82
-
83
- async def _consume_events() -> None:
84
- nonlocal printed_any
85
- try:
86
- async for event in agen:
87
- if not event.content or not event.content.parts:
88
- continue
89
- for part in event.content.parts:
90
- part_text = getattr(part, "text", None)
91
- # We only print model-authored text to avoid echoing user input.
92
- if part_text and getattr(event, "author", None) != "user":
93
- sys.stdout.write(part_text)
94
- sys.stdout.flush()
95
- printed_any = True
96
- except Exception:
97
- # Runner/live failures are expected to be surfaced as terminal errors
98
- # in session state + audit logs; don't crash the CLI.
99
- raise
100
-
101
- consumer_task = asyncio.create_task(_consume_events())
102
-
103
- # Send "user started speaking" signal.
104
- live_queue.send_activity_start()
105
-
106
- pcm_bytes = await asyncio.to_thread(
107
- _record_mic_pcm_blocking, rate=input_rate, seconds=seconds
108
- )
109
- live_queue.send_realtime(
110
- types.Blob(data=pcm_bytes, mime_type=_mime_type_for_rate(input_rate))
111
- )
112
-
113
- # Send "user finished speaking" signal and close the queue.
114
- live_queue.send_activity_end()
115
- live_queue.close()
116
-
117
- # Wait for event stream to drain.
118
- await consumer_task
119
-
120
- if not printed_any:
121
- print("\n[gemcode live-audio] No model text received (audio may have been silent).")
122
-
123
- await runner.close()
124
-
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