hud-python 0.5.35__tar.gz → 0.5.36__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.
- {hud_python-0.5.35 → hud_python-0.5.36}/PKG-INFO +1 -1
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build.py +2 -2
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/__init__.py +6 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/__init__.py +2 -0
- hud_python-0.5.36/hud/tools/coding/gemini_edit.py +340 -0
- hud_python-0.5.36/hud/tools/coding/gemini_write.py +92 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_gemini_tools.py +78 -15
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/__init__.py +2 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/base.py +20 -4
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/gemini.py +140 -44
- hud_python-0.5.36/hud/tools/filesystem/gemini_read_many.py +207 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_glob.py +13 -4
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_grep.py +47 -1
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_list.py +1 -1
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_read.py +46 -8
- hud_python-0.5.36/hud/tools/filesystem/tests/test_read_many.py +121 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_version.py +1 -1
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/version.py +1 -1
- {hud_python-0.5.35 → hud_python-0.5.36}/pyproject.toml +1 -1
- hud_python-0.5.35/hud/tools/coding/gemini_edit.py +0 -252
- {hud_python-0.5.35 → hud_python-0.5.36}/.gitignore +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/LICENSE +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/README.md +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/examples/README.md +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/__main__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/claude.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gateway.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gemini_cua.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/grounded_openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/integration_test_agent.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/response_agent.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/openai_chat.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/operator.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/resolver.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/conftest.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_base_runtime.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_claude.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_grounded_openai_agent.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_integration_test_agent.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_operator.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_resolver.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_run_eval.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/__main__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/analyze.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/build.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/cancel.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/harbor.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/conftest.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/test_harbor.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/debug.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/deploy.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/dev.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/eval.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/dev.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tasks.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/templates.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tests/test_dev.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/link.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/models.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/push.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/rl.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/scenario.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/sync.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analysis_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze_metadata.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze_module.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build_failure.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build_module.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_main.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_root.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_convert.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_debug.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_debug_directory_mode.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_deploy.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_dev.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_eval.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_eval_bedrock.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_lockfile_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_main_module.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_mcp_server.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push_happy.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push_wrapper.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_rl.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_scenario.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_sync.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/analysis.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/api.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/args.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/build_display.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/build_logs.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/collect.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/config.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/docker.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/env_check.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/environment.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/git.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/interactive.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/lockfile.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/logging.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/metadata.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/name_check.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/project_config.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/server.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/source_hash.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tasks.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/taskset.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_collect.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_config.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_docker.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_docker_hints.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_env_check.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_environment.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_git.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_interactive_module.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_logging_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_metadata.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_source_hash.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_tasks.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/validation.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/version_check.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/viewer.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/loader.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/runner.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/test_loader.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/test_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connection.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/local.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/mcp_config.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/remote.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/environment.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/adk.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/anthropic.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/langchain.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/llamaindex.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/mock.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/router.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/scenarios.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_connection.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_connectors.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_environment.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_integrations.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_local_connectors.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_scenarios.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_session_id.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_tools.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/formats.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/schema.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/tool_wrappers.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/display.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/instrument.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/manager.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/parallel.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/task.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_eval.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_manager.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_parallel.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_task.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/chat.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/graders.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/permissions.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/skills.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/tests/test_graders.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/mcp_patches.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/warnings.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/py.typed +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/helper/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/low_level.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/router.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/server.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_add_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_handlers.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_integration.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_more.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_prefix_naming.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_run_wrapper.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_server_extra.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_sigterm_runner.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/chat.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/chat_service.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/reply_metadata.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/test_chat.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/test_chat_service.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/settings.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/exceptions.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/hints.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/requests.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_exceptions.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_hints.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_requests.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/exporter.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/instrument.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_eval_telemetry.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_exporter.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_instrument.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/agent.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/apply_patch.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/bash.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/edit.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/gemini_shell.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/session.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/shell.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_apply_patch.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash_extended.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash_integration.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_edit.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_shell.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/anthropic.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/glm.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/hud.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/openai.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/qwen.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/settings.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_compression.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_computer.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_computer_actions.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_glm_computer.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/elicitation.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/pyautogui.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/test_base_executor.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/test_pyautogui_executor.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/xdo.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/glob.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/grep.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/list.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/read.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/config.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/grounded_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/grounder.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/tests/test_grounded_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/code_execution.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/google_search.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/tool_search.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/url_context.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/web_fetch.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/web_search.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/jupyter.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/claude.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/session.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_claude.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_gemini.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_session.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/native_types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/playwright.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/response.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/submit.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_agent_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_base.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_elicitation.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_jupyter_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_native_tool_e2e.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_native_types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_playwright_tool.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_response.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_submit.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_tools.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_tools_init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/utils.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/types.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/env.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/hud_console.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/mcp.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/pretty_errors.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/serialization.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/strict_schema.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/__init__.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_init.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_pretty_errors.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_serialization.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_tool_shorthand.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tool_shorthand.py +0 -0
- {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/types.py +0 -0
|
@@ -61,12 +61,12 @@ class TestIncrementVersion:
|
|
|
61
61
|
def test_increment_minor(self):
|
|
62
62
|
"""Test incrementing minor version."""
|
|
63
63
|
assert increment_version("1.2.3", "minor") == "1.3.0"
|
|
64
|
-
assert increment_version("0.5.
|
|
64
|
+
assert increment_version("0.5.36", "minor") == "0.6.0"
|
|
65
65
|
|
|
66
66
|
def test_increment_major(self):
|
|
67
67
|
"""Test incrementing major version."""
|
|
68
68
|
assert increment_version("1.2.3", "major") == "2.0.0"
|
|
69
|
-
assert increment_version("0.5.
|
|
69
|
+
assert increment_version("0.5.36", "major") == "1.0.0"
|
|
70
70
|
|
|
71
71
|
def test_increment_with_v_prefix(self):
|
|
72
72
|
"""Test incrementing version with v prefix."""
|
|
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
|
|
|
43
43
|
EditTool,
|
|
44
44
|
GeminiEditTool,
|
|
45
45
|
GeminiShellTool,
|
|
46
|
+
GeminiWriteTool,
|
|
46
47
|
ShellTool,
|
|
47
48
|
)
|
|
48
49
|
from .computer import (
|
|
@@ -54,6 +55,7 @@ if TYPE_CHECKING:
|
|
|
54
55
|
QwenComputerTool,
|
|
55
56
|
)
|
|
56
57
|
from .filesystem import (
|
|
58
|
+
GeminiReadManyTool,
|
|
57
59
|
GlobTool,
|
|
58
60
|
GrepTool,
|
|
59
61
|
ListTool,
|
|
@@ -74,7 +76,9 @@ __all__ = [
|
|
|
74
76
|
"GeminiComputerTool",
|
|
75
77
|
"GeminiEditTool",
|
|
76
78
|
"GeminiMemoryTool",
|
|
79
|
+
"GeminiReadManyTool",
|
|
77
80
|
"GeminiShellTool",
|
|
81
|
+
"GeminiWriteTool",
|
|
78
82
|
"GlobTool",
|
|
79
83
|
"GoogleSearchTool",
|
|
80
84
|
"GrepTool",
|
|
@@ -121,6 +125,7 @@ def __getattr__(name: str) -> Any:
|
|
|
121
125
|
"ApplyPatchTool",
|
|
122
126
|
"GeminiShellTool",
|
|
123
127
|
"GeminiEditTool",
|
|
128
|
+
"GeminiWriteTool",
|
|
124
129
|
):
|
|
125
130
|
from . import coding
|
|
126
131
|
|
|
@@ -132,6 +137,7 @@ def __getattr__(name: str) -> Any:
|
|
|
132
137
|
"GrepTool",
|
|
133
138
|
"GlobTool",
|
|
134
139
|
"ListTool",
|
|
140
|
+
"GeminiReadManyTool",
|
|
135
141
|
):
|
|
136
142
|
from . import filesystem
|
|
137
143
|
|
|
@@ -23,6 +23,7 @@ from hud.tools.coding.bash import BashTool, ClaudeBashSession, _BashSession
|
|
|
23
23
|
from hud.tools.coding.edit import Command, EditTool
|
|
24
24
|
from hud.tools.coding.gemini_edit import GeminiEditTool
|
|
25
25
|
from hud.tools.coding.gemini_shell import GeminiShellOutput, GeminiShellTool
|
|
26
|
+
from hud.tools.coding.gemini_write import GeminiWriteTool
|
|
26
27
|
from hud.tools.coding.session import BashSession, ShellCallOutcome, ShellCommandOutput
|
|
27
28
|
from hud.tools.coding.shell import ShellResult, ShellTool
|
|
28
29
|
from hud.tools.coding.utils import (
|
|
@@ -49,6 +50,7 @@ __all__ = [
|
|
|
49
50
|
"GeminiEditTool",
|
|
50
51
|
"GeminiShellOutput",
|
|
51
52
|
"GeminiShellTool",
|
|
53
|
+
"GeminiWriteTool",
|
|
52
54
|
"ShellCallOutcome",
|
|
53
55
|
"ShellCommandOutput",
|
|
54
56
|
"ShellResult",
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Gemini-style edit tool implementation.
|
|
2
|
+
|
|
3
|
+
Based on Gemini CLI's replace tool:
|
|
4
|
+
https://github.com/google-gemini/gemini-cli
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
from mcp.types import ContentBlock # noqa: TC002 - used at runtime by FunctionTool
|
|
15
|
+
|
|
16
|
+
from hud.tools.base import BaseTool
|
|
17
|
+
from hud.tools.native_types import NativeToolSpec, NativeToolSpecs
|
|
18
|
+
from hud.tools.types import ContentResult, ToolError
|
|
19
|
+
from hud.types import AgentType
|
|
20
|
+
|
|
21
|
+
from .utils import (
|
|
22
|
+
read_file_sync,
|
|
23
|
+
write_file_sync,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _escape_regex(s: str) -> str:
|
|
28
|
+
"""Escape regex special characters."""
|
|
29
|
+
return re.sub(r"[.*+?^${}()|[\]\\]", r"\\\g<0>", s)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tokenize_for_regex(s: str) -> list[str]:
|
|
33
|
+
"""Tokenize string by splitting on delimiters (matching Gemini CLI).
|
|
34
|
+
|
|
35
|
+
Pads delimiters with spaces before splitting so each delimiter
|
|
36
|
+
becomes its own token. E.g., "foo(bar)" -> ["foo", "(", "bar", ")"].
|
|
37
|
+
"""
|
|
38
|
+
processed = s
|
|
39
|
+
for delim in "():[]{}><= ":
|
|
40
|
+
processed = processed.replace(delim, f" {delim} ")
|
|
41
|
+
return [t for t in processed.split() if t]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _detect_line_ending(content: str) -> str:
|
|
45
|
+
"""Detect the dominant line ending in content."""
|
|
46
|
+
crlf = content.count("\r\n")
|
|
47
|
+
lf = content.count("\n") - crlf
|
|
48
|
+
return "\r\n" if crlf > lf else "\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _restore_trailing_newline(new_content: str, original_content: str) -> str:
|
|
52
|
+
"""Preserve the original file's trailing newline state."""
|
|
53
|
+
had_trailing = original_content.endswith("\n")
|
|
54
|
+
has_trailing = new_content.endswith("\n")
|
|
55
|
+
if had_trailing and not has_trailing:
|
|
56
|
+
return new_content + "\n"
|
|
57
|
+
if not had_trailing and has_trailing:
|
|
58
|
+
return new_content.rstrip("\n")
|
|
59
|
+
return new_content
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _apply_relative_indentation(
|
|
63
|
+
base_indent: str,
|
|
64
|
+
old_lines: list[str],
|
|
65
|
+
new_lines: list[str],
|
|
66
|
+
) -> list[str]:
|
|
67
|
+
"""Apply indentation preserving relative indent levels.
|
|
68
|
+
|
|
69
|
+
Uses the first old line's indent as reference, computes each
|
|
70
|
+
new line's relative indent, then applies base_indent + relative.
|
|
71
|
+
"""
|
|
72
|
+
if not new_lines:
|
|
73
|
+
return new_lines
|
|
74
|
+
|
|
75
|
+
# Determine reference indent from old_lines
|
|
76
|
+
if old_lines:
|
|
77
|
+
ref_match = re.match(r"^(\s*)", old_lines[0])
|
|
78
|
+
ref_indent = ref_match.group(1) if ref_match else ""
|
|
79
|
+
else:
|
|
80
|
+
ref_indent = ""
|
|
81
|
+
|
|
82
|
+
result = []
|
|
83
|
+
for j, line in enumerate(new_lines):
|
|
84
|
+
if not line.strip():
|
|
85
|
+
result.append("")
|
|
86
|
+
continue
|
|
87
|
+
if j == 0:
|
|
88
|
+
result.append(f"{base_indent}{line.lstrip()}")
|
|
89
|
+
else:
|
|
90
|
+
line_match = re.match(r"^(\s*)", line)
|
|
91
|
+
line_indent = line_match.group(1) if line_match else ""
|
|
92
|
+
extra = line_indent[len(ref_indent) :] if len(line_indent) > len(ref_indent) else ""
|
|
93
|
+
result.append(f"{base_indent}{extra}{line.lstrip()}")
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _flexible_match(content: str, old_string: str, new_string: str) -> tuple[str, int]:
|
|
98
|
+
"""Attempt flexible whitespace-insensitive matching.
|
|
99
|
+
|
|
100
|
+
Matches Gemini CLI behavior: strips each line and compares,
|
|
101
|
+
preserves relative indentation in replacement.
|
|
102
|
+
"""
|
|
103
|
+
source_lines = content.split("\n")
|
|
104
|
+
search_lines = [line.strip() for line in old_string.split("\n")]
|
|
105
|
+
replace_lines = new_string.split("\n")
|
|
106
|
+
old_lines = old_string.split("\n")
|
|
107
|
+
|
|
108
|
+
occurrences = 0
|
|
109
|
+
i = 0
|
|
110
|
+
while i <= len(source_lines) - len(search_lines):
|
|
111
|
+
window = source_lines[i : i + len(search_lines)]
|
|
112
|
+
window_stripped = [line.strip() for line in window]
|
|
113
|
+
|
|
114
|
+
if window_stripped == search_lines:
|
|
115
|
+
occurrences += 1
|
|
116
|
+
indent_match = re.match(r"^(\s*)", window[0])
|
|
117
|
+
base_indent = indent_match.group(1) if indent_match else ""
|
|
118
|
+
indented = _apply_relative_indentation(base_indent, old_lines, replace_lines)
|
|
119
|
+
source_lines[i : i + len(search_lines)] = indented
|
|
120
|
+
i += len(indented)
|
|
121
|
+
else:
|
|
122
|
+
i += 1
|
|
123
|
+
|
|
124
|
+
return "\n".join(source_lines), occurrences
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GeminiEditTool(BaseTool):
|
|
128
|
+
"""Gemini CLI-style file editing tool (replace).
|
|
129
|
+
|
|
130
|
+
Replaces text within a file. Uses three matching strategies:
|
|
131
|
+
1. Exact string matching
|
|
132
|
+
2. Flexible matching (whitespace-insensitive line comparison)
|
|
133
|
+
3. Regex-based flexible matching
|
|
134
|
+
|
|
135
|
+
When old_string is empty and the file does not exist, creates a new file
|
|
136
|
+
with new_string as content.
|
|
137
|
+
|
|
138
|
+
Parameters (matching Gemini CLI exactly):
|
|
139
|
+
file_path: Path to the file to modify (required)
|
|
140
|
+
instruction: Semantic description of the change (required)
|
|
141
|
+
old_string: Exact literal text to replace (required)
|
|
142
|
+
new_string: Exact literal text to replace with (required)
|
|
143
|
+
allow_multiple: If true, replace all occurrences (default: false)
|
|
144
|
+
|
|
145
|
+
Native specs: Uses function calling (no native API), but has role="editor"
|
|
146
|
+
for mutual exclusion with EditTool/ApplyPatchTool.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
native_specs: ClassVar[NativeToolSpecs] = {
|
|
150
|
+
AgentType.GEMINI: NativeToolSpec(role="editor"),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_base_directory: str
|
|
154
|
+
_file_history: dict[Path, list[str]]
|
|
155
|
+
|
|
156
|
+
def __init__(self, base_directory: str = ".") -> None:
|
|
157
|
+
super().__init__(
|
|
158
|
+
env=None,
|
|
159
|
+
name="replace",
|
|
160
|
+
title="Edit",
|
|
161
|
+
description=(
|
|
162
|
+
"Replaces text within a file. Requires providing significant context "
|
|
163
|
+
"around the change. Always use read_file to examine content before editing. "
|
|
164
|
+
"old_string MUST be exact literal text including whitespace and indentation. "
|
|
165
|
+
"new_string MUST be exact literal text for the replacement. "
|
|
166
|
+
"To create a new file, set old_string to empty string."
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
self._base_directory = str(Path(base_directory).resolve())
|
|
170
|
+
self._file_history = defaultdict(list)
|
|
171
|
+
|
|
172
|
+
def _resolve_path(self, file_path: str) -> Path:
|
|
173
|
+
"""Resolve file path relative to base directory."""
|
|
174
|
+
path = Path(file_path)
|
|
175
|
+
if path.is_absolute():
|
|
176
|
+
return path
|
|
177
|
+
return Path(self._base_directory) / path
|
|
178
|
+
|
|
179
|
+
async def __call__(
|
|
180
|
+
self,
|
|
181
|
+
file_path: str,
|
|
182
|
+
instruction: str,
|
|
183
|
+
old_string: str,
|
|
184
|
+
new_string: str,
|
|
185
|
+
allow_multiple: bool = False,
|
|
186
|
+
) -> list[ContentBlock]:
|
|
187
|
+
"""Edit a file by replacing text, or create a new file.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
file_path: Path to the file to modify
|
|
191
|
+
instruction: Clear description of the change purpose
|
|
192
|
+
old_string: Exact literal text to replace (empty = create file)
|
|
193
|
+
new_string: Exact literal text to replace with
|
|
194
|
+
allow_multiple: If true, replace all occurrences (default: false)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of ContentBlocks with Gemini CLI-style result
|
|
198
|
+
"""
|
|
199
|
+
if not file_path:
|
|
200
|
+
raise ToolError("The 'file_path' parameter must be non-empty.")
|
|
201
|
+
if not instruction:
|
|
202
|
+
raise ToolError("The 'instruction' parameter must be non-empty.")
|
|
203
|
+
if old_string is None:
|
|
204
|
+
raise ToolError("The 'old_string' parameter is required.")
|
|
205
|
+
if new_string is None:
|
|
206
|
+
raise ToolError("The 'new_string' parameter is required.")
|
|
207
|
+
|
|
208
|
+
path = self._resolve_path(file_path)
|
|
209
|
+
|
|
210
|
+
# File creation: empty old_string on non-existent file
|
|
211
|
+
if old_string == "" and not path.exists():
|
|
212
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
write_file_sync(path, new_string)
|
|
214
|
+
return ContentResult(output=f"Created new file: {file_path}").to_content_blocks()
|
|
215
|
+
|
|
216
|
+
if old_string == "" and path.exists():
|
|
217
|
+
raise ToolError(
|
|
218
|
+
f"File already exists, cannot create: {file_path}. "
|
|
219
|
+
"Use a non-empty old_string to edit an existing file."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if not path.exists():
|
|
223
|
+
raise ToolError(f"File not found: {file_path}")
|
|
224
|
+
if path.is_dir():
|
|
225
|
+
raise ToolError(f"Path is a directory: {file_path}")
|
|
226
|
+
|
|
227
|
+
# Read current content
|
|
228
|
+
file_content = read_file_sync(path)
|
|
229
|
+
original_content = file_content
|
|
230
|
+
|
|
231
|
+
# Detect and normalize line endings (restore later)
|
|
232
|
+
original_ending = _detect_line_ending(file_content)
|
|
233
|
+
file_content = file_content.replace("\r\n", "\n")
|
|
234
|
+
old_string_norm = old_string.replace("\r\n", "\n")
|
|
235
|
+
new_string_norm = new_string.replace("\r\n", "\n")
|
|
236
|
+
|
|
237
|
+
# Strategy 1: Exact matching
|
|
238
|
+
occurrences = file_content.count(old_string_norm)
|
|
239
|
+
new_content = None
|
|
240
|
+
match_strategy = "exact"
|
|
241
|
+
|
|
242
|
+
if occurrences > 0:
|
|
243
|
+
if allow_multiple:
|
|
244
|
+
new_content = file_content.replace(old_string_norm, new_string_norm)
|
|
245
|
+
elif occurrences == 1:
|
|
246
|
+
new_content = file_content.replace(old_string_norm, new_string_norm, 1)
|
|
247
|
+
else:
|
|
248
|
+
raise ToolError(
|
|
249
|
+
f"Multiple occurrences ({occurrences}) found for "
|
|
250
|
+
f"old_string in {file_path}. "
|
|
251
|
+
"Use allow_multiple: true to replace all, or provide "
|
|
252
|
+
"more context to match a single occurrence."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Strategy 2: Flexible matching (whitespace-insensitive)
|
|
256
|
+
if new_content is None:
|
|
257
|
+
flex_content, flex_occurrences = _flexible_match(
|
|
258
|
+
file_content, old_string_norm, new_string_norm
|
|
259
|
+
)
|
|
260
|
+
if flex_occurrences > 0:
|
|
261
|
+
if allow_multiple or flex_occurrences == 1:
|
|
262
|
+
new_content = flex_content
|
|
263
|
+
occurrences = flex_occurrences
|
|
264
|
+
match_strategy = "flexible"
|
|
265
|
+
else:
|
|
266
|
+
raise ToolError(
|
|
267
|
+
f"Multiple occurrences ({flex_occurrences}) found "
|
|
268
|
+
f"for old_string in {file_path}. "
|
|
269
|
+
"Use allow_multiple: true to replace all."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Strategy 3: Regex-based flexible matching
|
|
273
|
+
if new_content is None:
|
|
274
|
+
tokens = _tokenize_for_regex(old_string_norm)
|
|
275
|
+
if tokens:
|
|
276
|
+
escaped_tokens = [_escape_regex(t) for t in tokens]
|
|
277
|
+
pattern = r"^([ \t]*)" + r"\s*".join(escaped_tokens)
|
|
278
|
+
if allow_multiple:
|
|
279
|
+
regex_matches = list(re.finditer(pattern, file_content, re.MULTILINE))
|
|
280
|
+
if regex_matches:
|
|
281
|
+
# Replace from end to start to preserve offsets
|
|
282
|
+
new_content = file_content
|
|
283
|
+
for m in reversed(regex_matches):
|
|
284
|
+
new_content = (
|
|
285
|
+
new_content[: m.start()]
|
|
286
|
+
+ m.group(1)
|
|
287
|
+
+ new_string_norm
|
|
288
|
+
+ new_content[m.end() :]
|
|
289
|
+
)
|
|
290
|
+
occurrences = len(regex_matches)
|
|
291
|
+
match_strategy = "regex"
|
|
292
|
+
else:
|
|
293
|
+
regex_match = re.search(pattern, file_content, re.MULTILINE)
|
|
294
|
+
if regex_match:
|
|
295
|
+
indent = regex_match.group(1)
|
|
296
|
+
new_content = (
|
|
297
|
+
file_content[: regex_match.start()]
|
|
298
|
+
+ indent
|
|
299
|
+
+ new_string_norm
|
|
300
|
+
+ file_content[regex_match.end() :]
|
|
301
|
+
)
|
|
302
|
+
occurrences = 1
|
|
303
|
+
match_strategy = "regex"
|
|
304
|
+
|
|
305
|
+
# Handle no match found
|
|
306
|
+
if new_content is None or occurrences == 0:
|
|
307
|
+
raise ToolError(
|
|
308
|
+
f"Failed to edit, 0 occurrences found for old_string "
|
|
309
|
+
f"in {file_path}. "
|
|
310
|
+
"Ensure you're not escaping content incorrectly and "
|
|
311
|
+
"check whitespace, indentation, and context. "
|
|
312
|
+
"Use read_file tool to verify."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Check if old_string equals new_string
|
|
316
|
+
if old_string_norm == new_string_norm:
|
|
317
|
+
raise ToolError(
|
|
318
|
+
"No changes to apply. The old_string and new_string "
|
|
319
|
+
f"are identical in file: {file_path}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Restore trailing newline state and line endings
|
|
323
|
+
new_content = _restore_trailing_newline(new_content, file_content)
|
|
324
|
+
if original_ending == "\r\n":
|
|
325
|
+
new_content = new_content.replace("\n", "\r\n")
|
|
326
|
+
|
|
327
|
+
# Write new content
|
|
328
|
+
write_file_sync(path, new_content)
|
|
329
|
+
|
|
330
|
+
# Save to history for potential undo
|
|
331
|
+
self._file_history[path].append(original_content)
|
|
332
|
+
|
|
333
|
+
result = f"Successfully modified file: {file_path} ({occurrences} replacements)."
|
|
334
|
+
if match_strategy != "exact":
|
|
335
|
+
result += f" [matched using {match_strategy} strategy]"
|
|
336
|
+
|
|
337
|
+
return ContentResult(output=result).to_content_blocks()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
__all__ = ["GeminiEditTool"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Gemini-style write_file tool implementation.
|
|
2
|
+
|
|
3
|
+
Based on Gemini CLI's write_file tool:
|
|
4
|
+
https://github.com/google-gemini/gemini-cli
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import ClassVar
|
|
11
|
+
|
|
12
|
+
from mcp.types import ContentBlock # noqa: TC002 - used at runtime by FunctionTool
|
|
13
|
+
|
|
14
|
+
from hud.tools.base import BaseTool
|
|
15
|
+
from hud.tools.native_types import NativeToolSpec, NativeToolSpecs
|
|
16
|
+
from hud.tools.types import ContentResult, ToolError
|
|
17
|
+
from hud.types import AgentType
|
|
18
|
+
|
|
19
|
+
from .utils import resolve_path_safely, write_file_sync
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GeminiWriteTool(BaseTool):
|
|
23
|
+
"""Gemini CLI-style file writing tool.
|
|
24
|
+
|
|
25
|
+
Creates or overwrites a file with the provided content.
|
|
26
|
+
Creates parent directories if they don't exist.
|
|
27
|
+
|
|
28
|
+
Parameters (matching Gemini CLI):
|
|
29
|
+
file_path: Path to the file to write (required)
|
|
30
|
+
content: The content to write to the file (required)
|
|
31
|
+
|
|
32
|
+
Native specs: Uses function calling (no native API), role="writer"
|
|
33
|
+
for mutual exclusion with other write tools.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
native_specs: ClassVar[NativeToolSpecs] = {
|
|
37
|
+
AgentType.GEMINI: NativeToolSpec(role="writer"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_base_directory: str
|
|
41
|
+
|
|
42
|
+
def __init__(self, base_directory: str = ".") -> None:
|
|
43
|
+
super().__init__(
|
|
44
|
+
env=None,
|
|
45
|
+
name="write_file",
|
|
46
|
+
title="WriteFile",
|
|
47
|
+
description=(
|
|
48
|
+
"Creates a new file or overwrites an existing file with the provided content. "
|
|
49
|
+
"Creates parent directories if they don't exist. "
|
|
50
|
+
"Use this for creating new files. "
|
|
51
|
+
"For editing existing files, prefer the replace tool."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
self._base_directory = str(Path(base_directory).resolve())
|
|
55
|
+
|
|
56
|
+
def _resolve_path(self, file_path: str) -> Path:
|
|
57
|
+
"""Resolve file path relative to base directory with containment check."""
|
|
58
|
+
return resolve_path_safely(file_path, Path(self._base_directory))
|
|
59
|
+
|
|
60
|
+
async def __call__(
|
|
61
|
+
self,
|
|
62
|
+
file_path: str,
|
|
63
|
+
content: str,
|
|
64
|
+
) -> list[ContentBlock]:
|
|
65
|
+
"""Write content to a file.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
file_path: Path to the file to write
|
|
69
|
+
content: The content to write to the file
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of ContentBlocks with result message
|
|
73
|
+
"""
|
|
74
|
+
if not file_path or not file_path.strip():
|
|
75
|
+
raise ToolError("The 'file_path' parameter must be non-empty.")
|
|
76
|
+
|
|
77
|
+
path = self._resolve_path(file_path)
|
|
78
|
+
|
|
79
|
+
if path.exists() and path.is_dir():
|
|
80
|
+
raise ToolError(f"Path is a directory: {file_path}")
|
|
81
|
+
|
|
82
|
+
is_new = not path.exists()
|
|
83
|
+
write_file_sync(path, content)
|
|
84
|
+
|
|
85
|
+
action = "Created" if is_new else "Overwrote"
|
|
86
|
+
line_count = content.count("\n") + (1 if content else 0)
|
|
87
|
+
result = f"{action} file: {file_path} ({line_count} lines)"
|
|
88
|
+
|
|
89
|
+
return ContentResult(output=result).to_content_blocks()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = ["GeminiWriteTool"]
|
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
|
|
11
11
|
from hud.tools.coding.gemini_edit import GeminiEditTool
|
|
12
12
|
from hud.tools.coding.gemini_shell import GeminiShellTool
|
|
13
|
+
from hud.tools.coding.gemini_write import GeminiWriteTool
|
|
13
14
|
from hud.tools.types import ToolError
|
|
14
15
|
|
|
15
16
|
|
|
@@ -152,22 +153,20 @@ class TestGeminiEditTool:
|
|
|
152
153
|
)
|
|
153
154
|
|
|
154
155
|
@pytest.mark.asyncio
|
|
155
|
-
async def
|
|
156
|
-
"""Test call with multiple occurrences without
|
|
156
|
+
async def test_call_multiple_occurrences_no_allow_multiple(self) -> None:
|
|
157
|
+
"""Test call with multiple occurrences without allow_multiple raises error."""
|
|
157
158
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
158
159
|
test_file = Path(tmpdir) / "test.txt"
|
|
159
160
|
test_file.write_text("hello hello hello")
|
|
160
161
|
|
|
161
162
|
tool = GeminiEditTool(base_directory=tmpdir)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
assert test_file.read_text() == "world hello hello"
|
|
170
|
-
assert "1 replacements" in result[0].text # type: ignore[union-attr]
|
|
163
|
+
with pytest.raises(ToolError, match="Multiple occurrences"):
|
|
164
|
+
await tool(
|
|
165
|
+
file_path="test.txt",
|
|
166
|
+
instruction="test edit",
|
|
167
|
+
old_string="hello",
|
|
168
|
+
new_string="world",
|
|
169
|
+
)
|
|
171
170
|
|
|
172
171
|
@pytest.mark.asyncio
|
|
173
172
|
async def test_call_successful_edit(self) -> None:
|
|
@@ -193,8 +192,8 @@ class TestGeminiEditTool:
|
|
|
193
192
|
assert "Successfully modified" in result[0].text # type: ignore[union-attr]
|
|
194
193
|
|
|
195
194
|
@pytest.mark.asyncio
|
|
196
|
-
async def
|
|
197
|
-
"""Test
|
|
195
|
+
async def test_call_allow_multiple(self) -> None:
|
|
196
|
+
"""Test replacing all occurrences with allow_multiple=True."""
|
|
198
197
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
199
198
|
test_file = Path(tmpdir) / "test.txt"
|
|
200
199
|
test_file.write_text("hello hello hello")
|
|
@@ -205,10 +204,9 @@ class TestGeminiEditTool:
|
|
|
205
204
|
instruction="Replace all hello with world",
|
|
206
205
|
old_string="hello",
|
|
207
206
|
new_string="world",
|
|
208
|
-
|
|
207
|
+
allow_multiple=True,
|
|
209
208
|
)
|
|
210
209
|
|
|
211
|
-
# Verify file was modified
|
|
212
210
|
assert test_file.read_text() == "world world world"
|
|
213
211
|
|
|
214
212
|
@pytest.mark.asyncio
|
|
@@ -229,3 +227,68 @@ class TestGeminiEditTool:
|
|
|
229
227
|
# Check history was saved
|
|
230
228
|
assert len(tool._file_history[test_file]) == 1
|
|
231
229
|
assert tool._file_history[test_file][0] == "original content"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestGeminiWriteTool:
|
|
233
|
+
"""Tests for GeminiWriteTool."""
|
|
234
|
+
|
|
235
|
+
def test_init(self) -> None:
|
|
236
|
+
"""Test initialization."""
|
|
237
|
+
tool = GeminiWriteTool()
|
|
238
|
+
assert tool.name == "write_file"
|
|
239
|
+
|
|
240
|
+
def test_init_with_base_directory(self) -> None:
|
|
241
|
+
"""Test initialization with custom base directory."""
|
|
242
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
243
|
+
tool = GeminiWriteTool(base_directory=tmpdir)
|
|
244
|
+
assert tool._base_directory == str(Path(tmpdir).resolve())
|
|
245
|
+
|
|
246
|
+
@pytest.mark.asyncio
|
|
247
|
+
async def test_write_new_file(self) -> None:
|
|
248
|
+
"""Test writing a new file."""
|
|
249
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
250
|
+
tool = GeminiWriteTool(base_directory=tmpdir)
|
|
251
|
+
result = await tool(file_path="new.txt", content="hello world")
|
|
252
|
+
|
|
253
|
+
written = (Path(tmpdir) / "new.txt").read_text()
|
|
254
|
+
assert written == "hello world"
|
|
255
|
+
assert "Created" in result[0].text # type: ignore[union-attr]
|
|
256
|
+
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_overwrite_file(self) -> None:
|
|
259
|
+
"""Test overwriting an existing file."""
|
|
260
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
261
|
+
existing = Path(tmpdir) / "existing.txt"
|
|
262
|
+
existing.write_text("old content")
|
|
263
|
+
|
|
264
|
+
tool = GeminiWriteTool(base_directory=tmpdir)
|
|
265
|
+
result = await tool(file_path="existing.txt", content="new content")
|
|
266
|
+
|
|
267
|
+
assert existing.read_text() == "new content"
|
|
268
|
+
assert "Overwrote" in result[0].text # type: ignore[union-attr]
|
|
269
|
+
|
|
270
|
+
@pytest.mark.asyncio
|
|
271
|
+
async def test_create_parent_dirs(self) -> None:
|
|
272
|
+
"""Test that parent directories are created."""
|
|
273
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
274
|
+
tool = GeminiWriteTool(base_directory=tmpdir)
|
|
275
|
+
await tool(file_path="sub/deep/file.txt", content="nested")
|
|
276
|
+
|
|
277
|
+
assert (Path(tmpdir) / "sub" / "deep" / "file.txt").read_text() == "nested"
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
async def test_empty_file_path_error(self) -> None:
|
|
281
|
+
"""Test empty file_path raises error."""
|
|
282
|
+
tool = GeminiWriteTool()
|
|
283
|
+
with pytest.raises(ToolError, match="non-empty"):
|
|
284
|
+
await tool(file_path="", content="content")
|
|
285
|
+
|
|
286
|
+
@pytest.mark.asyncio
|
|
287
|
+
async def test_write_to_directory_error(self) -> None:
|
|
288
|
+
"""Test writing to a directory path raises error."""
|
|
289
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
290
|
+
tool = GeminiWriteTool(base_directory=tmpdir)
|
|
291
|
+
subdir = Path(tmpdir) / "adir"
|
|
292
|
+
subdir.mkdir()
|
|
293
|
+
with pytest.raises(ToolError, match="directory"):
|
|
294
|
+
await tool(file_path="adir", content="content")
|
|
@@ -56,6 +56,7 @@ from hud.tools.filesystem.gemini import (
|
|
|
56
56
|
GeminiReadTool,
|
|
57
57
|
GeminiSearchTool,
|
|
58
58
|
)
|
|
59
|
+
from hud.tools.filesystem.gemini_read_many import GeminiReadManyTool
|
|
59
60
|
|
|
60
61
|
# OpenCode-style tools (default)
|
|
61
62
|
from hud.tools.filesystem.glob import GlobTool
|
|
@@ -72,6 +73,7 @@ __all__ = [
|
|
|
72
73
|
"FileMatch",
|
|
73
74
|
"GeminiGlobTool",
|
|
74
75
|
"GeminiListTool",
|
|
76
|
+
"GeminiReadManyTool",
|
|
75
77
|
"GeminiReadTool",
|
|
76
78
|
"GeminiSearchTool",
|
|
77
79
|
"GlobTool",
|