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.
- {gemcode-0.3.99/src/gemcode.egg-info → gemcode-0.3.101}/PKG-INFO +4 -1
- {gemcode-0.3.99 → gemcode-0.3.101}/pyproject.toml +2 -1
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/cli.py +106 -8
- gemcode-0.3.101/src/gemcode/live_audio_engine.py +252 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/repl_slash.py +0 -3
- {gemcode-0.3.99 → gemcode-0.3.101/src/gemcode.egg-info}/PKG-INFO +4 -1
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/requires.txt +4 -0
- gemcode-0.3.99/src/gemcode/live_audio_engine.py +0 -124
- {gemcode-0.3.99 → gemcode-0.3.101}/LICENSE +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/MANIFEST.in +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/README.md +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/setup.cfg +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/autotune.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/config.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/learning.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/rules.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/skills.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/version.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/wal.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_add_dir.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_credentials.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_paths.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_permissions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_skills.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tools.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.99 → gemcode-0.3.101}/tests/test_web_sse_adapter.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|